From 37ef32c24a77c68c8ee431a710fd99c7e5a19f15 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 20 Apr 2026 13:55:17 +0200 Subject: [PATCH 01/95] Start implementing vihaco component --- Cargo.lock | 521 +++++++++++++++++++++++++- Cargo.toml | 2 +- crates/ppvm-vihaco/Cargo.toml | 10 + crates/ppvm-vihaco/src/component.rs | 133 +++++++ crates/ppvm-vihaco/src/instruction.rs | 51 +++ crates/ppvm-vihaco/src/lib.rs | 8 + crates/ppvm-vihaco/src/message.rs | 13 + ppvm-python/uv.lock | 92 ----- 8 files changed, 732 insertions(+), 98 deletions(-) create mode 100644 crates/ppvm-vihaco/Cargo.toml create mode 100644 crates/ppvm-vihaco/src/component.rs create mode 100644 crates/ppvm-vihaco/src/instruction.rs create mode 100644 crates/ppvm-vihaco/src/lib.rs create mode 100644 crates/ppvm-vihaco/src/message.rs diff --git a/Cargo.lock b/Cargo.lock index f103c3cc0..c480cf396 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,12 +30,56 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -51,6 +95,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -77,6 +130,21 @@ dependencies = [ "virtue", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.9.4" @@ -95,6 +163,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bnum" version = "0.13.0" @@ -161,7 +238,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "rand_core", ] @@ -217,6 +294,42 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "codespan" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583f52b0658b321b25fd6b209b6c76cf058f433071297de64e5980c3d9aad937" +dependencies = [ + "codespan-reporting", + "serde", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "console" version = "0.15.11" @@ -226,7 +339,25 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "windows-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", ] [[package]] @@ -302,6 +433,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.21.3" @@ -352,24 +493,82 @@ dependencies = [ "rayon", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + [[package]] name = "encode_unicode" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "fnv" version = "1.0.7" @@ -397,6 +596,16 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -481,6 +690,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "indexmap" version = "2.11.4" @@ -523,6 +738,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -547,6 +768,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.81" @@ -557,6 +802,47 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.14.0", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +dependencies = [ + "regex-automata", + "rustversion", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -581,9 +867,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" @@ -600,6 +886,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "num" version = "0.4.3" @@ -679,12 +971,28 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.11" @@ -704,6 +1012,31 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "plotters" version = "0.3.7" @@ -738,6 +1071,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppvm" version = "0.1.0" @@ -809,6 +1151,22 @@ dependencies = [ "rayon", ] +[[package]] +name = "ppvm-vihaco" +version = "0.1.0" +dependencies = [ + "eyre", + "ppvm-runtime", + "ppvm-tableau", + "vihaco", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -1062,18 +1420,46 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1103,6 +1489,44 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" +[[package]] +name = "term" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -1113,12 +1537,39 @@ dependencies = [ "serde_json", ] +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unescaper" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e" +dependencies = [ + "thiserror", +] + [[package]] name = "unicode-ident" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -1137,12 +1588,57 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vihaco" +version = "0.1.0" +source = "git+https://github.com/QuEraComputing/stellarscope.git#0b69cfcc7c6c31c29b2f63a2f5c37f2d38bc773b" +dependencies = [ + "byteorder", + "codespan", + "colored", + "env_logger", + "eyre", + "log", + "vihaco-macros", + "vihaco-parser", +] + +[[package]] +name = "vihaco-macros" +version = "0.1.0" +source = "git+https://github.com/QuEraComputing/stellarscope.git#0b69cfcc7c6c31c29b2f63a2f5c37f2d38bc773b" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "vihaco-parser" +version = "0.1.0" +source = "git+https://github.com/QuEraComputing/stellarscope.git#0b69cfcc7c6c31c29b2f63a2f5c37f2d38bc773b" +dependencies = [ + "codespan", + "codespan-reporting", + "eyre", + "lalrpop", + "lalrpop-util", + "unescaper", +] + [[package]] name = "virtue" version = "0.0.18" @@ -1295,9 +1791,15 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.59.0", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.59.0" @@ -1307,6 +1809,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index aafbc4bfa..60151b0f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ ppvm-tableau = { version = "0.1.0", path = "crates/ppvm-tableau" } ppvm-sym = { version = "0.1.0", path = "crates/ppvm-sym" } [workspace] -members = [ "crates/ppvm-runtime", "crates/ppvm-sym", "crates/ppvm-python-native", "crates/ppvm-tableau"] +members = [ "crates/ppvm-runtime", "crates/ppvm-sym", "crates/ppvm-python-native", "crates/ppvm-tableau", "crates/ppvm-vihaco"] [[example]] name = "symbolic" diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml new file mode 100644 index 000000000..026b0f813 --- /dev/null +++ b/crates/ppvm-vihaco/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ppvm-vihaco" +version = "0.1.0" +edition = "2024" + +[dependencies] +eyre = "0.6.12" +ppvm-runtime = { version = "0.1.0", path = "../ppvm-runtime" } +ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon"] } +vihaco = { git = "https://github.com/QuEraComputing/stellarscope.git", version = "0.1.0" } diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs new file mode 100644 index 000000000..e4db3ef6d --- /dev/null +++ b/crates/ppvm-vihaco/src/component.rs @@ -0,0 +1,133 @@ +use crate::instruction::CircuitInstruction; +use crate::message::CircuitMessage; +use eyre::{Result, eyre}; +use ppvm_runtime::config::fxhash::ByteF64; +use ppvm_tableau::prelude::*; +use vihaco::{Event, ExecContext, component}; + +pub struct Circuit { + pub tab: GeneralizedTableau, I>, +} + +#[derive(Debug, Clone, Event)] +pub struct MeasurementResult { + qubit: usize, + + /// None if lost, else 0 or 1 according to outcome + outcome: Option, +} + +#[component(instruction = CircuitInstruction, message = CircuitMessage)] +impl Circuit +where + I: TableauIndex + Send + Sync + std::fmt::Debug, +{ + fn execute( + &mut self, + inst: CircuitInstruction, + msg: CircuitMessage, + ctx: &mut ExecContext, + ) -> Result<()> { + match (inst, msg) { + // Single-qubit Clifford + (CircuitInstruction::X, CircuitMessage::Qubit(addr)) => self.tab.x(addr), + (CircuitInstruction::Y, CircuitMessage::Qubit(addr)) => self.tab.y(addr), + (CircuitInstruction::Z, CircuitMessage::Qubit(addr)) => self.tab.z(addr), + (CircuitInstruction::H, CircuitMessage::Qubit(addr)) => self.tab.h(addr), + (CircuitInstruction::S, CircuitMessage::Qubit(addr)) => self.tab.s(addr), + (CircuitInstruction::SAdj, CircuitMessage::Qubit(addr)) => self.tab.s_adj(addr), + (CircuitInstruction::SqrtX, CircuitMessage::Qubit(addr)) => self.tab.sqrt_x(addr), + (CircuitInstruction::SqrtY, CircuitMessage::Qubit(addr)) => self.tab.sqrt_y(addr), + (CircuitInstruction::SqrtXAdj, CircuitMessage::Qubit(addr)) => { + self.tab.sqrt_x_adj(addr) + } + (CircuitInstruction::SqrtYAdj, CircuitMessage::Qubit(addr)) => { + self.tab.sqrt_y_adj(addr) + } + + // Controlled gates + (CircuitInstruction::CNOT, CircuitMessage::TwoQubit(addr0, addr1)) => { + self.tab.cnot(addr0, addr1) + } + (CircuitInstruction::CZ, CircuitMessage::TwoQubit(addr0, addr1)) => { + self.tab.cz(addr0, addr1) + } + + // T gate + (CircuitInstruction::T, CircuitMessage::Qubit(addr)) => self.tab.t(addr), + (CircuitInstruction::TAdj, CircuitMessage::Qubit(addr)) => self.tab.t_adj(addr), + + // Single-qubit rotations + (CircuitInstruction::RX, CircuitMessage::QubitAndFloat(addr, angle)) => { + self.tab.rx(addr, angle) + } + (CircuitInstruction::RY, CircuitMessage::QubitAndFloat(addr, angle)) => { + self.tab.ry(addr, angle) + } + (CircuitInstruction::RZ, CircuitMessage::QubitAndFloat(addr, angle)) => { + self.tab.rz(addr, angle) + } + + // Two-qubit rotations + (CircuitInstruction::RXX, CircuitMessage::TwoQubitAndFloat(addr0, addr1, angle)) => { + self.tab.rxx(addr0, addr1, angle) + } + (CircuitInstruction::RYY, CircuitMessage::TwoQubitAndFloat(addr0, addr1, angle)) => { + self.tab.ryy(addr0, addr1, angle) + } + (CircuitInstruction::RZZ, CircuitMessage::TwoQubitAndFloat(addr0, addr1, angle)) => { + self.tab.rzz(addr0, addr1, angle) + } + + // U3 + (CircuitInstruction::U3, CircuitMessage::QubitU3(addr, theta, phi, lam)) => { + self.tab.u3(addr, theta, phi, lam) + } + + // Measure & Reset + (CircuitInstruction::Measure, CircuitMessage::Qubit(addr)) => { + let outcome = self.tab.measure(addr); + ctx.emit(MeasurementResult { + qubit: addr, + outcome: outcome, + }); + } + (CircuitInstruction::Reset, CircuitMessage::Qubit(addr)) => self.tab.reset(addr), + + // Noise + (CircuitInstruction::Depolarize, CircuitMessage::QubitAndFloat(addr, p)) => { + self.tab.depolarize(addr, p) + } + ( + CircuitInstruction::Depolarize2, + CircuitMessage::TwoQubitAndFloat(addr0, addr1, p), + ) => self.tab.depolarize2(addr0, addr1, p), + (CircuitInstruction::PauliError, CircuitMessage::QubitAndFloatArr3(addr0, ps)) => { + self.tab.pauli_error(addr0, ps) + } + ( + CircuitInstruction::TwoQubitPauliError, + CircuitMessage::TwoQubitAndFloatArr15(addr0, addr1, ps), + ) => self.tab.two_qubit_pauli_error(addr0, addr1, ps), + + // Loss + (CircuitInstruction::Loss, CircuitMessage::QubitAndFloat(addr, p)) => { + self.tab.loss_channel(addr, p) + } + ( + CircuitInstruction::CorrelatedLoss, + CircuitMessage::TwoQubitAndFloatArr3(addr0, addr1, ps), + ) => self.tab.correlated_loss_channel(addr0, addr1, ps), + + // Fallback + _ => { + return Err(eyre!( + "Invalid gate arguments {:?} for gate {:?}", + msg, + inst + )); + } + }; + Ok(()) + } +} diff --git a/crates/ppvm-vihaco/src/instruction.rs b/crates/ppvm-vihaco/src/instruction.rs new file mode 100644 index 000000000..d4dcb6374 --- /dev/null +++ b/crates/ppvm-vihaco/src/instruction.rs @@ -0,0 +1,51 @@ +use vihaco::Instruction; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Instruction)] +pub enum CircuitInstruction { + // Single-Qubit Clifford gates + X, + Y, + Z, + H, + S, + SAdj, + SqrtX, + SqrtY, + SqrtXAdj, + SqrtYAdj, + + // Controlled gates + CNOT, + CZ, + + // T gate + T, + TAdj, + + // Single-qubit rotations + RX, + RY, + RZ, + + // Two-qubit rotations + RXX, + RYY, + RZZ, + + // U3 + U3, + + // Measureme & Reset + Measure, + Reset, + + // Loss + Loss, + CorrelatedLoss, + + // Noise + PauliError, + TwoQubitPauliError, + Depolarize, + Depolarize2, +} diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs new file mode 100644 index 000000000..c5d610c79 --- /dev/null +++ b/crates/ppvm-vihaco/src/lib.rs @@ -0,0 +1,8 @@ +pub mod component; +pub mod instruction; +pub mod message; + +pub mod prelude { + pub use crate::component::Circuit; + pub use crate::instruction::CircuitInstruction; +} diff --git a/crates/ppvm-vihaco/src/message.rs b/crates/ppvm-vihaco/src/message.rs new file mode 100644 index 000000000..9182d5624 --- /dev/null +++ b/crates/ppvm-vihaco/src/message.rs @@ -0,0 +1,13 @@ +use vihaco::Message; + +#[derive(Debug, Clone, Copy, Message)] +pub enum CircuitMessage { + Qubit(usize), // X, Y, Z, ... + QubitAndFloat(usize, f64), // RX, depolarize, ... + TwoQubit(usize, usize), // CX, CZ + TwoQubitAndFloat(usize, usize, f64), // RXX, ... + QubitU3(usize, f64, f64, f64), // U3 + QubitAndFloatArr3(usize, [f64; 3]), // PauliError + TwoQubitAndFloatArr3(usize, usize, [f64; 3]), // Correlated loss + TwoQubitAndFloatArr15(usize, usize, [f64; 15]), // TwoQubitPauliError +} diff --git a/ppvm-python/uv.lock b/ppvm-python/uv.lock index 44a78df85..f02e9362b 100644 --- a/ppvm-python/uv.lock +++ b/ppvm-python/uv.lock @@ -178,15 +178,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] -[[package]] -name = "cfgv" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, -] - [[package]] name = "charset-normalizer" version = "3.4.7" @@ -534,15 +525,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] -[[package]] -name = "distlib" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, -] - [[package]] name = "exceptiongroup" version = "1.3.1" @@ -573,15 +555,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, ] -[[package]] -name = "filelock" -version = "3.25.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, -] - [[package]] name = "fonttools" version = "4.62.1" @@ -672,15 +645,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, ] -[[package]] -name = "identify" -version = "2.6.18" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, -] - [[package]] name = "idna" version = "3.11" @@ -1520,15 +1484,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] -[[package]] -name = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - [[package]] name = "numpy" version = "2.2.6" @@ -1862,7 +1817,6 @@ dependencies = [ dev = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pre-commit" }, { name = "pytest" }, ] doc = [ @@ -1883,7 +1837,6 @@ requires-dist = [{ name = "ppvm-python-native", directory = "../crates/ppvm-pyth [package.metadata.requires-dev] dev = [ { name = "numpy", specifier = ">=2.2.6" }, - { name = "pre-commit", specifier = ">=4.5.1" }, { name = "pytest", specifier = ">=9.0.2" }, ] doc = [ @@ -1907,22 +1860,6 @@ source = { directory = "../crates/ppvm-python-native" } [package.metadata.requires-dev] dev = [{ name = "maturin", specifier = ">=1.12.6" }] -[[package]] -name = "pre-commit" -version = "4.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, -] - [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -2074,19 +2011,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] -[[package]] -name = "python-discovery" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -2538,22 +2462,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, ] -[[package]] -name = "virtualenv" -version = "21.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, - { name = "python-discovery" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, -] - [[package]] name = "watchdog" version = "6.0.0" From 78ebef29d997bd4bcade5b99eedc07885961e456 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 20 Apr 2026 15:10:27 +0200 Subject: [PATCH 02/95] Implement batched instructions --- crates/ppvm-vihaco/src/component.rs | 194 +++++++++++++++++++--------- crates/ppvm-vihaco/src/message.rs | 12 +- 2 files changed, 141 insertions(+), 65 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index e4db3ef6d..93fa15a6e 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -5,6 +5,15 @@ use ppvm_runtime::config::fxhash::ByteF64; use ppvm_tableau::prelude::*; use vihaco::{Event, ExecContext, component}; +macro_rules! batch_for { + ($tab:expr, $method:ident, $addrs:expr) => { + for addr in &$addrs { $tab.$method(*addr); } + }; + ($tab:expr, $method:ident, $addrs:expr, $($arg:expr),+) => { + for addr in &$addrs { $tab.$method(*addr, $($arg),+); } + }; +} + pub struct Circuit { pub tab: GeneralizedTableau, I>, } @@ -17,6 +26,13 @@ pub struct MeasurementResult { outcome: Option, } +#[derive(Debug, Clone, Event)] +pub struct MeasurementResultBatch { + qubits: Vec, + + outcomes: Vec>, +} + #[component(instruction = CircuitInstruction, message = CircuitMessage)] impl Circuit where @@ -28,99 +44,149 @@ where msg: CircuitMessage, ctx: &mut ExecContext, ) -> Result<()> { + use CircuitInstruction::*; + use CircuitMessage::*; + match (inst, msg) { // Single-qubit Clifford - (CircuitInstruction::X, CircuitMessage::Qubit(addr)) => self.tab.x(addr), - (CircuitInstruction::Y, CircuitMessage::Qubit(addr)) => self.tab.y(addr), - (CircuitInstruction::Z, CircuitMessage::Qubit(addr)) => self.tab.z(addr), - (CircuitInstruction::H, CircuitMessage::Qubit(addr)) => self.tab.h(addr), - (CircuitInstruction::S, CircuitMessage::Qubit(addr)) => self.tab.s(addr), - (CircuitInstruction::SAdj, CircuitMessage::Qubit(addr)) => self.tab.s_adj(addr), - (CircuitInstruction::SqrtX, CircuitMessage::Qubit(addr)) => self.tab.sqrt_x(addr), - (CircuitInstruction::SqrtY, CircuitMessage::Qubit(addr)) => self.tab.sqrt_y(addr), - (CircuitInstruction::SqrtXAdj, CircuitMessage::Qubit(addr)) => { - self.tab.sqrt_x_adj(addr) - } - (CircuitInstruction::SqrtYAdj, CircuitMessage::Qubit(addr)) => { - self.tab.sqrt_y_adj(addr) - } + (X, Qubit(addr)) => self.tab.x(addr), + (Y, Qubit(addr)) => self.tab.y(addr), + (Z, Qubit(addr)) => self.tab.z(addr), + (H, Qubit(addr)) => self.tab.h(addr), + (S, Qubit(addr)) => self.tab.s(addr), + (SAdj, Qubit(addr)) => self.tab.s_adj(addr), + (SqrtX, Qubit(addr)) => self.tab.sqrt_x(addr), + (SqrtY, Qubit(addr)) => self.tab.sqrt_y(addr), + (SqrtXAdj, Qubit(addr)) => self.tab.sqrt_x_adj(addr), + (SqrtYAdj, Qubit(addr)) => self.tab.sqrt_y_adj(addr), // Controlled gates - (CircuitInstruction::CNOT, CircuitMessage::TwoQubit(addr0, addr1)) => { - self.tab.cnot(addr0, addr1) - } - (CircuitInstruction::CZ, CircuitMessage::TwoQubit(addr0, addr1)) => { - self.tab.cz(addr0, addr1) - } + (CNOT, TwoQubit(addr0, addr1)) => self.tab.cnot(addr0, addr1), + (CZ, TwoQubit(addr0, addr1)) => self.tab.cz(addr0, addr1), // T gate - (CircuitInstruction::T, CircuitMessage::Qubit(addr)) => self.tab.t(addr), - (CircuitInstruction::TAdj, CircuitMessage::Qubit(addr)) => self.tab.t_adj(addr), + (T, Qubit(addr)) => self.tab.t(addr), + (TAdj, Qubit(addr)) => self.tab.t_adj(addr), // Single-qubit rotations - (CircuitInstruction::RX, CircuitMessage::QubitAndFloat(addr, angle)) => { - self.tab.rx(addr, angle) - } - (CircuitInstruction::RY, CircuitMessage::QubitAndFloat(addr, angle)) => { - self.tab.ry(addr, angle) - } - (CircuitInstruction::RZ, CircuitMessage::QubitAndFloat(addr, angle)) => { - self.tab.rz(addr, angle) - } + (RX, QubitAndFloat(addr, angle)) => self.tab.rx(addr, angle), + (RY, QubitAndFloat(addr, angle)) => self.tab.ry(addr, angle), + (RZ, QubitAndFloat(addr, angle)) => self.tab.rz(addr, angle), // Two-qubit rotations - (CircuitInstruction::RXX, CircuitMessage::TwoQubitAndFloat(addr0, addr1, angle)) => { - self.tab.rxx(addr0, addr1, angle) - } - (CircuitInstruction::RYY, CircuitMessage::TwoQubitAndFloat(addr0, addr1, angle)) => { - self.tab.ryy(addr0, addr1, angle) - } - (CircuitInstruction::RZZ, CircuitMessage::TwoQubitAndFloat(addr0, addr1, angle)) => { - self.tab.rzz(addr0, addr1, angle) - } + (RXX, TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rxx(addr0, addr1, angle), + (RYY, TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.ryy(addr0, addr1, angle), + (RZZ, TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rzz(addr0, addr1, angle), // U3 - (CircuitInstruction::U3, CircuitMessage::QubitU3(addr, theta, phi, lam)) => { - self.tab.u3(addr, theta, phi, lam) - } + (U3, QubitU3(addr, theta, phi, lam)) => self.tab.u3(addr, theta, phi, lam), // Measure & Reset - (CircuitInstruction::Measure, CircuitMessage::Qubit(addr)) => { + (Measure, Qubit(addr)) => { let outcome = self.tab.measure(addr); ctx.emit(MeasurementResult { qubit: addr, outcome: outcome, }); } - (CircuitInstruction::Reset, CircuitMessage::Qubit(addr)) => self.tab.reset(addr), + (Reset, Qubit(addr)) => self.tab.reset(addr), // Noise - (CircuitInstruction::Depolarize, CircuitMessage::QubitAndFloat(addr, p)) => { - self.tab.depolarize(addr, p) + (Depolarize, QubitAndFloat(addr, p)) => self.tab.depolarize(addr, p), + (Depolarize2, TwoQubitAndFloat(addr0, addr1, p)) => { + self.tab.depolarize2(addr0, addr1, p) } - ( - CircuitInstruction::Depolarize2, - CircuitMessage::TwoQubitAndFloat(addr0, addr1, p), - ) => self.tab.depolarize2(addr0, addr1, p), - (CircuitInstruction::PauliError, CircuitMessage::QubitAndFloatArr3(addr0, ps)) => { - self.tab.pauli_error(addr0, ps) + (PauliError, QubitAndFloatArr3(addr0, ps)) => self.tab.pauli_error(addr0, ps), + (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { + self.tab.two_qubit_pauli_error(addr0, addr1, ps) } - ( - CircuitInstruction::TwoQubitPauliError, - CircuitMessage::TwoQubitAndFloatArr15(addr0, addr1, ps), - ) => self.tab.two_qubit_pauli_error(addr0, addr1, ps), // Loss - (CircuitInstruction::Loss, CircuitMessage::QubitAndFloat(addr, p)) => { - self.tab.loss_channel(addr, p) + (Loss, QubitAndFloat(addr, p)) => self.tab.loss_channel(addr, p), + (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { + self.tab.correlated_loss_channel(addr0, addr1, ps) + } + + // Batch: dedicated batch methods + (SqrtX, QubitBatch(addrs)) => self.tab.sqrt_x_batch(&addrs), + (SqrtY, QubitBatch(addrs)) => self.tab.sqrt_y_batch(&addrs), + (SqrtXAdj, QubitBatch(addrs)) => self.tab.sqrt_x_adj_batch(&addrs), + (SqrtYAdj, QubitBatch(addrs)) => self.tab.sqrt_y_adj_batch(&addrs), + (H, QubitBatch(addrs)) => self.tab.h_batch(&addrs), + (CZ, TwoQubitBatch(pairs)) => self.tab.cz_batch(&pairs), + + // Batch: single-qubit for loops + (X, QubitBatch(addrs)) => batch_for!(self.tab, x, addrs), + (Y, QubitBatch(addrs)) => batch_for!(self.tab, y, addrs), + (Z, QubitBatch(addrs)) => batch_for!(self.tab, z, addrs), + (S, QubitBatch(addrs)) => batch_for!(self.tab, s, addrs), + (SAdj, QubitBatch(addrs)) => batch_for!(self.tab, s_adj, addrs), + (T, QubitBatch(addrs)) => batch_for!(self.tab, t, addrs), + (TAdj, QubitBatch(addrs)) => batch_for!(self.tab, t_adj, addrs), + (Reset, QubitBatch(addrs)) => batch_for!(self.tab, reset, addrs), + (RX, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, rx, addrs, angle), + (RY, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, ry, addrs, angle), + (RZ, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, rz, addrs, angle), + (Depolarize, QubitBatchAndFloat(addrs, p)) => { + batch_for!(self.tab, depolarize, addrs, p) + } + (Loss, QubitBatchAndFloat(addrs, p)) => batch_for!(self.tab, loss_channel, addrs, p), + (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { + batch_for!(self.tab, pauli_error, addrs, ps) + } + (U3, QubitBatchU3(addrs, theta, phi, lam)) => { + batch_for!(self.tab, u3, addrs, theta, phi, lam) + } + + // Batch: two-qubit for loops + (CNOT, TwoQubitBatch(pairs)) => { + for (a, b) in &pairs { + self.tab.cnot(*a, *b); + } + } + (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { + for (a, b) in &pairs { + self.tab.rxx(*a, *b, angle); + } + } + (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { + for (a, b) in &pairs { + self.tab.ryy(*a, *b, angle); + } + } + (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { + for (a, b) in &pairs { + self.tab.rzz(*a, *b, angle); + } + } + (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { + for (a, b) in &pairs { + self.tab.depolarize2(*a, *b, p); + } + } + (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { + for (a, b) in &pairs { + self.tab.two_qubit_pauli_error(*a, *b, ps); + } + } + (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { + for (a, b) in &pairs { + self.tab.correlated_loss_channel(*a, *b, ps); + } + } + + // Batch: measure (emits per qubit) + (Measure, QubitBatch(addrs)) => { + let outcomes = addrs.iter().map(|&addr| self.tab.measure(addr)); + let results = MeasurementResultBatch { + qubits: addrs.clone(), + outcomes: outcomes.collect(), + }; + ctx.emit(results); } - ( - CircuitInstruction::CorrelatedLoss, - CircuitMessage::TwoQubitAndFloatArr3(addr0, addr1, ps), - ) => self.tab.correlated_loss_channel(addr0, addr1, ps), // Fallback - _ => { + (inst, msg) => { return Err(eyre!( "Invalid gate arguments {:?} for gate {:?}", msg, diff --git a/crates/ppvm-vihaco/src/message.rs b/crates/ppvm-vihaco/src/message.rs index 9182d5624..de9bd4f49 100644 --- a/crates/ppvm-vihaco/src/message.rs +++ b/crates/ppvm-vihaco/src/message.rs @@ -1,6 +1,6 @@ use vihaco::Message; -#[derive(Debug, Clone, Copy, Message)] +#[derive(Debug, Clone, Message)] pub enum CircuitMessage { Qubit(usize), // X, Y, Z, ... QubitAndFloat(usize, f64), // RX, depolarize, ... @@ -10,4 +10,14 @@ pub enum CircuitMessage { QubitAndFloatArr3(usize, [f64; 3]), // PauliError TwoQubitAndFloatArr3(usize, usize, [f64; 3]), // Correlated loss TwoQubitAndFloatArr15(usize, usize, [f64; 15]), // TwoQubitPauliError + + // batched instructions + QubitBatch(Vec), // X, Y, Z, ... + QubitBatchAndFloat(Vec, f64), // RX, depolarize, ... + TwoQubitBatch(Vec<(usize, usize)>), // CX, CZ + TwoQubitBatchAndFloat(Vec<(usize, usize)>, f64), // RXX, ... + QubitBatchU3(Vec, f64, f64, f64), // U3 + QubitBatchAndFloatArr3(Vec, [f64; 3]), // PauliError + TwoQubitBatchAndFloatArr3(Vec<(usize, usize)>, [f64; 3]), // Correlated loss + TwoQubitBatchAndFloatArr15(Vec<(usize, usize)>, [f64; 15]), // TwoQubitPauliError } From a46ecb2d6e64f735df64d4eed75f3fc073a49a37 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 21 Apr 2026 14:01:31 +0200 Subject: [PATCH 03/95] Make measurement result an outcome --- crates/ppvm-vihaco/src/component.rs | 42 +++++++++++------------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 93fa15a6e..35fd41e51 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -3,7 +3,7 @@ use crate::message::CircuitMessage; use eyre::{Result, eyre}; use ppvm_runtime::config::fxhash::ByteF64; use ppvm_tableau::prelude::*; -use vihaco::{Event, ExecContext, component}; +use vihaco::{ExecContext, component}; macro_rules! batch_for { ($tab:expr, $method:ident, $addrs:expr) => { @@ -18,22 +18,12 @@ pub struct Circuit { pub tab: GeneralizedTableau, I>, } -#[derive(Debug, Clone, Event)] -pub struct MeasurementResult { - qubit: usize, - - /// None if lost, else 0 or 1 according to outcome - outcome: Option, +pub enum CircuitOutcome { + MeasurementResult(Option), + MeasurementResultBatch(Vec>), } -#[derive(Debug, Clone, Event)] -pub struct MeasurementResultBatch { - qubits: Vec, - - outcomes: Vec>, -} - -#[component(instruction = CircuitInstruction, message = CircuitMessage)] +#[component(instruction = CircuitInstruction, message = CircuitMessage, outcome = Option)] impl Circuit where I: TableauIndex + Send + Sync + std::fmt::Debug, @@ -42,8 +32,8 @@ where &mut self, inst: CircuitInstruction, msg: CircuitMessage, - ctx: &mut ExecContext, - ) -> Result<()> { + _ctx: &mut ExecContext, + ) -> Result> { use CircuitInstruction::*; use CircuitMessage::*; @@ -84,10 +74,7 @@ where // Measure & Reset (Measure, Qubit(addr)) => { let outcome = self.tab.measure(addr); - ctx.emit(MeasurementResult { - qubit: addr, - outcome: outcome, - }); + return Ok(Some(CircuitOutcome::MeasurementResult(outcome))); } (Reset, Qubit(addr)) => self.tab.reset(addr), @@ -107,6 +94,7 @@ where self.tab.correlated_loss_channel(addr0, addr1, ps) } + /* BATCH OPERATIONS START HERE */ // Batch: dedicated batch methods (SqrtX, QubitBatch(addrs)) => self.tab.sqrt_x_batch(&addrs), (SqrtY, QubitBatch(addrs)) => self.tab.sqrt_y_batch(&addrs), @@ -115,6 +103,7 @@ where (H, QubitBatch(addrs)) => self.tab.h_batch(&addrs), (CZ, TwoQubitBatch(pairs)) => self.tab.cz_batch(&pairs), + // TODO: replace things below by actual batched methods once they are available // Batch: single-qubit for loops (X, QubitBatch(addrs)) => batch_for!(self.tab, x, addrs), (Y, QubitBatch(addrs)) => batch_for!(self.tab, y, addrs), @@ -178,11 +167,9 @@ where // Batch: measure (emits per qubit) (Measure, QubitBatch(addrs)) => { let outcomes = addrs.iter().map(|&addr| self.tab.measure(addr)); - let results = MeasurementResultBatch { - qubits: addrs.clone(), - outcomes: outcomes.collect(), - }; - ctx.emit(results); + return Ok(Some(CircuitOutcome::MeasurementResultBatch( + outcomes.collect(), + ))); } // Fallback @@ -194,6 +181,7 @@ where )); } }; - Ok(()) + + Ok(None) } } From 8a9071fc3633293bd88c422804a963ace9894823 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 21 Apr 2026 15:08:24 +0200 Subject: [PATCH 04/95] Simplify outcome type --- crates/ppvm-vihaco/src/component.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 35fd41e51..6b40bf88e 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -18,12 +18,9 @@ pub struct Circuit { pub tab: GeneralizedTableau, I>, } -pub enum CircuitOutcome { - MeasurementResult(Option), - MeasurementResultBatch(Vec>), -} +pub type MeasurementResult = Vec>; -#[component(instruction = CircuitInstruction, message = CircuitMessage, outcome = Option)] +#[component(instruction = CircuitInstruction, message = CircuitMessage, outcome = Option)] impl Circuit where I: TableauIndex + Send + Sync + std::fmt::Debug, @@ -33,7 +30,7 @@ where inst: CircuitInstruction, msg: CircuitMessage, _ctx: &mut ExecContext, - ) -> Result> { + ) -> Result> { use CircuitInstruction::*; use CircuitMessage::*; @@ -74,7 +71,7 @@ where // Measure & Reset (Measure, Qubit(addr)) => { let outcome = self.tab.measure(addr); - return Ok(Some(CircuitOutcome::MeasurementResult(outcome))); + return Ok(Some(vec![outcome])); } (Reset, Qubit(addr)) => self.tab.reset(addr), @@ -167,9 +164,7 @@ where // Batch: measure (emits per qubit) (Measure, QubitBatch(addrs)) => { let outcomes = addrs.iter().map(|&addr| self.tab.measure(addr)); - return Ok(Some(CircuitOutcome::MeasurementResultBatch( - outcomes.collect(), - ))); + return Ok(Some(outcomes.collect())); } // Fallback From 71c17035d3667ad4812a03e3ee539a6320bda226 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 27 Apr 2026 11:40:26 +0200 Subject: [PATCH 05/95] Fix generic types, hardcode f64 coeffs; temporarily switch to local stellarscope to support generics --- Cargo.lock | 5 ++--- crates/ppvm-vihaco/Cargo.toml | 4 +++- crates/ppvm-vihaco/src/component.rs | 13 +++++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c480cf396..fa79ad227 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1155,7 +1155,9 @@ dependencies = [ name = "ppvm-vihaco" version = "0.1.0" dependencies = [ + "bitvec", "eyre", + "num", "ppvm-runtime", "ppvm-tableau", "vihaco", @@ -1603,7 +1605,6 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vihaco" version = "0.1.0" -source = "git+https://github.com/QuEraComputing/stellarscope.git#0b69cfcc7c6c31c29b2f63a2f5c37f2d38bc773b" dependencies = [ "byteorder", "codespan", @@ -1618,7 +1619,6 @@ dependencies = [ [[package]] name = "vihaco-macros" version = "0.1.0" -source = "git+https://github.com/QuEraComputing/stellarscope.git#0b69cfcc7c6c31c29b2f63a2f5c37f2d38bc773b" dependencies = [ "convert_case", "proc-macro2", @@ -1629,7 +1629,6 @@ dependencies = [ [[package]] name = "vihaco-parser" version = "0.1.0" -source = "git+https://github.com/QuEraComputing/stellarscope.git#0b69cfcc7c6c31c29b2f63a2f5c37f2d38bc773b" dependencies = [ "codespan", "codespan-reporting", diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 026b0f813..e09568f57 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -4,7 +4,9 @@ version = "0.1.0" edition = "2024" [dependencies] +bitvec = "1.0.1" eyre = "0.6.12" +num = "0.4.3" ppvm-runtime = { version = "0.1.0", path = "../ppvm-runtime" } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon"] } -vihaco = { git = "https://github.com/QuEraComputing/stellarscope.git", version = "0.1.0" } +vihaco = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco" } diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 6b40bf88e..c47fae729 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -1,7 +1,9 @@ use crate::instruction::CircuitInstruction; use crate::message::CircuitMessage; +use bitvec::view::BitView; use eyre::{Result, eyre}; -use ppvm_runtime::config::fxhash::ByteF64; +use num::PrimInt; +use num::complex::Complex64; use ppvm_tableau::prelude::*; use vihaco::{ExecContext, component}; @@ -14,16 +16,19 @@ macro_rules! batch_for { }; } -pub struct Circuit { - pub tab: GeneralizedTableau, I>, +pub struct Circuit, I: TableauIndex, C: SparseVector> { + pub tab: GeneralizedTableau, } pub type MeasurementResult = Vec>; #[component(instruction = CircuitInstruction, message = CircuitMessage, outcome = Option)] -impl Circuit +impl Circuit where + T: Config, + <::Storage as BitView>::Store: PrimInt, I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, { fn execute( &mut self, From 02c8135f0ceb18b3ccfe3f231cdd0b0c00a0a1b7 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 28 Apr 2026 13:56:23 +0200 Subject: [PATCH 06/95] Draft machine impl --- Cargo.lock | 12 + crates/ppvm-tableau/src/data.rs | 23 +- crates/ppvm-vihaco/Cargo.toml | 2 + crates/ppvm-vihaco/src/component.rs | 12 + crates/ppvm-vihaco/src/instruction.rs | 8 + crates/ppvm-vihaco/src/lib.rs | 1 + crates/ppvm-vihaco/src/machine.rs | 288 +++++++++++++++++++++ crates/ppvm-vihaco/tests/bell.sst | 14 + crates/ppvm-vihaco/tests/hello_circuit.sst | 12 + 9 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 crates/ppvm-vihaco/src/machine.rs create mode 100644 crates/ppvm-vihaco/tests/bell.sst create mode 100644 crates/ppvm-vihaco/tests/hello_circuit.sst diff --git a/Cargo.lock b/Cargo.lock index fa79ad227..25518f1af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1161,6 +1161,8 @@ dependencies = [ "ppvm-runtime", "ppvm-tableau", "vihaco", + "vihaco-cpu", + "vihaco-parser", ] [[package]] @@ -1616,6 +1618,16 @@ dependencies = [ "vihaco-parser", ] +[[package]] +name = "vihaco-cpu" +version = "0.1.0" +dependencies = [ + "eyre", + "log", + "vihaco", + "vihaco-parser", +] + [[package]] name = "vihaco-macros" version = "0.1.0" diff --git a/crates/ppvm-tableau/src/data.rs b/crates/ppvm-tableau/src/data.rs index 5975a1717..fcf710988 100644 --- a/crates/ppvm-tableau/src/data.rs +++ b/crates/ppvm-tableau/src/data.rs @@ -27,7 +27,7 @@ pub struct Tableau { } impl Tableau { - pub fn new(n_qubits: usize) -> Self { + fn new_data(n_qubits: usize) -> Vec> { // Initialize tableau for 0 state let mut data: Vec> = Vec::with_capacity(2 * n_qubits); @@ -44,7 +44,11 @@ impl Tableau { pw.set(i, Pauli::Z); data.push(pw); } + data + } + pub fn new(n_qubits: usize) -> Self { + let data = Tableau::::new_data(n_qubits); Self { n_qubits, data, @@ -58,6 +62,11 @@ impl Tableau { t } + pub fn reset_all(&mut self) { + let data = Tableau::::new_data(self.n_qubits); + self.data = data; + } + #[inline] pub fn stabilizers(&self) -> &[PhasedPauliWordNoHash] { &self.data[self.n_qubits..] @@ -545,6 +554,18 @@ where s } + pub fn reset_all(&mut self) { + self.tableau.reset_all(); + + let mut coefficients = C::new(); + let complex_one = Complex { + re: T::Coeff::one(), + im: T::Coeff::zero(), + }; + coefficients.unsafe_insert(I::zero(), complex_one); + self.coefficients = coefficients; + } + /// Clone the quantum state but reinitialize the RNG, producing an independent simulation /// branch. If `seed` is `Some`, the new RNG is seeded deterministically; if `None`, it is /// seeded from OS entropy. diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index e09568f57..52b3f0950 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -10,3 +10,5 @@ num = "0.4.3" ppvm-runtime = { version = "0.1.0", path = "../ppvm-runtime" } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon"] } vihaco = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco" } +vihaco-cpu = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco-cpu" } +vihaco-parser = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco-parser" } diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index c47fae729..ca2c83516 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -185,3 +185,15 @@ where Ok(None) } } + +impl vihaco::Reset for Circuit +where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, +{ + fn reset(&mut self) { + self.tab.reset_all(); + } +} diff --git a/crates/ppvm-vihaco/src/instruction.rs b/crates/ppvm-vihaco/src/instruction.rs index d4dcb6374..09a36815d 100644 --- a/crates/ppvm-vihaco/src/instruction.rs +++ b/crates/ppvm-vihaco/src/instruction.rs @@ -15,7 +15,9 @@ pub enum CircuitInstruction { SqrtYAdj, // Controlled gates + #[mnemonic = "cnot"] CNOT, + #[mnemonic = "cz"] CZ, // T gate @@ -23,13 +25,19 @@ pub enum CircuitInstruction { TAdj, // Single-qubit rotations + #[mnemonic = "rx"] RX, + #[mnemonic = "ry"] RY, + #[mnemonic = "rz"] RZ, // Two-qubit rotations + #[mnemonic = "rxx"] RXX, + #[mnemonic = "ryy"] RYY, + #[mnemonic = "rzz"] RZZ, // U3 diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index c5d610c79..520f4d6b4 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -1,5 +1,6 @@ pub mod component; pub mod instruction; +pub mod machine; pub mod message; pub mod prelude { diff --git a/crates/ppvm-vihaco/src/machine.rs b/crates/ppvm-vihaco/src/machine.rs new file mode 100644 index 000000000..c8836d160 --- /dev/null +++ b/crates/ppvm-vihaco/src/machine.rs @@ -0,0 +1,288 @@ +use num::complex::Complex64; +use ppvm_runtime::config::fx64hash::Byte8F64; +use ppvm_tableau::data::GeneralizedTableau; +use vihaco::machine::Machine; +use vihaco::observer::stdio::{StdoutEvent, StdoutObserver}; +use vihaco::traits::{GetProgramGlobal, ProgramCounter, StackMemory}; +use vihaco::{GeneratedMachine, ProgramLoader}; +use vihaco_cpu::{CPU, CPUMessage, StepOutcome}; + +use crate::message::CircuitMessage; +use crate::prelude::{Circuit, CircuitInstruction}; + +pub type Instruction = PPVM128Instruction; + +#[derive(vihaco::Machine)] +pub struct PPVM128 { + #[program] + loader: ProgramLoader, + + #[device(0x00, resolve_with = resolve_cpu, custom_parser)] + cpu: CPU, + + #[device(0x01, resolve_with = resolve_circuit)] + circuit: Circuit, u128, Vec<(Complex64, u128)>>, + + #[observe(StdoutEvent)] + stdout: StdoutObserver, +} + +impl From for PPVM128Instruction { + fn from(value: vihaco_cpu::Instruction) -> Self { + Self::Cpu(value) + } +} + +impl From for PPVM128Instruction { + fn from(value: CircuitInstruction) -> Self { + Self::Circuit(value) + } +} + +impl PPVM128 { + fn resolve_cpu(&mut self, inst: &vihaco_cpu::Instruction) -> eyre::Result { + match inst { + vihaco_cpu::Instruction::IndirectCall => { + let function_id: u32 = self.cpu.stack_top()?.get_function_ref()?; + let function = self.loader.get_function(function_id as usize)?; + Ok(CPUMessage::FunctionInfo { + arity: function.signature.params.len() as u32, + start_address: function.start_address, + }) + } + vihaco_cpu::Instruction::Print => { + let value = *self.cpu.stack_top()?; + match value { + vihaco::Value::String(addr) => { + let string = self.loader.get_string(addr as usize)?.clone(); + Ok(CPUMessage::Print(string)) + } + value => Ok(CPUMessage::Print(value.to_string())), + } + } + vihaco_cpu::Instruction::Const(v) => { + self.cpu.stack_push(*v); + Ok(CPUMessage::None) + } + _ => Ok(CPUMessage::None), + } + } + + fn resolve_circuit(&mut self, inst: &CircuitInstruction) -> eyre::Result { + use crate::instruction::CircuitInstruction::*; + match inst { + X | Y | Z | H | S | SAdj | SqrtX | SqrtY | SqrtXAdj | SqrtYAdj | T | TAdj | Measure + | Reset => { + let q = self.pop_qubit()?; + Ok(CircuitMessage::Qubit(q)) + } + CNOT | CZ => { + let q1 = self.pop_qubit()?; + let q0 = self.pop_qubit()?; + Ok(CircuitMessage::TwoQubit(q0, q1)) + } + RX | RY | RZ | Depolarize | Loss => { + let theta = self.pop_f64()?; + let q = self.pop_qubit()?; + Ok(CircuitMessage::QubitAndFloat(q, theta)) + } + RXX | RYY | RZZ | Depolarize2 => { + let theta = self.pop_f64()?; + let q0 = self.pop_qubit()?; + let q1 = self.pop_qubit()?; + Ok(CircuitMessage::TwoQubitAndFloat(q0, q1, theta)) + } + U3 => { + let lam = self.pop_f64()?; + let phi = self.pop_f64()?; + let theta = self.pop_f64()?; + let q = self.pop_qubit()?; + Ok(CircuitMessage::QubitU3(q, theta, phi, lam)) + } + + // TODO: pop actual float arrays? + PauliError => { + let pz = self.pop_f64()?; + let py = self.pop_f64()?; + let px = self.pop_f64()?; + let q = self.pop_qubit()?; + Ok(CircuitMessage::QubitAndFloatArr3(q, [px, py, pz])) + } + CorrelatedLoss => { + let p2 = self.pop_f64()?; + let p1 = self.pop_f64()?; + let p0 = self.pop_f64()?; + let q0 = self.pop_qubit()?; + let q1 = self.pop_qubit()?; + Ok(CircuitMessage::TwoQubitAndFloatArr3(q0, q1, [p0, p1, p2])) + } + TwoQubitPauliError => { + todo!() + } + } + } + + fn pop_qubit(&mut self) -> eyre::Result { + match self.cpu.stack_pop()? { + vihaco::Value::U32(v) => Ok(v as usize), + vihaco::Value::U64(v) => usize::try_from(v).map_err(Into::into), + vihaco::Value::I64(v) => usize::try_from(v).map_err(Into::into), + v => Err(eyre::eyre!("Expected qubit address, got {:?}", v)), + } + } + + fn pop_f64(&mut self) -> eyre::Result { + match self.cpu.stack_pop()? { + vihaco::Value::F64(v) => Ok(v), + v => Err(eyre::eyre!("Expected f64 argument, got {:?}", v)), + } + } +} + +impl vihaco::Reset for PPVM128 { + fn reset(&mut self) { + self.cpu.reset(); + self.circuit.reset(); + self.loader.pc = 0; + } +} + +impl Machine for PPVM128 { + type MachineStepResult = StepOutcome; + + fn init(&mut self) -> eyre::Result<()> { + self.circuit = Circuit { + tab: GeneralizedTableau::new(10, 1e-10), + }; + Ok(()) + } + + fn load( + &mut self, + module: &vihaco::module::Module< + Instruction, + vihaco::Value, + vihaco::Type, + vihaco::module::NoInfo, + >, + ) -> eyre::Result<()> { + self.loader.module = module.clone(); + Ok(()) + } + + fn step(&mut self) -> eyre::Result { + let mut ctx = vihaco::ExecContext::new(0); + let inst = self.peek_instruction()?.clone(); + let outcome = match inst { + PPVM128Instruction::Cpu(cpu_inst) => { + let msg = self.resolve_cpu(&cpu_inst)?; + vihaco::GeneratedComponent::execute_generated( + &mut self.cpu, + cpu_inst, + msg, + &mut ctx, + )? + } + PPVM128Instruction::Circuit(circuit_inst) => { + let msg = self.resolve_circuit(&circuit_inst)?; + vihaco::GeneratedComponent::execute_generated( + &mut self.circuit, + circuit_inst, + msg, + &mut ctx, + )?; + StepOutcome::Continue + } + }; + + if outcome == StepOutcome::Continue { + if let Some(target) = self.cpu.take_pending_pc() { + *self.pc_mut() = target; + } else { + *self.pc_mut() += 1; + } + } + + for event in ctx.into_events() { + let _ = ::deliver_any(self, event.as_ref()); + } + + Ok(outcome) + } + + fn run(&mut self) -> eyre::Result<()> { + self.init()?; + loop { + match Machine::step(self)? { + StepOutcome::Continue => continue, + StepOutcome::Breakpoint | StepOutcome::Halt => break, + StepOutcome::Return => return Ok(()), + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use vihaco::{Type, Value, module::Module}; + + use super::*; + + #[test] + fn test_run_ppvm() { + let mut module: Module = Module::default(); + + /* + const.u64 0 + gate h + */ + let zero = PPVM128Instruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))); + let one = PPVM128Instruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(1))); + module.code.push(zero.clone()); + module + .code + .push(PPVM128Instruction::Circuit(CircuitInstruction::H)); + + /* + const.u64 0 + gate t + */ + + module.code.push(zero.clone()); + module + .code + .push(PPVM128Instruction::Circuit(CircuitInstruction::T)); + + /* + const.u64 0 + const.u64 1 + gate cnot + */ + module.code.push(zero.clone()); + module.code.push(one.clone()); + module + .code + .push(PPVM128Instruction::Circuit(CircuitInstruction::CNOT)); + + let mut machine = PPVM128 { + loader: ProgramLoader::default(), + cpu: CPU::default(), + circuit: Circuit { + tab: GeneralizedTableau::new(2, 1e-10), + }, + stdout: StdoutObserver::default(), + }; + + println!("{:?}", module.code); + + machine.load(&module).unwrap(); + + for _ in 0..module.code.len() { + machine.step().unwrap(); + } + + println!("{}", machine.circuit.tab); + assert_eq!(machine.circuit.tab.coefficients.len(), 2); + } +} diff --git a/crates/ppvm-vihaco/tests/bell.sst b/crates/ppvm-vihaco/tests/bell.sst new file mode 100644 index 000000000..658659419 --- /dev/null +++ b/crates/ppvm-vihaco/tests/bell.sst @@ -0,0 +1,14 @@ +fn @main() { + const.u64 0 + gate h + + const.u64 0 + const.u64 1 + gate cnot + + const.u64 0 + gate measure + + const.u64 0 + gate measure +} \ No newline at end of file diff --git a/crates/ppvm-vihaco/tests/hello_circuit.sst b/crates/ppvm-vihaco/tests/hello_circuit.sst new file mode 100644 index 000000000..686a59d31 --- /dev/null +++ b/crates/ppvm-vihaco/tests/hello_circuit.sst @@ -0,0 +1,12 @@ +fn @main() { + const.u64 0 + gate h + + const.u64 0 + const.u64 1 + gate cnot + + const.u64 0 + const.f64 0.1 + gate rx +} From 931cb124480626b37c861e8100ce41f6b40b2257 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 11 May 2026 15:58:11 +0200 Subject: [PATCH 07/95] Add TODO --- crates/ppvm-vihaco/src/machine.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/ppvm-vihaco/src/machine.rs b/crates/ppvm-vihaco/src/machine.rs index c8836d160..0800daff7 100644 --- a/crates/ppvm-vihaco/src/machine.rs +++ b/crates/ppvm-vihaco/src/machine.rs @@ -12,6 +12,8 @@ use crate::prelude::{Circuit, CircuitInstruction}; pub type Instruction = PPVM128Instruction; +// TODO: move hard-coding multiple bit widths to wrapper component and switch on component execution during runtime + #[derive(vihaco::Machine)] pub struct PPVM128 { #[program] From 1ed6da3b9d9b97011310074a0f9c4ad988063c63 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 13 May 2026 17:02:20 +0200 Subject: [PATCH 08/95] Start refactoring to new vihaco --- Cargo.lock | 355 ++++++++------------------ crates/ppvm-vihaco/Cargo.toml | 2 + crates/ppvm-vihaco/src/component.rs | 29 ++- crates/ppvm-vihaco/src/instruction.rs | 47 ++-- crates/ppvm-vihaco/src/lib.rs | 2 +- 5 files changed, 163 insertions(+), 272 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25518f1af..56da4852d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anes" version = "0.1.6" @@ -96,12 +102,12 @@ dependencies = [ ] [[package]] -name = "ascii-canvas" -version = "4.0.0" +name = "ar_archive_writer" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ - "term", + "object", ] [[package]] @@ -130,21 +136,6 @@ dependencies = [ "virtue", ] -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bitflags" version = "2.9.4" @@ -163,15 +154,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "bnum" version = "0.13.0" @@ -225,6 +207,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.3" @@ -238,10 +230,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures", "rand_core", ] +[[package]] +name = "chumsky" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14377e276b2c8300513dff55ba4cc4142b44e5d6de6d00eb5b2307d650bb4ec1" +dependencies = [ + "hashbrown 0.15.5", + "regex-automata 0.3.9", + "serde", + "stacker", + "unicode-ident", + "unicode-segmentation", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -327,7 +333,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -351,15 +357,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -433,16 +430,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "darling" version = "0.21.3" @@ -493,31 +480,12 @@ dependencies = [ "rayon", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "ena" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" -dependencies = [ - "log", -] - [[package]] name = "encode_unicode" version = "1.0.0" @@ -564,10 +532,10 @@ dependencies = [ ] [[package]] -name = "fixedbitset" -version = "0.5.7" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fnv" @@ -596,16 +564,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.3.3" @@ -663,6 +621,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -802,47 +762,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keccak" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" -dependencies = [ - "cpufeatures 0.2.17", -] - -[[package]] -name = "lalrpop" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" -dependencies = [ - "ascii-canvas", - "bit-set", - "ena", - "itertools 0.14.0", - "lalrpop-util", - "petgraph", - "pico-args", - "regex", - "regex-syntax", - "sha3", - "string_cache", - "term", - "unicode-xid", - "walkdir", -] - -[[package]] -name = "lalrpop-util" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" -dependencies = [ - "regex-automata", - "rustversion", -] - [[package]] name = "leb128fmt" version = "0.1.0" @@ -886,12 +805,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - [[package]] name = "num" version = "0.4.3" @@ -965,6 +878,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -983,16 +905,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - [[package]] name = "parking_lot_core" version = "0.9.11" @@ -1012,31 +924,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "plotters" version = "0.3.7" @@ -1156,6 +1043,7 @@ name = "ppvm-vihaco" version = "0.1.0" dependencies = [ "bitvec", + "chumsky", "eyre", "num", "ppvm-runtime", @@ -1163,14 +1051,9 @@ dependencies = [ "vihaco", "vihaco-cpu", "vihaco-parser", + "vihaco-parser-core", ] -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "prettyplease" version = "0.2.37" @@ -1190,6 +1073,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "pyo3" version = "0.27.1" @@ -1327,8 +1220,19 @@ checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.11", + "regex-syntax 0.8.6", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", ] [[package]] @@ -1339,9 +1243,15 @@ checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.6", ] +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + [[package]] name = "regex-syntax" version = "0.8.6" @@ -1425,14 +1335,10 @@ dependencies = [ ] [[package]] -name = "sha3" -version = "0.10.9" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" -dependencies = [ - "digest", - "keccak", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "similar" @@ -1440,12 +1346,6 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - [[package]] name = "smallvec" version = "1.15.1" @@ -1453,15 +1353,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "string_cache" -version = "0.8.9" +name = "stacker" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", ] [[package]] @@ -1493,15 +1394,6 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" -[[package]] -name = "term" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -1511,26 +1403,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -1541,21 +1413,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "unescaper" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e" -dependencies = [ - "thiserror", -] - [[package]] name = "unicode-ident" version = "1.0.19" @@ -1609,23 +1466,30 @@ name = "vihaco" version = "0.1.0" dependencies = [ "byteorder", + "chumsky", "codespan", "colored", "env_logger", "eyre", "log", + "smallvec", "vihaco-macros", "vihaco-parser", + "vihaco-parser-core", ] [[package]] name = "vihaco-cpu" version = "0.1.0" dependencies = [ + "chumsky", + "codespan", "eyre", "log", "vihaco", + "vihaco-macros", "vihaco-parser", + "vihaco-parser-core", ] [[package]] @@ -1642,12 +1506,17 @@ dependencies = [ name = "vihaco-parser" version = "0.1.0" dependencies = [ - "codespan", - "codespan-reporting", - "eyre", - "lalrpop", - "lalrpop-util", - "unescaper", + "proc-macro2", + "quote", + "syn", + "vihaco-parser-core", +] + +[[package]] +name = "vihaco-parser-core" +version = "0.1.0" +dependencies = [ + "chumsky", ] [[package]] @@ -1802,7 +1671,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 52b3f0950..8543d88d5 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] bitvec = "1.0.1" +chumsky = "0.10.0" eyre = "0.6.12" num = "0.4.3" ppvm-runtime = { version = "0.1.0", path = "../ppvm-runtime" } @@ -12,3 +13,4 @@ ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon vihaco = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco" } vihaco-cpu = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco-cpu" } vihaco-parser = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco-parser" } +vihaco-parser-core = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco-parser-core" } diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index ca2c83516..f36b9a65b 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -5,7 +5,7 @@ use eyre::{Result, eyre}; use num::PrimInt; use num::complex::Complex64; use ppvm_tableau::prelude::*; -use vihaco::{ExecContext, component}; +use vihaco::{Effects, component}; macro_rules! batch_for { ($tab:expr, $method:ident, $addrs:expr) => { @@ -22,7 +22,12 @@ pub struct Circuit, I: TableauIndex, C: SparseVector>; -#[component(instruction = CircuitInstruction, message = CircuitMessage, outcome = Option)] +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct MeasurementEffect { + measurement_results: MeasurementResult, +} + +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] impl Circuit where T: Config, @@ -34,8 +39,7 @@ where &mut self, inst: CircuitInstruction, msg: CircuitMessage, - _ctx: &mut ExecContext, - ) -> Result> { + ) -> Result> { use CircuitInstruction::*; use CircuitMessage::*; @@ -76,7 +80,9 @@ where // Measure & Reset (Measure, Qubit(addr)) => { let outcome = self.tab.measure(addr); - return Ok(Some(vec![outcome])); + return Ok(Effects::one(MeasurementEffect { + measurement_results: vec![outcome], + })); } (Reset, Qubit(addr)) => self.tab.reset(addr), @@ -169,7 +175,9 @@ where // Batch: measure (emits per qubit) (Measure, QubitBatch(addrs)) => { let outcomes = addrs.iter().map(|&addr| self.tab.measure(addr)); - return Ok(Some(outcomes.collect())); + return Ok(Effects::one(MeasurementEffect { + measurement_results: outcomes.collect(), + })); } // Fallback @@ -182,8 +190,15 @@ where } }; - Ok(None) + Ok(Effects::None) } + + // #[derive(observe)] + // fn foo(&mut self, effect: Effect) { + // match (effect.msg, effect.inst) { + // (CircuitInstruction, _) => self._execute(inst, msg, ctx), + // } + // } } impl vihaco::Reset for Circuit diff --git a/crates/ppvm-vihaco/src/instruction.rs b/crates/ppvm-vihaco/src/instruction.rs index 09a36815d..f5387d3e0 100644 --- a/crates/ppvm-vihaco/src/instruction.rs +++ b/crates/ppvm-vihaco/src/instruction.rs @@ -1,45 +1,51 @@ use vihaco::Instruction; +use vihaco_parser::Parse; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Instruction)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Instruction, Parse)] pub enum CircuitInstruction { + // NOTE: longer tokens need to go first + TwoQubitPauliError, // needs to go before T + // Single-Qubit Clifford gates X, Y, Z, H, - S, - SAdj, - SqrtX, - SqrtY, + + #[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 - #[mnemonic = "cnot"] CNOT, - #[mnemonic = "cz"] CZ, // T gate - T, TAdj, - - // Single-qubit rotations - #[mnemonic = "rx"] - RX, - #[mnemonic = "ry"] - RY, - #[mnemonic = "rz"] - RZ, + T, // Two-qubit rotations - #[mnemonic = "rxx"] RXX, - #[mnemonic = "ryy"] RYY, - #[mnemonic = "rzz"] RZZ, + // Single-qubit rotations + RX, + RY, + RZ, + // U3 U3, @@ -53,7 +59,6 @@ pub enum CircuitInstruction { // Noise PauliError, - TwoQubitPauliError, - Depolarize, Depolarize2, + Depolarize, } diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 520f4d6b4..b50cb0fc9 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -1,6 +1,6 @@ pub mod component; pub mod instruction; -pub mod machine; +// pub mod machine; pub mod message; pub mod prelude { From 75a5c11b21a7a5d31200021a8777cf4760e78b0c Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Fri, 15 May 2026 16:31:59 +0200 Subject: [PATCH 09/95] Rename Circuit component --- crates/ppvm-vihaco/src/component.rs | 6 +++--- crates/ppvm-vihaco/src/lib.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index f36b9a65b..b60fa8752 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -16,7 +16,7 @@ macro_rules! batch_for { }; } -pub struct Circuit, I: TableauIndex, C: SparseVector> { +pub struct CircuitExecutor, I: TableauIndex, C: SparseVector> { pub tab: GeneralizedTableau, } @@ -28,7 +28,7 @@ pub struct MeasurementEffect { } #[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] -impl Circuit +impl CircuitExecutor where T: Config, <::Storage as BitView>::Store: PrimInt, @@ -201,7 +201,7 @@ where // } } -impl vihaco::Reset for Circuit +impl vihaco::Reset for CircuitExecutor where T: Config, <::Storage as BitView>::Store: PrimInt, diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index b50cb0fc9..d50cef861 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -4,6 +4,6 @@ pub mod instruction; pub mod message; pub mod prelude { - pub use crate::component::Circuit; + pub use crate::component::CircuitExecutor; pub use crate::instruction::CircuitInstruction; } From e49316505dfadca152a33049a229f06169baa28f Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 19 May 2026 16:06:39 +0200 Subject: [PATCH 10/95] Also reset loss state --- crates/ppvm-tableau/src/data.rs | 3 +++ crates/ppvm-vihaco/src/component.rs | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/ppvm-tableau/src/data.rs b/crates/ppvm-tableau/src/data.rs index 74208587b..12ad3867d 100644 --- a/crates/ppvm-tableau/src/data.rs +++ b/crates/ppvm-tableau/src/data.rs @@ -653,6 +653,9 @@ where }; coefficients.unsafe_insert(I::zero(), complex_one); self.coefficients = coefficients; + for l in self.is_lost.iter_mut() { + *l &= false; + } } /// Clone the quantum state but reinitialize the RNG, producing an independent simulation diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index b60fa8752..3f6474df3 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -39,6 +39,14 @@ where &mut self, inst: CircuitInstruction, msg: CircuitMessage, + ) -> Result> { + self.execute_instruction(inst, msg) + } + + fn execute_instruction( + &mut self, + inst: CircuitInstruction, + msg: CircuitMessage, ) -> Result> { use CircuitInstruction::*; use CircuitMessage::*; @@ -194,7 +202,7 @@ where } // #[derive(observe)] - // fn foo(&mut self, effect: Effect) { + // fn observe_circuit_instruction(&mut self, effect: Effect) { // match (effect.msg, effect.inst) { // (CircuitInstruction, _) => self._execute(inst, msg, ctx), // } From 1394c578a067a34d0cc03b6d1531f0f5e973edc9 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 19 May 2026 17:44:28 +0200 Subject: [PATCH 11/95] Add a component that dispatches and make components observe effects --- Cargo.lock | 1 + crates/ppvm-vihaco/Cargo.toml | 1 + crates/ppvm-vihaco/src/component.rs | 207 ++++++++++++++++++---------- 3 files changed, 140 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c90f74090..8c241fdab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1159,6 +1159,7 @@ name = "ppvm-vihaco" version = "0.1.0" dependencies = [ "bitvec", + "bnum", "chumsky 0.10.1", "eyre", "num", diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 8543d88d5..5fcfdc4f8 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] bitvec = "1.0.1" +bnum = { version = "0.13.0", features = ["num-traits"] } chumsky = "0.10.0" eyre = "0.6.12" num = "0.4.3" diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 3f6474df3..25b7d9bcd 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -1,18 +1,20 @@ use crate::instruction::CircuitInstruction; use crate::message::CircuitMessage; use bitvec::view::BitView; +use bnum::types::{U256, U512, U1024, U2048}; use eyre::{Result, eyre}; use num::PrimInt; use num::complex::Complex64; +use ppvm_runtime::config::fx64hash::Byte8F64; use ppvm_tableau::prelude::*; -use vihaco::{Effects, component}; +use vihaco::{Effects, component, observe}; macro_rules! batch_for { ($tab:expr, $method:ident, $addrs:expr) => { - for addr in &$addrs { $tab.$method(*addr); } + for addr in $addrs { $tab.$method(*addr); } }; ($tab:expr, $method:ident, $addrs:expr, $($arg:expr),+) => { - for addr in &$addrs { $tab.$method(*addr, $($arg),+); } + for addr in $addrs { $tab.$method(*addr, $($arg),+); } }; } @@ -40,84 +42,84 @@ where inst: CircuitInstruction, msg: CircuitMessage, ) -> Result> { - self.execute_instruction(inst, msg) + self.execute_instruction(&inst, &msg) } fn execute_instruction( &mut self, - inst: CircuitInstruction, - msg: CircuitMessage, + inst: &CircuitInstruction, + msg: &CircuitMessage, ) -> Result> { use CircuitInstruction::*; use CircuitMessage::*; match (inst, msg) { // Single-qubit Clifford - (X, Qubit(addr)) => self.tab.x(addr), - (Y, Qubit(addr)) => self.tab.y(addr), - (Z, Qubit(addr)) => self.tab.z(addr), - (H, Qubit(addr)) => self.tab.h(addr), - (S, Qubit(addr)) => self.tab.s(addr), - (SAdj, Qubit(addr)) => self.tab.s_adj(addr), - (SqrtX, Qubit(addr)) => self.tab.sqrt_x(addr), - (SqrtY, Qubit(addr)) => self.tab.sqrt_y(addr), - (SqrtXAdj, Qubit(addr)) => self.tab.sqrt_x_adj(addr), - (SqrtYAdj, Qubit(addr)) => self.tab.sqrt_y_adj(addr), + (X, &Qubit(addr)) => self.tab.x(addr), + (Y, &Qubit(addr)) => self.tab.y(addr), + (Z, &Qubit(addr)) => self.tab.z(addr), + (H, &Qubit(addr)) => self.tab.h(addr), + (S, &Qubit(addr)) => self.tab.s(addr), + (SAdj, &Qubit(addr)) => self.tab.s_adj(addr), + (SqrtX, &Qubit(addr)) => self.tab.sqrt_x(addr), + (SqrtY, &Qubit(addr)) => self.tab.sqrt_y(addr), + (SqrtXAdj, &Qubit(addr)) => self.tab.sqrt_x_adj(addr), + (SqrtYAdj, &Qubit(addr)) => self.tab.sqrt_y_adj(addr), // Controlled gates - (CNOT, TwoQubit(addr0, addr1)) => self.tab.cnot(addr0, addr1), - (CZ, TwoQubit(addr0, addr1)) => self.tab.cz(addr0, addr1), + (CNOT, &TwoQubit(addr0, addr1)) => self.tab.cnot(addr0, addr1), + (CZ, &TwoQubit(addr0, addr1)) => self.tab.cz(addr0, addr1), // T gate - (T, Qubit(addr)) => self.tab.t(addr), - (TAdj, Qubit(addr)) => self.tab.t_adj(addr), + (T, &Qubit(addr)) => self.tab.t(addr), + (TAdj, &Qubit(addr)) => self.tab.t_adj(addr), // Single-qubit rotations - (RX, QubitAndFloat(addr, angle)) => self.tab.rx(addr, angle), - (RY, QubitAndFloat(addr, angle)) => self.tab.ry(addr, angle), - (RZ, QubitAndFloat(addr, angle)) => self.tab.rz(addr, angle), + (RX, &QubitAndFloat(addr, angle)) => self.tab.rx(addr, angle), + (RY, &QubitAndFloat(addr, angle)) => self.tab.ry(addr, angle), + (RZ, &QubitAndFloat(addr, angle)) => self.tab.rz(addr, angle), // Two-qubit rotations - (RXX, TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rxx(addr0, addr1, angle), - (RYY, TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.ryy(addr0, addr1, angle), - (RZZ, TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rzz(addr0, addr1, angle), + (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rxx(addr0, addr1, angle), + (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.ryy(addr0, addr1, angle), + (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rzz(addr0, addr1, angle), // U3 - (U3, QubitU3(addr, theta, phi, lam)) => self.tab.u3(addr, theta, phi, lam), + (U3, &QubitU3(addr, theta, phi, lam)) => self.tab.u3(addr, theta, phi, lam), // Measure & Reset - (Measure, Qubit(addr)) => { + (Measure, &Qubit(addr)) => { let outcome = self.tab.measure(addr); return Ok(Effects::one(MeasurementEffect { measurement_results: vec![outcome], })); } - (Reset, Qubit(addr)) => self.tab.reset(addr), + (Reset, &Qubit(addr)) => self.tab.reset(addr), // Noise - (Depolarize, QubitAndFloat(addr, p)) => self.tab.depolarize(addr, p), - (Depolarize2, TwoQubitAndFloat(addr0, addr1, p)) => { + (Depolarize, &QubitAndFloat(addr, p)) => self.tab.depolarize(addr, p), + (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { self.tab.depolarize2(addr0, addr1, p) } - (PauliError, QubitAndFloatArr3(addr0, ps)) => self.tab.pauli_error(addr0, ps), + (PauliError, QubitAndFloatArr3(addr0, ps)) => self.tab.pauli_error(*addr0, *ps), (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { - self.tab.two_qubit_pauli_error(addr0, addr1, ps) + self.tab.two_qubit_pauli_error(*addr0, *addr1, *ps) } // Loss - (Loss, QubitAndFloat(addr, p)) => self.tab.loss_channel(addr, p), + (Loss, &QubitAndFloat(addr, p)) => self.tab.loss_channel(addr, p), (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { - self.tab.correlated_loss_channel(addr0, addr1, ps) + self.tab.correlated_loss_channel(*addr0, *addr1, *ps) } /* BATCH OPERATIONS START HERE */ // Batch: dedicated batch methods - (SqrtX, QubitBatch(addrs)) => self.tab.sqrt_x_batch(&addrs), - (SqrtY, QubitBatch(addrs)) => self.tab.sqrt_y_batch(&addrs), - (SqrtXAdj, QubitBatch(addrs)) => self.tab.sqrt_x_adj_batch(&addrs), - (SqrtYAdj, QubitBatch(addrs)) => self.tab.sqrt_y_adj_batch(&addrs), - (H, QubitBatch(addrs)) => self.tab.h_batch(&addrs), - (CZ, TwoQubitBatch(pairs)) => self.tab.cz_batch(&pairs), + (SqrtX, QubitBatch(addrs)) => self.tab.sqrt_x_batch(addrs), + (SqrtY, QubitBatch(addrs)) => self.tab.sqrt_y_batch(addrs), + (SqrtXAdj, QubitBatch(addrs)) => self.tab.sqrt_x_adj_batch(addrs), + (SqrtYAdj, QubitBatch(addrs)) => self.tab.sqrt_y_adj_batch(addrs), + (H, QubitBatch(addrs)) => self.tab.h_batch(addrs), + (CZ, TwoQubitBatch(pairs)) => self.tab.cz_batch(pairs), // TODO: replace things below by actual batched methods once they are available // Batch: single-qubit for loops @@ -129,54 +131,54 @@ where (T, QubitBatch(addrs)) => batch_for!(self.tab, t, addrs), (TAdj, QubitBatch(addrs)) => batch_for!(self.tab, t_adj, addrs), (Reset, QubitBatch(addrs)) => batch_for!(self.tab, reset, addrs), - (RX, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, rx, addrs, angle), - (RY, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, ry, addrs, angle), - (RZ, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, rz, addrs, angle), + (RX, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, rx, addrs, *angle), + (RY, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, ry, addrs, *angle), + (RZ, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, rz, addrs, *angle), (Depolarize, QubitBatchAndFloat(addrs, p)) => { - batch_for!(self.tab, depolarize, addrs, p) + batch_for!(self.tab, depolarize, addrs, *p) } - (Loss, QubitBatchAndFloat(addrs, p)) => batch_for!(self.tab, loss_channel, addrs, p), + (Loss, QubitBatchAndFloat(addrs, p)) => batch_for!(self.tab, loss_channel, addrs, *p), (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { - batch_for!(self.tab, pauli_error, addrs, ps) + batch_for!(self.tab, pauli_error, addrs, *ps) } (U3, QubitBatchU3(addrs, theta, phi, lam)) => { - batch_for!(self.tab, u3, addrs, theta, phi, lam) + batch_for!(self.tab, u3, addrs, *theta, *phi, *lam) } // Batch: two-qubit for loops (CNOT, TwoQubitBatch(pairs)) => { - for (a, b) in &pairs { - self.tab.cnot(*a, *b); + for &(a, b) in pairs { + self.tab.cnot(a, b); } } (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { - for (a, b) in &pairs { - self.tab.rxx(*a, *b, angle); + for &(a, b) in pairs { + self.tab.rxx(a, b, *angle); } } (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { - for (a, b) in &pairs { - self.tab.ryy(*a, *b, angle); + for &(a, b) in pairs { + self.tab.ryy(a, b, *angle); } } (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { - for (a, b) in &pairs { - self.tab.rzz(*a, *b, angle); + for &(a, b) in pairs { + self.tab.rzz(a, b, *angle); } } (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { - for (a, b) in &pairs { - self.tab.depolarize2(*a, *b, p); + for &(a, b) in pairs { + self.tab.depolarize2(a, b, *p); } } (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { - for (a, b) in &pairs { - self.tab.two_qubit_pauli_error(*a, *b, ps); + for &(a, b) in pairs { + self.tab.two_qubit_pauli_error(a, b, *ps); } } (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { - for (a, b) in &pairs { - self.tab.correlated_loss_channel(*a, *b, ps); + for &(a, b) in pairs { + self.tab.correlated_loss_channel(a, b, *ps); } } @@ -200,13 +202,6 @@ where Ok(Effects::None) } - - // #[derive(observe)] - // fn observe_circuit_instruction(&mut self, effect: Effect) { - // match (effect.msg, effect.inst) { - // (CircuitInstruction, _) => self._execute(inst, msg, ctx), - // } - // } } impl vihaco::Reset for CircuitExecutor @@ -220,3 +215,77 @@ where self.tab.reset_all(); } } + +pub enum Circuit { + Bits64(CircuitExecutor, usize, Vec<(Complex64, usize)>>), + Bits128(CircuitExecutor, u128, Vec<(Complex64, u128)>>), + Bits256(CircuitExecutor, U256, Vec<(Complex64, U256)>>), + Bits512(CircuitExecutor, U512, Vec<(Complex64, U512)>>), + Bits1024(CircuitExecutor, U1024, Vec<(Complex64, U1024)>>), + Bits2048(CircuitExecutor, U2048, Vec<(Complex64, U2048)>>), +} + +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] +impl Circuit { + pub fn new(n_qubits: usize, coefficient_threshold: f64) -> Self { + if n_qubits <= 64 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits64(CircuitExecutor { tab }) + } else if n_qubits <= 128 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits128(CircuitExecutor { tab }) + } else if n_qubits <= 256 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits256(CircuitExecutor { tab }) + } else if n_qubits <= 512 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits512(CircuitExecutor { tab }) + } else if n_qubits <= 1024 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits1024(CircuitExecutor { tab }) + } else if n_qubits <= 2048 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits2048(CircuitExecutor { tab }) + } else { + panic!("No matching executor for {} qubits", n_qubits); + } + } + + fn execute( + &mut self, + inst: CircuitInstruction, + msg: CircuitMessage, + ) -> Result> { + self.execute_instruction(&inst, &msg) + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + match self { + Self::Bits64(ex) => ex.execute_instruction(inst, msg), + Self::Bits128(ex) => ex.execute_instruction(inst, msg), + Self::Bits256(ex) => ex.execute_instruction(inst, msg), + Self::Bits512(ex) => ex.execute_instruction(inst, msg), + Self::Bits1024(ex) => ex.execute_instruction(inst, msg), + Self::Bits2048(ex) => ex.execute_instruction(inst, msg), + } + } +} + +pub struct CircuitEffect { + pub inst: CircuitInstruction, + pub msg: CircuitMessage, +} + +#[observe(CircuitEffect, effect=MeasurementEffect)] +impl Circuit { + fn observe_circuit_effect( + &mut self, + effect: &CircuitEffect, + ) -> Result> { + self.execute_instruction(&effect.inst, &effect.msg) + } +} From 91f7c41e2c36d6936019557b9c1b785584e3ab13 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 19 May 2026 17:45:21 +0200 Subject: [PATCH 12/95] More prelude exports --- crates/ppvm-vihaco/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index d50cef861..78197254b 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -4,6 +4,7 @@ pub mod instruction; pub mod message; pub mod prelude { - pub use crate::component::CircuitExecutor; + pub use crate::component::{Circuit, CircuitEffect}; pub use crate::instruction::CircuitInstruction; + pub use crate::message::CircuitMessage; } From 4e65f3323faa9f83d8e5d9b7d4a90d54f77d083d Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 20 May 2026 11:42:58 +0200 Subject: [PATCH 13/95] Refactor machine into composite --- Cargo.lock | 1 + crates/ppvm-vihaco/Cargo.toml | 1 + crates/ppvm-vihaco/src/component.rs | 22 +- crates/ppvm-vihaco/src/composite.rs | 449 ++++++++++++++++++ crates/ppvm-vihaco/src/instruction.rs | 51 ++ crates/ppvm-vihaco/src/lib.rs | 3 +- crates/ppvm-vihaco/src/machine.rs | 290 ----------- .../ppvm-vihaco/src/measurement_observer.rs | 22 + 8 files changed, 541 insertions(+), 298 deletions(-) create mode 100644 crates/ppvm-vihaco/src/composite.rs delete mode 100644 crates/ppvm-vihaco/src/machine.rs create mode 100644 crates/ppvm-vihaco/src/measurement_observer.rs diff --git a/Cargo.lock b/Cargo.lock index 8c241fdab..c84934181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1162,6 +1162,7 @@ dependencies = [ "bnum", "chumsky 0.10.1", "eyre", + "log", "num", "ppvm-runtime", "ppvm-tableau", diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 5fcfdc4f8..dae8fab98 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -8,6 +8,7 @@ bitvec = "1.0.1" bnum = { version = "0.13.0", features = ["num-traits"] } chumsky = "0.10.0" eyre = "0.6.12" +log = "0.4.29" num = "0.4.3" ppvm-runtime = { version = "0.1.0", path = "../ppvm-runtime" } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon"] } diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 25b7d9bcd..3d48fc7c5 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -1,4 +1,5 @@ use crate::instruction::CircuitInstruction; +use crate::measurement_observer::MeasurementEffect; use crate::message::CircuitMessage; use bitvec::view::BitView; use bnum::types::{U256, U512, U1024, U2048}; @@ -22,13 +23,6 @@ pub struct CircuitExecutor, I: TableauIndex, C: SparseVec pub tab: GeneralizedTableau, } -pub type MeasurementResult = Vec>; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct MeasurementEffect { - measurement_results: MeasurementResult, -} - #[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] impl CircuitExecutor where @@ -275,6 +269,7 @@ impl Circuit { } } +#[derive(Debug, Clone)] pub struct CircuitEffect { pub inst: CircuitInstruction, pub msg: CircuitMessage, @@ -289,3 +284,16 @@ impl Circuit { self.execute_instruction(&effect.inst, &effect.msg) } } + +impl vihaco::Reset for Circuit { + fn reset(&mut self) { + match self { + Self::Bits64(ex) => ex.reset(), + Self::Bits128(ex) => ex.reset(), + Self::Bits256(ex) => ex.reset(), + Self::Bits512(ex) => ex.reset(), + Self::Bits1024(ex) => ex.reset(), + Self::Bits2048(ex) => ex.reset(), + }; + } +} diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs new file mode 100644 index 000000000..1c8a410d8 --- /dev/null +++ b/crates/ppvm-vihaco/src/composite.rs @@ -0,0 +1,449 @@ +use vihaco::Effects; +use vihaco::Observe; +use vihaco::ProgramLoader; +use vihaco::composite; +use vihaco::observe; +use vihaco::observer::stdio::StdoutEffect; +use vihaco::observer::stdio::StdoutObserver; +use vihaco::traits::{GetProgramGlobal, ProgramCounter, StackMemory}; +use vihaco_cpu::{CPU, CPUMessage, StepOutcome}; + +use crate::component::CircuitEffect; +use crate::measurement_observer::MeasurementResult; +use crate::measurement_observer::{MeasurementEffect, MeasurementObserver}; +use crate::message::CircuitMessage; +use crate::prelude::{Circuit, CircuitInstruction}; + +pub type Instruction = PPVMInstruction; + +#[composite] +pub struct PPVM { + #[program] + loader: ProgramLoader, + + #[device(0x00, resolve_with = resolve_cpu, custom_parser)] + cpu: CPU, + + #[device(0x01, resolve_with = resolve_circuit)] + circuit: Circuit, + + stdout: StdoutObserver, + + measurement_record: MeasurementObserver, +} + +// impl From for PPVMInstruction { +// fn from(value: vihaco_cpu::Instruction) -> Self { +// Self::Cpu(value) +// } +// } + +// impl From for PPVMInstruction { +// fn from(value: CircuitInstruction) -> Self { +// Self::Circuit(value) +// } +// } + +#[derive(Debug, Clone)] +pub enum PPVMEffect { + Step(StepOutcome), + Stdout(StdoutEffect), + Circuit(CircuitEffect), + Measurement(MeasurementEffect), +} + +#[observe(vihaco::observer::stdio::StdoutEffect, effect = PPVMEffect)] +impl PPVM { + fn observe_stdout_effect(&mut self, effect: &StdoutEffect) -> eyre::Result> { + Observe::::observe(&mut self.stdout, effect) + } +} + +#[observe(CircuitEffect, effect = PPVMEffect)] +impl PPVM { + fn observe_circuit_effect( + &mut self, + effect: &CircuitEffect, + ) -> eyre::Result> { + Observe::::observe(&mut self.circuit, effect) + } +} + +#[observe(MeasurementEffect, effect = PPVMEffect)] +impl PPVM { + fn observe_measurement_effect( + &mut self, + effect: &MeasurementEffect, + ) -> eyre::Result> { + Observe::::observe(&mut self.measurement_record, effect) + } +} + +impl From for PPVMEffect { + fn from(value: StdoutEffect) -> Self { + Self::Stdout(value) + } +} + +impl From for PPVMEffect { + fn from(value: MeasurementEffect) -> Self { + Self::Measurement(value) + } +} + +impl std::fmt::Display for PPVMInstruction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PPVMInstruction::Cpu(inst) => inst.fmt(f), + PPVMInstruction::Circuit(inst) => inst.fmt(f), + } + } +} + +impl From for PPVMInstruction { + fn from(value: vihaco_cpu::Instruction) -> Self { + Self::Cpu(value) + } +} + +impl From for PPVMInstruction { + fn from(value: CircuitInstruction) -> Self { + Self::Circuit(value) + } +} + +impl PPVM { + fn resolve_cpu(&mut self, inst: &vihaco_cpu::Instruction) -> eyre::Result { + match inst { + vihaco_cpu::Instruction::IndirectCall => { + let function_id: u32 = self.cpu.stack_top()?.get_function_ref()?; + let function = self.loader.get_function(function_id as usize)?; + Ok(CPUMessage::FunctionInfo { + arity: function.signature.params.len() as u32, + start_address: function.start_address, + }) + } + vihaco_cpu::Instruction::Print => { + let value = *self.cpu.stack_top()?; + match value { + vihaco::Value::String(addr) => { + let string = self.loader.get_string(addr as usize)?.clone(); + Ok(CPUMessage::Print(string)) + } + value => Ok(CPUMessage::Print(value.to_string())), + } + } + vihaco_cpu::Instruction::Const(v) => { + self.cpu.stack_push(*v); + Ok(CPUMessage::None) + } + _ => Ok(CPUMessage::None), + } + } + + fn resolve_circuit(&mut self, inst: &CircuitInstruction) -> eyre::Result { + use crate::instruction::CircuitInstruction::*; + match inst { + X | Y | Z | H | S | SAdj | SqrtX | SqrtY | SqrtXAdj | SqrtYAdj | T | TAdj | Measure + | Reset => { + let q = self.pop_qubit()?; + Ok(CircuitMessage::Qubit(q)) + } + CNOT | CZ => { + let q1 = self.pop_qubit()?; + let q0 = self.pop_qubit()?; + Ok(CircuitMessage::TwoQubit(q0, q1)) + } + RX | RY | RZ | Depolarize | Loss => { + let theta = self.pop_f64()?; + let q = self.pop_qubit()?; + Ok(CircuitMessage::QubitAndFloat(q, theta)) + } + RXX | RYY | RZZ | Depolarize2 => { + let theta = self.pop_f64()?; + let q0 = self.pop_qubit()?; + let q1 = self.pop_qubit()?; + Ok(CircuitMessage::TwoQubitAndFloat(q0, q1, theta)) + } + U3 => { + let lam = self.pop_f64()?; + let phi = self.pop_f64()?; + let theta = self.pop_f64()?; + let q = self.pop_qubit()?; + Ok(CircuitMessage::QubitU3(q, theta, phi, lam)) + } + + // TODO: pop actual float arrays? + PauliError => { + let pz = self.pop_f64()?; + let py = self.pop_f64()?; + let px = self.pop_f64()?; + let q = self.pop_qubit()?; + Ok(CircuitMessage::QubitAndFloatArr3(q, [px, py, pz])) + } + CorrelatedLoss => { + let p2 = self.pop_f64()?; + let p1 = self.pop_f64()?; + let p0 = self.pop_f64()?; + let q0 = self.pop_qubit()?; + let q1 = self.pop_qubit()?; + Ok(CircuitMessage::TwoQubitAndFloatArr3(q0, q1, [p0, p1, p2])) + } + TwoQubitPauliError => { + todo!() + } + } + } + + fn pop_qubit(&mut self) -> eyre::Result { + match self.cpu.stack_pop()? { + vihaco::Value::U32(v) => Ok(v as usize), + vihaco::Value::U64(v) => usize::try_from(v).map_err(Into::into), + vihaco::Value::I64(v) => usize::try_from(v).map_err(Into::into), + v => Err(eyre::eyre!("Expected qubit address, got {:?}", v)), + } + } + + fn pop_f64(&mut self) -> eyre::Result { + match self.cpu.stack_pop()? { + vihaco::Value::F64(v) => Ok(v), + v => Err(eyre::eyre!("Expected f64 argument, got {:?}", v)), + } + } + + pub fn init(&mut self) -> eyre::Result<()> { + let n_qubits = 10; + let coefficient_threshold = 1e-10; + self.circuit = Circuit::new(n_qubits, coefficient_threshold); + Ok(()) + } + + pub fn load( + &mut self, + module: &vihaco::module::Module< + Instruction, + vihaco::Value, + vihaco::Type, + vihaco::module::NoInfo, + >, + ) -> eyre::Result<()> { + self.loader.module = module.clone(); + Ok(()) + } + + pub fn step_once(&mut self) -> eyre::Result { + let inst = self.peek_instruction()?.clone(); + let effects = self.execute_effects(inst)?; + self.continue_effects(effects) + } + + fn execute_effects(&mut self, inst: Instruction) -> eyre::Result> { + log::debug!("exec inst: {:?}, stack: {:?}", inst, self.cpu.stack()); + match inst { + PPVMInstruction::Cpu(cpu_inst) => { + let msg = self.resolve_cpu(&cpu_inst)?; + let stdout_effect = match (&cpu_inst, &msg) { + (vihaco_cpu::Instruction::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)?, + )?; + if outcome == StepOutcome::Continue { + if let Some(target) = self.cpu.take_pending_pc() { + *self.loader.pc_mut() = target; + } else { + *self.loader.pc_mut() += 1; + } + } + let mut effects = Effects::one(PPVMEffect::Step(outcome)); + if let Some(stdout_effect) = stdout_effect { + effects = effects.append(stdout_effect); + } + Ok(effects) + } + PPVMInstruction::Circuit(inst) => { + let msg = self.resolve_circuit(&inst)?; + let measurement_effect = vihaco::expect_exactly_one_effect( + ::execute_generated( + &mut self.circuit, + inst, + msg, + )?, + )?; + *self.loader.pc_mut() += 1; + Ok(Effects::one(PPVMEffect::Measurement(measurement_effect))) + } + } + } + + fn continue_effects(&mut self, effects: Effects) -> eyre::Result { + let mut step_outcome = None; + for effect in effects { + match effect { + PPVMEffect::Step(outcome) => { + if step_outcome.replace(outcome).is_some() { + return Err(eyre::eyre!( + "expected exactly one PPVM step effect, got multiple" + )); + } + } + effect => self.continue_observer_effect(effect)?, + } + } + + step_outcome.ok_or_else(|| eyre::eyre!("expected exactly one PPVM step effect, got 0")) + } + + fn continue_observer_effect(&mut self, effect: PPVMEffect) -> eyre::Result<()> { + match effect { + PPVMEffect::Stdout(effect) => { + let follow_ups = Observe::::observe(self, &effect)?; + self.continue_observer_effects(follow_ups) + } + PPVMEffect::Circuit(effect) => { + let follow_ups = Observe::::observe(self, &effect)?; + self.continue_observer_effects(follow_ups) + } + PPVMEffect::Measurement(effect) => { + let follow_ups = Observe::::observe(self, &effect)?; + // TODO: do I need to push those values to stack? + // If so, what should we do with None (= lost qubit)? + // for outcome in effect.measurement_results { + // self.cpu.stack_push(outcome) + // } + self.continue_observer_effects(follow_ups) + } + PPVMEffect::Step(_) => Err(eyre::eyre!( + "unexpected Step effect while continuing PPVM observer follow-ups" + )), + } + } + + fn continue_observer_effects(&mut self, effects: Effects) -> eyre::Result<()> { + for effect in effects { + self.continue_observer_effect(effect)?; + } + Ok(()) + } + + pub fn run(&mut self) -> eyre::Result { + self.init()?; + + loop { + let action = self.step_once()?; + if action == StepOutcome::Continue { + continue; + } + return Ok(action); + } + } + + pub fn stdout(&self) -> &[u8] { + self.stdout.output() + } + + pub fn measurement_record(&self) -> Vec { + self.measurement_record.record.clone() + } +} + +impl vihaco::Reset for PPVM { + fn reset(&mut self) { + self.cpu.reset(); + self.circuit.reset(); + self.loader.pc = 0; + } +} + +#[cfg(test)] +mod tests { + use vihaco::{Type, Value, module::Module}; + + use super::*; + + #[test] + fn test_run_ppvm() -> eyre::Result<()> { + let mut module: Module = Module::default(); + + /* + const.u64 0 + gate h + */ + let zero = PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))); + let one = PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(1))); + module.code.push(zero.clone()); + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::H)); + + /* + const.u64 0 + gate t + */ + + module.code.push(zero.clone()); + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::T)); + + /* + const.u64 0 + const.u64 1 + gate cnot + */ + module.code.push(zero.clone()); + module.code.push(one.clone()); + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::CNOT)); + + let mut machine = PPVM { + loader: ProgramLoader::default(), + cpu: CPU::default(), + circuit: Circuit::new(2, 1e-10), + stdout: StdoutObserver::default(), + measurement_record: MeasurementObserver { record: Vec::new() }, + }; + + println!("{:?}", module.code); + + machine.load(&module)?; + + for _ in 0..module.code.len() { + machine.step_once()?; + } + + let num_coefficients = match &machine.circuit { + Circuit::Bits64(ex) => { + println!("{}", ex.tab); + ex.tab.coefficients.len() + } + Circuit::Bits128(ex) => { + println!("{}", ex.tab); + ex.tab.coefficients.len() + } + Circuit::Bits256(ex) => { + println!("{}", ex.tab); + ex.tab.coefficients.len() + } + Circuit::Bits512(ex) => { + println!("{}", ex.tab); + ex.tab.coefficients.len() + } + Circuit::Bits1024(ex) => { + println!("{}", ex.tab); + ex.tab.coefficients.len() + } + Circuit::Bits2048(ex) => { + println!("{}", ex.tab); + ex.tab.coefficients.len() + } + }; + + assert_eq!(num_coefficients, 2); + Ok(()) + } +} diff --git a/crates/ppvm-vihaco/src/instruction.rs b/crates/ppvm-vihaco/src/instruction.rs index f5387d3e0..85f7179b6 100644 --- a/crates/ppvm-vihaco/src/instruction.rs +++ b/crates/ppvm-vihaco/src/instruction.rs @@ -62,3 +62,54 @@ pub enum CircuitInstruction { Depolarize2, Depolarize, } + +impl std::fmt::Display for CircuitInstruction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use CircuitInstruction::*; + match self { + TwoQubitPauliError => write!(f, "TwoQubitPauliError"), + + X => write!(f, "X"), + Y => write!(f, "Y"), + Z => write!(f, "Z"), + H => write!(f, "H"), + + SqrtXAdj => write!(f, "SqrtXAdj"), + + SqrtX => write!(f, "SqrtX"), + + SqrtYAdj => write!(f, "SqrtYAdj"), + + SqrtY => write!(f, "SqrtY"), + + SAdj => write!(f, "SAdj"), + S => write!(f, "S"), + + CNOT => write!(f, "CNOT"), + CZ => write!(f, "CZ"), + + TAdj => write!(f, "TAdj"), + T => write!(f, "T"), + + RXX => write!(f, "RXX"), + RYY => write!(f, "RYY"), + RZZ => write!(f, "RZZ"), + + RX => write!(f, "RX"), + RY => write!(f, "RY"), + RZ => write!(f, "RZ"), + + U3 => write!(f, "U3"), + + Measure => write!(f, "Measure"), + Reset => write!(f, "Reset"), + + Loss => write!(f, "Loss"), + CorrelatedLoss => write!(f, "CorrelatedLoss"), + + PauliError => write!(f, "PauliError"), + Depolarize2 => write!(f, "Depolarize2"), + Depolarize => write!(f, "Depolarize"), + } + } +} diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 78197254b..79872591e 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -1,6 +1,7 @@ pub mod component; +pub mod composite; pub mod instruction; -// pub mod machine; +pub mod measurement_observer; pub mod message; pub mod prelude { diff --git a/crates/ppvm-vihaco/src/machine.rs b/crates/ppvm-vihaco/src/machine.rs deleted file mode 100644 index 0800daff7..000000000 --- a/crates/ppvm-vihaco/src/machine.rs +++ /dev/null @@ -1,290 +0,0 @@ -use num::complex::Complex64; -use ppvm_runtime::config::fx64hash::Byte8F64; -use ppvm_tableau::data::GeneralizedTableau; -use vihaco::machine::Machine; -use vihaco::observer::stdio::{StdoutEvent, StdoutObserver}; -use vihaco::traits::{GetProgramGlobal, ProgramCounter, StackMemory}; -use vihaco::{GeneratedMachine, ProgramLoader}; -use vihaco_cpu::{CPU, CPUMessage, StepOutcome}; - -use crate::message::CircuitMessage; -use crate::prelude::{Circuit, CircuitInstruction}; - -pub type Instruction = PPVM128Instruction; - -// TODO: move hard-coding multiple bit widths to wrapper component and switch on component execution during runtime - -#[derive(vihaco::Machine)] -pub struct PPVM128 { - #[program] - loader: ProgramLoader, - - #[device(0x00, resolve_with = resolve_cpu, custom_parser)] - cpu: CPU, - - #[device(0x01, resolve_with = resolve_circuit)] - circuit: Circuit, u128, Vec<(Complex64, u128)>>, - - #[observe(StdoutEvent)] - stdout: StdoutObserver, -} - -impl From for PPVM128Instruction { - fn from(value: vihaco_cpu::Instruction) -> Self { - Self::Cpu(value) - } -} - -impl From for PPVM128Instruction { - fn from(value: CircuitInstruction) -> Self { - Self::Circuit(value) - } -} - -impl PPVM128 { - fn resolve_cpu(&mut self, inst: &vihaco_cpu::Instruction) -> eyre::Result { - match inst { - vihaco_cpu::Instruction::IndirectCall => { - let function_id: u32 = self.cpu.stack_top()?.get_function_ref()?; - let function = self.loader.get_function(function_id as usize)?; - Ok(CPUMessage::FunctionInfo { - arity: function.signature.params.len() as u32, - start_address: function.start_address, - }) - } - vihaco_cpu::Instruction::Print => { - let value = *self.cpu.stack_top()?; - match value { - vihaco::Value::String(addr) => { - let string = self.loader.get_string(addr as usize)?.clone(); - Ok(CPUMessage::Print(string)) - } - value => Ok(CPUMessage::Print(value.to_string())), - } - } - vihaco_cpu::Instruction::Const(v) => { - self.cpu.stack_push(*v); - Ok(CPUMessage::None) - } - _ => Ok(CPUMessage::None), - } - } - - fn resolve_circuit(&mut self, inst: &CircuitInstruction) -> eyre::Result { - use crate::instruction::CircuitInstruction::*; - match inst { - X | Y | Z | H | S | SAdj | SqrtX | SqrtY | SqrtXAdj | SqrtYAdj | T | TAdj | Measure - | Reset => { - let q = self.pop_qubit()?; - Ok(CircuitMessage::Qubit(q)) - } - CNOT | CZ => { - let q1 = self.pop_qubit()?; - let q0 = self.pop_qubit()?; - Ok(CircuitMessage::TwoQubit(q0, q1)) - } - RX | RY | RZ | Depolarize | Loss => { - let theta = self.pop_f64()?; - let q = self.pop_qubit()?; - Ok(CircuitMessage::QubitAndFloat(q, theta)) - } - RXX | RYY | RZZ | Depolarize2 => { - let theta = self.pop_f64()?; - let q0 = self.pop_qubit()?; - let q1 = self.pop_qubit()?; - Ok(CircuitMessage::TwoQubitAndFloat(q0, q1, theta)) - } - U3 => { - let lam = self.pop_f64()?; - let phi = self.pop_f64()?; - let theta = self.pop_f64()?; - let q = self.pop_qubit()?; - Ok(CircuitMessage::QubitU3(q, theta, phi, lam)) - } - - // TODO: pop actual float arrays? - PauliError => { - let pz = self.pop_f64()?; - let py = self.pop_f64()?; - let px = self.pop_f64()?; - let q = self.pop_qubit()?; - Ok(CircuitMessage::QubitAndFloatArr3(q, [px, py, pz])) - } - CorrelatedLoss => { - let p2 = self.pop_f64()?; - let p1 = self.pop_f64()?; - let p0 = self.pop_f64()?; - let q0 = self.pop_qubit()?; - let q1 = self.pop_qubit()?; - Ok(CircuitMessage::TwoQubitAndFloatArr3(q0, q1, [p0, p1, p2])) - } - TwoQubitPauliError => { - todo!() - } - } - } - - fn pop_qubit(&mut self) -> eyre::Result { - match self.cpu.stack_pop()? { - vihaco::Value::U32(v) => Ok(v as usize), - vihaco::Value::U64(v) => usize::try_from(v).map_err(Into::into), - vihaco::Value::I64(v) => usize::try_from(v).map_err(Into::into), - v => Err(eyre::eyre!("Expected qubit address, got {:?}", v)), - } - } - - fn pop_f64(&mut self) -> eyre::Result { - match self.cpu.stack_pop()? { - vihaco::Value::F64(v) => Ok(v), - v => Err(eyre::eyre!("Expected f64 argument, got {:?}", v)), - } - } -} - -impl vihaco::Reset for PPVM128 { - fn reset(&mut self) { - self.cpu.reset(); - self.circuit.reset(); - self.loader.pc = 0; - } -} - -impl Machine for PPVM128 { - type MachineStepResult = StepOutcome; - - fn init(&mut self) -> eyre::Result<()> { - self.circuit = Circuit { - tab: GeneralizedTableau::new(10, 1e-10), - }; - Ok(()) - } - - fn load( - &mut self, - module: &vihaco::module::Module< - Instruction, - vihaco::Value, - vihaco::Type, - vihaco::module::NoInfo, - >, - ) -> eyre::Result<()> { - self.loader.module = module.clone(); - Ok(()) - } - - fn step(&mut self) -> eyre::Result { - let mut ctx = vihaco::ExecContext::new(0); - let inst = self.peek_instruction()?.clone(); - let outcome = match inst { - PPVM128Instruction::Cpu(cpu_inst) => { - let msg = self.resolve_cpu(&cpu_inst)?; - vihaco::GeneratedComponent::execute_generated( - &mut self.cpu, - cpu_inst, - msg, - &mut ctx, - )? - } - PPVM128Instruction::Circuit(circuit_inst) => { - let msg = self.resolve_circuit(&circuit_inst)?; - vihaco::GeneratedComponent::execute_generated( - &mut self.circuit, - circuit_inst, - msg, - &mut ctx, - )?; - StepOutcome::Continue - } - }; - - if outcome == StepOutcome::Continue { - if let Some(target) = self.cpu.take_pending_pc() { - *self.pc_mut() = target; - } else { - *self.pc_mut() += 1; - } - } - - for event in ctx.into_events() { - let _ = ::deliver_any(self, event.as_ref()); - } - - Ok(outcome) - } - - fn run(&mut self) -> eyre::Result<()> { - self.init()?; - loop { - match Machine::step(self)? { - StepOutcome::Continue => continue, - StepOutcome::Breakpoint | StepOutcome::Halt => break, - StepOutcome::Return => return Ok(()), - } - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use vihaco::{Type, Value, module::Module}; - - use super::*; - - #[test] - fn test_run_ppvm() { - let mut module: Module = Module::default(); - - /* - const.u64 0 - gate h - */ - let zero = PPVM128Instruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))); - let one = PPVM128Instruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(1))); - module.code.push(zero.clone()); - module - .code - .push(PPVM128Instruction::Circuit(CircuitInstruction::H)); - - /* - const.u64 0 - gate t - */ - - module.code.push(zero.clone()); - module - .code - .push(PPVM128Instruction::Circuit(CircuitInstruction::T)); - - /* - const.u64 0 - const.u64 1 - gate cnot - */ - module.code.push(zero.clone()); - module.code.push(one.clone()); - module - .code - .push(PPVM128Instruction::Circuit(CircuitInstruction::CNOT)); - - let mut machine = PPVM128 { - loader: ProgramLoader::default(), - cpu: CPU::default(), - circuit: Circuit { - tab: GeneralizedTableau::new(2, 1e-10), - }, - stdout: StdoutObserver::default(), - }; - - println!("{:?}", module.code); - - machine.load(&module).unwrap(); - - for _ in 0..module.code.len() { - machine.step().unwrap(); - } - - println!("{}", machine.circuit.tab); - assert_eq!(machine.circuit.tab.coefficients.len(), 2); - } -} diff --git a/crates/ppvm-vihaco/src/measurement_observer.rs b/crates/ppvm-vihaco/src/measurement_observer.rs new file mode 100644 index 000000000..3a4794da6 --- /dev/null +++ b/crates/ppvm-vihaco/src/measurement_observer.rs @@ -0,0 +1,22 @@ +use eyre::Result; +use vihaco::{Effects, observe}; + +pub type MeasurementResult = Vec>; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct MeasurementEffect { + pub measurement_results: MeasurementResult, +} + +#[derive(Debug, Default)] +pub struct MeasurementObserver { + pub record: Vec, +} + +#[observe(MeasurementEffect)] +impl MeasurementObserver { + fn observe_measurement_effect(&mut self, effect: &MeasurementEffect) -> Result> { + self.record.push(effect.measurement_results.clone()); + Ok(Effects::none()) + } +} From 6c56b58430b15f061ff3003161dd2610b5e7f9ca Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 20 May 2026 12:08:21 +0200 Subject: [PATCH 14/95] Fix measurement effect return and don't manually push const --- crates/ppvm-vihaco/src/composite.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 1c8a410d8..c0daa7111 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -133,10 +133,6 @@ impl PPVM { value => Ok(CPUMessage::Print(value.to_string())), } } - vihaco_cpu::Instruction::Const(v) => { - self.cpu.stack_push(*v); - Ok(CPUMessage::None) - } _ => Ok(CPUMessage::None), } } @@ -266,15 +262,17 @@ impl PPVM { } PPVMInstruction::Circuit(inst) => { let msg = self.resolve_circuit(&inst)?; - let measurement_effect = vihaco::expect_exactly_one_effect( - ::execute_generated( - &mut self.circuit, - inst, - msg, - )?, + let circuit_effects = ::execute_generated( + &mut self.circuit, + inst, + msg, )?; *self.loader.pc_mut() += 1; - Ok(Effects::one(PPVMEffect::Measurement(measurement_effect))) + let mut effects = Effects::one(PPVMEffect::Step(StepOutcome::Continue)); + for measurement_effect in circuit_effects { + effects = effects.append(PPVMEffect::Measurement(measurement_effect)); + } + Ok(effects) } } } @@ -414,6 +412,7 @@ mod tests { for _ in 0..module.code.len() { machine.step_once()?; + assert!(machine.cpu.stack().len() <= 2); } let num_coefficients = match &machine.circuit { From 5ce25eebcf709b6801504cde27bff22df1f77861 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 20 May 2026 12:09:08 +0200 Subject: [PATCH 15/95] Delete some old code --- crates/ppvm-vihaco/src/composite.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index c0daa7111..579a7b031 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -32,18 +32,6 @@ pub struct PPVM { measurement_record: MeasurementObserver, } -// impl From for PPVMInstruction { -// fn from(value: vihaco_cpu::Instruction) -> Self { -// Self::Cpu(value) -// } -// } - -// impl From for PPVMInstruction { -// fn from(value: CircuitInstruction) -> Self { -// Self::Circuit(value) -// } -// } - #[derive(Debug, Clone)] pub enum PPVMEffect { Step(StepOutcome), From 939d812f85d3911e987709a05e0ab521fa36ccf6 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 20 May 2026 14:54:10 +0200 Subject: [PATCH 16/95] Device info for PPVM --- crates/ppvm-vihaco/src/component.rs | 6 ++ crates/ppvm-vihaco/src/composite.rs | 125 +++++++++++++++++++++++----- 2 files changed, 110 insertions(+), 21 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 3d48fc7c5..99fe914b9 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -297,3 +297,9 @@ impl vihaco::Reset for Circuit { }; } } + +impl Default for Circuit { + fn default() -> Self { + Self::new(0, 1e-10) + } +} diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 579a7b031..586333a8f 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -14,12 +14,27 @@ use crate::measurement_observer::{MeasurementEffect, MeasurementObserver}; use crate::message::CircuitMessage; use crate::prelude::{Circuit, CircuitInstruction}; +#[derive(Debug, Clone, PartialEq)] +pub struct PPVMDeviceInfo { + pub n_qubits: usize, + pub coefficient_threshold: f64, +} +impl Default for PPVMDeviceInfo { + fn default() -> Self { + Self { + n_qubits: 0, + coefficient_threshold: 1e-10, + } + } +} + pub type Instruction = PPVMInstruction; #[composite] +#[derive(Default)] pub struct PPVM { #[program] - loader: ProgramLoader, + loader: ProgramLoader, #[device(0x00, resolve_with = resolve_cpu, custom_parser)] cpu: CPU, @@ -196,20 +211,17 @@ impl PPVM { } pub fn init(&mut self) -> eyre::Result<()> { - let n_qubits = 10; - let coefficient_threshold = 1e-10; - self.circuit = Circuit::new(n_qubits, coefficient_threshold); + let info = &self.loader.module.extra; + if info.n_qubits == 0 { + return Err(eyre::eyre!("device circuit.n_qubits must be declared")); + } + self.circuit = Circuit::new(info.n_qubits, info.coefficient_threshold); Ok(()) } pub fn load( &mut self, - module: &vihaco::module::Module< - Instruction, - vihaco::Value, - vihaco::Type, - vihaco::module::NoInfo, - >, + module: &vihaco::module::Module, ) -> eyre::Result<()> { self.loader.module = module.clone(); Ok(()) @@ -352,7 +364,9 @@ mod tests { #[test] fn test_run_ppvm() -> eyre::Result<()> { - let mut module: Module = Module::default(); + let mut module: Module = Module::default(); + + module.extra.n_qubits = 2; /* const.u64 0 @@ -386,17 +400,9 @@ mod tests { .code .push(PPVMInstruction::Circuit(CircuitInstruction::CNOT)); - let mut machine = PPVM { - loader: ProgramLoader::default(), - cpu: CPU::default(), - circuit: Circuit::new(2, 1e-10), - stdout: StdoutObserver::default(), - measurement_record: MeasurementObserver { record: Vec::new() }, - }; - - println!("{:?}", module.code); - + let mut machine = PPVM::default(); machine.load(&module)?; + machine.init()?; for _ in 0..module.code.len() { machine.step_once()?; @@ -433,4 +439,81 @@ mod tests { assert_eq!(num_coefficients, 2); Ok(()) } + + #[test] + fn test_device_decl() -> eyre::Result<()> { + // Equivalent .sst source: + // + // device circuit.n_qubits 5; + // device circuit.coefficient_threshold 1e-10; + // + // fn @main() { ...5-qubit GHZ + 5 measurements... } + + let mut module: Module = Module::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 + gate h + */ + module + .code + .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + Value::U64(0), + ))); + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::H)); + + for i in 0..4u64 { + /* + const.u64 i + const.u64 i+1 + gate cnot + */ + module + .code + .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + Value::U64(i), + ))); + module + .code + .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + Value::U64(i + 1), + ))); + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::CNOT)); + } + + // Measure all 5 qubits. + for q in 0..5u64 { + /* + const.u64 q + gate measure + */ + module + .code + .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + Value::U64(q), + ))); + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::Measure)); + } + + let mut machine = PPVM::default(); + machine.load(&module)?; + machine.init()?; + + for _ in 0..module.code.len() { + machine.step_once()?; + } + + assert_eq!(machine.measurement_record().len(), 5); + Ok(()) + } } From 3d3fb8231eba7d6c4e0630ef3cb3e246775b1636 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Thu, 21 May 2026 13:55:48 +0200 Subject: [PATCH 17/95] Implement parsing for PPVM --- crates/ppvm-vihaco/src/composite.rs | 49 +++-- crates/ppvm-vihaco/src/lib.rs | 22 +- crates/ppvm-vihaco/src/syntax.rs | 230 +++++++++++++++++++++ crates/ppvm-vihaco/tests/bell.sst | 4 + crates/ppvm-vihaco/tests/hello_circuit.sst | 4 + 5 files changed, 293 insertions(+), 16 deletions(-) create mode 100644 crates/ppvm-vihaco/src/syntax.rs diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 586333a8f..895650d1f 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -1,18 +1,16 @@ -use vihaco::Effects; -use vihaco::Observe; -use vihaco::ProgramLoader; -use vihaco::composite; -use vihaco::observe; -use vihaco::observer::stdio::StdoutEffect; -use vihaco::observer::stdio::StdoutObserver; +use chumsky::Parser; +use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; +use vihaco::syntax::{ParsedModule, Resolve}; use vihaco::traits::{GetProgramGlobal, ProgramCounter, StackMemory}; +use vihaco::{Effects, Observe, ProgramLoader, composite, observe}; use vihaco_cpu::{CPU, CPUMessage, StepOutcome}; +use vihaco_parser_core::Parse; -use crate::component::CircuitEffect; -use crate::measurement_observer::MeasurementResult; -use crate::measurement_observer::{MeasurementEffect, MeasurementObserver}; +use crate::component::{Circuit, CircuitEffect}; +use crate::instruction::CircuitInstruction; +use crate::measurement_observer::{MeasurementEffect, MeasurementObserver, MeasurementResult}; use crate::message::CircuitMessage; -use crate::prelude::{Circuit, CircuitInstruction}; +use crate::syntax::{PPVMHeader, PPVMResolver}; #[derive(Debug, Clone, PartialEq)] pub struct PPVMDeviceInfo { @@ -36,7 +34,7 @@ pub struct PPVM { #[program] loader: ProgramLoader, - #[device(0x00, resolve_with = resolve_cpu, custom_parser)] + #[device(0x00, resolve_with = resolve_cpu)] cpu: CPU, #[device(0x01, resolve_with = resolve_circuit)] @@ -346,6 +344,33 @@ impl PPVM { pub fn measurement_record(&self) -> Vec { self.measurement_record.record.clone() } + + pub fn load_program(&mut self, program: &str) -> eyre::Result<()> { + let parsed = ParsedModule::::parser() + .parse(program) + .into_result() + .map_err(|errs| eyre::eyre!("parsing failed: {errs:?}"))?; + let module = PPVMResolver::new().resolve_module(parsed)?; + self.load(&module)?; + Ok(()) + } + + pub fn load_file(&mut self, path: &str) -> eyre::Result<()> { + let raw_program = std::fs::read_to_string(path)?; + self.load_program(&raw_program) + } + + pub fn run_program(&mut self, program: &str) -> eyre::Result<()> { + self.load_program(program)?; + self.run()?; + Ok(()) + } + + pub fn run_file(&mut self, path: &str) -> eyre::Result<()> { + self.load_file(path)?; + self.run()?; + Ok(()) + } } impl vihaco::Reset for PPVM { diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 79872591e..0e2e61668 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -1,11 +1,25 @@ +use crate::composite::PPVM; + pub mod component; pub mod composite; pub mod instruction; -pub mod measurement_observer; +mod measurement_observer; pub mod message; +mod syntax; + +pub fn run_file(path: &str) -> eyre::Result { + let mut machine = PPVM::default(); + machine.run_file(path)?; + Ok(machine) +} + +pub fn run_program(program: &str) -> eyre::Result { + let mut machine = PPVM::default(); + machine.run_program(program)?; + Ok(machine) +} pub mod prelude { - pub use crate::component::{Circuit, CircuitEffect}; - pub use crate::instruction::CircuitInstruction; - pub use crate::message::CircuitMessage; + pub use crate::component::Circuit; + pub use crate::composite::PPVM; } diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs new file mode 100644 index 000000000..524ce7ff7 --- /dev/null +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -0,0 +1,230 @@ +use std::collections::HashMap; + +use chumsky::{Parser, error::Simple, extra}; +use vihaco::{ + Type, Value, + module::Module, + syntax::{BodyItem, RawForm, RawOperand, Resolve}, +}; +use vihaco_parser_core::Parse; + +use crate::{ + composite::{PPVMDeviceInfo, PPVMInstruction}, + instruction::CircuitInstruction, +}; + +#[derive(Debug, Clone, PartialEq, vihaco_parser::Parse)] +#[head = "device "] +pub enum PPVMHeader { + #[token = "circuit.n_qubits"] + NumQubits(usize), + + #[token = "circuit.coefficient_threshold"] + CoefficientThrehsold(f64), +} + +#[derive(Debug, Default)] +pub struct PPVMResolver { + strings: Vec, +} + +impl PPVMResolver { + pub fn new() -> Self { + Self::default() + } + + // fn intern(&mut self, s: &str) -> u32 { + // if let Some(idx) = self.strings.iter().position(|existing| existing == s) { + // return idx as u32; + // } + // let idx = self.strings.len() as u32; + // self.strings.push(s.to_string()); + // idx + // } + + fn apply_header(info: &mut PPVMDeviceInfo, header: PPVMHeader) -> eyre::Result<()> { + match header { + PPVMHeader::NumQubits(n) => { + info.n_qubits = n; + } + PPVMHeader::CoefficientThrehsold(t) => { + info.coefficient_threshold = t; + } + } + Ok(()) + } + + fn lower_raw(&mut self, raw: RawForm) -> eyre::Result> { + match raw.mnemonic.as_str() { + "ret" => { + require_no_operands(&raw)?; + Ok(vec![vihaco_cpu::Instruction::Return(0).into()]) + } + other => Err(eyre::eyre!( + "PPVMResolver: unhandled raw form `{other}` (operands: {:?})", + raw.operands + )), + } + } +} + +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 mut code: Vec = Vec::new(); + let mut labels: HashMap = HashMap::new(); + let mut patches: Vec<(usize, BranchPatch)> = Vec::new(); + for function in parsed.functions { + 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()); + patches.push((idx, patch)); + continue; + } + code.extend(self.lower_raw(raw)?); + } + } + } + } + for (idx, patch) in patches { + patch.apply(&mut code, idx, &labels)?; + } + + let mut module = Module::default(); + module.code = code; + module.strings = std::mem::take(&mut self.strings); + module.extra = info; + Ok(module) + } +} + +type E<'src> = extra::Err>; + +impl<'src> Parse<'src> for PPVMInstruction { + fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> { + use chumsky::prelude::*; + + let cpu = ::parser().map(PPVMInstruction::Cpu); + + // Reuse the derived parser for all CircuitInstruction variants; + // just gate it behind the `gate ` keyword. + let circuit = just("gate") + .then(text::whitespace().at_least(1)) + .ignore_then(::parser()) + .map(PPVMInstruction::Circuit); + + // Try `gate ...` first so CPU doesn't see "gate" as an identifier. + choice((circuit, cpu)) + } +} + +// ---- Everything below is 1:1 copy from Acamar with Acamar -> PPVM renaming ---- + +fn require_no_operands(raw: &RawForm) -> eyre::Result<()> { + if !raw.operands.is_empty() { + return Err(eyre::eyre!( + "`{}` takes no operands, got {}", + raw.mnemonic, + raw.operands.len() + )); + } + Ok(()) +} + +/// 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()) +} + +/// `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::>>()?; + + 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())) + } + _ => 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() + } + } + } + + 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() + } + }; + code[idx] = resolved; + Ok(()) + } +} diff --git a/crates/ppvm-vihaco/tests/bell.sst b/crates/ppvm-vihaco/tests/bell.sst index 658659419..d71736c67 100644 --- a/crates/ppvm-vihaco/tests/bell.sst +++ b/crates/ppvm-vihaco/tests/bell.sst @@ -1,3 +1,5 @@ +device circuit.n_qubits 2 + fn @main() { const.u64 0 gate h @@ -11,4 +13,6 @@ fn @main() { const.u64 0 gate measure + + ret } \ No newline at end of file diff --git a/crates/ppvm-vihaco/tests/hello_circuit.sst b/crates/ppvm-vihaco/tests/hello_circuit.sst index 686a59d31..bfb796c5c 100644 --- a/crates/ppvm-vihaco/tests/hello_circuit.sst +++ b/crates/ppvm-vihaco/tests/hello_circuit.sst @@ -1,3 +1,5 @@ +device circuit.n_qubits 2; + fn @main() { const.u64 0 gate h @@ -9,4 +11,6 @@ fn @main() { const.u64 0 const.f64 0.1 gate rx + + ret } From 71d50d3c877cc240e54c77db2930be696e9dc0f7 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Thu, 21 May 2026 14:29:18 +0200 Subject: [PATCH 18/95] Add tests and fix some bugs --- crates/ppvm-vihaco/src/composite.rs | 78 ++++++ crates/ppvm-vihaco/src/syntax.rs | 295 +++++++++++++++++++++++ crates/ppvm-vihaco/tests/bell.sst | 2 +- crates/ppvm-vihaco/tests/sst_fixtures.rs | 30 +++ 4 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 crates/ppvm-vihaco/tests/sst_fixtures.rs diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 895650d1f..96a4fe780 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -1,4 +1,6 @@ use chumsky::Parser; +use vihaco::frame::Frame; +use vihaco::machine::StackFrame; use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; use vihaco::syntax::{ParsedModule, Resolve}; use vihaco::traits::{GetProgramGlobal, ProgramCounter, StackMemory}; @@ -214,6 +216,14 @@ impl PPVM { return Err(eyre::eyre!("device circuit.n_qubits must be declared")); } self.circuit = Circuit::new(info.n_qubits, info.coefficient_threshold); + + // push entry frame + self.cpu.push_frame(Frame { + base: 0, + span: (0, 0, 0), + function: None, + }); + Ok(()) } @@ -541,4 +551,72 @@ mod tests { assert_eq!(machine.measurement_record().len(), 5); Ok(()) } + + // ─── Parser-driven entry points ─────────────────────────────────────── + + #[test] + fn load_program_populates_device_info_and_code() -> eyre::Result<()> { + let source = "device circuit.n_qubits 2;\n\ + device circuit.coefficient_threshold 1e-8;\n\ + fn @main() {\n\ + const.u64 0\n\ + gate h\n\ + ret\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 / gate h / ret = 3 + assert_eq!(machine.loader.module.code.len(), 3); + Ok(()) + } + + #[test] + fn run_program_executes_bell_circuit() -> eyre::Result<()> { + let source = "device circuit.n_qubits 2;\n\ + fn @main() {\n\ + const.u64 0\n\ + gate h\n\ + const.u64 0\n\ + const.u64 1\n\ + gate cnot\n\ + const.u64 0\n\ + gate measure\n\ + const.u64 1\n\ + gate measure\n\ + ret\n\ + }\n"; + let mut machine = PPVM::default(); + machine.run_program(source)?; + let record = machine.measurement_record(); + assert_eq!(record.len(), 2); + Ok(()) + } + + #[test] + fn init_fails_when_n_qubits_undeclared() -> eyre::Result<()> { + let source = "fn @main() { ret }\n"; + let mut machine = PPVM::default(); + machine.load_program(source)?; + let err = machine.init().unwrap_err(); + assert!(err.to_string().contains("circuit.n_qubits"), "err: {err}"); + Ok(()) + } + + #[test] + fn run_program_reports_parse_errors() { + let source = "device circuit.n_qubits 2;\n\ + fn @main() {\n\ + gate not_a_real_gate\n\ + ret\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("unhandled raw form"), + "err: {err}" + ); + } } diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index 524ce7ff7..3a83ec6a0 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -17,9 +17,11 @@ use crate::{ #[head = "device "] pub enum PPVMHeader { #[token = "circuit.n_qubits"] + #[delimiters(open = "", close = "", separator = "")] NumQubits(usize), #[token = "circuit.coefficient_threshold"] + #[delimiters(open = "", close = "", separator = "")] CoefficientThrehsold(f64), } @@ -228,3 +230,296 @@ impl BranchPatch { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use chumsky::Parser as _; + 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, + } + } + + // ─── 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::CoefficientThrehsold(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 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::CoefficientThrehsold(5e-6)).unwrap(); + assert_eq!(info.coefficient_threshold, 5e-6); + } + + // ─── 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("gate h") + .into_result() + .unwrap(); + assert!(matches!( + got, + PPVMInstruction::Circuit(CircuitInstruction::H) + )); + } + + #[test] + fn ppvm_instruction_parses_gate_cnot() { + let got = ::parser() + .parse("gate cnot") + .into_result() + .unwrap(); + assert!(matches!( + got, + PPVMInstruction::Circuit(CircuitInstruction::CNOT) + )); + } + + #[test] + fn ppvm_instruction_parses_gate_measure() { + let got = ::parser() + .parse("gate measure") + .into_result() + .unwrap(); + assert!(matches!( + got, + PPVMInstruction::Circuit(CircuitInstruction::Measure) + )); + } + + #[test] + fn ppvm_instruction_parses_gate_rx() { + let got = ::parser() + .parse("gate rx") + .into_result() + .unwrap(); + assert!(matches!( + got, + PPVMInstruction::Circuit(CircuitInstruction::RX) + )); + } + + #[test] + fn ppvm_instruction_rejects_bare_circuit_token_without_gate_prefix() { + // `h` on its own must not parse as Circuit(H) — only `gate h` does. + // Without `gate `, 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_operand_errors() { + let mut r = PPVMResolver::new(); + let err = r + .lower_raw(raw("ret", vec![RawOperand::UInt(1)])) + .unwrap_err(); + assert!(err.to_string().contains("takes no operands"), "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_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\ + gate h\n\ + const.u64 0\n\ + const.u64 1\n\ + gate cnot\n\ + ret\n\ + }\n", + ); + let m = PPVMResolver::new().resolve_module(parsed).unwrap(); + // const.u64 0 / gate h / const.u64 0 / const.u64 1 / gate 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}" + ); + } +} diff --git a/crates/ppvm-vihaco/tests/bell.sst b/crates/ppvm-vihaco/tests/bell.sst index d71736c67..f20c23280 100644 --- a/crates/ppvm-vihaco/tests/bell.sst +++ b/crates/ppvm-vihaco/tests/bell.sst @@ -1,4 +1,4 @@ -device circuit.n_qubits 2 +device circuit.n_qubits 2; fn @main() { const.u64 0 diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs new file mode 100644 index 000000000..2196be082 --- /dev/null +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -0,0 +1,30 @@ +//! End-to-end fixture coverage: parse + resolve + run each `.sst` file in +//! this directory via the public `PPVM` API. + +use ppvm_vihaco::composite::PPVM; + +#[test] +fn bell_sst_runs_and_records_two_measurements() { + let mut machine = PPVM::default(); + machine + .run_file("tests/bell.sst") + .unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); + assert_eq!(machine.measurement_record().len(), 2); +} + +#[test] +fn hello_circuit_sst_parses_and_runs() { + let mut machine = PPVM::default(); + machine + .run_file("tests/hello_circuit.sst") + .unwrap_or_else(|e| panic!("run hello_circuit.sst: {e:?}")); + // hello_circuit.sst applies H + CNOT + RX(0.1); no measurements. + assert_eq!(machine.measurement_record().len(), 0); +} + +#[test] +fn run_file_via_library_helper() { + let machine = ppvm_vihaco::run_file("tests/bell.sst") + .unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); + assert_eq!(machine.measurement_record().len(), 2); +} From b16ffa907e9d54b163ed4a776f7d02adb0cae77a Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Thu, 21 May 2026 15:28:50 +0200 Subject: [PATCH 19/95] Unused import --- crates/ppvm-vihaco/src/syntax.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index 3a83ec6a0..5e0d31964 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -234,7 +234,6 @@ impl BranchPatch { #[cfg(test)] mod tests { use super::*; - use chumsky::Parser as _; use vihaco::syntax::ParsedModule; fn parse_module(source: &str) -> ParsedModule { From 3ebf0db0e44b05343ff161ea12ff59394dcb4019 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Thu, 21 May 2026 16:54:37 +0200 Subject: [PATCH 20/95] Push measurement and is_lost to stack; test control flow --- crates/ppvm-vihaco/src/composite.rs | 14 ++++--- .../ppvm-vihaco/tests/branch_on_outcome.sst | 32 ++++++++++++++ .../ppvm-vihaco/tests/branch_on_outcome_x.sst | 33 +++++++++++++++ crates/ppvm-vihaco/tests/sst_fixtures.rs | 42 +++++++++++++++++++ 4 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 crates/ppvm-vihaco/tests/branch_on_outcome.sst create mode 100644 crates/ppvm-vihaco/tests/branch_on_outcome_x.sst diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 96a4fe780..5ba6dd032 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -315,11 +315,15 @@ impl PPVM { } PPVMEffect::Measurement(effect) => { let follow_ups = Observe::::observe(self, &effect)?; - // TODO: do I need to push those values to stack? - // If so, what should we do with None (= lost qubit)? - // for outcome in effect.measurement_results { - // self.cpu.stack_push(outcome) - // } + // NOTE: push measurements to stack; two booleans: outcome, is_lost + for outcome in effect.measurement_results { + let (is_lost, m) = match outcome { + Some(m) => (false, m), + None => (true, false), + }; + self.cpu.stack_push(m); + self.cpu.stack_push(is_lost); + } self.continue_observer_effects(follow_ups) } PPVMEffect::Step(_) => Err(eyre::eyre!( diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome.sst b/crates/ppvm-vihaco/tests/branch_on_outcome.sst new file mode 100644 index 000000000..4495d5de5 --- /dev/null +++ b/crates/ppvm-vihaco/tests/branch_on_outcome.sst @@ -0,0 +1,32 @@ +device circuit.n_qubits 2; + +fn @main() { + // Scratch slot at local 0 for dropping the is_lost flag after measure. + const.bool false + + const.u64 0 + gate h + + const.u64 0 + gate measure + + // Stack here: [scratch=false, outcome, is_lost]. `store.bool 0` pops is_lost + // and writes it into the scratch slot, leaving [is_lost, outcome] with the + // outcome on top — exactly what `cond_br` needs. + store.bool 0 + + cond_br @one, @zero + + @one: + const.u64 1 + gate x + br @measure_q1 + + @zero: + br @measure_q1 + + @measure_q1: + const.u64 1 + gate measure + ret +} diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst new file mode 100644 index 000000000..7720fc9e7 --- /dev/null +++ b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst @@ -0,0 +1,33 @@ +device circuit.n_qubits 2; + +fn @main() { + // Scratch slot at local 0 for dropping the is_lost flag after measure. + const.bool false + + // X on q0 -> |1>, measure -> outcome is deterministically 1. + const.u64 0 + gate x + + const.u64 0 + gate measure + + // Stack here: [scratch=false, outcome, is_lost]. `store.bool 0` pops + // is_lost and writes it into the scratch slot, leaving [is_lost, outcome] + // with the outcome on top — exactly what `cond_br` needs. + store.bool 0 + + cond_br @one, @zero + + @one: + const.u64 1 + gate x + br @measure_q1 + + @zero: + br @measure_q1 + + @measure_q1: + const.u64 1 + gate measure + ret +} diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 2196be082..c43b4a841 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -28,3 +28,45 @@ fn run_file_via_library_helper() { .unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); assert_eq!(machine.measurement_record().len(), 2); } + +#[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, + // which flips q1 before measuring it, yielding m1 = 1 as well. + let machine = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") + .unwrap_or_else(|e| panic!("run branch_on_outcome_x.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 2, "expected exactly two measurements"); + assert_eq!(record[0], vec![Some(true)], "X-prepared q0 must measure 1"); + assert_eq!(record[1], vec![Some(true)], "branch must have flipped q1"); +} + +#[test] +fn branch_on_outcome_statistics_balanced_and_invariant_holds() { + // `branch_on_outcome.sst` puts q0 in |+>, so its measurement is a fair + // coin. The branch then flips q1 iff the outcome was 1, making m1 == m0 + // an invariant on every shot. Run many shots and check both properties. + const SHOTS: usize = 400; + let mut ones = 0usize; + for _ in 0..SHOTS { + let machine = ppvm_vihaco::run_file("tests/branch_on_outcome.sst") + .unwrap_or_else(|e| panic!("run branch_on_outcome.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 2); + let m0 = record[0][0].expect("q0 should not be lost"); + let m1 = record[1][0].expect("q1 should not be lost"); + assert_eq!(m0, m1, "branch must steer q1 to match q0 on every shot"); + if m0 { + ones += 1; + } + } + // Fair coin with SHOTS=400: mean=200, stddev=10. ±6σ window catches a + // truly broken RNG without flaking on a healthy one. + let lo = SHOTS / 2 - 60; + let hi = SHOTS / 2 + 60; + assert!( + (lo..=hi).contains(&ones), + "expected {lo}..={hi} ones out of {SHOTS}, got {ones}" + ); +} From 5734844b0ddc4682177a546a6ca71b50099ef49e Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Fri, 22 May 2026 09:59:18 +0200 Subject: [PATCH 21/95] Implement function call support --- .gitignore | 1 + crates/ppvm-vihaco/src/syntax.rs | 115 +++++++++++++++--- crates/ppvm-vihaco/tests/function_call.sst | 18 +++ .../ppvm-vihaco/tests/function_call_ret.sst | 46 +++++++ crates/ppvm-vihaco/tests/sst_fixtures.rs | 28 ++++- 5 files changed, 187 insertions(+), 21 deletions(-) create mode 100644 crates/ppvm-vihaco/tests/function_call.sst create mode 100644 crates/ppvm-vihaco/tests/function_call_ret.sst diff --git a/.gitignore b/.gitignore index fcffeb6d9..a60cf6a28 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .vscode/ *__pycache__* profile.json.gz +debug/ # mkdocs build output and cache ppvm-python/site/ diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index 5e0d31964..c243fc049 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -59,8 +59,17 @@ impl PPVMResolver { fn lower_raw(&mut self, raw: RawForm) -> eyre::Result> { match raw.mnemonic.as_str() { "ret" => { - require_no_operands(&raw)?; - Ok(vec![vihaco_cpu::Instruction::Return(0).into()]) + 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()]) } other => Err(eyre::eyre!( "PPVMResolver: unhandled raw form `{other}` (operands: {:?})", @@ -83,8 +92,15 @@ impl Resolve for PPVMResolver { let mut code: Vec = Vec::new(); let mut labels: HashMap = HashMap::new(); - let mut patches: Vec<(usize, BranchPatch)> = Vec::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), @@ -98,7 +114,13 @@ impl Resolve for PPVMResolver { if let Some(patch) = raw_as_branch(&raw) { let idx = code.len(); code.push(patch.placeholder()); - patches.push((idx, patch)); + 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)?); @@ -106,7 +128,10 @@ impl Resolve for PPVMResolver { } } } - for (idx, patch) in patches { + for (idx, patch) in branch_patches { + patch.apply(&mut code, idx, &labels)?; + } + for (idx, patch) in call_patches { patch.apply(&mut code, idx, &labels)?; } @@ -140,17 +165,6 @@ impl<'src> Parse<'src> for PPVMInstruction { // ---- Everything below is 1:1 copy from Acamar with Acamar -> PPVM renaming ---- -fn require_no_operands(raw: &RawForm) -> eyre::Result<()> { - if !raw.operands.is_empty() { - return Err(eyre::eyre!( - "`{}` takes no operands, got {}", - raw.mnemonic, - raw.operands.len() - )); - } - Ok(()) -} - /// 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. @@ -231,6 +245,55 @@ impl BranchPatch { } } +/// `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(), + })) + } + 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(()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -396,12 +459,26 @@ mod tests { } #[test] - fn lower_raw_ret_with_operand_errors() { + 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::UInt(1)])) + .lower_raw(raw("ret", vec![RawOperand::Symbol("foo".into())])) .unwrap_err(); - assert!(err.to_string().contains("takes no operands"), "err: {err}"); + assert!( + err.to_string().contains("`ret` takes 0 or 1 unsigned int"), + "err: {err}" + ); } #[test] diff --git a/crates/ppvm-vihaco/tests/function_call.sst b/crates/ppvm-vihaco/tests/function_call.sst new file mode 100644 index 000000000..9229f46dc --- /dev/null +++ b/crates/ppvm-vihaco/tests/function_call.sst @@ -0,0 +1,18 @@ +device circuit.n_qubits 2; + +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 +} + +fn @run_circuit() { + const.u64 1 + gate h + + const.u64 1 + gate measure + + halt +} diff --git a/crates/ppvm-vihaco/tests/function_call_ret.sst b/crates/ppvm-vihaco/tests/function_call_ret.sst new file mode 100644 index 000000000..71cd1eb06 --- /dev/null +++ b/crates/ppvm-vihaco/tests/function_call_ret.sst @@ -0,0 +1,46 @@ +device circuit.n_qubits 2; + +// TODO: aspirational — depends on `ret ` restoring the caller's PC and +// leaving the top `n` values on the caller's stack. The runtime doesn't do +// this today (Frame has no return_pc), so this fixture currently fails. + +fn @main() { + // Put q0 into |+>. + const.u64 0 + gate h + + // Measure q1 via a helper that returns the outcome on top of the stack. + call 0, @measure_q1 + + // Stack: [outcome]. cond_br pops it and branches on its value. + cond_br @one, @zero + + @one: + // outcome was 1: apply X to q0 as a correction. + const.u64 0 + gate x + br @done + + @zero: + br @done + + @done: + ret +} + +fn @measure_q1() -> bool { + // Scratch slot at local 0 so we can `store.bool 0` away the is_lost flag + // after measure, leaving outcome on top — same trick as branch_on_outcome. + const.bool false + + const.u64 1 + gate h + + const.u64 1 + gate measure + // Frame: [scratch=false, outcome, is_lost]. + store.bool 0 + // Frame: [is_lost, outcome] with outcome on top. + + ret 1 +} diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index c43b4a841..05f6ab820 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -24,11 +24,35 @@ fn hello_circuit_sst_parses_and_runs() { #[test] fn run_file_via_library_helper() { - let machine = ppvm_vihaco::run_file("tests/bell.sst") - .unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); + let machine = + ppvm_vihaco::run_file("tests/bell.sst").unwrap_or_else(|e| panic!("run bell.sst: {e:?}")); assert_eq!(machine.measurement_record().len(), 2); } +#[test] +fn function_call_jumps_into_callee_body() { + // `function_call.sst` has main `call` into `@run_circuit`, which puts q1 + // in |+>, measures it, and `halt`s. Verifies CallPatch resolves the + // symbolic target and op_call actually transfers control there. + let machine = ppvm_vihaco::run_file("tests/function_call.sst") + .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1, "expected exactly one measurement"); + assert_eq!(record[0].len(), 1); + assert!(record[0][0].is_some(), "measurement should not be lost"); +} + +#[test] +#[ignore] +fn function_call_returns() { + let machine = ppvm_vihaco::run_file("tests/function_call_ret.sst") + .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1, "expected exactly one measurement"); + assert_eq!(record[0].len(), 1); + assert!(record[0][0].is_some(), "measurement should not be lost"); +} + #[test] fn branch_on_outcome_deterministic_x_path() { // `branch_on_outcome_x.sst` applies X to q0 instead of H, so the outcome From b88859f55709e0c47af40b240089224fe9a49dbd Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Fri, 22 May 2026 10:33:48 +0200 Subject: [PATCH 22/95] Add magic number to header --- crates/ppvm-vihaco/src/composite.rs | 31 +++++++++++++++++-- crates/ppvm-vihaco/src/syntax.rs | 13 ++++++-- crates/ppvm-vihaco/tests/bell.sst | 1 + .../ppvm-vihaco/tests/branch_on_outcome.sst | 1 + .../ppvm-vihaco/tests/branch_on_outcome_x.sst | 1 + crates/ppvm-vihaco/tests/function_call.sst | 1 + .../ppvm-vihaco/tests/function_call_ret.sst | 1 + crates/ppvm-vihaco/tests/hello_circuit.sst | 1 + 8 files changed, 44 insertions(+), 6 deletions(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 5ba6dd032..381b91de2 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -12,16 +12,19 @@ use crate::component::{Circuit, CircuitEffect}; use crate::instruction::CircuitInstruction; use crate::measurement_observer::{MeasurementEffect, MeasurementObserver, MeasurementResult}; use crate::message::CircuitMessage; -use crate::syntax::{PPVMHeader, PPVMResolver}; +use crate::syntax::{PPVM_MAGIC, PPVMHeader, PPVMResolver}; #[derive(Debug, Clone, PartialEq)] pub struct PPVMDeviceInfo { + pub magic: u32, pub n_qubits: usize, pub coefficient_threshold: f64, } + impl Default for PPVMDeviceInfo { fn default() -> Self { Self { + magic: 0, n_qubits: 0, coefficient_threshold: 1e-10, } @@ -212,6 +215,16 @@ impl PPVM { pub fn init(&mut self) -> eyre::Result<()> { let info = &self.loader.module.extra; + if info.magic != PPVM_MAGIC { + if info.magic == 0 { + return Err(eyre::eyre!("missing 'magic ppvm;' header (0x5050564D)")); + } else { + return Err(eyre::eyre!( + "Expected magic header 'ppvm' (0x5050564D), got {}", + info.magic + )); + } + } if info.n_qubits == 0 { return Err(eyre::eyre!("device circuit.n_qubits must be declared")); } @@ -405,6 +418,7 @@ mod tests { fn test_run_ppvm() -> eyre::Result<()> { let mut module: Module = Module::default(); + module.extra.magic = PPVM_MAGIC; module.extra.n_qubits = 2; /* @@ -490,6 +504,7 @@ mod tests { let mut module: Module = Module::default(); + module.extra.magic = PPVM_MAGIC; module.extra.n_qubits = 5; module.extra.coefficient_threshold = 1e-10; @@ -578,7 +593,7 @@ mod tests { #[test] fn run_program_executes_bell_circuit() -> eyre::Result<()> { - let source = "device circuit.n_qubits 2;\n\ + let source = "magic ppvm;\ndevice circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ gate h\n\ @@ -600,7 +615,7 @@ mod tests { #[test] fn init_fails_when_n_qubits_undeclared() -> eyre::Result<()> { - let source = "fn @main() { ret }\n"; + let source = "magic ppvm;\nfn @main() { ret }\n"; let mut machine = PPVM::default(); machine.load_program(source)?; let err = machine.init().unwrap_err(); @@ -608,6 +623,16 @@ mod tests { Ok(()) } + #[test] + fn init_fails_when_magic_undeclared() -> eyre::Result<()> { + let source = "fn @main() { ret }\n"; + let mut machine = PPVM::default(); + machine.load_program(source)?; + let err = machine.init().unwrap_err(); + assert!(err.to_string().contains("magic"), "err: {err}"); + Ok(()) + } + #[test] fn run_program_reports_parse_errors() { let source = "device circuit.n_qubits 2;\n\ diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index c243fc049..be4faeee9 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -13,14 +13,18 @@ use crate::{ instruction::CircuitInstruction, }; +pub const PPVM_MAGIC: u32 = 0x5050564D; + #[derive(Debug, Clone, PartialEq, vihaco_parser::Parse)] -#[head = "device "] pub enum PPVMHeader { - #[token = "circuit.n_qubits"] + #[token = "magic ppvm"] + Magic, + + #[token = "device circuit.n_qubits"] #[delimiters(open = "", close = "", separator = "")] NumQubits(usize), - #[token = "circuit.coefficient_threshold"] + #[token = "device circuit.coefficient_threshold"] #[delimiters(open = "", close = "", separator = "")] CoefficientThrehsold(f64), } @@ -46,6 +50,9 @@ impl PPVMResolver { fn apply_header(info: &mut PPVMDeviceInfo, header: PPVMHeader) -> eyre::Result<()> { match header { + PPVMHeader::Magic => { + info.magic = PPVM_MAGIC; + } PPVMHeader::NumQubits(n) => { info.n_qubits = n; } diff --git a/crates/ppvm-vihaco/tests/bell.sst b/crates/ppvm-vihaco/tests/bell.sst index f20c23280..ec50f1bb0 100644 --- a/crates/ppvm-vihaco/tests/bell.sst +++ b/crates/ppvm-vihaco/tests/bell.sst @@ -1,3 +1,4 @@ +magic ppvm; device circuit.n_qubits 2; fn @main() { diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome.sst b/crates/ppvm-vihaco/tests/branch_on_outcome.sst index 4495d5de5..d3b5acf39 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome.sst @@ -1,3 +1,4 @@ +magic ppvm; device circuit.n_qubits 2; fn @main() { diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst index 7720fc9e7..8db68843d 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst @@ -1,3 +1,4 @@ +magic ppvm; device circuit.n_qubits 2; fn @main() { diff --git a/crates/ppvm-vihaco/tests/function_call.sst b/crates/ppvm-vihaco/tests/function_call.sst index 9229f46dc..ce3a74fd5 100644 --- a/crates/ppvm-vihaco/tests/function_call.sst +++ b/crates/ppvm-vihaco/tests/function_call.sst @@ -1,3 +1,4 @@ +magic ppvm; device circuit.n_qubits 2; fn @main() { diff --git a/crates/ppvm-vihaco/tests/function_call_ret.sst b/crates/ppvm-vihaco/tests/function_call_ret.sst index 71cd1eb06..fb8ced1bc 100644 --- a/crates/ppvm-vihaco/tests/function_call_ret.sst +++ b/crates/ppvm-vihaco/tests/function_call_ret.sst @@ -1,3 +1,4 @@ +magic ppvm; device circuit.n_qubits 2; // TODO: aspirational — depends on `ret ` restoring the caller's PC and diff --git a/crates/ppvm-vihaco/tests/hello_circuit.sst b/crates/ppvm-vihaco/tests/hello_circuit.sst index bfb796c5c..e2d870fa6 100644 --- a/crates/ppvm-vihaco/tests/hello_circuit.sst +++ b/crates/ppvm-vihaco/tests/hello_circuit.sst @@ -1,3 +1,4 @@ +magic ppvm; device circuit.n_qubits 2; fn @main() { From 6b236c33a8b79dee6e4544206a308ca1c946da17 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Fri, 22 May 2026 11:57:35 +0200 Subject: [PATCH 23/95] Handle function calls and returns properly This commit depends on upstream fixes in vihaco-cpu and requires stellarscope#59 to be merged --- crates/ppvm-vihaco/src/composite.rs | 2 + crates/ppvm-vihaco/tests/function_call.sst | 3 +- .../tests/function_call_branch_both.sst | 59 +++++++++++++++++++ crates/ppvm-vihaco/tests/sst_fixtures.rs | 40 ++++++++++++- 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 crates/ppvm-vihaco/tests/function_call_branch_both.sst diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 381b91de2..ae8f134de 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -235,6 +235,7 @@ impl PPVM { base: 0, span: (0, 0, 0), function: None, + ret_pc: 0, }); Ok(()) @@ -259,6 +260,7 @@ impl PPVM { match inst { PPVMInstruction::Cpu(cpu_inst) => { let msg = self.resolve_cpu(&cpu_inst)?; + self.cpu.set_current_pc(self.loader.pc()); let stdout_effect = match (&cpu_inst, &msg) { (vihaco_cpu::Instruction::Print, vihaco_cpu::CPUMessage::Print(text)) => { Some(PPVMEffect::Stdout(StdoutEffect(text.clone()))) diff --git a/crates/ppvm-vihaco/tests/function_call.sst b/crates/ppvm-vihaco/tests/function_call.sst index ce3a74fd5..9eb034b38 100644 --- a/crates/ppvm-vihaco/tests/function_call.sst +++ b/crates/ppvm-vihaco/tests/function_call.sst @@ -6,6 +6,7 @@ fn @main() { // 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 } fn @run_circuit() { @@ -15,5 +16,5 @@ fn @run_circuit() { const.u64 1 gate measure - halt + ret 1 } diff --git a/crates/ppvm-vihaco/tests/function_call_branch_both.sst b/crates/ppvm-vihaco/tests/function_call_branch_both.sst new file mode 100644 index 000000000..2d0297785 --- /dev/null +++ b/crates/ppvm-vihaco/tests/function_call_branch_both.sst @@ -0,0 +1,59 @@ +magic ppvm; +device circuit.n_qubits 2; + +// Exercises a helper that returns BOTH measurement outputs (outcome and +// is_lost) via `ret 2`, with main branching on both values in sequence. +// +// Layout after `call 0, @measure_q0_both` returns: stack = [outcome, is_lost] +// with is_lost on top (gate measure pushes outcome, then is_lost). +// +// Loss prob is 0.5, so: +// P(lost) = 0.5 → q1 flipped to |1> → m1 = 1 +// P(kept ∧ outcome = 1) = 0.25 → q1 flipped to |1> → m1 = 1 +// P(kept ∧ outcome = 0) = 0.25 → q1 stays in |0> → m1 = 0 +// → P(m1 = 1) = 0.75. +fn @main() { + const.u64 0 + gate h + + const.u64 0 + const.f64 0.5 + gate loss + + call 0, @measure_q0_both + + // Stack: [outcome, is_lost]. cond_br pops is_lost. + cond_br @lost, @kept + + @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 + gate x + br @final + + @kept: + // is_lost = false. Stack: [outcome]. cond_br pops outcome. + cond_br @outcome_one, @outcome_zero + + @outcome_one: + const.u64 1 + gate x + br @final + + @outcome_zero: + br @final + + @final: + const.u64 1 + gate measure + halt +} + +fn @measure_q0_both() { + const.u64 0 + gate measure + // Stack: [outcome, is_lost] + ret 2 +} diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 05f6ab820..05a7e62fb 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -43,7 +43,6 @@ fn function_call_jumps_into_callee_body() { } #[test] -#[ignore] fn function_call_returns() { let machine = ppvm_vihaco::run_file("tests/function_call_ret.sst") .unwrap_or_else(|e| panic!("run function_call.sst: {e:?}")); @@ -94,3 +93,42 @@ fn branch_on_outcome_statistics_balanced_and_invariant_holds() { "expected {lo}..={hi} ones out of {SHOTS}, got {ones}" ); } + +#[test] +fn function_call_branch_on_both_returned_values() { + // `function_call_branch_both.sst`: helper returns BOTH outcome and + // is_lost via `ret 2`. Main first branches on is_lost, then on outcome, + // steering q1 to |1> on the lost path and on the kept-outcome=1 path, + // leaving q1 in |0> only on the kept-outcome=0 path. With loss prob 0.5 + // and a |+> prep: + // P(m1 = 1) = P(lost) + P(kept) · P(outcome = 1 | kept) + // = 0.5 + 0.5 · 0.5 = 0.75 + // P(m0 = lost) = 0.5 + const SHOTS: usize = 400; + let mut q0_lost = 0usize; + let mut q1_ones = 0usize; + for _ in 0..SHOTS { + let machine = ppvm_vihaco::run_file("tests/function_call_branch_both.sst") + .unwrap_or_else(|e| panic!("run function_call_branch_both.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 2, "expected exactly two measurements"); + assert_eq!(record[0].len(), 1); + assert_eq!(record[1].len(), 1); + if record[0][0].is_none() { + q0_lost += 1; + } + if record[1][0] == Some(true) { + q1_ones += 1; + } + } + // P(lost) = 0.5, SHOTS=400: mean=200, stddev=10. ±6σ window. + assert!( + (140..=260).contains(&q0_lost), + "expected ~200 lost shots, got {q0_lost}" + ); + // P(m1=1) = 0.75, SHOTS=400: mean=300, stddev≈8.66. ±6σ → ~248..352. + assert!( + (240..=360).contains(&q1_ones), + "expected ~300 q1=true shots, got {q1_ones}" + ); +} From 981b1a78fee3e4576b8e41fd6740a1fc5dd08f52 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Fri, 22 May 2026 16:20:23 +0200 Subject: [PATCH 24/95] Remove magic number from text format --- crates/ppvm-vihaco/src/composite.rs | 32 ++++--------------- crates/ppvm-vihaco/src/syntax.rs | 22 ++----------- crates/ppvm-vihaco/tests/bell.sst | 1 - .../ppvm-vihaco/tests/branch_on_outcome.sst | 1 - .../ppvm-vihaco/tests/branch_on_outcome_x.sst | 1 - crates/ppvm-vihaco/tests/function_call.sst | 1 - .../tests/function_call_branch_both.sst | 1 - .../ppvm-vihaco/tests/function_call_ret.sst | 1 - crates/ppvm-vihaco/tests/hello_circuit.sst | 1 - 9 files changed, 9 insertions(+), 52 deletions(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index ae8f134de..b8b8925ee 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -12,7 +12,9 @@ use crate::component::{Circuit, CircuitEffect}; use crate::instruction::CircuitInstruction; use crate::measurement_observer::{MeasurementEffect, MeasurementObserver, MeasurementResult}; use crate::message::CircuitMessage; -use crate::syntax::{PPVM_MAGIC, PPVMHeader, PPVMResolver}; +use crate::syntax::{PPVMHeader, PPVMResolver}; + +pub const PPVM_MAGIC: u32 = 0x5050564D; #[derive(Debug, Clone, PartialEq)] pub struct PPVMDeviceInfo { @@ -24,7 +26,7 @@ pub struct PPVMDeviceInfo { impl Default for PPVMDeviceInfo { fn default() -> Self { Self { - magic: 0, + magic: PPVM_MAGIC, n_qubits: 0, coefficient_threshold: 1e-10, } @@ -215,16 +217,6 @@ impl PPVM { pub fn init(&mut self) -> eyre::Result<()> { let info = &self.loader.module.extra; - if info.magic != PPVM_MAGIC { - if info.magic == 0 { - return Err(eyre::eyre!("missing 'magic ppvm;' header (0x5050564D)")); - } else { - return Err(eyre::eyre!( - "Expected magic header 'ppvm' (0x5050564D), got {}", - info.magic - )); - } - } if info.n_qubits == 0 { return Err(eyre::eyre!("device circuit.n_qubits must be declared")); } @@ -420,7 +412,6 @@ mod tests { fn test_run_ppvm() -> eyre::Result<()> { let mut module: Module = Module::default(); - module.extra.magic = PPVM_MAGIC; module.extra.n_qubits = 2; /* @@ -506,7 +497,6 @@ mod tests { let mut module: Module = Module::default(); - module.extra.magic = PPVM_MAGIC; module.extra.n_qubits = 5; module.extra.coefficient_threshold = 1e-10; @@ -595,7 +585,7 @@ mod tests { #[test] fn run_program_executes_bell_circuit() -> eyre::Result<()> { - let source = "magic ppvm;\ndevice circuit.n_qubits 2;\n\ + let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ gate h\n\ @@ -617,21 +607,11 @@ mod tests { #[test] fn init_fails_when_n_qubits_undeclared() -> eyre::Result<()> { - let source = "magic ppvm;\nfn @main() { ret }\n"; - let mut machine = PPVM::default(); - machine.load_program(source)?; - let err = machine.init().unwrap_err(); - assert!(err.to_string().contains("circuit.n_qubits"), "err: {err}"); - Ok(()) - } - - #[test] - fn init_fails_when_magic_undeclared() -> eyre::Result<()> { let source = "fn @main() { ret }\n"; let mut machine = PPVM::default(); machine.load_program(source)?; let err = machine.init().unwrap_err(); - assert!(err.to_string().contains("magic"), "err: {err}"); + assert!(err.to_string().contains("circuit.n_qubits"), "err: {err}"); Ok(()) } diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index be4faeee9..2b7b392ba 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -13,18 +13,14 @@ use crate::{ instruction::CircuitInstruction, }; -pub const PPVM_MAGIC: u32 = 0x5050564D; - #[derive(Debug, Clone, PartialEq, vihaco_parser::Parse)] +#[head = "device "] pub enum PPVMHeader { - #[token = "magic ppvm"] - Magic, - - #[token = "device circuit.n_qubits"] + #[token = "circuit.n_qubits"] #[delimiters(open = "", close = "", separator = "")] NumQubits(usize), - #[token = "device circuit.coefficient_threshold"] + #[token = "circuit.coefficient_threshold"] #[delimiters(open = "", close = "", separator = "")] CoefficientThrehsold(f64), } @@ -39,20 +35,8 @@ impl PPVMResolver { Self::default() } - // fn intern(&mut self, s: &str) -> u32 { - // if let Some(idx) = self.strings.iter().position(|existing| existing == s) { - // return idx as u32; - // } - // let idx = self.strings.len() as u32; - // self.strings.push(s.to_string()); - // idx - // } - fn apply_header(info: &mut PPVMDeviceInfo, header: PPVMHeader) -> eyre::Result<()> { match header { - PPVMHeader::Magic => { - info.magic = PPVM_MAGIC; - } PPVMHeader::NumQubits(n) => { info.n_qubits = n; } diff --git a/crates/ppvm-vihaco/tests/bell.sst b/crates/ppvm-vihaco/tests/bell.sst index ec50f1bb0..f20c23280 100644 --- a/crates/ppvm-vihaco/tests/bell.sst +++ b/crates/ppvm-vihaco/tests/bell.sst @@ -1,4 +1,3 @@ -magic ppvm; device circuit.n_qubits 2; fn @main() { diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome.sst b/crates/ppvm-vihaco/tests/branch_on_outcome.sst index d3b5acf39..4495d5de5 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome.sst @@ -1,4 +1,3 @@ -magic ppvm; device circuit.n_qubits 2; fn @main() { diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst index 8db68843d..7720fc9e7 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst @@ -1,4 +1,3 @@ -magic ppvm; device circuit.n_qubits 2; fn @main() { diff --git a/crates/ppvm-vihaco/tests/function_call.sst b/crates/ppvm-vihaco/tests/function_call.sst index 9eb034b38..504f86b33 100644 --- a/crates/ppvm-vihaco/tests/function_call.sst +++ b/crates/ppvm-vihaco/tests/function_call.sst @@ -1,4 +1,3 @@ -magic ppvm; device circuit.n_qubits 2; fn @main() { diff --git a/crates/ppvm-vihaco/tests/function_call_branch_both.sst b/crates/ppvm-vihaco/tests/function_call_branch_both.sst index 2d0297785..4cf3dd578 100644 --- a/crates/ppvm-vihaco/tests/function_call_branch_both.sst +++ b/crates/ppvm-vihaco/tests/function_call_branch_both.sst @@ -1,4 +1,3 @@ -magic ppvm; device circuit.n_qubits 2; // Exercises a helper that returns BOTH measurement outputs (outcome and diff --git a/crates/ppvm-vihaco/tests/function_call_ret.sst b/crates/ppvm-vihaco/tests/function_call_ret.sst index fb8ced1bc..71cd1eb06 100644 --- a/crates/ppvm-vihaco/tests/function_call_ret.sst +++ b/crates/ppvm-vihaco/tests/function_call_ret.sst @@ -1,4 +1,3 @@ -magic ppvm; device circuit.n_qubits 2; // TODO: aspirational — depends on `ret ` restoring the caller's PC and diff --git a/crates/ppvm-vihaco/tests/hello_circuit.sst b/crates/ppvm-vihaco/tests/hello_circuit.sst index e2d870fa6..bfb796c5c 100644 --- a/crates/ppvm-vihaco/tests/hello_circuit.sst +++ b/crates/ppvm-vihaco/tests/hello_circuit.sst @@ -1,4 +1,3 @@ -magic ppvm; device circuit.n_qubits 2; fn @main() { From 5ad1f548ed261bccd5c5da29b598578aac593631 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Fri, 22 May 2026 16:52:06 +0200 Subject: [PATCH 25/95] Make MeasurementOutcome an enum and push a single u32 rather than two booleans to stack --- crates/ppvm-vihaco/src/component.rs | 8 ++-- crates/ppvm-vihaco/src/composite.rs | 10 ++-- crates/ppvm-vihaco/src/lib.rs | 2 +- ...easurement_observer.rs => measurements.rs} | 20 +++++++- .../ppvm-vihaco/tests/branch_on_outcome.sst | 11 ++--- .../ppvm-vihaco/tests/branch_on_outcome_x.sst | 11 ++--- .../tests/function_call_branch_both.sst | 25 ++++++---- .../ppvm-vihaco/tests/function_call_ret.sst | 14 ++---- crates/ppvm-vihaco/tests/sst_fixtures.rs | 47 +++++++++++++------ 9 files changed, 88 insertions(+), 60 deletions(-) rename crates/ppvm-vihaco/src/{measurement_observer.rs => measurements.rs} (55%) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 99fe914b9..ec1d0c534 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -1,6 +1,6 @@ -use crate::instruction::CircuitInstruction; -use crate::measurement_observer::MeasurementEffect; +use crate::measurements::MeasurementEffect; use crate::message::CircuitMessage; +use crate::{instruction::CircuitInstruction, measurements::MeasurementOutcome}; use bitvec::view::BitView; use bnum::types::{U256, U512, U1024, U2048}; use eyre::{Result, eyre}; @@ -83,7 +83,7 @@ where // Measure & Reset (Measure, &Qubit(addr)) => { - let outcome = self.tab.measure(addr); + let outcome: MeasurementOutcome = self.tab.measure(addr).into(); return Ok(Effects::one(MeasurementEffect { measurement_results: vec![outcome], })); @@ -178,7 +178,7 @@ where // Batch: measure (emits per qubit) (Measure, QubitBatch(addrs)) => { - let outcomes = addrs.iter().map(|&addr| self.tab.measure(addr)); + let outcomes = addrs.iter().map(|&addr| self.tab.measure(addr).into()); return Ok(Effects::one(MeasurementEffect { measurement_results: outcomes.collect(), })); diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index b8b8925ee..44c27dfa0 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -4,13 +4,13 @@ use vihaco::machine::StackFrame; use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; use vihaco::syntax::{ParsedModule, Resolve}; use vihaco::traits::{GetProgramGlobal, ProgramCounter, StackMemory}; -use vihaco::{Effects, Observe, ProgramLoader, composite, observe}; +use vihaco::{Effects, Observe, ProgramLoader, Value, composite, observe}; use vihaco_cpu::{CPU, CPUMessage, StepOutcome}; use vihaco_parser_core::Parse; use crate::component::{Circuit, CircuitEffect}; use crate::instruction::CircuitInstruction; -use crate::measurement_observer::{MeasurementEffect, MeasurementObserver, MeasurementResult}; +use crate::measurements::{MeasurementEffect, MeasurementObserver, MeasurementResult}; use crate::message::CircuitMessage; use crate::syntax::{PPVMHeader, PPVMResolver}; @@ -324,12 +324,8 @@ impl PPVM { let follow_ups = Observe::::observe(self, &effect)?; // NOTE: push measurements to stack; two booleans: outcome, is_lost for outcome in effect.measurement_results { - let (is_lost, m) = match outcome { - Some(m) => (false, m), - None => (true, false), - }; + let m = Value::U32(outcome as u32); self.cpu.stack_push(m); - self.cpu.stack_push(is_lost); } self.continue_observer_effects(follow_ups) } diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 0e2e61668..3c0af0f7a 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -3,7 +3,7 @@ use crate::composite::PPVM; pub mod component; pub mod composite; pub mod instruction; -mod measurement_observer; +pub mod measurements; pub mod message; mod syntax; diff --git a/crates/ppvm-vihaco/src/measurement_observer.rs b/crates/ppvm-vihaco/src/measurements.rs similarity index 55% rename from crates/ppvm-vihaco/src/measurement_observer.rs rename to crates/ppvm-vihaco/src/measurements.rs index 3a4794da6..05634e8ea 100644 --- a/crates/ppvm-vihaco/src/measurement_observer.rs +++ b/crates/ppvm-vihaco/src/measurements.rs @@ -1,13 +1,31 @@ use eyre::Result; use vihaco::{Effects, observe}; -pub type MeasurementResult = Vec>; +#[repr(u8)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum MeasurementOutcome { + Zero = 0, + One = 1, + Lost = 2, +} + +pub type MeasurementResult = Vec; #[derive(Debug, Clone, Eq, PartialEq)] pub struct MeasurementEffect { pub measurement_results: MeasurementResult, } +impl From> for MeasurementOutcome { + fn from(m: Option) -> Self { + match m { + Some(false) => Self::Zero, + Some(true) => Self::One, + None => Self::Lost, + } + } +} + #[derive(Debug, Default)] pub struct MeasurementObserver { pub record: Vec, diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome.sst b/crates/ppvm-vihaco/tests/branch_on_outcome.sst index 4495d5de5..024f3af93 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome.sst @@ -1,19 +1,16 @@ device circuit.n_qubits 2; fn @main() { - // Scratch slot at local 0 for dropping the is_lost flag after measure. - const.bool false - const.u64 0 gate h const.u64 0 gate measure - // Stack here: [scratch=false, outcome, is_lost]. `store.bool 0` pops is_lost - // and writes it into the scratch slot, leaving [is_lost, outcome] with the - // outcome on top — exactly what `cond_br` needs. - store.bool 0 + // 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 cond_br @one, @zero diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst index 7720fc9e7..82134da65 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst @@ -1,9 +1,6 @@ device circuit.n_qubits 2; fn @main() { - // Scratch slot at local 0 for dropping the is_lost flag after measure. - const.bool false - // X on q0 -> |1>, measure -> outcome is deterministically 1. const.u64 0 gate x @@ -11,10 +8,10 @@ fn @main() { const.u64 0 gate measure - // Stack here: [scratch=false, outcome, is_lost]. `store.bool 0` pops - // is_lost and writes it into the scratch slot, leaving [is_lost, outcome] - // with the outcome on top — exactly what `cond_br` needs. - store.bool 0 + // 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 cond_br @one, @zero diff --git a/crates/ppvm-vihaco/tests/function_call_branch_both.sst b/crates/ppvm-vihaco/tests/function_call_branch_both.sst index 4cf3dd578..921c06fd2 100644 --- a/crates/ppvm-vihaco/tests/function_call_branch_both.sst +++ b/crates/ppvm-vihaco/tests/function_call_branch_both.sst @@ -1,10 +1,10 @@ device circuit.n_qubits 2; -// Exercises a helper that returns BOTH measurement outputs (outcome and -// is_lost) via `ret 2`, with main branching on both values in sequence. +// Exercises a helper that returns the tri-state measurement outcome via +// `ret 1`, with main branching on loss first, then on the 0/1 outcome. // -// Layout after `call 0, @measure_q0_both` returns: stack = [outcome, is_lost] -// with is_lost on top (gate measure pushes outcome, then is_lost). +// Layout after `call 0, @measure_q0` returns: stack = [outcome] where +// outcome ∈ {0, 1, 2} (2 = Lost). // // Loss prob is 0.5, so: // P(lost) = 0.5 → q1 flipped to |1> → m1 = 1 @@ -19,9 +19,12 @@ fn @main() { const.f64 0.5 gate loss - call 0, @measure_q0_both + call 0, @measure_q0 - // Stack: [outcome, is_lost]. cond_br pops is_lost. + // Stack: [outcome]. Branch on Lost (outcome == 2) first. + dup + const.u32 2 + eq.u32 cond_br @lost, @kept @lost: @@ -33,7 +36,9 @@ fn @main() { br @final @kept: - // is_lost = false. Stack: [outcome]. cond_br pops outcome. + // 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: @@ -50,9 +55,9 @@ fn @main() { halt } -fn @measure_q0_both() { +fn @measure_q0() { const.u64 0 gate measure - // Stack: [outcome, is_lost] - ret 2 + // Stack: [outcome] + ret 1 } diff --git a/crates/ppvm-vihaco/tests/function_call_ret.sst b/crates/ppvm-vihaco/tests/function_call_ret.sst index 71cd1eb06..d0b859cfb 100644 --- a/crates/ppvm-vihaco/tests/function_call_ret.sst +++ b/crates/ppvm-vihaco/tests/function_call_ret.sst @@ -12,7 +12,9 @@ fn @main() { // Measure q1 via a helper that returns the outcome on top of the stack. call 0, @measure_q1 - // Stack: [outcome]. cond_br pops it and branches on its value. + // Stack: [outcome]. Compare to 1 to derive a bool for cond_br. + const.u32 1 + eq.u32 cond_br @one, @zero @one: @@ -28,19 +30,13 @@ fn @main() { ret } -fn @measure_q1() -> bool { - // Scratch slot at local 0 so we can `store.bool 0` away the is_lost flag - // after measure, leaving outcome on top — same trick as branch_on_outcome. - const.bool false - +fn @measure_q1() -> u32 { const.u64 1 gate h const.u64 1 gate measure - // Frame: [scratch=false, outcome, is_lost]. - store.bool 0 - // Frame: [is_lost, outcome] with outcome on top. + // Stack: [outcome] ret 1 } diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 05a7e62fb..613e18693 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -2,6 +2,7 @@ //! this directory via the public `PPVM` API. use ppvm_vihaco::composite::PPVM; +use ppvm_vihaco::measurements::MeasurementOutcome; #[test] fn bell_sst_runs_and_records_two_measurements() { @@ -39,7 +40,10 @@ fn function_call_jumps_into_callee_body() { let record = machine.measurement_record(); assert_eq!(record.len(), 1, "expected exactly one measurement"); assert_eq!(record[0].len(), 1); - assert!(record[0][0].is_some(), "measurement should not be lost"); + assert!( + record[0][0] != MeasurementOutcome::Lost, + "measurement should not be lost" + ); } #[test] @@ -49,7 +53,10 @@ fn function_call_returns() { let record = machine.measurement_record(); assert_eq!(record.len(), 1, "expected exactly one measurement"); assert_eq!(record[0].len(), 1); - assert!(record[0][0].is_some(), "measurement should not be lost"); + assert!( + record[0][0] != MeasurementOutcome::Lost, + "measurement should not be lost" + ); } #[test] @@ -61,8 +68,16 @@ fn branch_on_outcome_deterministic_x_path() { .unwrap_or_else(|e| panic!("run branch_on_outcome_x.sst: {e:?}")); let record = machine.measurement_record(); assert_eq!(record.len(), 2, "expected exactly two measurements"); - assert_eq!(record[0], vec![Some(true)], "X-prepared q0 must measure 1"); - assert_eq!(record[1], vec![Some(true)], "branch must have flipped q1"); + assert_eq!( + record[0], + vec![MeasurementOutcome::One], + "X-prepared q0 must measure 1" + ); + assert_eq!( + record[1], + vec![MeasurementOutcome::One], + "branch must have flipped q1" + ); } #[test] @@ -77,10 +92,14 @@ fn branch_on_outcome_statistics_balanced_and_invariant_holds() { .unwrap_or_else(|e| panic!("run branch_on_outcome.sst: {e:?}")); let record = machine.measurement_record(); assert_eq!(record.len(), 2); - let m0 = record[0][0].expect("q0 should not be lost"); - let m1 = record[1][0].expect("q1 should not be lost"); + let m0 = record[0][0]; + let m1 = record[1][0]; assert_eq!(m0, m1, "branch must steer q1 to match q0 on every shot"); - if m0 { + assert!( + m0 != MeasurementOutcome::Lost, + "measurement should not be lost" + ); + if m0 == MeasurementOutcome::One { ones += 1; } } @@ -96,11 +115,11 @@ fn branch_on_outcome_statistics_balanced_and_invariant_holds() { #[test] fn function_call_branch_on_both_returned_values() { - // `function_call_branch_both.sst`: helper returns BOTH outcome and - // is_lost via `ret 2`. Main first branches on is_lost, then on outcome, - // steering q1 to |1> on the lost path and on the kept-outcome=1 path, - // leaving q1 in |0> only on the kept-outcome=0 path. With loss prob 0.5 - // and a |+> prep: + // `function_call_branch_both.sst`: helper returns the tri-state outcome + // (0/1/Lost) via `ret 1`. Main first branches on is_lost, then on the + // 0/1 outcome, steering q1 to |1> on the lost path and on the + // kept-outcome=1 path, leaving q1 in |0> only on the kept-outcome=0 + // path. With loss prob 0.5 and a |+> prep: // P(m1 = 1) = P(lost) + P(kept) · P(outcome = 1 | kept) // = 0.5 + 0.5 · 0.5 = 0.75 // P(m0 = lost) = 0.5 @@ -114,10 +133,10 @@ fn function_call_branch_on_both_returned_values() { assert_eq!(record.len(), 2, "expected exactly two measurements"); assert_eq!(record[0].len(), 1); assert_eq!(record[1].len(), 1); - if record[0][0].is_none() { + if record[0][0] == MeasurementOutcome::Lost { q0_lost += 1; } - if record[1][0] == Some(true) { + if record[1][0] == MeasurementOutcome::One { q1_ones += 1; } } From 8ab3ebe052f66cf64c3c27a05996041cd06ba764 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 09:53:08 +0200 Subject: [PATCH 26/95] Add comment to measurement outcome --- crates/ppvm-vihaco/src/measurements.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/ppvm-vihaco/src/measurements.rs b/crates/ppvm-vihaco/src/measurements.rs index 05634e8ea..bb15f5c67 100644 --- a/crates/ppvm-vihaco/src/measurements.rs +++ b/crates/ppvm-vihaco/src/measurements.rs @@ -1,6 +1,12 @@ use eyre::Result; use vihaco::{Effects, observe}; +/// Measurement results are represent as an integer enum +/// 0: state |0> +/// 1: state |1> +/// 2: qubit has been lost prior to measurement +/// In byte-code, this is represented as a u32 integer, which is simpler than +/// e.g. two boolean values and matches semantics elsewhere #[repr(u8)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum MeasurementOutcome { From e4aaeb8613e2dd52d5d3c9564923aa80394a3004 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 10:10:09 +0200 Subject: [PATCH 27/95] Use smallvec for measurement result --- Cargo.lock | 1 + crates/ppvm-vihaco/Cargo.toml | 1 + crates/ppvm-vihaco/src/component.rs | 2 +- crates/ppvm-vihaco/src/measurements.rs | 6 +++++- crates/ppvm-vihaco/tests/sst_fixtures.rs | 8 ++++---- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c84934181..2c69e3b3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1166,6 +1166,7 @@ dependencies = [ "num", "ppvm-runtime", "ppvm-tableau", + "smallvec", "vihaco", "vihaco-cpu", "vihaco-parser", diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index dae8fab98..5eafca9a8 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -12,6 +12,7 @@ log = "0.4.29" num = "0.4.3" ppvm-runtime = { version = "0.1.0", path = "../ppvm-runtime" } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon"] } +smallvec = "1.15.1" vihaco = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco" } vihaco-cpu = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco-cpu" } vihaco-parser = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco-parser" } diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index ec1d0c534..41fb48e57 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -85,7 +85,7 @@ where (Measure, &Qubit(addr)) => { let outcome: MeasurementOutcome = self.tab.measure(addr).into(); return Ok(Effects::one(MeasurementEffect { - measurement_results: vec![outcome], + measurement_results: smallvec::smallvec![outcome], })); } (Reset, &Qubit(addr)) => self.tab.reset(addr), diff --git a/crates/ppvm-vihaco/src/measurements.rs b/crates/ppvm-vihaco/src/measurements.rs index bb15f5c67..ad126b09d 100644 --- a/crates/ppvm-vihaco/src/measurements.rs +++ b/crates/ppvm-vihaco/src/measurements.rs @@ -1,4 +1,5 @@ use eyre::Result; +use smallvec::SmallVec; use vihaco::{Effects, observe}; /// Measurement results are represent as an integer enum @@ -15,7 +16,10 @@ pub enum MeasurementOutcome { Lost = 2, } -pub type MeasurementResult = Vec; +/// Inline outcomes per measurement effect before spilling to heap storage. +pub const MEASUREMENT_RESULT_INLINE_CAPACITY: usize = 8; + +pub type MeasurementResult = SmallVec<[MeasurementOutcome; MEASUREMENT_RESULT_INLINE_CAPACITY]>; #[derive(Debug, Clone, Eq, PartialEq)] pub struct MeasurementEffect { diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 613e18693..2889902f8 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -69,13 +69,13 @@ fn branch_on_outcome_deterministic_x_path() { let record = machine.measurement_record(); assert_eq!(record.len(), 2, "expected exactly two measurements"); assert_eq!( - record[0], - vec![MeasurementOutcome::One], + record[0].as_slice(), + &[MeasurementOutcome::One], "X-prepared q0 must measure 1" ); assert_eq!( - record[1], - vec![MeasurementOutcome::One], + record[1].as_slice(), + &[MeasurementOutcome::One], "branch must have flipped q1" ); } From 15a143c355c1b5c5eec4e607a85fadf90cb138ce Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 10:14:16 +0200 Subject: [PATCH 28/95] Make batched operations also smallvec --- crates/ppvm-vihaco/src/measurements.rs | 5 +---- crates/ppvm-vihaco/src/message.rs | 17 +++++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/ppvm-vihaco/src/measurements.rs b/crates/ppvm-vihaco/src/measurements.rs index ad126b09d..b7cb96ef2 100644 --- a/crates/ppvm-vihaco/src/measurements.rs +++ b/crates/ppvm-vihaco/src/measurements.rs @@ -16,10 +16,7 @@ pub enum MeasurementOutcome { Lost = 2, } -/// Inline outcomes per measurement effect before spilling to heap storage. -pub const MEASUREMENT_RESULT_INLINE_CAPACITY: usize = 8; - -pub type MeasurementResult = SmallVec<[MeasurementOutcome; MEASUREMENT_RESULT_INLINE_CAPACITY]>; +pub type MeasurementResult = SmallVec<[MeasurementOutcome; 8]>; #[derive(Debug, Clone, Eq, PartialEq)] pub struct MeasurementEffect { diff --git a/crates/ppvm-vihaco/src/message.rs b/crates/ppvm-vihaco/src/message.rs index de9bd4f49..31284cfdd 100644 --- a/crates/ppvm-vihaco/src/message.rs +++ b/crates/ppvm-vihaco/src/message.rs @@ -1,3 +1,4 @@ +use smallvec::SmallVec; use vihaco::Message; #[derive(Debug, Clone, Message)] @@ -12,12 +13,12 @@ pub enum CircuitMessage { TwoQubitAndFloatArr15(usize, usize, [f64; 15]), // TwoQubitPauliError // batched instructions - QubitBatch(Vec), // X, Y, Z, ... - QubitBatchAndFloat(Vec, f64), // RX, depolarize, ... - TwoQubitBatch(Vec<(usize, usize)>), // CX, CZ - TwoQubitBatchAndFloat(Vec<(usize, usize)>, f64), // RXX, ... - QubitBatchU3(Vec, f64, f64, f64), // U3 - QubitBatchAndFloatArr3(Vec, [f64; 3]), // PauliError - TwoQubitBatchAndFloatArr3(Vec<(usize, usize)>, [f64; 3]), // Correlated loss - TwoQubitBatchAndFloatArr15(Vec<(usize, usize)>, [f64; 15]), // TwoQubitPauliError + QubitBatch(SmallVec<[usize; 8]>), // X, Y, Z, ... + QubitBatchAndFloat(SmallVec<[usize; 8]>, f64), // RX, depolarize, ... + TwoQubitBatch(SmallVec<[(usize, usize); 8]>), // CX, CZ + TwoQubitBatchAndFloat(SmallVec<[(usize, usize); 8]>, f64), // RXX, ... + QubitBatchU3(SmallVec<[usize; 8]>, f64, f64, f64), // U3 + QubitBatchAndFloatArr3(SmallVec<[usize; 8]>, [f64; 3]), // PauliError + TwoQubitBatchAndFloatArr3(SmallVec<[(usize, usize); 8]>, [f64; 3]), // Correlated loss + TwoQubitBatchAndFloatArr15(SmallVec<[(usize, usize); 8]>, [f64; 15]), // TwoQubitPauliError } From 14423a428abeced4a1a118031f43d8f45d7066ee Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 10:15:57 +0200 Subject: [PATCH 29/95] Better naming for error shotrhand type --- crates/ppvm-vihaco/src/syntax.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index 2b7b392ba..b048097e1 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -134,10 +134,10 @@ impl Resolve for PPVMResolver { } } -type E<'src> = extra::Err>; +type Err<'src> = extra::Err>; impl<'src> Parse<'src> for PPVMInstruction { - fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> { + fn parser() -> impl Parser<'src, &'src str, Self, Err<'src>> { use chumsky::prelude::*; let cpu = ::parser().map(PPVMInstruction::Cpu); From 7d6ff202d187f727a8f3d8d4ba03712229bbed64 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 11:46:16 +0200 Subject: [PATCH 30/95] Add bytecode emission and loading --- crates/ppvm-vihaco/src/bytecode.rs | 386 ++++++++++++++++++++++++++++ crates/ppvm-vihaco/src/composite.rs | 32 ++- crates/ppvm-vihaco/src/lib.rs | 68 ++++- 3 files changed, 475 insertions(+), 11 deletions(-) create mode 100644 crates/ppvm-vihaco/src/bytecode.rs diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs new file mode 100644 index 000000000..2971a1448 --- /dev/null +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -0,0 +1,386 @@ +//! Binary bytecode (`.ssb`) round-trip for PPVM modules. +//! +//! Serializes a resolved [`Module`] to a little-endian container — header +//! (magic + version + device info) → strings section → code section — and +//! reads it back. The per-instruction codec is the existing `WriteBytes` / +//! `FromBytes` generated for [`PPVMInstruction`]; this module only frames the +//! container around it. + +use std::io::{Read, Write}; + +use vihaco::instruction::{FromBytes, WriteBytes}; +use vihaco::{Type, Value, module::Module}; + +use crate::composite::{PPVM_MAGIC, PPVMDeviceInfo, PPVMInstruction}; + +/// Current `.ssb` format version. The reader rejects any other version. +pub const PPVM_BYTECODE_VERSION: u16 = 1; + +type PPVMModule = Module; + +/// Byte length of the fixed v1 header (magic 4, version 2, header_size 4, +/// n_qubits 4, coefficient_threshold 8) and the offset where the strings +/// section begins. +const HEADER_SIZE: u32 = 4 + 2 + 4 + 4 + 8; + +/// Serialize a resolved module to the v1 `.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 { + return Err(eyre::eyre!( + "bytecode v1 cannot represent a populated `{table}`" + )); + } + + let info = &module.extra; + let n_qubits = u32::try_from(info.n_qubits) + .map_err(|_| eyre::eyre!("n_qubits {} does not fit in u32", info.n_qubits))?; + + // Header. + w.write_all(&PPVM_MAGIC.to_le_bytes())?; + w.write_all(&PPVM_BYTECODE_VERSION.to_le_bytes())?; + w.write_all(&HEADER_SIZE.to_le_bytes())?; + w.write_all(&n_qubits.to_le_bytes())?; + w.write_all(&info.coefficient_threshold.to_le_bytes())?; + + // Strings section: count, then each entry as len-prefixed UTF-8. + let string_count = + u32::try_from(module.strings.len()).map_err(|_| eyre::eyre!("string count exceeds u32"))?; + w.write_all(&string_count.to_le_bytes())?; + for s in &module.strings { + let len = u32::try_from(s.len()).map_err(|_| eyre::eyre!("string length exceeds u32"))?; + w.write_all(&len.to_le_bytes())?; + w.write_all(s.as_bytes())?; + } + + // 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)?; + } + + Ok(()) +} + +/// Reconstruct a module from a v1 `.ssb` byte stream. +pub fn read_module(r: &mut R) -> eyre::Result { + // Header. + let magic = read_u32(r)?; + if magic != PPVM_MAGIC { + return Err(eyre::eyre!( + "not a PPVM bytecode file (magic 0x{magic:08X})" + )); + } + let version = read_u16(r)?; + if version != PPVM_BYTECODE_VERSION { + return Err(eyre::eyre!("unsupported bytecode version {version}")); + } + let header_size = read_u32(r)?; + let n_qubits = read_u32(r)? as usize; + let coefficient_threshold = read_f64(r)?; + + // Sections begin at `header_size`; skip any header bytes beyond v1's fixed + // fields (forward compat / self-description). + if header_size < HEADER_SIZE { + return Err(eyre::eyre!( + "header_size {header_size} smaller than minimum {HEADER_SIZE}" + )); + } + skip_bytes(r, u64::from(header_size - HEADER_SIZE))?; + + // Don't pre-allocate from an untrusted count; grow as entries are read. + let string_count = read_u32(r)?; + let mut strings = Vec::new(); + for _ in 0..string_count { + let len = read_u32(r)? as usize; + let mut bytes = vec![0u8; len]; + r.read_exact(&mut bytes)?; + strings.push(String::from_utf8(bytes)?); + } + + let code_count = read_u32(r)?; + let mut code = Vec::new(); + for _ in 0..code_count { + code.push(PPVMInstruction::from_bytes(r)?); + } + + Ok(PPVMModule { + extra: PPVMDeviceInfo { + magic, + n_qubits, + coefficient_threshold, + }, + strings, + code, + ..Default::default() + }) +} + +/// Serialize a module to an owned byte vector. +pub fn module_to_bytes(module: &PPVMModule) -> eyre::Result> { + let mut buf = Vec::new(); + write_module(module, &mut buf)?; + Ok(buf) +} + +/// Reconstruct a module from a byte slice. +pub fn module_from_bytes(bytes: &[u8]) -> eyre::Result { + read_module(&mut &bytes[..]) +} + +/// "Dump": compile `.sst` source straight to the `.ssb` byte stream. +pub fn compile_to_bytes(source: &str) -> eyre::Result> { + let module = crate::compile_program(source)?; + module_to_bytes(&module) +} + +fn read_u16(r: &mut R) -> eyre::Result { + let mut b = [0u8; 2]; + r.read_exact(&mut b)?; + Ok(u16::from_le_bytes(b)) +} + +fn read_u32(r: &mut R) -> eyre::Result { + let mut b = [0u8; 4]; + r.read_exact(&mut b)?; + Ok(u32::from_le_bytes(b)) +} + +fn read_f64(r: &mut R) -> eyre::Result { + let mut b = [0u8; 8]; + r.read_exact(&mut b)?; + Ok(f64::from_le_bytes(b)) +} + +fn skip_bytes(r: &mut R, n: u64) -> eyre::Result<()> { + let skipped = std::io::copy(&mut r.take(n), &mut std::io::sink())?; + if skipped != n { + return Err(eyre::eyre!("unexpected EOF skipping {n} header bytes")); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn empty_module() -> PPVMModule { + PPVMModule::default() + } + + #[test] + fn round_trips_device_info() { + let mut m = empty_module(); + m.extra.n_qubits = 7; + m.extra.coefficient_threshold = 1e-9; + + let mut buf = Vec::new(); + write_module(&m, &mut buf).unwrap(); + let back = read_module(&mut buf.as_slice()).unwrap(); + + assert_eq!(back, m); + } + + #[test] + fn round_trips_code() { + use crate::instruction::CircuitInstruction; + use vihaco_cpu::Instruction as Cpu; + + let mut m = empty_module(); + m.extra.n_qubits = 2; + m.code = vec![ + PPVMInstruction::Cpu(Cpu::Const(Value::U64(0))), + PPVMInstruction::Circuit(CircuitInstruction::H), + PPVMInstruction::Cpu(Cpu::Branch(1)), + PPVMInstruction::Cpu(Cpu::ConditionalBranch(0, 1)), + PPVMInstruction::Cpu(Cpu::Call(0, 1)), + PPVMInstruction::Cpu(Cpu::Return(0)), + ]; + + let mut buf = Vec::new(); + write_module(&m, &mut buf).unwrap(); + let back = read_module(&mut buf.as_slice()).unwrap(); + + assert_eq!(back, m); + } + + #[test] + fn read_honors_header_size_with_padding() { + 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))]; + + let mut buf = Vec::new(); + write_module(&m, &mut buf).unwrap(); + + // Simulate a larger header: 4 padding bytes after the fixed fields, + // with header_size bumped to match. The reader must skip to it. + buf[6..10].copy_from_slice(&(HEADER_SIZE + 4).to_le_bytes()); + for i in 0..4 { + buf.insert(HEADER_SIZE as usize + i, 0x00); + } + + let back = read_module(&mut buf.as_slice()).unwrap(); + assert_eq!(back, m); + } + + #[test] + fn compile_to_bytes_round_trips_through_resolve() { + let src = "device circuit.n_qubits 2;\n\ + fn @main() {\n\ + const.u64 0\n\ + gate h\n\ + const.u64 0\n\ + const.u64 1\n\ + gate cnot\n\ + ret\n\ + }\n"; + + let bytes = compile_to_bytes(src).unwrap(); + let back = module_from_bytes(&bytes).unwrap(); + let expected = crate::compile_program(src).unwrap(); + + assert_eq!(back, expected); + } + + #[test] + fn loaded_bytecode_executes_like_text() { + let src = "device circuit.n_qubits 2;\n\ + fn @main() {\n\ + const.u64 0\n gate h\n\ + const.u64 0\n const.u64 1\n gate cnot\n\ + const.u64 0\n gate measure\n\ + const.u64 1\n gate measure\n\ + ret\n }\n"; + let bytes = compile_to_bytes(src).unwrap(); + + let mut machine = crate::composite::PPVM::default(); + machine.load_bytecode(&bytes).unwrap(); + machine.run().unwrap(); + + assert_eq!(machine.measurement_record().len(), 2); + } + + #[test] + fn load_bytecode_file_reads_from_disk() { + let src = "device circuit.n_qubits 1;\n\ + fn @main() { const.u64 0\n gate measure\n ret }\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(); + + let mut machine = crate::composite::PPVM::default(); + machine.load_bytecode_file(path.to_str().unwrap()).unwrap(); + machine.run().unwrap(); + + assert_eq!(machine.measurement_record().len(), 1); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn read_rejects_truncated_input() { + let mut m = empty_module(); + m.extra.n_qubits = 2; + m.code = vec![PPVMInstruction::Cpu(vihaco_cpu::Instruction::Return(0))]; + + let mut buf = Vec::new(); + write_module(&m, &mut buf).unwrap(); + buf.truncate(buf.len() - 3); // cut off mid-instruction + + assert!(read_module(&mut buf.as_slice()).is_err()); + } + + #[test] + fn write_rejects_populated_functions_table() { + use vihaco::module::{FunctionInfo, Signature}; + + let mut m = empty_module(); + m.extra.n_qubits = 1; + m.functions.push(FunctionInfo { + name: 0, + signature: Signature { + params: vec![], + ret: vec![], + }, + local_count: 0, + start_address: 0, + end_address: 0, + file: 0, + }); + + let mut buf = Vec::new(); + let err = write_module(&m, &mut buf).unwrap_err(); + assert!(err.to_string().contains("functions"), "err: {err}"); + } + + #[test] + fn read_rejects_bad_magic() { + let mut m = empty_module(); + m.extra.n_qubits = 2; + let mut buf = Vec::new(); + write_module(&m, &mut buf).unwrap(); + // Corrupt the magic (first 4 bytes). + buf[0] ^= 0xFF; + + let err = read_module(&mut buf.as_slice()).unwrap_err(); + assert!( + err.to_string().contains("not a PPVM bytecode file"), + "err: {err}" + ); + } + + #[test] + fn round_trips_strings() { + let mut m = empty_module(); + m.extra.n_qubits = 1; + m.strings = vec![ + String::new(), + "hello".to_string(), + "tab\tnl\nquote\"".to_string(), + "üñîçødé ⚛".to_string(), + ]; + + let mut buf = Vec::new(); + write_module(&m, &mut buf).unwrap(); + let back = read_module(&mut buf.as_slice()).unwrap(); + + assert_eq!(back, m); + } + + #[test] + fn read_rejects_unsupported_version() { + let mut m = empty_module(); + m.extra.n_qubits = 2; + let mut buf = Vec::new(); + write_module(&m, &mut buf).unwrap(); + // Bump the version (bytes 4..6) past the supported one. + let bad = (PPVM_BYTECODE_VERSION + 1).to_le_bytes(); + buf[4] = bad[0]; + buf[5] = bad[1]; + + let err = read_module(&mut buf.as_slice()).unwrap_err(); + assert!( + err.to_string().contains("unsupported bytecode version"), + "err: {err}" + ); + } +} diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 44c27dfa0..fa125c4b3 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -1,18 +1,14 @@ -use chumsky::Parser; use vihaco::frame::Frame; use vihaco::machine::StackFrame; use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; -use vihaco::syntax::{ParsedModule, Resolve}; use vihaco::traits::{GetProgramGlobal, ProgramCounter, StackMemory}; use vihaco::{Effects, Observe, ProgramLoader, Value, composite, observe}; use vihaco_cpu::{CPU, CPUMessage, StepOutcome}; -use vihaco_parser_core::Parse; use crate::component::{Circuit, CircuitEffect}; use crate::instruction::CircuitInstruction; use crate::measurements::{MeasurementEffect, MeasurementObserver, MeasurementResult}; use crate::message::CircuitMessage; -use crate::syntax::{PPVMHeader, PPVMResolver}; pub const PPVM_MAGIC: u32 = 0x5050564D; @@ -108,6 +104,16 @@ impl std::fmt::Display for PPVMInstruction { } } +impl PartialEq for PPVMInstruction { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (PPVMInstruction::Cpu(a), PPVMInstruction::Cpu(b)) => a == b, + (PPVMInstruction::Circuit(a), PPVMInstruction::Circuit(b)) => a == b, + _ => false, + } + } +} + impl From for PPVMInstruction { fn from(value: vihaco_cpu::Instruction) -> Self { Self::Cpu(value) @@ -363,11 +369,7 @@ impl PPVM { } pub fn load_program(&mut self, program: &str) -> eyre::Result<()> { - let parsed = ParsedModule::::parser() - .parse(program) - .into_result() - .map_err(|errs| eyre::eyre!("parsing failed: {errs:?}"))?; - let module = PPVMResolver::new().resolve_module(parsed)?; + let module = crate::compile_program(program)?; self.load(&module)?; Ok(()) } @@ -377,6 +379,18 @@ impl PPVM { self.load_program(&raw_program) } + /// Load a module from an in-memory `.ssb` byte stream. + pub fn load_bytecode(&mut self, bytes: &[u8]) -> eyre::Result<()> { + let module = crate::bytecode::module_from_bytes(bytes)?; + self.load(&module) + } + + /// Read a `.ssb` file and load the module it contains. + pub fn load_bytecode_file(&mut self, path: &str) -> eyre::Result<()> { + let bytes = std::fs::read(path)?; + self.load_bytecode(&bytes) + } + pub fn run_program(&mut self, program: &str) -> eyre::Result<()> { self.load_program(program)?; self.run()?; diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 3c0af0f7a..7725b0808 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -1,5 +1,4 @@ -use crate::composite::PPVM; - +pub mod bytecode; pub mod component; pub mod composite; pub mod instruction; @@ -7,6 +6,14 @@ pub mod measurements; pub mod message; mod syntax; +use chumsky::Parser; +use vihaco::syntax::{ParsedModule, Resolve}; +use vihaco::{Type, Value, module::Module}; +use vihaco_parser_core::Parse; + +use crate::composite::{PPVM, PPVMDeviceInfo, PPVMInstruction}; +use crate::syntax::{PPVMHeader, PPVMResolver}; + pub fn run_file(path: &str) -> eyre::Result { let mut machine = PPVM::default(); machine.run_file(path)?; @@ -19,7 +26,64 @@ pub fn run_program(program: &str) -> eyre::Result { Ok(machine) } +/// Parse and resolve `.sst` source into a runnable module. +pub fn compile_program( + source: &str, +) -> eyre::Result> { + let parsed = ParsedModule::::parser() + .parse(source) + .into_result() + .map_err(|errs| eyre::eyre!("parsing failed: {errs:?}"))?; + PPVMResolver::new().resolve_module(parsed) +} + +/// Dump `.sst` source to a `.ssb` bytecode file. +pub fn dump_program(program: &str, output_path: &str) -> eyre::Result<()> { + let bytes = bytecode::compile_to_bytes(program)?; + std::fs::write(output_path, bytes)?; + Ok(()) +} + +/// Read a `.sst` file and dump it to a `.ssb` bytecode file. +pub fn dump_file(input_path: &str, output_path: &str) -> eyre::Result<()> { + let program = std::fs::read_to_string(input_path)?; + dump_program(&program, output_path) +} + pub mod prelude { pub use crate::component::Circuit; pub use crate::composite::PPVM; } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dump_program_writes_loadable_bytecode() { + let src = "device circuit.n_qubits 1;\n\ + fn @main() { const.u64 0\n gate measure\n ret }\n"; + let path = std::env::temp_dir().join("ppvm_dump_program_test.ssb"); + dump_program(src, path.to_str().unwrap()).unwrap(); + + let mut machine = PPVM::default(); + machine.load_bytecode_file(path.to_str().unwrap()).unwrap(); + machine.run().unwrap(); + + assert_eq!(machine.measurement_record().len(), 1); + let _ = std::fs::remove_file(&path); + } + + #[test] + fn dump_file_reads_sst_and_writes_bytecode() { + let out = std::env::temp_dir().join("ppvm_dump_file_test.ssb"); + dump_file("tests/function_call.sst", out.to_str().unwrap()).unwrap(); + + let mut machine = PPVM::default(); + machine.load_bytecode_file(out.to_str().unwrap()).unwrap(); + machine.run().unwrap(); + + assert_eq!(machine.measurement_record().len(), 1); + let _ = std::fs::remove_file(&out); + } +} From 991447fb8acfba7466dbf18e05ae66ef1f8b060a Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 11:51:57 +0200 Subject: [PATCH 31/95] More tests --- crates/ppvm-vihaco/tests/sst_fixtures.rs | 84 ++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 2889902f8..e5dfa7cd3 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -4,6 +4,23 @@ use ppvm_vihaco::composite::PPVM; use ppvm_vihaco::measurements::MeasurementOutcome; +/// Dump a fixture to a `.ssb` file, load it back, and run it. Exercises the +/// full bytecode round-trip through disk: `dump_file` → `load_bytecode_file`. +fn dump_load_run(sst_path: &str, ssb_name: &str) -> PPVM { + let out = std::env::temp_dir().join(ssb_name); + let out = out.to_str().expect("utf-8 temp path"); + ppvm_vihaco::dump_file(sst_path, out).unwrap_or_else(|e| panic!("dump {sst_path}: {e:?}")); + + let mut machine = PPVM::default(); + machine + .load_bytecode_file(out) + .unwrap_or_else(|e| panic!("load {out}: {e:?}")); + machine.run().unwrap_or_else(|e| panic!("run {out}: {e:?}")); + + let _ = std::fs::remove_file(out); + machine +} + #[test] fn bell_sst_runs_and_records_two_measurements() { let mut machine = PPVM::default(); @@ -151,3 +168,70 @@ fn function_call_branch_on_both_returned_values() { "expected ~300 q1=true shots, got {q1_ones}" ); } + +// ─── Bytecode round-trip: dump → load → execute each fixture ────────────── + +#[test] +fn dumped_bell_records_two_measurements() { + let machine = dump_load_run("tests/bell.sst", "ppvm_dump_bell.ssb"); + assert_eq!(machine.measurement_record().len(), 2); +} + +#[test] +fn dumped_hello_circuit_runs_with_no_measurements() { + let machine = dump_load_run("tests/hello_circuit.sst", "ppvm_dump_hello_circuit.ssb"); + assert_eq!(machine.measurement_record().len(), 0); +} + +#[test] +fn dumped_function_call_executes_callee() { + let machine = dump_load_run("tests/function_call.sst", "ppvm_dump_function_call.ssb"); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1); + assert_eq!(record[0].len(), 1); + assert!(record[0][0] != MeasurementOutcome::Lost); +} + +#[test] +fn dumped_function_call_ret_executes() { + let machine = dump_load_run( + "tests/function_call_ret.sst", + "ppvm_dump_function_call_ret.ssb", + ); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1); + assert_eq!(record[0].len(), 1); +} + +#[test] +fn dumped_branch_on_outcome_x_is_deterministic() { + // X-prepared q0 measures 1, so the branch flips q1 → both outcomes are 1. + // Confirms branch targets survive the dump/load round-trip. + let machine = dump_load_run("tests/branch_on_outcome_x.sst", "ppvm_dump_branch_x.ssb"); + let record = machine.measurement_record(); + assert_eq!(record.len(), 2); + assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); + assert_eq!(record[1].as_slice(), &[MeasurementOutcome::One]); +} + +#[test] +fn dumped_branch_on_outcome_preserves_invariant() { + // q0 in |+> is a fair coin, but the branch steers q1 to match q0 every + // shot — that invariant must hold after a round-trip. + let machine = dump_load_run("tests/branch_on_outcome.sst", "ppvm_dump_branch.ssb"); + let record = machine.measurement_record(); + assert_eq!(record.len(), 2); + assert_eq!(record[0][0], record[1][0]); +} + +#[test] +fn dumped_function_call_branch_both_runs() { + let machine = dump_load_run( + "tests/function_call_branch_both.sst", + "ppvm_dump_function_call_branch_both.ssb", + ); + let record = machine.measurement_record(); + assert_eq!(record.len(), 2); + assert_eq!(record[0].len(), 1); + assert_eq!(record[1].len(), 1); +} From 2a397c91d0fc811bce1d348eba562e718a32c86a Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 14:26:38 +0200 Subject: [PATCH 32/95] Expose some more stuff --- crates/ppvm-vihaco/Cargo.toml | 8 ++++---- crates/ppvm-vihaco/src/lib.rs | 16 ++++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 5eafca9a8..61579ac5d 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -13,7 +13,7 @@ num = "0.4.3" ppvm-runtime = { version = "0.1.0", path = "../ppvm-runtime" } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon"] } smallvec = "1.15.1" -vihaco = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco" } -vihaco-cpu = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco-cpu" } -vihaco-parser = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco-parser" } -vihaco-parser-core = { version = "0.1.0", path = "../../../stellarscope/crates/vihaco-parser-core" } +vihaco = { version = "0.1.0", path = "../../../vihaco/crates/vihaco" } +vihaco-cpu = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-cpu" } +vihaco-parser = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-parser" } +vihaco-parser-core = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-parser-core" } diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 7725b0808..bd7f591f1 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -26,15 +26,18 @@ pub fn run_program(program: &str) -> eyre::Result { Ok(machine) } -/// Parse and resolve `.sst` source into a runnable module. +/// 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 compile_program( source: &str, ) -> eyre::Result> { - let parsed = ParsedModule::::parser() - .parse(source) - .into_result() - .map_err(|errs| eyre::eyre!("parsing failed: {errs:?}"))?; - PPVMResolver::new().resolve_module(parsed) + PPVMResolver::new().resolve_module(parse_program(source)?) } /// Dump `.sst` source to a `.ssb` bytecode file. @@ -53,6 +56,7 @@ pub fn dump_file(input_path: &str, output_path: &str) -> eyre::Result<()> { pub mod prelude { pub use crate::component::Circuit; pub use crate::composite::PPVM; + pub use crate::syntax::{PPVMHeader, PPVMResolver}; } #[cfg(test)] From 8d711ba5c01190bb953befca5a02e5c98b2a3e1c Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 14:35:30 +0200 Subject: [PATCH 33/95] Inspect source to check if it's bytecode before loading --- crates/ppvm-vihaco/src/bytecode.rs | 10 ++++ crates/ppvm-vihaco/src/composite.rs | 12 ++++- crates/ppvm-vihaco/tests/sst_fixtures.rs | 61 ++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs index 2971a1448..77b945cc3 100644 --- a/crates/ppvm-vihaco/src/bytecode.rs +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -146,6 +146,16 @@ pub fn module_from_bytes(bytes: &[u8]) -> eyre::Result { read_module(&mut &bytes[..]) } +/// Cheap sniff: does this byte stream begin with the PPVM `.ssb` magic? +/// +/// Reads the leading four bytes the same way [`read_module`] does — as a +/// little-endian `u32` — so a positive result here means [`read_module`] will +/// accept the magic. A stream shorter than the magic is not bytecode. +pub fn is_bytecode(bytes: &[u8]) -> bool { + bytes.len() >= 4 + && u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) == PPVM_MAGIC +} + /// "Dump": compile `.sst` source straight to the `.ssb` byte stream. pub fn compile_to_bytes(source: &str) -> eyre::Result> { let module = crate::compile_program(source)?; diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index fa125c4b3..4680cd08f 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -374,9 +374,17 @@ impl PPVM { Ok(()) } + /// Load from a file, auto-detecting the format: if it starts with the PPVM + /// magic it is loaded as `.ssb` bytecode, otherwise it is parsed as `.sst` + /// source text. A magic match commits to the bytecode path — a corrupt + /// `.ssb` errors rather than silently falling back to the text parser. pub fn load_file(&mut self, path: &str) -> eyre::Result<()> { - let raw_program = std::fs::read_to_string(path)?; - self.load_program(&raw_program) + let bytes = std::fs::read(path)?; + if crate::bytecode::is_bytecode(&bytes) { + self.load_bytecode(&bytes) + } else { + self.load_program(std::str::from_utf8(&bytes)?) + } } /// Load a module from an in-memory `.ssb` byte stream. diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index e5dfa7cd3..1604452aa 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -169,6 +169,67 @@ fn function_call_branch_on_both_returned_values() { ); } +// ─── Auto-detect via load_file: route by content, not extension ─────────── + +#[test] +fn is_bytecode_distinguishes_ssb_from_sst() { + let sst = std::fs::read("tests/bell.sst").expect("read bell.sst"); + assert!( + !ppvm_vihaco::bytecode::is_bytecode(&sst), + ".sst source must not be detected as bytecode" + ); + + let ssb = ppvm_vihaco::bytecode::compile_to_bytes( + &String::from_utf8(sst).expect("bell.sst is utf-8"), + ) + .expect("compile bell.sst"); + assert!( + ppvm_vihaco::bytecode::is_bytecode(&ssb), + "dumped .ssb must be detected as bytecode" + ); + + // Inputs shorter than the 4-byte magic are never bytecode. Note "PPVM" as + // text also fails: the magic is a little-endian u32, so its on-disk bytes + // are "MVPP", not "PPVM". + assert!(!ppvm_vihaco::bytecode::is_bytecode(b"PPV")); + assert!(!ppvm_vihaco::bytecode::is_bytecode(b"")); + assert!(!ppvm_vihaco::bytecode::is_bytecode(b"PPVM")); +} + +#[test] +fn load_file_auto_detects_bytecode_and_text() { + // Use the deterministic X-prepared fixture: q0 measures 1, the branch + // flips q1, so both routes must yield exactly [1], [1]. Any divergence — + // or a binary file mis-parsed as text — fails loudly. + let from_text = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") + .unwrap_or_else(|e| panic!("run .sst via load_file: {e:?}")); + + // Dump the same fixture to a `.ssb` and run *that file* through the same + // run_file entry point. If load_file didn't sniff the magic it would try + // to parse the binary as text and error. + let out = std::env::temp_dir().join("ppvm_autodetect_branch_x.ssb"); + let out = out.to_str().expect("utf-8 temp path"); + ppvm_vihaco::dump_file("tests/branch_on_outcome_x.sst", out) + .unwrap_or_else(|e| panic!("dump: {e:?}")); + let from_binary = ppvm_vihaco::run_file(out).unwrap_or_else(|e| panic!("run .ssb: {e:?}")); + let _ = std::fs::remove_file(out); + + for (label, machine) in [("text", &from_text), ("binary", &from_binary)] { + let record = machine.measurement_record(); + assert_eq!(record.len(), 2, "{label}: expected two measurements"); + assert_eq!( + record[0].as_slice(), + &[MeasurementOutcome::One], + "{label}: X-prepared q0 must measure 1" + ); + assert_eq!( + record[1].as_slice(), + &[MeasurementOutcome::One], + "{label}: branch must flip q1" + ); + } +} + // ─── Bytecode round-trip: dump → load → execute each fixture ────────────── #[test] From a13fe34ae3377c1f16703491640c64fc9ff41889 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 14:42:56 +0200 Subject: [PATCH 34/95] Implement CLI to run ppvm --- Cargo.lock | 24 ++++++++++++ Cargo.toml | 2 +- crates/ppvm-cli/Cargo.toml | 13 +++++++ crates/ppvm-cli/src/commands.rs | 63 +++++++++++++++++++++++++++++++ crates/ppvm-cli/src/lib.rs | 1 + crates/ppvm-cli/src/main.rs | 67 +++++++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 crates/ppvm-cli/Cargo.toml create mode 100644 crates/ppvm-cli/src/commands.rs create mode 100644 crates/ppvm-cli/src/lib.rs create mode 100644 crates/ppvm-cli/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 2c69e3b3e..36dcfa1b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,6 +311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -319,8 +320,22 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1069,6 +1084,15 @@ dependencies = [ "ppvm-tableau", ] +[[package]] +name = "ppvm-cli" +version = "0.1.0" +dependencies = [ + "clap", + "eyre", + "ppvm-vihaco", +] + [[package]] name = "ppvm-python-native" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index fcd3987b7..f651490ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ # Runnable copies of the Rust code blocks in skills/ppvm-usage/SKILL.md. # Built by `cargo build --workspace --all-targets` in CI so the skill # can't silently drift away from the public API. - "skills/ppvm-usage/examples/rust", + "skills/ppvm-usage/examples/rust", "crates/ppvm-cli", ] [[example]] diff --git a/crates/ppvm-cli/Cargo.toml b/crates/ppvm-cli/Cargo.toml new file mode 100644 index 000000000..bb13afd7e --- /dev/null +++ b/crates/ppvm-cli/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "ppvm-cli" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.6.1", features = ["derive"] } +eyre = "0.6.12" +ppvm-vihaco = { version = "0.1.0", path = "../ppvm-vihaco" } + +[[bin]] +name = "ppvm" +path = "src/main.rs" diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs new file mode 100644 index 000000000..3cd8a07de --- /dev/null +++ b/crates/ppvm-cli/src/commands.rs @@ -0,0 +1,63 @@ +use eyre::Result; +use ppvm_vihaco::run_file; +use std::path::Path; + +pub fn run(file: &str, show_measurements: bool) -> Result<()> { + let ppvm = run_file(file)?; + if show_measurements { + let outcomes = ppvm.measurement_record(); + println!("Measurement record:\n{:?}", outcomes); + } else { + println!("Successfully ran file {}", file); + } + Ok(()) +} + +pub fn parse(file: &str, format: &str) -> Result<()> { + let source = std::fs::read_to_string(file)?; + let parsed = ppvm_vihaco::parse_program(&source)?; + + match format { + "json" => { + eprintln!("Warning: JSON format not yet supported for AST, using debug format"); + println!("{:#?}", parsed); + } + "debug" => { + println!("{:#?}", parsed); + } + _ => { + // Pretty summary. + println!("Module:"); + println!(" Headers: {}", parsed.headers.len()); + for (i, header) in parsed.headers.iter().enumerate() { + println!(" [{}] {:?}", i, header); + } + println!(" Functions: {}", parsed.functions.len()); + for (i, func) in parsed.functions.iter().enumerate() { + println!( + " [{}] {}({} params, {} body items)", + i, + func.name, + func.params.len(), + func.body.len() + ); + } + } + } + + Ok(()) +} + +pub fn dump(file: &str, output: Option<&str>) -> Result<()> { + let output_file = match output { + Some(output_file_name) => output_file_name.to_string(), + None => Path::new(file) + .with_extension("ssb") + .to_string_lossy() + .into_owned(), + }; + + ppvm_vihaco::dump_file(file, &output_file)?; + eprintln!("Bytecode written to {output_file}"); + Ok(()) +} diff --git a/crates/ppvm-cli/src/lib.rs b/crates/ppvm-cli/src/lib.rs new file mode 100644 index 000000000..82b6da3c0 --- /dev/null +++ b/crates/ppvm-cli/src/lib.rs @@ -0,0 +1 @@ +pub mod commands; diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs new file mode 100644 index 000000000..601f75e6a --- /dev/null +++ b/crates/ppvm-cli/src/main.rs @@ -0,0 +1,67 @@ +use clap::{Parser, Subcommand}; +use eyre::Result; +use ppvm_cli::commands; + +#[derive(Parser)] +#[command(name = "ppvm")] +#[command(about = "Pauli propagation virtual machine", long_about = None)] +pub struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Parse a .sst file and output the AST + Parse { + /// Input .sst file + #[arg(value_name = "FILE")] + file: String, + + /// Output format (json, debug, pretty) + #[arg(short, long, default_value = "pretty")] + format: String, + }, + /// Compile a .sst file to bytecode + Dump { + /// Input .sst file + #[arg(value_name = "FILE")] + file: String, + + /// Output file (optional, defaults to .ssb) + #[arg(short, long)] + output: Option, + }, + + /// Run a .sst or .ssb program + Run { + /// Input file (.sst source or .ssb bytecode) + #[arg(value_name = "FILE")] + file: String, + + /// Show measurement output + #[arg(short, long, default_value = "true")] + show_measurements: bool, + }, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Parse { file, format } => { + commands::parse(&file, &format)?; + } + Commands::Dump { file, output } => { + commands::dump(&file, output.as_deref())?; + } + Commands::Run { + file, + show_measurements, + } => { + commands::run(&file, show_measurements)?; + } + } + + Ok(()) +} From bdcffc878e77e0016471ac37113aa0cc2aed5e81 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 14:46:29 +0200 Subject: [PATCH 35/95] Fix show-measurements flag and make formats an enum --- crates/ppvm-cli/src/commands.rs | 44 ++++++++++++++++++++++----------- crates/ppvm-cli/src/main.rs | 31 +++++++++++++---------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index 3cd8a07de..f997d5993 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -1,32 +1,40 @@ -use eyre::Result; +use eyre::{Result, WrapErr}; use ppvm_vihaco::run_file; use std::path::Path; -pub fn run(file: &str, show_measurements: bool) -> Result<()> { - let ppvm = run_file(file)?; - if show_measurements { +/// Output format for `parse`. +#[derive(Clone, Debug, clap::ValueEnum)] +pub enum Format { + Pretty, + Debug, + Json, +} + +pub fn run(file: &str, quiet: bool) -> Result<()> { + let ppvm = run_file(file).wrap_err_with(|| format!("failed to run {file}"))?; + if quiet { + println!("Successfully ran file {file}"); + } else { let outcomes = ppvm.measurement_record(); println!("Measurement record:\n{:?}", outcomes); - } else { - println!("Successfully ran file {}", file); } Ok(()) } -pub fn parse(file: &str, format: &str) -> Result<()> { - let source = std::fs::read_to_string(file)?; +pub fn parse(file: &str, format: Format) -> Result<()> { + let source = + std::fs::read_to_string(file).wrap_err_with(|| format!("failed to read {file}"))?; let parsed = ppvm_vihaco::parse_program(&source)?; match format { - "json" => { + Format::Json => { eprintln!("Warning: JSON format not yet supported for AST, using debug format"); println!("{:#?}", parsed); } - "debug" => { + Format::Debug => { println!("{:#?}", parsed); } - _ => { - // Pretty summary. + Format::Pretty => { println!("Module:"); println!(" Headers: {}", parsed.headers.len()); for (i, header) in parsed.headers.iter().enumerate() { @@ -48,7 +56,7 @@ pub fn parse(file: &str, format: &str) -> Result<()> { Ok(()) } -pub fn dump(file: &str, output: Option<&str>) -> Result<()> { +pub fn dump(file: &str, output: Option<&str>, force: bool) -> Result<()> { let output_file = match output { Some(output_file_name) => output_file_name.to_string(), None => Path::new(file) @@ -57,7 +65,15 @@ pub fn dump(file: &str, output: Option<&str>) -> Result<()> { .into_owned(), }; - ppvm_vihaco::dump_file(file, &output_file)?; + // Don't clobber an existing file unless asked to. + if !force && Path::new(&output_file).exists() { + return Err(eyre::eyre!( + "{output_file} already exists; pass --force to overwrite" + )); + } + + ppvm_vihaco::dump_file(file, &output_file) + .wrap_err_with(|| format!("failed to dump {file}"))?; eprintln!("Bytecode written to {output_file}"); Ok(()) } diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index 601f75e6a..dad698edc 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -18,9 +18,9 @@ enum Commands { #[arg(value_name = "FILE")] file: String, - /// Output format (json, debug, pretty) - #[arg(short, long, default_value = "pretty")] - format: String, + /// Output format + #[arg(short, long, value_enum, default_value = "pretty")] + format: commands::Format, }, /// Compile a .sst file to bytecode Dump { @@ -31,6 +31,10 @@ enum Commands { /// Output file (optional, defaults to .ssb) #[arg(short, long)] output: Option, + + /// Overwrite the output file if it already exists + #[arg(short, long)] + force: bool, }, /// Run a .sst or .ssb program @@ -39,9 +43,9 @@ enum Commands { #[arg(value_name = "FILE")] file: String, - /// Show measurement output - #[arg(short, long, default_value = "true")] - show_measurements: bool, + /// Suppress the measurement record + #[arg(short, long)] + quiet: bool, }, } @@ -50,16 +54,17 @@ fn main() -> Result<()> { match cli.command { Commands::Parse { file, format } => { - commands::parse(&file, &format)?; + commands::parse(&file, format)?; } - Commands::Dump { file, output } => { - commands::dump(&file, output.as_deref())?; - } - Commands::Run { + Commands::Dump { file, - show_measurements, + output, + force, } => { - commands::run(&file, show_measurements)?; + commands::dump(&file, output.as_deref(), force)?; + } + Commands::Run { file, quiet } => { + commands::run(&file, quiet)?; } } From 921eabe39ff2e6981f16b692d6c1703591f9f9d3 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 14:56:02 +0200 Subject: [PATCH 36/95] Add measurement format flag to run --- crates/ppvm-cli/src/commands.rs | 44 +++++++++++++++++++++++++++++---- crates/ppvm-cli/src/main.rs | 12 +++++++-- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index f997d5993..7d955dc69 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -1,4 +1,5 @@ use eyre::{Result, WrapErr}; +use ppvm_vihaco::measurements::{MeasurementOutcome, MeasurementResult}; use ppvm_vihaco::run_file; use std::path::Path; @@ -10,17 +11,50 @@ pub enum Format { Json, } -pub fn run(file: &str, quiet: bool) -> Result<()> { +/// Output format for the measurement record from `run`. +#[derive(Clone, Debug, clap::ValueEnum)] +pub enum MeasurementFormat { + /// One bit string per measurement event, space-separated; lost qubit = `L`. + Bits, + /// Raw debug representation of the record. + Debug, +} + +pub fn run(file: &str, quiet: bool, format: MeasurementFormat) -> Result<()> { let ppvm = run_file(file).wrap_err_with(|| format!("failed to run {file}"))?; if quiet { - println!("Successfully ran file {file}"); - } else { - let outcomes = ppvm.measurement_record(); - println!("Measurement record:\n{:?}", outcomes); + return Ok(()); + } + let record = ppvm.measurement_record(); + match format { + MeasurementFormat::Bits => println!("Measurements: {}", format_bits(&record)), + MeasurementFormat::Debug => println!("Measurement record:\n{:?}", record), } Ok(()) } +/// Render each measurement event as a bit string (lost qubit = `L`), events +/// space-separated. Empty record renders as `(none)`. +fn format_bits(record: &[MeasurementResult]) -> String { + if record.is_empty() { + return "(none)".to_string(); + } + record + .iter() + .map(|event| { + event + .iter() + .map(|outcome| match outcome { + MeasurementOutcome::Zero => '0', + MeasurementOutcome::One => '1', + MeasurementOutcome::Lost => 'L', + }) + .collect::() + }) + .collect::>() + .join(" ") +} + pub fn parse(file: &str, format: Format) -> Result<()> { let source = std::fs::read_to_string(file).wrap_err_with(|| format!("failed to read {file}"))?; diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index dad698edc..a0973e462 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -46,6 +46,10 @@ enum Commands { /// Suppress the measurement record #[arg(short, long)] quiet: bool, + + /// Measurement output format + #[arg(short, long, value_enum, default_value = "bits")] + format: commands::MeasurementFormat, }, } @@ -63,8 +67,12 @@ fn main() -> Result<()> { } => { commands::dump(&file, output.as_deref(), force)?; } - Commands::Run { file, quiet } => { - commands::run(&file, quiet)?; + Commands::Run { + file, + quiet, + format, + } => { + commands::run(&file, quiet, format)?; } } From 8364bd31dcb87506384d517ca2cb52db9294bae4 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 15:02:09 +0200 Subject: [PATCH 37/95] Add a bunch of tests --- crates/ppvm-cli/src/commands.rs | 145 ++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index 7d955dc69..9eb9334cb 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -111,3 +111,148 @@ pub fn dump(file: &str, output: Option<&str>, force: bool) -> Result<()> { eprintln!("Bytecode written to {output_file}"); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + 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 gate measure\n ret }\n"; + + fn row(outcomes: &[MeasurementOutcome]) -> MeasurementResult { + outcomes.iter().copied().collect() + } + + /// Write `contents` to a uniquely-named temp file and return its path. + fn temp_file(name: &str, contents: &str) -> String { + let path = std::env::temp_dir().join(name); + fs::write(&path, contents).unwrap(); + path.to_string_lossy().into_owned() + } + + // ─── format_bits ─────────────────────────────────────────────────── + + #[test] + fn format_bits_empty_record_is_none() { + assert_eq!(format_bits(&[]), "(none)"); + } + + #[test] + fn format_bits_concatenates_qubits_within_an_event() { + let record = vec![row(&[ + MeasurementOutcome::One, + MeasurementOutcome::Zero, + MeasurementOutcome::One, + ])]; + assert_eq!(format_bits(&record), "101"); + } + + #[test] + fn format_bits_separates_events_with_spaces() { + let record = vec![ + row(&[MeasurementOutcome::One]), + row(&[MeasurementOutcome::Zero]), + ]; + assert_eq!(format_bits(&record), "1 0"); + } + + #[test] + fn format_bits_renders_lost_qubit_as_l() { + let record = vec![ + row(&[MeasurementOutcome::One, MeasurementOutcome::Lost]), + row(&[MeasurementOutcome::Zero]), + ]; + assert_eq!(format_bits(&record), "1L 0"); + } + + // ─── run ─────────────────────────────────────────────────────────── + + #[test] + fn run_succeeds_on_valid_file() { + let src = temp_file("ppvm_cli_run_ok.sst", PROGRAM); + let res = run(&src, true, MeasurementFormat::Bits); + let _ = fs::remove_file(&src); + assert!(res.is_ok(), "got: {res:?}"); + } + + #[test] + fn run_errors_with_context_on_missing_file() { + let err = run("/no/such/file.sst", false, MeasurementFormat::Bits).unwrap_err(); + assert!(err.to_string().contains("failed to run"), "got: {err}"); + } + + // ─── parse ───────────────────────────────────────────────────────── + + #[test] + fn parse_succeeds_on_valid_file() { + let src = temp_file("ppvm_cli_parse_ok.sst", PROGRAM); + let res = parse(&src, Format::Debug); + let _ = fs::remove_file(&src); + assert!(res.is_ok(), "got: {res:?}"); + } + + #[test] + fn parse_errors_with_context_on_missing_file() { + let err = parse("/no/such/file.sst", Format::Pretty).unwrap_err(); + assert!(err.to_string().contains("failed to read"), "got: {err}"); + } + + // ─── dump ────────────────────────────────────────────────────────── + + #[test] + fn dump_writes_default_ssb_path_when_output_omitted() { + let src = temp_file("ppvm_cli_dump_default.sst", PROGRAM); + let expected = Path::new(&src).with_extension("ssb"); + let _ = fs::remove_file(&expected); // clear any leftover from a prior run + + dump(&src, None, false).unwrap(); + assert!(expected.exists(), "default .ssb should have been written"); + + let _ = fs::remove_file(&src); + let _ = fs::remove_file(&expected); + } + + #[test] + fn dump_writes_explicit_output_path() { + let src = temp_file("ppvm_cli_dump_explicit.sst", PROGRAM); + let out = std::env::temp_dir().join("ppvm_cli_dump_explicit_out.ssb"); + let _ = fs::remove_file(&out); + + dump(&src, Some(out.to_str().unwrap()), false).unwrap(); + assert!(out.exists()); + + let _ = fs::remove_file(&src); + let _ = fs::remove_file(&out); + } + + #[test] + fn dump_refuses_to_clobber_without_force() { + let src = temp_file("ppvm_cli_dump_clobber.sst", PROGRAM); + let out = std::env::temp_dir().join("ppvm_cli_dump_clobber_out.ssb"); + fs::write(&out, b"existing").unwrap(); + + let err = dump(&src, Some(out.to_str().unwrap()), false).unwrap_err(); + assert!(err.to_string().contains("already exists"), "got: {err}"); + // The existing file must be left untouched. + assert_eq!(fs::read(&out).unwrap(), b"existing"); + + let _ = fs::remove_file(&src); + let _ = fs::remove_file(&out); + } + + #[test] + fn dump_overwrites_existing_with_force() { + let src = temp_file("ppvm_cli_dump_force.sst", PROGRAM); + let out = std::env::temp_dir().join("ppvm_cli_dump_force_out.ssb"); + fs::write(&out, b"existing").unwrap(); + + dump(&src, Some(out.to_str().unwrap()), true).unwrap(); + // Replaced with real bytecode, not the placeholder. + assert_ne!(fs::read(&out).unwrap(), b"existing"); + + let _ = fs::remove_file(&src); + let _ = fs::remove_file(&out); + } +} From e7e00d85cf168e0d36190577cf4e1d428791b728 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 15:05:13 +0200 Subject: [PATCH 38/95] Remove lib.rs --- crates/ppvm-cli/src/lib.rs | 1 - crates/ppvm-cli/src/main.rs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 crates/ppvm-cli/src/lib.rs diff --git a/crates/ppvm-cli/src/lib.rs b/crates/ppvm-cli/src/lib.rs deleted file mode 100644 index 82b6da3c0..000000000 --- a/crates/ppvm-cli/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod commands; diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index a0973e462..6fb9b4ccf 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -1,6 +1,7 @@ use clap::{Parser, Subcommand}; use eyre::Result; -use ppvm_cli::commands; + +mod commands; #[derive(Parser)] #[command(name = "ppvm")] From 1ae6837c61d58d81e6d5b729fe29bfa841cce7e2 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 15:11:19 +0200 Subject: [PATCH 39/95] Add a short README and example --- crates/ppvm-cli/README.md | 63 ++++++++++++++++++++++++++++++++ crates/ppvm-cli/examples/ghz.sst | 27 ++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 crates/ppvm-cli/README.md create mode 100644 crates/ppvm-cli/examples/ghz.sst diff --git a/crates/ppvm-cli/README.md b/crates/ppvm-cli/README.md new file mode 100644 index 000000000..481915e0f --- /dev/null +++ b/crates/ppvm-cli/README.md @@ -0,0 +1,63 @@ +# ppvm-cli + +Command-line front-end for the Pauli-propagation virtual machine. Parses, +dumps, and runs `.sst` programs (and their compiled `.ssb` bytecode). + +## Install + +From this crate directory: + +```sh +cargo install --path crates/ppvm-cli +``` + +This builds the release binary and copies it to `~/.cargo/bin/ppvm`. As long as +`~/.cargo/bin` is on your `PATH`, you can then invoke `ppvm` from anywhere. + +During development you can skip the install and use `cargo run` instead — just +put CLI arguments after `--`: + +```sh +cargo run -p ppvm-cli -- run examples/ghz.sst +``` + +## Run + +`run` executes a program and prints its measurement record. The example +[`examples/ghz.sst`](examples/ghz.sst) prepares a 3-qubit GHZ state and measures +every qubit, so each shot reads `0 0 0` or `1 1 1`: + +```sh +$ ppvm run examples/ghz.sst +Measurements: 1 1 1 +``` + +Each measurement event is shown as a bit string (a lost qubit prints as `L`), +events separated by spaces. Use `-f debug` for the raw record, or `-q` to +suppress the output entirely: + +```sh +$ ppvm run examples/ghz.sst -f debug +Measurement record: +[[One], [One], [One]] +``` + +## Dump + +`dump` compiles a `.sst` program to `.ssb` bytecode. With no `-o`, it writes +next to the input (`ghz.sst` → `ghz.ssb`): + +```sh +$ ppvm dump examples/ghz.sst +Bytecode written to examples/ghz.ssb +``` + +`run` auto-detects the format from the file's contents, so the bytecode runs the +same way as the source: + +```sh +$ ppvm run examples/ghz.ssb +Measurements: 0 0 0 +``` + +`dump` refuses to overwrite an existing file unless you pass `-f`/`--force`. diff --git a/crates/ppvm-cli/examples/ghz.sst b/crates/ppvm-cli/examples/ghz.sst new file mode 100644 index 000000000..24690d55a --- /dev/null +++ b/crates/ppvm-cli/examples/ghz.sst @@ -0,0 +1,27 @@ +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 + gate h + + const.u64 0 + const.u64 1 + gate cnot + + const.u64 1 + const.u64 2 + gate cnot + + const.u64 0 + gate measure + + const.u64 1 + gate measure + + const.u64 2 + gate measure + + ret +} From b775169a1e69b42cc13443e4522f107d0a6d490a Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 16:07:34 +0200 Subject: [PATCH 40/95] Also advance on breakpoint --- crates/ppvm-vihaco/src/composite.rs | 78 +++++++++++++++++++++++++-- crates/ppvm-vihaco/src/instruction.rs | 2 +- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 4680cd08f..d0284b7ab 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -253,6 +253,17 @@ impl PPVM { self.continue_effects(effects) } + /// Program counter: the index of the next instruction to execute. + pub fn current_pc(&self) -> u32 { + self.loader.pc() + } + + /// The next instruction to execute, or `None` once execution has run off + /// the end of the code. Intended for debuggers/inspection. + pub fn current_instruction(&self) -> Option { + self.peek_instruction().ok().cloned() + } + fn execute_effects(&mut self, inst: Instruction) -> eyre::Result> { log::debug!("exec inst: {:?}, stack: {:?}", inst, self.cpu.stack()); match inst { @@ -268,7 +279,9 @@ impl PPVM { let outcome = vihaco::expect_exactly_one_effect( vihaco::GeneratedComponent::execute_generated(&mut self.cpu, cpu_inst, msg)?, )?; - if outcome == StepOutcome::Continue { + // Advance past a breakpoint as well, so the debugger that paused + // on it doesn't re-hit the same instruction on the next step. + if matches!(outcome, StepOutcome::Continue | StepOutcome::Breakpoint) { if let Some(target) = self.cpu.take_pending_pc() { *self.loader.pc_mut() = target; } else { @@ -352,11 +365,12 @@ impl PPVM { self.init()?; loop { - let action = self.step_once()?; - if action == StepOutcome::Continue { - continue; + // Breakpoints only pause the interactive debugger; a batch run + // skips straight past them. + match self.step_once()? { + StepOutcome::Continue | StepOutcome::Breakpoint => continue, + action => return Ok(action), } - return Ok(action); } } @@ -648,4 +662,58 @@ mod tests { "err: {err}" ); } + + // ─── Breakpoints ────────────────────────────────────────────────────── + + /// Bell circuit with a `breakpoint` between the two measurements. + const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 2;\n\ + fn @main() {\n\ + const.u64 0\n\ + gate h\n\ + const.u64 0\n\ + const.u64 1\n\ + gate cnot\n\ + const.u64 0\n\ + gate measure\n\ + breakpoint\n\ + const.u64 1\n\ + gate measure\n\ + ret\n\ + }\n"; + + #[test] + fn run_ignores_breakpoints() -> eyre::Result<()> { + // A batch run must execute straight through the breakpoint and record + // both measurements, exactly as if it weren't there. + let mut machine = PPVM::default(); + machine.run_program(BREAKPOINT_PROGRAM)?; + assert_eq!(machine.measurement_record().len(), 2); + Ok(()) + } + + #[test] + fn step_once_advances_past_breakpoint() -> eyre::Result<()> { + let mut machine = PPVM::default(); + machine.load_program(BREAKPOINT_PROGRAM)?; + machine.init()?; + + // Step until the breakpoint pauses us. + let mut outcome = StepOutcome::Continue; + for _ in 0..machine.loader.module.code.len() { + outcome = machine.step_once()?; + if outcome == StepOutcome::Breakpoint { + break; + } + } + assert_eq!(outcome, StepOutcome::Breakpoint, "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. + let next = machine.step_once()?; + assert_ne!(next, StepOutcome::Breakpoint, "must move past the breakpoint"); + assert!(machine.current_pc() > pc_at_break, "pc must advance"); + + Ok(()) + } } diff --git a/crates/ppvm-vihaco/src/instruction.rs b/crates/ppvm-vihaco/src/instruction.rs index 85f7179b6..417b65ca8 100644 --- a/crates/ppvm-vihaco/src/instruction.rs +++ b/crates/ppvm-vihaco/src/instruction.rs @@ -49,7 +49,7 @@ pub enum CircuitInstruction { // U3 U3, - // Measureme & Reset + // Measurement & Reset Measure, Reset, From d58cc48b1bdf746e76274aa2d109dbfd70042a5d Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 2 Jun 2026 16:29:28 +0200 Subject: [PATCH 41/95] Add debug command to step through breakpoints --- crates/ppvm-cli/README.md | 51 +++++++- crates/ppvm-cli/src/commands.rs | 181 ++++++++++++++++++++++++++++ crates/ppvm-cli/src/main.rs | 17 +++ crates/ppvm-vihaco/src/composite.rs | 6 +- 4 files changed, 253 insertions(+), 2 deletions(-) diff --git a/crates/ppvm-cli/README.md b/crates/ppvm-cli/README.md index 481915e0f..0bdf04e1a 100644 --- a/crates/ppvm-cli/README.md +++ b/crates/ppvm-cli/README.md @@ -1,7 +1,8 @@ # ppvm-cli Command-line front-end for the Pauli-propagation virtual machine. Parses, -dumps, and runs `.sst` programs (and their compiled `.ssb` bytecode). +dumps, runs, and steps through `.sst` programs (and their compiled `.ssb` +bytecode). ## Install @@ -61,3 +62,51 @@ Measurements: 0 0 0 ``` `dump` refuses to overwrite an existing file unless you pass `-f`/`--force`. + +## Debug + +`debug` steps through a program interactively. At each pause it prints the +program counter, the next instruction, and the measurements so far, then waits +for a command (type the letter and press Enter; a bare Enter steps): + +- `s` — step one instruction +- `c` — continue to the next breakpoint (or the end) +- `q` — quit + +By default it pauses at `breakpoint` instructions in the program. Add one +wherever you want execution to stop: + +``` +fn @main() { + const.u64 0 + gate h + breakpoint // execution pauses here + const.u64 0 + gate measure + ret +} +``` + +```sh +$ printf 's\nc\n' | ppvm debug program.sst +-- breakpoint hit -- +pc=3 next: const.u64 0 +measurements: (none) +> s step | c continue | q quit: pc=4 next: Measure +measurements: (none) +> s step | c continue | q quit: Program finished. +Measurements: 0 +``` + +To step through a program that has no breakpoints, pass `-b`/`--break-at-start` +to pause before the very first instruction: + +```sh +$ ppvm debug examples/ghz.sst -b +pc=0 next: const.u64 0 +measurements: (none) +> s step | c continue | q quit: +``` + +Batch `run` ignores `breakpoint` instructions entirely, so the same file still +runs straight through with `ppvm run`. diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index 9eb9334cb..200435ce3 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -1,6 +1,8 @@ use eyre::{Result, WrapErr}; +use ppvm_vihaco::composite::{PPVM, StepOutcome}; use ppvm_vihaco::measurements::{MeasurementOutcome, MeasurementResult}; use ppvm_vihaco::run_file; +use std::io::{BufRead, Write}; use std::path::Path; /// Output format for `parse`. @@ -112,6 +114,121 @@ pub fn dump(file: &str, output: Option<&str>, force: bool) -> Result<()> { Ok(()) } +/// A command entered at the debugger prompt. +enum DebugCommand { + Step, + Continue, + Quit, +} + +/// Step through a program interactively, pausing at `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<()> { + let stdin = std::io::stdin(); + let mut input = stdin.lock(); + let mut output = std::io::stdout(); + debug_loop(file, break_at_start, &mut input, &mut output) +} + +/// Core debugger loop, generic over its IO so it can be driven by tests. +fn debug_loop( + file: &str, + break_at_start: bool, + input: &mut impl BufRead, + output: &mut impl Write, +) -> Result<()> { + let mut machine = PPVM::default(); + machine + .load_file(file) + .wrap_err_with(|| format!("failed to load {file}"))?; + machine.init()?; + + let mut paused = break_at_start; + let mut ever_paused = paused; + + loop { + // Safety net: stop if execution has run off the end of the code. + if machine.current_instruction().is_none() { + writeln!(output, "Program counter past end of code.")?; + break; + } + + if paused { + print_location(&machine, output)?; + match read_command(input, output)? { + DebugCommand::Quit => { + writeln!(output, "Quit.")?; + return Ok(()); + } + DebugCommand::Continue => paused = false, + DebugCommand::Step => {} + } + } + + match machine.step_once()? { + StepOutcome::Continue => {} + StepOutcome::Breakpoint => { + paused = true; + ever_paused = true; + writeln!(output, "-- breakpoint hit --")?; + } + StepOutcome::Return | StepOutcome::Halt => { + writeln!(output, "Program finished.")?; + break; + } + } + } + + writeln!( + output, + "Measurements: {}", + format_bits(&machine.measurement_record()) + )?; + if !ever_paused { + writeln!( + output, + "(no breakpoint was hit; pass --break-at-start to step from the beginning)" + )?; + } + Ok(()) +} + +/// Print the program counter, the next instruction, and measurements so far. +fn print_location(machine: &PPVM, output: &mut impl Write) -> Result<()> { + let pc = machine.current_pc(); + match machine.current_instruction() { + Some(inst) => writeln!(output, "pc={pc} next: {inst}")?, + None => writeln!(output, "pc={pc} (end of code)")?, + } + writeln!( + output, + "measurements: {}", + format_bits(&machine.measurement_record()) + )?; + Ok(()) +} + +/// Prompt for and read a debugger command. A bare Enter steps; EOF quits. +fn read_command(input: &mut impl BufRead, output: &mut impl Write) -> Result { + loop { + write!(output, "> s step | c continue | q quit: ")?; + output.flush()?; + + let mut line = String::new(); + if input.read_line(&mut line)? == 0 { + // EOF (e.g. stdin closed): treat as quit so we never spin. + return Ok(DebugCommand::Quit); + } + match line.trim() { + "" | "s" | "step" => return Ok(DebugCommand::Step), + "c" | "continue" => return Ok(DebugCommand::Continue), + "q" | "quit" => return Ok(DebugCommand::Quit), + other => writeln!(output, "unknown command: {other:?}")?, + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -255,4 +372,68 @@ mod tests { let _ = fs::remove_file(&src); let _ = fs::remove_file(&out); } + + // ─── 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 gate measure\n ret }\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 { + let src = temp_file(name, program); + let mut input = script.as_bytes(); + let mut output: Vec = Vec::new(); + debug_loop(&src, break_at_start, &mut input, &mut output).unwrap(); + let _ = fs::remove_file(&src); + String::from_utf8(output).unwrap() + } + + #[test] + fn debug_break_at_start_steps_through_to_finish() { + // PROGRAM is const.u64 0 / gate measure / ret = 3 steps. + let out = run_debug(PROGRAM, "ppvm_cli_debug_step.sst", true, "s\ns\ns\n"); + assert!(out.contains("next: Measure"), "should display the gate: {out}"); + assert!(out.contains("Program finished."), "{out}"); + assert!(out.contains("Measurements: 0"), "q0 in |0> measures 0: {out}"); + } + + #[test] + fn debug_continue_runs_to_end() { + let out = run_debug(PROGRAM, "ppvm_cli_debug_continue.sst", true, "c\n"); + assert!(out.contains("Program finished."), "{out}"); + assert!(out.contains("Measurements: 0"), "{out}"); + } + + #[test] + fn debug_honors_authored_breakpoint() { + // Not breaking at start: must run until the `breakpoint` pauses it. + let out = run_debug( + BREAKPOINT_PROGRAM, + "ppvm_cli_debug_bp.sst", + false, + "c\n", + ); + assert!(out.contains("-- 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}"); + } + + #[test] + fn debug_quit_stops_before_finishing() { + let out = run_debug(PROGRAM, "ppvm_cli_debug_quit.sst", true, "q\n"); + assert!(out.contains("Quit."), "{out}"); + assert!(!out.contains("Program finished."), "{out}"); + assert!(!out.contains("Measurements:"), "quit prints no record: {out}"); + } + + #[test] + fn debug_without_breakpoint_prints_hint() { + // No breakpoint, no break-at-start, empty input: runs straight through + // and tells the user how to step. + let out = run_debug(PROGRAM, "ppvm_cli_debug_hint.sst", false, ""); + assert!(out.contains("Program finished."), "{out}"); + assert!(out.contains("no breakpoint was hit"), "{out}"); + } } diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index 6fb9b4ccf..6e7027f69 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -52,6 +52,17 @@ enum Commands { #[arg(short, long, value_enum, default_value = "bits")] format: commands::MeasurementFormat, }, + + /// Step through a program interactively, pausing at `breakpoint` instructions + Debug { + /// Input file (.sst source or .ssb bytecode) + #[arg(value_name = "FILE")] + file: String, + + /// Pause before the first instruction so any program can be stepped + #[arg(short, long)] + break_at_start: bool, + }, } fn main() -> Result<()> { @@ -75,6 +86,12 @@ fn main() -> Result<()> { } => { commands::run(&file, quiet, format)?; } + Commands::Debug { + file, + break_at_start, + } => { + commands::debug(&file, break_at_start)?; + } } Ok(()) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index d0284b7ab..be273a8c6 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -3,7 +3,11 @@ 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_cpu::{CPU, CPUMessage, StepOutcome}; +use vihaco_cpu::{CPU, CPUMessage}; + +/// Re-exported so consumers (e.g. the CLI debugger) can match on step results +/// without depending on `vihaco-cpu` directly. +pub use vihaco_cpu::StepOutcome; use crate::component::{Circuit, CircuitEffect}; use crate::instruction::CircuitInstruction; From bba00b8d9d9eb468bacac8184153412c8b0f92ab Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 10:26:53 +0200 Subject: [PATCH 42/95] Fix reset impl: also reset measurement records --- crates/ppvm-vihaco/src/composite.rs | 39 ++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index be273a8c6..38863f4e8 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -435,6 +435,7 @@ impl vihaco::Reset for PPVM { self.cpu.reset(); self.circuit.reset(); self.loader.pc = 0; + self.measurement_record.record.clear(); } } @@ -641,6 +642,38 @@ mod tests { Ok(()) } + #[test] + fn reset_clears_the_measurement_record() -> eyre::Result<()> { + use vihaco::Reset; + + let source = "device circuit.n_qubits 2;\n\ + fn @main() {\n\ + const.u64 0\n\ + gate h\n\ + const.u64 0\n\ + const.u64 1\n\ + gate cnot\n\ + const.u64 0\n\ + gate measure\n\ + const.u64 1\n\ + gate measure\n\ + ret\n\ + }\n"; + let mut machine = PPVM::default(); + machine.run_program(source)?; + assert_eq!(machine.measurement_record().len(), 2); + + // Resetting the machine must discard the recorded measurements, so a + // subsequent run does not see stale results leaking in from before. + machine.reset(); + assert!( + machine.measurement_record().is_empty(), + "reset must clear the measurement record" + ); + + Ok(()) + } + #[test] fn init_fails_when_n_qubits_undeclared() -> eyre::Result<()> { let source = "fn @main() { ret }\n"; @@ -715,7 +748,11 @@ mod tests { // Stepping again must make progress (advance the pc) rather than // re-hitting the same breakpoint instruction. let next = machine.step_once()?; - assert_ne!(next, StepOutcome::Breakpoint, "must move past the breakpoint"); + assert_ne!( + next, + StepOutcome::Breakpoint, + "must move past the breakpoint" + ); assert!(machine.current_pc() > pc_at_break, "pc must advance"); Ok(()) From 487e8532d49a7f5e91bb8392103fa051887422af Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 10:52:05 +0200 Subject: [PATCH 43/95] Skip intra-shot rayon parallelism inside shot-level parallelism When a shot runs on a rayon worker (shot-level parallelism), the tableau's per-gate coefficient parallelism would only oversubscribe the pool. Gate it on `current_thread_index().is_none()` so single-shot runs still parallelize. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/ppvm-tableau/src/data.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/ppvm-tableau/src/data.rs b/crates/ppvm-tableau/src/data.rs index 12ad3867d..2b40cbc54 100644 --- a/crates/ppvm-tableau/src/data.rs +++ b/crates/ppvm-tableau/src/data.rs @@ -400,7 +400,11 @@ where Complex: std::ops::Mul> + std::ops::AddAssign + From + Copy, { - if items.len() >= RAYON_COEFF_THRESHOLD { + // Skip intra-shot parallelism when we are already running inside a rayon + // worker (i.e. shot-level parallelism is driving this call): nesting would + // just oversubscribe the pool. Single-shot runs are on the main thread + // (`current_thread_index() == None`) and still parallelize as before. + if items.len() >= RAYON_COEFF_THRESHOLD && rayon::current_thread_index().is_none() { use rayon::prelude::*; // Parallel phase: compute all (branch_idx, branch_coeff, idx, nonbranch_coeff) tuples. @@ -502,7 +506,9 @@ where Complex: std::ops::Mul> + std::ops::AddAssign + From + Copy, { - if items.len() >= RAYON_COEFF_THRESHOLD { + // See `branch_coefficients_parallel`: avoid nesting rayon inside shot-level + // parallelism; the main-thread (single-shot) path is unaffected. + if items.len() >= RAYON_COEFF_THRESHOLD && rayon::current_thread_index().is_none() { use rayon::prelude::*; let pairs: Vec<(I, Complex)> = items From 0c4c7a836988c11ff381c897593072315d6ff655 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 10:59:10 +0200 Subject: [PATCH 44/95] Add multi-shot run API to ppvm-vihaco Adds run_shots/run_shots_serial/run_shots_parallel: compile a module once and run it for many shots on fresh PPVM instances. Parallel execution is gated behind a new `rayon` feature and a scoped thread pool, used only above a shot threshold; otherwise serial. Seeding is plumbed through Circuit::new_with_seed, PPVM::init_with_seed and a new run_with_seed (run() delegates to it). Per-shot seeds are derived as base + shot_index, so shots stay distinct and serial/parallel results are identical for a given seed. Also adds load_module_file for compile-once. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 1 + crates/ppvm-vihaco/Cargo.toml | 5 + crates/ppvm-vihaco/src/bytecode.rs | 7 +- crates/ppvm-vihaco/src/component.rs | 26 +++++ crates/ppvm-vihaco/src/composite.rs | 26 ++++- crates/ppvm-vihaco/src/lib.rs | 17 ++++ crates/ppvm-vihaco/src/shots.rs | 150 ++++++++++++++++++++++++++++ 7 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 crates/ppvm-vihaco/src/shots.rs diff --git a/Cargo.lock b/Cargo.lock index 36dcfa1b2..dd6ac03c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1190,6 +1190,7 @@ dependencies = [ "num", "ppvm-runtime", "ppvm-tableau", + "rayon", "smallvec", "vihaco", "vihaco-cpu", diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 61579ac5d..752aebe7b 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -3,6 +3,10 @@ name = "ppvm-vihaco" version = "0.1.0" edition = "2024" +[features] +# Enables shot-level parallelism in `run_shots_parallel`/`run_shots`. +rayon = ["dep:rayon"] + [dependencies] bitvec = "1.0.1" bnum = { version = "0.13.0", features = ["num-traits"] } @@ -10,6 +14,7 @@ chumsky = "0.10.0" eyre = "0.6.12" log = "0.4.29" num = "0.4.3" +rayon = { version = "1.10", optional = true } ppvm-runtime = { version = "0.1.0", path = "../ppvm-runtime" } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon"] } smallvec = "1.15.1" diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs index 77b945cc3..94493ce98 100644 --- a/crates/ppvm-vihaco/src/bytecode.rs +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -9,15 +9,13 @@ use std::io::{Read, Write}; use vihaco::instruction::{FromBytes, WriteBytes}; -use vihaco::{Type, Value, module::Module}; +use crate::PPVMModule; use crate::composite::{PPVM_MAGIC, PPVMDeviceInfo, PPVMInstruction}; /// Current `.ssb` format version. The reader rejects any other version. pub const PPVM_BYTECODE_VERSION: u16 = 1; -type PPVMModule = Module; - /// Byte length of the fixed v1 header (magic 4, version 2, header_size 4, /// n_qubits 4, coefficient_threshold 8) and the offset where the strings /// section begins. @@ -152,8 +150,7 @@ pub fn module_from_bytes(bytes: &[u8]) -> eyre::Result { /// little-endian `u32` — so a positive result here means [`read_module`] will /// accept the magic. A stream shorter than the magic is not bytecode. pub fn is_bytecode(bytes: &[u8]) -> bool { - bytes.len() >= 4 - && u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) == PPVM_MAGIC + bytes.len() >= 4 && u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) == PPVM_MAGIC } /// "Dump": compile `.sst` source straight to the `.ssb` byte stream. diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 41fb48e57..56c832565 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -245,6 +245,32 @@ impl Circuit { } } + /// Same as [`Circuit::new`], but seed the RNG deterministically so a shot + /// is reproducible. + pub fn new_with_seed(n_qubits: usize, coefficient_threshold: f64, seed: u64) -> Self { + macro_rules! seeded { + ($variant:ident) => {{ + let tab = GeneralizedTableau::new_with_seed(n_qubits, coefficient_threshold, seed); + Self::$variant(CircuitExecutor { tab }) + }}; + } + if n_qubits <= 64 { + seeded!(Bits64) + } else if n_qubits <= 128 { + seeded!(Bits128) + } else if n_qubits <= 256 { + seeded!(Bits256) + } else if n_qubits <= 512 { + seeded!(Bits512) + } else if n_qubits <= 1024 { + seeded!(Bits1024) + } else if n_qubits <= 2048 { + seeded!(Bits2048) + } else { + panic!("No matching executor for {} qubits", n_qubits); + } + } + fn execute( &mut self, inst: CircuitInstruction, diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 38863f4e8..5e8342e20 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -226,11 +226,24 @@ impl PPVM { } pub fn init(&mut self) -> eyre::Result<()> { + self.init_inner(None) + } + + /// Like [`PPVM::init`], but seed the circuit RNG deterministically so the + /// run is reproducible. + pub fn init_with_seed(&mut self, seed: u64) -> eyre::Result<()> { + self.init_inner(Some(seed)) + } + + fn init_inner(&mut self, seed: Option) -> eyre::Result<()> { let info = &self.loader.module.extra; if info.n_qubits == 0 { return Err(eyre::eyre!("device circuit.n_qubits must be declared")); } - self.circuit = Circuit::new(info.n_qubits, info.coefficient_threshold); + self.circuit = match seed { + Some(seed) => Circuit::new_with_seed(info.n_qubits, info.coefficient_threshold, seed), + None => Circuit::new(info.n_qubits, info.coefficient_threshold), + }; // push entry frame self.cpu.push_frame(Frame { @@ -366,7 +379,16 @@ impl PPVM { } pub fn run(&mut self) -> eyre::Result { - self.init()?; + self.run_with_seed(None) + } + + /// Like [`PPVM::run`], but seed the circuit RNG deterministically when + /// `seed` is `Some`, making the run reproducible. + pub fn run_with_seed(&mut self, seed: Option) -> eyre::Result { + match seed { + Some(seed) => self.init_with_seed(seed)?, + None => self.init()?, + } loop { // Breakpoints only pause the interactive debugger; a batch run diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index bd7f591f1..85a57a574 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -4,6 +4,7 @@ pub mod composite; pub mod instruction; pub mod measurements; pub mod message; +pub mod shots; mod syntax; use chumsky::Parser; @@ -14,6 +15,22 @@ use vihaco_parser_core::Parse; use crate::composite::{PPVM, PPVMDeviceInfo, PPVMInstruction}; use crate::syntax::{PPVMHeader, PPVMResolver}; +/// A fully resolved PPVM module, ready to load into a [`PPVM`]. +pub type PPVMModule = Module; + +/// Read a file and produce a loadable module, auto-detecting the format: a +/// leading PPVM magic is parsed as `.ssb` bytecode, otherwise as `.sst` source. +/// Mirrors [`PPVM::load_file`] but returns the module so it can be compiled once +/// and run for many shots. +pub fn load_module_file(path: &str) -> eyre::Result { + let bytes = std::fs::read(path)?; + if bytecode::is_bytecode(&bytes) { + bytecode::module_from_bytes(&bytes) + } else { + compile_program(std::str::from_utf8(&bytes)?) + } +} + pub fn run_file(path: &str) -> eyre::Result { let mut machine = PPVM::default(); machine.run_file(path)?; diff --git a/crates/ppvm-vihaco/src/shots.rs b/crates/ppvm-vihaco/src/shots.rs new file mode 100644 index 000000000..59eef0cf3 --- /dev/null +++ b/crates/ppvm-vihaco/src/shots.rs @@ -0,0 +1,150 @@ +//! Running a compiled program for many shots, optionally across threads. +//! +//! Each shot runs on a fresh [`PPVM`] so shots are fully independent; the +//! module is compiled once and shared. With the `rayon` feature, [`run_shots`] +//! parallelizes across shots when asked for more than one thread and there are +//! enough shots to amortize the overhead. + +use crate::PPVMModule; +use crate::composite::PPVM; +use crate::measurements::MeasurementResult; + +/// Below this many shots, parallelism's overhead outweighs its benefit and we +/// always run serially. Provisional — tune with benchmarks. +pub const PARALLEL_SHOT_THRESHOLD: usize = 128; + +/// Per-shot seed derived from the base seed and the shot index, so every shot +/// gets a distinct RNG stream (a shared seed would make all shots identical). +/// Depends only on `(base, index)`, so serial and parallel runs are bit-for-bit +/// identical for a given seed regardless of thread count. +fn shot_seed(base: Option, index: usize) -> Option { + base.map(|b| b.wrapping_add(index as u64)) +} + +/// Run a single shot on a fresh machine and return its measurement record. +fn run_one_shot(module: &PPVMModule, seed: Option) -> eyre::Result> { + let mut machine = PPVM::default(); + machine.load(module)?; + machine.run_with_seed(seed)?; + Ok(machine.measurement_record()) +} + +/// Run `shots` shots serially. One entry per shot, in order. +pub fn run_shots_serial( + module: &PPVMModule, + shots: usize, + seed: Option, +) -> eyre::Result>> { + (0..shots) + .map(|i| run_one_shot(module, shot_seed(seed, i))) + .collect() +} + +/// Run `shots` shots across a scoped rayon pool of `threads` threads. One entry +/// per shot, in order (preserved by the indexed parallel iterator). +#[cfg(feature = "rayon")] +pub fn run_shots_parallel( + module: &PPVMModule, + shots: usize, + threads: usize, + seed: Option, +) -> eyre::Result>> { + use rayon::prelude::*; + + let pool = rayon::ThreadPoolBuilder::new() + .num_threads(threads) + .build()?; + pool.install(|| { + (0..shots) + .into_par_iter() + .map(|i| run_one_shot(module, shot_seed(seed, i))) + .collect() + }) +} + +/// Run `shots` shots, choosing serial or parallel execution. Goes parallel only +/// when built with `rayon`, more than one thread is requested, and there are +/// enough shots to be worth it; otherwise runs serially. +pub fn run_shots( + module: &PPVMModule, + shots: usize, + threads: usize, + seed: Option, +) -> eyre::Result>> { + #[cfg(feature = "rayon")] + if threads > 1 && shots >= PARALLEL_SHOT_THRESHOLD { + return run_shots_parallel(module, shots, threads, seed); + } + #[cfg(not(feature = "rayon"))] + let _ = threads; + + run_shots_serial(module, shots, seed) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::compile_program; + use crate::measurements::MeasurementOutcome; + + /// Measures q0 in |0>: every shot is deterministically `0`. + const DETERMINISTIC: &str = + "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n gate measure\n ret }\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 gate h\n const.u64 0\n gate measure\n ret }\n"; + + fn module(src: &str) -> PPVMModule { + compile_program(src).unwrap() + } + + #[test] + fn serial_runs_one_record_per_shot() { + let m = module(DETERMINISTIC); + let records = run_shots_serial(&m, 5, None).unwrap(); + assert_eq!(records.len(), 5); + for shot in &records { + // One measurement event, one qubit, deterministically |0>. + assert_eq!(shot.len(), 1); + assert_eq!(shot[0].as_slice(), [MeasurementOutcome::Zero]); + } + } + + #[test] + fn dispatcher_runs_all_shots() { + let m = module(DETERMINISTIC); + // threads = 1 forces the serial path regardless of the rayon feature. + let records = run_shots(&m, 10, 1, None).unwrap(); + assert_eq!(records.len(), 10); + } + + #[test] + fn same_seed_is_reproducible() { + let m = module(RANDOM); + let a = run_shots_serial(&m, 20, Some(42)).unwrap(); + let b = run_shots_serial(&m, 20, Some(42)).unwrap(); + assert_eq!(a, b); + } + + #[test] + fn per_shot_seeds_differ() { + // If every shot shared one seed, all 20 outcomes would be identical. + // Distinct per-shot seeds must produce a mix of 0s and 1s. + let m = module(RANDOM); + let records = run_shots_serial(&m, 20, Some(42)).unwrap(); + let first = &records[0]; + assert!( + records.iter().any(|r| r != first), + "expected varied outcomes across shots, got {records:?}" + ); + } + + #[cfg(feature = "rayon")] + #[test] + fn serial_and_parallel_match_for_same_seed() { + let m = module(RANDOM); + let serial = run_shots_serial(&m, 64, Some(7)).unwrap(); + let parallel = run_shots_parallel(&m, 64, 4, Some(7)).unwrap(); + assert_eq!(serial, parallel); + } +} From dd7147ca7e4a6b6ea8c6dd7056701bc35c34fe32 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 11:08:10 +0200 Subject: [PATCH 45/95] Run multiple shots from the CLI with optional threads, seed, and output file `ppvm run` now takes --shots, --threads, --seed and --output: it compiles the module once and runs it for many shots via run_shots, going parallel when more than one thread is requested. Output is one flat bit string per shot (loss = 2), shots newline-separated, written to a file with -o or to stdout otherwise. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/ppvm-cli/Cargo.toml | 2 +- crates/ppvm-cli/src/commands.rs | 170 ++++++++++++++++++++++---------- crates/ppvm-cli/src/main.rs | 30 +++++- 3 files changed, 147 insertions(+), 55 deletions(-) diff --git a/crates/ppvm-cli/Cargo.toml b/crates/ppvm-cli/Cargo.toml index bb13afd7e..4ba3d8841 100644 --- a/crates/ppvm-cli/Cargo.toml +++ b/crates/ppvm-cli/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] clap = { version = "4.6.1", features = ["derive"] } eyre = "0.6.12" -ppvm-vihaco = { version = "0.1.0", path = "../ppvm-vihaco" } +ppvm-vihaco = { version = "0.1.0", path = "../ppvm-vihaco", features = ["rayon"] } [[bin]] name = "ppvm" diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index 200435ce3..93ea6a248 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -1,7 +1,6 @@ use eyre::{Result, WrapErr}; use ppvm_vihaco::composite::{PPVM, StepOutcome}; -use ppvm_vihaco::measurements::{MeasurementOutcome, MeasurementResult}; -use ppvm_vihaco::run_file; +use ppvm_vihaco::measurements::MeasurementResult; use std::io::{BufRead, Write}; use std::path::Path; @@ -16,45 +15,64 @@ pub enum Format { /// Output format for the measurement record from `run`. #[derive(Clone, Debug, clap::ValueEnum)] pub enum MeasurementFormat { - /// One bit string per measurement event, space-separated; lost qubit = `L`. + /// One flat bit string per shot: `0`/`1`, lost qubit = `2`. Bits, - /// Raw debug representation of the record. + /// Raw debug representation of all shots. Debug, } -pub fn run(file: &str, quiet: bool, format: MeasurementFormat) -> Result<()> { - let ppvm = run_file(file).wrap_err_with(|| format!("failed to run {file}"))?; +pub fn run( + file: &str, + shots: usize, + threads: usize, + seed: Option, + output: Option<&str>, + quiet: bool, + format: MeasurementFormat, +) -> Result<()> { + // Compile once, then run every shot against the shared module. + let module = + ppvm_vihaco::load_module_file(file).wrap_err_with(|| format!("failed to load {file}"))?; + let records = ppvm_vihaco::shots::run_shots(&module, shots, threads, seed) + .wrap_err_with(|| format!("failed to run {file}"))?; if quiet { return Ok(()); } - let record = ppvm.measurement_record(); - match format { - MeasurementFormat::Bits => println!("Measurements: {}", format_bits(&record)), - MeasurementFormat::Debug => println!("Measurement record:\n{:?}", record), + + let text = match format { + MeasurementFormat::Bits => format_shots(&records), + MeasurementFormat::Debug => format!("{records:?}"), + }; + + match output { + Some(path) => { + std::fs::write(path, format!("{text}\n")) + .wrap_err_with(|| format!("failed to write {path}"))?; + eprintln!("Results written to {path}"); + } + None => println!("{text}"), } Ok(()) } -/// Render each measurement event as a bit string (lost qubit = `L`), events -/// space-separated. Empty record renders as `(none)`. -fn format_bits(record: &[MeasurementResult]) -> String { - if record.is_empty() { - return "(none)".to_string(); - } - record +/// Render one shot per line, each as a flat bit string. +fn format_shots(records: &[Vec]) -> String { + records .iter() - .map(|event| { - event - .iter() - .map(|outcome| match outcome { - MeasurementOutcome::Zero => '0', - MeasurementOutcome::One => '1', - MeasurementOutcome::Lost => 'L', - }) - .collect::() - }) + .map(|shot| format_shot(shot)) .collect::>() - .join(" ") + .join("\n") +} + +/// Render a shot's full measurement record as one flat bit string, all events +/// and qubits concatenated: `Zero` → `0`, `One` → `1`, `Lost` → `2` (the +/// outcome's own enum value). An empty record renders as the empty string. +fn format_shot(record: &[MeasurementResult]) -> String { + record + .iter() + .flatten() + .map(|outcome| char::from(b'0' + *outcome as u8)) + .collect() } pub fn parse(file: &str, format: Format) -> Result<()> { @@ -183,7 +201,7 @@ fn debug_loop( writeln!( output, "Measurements: {}", - format_bits(&machine.measurement_record()) + format_shot(&machine.measurement_record()) )?; if !ever_paused { writeln!( @@ -204,7 +222,7 @@ fn print_location(machine: &PPVM, output: &mut impl Write) -> Result<()> { writeln!( output, "measurements: {}", - format_bits(&machine.measurement_record()) + format_shot(&machine.measurement_record()) )?; Ok(()) } @@ -232,6 +250,7 @@ fn read_command(input: &mut impl BufRead, output: &mut impl Write) -> Result (deterministic). @@ -249,39 +268,48 @@ mod tests { path.to_string_lossy().into_owned() } - // ─── format_bits ─────────────────────────────────────────────────── + // ─── format_shot ───────────────────────────────────────────────────── #[test] - fn format_bits_empty_record_is_none() { - assert_eq!(format_bits(&[]), "(none)"); + fn format_shot_empty_record_is_empty() { + assert_eq!(format_shot(&[]), ""); } #[test] - fn format_bits_concatenates_qubits_within_an_event() { + fn format_shot_concatenates_qubits_within_an_event() { let record = vec![row(&[ MeasurementOutcome::One, MeasurementOutcome::Zero, MeasurementOutcome::One, ])]; - assert_eq!(format_bits(&record), "101"); + assert_eq!(format_shot(&record), "101"); } #[test] - fn format_bits_separates_events_with_spaces() { + fn format_shot_flattens_events_with_no_separator() { let record = vec![ row(&[MeasurementOutcome::One]), row(&[MeasurementOutcome::Zero]), ]; - assert_eq!(format_bits(&record), "1 0"); + assert_eq!(format_shot(&record), "10"); } #[test] - fn format_bits_renders_lost_qubit_as_l() { + fn format_shot_renders_lost_qubit_as_two() { let record = vec![ row(&[MeasurementOutcome::One, MeasurementOutcome::Lost]), row(&[MeasurementOutcome::Zero]), ]; - assert_eq!(format_bits(&record), "1L 0"); + assert_eq!(format_shot(&record), "120"); + } + + #[test] + fn format_shots_joins_shots_with_newlines() { + let shots = vec![ + vec![row(&[MeasurementOutcome::One])], + vec![row(&[MeasurementOutcome::Zero])], + ]; + assert_eq!(format_shots(&shots), "1\n0"); } // ─── run ─────────────────────────────────────────────────────────── @@ -289,15 +317,48 @@ mod tests { #[test] fn run_succeeds_on_valid_file() { let src = temp_file("ppvm_cli_run_ok.sst", PROGRAM); - let res = run(&src, true, MeasurementFormat::Bits); + let res = run(&src, 3, 1, None, None, true, MeasurementFormat::Bits); let _ = fs::remove_file(&src); assert!(res.is_ok(), "got: {res:?}"); } + #[test] + fn run_writes_one_line_per_shot_to_output_file() { + let src = temp_file("ppvm_cli_run_output.sst", PROGRAM); + let out = std::env::temp_dir().join("ppvm_cli_run_output.txt"); + let _ = fs::remove_file(&out); + + run( + &src, + 4, + 1, + None, + out.to_str(), + false, + MeasurementFormat::Bits, + ) + .unwrap(); + let contents = fs::read_to_string(&out).unwrap(); + // Four deterministic shots of |0>, one per line. + assert_eq!(contents, "0\n0\n0\n0\n"); + + let _ = fs::remove_file(&src); + let _ = fs::remove_file(&out); + } + #[test] fn run_errors_with_context_on_missing_file() { - let err = run("/no/such/file.sst", false, MeasurementFormat::Bits).unwrap_err(); - assert!(err.to_string().contains("failed to run"), "got: {err}"); + let err = run( + "/no/such/file.sst", + 1, + 1, + None, + None, + false, + MeasurementFormat::Bits, + ) + .unwrap_err(); + assert!(err.to_string().contains("failed to load"), "got: {err}"); } // ─── parse ───────────────────────────────────────────────────────── @@ -376,8 +437,7 @@ 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 gate measure\n ret }\n"; + const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 1;\nfn @main() { breakpoint\n const.u64 0\n gate measure\n ret }\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 { @@ -393,9 +453,15 @@ mod tests { fn debug_break_at_start_steps_through_to_finish() { // PROGRAM is const.u64 0 / gate measure / ret = 3 steps. let out = run_debug(PROGRAM, "ppvm_cli_debug_step.sst", true, "s\ns\ns\n"); - assert!(out.contains("next: Measure"), "should display the gate: {out}"); + assert!( + out.contains("next: Measure"), + "should display the gate: {out}" + ); assert!(out.contains("Program finished."), "{out}"); - assert!(out.contains("Measurements: 0"), "q0 in |0> measures 0: {out}"); + assert!( + out.contains("Measurements: 0"), + "q0 in |0> measures 0: {out}" + ); } #[test] @@ -408,12 +474,7 @@ mod tests { #[test] fn debug_honors_authored_breakpoint() { // Not breaking at start: must run until the `breakpoint` pauses it. - let out = run_debug( - BREAKPOINT_PROGRAM, - "ppvm_cli_debug_bp.sst", - false, - "c\n", - ); + let out = run_debug(BREAKPOINT_PROGRAM, "ppvm_cli_debug_bp.sst", false, "c\n"); assert!(out.contains("-- breakpoint hit --"), "{out}"); assert!(out.contains("Program finished."), "{out}"); // A breakpoint was hit, so no "use --break-at-start" hint. @@ -425,7 +486,10 @@ mod tests { let out = run_debug(PROGRAM, "ppvm_cli_debug_quit.sst", true, "q\n"); assert!(out.contains("Quit."), "{out}"); assert!(!out.contains("Program finished."), "{out}"); - assert!(!out.contains("Measurements:"), "quit prints no record: {out}"); + assert!( + !out.contains("Measurements:"), + "quit prints no record: {out}" + ); } #[test] diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index 6e7027f69..4c2e2372c 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -44,6 +44,22 @@ enum Commands { #[arg(value_name = "FILE")] file: String, + /// Number of shots to run + #[arg(short, long, default_value = "1")] + shots: usize, + + /// Number of threads (> 1 enables parallel execution) + #[arg(short, long, default_value = "1")] + threads: usize, + + /// Seed the RNG for reproducible results + #[arg(long)] + seed: Option, + + /// Write results to a file instead of stdout + #[arg(short, long)] + output: Option, + /// Suppress the measurement record #[arg(short, long)] quiet: bool, @@ -81,10 +97,22 @@ fn main() -> Result<()> { } Commands::Run { file, + shots, + threads, + seed, + output, quiet, format, } => { - commands::run(&file, quiet, format)?; + commands::run( + &file, + shots, + threads, + seed, + output.as_deref(), + quiet, + format, + )?; } Commands::Debug { file, From 2aa534cf759aed339ad2a3b85100d5ad15d94fae Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 11:13:47 +0200 Subject: [PATCH 46/95] Update ppvm-cli README for multi-shot run Document --shots/--threads/--seed/--output and the flat bit-string output (loss = 2, one shot per line). Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/ppvm-cli/README.md | 50 ++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/crates/ppvm-cli/README.md b/crates/ppvm-cli/README.md index 0bdf04e1a..d4b56d391 100644 --- a/crates/ppvm-cli/README.md +++ b/crates/ppvm-cli/README.md @@ -24,23 +24,45 @@ cargo run -p ppvm-cli -- run examples/ghz.sst ## Run -`run` executes a program and prints its measurement record. The example -[`examples/ghz.sst`](examples/ghz.sst) prepares a 3-qubit GHZ state and measures -every qubit, so each shot reads `0 0 0` or `1 1 1`: +`run` executes a program for one or more shots and prints the measurement +results. The example [`examples/ghz.sst`](examples/ghz.sst) prepares a 3-qubit +GHZ state and measures every qubit, so each shot reads `000` or `111`: ```sh $ ppvm run examples/ghz.sst -Measurements: 1 1 1 +000 ``` -Each measurement event is shown as a bit string (a lost qubit prints as `L`), -events separated by spaces. Use `-f debug` for the raw record, or `-q` to -suppress the output entirely: +Each shot is printed as a single flat bit string — `0`/`1`, with a lost qubit +shown as `2` — and shots are separated by newlines. Use `-s`/`--shots` to run +more than one: ```sh -$ ppvm run examples/ghz.sst -f debug -Measurement record: -[[One], [One], [One]] +$ ppvm run examples/ghz.sst --shots 5 +000 +000 +111 +000 +111 +``` + +Other options: + +- `-t`/`--threads ` — run shots across `N` threads. More than one enables + parallel execution (defaults to 1). +- `--seed ` — seed the RNG for reproducible results. The same seed yields the + same shots regardless of the thread count. +- `-o`/`--output ` — write the results to a file (one shot per line) + instead of stdout. +- `-f debug` — print the raw record for every shot instead of bit strings. +- `-q`/`--quiet` — run without printing anything. + +```sh +$ ppvm run examples/ghz.sst --shots 2 -f debug +[[[Zero], [Zero], [Zero]], [[One], [One], [One]]] + +$ ppvm run examples/ghz.sst --shots 1000 --threads 8 -o results.txt +Results written to results.txt ``` ## Dump @@ -58,7 +80,7 @@ same way as the source: ```sh $ ppvm run examples/ghz.ssb -Measurements: 0 0 0 +000 ``` `dump` refuses to overwrite an existing file unless you pass `-f`/`--force`. @@ -91,9 +113,9 @@ fn @main() { $ printf 's\nc\n' | ppvm debug program.sst -- breakpoint hit -- pc=3 next: const.u64 0 -measurements: (none) +measurements: > s step | c continue | q quit: pc=4 next: Measure -measurements: (none) +measurements: > s step | c continue | q quit: Program finished. Measurements: 0 ``` @@ -104,7 +126,7 @@ to pause before the very first instruction: ```sh $ ppvm debug examples/ghz.sst -b pc=0 next: const.u64 0 -measurements: (none) +measurements: > s step | c continue | q quit: ``` From a2a63f57c2d87d7a1082857640c0a7932170b9e7 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 11:18:07 +0200 Subject: [PATCH 47/95] Add missing resolve for two-qubit pauli error --- crates/ppvm-vihaco/src/composite.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 5e8342e20..b45cf542c 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -204,7 +204,13 @@ impl PPVM { Ok(CircuitMessage::TwoQubitAndFloatArr3(q0, q1, [p0, p1, p2])) } TwoQubitPauliError => { - todo!() + let mut ps = [0.0; 15]; + for p in ps.iter_mut() { + *p = self.pop_f64()?; + } + let q0 = self.pop_qubit()?; + let q1 = self.pop_qubit()?; + Ok(CircuitMessage::TwoQubitAndFloatArr15(q0, q1, ps)) } } } From e6bc2be787d32edf6a5a27eb3ab1160a69a59344 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 11:18:57 +0200 Subject: [PATCH 48/95] Fix pop order --- crates/ppvm-vihaco/src/composite.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index b45cf542c..0b33a9e28 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -205,7 +205,7 @@ impl PPVM { } TwoQubitPauliError => { let mut ps = [0.0; 15]; - for p in ps.iter_mut() { + for p in ps.iter_mut().rev() { *p = self.pop_f64()?; } let q0 = self.pop_qubit()?; From ec98a4cf165533a3972ba3440c8bca9dff77cb1a Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 11:42:35 +0200 Subject: [PATCH 49/95] Add XY rotation gate --- crates/ppvm-runtime/src/traits/branch/mod.rs | 2 +- crates/ppvm-runtime/src/traits/branch/rot1.rs | 18 ++++++ crates/ppvm-runtime/src/traits/mod.rs | 2 +- crates/ppvm-tableau/src/gates/rot1.rs | 56 +++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/crates/ppvm-runtime/src/traits/branch/mod.rs b/crates/ppvm-runtime/src/traits/branch/mod.rs index 0c50ddd43..774d6b38c 100644 --- a/crates/ppvm-runtime/src/traits/branch/mod.rs +++ b/crates/ppvm-runtime/src/traits/branch/mod.rs @@ -10,7 +10,7 @@ mod u3; pub use crx::CRx; pub use proj::Projection; -pub use rot1::RotationOne; +pub use rot1::{RotXY, RotationOne}; pub use rot2::RotationTwo; pub use tgate::TGate; pub use u3::U3Gate; diff --git a/crates/ppvm-runtime/src/traits/branch/rot1.rs b/crates/ppvm-runtime/src/traits/branch/rot1.rs index 266ae6c0d..837fd16b3 100644 --- a/crates/ppvm-runtime/src/traits/branch/rot1.rs +++ b/crates/ppvm-runtime/src/traits/branch/rot1.rs @@ -21,3 +21,21 @@ pub trait RotationOne { self.rotate_1(Pauli::Z, addr0, theta.into()) } } + +/// Rotation about an axis in the x/y plane: +/// `exp(-i θ/2 · (cos(axis_angle)·X + sin(axis_angle)·Y))`. +pub trait RotXY { + /// `R(axis_angle, θ)` on qubit `addr0`. + fn r(&mut self, addr0: usize, axis_angle: T::Coeff, theta: T::Coeff); +} + +impl> RotXY for S { + fn r(&mut self, addr0: usize, axis_angle: ::Coeff, theta: ::Coeff) { + // R(axis_angle, θ) = RZ(axis_angle)·RX(θ)·RZ(−axis_angle), since the + // in-plane axis is X rotated about Z by `axis_angle`. Applied in + // forward (Schrödinger) order, the rightmost factor goes first. + self.rz(addr0, -axis_angle.clone()); + self.rx(addr0, theta); + self.rz(addr0, axis_angle); + } +} diff --git a/crates/ppvm-runtime/src/traits/mod.rs b/crates/ppvm-runtime/src/traits/mod.rs index 853e435e8..61ce29913 100644 --- a/crates/ppvm-runtime/src/traits/mod.rs +++ b/crates/ppvm-runtime/src/traits/mod.rs @@ -14,7 +14,7 @@ mod strategy; mod trace; mod word_trait; -pub use branch::{CRx, Projection, RotationOne, RotationTwo, TGate, U3Gate}; +pub use branch::{CRx, Projection, RotXY, RotationOne, RotationTwo, TGate, U3Gate}; pub use clifford::{Clifford, CliffordExtensions}; pub use coefficient::{Coefficient, ComplexCoefficient}; pub use map::{ diff --git a/crates/ppvm-tableau/src/gates/rot1.rs b/crates/ppvm-tableau/src/gates/rot1.rs index e21bf754d..e3a93c844 100644 --- a/crates/ppvm-tableau/src/gates/rot1.rs +++ b/crates/ppvm-tableau/src/gates/rot1.rs @@ -112,6 +112,62 @@ mod tests { assert!(!tab.measure(0).unwrap()); } + /// R(axis_angle=0, θ=0) = identity: |0⟩ stays |0⟩, no branching. + #[test] + fn test_r_identity() { + let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12); + tab.r(0, 0.0, 0.0); + assert_eq!(tab.coefficients.len(), 1); + assert!(!tab.measure(0).unwrap()); + } + + /// R(axis_angle=0, θ=π) = RX(π): flips |0⟩ → |1⟩ with no branching. + #[test] + fn test_r_axis_zero_is_rx() { + let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12); + tab.r(0, 0.0, PI); + assert_eq!(tab.coefficients.len(), 1); + assert!(tab.measure(0).unwrap()); + } + + /// R(axis_angle=π/2, θ=π) = RY(π): flips |0⟩ → |1⟩ with no branching. + #[test] + fn test_r_axis_half_pi_is_ry() { + let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12); + tab.r(0, FRAC_PI_2, PI); + assert_eq!(tab.coefficients.len(), 1); + assert!(tab.measure(0).unwrap()); + } + + /// A partial rotation about an in-plane axis branches into two terms. + #[test] + fn test_r_branches() { + let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12); + tab.r(0, 0.37 * PI, FRAC_PI_2); + assert_eq!(tab.coefficients.len(), 2); + } + + /// R(axis_angle, θ) must give identical per-seed measurement statistics + /// to the manual decomposition RZ(axis_angle)·RX(θ)·RZ(−axis_angle). + #[test] + fn test_r_matches_rz_rx_rz() { + let (axis_angle, theta) = (0.21 * PI, 0.34 * PI); + + let mut tab_r: TestTableau = GeneralizedTableau::new_with_seed(1, 1e-12, 0); + tab_r.r(0, axis_angle, theta); + + let mut tab_manual: TestTableau = GeneralizedTableau::new_with_seed(1, 1e-12, 0); + tab_manual.rz(0, -axis_angle); + tab_manual.rx(0, theta); + tab_manual.rz(0, axis_angle); + + for seed in 0..200 { + let result_r = tab_r.fork(Some(seed)).measure(0).unwrap(); + let result_manual = tab_manual.fork(Some(seed)).measure(0).unwrap(); + assert_eq!(result_r, result_manual, "mismatch at seed {}", seed); + } + } + #[test] fn test_two_qubit_case() { let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-10); From f80dd7401600783a18c79cfef9a767731db53efd Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 12:25:46 +0200 Subject: [PATCH 50/95] Add R gate --- .../ppvm-python-native/ppvm_python_native.pyi | 2 + crates/ppvm-python-native/src/interface.rs | 5 +++ .../src/interface_tableau.rs | 4 ++ crates/ppvm-runtime/src/sum/rot1.rs | 40 +++++++++++++++++++ crates/ppvm-runtime/src/traits/branch/rot1.rs | 16 ++------ crates/ppvm-tableau/src/gates/rot1.rs | 14 +++++++ ppvm-python/src/ppvm/mixins.py | 16 ++++++++ .../test/generalized_tableau/test_basics.py | 14 +++++++ ppvm-python/test/test_basics.py | 10 +++++ 9 files changed, 109 insertions(+), 12 deletions(-) diff --git a/crates/ppvm-python-native/ppvm_python_native.pyi b/crates/ppvm-python-native/ppvm_python_native.pyi index 0fab903ac..20ebe6e38 100644 --- a/crates/ppvm-python-native/ppvm_python_native.pyi +++ b/crates/ppvm-python-native/ppvm_python_native.pyi @@ -34,6 +34,7 @@ class _PauliSumBase: def rx(self, addr0: int, theta: float) -> None: ... def ry(self, addr0: int, theta: float) -> None: ... def rz(self, addr0: int, theta: float) -> None: ... + def r(self, addr0: int, axis_angle: float, theta: float) -> None: ... def rxx(self, addr0: int, addr1: int, theta: float) -> None: ... def ryy(self, addr0: int, addr1: int, theta: float) -> None: ... def rzz(self, addr0: int, addr1: int, theta: float) -> None: ... @@ -123,6 +124,7 @@ class _GeneralizedTableauBase: def ry(self, addr0: int, theta: float) -> None: ... def rz(self, addr0: int, theta: float) -> None: ... def u3(self, addr0: int, theta: float, phi: float, lam: float) -> None: ... + def r(self, addr0: int, axis_angle: float, theta: float) -> None: ... def rxx(self, addr0: int, addr1: int, theta: float) -> None: ... def ryy(self, addr0: int, addr1: int, theta: float) -> None: ... def rzz(self, addr0: int, addr1: int, theta: float) -> None: ... diff --git a/crates/ppvm-python-native/src/interface.rs b/crates/ppvm-python-native/src/interface.rs index 88afc07e3..24eb408be 100644 --- a/crates/ppvm-python-native/src/interface.rs +++ b/crates/ppvm-python-native/src/interface.rs @@ -194,6 +194,11 @@ macro_rules! create_interface { self.inner.truncate(); } + pub fn r(&mut self, addr0: usize, axis_angle: f64, theta: f64) { + self.inner.r(addr0, axis_angle, theta); + self.inner.truncate(); + } + // rot2 pub fn rxx(&mut self, addr0: usize, addr1: usize, theta: f64) { self.inner.rxx(addr0, addr1, theta); diff --git a/crates/ppvm-python-native/src/interface_tableau.rs b/crates/ppvm-python-native/src/interface_tableau.rs index 2cccf2ede..e0ed9b532 100644 --- a/crates/ppvm-python-native/src/interface_tableau.rs +++ b/crates/ppvm-python-native/src/interface_tableau.rs @@ -124,6 +124,10 @@ macro_rules! create_interface { self.inner.u3(addr0, theta, phi, lam); } + pub fn r(&mut self, addr0: usize, axis_angle: f64, theta: f64) { + self.inner.r(addr0, axis_angle, theta); + } + // rot2 pub fn rxx(&mut self, addr0: usize, addr1: usize, theta: f64) { self.inner.rxx(addr0, addr1, theta); diff --git a/crates/ppvm-runtime/src/sum/rot1.rs b/crates/ppvm-runtime/src/sum/rot1.rs index dd6bcef5e..700ff34c1 100644 --- a/crates/ppvm-runtime/src/sum/rot1.rs +++ b/crates/ppvm-runtime/src/sum/rot1.rs @@ -126,6 +126,20 @@ where } } +impl RotXY for PauliSum +where + PauliSum: RotationOne, +{ + fn r(&mut self, addr0: usize, axis_angle: ::Coeff, theta: ::Coeff) { + // R(axis_angle, θ) = RZ(axis_angle)·RX(θ)·RZ(−axis_angle). PauliSum runs + // in the Heisenberg picture (observables propagate backward), so the + // sub-rotations are emitted in reverse of the tableau's forward order. + self.rz(addr0, axis_angle.clone()); + self.rx(addr0, theta); + self.rz(addr0, -axis_angle); + } +} + /// 2-bit Pauli code: 00 I, 01 X, 10 Z, 11 Y /// Returns \(ε, k\) so that –i \[P_i, P_j\]/2 = ε · P_k. /// For every commuting pair it yields (0, 0). @@ -234,6 +248,32 @@ mod tests { assert_eq!(answer, expect); } + #[test] + fn test_r() { + use std::f64::consts::FRAC_PI_2; + let theta = 2.1; + + // r(axis_angle=0, θ) == rx(θ). + let mut via_r: PauliSum> = PauliSum::builder().n_qubits(1).build(); + via_r += ("Z", 1.0); + via_r.r(0, 0.0, theta); + let mut via_rx: PauliSum> = PauliSum::builder().n_qubits(1).build(); + via_rx += ("Z", 1.0); + via_rx.rx(0, theta); + assert!((via_r.overlap(&via_rx) - 1.0).abs() < 1e-9); + + // r(axis_angle=π/2, θ) must equal ry(θ) — NOT ry(−θ). This is the case + // that distinguishes the Heisenberg (backward) order from the + // Schrödinger one: a forward-ordered impl would yield ry(−θ) here. + let mut via_r: PauliSum> = PauliSum::builder().n_qubits(1).build(); + via_r += ("Z", 1.0); + via_r.r(0, FRAC_PI_2, theta); + let mut via_ry: PauliSum> = PauliSum::builder().n_qubits(1).build(); + via_ry += ("Z", 1.0); + via_ry.ry(0, theta); + assert!((via_r.overlap(&via_ry) - 1.0).abs() < 1e-9); + } + #[test] fn test_rz() { let mut answer: PauliSum> = PauliSum::builder().n_qubits(1).build(); diff --git a/crates/ppvm-runtime/src/traits/branch/rot1.rs b/crates/ppvm-runtime/src/traits/branch/rot1.rs index 837fd16b3..d610a601f 100644 --- a/crates/ppvm-runtime/src/traits/branch/rot1.rs +++ b/crates/ppvm-runtime/src/traits/branch/rot1.rs @@ -23,19 +23,11 @@ pub trait RotationOne { } /// Rotation about an axis in the x/y plane: -/// `exp(-i θ/2 · (cos(axis_angle)·X + sin(axis_angle)·Y))`. +/// `R(axis_angle, θ) = exp(-i θ/2 · (cos(axis_angle)·X + sin(axis_angle)·Y))`. +/// +/// The in-plane axis is `X` rotated about `Z` by `axis_angle`, so +/// `R(axis_angle, θ) = RZ(axis_angle)·RX(θ)·RZ(−axis_angle)`. pub trait RotXY { /// `R(axis_angle, θ)` on qubit `addr0`. fn r(&mut self, addr0: usize, axis_angle: T::Coeff, theta: T::Coeff); } - -impl> RotXY for S { - fn r(&mut self, addr0: usize, axis_angle: ::Coeff, theta: ::Coeff) { - // R(axis_angle, θ) = RZ(axis_angle)·RX(θ)·RZ(−axis_angle), since the - // in-plane axis is X rotated about Z by `axis_angle`. Applied in - // forward (Schrödinger) order, the rightmost factor goes first. - self.rz(addr0, -axis_angle.clone()); - self.rx(addr0, theta); - self.rz(addr0, axis_angle); - } -} diff --git a/crates/ppvm-tableau/src/gates/rot1.rs b/crates/ppvm-tableau/src/gates/rot1.rs index e3a93c844..b88d99ef5 100644 --- a/crates/ppvm-tableau/src/gates/rot1.rs +++ b/crates/ppvm-tableau/src/gates/rot1.rs @@ -42,6 +42,20 @@ where } } +impl, I>> RotXY for GeneralizedTableau +where + GeneralizedTableau: RotationOne, +{ + fn r(&mut self, addr0: usize, axis_angle: T::Coeff, theta: T::Coeff) { + // R(axis_angle, θ) = RZ(axis_angle)·RX(θ)·RZ(−axis_angle). The tableau + // runs in the Schrödinger picture, so the sub-rotations are applied in + // forward order: RZ(−axis_angle) first, then RX(θ), then RZ(axis_angle). + self.rz(addr0, -axis_angle.clone()); + self.rx(addr0, theta); + self.rz(addr0, axis_angle); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/ppvm-python/src/ppvm/mixins.py b/ppvm-python/src/ppvm/mixins.py index 3e798746b..eeef25418 100644 --- a/ppvm-python/src/ppvm/mixins.py +++ b/ppvm-python/src/ppvm/mixins.py @@ -112,6 +112,22 @@ def rz(self, addr0: int, theta: float) -> None: """ self._interface.rz(addr0, theta) + def r(self, addr0: int, axis_angle: float, theta: float) -> None: + """Apply a rotation about an axis in the X-Y plane to the specified qubit. + + ```math + R(\\phi, \\theta) = e^{-i \\frac{\\theta}{2} (\\cos\\phi\\, X + \\sin\\phi\\, Y)} + = R_Z(\\phi) R_X(\\theta) R_Z(-\\phi) + ``` + + Args: + addr0: The index of the target qubit. + axis_angle: The angle ``φ`` (in radians) of the rotation axis + within the X-Y plane, measured from the X-axis. + theta: The rotation angle in radians. + """ + self._interface.r(addr0, axis_angle, theta) + # Two qubit rotations def rxx(self, addr0: int, addr1: int, theta: float) -> None: """Apply an RXX (Ising XX) rotation gate to two qubits. diff --git a/ppvm-python/test/generalized_tableau/test_basics.py b/ppvm-python/test/generalized_tableau/test_basics.py index 997f85142..4ccff54ff 100644 --- a/ppvm-python/test/generalized_tableau/test_basics.py +++ b/ppvm-python/test/generalized_tableau/test_basics.py @@ -135,6 +135,20 @@ def test_ry_pi_equals_y(): assert tab.measure(0) +def test_r_axis_zero_equals_rx(): + # r(axis_angle=0, θ=π) = rx(π), so |0> → |1> + tab = GeneralizedTableau(2) + tab.r(0, 0.0, math.pi) + assert tab.measure(0) + + +def test_r_axis_half_pi_equals_ry(): + # r(axis_angle=π/2, θ=π) = ry(π), so |0> → |1> + tab = GeneralizedTableau(2) + tab.r(0, math.pi / 2, math.pi) + assert tab.measure(0) + + def test_clifford_extensions_sqrt_x(): # sqrt_x followed by sqrt_x_adj is identity tab = GeneralizedTableau(2) diff --git a/ppvm-python/test/test_basics.py b/ppvm-python/test/test_basics.py index 7695956cb..47b9ea756 100644 --- a/ppvm-python/test/test_basics.py +++ b/ppvm-python/test/test_basics.py @@ -191,6 +191,16 @@ def t(state): s.rz(0, -PI / 2) assert pytest.approx(t(s).get("YI", 0.0)) == 1.0 + # r(axis_angle=0, θ=π/2) = rx(π/2): ZI → YI + s = PauliSum(n_qubits=2, initial_terms=["ZI"], coefficients=[1.0]) + s.r(0, 0.0, PI / 2) + assert pytest.approx(t(s).get("YI", 0.0)) == 1.0 + + # r(axis_angle=π/2, θ=π/2) = ry(π/2): ZI → −XI + s = PauliSum(n_qubits=2, initial_terms=["ZI"], coefficients=[1.0]) + s.r(0, PI / 2, PI / 2) + assert pytest.approx(t(s).get("XI", 0.0)) == -1.0 + # rxx(π/2): ZI → YX [cos·ZI + sin·YX at θ=π/2 → YX] s = PauliSum(n_qubits=2, initial_terms=["ZI"], coefficients=[1.0]) s.rxx(0, 1, PI / 2) From 225c34ee4d36008fb2a7c878d4455978421792a6 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 13:05:09 +0200 Subject: [PATCH 51/95] Add R to CircuitInstruction set --- crates/ppvm-vihaco/src/bytecode.rs | 3 +++ crates/ppvm-vihaco/src/component.rs | 3 +++ crates/ppvm-vihaco/src/composite.rs | 6 ++++++ crates/ppvm-vihaco/src/instruction.rs | 5 +++++ crates/ppvm-vihaco/src/message.rs | 1 + crates/ppvm-vihaco/tests/rotxy.sst | 15 ++++++++++++++ crates/ppvm-vihaco/tests/sst_fixtures.rs | 26 ++++++++++++++++++++++++ 7 files changed, 59 insertions(+) create mode 100644 crates/ppvm-vihaco/tests/rotxy.sst diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs index 94493ce98..a87b75170 100644 --- a/crates/ppvm-vihaco/src/bytecode.rs +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -187,6 +187,8 @@ fn skip_bytes(r: &mut R, n: u64) -> eyre::Result<()> { #[cfg(test)] mod tests { + use vihaco::Value; + use super::*; fn empty_module() -> PPVMModule { @@ -216,6 +218,7 @@ mod tests { m.code = vec![ PPVMInstruction::Cpu(Cpu::Const(Value::U64(0))), PPVMInstruction::Circuit(CircuitInstruction::H), + PPVMInstruction::Circuit(CircuitInstruction::R), PPVMInstruction::Cpu(Cpu::Branch(1)), PPVMInstruction::Cpu(Cpu::ConditionalBranch(0, 1)), PPVMInstruction::Cpu(Cpu::Call(0, 1)), diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 56c832565..5096a34a9 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -81,6 +81,9 @@ where // U3 (U3, &QubitU3(addr, theta, phi, lam)) => self.tab.u3(addr, theta, phi, lam), + // RXY: rotation about an axis in the x/y plane + (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => self.tab.r(addr, axis_angle, theta), + // Measure & Reset (Measure, &Qubit(addr)) => { let outcome: MeasurementOutcome = self.tab.measure(addr).into(); diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 0b33a9e28..6abd89d6b 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -179,6 +179,12 @@ impl PPVM { let q1 = self.pop_qubit()?; Ok(CircuitMessage::TwoQubitAndFloat(q0, q1, theta)) } + R => { + let theta = self.pop_f64()?; + let axis_angle = self.pop_f64()?; + let q = self.pop_qubit()?; + Ok(CircuitMessage::QubitAndTwoFloats(q, axis_angle, theta)) + } U3 => { let lam = self.pop_f64()?; let phi = self.pop_f64()?; diff --git a/crates/ppvm-vihaco/src/instruction.rs b/crates/ppvm-vihaco/src/instruction.rs index 417b65ca8..9f6f34915 100644 --- a/crates/ppvm-vihaco/src/instruction.rs +++ b/crates/ppvm-vihaco/src/instruction.rs @@ -53,6 +53,9 @@ pub enum CircuitInstruction { Measure, Reset, + // RXY + R, + // Loss Loss, CorrelatedLoss, @@ -104,6 +107,8 @@ impl std::fmt::Display for CircuitInstruction { Measure => write!(f, "Measure"), Reset => write!(f, "Reset"), + R => write!(f, "R"), + Loss => write!(f, "Loss"), CorrelatedLoss => write!(f, "CorrelatedLoss"), diff --git a/crates/ppvm-vihaco/src/message.rs b/crates/ppvm-vihaco/src/message.rs index 31284cfdd..09928050a 100644 --- a/crates/ppvm-vihaco/src/message.rs +++ b/crates/ppvm-vihaco/src/message.rs @@ -5,6 +5,7 @@ use vihaco::Message; pub enum CircuitMessage { Qubit(usize), // X, Y, Z, ... QubitAndFloat(usize, f64), // RX, depolarize, ... + QubitAndTwoFloats(usize, f64, f64), // R TwoQubit(usize, usize), // CX, CZ TwoQubitAndFloat(usize, usize, f64), // RXX, ... QubitU3(usize, f64, f64, f64), // U3 diff --git a/crates/ppvm-vihaco/tests/rotxy.sst b/crates/ppvm-vihaco/tests/rotxy.sst new file mode 100644 index 000000000..b96985fdb --- /dev/null +++ b/crates/ppvm-vihaco/tests/rotxy.sst @@ -0,0 +1,15 @@ +device circuit.n_qubits 1; + +fn @main() { + // R(axis_angle = π/2, θ = π) == RY(π), so |0> is sent to |1>. + // Stack order for `gate r`: qubit, then axis_angle, then theta. + const.u64 0 + const.f64 1.5707963267948966 + const.f64 3.141592653589793 + gate r + + const.u64 0 + gate measure + + ret +} diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 1604452aa..96cd96b16 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -40,6 +40,32 @@ fn hello_circuit_sst_parses_and_runs() { assert_eq!(machine.measurement_record().len(), 0); } +#[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 `gate r` path end to + // end: parse → resolve (pop θ, axis_angle, qubit) → execute via `tab.r`. + let machine = + ppvm_vihaco::run_file("tests/rotxy.sst").unwrap_or_else(|e| panic!("run rotxy.sst: {e:?}")); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1, "expected exactly one measurement"); + assert_eq!( + record[0].as_slice(), + &[MeasurementOutcome::One], + "R(π/2, π) = RY(π) must flip q0 to 1" + ); +} + +#[test] +fn dumped_rotxy_runs_and_flips_qubit() { + // Same program, but through the bytecode round-trip: confirms the `R` + // instruction survives dump → `.ssb` → load → execute. + let machine = dump_load_run("tests/rotxy.sst", "ppvm_dump_rotxy.ssb"); + let record = machine.measurement_record(); + assert_eq!(record.len(), 1); + assert_eq!(record[0].as_slice(), &[MeasurementOutcome::One]); +} + #[test] fn run_file_via_library_helper() { let machine = From 3474c03f22c259d3626260a5dc05704225a20d29 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 14:39:55 +0200 Subject: [PATCH 52/95] Split out CircuitInstruction, Message and effect into separate crate --- Cargo.lock | 13 ++ Cargo.toml | 2 +- crates/ppvm-vihaco/Cargo.toml | 1 + crates/ppvm-vihaco/src/bytecode.rs | 2 +- crates/ppvm-vihaco/src/component.rs | 10 +- crates/ppvm-vihaco/src/composite.rs | 7 +- crates/ppvm-vihaco/src/instruction.rs | 120 ---------- crates/ppvm-vihaco/src/lib.rs | 2 - crates/ppvm-vihaco/src/message.rs | 25 -- crates/ppvm-vihaco/src/syntax.rs | 6 +- crates/vihaco-circuit-isa/Cargo.toml | 12 + crates/vihaco-circuit-isa/src/lib.rs | 320 ++++++++++++++++++++++++++ 12 files changed, 355 insertions(+), 165 deletions(-) delete mode 100644 crates/ppvm-vihaco/src/instruction.rs delete mode 100644 crates/ppvm-vihaco/src/message.rs create mode 100644 crates/vihaco-circuit-isa/Cargo.toml create mode 100644 crates/vihaco-circuit-isa/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index dd6ac03c1..33b45a08b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,6 +1193,7 @@ dependencies = [ "rayon", "smallvec", "vihaco", + "vihaco-circuit-isa", "vihaco-cpu", "vihaco-parser", "vihaco-parser-core", @@ -1764,6 +1765,18 @@ dependencies = [ "vihaco-parser-core", ] +[[package]] +name = "vihaco-circuit-isa" +version = "0.1.0" +dependencies = [ + "chumsky 0.10.1", + "eyre", + "smallvec", + "vihaco", + "vihaco-parser", + "vihaco-parser-core", +] + [[package]] name = "vihaco-cpu" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f651490ac..7966e210d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ # Runnable copies of the Rust code blocks in skills/ppvm-usage/SKILL.md. # Built by `cargo build --workspace --all-targets` in CI so the skill # can't silently drift away from the public API. - "skills/ppvm-usage/examples/rust", "crates/ppvm-cli", + "skills/ppvm-usage/examples/rust", "crates/ppvm-cli", "crates/vihaco-circuit-isa", ] [[example]] diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 752aebe7b..dd97f432e 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -22,3 +22,4 @@ vihaco = { version = "0.1.0", path = "../../../vihaco/crates/vihaco" } vihaco-cpu = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-cpu" } vihaco-parser = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-parser" } vihaco-parser-core = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-parser-core" } +vihaco-circuit-isa = { version = "0.1.0", path = "../vihaco-circuit-isa" } diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs index a87b75170..9ec23f8d1 100644 --- a/crates/ppvm-vihaco/src/bytecode.rs +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -210,7 +210,7 @@ mod tests { #[test] fn round_trips_code() { - use crate::instruction::CircuitInstruction; + use vihaco_circuit_isa::CircuitInstruction; use vihaco_cpu::Instruction as Cpu; let mut m = empty_module(); diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 5096a34a9..96f580ce4 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -1,6 +1,5 @@ use crate::measurements::MeasurementEffect; -use crate::message::CircuitMessage; -use crate::{instruction::CircuitInstruction, measurements::MeasurementOutcome}; +use crate::measurements::MeasurementOutcome; use bitvec::view::BitView; use bnum::types::{U256, U512, U1024, U2048}; use eyre::{Result, eyre}; @@ -9,6 +8,7 @@ use num::complex::Complex64; use ppvm_runtime::config::fx64hash::Byte8F64; use ppvm_tableau::prelude::*; use vihaco::{Effects, component, observe}; +use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; macro_rules! batch_for { ($tab:expr, $method:ident, $addrs:expr) => { @@ -298,12 +298,6 @@ impl Circuit { } } -#[derive(Debug, Clone)] -pub struct CircuitEffect { - pub inst: CircuitInstruction, - pub msg: CircuitMessage, -} - #[observe(CircuitEffect, effect=MeasurementEffect)] impl Circuit { fn observe_circuit_effect( diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 6abd89d6b..f8a1667d6 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -9,10 +9,9 @@ use vihaco_cpu::{CPU, CPUMessage}; /// without depending on `vihaco-cpu` directly. pub use vihaco_cpu::StepOutcome; -use crate::component::{Circuit, CircuitEffect}; -use crate::instruction::CircuitInstruction; +use crate::component::Circuit; use crate::measurements::{MeasurementEffect, MeasurementObserver, MeasurementResult}; -use crate::message::CircuitMessage; +use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; pub const PPVM_MAGIC: u32 = 0x5050564D; @@ -156,7 +155,7 @@ impl PPVM { } fn resolve_circuit(&mut self, inst: &CircuitInstruction) -> eyre::Result { - use crate::instruction::CircuitInstruction::*; + use CircuitInstruction::*; match inst { X | Y | Z | H | S | SAdj | SqrtX | SqrtY | SqrtXAdj | SqrtYAdj | T | TAdj | Measure | Reset => { diff --git a/crates/ppvm-vihaco/src/instruction.rs b/crates/ppvm-vihaco/src/instruction.rs deleted file mode 100644 index 9f6f34915..000000000 --- a/crates/ppvm-vihaco/src/instruction.rs +++ /dev/null @@ -1,120 +0,0 @@ -use vihaco::Instruction; -use vihaco_parser::Parse; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Instruction, Parse)] -pub enum CircuitInstruction { - // NOTE: longer tokens need to go first - TwoQubitPauliError, // 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, -} - -impl std::fmt::Display for CircuitInstruction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use CircuitInstruction::*; - match self { - TwoQubitPauliError => write!(f, "TwoQubitPauliError"), - - X => write!(f, "X"), - Y => write!(f, "Y"), - Z => write!(f, "Z"), - H => write!(f, "H"), - - SqrtXAdj => write!(f, "SqrtXAdj"), - - SqrtX => write!(f, "SqrtX"), - - SqrtYAdj => write!(f, "SqrtYAdj"), - - SqrtY => write!(f, "SqrtY"), - - SAdj => write!(f, "SAdj"), - S => write!(f, "S"), - - CNOT => write!(f, "CNOT"), - CZ => write!(f, "CZ"), - - TAdj => write!(f, "TAdj"), - T => write!(f, "T"), - - RXX => write!(f, "RXX"), - RYY => write!(f, "RYY"), - RZZ => write!(f, "RZZ"), - - RX => write!(f, "RX"), - RY => write!(f, "RY"), - RZ => write!(f, "RZ"), - - U3 => write!(f, "U3"), - - Measure => write!(f, "Measure"), - Reset => write!(f, "Reset"), - - R => write!(f, "R"), - - Loss => write!(f, "Loss"), - CorrelatedLoss => write!(f, "CorrelatedLoss"), - - PauliError => write!(f, "PauliError"), - Depolarize2 => write!(f, "Depolarize2"), - Depolarize => write!(f, "Depolarize"), - } - } -} diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 85a57a574..5cc8c37fb 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -1,9 +1,7 @@ pub mod bytecode; pub mod component; pub mod composite; -pub mod instruction; pub mod measurements; -pub mod message; pub mod shots; mod syntax; diff --git a/crates/ppvm-vihaco/src/message.rs b/crates/ppvm-vihaco/src/message.rs deleted file mode 100644 index 09928050a..000000000 --- a/crates/ppvm-vihaco/src/message.rs +++ /dev/null @@ -1,25 +0,0 @@ -use smallvec::SmallVec; -use vihaco::Message; - -#[derive(Debug, Clone, Message)] -pub enum CircuitMessage { - Qubit(usize), // X, Y, Z, ... - QubitAndFloat(usize, f64), // RX, depolarize, ... - QubitAndTwoFloats(usize, f64, f64), // R - TwoQubit(usize, usize), // CX, CZ - TwoQubitAndFloat(usize, usize, f64), // RXX, ... - QubitU3(usize, f64, f64, f64), // U3 - QubitAndFloatArr3(usize, [f64; 3]), // PauliError - TwoQubitAndFloatArr3(usize, usize, [f64; 3]), // Correlated loss - TwoQubitAndFloatArr15(usize, usize, [f64; 15]), // TwoQubitPauliError - - // batched instructions - QubitBatch(SmallVec<[usize; 8]>), // X, Y, Z, ... - QubitBatchAndFloat(SmallVec<[usize; 8]>, f64), // RX, depolarize, ... - TwoQubitBatch(SmallVec<[(usize, usize); 8]>), // CX, CZ - TwoQubitBatchAndFloat(SmallVec<[(usize, usize); 8]>, f64), // RXX, ... - QubitBatchU3(SmallVec<[usize; 8]>, f64, f64, f64), // U3 - QubitBatchAndFloatArr3(SmallVec<[usize; 8]>, [f64; 3]), // PauliError - TwoQubitBatchAndFloatArr3(SmallVec<[(usize, usize); 8]>, [f64; 3]), // Correlated loss - TwoQubitBatchAndFloatArr15(SmallVec<[(usize, usize); 8]>, [f64; 15]), // TwoQubitPauliError -} diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index b048097e1..f6b496f01 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -6,12 +6,10 @@ use vihaco::{ module::Module, syntax::{BodyItem, RawForm, RawOperand, Resolve}, }; +use vihaco_circuit_isa::CircuitInstruction; use vihaco_parser_core::Parse; -use crate::{ - composite::{PPVMDeviceInfo, PPVMInstruction}, - instruction::CircuitInstruction, -}; +use crate::composite::{PPVMDeviceInfo, PPVMInstruction}; #[derive(Debug, Clone, PartialEq, vihaco_parser::Parse)] #[head = "device "] diff --git a/crates/vihaco-circuit-isa/Cargo.toml b/crates/vihaco-circuit-isa/Cargo.toml new file mode 100644 index 000000000..fbaf5b02a --- /dev/null +++ b/crates/vihaco-circuit-isa/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "vihaco-circuit-isa" +version = "0.1.0" +edition = "2024" + +[dependencies] +chumsky = "0.10" +eyre = "0.6.12" +smallvec = "1.15.1" +vihaco = { version = "0.1.0", path = "../../../vihaco/crates/vihaco" } +vihaco-parser = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-parser" } +vihaco-parser-core = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-parser-core" } diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs new file mode 100644 index 000000000..9e08983ac --- /dev/null +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -0,0 +1,320 @@ +use smallvec::SmallVec; +use vihaco::Instruction; +use vihaco::Message; +use vihaco_parser::Parse; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Instruction, Parse)] +pub enum CircuitInstruction { + // NOTE: longer tokens need to go first + TwoQubitPauliError, // 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, +} + +impl std::fmt::Display for CircuitInstruction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use CircuitInstruction::*; + match self { + TwoQubitPauliError => write!(f, "TwoQubitPauliError"), + + X => write!(f, "X"), + Y => write!(f, "Y"), + Z => write!(f, "Z"), + H => write!(f, "H"), + + SqrtXAdj => write!(f, "SqrtXAdj"), + + SqrtX => write!(f, "SqrtX"), + + SqrtYAdj => write!(f, "SqrtYAdj"), + + SqrtY => write!(f, "SqrtY"), + + SAdj => write!(f, "SAdj"), + S => write!(f, "S"), + + CNOT => write!(f, "CNOT"), + CZ => write!(f, "CZ"), + + TAdj => write!(f, "TAdj"), + T => write!(f, "T"), + + RXX => write!(f, "RXX"), + RYY => write!(f, "RYY"), + RZZ => write!(f, "RZZ"), + + RX => write!(f, "RX"), + RY => write!(f, "RY"), + RZ => write!(f, "RZ"), + + U3 => write!(f, "U3"), + + Measure => write!(f, "Measure"), + Reset => write!(f, "Reset"), + + R => write!(f, "R"), + + Loss => write!(f, "Loss"), + CorrelatedLoss => write!(f, "CorrelatedLoss"), + + PauliError => write!(f, "PauliError"), + Depolarize2 => write!(f, "Depolarize2"), + Depolarize => write!(f, "Depolarize"), + } + } +} + +#[derive(Debug, Clone, Message)] +pub enum CircuitMessage { + Qubit(usize), // X, Y, Z, ... + QubitAndFloat(usize, f64), // RX, depolarize, ... + QubitAndTwoFloats(usize, f64, f64), // R + TwoQubit(usize, usize), // CX, CZ + TwoQubitAndFloat(usize, usize, f64), // RXX, ... + QubitU3(usize, f64, f64, f64), // U3 + QubitAndFloatArr3(usize, [f64; 3]), // PauliError + TwoQubitAndFloatArr3(usize, usize, [f64; 3]), // Correlated loss + TwoQubitAndFloatArr15(usize, usize, [f64; 15]), // TwoQubitPauliError + + // batched instructions + QubitBatch(SmallVec<[usize; 8]>), // X, Y, Z, ... + QubitBatchAndFloat(SmallVec<[usize; 8]>, f64), // RX, depolarize, ... + TwoQubitBatch(SmallVec<[(usize, usize); 8]>), // CX, CZ + TwoQubitBatchAndFloat(SmallVec<[(usize, usize); 8]>, f64), // RXX, ... + QubitBatchU3(SmallVec<[usize; 8]>, f64, f64, f64), // U3 + QubitBatchAndFloatArr3(SmallVec<[usize; 8]>, [f64; 3]), // PauliError + TwoQubitBatchAndFloatArr3(SmallVec<[(usize, usize); 8]>, [f64; 3]), // Correlated loss + TwoQubitBatchAndFloatArr15(SmallVec<[(usize, usize); 8]>, [f64; 15]), // TwoQubitPauliError +} + +#[derive(Debug, Clone)] +pub struct CircuitEffect { + pub inst: CircuitInstruction, + pub msg: CircuitMessage, +} + +#[cfg(test)] +mod tests { + use super::CircuitInstruction::*; + use super::*; + + use chumsky::Parser as _; + use vihaco::instruction::{FromBytes, OpCode, WriteBytes}; + use vihaco_parser_core::Parse as _; + + /// 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, + 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) + .into_result() + .unwrap_or_else(|e| panic!("parse of `{src}` failed: {e:?}")) + } + + // ─── Parse: tokens are the lowercased variant name ──────────────────── + + #[test] + fn parses_simple_lowercase_tokens() { + assert_eq!(parse("x"), X); + assert_eq!(parse("y"), Y); + assert_eq!(parse("z"), Z); + assert_eq!(parse("h"), H); + assert_eq!(parse("cnot"), CNOT); + assert_eq!(parse("cz"), CZ); + assert_eq!(parse("u3"), U3); + assert_eq!(parse("measure"), Measure); + assert_eq!(parse("reset"), Reset); + assert_eq!(parse("rxx"), RXX); + assert_eq!(parse("depolarize2"), Depolarize2); + assert_eq!(parse("depolarize"), Depolarize); + } + + // ─── Parse: prefix-sensitive disambiguation ─────────────────────────── + // + // These pairs share a prefix, so the declaration order in the enum is + // load-bearing: the longer token must win. These tests pin that contract. + + #[test] + fn parses_t_family_without_prefix_collision() { + // `t` is a prefix of both `tadj` and `twoqubitpaulierror`. + assert_eq!(parse("t"), T); + assert_eq!(parse("tadj"), TAdj); + assert_eq!(parse("twoqubitpaulierror"), TwoQubitPauliError); + } + + #[test] + fn parses_s_family_without_prefix_collision() { + // `s` is a prefix of `s_adj`, `sqrt_x`, `sqrt_y`, etc. + assert_eq!(parse("s"), S); + assert_eq!(parse("s_adj"), SAdj); + assert_eq!(parse("sqrt_x"), SqrtX); + assert_eq!(parse("sqrt_x_adj"), SqrtXAdj); + assert_eq!(parse("sqrt_y"), SqrtY); + assert_eq!(parse("sqrt_y_adj"), SqrtYAdj); + } + + #[test] + fn rejects_unknown_token() { + assert!(CircuitInstruction::parser().parse("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"); + // 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}"); + } +} From e3af9c8cf3703431bd4a5a36f7ebf970319ae2da Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 14:42:17 +0200 Subject: [PATCH 53/95] Add license headers --- crates/ppvm-cli/src/commands.rs | 3 +++ crates/ppvm-cli/src/main.rs | 3 +++ crates/ppvm-vihaco/src/bytecode.rs | 3 +++ crates/ppvm-vihaco/src/component.rs | 3 +++ crates/ppvm-vihaco/src/composite.rs | 3 +++ crates/ppvm-vihaco/src/lib.rs | 3 +++ crates/ppvm-vihaco/src/measurements.rs | 3 +++ crates/ppvm-vihaco/src/shots.rs | 3 +++ crates/ppvm-vihaco/src/syntax.rs | 3 +++ crates/ppvm-vihaco/tests/sst_fixtures.rs | 3 +++ crates/vihaco-circuit-isa/src/lib.rs | 3 +++ 11 files changed, 33 insertions(+) diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index 93ea6a248..da8bbaa21 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + use eyre::{Result, WrapErr}; use ppvm_vihaco::composite::{PPVM, StepOutcome}; use ppvm_vihaco::measurements::MeasurementResult; diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index 4c2e2372c..6af569f1b 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + use clap::{Parser, Subcommand}; use eyre::Result; diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs index 9ec23f8d1..4aef64e0d 100644 --- a/crates/ppvm-vihaco/src/bytecode.rs +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + //! Binary bytecode (`.ssb`) round-trip for PPVM modules. //! //! Serializes a resolved [`Module`] to a little-endian container — header diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 96f580ce4..2d0bd1bfa 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + use crate::measurements::MeasurementEffect; use crate::measurements::MeasurementOutcome; use bitvec::view::BitView; diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index f8a1667d6..530fd0fd8 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + use vihaco::frame::Frame; use vihaco::machine::StackFrame; use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 5cc8c37fb..01e5b839b 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + pub mod bytecode; pub mod component; pub mod composite; diff --git a/crates/ppvm-vihaco/src/measurements.rs b/crates/ppvm-vihaco/src/measurements.rs index b7cb96ef2..19162081c 100644 --- a/crates/ppvm-vihaco/src/measurements.rs +++ b/crates/ppvm-vihaco/src/measurements.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + use eyre::Result; use smallvec::SmallVec; use vihaco::{Effects, observe}; diff --git a/crates/ppvm-vihaco/src/shots.rs b/crates/ppvm-vihaco/src/shots.rs index 59eef0cf3..0bb1e7aa6 100644 --- a/crates/ppvm-vihaco/src/shots.rs +++ b/crates/ppvm-vihaco/src/shots.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + //! Running a compiled program for many shots, optionally across threads. //! //! Each shot runs on a fresh [`PPVM`] so shots are fully independent; the diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index f6b496f01..012eb0d2d 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + use std::collections::HashMap; use chumsky::{Parser, error::Simple, extra}; diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 96cd96b16..3acc424bc 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + //! End-to-end fixture coverage: parse + resolve + run each `.sst` file in //! this directory via the public `PPVM` API. diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs index 9e08983ac..ee5fd59ed 100644 --- a/crates/vihaco-circuit-isa/src/lib.rs +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + use smallvec::SmallVec; use vihaco::Instruction; use vihaco::Message; From 5d433422922de207218c772241b63cc45271f54f Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 16:04:03 +0200 Subject: [PATCH 54/95] Make thread count global flag and make it consistent throughout --- crates/ppvm-cli/src/commands.rs | 20 ++------ crates/ppvm-cli/src/main.rs | 23 ++++----- crates/ppvm-vihaco/src/shots.rs | 86 ++++++++++++++++++++++++--------- 3 files changed, 76 insertions(+), 53 deletions(-) diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index da8bbaa21..44f1858f6 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -27,16 +27,16 @@ pub enum MeasurementFormat { pub fn run( file: &str, shots: usize, - threads: usize, seed: Option, output: Option<&str>, quiet: bool, format: MeasurementFormat, ) -> Result<()> { - // Compile once, then run every shot against the shared module. + // Compile once, then run every shot against the shared module. The thread + // pool is sized once in `main` via the top-level `--threads` flag. let module = ppvm_vihaco::load_module_file(file).wrap_err_with(|| format!("failed to load {file}"))?; - let records = ppvm_vihaco::shots::run_shots(&module, shots, threads, seed) + let records = ppvm_vihaco::shots::run_shots(&module, shots, seed) .wrap_err_with(|| format!("failed to run {file}"))?; if quiet { return Ok(()); @@ -320,7 +320,7 @@ mod tests { #[test] fn run_succeeds_on_valid_file() { let src = temp_file("ppvm_cli_run_ok.sst", PROGRAM); - let res = run(&src, 3, 1, None, None, true, MeasurementFormat::Bits); + let res = run(&src, 3, None, None, true, MeasurementFormat::Bits); let _ = fs::remove_file(&src); assert!(res.is_ok(), "got: {res:?}"); } @@ -331,16 +331,7 @@ mod tests { let out = std::env::temp_dir().join("ppvm_cli_run_output.txt"); let _ = fs::remove_file(&out); - run( - &src, - 4, - 1, - None, - out.to_str(), - false, - MeasurementFormat::Bits, - ) - .unwrap(); + run(&src, 4, None, out.to_str(), false, MeasurementFormat::Bits).unwrap(); let contents = fs::read_to_string(&out).unwrap(); // Four deterministic shots of |0>, one per line. assert_eq!(contents, "0\n0\n0\n0\n"); @@ -354,7 +345,6 @@ mod tests { let err = run( "/no/such/file.sst", 1, - 1, None, None, false, diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index 6af569f1b..ab727f416 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -10,6 +10,10 @@ mod commands; #[command(name = "ppvm")] #[command(about = "Pauli propagation virtual machine", long_about = None)] pub struct Cli { + /// Number of threads for all parallel work (1 = fully serial & deterministic) + #[arg(short, long, default_value = "1")] + threads: usize, + #[command(subcommand)] command: Commands, } @@ -51,10 +55,6 @@ enum Commands { #[arg(short, long, default_value = "1")] shots: usize, - /// Number of threads (> 1 enables parallel execution) - #[arg(short, long, default_value = "1")] - threads: usize, - /// Seed the RNG for reproducible results #[arg(long)] seed: Option, @@ -87,6 +87,10 @@ enum Commands { fn main() -> Result<()> { let cli = Cli::parse(); + // Size the process-wide thread pool once; governs all parallelism (across + // shots and within a single machine). `--threads 1` is fully serial. + ppvm_vihaco::shots::set_global_threads(cli.threads)?; + match cli.command { Commands::Parse { file, format } => { commands::parse(&file, format)?; @@ -101,21 +105,12 @@ fn main() -> Result<()> { Commands::Run { file, shots, - threads, seed, output, quiet, format, } => { - commands::run( - &file, - shots, - threads, - seed, - output.as_deref(), - quiet, - format, - )?; + commands::run(&file, shots, seed, output.as_deref(), quiet, format)?; } Commands::Debug { file, diff --git a/crates/ppvm-vihaco/src/shots.rs b/crates/ppvm-vihaco/src/shots.rs index 0bb1e7aa6..4fbb91719 100644 --- a/crates/ppvm-vihaco/src/shots.rs +++ b/crates/ppvm-vihaco/src/shots.rs @@ -5,8 +5,9 @@ //! //! Each shot runs on a fresh [`PPVM`] so shots are fully independent; the //! module is compiled once and shared. With the `rayon` feature, [`run_shots`] -//! parallelizes across shots when asked for more than one thread and there are -//! enough shots to amortize the overhead. +//! parallelizes across shots when the global pool (sized once by +//! [`set_global_threads`]) has more than one thread and there are enough shots +//! to amortize the overhead. use crate::PPVMModule; use crate::composite::PPVM; @@ -43,47 +44,71 @@ pub fn run_shots_serial( .collect() } -/// Run `shots` shots across a scoped rayon pool of `threads` threads. One entry -/// per shot, in order (preserved by the indexed parallel iterator). +/// Run `shots` shots across the global rayon pool. One entry per shot, in order +/// (preserved by the indexed parallel iterator). The pool size is whatever +/// [`set_global_threads`] configured at startup; each shot runs on a worker +/// thread, so the intra-shot parallelism guard keeps a single shot serial and +/// the pool is never oversubscribed. #[cfg(feature = "rayon")] pub fn run_shots_parallel( module: &PPVMModule, shots: usize, - threads: usize, seed: Option, ) -> eyre::Result>> { use rayon::prelude::*; - let pool = rayon::ThreadPoolBuilder::new() - .num_threads(threads) - .build()?; - pool.install(|| { - (0..shots) - .into_par_iter() - .map(|i| run_one_shot(module, shot_seed(seed, i))) - .collect() - }) + (0..shots) + .into_par_iter() + .map(|i| run_one_shot(module, shot_seed(seed, i))) + .collect() +} + +/// Decide whether to spread shots across the rayon pool. Worth it only with a +/// multi-thread pool and enough shots to amortize the overhead; a single-thread +/// pool always takes the serial path, keeping results deterministic. +#[cfg(feature = "rayon")] +fn should_parallelize(num_threads: usize, shots: usize) -> bool { + num_threads > 1 && shots >= PARALLEL_SHOT_THRESHOLD } /// Run `shots` shots, choosing serial or parallel execution. Goes parallel only -/// when built with `rayon`, more than one thread is requested, and there are -/// enough shots to be worth it; otherwise runs serially. +/// when built with `rayon`, the global pool has more than one thread, and there +/// are enough shots to be worth it; otherwise runs serially. The pool size is +/// set once at startup by [`set_global_threads`]. pub fn run_shots( module: &PPVMModule, shots: usize, - threads: usize, seed: Option, ) -> eyre::Result>> { #[cfg(feature = "rayon")] - if threads > 1 && shots >= PARALLEL_SHOT_THRESHOLD { - return run_shots_parallel(module, shots, threads, seed); + if should_parallelize(rayon::current_num_threads(), shots) { + return run_shots_parallel(module, shots, seed); } - #[cfg(not(feature = "rayon"))] - let _ = threads; run_shots_serial(module, shots, seed) } +/// Configure the process-wide rayon thread pool. Call once, before any parallel +/// work runs. A count of `1` forces fully serial, deterministic execution — both +/// across shots and within a single machine's coefficient propagation. +#[cfg(feature = "rayon")] +pub fn set_global_threads(threads: usize) -> eyre::Result<()> { + rayon::ThreadPoolBuilder::new() + .num_threads(threads) + .build_global()?; + Ok(()) +} + +/// Without the `rayon` feature there is no pool to size; anything but a single +/// thread is meaningless, so reject it rather than silently run serially. +#[cfg(not(feature = "rayon"))] +pub fn set_global_threads(threads: usize) -> eyre::Result<()> { + if threads > 1 { + eyre::bail!("this build has no parallelism support; --threads must be 1"); + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -116,8 +141,8 @@ mod tests { #[test] fn dispatcher_runs_all_shots() { let m = module(DETERMINISTIC); - // threads = 1 forces the serial path regardless of the rayon feature. - let records = run_shots(&m, 10, 1, None).unwrap(); + // 10 shots is below the parallel threshold, so this takes the serial path. + let records = run_shots(&m, 10, None).unwrap(); assert_eq!(records.len(), 10); } @@ -147,7 +172,20 @@ mod tests { fn serial_and_parallel_match_for_same_seed() { let m = module(RANDOM); let serial = run_shots_serial(&m, 64, Some(7)).unwrap(); - let parallel = run_shots_parallel(&m, 64, 4, Some(7)).unwrap(); + let parallel = run_shots_parallel(&m, 64, Some(7)).unwrap(); assert_eq!(serial, parallel); } + + #[cfg(feature = "rayon")] + #[test] + fn parallelizes_only_with_multiple_threads_above_threshold() { + // A single-thread pool is always serial, no matter how many shots. + assert!(!should_parallelize(1, 100_000)); + // Multiple threads, but too few shots to amortize the overhead: serial. + assert!(!should_parallelize(8, PARALLEL_SHOT_THRESHOLD - 1)); + // Multiple threads at the threshold: parallel. + assert!(should_parallelize(8, PARALLEL_SHOT_THRESHOLD)); + // Multiple threads, plenty of shots: parallel. + assert!(should_parallelize(8, 100_000)); + } } From 3a96ddcc75417b96a5b50736fee614bdbb336aad Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 16:17:12 +0200 Subject: [PATCH 55/95] Execute single instruction and tableau printing methods --- crates/ppvm-vihaco/src/component.rs | 13 ++++ crates/ppvm-vihaco/src/composite.rs | 101 ++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 2d0bd1bfa..0317bf146 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -299,6 +299,19 @@ impl Circuit { Self::Bits2048(ex) => ex.execute_instruction(inst, msg), } } + + /// Render the current tableau / Pauli state, dispatching across the executor + /// size variants. Used by the REPL's `show` command. + pub fn state_string(&self) -> String { + match self { + Self::Bits64(ex) => ex.tab.to_string(), + Self::Bits128(ex) => ex.tab.to_string(), + Self::Bits256(ex) => ex.tab.to_string(), + Self::Bits512(ex) => ex.tab.to_string(), + Self::Bits1024(ex) => ex.tab.to_string(), + Self::Bits2048(ex) => ex.tab.to_string(), + } + } } #[observe(CircuitEffect, effect=MeasurementEffect)] diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 530fd0fd8..d579aede5 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -295,6 +295,26 @@ impl PPVM { self.peek_instruction().ok().cloned() } + /// Append one REPL command's lowered VM ops and run just that block against + /// the persistent state, advancing the pc through it. NOTE: an out-of-range + /// qubit index *panics* in the tableau rather than erroring, so callers must + /// bounds-check qubit operands against `n_qubits` first. + pub fn execute_single_instruction(&mut self, instrs: &[PPVMInstruction]) -> eyre::Result<()> { + let start = self.loader.module.code.len() as u32; + self.loader.module.code.extend_from_slice(instrs); + *self.loader.pc_mut() = start; + for _ in 0..instrs.len() { + self.step_once()?; + } + Ok(()) + } + + /// Render the current circuit state (tableau / Pauli sum) for the REPL's + /// `show` command. Delegates to the circuit's size-specific tableau. + pub fn state_string(&self) -> String { + self.circuit.state_string() + } + fn execute_effects(&mut self, inst: Instruction) -> eyre::Result> { log::debug!("exec inst: {:?}, stack: {:?}", inst, self.cpu.stack()); match inst { @@ -636,6 +656,87 @@ mod tests { Ok(()) } + // ─── Incremental execution (REPL) ───────────────────────────────────── + + #[test] + fn execute_single_instruction_persists_state_across_calls() -> eyre::Result<()> { + use crate::measurements::MeasurementOutcome; + + // A 1-qubit device with no code; the REPL builds up instructions + // incrementally, one command at a time, rather than loading a program. + let mut module: Module = Module::default(); + module.extra.n_qubits = 1; + + let mut machine = PPVM::default(); + machine.load(&module)?; + machine.init()?; + + // First command: X on q0 (|0> -> |1>). + let x = [ + PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))), + PPVMInstruction::Circuit(CircuitInstruction::X), + ]; + machine.execute_single_instruction(&x)?; + // No measurement yet. + assert!(machine.measurement_record().is_empty()); + + // Second command: measure q0. The X from the first command must persist, + // so the outcome is deterministically |1>. + let measure = [ + PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))), + PPVMInstruction::Circuit(CircuitInstruction::Measure), + ]; + machine.execute_single_instruction(&measure)?; + + let record = machine.measurement_record(); + assert_eq!(record.len(), 1); + assert_eq!(record[0].as_slice(), [MeasurementOutcome::One]); + Ok(()) + } + + #[test] + fn execute_single_instruction_propagates_engine_errors() -> eyre::Result<()> { + // The REPL relies on engine errors surfacing as `Err` (so it can print + // them and keep looping) rather than panicking. A gate with no qubit + // operand on the stack is one such propagating error. + // + // NOTE: an out-of-range qubit index (>= n_qubits) currently *panics* in + // the tableau rather than erroring, so the REPL command layer must + // bounds-check qubit indices before calling `execute`. + let mut module: Module = Module::default(); + module.extra.n_qubits = 1; + + let mut machine = PPVM::default(); + machine.load(&module)?; + machine.init()?; + + // `gate h` with nothing on the stack: `pop_qubit` fails. + let missing_operand = [PPVMInstruction::Circuit(CircuitInstruction::H)]; + assert!( + machine + .execute_single_instruction(&missing_operand) + .is_err() + ); + Ok(()) + } + + #[test] + fn state_string_renders_a_small_device() -> eyre::Result<()> { + let source = "device circuit.n_qubits 2;\nfn @main() { ret }\n"; + let mut machine = PPVM::default(); + machine.load_program(source)?; + machine.init()?; + + let rendered = machine.state_string(); + assert!( + !rendered.is_empty(), + "state_string should render the tableau" + ); + // PPVM delegates to the circuit. + assert_eq!(rendered, machine.circuit.state_string()); + Ok(()) + } + // ─── Parser-driven entry points ─────────────────────────────────────── #[test] From f943ffaaceef38b7a74d1697b8fa4436c8bf1479 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 17:27:54 +0200 Subject: [PATCH 56/95] Add REPL --- crates/ppvm-cli/src/main.rs | 19 +- crates/ppvm-cli/src/repl.rs | 346 +++++++++++++++++++++++++++ crates/ppvm-vihaco/src/composite.rs | 129 +++++++++- crates/ppvm-vihaco/src/lib.rs | 5 + crates/vihaco-circuit-isa/src/lib.rs | 2 +- 5 files changed, 489 insertions(+), 12 deletions(-) create mode 100644 crates/ppvm-cli/src/repl.rs diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index ab727f416..c9cb5a007 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -5,6 +5,7 @@ use clap::{Parser, Subcommand}; use eyre::Result; mod commands; +mod repl; #[derive(Parser)] #[command(name = "ppvm")] @@ -14,8 +15,9 @@ pub struct Cli { #[arg(short, long, default_value = "1")] threads: usize, + /// Subcommand to run; with none, drops into the interactive REPL. #[command(subcommand)] - command: Commands, + command: Option, } #[derive(Subcommand)] @@ -92,30 +94,31 @@ fn main() -> Result<()> { ppvm_vihaco::shots::set_global_threads(cli.threads)?; match cli.command { - Commands::Parse { file, format } => { + None => repl::repl()?, + Some(Commands::Parse { file, format }) => { commands::parse(&file, format)?; } - Commands::Dump { + Some(Commands::Dump { file, output, force, - } => { + }) => { commands::dump(&file, output.as_deref(), force)?; } - Commands::Run { + Some(Commands::Run { file, shots, seed, output, quiet, format, - } => { + }) => { commands::run(&file, shots, seed, output.as_deref(), quiet, format)?; } - Commands::Debug { + Some(Commands::Debug { file, break_at_start, - } => { + }) => { commands::debug(&file, break_at_start)?; } } diff --git a/crates/ppvm-cli/src/repl.rs b/crates/ppvm-cli/src/repl.rs new file mode 100644 index 000000000..77754af69 --- /dev/null +++ b/crates/ppvm-cli/src/repl.rs @@ -0,0 +1,346 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Interactive REPL — a quantum-circuit playground. Allocate a fixed-size +//! device with `device N`, then apply gates and measurements one line at a time +//! against a single persistent machine, seeing measurement outcomes inline. + +use eyre::{Result, WrapErr, bail, eyre}; +use ppvm_vihaco::CircuitInstruction; +use ppvm_vihaco::composite::PPVM; +use ppvm_vihaco::measurements::MeasurementResult; +use std::io::{BufRead, Write}; + +/// How a gate command lowers: the engine instruction plus how many qubit and +/// float operands it consumes (qubits first, then floats — the order +/// `apply_circuit_instruction` expects). +struct GateSpec { + inst: CircuitInstruction, + qubits: usize, + floats: usize, +} + +/// Resolve a gate command name to its spec, or `None` if it is not a gate. +/// `TwoQubitPauliError` is intentionally absent — its tableau arm is `todo!()`. +fn gate_spec(name: &str) -> Option { + use CircuitInstruction::*; + let (inst, qubits, floats) = match name { + "x" => (X, 1, 0), + "y" => (Y, 1, 0), + "z" => (Z, 1, 0), + "h" => (H, 1, 0), + "s" => (S, 1, 0), + "sadj" => (SAdj, 1, 0), + "sqrtx" => (SqrtX, 1, 0), + "sqrty" => (SqrtY, 1, 0), + "sqrtxadj" => (SqrtXAdj, 1, 0), + "sqrtyadj" => (SqrtYAdj, 1, 0), + "t" => (T, 1, 0), + "tadj" => (TAdj, 1, 0), + "measure" => (Measure, 1, 0), + "reset" => (Reset, 1, 0), + "cnot" => (CNOT, 2, 0), + "cz" => (CZ, 2, 0), + "rx" => (RX, 1, 1), + "ry" => (RY, 1, 1), + "rz" => (RZ, 1, 1), + "r" => (R, 1, 2), + "rxx" => (RXX, 2, 1), + "ryy" => (RYY, 2, 1), + "rzz" => (RZZ, 2, 1), + "u3" => (U3, 1, 3), + "depolarize" => (Depolarize, 1, 1), + "depolarize2" => (Depolarize2, 2, 1), + "loss" => (Loss, 1, 1), + "paulierror" => (PauliError, 1, 3), + "correlatedloss" => (CorrelatedLoss, 2, 3), + _ => return None, + }; + Some(GateSpec { + inst, + qubits, + floats, + }) +} + +/// Whether the loop should continue prompting or exit. +enum Outcome { + Continue, + Quit, +} + +/// Launch the REPL on stdin/stdout. +pub fn repl() -> Result<()> { + let stdin = std::io::stdin(); + let mut input = stdin.lock(); + let mut output = std::io::stdout(); + repl_loop(&mut input, &mut output) +} + +/// Core REPL loop, generic over its IO so tests can drive it with scripted +/// input. Holds a single `Option` — `None` until `device N`. Command-level +/// errors are printed and the loop continues; only `quit`/`exit`/EOF exit. +fn repl_loop(input: &mut impl BufRead, output: &mut impl Write) -> Result<()> { + let mut machine: Option = None; + loop { + write!(output, "ppvm> ")?; + output.flush()?; + + let mut line = String::new(); + if input.read_line(&mut line)? == 0 { + // EOF: leave cleanly, ending the dangling prompt line. + writeln!(output)?; + break; + } + let line = line.trim(); + if line.is_empty() { + continue; + } + + match dispatch(line, &mut machine, output) { + Ok(Outcome::Continue) => {} + Ok(Outcome::Quit) => break, + Err(e) => writeln!(output, "error: {e}")?, + } + } + Ok(()) +} + +/// Parse and run one command line. +fn dispatch(line: &str, machine: &mut Option, output: &mut impl Write) -> Result { + let mut tokens = line.split_whitespace(); + let cmd = tokens.next().expect("line is non-empty after trim"); + let args: Vec<&str> = tokens.collect(); + + match cmd { + "quit" | "exit" => return Ok(Outcome::Quit), + "help" => print_help(output)?, + "device" => cmd_device(&args, machine, output)?, + "show" => cmd_show(machine, output)?, + _ => cmd_gate(cmd, &args, machine, output)?, + } + Ok(Outcome::Continue) +} + +/// `device N` — (re)create a fresh N-qubit device, discarding any prior state. +fn cmd_device(args: &[&str], machine: &mut Option, output: &mut impl Write) -> Result<()> { + let [n] = args else { + bail!("usage: device N"); + }; + let n: usize = n + .parse() + .wrap_err_with(|| format!("invalid qubit count {n:?}"))?; + if n == 0 { + bail!("device must have at least 1 qubit"); + } + + let existed = machine.is_some(); + *machine = Some(PPVM::with_qubits(n)?); + if existed { + writeln!( + output, + "ok: fresh {n}-qubit device (previous state discarded)" + )?; + } else { + writeln!(output, "ok: fresh {n}-qubit device")?; + } + Ok(()) +} + +/// `show` — print the current tableau / Pauli state. +fn cmd_show(machine: &mut Option, output: &mut impl Write) -> Result<()> { + let machine = require_device(machine)?; + writeln!(output, "{}", machine.state_string())?; + Ok(()) +} + +/// A gate command: ` [param…]`. Applies the gate and, if it +/// produced any measurement outcomes, prints them as `=> `. +fn cmd_gate( + name: &str, + args: &[&str], + machine: &mut Option, + output: &mut impl Write, +) -> Result<()> { + let spec = gate_spec(name).ok_or_else(|| eyre!("unknown command {name:?}; try \"help\""))?; + let machine = require_device(machine)?; + + let expected = spec.qubits + spec.floats; + if args.len() != expected { + bail!( + "{name} takes {} qubit(s) and {} param(s), got {} argument(s)", + spec.qubits, + spec.floats, + args.len() + ); + } + + let (qubit_args, param_args) = args.split_at(spec.qubits); + let qubits = qubit_args + .iter() + .map(|t| { + t.parse::() + .wrap_err_with(|| format!("invalid qubit index {t:?}")) + }) + .collect::>>()?; + let params = param_args + .iter() + .map(|t| { + t.parse::() + .wrap_err_with(|| format!("invalid parameter {t:?}")) + }) + .collect::>>()?; + + // New entries in the measurement record are this command's outcomes. + let before = machine.measurement_record().len(); + machine.apply_circuit_instruction(spec.inst, &qubits, ¶ms)?; + let record = machine.measurement_record(); + if record.len() > before { + writeln!(output, "=> {}", format_outcomes(&record[before..]))?; + } + Ok(()) +} + +/// Borrow the device, or error if none has been allocated yet. +fn require_device(machine: &mut Option) -> Result<&mut PPVM> { + machine + .as_mut() + .ok_or_else(|| eyre!("no device; run \"device N\" first")) +} + +/// Render measurement outcomes as a flat bit string: `0`/`1`, lost qubit = `2` +/// (the same convention as the `run` command's output). +fn format_outcomes(records: &[MeasurementResult]) -> String { + records + .iter() + .flatten() + .map(|outcome| char::from(b'0' + *outcome as u8)) + .collect() +} + +fn print_help(output: &mut impl Write) -> Result<()> { + writeln!(output, "meta:")?; + writeln!( + output, + " device N (re)create a fresh N-qubit device" + )?; + writeln!(output, " show print the current state")?; + writeln!(output, " help show this help")?; + writeln!(output, " quit | exit | EOF leave the REPL")?; + writeln!( + output, + "gates ( = qubit index, angles/probs are floats):" + )?; + writeln!( + output, + " x y z h s sadj sqrtx sqrty sqrtxadj sqrtyadj t tadj reset measure " + )?; + writeln!(output, " cnot | cz ")?; + writeln!(output, " rx ry rz | r ")?; + writeln!(output, " u3 ")?; + writeln!(output, " rxx ryy rzz ")?; + writeln!( + output, + " depolarize loss

| depolarize2

" + )?; + writeln!( + output, + " paulierror | correlatedloss " + )?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Drive the loop with a scripted session, returning everything it wrote. + fn session(script: &str) -> String { + let mut input = std::io::Cursor::new(script.as_bytes().to_vec()); + let mut output: Vec = Vec::new(); + repl_loop(&mut input, &mut output).unwrap(); + String::from_utf8(output).unwrap() + } + + #[test] + fn device_then_x_then_measure_is_one() { + let out = session("device 1\nx 0\nmeasure 0\nquit\n"); + assert!(out.contains("ok: fresh 1-qubit device"), "{out}"); + assert!(out.contains("=> 1"), "X|0> measured should be 1:\n{out}"); + } + + #[test] + fn fresh_measure_is_zero() { + let out = session("device 1\nmeasure 0\nquit\n"); + assert!(out.contains("=> 0"), "|0> measured should be 0:\n{out}"); + } + + #[test] + fn gate_before_device_errors_and_continues() { + // The error is printed and the loop keeps going: the later device+measure + // still works. + let out = session("x 0\ndevice 1\nmeasure 0\nquit\n"); + assert!(out.contains("no device"), "{out}"); + assert!( + out.contains("=> 0"), + "loop should continue after the error:\n{out}" + ); + } + + #[test] + fn device_twice_reports_discarded_state() { + let out = session("device 1\ndevice 2\nquit\n"); + assert!(out.contains("previous state discarded"), "{out}"); + } + + #[test] + fn show_renders_the_state() { + let out = session("device 1\nshow\nquit\n"); + let expected = PPVM::with_qubits(1).unwrap().state_string(); + assert!(out.contains(expected.trim()), "show output missing:\n{out}"); + } + + #[test] + fn unknown_command_errors_and_continues() { + let out = session("device 1\nbogus 0\nmeasure 0\nquit\n"); + assert!(out.contains("unknown command"), "{out}"); + assert!(out.contains("=> 0"), "loop should continue:\n{out}"); + } + + #[test] + fn eof_exits_cleanly() { + // No quit line: the session ends at EOF without hanging or erroring. + let out = session("device 1\n"); + assert!(out.contains("ok: fresh 1-qubit device"), "{out}"); + } + + #[test] + fn cnot_respects_control_and_target_order() { + // x 0 -> |10>; cnot 0 1 (control q0=1) flips q1 -> |11>. Both measure 1. + // If control/target were swapped, q1 would stay 0. + let out = session("device 2\nx 0\ncnot 0 1\nmeasure 0\nmeasure 1\nquit\n"); + // Each scripted line follows a "ppvm> " prompt with no echo, so a result + // line reads "ppvm> => 1"; take the text after "=> ". + let measurements: Vec<&str> = out.lines().filter_map(|l| l.split("=> ").nth(1)).collect(); + assert_eq!(measurements, vec!["1", "1"], "{out}"); + } + + #[test] + fn two_qubit_float_gate_runs() { + // rxx (2 qubits + 1 float) should parse and apply without error. + let out = session("device 2\nrxx 0 1 0.5\nquit\n"); + assert!(!out.contains("error"), "rxx should run cleanly:\n{out}"); + } + + #[test] + fn bad_arity_errors() { + let out = session("device 1\nx\nquit\n"); + assert!(out.contains("takes"), "arity error expected:\n{out}"); + } + + #[test] + fn out_of_range_qubit_errors_not_panics() { + let out = session("device 1\nx 3\nquit\n"); + assert!(out.contains("out of range"), "{out}"); + } +} diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index d579aede5..404de0f1e 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -177,8 +177,8 @@ impl PPVM { } RXX | RYY | RZZ | Depolarize2 => { let theta = self.pop_f64()?; - let q0 = self.pop_qubit()?; let q1 = self.pop_qubit()?; + let q0 = self.pop_qubit()?; Ok(CircuitMessage::TwoQubitAndFloat(q0, q1, theta)) } R => { @@ -207,8 +207,8 @@ impl PPVM { let p2 = self.pop_f64()?; let p1 = self.pop_f64()?; let p0 = self.pop_f64()?; - let q0 = self.pop_qubit()?; let q1 = self.pop_qubit()?; + let q0 = self.pop_qubit()?; Ok(CircuitMessage::TwoQubitAndFloatArr3(q0, q1, [p0, p1, p2])) } TwoQubitPauliError => { @@ -216,8 +216,8 @@ impl PPVM { for p in ps.iter_mut().rev() { *p = self.pop_f64()?; } - let q0 = self.pop_qubit()?; let q1 = self.pop_qubit()?; + let q0 = self.pop_qubit()?; Ok(CircuitMessage::TwoQubitAndFloatArr15(q0, q1, ps)) } } @@ -315,6 +315,56 @@ impl PPVM { self.circuit.state_string() } + /// Build a fresh, initialized `n_qubits`-qubit device with no code. The + /// REPL's `device` command uses this to (re)create the machine. Errors if + /// `n_qubits` is zero (a device must have at least one qubit). + pub fn with_qubits(n_qubits: usize) -> eyre::Result { + let mut machine = Self::default(); + let mut module = vihaco::module::Module::< + PPVMInstruction, + Value, + vihaco::Type, + PPVMDeviceInfo, + >::default(); + module.extra.n_qubits = n_qubits; + machine.load(&module)?; + machine.init()?; + Ok(machine) + } + + /// Lower a single circuit instruction — qubit operands first, then float + /// params, per the push-in-order / pop-in-reverse convention — and execute + /// it against the persistent state. Qubit indices are bounds-checked against + /// the device size first, because the tableau panics (rather than erroring) + /// on an out-of-range qubit. + pub fn apply_circuit_instruction( + &mut self, + inst: CircuitInstruction, + qubits: &[usize], + params: &[f64], + ) -> eyre::Result<()> { + let n_qubits = self.loader.module.extra.n_qubits; + for &q in qubits { + if q >= n_qubits { + eyre::bail!("qubit {q} out of range for {n_qubits}-qubit device"); + } + } + + let mut instrs = Vec::with_capacity(qubits.len() + params.len() + 1); + for &q in qubits { + instrs.push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + Value::U64(q as u64), + ))); + } + for &p in params { + instrs.push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + Value::F64(p), + ))); + } + instrs.push(PPVMInstruction::Circuit(inst)); + self.execute_single_instruction(&instrs) + } + fn execute_effects(&mut self, inst: Instruction) -> eyre::Result> { log::debug!("exec inst: {:?}, stack: {:?}", inst, self.cpu.stack()); match inst { @@ -737,6 +787,79 @@ mod tests { Ok(()) } + #[test] + fn resolve_circuit_pops_operands_in_reverse_of_push_order() -> eyre::Result<()> { + // Convention: operands are pushed in argument order (q0, q1, then any + // floats) and popped in reverse. So every two-qubit gate 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(); + module.extra.n_qubits = 8; + let mut machine = PPVM::default(); + machine.load(&module)?; + machine.init()?; + + // CNOT: push q0=2, q1=5. + machine.cpu.stack_push(Value::U32(2)); + machine.cpu.stack_push(Value::U32(5)); + assert_eq!( + machine.resolve_circuit(&CircuitInstruction::CNOT)?, + CircuitMessage::TwoQubit(2, 5) + ); + + // RXX: push q0=2, q1=5, theta — same qubit order as CNOT. + machine.cpu.stack_push(Value::U32(2)); + machine.cpu.stack_push(Value::U32(5)); + machine.cpu.stack_push(Value::F64(0.3)); + assert_eq!( + machine.resolve_circuit(&CircuitInstruction::RXX)?, + CircuitMessage::TwoQubitAndFloat(2, 5, 0.3) + ); + + // CorrelatedLoss: push q0=2, q1=5, p0, p1, p2. + machine.cpu.stack_push(Value::U32(2)); + machine.cpu.stack_push(Value::U32(5)); + machine.cpu.stack_push(Value::F64(0.1)); + machine.cpu.stack_push(Value::F64(0.2)); + machine.cpu.stack_push(Value::F64(0.3)); + assert_eq!( + machine.resolve_circuit(&CircuitInstruction::CorrelatedLoss)?, + CircuitMessage::TwoQubitAndFloatArr3(2, 5, [0.1, 0.2, 0.3]) + ); + Ok(()) + } + + #[test] + fn with_qubits_builds_an_initialized_device() -> eyre::Result<()> { + use crate::measurements::MeasurementOutcome; + + let mut m = PPVM::with_qubits(2)?; + // The device is ready to take instructions immediately. + m.apply_circuit_instruction(CircuitInstruction::X, &[0], &[])?; + m.apply_circuit_instruction(CircuitInstruction::Measure, &[0], &[])?; + let record = m.measurement_record(); + assert_eq!(record.len(), 1); + assert_eq!(record[0].as_slice(), [MeasurementOutcome::One]); + Ok(()) + } + + #[test] + fn with_qubits_zero_is_an_error() { + assert!(PPVM::with_qubits(0).is_err()); + } + + #[test] + fn apply_circuit_instruction_bounds_checks_qubits() -> eyre::Result<()> { + // q1 is out of range on a 1-qubit device: this must error rather than + // panic in the tableau. + let mut m = PPVM::with_qubits(1)?; + let err = m + .apply_circuit_instruction(CircuitInstruction::X, &[1], &[]) + .unwrap_err(); + assert!(err.to_string().contains("out of range"), "got: {err}"); + Ok(()) + } + // ─── Parser-driven entry points ─────────────────────────────────────── #[test] diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 01e5b839b..803efccf5 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -8,6 +8,11 @@ pub mod measurements; pub mod shots; mod syntax; +/// Re-exported so consumers (e.g. the CLI REPL) can name gates for +/// [`composite::PPVM::apply_circuit_instruction`] without depending on the ISA +/// crate directly. +pub use vihaco_circuit_isa::CircuitInstruction; + use chumsky::Parser; use vihaco::syntax::{ParsedModule, Resolve}; use vihaco::{Type, Value, module::Module}; diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs index ee5fd59ed..28438072d 100644 --- a/crates/vihaco-circuit-isa/src/lib.rs +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -124,7 +124,7 @@ impl std::fmt::Display for CircuitInstruction { } } -#[derive(Debug, Clone, Message)] +#[derive(Debug, Clone, PartialEq, Message)] pub enum CircuitMessage { Qubit(usize), // X, Y, Z, ... QubitAndFloat(usize, f64), // RX, depolarize, ... From b62884c2bcf3d733ba9ec0a74f87ce3c201b11b2 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 3 Jun 2026 18:14:50 +0200 Subject: [PATCH 57/95] Use rustyline for history --- Cargo.lock | 89 +++++++++++++++++++++++++++++++++++++ crates/ppvm-cli/Cargo.toml | 1 + crates/ppvm-cli/src/repl.rs | 59 +++++++++++++++++++++--- 3 files changed, 144 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33b45a08b..8af865c86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,6 +238,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.10.0" @@ -344,6 +350,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "codespan" version = "0.13.1" @@ -534,6 +549,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "endian-type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" + [[package]] name = "env_filter" version = "1.0.1" @@ -573,6 +594,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "eyre" version = "0.6.12" @@ -721,6 +748,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -896,6 +932,27 @@ dependencies = [ "autocfg", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num" version = "0.4.3" @@ -1091,6 +1148,7 @@ dependencies = [ "clap", "eyre", "ppvm-vihaco", + "rustyline", ] [[package]] @@ -1342,6 +1400,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "radix_trie" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.9.4" @@ -1503,6 +1571,27 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "rustyline" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys", +] + [[package]] name = "same-file" version = "1.0.6" diff --git a/crates/ppvm-cli/Cargo.toml b/crates/ppvm-cli/Cargo.toml index 4ba3d8841..4ff2b6937 100644 --- a/crates/ppvm-cli/Cargo.toml +++ b/crates/ppvm-cli/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" clap = { version = "4.6.1", features = ["derive"] } eyre = "0.6.12" ppvm-vihaco = { version = "0.1.0", path = "../ppvm-vihaco", features = ["rayon"] } +rustyline = "18.0.0" [[bin]] name = "ppvm" diff --git a/crates/ppvm-cli/src/repl.rs b/crates/ppvm-cli/src/repl.rs index 77754af69..9dfedeb5c 100644 --- a/crates/ppvm-cli/src/repl.rs +++ b/crates/ppvm-cli/src/repl.rs @@ -9,7 +9,12 @@ use eyre::{Result, WrapErr, bail, eyre}; use ppvm_vihaco::CircuitInstruction; use ppvm_vihaco::composite::PPVM; use ppvm_vihaco::measurements::MeasurementResult; -use std::io::{BufRead, Write}; +use rustyline::DefaultEditor; +use rustyline::error::ReadlineError; +#[cfg(test)] +use std::io::BufRead; +use std::io::Write; +use std::path::PathBuf; /// How a gate command lowers: the engine instruction plus how many qubit and /// float operands it consumes (qubits first, then floats — the order @@ -69,17 +74,61 @@ enum Outcome { Quit, } -/// Launch the REPL on stdin/stdout. +/// Launch the interactive REPL with line editing and command history. +/// History recall (up/down arrows), cursor movement, and Ctrl-R search come +/// from rustyline; the per-command logic is the same `dispatch` the scripted +/// tests drive through `repl_loop`. pub fn repl() -> Result<()> { - let stdin = std::io::stdin(); - let mut input = stdin.lock(); + let mut rl = DefaultEditor::new()?; + let history = history_path(); + if let Some(path) = &history { + let _ = rl.load_history(path); // best-effort: a missing file is fine + } + + let mut machine: Option = None; let mut output = std::io::stdout(); - repl_loop(&mut input, &mut output) + loop { + match rl.readline("ppvm> ") { + Ok(line) => { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + let _ = rl.add_history_entry(trimmed); + match dispatch(trimmed, &mut machine, &mut output) { + Ok(Outcome::Continue) => {} + Ok(Outcome::Quit) => break, + Err(e) => writeln!(output, "error: {e}")?, + } + } + // Ctrl-C: abandon the current line, keep the session (shell-like). + Err(ReadlineError::Interrupted) => continue, + // Ctrl-D on an empty line: leave cleanly. + Err(ReadlineError::Eof) => break, + Err(e) => return Err(e).wrap_err("readline failed"), + } + } + + if let Some(path) = &history { + let _ = rl.save_history(path); // best-effort: don't fail the session + } + Ok(()) +} + +/// Where to persist history across sessions: `$HOME/.ppvm_history`. `None` +/// (so history is session-only) when there's no `HOME` — e.g. on Windows, +/// where you'd reach for the `dirs` crate instead. +fn history_path() -> Option { + std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".ppvm_history")) } /// Core REPL loop, generic over its IO so tests can drive it with scripted /// input. Holds a single `Option` — `None` until `device N`. Command-level /// errors are printed and the loop continues; only `quit`/`exit`/EOF exit. +/// +/// Test-only: the interactive entry point is `repl`, which runs the same +/// `dispatch` under a rustyline editor for history and line editing. +#[cfg(test)] fn repl_loop(input: &mut impl BufRead, output: &mut impl Write) -> Result<()> { let mut machine: Option = None; loop { From 51039f4734fa63657e22bb85cec54741299fcd46 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 9 Jun 2026 14:46:04 +0200 Subject: [PATCH 58/95] Add configurable backend kind and no-op trace truncate --- crates/ppvm-vihaco/src/bytecode.rs | 166 ++++++++++++++++++++++++--- crates/ppvm-vihaco/src/composite.rs | 21 ++++ crates/ppvm-vihaco/src/syntax.rs | 113 +++++++++++++++++- crates/vihaco-circuit-isa/src/lib.rs | 14 ++- 4 files changed, 299 insertions(+), 15 deletions(-) diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs index 4aef64e0d..5cc42d8c4 100644 --- a/crates/ppvm-vihaco/src/bytecode.rs +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -14,15 +14,19 @@ use std::io::{Read, Write}; use vihaco::instruction::{FromBytes, WriteBytes}; use crate::PPVMModule; -use crate::composite::{PPVM_MAGIC, PPVMDeviceInfo, PPVMInstruction}; +use crate::composite::{BackendKind, PPVM_MAGIC, PPVMDeviceInfo, PPVMInstruction}; /// Current `.ssb` format version. The reader rejects any other version. pub const PPVM_BYTECODE_VERSION: u16 = 1; -/// Byte length of the fixed v1 header (magic 4, version 2, header_size 4, -/// n_qubits 4, coefficient_threshold 8) and the offset where the strings -/// section begins. -const HEADER_SIZE: u32 = 4 + 2 + 4 + 4 + 8; +/// 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 +/// populated; the reader uses `header_size` to skip to the strings section. +/// +/// Field widths (bytes): magic(4) + version(2) + header_size(4) + n_qubits(4) +/// + coefficient_threshold(8) + backend(1) + max_pauli_weight_present(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. pub fn write_module(module: &PPVMModule, w: &mut W) -> eyre::Result<()> { @@ -53,12 +57,49 @@ pub fn write_module(module: &PPVMModule, w: &mut W) -> eyre::Result<() let n_qubits = u32::try_from(info.n_qubits) .map_err(|_| eyre::eyre!("n_qubits {} does not fit in u32", info.n_qubits))?; + // The header is `FIXED_HEADER_SIZE` bytes plus, when an observable is + // present, a u32 length followed by its UTF-8 bytes. + let observable_bytes: &[u8] = info + .observable + .as_ref() + .map(String::as_bytes) + .unwrap_or(&[]); + let observable_present: u8 = u8::from(info.observable.is_some()); + let observable_len = u32::try_from(observable_bytes.len()).map_err(|_| { + eyre::eyre!( + "observable length {} does not fit in u32", + observable_bytes.len() + ) + })?; + let observable_trailer: u32 = if info.observable.is_some() { + 4 + observable_len + } else { + 0 + }; + let header_size = FIXED_HEADER_SIZE + observable_trailer; + // Header. w.write_all(&PPVM_MAGIC.to_le_bytes())?; w.write_all(&PPVM_BYTECODE_VERSION.to_le_bytes())?; - w.write_all(&HEADER_SIZE.to_le_bytes())?; + w.write_all(&header_size.to_le_bytes())?; w.write_all(&n_qubits.to_le_bytes())?; w.write_all(&info.coefficient_threshold.to_le_bytes())?; + w.write_all(&[backend_to_u8(info.backend)])?; + let (mpw_present, mpw_value) = match info.max_pauli_weight { + Some(w) => ( + 1u8, + u64::try_from(w) + .map_err(|_| eyre::eyre!("max_pauli_weight {} does not fit in u64", w))?, + ), + None => (0u8, 0u64), + }; + w.write_all(&[mpw_present])?; + w.write_all(&mpw_value.to_le_bytes())?; + w.write_all(&[observable_present])?; + if info.observable.is_some() { + w.write_all(&observable_len.to_le_bytes())?; + w.write_all(observable_bytes)?; + } // Strings section: count, then each entry as len-prefixed UTF-8. let string_count = @@ -97,15 +138,49 @@ pub fn read_module(r: &mut R) -> eyre::Result { let header_size = read_u32(r)?; let n_qubits = read_u32(r)? as usize; let coefficient_threshold = read_f64(r)?; + let backend = backend_from_u8(read_u8(r)?)?; + let mpw_present = read_u8(r)?; + let mpw_value = read_u64(r)?; + let max_pauli_weight = match mpw_present { + 0 => None, + 1 => Some(usize::try_from(mpw_value).map_err(|_| { + eyre::eyre!("max_pauli_weight {mpw_value} does not fit in usize on this platform") + })?), + other => { + return Err(eyre::eyre!( + "invalid max_pauli_weight presence byte {other}" + )); + } + }; + let observable_present = read_u8(r)?; + let observable = match observable_present { + 0 => None, + 1 => { + let len = read_u32(r)? as usize; + let mut bytes = vec![0u8; len]; + r.read_exact(&mut bytes)?; + Some(String::from_utf8(bytes)?) + } + other => { + return Err(eyre::eyre!("invalid observable presence byte {other}")); + } + }; - // Sections begin at `header_size`; skip any header bytes beyond v1's fixed - // fields (forward compat / self-description). - if header_size < HEADER_SIZE { + // Sections begin at `header_size`; skip any header bytes beyond what this + // reader knows about (forward compat / self-description). + let consumed = FIXED_HEADER_SIZE + + if observable.is_some() { + 4 + u32::try_from(observable.as_deref().unwrap().len()) + .map_err(|_| eyre::eyre!("observable length does not fit in u32"))? + } else { + 0 + }; + if header_size < consumed { return Err(eyre::eyre!( - "header_size {header_size} smaller than minimum {HEADER_SIZE}" + "header_size {header_size} smaller than the {consumed} bytes already consumed" )); } - skip_bytes(r, u64::from(header_size - HEADER_SIZE))?; + skip_bytes(r, u64::from(header_size - consumed))?; // Don't pre-allocate from an untrusted count; grow as entries are read. let string_count = read_u32(r)?; @@ -128,6 +203,9 @@ pub fn read_module(r: &mut R) -> eyre::Result { magic, n_qubits, coefficient_threshold, + backend, + observable, + max_pauli_weight, }, strings, code, @@ -135,6 +213,23 @@ pub fn read_module(r: &mut R) -> eyre::Result { }) } +fn backend_to_u8(backend: BackendKind) -> u8 { + match backend { + BackendKind::Tableau => 0, + BackendKind::PauliSum => 1, + BackendKind::LossyPauliSum => 2, + } +} + +fn backend_from_u8(byte: u8) -> eyre::Result { + match byte { + 0 => Ok(BackendKind::Tableau), + 1 => Ok(BackendKind::PauliSum), + 2 => Ok(BackendKind::LossyPauliSum), + other => Err(eyre::eyre!("invalid backend tag {other}")), + } +} + /// Serialize a module to an owned byte vector. pub fn module_to_bytes(module: &PPVMModule) -> eyre::Result> { let mut buf = Vec::new(); @@ -162,6 +257,12 @@ pub fn compile_to_bytes(source: &str) -> eyre::Result> { module_to_bytes(&module) } +fn read_u8(r: &mut R) -> eyre::Result { + let mut b = [0u8; 1]; + r.read_exact(&mut b)?; + Ok(b[0]) +} + fn read_u16(r: &mut R) -> eyre::Result { let mut b = [0u8; 2]; r.read_exact(&mut b)?; @@ -174,6 +275,12 @@ fn read_u32(r: &mut R) -> eyre::Result { Ok(u32::from_le_bytes(b)) } +fn read_u64(r: &mut R) -> eyre::Result { + let mut b = [0u8; 8]; + r.read_exact(&mut b)?; + Ok(u64::from_le_bytes(b)) +} + fn read_f64(r: &mut R) -> eyre::Result { let mut b = [0u8; 8]; r.read_exact(&mut b)?; @@ -211,6 +318,37 @@ mod tests { assert_eq!(back, m); } + #[test] + fn round_trips_paulisum_device_info() { + let mut m = empty_module(); + m.extra.n_qubits = 6; + m.extra.backend = BackendKind::PauliSum; + m.extra.observable = Some("ZZIIII".to_string()); + m.extra.max_pauli_weight = Some(8); + + let mut buf = Vec::new(); + write_module(&m, &mut buf).unwrap(); + let back = read_module(&mut buf.as_slice()).unwrap(); + + assert_eq!(back, m); + } + + #[test] + fn round_trips_lossy_backend_without_observable() { + let mut m = empty_module(); + m.extra.n_qubits = 4; + m.extra.backend = BackendKind::LossyPauliSum; + // observable and max_pauli_weight stay None — verifies the absent path. + + let mut buf = Vec::new(); + write_module(&m, &mut buf).unwrap(); + let back = read_module(&mut buf.as_slice()).unwrap(); + + assert_eq!(back, m); + assert_eq!(back.extra.observable, None); + assert_eq!(back.extra.max_pauli_weight, None); + } + #[test] fn round_trips_code() { use vihaco_circuit_isa::CircuitInstruction; @@ -247,9 +385,11 @@ mod tests { // Simulate a larger header: 4 padding bytes after the fixed fields, // with header_size bumped to match. The reader must skip to it. - buf[6..10].copy_from_slice(&(HEADER_SIZE + 4).to_le_bytes()); + // (This test uses an empty observable, so the on-disk header size + // equals FIXED_HEADER_SIZE.) + buf[6..10].copy_from_slice(&(FIXED_HEADER_SIZE + 4).to_le_bytes()); for i in 0..4 { - buf.insert(HEADER_SIZE as usize + i, 0x00); + buf.insert(FIXED_HEADER_SIZE as usize + i, 0x00); } let back = read_module(&mut buf.as_slice()).unwrap(); diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 404de0f1e..4cff61da3 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -18,11 +18,26 @@ use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; 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)] +pub enum BackendKind { + #[default] + Tableau, + PauliSum, + #[token = "lossy_paulisum"] + LossyPauliSum, +} + #[derive(Debug, Clone, PartialEq)] pub struct PPVMDeviceInfo { pub magic: u32, pub n_qubits: usize, pub coefficient_threshold: f64, + pub backend: BackendKind, + pub observable: Option, + pub max_pauli_weight: Option, } impl Default for PPVMDeviceInfo { @@ -31,6 +46,9 @@ impl Default for PPVMDeviceInfo { magic: PPVM_MAGIC, n_qubits: 0, coefficient_threshold: 1e-10, + backend: BackendKind::default(), + observable: None, + max_pauli_weight: None, } } } @@ -220,6 +238,9 @@ impl PPVM { let q0 = self.pop_qubit()?; Ok(CircuitMessage::TwoQubitAndFloatArr15(q0, q1, ps)) } + Trace | Truncate => Err(eyre::eyre!( + "{inst} operand resolution not yet wired (Phase 2 Task 7)" + )), } } diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index 012eb0d2d..87c8f700c 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -12,7 +12,7 @@ use vihaco::{ use vihaco_circuit_isa::CircuitInstruction; use vihaco_parser_core::Parse; -use crate::composite::{PPVMDeviceInfo, PPVMInstruction}; +use crate::composite::{BackendKind, PPVMDeviceInfo, PPVMInstruction}; #[derive(Debug, Clone, PartialEq, vihaco_parser::Parse)] #[head = "device "] @@ -24,6 +24,18 @@ pub enum PPVMHeader { #[token = "circuit.coefficient_threshold"] #[delimiters(open = "", close = "", separator = "")] CoefficientThrehsold(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 = "")] + MaxPauliWeight(usize), } #[derive(Debug, Default)] @@ -44,6 +56,15 @@ impl PPVMResolver { PPVMHeader::CoefficientThrehsold(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); + } } Ok(()) } @@ -350,6 +371,51 @@ mod tests { 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(); @@ -364,6 +430,35 @@ mod tests { 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] @@ -494,6 +589,22 @@ mod tests { 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. diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs index 28438072d..20ea0c337 100644 --- a/crates/vihaco-circuit-isa/src/lib.rs +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -10,6 +10,8 @@ use vihaco_parser::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, @@ -76,6 +78,8 @@ impl std::fmt::Display for CircuitInstruction { use CircuitInstruction::*; match self { TwoQubitPauliError => write!(f, "TwoQubitPauliError"), + Truncate => write!(f, "Truncate"), + Trace => write!(f, "Trace"), X => write!(f, "X"), Y => write!(f, "Y"), @@ -126,6 +130,7 @@ impl std::fmt::Display for CircuitInstruction { #[derive(Debug, Clone, PartialEq, Message)] pub enum CircuitMessage { + None, // Truncate (no operand) Qubit(usize), // X, Y, Z, ... QubitAndFloat(usize, f64), // RX, depolarize, ... QubitAndTwoFloats(usize, f64, f64), // R @@ -135,6 +140,7 @@ pub enum CircuitMessage { QubitAndFloatArr3(usize, [f64; 3]), // PauliError TwoQubitAndFloatArr3(usize, usize, [f64; 3]), // Correlated loss TwoQubitAndFloatArr15(usize, usize, [f64; 15]), // TwoQubitPauliError + PauliPatternStr(u32), // Trace (string-table addr) // batched instructions QubitBatch(SmallVec<[usize; 8]>), // X, Y, Z, ... @@ -167,6 +173,8 @@ mod tests { /// newly added variant is automatically covered. const ALL: &[CircuitInstruction] = &[ TwoQubitPauliError, + Truncate, + Trace, X, Y, Z, @@ -230,9 +238,11 @@ mod tests { #[test] fn parses_t_family_without_prefix_collision() { - // `t` is a prefix of both `tadj` and `twoqubitpaulierror`. + // `t` is a prefix of `tadj`, `trace`, `truncate`, and `twoqubitpaulierror`. assert_eq!(parse("t"), T); assert_eq!(parse("tadj"), TAdj); + assert_eq!(parse("trace"), Trace); + assert_eq!(parse("truncate"), Truncate); assert_eq!(parse("twoqubitpaulierror"), TwoQubitPauliError); } @@ -265,6 +275,8 @@ mod tests { 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"); From 220d6ca9eb921dee1ae0c827d19abe5e904c19dc Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 9 Jun 2026 15:47:25 +0200 Subject: [PATCH 59/95] Draft backend dispatch --- crates/ppvm-vihaco/src/component.rs | 396 ++++++++++++++++++++++++++-- crates/ppvm-vihaco/src/composite.rs | 21 +- 2 files changed, 387 insertions(+), 30 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 0317bf146..8e6e9a6d8 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 +use crate::composite::{BackendKind, PPVMDeviceInfo}; use crate::measurements::MeasurementEffect; use crate::measurements::MeasurementOutcome; use bitvec::view::BitView; @@ -9,10 +10,37 @@ use eyre::{Result, eyre}; use num::PrimInt; use num::complex::Complex64; use ppvm_runtime::config::fx64hash::Byte8F64; +use ppvm_runtime::config::indexmap::ByteFxHashF64; +use ppvm_runtime::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; use ppvm_tableau::prelude::*; use vihaco::{Effects, component, observe}; use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; +/// Truncation strategy used by every `PauliSum` / `LossyPauliSum` size bucket. +/// Coefficient-threshold pruning is always on; the Pauli-weight cap is set per +/// run from the header (defaults to `usize::MAX` = no cap). +type PauliSumStrategy = CombinedStrategy; + +/// `PauliSum`'s `T` for the lossless backend: `[u8; N]` storage, fx hash, +/// f64 coefficients, the strategy above. +type PauliSumConfig = ByteFxHashF64; + +/// Same as `PauliSumConfig` but with `LossyPauliWord` as the word type, so the +/// loss-channel methods are dispatchable on the resulting `PauliSum`. +/// `LossyPauliWord`'s second type parameter (hasher) defaults to +/// `fxhash::FxBuildHasher`, matching `ByteFxHashF64`'s internal hasher. +type LossyPauliSumConfig = + ByteFxHashF64>; + +/// Build a `PauliSumStrategy` value from a `PPVMDeviceInfo`. Pulled out so the +/// six size-bucket constructors don't each repeat the strategy spelling. +fn paulisum_strategy(info: &PPVMDeviceInfo) -> PauliSumStrategy { + CombinedStrategy( + CoefficientThreshold(info.coefficient_threshold), + MaxPauliWeight(info.max_pauli_weight.unwrap_or(usize::MAX)), + ) +} + macro_rules! batch_for { ($tab:expr, $method:ident, $addrs:expr) => { for addr in $addrs { $tab.$method(*addr); } @@ -216,7 +244,101 @@ where } } -pub enum Circuit { +/// PauliSum-backed executor (Heisenberg picture). Holds a `PauliSum` and +/// answers the same `CircuitInstruction` vocabulary as `CircuitExecutor`, but +/// without measurement / reset support. +/// +/// Skeleton only — `execute_instruction` is a no-op until Task 5 fills in the +/// gate-dispatch table per the plan's Gate Support Matrix. Not yet wired into +/// the `Circuit` enum (that happens in Task 4). +pub struct PauliSumExecutor> { + pub state: PauliSum, +} + +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] +impl PauliSumExecutor +where + T: Config, +{ + fn execute( + &mut self, + inst: CircuitInstruction, + msg: CircuitMessage, + ) -> Result> { + self.execute_instruction(&inst, &msg) + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + let _ = (inst, msg, &mut self.state); + // TODO(Task 5): dispatch (inst, msg) onto self.state per the Gate + // Support Matrix; reject Measure / Reset / Loss / CorrelatedLoss with + // a clear "not supported on PauliSum backend" error. + Ok(Effects::None) + } +} + +impl vihaco::Reset for PauliSumExecutor +where + T: Config, +{ + fn reset(&mut self) { + // TODO(Task 5/6): rebuild self.state from the seeded observable. + } +} + +/// LossyPauliSum-backed executor. Same shape as `PauliSumExecutor`; the +/// distinction lives at the dispatch level (this executor accepts `Loss` / +/// `CorrelatedLoss`) and at the concrete `T` used by the enclosing +/// `Circuit::LossyPauliSum` variant (a Config whose `PauliWordType` is +/// `LossyPauliWord`, picked in Task 4). +/// +/// Skeleton only — Task 5 fills in the dispatch. +pub struct LossyPauliSumExecutor> { + pub state: PauliSum, +} + +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] +impl LossyPauliSumExecutor +where + T: Config, +{ + fn execute( + &mut self, + inst: CircuitInstruction, + msg: CircuitMessage, + ) -> Result> { + self.execute_instruction(&inst, &msg) + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + let _ = (inst, msg, &mut self.state); + // TODO(Task 5): dispatch (inst, msg) onto self.state per the Gate + // Support Matrix; reject Measure / Reset with a clear "not supported + // on LossyPauliSum backend" error. + Ok(Effects::None) + } +} + +impl vihaco::Reset for LossyPauliSumExecutor +where + T: Config, +{ + fn reset(&mut self) { + // TODO(Task 5/6): rebuild self.state from the seeded observable. + } +} + +/// Tableau-backed inner enum (Schrödinger picture). Carries the six +/// size-bucketed `CircuitExecutor` variants; bucket is picked from `n_qubits`. +pub enum TableauCircuit { Bits64(CircuitExecutor, usize, Vec<(Complex64, usize)>>), Bits128(CircuitExecutor, u128, Vec<(Complex64, u128)>>), Bits256(CircuitExecutor, U256, Vec<(Complex64, U256)>>), @@ -225,8 +347,7 @@ pub enum Circuit { Bits2048(CircuitExecutor, U2048, Vec<(Complex64, U2048)>>), } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] -impl Circuit { +impl TableauCircuit { pub fn new(n_qubits: usize, coefficient_threshold: f64) -> Self { if n_qubits <= 64 { let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); @@ -251,8 +372,8 @@ impl Circuit { } } - /// Same as [`Circuit::new`], but seed the RNG deterministically so a shot - /// is reproducible. + /// Same as [`TableauCircuit::new`], but seed the RNG deterministically so a + /// shot is reproducible. pub fn new_with_seed(n_qubits: usize, coefficient_threshold: f64, seed: u64) -> Self { macro_rules! seeded { ($variant:ident) => {{ @@ -277,14 +398,6 @@ impl Circuit { } } - fn execute( - &mut self, - inst: CircuitInstruction, - msg: CircuitMessage, - ) -> Result> { - self.execute_instruction(&inst, &msg) - } - fn execute_instruction( &mut self, inst: &CircuitInstruction, @@ -300,8 +413,6 @@ impl Circuit { } } - /// Render the current tableau / Pauli state, dispatching across the executor - /// size variants. Used by the REPL's `show` command. pub fn state_string(&self) -> String { match self { Self::Bits64(ex) => ex.tab.to_string(), @@ -314,17 +425,171 @@ impl Circuit { } } -#[observe(CircuitEffect, effect=MeasurementEffect)] -impl Circuit { - fn observe_circuit_effect( +impl vihaco::Reset for TableauCircuit { + fn reset(&mut self) { + match self { + Self::Bits64(ex) => ex.reset(), + Self::Bits128(ex) => ex.reset(), + Self::Bits256(ex) => ex.reset(), + Self::Bits512(ex) => ex.reset(), + Self::Bits1024(ex) => ex.reset(), + Self::Bits2048(ex) => ex.reset(), + }; + } +} + +/// PauliSum-backed inner enum (Heisenberg picture). Per Decision 7 of the plan, +/// the size buckets carry `[u8; N]`-storage `ByteFxHashF64` configs (N = 8, 16, +/// …, 256) rather than the tableau's `[u64; N]` configs; bucket labels match +/// the semantic qubit count (`Bits64` = 64 qubits) so the outer enum's dispatch +/// is uniform across backends. +pub enum PauliSumCircuit { + Bits64(PauliSumExecutor>), + Bits128(PauliSumExecutor>), + Bits256(PauliSumExecutor>), + Bits512(PauliSumExecutor>), + Bits1024(PauliSumExecutor>), + Bits2048(PauliSumExecutor>), +} + +impl PauliSumCircuit { + pub fn new(info: &PPVMDeviceInfo) -> Self { + macro_rules! build { + ($variant:ident, $N:literal) => {{ + let state = PauliSum::>::builder() + .n_qubits(info.n_qubits) + .strategy(paulisum_strategy(info)) + .build(); + Self::$variant(PauliSumExecutor { state }) + }}; + } + if info.n_qubits <= 64 { + build!(Bits64, 8) + } else if info.n_qubits <= 128 { + build!(Bits128, 16) + } else if info.n_qubits <= 256 { + build!(Bits256, 32) + } else if info.n_qubits <= 512 { + build!(Bits512, 64) + } else if info.n_qubits <= 1024 { + build!(Bits1024, 128) + } else if info.n_qubits <= 2048 { + build!(Bits2048, 256) + } else { + panic!("No matching PauliSum executor for {} qubits", info.n_qubits); + } + } + + fn execute_instruction( &mut self, - effect: &CircuitEffect, + inst: &CircuitInstruction, + msg: &CircuitMessage, ) -> Result> { - self.execute_instruction(&effect.inst, &effect.msg) + match self { + Self::Bits64(ex) => ex.execute_instruction(inst, msg), + Self::Bits128(ex) => ex.execute_instruction(inst, msg), + Self::Bits256(ex) => ex.execute_instruction(inst, msg), + Self::Bits512(ex) => ex.execute_instruction(inst, msg), + Self::Bits1024(ex) => ex.execute_instruction(inst, msg), + Self::Bits2048(ex) => ex.execute_instruction(inst, msg), + } + } + + pub fn state_string(&self) -> String { + match self { + Self::Bits64(ex) => ex.state.to_string(), + Self::Bits128(ex) => ex.state.to_string(), + Self::Bits256(ex) => ex.state.to_string(), + Self::Bits512(ex) => ex.state.to_string(), + Self::Bits1024(ex) => ex.state.to_string(), + Self::Bits2048(ex) => ex.state.to_string(), + } } } -impl vihaco::Reset for Circuit { +impl vihaco::Reset for PauliSumCircuit { + fn reset(&mut self) { + match self { + Self::Bits64(ex) => ex.reset(), + Self::Bits128(ex) => ex.reset(), + Self::Bits256(ex) => ex.reset(), + Self::Bits512(ex) => ex.reset(), + Self::Bits1024(ex) => ex.reset(), + Self::Bits2048(ex) => ex.reset(), + }; + } +} + +/// LossyPauliSum-backed inner enum. Identical shape to [`PauliSumCircuit`] +/// but with `LossyPauliWord`-keyed configs so loss-channel methods dispatch. +pub enum LossyPauliSumCircuit { + Bits64(LossyPauliSumExecutor>), + Bits128(LossyPauliSumExecutor>), + Bits256(LossyPauliSumExecutor>), + Bits512(LossyPauliSumExecutor>), + Bits1024(LossyPauliSumExecutor>), + Bits2048(LossyPauliSumExecutor>), +} + +impl LossyPauliSumCircuit { + pub fn new(info: &PPVMDeviceInfo) -> Self { + macro_rules! build { + ($variant:ident, $N:literal) => {{ + let state = PauliSum::>::builder() + .n_qubits(info.n_qubits) + .strategy(paulisum_strategy(info)) + .build(); + Self::$variant(LossyPauliSumExecutor { state }) + }}; + } + if info.n_qubits <= 64 { + build!(Bits64, 8) + } else if info.n_qubits <= 128 { + build!(Bits128, 16) + } else if info.n_qubits <= 256 { + build!(Bits256, 32) + } else if info.n_qubits <= 512 { + build!(Bits512, 64) + } else if info.n_qubits <= 1024 { + build!(Bits1024, 128) + } else if info.n_qubits <= 2048 { + build!(Bits2048, 256) + } else { + panic!( + "No matching LossyPauliSum executor for {} qubits", + info.n_qubits + ); + } + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + match self { + Self::Bits64(ex) => ex.execute_instruction(inst, msg), + Self::Bits128(ex) => ex.execute_instruction(inst, msg), + Self::Bits256(ex) => ex.execute_instruction(inst, msg), + Self::Bits512(ex) => ex.execute_instruction(inst, msg), + Self::Bits1024(ex) => ex.execute_instruction(inst, msg), + Self::Bits2048(ex) => ex.execute_instruction(inst, msg), + } + } + + pub fn state_string(&self) -> String { + match self { + Self::Bits64(ex) => ex.state.to_string(), + Self::Bits128(ex) => ex.state.to_string(), + Self::Bits256(ex) => ex.state.to_string(), + Self::Bits512(ex) => ex.state.to_string(), + Self::Bits1024(ex) => ex.state.to_string(), + Self::Bits2048(ex) => ex.state.to_string(), + } + } +} + +impl vihaco::Reset for LossyPauliSumCircuit { fn reset(&mut self) { match self { Self::Bits64(ex) => ex.reset(), @@ -337,8 +602,95 @@ impl vihaco::Reset for Circuit { } } +/// Outer `Circuit` enum: backend selector. Picks one of the three inner enums +/// based on `info.backend` at construction time; from there, every per-step +/// call routes outer → inner → executor. +pub enum Circuit { + Tableau(TableauCircuit), + PauliSum(PauliSumCircuit), + LossyPauliSum(LossyPauliSumCircuit), +} + +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] +impl Circuit { + pub fn new(info: &PPVMDeviceInfo) -> Self { + match info.backend { + BackendKind::Tableau => Self::Tableau(TableauCircuit::new( + info.n_qubits, + info.coefficient_threshold, + )), + BackendKind::PauliSum => Self::PauliSum(PauliSumCircuit::new(info)), + BackendKind::LossyPauliSum => Self::LossyPauliSum(LossyPauliSumCircuit::new(info)), + } + } + + /// Same as [`Circuit::new`], but seed the RNG deterministically so a shot + /// is reproducible. PauliSum / LossyPauliSum are deterministic — the seed + /// is accepted but ignored on those backends. + pub fn new_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Self { + match info.backend { + BackendKind::Tableau => Self::Tableau(TableauCircuit::new_with_seed( + info.n_qubits, + info.coefficient_threshold, + seed, + )), + BackendKind::PauliSum => Self::PauliSum(PauliSumCircuit::new(info)), + BackendKind::LossyPauliSum => Self::LossyPauliSum(LossyPauliSumCircuit::new(info)), + } + } + + fn execute( + &mut self, + inst: CircuitInstruction, + msg: CircuitMessage, + ) -> Result> { + self.execute_instruction(&inst, &msg) + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + match self { + Self::Tableau(c) => c.execute_instruction(inst, msg), + Self::PauliSum(c) => c.execute_instruction(inst, msg), + Self::LossyPauliSum(c) => c.execute_instruction(inst, msg), + } + } + + /// Render the current state. Used by the REPL's `show` command. + pub fn state_string(&self) -> String { + match self { + Self::Tableau(c) => c.state_string(), + Self::PauliSum(c) => c.state_string(), + Self::LossyPauliSum(c) => c.state_string(), + } + } +} + +#[observe(CircuitEffect, effect=MeasurementEffect)] +impl Circuit { + fn observe_circuit_effect( + &mut self, + effect: &CircuitEffect, + ) -> Result> { + self.execute_instruction(&effect.inst, &effect.msg) + } +} + +impl vihaco::Reset for Circuit { + fn reset(&mut self) { + match self { + Self::Tableau(c) => c.reset(), + Self::PauliSum(c) => c.reset(), + Self::LossyPauliSum(c) => c.reset(), + }; + } +} + impl Default for Circuit { fn default() -> Self { - Self::new(0, 1e-10) + Self::new(&PPVMDeviceInfo::default()) } } diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 4cff61da3..1097957d6 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -13,6 +13,8 @@ use vihaco_cpu::{CPU, CPUMessage}; pub use vihaco_cpu::StepOutcome; use crate::component::Circuit; +#[cfg(test)] +use crate::component::TableauCircuit; use crate::measurements::{MeasurementEffect, MeasurementObserver, MeasurementResult}; use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; @@ -276,8 +278,8 @@ impl PPVM { return Err(eyre::eyre!("device circuit.n_qubits must be declared")); } self.circuit = match seed { - Some(seed) => Circuit::new_with_seed(info.n_qubits, info.coefficient_threshold, seed), - None => Circuit::new(info.n_qubits, info.coefficient_threshold), + Some(seed) => Circuit::new_with_seed(info, seed), + None => Circuit::new(info), }; // push entry frame @@ -620,30 +622,33 @@ mod tests { } let num_coefficients = match &machine.circuit { - Circuit::Bits64(ex) => { + Circuit::Tableau(TableauCircuit::Bits64(ex)) => { println!("{}", ex.tab); ex.tab.coefficients.len() } - Circuit::Bits128(ex) => { + Circuit::Tableau(TableauCircuit::Bits128(ex)) => { println!("{}", ex.tab); ex.tab.coefficients.len() } - Circuit::Bits256(ex) => { + Circuit::Tableau(TableauCircuit::Bits256(ex)) => { println!("{}", ex.tab); ex.tab.coefficients.len() } - Circuit::Bits512(ex) => { + Circuit::Tableau(TableauCircuit::Bits512(ex)) => { println!("{}", ex.tab); ex.tab.coefficients.len() } - Circuit::Bits1024(ex) => { + Circuit::Tableau(TableauCircuit::Bits1024(ex)) => { println!("{}", ex.tab); ex.tab.coefficients.len() } - Circuit::Bits2048(ex) => { + Circuit::Tableau(TableauCircuit::Bits2048(ex)) => { println!("{}", ex.tab); ex.tab.coefficients.len() } + Circuit::PauliSum(_) | Circuit::LossyPauliSum(_) => { + panic!("test expects the default Tableau backend, got a PauliSum variant"); + } }; assert_eq!(num_coefficients, 2); From d5cafbf5899e4d752b8436df7c537f5b00b276fb Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 9 Jun 2026 16:11:11 +0200 Subject: [PATCH 60/95] Gate dispatch for PauliSum executors --- crates/ppvm-vihaco/src/component.rs | 276 +++++++++++++++++++++++++++- 1 file changed, 268 insertions(+), 8 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 8e6e9a6d8..678db8a08 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -50,6 +50,17 @@ macro_rules! batch_for { }; } +/// Two-qubit sibling of [`batch_for!`]: drives a method that takes two qubit +/// addresses (plus optional extra args) over a slice of `(usize, usize)` pairs. +macro_rules! batch_pairs_for { + ($state:expr, $method:ident, $pairs:expr) => { + for &(a, b) in $pairs { $state.$method(a, b); } + }; + ($state:expr, $method:ident, $pairs:expr, $($arg:expr),+) => { + for &(a, b) in $pairs { $state.$method(a, b, $($arg),+); } + }; +} + pub struct CircuitExecutor, I: TableauIndex, C: SparseVector> { pub tab: GeneralizedTableau, } @@ -273,10 +284,132 @@ where inst: &CircuitInstruction, msg: &CircuitMessage, ) -> Result> { - let _ = (inst, msg, &mut self.state); - // TODO(Task 5): dispatch (inst, msg) onto self.state per the Gate - // Support Matrix; reject Measure / Reset / Loss / CorrelatedLoss with - // a clear "not supported on PauliSum backend" error. + use CircuitInstruction::*; + use CircuitMessage::*; + + match (inst, msg) { + // Single-qubit Clifford + (X, &Qubit(addr)) => self.state.x(addr), + (Y, &Qubit(addr)) => self.state.y(addr), + (Z, &Qubit(addr)) => self.state.z(addr), + (H, &Qubit(addr)) => self.state.h(addr), + (S, &Qubit(addr)) => self.state.s(addr), + (SAdj, &Qubit(addr)) => self.state.s_adj(addr), + (SqrtX, &Qubit(addr)) => self.state.sqrt_x(addr), + (SqrtY, &Qubit(addr)) => self.state.sqrt_y(addr), + (SqrtXAdj, &Qubit(addr)) => self.state.sqrt_x_adj(addr), + (SqrtYAdj, &Qubit(addr)) => self.state.sqrt_y_adj(addr), + + // Controlled gates + (CNOT, &TwoQubit(addr0, addr1)) => self.state.cnot(addr0, addr1), + (CZ, &TwoQubit(addr0, addr1)) => self.state.cz(addr0, addr1), + + // Single-qubit rotations + (RX, &QubitAndFloat(addr, angle)) => self.state.rx(addr, angle), + (RY, &QubitAndFloat(addr, angle)) => self.state.ry(addr, angle), + (RZ, &QubitAndFloat(addr, angle)) => self.state.rz(addr, angle), + + // Two-qubit rotations + (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.rxx(addr0, addr1, angle), + (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.ryy(addr0, addr1, angle), + (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.rzz(addr0, addr1, angle), + + // RXY: rotation about an axis in the x/y plane + (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => { + self.state.r(addr, axis_angle, theta) + } + + // Noise + (Depolarize, &QubitAndFloat(addr, p)) => self.state.depolarize(addr, p), + (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { + self.state.depolarize2(addr0, addr1, p) + } + (PauliError, QubitAndFloatArr3(addr0, ps)) => self.state.pauli_error(*addr0, *ps), + (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { + self.state.two_qubit_pauli_error(*addr0, *addr1, *ps) + } + + // Truncate: pruning per the configured strategy. + (Truncate, None) => self.state.truncate(), + + // Batched arms: simple for-loop dispatch (no dedicated batch + // methods on PauliSum, unlike GeneralizedTableau). + (X, QubitBatch(addrs)) => batch_for!(self.state, x, addrs), + (Y, QubitBatch(addrs)) => batch_for!(self.state, y, addrs), + (Z, QubitBatch(addrs)) => batch_for!(self.state, z, addrs), + (H, QubitBatch(addrs)) => batch_for!(self.state, h, addrs), + (S, QubitBatch(addrs)) => batch_for!(self.state, s, addrs), + (SAdj, QubitBatch(addrs)) => batch_for!(self.state, s_adj, addrs), + (SqrtX, QubitBatch(addrs)) => batch_for!(self.state, sqrt_x, addrs), + (SqrtY, QubitBatch(addrs)) => batch_for!(self.state, sqrt_y, addrs), + (SqrtXAdj, QubitBatch(addrs)) => batch_for!(self.state, sqrt_x_adj, addrs), + (SqrtYAdj, QubitBatch(addrs)) => batch_for!(self.state, sqrt_y_adj, addrs), + (RX, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, rx, addrs, *angle), + (RY, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, ry, addrs, *angle), + (RZ, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, rz, addrs, *angle), + (Depolarize, QubitBatchAndFloat(addrs, p)) => { + batch_for!(self.state, depolarize, addrs, *p) + } + (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { + batch_for!(self.state, pauli_error, addrs, *ps) + } + (CNOT, TwoQubitBatch(pairs)) => batch_pairs_for!(self.state, cnot, pairs), + (CZ, TwoQubitBatch(pairs)) => batch_pairs_for!(self.state, cz, pairs), + (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { + batch_pairs_for!(self.state, rxx, pairs, *angle) + } + (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { + batch_pairs_for!(self.state, ryy, pairs, *angle) + } + (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { + batch_pairs_for!(self.state, rzz, pairs, *angle) + } + (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { + batch_pairs_for!(self.state, depolarize2, pairs, *p) + } + (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { + batch_pairs_for!(self.state, two_qubit_pauli_error, pairs, *ps) + } + + // Not supported on PauliSum (Decision 11 + Gate Support Matrix). + (Measure | Reset, _) => { + return Err(eyre!("{inst} is not supported on the PauliSum backend")); + } + (Loss | CorrelatedLoss, _) => { + return Err(eyre!( + "{inst} is not supported on the PauliSum backend; use the LossyPauliSum backend instead" + )); + } + + // T / T_adj / U3 are listed as supported on PauliSum in the plan's + // Gate Support Matrix, but ppvm-runtime does not yet implement + // TGate or U3Gate for PauliSum (only for GeneralizedTableau). + // Flag this finding here; lifting the upstream impls is out of + // scope for Task 5. + (T | TAdj | U3, _) => { + return Err(eyre!( + "{inst} on PauliSum requires upstream ppvm-runtime support that is not yet implemented" + )); + } + + // Trace: deferred until Task 6 introduces `TraceEffect` and the + // effect-union for the Circuit component. + (Trace, _) => { + return Err(eyre!( + "Trace is not yet wired on the PauliSum backend (Phase 2 Task 6)" + )); + } + + // Fallback (batched messages, mismatched shapes, etc.) + (inst, msg) => { + return Err(eyre!( + "Invalid gate arguments {:?} for gate {:?} on the PauliSum backend", + msg, + inst + )); + } + }; + Ok(Effects::None) } } @@ -319,10 +452,137 @@ where inst: &CircuitInstruction, msg: &CircuitMessage, ) -> Result> { - let _ = (inst, msg, &mut self.state); - // TODO(Task 5): dispatch (inst, msg) onto self.state per the Gate - // Support Matrix; reject Measure / Reset with a clear "not supported - // on LossyPauliSum backend" error. + use CircuitInstruction::*; + use CircuitMessage::*; + + match (inst, msg) { + // Single-qubit Clifford + (X, &Qubit(addr)) => self.state.x(addr), + (Y, &Qubit(addr)) => self.state.y(addr), + (Z, &Qubit(addr)) => self.state.z(addr), + (H, &Qubit(addr)) => self.state.h(addr), + (S, &Qubit(addr)) => self.state.s(addr), + (SAdj, &Qubit(addr)) => self.state.s_adj(addr), + (SqrtX, &Qubit(addr)) => self.state.sqrt_x(addr), + (SqrtY, &Qubit(addr)) => self.state.sqrt_y(addr), + (SqrtXAdj, &Qubit(addr)) => self.state.sqrt_x_adj(addr), + (SqrtYAdj, &Qubit(addr)) => self.state.sqrt_y_adj(addr), + + // Controlled gates + (CNOT, &TwoQubit(addr0, addr1)) => self.state.cnot(addr0, addr1), + (CZ, &TwoQubit(addr0, addr1)) => self.state.cz(addr0, addr1), + + // Single-qubit rotations + (RX, &QubitAndFloat(addr, angle)) => self.state.rx(addr, angle), + (RY, &QubitAndFloat(addr, angle)) => self.state.ry(addr, angle), + (RZ, &QubitAndFloat(addr, angle)) => self.state.rz(addr, angle), + + // Two-qubit rotations + (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.rxx(addr0, addr1, angle), + (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.ryy(addr0, addr1, angle), + (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.rzz(addr0, addr1, angle), + + // RXY: rotation about an axis in the x/y plane + (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => { + self.state.r(addr, axis_angle, theta) + } + + // Noise + (Depolarize, &QubitAndFloat(addr, p)) => self.state.depolarize(addr, p), + (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { + self.state.depolarize2(addr0, addr1, p) + } + (PauliError, QubitAndFloatArr3(addr0, ps)) => self.state.pauli_error(*addr0, *ps), + (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { + self.state.two_qubit_pauli_error(*addr0, *addr1, *ps) + } + + // Loss (accepted on LossyPauliSum; rejected on plain PauliSum) + (Loss, &QubitAndFloat(addr, p)) => self.state.loss_channel(addr, p), + (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { + self.state.correlated_loss_channel(*addr0, *addr1, *ps) + } + + // Truncate: pruning per the configured strategy. + (Truncate, None) => self.state.truncate(), + + // Batched arms: simple for-loop dispatch (no dedicated batch + // methods on PauliSum). + (X, QubitBatch(addrs)) => batch_for!(self.state, x, addrs), + (Y, QubitBatch(addrs)) => batch_for!(self.state, y, addrs), + (Z, QubitBatch(addrs)) => batch_for!(self.state, z, addrs), + (H, QubitBatch(addrs)) => batch_for!(self.state, h, addrs), + (S, QubitBatch(addrs)) => batch_for!(self.state, s, addrs), + (SAdj, QubitBatch(addrs)) => batch_for!(self.state, s_adj, addrs), + (SqrtX, QubitBatch(addrs)) => batch_for!(self.state, sqrt_x, addrs), + (SqrtY, QubitBatch(addrs)) => batch_for!(self.state, sqrt_y, addrs), + (SqrtXAdj, QubitBatch(addrs)) => batch_for!(self.state, sqrt_x_adj, addrs), + (SqrtYAdj, QubitBatch(addrs)) => batch_for!(self.state, sqrt_y_adj, addrs), + (RX, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, rx, addrs, *angle), + (RY, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, ry, addrs, *angle), + (RZ, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, rz, addrs, *angle), + (Depolarize, QubitBatchAndFloat(addrs, p)) => { + batch_for!(self.state, depolarize, addrs, *p) + } + (Loss, QubitBatchAndFloat(addrs, p)) => { + batch_for!(self.state, loss_channel, addrs, *p) + } + (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { + batch_for!(self.state, pauli_error, addrs, *ps) + } + (CNOT, TwoQubitBatch(pairs)) => batch_pairs_for!(self.state, cnot, pairs), + (CZ, TwoQubitBatch(pairs)) => batch_pairs_for!(self.state, cz, pairs), + (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { + batch_pairs_for!(self.state, rxx, pairs, *angle) + } + (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { + batch_pairs_for!(self.state, ryy, pairs, *angle) + } + (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { + batch_pairs_for!(self.state, rzz, pairs, *angle) + } + (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { + batch_pairs_for!(self.state, depolarize2, pairs, *p) + } + (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { + batch_pairs_for!(self.state, correlated_loss_channel, pairs, *ps) + } + (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { + batch_pairs_for!(self.state, two_qubit_pauli_error, pairs, *ps) + } + + // Not supported on LossyPauliSum (Decision 11 + Gate Support Matrix). + (Measure | Reset, _) => { + return Err(eyre!( + "{inst} is not supported on the LossyPauliSum backend" + )); + } + + // See PauliSumExecutor: T/T_adj/U3 require upstream ppvm-runtime + // impls that don't exist yet. + (T | TAdj | U3, _) => { + return Err(eyre!( + "{inst} on LossyPauliSum requires upstream ppvm-runtime support that is not yet implemented" + )); + } + + // Trace: deferred until Task 6 introduces `TraceEffect`. + (Trace, _) => { + return Err(eyre!( + "Trace is not yet wired on the LossyPauliSum backend (Phase 2 Task 6)" + )); + } + + // Fallback (batched messages, mismatched shapes, etc.) + (inst, msg) => { + return Err(eyre!( + "Invalid gate arguments {:?} for gate {:?} on the LossyPauliSum backend", + msg, + inst + )); + } + }; + Ok(Effects::None) } } From 8659956f7ac1015d7d5e56fcdfc78b90ee3ab660 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 9 Jun 2026 16:26:08 +0200 Subject: [PATCH 61/95] TraceEffect and CircuitOutcomeEffect --- crates/ppvm-vihaco/src/component.rs | 57 ++++++++++++++------------ crates/ppvm-vihaco/src/composite.rs | 53 ++++++++++++++++++++++-- crates/ppvm-vihaco/src/measurements.rs | 43 +++++++++++++++++++ 3 files changed, 122 insertions(+), 31 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 678db8a08..a819d5e72 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::composite::{BackendKind, PPVMDeviceInfo}; -use crate::measurements::MeasurementEffect; -use crate::measurements::MeasurementOutcome; +use crate::measurements::{CircuitOutcomeEffect, MeasurementEffect, MeasurementOutcome}; use bitvec::view::BitView; use bnum::types::{U256, U512, U1024, U2048}; use eyre::{Result, eyre}; @@ -65,7 +64,7 @@ pub struct CircuitExecutor, I: TableauIndex, C: SparseVec pub tab: GeneralizedTableau, } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl CircuitExecutor where T: Config, @@ -77,7 +76,7 @@ where &mut self, inst: CircuitInstruction, msg: CircuitMessage, - ) -> Result> { + ) -> Result> { self.execute_instruction(&inst, &msg) } @@ -85,7 +84,7 @@ where &mut self, inst: &CircuitInstruction, msg: &CircuitMessage, - ) -> Result> { + ) -> Result> { use CircuitInstruction::*; use CircuitMessage::*; @@ -129,9 +128,11 @@ where // Measure & Reset (Measure, &Qubit(addr)) => { let outcome: MeasurementOutcome = self.tab.measure(addr).into(); - return Ok(Effects::one(MeasurementEffect { - measurement_results: smallvec::smallvec![outcome], - })); + return Ok(Effects::one(CircuitOutcomeEffect::Measurement( + MeasurementEffect { + measurement_results: smallvec::smallvec![outcome], + }, + ))); } (Reset, &Qubit(addr)) => self.tab.reset(addr), @@ -224,9 +225,11 @@ where // Batch: measure (emits per qubit) (Measure, QubitBatch(addrs)) => { let outcomes = addrs.iter().map(|&addr| self.tab.measure(addr).into()); - return Ok(Effects::one(MeasurementEffect { - measurement_results: outcomes.collect(), - })); + return Ok(Effects::one(CircuitOutcomeEffect::Measurement( + MeasurementEffect { + measurement_results: outcomes.collect(), + }, + ))); } // Fallback @@ -266,7 +269,7 @@ pub struct PauliSumExecutor> { pub state: PauliSum, } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl PauliSumExecutor where T: Config, @@ -275,7 +278,7 @@ where &mut self, inst: CircuitInstruction, msg: CircuitMessage, - ) -> Result> { + ) -> Result> { self.execute_instruction(&inst, &msg) } @@ -283,7 +286,7 @@ where &mut self, inst: &CircuitInstruction, msg: &CircuitMessage, - ) -> Result> { + ) -> Result> { use CircuitInstruction::*; use CircuitMessage::*; @@ -396,7 +399,7 @@ where // effect-union for the Circuit component. (Trace, _) => { return Err(eyre!( - "Trace is not yet wired on the PauliSum backend (Phase 2 Task 6)" + "Trace is not yet wired on the PauliSum backend (Phase 2 Task 7)" )); } @@ -434,7 +437,7 @@ pub struct LossyPauliSumExecutor> { pub state: PauliSum, } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl LossyPauliSumExecutor where T: Config, @@ -443,7 +446,7 @@ where &mut self, inst: CircuitInstruction, msg: CircuitMessage, - ) -> Result> { + ) -> Result> { self.execute_instruction(&inst, &msg) } @@ -451,7 +454,7 @@ where &mut self, inst: &CircuitInstruction, msg: &CircuitMessage, - ) -> Result> { + ) -> Result> { use CircuitInstruction::*; use CircuitMessage::*; @@ -569,7 +572,7 @@ where // Trace: deferred until Task 6 introduces `TraceEffect`. (Trace, _) => { return Err(eyre!( - "Trace is not yet wired on the LossyPauliSum backend (Phase 2 Task 6)" + "Trace is not yet wired on the LossyPauliSum backend (Phase 2 Task 7)" )); } @@ -662,7 +665,7 @@ impl TableauCircuit { &mut self, inst: &CircuitInstruction, msg: &CircuitMessage, - ) -> Result> { + ) -> Result> { match self { Self::Bits64(ex) => ex.execute_instruction(inst, msg), Self::Bits128(ex) => ex.execute_instruction(inst, msg), @@ -744,7 +747,7 @@ impl PauliSumCircuit { &mut self, inst: &CircuitInstruction, msg: &CircuitMessage, - ) -> Result> { + ) -> Result> { match self { Self::Bits64(ex) => ex.execute_instruction(inst, msg), Self::Bits128(ex) => ex.execute_instruction(inst, msg), @@ -826,7 +829,7 @@ impl LossyPauliSumCircuit { &mut self, inst: &CircuitInstruction, msg: &CircuitMessage, - ) -> Result> { + ) -> Result> { match self { Self::Bits64(ex) => ex.execute_instruction(inst, msg), Self::Bits128(ex) => ex.execute_instruction(inst, msg), @@ -871,7 +874,7 @@ pub enum Circuit { LossyPauliSum(LossyPauliSumCircuit), } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = MeasurementEffect)] +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl Circuit { pub fn new(info: &PPVMDeviceInfo) -> Self { match info.backend { @@ -903,7 +906,7 @@ impl Circuit { &mut self, inst: CircuitInstruction, msg: CircuitMessage, - ) -> Result> { + ) -> Result> { self.execute_instruction(&inst, &msg) } @@ -911,7 +914,7 @@ impl Circuit { &mut self, inst: &CircuitInstruction, msg: &CircuitMessage, - ) -> Result> { + ) -> Result> { match self { Self::Tableau(c) => c.execute_instruction(inst, msg), Self::PauliSum(c) => c.execute_instruction(inst, msg), @@ -929,12 +932,12 @@ impl Circuit { } } -#[observe(CircuitEffect, effect=MeasurementEffect)] +#[observe(CircuitEffect, effect=CircuitOutcomeEffect)] impl Circuit { fn observe_circuit_effect( &mut self, effect: &CircuitEffect, - ) -> Result> { + ) -> Result> { self.execute_instruction(&effect.inst, &effect.msg) } } diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 1097957d6..a776d5b3c 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -15,7 +15,10 @@ pub use vihaco_cpu::StepOutcome; use crate::component::Circuit; #[cfg(test)] use crate::component::TableauCircuit; -use crate::measurements::{MeasurementEffect, MeasurementObserver, MeasurementResult}; +use crate::measurements::{ + CircuitOutcomeEffect, MeasurementEffect, MeasurementObserver, MeasurementResult, TraceEffect, + TraceObserver, +}; use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; pub const PPVM_MAGIC: u32 = 0x5050564D; @@ -72,6 +75,8 @@ pub struct PPVM { stdout: StdoutObserver, measurement_record: MeasurementObserver, + + trace_record: TraceObserver, } #[derive(Debug, Clone)] @@ -80,6 +85,7 @@ pub enum PPVMEffect { Stdout(StdoutEffect), Circuit(CircuitEffect), Measurement(MeasurementEffect), + Trace(TraceEffect), } #[observe(vihaco::observer::stdio::StdoutEffect, effect = PPVMEffect)] @@ -94,7 +100,7 @@ impl PPVM { fn observe_circuit_effect( &mut self, effect: &CircuitEffect, - ) -> eyre::Result> { + ) -> eyre::Result> { Observe::::observe(&mut self.circuit, effect) } } @@ -109,6 +115,13 @@ impl PPVM { } } +#[observe(TraceEffect, effect = PPVMEffect)] +impl PPVM { + fn observe_trace_effect(&mut self, effect: &TraceEffect) -> eyre::Result> { + Observe::::observe(&mut self.trace_record, effect) + } +} + impl From for PPVMEffect { fn from(value: StdoutEffect) -> Self { Self::Stdout(value) @@ -121,6 +134,21 @@ impl From for PPVMEffect { } } +impl From for PPVMEffect { + fn from(value: TraceEffect) -> Self { + Self::Trace(value) + } +} + +impl From for PPVMEffect { + fn from(value: CircuitOutcomeEffect) -> Self { + match value { + CircuitOutcomeEffect::Measurement(m) => Self::Measurement(m), + CircuitOutcomeEffect::Trace(t) => Self::Trace(t), + } + } +} + impl std::fmt::Display for PPVMInstruction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -427,8 +455,8 @@ impl PPVM { )?; *self.loader.pc_mut() += 1; let mut effects = Effects::one(PPVMEffect::Step(StepOutcome::Continue)); - for measurement_effect in circuit_effects { - effects = effects.append(PPVMEffect::Measurement(measurement_effect)); + for outcome in circuit_effects { + effects = effects.append(PPVMEffect::from(outcome)); } Ok(effects) } @@ -472,6 +500,15 @@ impl PPVM { } self.continue_observer_effects(follow_ups) } + PPVMEffect::Trace(effect) => { + let value = effect.value; + let follow_ups = Observe::::observe(self, &effect)?; + // Mirror the measurement wiring: append to the trace record + // (via the observer above) AND push the value onto the CPU + // stack so user bytecode can consume it. Plan Task 7. + self.cpu.stack_push(Value::F64(value)); + self.continue_observer_effects(follow_ups) + } PPVMEffect::Step(_) => Err(eyre::eyre!( "unexpected Step effect while continuing PPVM observer follow-ups" )), @@ -515,6 +552,13 @@ impl PPVM { self.measurement_record.record.clone() } + /// Per-trace values collected by `Trace` instructions during the run. + /// Parallel to [`PPVM::measurement_record`]: one f64 per `Trace` executed, + /// in execution order. + pub fn trace_record(&self) -> Vec { + self.trace_record.record.clone() + } + pub fn load_program(&mut self, program: &str) -> eyre::Result<()> { let module = crate::compile_program(program)?; self.load(&module)?; @@ -565,6 +609,7 @@ impl vihaco::Reset for PPVM { self.circuit.reset(); self.loader.pc = 0; self.measurement_record.record.clear(); + self.trace_record.record.clear(); } } diff --git a/crates/ppvm-vihaco/src/measurements.rs b/crates/ppvm-vihaco/src/measurements.rs index 19162081c..c5e0bf113 100644 --- a/crates/ppvm-vihaco/src/measurements.rs +++ b/crates/ppvm-vihaco/src/measurements.rs @@ -48,3 +48,46 @@ impl MeasurementObserver { Ok(Effects::none()) } } + +/// Per-step trace value emitted by the `Trace` instruction on the PauliSum and +/// LossyPauliSum backends. The plan's Decision 5 keeps trace and measurement +/// records as two parallel streams; this effect feeds the trace stream. +#[derive(Debug, Clone, PartialEq)] +pub struct TraceEffect { + pub value: f64, +} + +#[derive(Debug, Default)] +pub struct TraceObserver { + pub record: Vec, +} + +#[observe(TraceEffect)] +impl TraceObserver { + fn observe_trace_effect(&mut self, effect: &TraceEffect) -> Result> { + self.record.push(effect.value); + Ok(Effects::none()) + } +} + +/// Union of the two effect types a circuit instruction can produce. The plan's +/// structural note (Task 6) calls for broadening the `#[component(..., effect = +/// ...)]` annotation on `Circuit` and the executors to a union so a single +/// `Trace` (or `Measure`) instruction can fan out to the right observer. +#[derive(Debug, Clone, PartialEq)] +pub enum CircuitOutcomeEffect { + Measurement(MeasurementEffect), + Trace(TraceEffect), +} + +impl From for CircuitOutcomeEffect { + fn from(value: MeasurementEffect) -> Self { + Self::Measurement(value) + } +} + +impl From for CircuitOutcomeEffect { + fn from(value: TraceEffect) -> Self { + Self::Trace(value) + } +} From 59b003d3fb34fc09748bde9808034138587e306a Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 9 Jun 2026 17:07:32 +0200 Subject: [PATCH 62/95] PauliPattern trace --- crates/ppvm-vihaco/src/component.rs | 37 ++++++++++----- crates/ppvm-vihaco/src/composite.rs | 70 ++++++++++++++++++++++++++-- crates/vihaco-circuit-isa/src/lib.rs | 3 +- 3 files changed, 94 insertions(+), 16 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index a819d5e72..680829732 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::composite::{BackendKind, PPVMDeviceInfo}; -use crate::measurements::{CircuitOutcomeEffect, MeasurementEffect, MeasurementOutcome}; +use crate::measurements::{ + CircuitOutcomeEffect, MeasurementEffect, MeasurementOutcome, TraceEffect, +}; use bitvec::view::BitView; use bnum::types::{U256, U512, U1024, U2048}; use eyre::{Result, eyre}; @@ -273,6 +275,7 @@ pub struct PauliSumExecutor> { impl PauliSumExecutor where T: Config, + for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, { fn execute( &mut self, @@ -395,12 +398,16 @@ where )); } - // Trace: deferred until Task 6 introduces `TraceEffect` and the - // effect-union for the Circuit component. - (Trace, _) => { - return Err(eyre!( - "Trace is not yet wired on the PauliSum backend (Phase 2 Task 7)" - )); + // Trace: parse the resolved pattern string and compute the trace. + // Per plan Decision 9, parsing happens on every execution; no + // module-load caching. + (Trace, PauliPatternStr(s)) => { + let pat = PauliPattern::parse(s) + .map_err(|e| eyre!("invalid Pauli pattern `{s}`: {e:?}"))?; + let value = self.state.trace(&pat); + return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { + value, + }))); } // Fallback (batched messages, mismatched shapes, etc.) @@ -441,6 +448,7 @@ pub struct LossyPauliSumExecutor> { impl LossyPauliSumExecutor where T: Config, + for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, { fn execute( &mut self, @@ -569,11 +577,16 @@ where )); } - // Trace: deferred until Task 6 introduces `TraceEffect`. - (Trace, _) => { - return Err(eyre!( - "Trace is not yet wired on the LossyPauliSum backend (Phase 2 Task 7)" - )); + // Trace: parse the resolved pattern string and compute the trace. + // Per plan Decision 9, parsing happens on every execution; no + // module-load caching. + (Trace, PauliPatternStr(s)) => { + let pat = PauliPattern::parse(s) + .map_err(|e| eyre!("invalid Pauli pattern `{s}`: {e:?}"))?; + let value = self.state.trace(&pat); + return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { + value, + }))); } // Fallback (batched messages, mismatched shapes, etc.) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index a776d5b3c..faba16c43 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -268,9 +268,11 @@ impl PPVM { let q0 = self.pop_qubit()?; Ok(CircuitMessage::TwoQubitAndFloatArr15(q0, q1, ps)) } - Trace | Truncate => Err(eyre::eyre!( - "{inst} operand resolution not yet wired (Phase 2 Task 7)" - )), + Trace => { + let s = self.pop_string()?; + Ok(CircuitMessage::PauliPatternStr(s)) + } + Truncate => Ok(CircuitMessage::None), } } @@ -290,6 +292,16 @@ impl PPVM { } } + /// Pop a `Value::String(addr)`, look up the addr in the module's string + /// table, and return the owned string. Used by `Trace` to resolve its + /// Pauli-pattern operand before the executor sees the message. + fn pop_string(&mut self) -> eyre::Result { + match self.cpu.stack_pop()? { + vihaco::Value::String(addr) => Ok(self.loader.get_string(addr as usize)?.clone()), + v => Err(eyre::eyre!("Expected string operand, got {:?}", v)), + } + } + pub fn init(&mut self) -> eyre::Result<()> { self.init_inner(None) } @@ -1088,4 +1100,56 @@ mod tests { Ok(()) } + + #[test] + fn paulisum_truncate_runs_without_error() -> eyre::Result<()> { + // Smoke test: a `gate truncate` reaches the PauliSum executor's + // Truncate arm and calls `state.truncate()`. No observer effect, no + // stack changes — the test passes if init + step_once both succeed. + let mut module: Module = Module::default(); + module.extra.n_qubits = 1; + module.extra.backend = BackendKind::PauliSum; + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::Truncate)); + + let mut machine = PPVM::default(); + machine.load(&module)?; + machine.init()?; + machine.step_once()?; + Ok(()) + } + + #[test] + fn paulisum_trace_populates_trace_record() -> eyre::Result<()> { + // End-to-end smoke test for Task 7: a `gate trace` pops a string + // operand, parses it as a PauliPattern, computes the trace, emits a + // TraceEffect, and the value lands in both `trace_record()` and the + // CPU stack. State is an empty PauliSum (no observable seeded — that's + // Task 8), so the trace should be exactly 0.0. + let mut module: Module = Module::default(); + module.extra.n_qubits = 1; + module.extra.backend = BackendKind::PauliSum; + // `Z0` matches a Z on qubit 0; the parser requires position anchors. + module.strings.push("Z0".to_string()); + + module + .code + .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + Value::String(0), + ))); + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::Trace)); + + let mut machine = PPVM::default(); + machine.load(&module)?; + machine.init()?; + for _ in 0..module.code.len() { + machine.step_once()?; + } + + assert_eq!(machine.trace_record(), vec![0.0]); + Ok(()) + } } diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs index 20ea0c337..aab6b0b31 100644 --- a/crates/vihaco-circuit-isa/src/lib.rs +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -140,7 +140,8 @@ pub enum CircuitMessage { QubitAndFloatArr3(usize, [f64; 3]), // PauliError TwoQubitAndFloatArr3(usize, usize, [f64; 3]), // Correlated loss TwoQubitAndFloatArr15(usize, usize, [f64; 15]), // TwoQubitPauliError - PauliPatternStr(u32), // Trace (string-table addr) + + PauliPatternStr(String), // Trace (resolved Pauli-pattern source) // batched instructions QubitBatch(SmallVec<[usize; 8]>), // X, Y, Z, ... From 30006a28511b912a2e13235802b065f3f1738279 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 9 Jun 2026 19:32:36 +0200 Subject: [PATCH 63/95] Update constructor and add observable header --- crates/ppvm-vihaco/src/component.rs | 71 +++++++++++++--------- crates/ppvm-vihaco/src/composite.rs | 93 +++++++++++++++++++++++++---- 2 files changed, 125 insertions(+), 39 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 680829732..18e1be14d 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -use crate::composite::{BackendKind, PPVMDeviceInfo}; +use crate::composite::PPVMDeviceInfo; use crate::measurements::{ CircuitOutcomeEffect, MeasurementEffect, MeasurementOutcome, TraceEffect, }; @@ -729,13 +729,17 @@ pub enum PauliSumCircuit { } impl PauliSumCircuit { - pub fn new(info: &PPVMDeviceInfo) -> Self { + /// Build a PauliSum-backed circuit, seeding `state += (observable, 1.0)`. + /// `observable` must already be validated against `info.n_qubits` (use + /// [`validate_single_pauli_observable`] at the boundary). + pub fn new(info: &PPVMDeviceInfo, observable: &str) -> Self { macro_rules! build { ($variant:ident, $N:literal) => {{ - let state = PauliSum::>::builder() + let mut state = PauliSum::>::builder() .n_qubits(info.n_qubits) .strategy(paulisum_strategy(info)) .build(); + state += (observable, 1.0); Self::$variant(PauliSumExecutor { state }) }}; } @@ -808,13 +812,16 @@ pub enum LossyPauliSumCircuit { } impl LossyPauliSumCircuit { - pub fn new(info: &PPVMDeviceInfo) -> Self { + /// Build a LossyPauliSum-backed circuit, seeding `state += (observable, + /// 1.0)`. `observable` must already be validated against `info.n_qubits`. + pub fn new(info: &PPVMDeviceInfo, observable: &str) -> Self { macro_rules! build { ($variant:ident, $N:literal) => {{ - let state = PauliSum::>::builder() + let mut state = PauliSum::>::builder() .n_qubits(info.n_qubits) .strategy(paulisum_strategy(info)) .build(); + state += (observable, 1.0); Self::$variant(LossyPauliSumExecutor { state }) }}; } @@ -889,30 +896,37 @@ pub enum Circuit { #[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl Circuit { - pub fn new(info: &PPVMDeviceInfo) -> Self { - match info.backend { - BackendKind::Tableau => Self::Tableau(TableauCircuit::new( - info.n_qubits, - info.coefficient_threshold, - )), - BackendKind::PauliSum => Self::PauliSum(PauliSumCircuit::new(info)), - BackendKind::LossyPauliSum => Self::LossyPauliSum(LossyPauliSumCircuit::new(info)), - } + /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and + /// `coefficient_threshold` from `info`; no observable required. + pub fn tableau(info: &PPVMDeviceInfo) -> Self { + Self::Tableau(TableauCircuit::new( + info.n_qubits, + info.coefficient_threshold, + )) } - /// Same as [`Circuit::new`], but seed the RNG deterministically so a shot - /// is reproducible. PauliSum / LossyPauliSum are deterministic — the seed - /// is accepted but ignored on those backends. - pub fn new_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Self { - match info.backend { - BackendKind::Tableau => Self::Tableau(TableauCircuit::new_with_seed( - info.n_qubits, - info.coefficient_threshold, - seed, - )), - BackendKind::PauliSum => Self::PauliSum(PauliSumCircuit::new(info)), - BackendKind::LossyPauliSum => Self::LossyPauliSum(LossyPauliSumCircuit::new(info)), - } + /// Same as [`Circuit::tableau`], but seed the tableau RNG deterministically + /// so a shot is reproducible. + pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Self { + Self::Tableau(TableauCircuit::new_with_seed( + info.n_qubits, + info.coefficient_threshold, + seed, + )) + } + + /// Build a PauliSum-backed circuit, seeding the state with `(observable, + /// 1.0)`. `observable` must already be validated against `info.n_qubits` + /// (see [`validate_single_pauli_observable`]); passing invalid input + /// results in a panic from the underlying word parser. + pub fn paulisum(info: &PPVMDeviceInfo, observable: &str) -> Self { + Self::PauliSum(PauliSumCircuit::new(info, observable)) + } + + /// Build a LossyPauliSum-backed circuit. Same contract as + /// [`Circuit::paulisum`]. + pub fn lossy_paulisum(info: &PPVMDeviceInfo, observable: &str) -> Self { + Self::LossyPauliSum(LossyPauliSumCircuit::new(info, observable)) } fn execute( @@ -967,6 +981,7 @@ impl vihaco::Reset for Circuit { impl Default for Circuit { fn default() -> Self { - Self::new(&PPVMDeviceInfo::default()) + // Default backend is Tableau, which doesn't require an observable. + Self::tableau(&PPVMDeviceInfo::default()) } } diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index faba16c43..d54bf968a 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 +use eyre::{Result, eyre}; use vihaco::frame::Frame; use vihaco::machine::StackFrame; use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; @@ -317,9 +318,17 @@ impl PPVM { if info.n_qubits == 0 { return Err(eyre::eyre!("device circuit.n_qubits must be declared")); } - self.circuit = match seed { - Some(seed) => Circuit::new_with_seed(info, seed), - None => Circuit::new(info), + self.circuit = match (info.backend, seed) { + (BackendKind::Tableau, None) => Circuit::tableau(info), + (BackendKind::Tableau, Some(seed)) => Circuit::tableau_with_seed(info, seed), + (BackendKind::PauliSum, _) => { + let observable = validate_single_pauli_observable(info)?; + Circuit::paulisum(info, observable) + } + (BackendKind::LossyPauliSum, _) => { + let observable = validate_single_pauli_observable(info)?; + Circuit::lossy_paulisum(info, observable) + } }; // push entry frame @@ -625,6 +634,35 @@ impl vihaco::Reset for PPVM { } } +/// Validate a single-Pauli-word observable header against `info.n_qubits` and +/// return the string slice for seeding. Phase 2 accepts dense words built from +/// `I`, `X`, `Y`, `Z` of length exactly `n_qubits`; Phase 3 (Task 11) replaces +/// this with the multi-term sum parser. +fn validate_single_pauli_observable(info: &PPVMDeviceInfo) -> Result<&str> { + let observable = info.observable.as_deref().ok_or_else(|| { + eyre!( + "the {:?} backend requires `device circuit.observable` to be set", + info.backend + ) + })?; + let len = observable.chars().count(); + if len != info.n_qubits { + return Err(eyre!( + "observable length {len} does not match circuit.n_qubits {}", + info.n_qubits + )); + } + if let Some(bad) = observable + .chars() + .find(|c| !matches!(c, 'I' | 'X' | 'Y' | 'Z')) + { + return Err(eyre!( + "observable contains invalid Pauli character `{bad}`; Phase 2 accepts only I/X/Y/Z (multi-term sums land in Phase 3)" + )); + } + Ok(observable) +} + #[cfg(test)] mod tests { use vihaco::{Type, Value, module::Module}; @@ -1104,11 +1142,12 @@ mod tests { #[test] fn paulisum_truncate_runs_without_error() -> eyre::Result<()> { // Smoke test: a `gate truncate` reaches the PauliSum executor's - // Truncate arm and calls `state.truncate()`. No observer effect, no - // stack changes — the test passes if init + step_once both succeed. + // 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(); module.extra.n_qubits = 1; module.extra.backend = BackendKind::PauliSum; + module.extra.observable = Some("Z".to_string()); module .code .push(PPVMInstruction::Circuit(CircuitInstruction::Truncate)); @@ -1122,14 +1161,13 @@ mod tests { #[test] fn paulisum_trace_populates_trace_record() -> eyre::Result<()> { - // End-to-end smoke test for Task 7: a `gate trace` pops a string - // operand, parses it as a PauliPattern, computes the trace, emits a - // TraceEffect, and the value lands in both `trace_record()` and the - // CPU stack. State is an empty PauliSum (no observable seeded — that's - // Task 8), so the trace should be exactly 0.0. + // 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(); module.extra.n_qubits = 1; module.extra.backend = BackendKind::PauliSum; + module.extra.observable = Some("Z".to_string()); // `Z0` matches a Z on qubit 0; the parser requires position anchors. module.strings.push("Z0".to_string()); @@ -1149,7 +1187,40 @@ mod tests { machine.step_once()?; } - assert_eq!(machine.trace_record(), vec![0.0]); + assert_eq!(machine.trace_record(), vec![1.0]); Ok(()) } + + #[test] + fn paulisum_init_rejects_missing_observable() { + // Task 8 requires `device circuit.observable` for PauliSum / Lossy. + let mut module: Module = Module::default(); + module.extra.n_qubits = 1; + module.extra.backend = BackendKind::PauliSum; + + let mut machine = PPVM::default(); + machine.load(&module).unwrap(); + let err = machine.init().unwrap_err(); + assert!( + err.to_string().contains("observable"), + "expected observable-related error, got: {err}" + ); + } + + #[test] + fn paulisum_init_rejects_mismatched_observable_length() { + let mut module: Module = Module::default(); + module.extra.n_qubits = 2; + module.extra.backend = BackendKind::PauliSum; + // Three letters but only two qubits — should error. + module.extra.observable = Some("ZZZ".to_string()); + + let mut machine = PPVM::default(); + machine.load(&module).unwrap(); + let err = machine.init().unwrap_err(); + assert!( + err.to_string().contains("does not match"), + "expected length-mismatch error, got: {err}" + ); + } } From a300333666dde145e57732e13e24751e013b6c1d Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 9 Jun 2026 19:39:52 +0200 Subject: [PATCH 64/95] Add some new tests and truncate no-op --- crates/ppvm-vihaco/src/component.rs | 14 ++++++++ crates/ppvm-vihaco/src/composite.rs | 52 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 18e1be14d..96b3e7588 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -234,6 +234,20 @@ where ))); } + // Truncate is a silent no-op on the Tableau backend — the tableau's + // gate methods already prune via the configured coefficient + // threshold, so there's nothing extra to do here. + (Truncate, None) => {} + + // Trace is not yet implemented for the Tableau backend; Phase 5 + // (plan Task 15) adds the `⟨ψ|P|ψ⟩` primitive upstream in + // `ppvm-tableau` and wires it through here. + (Trace, _) => { + return Err(eyre!( + "Trace is not yet implemented on the Tableau backend (Phase 5)" + )); + } + // Fallback (inst, msg) => { return Err(eyre!( diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index d54bf968a..b0230d7f4 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -1223,4 +1223,56 @@ mod tests { "expected length-mismatch error, got: {err}" ); } + + #[test] + fn tableau_truncate_is_silent_no_op() -> eyre::Result<()> { + // Task 9: `gate 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(); + module.extra.n_qubits = 1; + // backend defaults to Tableau; no observable needed. + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::Truncate)); + + let mut machine = PPVM::default(); + machine.load(&module)?; + machine.init()?; + machine.step_once()?; + // No observer effects emitted by Truncate. + assert!(machine.measurement_record().is_empty()); + assert!(machine.trace_record().is_empty()); + Ok(()) + } + + #[test] + fn tableau_trace_returns_phase_5_error() { + // Task 9: `gate trace` on the Tableau backend errors cleanly until + // Phase 5 (Task 15) lands the upstream `⟨ψ|P|ψ⟩` primitive. + let mut module: Module = Module::default(); + module.extra.n_qubits = 1; + module.strings.push("Z0".to_string()); + module + .code + .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + Value::String(0), + ))); + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::Trace)); + + let mut machine = PPVM::default(); + machine.load(&module).unwrap(); + machine.init().unwrap(); + // First step: const.string — succeeds. + machine.step_once().unwrap(); + // Second step: gate trace — errors with the Phase-5 message. + let err = machine.step_once().unwrap_err(); + assert!( + err.to_string() + .contains("not yet implemented on the Tableau backend"), + "expected Phase 5 placeholder error, got: {err}" + ); + } } From 067c34c1dfa6dff22002bf8755c925db7ee986d2 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 10 Jun 2026 09:09:51 +0200 Subject: [PATCH 65/95] Implement observable parser --- crates/ppvm-vihaco/src/lib.rs | 1 + crates/ppvm-vihaco/src/observable.rs | 350 +++++++++++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 crates/ppvm-vihaco/src/observable.rs diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 803efccf5..2c98875cd 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -5,6 +5,7 @@ pub mod bytecode; pub mod component; pub mod composite; pub mod measurements; +pub mod observable; pub mod shots; mod syntax; diff --git a/crates/ppvm-vihaco/src/observable.rs b/crates/ppvm-vihaco/src/observable.rs new file mode 100644 index 000000000..8b94ee8e1 --- /dev/null +++ b/crates/ppvm-vihaco/src/observable.rs @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Parse the `device circuit.observable` header value into Pauli-sum terms. +//! +//! Lives in `ppvm-vihaco` rather than `ppvm-runtime` because it only parses +//! header syntax that `ppvm-vihaco` consumes; `ppvm-runtime` has no notion of +//! a textual observable. The unrelated `PauliPattern::parse` in +//! `ppvm-runtime` is a *matcher* grammar (alternation, star, positional +//! anchors) and shares only the `I/X/Y/Z` alphabet with this one. +//! +//! Grammar (informal): +//! ```text +//! sum = WS* term (WS* sign WS* term)* WS* +//! term = coefficient (WS* '*')? WS* pauli_word +//! | pauli_word +//! sign = '+' | '-' +//! coefficient +//! = digits ('.' digits?)? ([eE] [+-]? digits)? +//! | '.' digits ([eE] [+-]? digits)? +//! pauli_word +//! = [IXYZ]{n_qubits} +//! ``` +//! +//! - The first term may have a leading `+` or `-` (no sign means `+`). +//! - Absent coefficient defaults to `1.0`. +//! - The `*` is only legal *after* a coefficient — bare `*ZZ` is rejected. +//! - The word must be exactly `n_qubits` characters from `I/X/Y/Z`. +//! +//! Rejected at parse time: +//! - Empty or whitespace-only input. +//! - Bare coefficients with no Pauli word. +//! - Words shorter or longer than `n_qubits`. +//! - Invalid Pauli characters. +//! - Missing `+`/`-` between terms. + +use chumsky::error::Simple; +use chumsky::extra; +use chumsky::prelude::*; +use eyre::{Result, eyre}; + +type Err<'src> = extra::Err>; + +/// Parse a Pauli-sum string like `"1.0*ZZ + 0.5*XX - 0.3*YY"` into a list of +/// `(word_source, coefficient)` pairs. Callers convert the word source to +/// their preferred Pauli-word type via `PauliWord::from` / `LossyPauliWord::from`. +pub fn parse_pauli_sum_terms(input: &str, n_qubits: usize) -> Result> { + if n_qubits == 0 { + return Err(eyre!("n_qubits must be at least 1")); + } + if input.trim().is_empty() { + return Err(eyre!("empty Pauli-sum input")); + } + pauli_sum_parser(n_qubits) + .parse(input) + .into_result() + .map_err(|errs| { + let joined = errs + .into_iter() + .map(|e| e.to_string()) + .collect::>() + .join("; "); + eyre!("invalid Pauli-sum `{input}`: {joined}") + }) +} + +fn pauli_sum_parser<'src>( + n_qubits: usize, +) -> impl Parser<'src, &'src str, Vec<(String, f64)>, Err<'src>> { + // Numeric literal: digits[.digits?]?[exp]? or .digits[exp]? + let digits = text::digits(10).at_least(1); + let exp = one_of("eE") + .then(one_of("+-").or_not()) + .then(digits) + .ignored(); + let mantissa = choice(( + digits + .then(just('.').then(digits.or_not()).or_not()) + .ignored(), + just('.').then(digits).ignored(), + )); + let coefficient = mantissa.then(exp.or_not()).to_slice().map(|s: &str| { + s.parse::() + .expect("mantissa+exponent grammar validates parseability") + }); + + // Pauli word: exactly n_qubits chars from IXYZ. + let pauli_word = one_of("IXYZ") + .repeated() + .exactly(n_qubits) + .collect::(); + + // Term: coefficient [*] word | bare word. + let term_with_coeff = coefficient + .then_ignore(just('*').padded().or_not()) + .then(pauli_word.clone()) + .map(|(c, w)| (w, c)); + let term_bare = pauli_word.map(|w| (w, 1.0)); + let term = choice((term_with_coeff, term_bare)); + + // Sign factor: +1 / -1. + let sign = choice((just('+').to(1.0_f64), just('-').to(-1.0_f64))); + + // First term: optional leading sign. + let first = sign + .clone() + .padded() + .or_not() + .then(term.clone()) + .map(|(s, (w, c))| (w, s.unwrap_or(1.0) * c)); + + // Subsequent terms: required + or - before the term. + let rest = sign.padded().then(term).map(|(s, (w, c))| (w, s * c)); + + let inner = first + .then(rest.repeated().collect::>()) + .map(|(first, mut rest)| { + rest.insert(0, first); + rest + }); + + inner.padded().then_ignore(end()) +} + +#[cfg(test)] +mod tests { + use super::*; + + // ─── Happy paths ──────────────────────────────────────────────────── + + #[test] + fn single_term_no_coefficient() { + let got = parse_pauli_sum_terms("ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 1.0)]); + } + + #[test] + fn single_term_with_explicit_star() { + let got = parse_pauli_sum_terms("0.5*ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 0.5)]); + } + + #[test] + fn single_term_without_star() { + let got = parse_pauli_sum_terms("0.5ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 0.5)]); + } + + #[test] + fn integer_coefficient() { + let got = parse_pauli_sum_terms("3XY", 2).unwrap(); + assert_eq!(got, vec![("XY".to_string(), 3.0)]); + } + + #[test] + fn leading_negative_no_coefficient() { + let got = parse_pauli_sum_terms("-ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), -1.0)]); + } + + #[test] + fn leading_negative_with_coefficient() { + let got = parse_pauli_sum_terms("-0.5*ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), -0.5)]); + } + + #[test] + fn leading_plus_no_coefficient() { + let got = parse_pauli_sum_terms("+ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 1.0)]); + } + + #[test] + fn multiple_terms_with_addition() { + let got = parse_pauli_sum_terms("1.0*ZZ + 0.5*XX", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 1.0), ("XX".to_string(), 0.5)]); + } + + #[test] + fn multiple_terms_with_subtraction() { + let got = parse_pauli_sum_terms("ZZ - XX", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 1.0), ("XX".to_string(), -1.0)]); + } + + #[test] + fn three_term_mixed() { + let got = parse_pauli_sum_terms("1.0*ZZ + 0.5*XX - 0.3*YY", 2).unwrap(); + assert_eq!( + got, + vec![ + ("ZZ".to_string(), 1.0), + ("XX".to_string(), 0.5), + ("YY".to_string(), -0.3), + ] + ); + } + + #[test] + fn scientific_notation_coefficient() { + let got = parse_pauli_sum_terms("1e-3*ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 1e-3)]); + } + + #[test] + fn unsigned_positive_exponent() { + let got = parse_pauli_sum_terms("1e3*ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 1000.0)]); + } + + #[test] + fn uppercase_exponent_with_explicit_sign() { + let got = parse_pauli_sum_terms("2.5E+2*ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 250.0)]); + } + + #[test] + fn coefficient_with_trailing_dot() { + let got = parse_pauli_sum_terms("1.*ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 1.0)]); + } + + #[test] + fn whitespace_tolerance_aggressive() { + let got = parse_pauli_sum_terms(" 1.0 * ZZ + 0.5 * XX ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 1.0), ("XX".to_string(), 0.5)]); + } + + #[test] + fn coefficient_starts_with_dot() { + let got = parse_pauli_sum_terms(".5*ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 0.5)]); + } + + #[test] + fn identity_in_word_is_valid() { + let got = parse_pauli_sum_terms("IZIZ", 4).unwrap(); + assert_eq!(got, vec![("IZIZ".to_string(), 1.0)]); + } + + #[test] + fn single_qubit_n_equals_one() { + let got = parse_pauli_sum_terms("0.5*X + Y - 0.25Z", 1).unwrap(); + assert_eq!( + got, + vec![ + ("X".to_string(), 0.5), + ("Y".to_string(), 1.0), + ("Z".to_string(), -0.25), + ] + ); + } + + #[test] + fn duplicate_word_emits_two_terms() { + // Coefficient collapse is the caller's job (via `PauliSum +=`); the + // parser stays a thin syntactic layer. + let got = parse_pauli_sum_terms("0.5*ZZ + 0.5*ZZ", 2).unwrap(); + assert_eq!(got, vec![("ZZ".to_string(), 0.5), ("ZZ".to_string(), 0.5)]); + } + + // ─── Error paths ──────────────────────────────────────────────────── + + #[test] + fn rejects_empty_input() { + let err = parse_pauli_sum_terms("", 2).unwrap_err(); + assert!(err.to_string().contains("empty"), "{err}"); + } + + #[test] + fn rejects_whitespace_only() { + let err = parse_pauli_sum_terms(" \t \n ", 2).unwrap_err(); + assert!(err.to_string().contains("empty"), "{err}"); + } + + #[test] + fn rejects_zero_qubits() { + let err = parse_pauli_sum_terms("ZZ", 0).unwrap_err(); + assert!(err.to_string().contains("n_qubits"), "{err}"); + } + + #[test] + fn rejects_bare_coefficient() { + let err = parse_pauli_sum_terms("0.5", 2).unwrap_err(); + assert!(err.to_string().contains("invalid Pauli-sum"), "{err}"); + } + + #[test] + fn rejects_bare_sign() { + let err = parse_pauli_sum_terms("+", 2).unwrap_err(); + assert!(err.to_string().contains("invalid Pauli-sum"), "{err}"); + } + + #[test] + fn rejects_short_word() { + let err = parse_pauli_sum_terms("Z", 2).unwrap_err(); + assert!(err.to_string().contains("invalid Pauli-sum"), "{err}"); + } + + #[test] + fn rejects_long_word() { + let err = parse_pauli_sum_terms("ZZZ", 2).unwrap_err(); + assert!(err.to_string().contains("invalid Pauli-sum"), "{err}"); + } + + #[test] + fn rejects_invalid_pauli_character() { + let err = parse_pauli_sum_terms("ZF", 2).unwrap_err(); + assert!(err.to_string().contains("invalid Pauli-sum"), "{err}"); + } + + #[test] + fn rejects_lowercase_pauli_character() { + let err = parse_pauli_sum_terms("zz", 2).unwrap_err(); + assert!(err.to_string().contains("invalid Pauli-sum"), "{err}"); + } + + #[test] + fn rejects_missing_separator_between_terms() { + let err = parse_pauli_sum_terms("ZZ XX", 2).unwrap_err(); + assert!(err.to_string().contains("invalid Pauli-sum"), "{err}"); + } + + #[test] + fn rejects_double_sign() { + let err = parse_pauli_sum_terms("++ZZ", 2).unwrap_err(); + assert!(err.to_string().contains("invalid Pauli-sum"), "{err}"); + } + + #[test] + fn rejects_star_without_coefficient() { + // Bare `*ZZ` is rejected — `*` is only legal after a coefficient. + let err = parse_pauli_sum_terms("*ZZ", 2).unwrap_err(); + assert!(err.to_string().contains("invalid Pauli-sum"), "{err}"); + } + + #[test] + fn rejects_trailing_garbage() { + let err = parse_pauli_sum_terms("ZZ garbage", 2).unwrap_err(); + assert!(err.to_string().contains("invalid Pauli-sum"), "{err}"); + } + + #[test] + fn rejects_trailing_garbage_after_complete_sum() { + // Distinct from `rejects_missing_separator_between_terms`: here a + // complete two-term sum parses successfully and the failure is the + // `end()` check after the last term. + let err = parse_pauli_sum_terms("ZZ + XX trailing", 2).unwrap_err(); + assert!(err.to_string().contains("invalid Pauli-sum"), "{err}"); + } +} From 380586742f804bf86e3b8389d6a7bba998b98acd Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 10 Jun 2026 09:21:34 +0200 Subject: [PATCH 66/95] Hook parser into constructors --- crates/ppvm-vihaco/src/component.rs | 39 +++++++++------- crates/ppvm-vihaco/src/composite.rs | 70 ++++++++++++++++++----------- 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 96b3e7588..7471aa45e 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -743,17 +743,19 @@ pub enum PauliSumCircuit { } impl PauliSumCircuit { - /// Build a PauliSum-backed circuit, seeding `state += (observable, 1.0)`. - /// `observable` must already be validated against `info.n_qubits` (use - /// [`validate_single_pauli_observable`] at the boundary). - pub fn new(info: &PPVMDeviceInfo, observable: &str) -> Self { + /// Build a PauliSum-backed circuit, seeding the state with every term: + /// `for (word, coef) in terms { state += (word, coef); }`. Words must + /// already be validated against `info.n_qubits` by the caller. + pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { macro_rules! build { ($variant:ident, $N:literal) => {{ let mut state = PauliSum::>::builder() .n_qubits(info.n_qubits) .strategy(paulisum_strategy(info)) .build(); - state += (observable, 1.0); + for (word, coef) in terms { + state += (word.as_str(), *coef); + } Self::$variant(PauliSumExecutor { state }) }}; } @@ -826,16 +828,19 @@ pub enum LossyPauliSumCircuit { } impl LossyPauliSumCircuit { - /// Build a LossyPauliSum-backed circuit, seeding `state += (observable, - /// 1.0)`. `observable` must already be validated against `info.n_qubits`. - pub fn new(info: &PPVMDeviceInfo, observable: &str) -> Self { + /// Build a LossyPauliSum-backed circuit, seeding every term via + /// `state += (word, coef)`. Words must already be validated against + /// `info.n_qubits` by the caller. + pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { macro_rules! build { ($variant:ident, $N:literal) => {{ let mut state = PauliSum::>::builder() .n_qubits(info.n_qubits) .strategy(paulisum_strategy(info)) .build(); - state += (observable, 1.0); + for (word, coef) in terms { + state += (word.as_str(), *coef); + } Self::$variant(LossyPauliSumExecutor { state }) }}; } @@ -929,18 +934,18 @@ impl Circuit { )) } - /// Build a PauliSum-backed circuit, seeding the state with `(observable, - /// 1.0)`. `observable` must already be validated against `info.n_qubits` - /// (see [`validate_single_pauli_observable`]); passing invalid input - /// results in a panic from the underlying word parser. - pub fn paulisum(info: &PPVMDeviceInfo, observable: &str) -> Self { - Self::PauliSum(PauliSumCircuit::new(info, observable)) + /// Build a PauliSum-backed circuit, seeding the state with every term in + /// `terms`. Each `(word, coef)` is added via `state += (word, coef)`; the + /// caller is responsible for having parsed/validated the words against + /// `info.n_qubits` (see `parse_observable_terms` in `composite.rs`). + pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { + Self::PauliSum(PauliSumCircuit::new(info, terms)) } /// Build a LossyPauliSum-backed circuit. Same contract as /// [`Circuit::paulisum`]. - pub fn lossy_paulisum(info: &PPVMDeviceInfo, observable: &str) -> Self { - Self::LossyPauliSum(LossyPauliSumCircuit::new(info, observable)) + pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { + Self::LossyPauliSum(LossyPauliSumCircuit::new(info, terms)) } fn execute( diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index b0230d7f4..4a843b2dc 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -322,12 +322,12 @@ impl PPVM { (BackendKind::Tableau, None) => Circuit::tableau(info), (BackendKind::Tableau, Some(seed)) => Circuit::tableau_with_seed(info, seed), (BackendKind::PauliSum, _) => { - let observable = validate_single_pauli_observable(info)?; - Circuit::paulisum(info, observable) + let terms = parse_observable_terms(info)?; + Circuit::paulisum(info, &terms) } (BackendKind::LossyPauliSum, _) => { - let observable = validate_single_pauli_observable(info)?; - Circuit::lossy_paulisum(info, observable) + let terms = parse_observable_terms(info)?; + Circuit::lossy_paulisum(info, &terms) } }; @@ -634,33 +634,18 @@ impl vihaco::Reset for PPVM { } } -/// Validate a single-Pauli-word observable header against `info.n_qubits` and -/// return the string slice for seeding. Phase 2 accepts dense words built from -/// `I`, `X`, `Y`, `Z` of length exactly `n_qubits`; Phase 3 (Task 11) replaces -/// this with the multi-term sum parser. -fn validate_single_pauli_observable(info: &PPVMDeviceInfo) -> Result<&str> { +/// Parse the `device circuit.observable` header into Pauli-sum terms ready to +/// seed a `PauliSum` / `LossyPauliSum` state. Single-Pauli observables from +/// Phase 2 keep working as the degenerate one-term case; multi-term sums like +/// `"1.0*ZZ + 0.5*XX"` are handled by [`parse_pauli_sum_terms`]. +fn parse_observable_terms(info: &PPVMDeviceInfo) -> Result> { let observable = info.observable.as_deref().ok_or_else(|| { eyre!( "the {:?} backend requires `device circuit.observable` to be set", info.backend ) })?; - let len = observable.chars().count(); - if len != info.n_qubits { - return Err(eyre!( - "observable length {len} does not match circuit.n_qubits {}", - info.n_qubits - )); - } - if let Some(bad) = observable - .chars() - .find(|c| !matches!(c, 'I' | 'X' | 'Y' | 'Z')) - { - return Err(eyre!( - "observable contains invalid Pauli character `{bad}`; Phase 2 accepts only I/X/Y/Z (multi-term sums land in Phase 3)" - )); - } - Ok(observable) + crate::observable::parse_pauli_sum_terms(observable, info.n_qubits) } #[cfg(test)] @@ -1191,6 +1176,37 @@ mod tests { Ok(()) } + #[test] + fn paulisum_multi_term_observable_seeds_all_terms() -> eyre::Result<()> { + // Task 11: a sum-valued observable seeds every term. With + // `"ZZ + 0.5*XX"` the state holds `1.0 * ZZ + 0.5 * XX`; tracing + // `[XZ]0[XZ]1` matches both words and returns 1.0 + 0.5 = 1.5. + let mut module: Module = Module::default(); + module.extra.n_qubits = 2; + module.extra.backend = BackendKind::PauliSum; + module.extra.observable = Some("ZZ + 0.5*XX".to_string()); + module.strings.push("[XZ]0[XZ]1".to_string()); + + module + .code + .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + Value::String(0), + ))); + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::Trace)); + + let mut machine = PPVM::default(); + machine.load(&module)?; + machine.init()?; + for _ in 0..module.code.len() { + machine.step_once()?; + } + + assert_eq!(machine.trace_record(), vec![1.5]); + Ok(()) + } + #[test] fn paulisum_init_rejects_missing_observable() { // Task 8 requires `device circuit.observable` for PauliSum / Lossy. @@ -1219,8 +1235,8 @@ mod tests { machine.load(&module).unwrap(); let err = machine.init().unwrap_err(); assert!( - err.to_string().contains("does not match"), - "expected length-mismatch error, got: {err}" + err.to_string().contains("invalid Pauli-sum"), + "expected parser rejection, got: {err}" ); } From 35856ad77d086e5406e7c22cf358cacefef0d15d Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 10 Jun 2026 09:52:59 +0200 Subject: [PATCH 67/95] Intern strings and add a bunch of end-to-end tests --- crates/ppvm-vihaco/src/syntax.rs | 17 ++++ .../ppvm-vihaco/tests/paulisum_bell_trace.sst | 18 ++++ .../tests/paulisum_measure_error.sst | 9 ++ .../tests/paulisum_multi_term_trace.sst | 11 +++ .../tests/paulisum_trotter_truncate.sst | 37 +++++++ crates/ppvm-vihaco/tests/sst_fixtures.rs | 96 +++++++++++++++++++ 6 files changed, 188 insertions(+) create mode 100644 crates/ppvm-vihaco/tests/paulisum_bell_trace.sst create mode 100644 crates/ppvm-vihaco/tests/paulisum_measure_error.sst create mode 100644 crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst create mode 100644 crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index 87c8f700c..d17a5b374 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -84,6 +84,23 @@ impl PPVMResolver { }; 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 diff --git a/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst new file mode 100644 index 000000000..9d6ed979b --- /dev/null +++ b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst @@ -0,0 +1,18 @@ +device circuit.n_qubits 2; +device circuit.backend paulisum; +device circuit.observable ZZ; + +fn @main() { + // Textbook H(0); CNOT(0,1) — emit reversed for Heisenberg propagation. + const.u64 0 + const.u64 1 + gate cnot + + const.u64 0 + gate h + + // Trace against |00>: match Z-or-identity on every qubit. + const.str "Z?*" + gate trace + ret +} diff --git a/crates/ppvm-vihaco/tests/paulisum_measure_error.sst b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst new file mode 100644 index 000000000..f7f680ffe --- /dev/null +++ b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst @@ -0,0 +1,9 @@ +device circuit.n_qubits 1; +device circuit.backend paulisum; +device circuit.observable Z; + +fn @main() { + const.u64 0 + gate measure + ret +} diff --git a/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst new file mode 100644 index 000000000..aae630dd8 --- /dev/null +++ b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst @@ -0,0 +1,11 @@ +device circuit.n_qubits 2; +device circuit.backend paulisum; +device circuit.observable 1.0*ZZ+0.5*XX; + +fn @main() { + // No gates — the multi-term observable seeds the state directly. + // Pattern `[XZ]?*` matches both ZZ (coef 1.0) and XX (coef 0.5). + const.str "[XZ]?*" + gate trace + ret +} diff --git a/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst new file mode 100644 index 000000000..3725baf46 --- /dev/null +++ b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst @@ -0,0 +1,37 @@ +device circuit.n_qubits 2; +device circuit.backend paulisum; +device circuit.observable ZZ; +device circuit.coefficient_threshold 1e-6; + +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 + gate rxx + + const.u64 0 + const.u64 1 + const.f64 0.05 + gate rzz + + gate truncate + + const.u64 0 + const.u64 1 + const.f64 0.1 + gate rxx + + const.u64 0 + const.u64 1 + const.f64 0.05 + gate rzz + + gate truncate + + const.str "Z?*" + gate trace + ret +} diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 3acc424bc..89c033bfc 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -198,6 +198,102 @@ fn function_call_branch_on_both_returned_values() { ); } +// ─── Task 12: PauliSum / LossyPauliSum end-to-end via .sst source ──────── + +#[test] +fn paulisum_bell_zz_trace_through_sst() { + // Bell-state ⟨ZZ⟩ via PauliSum. Textbook circuit H(0); CNOT(0,1) is + // emitted reversed for Heisenberg propagation: `gate cnot; gate h`. + // Conjugating ZZ by CNOT(0,1) gives Z_1 (= IZ); H on q0 leaves IZ + // untouched. Tracing against |00> matches IZ (pattern `Z?*`) and + // returns +1.0 — matching ⟨Φ+|ZZ|Φ+⟩ = 1. + let machine = ppvm_vihaco::run_file("tests/paulisum_bell_trace.sst") + .unwrap_or_else(|e| panic!("run paulisum_bell_trace.sst: {e:?}")); + let trace = machine.trace_record(); + assert_eq!(trace.len(), 1, "expected one trace emission"); + assert!( + (trace[0] - 1.0).abs() < 1e-12, + "expected ⟨ZZ⟩ = 1.0, got {}", + trace[0] + ); +} + +#[test] +fn paulisum_multi_term_observable_trace_through_sst() { + // Phase 3 wiring: a 2-term observable `1.0*ZZ+0.5*XX` parses on the + // header, seeds both terms, and an end-of-program trace picks up their + // contributions. With no gates applied, tracing `[XZ]?*` matches both + // ZZ and XX directly → coef sum = 1.0 + 0.5 = 1.5. + let machine = ppvm_vihaco::run_file("tests/paulisum_multi_term_trace.sst") + .unwrap_or_else(|e| panic!("run paulisum_multi_term_trace.sst: {e:?}")); + let trace = machine.trace_record(); + assert_eq!(trace.len(), 1); + assert!( + (trace[0] - 1.5).abs() < 1e-12, + "expected 1.5, got {}", + trace[0] + ); +} + +#[test] +fn paulisum_trotter_matches_pure_rust_reference() { + // Two Trotter layers of RXX(0.1) + RZZ(0.05), interleaved with explicit + // `gate truncate`. The .sst-driven path should agree bit-for-bit with a + // pure Rust PauliSum running the same gates: `indexmap::ByteFxHashF64` + // gives deterministic iteration order (Decision 7), so truncation order + // and float accumulation are stable across both paths. + + let machine = ppvm_vihaco::run_file("tests/paulisum_trotter_truncate.sst") + .unwrap_or_else(|e| panic!("run paulisum_trotter_truncate.sst: {e:?}")); + let through_sst = machine.trace_record(); + assert_eq!(through_sst.len(), 1, "expected one trace emission"); + + // Pure Rust reference: same N=8 / strategy / gate order as the PauliSum + // Bits64 bucket in `ppvm_vihaco::component`. + use ppvm_runtime::config::indexmap::ByteFxHashF64; + use ppvm_runtime::prelude::*; + use ppvm_runtime::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; + type RefConfig = ByteFxHashF64<8, CombinedStrategy>; + + let mut state: PauliSum = PauliSum::builder() + .n_qubits(2) + .strategy(CombinedStrategy( + CoefficientThreshold(1e-6), + MaxPauliWeight(usize::MAX), + )) + .build(); + state += ("ZZ", 1.0); + state.rxx(0, 1, 0.1); + state.rzz(0, 1, 0.05); + state.truncate(); + state.rxx(0, 1, 0.1); + state.rzz(0, 1, 0.05); + state.truncate(); + let pat = PauliPattern::parse("Z?*").expect("parse pattern"); + let reference = state.trace(&pat); + + assert_eq!( + through_sst[0], reference, + ".sst-driven trace must match pure Rust reference bit-for-bit" + ); +} + +#[test] +fn paulisum_measure_returns_unsupported_error() { + // Per Decision 11, Measure on PauliSum hits the dispatch fallback with a + // clear "not supported on the PauliSum backend" error. + let mut machine = PPVM::default(); + machine + .load_file("tests/paulisum_measure_error.sst") + .unwrap_or_else(|e| panic!("load paulisum_measure_error.sst: {e:?}")); + let err = machine.run().unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("not supported on the PauliSum backend"), + "expected PauliSum-rejection error, got: {msg}" + ); +} + // ─── Auto-detect via load_file: route by content, not extension ─────────── #[test] From c83bd4ec9120d3788776f04313f82f2bcd5be643 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 10 Jun 2026 10:17:36 +0200 Subject: [PATCH 68/95] Add ShotRecord, formatting and update CLI --- crates/ppvm-cli/src/commands.rs | 100 +++++++++++++++++++++++++++++--- crates/ppvm-vihaco/src/shots.rs | 28 ++++++--- 2 files changed, 112 insertions(+), 16 deletions(-) diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index 44f1858f6..3dc0b35f0 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -4,6 +4,7 @@ use eyre::{Result, WrapErr}; use ppvm_vihaco::composite::{PPVM, StepOutcome}; use ppvm_vihaco::measurements::MeasurementResult; +use ppvm_vihaco::shots::ShotRecord; use std::io::{BufRead, Write}; use std::path::Path; @@ -43,7 +44,7 @@ pub fn run( } let text = match format { - MeasurementFormat::Bits => format_shots(&records), + MeasurementFormat::Bits => format_shot_records(&records), MeasurementFormat::Debug => format!("{records:?}"), }; @@ -58,15 +59,31 @@ pub fn run( Ok(()) } -/// Render one shot per line, each as a flat bit string. -fn format_shots(records: &[Vec]) -> String { +/// Render one shot per line. +fn format_shot_records(records: &[ShotRecord]) -> String { records .iter() - .map(|shot| format_shot(shot)) + .map(format_shot_record) .collect::>() .join("\n") } +/// Render one shot. Layout: +/// - Tableau-style (only measurements): `bits` — same as the pre-trace format. +/// - PauliSum-style (only traces): comma-separated floats. +/// - Both present: `bits | t0,t1,...`. +/// - Both empty: empty string. +fn format_shot_record(record: &ShotRecord) -> String { + let bits = format_shot(&record.measurements); + let traces = format_traces(&record.traces); + match (bits.is_empty(), traces.is_empty()) { + (false, false) => format!("{bits} | {traces}"), + (false, true) => bits, + (true, false) => traces, + (true, true) => String::new(), + } +} + /// Render a shot's full measurement record as one flat bit string, all events /// and qubits concatenated: `Zero` → `0`, `One` → `1`, `Lost` → `2` (the /// outcome's own enum value). An empty record renders as the empty string. @@ -78,6 +95,16 @@ fn format_shot(record: &[MeasurementResult]) -> String { .collect() } +/// Render trace values as comma-separated floats with default `f64` formatting +/// (`1.0` → `"1"`, `1.5` → `"1.5"`, etc.). Empty record renders as empty string. +fn format_traces(traces: &[f64]) -> String { + traces + .iter() + .map(|t| format!("{t}")) + .collect::>() + .join(",") +} + pub fn parse(file: &str, format: Format) -> Result<()> { let source = std::fs::read_to_string(file).wrap_err_with(|| format!("failed to read {file}"))?; @@ -206,6 +233,10 @@ fn debug_loop( "Measurements: {}", format_shot(&machine.measurement_record()) )?; + let traces = machine.trace_record(); + if !traces.is_empty() { + writeln!(output, "Traces: {}", format_traces(&traces))?; + } if !ever_paused { writeln!( output, @@ -227,6 +258,10 @@ fn print_location(machine: &PPVM, output: &mut impl Write) -> Result<()> { "measurements: {}", format_shot(&machine.measurement_record()) )?; + let traces = machine.trace_record(); + if !traces.is_empty() { + writeln!(output, "traces: {}", format_traces(&traces))?; + } Ok(()) } @@ -307,12 +342,41 @@ mod tests { } #[test] - fn format_shots_joins_shots_with_newlines() { + fn format_shot_records_joins_shots_with_newlines() { let shots = vec![ - vec![row(&[MeasurementOutcome::One])], - vec![row(&[MeasurementOutcome::Zero])], + ShotRecord { + measurements: vec![row(&[MeasurementOutcome::One])], + traces: vec![], + }, + ShotRecord { + measurements: vec![row(&[MeasurementOutcome::Zero])], + traces: vec![], + }, ]; - assert_eq!(format_shots(&shots), "1\n0"); + assert_eq!(format_shot_records(&shots), "1\n0"); + } + + #[test] + fn format_shot_record_traces_only_shows_floats() { + let shot = ShotRecord { + measurements: vec![], + traces: vec![1.0, 0.5], + }; + assert_eq!(format_shot_record(&shot), "1,0.5"); + } + + #[test] + fn format_shot_record_both_present_joins_with_pipe() { + let shot = ShotRecord { + measurements: vec![row(&[MeasurementOutcome::One])], + traces: vec![0.25], + }; + assert_eq!(format_shot_record(&shot), "1 | 0.25"); + } + + #[test] + fn format_shot_record_both_empty_is_empty_string() { + assert_eq!(format_shot_record(&ShotRecord::default()), ""); } // ─── run ─────────────────────────────────────────────────────────── @@ -340,6 +404,26 @@ mod tests { let _ = fs::remove_file(&out); } + #[test] + fn run_outputs_trace_record_for_paulisum_backend() { + // Single-qubit PauliSum with observable Z, no gates, trace `Z?*` → 1.0. + // The .sst-driven path should surface the trace value in `run`'s output. + const TRACE_PROGRAM: &str = "device circuit.n_qubits 1;\n\ + device circuit.backend paulisum;\n\ + device circuit.observable Z;\n\ + fn @main() { const.str \"Z?*\"\n gate trace\n ret }\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); + + run(&src, 1, None, out.to_str(), false, MeasurementFormat::Bits).unwrap(); + let contents = fs::read_to_string(&out).unwrap(); + assert_eq!(contents, "1\n", "expected trace value 1.0 in output"); + + let _ = fs::remove_file(&src); + let _ = fs::remove_file(&out); + } + #[test] fn run_errors_with_context_on_missing_file() { let err = run( diff --git a/crates/ppvm-vihaco/src/shots.rs b/crates/ppvm-vihaco/src/shots.rs index 4fbb91719..7d6ca8e5c 100644 --- a/crates/ppvm-vihaco/src/shots.rs +++ b/crates/ppvm-vihaco/src/shots.rs @@ -13,6 +13,14 @@ use crate::PPVMModule; use crate::composite::PPVM; use crate::measurements::MeasurementResult; +/// One shot's full output: the measurement record and the trace-instruction +/// record. Either may be empty depending on what the program emits. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ShotRecord { + pub measurements: Vec, + pub traces: Vec, +} + /// Below this many shots, parallelism's overhead outweighs its benefit and we /// always run serially. Provisional — tune with benchmarks. pub const PARALLEL_SHOT_THRESHOLD: usize = 128; @@ -25,12 +33,15 @@ fn shot_seed(base: Option, index: usize) -> Option { base.map(|b| b.wrapping_add(index as u64)) } -/// Run a single shot on a fresh machine and return its measurement record. -fn run_one_shot(module: &PPVMModule, seed: Option) -> eyre::Result> { +/// Run a single shot on a fresh machine and return both records. +fn run_one_shot(module: &PPVMModule, seed: Option) -> eyre::Result { let mut machine = PPVM::default(); machine.load(module)?; machine.run_with_seed(seed)?; - Ok(machine.measurement_record()) + Ok(ShotRecord { + measurements: machine.measurement_record(), + traces: machine.trace_record(), + }) } /// Run `shots` shots serially. One entry per shot, in order. @@ -38,7 +49,7 @@ pub fn run_shots_serial( module: &PPVMModule, shots: usize, seed: Option, -) -> eyre::Result>> { +) -> eyre::Result> { (0..shots) .map(|i| run_one_shot(module, shot_seed(seed, i))) .collect() @@ -54,7 +65,7 @@ pub fn run_shots_parallel( module: &PPVMModule, shots: usize, seed: Option, -) -> eyre::Result>> { +) -> eyre::Result> { use rayon::prelude::*; (0..shots) @@ -79,7 +90,7 @@ pub fn run_shots( module: &PPVMModule, shots: usize, seed: Option, -) -> eyre::Result>> { +) -> eyre::Result> { #[cfg(feature = "rayon")] if should_parallelize(rayon::current_num_threads(), shots) { return run_shots_parallel(module, shots, seed); @@ -133,8 +144,9 @@ mod tests { assert_eq!(records.len(), 5); for shot in &records { // One measurement event, one qubit, deterministically |0>. - assert_eq!(shot.len(), 1); - assert_eq!(shot[0].as_slice(), [MeasurementOutcome::Zero]); + assert_eq!(shot.measurements.len(), 1); + assert_eq!(shot.measurements[0].as_slice(), [MeasurementOutcome::Zero]); + assert!(shot.traces.is_empty(), "Tableau backend emits no traces"); } } From 44fb416fe1d3d0afbad0eb554e3bc44acfcbf044 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 10 Jun 2026 10:26:27 +0200 Subject: [PATCH 69/95] Update skills --- AGENTS.md | 7 +- crates/ppvm-cli/examples/heisenberg_zz.sst | 17 +++++ skills/ppvm-usage/SKILL.md | 83 +++++++++++++++++++++- 3 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 crates/ppvm-cli/examples/heisenberg_zz.sst diff --git a/AGENTS.md b/AGENTS.md index ab463d7f4..9cbbc4747 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,9 +24,10 @@ ion add QuEraComputing/ppvm/skills/ppvm-usage The skill (`skills/ppvm-usage/SKILL.md` in this repo) covers the Heisenberg / Schrödinger gate-order trap, `Config`-generic `PauliSum` usage, truncation -strategies, and Python / Rust call sites for both backends. Read it before -the Developer Guide if your task is *using* ppvm rather than modifying its -internals. +strategies, Python / Rust call sites for both backends, and the `.sst` +textual program format run by `ppvm-cli` (backend selection, multi-term +observables, `gate trace` / `gate truncate`). Read it before the Developer +Guide if your task is *using* ppvm rather than modifying its internals. ## TL;DR for agents diff --git a/crates/ppvm-cli/examples/heisenberg_zz.sst b/crates/ppvm-cli/examples/heisenberg_zz.sst new file mode 100644 index 000000000..eebe038c3 --- /dev/null +++ b/crates/ppvm-cli/examples/heisenberg_zz.sst @@ -0,0 +1,17 @@ +device circuit.n_qubits 2; +device circuit.backend paulisum; +device circuit.observable 1.0*ZZ+0.5*XX; +device circuit.coefficient_threshold 1e-10; + +// Heisenberg-picture run on a PauliSum target. The header observable +// `1.0*ZZ + 0.5*XX` seeds two terms; tracing `[XZ]?*` filters both ZZ +// (coef 1.0) and XX (coef 0.5), so the trace is their coefficient sum. +// +// No gates here, so the trace returns the seeded observable's coefficient +// sum directly: 1.0 + 0.5 = 1.5. Add `gate cnot; gate h; gate truncate` +// in textbook-reversed order to evolve before tracing. +fn @main() { + const.str "[XZ]?*" + gate trace + ret +} diff --git a/skills/ppvm-usage/SKILL.md b/skills/ppvm-usage/SKILL.md index 9bfc6bdb2..18b0a70b8 100644 --- a/skills/ppvm-usage/SKILL.md +++ b/skills/ppvm-usage/SKILL.md @@ -1,6 +1,6 @@ --- name: ppvm-usage -description: Authoritative usage guide for ppvm, a fast quantum-circuit simulator with a Rust core and Python bindings (`ppvm-runtime`, `ppvm-tableau`, `ppvm-sym`, `ppvm-stim`, `ppvm` Python package). Use this skill whenever a task touches ppvm — importing `ppvm` in Python, depending on any `ppvm-*` crate in Rust, writing or modifying Pauli-propagation code, building or running circuits against the generalized stabilizer tableau, executing Stim programs, modelling depolarizing or loss noise, or even just answering "how do I do X in ppvm". Use it even when the user only hints at ppvm (mentions Pauli strings + truncation, or `GeneralizedTableau`, or "Bloqade simulation backend"). Skipping this skill is a top source of broken examples — the API has several non-obvious conventions (Heisenberg gate order, `Config`-generic types, kwargs-not-classes truncation) that look reasonable but are wrong if guessed. +description: Authoritative usage guide for ppvm, a fast quantum-circuit simulator with a Rust core and Python bindings (`ppvm-runtime`, `ppvm-tableau`, `ppvm-sym`, `ppvm-stim`, `ppvm-vihaco`, `ppvm` Python package). Use this skill whenever a task touches ppvm — importing `ppvm` in Python, depending on any `ppvm-*` crate in Rust, writing or running `.sst` programs through `ppvm-cli`, writing or modifying Pauli-propagation code, building or running circuits against the generalized stabilizer tableau, executing Stim programs, modelling depolarizing or loss noise, or even just answering "how do I do X in ppvm". Use it even when the user only hints at ppvm (mentions Pauli strings + truncation, or `GeneralizedTableau`, or "Bloqade simulation backend"). Skipping this skill is a top source of broken examples — the API has several non-obvious conventions (Heisenberg gate order, `Config`-generic types, kwargs-not-classes truncation) that look reasonable but are wrong if guessed. allowed-tools: Bash, Read, Write, Edit --- @@ -351,11 +351,90 @@ What *not* to do: - Don't add a `# TODO: upstream this to ppvm` and move on. - Don't reimplement a ppvm primitive in the user's project just because the upstream version is awkward — fix the upstream awkwardness with an issue. +## Running programs from `.sst` source (`ppvm-vihaco` / `ppvm-cli`) + +`.sst` is the textual program format that `ppvm-cli` runs. A module has a +`device` header block selecting the backend and any initial state, then one +or more `fn @()` bodies. Each `gate ` instruction pops typed +operands from the CPU stack and dispatches to the runtime. + +### Backend selection + +```sst +device circuit.n_qubits 2; +device circuit.backend paulisum; // tableau | paulisum | lossy_paulisum +device circuit.observable 1.0*ZZ+0.5*XX; // initial PauliSum state (PauliSum/Lossy only) +device circuit.coefficient_threshold 1e-6; +device circuit.max_pauli_weight 8; // PauliSum/Lossy only; defaults to no cap +``` + +- **`tableau`** (default): `GeneralizedTableau`, Schrödinger picture, measurements. +- **`paulisum`** / **`lossy_paulisum`**: `PauliSum`/`LossyPauliSum`, Heisenberg + picture. Require `circuit.observable`. `Measure`/`Reset` error at runtime. + +### Multi-term observable syntax + +`circuit.observable` accepts a Pauli-sum string: terms separated by `+`/`-`, +each term an optional coefficient (decimal or scientific) followed by an +`I/X/Y/Z` word of length exactly `n_qubits`. The `*` between coefficient and +word is optional. **No internal whitespace is allowed in the header value** +— the header parser stops at the first space — so write +`1.0*ZZ+0.5*XX-0.3*YY` rather than `1.0*ZZ + 0.5*XX - 0.3*YY`. + +### Gate-order convention + +The runtime applies `gate ...` instructions in code order on every backend. +Whoever emits the `.sst` is responsible for emitting gates in the right +direction for the chosen picture: **forward** for Tableau (Schrödinger), +**reversed** for PauliSum/Lossy (Heisenberg). Textbook `H(0); CNOT(0,1)` on +a PauliSum target compiles to `gate cnot; gate h`, not the other way around. + +### `gate trace` and `gate truncate` + +```sst +const.str "Z?*" +gate trace // PauliSum/Lossy: pushes state.trace(&pattern) as f64 +gate truncate // PauliSum/Lossy: state.truncate(); Tableau: silent no-op +``` + +`trace` pops a `Value::String` (Pauli-pattern source — same grammar as +`PauliPattern::parse`), evaluates the backend-specific trace, and appends to +the machine's `trace_record` *and* pushes the value back as `Value::F64`. + +**Asymmetric semantics by backend:** +- **PauliSum / LossyPauliSum:** `state.trace(&pat)` is a filter coefficient + sum — sum of `c_P` over terms whose word matches `pat`. Use `"Z?*"` to + compute `⟨0…0|state|0…0⟩`. +- **Tableau:** `trace` returns `Σ_{P matches pat} ⟨ψ|P|ψ⟩` (Phase 5, not + yet wired — currently errors with "trace not yet implemented on the + Tableau backend"). + +These are honest natural primitives for each backend; the same operand +will not give the same number across backends. Users shouldn't expect +agreement on a shared input. + +`truncate` takes no operand and applies the configured strategy +(`CoefficientThreshold` + `MaxPauliWeight`) to the current state. On +Tableau it's a silent no-op — gate methods already prune via +`coefficient_threshold`. **Without explicit `gate truncate` calls in the +.sst, PauliSum runs do not truncate** — the compiler that emits the .sst +decides where to place them. + +### Running + +```bash +ppvm run program.sst --shots 100 --seed 42 +``` + +For PauliSum runs the trace values appear in the per-shot output (`run` +shows `bits | trace0,trace1,...` when both records are non-empty, just +the floats when only traces, just the bits when only measurements). + ## Where to go next - **`docs/src/pages/develop.astro`** (rendered at `/develop/`) — canonical developer guide: architecture, build/test, extending ppvm, "where to look for X" table. Read this if your task is to *modify* ppvm rather than *use* it. - **`docs/src/pages/api.astro`** (rendered at `/api/`) — full Rust + Python API reference, generated from rustdoc and griffe. -- **Examples:** `examples/trotter.rs`, `examples/symbolic.rs`, `examples/msd.rs` (Rust); `ppvm-python/docs/examples/trotter.py`, `msd.py` (Python). +- **Examples:** `examples/trotter.rs`, `examples/symbolic.rs`, `examples/msd.rs` (Rust); `ppvm-python/docs/examples/trotter.py`, `msd.py` (Python); `crates/ppvm-cli/examples/*.sst` and `crates/ppvm-vihaco/tests/*.sst` (`.sst` source). - **`AGENTS.md`** at repo root — pointer file with the agent-specific TL;DR. The repo's `Config`-trait generics are load-bearing. If you're tempted to introduce runtime dispatch on the Rust side to "simplify", that's a strong signal you should refactor the type alias and stay inside the bound instead. From 5e8e9e0314a5dc3d3b17de5fd31f05877a40639d Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 10 Jun 2026 14:51:30 +0200 Subject: [PATCH 70/95] Trace for tableau --- crates/ppvm-tableau/src/data.rs | 30 +++ crates/ppvm-tableau/src/expectation.rs | 267 +++++++++++++++++++++++++ crates/ppvm-tableau/src/lib.rs | 2 + crates/ppvm-tableau/src/measure.rs | 9 +- 4 files changed, 303 insertions(+), 5 deletions(-) create mode 100644 crates/ppvm-tableau/src/expectation.rs diff --git a/crates/ppvm-tableau/src/data.rs b/crates/ppvm-tableau/src/data.rs index 2b40cbc54..5474e9aaf 100644 --- a/crates/ppvm-tableau/src/data.rs +++ b/crates/ppvm-tableau/src/data.rs @@ -814,6 +814,36 @@ where (p_word.phase, stab_anticomm_bits, destab_anticomm_bits) } + /// Multi-qubit generalization of [`compute_decomposition`]: conjugate an + /// arbitrary `PauliWord` through the tableau and return the same triple + /// `(phase, stab_anticomm_bits, destab_anticomm_bits)`. + /// + /// Algorithm: call [`compute_decomposition`] for each non-identity qubit + /// in the input, then multiply the resulting single-qubit conjugates in + /// canonical-basis form `i^φ X^x Z^z`. Pauli multiplication picks up a + /// `(-1)^{popcount(z_running & x_new)}` cross-phase from + /// `Z^z_a X^x_b = (-1)^{z_a · x_b} X^x_b Z^z_a`. + pub(crate) fn compute_decomposition_word(&self, word: &W) -> (u8, I, I) + where + <::Storage as BitView>::Store: PrimInt, + { + let mut phase = 0u8; + let mut stab_anticomm = I::zero(); + let mut destab_anticomm = I::zero(); + for q in 0..self.n_qubits() { + let p_q = word.get(q); + if p_q == Pauli::I { + continue; + } + let (q_phase, q_stab, q_destab) = self.compute_decomposition(q, p_q); + let cross = 2 * (symplectic_inner(destab_anticomm, q_stab) as u8 % 2); + phase = (phase + q_phase + cross) % 4; + stab_anticomm = stab_anticomm ^ q_stab; + destab_anticomm = destab_anticomm ^ q_destab; + } + (phase, stab_anticomm, destab_anticomm) + } + /// every basis index is a bit string alpha defining the basis state /// the phase when applying a Pauli is the product of all destabilizer phases /// and the phase contributions from the commutation relations diff --git a/crates/ppvm-tableau/src/expectation.rs b/crates/ppvm-tableau/src/expectation.rs new file mode 100644 index 000000000..339a809ea --- /dev/null +++ b/crates/ppvm-tableau/src/expectation.rs @@ -0,0 +1,267 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Pauli-string expectation values for `GeneralizedTableau`. +//! +//! Two entry points: +//! +//! - [`GeneralizedTableau::expectation`] — single-Pauli `⟨ψ|P|ψ⟩` for a +//! `PauliWord`. Conjugates `P` through the tableau and overlaps the +//! resulting Pauli with the sparse coefficient vector using the same +//! formulas as the measurement code. +//! - [`GeneralizedTableau::trace`] — `Σ_{P matches pattern} ⟨ψ|P|ψ⟩` for a +//! `PauliPattern`. Enumerates the matching Paulis and sums their +//! expectations. +//! +//! Decision 9 in the multi-backend plan calls these out as the natural +//! primitive for the tableau backend; semantics intentionally diverge from +//! the PauliSum trace. + +use crate::data::GeneralizedTableau; +use crate::prelude::*; +use bitvec::view::BitView; +use fxhash::FxHashMap as HashMap; +use num::PrimInt; +use num::complex::{Complex, Complex64, ComplexFloat}; +use num::traits::{One, ToPrimitive, Zero}; +use ppvm_runtime::pattern::PauliPattern; +use std::fmt::Debug; + +impl GeneralizedTableau +where + T: Config, + <::Storage as BitView>::Store: PrimInt, + C: SparseVector, I> + Debug, + T::Coeff: One + + Zero + + Clone + + num::Num + + ToPrimitive + + Debug + + std::ops::Mul + + PartialOrd + + Send + + Sync, + Complex: std::ops::Mul> + + From + + std::ops::MulAssign + + std::ops::AddAssign + + One + + ComplexFloat + + Copy, + I: TableauIndex + Debug + Send + Sync, +{ + /// `⟨ψ|word|ψ⟩` for the multi-qubit Pauli `word`. + /// + /// Conjugates `word` through the Clifford tableau (giving a Pauli on the + /// canonical basis: an X-mask, Z-mask, and `i^φ` phase), then sums + /// `⟨α|P_conj|β⟩ c_α* c_β` over the sparse coefficient vector. Always + /// returns a real number (Hermitian operator on a normalized state). + pub fn expectation(&self, word: &W) -> f64 { + let (phase, stab_anticomm, destab_anticomm) = self.compute_decomposition_word(word); + let entries: Vec<(Complex, I)> = self.coefficients.clone().into_iter().collect(); + if stab_anticomm == I::zero() { + Self::overlap_case_b(&entries, phase, destab_anticomm) + } else { + let coeff_map: HashMap> = + entries.into_iter().map(|(v, i)| (i, v)).collect(); + let odd_phase_mask = self.odd_phase_destabilizer_mask(); + Self::overlap_case_a( + &coeff_map, + phase, + destab_anticomm, + stab_anticomm, + odd_phase_mask, + ) + } + } + + /// `Σ_{P matches pattern} ⟨ψ|P|ψ⟩`. + /// + /// Enumerates every `PauliWord` accepted by `pattern` via + /// [`PauliPattern::enumerate_matches`] and sums their expectations. + /// Star quantifiers (`X*`) panic — the pattern must be bounded; use + /// counted repetition (`Z?{n}`) or positional anchors instead. + pub fn trace(&self, pattern: &PauliPattern) -> f64 { + let mut sum = 0.0f64; + for word in pattern.enumerate_matches::(self.n_qubits()) { + sum += self.expectation(&word); + } + sum + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ppvm_runtime::config::fxhash::ByteF64; + + type TestTableau = GeneralizedTableau>; + + fn word(s: &str) -> PauliWord { + s.into() + } + + fn assert_close(actual: f64, expected: f64, tol: f64) { + assert!( + (actual - expected).abs() < tol, + "expected {expected}, got {actual} (|Δ| = {})", + (actual - expected).abs() + ); + } + + // ─── Single-qubit expectations ────────────────────────────────────── + + #[test] + fn expectation_z_on_zero_state_is_one() { + let tab: TestTableau = GeneralizedTableau::new(1, 1e-12); + assert_close(tab.expectation(&word("Z")), 1.0, 1e-12); + } + + #[test] + fn expectation_x_on_zero_state_is_zero() { + let tab: TestTableau = GeneralizedTableau::new(1, 1e-12); + assert_close(tab.expectation(&word("X")), 0.0, 1e-12); + } + + #[test] + fn expectation_identity_on_zero_state_is_one() { + let tab: TestTableau = GeneralizedTableau::new(1, 1e-12); + assert_close(tab.expectation(&word("I")), 1.0, 1e-12); + } + + #[test] + fn expectation_x_on_plus_state_is_one() { + let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12); + tab.h(0); + assert_close(tab.expectation(&word("X")), 1.0, 1e-12); + assert_close(tab.expectation(&word("Z")), 0.0, 1e-12); + } + + // ─── Bell state ⟨Φ+|·|Φ+⟩ ──────────────────────────────────────────── + + fn bell() -> TestTableau { + let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12); + tab.h(0); + tab.cnot(0, 1); + tab + } + + #[test] + fn bell_state_pauli_expectations() { + let tab = bell(); + assert_close(tab.expectation(&word("II")), 1.0, 1e-12); + assert_close(tab.expectation(&word("ZZ")), 1.0, 1e-12); + assert_close(tab.expectation(&word("XX")), 1.0, 1e-12); + assert_close(tab.expectation(&word("YY")), -1.0, 1e-12); + // Cross terms vanish for the Bell state. + assert_close(tab.expectation(&word("IZ")), 0.0, 1e-12); + assert_close(tab.expectation(&word("ZI")), 0.0, 1e-12); + assert_close(tab.expectation(&word("XZ")), 0.0, 1e-12); + assert_close(tab.expectation(&word("YX")), 0.0, 1e-12); + } + + // ─── GHZ state ──────────────────────────────────────────────────── + + #[test] + fn ghz_state_expectations() { + let mut tab: TestTableau = GeneralizedTableau::new(3, 1e-12); + tab.h(0); + tab.cnot(0, 1); + tab.cnot(1, 2); + // GHZ = (|000⟩ + |111⟩)/√2. For Z^z: eigenvalue is (-1)^{popcount(z)·x} + // on |xxx⟩, so on the two basis states it agrees iff popcount(z) is + // even; the diagonal expectation is then +1, otherwise 0. + assert_close(tab.expectation(&word("III")), 1.0, 1e-12); // popcount 0 → +1 + assert_close(tab.expectation(&word("ZZZ")), 0.0, 1e-12); // popcount 3 → 0 + assert_close(tab.expectation(&word("ZIZ")), 1.0, 1e-12); // popcount 2 → +1 + assert_close(tab.expectation(&word("ZZI")), 1.0, 1e-12); // popcount 2 → +1 + assert_close(tab.expectation(&word("IZI")), 0.0, 1e-12); // popcount 1 → 0 + // XXX flips |000⟩ ↔ |111⟩, both in the GHZ superposition → +1. + assert_close(tab.expectation(&word("XXX")), 1.0, 1e-12); + // Y has off-diagonal action with imaginary phase; YYY contributes 0. + assert_close(tab.expectation(&word("YYY")), 0.0, 1e-12); + } + + // ─── Single-qubit rotation: |ψ⟩ = RY(θ)|0⟩ ──────────────────────── + + #[test] + fn ry_rotation_z_expectation_is_cos_theta() { + // RY(θ)|0⟩ = cos(θ/2)|0⟩ + sin(θ/2)|1⟩. ⟨ψ|Z|ψ⟩ = cos(θ). + for theta in [0.0, 0.3, 1.0, std::f64::consts::PI / 2.0] { + let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12); + tab.ry(0, theta); + assert_close(tab.expectation(&word("Z")), theta.cos(), 1e-12); + assert_close(tab.expectation(&word("X")), theta.sin(), 1e-12); + } + } + + // ─── X / Y on a non-Clifford superposition ─────────────────────── + // + // After H(0); T(0) the state is |ψ⟩ = (|0⟩ + e^{iπ/4}|1⟩)/√2: + // ⟨ψ|X|ψ⟩ = cos(π/4) = √2/2, + // ⟨ψ|Y|ψ⟩ = sin(π/4) = √2/2. + // The T gate populates two branches in the sparse coefficient vector, + // so the overlap is a cross-product between them — case_a, not case_b. + // Y in particular drives `phase_decomp` to an odd value (Y on a |+⟩-style + // frame conjugates to -Y), forcing the `phase == 1 | 3` arms of + // `overlap_case_a`'s `match`. A sign bug there would flip ⟨Y⟩. + + #[test] + fn t_plus_x_expectation_is_cos_pi_over_4() { + let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12); + tab.h(0); + tab.t(0); + assert_close( + tab.expectation(&word("X")), + std::f64::consts::FRAC_1_SQRT_2, + 1e-12, + ); + } + + #[test] + fn t_plus_y_expectation_is_sin_pi_over_4() { + let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12); + tab.h(0); + tab.t(0); + assert_close( + tab.expectation(&word("Y")), + std::f64::consts::FRAC_1_SQRT_2, + 1e-12, + ); + } + + // ─── trace(pattern) ─────────────────────────────────────────────── + + #[test] + fn trace_of_z_or_identity_pattern_on_bell_is_two() { + // For |Φ+⟩ = (|00⟩+|11⟩)/√2: + // Σ_{P ∈ {I,Z}^2} ⟨Φ+|P|Φ+⟩ = ⟨II⟩ + ⟨IZ⟩ + ⟨ZI⟩ + ⟨ZZ⟩ + // = 1 + 0 + 0 + 1 = 2. + // Equivalently 2^n |⟨0…0|Φ+⟩|² = 4 · 1/2 = 2. + let tab = bell(); + let pat = PauliPattern::parse("Z?{2}").expect("parse Z?{2}"); + assert_close(tab.trace(&pat), 2.0, 1e-12); + } + + #[test] + fn trace_of_y_or_identity_pattern_on_bell_is_zero() { + // For |Φ+⟩, Σ_{P ∈ {I,Y}^2} ⟨Φ+|P|Φ+⟩ = ⟨II⟩ + ⟨IY⟩ + ⟨YI⟩ + ⟨YY⟩ + // = 1 + 0 + 0 + (-1) = 0. + // Equivalently 2^n |⟨+i+i|Φ+⟩|² = 4 · 0 = 0 — the projection onto + // the all-|+i⟩ state has zero amplitude. `trace` does the + // enumeration sum here without ever mutating state or calling + // `normalize`, so the zero-probability case doesn't panic. + let tab = bell(); + let pat = PauliPattern::parse("Y?{2}").expect("parse Y?{2}"); + assert_close(tab.trace(&pat), 0.0, 1e-12); + } + + #[test] + fn trace_of_positional_pattern_on_bell_matches_single_pauli() { + // `Z0Z1` matches exactly the word ZZ; trace should equal ⟨ZZ⟩ = 1. + let tab = bell(); + let pat = PauliPattern::parse("Z0Z1").expect("parse Z0Z1"); + assert_close(tab.trace(&pat), 1.0, 1e-12); + } +} diff --git a/crates/ppvm-tableau/src/lib.rs b/crates/ppvm-tableau/src/lib.rs index 57418121d..6f9079953 100644 --- a/crates/ppvm-tableau/src/lib.rs +++ b/crates/ppvm-tableau/src/lib.rs @@ -33,6 +33,8 @@ pub mod data; /// `Display` implementations for tableau types. pub mod display; +/// Pauli-string expectation values and pattern traces. +pub mod expectation; /// Gate implementations (Clifford, T, rotations). pub mod gates; /// Z-basis measurement, including loss-aware variants. diff --git a/crates/ppvm-tableau/src/measure.rs b/crates/ppvm-tableau/src/measure.rs index 37555bd32..e25b9dc50 100644 --- a/crates/ppvm-tableau/src/measure.rs +++ b/crates/ppvm-tableau/src/measure.rs @@ -97,8 +97,7 @@ where // Since conj(c)*c = |c|^2 (always real), the phase factor contribution // to z_overlap.re is: phase 0 → +|c|^2, phase 2 → −|c|^2, // phase 1,3 → 0 (imaginary × real = imaginary, doesn't contribute to .re) - let z_overlap_re = - Self::compute_overlap_case_b(&entries, phase_decomp, destab_anticomm_bits); + let z_overlap_re = Self::overlap_case_b(&entries, phase_decomp, destab_anticomm_bits); let prob_1 = 0.5 - 0.5 * z_overlap_re; let outcome = self.tableau.rng.random::() < prob_1; @@ -132,7 +131,7 @@ where // Compute z_overlap.re directly (the imaginary part is always ~0) let odd_phase_mask = self.odd_phase_destabilizer_mask(); - let z_overlap_re = Self::compute_overlap_case_a( + let z_overlap_re = Self::overlap_case_a( &coeff_map, phase_decomp, destab_anticomm_bits, @@ -290,7 +289,7 @@ where { /// Case_b overlap: self-pairing (branch_index = idx), so overlap = ±|c|^2. /// Only even phases contribute to the real part. - fn compute_overlap_case_b( + pub(crate) fn overlap_case_b( entries: &[(Complex, I)], phase_decomp: u8, destab_anticomm_bits: I, @@ -314,7 +313,7 @@ where /// Case_a overlap: cross-index pairing via HashMap lookup. /// Accumulates only the real part of z_overlap. - fn compute_overlap_case_a( + pub(crate) fn overlap_case_a( coeff_map: &HashMap>, phase_decomp: u8, destab_anticomm_bits: I, From 813fe35b482efeea764352eb12abb39f7289e43b Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 10 Jun 2026 15:06:17 +0200 Subject: [PATCH 71/95] Add a test comparing PauliSum.trace and average over Tableau expect --- crates/ppvm-tableau/src/expectation.rs | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/crates/ppvm-tableau/src/expectation.rs b/crates/ppvm-tableau/src/expectation.rs index 339a809ea..9ef5e4df0 100644 --- a/crates/ppvm-tableau/src/expectation.rs +++ b/crates/ppvm-tableau/src/expectation.rs @@ -264,4 +264,53 @@ mod tests { let pat = PauliPattern::parse("Z0Z1").expect("parse Z0Z1"); assert_close(tab.trace(&pat), 1.0, 1e-12); } + + // ─── Cross-backend: forward tableau shots vs backward PauliSum ──────── + // + // Run a noisy Clifford circuit many times on a tableau and average the + // per-shot ⟨ψ|ZZ|ψ⟩. Independently, Heisenberg-propagate ZZ backward + // through the same circuit via `PauliSum` and read off ⟨0…0|U†ZZ U|0…0⟩ + // as the sum of coefficients over Z/I-only Paulis. The Monte-Carlo + // average and the deterministic value must agree within sampling error. + // + // `PauliSum::g(i)` performs O → g† O g, so the backward sweep applies + // gates in reverse time order. Depolarize is self-dual under Heisenberg. + #[test] + fn forward_shots_match_backward_pauli_sum_under_depolarizing_noise() { + use ppvm_runtime::config::indexmap::ByteFxHashF64; + + let p = 0.05_f64; + let n_shots: u64 = 4000; + let n_qubits = 2; + + let mut sum = 0.0_f64; + for shot in 0..n_shots { + let mut tab: TestTableau = GeneralizedTableau::new_with_seed(n_qubits, 1e-12, shot); + tab.h(0); + tab.depolarize(0, p); + tab.cnot(0, 1); + tab.depolarize(0, p); + tab.depolarize(1, p); + sum += tab.expectation(&word("ZZ")); + } + let avg = sum / (n_shots as f64); + + let mut ps: PauliSum> = PauliSum::builder().n_qubits(n_qubits).build(); + ps += ("ZZ", 1.0); + ps.depolarize(1, p); + ps.depolarize(0, p); + ps.cnot(0, 1); + ps.depolarize(0, p); + ps.h(0); + let z_or_i = PauliPattern::parse("Z?{2}").expect("parse Z?{2}"); + let exact = ps.trace(&z_or_i); + + // Per-shot |⟨ZZ⟩| ≤ 1 ⇒ σ_mean ≤ 1/√N; 5σ keeps this robust to RNG draws. + let tol = 5.0 / (n_shots as f64).sqrt(); + assert!( + (avg - exact).abs() < tol, + "tableau avg {avg} vs PauliSum exact {exact}, |Δ|={} (tol {tol})", + (avg - exact).abs() + ); + } } From ca329882db946b87915c983abb9d2a00c211d716 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 10 Jun 2026 16:10:46 +0200 Subject: [PATCH 72/95] Wire in trace in the tableau backend --- crates/ppvm-vihaco/src/component.rs | 18 +++-- crates/ppvm-vihaco/src/composite.rs | 22 +++--- .../tests/paulisum_ghz_xxx_trace.sst | 27 ++++++++ .../ppvm-vihaco/tests/paulisum_ry_z_trace.sst | 17 +++++ crates/ppvm-vihaco/tests/sst_fixtures.rs | 69 +++++++++++++++++++ .../ppvm-vihaco/tests/tableau_bell_trace.sst | 18 +++++ .../tests/tableau_ghz_xxx_trace.sst | 20 ++++++ .../ppvm-vihaco/tests/tableau_ry_z_trace.sst | 13 ++++ 8 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst create mode 100644 crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst create mode 100644 crates/ppvm-vihaco/tests/tableau_bell_trace.sst create mode 100644 crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst create mode 100644 crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 7471aa45e..6544aa9dc 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -239,13 +239,17 @@ where // threshold, so there's nothing extra to do here. (Truncate, None) => {} - // Trace is not yet implemented for the Tableau backend; Phase 5 - // (plan Task 15) adds the `⟨ψ|P|ψ⟩` primitive upstream in - // `ppvm-tableau` and wires it through here. - (Trace, _) => { - return Err(eyre!( - "Trace is not yet implemented on the Tableau backend (Phase 5)" - )); + // Trace: parse the resolved pattern and compute Σ_{P matches} ⟨ψ|P|ψ⟩ + // on the tableau state. Asymmetric with the PauliSum semantics by + // design (Decision 9): on the tableau this is a sum of expectations, + // not a coefficient filter. + (Trace, PauliPatternStr(s)) => { + let pat = PauliPattern::parse(s) + .map_err(|e| eyre!("invalid Pauli pattern `{s}`: {e:?}"))?; + let value = self.tab.trace(&pat); + return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { + value, + }))); } // Fallback diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 4a843b2dc..0b07d798e 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -1263,9 +1263,11 @@ mod tests { } #[test] - fn tableau_trace_returns_phase_5_error() { - // Task 9: `gate trace` on the Tableau backend errors cleanly until - // Phase 5 (Task 15) lands the upstream `⟨ψ|P|ψ⟩` primitive. + fn tableau_trace_emits_expectation_on_zero_state() { + // Task 16: `gate 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(); module.extra.n_qubits = 1; module.strings.push("Z0".to_string()); @@ -1281,14 +1283,14 @@ mod tests { let mut machine = PPVM::default(); machine.load(&module).unwrap(); machine.init().unwrap(); - // First step: const.string — succeeds. - machine.step_once().unwrap(); - // Second step: gate trace — errors with the Phase-5 message. - let err = machine.step_once().unwrap_err(); + machine.step_once().unwrap(); // const.string + machine.step_once().unwrap(); // gate trace + let trace = machine.trace_record(); + assert_eq!(trace.len(), 1); assert!( - err.to_string() - .contains("not yet implemented on the Tableau backend"), - "expected Phase 5 placeholder error, got: {err}" + (trace[0] - 1.0).abs() < 1e-12, + "expected ⟨0|Z|0⟩ = 1.0, got {}", + trace[0] ); } } diff --git a/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst new file mode 100644 index 000000000..0ea3ae720 --- /dev/null +++ b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst @@ -0,0 +1,27 @@ +device circuit.n_qubits 3; +device circuit.backend paulisum; +device circuit.observable XXX; + +fn @main() { + // Heisenberg backward through GHZ prep: forward is H(0); CNOT(0,1); CNOT(1,2), + // so we apply CNOT(1,2); CNOT(0,1); H(0) here. + // + // XXX --CNOT(1,2)--> XXI (X_2 on target absorbs into X_1 on control) + // --CNOT(0,1)--> XII + // --H(0)------> ZII + // Trace against Z/I-only Paulis picks up the coefficient of ZII = 1.0. + const.u64 1 + const.u64 2 + gate cnot + + const.u64 0 + const.u64 1 + gate cnot + + const.u64 0 + gate h + + const.str "Z?*" + gate trace + ret +} diff --git a/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst new file mode 100644 index 000000000..dc3a6913d --- /dev/null +++ b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst @@ -0,0 +1,17 @@ +device circuit.n_qubits 1; +device circuit.backend paulisum; +device circuit.observable Z; + +fn @main() { + // PauliSum already runs in the Heisenberg picture: `gate ry` performs + // RY(θ)† Z RY(θ) = cos(θ)·Z + sin(θ)·X. A single gate means no + // reversal is needed beyond that. Trace against Z/I-only Paulis picks + // up the cos(θ) coefficient on Z; the sin(θ)·X term contributes 0. + const.u64 0 + const.f64 0.7 + gate ry + + const.str "Z?*" + gate trace + ret +} diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 89c033bfc..13a06fed6 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -294,6 +294,75 @@ fn paulisum_measure_returns_unsupported_error() { ); } +// ─── Task 16: Tableau-side Trace, cross-backend agreement ──────────────── + +#[test] +fn tableau_bell_zz_trace_through_sst() { + // Bell-state ⟨ZZ⟩ via the Tableau backend's `Trace` instruction: + // forward H(0); CNOT(0, 1) leaves |ψ⟩ = |Φ+⟩, and `Z0Z1` matches exactly + // the Pauli word ZZ, so `tab.trace(&pat) = ⟨Φ+|ZZ|Φ+⟩ = 1`. + let machine = ppvm_vihaco::run_file("tests/tableau_bell_trace.sst") + .unwrap_or_else(|e| panic!("run tableau_bell_trace.sst: {e:?}")); + let trace = machine.trace_record(); + assert_eq!(trace.len(), 1, "expected one trace emission"); + assert!( + (trace[0] - 1.0).abs() < 1e-12, + "expected ⟨ZZ⟩ = 1.0, got {}", + trace[0] + ); +} + +fn assert_cross_backend_agreement(tableau_sst: &str, paulisum_sst: &str) { + let tab = + ppvm_vihaco::run_file(tableau_sst).unwrap_or_else(|e| panic!("run {tableau_sst}: {e:?}")); + let ps = + ppvm_vihaco::run_file(paulisum_sst).unwrap_or_else(|e| panic!("run {paulisum_sst}: {e:?}")); + let tab_v = tab.trace_record(); + let ps_v = ps.trace_record(); + assert_eq!(tab_v.len(), 1, "{tableau_sst}: expected one trace emission"); + assert_eq!(ps_v.len(), 1, "{paulisum_sst}: expected one trace emission"); + assert!( + (tab_v[0] - ps_v[0]).abs() < 1e-12, + "tableau {} vs PauliSum {} (|Δ|={}) for {tableau_sst} ↔ {paulisum_sst}", + tab_v[0], + ps_v[0], + (tab_v[0] - ps_v[0]).abs() + ); +} + +#[test] +fn tableau_and_paulisum_agree_on_bell_zz_trace() { + // ⟨Φ+|ZZ|Φ+⟩ = 1. Tableau forward-evolves |0…0⟩ and matches `Z0Z1`; + // PauliSum Heisenberg-propagates ZZ backward through the reversed + // circuit, then sums Z/I-only coefficients. + assert_cross_backend_agreement( + "tests/tableau_bell_trace.sst", + "tests/paulisum_bell_trace.sst", + ); +} + +#[test] +fn tableau_and_paulisum_agree_on_ghz_xxx_trace() { + // ⟨GHZ|XXX|GHZ⟩ = 1. Exercises a 3-qubit Clifford chain (H + two + // CNOTs) and the non-trivial Heisenberg evolution XXX → ZII through + // the reversed circuit on the PauliSum side. + assert_cross_backend_agreement( + "tests/tableau_ghz_xxx_trace.sst", + "tests/paulisum_ghz_xxx_trace.sst", + ); +} + +#[test] +fn tableau_and_paulisum_agree_on_ry_z_trace() { + // ⟨RY(θ)|0⟩|Z|RY(θ)|0⟩⟩ = cos(θ). Exercises a non-Clifford rotation: + // the tableau opens a branch via `tab.ry`, while the PauliSum applies + // the Heisenberg dual RY(θ)†·Z·RY(θ) = cos(θ)·Z + sin(θ)·X in one step. + assert_cross_backend_agreement( + "tests/tableau_ry_z_trace.sst", + "tests/paulisum_ry_z_trace.sst", + ); +} + // ─── Auto-detect via load_file: route by content, not extension ─────────── #[test] diff --git a/crates/ppvm-vihaco/tests/tableau_bell_trace.sst b/crates/ppvm-vihaco/tests/tableau_bell_trace.sst new file mode 100644 index 000000000..02daa0109 --- /dev/null +++ b/crates/ppvm-vihaco/tests/tableau_bell_trace.sst @@ -0,0 +1,18 @@ +device circuit.n_qubits 2; + +fn @main() { + // Forward Bell prep: H(0); CNOT(0, 1) → |Φ+⟩. + const.u64 0 + gate h + + const.u64 0 + const.u64 1 + gate 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" + gate trace + ret +} diff --git a/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst new file mode 100644 index 000000000..7c1669db8 --- /dev/null +++ b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst @@ -0,0 +1,20 @@ +device circuit.n_qubits 3; + +fn @main() { + // Forward GHZ prep: H(0); CNOT(0, 1); CNOT(1, 2) → (|000⟩+|111⟩)/√2. + const.u64 0 + gate h + + const.u64 0 + const.u64 1 + gate cnot + + const.u64 1 + const.u64 2 + gate cnot + + // `X{3}` matches exactly the Pauli word XXX. ⟨GHZ|XXX|GHZ⟩ = 1. + const.str "X{3}" + gate trace + ret +} diff --git a/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst new file mode 100644 index 000000000..cfaee6681 --- /dev/null +++ b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst @@ -0,0 +1,13 @@ +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 + gate ry + + const.str "Z{1}" + gate trace + ret +} From 9a9ee8a5623aa939db817df99d4f08db53c81eb2 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 10 Jun 2026 17:07:27 +0200 Subject: [PATCH 73/95] Rename gate to circuit for instructions --- crates/ppvm-cli/README.md | 4 +- crates/ppvm-cli/examples/ghz.sst | 12 ++-- crates/ppvm-cli/examples/heisenberg_zz.sst | 4 +- crates/ppvm-cli/src/commands.rs | 8 +-- crates/ppvm-vihaco/src/bytecode.rs | 14 ++--- crates/ppvm-vihaco/src/component.rs | 6 +- crates/ppvm-vihaco/src/composite.rs | 56 +++++++++---------- crates/ppvm-vihaco/src/lib.rs | 2 +- crates/ppvm-vihaco/src/shots.rs | 4 +- crates/ppvm-vihaco/src/syntax.rs | 29 +++++----- crates/ppvm-vihaco/tests/bell.sst | 8 +-- .../ppvm-vihaco/tests/branch_on_outcome.sst | 8 +-- .../ppvm-vihaco/tests/branch_on_outcome_x.sst | 8 +-- crates/ppvm-vihaco/tests/function_call.sst | 4 +- .../tests/function_call_branch_both.sst | 12 ++-- .../ppvm-vihaco/tests/function_call_ret.sst | 8 +-- crates/ppvm-vihaco/tests/hello_circuit.sst | 6 +- .../ppvm-vihaco/tests/paulisum_bell_trace.sst | 6 +- .../tests/paulisum_ghz_xxx_trace.sst | 8 +-- .../tests/paulisum_measure_error.sst | 2 +- .../tests/paulisum_multi_term_trace.sst | 2 +- .../ppvm-vihaco/tests/paulisum_ry_z_trace.sst | 6 +- .../tests/paulisum_trotter_truncate.sst | 14 ++--- crates/ppvm-vihaco/tests/rotxy.sst | 6 +- crates/ppvm-vihaco/tests/sst_fixtures.rs | 8 +-- .../ppvm-vihaco/tests/tableau_bell_trace.sst | 6 +- .../tests/tableau_ghz_xxx_trace.sst | 8 +-- .../ppvm-vihaco/tests/tableau_ry_z_trace.sst | 4 +- 28 files changed, 132 insertions(+), 131 deletions(-) diff --git a/crates/ppvm-cli/README.md b/crates/ppvm-cli/README.md index d4b56d391..3ddaac63e 100644 --- a/crates/ppvm-cli/README.md +++ b/crates/ppvm-cli/README.md @@ -101,10 +101,10 @@ wherever you want execution to stop: ``` fn @main() { const.u64 0 - gate h + circuit h breakpoint // execution pauses here const.u64 0 - gate measure + circuit measure ret } ``` diff --git a/crates/ppvm-cli/examples/ghz.sst b/crates/ppvm-cli/examples/ghz.sst index 24690d55a..9eb42fb26 100644 --- a/crates/ppvm-cli/examples/ghz.sst +++ b/crates/ppvm-cli/examples/ghz.sst @@ -4,24 +4,24 @@ device circuit.n_qubits 3; // The three outcomes are perfectly correlated, so each shot reads 0 0 0 or 1 1 1. fn @main() { const.u64 0 - gate h + circuit h const.u64 0 const.u64 1 - gate cnot + circuit cnot const.u64 1 const.u64 2 - gate cnot + circuit cnot const.u64 0 - gate measure + circuit measure const.u64 1 - gate measure + circuit measure const.u64 2 - gate measure + circuit measure ret } diff --git a/crates/ppvm-cli/examples/heisenberg_zz.sst b/crates/ppvm-cli/examples/heisenberg_zz.sst index eebe038c3..2465e64f4 100644 --- a/crates/ppvm-cli/examples/heisenberg_zz.sst +++ b/crates/ppvm-cli/examples/heisenberg_zz.sst @@ -8,10 +8,10 @@ device circuit.coefficient_threshold 1e-10; // (coef 1.0) and XX (coef 0.5), so the trace is their coefficient sum. // // No gates here, so the trace returns the seeded observable's coefficient -// sum directly: 1.0 + 0.5 = 1.5. Add `gate cnot; gate h; gate truncate` +// 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]?*" - gate trace + circuit trace ret } diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index 3dc0b35f0..b6d3db799 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -293,7 +293,7 @@ mod tests { /// Minimal program that compiles and measures q0 in |0> (deterministic). const PROGRAM: &str = - "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n gate measure\n ret }\n"; + "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n circuit measure\n ret }\n"; fn row(outcomes: &[MeasurementOutcome]) -> MeasurementResult { outcomes.iter().copied().collect() @@ -411,7 +411,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 gate trace\n ret }\n"; + fn @main() { const.str \"Z?*\"\n circuit trace\n ret }\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); @@ -514,7 +514,7 @@ 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 gate measure\n ret }\n"; + const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 1;\nfn @main() { breakpoint\n const.u64 0\n circuit measure\n ret }\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 +528,7 @@ mod tests { #[test] fn debug_break_at_start_steps_through_to_finish() { - // PROGRAM is const.u64 0 / gate measure / ret = 3 steps. + // PROGRAM is const.u64 0 / 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"), diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs index 5cc42d8c4..ce77f788e 100644 --- a/crates/ppvm-vihaco/src/bytecode.rs +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -401,10 +401,10 @@ mod tests { let src = "device circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ - gate h\n\ + circuit h\n\ const.u64 0\n\ const.u64 1\n\ - gate cnot\n\ + circuit cnot\n\ ret\n\ }\n"; @@ -419,10 +419,10 @@ mod tests { fn loaded_bytecode_executes_like_text() { let src = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - const.u64 0\n gate h\n\ - const.u64 0\n const.u64 1\n gate cnot\n\ - const.u64 0\n gate measure\n\ - const.u64 1\n gate measure\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"; let bytes = compile_to_bytes(src).unwrap(); @@ -436,7 +436,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 gate measure\n ret }\n"; + fn @main() { const.u64 0\n circuit measure\n ret }\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(); diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 6544aa9dc..5e194b9ec 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -255,7 +255,7 @@ where // Fallback (inst, msg) => { return Err(eyre!( - "Invalid gate arguments {:?} for gate {:?}", + "Invalid circuit instruction arguments {:?} for instruction {:?}", msg, inst )); @@ -431,7 +431,7 @@ where // Fallback (batched messages, mismatched shapes, etc.) (inst, msg) => { return Err(eyre!( - "Invalid gate arguments {:?} for gate {:?} on the PauliSum backend", + "Invalid circuit instruction arguments {:?} for instruction {:?} on the PauliSum backend", msg, inst )); @@ -610,7 +610,7 @@ where // Fallback (batched messages, mismatched shapes, etc.) (inst, msg) => { return Err(eyre!( - "Invalid gate arguments {:?} for gate {:?} on the LossyPauliSum backend", + "Invalid circuit instruction arguments {:?} for instruction {:?} on the LossyPauliSum backend", msg, inst )); diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 0b07d798e..93d022499 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -662,7 +662,7 @@ mod tests { /* const.u64 0 - gate h + circuit h */ let zero = PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))); let one = PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(1))); @@ -673,7 +673,7 @@ mod tests { /* const.u64 0 - gate t + circuit t */ module.code.push(zero.clone()); @@ -684,7 +684,7 @@ mod tests { /* const.u64 0 const.u64 1 - gate cnot + circuit cnot */ module.code.push(zero.clone()); module.code.push(one.clone()); @@ -752,7 +752,7 @@ mod tests { // 5-qubit GHZ: H on q0, then CNOT(q_i, q_{i+1}) for i = 0..4. /* const.u64 0 - gate h + circuit h */ module .code @@ -767,7 +767,7 @@ mod tests { /* const.u64 i const.u64 i+1 - gate cnot + circuit cnot */ module .code @@ -788,7 +788,7 @@ mod tests { for q in 0..5u64 { /* const.u64 q - gate measure + circuit measure */ module .code @@ -853,7 +853,7 @@ mod tests { #[test] fn execute_single_instruction_propagates_engine_errors() -> eyre::Result<()> { // The REPL relies on engine errors surfacing as `Err` (so it can print - // them and keep looping) rather than panicking. A gate with no qubit + // them and keep looping) rather than panicking. A circuit with no qubit // operand on the stack is one such propagating error. // // NOTE: an out-of-range qubit index (>= n_qubits) currently *panics* in @@ -866,7 +866,7 @@ mod tests { machine.load(&module)?; machine.init()?; - // `gate h` with nothing on the stack: `pop_qubit` fails. + // `circuit h` with nothing on the stack: `pop_qubit` fails. let missing_operand = [PPVMInstruction::Circuit(CircuitInstruction::H)]; assert!( machine @@ -896,7 +896,7 @@ mod tests { #[test] fn resolve_circuit_pops_operands_in_reverse_of_push_order() -> eyre::Result<()> { // Convention: operands are pushed in argument order (q0, q1, then any - // floats) and popped in reverse. So every two-qubit gate must read q0 as + // 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(); @@ -974,14 +974,14 @@ mod tests { device circuit.coefficient_threshold 1e-8;\n\ fn @main() {\n\ const.u64 0\n\ - gate h\n\ + circuit h\n\ ret\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 / gate h / ret = 3 + // const.u64 0 / circuit h / ret = 3 assert_eq!(machine.loader.module.code.len(), 3); Ok(()) } @@ -991,14 +991,14 @@ mod tests { let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ - gate h\n\ + circuit h\n\ const.u64 0\n\ const.u64 1\n\ - gate cnot\n\ + circuit cnot\n\ const.u64 0\n\ - gate measure\n\ + circuit measure\n\ const.u64 1\n\ - gate measure\n\ + circuit measure\n\ ret\n\ }\n"; let mut machine = PPVM::default(); @@ -1015,14 +1015,14 @@ mod tests { let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ - gate h\n\ + circuit h\n\ const.u64 0\n\ const.u64 1\n\ - gate cnot\n\ + circuit cnot\n\ const.u64 0\n\ - gate measure\n\ + circuit measure\n\ const.u64 1\n\ - gate measure\n\ + circuit measure\n\ ret\n\ }\n"; let mut machine = PPVM::default(); @@ -1054,7 +1054,7 @@ mod tests { fn run_program_reports_parse_errors() { let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - gate not_a_real_gate\n\ + circuit not_a_real_gate\n\ ret\n\ }\n"; let mut machine = PPVM::default(); @@ -1072,15 +1072,15 @@ mod tests { const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ - gate h\n\ + circuit h\n\ const.u64 0\n\ const.u64 1\n\ - gate cnot\n\ + circuit cnot\n\ const.u64 0\n\ - gate measure\n\ + circuit measure\n\ breakpoint\n\ const.u64 1\n\ - gate measure\n\ + circuit measure\n\ ret\n\ }\n"; @@ -1126,7 +1126,7 @@ mod tests { #[test] fn paulisum_truncate_runs_without_error() -> eyre::Result<()> { - // Smoke test: a `gate truncate` reaches the PauliSum executor's + // Smoke test: a `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(); @@ -1242,7 +1242,7 @@ mod tests { #[test] fn tableau_truncate_is_silent_no_op() -> eyre::Result<()> { - // Task 9: `gate truncate` on the default Tableau backend should run + // Task 9: `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(); @@ -1264,7 +1264,7 @@ mod tests { #[test] fn tableau_trace_emits_expectation_on_zero_state() { - // Task 16: `gate trace` on the Tableau backend now computes + // Task 16: `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. @@ -1284,7 +1284,7 @@ mod tests { machine.load(&module).unwrap(); machine.init().unwrap(); machine.step_once().unwrap(); // const.string - machine.step_once().unwrap(); // gate trace + machine.step_once().unwrap(); // circuit trace let trace = machine.trace_record(); assert_eq!(trace.len(), 1); assert!( diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 2c98875cd..ecd8a8cba 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -90,7 +90,7 @@ mod tests { #[test] fn dump_program_writes_loadable_bytecode() { let src = "device circuit.n_qubits 1;\n\ - fn @main() { const.u64 0\n gate measure\n ret }\n"; + fn @main() { const.u64 0\n circuit measure\n ret }\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 7d6ca8e5c..b1935382f 100644 --- a/crates/ppvm-vihaco/src/shots.rs +++ b/crates/ppvm-vihaco/src/shots.rs @@ -128,10 +128,10 @@ mod tests { /// Measures q0 in |0>: every shot is deterministically `0`. const DETERMINISTIC: &str = - "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n gate measure\n ret }\n"; + "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n circuit measure\n ret }\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 gate h\n const.u64 0\n gate measure\n ret }\n"; + 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"; 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 d17a5b374..9c31c7db7 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -181,14 +181,15 @@ impl<'src> Parse<'src> for PPVMInstruction { let cpu = ::parser().map(PPVMInstruction::Cpu); - // Reuse the derived parser for all CircuitInstruction variants; - // just gate it behind the `gate ` keyword. - let circuit = just("gate") + // Reuse the derived parser for all CircuitInstruction variants, + // gated behind the `circuit ` keyword (covers gates, noise channels, + // measure/reset, trace, and truncate — i.e. everything circuit-side). + let circuit = just("circuit") .then(text::whitespace().at_least(1)) .ignore_then(::parser()) .map(PPVMInstruction::Circuit); - // Try `gate ...` first so CPU doesn't see "gate" as an identifier. + // Try `circuit ...` first so CPU doesn't see "circuit" as an identifier. choice((circuit, cpu)) } } @@ -493,7 +494,7 @@ mod tests { #[test] fn ppvm_instruction_parses_gate_h() { let got = ::parser() - .parse("gate h") + .parse("circuit h") .into_result() .unwrap(); assert!(matches!( @@ -505,7 +506,7 @@ mod tests { #[test] fn ppvm_instruction_parses_gate_cnot() { let got = ::parser() - .parse("gate cnot") + .parse("circuit cnot") .into_result() .unwrap(); assert!(matches!( @@ -517,7 +518,7 @@ mod tests { #[test] fn ppvm_instruction_parses_gate_measure() { let got = ::parser() - .parse("gate measure") + .parse("circuit measure") .into_result() .unwrap(); assert!(matches!( @@ -529,7 +530,7 @@ mod tests { #[test] fn ppvm_instruction_parses_gate_rx() { let got = ::parser() - .parse("gate rx") + .parse("circuit rx") .into_result() .unwrap(); assert!(matches!( @@ -539,9 +540,9 @@ mod tests { } #[test] - fn ppvm_instruction_rejects_bare_circuit_token_without_gate_prefix() { - // `h` on its own must not parse as Circuit(H) — only `gate h` does. - // Without `gate `, the CPU parser is tried, which should reject + 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") @@ -629,15 +630,15 @@ mod tests { "device circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ - gate h\n\ + circuit h\n\ const.u64 0\n\ const.u64 1\n\ - gate cnot\n\ + circuit cnot\n\ ret\n\ }\n", ); let m = PPVMResolver::new().resolve_module(parsed).unwrap(); - // const.u64 0 / gate h / const.u64 0 / const.u64 1 / gate cnot / ret + // 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], diff --git a/crates/ppvm-vihaco/tests/bell.sst b/crates/ppvm-vihaco/tests/bell.sst index f20c23280..f2644119d 100644 --- a/crates/ppvm-vihaco/tests/bell.sst +++ b/crates/ppvm-vihaco/tests/bell.sst @@ -2,17 +2,17 @@ device circuit.n_qubits 2; fn @main() { const.u64 0 - gate h + circuit h const.u64 0 const.u64 1 - gate cnot + circuit cnot const.u64 0 - gate measure + circuit measure const.u64 0 - gate measure + circuit measure ret } \ No newline at end of file diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome.sst b/crates/ppvm-vihaco/tests/branch_on_outcome.sst index 024f3af93..911b9b8e9 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome.sst @@ -2,10 +2,10 @@ device circuit.n_qubits 2; fn @main() { const.u64 0 - gate h + circuit h const.u64 0 - gate measure + circuit measure // Stack: [outcome]. No loss gate, so outcome is 0 or 1. Compare to 1 // to derive a bool for cond_br. @@ -16,7 +16,7 @@ fn @main() { @one: const.u64 1 - gate x + circuit x br @measure_q1 @zero: @@ -24,6 +24,6 @@ fn @main() { @measure_q1: const.u64 1 - gate measure + circuit measure ret } diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst index 82134da65..f410cb8fb 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst @@ -3,10 +3,10 @@ device circuit.n_qubits 2; fn @main() { // X on q0 -> |1>, measure -> outcome is deterministically 1. const.u64 0 - gate x + circuit x const.u64 0 - gate measure + circuit measure // Stack: [outcome]. No loss gate, so outcome is 0 or 1. Compare to 1 // to derive a bool for cond_br. @@ -17,7 +17,7 @@ fn @main() { @one: const.u64 1 - gate x + circuit x br @measure_q1 @zero: @@ -25,6 +25,6 @@ fn @main() { @measure_q1: const.u64 1 - gate measure + circuit measure ret } diff --git a/crates/ppvm-vihaco/tests/function_call.sst b/crates/ppvm-vihaco/tests/function_call.sst index 504f86b33..0761fd256 100644 --- a/crates/ppvm-vihaco/tests/function_call.sst +++ b/crates/ppvm-vihaco/tests/function_call.sst @@ -10,10 +10,10 @@ fn @main() { fn @run_circuit() { const.u64 1 - gate h + circuit h const.u64 1 - gate measure + circuit measure 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 921c06fd2..1c10a6372 100644 --- a/crates/ppvm-vihaco/tests/function_call_branch_both.sst +++ b/crates/ppvm-vihaco/tests/function_call_branch_both.sst @@ -13,11 +13,11 @@ device circuit.n_qubits 2; // → P(m1 = 1) = 0.75. fn @main() { const.u64 0 - gate h + circuit h const.u64 0 const.f64 0.5 - gate loss + circuit loss call 0, @measure_q0 @@ -32,7 +32,7 @@ fn @main() { // path. The leftover `outcome` value on the stack is harmless because // we halt at @final without reading it. const.u64 1 - gate x + circuit x br @final @kept: @@ -43,7 +43,7 @@ fn @main() { @outcome_one: const.u64 1 - gate x + circuit x br @final @outcome_zero: @@ -51,13 +51,13 @@ fn @main() { @final: const.u64 1 - gate measure + circuit measure halt } fn @measure_q0() { const.u64 0 - gate measure + circuit measure // Stack: [outcome] ret 1 } diff --git a/crates/ppvm-vihaco/tests/function_call_ret.sst b/crates/ppvm-vihaco/tests/function_call_ret.sst index d0b859cfb..90d5907d0 100644 --- a/crates/ppvm-vihaco/tests/function_call_ret.sst +++ b/crates/ppvm-vihaco/tests/function_call_ret.sst @@ -7,7 +7,7 @@ device circuit.n_qubits 2; fn @main() { // Put q0 into |+>. const.u64 0 - gate h + circuit h // Measure q1 via a helper that returns the outcome on top of the stack. call 0, @measure_q1 @@ -20,7 +20,7 @@ fn @main() { @one: // outcome was 1: apply X to q0 as a correction. const.u64 0 - gate x + circuit x br @done @zero: @@ -32,10 +32,10 @@ fn @main() { fn @measure_q1() -> u32 { const.u64 1 - gate h + circuit h const.u64 1 - gate measure + circuit measure // Stack: [outcome] ret 1 diff --git a/crates/ppvm-vihaco/tests/hello_circuit.sst b/crates/ppvm-vihaco/tests/hello_circuit.sst index bfb796c5c..6144ce02b 100644 --- a/crates/ppvm-vihaco/tests/hello_circuit.sst +++ b/crates/ppvm-vihaco/tests/hello_circuit.sst @@ -2,15 +2,15 @@ device circuit.n_qubits 2; fn @main() { const.u64 0 - gate h + circuit h const.u64 0 const.u64 1 - gate cnot + circuit cnot const.u64 0 const.f64 0.1 - gate rx + circuit rx ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst index 9d6ed979b..74dacb781 100644 --- a/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst @@ -6,13 +6,13 @@ fn @main() { // Textbook H(0); CNOT(0,1) — emit reversed for Heisenberg propagation. const.u64 0 const.u64 1 - gate cnot + circuit cnot const.u64 0 - gate h + circuit h // Trace against |00>: match Z-or-identity on every qubit. const.str "Z?*" - gate trace + circuit trace ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst index 0ea3ae720..8e5f31a1b 100644 --- a/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst @@ -12,16 +12,16 @@ fn @main() { // Trace against Z/I-only Paulis picks up the coefficient of ZII = 1.0. const.u64 1 const.u64 2 - gate cnot + circuit cnot const.u64 0 const.u64 1 - gate cnot + circuit cnot const.u64 0 - gate h + circuit h const.str "Z?*" - gate trace + circuit trace ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_measure_error.sst b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst index f7f680ffe..a65de9a2f 100644 --- a/crates/ppvm-vihaco/tests/paulisum_measure_error.sst +++ b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst @@ -4,6 +4,6 @@ device circuit.observable Z; fn @main() { const.u64 0 - gate measure + circuit measure ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst index aae630dd8..48f91bcc2 100644 --- a/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst @@ -6,6 +6,6 @@ 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]?*" - gate trace + circuit trace ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst index dc3a6913d..1f8a66212 100644 --- a/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst @@ -3,15 +3,15 @@ device circuit.backend paulisum; device circuit.observable Z; fn @main() { - // PauliSum already runs in the Heisenberg picture: `gate ry` performs + // PauliSum already runs in the Heisenberg picture: `circuit ry` performs // RY(θ)† Z RY(θ) = cos(θ)·Z + sin(θ)·X. A single gate means no // reversal is needed beyond that. Trace against Z/I-only Paulis picks // up the cos(θ) coefficient on Z; the sin(θ)·X term contributes 0. const.u64 0 const.f64 0.7 - gate ry + circuit ry const.str "Z?*" - gate trace + circuit trace ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst index 3725baf46..ce50fb399 100644 --- a/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst +++ b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst @@ -10,28 +10,28 @@ fn @main() { const.u64 0 const.u64 1 const.f64 0.1 - gate rxx + circuit rxx const.u64 0 const.u64 1 const.f64 0.05 - gate rzz + circuit rzz - gate truncate + circuit truncate const.u64 0 const.u64 1 const.f64 0.1 - gate rxx + circuit rxx const.u64 0 const.u64 1 const.f64 0.05 - gate rzz + circuit rzz - gate truncate + circuit truncate const.str "Z?*" - gate trace + circuit trace ret } diff --git a/crates/ppvm-vihaco/tests/rotxy.sst b/crates/ppvm-vihaco/tests/rotxy.sst index b96985fdb..39bf045be 100644 --- a/crates/ppvm-vihaco/tests/rotxy.sst +++ b/crates/ppvm-vihaco/tests/rotxy.sst @@ -2,14 +2,14 @@ device circuit.n_qubits 1; fn @main() { // R(axis_angle = π/2, θ = π) == RY(π), so |0> is sent to |1>. - // Stack order for `gate r`: qubit, then axis_angle, then theta. + // Stack order for `circuit r`: qubit, then axis_angle, then theta. const.u64 0 const.f64 1.5707963267948966 const.f64 3.141592653589793 - gate r + circuit r const.u64 0 - gate measure + circuit measure ret } diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 13a06fed6..f7eb016c0 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 `gate r` path end to + // sending |0> → |1>, then measures it. Exercises the `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:?}")); @@ -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: `gate cnot; gate h`. + // emitted reversed for Heisenberg propagation: `circuit cnot; 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 - // `gate truncate`. The .sst-driven path should agree bit-for-bit with a + // `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. @@ -248,7 +248,7 @@ fn paulisum_trotter_matches_pure_rust_reference() { let through_sst = machine.trace_record(); assert_eq!(through_sst.len(), 1, "expected one trace emission"); - // Pure Rust reference: same N=8 / strategy / gate order as the PauliSum + // Pure Rust reference: same N=8 / strategy / circuit order as the PauliSum // Bits64 bucket in `ppvm_vihaco::component`. use ppvm_runtime::config::indexmap::ByteFxHashF64; use ppvm_runtime::prelude::*; diff --git a/crates/ppvm-vihaco/tests/tableau_bell_trace.sst b/crates/ppvm-vihaco/tests/tableau_bell_trace.sst index 02daa0109..a5e0a94c4 100644 --- a/crates/ppvm-vihaco/tests/tableau_bell_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_bell_trace.sst @@ -3,16 +3,16 @@ device circuit.n_qubits 2; fn @main() { // Forward Bell prep: H(0); CNOT(0, 1) → |Φ+⟩. const.u64 0 - gate h + circuit h const.u64 0 const.u64 1 - gate cnot + 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" - gate trace + circuit trace ret } diff --git a/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst index 7c1669db8..5a7c58989 100644 --- a/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst @@ -3,18 +3,18 @@ device circuit.n_qubits 3; fn @main() { // Forward GHZ prep: H(0); CNOT(0, 1); CNOT(1, 2) → (|000⟩+|111⟩)/√2. const.u64 0 - gate h + circuit h const.u64 0 const.u64 1 - gate cnot + circuit cnot const.u64 1 const.u64 2 - gate cnot + circuit cnot // `X{3}` matches exactly the Pauli word XXX. ⟨GHZ|XXX|GHZ⟩ = 1. const.str "X{3}" - gate trace + circuit trace ret } diff --git a/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst index cfaee6681..45c78d006 100644 --- a/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst @@ -5,9 +5,9 @@ fn @main() { // Hard-coded θ = 0.7 → cos(0.7) ≈ 0.7648421872844885. const.u64 0 const.f64 0.7 - gate ry + circuit ry const.str "Z{1}" - gate trace + circuit trace ret } From e60ee0b0c238222f60743439ca1424f4b67a265f Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 10 Jun 2026 19:40:43 +0200 Subject: [PATCH 74/95] Use a shared macro for PauliSum backend impls --- crates/ppvm-vihaco/src/component.rs | 379 +++++++++-------------- crates/ppvm-vihaco/tests/sst_fixtures.rs | 40 +++ 2 files changed, 188 insertions(+), 231 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 5e194b9ec..9a56f8e95 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -278,167 +278,197 @@ where } } -/// PauliSum-backed executor (Heisenberg picture). Holds a `PauliSum` and -/// answers the same `CircuitInstruction` vocabulary as `CircuitExecutor`, but -/// without measurement / reset support. +/// Shared dispatch body for `PauliSumExecutor` and `LossyPauliSumExecutor`. +/// Every non-loss `CircuitInstruction` lands here. `LossyPauliSumExecutor` +/// matches `Loss` / `CorrelatedLoss` (single + batched) before invoking this +/// macro and never reaches the loss-rejection arm below. /// -/// Skeleton only — `execute_instruction` is a no-op until Task 5 fills in the -/// gate-dispatch table per the plan's Gate Support Matrix. Not yet wired into -/// the `Circuit` enum (that happens in Task 4). -pub struct PauliSumExecutor> { - pub state: PauliSum, -} - -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] -impl PauliSumExecutor -where - T: Config, - for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, -{ - fn execute( - &mut self, - inst: CircuitInstruction, - msg: CircuitMessage, - ) -> Result> { - self.execute_instruction(&inst, &msg) - } - - fn execute_instruction( - &mut self, - inst: &CircuitInstruction, - msg: &CircuitMessage, - ) -> Result> { +/// `$self` is passed as an `ident` (typically `self`) so the macro's +/// expansion shares hygiene with the surrounding method's `self` parameter. +/// `$inst` / `$msg` are passed the same way; `$backend` is the human-readable +/// backend name baked into error messages. +macro_rules! dispatch_common_paulisum { + ($self:ident, $inst:ident, $msg:ident, $backend:literal) => {{ use CircuitInstruction::*; use CircuitMessage::*; - - match (inst, msg) { + match ($inst, $msg) { // Single-qubit Clifford - (X, &Qubit(addr)) => self.state.x(addr), - (Y, &Qubit(addr)) => self.state.y(addr), - (Z, &Qubit(addr)) => self.state.z(addr), - (H, &Qubit(addr)) => self.state.h(addr), - (S, &Qubit(addr)) => self.state.s(addr), - (SAdj, &Qubit(addr)) => self.state.s_adj(addr), - (SqrtX, &Qubit(addr)) => self.state.sqrt_x(addr), - (SqrtY, &Qubit(addr)) => self.state.sqrt_y(addr), - (SqrtXAdj, &Qubit(addr)) => self.state.sqrt_x_adj(addr), - (SqrtYAdj, &Qubit(addr)) => self.state.sqrt_y_adj(addr), + (X, &Qubit(addr)) => $self.state.x(addr), + (Y, &Qubit(addr)) => $self.state.y(addr), + (Z, &Qubit(addr)) => $self.state.z(addr), + (H, &Qubit(addr)) => $self.state.h(addr), + (S, &Qubit(addr)) => $self.state.s(addr), + (SAdj, &Qubit(addr)) => $self.state.s_adj(addr), + (SqrtX, &Qubit(addr)) => $self.state.sqrt_x(addr), + (SqrtY, &Qubit(addr)) => $self.state.sqrt_y(addr), + (SqrtXAdj, &Qubit(addr)) => $self.state.sqrt_x_adj(addr), + (SqrtYAdj, &Qubit(addr)) => $self.state.sqrt_y_adj(addr), // Controlled gates - (CNOT, &TwoQubit(addr0, addr1)) => self.state.cnot(addr0, addr1), - (CZ, &TwoQubit(addr0, addr1)) => self.state.cz(addr0, addr1), + (CNOT, &TwoQubit(addr0, addr1)) => $self.state.cnot(addr0, addr1), + (CZ, &TwoQubit(addr0, addr1)) => $self.state.cz(addr0, addr1), // Single-qubit rotations - (RX, &QubitAndFloat(addr, angle)) => self.state.rx(addr, angle), - (RY, &QubitAndFloat(addr, angle)) => self.state.ry(addr, angle), - (RZ, &QubitAndFloat(addr, angle)) => self.state.rz(addr, angle), + (RX, &QubitAndFloat(addr, angle)) => $self.state.rx(addr, angle), + (RY, &QubitAndFloat(addr, angle)) => $self.state.ry(addr, angle), + (RZ, &QubitAndFloat(addr, angle)) => $self.state.rz(addr, angle), // Two-qubit rotations - (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.rxx(addr0, addr1, angle), - (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.ryy(addr0, addr1, angle), - (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.rzz(addr0, addr1, angle), + (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => { + $self.state.rxx(addr0, addr1, angle) + } + (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => { + $self.state.ryy(addr0, addr1, angle) + } + (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => { + $self.state.rzz(addr0, addr1, angle) + } // RXY: rotation about an axis in the x/y plane (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => { - self.state.r(addr, axis_angle, theta) + $self.state.r(addr, axis_angle, theta) } // Noise - (Depolarize, &QubitAndFloat(addr, p)) => self.state.depolarize(addr, p), + (Depolarize, &QubitAndFloat(addr, p)) => $self.state.depolarize(addr, p), (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { - self.state.depolarize2(addr0, addr1, p) + $self.state.depolarize2(addr0, addr1, p) + } + (PauliError, QubitAndFloatArr3(addr0, ps)) => { + $self.state.pauli_error(*addr0, *ps) } - (PauliError, QubitAndFloatArr3(addr0, ps)) => self.state.pauli_error(*addr0, *ps), (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { - self.state.two_qubit_pauli_error(*addr0, *addr1, *ps) + $self.state.two_qubit_pauli_error(*addr0, *addr1, *ps) } // Truncate: pruning per the configured strategy. - (Truncate, None) => self.state.truncate(), + (Truncate, None) => $self.state.truncate(), // Batched arms: simple for-loop dispatch (no dedicated batch // methods on PauliSum, unlike GeneralizedTableau). - (X, QubitBatch(addrs)) => batch_for!(self.state, x, addrs), - (Y, QubitBatch(addrs)) => batch_for!(self.state, y, addrs), - (Z, QubitBatch(addrs)) => batch_for!(self.state, z, addrs), - (H, QubitBatch(addrs)) => batch_for!(self.state, h, addrs), - (S, QubitBatch(addrs)) => batch_for!(self.state, s, addrs), - (SAdj, QubitBatch(addrs)) => batch_for!(self.state, s_adj, addrs), - (SqrtX, QubitBatch(addrs)) => batch_for!(self.state, sqrt_x, addrs), - (SqrtY, QubitBatch(addrs)) => batch_for!(self.state, sqrt_y, addrs), - (SqrtXAdj, QubitBatch(addrs)) => batch_for!(self.state, sqrt_x_adj, addrs), - (SqrtYAdj, QubitBatch(addrs)) => batch_for!(self.state, sqrt_y_adj, addrs), - (RX, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, rx, addrs, *angle), - (RY, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, ry, addrs, *angle), - (RZ, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, rz, addrs, *angle), + (X, QubitBatch(addrs)) => batch_for!($self.state, x, addrs), + (Y, QubitBatch(addrs)) => batch_for!($self.state, y, addrs), + (Z, QubitBatch(addrs)) => batch_for!($self.state, z, addrs), + (H, QubitBatch(addrs)) => batch_for!($self.state, h, addrs), + (S, QubitBatch(addrs)) => batch_for!($self.state, s, addrs), + (SAdj, QubitBatch(addrs)) => batch_for!($self.state, s_adj, addrs), + (SqrtX, QubitBatch(addrs)) => batch_for!($self.state, sqrt_x, addrs), + (SqrtY, QubitBatch(addrs)) => batch_for!($self.state, sqrt_y, addrs), + (SqrtXAdj, QubitBatch(addrs)) => batch_for!($self.state, sqrt_x_adj, addrs), + (SqrtYAdj, QubitBatch(addrs)) => batch_for!($self.state, sqrt_y_adj, addrs), + (RX, QubitBatchAndFloat(addrs, angle)) => { + batch_for!($self.state, rx, addrs, *angle) + } + (RY, QubitBatchAndFloat(addrs, angle)) => { + batch_for!($self.state, ry, addrs, *angle) + } + (RZ, QubitBatchAndFloat(addrs, angle)) => { + batch_for!($self.state, rz, addrs, *angle) + } (Depolarize, QubitBatchAndFloat(addrs, p)) => { - batch_for!(self.state, depolarize, addrs, *p) + batch_for!($self.state, depolarize, addrs, *p) } (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { - batch_for!(self.state, pauli_error, addrs, *ps) + batch_for!($self.state, pauli_error, addrs, *ps) } - (CNOT, TwoQubitBatch(pairs)) => batch_pairs_for!(self.state, cnot, pairs), - (CZ, TwoQubitBatch(pairs)) => batch_pairs_for!(self.state, cz, pairs), + (CNOT, TwoQubitBatch(pairs)) => batch_pairs_for!($self.state, cnot, pairs), + (CZ, TwoQubitBatch(pairs)) => batch_pairs_for!($self.state, cz, pairs), (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { - batch_pairs_for!(self.state, rxx, pairs, *angle) + batch_pairs_for!($self.state, rxx, pairs, *angle) } (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { - batch_pairs_for!(self.state, ryy, pairs, *angle) + batch_pairs_for!($self.state, ryy, pairs, *angle) } (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { - batch_pairs_for!(self.state, rzz, pairs, *angle) + batch_pairs_for!($self.state, rzz, pairs, *angle) } (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { - batch_pairs_for!(self.state, depolarize2, pairs, *p) + batch_pairs_for!($self.state, depolarize2, pairs, *p) } (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { - batch_pairs_for!(self.state, two_qubit_pauli_error, pairs, *ps) + batch_pairs_for!($self.state, two_qubit_pauli_error, pairs, *ps) } - // Not supported on PauliSum (Decision 11 + Gate Support Matrix). + // Not supported on either backend (Decision 11 + Gate Support + // Matrix). Loss / CorrelatedLoss handling differs by backend + // and lives in the caller's impl block, not this macro. (Measure | Reset, _) => { - return Err(eyre!("{inst} is not supported on the PauliSum backend")); - } - (Loss | CorrelatedLoss, _) => { - return Err(eyre!( - "{inst} is not supported on the PauliSum backend; use the LossyPauliSum backend instead" - )); + return Err(eyre!("{} is not supported on the {} backend", $inst, $backend)); } - // T / T_adj / U3 are listed as supported on PauliSum in the plan's - // Gate Support Matrix, but ppvm-runtime does not yet implement - // TGate or U3Gate for PauliSum (only for GeneralizedTableau). - // Flag this finding here; lifting the upstream impls is out of - // scope for Task 5. + // T / T_adj / U3 are listed as supported on PauliSum in the + // plan's Gate Support Matrix, but ppvm-runtime does not yet + // implement TGate or U3Gate for PauliSum (only for + // GeneralizedTableau). (T | TAdj | U3, _) => { return Err(eyre!( - "{inst} on PauliSum requires upstream ppvm-runtime support that is not yet implemented" + "{} on {} requires upstream ppvm-runtime support that is not yet implemented", + $inst, + $backend )); } - // Trace: parse the resolved pattern string and compute the trace. - // Per plan Decision 9, parsing happens on every execution; no - // module-load caching. + // Trace: parse the resolved pattern string and compute the + // trace. Per plan Decision 9, parsing happens on every + // execution; no module-load caching. (Trace, PauliPatternStr(s)) => { let pat = PauliPattern::parse(s) - .map_err(|e| eyre!("invalid Pauli pattern `{s}`: {e:?}"))?; - let value = self.state.trace(&pat); + .map_err(|e| eyre!("invalid Pauli pattern `{}`: {:?}", s, e))?; + let value = $self.state.trace(&pat); return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { value, }))); } - // Fallback (batched messages, mismatched shapes, etc.) + // Fallback (mismatched shapes, etc.) (inst, msg) => { return Err(eyre!( - "Invalid circuit instruction arguments {:?} for instruction {:?} on the PauliSum backend", + "Invalid circuit instruction arguments {:?} for instruction {:?} on the {} backend", msg, - inst + inst, + $backend )); } }; - Ok(Effects::None) + }}; +} + +/// PauliSum-backed executor (Heisenberg picture). Holds a `PauliSum` and +/// answers the same `CircuitInstruction` vocabulary as `CircuitExecutor`, +/// but without measurement / reset / loss support. +pub struct PauliSumExecutor> { + pub state: PauliSum, +} + +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +impl PauliSumExecutor +where + T: Config, + for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, +{ + fn execute( + &mut self, + inst: CircuitInstruction, + msg: CircuitMessage, + ) -> Result> { + self.execute_instruction(&inst, &msg) + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + use CircuitInstruction::*; + + if matches!(inst, Loss | CorrelatedLoss) { + return Err(eyre!( + "{inst} is not supported on the PauliSum backend; use the LossyPauliSum backend instead" + )); + } + + dispatch_common_paulisum!(self, inst, msg, "PauliSum") } } @@ -451,13 +481,10 @@ where } } -/// LossyPauliSum-backed executor. Same shape as `PauliSumExecutor`; the -/// distinction lives at the dispatch level (this executor accepts `Loss` / -/// `CorrelatedLoss`) and at the concrete `T` used by the enclosing -/// `Circuit::LossyPauliSum` variant (a Config whose `PauliWordType` is -/// `LossyPauliWord`, picked in Task 4). -/// -/// Skeleton only — Task 5 fills in the dispatch. +/// LossyPauliSum-backed executor. Same dispatch as `PauliSumExecutor` plus +/// `Loss` / `CorrelatedLoss` channels. The concrete `T` used by the +/// enclosing `Circuit::LossyPauliSum` variant is a `Config` whose +/// `PauliWordType` is `LossyPauliWord` (see `LossyPauliSumConfig`). pub struct LossyPauliSumExecutor> { pub state: PauliSum, } @@ -484,140 +511,30 @@ where use CircuitInstruction::*; use CircuitMessage::*; + // Loss / CorrelatedLoss are the only instructions that differ from + // PauliSum; handle them here then delegate everything else to the + // shared dispatch. match (inst, msg) { - // Single-qubit Clifford - (X, &Qubit(addr)) => self.state.x(addr), - (Y, &Qubit(addr)) => self.state.y(addr), - (Z, &Qubit(addr)) => self.state.z(addr), - (H, &Qubit(addr)) => self.state.h(addr), - (S, &Qubit(addr)) => self.state.s(addr), - (SAdj, &Qubit(addr)) => self.state.s_adj(addr), - (SqrtX, &Qubit(addr)) => self.state.sqrt_x(addr), - (SqrtY, &Qubit(addr)) => self.state.sqrt_y(addr), - (SqrtXAdj, &Qubit(addr)) => self.state.sqrt_x_adj(addr), - (SqrtYAdj, &Qubit(addr)) => self.state.sqrt_y_adj(addr), - - // Controlled gates - (CNOT, &TwoQubit(addr0, addr1)) => self.state.cnot(addr0, addr1), - (CZ, &TwoQubit(addr0, addr1)) => self.state.cz(addr0, addr1), - - // Single-qubit rotations - (RX, &QubitAndFloat(addr, angle)) => self.state.rx(addr, angle), - (RY, &QubitAndFloat(addr, angle)) => self.state.ry(addr, angle), - (RZ, &QubitAndFloat(addr, angle)) => self.state.rz(addr, angle), - - // Two-qubit rotations - (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.rxx(addr0, addr1, angle), - (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.ryy(addr0, addr1, angle), - (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => self.state.rzz(addr0, addr1, angle), - - // RXY: rotation about an axis in the x/y plane - (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => { - self.state.r(addr, axis_angle, theta) - } - - // Noise - (Depolarize, &QubitAndFloat(addr, p)) => self.state.depolarize(addr, p), - (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { - self.state.depolarize2(addr0, addr1, p) + (Loss, &QubitAndFloat(addr, p)) => { + self.state.loss_channel(addr, p); + return Ok(Effects::None); } - (PauliError, QubitAndFloatArr3(addr0, ps)) => self.state.pauli_error(*addr0, *ps), - (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { - self.state.two_qubit_pauli_error(*addr0, *addr1, *ps) - } - - // Loss (accepted on LossyPauliSum; rejected on plain PauliSum) - (Loss, &QubitAndFloat(addr, p)) => self.state.loss_channel(addr, p), (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { - self.state.correlated_loss_channel(*addr0, *addr1, *ps) - } - - // Truncate: pruning per the configured strategy. - (Truncate, None) => self.state.truncate(), - - // Batched arms: simple for-loop dispatch (no dedicated batch - // methods on PauliSum). - (X, QubitBatch(addrs)) => batch_for!(self.state, x, addrs), - (Y, QubitBatch(addrs)) => batch_for!(self.state, y, addrs), - (Z, QubitBatch(addrs)) => batch_for!(self.state, z, addrs), - (H, QubitBatch(addrs)) => batch_for!(self.state, h, addrs), - (S, QubitBatch(addrs)) => batch_for!(self.state, s, addrs), - (SAdj, QubitBatch(addrs)) => batch_for!(self.state, s_adj, addrs), - (SqrtX, QubitBatch(addrs)) => batch_for!(self.state, sqrt_x, addrs), - (SqrtY, QubitBatch(addrs)) => batch_for!(self.state, sqrt_y, addrs), - (SqrtXAdj, QubitBatch(addrs)) => batch_for!(self.state, sqrt_x_adj, addrs), - (SqrtYAdj, QubitBatch(addrs)) => batch_for!(self.state, sqrt_y_adj, addrs), - (RX, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, rx, addrs, *angle), - (RY, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, ry, addrs, *angle), - (RZ, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.state, rz, addrs, *angle), - (Depolarize, QubitBatchAndFloat(addrs, p)) => { - batch_for!(self.state, depolarize, addrs, *p) + self.state.correlated_loss_channel(*addr0, *addr1, *ps); + return Ok(Effects::None); } (Loss, QubitBatchAndFloat(addrs, p)) => { - batch_for!(self.state, loss_channel, addrs, *p) - } - (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { - batch_for!(self.state, pauli_error, addrs, *ps) - } - (CNOT, TwoQubitBatch(pairs)) => batch_pairs_for!(self.state, cnot, pairs), - (CZ, TwoQubitBatch(pairs)) => batch_pairs_for!(self.state, cz, pairs), - (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { - batch_pairs_for!(self.state, rxx, pairs, *angle) - } - (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { - batch_pairs_for!(self.state, ryy, pairs, *angle) - } - (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { - batch_pairs_for!(self.state, rzz, pairs, *angle) - } - (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { - batch_pairs_for!(self.state, depolarize2, pairs, *p) + batch_for!(self.state, loss_channel, addrs, *p); + return Ok(Effects::None); } (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { - batch_pairs_for!(self.state, correlated_loss_channel, pairs, *ps) - } - (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { - batch_pairs_for!(self.state, two_qubit_pauli_error, pairs, *ps) - } - - // Not supported on LossyPauliSum (Decision 11 + Gate Support Matrix). - (Measure | Reset, _) => { - return Err(eyre!( - "{inst} is not supported on the LossyPauliSum backend" - )); - } - - // See PauliSumExecutor: T/T_adj/U3 require upstream ppvm-runtime - // impls that don't exist yet. - (T | TAdj | U3, _) => { - return Err(eyre!( - "{inst} on LossyPauliSum requires upstream ppvm-runtime support that is not yet implemented" - )); - } - - // Trace: parse the resolved pattern string and compute the trace. - // Per plan Decision 9, parsing happens on every execution; no - // module-load caching. - (Trace, PauliPatternStr(s)) => { - let pat = PauliPattern::parse(s) - .map_err(|e| eyre!("invalid Pauli pattern `{s}`: {e:?}"))?; - let value = self.state.trace(&pat); - return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { - value, - }))); - } - - // Fallback (batched messages, mismatched shapes, etc.) - (inst, msg) => { - return Err(eyre!( - "Invalid circuit instruction arguments {:?} for instruction {:?} on the LossyPauliSum backend", - msg, - inst - )); + batch_pairs_for!(self.state, correlated_loss_channel, pairs, *ps); + return Ok(Effects::None); } - }; + _ => {} + } - Ok(Effects::None) + dispatch_common_paulisum!(self, inst, msg, "LossyPauliSum") } } diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index f7eb016c0..4fbf4408e 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -278,6 +278,46 @@ fn paulisum_trotter_matches_pure_rust_reference() { ); } +#[test] +fn lossy_paulisum_loss_trace_matches_pure_rust_reference() { + // End-to-end LossyPauliSum: header seeds ZZ, body applies CNOT(0,1) + // then loss(q0, 0.3), then traces. The .sst-driven trace must agree + // bit-for-bit with a pure-Rust reference using the same Config (a + // `LossyPauliSumConfig<8>` matching the Bits64 bucket) and operations. + let machine = ppvm_vihaco::run_file("tests/lossy_paulisum_loss_trace.sst") + .unwrap_or_else(|e| panic!("run lossy_paulisum_loss_trace.sst: {e:?}")); + let through_sst = machine.trace_record(); + assert_eq!(through_sst.len(), 1, "expected one trace emission"); + + use ppvm_runtime::config::indexmap::ByteFxHashF64; + use ppvm_runtime::prelude::*; + use ppvm_runtime::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; + type RefConfig = ByteFxHashF64< + 8, + CombinedStrategy, + LossyPauliWord<[u8; 8]>, + >; + + let mut state: PauliSum = PauliSum::builder() + .n_qubits(2) + .strategy(CombinedStrategy( + CoefficientThreshold(1e-10), + MaxPauliWeight(usize::MAX), + )) + .build(); + state += ("ZZ", 1.0); + state.cnot(0, 1); + state.loss_channel(0, 0.3); + state.truncate(); + let pat = PauliPattern::parse("Z?*").expect("parse pattern"); + let reference = state.trace(&pat); + + assert_eq!( + through_sst[0], reference, + ".sst-driven trace must match pure Rust reference bit-for-bit" + ); +} + #[test] fn paulisum_measure_returns_unsupported_error() { // Per Decision 11, Measure on PauliSum hits the dispatch fallback with a From 0a056e97e2b787bae6394a2db5f11a1115190ad6 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 10 Jun 2026 19:41:07 +0200 Subject: [PATCH 75/95] Add a test for lossy paulisum and fix stale skill --- .../tests/lossy_paulisum_loss_trace.sst | 28 +++++++++++++++++++ skills/ppvm-usage/SKILL.md | 21 +++++++------- 2 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst diff --git a/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst b/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst new file mode 100644 index 000000000..36462b211 --- /dev/null +++ b/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst @@ -0,0 +1,28 @@ +device circuit.n_qubits 2; +device circuit.backend lossy_paulisum; +device circuit.observable ZZ; + +// LossyPauliSum end-to-end with a single-qubit loss channel. The header +// seeds `ZZ`, the body Heisenberg-propagates ZZ backward through CNOT(0,1) +// to Z_1 (= IZ), then applies loss(q0, p=0.3). The pattern `Z?*` matches +// all Z/I-only Paulis on the surviving qubits, so the trace picks up the +// IZ coefficient that survived the loss channel. +// +// The Rust-side test cross-checks this against a pure-Rust LossyPauliSum +// reference with the same Config/strategy, so we don't have to commit to +// the exact loss-channel value — only that .sst and direct API agree. +fn @main() { + const.u64 0 + const.u64 1 + circuit cnot + + const.u64 0 + const.f64 0.3 + circuit loss + + circuit truncate + + const.str "Z?*" + circuit trace + ret +} diff --git a/skills/ppvm-usage/SKILL.md b/skills/ppvm-usage/SKILL.md index 18b0a70b8..6d5902b40 100644 --- a/skills/ppvm-usage/SKILL.md +++ b/skills/ppvm-usage/SKILL.md @@ -355,7 +355,7 @@ What *not* to do: `.sst` is the textual program format that `ppvm-cli` runs. A module has a `device` header block selecting the backend and any initial state, then one -or more `fn @()` bodies. Each `gate ` instruction pops typed +or more `fn @()` bodies. Each `circuit ` instruction pops typed operands from the CPU stack and dispatches to the runtime. ### Backend selection @@ -383,18 +383,18 @@ word is optional. **No internal whitespace is allowed in the header value** ### Gate-order convention -The runtime applies `gate ...` instructions in code order on every backend. +The runtime applies `circuit ...` instructions in code order on every backend. Whoever emits the `.sst` is responsible for emitting gates in the right direction for the chosen picture: **forward** for Tableau (Schrödinger), **reversed** for PauliSum/Lossy (Heisenberg). Textbook `H(0); CNOT(0,1)` on -a PauliSum target compiles to `gate cnot; gate h`, not the other way around. +a PauliSum target compiles to `circuit cnot; circuit h`, not the other way around. -### `gate trace` and `gate truncate` +### `circuit trace` and `circuit truncate` ```sst const.str "Z?*" -gate trace // PauliSum/Lossy: pushes state.trace(&pattern) as f64 -gate truncate // PauliSum/Lossy: state.truncate(); Tableau: silent no-op +circuit trace // PauliSum/Lossy: pushes state.trace(&pattern) as f64 +circuit truncate // PauliSum/Lossy: state.truncate(); Tableau: silent no-op ``` `trace` pops a `Value::String` (Pauli-pattern source — same grammar as @@ -405,9 +405,10 @@ the machine's `trace_record` *and* pushes the value back as `Value::F64`. - **PauliSum / LossyPauliSum:** `state.trace(&pat)` is a filter coefficient sum — sum of `c_P` over terms whose word matches `pat`. Use `"Z?*"` to compute `⟨0…0|state|0…0⟩`. -- **Tableau:** `trace` returns `Σ_{P matches pat} ⟨ψ|P|ψ⟩` (Phase 5, not - yet wired — currently errors with "trace not yet implemented on the - Tableau backend"). +- **Tableau:** `trace` returns `Σ_{P matches pat} ⟨ψ|P|ψ⟩` — the sum of + Pauli expectation values over every word the pattern enumerates. Bounded + patterns only (`Z?{n}`, positional anchors, character classes); star + quantifiers panic because they enumerate an infinite set. These are honest natural primitives for each backend; the same operand will not give the same number across backends. Users shouldn't expect @@ -416,7 +417,7 @@ agreement on a shared input. `truncate` takes no operand and applies the configured strategy (`CoefficientThreshold` + `MaxPauliWeight`) to the current state. On Tableau it's a silent no-op — gate methods already prune via -`coefficient_threshold`. **Without explicit `gate truncate` calls in the +`coefficient_threshold`. **Without explicit `circuit truncate` calls in the .sst, PauliSum runs do not truncate** — the compiler that emits the .sst decides where to place them. From 0e430b0871278b29f7337842d8c60f9000ed4347 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Thu, 11 Jun 2026 10:49:09 +0200 Subject: [PATCH 76/95] Fix README since in-line comments don't work --- crates/ppvm-cli/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/ppvm-cli/README.md b/crates/ppvm-cli/README.md index 3ddaac63e..4e936bc8c 100644 --- a/crates/ppvm-cli/README.md +++ b/crates/ppvm-cli/README.md @@ -102,7 +102,10 @@ wherever you want execution to stop: fn @main() { const.u64 0 circuit h - breakpoint // execution pauses here + + // execution pauses here + breakpoint + const.u64 0 circuit measure ret From 7ee38efd102add5ef0e8c76802f277939b50ac15 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Thu, 11 Jun 2026 11:46:00 +0200 Subject: [PATCH 77/95] Add a few .sst examples --- .../ppvm-cli/examples/bit_flip_correction.sst | 66 +++++++++++++++++++ .../ppvm-cli/examples/loop_feedforward.pseudo | 16 +++++ crates/ppvm-cli/examples/loop_feedforward.sst | 44 +++++++++++++ crates/ppvm-cli/examples/simple_loop.sst | 33 ++++++++++ 4 files changed, 159 insertions(+) create mode 100644 crates/ppvm-cli/examples/bit_flip_correction.sst create mode 100644 crates/ppvm-cli/examples/loop_feedforward.pseudo create mode 100644 crates/ppvm-cli/examples/loop_feedforward.sst create mode 100644 crates/ppvm-cli/examples/simple_loop.sst diff --git a/crates/ppvm-cli/examples/bit_flip_correction.sst b/crates/ppvm-cli/examples/bit_flip_correction.sst new file mode 100644 index 000000000..6ed72b861 --- /dev/null +++ b/crates/ppvm-cli/examples/bit_flip_correction.sst @@ -0,0 +1,66 @@ +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 + + const.u64 0 + const.u64 3 + circuit cnot + const.u64 1 + const.u64 3 + circuit cnot + const.u64 3 + circuit measure + + const.u64 1 + const.u64 4 + circuit cnot + const.u64 2 + const.u64 4 + circuit cnot + const.u64 4 + circuit measure + + const.u32 1 + eq.u32 + cond_br @s12_one, @s12_zero + +@s12_one: + const.u32 1 + eq.u32 + cond_br @correct_q1, @correct_q2 + +@s12_zero: + const.u32 1 + eq.u32 + cond_br @correct_q0, @readout + +@correct_q0: + const.u64 0 + circuit x + br @readout + +@correct_q1: + const.u64 1 + circuit x + br @readout + +@correct_q2: + const.u64 2 + circuit x + +@readout: + const.u64 0 + circuit measure + const.u64 1 + circuit measure + const.u64 2 + circuit measure + ret +} \ No newline at end of file diff --git a/crates/ppvm-cli/examples/loop_feedforward.pseudo b/crates/ppvm-cli/examples/loop_feedforward.pseudo new file mode 100644 index 000000000..eeec2fce3 --- /dev/null +++ b/crates/ppvm-cli/examples/loop_feedforward.pseudo @@ -0,0 +1,16 @@ + def main(): + measurement_record = [] + + for _ in range(3): + circuit.h(q0) + + q0_outcome = circuit.measure(q0) + measurement_record.append(q0_outcome) + + if q0_outcome == 1: + circuit.x(q1) + + q1_outcome = circuit.measure(q1) + measurement_record.append(q1_outcome) + + return measurement_record diff --git a/crates/ppvm-cli/examples/loop_feedforward.sst b/crates/ppvm-cli/examples/loop_feedforward.sst new file mode 100644 index 000000000..ab7676310 --- /dev/null +++ b/crates/ppvm-cli/examples/loop_feedforward.sst @@ -0,0 +1,44 @@ +device circuit.n_qubits 2; + +// Loop + feed-forward demo. +// Repeat a body 3 times (a counted loop); in each round flip a fair coin on +// q0 and, based on the measured outcome, conditionally toggle q1 (the +// feed-forward). q1 is flipped once per coin that came up 1, so the final q1 +// measurement equals the parity (XOR) of the three coin flips. Each shot +// 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 + +@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 + + // Feed-forward: if the outcome was 1, apply X to q1. + const.u32 1 + eq.u32 + cond_br @flip, @next + +@flip: + const.u64 1 + circuit x + br @next + +@next: + // counter += 1, then loop while counter < 3. + const.u64 1 + add.u64 + dup + const.u64 3 + lt.u64 + cond_br @loop, @done + +@done: + // Final readout of q1. + const.u64 1 + circuit measure + ret +} diff --git a/crates/ppvm-cli/examples/simple_loop.sst b/crates/ppvm-cli/examples/simple_loop.sst new file mode 100644 index 000000000..d02833ac2 --- /dev/null +++ b/crates/ppvm-cli/examples/simple_loop.sst @@ -0,0 +1,33 @@ +device circuit.n_qubits 1; + +fn @main() { + const.u64 0 + +@loop: + const.u64 0 + circuit h + const.u64 0 + circuit measure + + // stop here to investigate + breakpoint + + const.u32 1 + eq.u32 + cond_br @flip, @next + +@flip: + const.u64 0 + circuit x + +@next: + const.u64 1 + add.u64 + dup + const.u64 2 + lt.u64 + cond_br @loop, @done + +@done: + ret +} \ No newline at end of file From 5596024b348f7c92c3e6ea224d042978abf29a51 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Fri, 19 Jun 2026 10:48:06 +0200 Subject: [PATCH 78/95] Update to latest vihaco changes --- Cargo.lock | 5 +++++ crates/ppvm-vihaco/Cargo.toml | 8 ++++---- crates/ppvm-vihaco/src/composite.rs | 4 ++-- crates/vihaco-circuit-isa/Cargo.toml | 6 +++--- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8af865c86..5f59b087c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1840,6 +1840,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vihaco" version = "0.1.0" +source = "git+https://github.com/QuEraComputing/vihaco#171aab75054f08954fc7ccf3374c9525df1fa7ae" dependencies = [ "byteorder", "chumsky 0.10.1", @@ -1869,6 +1870,7 @@ dependencies = [ [[package]] name = "vihaco-cpu" version = "0.1.0" +source = "git+https://github.com/QuEraComputing/vihaco#171aab75054f08954fc7ccf3374c9525df1fa7ae" dependencies = [ "chumsky 0.10.1", "codespan", @@ -1883,6 +1885,7 @@ dependencies = [ [[package]] name = "vihaco-macros" version = "0.1.0" +source = "git+https://github.com/QuEraComputing/vihaco#171aab75054f08954fc7ccf3374c9525df1fa7ae" dependencies = [ "convert_case", "proc-macro2", @@ -1893,6 +1896,7 @@ dependencies = [ [[package]] name = "vihaco-parser" version = "0.1.0" +source = "git+https://github.com/QuEraComputing/vihaco#171aab75054f08954fc7ccf3374c9525df1fa7ae" dependencies = [ "proc-macro2", "quote", @@ -1903,6 +1907,7 @@ dependencies = [ [[package]] name = "vihaco-parser-core" version = "0.1.0" +source = "git+https://github.com/QuEraComputing/vihaco#171aab75054f08954fc7ccf3374c9525df1fa7ae" dependencies = [ "chumsky 0.10.1", ] diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index dd97f432e..22193ea19 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -18,8 +18,8 @@ rayon = { version = "1.10", optional = true } ppvm-runtime = { version = "0.1.0", path = "../ppvm-runtime" } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon"] } smallvec = "1.15.1" -vihaco = { version = "0.1.0", path = "../../../vihaco/crates/vihaco" } -vihaco-cpu = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-cpu" } -vihaco-parser = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-parser" } -vihaco-parser-core = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-parser-core" } +vihaco = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } +vihaco-cpu = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } +vihaco-parser = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } +vihaco-parser-core = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } vihaco-circuit-isa = { version = "0.1.0", path = "../vihaco-circuit-isa" } diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 93d022499..dfc963a0b 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -67,10 +67,10 @@ pub struct PPVM { #[program] loader: ProgramLoader, - #[device(0x00, resolve_with = resolve_cpu)] + #[device(0x00)] cpu: CPU, - #[device(0x01, resolve_with = resolve_circuit)] + #[device(0x01)] circuit: Circuit, stdout: StdoutObserver, diff --git a/crates/vihaco-circuit-isa/Cargo.toml b/crates/vihaco-circuit-isa/Cargo.toml index fbaf5b02a..1e720896e 100644 --- a/crates/vihaco-circuit-isa/Cargo.toml +++ b/crates/vihaco-circuit-isa/Cargo.toml @@ -7,6 +7,6 @@ edition = "2024" chumsky = "0.10" eyre = "0.6.12" smallvec = "1.15.1" -vihaco = { version = "0.1.0", path = "../../../vihaco/crates/vihaco" } -vihaco-parser = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-parser" } -vihaco-parser-core = { version = "0.1.0", path = "../../../vihaco/crates/vihaco-parser-core" } +vihaco = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } +vihaco-parser = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } +vihaco-parser-core = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } From a24031a0f1bb20a011e5b0d37933f1025f5be5f9 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 23 Jun 2026 12:33:04 +0200 Subject: [PATCH 79/95] Update vihaco dependencies to public ones --- Cargo.lock | 31 ++++++++++++++++------------ crates/ppvm-vihaco/Cargo.toml | 8 +++---- crates/vihaco-circuit-isa/Cargo.toml | 6 +++--- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f59b087c..d9d559058 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1839,8 +1839,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vihaco" -version = "0.1.0" -source = "git+https://github.com/QuEraComputing/vihaco#171aab75054f08954fc7ccf3374c9525df1fa7ae" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "690c9db1827ed6e392340d3d9c65408213c7bb4c030b5e8991b03459e5c7bb17" dependencies = [ "byteorder", "chumsky 0.10.1", @@ -1850,7 +1851,7 @@ dependencies = [ "eyre", "log", "smallvec", - "vihaco-macros", + "vihaco-derive", "vihaco-parser", "vihaco-parser-core", ] @@ -1869,23 +1870,25 @@ dependencies = [ [[package]] name = "vihaco-cpu" -version = "0.1.0" -source = "git+https://github.com/QuEraComputing/vihaco#171aab75054f08954fc7ccf3374c9525df1fa7ae" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be6cffffe3fca901bcddc389422dd9d2f7041038571a758a03c63e53ce1b6d5e" dependencies = [ "chumsky 0.10.1", "codespan", "eyre", "log", "vihaco", - "vihaco-macros", + "vihaco-derive", "vihaco-parser", "vihaco-parser-core", ] [[package]] -name = "vihaco-macros" -version = "0.1.0" -source = "git+https://github.com/QuEraComputing/vihaco#171aab75054f08954fc7ccf3374c9525df1fa7ae" +name = "vihaco-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f11ac1123518d4bec265847a1a12e406c1b54f4ef2478db87f4e6986e26b918" dependencies = [ "convert_case", "proc-macro2", @@ -1895,8 +1898,9 @@ dependencies = [ [[package]] name = "vihaco-parser" -version = "0.1.0" -source = "git+https://github.com/QuEraComputing/vihaco#171aab75054f08954fc7ccf3374c9525df1fa7ae" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b0c3b54b4caeaaeeda2953fa273155887e89ebdb0ce80b65d23c435b1e70a5" dependencies = [ "proc-macro2", "quote", @@ -1906,8 +1910,9 @@ dependencies = [ [[package]] name = "vihaco-parser-core" -version = "0.1.0" -source = "git+https://github.com/QuEraComputing/vihaco#171aab75054f08954fc7ccf3374c9525df1fa7ae" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aea97e3bc67e4b5a97e21f2a4f64e4a87749038850968021ec903cc595353d20" dependencies = [ "chumsky 0.10.1", ] diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 22193ea19..9da5c73ff 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -18,8 +18,8 @@ rayon = { version = "1.10", optional = true } ppvm-runtime = { version = "0.1.0", path = "../ppvm-runtime" } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon"] } smallvec = "1.15.1" -vihaco = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } -vihaco-cpu = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } -vihaco-parser = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } -vihaco-parser-core = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } +vihaco = "0.1.1" +vihaco-cpu = "0.1.1" +vihaco-parser = "0.1.1" +vihaco-parser-core = "0.1.1" vihaco-circuit-isa = { version = "0.1.0", path = "../vihaco-circuit-isa" } diff --git a/crates/vihaco-circuit-isa/Cargo.toml b/crates/vihaco-circuit-isa/Cargo.toml index 1e720896e..344841021 100644 --- a/crates/vihaco-circuit-isa/Cargo.toml +++ b/crates/vihaco-circuit-isa/Cargo.toml @@ -7,6 +7,6 @@ edition = "2024" chumsky = "0.10" eyre = "0.6.12" smallvec = "1.15.1" -vihaco = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } -vihaco-parser = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } -vihaco-parser-core = { version = "0.1.0", git = "https://github.com/QuEraComputing/vihaco" } +vihaco = "0.1.1" +vihaco-parser = "0.1.1" +vihaco-parser-core = "0.1.1" From c9fa288e4ef039b1c51fcb693aa4a79a8dfb6c7c Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 23 Jun 2026 17:06:28 +0200 Subject: [PATCH 80/95] Update lock file --- ppvm-python/uv.lock | 803 +------------------------------------------- 1 file changed, 1 insertion(+), 802 deletions(-) diff --git a/ppvm-python/uv.lock b/ppvm-python/uv.lock index d99a83d4d..154353604 100644 --- a/ppvm-python/uv.lock +++ b/ppvm-python/uv.lock @@ -286,15 +286,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] -[[package]] -name = "babel" -version = "2.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, -] - [[package]] name = "backports-tarfile" version = "1.2.0" @@ -304,19 +295,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] -[[package]] -name = "backrefs" -version = "7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, - { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, - { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, -] - [[package]] name = "beartype" version = "0.22.9" @@ -326,19 +304,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, ] -[[package]] -name = "beautifulsoup4" -version = "4.14.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, -] - [[package]] name = "beliefmatching" version = "0.2.0" @@ -355,23 +320,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/48/49f5bc2a7ac44226d07381ac553f44966bcee1f4c17d3688d960023ac228/beliefmatching-0.2.0-py3-none-any.whl", hash = "sha256:601e5511e34acd00daad6f421a229aa60f3ff58a1c8800f416b2ffa236c7a22a", size = 12740, upload-time = "2025-06-28T21:19:43.487Z" }, ] -[[package]] -name = "bleach" -version = "6.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, -] - -[package.optional-dependencies] -css = [ - { name = "tinycss2" }, -] - [[package]] name = "bloqade-circuit" version = "0.14.2" @@ -935,15 +883,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] -[[package]] -name = "defusedxml" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -990,15 +929,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, ] -[[package]] -name = "fastjsonschema" -version = "2.21.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, -] - [[package]] name = "fonttools" version = "4.63.0" @@ -1205,18 +1135,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/37/3922951a55a3d0f0340e884929087ce08e333cbb16a86002535c095960fc/gcsfs-2026.4.0-py3-none-any.whl", hash = "sha256:d9e838834d8cce6cb623c6a6a5fad66a4d122dc5c609d4b1c1977b55f759dcc5", size = 72190, upload-time = "2026-04-29T21:04:09.997Z" }, ] -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, -] - [[package]] name = "google-api-core" version = "2.30.3" @@ -1376,27 +1294,6 @@ grpc = [ { name = "grpcio" }, ] -[[package]] -name = "griffe-inherited-docstrings" -version = "1.1.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffelib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/da/fd002dc5f215cd896bfccaebe8b4aa1cdeed8ea1d9d60633685bd61ff933/griffe_inherited_docstrings-1.1.3.tar.gz", hash = "sha256:cd1f937ec9336a790e5425e7f9b92f5a5ab17f292ba86917f1c681c0704cb64e", size = 26738, upload-time = "2026-02-21T09:38:44.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/20/4bc15f242181daad1c104e0a7d33be49e712461ea89e548152be0365b9ea/griffe_inherited_docstrings-1.1.3-py3-none-any.whl", hash = "sha256:aa7f6e624515c50d9325a5cfdf4b2acac547f1889aca89092d5da7278f739695", size = 6710, upload-time = "2026-02-20T11:06:38.75Z" }, -] - -[[package]] -name = "griffelib" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, -] - [[package]] name = "grpc-google-iam-v1" version = "0.14.4" @@ -1509,7 +1406,7 @@ name = "importlib-metadata" version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.14'" }, + { name = "zipp", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [ @@ -1740,33 +1637,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl", hash = "sha256:56cf861bab076b1178eb8c92e1311d273a9b9acea2ccc82c276abf839ebaef3a", size = 36271, upload-time = "2026-03-27T22:50:47.073Z" }, ] -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - [[package]] name = "jupyter-client" version = "8.8.0" @@ -1796,15 +1666,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] -[[package]] -name = "jupyterlab-pygments" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, -] - [[package]] name = "jupyterlab-widgets" version = "3.0.16" @@ -1814,23 +1675,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, ] -[[package]] -name = "jupytext" -version = "1.19.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "mdit-py-plugins" }, - { name = "nbformat" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/2d/15624c3d9440d85a280ff13d2d23afd989802f25470ac59932f4fef6f0c6/jupytext-1.19.3.tar.gz", hash = "sha256:713c3ed4441afe0f31474d28ea2e6b61a268c04c40fd78e5ccfd7f7ac9e9f766", size = 4305350, upload-time = "2026-05-17T09:09:29.294Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/ec/d9be3bd1db141e76b2f525c265f70e66edd30a51a3307d8edf0ef1909c54/jupytext-1.19.3-py3-none-any.whl", hash = "sha256:acf75492f80895ad8e664fd8db1708b617008dd0e71c341a1abc3d0d07310ed0", size = 170579, upload-time = "2026-05-17T09:09:27.478Z" }, -] - [[package]] name = "keyring" version = "25.7.0" @@ -2073,15 +1917,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, ] -[[package]] -name = "markdown" -version = "3.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, -] - [[package]] name = "markdown-it-py" version = "4.2.0" @@ -2265,18 +2100,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] -[[package]] -name = "mdit-py-plugins" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -2286,207 +2109,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, -] - -[[package]] -name = "mike" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "mkdocs" }, - { name = "pyparsing" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "verspec" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/47/fa87e9d56bef16cdfe34b059a437e8c6f7ec6f1b9c378871c3cf95ebea9c/mike-2.2.0.tar.gz", hash = "sha256:1e3858e32c0f125aac14432fc7848434358f9ae0962c5c5cde387ad47f6ad25e", size = 38450, upload-time = "2026-04-14T04:59:03.944Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl", hash = "sha256:e1f4981c1152eec7c2490a3401142292cc47d686194188416db2648fdfe1d040", size = 34026, upload-time = "2026-04-14T04:59:02.602Z" }, -] - -[[package]] -name = "mistune" -version = "3.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/84/620cc3f7e3adf6f5067e10f4dbae71295d8f9e16d5d3f9ef97c40f2f592c/mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28", size = 98003, upload-time = "2026-05-03T14:33:22.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, -] - -[[package]] -name = "mkdocs-autorefs" -version = "1.4.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, -] - -[[package]] -name = "mkdocs-gen-files" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mkdocs" }, - { name = "properdocs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/43/428f312149c161cae557eecd35f3c4a82b867998b1d47fb29fdfe927be26/mkdocs_gen_files-0.6.1.tar.gz", hash = "sha256:57d7ff2229e23d077e46d14a33db6d37c8823f6ce1a503c874c1764a71679763", size = 8746, upload-time = "2026-03-16T23:26:09.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/1b/3075eb67fe66e19db059f0a25744c4e56978a309603a20e1d3353d545b5e/mkdocs_gen_files-0.6.1-py3-none-any.whl", hash = "sha256:b3182bfc6219e35b8d26658cb988368659d5d023aac30c2a819247558fc12189", size = 8282, upload-time = "2026-03-16T23:26:08.292Z" }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, -] - -[[package]] -name = "mkdocs-jupyter" -version = "0.26.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ipykernel" }, - { name = "jupytext" }, - { name = "mkdocs" }, - { name = "mkdocs-material" }, - { name = "nbconvert" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/aa/f8d15409a9a3112486994a80d5a975694c7d145c4f8b5b484aeb383420ef/mkdocs_jupyter-0.26.3.tar.gz", hash = "sha256:e1e8bd48a1b96542e84e3028e3066112bac7b94d95ab69f8b91305c84003ca26", size = 1628353, upload-time = "2026-04-17T18:56:31.517Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl", hash = "sha256:cd6644fb578131157194d750fd4d10fc2fd8f1e84e00036ee62df3b5b4b84c82", size = 1459740, upload-time = "2026-04-17T18:56:30.031Z" }, -] - -[[package]] -name = "mkdocs-literate-nav" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mkdocs" }, - { name = "properdocs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/af/dd3776a7a713f798f79bec7eb9c661d5cfb83ddc17d9a3667595e53e1559/mkdocs_literate_nav-0.6.3.tar.gz", hash = "sha256:edbaca22343f861fe4e34aac47d55a0c9955c640dbf02eea99fe631e914cf9ee", size = 17526, upload-time = "2026-03-16T23:26:50.688Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/2c/bcf1ae903975ad6f169abb05c1eb0f94395478364deb89270cf034081b29/mkdocs_literate_nav-0.6.3-py3-none-any.whl", hash = "sha256:2c421561280fa9184f88cbf399bebbd4cc17ee507e978a31ce11fd6f3aabf233", size = 13355, upload-time = "2026-03-16T23:26:49.562Z" }, -] - -[[package]] -name = "mkdocs-material" -version = "9.7.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "babel" }, - { name = "backrefs" }, - { name = "colorama" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "mkdocs" }, - { name = "mkdocs-material-extensions" }, - { name = "paginate" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, -] - -[[package]] -name = "mkdocstrings" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, - { name = "mkdocs-autorefs" }, - { name = "pymdown-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, -] - -[package.optional-dependencies] -python = [ - { name = "mkdocstrings-python" }, -] - -[[package]] -name = "mkdocstrings-python" -version = "2.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffelib" }, - { name = "mkdocs-autorefs" }, - { name = "mkdocstrings" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, -] - [[package]] name = "more-itertools" version = "11.0.2" @@ -2634,61 +2256,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] -[[package]] -name = "nbclient" -version = "0.10.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "nbformat" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, -] - -[[package]] -name = "nbconvert" -version = "7.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beautifulsoup4" }, - { name = "bleach", extra = ["css"] }, - { name = "defusedxml" }, - { name = "jinja2" }, - { name = "jupyter-core" }, - { name = "jupyterlab-pygments" }, - { name = "markupsafe" }, - { name = "mistune" }, - { name = "nbclient" }, - { name = "nbformat" }, - { name = "packaging" }, - { name = "pandocfilters" }, - { name = "pygments" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, -] - -[[package]] -name = "nbformat" -version = "5.10.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fastjsonschema" }, - { name = "jsonschema" }, - { name = "jupyter-core" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, -] - [[package]] name = "nest-asyncio" version = "1.6.0" @@ -2813,15 +2380,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] -[[package]] -name = "paginate" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, -] - [[package]] name = "pandas" version = "2.3.3" @@ -2883,15 +2441,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] -[[package]] -name = "pandocfilters" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, -] - [[package]] name = "parso" version = "0.8.7" @@ -2901,15 +2450,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, ] -[[package]] -name = "pathspec" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, -] - [[package]] name = "pexpect" version = "4.9.0" @@ -3053,17 +2593,6 @@ dev = [ { name = "pytest" }, { name = "pytest-benchmark" }, ] -doc = [ - { name = "griffe-inherited-docstrings" }, - { name = "matplotlib" }, - { name = "mike" }, - { name = "mkdocs" }, - { name = "mkdocs-gen-files" }, - { name = "mkdocs-jupyter" }, - { name = "mkdocs-literate-nav" }, - { name = "mkdocs-material" }, - { name = "mkdocstrings", extra = ["python"] }, -] [package.metadata] requires-dist = [ @@ -3077,17 +2606,6 @@ dev = [ { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-benchmark", specifier = ">=5.2.3" }, ] -doc = [ - { name = "griffe-inherited-docstrings", specifier = ">=1.1.1" }, - { name = "matplotlib", specifier = ">=3.10.8" }, - { name = "mike", specifier = ">=2.1.3" }, - { name = "mkdocs", specifier = ">=1.6.1" }, - { name = "mkdocs-gen-files", specifier = ">=0.5.0" }, - { name = "mkdocs-jupyter", specifier = ">=0.25.1" }, - { name = "mkdocs-literate-nav", specifier = ">=0.6.1" }, - { name = "mkdocs-material", specifier = ">=9.5.44" }, - { name = "mkdocstrings", extras = ["python"], specifier = ">=0.27.0" }, -] [[package]] name = "prompt-toolkit" @@ -3229,29 +2747,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] -[[package]] -name = "properdocs" -version = "1.6.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ec/29/f27a4e1eddf72ed3db6e47818fbafe6debbf09fd7051f9c1a007239b46ef/properdocs-1.6.7.tar.gz", hash = "sha256:adc7b16e562890af0e098a7e5b02e3a81c20894a87d6a28d345c9300de73c26e", size = 276141, upload-time = "2026-03-20T20:07:48.167Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/4d/fc923f5c85318ee8cc903566dc4e0ebe41b2dfc1d2ecf5546db232397ed6/properdocs-1.6.7-py3-none-any.whl", hash = "sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd", size = 225406, upload-time = "2026-03-20T20:07:46.875Z" }, -] - [[package]] name = "proto-plus" version = "1.28.0" @@ -3528,19 +3023,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/c4/0e53ff3c9312ffb0e1677ebb62172660c5614077dca9c69ccfc31b90b7e4/pymatching-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:7ebce2e8bc98379f0bb7643a0ef0171142cf384ba626e46108c84249c868f0a6", size = 347977, upload-time = "2025-09-25T21:46:15.044Z" }, ] -[[package]] -name = "pymdown-extensions" -version = "10.21.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, -] - [[package]] name = "pymongo" version = "4.17.0" @@ -3738,82 +3220,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "pyyaml-env-tag" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, -] - [[package]] name = "pyzmq" version = "27.1.0" @@ -3887,20 +3293,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - [[package]] name = "requests" version = "2.34.2" @@ -3942,128 +3334,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, - { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, - { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, - { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, - { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, - { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, - { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, - { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, - { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, - { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, - { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, - { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, - { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, - { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, - { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, -] - [[package]] name = "s3fs" version = "2026.4.0" @@ -4281,15 +3551,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "soupsieve" -version = "2.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, -] - [[package]] name = "stack-data" version = "0.6.3" @@ -4352,18 +3613,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, ] -[[package]] -name = "tinycss2" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, -] - [[package]] name = "tomli" version = "2.4.1" @@ -4524,47 +3773,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/41/ac2dfdbc1f60c7af4f994c7a335cfa7040c01642b605d65f611cecc2a1e4/uvicorn-0.47.0-py3-none-any.whl", hash = "sha256:2c5715bc12d1892d84752049f400cd1c3cb018514967fdfeb97640443a6a9432", size = 71301, upload-time = "2026-05-14T18:16:51.762Z" }, ] -[[package]] -name = "verspec" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -] - [[package]] name = "wcwidth" version = "0.7.0" @@ -4574,15 +3782,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, ] -[[package]] -name = "webencodings" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, -] - [[package]] name = "widgetsnbextension" version = "4.0.15" From ba289e05551a26d9b1c41042e144c5897ff6bbab Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 23 Jun 2026 17:07:02 +0200 Subject: [PATCH 81/95] Fix prek CI --- crates/ppvm-cli/README.md | 2 +- crates/ppvm-cli/examples/simple_loop.sst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ppvm-cli/README.md b/crates/ppvm-cli/README.md index 4e936bc8c..5a9ff2249 100644 --- a/crates/ppvm-cli/README.md +++ b/crates/ppvm-cli/README.md @@ -105,7 +105,7 @@ fn @main() { // execution pauses here breakpoint - + const.u64 0 circuit measure ret diff --git a/crates/ppvm-cli/examples/simple_loop.sst b/crates/ppvm-cli/examples/simple_loop.sst index d02833ac2..83da11caf 100644 --- a/crates/ppvm-cli/examples/simple_loop.sst +++ b/crates/ppvm-cli/examples/simple_loop.sst @@ -11,7 +11,7 @@ fn @main() { // stop here to investigate breakpoint - + const.u32 1 eq.u32 cond_br @flip, @next From 569d9a6f53b83397c57405640e762a631f1e07c3 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 23 Jun 2026 17:07:18 +0200 Subject: [PATCH 82/95] Fix prek CI again --- crates/ppvm-cli/examples/bit_flip_correction.sst | 2 +- crates/ppvm-cli/examples/simple_loop.sst | 2 +- crates/ppvm-vihaco/tests/bell.sst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ppvm-cli/examples/bit_flip_correction.sst b/crates/ppvm-cli/examples/bit_flip_correction.sst index 6ed72b861..d888b80ae 100644 --- a/crates/ppvm-cli/examples/bit_flip_correction.sst +++ b/crates/ppvm-cli/examples/bit_flip_correction.sst @@ -63,4 +63,4 @@ fn @main() { const.u64 2 circuit measure ret -} \ No newline at end of file +} diff --git a/crates/ppvm-cli/examples/simple_loop.sst b/crates/ppvm-cli/examples/simple_loop.sst index 83da11caf..a52a8ac1f 100644 --- a/crates/ppvm-cli/examples/simple_loop.sst +++ b/crates/ppvm-cli/examples/simple_loop.sst @@ -30,4 +30,4 @@ fn @main() { @done: ret -} \ No newline at end of file +} diff --git a/crates/ppvm-vihaco/tests/bell.sst b/crates/ppvm-vihaco/tests/bell.sst index f2644119d..df2487bdb 100644 --- a/crates/ppvm-vihaco/tests/bell.sst +++ b/crates/ppvm-vihaco/tests/bell.sst @@ -15,4 +15,4 @@ fn @main() { circuit measure ret -} \ No newline at end of file +} From 84f1b571f41c5827780ed96d3f8bd4f87c7d2c8a Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 23 Jun 2026 17:13:39 +0200 Subject: [PATCH 83/95] Fix clippy issues --- crates/ppvm-vihaco/src/composite.rs | 2 +- crates/ppvm-vihaco/src/observable.rs | 5 ++--- crates/ppvm-vihaco/src/syntax.rs | 11 +++++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index dfc963a0b..a09ca6b96 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -84,7 +84,7 @@ pub struct PPVM { pub enum PPVMEffect { Step(StepOutcome), Stdout(StdoutEffect), - Circuit(CircuitEffect), + Circuit(Box), Measurement(MeasurementEffect), Trace(TraceEffect), } diff --git a/crates/ppvm-vihaco/src/observable.rs b/crates/ppvm-vihaco/src/observable.rs index 8b94ee8e1..f5bc92032 100644 --- a/crates/ppvm-vihaco/src/observable.rs +++ b/crates/ppvm-vihaco/src/observable.rs @@ -93,7 +93,7 @@ fn pauli_sum_parser<'src>( // Term: coefficient [*] word | bare word. let term_with_coeff = coefficient .then_ignore(just('*').padded().or_not()) - .then(pauli_word.clone()) + .then(pauli_word) .map(|(c, w)| (w, c)); let term_bare = pauli_word.map(|w| (w, 1.0)); let term = choice((term_with_coeff, term_bare)); @@ -103,10 +103,9 @@ fn pauli_sum_parser<'src>( // First term: optional leading sign. let first = sign - .clone() .padded() .or_not() - .then(term.clone()) + .then(term) .map(|(s, (w, c))| (w, s.unwrap_or(1.0) * c)); // Subsequent terms: required + or - before the term. diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index 9c31c7db7..96a7d03e5 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -165,10 +165,13 @@ impl Resolve for PPVMResolver { patch.apply(&mut code, idx, &labels)?; } - let mut module = Module::default(); - module.code = code; - module.strings = std::mem::take(&mut self.strings); - module.extra = info; + let strings = std::mem::take(&mut self.strings); + let module = Module { + code, + strings, + extra: info, + ..Default::default() + }; Ok(module) } } From 4d2010da143d9bf6b26ba15426ebce1fe7a61adc Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 23 Jun 2026 17:19:34 +0200 Subject: [PATCH 84/95] Fix ruff issues --- ppvm-python/src/ppvm/mixins.py | 4 +--- prek.toml | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ppvm-python/src/ppvm/mixins.py b/ppvm-python/src/ppvm/mixins.py index c6253b83b..f26627015 100644 --- a/ppvm-python/src/ppvm/mixins.py +++ b/ppvm-python/src/ppvm/mixins.py @@ -617,9 +617,7 @@ def rz(self, *args: Any, theta: float | None = None, truncate: bool = True) -> N targets, theta, truncate = _split_targets_parameter_truncate(args, theta, "theta", truncate) self._interface.rz(targets, theta, truncate) - def r( - self, addr0: int, axis_angle: float, theta: float, *, truncate: bool = True - ) -> None: + def r(self, addr0: int, axis_angle: float, theta: float, *, truncate: bool = True) -> None: """Apply a rotation about an axis in the X-Y plane to the specified qubit. See :meth:`RotationsMixin.r` for the gate definition. diff --git a/prek.toml b/prek.toml index af826676e..cff8a74dd 100644 --- a/prek.toml +++ b/prek.toml @@ -70,16 +70,16 @@ pass_filenames = false types = ["python"] # License headers: hawkeye (Rust-based SPDX header checker) -# Installed via mise (see mise.toml); `mise exec --` is used so the hook -# works even outside a mise-activated shell. Run `mise exec -- hawkeye -# format` manually to apply headers to new files. +# Installed via mise in CI (see mise.toml), but called directly here so local +# installs on PATH also work. Run `hawkeye format` manually to apply headers to +# new files. [[repos]] repo = "local" [[repos.hooks]] id = "hawkeye" name = "hawkeye license header check" language = "system" -entry = "mise exec -- hawkeye check" +entry = "hawkeye check" pass_filenames = false # Python: ty type check From e6b6c16591664f0f4882ecbed19b3522ddcb3849 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 24 Jun 2026 15:13:41 +0200 Subject: [PATCH 85/95] Fix wasm build failure due to rayon feature --- crates/ppvm-vihaco/Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index d654a2e7d..1f2a43b9f 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -16,7 +16,7 @@ log = "0.4.29" num = "0.4.3" rayon = { version = "1.10", optional = true } ppvm-traits = { version = "0.1.0", path = "../ppvm-traits" } -ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon"] } +ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau" } smallvec = "1.15.1" vihaco = "0.1.1" vihaco-cpu = "0.1.1" @@ -24,3 +24,7 @@ vihaco-parser = "0.1.1" vihaco-parser-core = "0.1.1" vihaco-circuit-isa = { version = "0.1.0", path = "../vihaco-circuit-isa" } ppvm-pauli-sum = { version = "0.1.0", path = "../ppvm-pauli-sum" } + +# enable rayon if not on wasm +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau", features = ["rayon"] } From 719fe0adc3ebe9d93db740a9bae630779c013c9e Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 24 Jun 2026 15:27:06 +0200 Subject: [PATCH 86/95] Exclude ppvm-cli from wasm build --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 631edd75b..7a821f477 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,8 +70,10 @@ jobs: run: cargo test -p ppvm-stim --features rayon # Cross-compile the whole workspace for browser wasm so a wasm regression - # surfaces in CI. `ppvm-python-native` is a CPython extension (never a wasm - # target) and is excluded. Native-only acceleration deps (gxhash, dashmap → + # surfaces in CI. `ppvm-python-native` (a CPython extension) and `ppvm-cli` + # (a terminal binary using clap/rustyline) are never browser-wasm targets and + # are excluded; the reusable engine lives in the library crates, which stay + # covered. Native-only acceleration deps (gxhash, dashmap → # rayon, ahash) are pruned automatically by the `cfg(not(target_arch = # "wasm32"))` dependency tables, and `rand`'s entropy uses the getrandom # `wasm_js` backend wired in `.cargo/config.toml` — so no extra flags here. @@ -89,7 +91,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build workspace for wasm32-unknown-unknown - run: cargo build --target wasm32-unknown-unknown --workspace --exclude ppvm-python-native + run: cargo build --target wasm32-unknown-unknown --workspace --exclude ppvm-python-native --exclude ppvm-cli python-tests: name: Python tests From 0cd5bc1021029dceb4ce9decdc22bea24af90770 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 29 Jun 2026 09:07:53 +0200 Subject: [PATCH 87/95] Fix skill merge conflict --- skills/ppvm-usage/SKILL.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/skills/ppvm-usage/SKILL.md b/skills/ppvm-usage/SKILL.md index 9a6e82678..6217ab914 100644 --- a/skills/ppvm-usage/SKILL.md +++ b/skills/ppvm-usage/SKILL.md @@ -1,10 +1,6 @@ --- name: ppvm-usage -<<<<<<< HEAD -description: Authoritative usage guide for ppvm, a fast quantum-circuit simulator with a Rust core and Python bindings (`ppvm-runtime`, `ppvm-tableau`, `ppvm-sym`, `ppvm-stim`, `ppvm-vihaco`, `ppvm` Python package). Use this skill whenever a task touches ppvm — importing `ppvm` in Python, depending on any `ppvm-*` crate in Rust, writing or running `.sst` programs through `ppvm-cli`, writing or modifying Pauli-propagation code, building or running circuits against the generalized stabilizer tableau, executing Stim programs, modelling depolarizing or loss noise, or even just answering "how do I do X in ppvm". Use it even when the user only hints at ppvm (mentions Pauli strings + truncation, or `GeneralizedTableau`, or "Bloqade simulation backend"). Skipping this skill is a top source of broken examples — the API has several non-obvious conventions (Heisenberg gate order, `Config`-generic types, kwargs-not-classes truncation) that look reasonable but are wrong if guessed. -======= -description: Authoritative usage guide for ppvm, a fast quantum-circuit simulator with a Rust core and Python bindings (`ppvm-traits`, `ppvm-pauli-word`, `ppvm-pauli-sum`, `ppvm-tableau`, `ppvm-sym`, `ppvm-stim`, `ppvm` Python package). Use this skill whenever a task touches ppvm — importing `ppvm` in Python, depending on any `ppvm-*` crate in Rust, writing or modifying Pauli-propagation code, building or running circuits against the generalized stabilizer tableau, executing Stim programs, modelling depolarizing or loss noise, or even just answering "how do I do X in ppvm". Use it even when the user only hints at ppvm (mentions Pauli strings + truncation, or `GeneralizedTableau`, or "Bloqade simulation backend"). Skipping this skill is a top source of broken examples — the API has several non-obvious conventions (Heisenberg gate order, `Config`-generic types, kwargs-not-classes truncation) that look reasonable but are wrong if guessed. ->>>>>>> main +description: Authoritative usage guide for ppvm, a fast quantum-circuit simulator with a Rust core and Python bindings (`ppvm-traits`, `ppvm-pauli-word`, `ppvm-pauli-sum`, `ppvm-tableau`, `ppvm-sym`, `ppvm-stim`, `ppvm-vihaco`, `ppvm` Python package). Use this skill whenever a task touches ppvm — importing `ppvm` in Python, depending on any `ppvm-*` crate in Rust, writing or running `.sst` programs through `ppvm-cli`, writing or modifying Pauli-propagation code, building or running circuits against the generalized stabilizer tableau, executing Stim programs, modelling depolarizing or loss noise, or even just answering "how do I do X in ppvm". Use it even when the user only hints at ppvm (mentions Pauli strings + truncation, or `GeneralizedTableau`, or "Bloqade simulation backend"). Skipping this skill is a top source of broken examples — the API has several non-obvious conventions (Heisenberg gate order, `Config`-generic types, kwargs-not-classes truncation) that look reasonable but are wrong if guessed. allowed-tools: Bash, Read, Write, Edit --- From c16a066b920af66f9c58be4cbcb94d63e66006c5 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 29 Jun 2026 09:11:43 +0200 Subject: [PATCH 88/95] Fix :meth: docstrings --- ppvm-python/src/ppvm/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ppvm-python/src/ppvm/mixins.py b/ppvm-python/src/ppvm/mixins.py index f26627015..6fbf6064a 100644 --- a/ppvm-python/src/ppvm/mixins.py +++ b/ppvm-python/src/ppvm/mixins.py @@ -620,14 +620,14 @@ def rz(self, *args: Any, theta: float | None = None, truncate: bool = True) -> N def r(self, addr0: int, axis_angle: float, theta: float, *, truncate: bool = True) -> None: """Apply a rotation about an axis in the X-Y plane to the specified qubit. - See :meth:`RotationsMixin.r` for the gate definition. + See `RotationsMixin.r` for the gate definition. Args: addr0: The index of the target qubit. axis_angle: The angle ``φ`` (in radians) of the rotation axis within the X-Y plane, measured from the X-axis. theta: The rotation angle in radians. - truncate: See :meth:`rx`. + truncate: See `rx`. """ self._interface.r(addr0, axis_angle, theta, truncate=truncate) From 51526460b85857907027c43df653fec23254cd1b Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 29 Jun 2026 09:38:52 +0200 Subject: [PATCH 89/95] Syntax: "circuit h" -> "circuit.h" --- crates/ppvm-cli/README.md | 4 +- .../ppvm-cli/examples/bit_flip_correction.sst | 26 +++++----- crates/ppvm-cli/examples/ghz.sst | 12 ++--- crates/ppvm-cli/examples/heisenberg_zz.sst | 4 +- crates/ppvm-cli/examples/loop_feedforward.sst | 8 +-- crates/ppvm-cli/examples/simple_loop.sst | 6 +-- crates/ppvm-cli/src/commands.rs | 8 +-- crates/ppvm-vihaco/src/bytecode.rs | 14 ++--- crates/ppvm-vihaco/src/composite.rs | 52 +++++++++---------- crates/ppvm-vihaco/src/lib.rs | 2 +- crates/ppvm-vihaco/src/shots.rs | 4 +- crates/ppvm-vihaco/src/syntax.rs | 22 ++++---- crates/ppvm-vihaco/tests/bell.sst | 8 +-- .../ppvm-vihaco/tests/branch_on_outcome.sst | 8 +-- .../ppvm-vihaco/tests/branch_on_outcome_x.sst | 8 +-- crates/ppvm-vihaco/tests/function_call.sst | 4 +- .../tests/function_call_branch_both.sst | 12 ++--- .../ppvm-vihaco/tests/function_call_ret.sst | 8 +-- crates/ppvm-vihaco/tests/hello_circuit.sst | 6 +-- .../tests/lossy_paulisum_loss_trace.sst | 8 +-- .../ppvm-vihaco/tests/paulisum_bell_trace.sst | 6 +-- .../tests/paulisum_ghz_xxx_trace.sst | 8 +-- .../tests/paulisum_measure_error.sst | 2 +- .../tests/paulisum_multi_term_trace.sst | 2 +- .../ppvm-vihaco/tests/paulisum_ry_z_trace.sst | 6 +-- .../tests/paulisum_trotter_truncate.sst | 14 ++--- crates/ppvm-vihaco/tests/rotxy.sst | 6 +-- crates/ppvm-vihaco/tests/sst_fixtures.rs | 6 +-- .../ppvm-vihaco/tests/tableau_bell_trace.sst | 6 +-- .../tests/tableau_ghz_xxx_trace.sst | 8 +-- .../ppvm-vihaco/tests/tableau_ry_z_trace.sst | 4 +- skills/ppvm-usage/SKILL.md | 10 ++-- 32 files changed, 151 insertions(+), 151 deletions(-) diff --git a/crates/ppvm-cli/README.md b/crates/ppvm-cli/README.md index 5a9ff2249..9a08f726c 100644 --- a/crates/ppvm-cli/README.md +++ b/crates/ppvm-cli/README.md @@ -101,13 +101,13 @@ wherever you want execution to stop: ``` fn @main() { const.u64 0 - circuit h + circuit.h // execution pauses here breakpoint const.u64 0 - circuit measure + circuit.measure ret } ``` diff --git a/crates/ppvm-cli/examples/bit_flip_correction.sst b/crates/ppvm-cli/examples/bit_flip_correction.sst index d888b80ae..a76d1e267 100644 --- a/crates/ppvm-cli/examples/bit_flip_correction.sst +++ b/crates/ppvm-cli/examples/bit_flip_correction.sst @@ -7,25 +7,25 @@ fn @main() { const.f64 0.25 const.f64 0.0 const.f64 0.0 - circuit paulierror + circuit.paulierror const.u64 0 const.u64 3 - circuit cnot + circuit.cnot const.u64 1 const.u64 3 - circuit cnot + circuit.cnot const.u64 3 - circuit measure + circuit.measure const.u64 1 const.u64 4 - circuit cnot + circuit.cnot const.u64 2 const.u64 4 - circuit cnot + circuit.cnot const.u64 4 - circuit measure + circuit.measure const.u32 1 eq.u32 @@ -43,24 +43,24 @@ fn @main() { @correct_q0: const.u64 0 - circuit x + circuit.x br @readout @correct_q1: const.u64 1 - circuit x + circuit.x br @readout @correct_q2: const.u64 2 - circuit x + circuit.x @readout: const.u64 0 - circuit measure + circuit.measure const.u64 1 - circuit measure + circuit.measure const.u64 2 - circuit measure + circuit.measure ret } diff --git a/crates/ppvm-cli/examples/ghz.sst b/crates/ppvm-cli/examples/ghz.sst index 9eb42fb26..aa88b8a65 100644 --- a/crates/ppvm-cli/examples/ghz.sst +++ b/crates/ppvm-cli/examples/ghz.sst @@ -4,24 +4,24 @@ device circuit.n_qubits 3; // The three outcomes are perfectly correlated, so each shot reads 0 0 0 or 1 1 1. fn @main() { const.u64 0 - circuit h + circuit.h const.u64 0 const.u64 1 - circuit cnot + circuit.cnot const.u64 1 const.u64 2 - circuit cnot + circuit.cnot const.u64 0 - circuit measure + circuit.measure const.u64 1 - circuit measure + circuit.measure const.u64 2 - circuit measure + circuit.measure ret } diff --git a/crates/ppvm-cli/examples/heisenberg_zz.sst b/crates/ppvm-cli/examples/heisenberg_zz.sst index 2465e64f4..028e7e098 100644 --- a/crates/ppvm-cli/examples/heisenberg_zz.sst +++ b/crates/ppvm-cli/examples/heisenberg_zz.sst @@ -8,10 +8,10 @@ device circuit.coefficient_threshold 1e-10; // (coef 1.0) and XX (coef 0.5), so the trace is their coefficient sum. // // No gates here, so the trace returns the seeded observable's coefficient -// sum directly: 1.0 + 0.5 = 1.5. Add `circuit cnot; circuit h; circuit truncate` +// 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 + circuit.trace ret } diff --git a/crates/ppvm-cli/examples/loop_feedforward.sst b/crates/ppvm-cli/examples/loop_feedforward.sst index ab7676310..f1a7f6d87 100644 --- a/crates/ppvm-cli/examples/loop_feedforward.sst +++ b/crates/ppvm-cli/examples/loop_feedforward.sst @@ -13,9 +13,9 @@ fn @main() { @loop: // Flip a fair coin: H then measure q0 (pushes outcome 0/1 as u32). const.u64 0 - circuit h + circuit.h const.u64 0 - circuit measure + circuit.measure // Feed-forward: if the outcome was 1, apply X to q1. const.u32 1 @@ -24,7 +24,7 @@ fn @main() { @flip: const.u64 1 - circuit x + circuit.x br @next @next: @@ -39,6 +39,6 @@ fn @main() { @done: // Final readout of q1. const.u64 1 - circuit measure + circuit.measure ret } diff --git a/crates/ppvm-cli/examples/simple_loop.sst b/crates/ppvm-cli/examples/simple_loop.sst index a52a8ac1f..3591973ac 100644 --- a/crates/ppvm-cli/examples/simple_loop.sst +++ b/crates/ppvm-cli/examples/simple_loop.sst @@ -5,9 +5,9 @@ fn @main() { @loop: const.u64 0 - circuit h + circuit.h const.u64 0 - circuit measure + circuit.measure // stop here to investigate breakpoint @@ -18,7 +18,7 @@ fn @main() { @flip: const.u64 0 - circuit x + circuit.x @next: const.u64 1 diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index b6d3db799..574d3c174 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -293,7 +293,7 @@ mod tests { /// 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"; + "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n circuit.measure\n ret }\n"; fn row(outcomes: &[MeasurementOutcome]) -> MeasurementResult { outcomes.iter().copied().collect() @@ -411,7 +411,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() { const.str \"Z?*\"\n circuit.trace\n ret }\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); @@ -514,7 +514,7 @@ 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"; + const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 1;\nfn @main() { breakpoint\n const.u64 0\n circuit.measure\n ret }\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 +528,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 const.u64 0 / 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"), diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs index ce77f788e..ef1153716 100644 --- a/crates/ppvm-vihaco/src/bytecode.rs +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -401,10 +401,10 @@ mod tests { let src = "device circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ - circuit h\n\ + circuit.h\n\ const.u64 0\n\ const.u64 1\n\ - circuit cnot\n\ + circuit.cnot\n\ ret\n\ }\n"; @@ -419,10 +419,10 @@ 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\ + 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"; let bytes = compile_to_bytes(src).unwrap(); @@ -436,7 +436,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() { const.u64 0\n circuit.measure\n ret }\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(); diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index a09ca6b96..d150e3762 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -662,7 +662,7 @@ mod tests { /* const.u64 0 - circuit h + circuit.h */ let zero = PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))); let one = PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(1))); @@ -673,7 +673,7 @@ mod tests { /* const.u64 0 - circuit t + circuit.t */ module.code.push(zero.clone()); @@ -684,7 +684,7 @@ mod tests { /* const.u64 0 const.u64 1 - circuit cnot + circuit.cnot */ module.code.push(zero.clone()); module.code.push(one.clone()); @@ -752,7 +752,7 @@ mod tests { // 5-qubit GHZ: H on q0, then CNOT(q_i, q_{i+1}) for i = 0..4. /* const.u64 0 - circuit h + circuit.h */ module .code @@ -767,7 +767,7 @@ mod tests { /* const.u64 i const.u64 i+1 - circuit cnot + circuit.cnot */ module .code @@ -788,7 +788,7 @@ mod tests { for q in 0..5u64 { /* const.u64 q - circuit measure + circuit.measure */ module .code @@ -866,7 +866,7 @@ mod tests { machine.load(&module)?; machine.init()?; - // `circuit h` with nothing on the stack: `pop_qubit` fails. + // `circuit.h` with nothing on the stack: `pop_qubit` fails. let missing_operand = [PPVMInstruction::Circuit(CircuitInstruction::H)]; assert!( machine @@ -974,14 +974,14 @@ mod tests { device circuit.coefficient_threshold 1e-8;\n\ fn @main() {\n\ const.u64 0\n\ - circuit h\n\ + circuit.h\n\ ret\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 + // const.u64 0 / circuit.h / ret = 3 assert_eq!(machine.loader.module.code.len(), 3); Ok(()) } @@ -991,14 +991,14 @@ mod tests { let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ - circuit h\n\ + circuit.h\n\ const.u64 0\n\ const.u64 1\n\ - circuit cnot\n\ + circuit.cnot\n\ const.u64 0\n\ - circuit measure\n\ + circuit.measure\n\ const.u64 1\n\ - circuit measure\n\ + circuit.measure\n\ ret\n\ }\n"; let mut machine = PPVM::default(); @@ -1015,14 +1015,14 @@ mod tests { let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ - circuit h\n\ + circuit.h\n\ const.u64 0\n\ const.u64 1\n\ - circuit cnot\n\ + circuit.cnot\n\ const.u64 0\n\ - circuit measure\n\ + circuit.measure\n\ const.u64 1\n\ - circuit measure\n\ + circuit.measure\n\ ret\n\ }\n"; let mut machine = PPVM::default(); @@ -1054,7 +1054,7 @@ 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\ + circuit.not_a_real_gate\n\ ret\n\ }\n"; let mut machine = PPVM::default(); @@ -1072,15 +1072,15 @@ mod tests { const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ - circuit h\n\ + circuit.h\n\ const.u64 0\n\ const.u64 1\n\ - circuit cnot\n\ + circuit.cnot\n\ const.u64 0\n\ - circuit measure\n\ + circuit.measure\n\ breakpoint\n\ const.u64 1\n\ - circuit measure\n\ + circuit.measure\n\ ret\n\ }\n"; @@ -1126,7 +1126,7 @@ 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.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(); @@ -1242,7 +1242,7 @@ 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.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(); @@ -1264,7 +1264,7 @@ mod tests { #[test] fn tableau_trace_emits_expectation_on_zero_state() { - // Task 16: `circuit trace` on the Tableau backend now computes + // Task 16: `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. @@ -1284,7 +1284,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.trace let trace = machine.trace_record(); assert_eq!(trace.len(), 1); assert!( diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index ecd8a8cba..f8360c732 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -90,7 +90,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() { const.u64 0\n circuit.measure\n ret }\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 b1935382f..966edaa71 100644 --- a/crates/ppvm-vihaco/src/shots.rs +++ b/crates/ppvm-vihaco/src/shots.rs @@ -128,10 +128,10 @@ mod tests { /// 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"; + "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n circuit.measure\n ret }\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() { const.u64 0\n circuit.h\n const.u64 0\n circuit.measure\n ret }\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 96a7d03e5..23ee9f410 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -185,14 +185,14 @@ impl<'src> Parse<'src> for PPVMInstruction { let cpu = ::parser().map(PPVMInstruction::Cpu); // Reuse the derived parser for all CircuitInstruction variants, - // gated behind the `circuit ` keyword (covers gates, noise channels, + // gated behind the `circuit.` prefix (covers gates, noise channels, // measure/reset, trace, and truncate — i.e. everything circuit-side). let circuit = just("circuit") - .then(text::whitespace().at_least(1)) + .then(just('.')) .ignore_then(::parser()) .map(PPVMInstruction::Circuit); - // Try `circuit ...` first so CPU doesn't see "circuit" as an identifier. + // Try `circuit.` first so CPU doesn't see "circuit" as an identifier. choice((circuit, cpu)) } } @@ -497,7 +497,7 @@ mod tests { #[test] fn ppvm_instruction_parses_gate_h() { let got = ::parser() - .parse("circuit h") + .parse("circuit.h") .into_result() .unwrap(); assert!(matches!( @@ -509,7 +509,7 @@ mod tests { #[test] fn ppvm_instruction_parses_gate_cnot() { let got = ::parser() - .parse("circuit cnot") + .parse("circuit.cnot") .into_result() .unwrap(); assert!(matches!( @@ -521,7 +521,7 @@ mod tests { #[test] fn ppvm_instruction_parses_gate_measure() { let got = ::parser() - .parse("circuit measure") + .parse("circuit.measure") .into_result() .unwrap(); assert!(matches!( @@ -533,7 +533,7 @@ mod tests { #[test] fn ppvm_instruction_parses_gate_rx() { let got = ::parser() - .parse("circuit rx") + .parse("circuit.rx") .into_result() .unwrap(); assert!(matches!( @@ -544,7 +544,7 @@ mod tests { #[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. + // `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() @@ -633,15 +633,15 @@ mod tests { "device circuit.n_qubits 2;\n\ fn @main() {\n\ const.u64 0\n\ - circuit h\n\ + circuit.h\n\ const.u64 0\n\ const.u64 1\n\ - circuit cnot\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 + // 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], diff --git a/crates/ppvm-vihaco/tests/bell.sst b/crates/ppvm-vihaco/tests/bell.sst index df2487bdb..8b23d1f37 100644 --- a/crates/ppvm-vihaco/tests/bell.sst +++ b/crates/ppvm-vihaco/tests/bell.sst @@ -2,17 +2,17 @@ device circuit.n_qubits 2; fn @main() { const.u64 0 - circuit h + circuit.h const.u64 0 const.u64 1 - circuit cnot + circuit.cnot const.u64 0 - circuit measure + circuit.measure const.u64 0 - circuit measure + circuit.measure ret } diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome.sst b/crates/ppvm-vihaco/tests/branch_on_outcome.sst index 911b9b8e9..d86de1896 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome.sst @@ -2,10 +2,10 @@ device circuit.n_qubits 2; fn @main() { const.u64 0 - circuit h + circuit.h const.u64 0 - circuit measure + circuit.measure // Stack: [outcome]. No loss gate, so outcome is 0 or 1. Compare to 1 // to derive a bool for cond_br. @@ -16,7 +16,7 @@ fn @main() { @one: const.u64 1 - circuit x + circuit.x br @measure_q1 @zero: @@ -24,6 +24,6 @@ fn @main() { @measure_q1: const.u64 1 - circuit measure + circuit.measure ret } diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst index f410cb8fb..501797c4f 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst @@ -3,10 +3,10 @@ device circuit.n_qubits 2; fn @main() { // X on q0 -> |1>, measure -> outcome is deterministically 1. const.u64 0 - circuit x + circuit.x const.u64 0 - circuit measure + circuit.measure // Stack: [outcome]. No loss gate, so outcome is 0 or 1. Compare to 1 // to derive a bool for cond_br. @@ -17,7 +17,7 @@ fn @main() { @one: const.u64 1 - circuit x + circuit.x br @measure_q1 @zero: @@ -25,6 +25,6 @@ fn @main() { @measure_q1: const.u64 1 - circuit measure + circuit.measure ret } diff --git a/crates/ppvm-vihaco/tests/function_call.sst b/crates/ppvm-vihaco/tests/function_call.sst index 0761fd256..c001a5160 100644 --- a/crates/ppvm-vihaco/tests/function_call.sst +++ b/crates/ppvm-vihaco/tests/function_call.sst @@ -10,10 +10,10 @@ fn @main() { fn @run_circuit() { const.u64 1 - circuit h + circuit.h const.u64 1 - circuit measure + circuit.measure 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 1c10a6372..46b538f12 100644 --- a/crates/ppvm-vihaco/tests/function_call_branch_both.sst +++ b/crates/ppvm-vihaco/tests/function_call_branch_both.sst @@ -13,11 +13,11 @@ device circuit.n_qubits 2; // → P(m1 = 1) = 0.75. fn @main() { const.u64 0 - circuit h + circuit.h const.u64 0 const.f64 0.5 - circuit loss + circuit.loss call 0, @measure_q0 @@ -32,7 +32,7 @@ fn @main() { // path. The leftover `outcome` value on the stack is harmless because // we halt at @final without reading it. const.u64 1 - circuit x + circuit.x br @final @kept: @@ -43,7 +43,7 @@ fn @main() { @outcome_one: const.u64 1 - circuit x + circuit.x br @final @outcome_zero: @@ -51,13 +51,13 @@ fn @main() { @final: const.u64 1 - circuit measure + circuit.measure halt } fn @measure_q0() { const.u64 0 - circuit measure + circuit.measure // Stack: [outcome] ret 1 } diff --git a/crates/ppvm-vihaco/tests/function_call_ret.sst b/crates/ppvm-vihaco/tests/function_call_ret.sst index 90d5907d0..ea7623adf 100644 --- a/crates/ppvm-vihaco/tests/function_call_ret.sst +++ b/crates/ppvm-vihaco/tests/function_call_ret.sst @@ -7,7 +7,7 @@ device circuit.n_qubits 2; fn @main() { // Put q0 into |+>. const.u64 0 - circuit h + circuit.h // Measure q1 via a helper that returns the outcome on top of the stack. call 0, @measure_q1 @@ -20,7 +20,7 @@ fn @main() { @one: // outcome was 1: apply X to q0 as a correction. const.u64 0 - circuit x + circuit.x br @done @zero: @@ -32,10 +32,10 @@ fn @main() { fn @measure_q1() -> u32 { const.u64 1 - circuit h + circuit.h const.u64 1 - circuit measure + circuit.measure // Stack: [outcome] ret 1 diff --git a/crates/ppvm-vihaco/tests/hello_circuit.sst b/crates/ppvm-vihaco/tests/hello_circuit.sst index 6144ce02b..21f8b619b 100644 --- a/crates/ppvm-vihaco/tests/hello_circuit.sst +++ b/crates/ppvm-vihaco/tests/hello_circuit.sst @@ -2,15 +2,15 @@ device circuit.n_qubits 2; fn @main() { const.u64 0 - circuit h + circuit.h const.u64 0 const.u64 1 - circuit cnot + circuit.cnot const.u64 0 const.f64 0.1 - circuit rx + circuit.rx ret } diff --git a/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst b/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst index 36462b211..3f42aef14 100644 --- a/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst +++ b/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst @@ -14,15 +14,15 @@ device circuit.observable ZZ; fn @main() { const.u64 0 const.u64 1 - circuit cnot + circuit.cnot const.u64 0 const.f64 0.3 - circuit loss + circuit.loss - circuit truncate + circuit.truncate const.str "Z?*" - circuit trace + circuit.trace ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst index 74dacb781..e36fe339f 100644 --- a/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst @@ -6,13 +6,13 @@ fn @main() { // Textbook H(0); CNOT(0,1) — emit reversed for Heisenberg propagation. const.u64 0 const.u64 1 - circuit cnot + circuit.cnot const.u64 0 - circuit h + circuit.h // Trace against |00>: match Z-or-identity on every qubit. const.str "Z?*" - circuit trace + circuit.trace ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst index 8e5f31a1b..53cb37c41 100644 --- a/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst @@ -12,16 +12,16 @@ fn @main() { // Trace against Z/I-only Paulis picks up the coefficient of ZII = 1.0. const.u64 1 const.u64 2 - circuit cnot + circuit.cnot const.u64 0 const.u64 1 - circuit cnot + circuit.cnot const.u64 0 - circuit h + circuit.h const.str "Z?*" - circuit trace + circuit.trace ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_measure_error.sst b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst index a65de9a2f..9d5f88991 100644 --- a/crates/ppvm-vihaco/tests/paulisum_measure_error.sst +++ b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst @@ -4,6 +4,6 @@ device circuit.observable Z; fn @main() { const.u64 0 - circuit measure + circuit.measure ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst index 48f91bcc2..7de4f4f99 100644 --- a/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst @@ -6,6 +6,6 @@ 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 + circuit.trace ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst index 1f8a66212..449d92a79 100644 --- a/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst @@ -3,15 +3,15 @@ device circuit.backend paulisum; device circuit.observable Z; fn @main() { - // PauliSum already runs in the Heisenberg picture: `circuit ry` performs + // PauliSum already runs in the Heisenberg picture: `circuit.ry` performs // RY(θ)† Z RY(θ) = cos(θ)·Z + sin(θ)·X. A single gate means no // reversal is needed beyond that. Trace against Z/I-only Paulis picks // up the cos(θ) coefficient on Z; the sin(θ)·X term contributes 0. const.u64 0 const.f64 0.7 - circuit ry + circuit.ry const.str "Z?*" - circuit trace + circuit.trace ret } diff --git a/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst index ce50fb399..a94ee86a7 100644 --- a/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst +++ b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst @@ -10,28 +10,28 @@ fn @main() { const.u64 0 const.u64 1 const.f64 0.1 - circuit rxx + circuit.rxx const.u64 0 const.u64 1 const.f64 0.05 - circuit rzz + circuit.rzz - circuit truncate + circuit.truncate const.u64 0 const.u64 1 const.f64 0.1 - circuit rxx + circuit.rxx const.u64 0 const.u64 1 const.f64 0.05 - circuit rzz + circuit.rzz - circuit truncate + circuit.truncate const.str "Z?*" - circuit trace + circuit.trace ret } diff --git a/crates/ppvm-vihaco/tests/rotxy.sst b/crates/ppvm-vihaco/tests/rotxy.sst index 39bf045be..99e6c50ed 100644 --- a/crates/ppvm-vihaco/tests/rotxy.sst +++ b/crates/ppvm-vihaco/tests/rotxy.sst @@ -2,14 +2,14 @@ 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. + // Stack order for `circuit.r`: qubit, then axis_angle, then theta. const.u64 0 const.f64 1.5707963267948966 const.f64 3.141592653589793 - circuit r + circuit.r const.u64 0 - circuit measure + circuit.measure ret } diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index 8c4fb9839..a66d9445d 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.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:?}")); @@ -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.cnot; 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.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 a5e0a94c4..d188046f3 100644 --- a/crates/ppvm-vihaco/tests/tableau_bell_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_bell_trace.sst @@ -3,16 +3,16 @@ device circuit.n_qubits 2; fn @main() { // Forward Bell prep: H(0); CNOT(0, 1) → |Φ+⟩. const.u64 0 - circuit h + circuit.h const.u64 0 const.u64 1 - circuit cnot + 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 + circuit.trace ret } diff --git a/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst index 5a7c58989..5dd1c6054 100644 --- a/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst @@ -3,18 +3,18 @@ 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 + circuit.h const.u64 0 const.u64 1 - circuit cnot + circuit.cnot const.u64 1 const.u64 2 - circuit cnot + circuit.cnot // `X{3}` matches exactly the Pauli word XXX. ⟨GHZ|XXX|GHZ⟩ = 1. const.str "X{3}" - circuit trace + circuit.trace ret } diff --git a/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst index 45c78d006..b50d74cdf 100644 --- a/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst @@ -5,9 +5,9 @@ fn @main() { // Hard-coded θ = 0.7 → cos(0.7) ≈ 0.7648421872844885. const.u64 0 const.f64 0.7 - circuit ry + circuit.ry const.str "Z{1}" - circuit trace + circuit.trace ret } diff --git a/skills/ppvm-usage/SKILL.md b/skills/ppvm-usage/SKILL.md index 6217ab914..7b6eb8f01 100644 --- a/skills/ppvm-usage/SKILL.md +++ b/skills/ppvm-usage/SKILL.md @@ -391,14 +391,14 @@ The runtime applies `circuit ...` instructions in code order on every backend. Whoever emits the `.sst` is responsible for emitting gates in the right direction for the chosen picture: **forward** for Tableau (Schrödinger), **reversed** for PauliSum/Lossy (Heisenberg). Textbook `H(0); CNOT(0,1)` on -a PauliSum target compiles to `circuit cnot; circuit h`, not the other way around. +a PauliSum target compiles to `circuit.cnot; circuit.h`, not the other way around. -### `circuit trace` and `circuit truncate` +### `circuit.trace` and `circuit.truncate` ```sst const.str "Z?*" -circuit trace // PauliSum/Lossy: pushes state.trace(&pattern) as f64 -circuit truncate // PauliSum/Lossy: state.truncate(); Tableau: silent no-op +circuit.trace // PauliSum/Lossy: pushes state.trace(&pattern) as f64 +circuit.truncate // PauliSum/Lossy: state.truncate(); Tableau: silent no-op ``` `trace` pops a `Value::String` (Pauli-pattern source — same grammar as @@ -421,7 +421,7 @@ agreement on a shared input. `truncate` takes no operand and applies the configured strategy (`CoefficientThreshold` + `MaxPauliWeight`) to the current state. On Tableau it's a silent no-op — gate methods already prune via -`coefficient_threshold`. **Without explicit `circuit truncate` calls in the +`coefficient_threshold`. **Without explicit `circuit.truncate` calls in the .sst, PauliSum runs do not truncate** — the compiler that emits the .sst decides where to place them. From 8beb96ac0cd027fb674ef6fdf1ed759d43c578d1 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 30 Jun 2026 11:04:42 +0200 Subject: [PATCH 90/95] Remove the REPL --- Cargo.lock | 89 -------- crates/ppvm-cli/Cargo.toml | 1 - crates/ppvm-cli/src/main.rs | 20 +- crates/ppvm-cli/src/repl.rs | 395 ------------------------------------ 4 files changed, 9 insertions(+), 496 deletions(-) delete mode 100644 crates/ppvm-cli/src/repl.rs diff --git a/Cargo.lock b/Cargo.lock index 7c38456d1..6b60e47aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,12 +269,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chacha20" version = "0.10.0" @@ -381,15 +375,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - [[package]] name = "codespan" version = "0.13.1" @@ -647,12 +632,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "endian-type" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" - [[package]] name = "env_filter" version = "1.0.1" @@ -692,12 +671,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - [[package]] name = "eyre" version = "0.6.12" @@ -846,15 +819,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1016,27 +980,6 @@ dependencies = [ "libmimalloc-sys", ] -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - -[[package]] -name = "nix" -version = "0.31.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" -dependencies = [ - "bitflags 2.13.0", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "num" version = "0.4.3" @@ -1242,7 +1185,6 @@ dependencies = [ "clap", "eyre", "ppvm-vihaco", - "rustyline", ] [[package]] @@ -1570,16 +1512,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "radix_trie" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a" -dependencies = [ - "endian-type", - "nibble_vec", -] - [[package]] name = "rand" version = "0.9.4" @@ -1741,27 +1673,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "rustyline" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0" -dependencies = [ - "bitflags 2.13.0", - "cfg-if", - "clipboard-win", - "home", - "libc", - "log", - "memchr", - "nix", - "radix_trie", - "unicode-segmentation", - "unicode-width 0.2.2", - "utf8parse", - "windows-sys", -] - [[package]] name = "same-file" version = "1.0.6" diff --git a/crates/ppvm-cli/Cargo.toml b/crates/ppvm-cli/Cargo.toml index 4ff2b6937..4ba3d8841 100644 --- a/crates/ppvm-cli/Cargo.toml +++ b/crates/ppvm-cli/Cargo.toml @@ -7,7 +7,6 @@ edition = "2024" clap = { version = "4.6.1", features = ["derive"] } eyre = "0.6.12" ppvm-vihaco = { version = "0.1.0", path = "../ppvm-vihaco", features = ["rayon"] } -rustyline = "18.0.0" [[bin]] name = "ppvm" diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index c9cb5a007..a956a331d 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -5,7 +5,6 @@ use clap::{Parser, Subcommand}; use eyre::Result; mod commands; -mod repl; #[derive(Parser)] #[command(name = "ppvm")] @@ -15,9 +14,9 @@ pub struct Cli { #[arg(short, long, default_value = "1")] threads: usize, - /// Subcommand to run; with none, drops into the interactive REPL. + /// Subcommand to run. #[command(subcommand)] - command: Option, + command: Commands, } #[derive(Subcommand)] @@ -94,31 +93,30 @@ fn main() -> Result<()> { ppvm_vihaco::shots::set_global_threads(cli.threads)?; match cli.command { - None => repl::repl()?, - Some(Commands::Parse { file, format }) => { + Commands::Parse { file, format } => { commands::parse(&file, format)?; } - Some(Commands::Dump { + Commands::Dump { file, output, force, - }) => { + } => { commands::dump(&file, output.as_deref(), force)?; } - Some(Commands::Run { + Commands::Run { file, shots, seed, output, quiet, format, - }) => { + } => { commands::run(&file, shots, seed, output.as_deref(), quiet, format)?; } - Some(Commands::Debug { + Commands::Debug { file, break_at_start, - }) => { + } => { commands::debug(&file, break_at_start)?; } } diff --git a/crates/ppvm-cli/src/repl.rs b/crates/ppvm-cli/src/repl.rs deleted file mode 100644 index 9dfedeb5c..000000000 --- a/crates/ppvm-cli/src/repl.rs +++ /dev/null @@ -1,395 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The PPVM Authors -// SPDX-License-Identifier: Apache-2.0 - -//! Interactive REPL — a quantum-circuit playground. Allocate a fixed-size -//! device with `device N`, then apply gates and measurements one line at a time -//! against a single persistent machine, seeing measurement outcomes inline. - -use eyre::{Result, WrapErr, bail, eyre}; -use ppvm_vihaco::CircuitInstruction; -use ppvm_vihaco::composite::PPVM; -use ppvm_vihaco::measurements::MeasurementResult; -use rustyline::DefaultEditor; -use rustyline::error::ReadlineError; -#[cfg(test)] -use std::io::BufRead; -use std::io::Write; -use std::path::PathBuf; - -/// How a gate command lowers: the engine instruction plus how many qubit and -/// float operands it consumes (qubits first, then floats — the order -/// `apply_circuit_instruction` expects). -struct GateSpec { - inst: CircuitInstruction, - qubits: usize, - floats: usize, -} - -/// Resolve a gate command name to its spec, or `None` if it is not a gate. -/// `TwoQubitPauliError` is intentionally absent — its tableau arm is `todo!()`. -fn gate_spec(name: &str) -> Option { - use CircuitInstruction::*; - let (inst, qubits, floats) = match name { - "x" => (X, 1, 0), - "y" => (Y, 1, 0), - "z" => (Z, 1, 0), - "h" => (H, 1, 0), - "s" => (S, 1, 0), - "sadj" => (SAdj, 1, 0), - "sqrtx" => (SqrtX, 1, 0), - "sqrty" => (SqrtY, 1, 0), - "sqrtxadj" => (SqrtXAdj, 1, 0), - "sqrtyadj" => (SqrtYAdj, 1, 0), - "t" => (T, 1, 0), - "tadj" => (TAdj, 1, 0), - "measure" => (Measure, 1, 0), - "reset" => (Reset, 1, 0), - "cnot" => (CNOT, 2, 0), - "cz" => (CZ, 2, 0), - "rx" => (RX, 1, 1), - "ry" => (RY, 1, 1), - "rz" => (RZ, 1, 1), - "r" => (R, 1, 2), - "rxx" => (RXX, 2, 1), - "ryy" => (RYY, 2, 1), - "rzz" => (RZZ, 2, 1), - "u3" => (U3, 1, 3), - "depolarize" => (Depolarize, 1, 1), - "depolarize2" => (Depolarize2, 2, 1), - "loss" => (Loss, 1, 1), - "paulierror" => (PauliError, 1, 3), - "correlatedloss" => (CorrelatedLoss, 2, 3), - _ => return None, - }; - Some(GateSpec { - inst, - qubits, - floats, - }) -} - -/// Whether the loop should continue prompting or exit. -enum Outcome { - Continue, - Quit, -} - -/// Launch the interactive REPL with line editing and command history. -/// History recall (up/down arrows), cursor movement, and Ctrl-R search come -/// from rustyline; the per-command logic is the same `dispatch` the scripted -/// tests drive through `repl_loop`. -pub fn repl() -> Result<()> { - let mut rl = DefaultEditor::new()?; - let history = history_path(); - if let Some(path) = &history { - let _ = rl.load_history(path); // best-effort: a missing file is fine - } - - let mut machine: Option = None; - let mut output = std::io::stdout(); - loop { - match rl.readline("ppvm> ") { - Ok(line) => { - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - let _ = rl.add_history_entry(trimmed); - match dispatch(trimmed, &mut machine, &mut output) { - Ok(Outcome::Continue) => {} - Ok(Outcome::Quit) => break, - Err(e) => writeln!(output, "error: {e}")?, - } - } - // Ctrl-C: abandon the current line, keep the session (shell-like). - Err(ReadlineError::Interrupted) => continue, - // Ctrl-D on an empty line: leave cleanly. - Err(ReadlineError::Eof) => break, - Err(e) => return Err(e).wrap_err("readline failed"), - } - } - - if let Some(path) = &history { - let _ = rl.save_history(path); // best-effort: don't fail the session - } - Ok(()) -} - -/// Where to persist history across sessions: `$HOME/.ppvm_history`. `None` -/// (so history is session-only) when there's no `HOME` — e.g. on Windows, -/// where you'd reach for the `dirs` crate instead. -fn history_path() -> Option { - std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".ppvm_history")) -} - -/// Core REPL loop, generic over its IO so tests can drive it with scripted -/// input. Holds a single `Option` — `None` until `device N`. Command-level -/// errors are printed and the loop continues; only `quit`/`exit`/EOF exit. -/// -/// Test-only: the interactive entry point is `repl`, which runs the same -/// `dispatch` under a rustyline editor for history and line editing. -#[cfg(test)] -fn repl_loop(input: &mut impl BufRead, output: &mut impl Write) -> Result<()> { - let mut machine: Option = None; - loop { - write!(output, "ppvm> ")?; - output.flush()?; - - let mut line = String::new(); - if input.read_line(&mut line)? == 0 { - // EOF: leave cleanly, ending the dangling prompt line. - writeln!(output)?; - break; - } - let line = line.trim(); - if line.is_empty() { - continue; - } - - match dispatch(line, &mut machine, output) { - Ok(Outcome::Continue) => {} - Ok(Outcome::Quit) => break, - Err(e) => writeln!(output, "error: {e}")?, - } - } - Ok(()) -} - -/// Parse and run one command line. -fn dispatch(line: &str, machine: &mut Option, output: &mut impl Write) -> Result { - let mut tokens = line.split_whitespace(); - let cmd = tokens.next().expect("line is non-empty after trim"); - let args: Vec<&str> = tokens.collect(); - - match cmd { - "quit" | "exit" => return Ok(Outcome::Quit), - "help" => print_help(output)?, - "device" => cmd_device(&args, machine, output)?, - "show" => cmd_show(machine, output)?, - _ => cmd_gate(cmd, &args, machine, output)?, - } - Ok(Outcome::Continue) -} - -/// `device N` — (re)create a fresh N-qubit device, discarding any prior state. -fn cmd_device(args: &[&str], machine: &mut Option, output: &mut impl Write) -> Result<()> { - let [n] = args else { - bail!("usage: device N"); - }; - let n: usize = n - .parse() - .wrap_err_with(|| format!("invalid qubit count {n:?}"))?; - if n == 0 { - bail!("device must have at least 1 qubit"); - } - - let existed = machine.is_some(); - *machine = Some(PPVM::with_qubits(n)?); - if existed { - writeln!( - output, - "ok: fresh {n}-qubit device (previous state discarded)" - )?; - } else { - writeln!(output, "ok: fresh {n}-qubit device")?; - } - Ok(()) -} - -/// `show` — print the current tableau / Pauli state. -fn cmd_show(machine: &mut Option, output: &mut impl Write) -> Result<()> { - let machine = require_device(machine)?; - writeln!(output, "{}", machine.state_string())?; - Ok(()) -} - -/// A gate command: ` [param…]`. Applies the gate and, if it -/// produced any measurement outcomes, prints them as `=> `. -fn cmd_gate( - name: &str, - args: &[&str], - machine: &mut Option, - output: &mut impl Write, -) -> Result<()> { - let spec = gate_spec(name).ok_or_else(|| eyre!("unknown command {name:?}; try \"help\""))?; - let machine = require_device(machine)?; - - let expected = spec.qubits + spec.floats; - if args.len() != expected { - bail!( - "{name} takes {} qubit(s) and {} param(s), got {} argument(s)", - spec.qubits, - spec.floats, - args.len() - ); - } - - let (qubit_args, param_args) = args.split_at(spec.qubits); - let qubits = qubit_args - .iter() - .map(|t| { - t.parse::() - .wrap_err_with(|| format!("invalid qubit index {t:?}")) - }) - .collect::>>()?; - let params = param_args - .iter() - .map(|t| { - t.parse::() - .wrap_err_with(|| format!("invalid parameter {t:?}")) - }) - .collect::>>()?; - - // New entries in the measurement record are this command's outcomes. - let before = machine.measurement_record().len(); - machine.apply_circuit_instruction(spec.inst, &qubits, ¶ms)?; - let record = machine.measurement_record(); - if record.len() > before { - writeln!(output, "=> {}", format_outcomes(&record[before..]))?; - } - Ok(()) -} - -/// Borrow the device, or error if none has been allocated yet. -fn require_device(machine: &mut Option) -> Result<&mut PPVM> { - machine - .as_mut() - .ok_or_else(|| eyre!("no device; run \"device N\" first")) -} - -/// Render measurement outcomes as a flat bit string: `0`/`1`, lost qubit = `2` -/// (the same convention as the `run` command's output). -fn format_outcomes(records: &[MeasurementResult]) -> String { - records - .iter() - .flatten() - .map(|outcome| char::from(b'0' + *outcome as u8)) - .collect() -} - -fn print_help(output: &mut impl Write) -> Result<()> { - writeln!(output, "meta:")?; - writeln!( - output, - " device N (re)create a fresh N-qubit device" - )?; - writeln!(output, " show print the current state")?; - writeln!(output, " help show this help")?; - writeln!(output, " quit | exit | EOF leave the REPL")?; - writeln!( - output, - "gates ( = qubit index, angles/probs are floats):" - )?; - writeln!( - output, - " x y z h s sadj sqrtx sqrty sqrtxadj sqrtyadj t tadj reset measure " - )?; - writeln!(output, " cnot | cz ")?; - writeln!(output, " rx ry rz | r ")?; - writeln!(output, " u3 ")?; - writeln!(output, " rxx ryy rzz ")?; - writeln!( - output, - " depolarize loss

| depolarize2

" - )?; - writeln!( - output, - " paulierror | correlatedloss " - )?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Drive the loop with a scripted session, returning everything it wrote. - fn session(script: &str) -> String { - let mut input = std::io::Cursor::new(script.as_bytes().to_vec()); - let mut output: Vec = Vec::new(); - repl_loop(&mut input, &mut output).unwrap(); - String::from_utf8(output).unwrap() - } - - #[test] - fn device_then_x_then_measure_is_one() { - let out = session("device 1\nx 0\nmeasure 0\nquit\n"); - assert!(out.contains("ok: fresh 1-qubit device"), "{out}"); - assert!(out.contains("=> 1"), "X|0> measured should be 1:\n{out}"); - } - - #[test] - fn fresh_measure_is_zero() { - let out = session("device 1\nmeasure 0\nquit\n"); - assert!(out.contains("=> 0"), "|0> measured should be 0:\n{out}"); - } - - #[test] - fn gate_before_device_errors_and_continues() { - // The error is printed and the loop keeps going: the later device+measure - // still works. - let out = session("x 0\ndevice 1\nmeasure 0\nquit\n"); - assert!(out.contains("no device"), "{out}"); - assert!( - out.contains("=> 0"), - "loop should continue after the error:\n{out}" - ); - } - - #[test] - fn device_twice_reports_discarded_state() { - let out = session("device 1\ndevice 2\nquit\n"); - assert!(out.contains("previous state discarded"), "{out}"); - } - - #[test] - fn show_renders_the_state() { - let out = session("device 1\nshow\nquit\n"); - let expected = PPVM::with_qubits(1).unwrap().state_string(); - assert!(out.contains(expected.trim()), "show output missing:\n{out}"); - } - - #[test] - fn unknown_command_errors_and_continues() { - let out = session("device 1\nbogus 0\nmeasure 0\nquit\n"); - assert!(out.contains("unknown command"), "{out}"); - assert!(out.contains("=> 0"), "loop should continue:\n{out}"); - } - - #[test] - fn eof_exits_cleanly() { - // No quit line: the session ends at EOF without hanging or erroring. - let out = session("device 1\n"); - assert!(out.contains("ok: fresh 1-qubit device"), "{out}"); - } - - #[test] - fn cnot_respects_control_and_target_order() { - // x 0 -> |10>; cnot 0 1 (control q0=1) flips q1 -> |11>. Both measure 1. - // If control/target were swapped, q1 would stay 0. - let out = session("device 2\nx 0\ncnot 0 1\nmeasure 0\nmeasure 1\nquit\n"); - // Each scripted line follows a "ppvm> " prompt with no echo, so a result - // line reads "ppvm> => 1"; take the text after "=> ". - let measurements: Vec<&str> = out.lines().filter_map(|l| l.split("=> ").nth(1)).collect(); - assert_eq!(measurements, vec!["1", "1"], "{out}"); - } - - #[test] - fn two_qubit_float_gate_runs() { - // rxx (2 qubits + 1 float) should parse and apply without error. - let out = session("device 2\nrxx 0 1 0.5\nquit\n"); - assert!(!out.contains("error"), "rxx should run cleanly:\n{out}"); - } - - #[test] - fn bad_arity_errors() { - let out = session("device 1\nx\nquit\n"); - assert!(out.contains("takes"), "arity error expected:\n{out}"); - } - - #[test] - fn out_of_range_qubit_errors_not_panics() { - let out = session("device 1\nx 3\nquit\n"); - assert!(out.contains("out of range"), "{out}"); - } -} From 7fa85d0a56d4cd73b25a516cb4149f66d9b470a7 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 30 Jun 2026 12:32:41 +0200 Subject: [PATCH 91/95] Fix copilot's dumb changes --- crates/ppvm-vihaco/src/measurements.rs | 4 ++-- crates/ppvm-vihaco/src/syntax.rs | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/ppvm-vihaco/src/measurements.rs b/crates/ppvm-vihaco/src/measurements.rs index c5e0bf113..1881c0d51 100644 --- a/crates/ppvm-vihaco/src/measurements.rs +++ b/crates/ppvm-vihaco/src/measurements.rs @@ -5,11 +5,11 @@ use eyre::Result; use smallvec::SmallVec; use vihaco::{Effects, observe}; -/// Measurement results are represent as an integer enum +/// Measurement results are represented as an integer enum: /// 0: state |0> /// 1: state |1> /// 2: qubit has been lost prior to measurement -/// In byte-code, this is represented as a u32 integer, which is simpler than +/// In bytecode, this is represented as a u32 integer, which is simpler than /// e.g. two boolean values and matches semantics elsewhere #[repr(u8)] #[derive(Debug, Clone, Copy, Eq, PartialEq)] diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index 23ee9f410..e11865068 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -23,8 +23,7 @@ pub enum PPVMHeader { #[token = "circuit.coefficient_threshold"] #[delimiters(open = "", close = "", separator = "")] - CoefficientThrehsold(f64), - + CoefficientThreshold(f64), #[token = "circuit.backend"] #[delimiters(open = "", close = "", separator = "")] Backend(BackendKind), @@ -53,7 +52,7 @@ impl PPVMResolver { PPVMHeader::NumQubits(n) => { info.n_qubits = n; } - PPVMHeader::CoefficientThrehsold(t) => { + PPVMHeader::CoefficientThreshold(t) => { info.coefficient_threshold = t; } PPVMHeader::Backend(b) => { @@ -364,7 +363,7 @@ mod tests { .parse("device circuit.coefficient_threshold 1e-10") .into_result() .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::CoefficientThrehsold(1e-10)); + assert_eq!(got, PPVMHeader::CoefficientThreshold(1e-10)); } #[test] @@ -447,7 +446,7 @@ mod tests { #[test] fn apply_header_sets_coefficient_threshold() { let mut info = PPVMDeviceInfo::default(); - PPVMResolver::apply_header(&mut info, PPVMHeader::CoefficientThrehsold(5e-6)).unwrap(); + PPVMResolver::apply_header(&mut info, PPVMHeader::CoefficientThreshold(5e-6)).unwrap(); assert_eq!(info.coefficient_threshold, 5e-6); } From 5a3765d4f6d63c6dda7f374abe0ab75e7034e1a4 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 30 Jun 2026 12:35:42 +0200 Subject: [PATCH 92/95] Clean up some dependencies --- Cargo.lock | 5 ----- Cargo.toml | 2 -- crates/ppvm-python-native/Cargo.toml | 1 - crates/ppvm-tableau-sum/Cargo.toml | 1 - crates/ppvm-vihaco/Cargo.toml | 1 - 5 files changed, 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b60e47aa..3c0f166b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1171,8 +1171,6 @@ dependencies = [ name = "ppvm" version = "0.1.0" dependencies = [ - "fxhash", - "num", "ppvm-pauli-sum", "ppvm-sym", "ppvm-tableau", @@ -1227,7 +1225,6 @@ name = "ppvm-python-native" version = "0.1.0" dependencies = [ "bnum", - "num", "paste", "ppvm-pauli-sum", "ppvm-stim", @@ -1305,7 +1302,6 @@ dependencies = [ "mimalloc", "num", "ppvm-pauli-sum", - "ppvm-pauli-word", "ppvm-tableau", "ppvm-traits", "rand 0.10.1", @@ -1341,7 +1337,6 @@ dependencies = [ "num", "ppvm-pauli-sum", "ppvm-tableau", - "ppvm-traits", "rayon", "smallvec", "vihaco", diff --git a/Cargo.toml b/Cargo.toml index 61dfd384c..29fe3a70f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] -fxhash = "0.2.1" -num = "0.4.3" ppvm-pauli-sum = { version = "0.1.0", path = "crates/ppvm-pauli-sum" } ppvm-tableau = { version = "0.1.0", path = "crates/ppvm-tableau" } ppvm-sym = { version = "0.1.0", path = "crates/ppvm-sym" } diff --git a/crates/ppvm-python-native/Cargo.toml b/crates/ppvm-python-native/Cargo.toml index 8f12521ed..7d948575b 100644 --- a/crates/ppvm-python-native/Cargo.toml +++ b/crates/ppvm-python-native/Cargo.toml @@ -13,7 +13,6 @@ test = false [dependencies] bnum = "0.13.0" -num = "0.4.3" paste = "1.0.15" ppvm-pauli-sum = { version = "0.1.0", path = "../ppvm-pauli-sum" } ppvm-stim = { version = "0.1.0", path = "../ppvm-stim", features = ["rayon"] } diff --git a/crates/ppvm-tableau-sum/Cargo.toml b/crates/ppvm-tableau-sum/Cargo.toml index 65355efec..47eb0bb61 100644 --- a/crates/ppvm-tableau-sum/Cargo.toml +++ b/crates/ppvm-tableau-sum/Cargo.toml @@ -8,7 +8,6 @@ bitvec = "1.0.1" fxhash = "0.2.1" num = "0.4.3" ppvm-traits = { version = "0.1.0", path = "../ppvm-traits" } -ppvm-pauli-word = { version = "0.1.0", path = "../ppvm-pauli-word" } ppvm-pauli-sum = { version = "0.1.0", path = "../ppvm-pauli-sum" } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau" } rand = "0.10.1" diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 1f2a43b9f..f1e479543 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -15,7 +15,6 @@ eyre = "0.6.12" log = "0.4.29" num = "0.4.3" rayon = { version = "1.10", optional = true } -ppvm-traits = { version = "0.1.0", path = "../ppvm-traits" } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau" } smallvec = "1.15.1" vihaco = "0.1.1" From f7df680bc3e78a68e366af88864f1a1e7cbe2522 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 1 Jul 2026 09:24:10 +0200 Subject: [PATCH 93/95] Fix machete --- Cargo.lock | 1 - crates/vihaco-circuit-isa/Cargo.toml | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c0f166b9..7e7dbfbe1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1925,7 +1925,6 @@ name = "vihaco-circuit-isa" version = "0.1.0" dependencies = [ "chumsky 0.10.1", - "eyre", "smallvec", "vihaco", "vihaco-parser", diff --git a/crates/vihaco-circuit-isa/Cargo.toml b/crates/vihaco-circuit-isa/Cargo.toml index 344841021..3b7a5a6ea 100644 --- a/crates/vihaco-circuit-isa/Cargo.toml +++ b/crates/vihaco-circuit-isa/Cargo.toml @@ -5,8 +5,10 @@ edition = "2024" [dependencies] 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" + +[package.metadata.cargo-machete] +ignored = ["eyre"] # transitive dependency in vihaco, somehow not handled correctly by machete From 9de97d8681d1e9677a0d11e4dbf931a5d8c4010b Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 1 Jul 2026 10:47:12 +0200 Subject: [PATCH 94/95] Add back eyre --- Cargo.lock | 1 + crates/vihaco-circuit-isa/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 7e7dbfbe1..3c0f166b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1925,6 +1925,7 @@ name = "vihaco-circuit-isa" version = "0.1.0" dependencies = [ "chumsky 0.10.1", + "eyre", "smallvec", "vihaco", "vihaco-parser", diff --git a/crates/vihaco-circuit-isa/Cargo.toml b/crates/vihaco-circuit-isa/Cargo.toml index 3b7a5a6ea..6c5e01887 100644 --- a/crates/vihaco-circuit-isa/Cargo.toml +++ b/crates/vihaco-circuit-isa/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] chumsky = "0.10" +eyre = "0.6.12" smallvec = "1.15.1" vihaco = "0.1.1" vihaco-parser = "0.1.1" From a12c623c932bf1a511f838d54e564e7f32e6f679 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 1 Jul 2026 15:30:31 +0200 Subject: [PATCH 95/95] Implement PauliSum resets --- crates/ppvm-vihaco/src/component.rs | 16 ++++-- crates/ppvm-vihaco/src/composite.rs | 87 +++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index f6147908f..ae9e70806 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -439,6 +439,8 @@ macro_rules! dispatch_common_paulisum { /// but without measurement / reset / loss support. pub struct PauliSumExecutor> { pub state: PauliSum, + /// Snapshot of the seeded observable, restored by `reset`. + initial: PauliSum, } #[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] @@ -475,9 +477,10 @@ where impl vihaco::Reset for PauliSumExecutor where T: Config, + PauliSum: Clone, { fn reset(&mut self) { - // TODO(Task 5/6): rebuild self.state from the seeded observable. + self.state = self.initial.clone(); } } @@ -487,6 +490,8 @@ where /// `PauliWordType` is `LossyPauliWord` (see `LossyPauliSumConfig`). pub struct LossyPauliSumExecutor> { pub state: PauliSum, + /// Snapshot of the seeded observable, restored by `reset`. + initial: PauliSum, } #[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] @@ -541,9 +546,10 @@ where impl vihaco::Reset for LossyPauliSumExecutor where T: Config, + PauliSum: Clone, { fn reset(&mut self) { - // TODO(Task 5/6): rebuild self.state from the seeded observable. + self.state = self.initial.clone(); } } @@ -677,7 +683,8 @@ impl PauliSumCircuit { for (word, coef) in terms { state += (word.as_str(), *coef); } - Self::$variant(PauliSumExecutor { state }) + let initial = state.clone(); + Self::$variant(PauliSumExecutor { state, initial }) }}; } if info.n_qubits <= 64 { @@ -762,7 +769,8 @@ impl LossyPauliSumCircuit { for (word, coef) in terms { state += (word.as_str(), *coef); } - Self::$variant(LossyPauliSumExecutor { state }) + let initial = state.clone(); + Self::$variant(LossyPauliSumExecutor { state, initial }) }}; } if info.n_qubits <= 64 { diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index d150e3762..e177a0fe8 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -1293,4 +1293,91 @@ mod tests { trace[0] ); } + + #[test] + fn paulisum_reset_restores_seeded_observable() -> eyre::Result<()> { + use vihaco::Reset; + + // Seed the observable `Z` (PauliSum backend), then apply H(0), which + // conjugates Z -> X in the Heisenberg picture and changes the state. + // reset() must rebuild the state from the seeded observable. + let mut module: Module = Module::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( + Value::U64(0), + ))); + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::H)); + + let mut machine = PPVM::default(); + machine.load(&module)?; + machine.init()?; + + let seeded = machine.state_string(); + for _ in 0..module.code.len() { + machine.step_once()?; + } + assert_ne!( + machine.state_string(), + seeded, + "H(0) should have changed the propagated observable" + ); + + machine.reset(); + assert_eq!( + machine.state_string(), + seeded, + "reset must rebuild the state from the seeded observable" + ); + Ok(()) + } + + #[test] + fn lossy_paulisum_reset_restores_seeded_observable() -> eyre::Result<()> { + use vihaco::Reset; + + // Same as `paulisum_reset_restores_seeded_observable`, but through the + // LossyPauliSum dispatch path. + let mut module: Module = Module::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( + Value::U64(0), + ))); + module + .code + .push(PPVMInstruction::Circuit(CircuitInstruction::H)); + + let mut machine = PPVM::default(); + machine.load(&module)?; + machine.init()?; + + let seeded = machine.state_string(); + for _ in 0..module.code.len() { + machine.step_once()?; + } + assert_ne!( + machine.state_string(), + seeded, + "H(0) should have changed the propagated observable" + ); + + machine.reset(); + assert_eq!( + machine.state_string(), + seeded, + "reset must rebuild the state from the seeded observable" + ); + Ok(()) + } }