diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000000..c97360901f6 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index fea93ace80a..1af57327300 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,19 @@ flamescope.json extra_tests/snippets/resources extra_tests/not_impl.py +Cargo.lock + + Lib/site-packages/* !Lib/site-packages/README.txt Lib/test/data/* !Lib/test/data/README + +Cargo.lock +refs/* +.gitignore +examples/breakpoint_resume_demo/demo.rpsnap +examples/breakpoint_resume_demo/actor_complex_demo.rpsnap +examples/breakpoint_resume_demo/comprehensive_demo.rpsnap +tmp/ +.DS_Store diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index ffcc1e37c17..00000000000 --- a/Cargo.lock +++ /dev/null @@ -1,4809 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloca" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" -dependencies = [ - "cc", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -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 = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" - -[[package]] -name = "ascii" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" - -[[package]] -name = "asn1-rs" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror 2.0.17", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "attribute-derive" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" -dependencies = [ - "attribute-derive-macro", - "derive-where", - "manyhow", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "attribute-derive-macro" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" -dependencies = [ - "collection_literals", - "interpolator", - "manyhow", - "proc-macro-utils", - "proc-macro2", - "quote", - "quote-use", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-lc-fips-sys" -version = "0.13.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57900537c00a0565a35b63c4c281b372edfc9744b072fd4a3b414350a8f5ed48" -dependencies = [ - "bindgen 0.72.1", - "cc", - "cmake", - "dunce", - "fs_extra", - "regex", -] - -[[package]] -name = "aws-lc-rs" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" -dependencies = [ - "aws-lc-fips-sys", - "aws-lc-sys", - "untrusted 0.7.1", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - -[[package]] -name = "bindgen" -version = "0.71.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[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 = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" -dependencies = [ - "allocator-api2", -] - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" - -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - -[[package]] -name = "caseless" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" -dependencies = [ - "unicode-normalization", -] - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - -[[package]] -name = "cc" -version = "1.2.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -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 = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading 0.8.9", -] - -[[package]] -name = "clap" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" -dependencies = [ - "clap_builder", -] - -[[package]] -name = "clap_builder" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" -dependencies = [ - "anstyle", - "clap_lex", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[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 = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - -[[package]] -name = "collection_literals" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "compact_str" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "windows-sys 0.59.0", -] - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cranelift" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68971376deb1edf5e9c0ac77ef00479d740ce7a60e6181adb0648afe1dc7b8f4" -dependencies = [ - "cranelift-codegen", - "cranelift-frontend", - "cranelift-module", -] - -[[package]] -name = "cranelift-assembler-x64" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30054f4aef4d614d37f27d5b77e36e165f0b27a71563be348e7c9fcfac41eed8" -dependencies = [ - "cranelift-assembler-x64-meta", -] - -[[package]] -name = "cranelift-assembler-x64-meta" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beab56413879d4f515e08bcf118b1cb85f294129bb117057f573d37bfbb925a" -dependencies = [ - "cranelift-srcgen", -] - -[[package]] -name = "cranelift-bforest" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d054747549a69b264d5299c8ca1b0dd45dc6bd0ee43f1edfcc42a8b12952c7a" -dependencies = [ - "cranelift-entity", -] - -[[package]] -name = "cranelift-bitset" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98b92d481b77a7dc9d07c96e24a16f29e0c9c27d042828fdf7e49e54ee9819bf" - -[[package]] -name = "cranelift-codegen" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eeccfc043d599b0ef1806942707fc51cdd1c3965c343956dc975a55d82a920f" -dependencies = [ - "bumpalo", - "cranelift-assembler-x64", - "cranelift-bforest", - "cranelift-bitset", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-control", - "cranelift-entity", - "cranelift-isle", - "gimli", - "hashbrown 0.15.5", - "log", - "regalloc2", - "rustc-hash", - "serde", - "smallvec", - "target-lexicon", - "wasmtime-internal-math", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1174cdb9d9d43b2bdaa612a07ed82af13db9b95526bc2c286c2aec4689bcc038" -dependencies = [ - "cranelift-assembler-x64-meta", - "cranelift-codegen-shared", - "cranelift-srcgen", - "heck", -] - -[[package]] -name = "cranelift-codegen-shared" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d572be73fae802eb115f45e7e67a9ed16acb4ee683b67c4086768786545419a" - -[[package]] -name = "cranelift-control" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1587465cc84c5cc793b44add928771945f3132bbf6b3621ee9473c631a87156" -dependencies = [ - "arbitrary", -] - -[[package]] -name = "cranelift-entity" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063b83448b1343e79282c3c7cbda7ed5f0816f0b763a4c15f7cecb0a17d87ea6" -dependencies = [ - "cranelift-bitset", -] - -[[package]] -name = "cranelift-frontend" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4461c2d2ca48bc72883f5f5c3129d9aefac832df1db824af9db8db3efee109" -dependencies = [ - "cranelift-codegen", - "log", - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cranelift-isle" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd811b25e18f14810d09c504e06098acc1d9dbfa24879bf0d6b6fb44415fc66" - -[[package]] -name = "cranelift-jit" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01527663ba63c10509d7c87fd1f8495d21170ba35bf714f57271495689d8fde5" -dependencies = [ - "anyhow", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-module", - "cranelift-native", - "libc", - "log", - "region", - "target-lexicon", - "wasmtime-internal-jit-icache-coherence", - "windows-sys 0.60.2", -] - -[[package]] -name = "cranelift-module" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72328edb49aeafb1655818c91c476623970cb7b8a89ffbdadd82ce7d13dedc1d" -dependencies = [ - "anyhow", - "cranelift-codegen", - "cranelift-control", -] - -[[package]] -name = "cranelift-native" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2417046989d8d6367a55bbab2e406a9195d176f4779be4aa484d645887217d37" -dependencies = [ - "cranelift-codegen", - "libc", - "target-lexicon", -] - -[[package]] -name = "cranelift-srcgen" -version = "0.126.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d039de901c8d928222b8128e1b9a9ab27b82a7445cb749a871c75d9cb25c57d" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "criterion" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf" -dependencies = [ - "alloca", - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "itertools 0.13.0", - "num-traits", - "oorandom", - "page_size", - "plotters", - "rayon", - "regex", - "serde", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4" -dependencies = [ - "cast", - "itertools 0.13.0", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -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 = "csv-core" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" -dependencies = [ - "memchr", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "der_derive", - "flagset", - "pem-rfc7468 0.7.0", - "zeroize", -] - -[[package]] -name = "der-parser" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", -] - -[[package]] -name = "der_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive-where" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dns-lookup" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e39034cee21a2f5bbb66ba0e3689819c4bb5d00382a282006e802a7ffa6c41d" -dependencies = [ - "cfg-if", - "libc", - "socket2", - "windows-sys 0.60.2", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_home" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -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 = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "error-code" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" - -[[package]] -name = "exitcode" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fd-lock" -version = "4.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" -dependencies = [ - "cfg-if", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" - -[[package]] -name = "flagset" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" - -[[package]] -name = "flame" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc2706461e1ee94f55cab2ed2e3d34ae9536cfa830358ef80acff1a3dacab30" -dependencies = [ - "lazy_static 0.2.11", - "serde", - "serde_derive", - "serde_json", - "thread-id", -] - -[[package]] -name = "flamer" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7693d9dd1ec1c54f52195dfe255b627f7cec7da33b679cd56de949e662b3db10" -dependencies = [ - "flame", - "quote", - "syn", -] - -[[package]] -name = "flamescope" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168cbad48fdda10be94de9c6319f9e8ac5d3cf0a1abda1864269dfcca3d302a" -dependencies = [ - "flame", - "indexmap", - "serde", - "serde_json", -] - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "libz-rs-sys", - "miniz_oxide", -] - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[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 = "get-size-derive2" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff47daa61505c85af126e9dd64af6a342a33dc0cccfe1be74ceadc7d352e6efd" -dependencies = [ - "attribute-derive", - "quote", - "syn", -] - -[[package]] -name = "get-size2" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af" -dependencies = [ - "compact_str", - "get-size-derive2", - "hashbrown 0.16.1", - "smallvec", -] - -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix", - "windows-link", -] - -[[package]] -name = "getopts" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" -dependencies = [ - "fallible-iterator", - "indexmap", - "stable_deref_trait", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "block-padding", - "generic-array", -] - -[[package]] -name = "insta" -version = "1.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b76866be74d68b1595eb8060cb9191dca9c021db2316558e52ddc5d55d41b66c" -dependencies = [ - "console", - "once_cell", - "similar", - "tempfile", -] - -[[package]] -name = "interpolator" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" - -[[package]] -name = "is-macro" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jiff" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "junction" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52f6e1bf39a7894f618c9d378904a11dbd7e10fe3ec20d1173600e79b1408d8" -dependencies = [ - "scopeguard", - "windows-sys 0.60.2", -] - -[[package]] -name = "keccak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "lazy_static" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lexical-parse-float" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" -dependencies = [ - "lexical-parse-integer", - "lexical-util", -] - -[[package]] -name = "lexical-parse-integer" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" -dependencies = [ - "lexical-util", -] - -[[package]] -name = "lexical-util" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" - -[[package]] -name = "lexopt" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa0e2a1fcbe2f6be6c42e342259976206b383122fc152e872795338b5a3f3a7" - -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - -[[package]] -name = "libc" -version = "0.2.178" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" - -[[package]] -name = "libffi" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0444124f3ffd67e1b0b0c661a7f81a278a135eb54aaad4078e79fbc8be50c8a5" -dependencies = [ - "libc", - "libffi-sys", -] - -[[package]] -name = "libffi-sys" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d722da8817ea580d0669da6babe2262d7b86a1af1103da24102b8bb9c101ce7" -dependencies = [ - "cc", -] - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libloading" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "libredox" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" -dependencies = [ - "bitflags 2.10.0", - "libc", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libz-rs-sys" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" -dependencies = [ - "zlib-rs", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lz4_flex" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6473172471198271ff72e9379150e9dfd70d8e533e0752a27e515b48dd375e" -dependencies = [ - "twox-hash", -] - -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "mac_address" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" -dependencies = [ - "nix 0.29.0", - "winapi", -] - -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - -[[package]] -name = "malachite-base" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0c91cb6071ed9ac48669d3c79bd2792db596c7e542dbadd217b385bb359f42d" -dependencies = [ - "hashbrown 0.16.1", - "itertools 0.14.0", - "libm", - "ryu", -] - -[[package]] -name = "malachite-bigint" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff3af5010102f29f2ef4ee6f7b1c5b3f08a6c261b5164e01c41cf43772b6f90" -dependencies = [ - "malachite-base", - "malachite-nz", - "num-integer", - "num-traits", - "paste", -] - -[[package]] -name = "malachite-nz" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9ecf4dd76246fd622de4811097966106aa43f9cd7cc36cb85e774fe84c8adc" -dependencies = [ - "itertools 0.14.0", - "libm", - "malachite-base", - "wide", -] - -[[package]] -name = "malachite-q" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7bc9d9adf5b0a7999d84f761c809bec3dc46fe983e4de547725d2b7730462a0" -dependencies = [ - "itertools 0.14.0", - "malachite-base", - "malachite-nz", -] - -[[package]] -name = "manyhow" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" -dependencies = [ - "manyhow-macros", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "manyhow-macros" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" -dependencies = [ - "proc-macro-utils", - "proc-macro2", - "quote", -] - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "memmap2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mt19937" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7151a832e54d2d6b2c827a20e5bcdd80359281cd2c354e725d4b82e7c471de" -dependencies = [ - "rand_core 0.9.3", -] - -[[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.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "oid-registry" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" -dependencies = [ - "asn1-rs", -] - -[[package]] -name = "once_cell" -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 = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-src" -version = "300.5.4+3.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" -dependencies = [ - "cc", -] - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "optional" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978aa494585d3ca4ad74929863093e87cac9790d81fe7aba2b3dc2890643a0fc" - -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "pem-rfc7468" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" -dependencies = [ - "base64ct", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared 0.13.1", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pkcs5" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" -dependencies = [ - "aes", - "cbc", - "der", - "pbkdf2", - "scrypt", - "sha2", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "pkcs5", - "rand_core 0.6.4", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - -[[package]] -name = "pmutil" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro-utils" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" -dependencies = [ - "proc-macro2", - "quote", - "smallvec", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "pymath" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b66ab66a8610ce209d8b36cd0fecc3a15c494f715e0cb26f0586057f293abc9" -dependencies = [ - "libc", -] - -[[package]] -name = "pyo3" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" -dependencies = [ - "indoc", - "libc", - "memoffset", - "once_cell", - "portable-atomic", - "pyo3-build-config", - "pyo3-ffi", - "pyo3-macros", - "unindent", -] - -[[package]] -name = "pyo3-build-config" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" -dependencies = [ - "target-lexicon", -] - -[[package]] -name = "pyo3-ffi" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" -dependencies = [ - "libc", - "pyo3-build-config", -] - -[[package]] -name = "pyo3-macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" -dependencies = [ - "proc-macro2", - "pyo3-macros-backend", - "quote", - "syn", -] - -[[package]] -name = "pyo3-macros-backend" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" -dependencies = [ - "heck", - "proc-macro2", - "pyo3-build-config", - "quote", - "syn", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "quote-use" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" -dependencies = [ - "quote", - "quote-use-macros", -] - -[[package]] -name = "quote-use-macros" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" -dependencies = [ - "proc-macro-utils", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "radium" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1775bc532a9bfde46e26eba441ca1171b91608d14a3bae71fea371f18a00cffe" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "regalloc2" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e249c660440317032a71ddac302f25f1d5dff387667bcc3978d1f77aa31ac34" -dependencies = [ - "allocator-api2", - "bumpalo", - "hashbrown 0.15.5", - "log", - "rustc-hash", - "smallvec", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "region" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" -dependencies = [ - "bitflags 1.3.2", - "libc", - "mach2", - "windows-sys 0.52.0", -] - -[[package]] -name = "result-like" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffa194499266bd8a1ac7da6ac7355aa0f81ffa1a5db2baaf20dd13854fd6f4e" -dependencies = [ - "result-like-derive", -] - -[[package]] -name = "result-like-derive" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d3b03471c9700a3a6bd166550daaa6124cb4a146ea139fb028e4edaa8f4277" -dependencies = [ - "pmutil", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted 0.9.0", - "windows-sys 0.52.0", -] - -[[package]] -name = "ruff_python_ast" -version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=2bffef59665ce7d2630dfd72ee99846663660db8#2bffef59665ce7d2630dfd72ee99846663660db8" -dependencies = [ - "aho-corasick", - "bitflags 2.10.0", - "compact_str", - "get-size2", - "is-macro", - "itertools 0.14.0", - "memchr", - "ruff_python_trivia", - "ruff_source_file", - "ruff_text_size", - "rustc-hash", - "thiserror 2.0.17", -] - -[[package]] -name = "ruff_python_parser" -version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=2bffef59665ce7d2630dfd72ee99846663660db8#2bffef59665ce7d2630dfd72ee99846663660db8" -dependencies = [ - "bitflags 2.10.0", - "bstr", - "compact_str", - "get-size2", - "memchr", - "ruff_python_ast", - "ruff_python_trivia", - "ruff_text_size", - "rustc-hash", - "static_assertions", - "unicode-ident", - "unicode-normalization", - "unicode_names2 1.3.0", -] - -[[package]] -name = "ruff_python_trivia" -version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=2bffef59665ce7d2630dfd72ee99846663660db8#2bffef59665ce7d2630dfd72ee99846663660db8" -dependencies = [ - "itertools 0.14.0", - "ruff_source_file", - "ruff_text_size", - "unicode-ident", -] - -[[package]] -name = "ruff_source_file" -version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=2bffef59665ce7d2630dfd72ee99846663660db8#2bffef59665ce7d2630dfd72ee99846663660db8" -dependencies = [ - "memchr", - "ruff_text_size", -] - -[[package]] -name = "ruff_text_size" -version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?rev=2bffef59665ce7d2630dfd72ee99846663660db8#2bffef59665ce7d2630dfd72ee99846663660db8" -dependencies = [ - "get-size2", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", -] - -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" -dependencies = [ - "aws-lc-rs", - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted 0.9.0", -] - -[[package]] -name = "rustpython" -version = "0.4.0" -dependencies = [ - "cfg-if", - "criterion", - "dirs-next", - "env_logger", - "flame", - "flamescope", - "lexopt", - "libc", - "log", - "pyo3", - "ruff_python_parser", - "rustpython-compiler", - "rustpython-pylib", - "rustpython-stdlib", - "rustpython-vm", - "rustyline", - "winresource", -] - -[[package]] -name = "rustpython-codegen" -version = "0.4.0" -dependencies = [ - "ahash", - "bitflags 2.10.0", - "indexmap", - "insta", - "itertools 0.14.0", - "log", - "malachite-bigint", - "memchr", - "num-complex", - "num-traits", - "ruff_python_ast", - "ruff_python_parser", - "ruff_text_size", - "rustpython-compiler-core", - "rustpython-literal", - "rustpython-wtf8", - "thiserror 2.0.17", - "unicode_names2 2.0.0", -] - -[[package]] -name = "rustpython-common" -version = "0.4.0" -dependencies = [ - "ascii", - "bitflags 2.10.0", - "cfg-if", - "getrandom 0.3.4", - "itertools 0.14.0", - "libc", - "lock_api", - "malachite-base", - "malachite-bigint", - "malachite-q", - "nix 0.30.1", - "num-complex", - "num-traits", - "once_cell", - "parking_lot", - "radium", - "rustpython-literal", - "rustpython-wtf8", - "siphasher", - "unicode_names2 2.0.0", - "widestring", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustpython-compiler" -version = "0.4.0" -dependencies = [ - "ruff_python_ast", - "ruff_python_parser", - "ruff_source_file", - "ruff_text_size", - "rustpython-codegen", - "rustpython-compiler-core", - "thiserror 2.0.17", -] - -[[package]] -name = "rustpython-compiler-core" -version = "0.4.0" -dependencies = [ - "bitflags 2.10.0", - "itertools 0.14.0", - "lz4_flex", - "malachite-bigint", - "num-complex", - "ruff_source_file", - "rustpython-wtf8", -] - -[[package]] -name = "rustpython-compiler-source" -version = "0.5.0+deprecated" -dependencies = [ - "ruff_source_file", - "ruff_text_size", -] - -[[package]] -name = "rustpython-derive" -version = "0.4.0" -dependencies = [ - "rustpython-compiler", - "rustpython-derive-impl", - "syn", -] - -[[package]] -name = "rustpython-derive-impl" -version = "0.4.0" -dependencies = [ - "itertools 0.14.0", - "maplit", - "proc-macro2", - "quote", - "rustpython-compiler-core", - "rustpython-doc", - "syn", - "syn-ext", - "textwrap", -] - -[[package]] -name = "rustpython-doc" -version = "0.4.0" -dependencies = [ - "phf 0.13.1", -] - -[[package]] -name = "rustpython-jit" -version = "0.4.0" -dependencies = [ - "approx", - "cranelift", - "cranelift-jit", - "cranelift-module", - "libffi", - "num-traits", - "rustpython-compiler-core", - "rustpython-derive", - "thiserror 2.0.17", -] - -[[package]] -name = "rustpython-literal" -version = "0.4.0" -dependencies = [ - "hexf-parse", - "is-macro", - "lexical-parse-float", - "num-traits", - "rand 0.9.2", - "rustpython-wtf8", - "unic-ucd-category", -] - -[[package]] -name = "rustpython-pylib" -version = "0.4.0" -dependencies = [ - "glob", - "rustpython-compiler-core", - "rustpython-derive", -] - -[[package]] -name = "rustpython-sre_engine" -version = "0.4.0" -dependencies = [ - "bitflags 2.10.0", - "criterion", - "num_enum", - "optional", - "rustpython-wtf8", -] - -[[package]] -name = "rustpython-stdlib" -version = "0.4.0" -dependencies = [ - "adler32", - "ahash", - "ascii", - "aws-lc-rs", - "base64", - "blake2", - "bzip2", - "cfg-if", - "chrono", - "crc32fast", - "crossbeam-utils", - "csv-core", - "der", - "digest", - "dns-lookup", - "dyn-clone", - "flate2", - "foreign-types-shared", - "gethostname", - "hex", - "indexmap", - "itertools 0.14.0", - "libc", - "libsqlite3-sys", - "libz-rs-sys", - "lzma-sys", - "mac_address", - "malachite-bigint", - "md-5", - "memchr", - "memmap2", - "mt19937", - "nix 0.30.1", - "num-complex", - "num-integer", - "num-traits", - "num_enum", - "oid-registry", - "openssl", - "openssl-probe", - "openssl-sys", - "page_size", - "parking_lot", - "paste", - "pem-rfc7468 1.0.0", - "phf 0.13.1", - "pkcs8", - "pymath", - "rand_core 0.9.3", - "rustix", - "rustls", - "rustls-native-certs", - "rustls-pemfile", - "rustls-platform-verifier", - "rustpython-common", - "rustpython-derive", - "rustpython-vm", - "schannel", - "sha-1", - "sha2", - "sha3", - "socket2", - "system-configuration", - "tcl-sys", - "termios", - "tk-sys", - "ucd", - "unic-char-property", - "unic-normal", - "unic-ucd-age", - "unic-ucd-bidi", - "unic-ucd-category", - "unic-ucd-ident", - "unicode-bidi-mirroring", - "unicode-casing", - "unicode_names2 2.0.0", - "uuid", - "webpki-roots", - "widestring", - "windows-sys 0.61.2", - "x509-cert", - "x509-parser", - "xml", - "xz2", -] - -[[package]] -name = "rustpython-venvlauncher" -version = "0.4.0" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "rustpython-vm" -version = "0.4.0" -dependencies = [ - "ahash", - "ascii", - "bitflags 2.10.0", - "bstr", - "caseless", - "cfg-if", - "chrono", - "constant_time_eq", - "crossbeam-utils", - "errno", - "exitcode", - "flame", - "flamer", - "getrandom 0.3.4", - "glob", - "half", - "hex", - "indexmap", - "is-macro", - "itertools 0.14.0", - "junction", - "libc", - "libffi", - "libloading 0.9.0", - "log", - "malachite-bigint", - "memchr", - "nix 0.30.1", - "num-complex", - "num-integer", - "num-traits", - "num_cpus", - "num_enum", - "once_cell", - "optional", - "parking_lot", - "paste", - "result-like", - "ruff_python_ast", - "ruff_python_parser", - "ruff_text_size", - "rustix", - "rustpython-codegen", - "rustpython-common", - "rustpython-compiler", - "rustpython-compiler-core", - "rustpython-derive", - "rustpython-jit", - "rustpython-literal", - "rustpython-sre_engine", - "rustyline", - "scoped-tls", - "scopeguard", - "serde", - "static_assertions", - "strum", - "strum_macros", - "thiserror 2.0.17", - "thread_local", - "timsort", - "uname", - "unic-ucd-bidi", - "unic-ucd-category", - "unic-ucd-ident", - "unicode-casing", - "wasm-bindgen", - "which", - "widestring", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustpython-wtf8" -version = "0.4.0" -dependencies = [ - "ascii", - "bstr", - "itertools 0.14.0", - "memchr", -] - -[[package]] -name = "rustpython_wasm" -version = "0.4.0" -dependencies = [ - "console_error_panic_hook", - "js-sys", - "ruff_python_parser", - "rustpython-common", - "rustpython-pylib", - "rustpython-stdlib", - "rustpython-vm", - "serde", - "serde-wasm-bindgen", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rustyline" -version = "17.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "clipboard-win", - "fd-lock", - "home", - "libc", - "log", - "memchr", - "nix 0.30.1", - "radix_trie", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "windows-sys 0.60.2", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "safe_arch" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629516c85c29fe757770fa03f2074cf1eac43d44c02a3de9fc2ef7b0e207dfdd" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "pbkdf2", - "salsa20", - "sha2", -] - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-wasm-bindgen" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "serde_spanned" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" -dependencies = [ - "serde_core", -] - -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha3" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" -dependencies = [ - "digest", - "keccak", -] - -[[package]] -name = "shared-build" -version = "0.2.0" -source = "git+https://github.com/arihant2math/tkinter.git?tag=v0.2.0#198fc35b1f18f4eda401f97a641908f321b1403a" -dependencies = [ - "bindgen 0.71.1", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "rand_core 0.6.4", -] - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn-ext" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b126de4ef6c2a628a68609dd00733766c3b015894698a438ebdf374933fc31d1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - -[[package]] -name = "tcl-sys" -version = "0.2.0" -source = "git+https://github.com/arihant2math/tkinter.git?tag=v0.2.0#198fc35b1f18f4eda401f97a641908f321b1403a" -dependencies = [ - "pkg-config", - "shared-build", -] - -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "termios" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" -dependencies = [ - "libc", -] - -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread-id" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" -dependencies = [ - "libc", - "redox_syscall 0.1.57", - "winapi", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "timsort" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "639ce8ef6d2ba56be0383a94dd13b92138d58de44c62618303bb798fa92bdc00" - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tk-sys" -version = "0.2.0" -source = "git+https://github.com/arihant2math/tkinter.git?tag=v0.2.0#198fc35b1f18f4eda401f97a641908f321b1403a" -dependencies = [ - "pkg-config", - "shared-build", -] - -[[package]] -name = "tls_codec" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" -dependencies = [ - "tls_codec_derive", - "zeroize", -] - -[[package]] -name = "tls_codec_derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "toml" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" - -[[package]] -name = "twox-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ucd" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4fa6e588762366f1eb4991ce59ad1b93651d0b769dfb4e4d1c5c4b943d1159" - -[[package]] -name = "uname" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" -dependencies = [ - "libc", -] - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-normal" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09d64d33589a94628bc2aeb037f35c2e25f3f049c7348b5aa5580b48e6bba62" -dependencies = [ - "unic-ucd-normal", -] - -[[package]] -name = "unic-ucd-age" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8cfdfe71af46b871dc6af2c24fcd360e2f3392ee4c5111877f2947f311671c" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-bidi" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1d568b51222484e1f8209ce48caa6b430bf352962b877d592c29ab31fb53d8c" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-category" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8d4591f5fcfe1bd4453baaf803c40e1b1e69ff8455c47620440b46efef91c0" -dependencies = [ - "matches", - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-hangul" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1dc690e19010e1523edb9713224cba5ef55b54894fe33424439ec9a40c0054" -dependencies = [ - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-normal" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86aed873b8202d22b13859dda5fe7c001d271412c31d411fd9b827e030569410" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-hangul", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-bidi-mirroring" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" - -[[package]] -name = "unicode-casing" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061dbb8cc7f108532b6087a0065eff575e892a4bcb503dc57323a197457cc202" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode_names2" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd" -dependencies = [ - "phf 0.11.3", - "unicode_names2_generator 1.3.0", -] - -[[package]] -name = "unicode_names2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d189085656ca1203291e965444e7f6a2723fbdd1dd9f34f8482e79bafd8338a0" -dependencies = [ - "phf 0.11.3", - "unicode_names2_generator 2.0.0", -] - -[[package]] -name = "unicode_names2_generator" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91e5b84611016120197efd7dc93ef76774f4e084cd73c9fb3ea4a86c570c56e" -dependencies = [ - "getopts", - "log", - "phf_codegen", - "rand 0.8.5", -] - -[[package]] -name = "unicode_names2_generator" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1262662dc96937c71115228ce2e1d30f41db71a7a45d3459e98783ef94052214" -dependencies = [ - "phf_codegen", - "rand 0.8.5", -] - -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" -dependencies = [ - "atomic", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasmtime-internal-jit-icache-coherence" -version = "39.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ccd36e25390258ce6720add639ffe5a7d81a5c904350aa08f5bbc60433d22" -dependencies = [ - "anyhow", - "cfg-if", - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "wasmtime-internal-math" -version = "39.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1b856e1bbf0230ab560ba4204e944b141971adc4e6cdf3feb6979c1a7b7953" -dependencies = [ - "libm", -] - -[[package]] -name = "web-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "webpki-roots" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "which" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" -dependencies = [ - "env_home", - "rustix", - "winsafe", -] - -[[package]] -name = "wide" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13ca908d26e4786149c48efcf6c0ea09ab0e06d1fe3c17dc1b4b0f1ca4a7e788" -dependencies = [ - "bytemuck", - "safe_arch", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[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.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" - -[[package]] -name = "winresource" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b021990998587d4438bb672b5c5f034cbc927f51b45e3807ab7323645ef4899" -dependencies = [ - "toml", - "version_check", -] - -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "x509-cert" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" -dependencies = [ - "const-oid", - "der", - "sha1", - "signature", - "spki", - "tls_codec", -] - -[[package]] -name = "x509-parser" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static 1.5.0", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror 2.0.17", - "time", -] - -[[package]] -name = "xml" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df5825faced2427b2da74d9100f1e2e93c533fff063506a81ede1cf517b2e7e" - -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - -[[package]] -name = "zerocopy" -version = "0.8.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zlib-rs" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36134c44663532e6519d7a6dfdbbe06f6f8192bde8ae9ed076e9b213f0e31df7" diff --git a/Cargo.toml b/Cargo.toml index 44f9d3190f7..502204b70bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,17 +54,20 @@ rustyline = { workspace = true } [dev-dependencies] criterion = { workspace = true } pyo3 = { version = "0.27", features = ["auto-initialize"] } +pvm-alto = { path = "crates/pvm-alto" } +pvm-host = { path = "crates/pvm-host" } +pvm-runtime = { path = "crates/pvm-runtime" } -[[bench]] -name = "execution" -harness = false +# [[bench]] +# name = "execution" +# harness = false -[[bench]] -name = "microbenchmarks" -harness = false +# [[bench]] +# name = "microbenchmarks" +# harness = false [[bin]] -name = "rustpython" +name = "pvm" path = "src/main.rs" [profile.dev.package."*"] @@ -119,6 +122,10 @@ template = "installer-config/installer.nsi" [package.metadata.packager.wix] template = "installer-config/installer.wxs" +# PVM Current Version +[package.metadata.pvm] +version = "0.1.3" + [workspace] resolver = "2" diff --git a/Lib/pvm_sdk/__init__.py b/Lib/pvm_sdk/__init__.py new file mode 100644 index 00000000000..ef5b7f04168 --- /dev/null +++ b/Lib/pvm_sdk/__init__.py @@ -0,0 +1,24 @@ +from . import pvm_random +from . import pvm_sys +from . import pvm_time +from . import runtime +from . import continuation +from . import runner +from . import actor +from . import verify +from . import types + +capture = continuation.capture + +__all__ = [ + "pvm_random", + "pvm_sys", + "pvm_time", + "runtime", + "continuation", + "runner", + "actor", + "verify", + "types", + "capture", +] diff --git a/Lib/pvm_sdk/actor.py b/Lib/pvm_sdk/actor.py new file mode 100644 index 00000000000..9f43c069a98 --- /dev/null +++ b/Lib/pvm_sdk/actor.py @@ -0,0 +1,28 @@ +from . import runtime + + +def continuation(*_args, **_kwargs): + def decorator(func): + return func + return decorator + + +class ActorRef: + def __init__(self, address): + self.address = address + + def async_call(self, method, *args, **kwargs): + if runtime.mode() != "checkpoint": + raise RuntimeError("actor async is only supported in checkpoint mode without FSM") + return _ActorAwaitable(self.address, method, *args, **kwargs) + + +class _ActorAwaitable: + def __init__(self, address, method, *args, **kwargs): + self.address = address + self.method = method + self.args = args + self.kwargs = kwargs + + def __await__(self): + raise RuntimeError("actor await not implemented") diff --git a/Lib/pvm_sdk/continuation.py b/Lib/pvm_sdk/continuation.py new file mode 100644 index 00000000000..1d04f60b8ae --- /dev/null +++ b/Lib/pvm_sdk/continuation.py @@ -0,0 +1,158 @@ +import hashlib +import json + +import pvm_host + + +def _encode_value(value): + if isinstance(value, bytes): + return {"__bytes__": value.hex()} + if isinstance(value, bytearray): + return {"__bytes__": bytes(value).hex()} + if isinstance(value, dict): + return {str(k): _encode_value(v) for k, v in value.items()} + if isinstance(value, list): + return [_encode_value(v) for v in value] + if value is None or isinstance(value, (bool, int, str)): + return value + raise TypeError("unsupported capture value type") + + +def _decode_value(value): + if isinstance(value, dict) and "__bytes__" in value: + return bytes.fromhex(value["__bytes__"]) + if isinstance(value, dict): + return {k: _decode_value(v) for k, v in value.items()} + if isinstance(value, list): + return [_decode_value(v) for v in value] + return value + + +def _encode_json(value): + try: + return json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + ).encode("ascii") + except AttributeError as exc: + if "check_circular" not in str(exc): + raise + try: + import importlib + import json as _json + importlib.reload(_json) + return _json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + ).encode("ascii") + except Exception: + raise exc + + +def _decode_json(data): + try: + return json.loads(data.decode("utf-8")) + except TypeError as exc: + if "Pattern" not in str(exc): + raise + try: + import importlib + import re as _re + import json as _json + importlib.reload(_re) + importlib.reload(_json) + return _json.loads(data.decode("utf-8")) + except Exception: + raise exc + + +class Capture: + def __init__(self): + object.__setattr__(self, "_data", {}) + + def __getattr__(self, name): + data = object.__getattribute__(self, "_data") + if name in data: + return data[name] + raise AttributeError(name) + + def __setattr__(self, name, value): + data = object.__getattribute__(self, "_data") + data[name] = value + + def to_dict(self): + return dict(self._data) + + @classmethod + def from_dict(cls, value): + inst = cls() + for k, v in value.items(): + inst._data[k] = v + return inst + + +def capture(): + return Capture() + + +def new_cid(self_obj, name): + ctx = pvm_host.context() + seed = b"" + tx_hash = ctx.get("tx_hash") + if isinstance(tx_hash, (bytes, bytearray)): + seed += bytes(tx_hash) + sender = ctx.get("sender") + if isinstance(sender, (bytes, bytearray)): + seed += bytes(sender) + seed += str(name).encode("utf-8") + return hashlib.sha256(seed).digest() + + +def _cont_key(cid): + return b"__continuation:" + cid + + +def save_cont(cid, state, ctx, handler, timeout_blocks=0, guard_unchanged=None): + if isinstance(ctx, Capture): + ctx_dict = ctx.to_dict() + else: + ctx_dict = dict(ctx) + guard_value = guard_unchanged + if isinstance(guard_value, Capture): + guard_value = guard_value.to_dict() + payload = { + "state": int(state), + "ctx": _encode_value(ctx_dict), + "handler": str(handler), + "timeout_blocks": int(timeout_blocks), + "guard_unchanged": _encode_value(guard_value), + } + pvm_host.set_state(_cont_key(cid), _encode_json(payload)) + + +def load_cont(cid): + raw = pvm_host.get_state(_cont_key(cid)) + if raw is None: + raise RuntimeError("continuation state missing") + data = _decode_json(raw) + ctx = _decode_value(data.get("ctx") or {}) + data["ctx"] = Capture.from_dict(ctx) + if "guard_unchanged" in data: + data["guard_unchanged"] = _decode_value(data["guard_unchanged"]) + return data + + +def delete_cont(cid): + pvm_host.delete_state(_cont_key(cid)) + + +def encode_payload(value): + return _encode_json(_encode_value(value)) + + +def decode_payload(data): + return _decode_value(_decode_json(data)) diff --git a/Lib/pvm_sdk/pvm_random.py b/Lib/pvm_sdk/pvm_random.py new file mode 100644 index 00000000000..b8cbde1e509 --- /dev/null +++ b/Lib/pvm_sdk/pvm_random.py @@ -0,0 +1,195 @@ +import pvm_host + +_seed_prefix = b"N" +_counter = 0 +_buffer = b"" +_buffer_pos = 0 + + +def _next_block(): + global _counter + domain = b"random" + _seed_prefix + _counter.to_bytes(8, "little") + _counter += 1 + return pvm_host.randomness(domain) + + +def _reset_state(): + global _counter, _buffer, _buffer_pos + _counter = 0 + _buffer = b"" + _buffer_pos = 0 + + +def _coerce_seed(a): + if a is None: + return b"N" + if isinstance(a, (bytes, bytearray)): + return b"B" + bytes(a) + if isinstance(a, str): + return b"S" + a.encode("utf-8") + if isinstance(a, int): + if a == 0: + data = b"\x00" + else: + bits = a.bit_length() + if a < 0: + bits += 1 + data = a.to_bytes((bits + 7) // 8, "big", signed=True) + return b"I" + data + raise TypeError("seed must be int, bytes, bytearray, str, or None") + + +def seed(a=None, version=2): + global _seed_prefix + _seed_prefix = _coerce_seed(a) + _reset_state() + + +def getstate(): + return (1, _seed_prefix, _counter, _buffer, _buffer_pos) + + +def setstate(state): + if not isinstance(state, tuple) or len(state) != 5: + raise ValueError("state must be a 5-item tuple from getstate()") + version, seed_prefix, counter, buffer_data, buffer_pos = state + if version != 1: + raise ValueError("unsupported state version") + if isinstance(seed_prefix, bytearray): + seed_prefix = bytes(seed_prefix) + if not isinstance(seed_prefix, (bytes, bytearray)): + raise TypeError("seed_prefix must be bytes") + if not isinstance(counter, int): + raise TypeError("counter must be int") + if isinstance(buffer_data, bytearray): + buffer_data = bytes(buffer_data) + if not isinstance(buffer_data, (bytes, bytearray)): + raise TypeError("buffer must be bytes") + if not isinstance(buffer_pos, int): + raise TypeError("buffer_pos must be int") + if buffer_pos < 0 or buffer_pos > len(buffer_data): + raise ValueError("buffer_pos out of range") + global _seed_prefix, _counter, _buffer, _buffer_pos + _seed_prefix = bytes(seed_prefix) + _counter = counter + _buffer = bytes(buffer_data) + _buffer_pos = buffer_pos + + +def _compact_buffer(): + global _buffer, _buffer_pos + if _buffer_pos <= 0: + return + if _buffer_pos >= len(_buffer): + _buffer = b"" + _buffer_pos = 0 + return + _buffer = _buffer[_buffer_pos :] + _buffer_pos = 0 + + +def _fill(n): + global _buffer, _buffer_pos + if _buffer_pos > 0: + _compact_buffer() + needed = n - (len(_buffer) - _buffer_pos) + while needed > 0: + _buffer += _next_block() + needed = n - (len(_buffer) - _buffer_pos) + + +def _randbytes(n): + global _buffer_pos + if n <= 0: + return b"" + _fill(n) + start = _buffer_pos + end = start + n + _buffer_pos = end + return _buffer[start:end] + + +def _randbelow(n): + if n <= 0: + raise ValueError("n must be > 0") + k = n.bit_length() + while True: + r = getrandbits(k) + if r < n: + return r + + +def getrandbits(k): + if k < 0: + raise ValueError("number of bits must be non-negative") + if k == 0: + return 0 + nbytes = (k + 7) // 8 + value = int.from_bytes(_randbytes(nbytes), "big") + return value >> (nbytes * 8 - k) + + +def random(): + return getrandbits(53) / (1 << 53) + + +def randbytes(n): + return _randbytes(n) + + +def randint(a, b): + if a > b: + raise ValueError("empty range for randint()") + return randrange(a, b + 1, 1) + + +def randrange(start, stop=None, step=1): + if stop is None: + if start > 0: + return _randbelow(start) + raise ValueError("empty range for randrange()") + if step == 0: + raise ValueError("step must not be zero") + width = stop - start + if step == 1: + if width > 0: + return start + _randbelow(width) + raise ValueError("empty range for randrange()") + if step > 0: + n = (width + step - 1) // step + else: + n = (width + step + 1) // step + if n <= 0: + raise ValueError("empty range for randrange()") + return start + step * _randbelow(n) + + +def choice(seq): + if not seq: + raise IndexError("cannot choose from an empty sequence") + return seq[_randbelow(len(seq))] + + +def shuffle(x): + for i in range(len(x) - 1, 0, -1): + j = _randbelow(i + 1) + x[i], x[j] = x[j], x[i] + + +def uniform(a, b): + return a + (b - a) * random() + + +def sample(population, k): + if k < 0: + raise ValueError("sample size must be non-negative") + pool = list(population) + n = len(pool) + if k > n: + raise ValueError("sample larger than population") + result = [] + for i in range(k): + j = _randbelow(n - i) + result.append(pool[j]) + pool[j] = pool[n - i - 1] + return result diff --git a/Lib/pvm_sdk/pvm_sys.py b/Lib/pvm_sdk/pvm_sys.py new file mode 100644 index 00000000000..d63c7e8b01d --- /dev/null +++ b/Lib/pvm_sdk/pvm_sys.py @@ -0,0 +1,7 @@ +import pvm_host + +_ctx = pvm_host.context() + +chain_id = _ctx.get("chain_id") +pvm_version = _ctx.get("pvm_version") +stdlib_hash = _ctx.get("stdlib_hash") diff --git a/Lib/pvm_sdk/pvm_time.py b/Lib/pvm_sdk/pvm_time.py new file mode 100644 index 00000000000..5b2a9585a12 --- /dev/null +++ b/Lib/pvm_sdk/pvm_time.py @@ -0,0 +1,9 @@ +import pvm_host + + +def time(): + return pvm_host.context()["timestamp_ms"] / 1000.0 + + +def time_ns(): + return pvm_host.context()["timestamp_ms"] * 1_000_000 diff --git a/Lib/pvm_sdk/runner.py b/Lib/pvm_sdk/runner.py new file mode 100644 index 00000000000..b189faeea4a --- /dev/null +++ b/Lib/pvm_sdk/runner.py @@ -0,0 +1,82 @@ +import pvm_host +from . import continuation as _continuation +from . import runtime + +try: + import rustpython_checkpoint as _checkpoint +except Exception: + _checkpoint = None + + +RUNNER_ADDRESS = b"__runner__" + + +def continuation(*_args, **_kwargs): + def decorator(func): + return func + return decorator + + +def _send_job(job_type, cid, reply_handler, *args, **kwargs): + payload = { + "kind": "runner_job", + "job_type": job_type, + "payload": { + "args": list(args), + "kwargs": kwargs, + }, + "cid": cid, + "reply_handler": reply_handler, + } + pvm_host.send_message(RUNNER_ADDRESS, _continuation.encode_payload(payload)) + + +def _result_key(cid): + return b"__runner_result:" + cid + + +def _try_get_result(cid): + raw = pvm_host.get_state(_result_key(cid)) + if raw is None: + return None + pvm_host.delete_state(_result_key(cid)) + return _continuation.decode_payload(raw) + + +class _RunnerAwaitable: + def __init__(self, job_type, *args, **kwargs): + self.job_type = job_type + self.args = args + self.kwargs = kwargs + self.cid = _continuation.new_cid(None, job_type) + + def __await__(self): + if runtime.mode() != "checkpoint": + raise RuntimeError("runner await is only supported in checkpoint mode without FSM") + if False: + yield None + try_get_result = _try_get_result + send_job = _send_job + checkpoint = _checkpoint + while True: + result = try_get_result(self.cid) + if result is not None: + return result + send_job(self.job_type, self.cid, "", *self.args, **self.kwargs) + if checkpoint is None: + raise RuntimeError("checkpoint support missing") + checkpoint.checkpoint_bytes() + + +def _request_checkpoint(): + if _checkpoint is None: + raise RuntimeError("checkpoint support missing") + _checkpoint.checkpoint_bytes() + + +def llm(*args, **kwargs): + return _RunnerAwaitable("llm", *args, **kwargs) + + +def http(*args, **kwargs): + return _RunnerAwaitable("http", *args, **kwargs) diff --git a/Lib/pvm_sdk/runtime.py b/Lib/pvm_sdk/runtime.py new file mode 100644 index 00000000000..64fa425481a --- /dev/null +++ b/Lib/pvm_sdk/runtime.py @@ -0,0 +1,6 @@ +import pvm_host + + +def mode(): + cfg = pvm_host.runtime_config() + return cfg.get("continuation_mode", "fsm") diff --git a/Lib/pvm_sdk/types.py b/Lib/pvm_sdk/types.py new file mode 100644 index 00000000000..a4224b18350 --- /dev/null +++ b/Lib/pvm_sdk/types.py @@ -0,0 +1,3 @@ +class SoftFloat(str): + def __new__(cls, value): + return str.__new__(cls, str(value)) diff --git a/Lib/pvm_sdk/verify.py b/Lib/pvm_sdk/verify.py new file mode 100644 index 00000000000..784191c2c0a --- /dev/null +++ b/Lib/pvm_sdk/verify.py @@ -0,0 +1,45 @@ +class VerifyBuilder: + def __init__(self): + self._data = { + "mode": "none", + "runners": 1, + "threshold": 1, + "checks": [], + } + + def mode(self, value): + self._data["mode"] = value + return self + + def runners(self, value): + self._data["runners"] = int(value) + return self + + def threshold(self, value): + self._data["threshold"] = int(value) + return self + + def check(self, value): + self._data["checks"].append(value) + return self + + def build(self): + return dict(self._data) + + +class Verify: + @staticmethod + def builder(): + return VerifyBuilder() + + @staticmethod + def json_schema_valid(schema): + return {"kind": "json_schema_valid", "schema": schema} + + @staticmethod + def structured_match(fields): + return {"kind": "structured_match", "fields": list(fields)} + + @staticmethod + def majority_vote(field): + return {"kind": "majority_vote", "field": field} diff --git a/crates/codegen/Cargo.toml b/crates/codegen/Cargo.toml index ce7e8d74f59..52796994e20 100644 --- a/crates/codegen/Cargo.toml +++ b/crates/codegen/Cargo.toml @@ -13,6 +13,7 @@ rustpython-compiler-core = { workspace = true } rustpython-literal = {workspace = true } rustpython-wtf8 = { workspace = true } ruff_python_ast = { workspace = true } +ruff_python_parser = { workspace = true } ruff_text_size = { workspace = true } ahash = { workspace = true } @@ -28,7 +29,6 @@ memchr = { workspace = true } unicode_names2 = { workspace = true } [dev-dependencies] -ruff_python_parser = { workspace = true } insta = { workspace = true } [lints] diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 7909e924251..33ee754c2ab 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -13,6 +13,7 @@ use crate::{ IndexMap, IndexSet, ToPythonName, error::{CodegenError, CodegenErrorType, InternalError, PatternUnreachableReason}, ir::{self, BlockIdx}, + pvm_fsm, symboltable::{self, CompilerScope, SymbolFlags, SymbolScope, SymbolTable}, unparse::UnparseExpr, }; @@ -119,6 +120,8 @@ pub struct CompileOpts { /// How optimized the bytecode output should be; any optimize > 0 does /// not emit assert statements pub optimize: u8, + /// Enable PVM FSM continuation transform + pub pvm_fsm: bool, } #[derive(Debug, Clone, Copy)] @@ -170,6 +173,15 @@ pub fn compile_top( mode: Mode, opts: CompileOpts, ) -> CompileResult { + let ast = if opts.pvm_fsm { + pvm_fsm::transform_mod(ast, &source_file).map_err(|err| CodegenError { + location: None, + error: err, + source_path: source_file.name().to_owned(), + })? + } else { + ast + }; match ast { ruff_python_ast::Mod::Module(module) => match mode { Mode::Exec | Mode::Eval => compile_program(&module, source_file, opts), diff --git a/crates/codegen/src/lib.rs b/crates/codegen/src/lib.rs index 291b57d7f67..2eb17cc4427 100644 --- a/crates/codegen/src/lib.rs +++ b/crates/codegen/src/lib.rs @@ -11,6 +11,7 @@ type IndexSet = indexmap::IndexSet; pub mod compile; pub mod error; pub mod ir; +mod pvm_fsm; mod string_parser; pub mod symboltable; mod unparse; diff --git a/crates/codegen/src/pvm_fsm.rs b/crates/codegen/src/pvm_fsm.rs new file mode 100644 index 00000000000..6a9fdc354b6 --- /dev/null +++ b/crates/codegen/src/pvm_fsm.rs @@ -0,0 +1,503 @@ +use ruff_python_ast::{ + Arguments, Decorator, Expr, ExprAttribute, ExprCall, ExprName, Mod, Parameters, Stmt, + StmtAssign, StmtExpr, StmtFunctionDef, visitor::{Visitor, walk_expr, walk_stmt}, +}; +use ruff_python_parser::parse_module; +use rustpython_compiler_core::SourceFile; + +use crate::error::CodegenErrorType; +use crate::unparse::UnparseExpr; + +#[derive(Debug, Clone)] +struct ContinuationMeta { + decorator_kind: DecoratorKind, + timeout_expr: String, + guard_expr: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum DecoratorKind { + Runner, + Actor, +} + +#[derive(Debug, Clone)] +struct AwaitStep { + target_attr: String, + job_type: String, + args: String, +} + +pub(crate) fn transform_mod(ast: Mod, source_file: &SourceFile) -> Result { + match ast { + Mod::Module(mut module) => { + let mut new_body = Vec::new(); + for stmt in module.body { + new_body.extend(transform_stmt(stmt, source_file)?); + } + module.body = new_body; + Ok(Mod::Module(module)) + } + other => Ok(other), + } +} + +fn transform_stmt(stmt: Stmt, source_file: &SourceFile) -> Result, CodegenErrorType> { + match stmt { + Stmt::ClassDef(mut class_def) => { + let mut new_body = Vec::with_capacity(class_def.body.len()); + for inner in class_def.body.into_iter() { + new_body.extend(transform_stmt(inner, source_file)?); + } + class_def.body = new_body; + Ok(vec![Stmt::ClassDef(class_def)]) + } + Stmt::FunctionDef(func_def) => transform_function(func_def, source_file), + other => Ok(vec![other]), + } +} + +fn transform_function( + func_def: StmtFunctionDef, + source_file: &SourceFile, +) -> Result, CodegenErrorType> { + if !func_def.is_async { + return Ok(vec![Stmt::FunctionDef(func_def)]); + } + + let meta = continuation_meta(&func_def.decorator_list, source_file)?; + if meta.is_none() { + if contains_await(&Stmt::FunctionDef(func_def.clone())) { + return Err(CodegenErrorType::SyntaxError( + "await is only allowed inside @runner.continuation/@actor.continuation".to_owned(), + )); + } + return Ok(vec![Stmt::FunctionDef(func_def)]); + } + let meta = meta.unwrap(); + + if func_def.decorator_list.len() != 1 { + return Err(CodegenErrorType::SyntaxError( + "continuation functions must not use extra decorators".to_owned(), + )); + } + + let (self_name, msg_name, params_str) = extract_params(&func_def.parameters)?; + let (ctx_name, steps, return_expr) = parse_body(&func_def.body, source_file)?; + + if steps.is_empty() { + return Err(CodegenErrorType::SyntaxError( + "continuation function must contain at least one await".to_owned(), + )); + } + + let new_stmts = build_fsm_functions( + func_def.name.as_str(), + ¶ms_str, + self_name, + msg_name, + &ctx_name, + &steps, + &return_expr, + &meta, + )?; + + Ok(new_stmts) +} + +fn continuation_meta( + decorators: &[Decorator], + source_file: &SourceFile, +) -> Result, CodegenErrorType> { + if decorators.is_empty() { + return Ok(None); + } + let mut found = None; + for deco in decorators { + if let Some((kind, timeout_expr, guard_expr)) = parse_decorator(&deco.expression, source_file)? { + if found.is_some() { + return Err(CodegenErrorType::SyntaxError( + "multiple continuation decorators are not allowed".to_owned(), + )); + } + found = Some(ContinuationMeta { + decorator_kind: kind, + timeout_expr, + guard_expr, + }); + } else { + return Err(CodegenErrorType::SyntaxError( + "continuation functions must only use @runner.continuation/@actor.continuation" + .to_owned(), + )); + } + } + Ok(found) +} + +fn parse_decorator( + expr: &Expr, + source_file: &SourceFile, +) -> Result, CodegenErrorType> { + let (kind, call) = match expr { + Expr::Attribute(attr) => { + if attr.attr.as_str() != "continuation" { + return Ok(None); + } + let kind = decorator_base_kind(&attr.value)?; + let timeout_expr = "0".to_owned(); + let guard_expr = "None".to_owned(); + return Ok(Some((kind, timeout_expr, guard_expr))); + } + Expr::Call(call) => { + let Expr::Attribute(attr) = call.func.as_ref() else { + return Ok(None); + }; + if attr.attr.as_str() != "continuation" { + return Ok(None); + } + let kind = decorator_base_kind(&attr.value)?; + (kind, call) + } + _ => return Ok(None), + }; + + let mut timeout_expr = "0".to_owned(); + let mut guard_expr = "None".to_owned(); + for kw in &call.arguments.keywords { + let Some(name) = &kw.arg else { + return Err(CodegenErrorType::SyntaxError( + "continuation decorator does not allow **kwargs".to_owned(), + )); + }; + let value = UnparseExpr::new(&kw.value, source_file).to_string(); + match name.as_str() { + "timeout_blocks" => timeout_expr = value, + "guard_unchanged" => guard_expr = value, + _ => { + return Err(CodegenErrorType::SyntaxError(format!( + "unsupported continuation decorator argument: {}", + name.as_str() + ))) + } + } + } + + Ok(Some((kind, timeout_expr, guard_expr))) +} + +fn decorator_base_kind(expr: &Expr) -> Result { + match expr { + Expr::Name(ExprName { id, .. }) if id.as_str() == "runner" => Ok(DecoratorKind::Runner), + Expr::Name(ExprName { id, .. }) if id.as_str() == "actor" => Ok(DecoratorKind::Actor), + _ => Err(CodegenErrorType::SyntaxError( + "continuation decorator must be runner.continuation or actor.continuation".to_owned(), + )), + } +} + +fn extract_params( + params: &Parameters, +) -> Result<(&str, &str, String), CodegenErrorType> { + let has_default = params + .posonlyargs + .iter() + .chain(¶ms.args) + .chain(¶ms.kwonlyargs) + .any(|param| param.default.is_some()); + if !params.posonlyargs.is_empty() + || !params.kwonlyargs.is_empty() + || params.vararg.is_some() + || params.kwarg.is_some() + || has_default + { + return Err(CodegenErrorType::SyntaxError( + "continuation functions must use simple (self, msg) parameters".to_owned(), + )); + } + if params.args.len() != 2 { + return Err(CodegenErrorType::SyntaxError( + "continuation functions must use exactly (self, msg) parameters".to_owned(), + )); + } + let self_name = params.args[0].name().as_str(); + let msg_name = params.args[1].name().as_str(); + Ok((self_name, msg_name, format!("{}, {}", self_name, msg_name))) +} + +fn parse_body( + body: &[Stmt], + source_file: &SourceFile, +) -> Result<(String, Vec, String), CodegenErrorType> { + if body.is_empty() { + return Err(CodegenErrorType::SyntaxError( + "continuation function body is empty".to_owned(), + )); + } + + let (ctx_name, start) = parse_capture(body)?; + let mut steps = Vec::new(); + let mut return_expr = "None".to_owned(); + + for stmt in &body[start..] { + match stmt { + Stmt::Assign(assign) => { + let step = parse_await_assign(assign, &ctx_name, source_file)?; + steps.push(step); + } + Stmt::Return(ret) => { + return_expr = match &ret.value { + Some(expr) => UnparseExpr::new(expr, source_file).to_string(), + None => "None".to_owned(), + }; + } + _ => { + return Err(CodegenErrorType::SyntaxError( + "only ctx. = await runner.* and return are supported".to_owned(), + )) + } + } + } + + Ok((ctx_name, steps, return_expr)) +} + +fn parse_capture(body: &[Stmt]) -> Result<(String, usize), CodegenErrorType> { + let mut start = 0; + if let Some(Stmt::Expr(StmtExpr { value, .. })) = body.first() { + if matches!(value.as_ref(), Expr::StringLiteral(_)) { + start = 1; + } + } + let first = body + .get(start) + .ok_or_else(|| CodegenErrorType::SyntaxError("empty body".to_owned()))?; + let Stmt::Assign(StmtAssign { targets, value, .. }) = first else { + return Err(CodegenErrorType::SyntaxError( + "first statement must be ctx = capture()".to_owned(), + )); + }; + if targets.len() != 1 { + return Err(CodegenErrorType::SyntaxError( + "capture assignment must have one target".to_owned(), + )); + } + let Expr::Name(ExprName { id, .. }) = &targets[0] else { + return Err(CodegenErrorType::SyntaxError( + "capture assignment target must be a name".to_owned(), + )); + }; + if !is_capture_call(value) { + return Err(CodegenErrorType::SyntaxError( + "first statement must be ctx = capture()".to_owned(), + )); + } + Ok((id.to_string(), start + 1)) +} + +fn is_capture_call(expr: &Expr) -> bool { + let Expr::Call(ExprCall { func, .. }) = expr else { + return false; + }; + match func.as_ref() { + Expr::Name(ExprName { id, .. }) => id.as_str() == "capture", + Expr::Attribute(ExprAttribute { value, attr, .. }) => { + if attr.as_str() != "capture" { + return false; + } + matches!(value.as_ref(), Expr::Name(ExprName { id, .. }) if id.as_str() == "pvm_sdk") + } + _ => false, + } +} + +fn parse_await_assign( + assign: &StmtAssign, + ctx_name: &str, + source_file: &SourceFile, +) -> Result { + if assign.targets.len() != 1 { + return Err(CodegenErrorType::SyntaxError( + "await assignment must have one target".to_owned(), + )); + } + let Expr::Attribute(attr) = &assign.targets[0] else { + return Err(CodegenErrorType::SyntaxError( + "await assignment target must be ctx.".to_owned(), + )); + }; + let Expr::Name(ExprName { id, .. }) = attr.value.as_ref() else { + return Err(CodegenErrorType::SyntaxError( + "await assignment target must be ctx.".to_owned(), + )); + }; + if id.as_str() != ctx_name { + return Err(CodegenErrorType::SyntaxError( + "await assignment target must be ctx.".to_owned(), + )); + } + let target_attr = attr.attr.as_str().to_owned(); + + let Expr::Await(await_expr) = assign.value.as_ref() else { + return Err(CodegenErrorType::SyntaxError( + "await assignment must await runner.*".to_owned(), + )); + }; + let Expr::Call(call) = await_expr.value.as_ref() else { + return Err(CodegenErrorType::SyntaxError( + "await must call runner.*".to_owned(), + )); + }; + let Expr::Attribute(func_attr) = call.func.as_ref() else { + return Err(CodegenErrorType::SyntaxError( + "await must call runner.*".to_owned(), + )); + }; + let Expr::Name(ExprName { id, .. }) = func_attr.value.as_ref() else { + return Err(CodegenErrorType::SyntaxError( + "await must call runner.*".to_owned(), + )); + }; + if id.as_str() != "runner" { + return Err(CodegenErrorType::SyntaxError( + "await must call runner.*".to_owned(), + )); + } + let job_type = func_attr.attr.as_str().to_owned(); + let args = format_call_args(&call.arguments, source_file)?; + + Ok(AwaitStep { + target_attr, + job_type, + args, + }) +} + +fn format_call_args( + args: &Arguments, + source_file: &SourceFile, +) -> Result { + let mut parts = Vec::new(); + for arg in &args.args { + parts.push(UnparseExpr::new(arg, source_file).to_string()); + } + for kw in &args.keywords { + let Some(name) = &kw.arg else { + return Err(CodegenErrorType::SyntaxError( + "runner calls do not allow **kwargs in continuation mode".to_owned(), + )); + }; + let value = UnparseExpr::new(&kw.value, source_file).to_string(); + parts.push(format!("{}={}", name.as_str(), value)); + } + Ok(parts.join(", ")) +} + +fn build_fsm_functions( + name: &str, + params: &str, + self_name: &str, + msg_name: &str, + ctx_name: &str, + steps: &[AwaitStep], + return_expr: &str, + meta: &ContinuationMeta, +) -> Result, CodegenErrorType> { + let first = &steps[0]; + let mut init_lines = Vec::new(); + init_lines.push(format!("def {}({}):", name, params)); + init_lines.push(" import pvm_sdk".to_owned()); + init_lines.push(format!( + " cid = pvm_sdk.continuation.new_cid({}, \"{}\")", + self_name, name + )); + init_lines.push(format!(" {} = pvm_sdk.capture()", ctx_name)); + init_lines.push(format!( + " pvm_sdk.continuation.save_cont(cid, state=0, ctx={}, handler=\"{}__resume\", timeout_blocks={}, guard_unchanged={})", + ctx_name, name, meta.timeout_expr, meta.guard_expr + )); + init_lines.push(format!( + " pvm_sdk.runner._send_job(\"{}\", cid, \"{}__resume\"{}{})", + first.job_type, + name, + if first.args.is_empty() { "" } else { ", " }, + first.args + )); + init_lines.push(" return None".to_owned()); + + let mut resume_lines = Vec::new(); + resume_lines.push(format!("def {}__resume({}):", name, params)); + resume_lines.push(" import pvm_sdk".to_owned()); + resume_lines.push(format!(" cid = {}.get(\"cid\")", msg_name)); + resume_lines.push(" st = pvm_sdk.continuation.load_cont(cid)".to_owned()); + resume_lines.push(format!(" {} = st.get(\"ctx\")", ctx_name)); + + for (idx, step) in steps.iter().enumerate() { + let is_last = idx == steps.len() - 1; + resume_lines.push(format!(" if st.get(\"state\") == {}:", idx)); + resume_lines.push(format!( + " {}.{} = {}.get(\"result\")", + ctx_name, step.target_attr, msg_name + )); + if is_last { + resume_lines.push(" pvm_sdk.continuation.delete_cont(cid)".to_owned()); + resume_lines.push(format!(" return {}", return_expr)); + } else { + resume_lines.push(format!( + " pvm_sdk.continuation.save_cont(cid, state={}, ctx={}, handler=\"{}__resume\", timeout_blocks=st.get(\"timeout_blocks\"), guard_unchanged=st.get(\"guard_unchanged\"))", + idx + 1, + ctx_name, + name + )); + let next_step = &steps[idx + 1]; + resume_lines.push(format!( + " pvm_sdk.runner._send_job(\"{}\", cid, \"{}__resume\"{}{})", + next_step.job_type, + name, + if next_step.args.is_empty() { "" } else { ", " }, + next_step.args + )); + resume_lines.push(" return None".to_owned()); + } + } + resume_lines.push(" return None".to_owned()); + + let code = format!( + "{}\n\n{}", + init_lines.join("\n"), + resume_lines.join("\n") + ); + + let parsed = parse_module(&code).map_err(|err| { + CodegenErrorType::SyntaxError(format!("pvm fsm transform failed: {}", err.error)) + })?; + let module = parsed.into_syntax(); + Ok(module.body) +} + +fn contains_await(stmt: &Stmt) -> bool { + let mut finder = AwaitFinder { found: false }; + finder.visit_stmt(stmt); + finder.found +} + +struct AwaitFinder { + found: bool, +} + +impl Visitor<'_> for AwaitFinder { + fn visit_expr(&mut self, expr: &Expr) { + if matches!(expr, Expr::Await(_)) { + self.found = true; + return; + } + walk_expr(self, expr); + } + + fn visit_stmt(&mut self, stmt: &Stmt) { + if self.found { + return; + } + walk_stmt(self, stmt); + } +} diff --git a/crates/compiler-source/src/lib.rs b/crates/compiler-source/src/lib.rs index 2d967e218d2..356b9224bf2 100644 --- a/crates/compiler-source/src/lib.rs +++ b/crates/compiler-source/src/lib.rs @@ -1,4 +1,4 @@ -pub use ruff_source_file::{LineIndex, OneIndexed as LineNumber, SourceLocation}; +pub use ruff_source_file::{LineIndex, OneIndexed as LineNumber, PositionEncoding, SourceLocation}; use ruff_text_size::TextRange; pub use ruff_text_size::TextSize; @@ -20,7 +20,7 @@ impl<'src> SourceCode<'src> { } pub fn source_location(&self, offset: TextSize) -> SourceLocation { - self.index.source_location(offset, self.text) + self.index.source_location(offset, self.text, PositionEncoding::Utf8) } pub fn get_range(&'src self, range: TextRange) -> &'src str { diff --git a/crates/pvm-alto/Cargo.toml b/crates/pvm-alto/Cargo.toml new file mode 100644 index 00000000000..4812c096247 --- /dev/null +++ b/crates/pvm-alto/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pvm-alto" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +pvm-host = { path = "../pvm-host" } +pvm-runtime = { path = "../pvm-runtime" } diff --git a/crates/pvm-alto/src/lib.rs b/crates/pvm-alto/src/lib.rs new file mode 100644 index 00000000000..d822927d377 --- /dev/null +++ b/crates/pvm-alto/src/lib.rs @@ -0,0 +1,218 @@ +use std::fs; +use std::io::Write; +use std::path::PathBuf; + +use pvm_host::{Bytes, HostApi, HostContext, HostError, HostResult}; +use pvm_runtime::{execute_tx_with_options, ExecutionOptions}; + +pub struct FsHost { + state_dir: PathBuf, + events_path: PathBuf, + gas_left: u64, + context: HostContext, + randomness_seed: [u8; 32], + timer_nonce: u64, +} + +impl FsHost { + pub fn new( + state_dir: impl Into, + events_path: impl Into, + gas_limit: u64, + context: HostContext, + ) -> Result { + let state_dir = state_dir.into(); + let events_path = events_path.into(); + + fs::create_dir_all(&state_dir).map_err(|_| HostError::StorageError)?; + if let Some(parent) = events_path.parent() { + fs::create_dir_all(parent).map_err(|_| HostError::StorageError)?; + } + + Ok(Self { + state_dir, + events_path, + gas_left: gas_limit, + randomness_seed: context.tx_hash, + context, + timer_nonce: 0, + }) + } + + pub fn with_randomness_seed(mut self, seed: [u8; 32]) -> Self { + self.randomness_seed = seed; + self + } + + fn key_path(&self, key: &[u8]) -> PathBuf { + let name = if key.is_empty() { + "__empty__".to_owned() + } else { + encode_hex(key) + }; + self.state_dir.join(name) + } +} + +impl HostApi for FsHost { + fn state_get(&self, key: &[u8]) -> HostResult> { + let path = self.key_path(key); + match fs::read(&path) { + Ok(bytes) => Ok(Some(bytes)), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(_) => Err(HostError::StorageError), + } + } + + fn state_set(&mut self, key: &[u8], value: &[u8]) -> HostResult<()> { + let path = self.key_path(key); + fs::write(path, value).map_err(|_| HostError::StorageError) + } + + fn state_delete(&mut self, key: &[u8]) -> HostResult<()> { + let path = self.key_path(key); + match fs::remove_file(path) { + Ok(_) => Ok(()), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(_) => Err(HostError::StorageError), + } + } + + fn emit_event(&mut self, topic: &str, data: &[u8]) -> HostResult<()> { + let mut file = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&self.events_path) + .map_err(|_| HostError::StorageError)?; + let line = format!("{}:{}\n", topic, encode_hex(data)); + file.write_all(line.as_bytes()) + .map_err(|_| HostError::StorageError) + } + + fn charge_gas(&mut self, amount: u64) -> HostResult<()> { + if amount > self.gas_left { + return Err(HostError::OutOfGas); + } + self.gas_left -= amount; + Ok(()) + } + + fn gas_left(&self) -> u64 { + self.gas_left + } + + fn context(&self) -> HostContext { + self.context.clone() + } + + fn randomness(&self, domain: &[u8]) -> HostResult<[u8; 32]> { + Ok(pseudo_random(&self.randomness_seed, domain)) + } + + fn send_message(&mut self, target: &[u8], payload: &[u8]) -> HostResult<()> { + let line = format!( + "message:{}:{}\n", + encode_hex(target), + encode_hex(payload) + ); + fs::OpenOptions::new() + .create(true) + .append(true) + .open(&self.events_path) + .and_then(|mut file| file.write_all(line.as_bytes())) + .map_err(|_| HostError::StorageError) + } + + fn schedule_timer(&mut self, height: u64, payload: &[u8]) -> HostResult { + self.timer_nonce = self.timer_nonce.wrapping_add(1); + let mut id = Vec::with_capacity(8); + id.extend_from_slice(&self.timer_nonce.to_le_bytes()); + let line = format!( + "timer.schedule:{}:{}:{}\n", + height, + encode_hex(&id), + encode_hex(payload) + ); + fs::OpenOptions::new() + .create(true) + .append(true) + .open(&self.events_path) + .and_then(|mut file| file.write_all(line.as_bytes())) + .map_err(|_| HostError::StorageError)?; + Ok(id) + } + + fn cancel_timer(&mut self, timer_id: &[u8]) -> HostResult<()> { + let line = format!( + "timer.cancel:{}\n", + encode_hex(timer_id) + ); + fs::OpenOptions::new() + .create(true) + .append(true) + .open(&self.events_path) + .and_then(|mut file| file.write_all(line.as_bytes())) + .map_err(|_| HostError::StorageError) + } +} + +pub struct FsTxConfig { + pub state_dir: PathBuf, + pub events_path: PathBuf, + pub gas_limit: u64, + pub context: HostContext, +} + +pub fn execute_tx_fs( + code: &[u8], + input: &[u8], + config: FsTxConfig, + options: &ExecutionOptions, +) -> Result { + let mut host = FsHost::new( + config.state_dir, + config.events_path, + config.gas_limit, + config.context, + )?; + execute_tx_with_options(&mut host, code, input, options) +} + +pub fn default_options() -> ExecutionOptions { + ExecutionOptions::default() + .with_source_path("contract.py") + .with_entrypoint("main") + .deterministic() +} + +fn encode_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for &byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + +fn fnv1a64(mut hash: u64, bytes: &[u8]) -> u64 { + const FNV_PRIME: u64 = 0x00000100000001b3; + for &byte in bytes { + hash ^= u64::from(byte); + hash = hash.wrapping_mul(FNV_PRIME); + } + hash +} + +fn pseudo_random(seed: &[u8; 32], domain: &[u8]) -> [u8; 32] { + const FNV_OFFSET: u64 = 0xcbf29ce484222325; + let mut out = [0u8; 32]; + for (idx, chunk) in out.chunks_exact_mut(8).enumerate() { + let mut hash = FNV_OFFSET; + hash = fnv1a64(hash, seed); + hash = fnv1a64(hash, domain); + hash = fnv1a64(hash, &(idx as u64).to_le_bytes()); + chunk.copy_from_slice(&hash.to_le_bytes()); + } + out +} diff --git a/crates/pvm-host/Cargo.toml b/crates/pvm-host/Cargo.toml new file mode 100644 index 00000000000..54e9c6579a9 --- /dev/null +++ b/crates/pvm-host/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "pvm-host" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] diff --git a/crates/pvm-host/src/lib.rs b/crates/pvm-host/src/lib.rs new file mode 100644 index 00000000000..72d878a7be3 --- /dev/null +++ b/crates/pvm-host/src/lib.rs @@ -0,0 +1,109 @@ +use core::fmt; + +pub type Bytes = Vec; + +#[derive(Clone, Debug)] +pub struct HostContext { + pub block_height: u64, + pub block_hash: [u8; 32], + pub tx_hash: [u8; 32], + pub sender: Bytes, + pub timestamp_ms: u64, + pub actor_addr: Bytes, + pub msg_id: Bytes, + pub nonce: u64, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum HostError { + OutOfGas, + InvalidInput, + NotFound, + StorageError, + Forbidden, + Internal, +} + +impl HostError { + pub const fn code(&self) -> u32 { + match self { + HostError::OutOfGas => 1, + HostError::InvalidInput => 2, + HostError::NotFound => 3, + HostError::StorageError => 4, + HostError::Forbidden => 5, + HostError::Internal => 6, + } + } + + pub const fn as_str(&self) -> &'static str { + match self { + HostError::OutOfGas => "out_of_gas", + HostError::InvalidInput => "invalid_input", + HostError::NotFound => "not_found", + HostError::StorageError => "storage_error", + HostError::Forbidden => "forbidden", + HostError::Internal => "internal", + } + } + + pub fn from_code(code: u32) -> Option { + match code { + 1 => Some(HostError::OutOfGas), + 2 => Some(HostError::InvalidInput), + 3 => Some(HostError::NotFound), + 4 => Some(HostError::StorageError), + 5 => Some(HostError::Forbidden), + 6 => Some(HostError::Internal), + _ => None, + } + } + + pub fn from_name(name: &str) -> Option { + match name { + "out_of_gas" => Some(HostError::OutOfGas), + "invalid_input" => Some(HostError::InvalidInput), + "not_found" => Some(HostError::NotFound), + "storage_error" => Some(HostError::StorageError), + "forbidden" => Some(HostError::Forbidden), + "internal" => Some(HostError::Internal), + _ => None, + } + } +} + +impl fmt::Display for HostError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let msg = match self { + HostError::OutOfGas => "out of gas", + HostError::InvalidInput => "invalid input", + HostError::NotFound => "not found", + HostError::StorageError => "storage error", + HostError::Forbidden => "forbidden", + HostError::Internal => "internal error", + }; + f.write_str(msg) + } +} + +impl std::error::Error for HostError {} + +pub type HostResult = Result; + +pub trait HostApi { + fn state_get(&self, key: &[u8]) -> HostResult>; + fn state_set(&mut self, key: &[u8], value: &[u8]) -> HostResult<()>; + fn state_delete(&mut self, key: &[u8]) -> HostResult<()>; + + fn emit_event(&mut self, topic: &str, data: &[u8]) -> HostResult<()>; + + fn charge_gas(&mut self, amount: u64) -> HostResult<()>; + fn gas_left(&self) -> u64; + + fn context(&self) -> HostContext; + fn randomness(&self, domain: &[u8]) -> HostResult<[u8; 32]>; + + fn send_message(&mut self, target: &[u8], payload: &[u8]) -> HostResult<()>; + fn schedule_timer(&mut self, height: u64, payload: &[u8]) -> HostResult; + fn cancel_timer(&mut self, timer_id: &[u8]) -> HostResult<()>; +} diff --git a/crates/pvm-runtime/Cargo.toml b/crates/pvm-runtime/Cargo.toml new file mode 100644 index 00000000000..ea01fd702a2 --- /dev/null +++ b/crates/pvm-runtime/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pvm-runtime" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +repository.workspace = true +license.workspace = true + +[features] +default = ["stdlib"] +stdlib = ["rustpython/stdlib"] + +[dependencies] +pvm-host = { path = "../pvm-host" } +rustpython = { path = "../.." } +rustpython-common = { workspace = true } +rustpython-vm = { workspace = true } diff --git a/crates/pvm-runtime/src/continuation.rs b/crates/pvm-runtime/src/continuation.rs new file mode 100644 index 00000000000..0b9550496dd --- /dev/null +++ b/crates/pvm-runtime/src/continuation.rs @@ -0,0 +1,32 @@ +use rustpython_vm::vm::ContinuationMode; + +#[derive(Clone, Debug)] +pub struct ContinuationOptions { + pub mode: ContinuationMode, + pub resume_bytes: Option>, + pub resume_key: Option>, + pub checkpoint_key: Option>, +} + +impl Default for ContinuationOptions { + fn default() -> Self { + Self { + mode: ContinuationMode::Fsm, + resume_bytes: None, + resume_key: None, + checkpoint_key: None, + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct RuntimeConfig { + pub continuation_mode: ContinuationMode, +} + +impl RuntimeConfig { + pub fn from_options(options: Option<&ContinuationOptions>) -> Self { + let continuation_mode = options.map(|o| o.mode).unwrap_or(ContinuationMode::Fsm); + Self { continuation_mode } + } +} diff --git a/crates/pvm-runtime/src/determinism.rs b/crates/pvm-runtime/src/determinism.rs new file mode 100644 index 00000000000..01a8d38778b --- /dev/null +++ b/crates/pvm-runtime/src/determinism.rs @@ -0,0 +1,148 @@ +#[derive(Clone, Debug)] +pub struct DeterminismOptions { + pub enabled: bool, + pub hash_seed: u32, + pub stdlib_whitelist: Vec, + pub stdlib_blacklist: Vec, + pub stdlib_hash: Option, + pub enable_softfloat: bool, + pub enable_gas: bool, + pub trace_imports: bool, + pub trace_allow_all: bool, + pub trace_path: Option, +} + +impl DeterminismOptions { + pub fn deterministic(hash_seed: Option) -> Self { + let mut options = Self::default(); + options.enabled = true; + options.hash_seed = hash_seed.unwrap_or(0); + options + } + + pub fn default_whitelist() -> Vec { + vec![ + "builtins", + "types", + "collections", + "collections.abc", + "abc", + "enum", + "dataclasses", + "typing", + "functools", + "itertools", + "operator", + "re", + "sre_compile", + "sre_parse", + "sre_constants", + "_sre", + "string", + "codecs", + "encodings", + "unicodedata", + "math", + "keyword", + "reprlib", + "json", + "copyreg", + "base64", + "binascii", + "struct", + "hashlib", + "hmac", + "warnings", + "heapq", + "bisect", + "_collections", + "_collections_abc", + "_functools", + "_abc", + "_py_abc", + "_struct", + "_weakrefset", + "_weakref", + "weakref", + "_thread", + "_json", + "_hashlib", + "_md5", + "_sha1", + "_sha256", + "_sha512", + "_sha3", + "_blake2", + "_bisect", + "_heapq", + "_warnings", + "_operator", + "pvm_host", + "pvm_sdk", + "pvm_sdk.pvm_time", + "pvm_sdk.pvm_random", + "pvm_sdk.pvm_sys", + "pvm_sdk.runtime", + "pvm_sdk.continuation", + "pvm_sdk.runner", + "pvm_sdk.actor", + "pvm_sdk.verify", + "pvm_sdk.types", + "rustpython_checkpoint", + "pvm_time", + "pvm_random", + "pvm_sys", + ] + .into_iter() + .map(|item| item.to_owned()) + .collect() + } + + pub fn default_blacklist() -> Vec { + vec![ + "time", + "datetime", + "random", + "secrets", + "uuid", + "os", + "sys", + "socket", + "ssl", + "subprocess", + "ctypes", + "threading", + "multiprocessing", + "signal", + "select", + "asyncio", + "pathlib", + "glob", + "tempfile", + "shutil", + "zipfile", + "inspect", + "traceback", + ] + .into_iter() + .map(|item| item.to_owned()) + .collect() + } +} + +impl Default for DeterminismOptions { + fn default() -> Self { + Self { + enabled: false, + hash_seed: 0, + stdlib_whitelist: Self::default_whitelist(), + stdlib_blacklist: Self::default_blacklist(), + stdlib_hash: None, + enable_softfloat: false, + enable_gas: false, + trace_imports: false, + trace_allow_all: false, + trace_path: None, + } + } +} diff --git a/crates/pvm-runtime/src/guard.rs b/crates/pvm-runtime/src/guard.rs new file mode 100644 index 00000000000..ea105c8754a --- /dev/null +++ b/crates/pvm-runtime/src/guard.rs @@ -0,0 +1,221 @@ +use crate::determinism::DeterminismOptions; +use rustpython_vm::{ + PyObjectRef, PyResult, VirtualMachine, + builtins::PyListRef, + compiler::Mode, +}; + +const GUARD_SOURCE: &str = r#" +import builtins +import sys + +_ALLOW = set(PVM_WHITELIST) +_DENY = set(PVM_BLACKLIST) +_REAL_IMPORT = builtins.__import__ +_HOST = _REAL_IMPORT(PVM_HOST_MODULE, None, None, (), 0) +_TRACE_IMPORTS = bool(PVM_TRACE_IMPORTS) +_TRACE_ALLOW_ALL = bool(PVM_TRACE_ALLOW_ALL) +_TRACE = [] +_TRACE_BLOCKED = [] +sys._pvm_import_trace = _TRACE +sys._pvm_import_blocked = _TRACE_BLOCKED + +_ALIAS = { + "time": "pvm_sdk.pvm_time", + "random": "pvm_sdk.pvm_random", + "pvm_time": "pvm_sdk.pvm_time", + "pvm_random": "pvm_sdk.pvm_random", + "pvm_sys": "pvm_sdk.pvm_sys", +} + + +def _resolve_name(name, globals, level): + if level and globals: + pkg = globals.get("__package__") or globals.get("__name__") + if pkg: + parts = pkg.split(".") + if level <= len(parts): + base = ".".join(parts[: len(parts) - level + 1]) + return base + ("." + name if name else "") + return name + + +def _is_allowed(name, globals=None, level=0): + resolved = _resolve_name(name, globals, level) + if resolved == "sys" and globals: + importer = globals.get("__package__") or globals.get("__name__") + if importer and _allowed_by_whitelist(importer): + return True + parts = resolved.split(".") if resolved else [] + for i in range(1, len(parts) + 1): + prefix = ".".join(parts[:i]) + if prefix in _DENY: + return False + if resolved in _DENY: + return False + if resolved in _ALLOW: + return True + for i in range(1, len(parts) + 1): + prefix = ".".join(parts[:i]) + if prefix in _ALLOW: + return True + if resolved: + prefix = resolved + "." + for item in _ALLOW: + if item.startswith(prefix): + return True + return False + + +def _allowed_by_whitelist(name): + parts = name.split(".") if name else [] + if name in _ALLOW: + return True + for i in range(1, len(parts) + 1): + prefix = ".".join(parts[:i]) + if prefix in _ALLOW: + return True + if name: + prefix = name + "." + for item in _ALLOW: + if item.startswith(prefix): + return True + return False + + +def _alias(name, target): + try: + if "." in target: + leaf = target.rsplit(".", 1)[-1] + mod = _REAL_IMPORT(target, None, None, (leaf,), 0) + else: + mod = _REAL_IMPORT(target, None, None, (), 0) + except Exception: + return + sys.modules[name] = mod + + +def _record_import(name, allowed): + if not _TRACE_IMPORTS: + return + if not name: + return + _TRACE.append(name) + if not allowed: + _TRACE_BLOCKED.append(name) + + +if PVM_SYS_PATH is not None: + sys.path[:] = PVM_SYS_PATH + try: + sys.path_importer_cache.clear() + except Exception: + sys.path_importer_cache = {} + +for _name, _target in _ALIAS.items(): + _alias(_name, _target) + + +def _pvm_import(name, globals=None, locals=None, fromlist=(), level=0): + resolved = _resolve_name(name, globals, level) + if resolved in _ALIAS: + _record_import(_ALIAS[resolved], True) + mod = sys.modules.get(resolved) + if mod is None: + raise _HOST.DeterministicValidationError("alias module missing: " + resolved) + return mod + allowed = _is_allowed(name, globals, level) + _record_import(resolved or name, allowed) + if not allowed and not _TRACE_ALLOW_ALL: + raise _HOST.NonDeterministicError("module not allowed: " + name) + return _REAL_IMPORT(name, globals, locals, fromlist, level) + + +builtins.__import__ = _pvm_import + + +class _PvmImportGuard: + def find_spec(self, fullname, path=None, target=None): + if not _is_allowed(fullname): + if not _TRACE_ALLOW_ALL: + raise _HOST.NonDeterministicError("module not allowed: " + fullname) + return None + + +sys.meta_path.insert(0, _PvmImportGuard()) + + +def _blocked_open(*_args, **_kwargs): + raise _HOST.DeterministicValidationError( + "file IO is disabled in deterministic mode" + ) + + +builtins.open = _blocked_open +try: + import io as _io + _io.open = _blocked_open +except Exception: + pass + +if hasattr(builtins, "execfile"): + builtins.execfile = _blocked_open +"#; + +pub(crate) fn install( + vm: &VirtualMachine, + options: &DeterminismOptions, + host_module_name: &str, +) -> PyResult<()> { + if !options.enabled { + return Ok(()); + } + + let scope = vm.new_scope_with_builtins(); + let mut whitelist_items = options.stdlib_whitelist.clone(); + if !whitelist_items.iter().any(|item| item == host_module_name) { + whitelist_items.push(host_module_name.to_owned()); + } + let whitelist = to_pylist(vm, &whitelist_items); + scope + .globals + .set_item("PVM_WHITELIST", whitelist.into(), vm)?; + let blacklist = to_pylist(vm, &options.stdlib_blacklist); + scope + .globals + .set_item("PVM_BLACKLIST", blacklist.into(), vm)?; + let sys_paths = vm.state.config.paths.module_search_paths.clone(); + let sys_paths_list = to_pylist(vm, &sys_paths); + scope + .globals + .set_item("PVM_SYS_PATH", sys_paths_list.into(), vm)?; + scope.globals.set_item( + "PVM_HOST_MODULE", + vm.ctx.new_str(host_module_name).into(), + vm, + )?; + scope.globals.set_item( + "PVM_TRACE_IMPORTS", + vm.ctx.new_bool(options.trace_imports).into(), + vm, + )?; + scope.globals.set_item( + "PVM_TRACE_ALLOW_ALL", + vm.ctx.new_bool(options.trace_allow_all).into(), + vm, + )?; + + let code = vm + .compile(GUARD_SOURCE, Mode::Exec, "".to_owned()) + .map_err(|err| vm.new_syntax_error(&err, Some(GUARD_SOURCE)))?; + vm.run_code_obj(code, scope)?; + Ok(()) +} + +fn to_pylist(vm: &VirtualMachine, items: &[String]) -> PyListRef { + let entries: Vec = items + .iter() + .map(|item| vm.ctx.new_str(item.as_str()).into()) + .collect(); + vm.ctx.new_list(entries) +} diff --git a/crates/pvm-runtime/src/host.rs b/crates/pvm-runtime/src/host.rs new file mode 100644 index 00000000000..86f449013c3 --- /dev/null +++ b/crates/pvm-runtime/src/host.rs @@ -0,0 +1,48 @@ +use pvm_host::HostApi; +use crate::continuation::RuntimeConfig; +use std::cell::Cell; +use std::marker::PhantomData; +use std::mem; + +type HostPtr = *mut (dyn HostApi + 'static); + +thread_local! { + static HOST: Cell> = Cell::new(None); + static RUNTIME_CONFIG: Cell> = Cell::new(None); +} + +pub struct HostGuard<'a> { + _marker: PhantomData<&'a mut dyn HostApi>, +} + +impl<'a> HostGuard<'a> { + pub fn install(host: &'a mut dyn HostApi, runtime_config: RuntimeConfig) -> Self { + let ptr = host as *mut dyn HostApi; + // Erase the lifetime; the guard ensures the pointer is only used in-scope. + let ptr = unsafe { mem::transmute::<*mut dyn HostApi, HostPtr>(ptr) }; + HOST.with(|cell| cell.set(Some(ptr))); + RUNTIME_CONFIG.with(|cell| cell.set(Some(runtime_config))); + Self { + _marker: PhantomData, + } + } +} + +impl Drop for HostGuard<'_> { + fn drop(&mut self) { + HOST.with(|cell| cell.set(None)); + RUNTIME_CONFIG.with(|cell| cell.set(None)); + } +} + +pub(crate) fn with_host(f: impl FnOnce(&mut dyn HostApi) -> R) -> Option { + HOST.with(|cell| { + let ptr = cell.get()?; + // Safety: host pointer is installed for the duration of an execution. + Some(unsafe { f(&mut *ptr) }) + }) +} + +pub(crate) fn runtime_config() -> Option { + RUNTIME_CONFIG.with(|cell| cell.get()) +} diff --git a/crates/pvm-runtime/src/lib.rs b/crates/pvm-runtime/src/lib.rs new file mode 100644 index 00000000000..210e9c42c10 --- /dev/null +++ b/crates/pvm-runtime/src/lib.rs @@ -0,0 +1,589 @@ +mod host; +mod continuation; +mod determinism; +mod guard; +mod module; + +pub use continuation::{ContinuationOptions, RuntimeConfig}; +pub use determinism::DeterminismOptions; +use pvm_host::{Bytes, HostApi, HostError}; +use std::collections::HashSet; +use std::fs; +use std::path::Path; +use rustpython::InterpreterConfig; +use rustpython_vm::{ + AsObject, + PyObjectRef, PyResult, Settings, VirtualMachine, + builtins::{PyBaseExceptionRef, PyListRef, PyNone}, + compiler::Mode, + convert::TryFromObject, + scope::Scope, +}; +use rustpython_vm::vm::ContinuationMode; + +#[derive(Clone, Debug)] +pub struct ExecutionOptions { + pub argv: Vec, + pub module_name: String, + pub source_path: String, + pub input_var: String, + pub output_var: String, + pub entrypoint: Option, + pub host_module_name: String, + pub init_stdlib: bool, + pub deterministic: bool, + pub hash_seed: Option, + pub determinism: Option, + pub set_main_module: bool, + pub continuation: Option, +} + +impl Default for ExecutionOptions { + fn default() -> Self { + Self { + argv: Vec::new(), + module_name: "__main__".to_owned(), + source_path: "".to_owned(), + input_var: "__pvm_input__".to_owned(), + output_var: "__pvm_output__".to_owned(), + entrypoint: None, + host_module_name: "pvm_host".to_owned(), + init_stdlib: true, + deterministic: false, + hash_seed: None, + determinism: None, + set_main_module: true, + continuation: Some(ContinuationOptions::default()), + } + } +} + +impl ExecutionOptions { + pub fn with_entrypoint(mut self, entrypoint: impl Into) -> Self { + self.entrypoint = Some(entrypoint.into()); + self + } + + pub fn with_module_name(mut self, module_name: impl Into) -> Self { + self.module_name = module_name.into(); + self + } + + pub fn with_source_path(mut self, source_path: impl Into) -> Self { + self.source_path = source_path.into(); + self + } + + pub fn with_argv(mut self, argv: Vec) -> Self { + self.argv = argv; + self + } + + pub fn deterministic(mut self) -> Self { + self.deterministic = true; + self + } + + pub fn with_determinism(mut self, determinism: DeterminismOptions) -> Self { + self.determinism = Some(determinism); + self + } +} + +pub fn execute_tx(host: &mut dyn HostApi, code: &[u8], input: &[u8]) -> Result { + execute_tx_with_options(host, code, input, &ExecutionOptions::default()) +} + +pub fn execute_tx_with_options( + host: &mut dyn HostApi, + code: &[u8], + input: &[u8], + options: &ExecutionOptions, +) -> Result { + let source = std::str::from_utf8(code).map_err(|_| HostError::InvalidInput)?; + let mut settings = Settings::default(); + settings.argv = if options.argv.is_empty() { + vec![options.source_path.clone()] + } else { + options.argv.clone() + }; + + let mut determinism = options.determinism.clone().or_else(|| { + if options.deterministic { + Some(DeterminismOptions::deterministic(options.hash_seed)) + } else { + None + } + }); + + if let Some(det) = determinism.as_ref().filter(|item| item.enabled) { + settings.hash_seed = Some(det.hash_seed); + settings.ignore_environment = true; + settings.import_site = false; + settings.user_site_directory = false; + settings.isolated = true; + settings.safe_path = true; + settings.install_signal_handlers = false; + } else if let Some(seed) = options.hash_seed { + settings.hash_seed = Some(seed); + } + + if let Some(cont) = options.continuation.as_ref() { + settings.continuation_mode = Some(cont.mode); + settings.checkpoint_exit = false; + } + + let mut config = InterpreterConfig::new().settings(settings); + #[cfg(feature = "stdlib")] + { + if options.init_stdlib { + config = config.init_stdlib(); + } + } + config = config.add_native_module(options.host_module_name.clone(), module::make_module); + if options.host_module_name != "pvm_host_module" { + config = config.add_native_module("pvm_host_module".to_owned(), module::make_module); + } + let interpreter = config.interpreter(); + + let resume_bytes = if let Some(cont) = options.continuation.as_ref() { + if cont.mode == ContinuationMode::Checkpoint { + if let Some(bytes) = cont.resume_bytes.as_ref() { + Some(bytes.clone()) + } else if let Some(key) = cont.resume_key.as_ref() { + host.state_get(key)? + } else { + None + } + } else { + None + } + } else { + None + }; + if resume_bytes.is_some() { + if let Some(det) = determinism.as_mut().filter(|item| item.enabled) { + // Snapshot restore may import os/path modules; allow them during resume. + let allow = [ + "os", + "posix", + "posixpath", + "genericpath", + "stat", + "_stat", + "errno", + "nt", + "ntpath", + "pvm_host_module", + "importlib", + "_frozen_importlib", + "_frozen_importlib_external", + ]; + det.stdlib_blacklist + .retain(|item| !allow.iter().any(|name| name == item)); + for name in allow { + if !det.stdlib_whitelist.iter().any(|item| item == name) { + det.stdlib_whitelist.push(name.to_owned()); + } + } + } + } + + let runtime_config = RuntimeConfig::from_options(options.continuation.as_ref()); + let _host_guard = host::HostGuard::install(host, runtime_config); + + interpreter.enter(|vm| { + if let Some(det) = determinism.as_ref().filter(|item| item.enabled) { + if let Err(err) = guard::install(vm, det, options.host_module_name.as_str()) { + vm.print_exception(err); + return Err(HostError::Internal); + } + } + let res = if let Some(data) = resume_bytes.as_ref() { + if let Err(err) = ensure_sys_path(vm) { + Err(err) + } else { + vm.resume_from_bytes(options.source_path.as_str(), data) + .and_then(|_| Ok(Vec::new())) + } + } else { + run_source(vm, source, input, options) + }; + let trace_result = determinism + .as_ref() + .filter(|item| item.enabled) + .map(|det| export_import_trace(vm, det)) + .unwrap_or(Ok(())); + match res { + Ok(bytes) => { + if let Some(checkpoint) = vm.take_checkpoint_bytes() { + if let Some(cont) = options.continuation.as_ref() { + if let Some(key) = cont.checkpoint_key.as_ref() { + let result = host::with_host(|host| host.state_set(key, &checkpoint)) + .ok_or(HostError::Internal)?; + result?; + } + } + return Ok(Vec::new()); + } + if let Err(err) = trace_result { + return Err(err); + } + Ok(bytes) + } + Err(err) => { + if let Some(checkpoint) = vm.take_checkpoint_bytes() { + if let Some(cont) = options.continuation.as_ref() { + if let Some(key) = cont.checkpoint_key.as_ref() { + let result = host::with_host(|host| host.state_set(key, &checkpoint)) + .ok_or(HostError::Internal)?; + result?; + } + } + return Ok(Vec::new()); + } + if let Err(trace_err) = trace_result { + eprintln!("pvm import trace failed: {trace_err}"); + } + if std::env::var_os("PVM_PRINT_EXCEPTION").is_some() { + vm.print_exception(err.clone()); + } + let host_error = map_exception(vm, &err, options); + if host_error == HostError::Internal { + vm.print_exception(err.clone()); + } + Err(host_error) + } + } + }) +} + +fn run_source( + vm: &VirtualMachine, + source: &str, + input: &[u8], + options: &ExecutionOptions, +) -> PyResult { + let scope = setup_main_module(vm, options)?; + let input_obj = vm.ctx.new_bytes(input.to_vec()); + scope + .globals + .set_item(options.input_var.as_str(), input_obj.clone().into(), vm)?; + + let code_obj = vm + .compile(source, Mode::Exec, options.source_path.clone()) + .map_err(|err| vm.new_syntax_error(&err, Some(source)))?; + vm.run_code_obj(code_obj, scope.clone())?; + + let output = if let Some(entrypoint) = &options.entrypoint { + let callable = scope + .globals + .get_item_opt(entrypoint.as_str(), vm)? + .ok_or_else(|| { + vm.new_name_error( + format!( + "pvm entrypoint '{}' not found in module '{}'", + entrypoint, options.module_name + ), + vm.ctx.new_str(entrypoint.as_str()), + ) + })?; + Some(callable.call((input_obj,), vm)?) + } else { + scope.globals.get_item_opt(options.output_var.as_str(), vm)? + }; + + extract_output(vm, output) +} + +fn ensure_sys_path(vm: &VirtualMachine) -> PyResult<()> { + let obj = vm.sys_module.get_attr("path", vm)?; + let list = PyListRef::try_from_object(vm, obj)?; + let items = list.borrow_vec(); + let mut existing = HashSet::new(); + for item in items.iter() { + let value = item.str(vm)?; + existing.insert(value.to_string()); + } + for path in vm.state.config.paths.module_search_paths.iter().rev() { + if !existing.contains(path) { + vm.insert_sys_path(vm.ctx.new_str(path.as_str()).into())?; + } + } + Ok(()) +} + +fn setup_main_module(vm: &VirtualMachine, options: &ExecutionOptions) -> PyResult { + let scope = vm.new_scope_with_builtins(); + let main_module = vm.new_module(options.module_name.as_str(), scope.globals.clone(), None); + main_module + .dict() + .set_item("__annotations__", vm.ctx.new_dict().into(), vm) + .expect("Failed to initialize __main__.__annotations__"); + main_module + .dict() + .set_item("__file__", vm.ctx.new_str(options.source_path.clone()).into(), vm) + .expect("Failed to initialize __main__.__file__"); + main_module + .dict() + .set_item("__cached__", vm.ctx.none(), vm) + .expect("Failed to initialize __main__.__cached__"); + + let modules = vm.sys_module.get_attr("modules", vm)?; + modules.set_item(options.module_name.as_str(), main_module.clone().into(), vm)?; + if options.set_main_module && options.module_name != "__main__" { + modules.set_item("__main__", main_module.into(), vm)?; + } + + Ok(scope) +} + +fn extract_output(vm: &VirtualMachine, output: Option) -> PyResult { + let Some(output) = output else { + return Ok(Vec::new()); + }; + + if output.downcast_ref::().is_some() { + return Ok(Vec::new()); + } + + output + .try_bytes_like(vm, |bytes| bytes.to_vec()) + .map_err(|_| { + vm.new_type_error("pvm output must be bytes-like or None".to_owned()) + }) +} + +fn map_exception( + vm: &VirtualMachine, + err: &PyBaseExceptionRef, + options: &ExecutionOptions, +) -> HostError { + if let Some(host_error) = host_error_from_exception(vm, err, options) { + return host_error; + } + if let Some(host_error) = determinism_error_from_exception(vm, err, options) { + vm.print_exception(err.clone()); + return host_error; + } + + let is_syntax = err.fast_isinstance(vm.ctx.exceptions.syntax_error); + if is_syntax { + return HostError::InvalidInput; + } + + let is_type = err.fast_isinstance(vm.ctx.exceptions.type_error); + if is_type { + return HostError::InvalidInput; + } + + HostError::Internal +} + +fn determinism_error_from_exception( + vm: &VirtualMachine, + err: &PyBaseExceptionRef, + options: &ExecutionOptions, +) -> Option { + let module = get_host_module(vm, options)?; + let det_err_obj = module.get_attr("DeterministicValidationError", vm).ok()?; + let det_err_type = rustpython_vm::builtins::PyTypeRef::try_from_object(vm, det_err_obj).ok()?; + if err.fast_isinstance(&det_err_type) { + return Some(HostError::InvalidInput); + } + + let nondet_obj = module.get_attr("NonDeterministicError", vm).ok()?; + let nondet_type = rustpython_vm::builtins::PyTypeRef::try_from_object(vm, nondet_obj).ok()?; + if err.fast_isinstance(&nondet_type) { + return Some(HostError::Forbidden); + } + + let ooo_obj = module.get_attr("OutOfGasError", vm).ok()?; + let ooo_type = rustpython_vm::builtins::PyTypeRef::try_from_object(vm, ooo_obj).ok()?; + if err.fast_isinstance(&ooo_type) { + return Some(HostError::OutOfGas); + } + + None +} + +fn host_error_from_exception( + vm: &VirtualMachine, + err: &PyBaseExceptionRef, + options: &ExecutionOptions, +) -> Option { + let module = get_host_module(vm, options)?; + let host_error_obj = module.get_attr("HostError", vm).ok()?; + let host_error_type = + rustpython_vm::builtins::PyTypeRef::try_from_object(vm, host_error_obj).ok()?; + if !err.fast_isinstance(&host_error_type) { + return None; + } + let code_obj = err.as_object().get_attr("code", vm).ok()?; + let code = u32::try_from_object(vm, code_obj).ok()?; + HostError::from_code(code).or_else(|| { + let name_obj = err.as_object().get_attr("name", vm).ok()?; + let name = name_obj.str(vm).ok()?.to_string(); + HostError::from_name(name.as_str()) + }) +} + +fn get_host_module(vm: &VirtualMachine, options: &ExecutionOptions) -> Option { + let modules_obj = vm.sys_module.get_attr("modules", vm).ok()?; + let modules = rustpython_vm::builtins::PyDictRef::try_from_object(vm, modules_obj).ok()?; + modules + .get_item_opt(options.host_module_name.as_str(), vm) + .ok() + .flatten() +} + +fn export_import_trace(vm: &VirtualMachine, det: &DeterminismOptions) -> Result<(), HostError> { + if !det.trace_imports { + return Ok(()); + } + let Some(path) = det.trace_path.as_ref() else { + return Ok(()); + }; + + let trace = read_trace_list(vm, "_pvm_import_trace")?; + let blocked = read_trace_list(vm, "_pvm_import_blocked")?; + let unique = dedup_in_order(&trace); + let blocked_unique = dedup_in_order(&blocked); + + let blacklist = det.stdlib_blacklist.clone(); + let mut whitelist = det.stdlib_whitelist.clone(); + let mut missing = Vec::new(); + let mut blacklisted = Vec::new(); + + for name in &unique { + if denied_by_list(&blacklist, name) { + blacklisted.push(name.clone()); + continue; + } + if allowed_by_list(&whitelist, name) { + continue; + } + missing.push(name.clone()); + whitelist.push(name.clone()); + } + + let payload = format!( + "{{\"trace\":{},\"unique\":{},\"blocked\":{},\"missing\":{},\"blacklisted\":{},\"whitelist_base\":{},\"whitelist_suggested\":{},\"blacklist\":{}}}\n", + json_list(&trace), + json_list(&unique), + json_list(&blocked_unique), + json_list(&missing), + json_list(&blacklisted), + json_list(&det.stdlib_whitelist), + json_list(&whitelist), + json_list(&blacklist), + ); + + write_trace_file(path, &payload)?; + Ok(()) +} + +fn read_trace_list(vm: &VirtualMachine, name: &str) -> Result, HostError> { + let name_obj = vm.ctx.new_str(name); + let obj = vm + .sys_module + .get_attr(&name_obj, vm) + .map_err(|_| HostError::Internal)?; + let list = PyListRef::try_from_object(vm, obj).map_err(|_| HostError::Internal)?; + let items = list.borrow_vec(); + let mut out = Vec::with_capacity(items.len()); + for item in items.iter() { + let value = item.str(vm).map_err(|_| HostError::Internal)?; + out.push(value.to_string()); + } + Ok(out) +} + +fn dedup_in_order(items: &[String]) -> Vec { + let mut seen: HashSet<&str> = HashSet::new(); + let mut out = Vec::new(); + for item in items { + if seen.insert(item.as_str()) { + out.push(item.clone()); + } + } + out +} + +fn allowed_by_list(list: &[String], name: &str) -> bool { + if name.is_empty() { + return false; + } + if list.iter().any(|item| item == name) { + return true; + } + let mut prefix = String::new(); + for (idx, part) in name.split('.').enumerate() { + if idx > 0 { + prefix.push('.'); + } + prefix.push_str(part); + if list.iter().any(|item| item == &prefix) { + return true; + } + } + let name_prefix = format!("{name}."); + list.iter().any(|item| item.starts_with(&name_prefix)) +} + +fn denied_by_list(list: &[String], name: &str) -> bool { + if name.is_empty() { + return false; + } + let mut prefix = String::new(); + for (idx, part) in name.split('.').enumerate() { + if idx > 0 { + prefix.push('.'); + } + prefix.push_str(part); + if list.iter().any(|item| item == &prefix) { + return true; + } + } + false +} + +fn write_trace_file(path: &str, payload: &str) -> Result<(), HostError> { + let path = Path::new(path); + if let Some(parent) = path.parent() { + if !parent.as_os_str().is_empty() { + fs::create_dir_all(parent).map_err(|_| HostError::Internal)?; + } + } + fs::write(path, payload).map_err(|_| HostError::Internal)?; + Ok(()) +} + +fn json_list(items: &[String]) -> String { + let mut out = String::from("["); + for (idx, item) in items.iter().enumerate() { + if idx > 0 { + out.push(','); + } + out.push('"'); + out.push_str(&json_escape(item)); + out.push('"'); + } + out.push(']'); + out +} + +fn json_escape(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + for ch in input.chars() { + match ch { + '\\' => out.push_str("\\\\"), + '"' => out.push_str("\\\""), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + _ => out.push(ch), + } + } + out +} diff --git a/crates/pvm-runtime/src/module.rs b/crates/pvm-runtime/src/module.rs new file mode 100644 index 00000000000..db5fd1ede57 --- /dev/null +++ b/crates/pvm-runtime/src/module.rs @@ -0,0 +1,193 @@ +pub(crate) use pvm_host_module::make_module; + +#[rustpython_vm::pymodule] +mod pvm_host_module { + use crate::host; + use crate::continuation::RuntimeConfig; + use ::pvm_host::{HostApi, HostContext, HostError}; + use rustpython_vm::{ + AsObject, + PyObjectRef, PyResult, VirtualMachine, + builtins::{PyBaseExceptionRef, PyStrRef, PyTypeRef}, + function::ArgBytesLike, + }; + + fn host_error(vm: &VirtualMachine, err: HostError) -> PyBaseExceptionRef { + let exc = vm.new_exception( + host_error_type(vm), + vec![vm.ctx.new_str(err.to_string()).into()], + ); + let _ = exc + .as_object() + .set_attr("code", vm.new_pyobj(err.code()), vm); + let _ = exc + .as_object() + .set_attr("name", vm.ctx.new_str(err.as_str()), vm); + exc + } + + fn with_host( + vm: &VirtualMachine, + f: impl FnOnce(&mut dyn HostApi) -> Result, + ) -> PyResult { + let result = host::with_host(f) + .ok_or_else(|| vm.new_runtime_error("pvm host is not initialized".to_owned()))?; + result.map_err(|err| host_error(vm, err)) + } + + #[pyfunction] + fn get_state(key: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + let value = with_host(vm, |host| key.with_ref(|bytes| host.state_get(bytes)))?; + Ok(match value { + Some(data) => vm.ctx.new_bytes(data).into(), + None => vm.ctx.none(), + }) + } + + #[pyfunction] + fn set_state(key: ArgBytesLike, value: ArgBytesLike, vm: &VirtualMachine) -> PyResult<()> { + with_host(vm, |host| { + key.with_ref(|k| value.with_ref(|v| host.state_set(k, v))) + })?; + Ok(()) + } + + #[pyfunction] + fn delete_state(key: ArgBytesLike, vm: &VirtualMachine) -> PyResult<()> { + with_host(vm, |host| key.with_ref(|bytes| host.state_delete(bytes)))?; + Ok(()) + } + + #[pyfunction] + fn emit_event(topic: PyStrRef, data: ArgBytesLike, vm: &VirtualMachine) -> PyResult<()> { + with_host(vm, |host| data.with_ref(|bytes| host.emit_event(topic.as_str(), bytes)))?; + Ok(()) + } + + #[pyfunction] + fn charge_gas(amount: u64, vm: &VirtualMachine) -> PyResult<()> { + with_host(vm, |host| host.charge_gas(amount))?; + Ok(()) + } + + #[pyfunction] + fn gas_left(vm: &VirtualMachine) -> PyResult { + with_host(vm, |host| Ok(host.gas_left())) + } + + #[pyfunction] + fn context(vm: &VirtualMachine) -> PyResult { + let ctx = with_host(vm, |host| Ok(host.context()))?; + Ok(host_context_to_dict(vm, ctx)?.into()) + } + + #[pyfunction] + fn randomness(domain: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + let bytes = with_host(vm, |host| domain.with_ref(|d| host.randomness(d)))?; + Ok(vm.ctx.new_bytes(bytes.to_vec()).into()) + } + + #[pyfunction] + fn send_message(target: ArgBytesLike, payload: ArgBytesLike, vm: &VirtualMachine) -> PyResult<()> { + with_host(vm, |host| { + target.with_ref(|t| payload.with_ref(|p| host.send_message(t, p))) + })?; + Ok(()) + } + + #[pyfunction] + fn schedule_timer(height: u64, payload: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + let timer_id = with_host(vm, |host| { + payload.with_ref(|p| host.schedule_timer(height, p)) + })?; + Ok(vm.ctx.new_bytes(timer_id).into()) + } + + #[pyfunction] + fn cancel_timer(timer_id: ArgBytesLike, vm: &VirtualMachine) -> PyResult<()> { + with_host(vm, |host| timer_id.with_ref(|id| host.cancel_timer(id)))?; + Ok(()) + } + + #[pyfunction] + fn runtime_config(vm: &VirtualMachine) -> PyResult { + let config = host::runtime_config(); + Ok(runtime_config_to_dict(vm, config)?.into()) + } + + #[pyattr(name = "HostError", once)] + fn host_error_type(vm: &VirtualMachine) -> PyTypeRef { + vm.ctx.new_exception_type( + "pvm_host", + "HostError", + Some(vec![vm.ctx.exceptions.runtime_error.to_owned()]), + ) + } + + #[pyattr(name = "DeterministicValidationError", once)] + fn deterministic_validation_error_type(vm: &VirtualMachine) -> PyTypeRef { + vm.ctx.new_exception_type( + "pvm_host", + "DeterministicValidationError", + Some(vec![vm.ctx.exceptions.value_error.to_owned()]), + ) + } + + #[pyattr(name = "NonDeterministicError", once)] + fn nondeterministic_error_type(vm: &VirtualMachine) -> PyTypeRef { + vm.ctx.new_exception_type( + "pvm_host", + "NonDeterministicError", + Some(vec![vm.ctx.exceptions.runtime_error.to_owned()]), + ) + } + + #[pyattr(name = "OutOfGasError", once)] + fn out_of_gas_error_type(vm: &VirtualMachine) -> PyTypeRef { + vm.ctx.new_exception_type( + "pvm_host", + "OutOfGasError", + Some(vec![vm.ctx.exceptions.runtime_error.to_owned()]), + ) + } + + fn host_context_to_dict( + vm: &VirtualMachine, + ctx: HostContext, + ) -> PyResult { + let dict = vm.ctx.new_dict(); + dict.set_item("block_height", vm.new_pyobj(ctx.block_height), vm)?; + dict.set_item( + "block_hash", + vm.ctx.new_bytes(ctx.block_hash.to_vec()).into(), + vm, + )?; + dict.set_item( + "tx_hash", + vm.ctx.new_bytes(ctx.tx_hash.to_vec()).into(), + vm, + )?; + dict.set_item("sender", vm.ctx.new_bytes(ctx.sender).into(), vm)?; + dict.set_item("timestamp_ms", vm.new_pyobj(ctx.timestamp_ms), vm)?; + dict.set_item("actor_addr", vm.ctx.new_bytes(ctx.actor_addr).into(), vm)?; + dict.set_item("msg_id", vm.ctx.new_bytes(ctx.msg_id).into(), vm)?; + dict.set_item("nonce", vm.new_pyobj(ctx.nonce), vm)?; + Ok(dict) + } + + fn runtime_config_to_dict( + vm: &VirtualMachine, + cfg: Option, + ) -> PyResult { + let dict = vm.ctx.new_dict(); + if let Some(cfg) = cfg { + dict.set_item( + "continuation_mode", + vm.ctx.new_str(cfg.continuation_mode.to_string()).into(), + vm, + )?; + } + Ok(dict) + } + +} diff --git a/crates/stdlib/src/json.rs b/crates/stdlib/src/json.rs index eb6ed3a5f64..e24a1d71508 100644 --- a/crates/stdlib/src/json.rs +++ b/crates/stdlib/src/json.rs @@ -34,23 +34,42 @@ mod _json { type Args = PyObjectRef; fn py_new(_cls: &Py, ctx: Self::Args, vm: &VirtualMachine) -> PyResult { - let strict = ctx.get_attr("strict", vm)?.try_to_bool(vm)?; - let object_hook = vm.option_if_none(ctx.get_attr("object_hook", vm)?); - let object_pairs_hook = vm.option_if_none(ctx.get_attr("object_pairs_hook", vm)?); - let parse_float = ctx.get_attr("parse_float", vm)?; - let parse_float = if vm.is_none(&parse_float) || parse_float.is(vm.ctx.types.float_type) - { - None - } else { - Some(parse_float) + let strict = match vm.get_attribute_opt(ctx.clone(), "strict")? { + Some(value) => value.try_to_bool(vm)?, + None => true, }; - let parse_int = ctx.get_attr("parse_int", vm)?; - let parse_int = if vm.is_none(&parse_int) || parse_int.is(vm.ctx.types.int_type) { - None - } else { - Some(parse_int) + let object_hook = match vm.get_attribute_opt(ctx.clone(), "object_hook")? { + Some(value) => vm.option_if_none(value), + None => None, + }; + let object_pairs_hook = match vm.get_attribute_opt(ctx.clone(), "object_pairs_hook")? { + Some(value) => vm.option_if_none(value), + None => None, + }; + let parse_float = match vm.get_attribute_opt(ctx.clone(), "parse_float")? { + Some(value) => { + if vm.is_none(&value) || value.is(vm.ctx.types.float_type) { + None + } else { + Some(value) + } + } + None => None, + }; + let parse_int = match vm.get_attribute_opt(ctx.clone(), "parse_int")? { + Some(value) => { + if vm.is_none(&value) || value.is(vm.ctx.types.int_type) { + None + } else { + Some(value) + } + } + None => None, + }; + let parse_constant = match vm.get_attribute_opt(ctx.clone(), "parse_constant")? { + Some(value) if !vm.is_none(&value) => value, + _ => vm.ctx.types.float_type.to_owned().into(), }; - let parse_constant = ctx.get_attr("parse_constant", vm)?; Ok(Self { strict, @@ -66,6 +85,11 @@ mod _json { #[pyclass(with(Callable, Constructor))] impl JsonScanner { + #[pymethod] + fn __getnewargs__(&self, vm: &VirtualMachine) -> PyResult { + Ok(vm.new_tuple(vec![self.ctx.clone()]).into()) + } + fn parse( &self, s: &str, diff --git a/crates/vm/build.rs b/crates/vm/build.rs index f76bf3f5cbd..6c76097a541 100644 --- a/crates/vm/build.rs +++ b/crates/vm/build.rs @@ -12,6 +12,8 @@ fn main() { println!("cargo:rerun-if-changed={display}"); } println!("cargo:rerun-if-changed=../../Lib/importlib/_bootstrap.py"); + println!("cargo:rerun-if-changed=../../Cargo.toml"); + println!("cargo:rerun-if-env-changed=PVM_VERSION"); println!("cargo:rustc-env=RUSTPYTHON_GIT_HASH={}", git_hash()); println!( @@ -21,6 +23,7 @@ fn main() { println!("cargo:rustc-env=RUSTPYTHON_GIT_TAG={}", git_tag()); println!("cargo:rustc-env=RUSTPYTHON_GIT_BRANCH={}", git_branch()); println!("cargo:rustc-env=RUSTC_VERSION={}", rustc_version()); + println!("cargo:rustc-env=PVM_VERSION={}", pvm_version()); println!( "cargo:rustc-env=RUSTPYTHON_TARGET_TRIPLE={}", @@ -63,6 +66,45 @@ fn rustc_version() -> String { command(rustc, &["-V"]) } +fn pvm_version() -> String { + if let Ok(version) = env::var("PVM_VERSION") { + if !version.trim().is_empty() { + return version; + } + } + + let manifest_dir = match env::var("CARGO_MANIFEST_DIR") { + Ok(dir) => PathBuf::from(dir), + Err(_) => return "0.0.0".to_owned(), + }; + let root_manifest = manifest_dir.join("../../Cargo.toml"); + let manifest = match std::fs::read_to_string(root_manifest) { + Ok(contents) => contents, + Err(_) => return "0.0.0".to_owned(), + }; + + let mut in_pvm_section = false; + for line in manifest.lines() { + let line = line.trim(); + if line.starts_with('[') && line.ends_with(']') { + in_pvm_section = line == "[package.metadata.pvm]"; + continue; + } + if !in_pvm_section || line.is_empty() || line.starts_with('#') { + continue; + } + let (key, value) = match line.split_once('=') { + Some(pair) => pair, + None => continue, + }; + if key.trim() == "version" { + return value.trim().trim_matches('"').to_owned(); + } + } + + "0.0.0".to_owned() +} + fn command(cmd: impl AsRef, args: &[&str]) -> String { match Command::new(cmd).args(args).output() { Ok(output) => match String::from_utf8(output.stdout) { diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 3f470e5453f..b570180fe54 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -18,6 +18,7 @@ use crate::{ types::PyTypeFlags, vm::{Context, PyMethod}, }; +use crate::vm::checkpoint; use indexmap::IndexMap; use itertools::Itertools; use rustpython_common::{boxvec::BoxVec, lock::PyMutex, wtf8::Wtf8Buf}; @@ -27,15 +28,15 @@ use std::sync::atomic; use std::{fmt, iter::zip}; #[derive(Clone, Debug)] -struct Block { +pub(crate) struct Block { /// The type of block. - typ: BlockType, + pub(crate) typ: BlockType, /// The level of the value stack when the block was entered. - level: usize, + pub(crate) level: usize, } #[derive(Clone, Debug)] -enum BlockType { +pub(crate) enum BlockType { Loop, TryExcept { handler: bytecode::Label, @@ -60,7 +61,7 @@ pub type FrameRef = PyRef; /// This could be return of function, exception being /// raised, a break or continue being hit, etc.. #[derive(Clone, Debug)] -enum UnwindReason { +pub(crate) enum UnwindReason { /// We are returning a value from a return statement. Returning { value: PyObjectRef }, @@ -222,6 +223,76 @@ impl Frame { } Ok(locals.clone()) } + + #[allow(dead_code)] + pub(crate) fn checkpoint_stack(&self, vm: &VirtualMachine) -> PyResult> { + let state = self.state.lock(); + if !state.blocks.is_empty() { + return Err(vm.new_runtime_error( + "checkpoint does not support active block stacks".to_owned(), + )); + } + Ok(state.stack.iter().cloned().collect()) + } + + /// Get the value stack without checking blocks (for multi-frame checkpoint support) + pub(crate) fn get_stack(&self, _vm: &VirtualMachine) -> PyResult> { + let state = self.state.lock(); + Ok(state.stack.iter().cloned().collect()) + } + + #[allow(dead_code)] + pub(crate) fn restore_stack( + &self, + stack: Vec, + vm: &VirtualMachine, + ) -> PyResult<()> { + let mut state = self.state.lock(); + if stack.len() > state.stack.capacity() { + return Err(vm.new_runtime_error( + "checkpoint stack exceeds frame capacity".to_owned(), + )); + } + state.stack.clear(); + for value in stack { + state.stack.push(value); + } + Ok(()) + } + + /// Push a value onto the frame's value stack + /// This is used when resuming from a checkpoint and an inner frame returns + pub(crate) fn push_stack_value(&self, value: PyObjectRef) { + let mut state = self.state.lock(); + state.stack.push(value); + } + + /// Push a block onto the frame's block stack + /// This is used when resuming from a checkpoint to restore control flow state + pub(crate) fn push_block(&self, block: Block) { + let mut state = self.state.lock(); + state.blocks.push(block); + } + + /// Get a clone of the current block stack + /// This is used when creating a checkpoint to save control flow state + pub(crate) fn get_blocks(&self) -> Vec { + let state = self.state.lock(); + state.blocks.clone() + } + + pub(crate) fn set_lasti(&self, value: u32) { + #[cfg(feature = "threading")] + { + let mut state = self.state.lock(); + state.lasti = value; + self.lasti.store(value, atomic::Ordering::Relaxed); + } + #[cfg(not(feature = "threading"))] + { + self.lasti.set(value); + } + } } impl Py { @@ -412,6 +483,93 @@ impl ExecutingFrame<'_> { if !do_extend_arg { arg_state.reset() } + if let Some(request) = maybe_checkpoint_request(vm, op, idx as u32) { + // Save checkpoint using the new multi-frame API + // Pass the current instruction index (which has already been validated as PopTop) + // The resume point is the next instruction after PopTop + let resume_lasti = (idx as u32).checked_add(1).ok_or_else(|| { + vm.new_runtime_error("checkpoint lasti overflow".to_owned()) + })?; + + // Collect current frame's stack and blocks (must do this while we still hold the state lock) + let current_stack: Vec = self.state.stack.iter().cloned().collect(); + let current_blocks: Vec = self.state.blocks.clone(); + + // Prepare locals dict for current frame (to avoid locking fastlocals later) + // For now, use a minimal approach to avoid any potential deadlocks + let current_locals = { + let locals_dict = vm.ctx.new_dict(); + // Try to lock fastlocals - if this fails/hangs, we have a problem + if let Some(fastlocals) = self.fastlocals.try_lock() { + for (idx, varname) in self.code.code.varnames.iter().enumerate() { + if let Some(value) = &fastlocals[idx] { + let _ = locals_dict.set_item(*varname, value.clone(), vm); + } + } + // Note: Not handling cell/free vars for now to avoid complexity + } + // If try_lock fails, use empty dict + Some(locals_dict.into()) + }; + + let save_result = match request.target { + crate::vm::CheckpointTarget::File(path) => { + checkpoint::save_checkpoint_with_lasti_stack_blocks_and_locals( + &vm, + &path, + resume_lasti, + current_stack, + current_blocks, + current_locals, + ) + .map(|_| None) + } + crate::vm::CheckpointTarget::Bytes => { + checkpoint::save_checkpoint_bytes_with_lasti_stack_blocks_and_locals( + &vm, + resume_lasti, + current_stack, + current_blocks, + current_locals, + ) + .map(Some) + } + }; + let checkpoint_bytes = match save_result { + Ok(bytes) => bytes, + Err(exc) => { + eprintln!(" Exception class: {}", exc.class().name()); + return Err(exc); + } + }; + if let Some(bytes) = checkpoint_bytes { + *vm.state.checkpoint_result.lock() = Some(bytes); + } + + // Flush output buffers before exiting + // Try to flush Python's stdout/stderr by accessing the sys module + if let Ok(sys_module) = vm.import("sys", 0) { + if let Ok(stdout) = sys_module.get_attr("stdout", vm) { + let _ = vm.call_method(&stdout, "flush", ()); + } + if let Ok(stderr) = sys_module.get_attr("stderr", vm) { + let _ = vm.call_method(&stderr, "flush", ()); + } + } + + // Also flush Rust-level output + use std::io::Write; + let _ = std::io::stdout().flush(); + let _ = std::io::stderr().flush(); + + if vm.state.config.settings.checkpoint_exit { + std::process::exit(0); + } + return Err(vm.new_exception_msg( + vm.ctx.exceptions.system_exit.to_owned(), + "checkpoint exit".to_owned(), + )); + } } } @@ -2554,6 +2712,27 @@ impl ExecutingFrame<'_> { } } +fn maybe_checkpoint_request( + vm: &VirtualMachine, + op: bytecode::Instruction, + idx: u32, +) -> Option { + let mut request = vm.state.checkpoint_request.lock(); + let Some(pending) = request.as_ref() else { + return None; + }; + if pending.expected_lasti != idx { + return None; + } + if op != bytecode::Instruction::PopTop { + *request = None; + return None; + } + let pending = pending.clone(); + *request = None; + Some(pending) +} + impl fmt::Debug for Frame { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let state = self.state.lock(); diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index d52a33884ce..a562561b991 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -804,7 +804,9 @@ impl PyObject { // we've been resurrected by __del__ Some(false) => Err(()), None => { - warn!("couldn't run __del__ method for object"); + if crate::vm::thread::has_vm() { + warn!("couldn't run __del__ method for object"); + } Ok(()) } } diff --git a/crates/vm/src/stdlib/mod.rs b/crates/vm/src/stdlib/mod.rs index 9fae516fe04..7ed6082135d 100644 --- a/crates/vm/src/stdlib/mod.rs +++ b/crates/vm/src/stdlib/mod.rs @@ -11,6 +11,7 @@ pub mod io; mod itertools; mod marshal; mod operator; +mod rustpython_checkpoint; // TODO: maybe make this an extension module, if we ever get those // mod re; mod sre; @@ -95,6 +96,7 @@ pub fn get_module_inits() -> StdlibMap { "_io" => io::make_module, "marshal" => marshal::make_module, "_operator" => operator::make_module, + "rustpython_checkpoint" => rustpython_checkpoint::make_module, "_signal" => signal::make_module, "_sre" => sre::make_module, "_stat" => stat::make_module, diff --git a/crates/vm/src/stdlib/rustpython_checkpoint.rs b/crates/vm/src/stdlib/rustpython_checkpoint.rs new file mode 100644 index 00000000000..bfc68f3203d --- /dev/null +++ b/crates/vm/src/stdlib/rustpython_checkpoint.rs @@ -0,0 +1,35 @@ +pub(crate) use rustpython_checkpoint::make_module; + +#[pymodule] +mod rustpython_checkpoint { + use crate::{PyResult, VirtualMachine, builtins::PyStrRef}; + use crate::vm::{CheckpointRequest, CheckpointTarget}; + + #[pyfunction] + fn checkpoint(path: PyStrRef, vm: &VirtualMachine) -> PyResult<()> { + let frame = vm + .current_frame() + .ok_or_else(|| vm.new_runtime_error("checkpoint requires an active frame".to_owned()))?; + let expected_lasti = frame.lasti(); + let mut request = vm.state.checkpoint_request.lock(); + *request = Some(crate::vm::CheckpointRequest { + target: CheckpointTarget::File(path.as_str().to_owned()), + expected_lasti, + }); + Ok(()) + } + + #[pyfunction] + fn checkpoint_bytes(vm: &VirtualMachine) -> PyResult<()> { + let frame = vm + .current_frame() + .ok_or_else(|| vm.new_runtime_error("checkpoint requires an active frame".to_owned()))?; + let expected_lasti = frame.lasti(); + let mut request = vm.state.checkpoint_request.lock(); + *request = Some(CheckpointRequest { + target: CheckpointTarget::Bytes, + expected_lasti, + }); + Ok(()) + } +} diff --git a/crates/vm/src/version.rs b/crates/vm/src/version.rs index 0a598842a56..94d1daa18bb 100644 --- a/crates/vm/src/version.rs +++ b/crates/vm/src/version.rs @@ -31,10 +31,13 @@ pub fn get_version() -> String { #[cfg(not(windows))] let msc_info = String::new(); + let pvm_version = env!("PVM_VERSION"); + format!( - "{:.80} ({:.80}) \n[RustPython {} with {:.80}{}]", // \n is PyPy convention + "{:.80} ({:.80}) \nPVM VERSION {} , based on RustPython {} with {:.80}{}", // \n is PyPy convention get_version_number(), get_build_info(), + pvm_version, env!("CARGO_PKG_VERSION"), COMPILER, msc_info, diff --git a/crates/vm/src/vm/checkpoint.rs b/crates/vm/src/vm/checkpoint.rs new file mode 100644 index 00000000000..46e46a9adf9 --- /dev/null +++ b/crates/vm/src/vm/checkpoint.rs @@ -0,0 +1,477 @@ +use crate::{ + PyPayload, PyResult, VirtualMachine, + builtins::{PyDictRef, code::PyCode}, + convert::TryFromObject, + frame::FrameRef, + scope::Scope, + vm::snapshot, +}; +use crate::bytecode; +use crate::builtins::function::PyFunction; +use std::fs; + +#[allow(dead_code)] +pub(crate) fn save_checkpoint(vm: &VirtualMachine, path: &str) -> PyResult<()> { + let frames = vm.frames.borrow(); + if frames.is_empty() { + return Err(vm.new_runtime_error("checkpoint requires an active frame".to_owned())); + } + + // Get all frames in the stack + let frame_refs: Vec<_> = frames.iter().map(|f| f.to_owned()).collect(); + drop(frames); // Release borrow + + + // Temporarily skip validation to avoid potential deadlock + // TODO: Re-enable validation after fixing the issue + // for frame in &frame_refs { + // validate_frame_for_checkpoint(vm, frame)?; + // } + + let data = save_checkpoint_bytes_from_frames(vm, &frame_refs, None)?; + fs::write(path, &data).map_err(|err| vm.new_os_error(format!("checkpoint write failed: {err}")))?; + Ok(()) +} + +// Version that accepts the innermost frame's resume_lasti (already validated) +pub(crate) fn save_checkpoint_with_lasti(vm: &VirtualMachine, path: &str, innermost_resume_lasti: u32) -> PyResult<()> { + save_checkpoint_with_lasti_stack_and_blocks(vm, path, innermost_resume_lasti, Vec::new(), Vec::new()) +} + +// Version that accepts both resume_lasti and the innermost frame's stack +pub(crate) fn save_checkpoint_with_lasti_stack_and_blocks( + vm: &VirtualMachine, + path: &str, + innermost_resume_lasti: u32, + innermost_stack: Vec, + innermost_blocks: Vec +) -> PyResult<()> { + save_checkpoint_with_lasti_stack_blocks_and_locals( + vm, path, innermost_resume_lasti, innermost_stack, innermost_blocks, None + ) +} + +// Version that also accepts prepared locals for innermost frame +pub(crate) fn save_checkpoint_with_lasti_stack_blocks_and_locals( + vm: &VirtualMachine, + path: &str, + innermost_resume_lasti: u32, + innermost_stack: Vec, + innermost_blocks: Vec, + innermost_locals: Option, +) -> PyResult<()> { + let frames = vm.frames.borrow(); + if frames.is_empty() { + return Err(vm.new_runtime_error("checkpoint requires an active frame".to_owned())); + } + + + // Get all frames in the stack + let frame_refs: Vec<_> = frames.iter().map(|f| f.to_owned()).collect(); + drop(frames); // Release borrow + + + let data = save_checkpoint_bytes_from_frames_with_stack_blocks_and_locals( + vm, + &frame_refs, + Some(innermost_resume_lasti), + innermost_stack, + innermost_blocks, + innermost_locals + )?; + fs::write(path, &data).map_err(|err| vm.new_os_error(format!("checkpoint write failed: {err}")))?; + Ok(()) +} + +pub(crate) fn save_checkpoint_bytes_with_lasti_stack_blocks_and_locals( + vm: &VirtualMachine, + innermost_resume_lasti: u32, + innermost_stack: Vec, + innermost_blocks: Vec, + innermost_locals: Option, +) -> PyResult> { + let frames = vm.frames.borrow(); + if frames.is_empty() { + return Err(vm.new_runtime_error("checkpoint requires an active frame".to_owned())); + } + + let frame_refs: Vec<_> = frames.iter().map(|f| f.to_owned()).collect(); + drop(frames); + + save_checkpoint_bytes_from_frames_with_stack_blocks_and_locals( + vm, + &frame_refs, + Some(innermost_resume_lasti), + innermost_stack, + innermost_blocks, + innermost_locals, + ) +} + +#[allow(dead_code)] +pub(crate) fn save_checkpoint_bytes(vm: &VirtualMachine) -> PyResult> { + let frames = vm.frames.borrow(); + if frames.is_empty() { + return Err(vm.new_runtime_error("checkpoint requires an active frame".to_owned())); + } + + // Get all frames in the stack + let frame_refs: Vec<_> = frames.iter().map(|f| f.to_owned()).collect(); + drop(frames); // Release borrow + + // Validate all frames + for frame in &frame_refs { + validate_frame_for_checkpoint(vm, frame)?; + } + + save_checkpoint_bytes_from_frames(vm, &frame_refs, None) +} + +pub(crate) fn save_checkpoint_from_exec( + vm: &VirtualMachine, + source_path: &str, + lasti: u32, + code: &PyCode, + globals: &PyDictRef, + path: &str, +) -> PyResult<()> { + let data = save_checkpoint_bytes_from_exec(vm, source_path, lasti, code, globals)?; + fs::write(path, &data).map_err(|err| vm.new_os_error(format!("checkpoint write failed: {err}")))?; + Ok(()) +} + +pub(crate) fn save_checkpoint_bytes_from_exec( + vm: &VirtualMachine, + source_path: &str, + lasti: u32, + code: &PyCode, + globals: &PyDictRef, +) -> PyResult> { + snapshot::dump_checkpoint_state(vm, source_path, lasti, code, globals) +} + +pub(crate) fn resume_script_from_checkpoint( + vm: &VirtualMachine, + _scope: Scope, + script_path: &str, + checkpoint_path: &str, +) -> PyResult<()> { + let data = fs::read(checkpoint_path) + .map_err(|err| vm.new_os_error(format!("checkpoint read failed: {err}")))?; + resume_script_from_bytes(vm, script_path, &data) +} + +pub(crate) fn resume_script_from_bytes( + vm: &VirtualMachine, + script_path: &str, + data: &[u8], +) -> PyResult<()> { + let (state, objects) = snapshot::load_checkpoint_state(vm, data)?; + + if state.source_path != script_path { + return Err(vm.new_value_error(format!( + "checkpoint source_path '{}' does not match script '{}'", + state.source_path, script_path + ))); + } + + // Get globals + let globals_obj = objects + .get(state.root as usize) + .cloned() + .ok_or_else(|| vm.new_runtime_error("checkpoint globals missing".to_owned()))?; + let globals_dict = PyDictRef::try_from_object(vm, globals_obj)?; + + if !globals_dict.contains_key("__file__", vm) { + globals_dict.set_item("__file__", vm.ctx.new_str(script_path).into(), vm)?; + globals_dict.set_item("__cached__", vm.ctx.none(), vm)?; + } + + // Rebuild all frames from bottom to top + let mut frame_refs = Vec::new(); + for (i, frame_state) in state.frames.iter().enumerate() { + let code = snapshot::decode_code_object(vm, &frame_state.code) + .map_err(|err| vm.new_value_error(format!("checkpoint frame {i} code invalid: {err:?}")))?; + let code_obj: crate::PyRef = vm.ctx.new_pyref(PyCode::new(code)); + + // Get locals for this frame + let locals_obj = objects + .get(frame_state.locals as usize) + .cloned() + .ok_or_else(|| vm.new_runtime_error(format!("checkpoint frame {i} locals missing")))?; + + let locals_dict = PyDictRef::try_from_object(vm, locals_obj.clone())?; + + let varnames = &code_obj.code.varnames; + + // Try to iterate all keys in the dict + let dict_items: Vec<_> = locals_dict.clone().into_iter().collect(); + for (key, value) in dict_items.iter() { + if let Some(key_str) = key.downcast_ref::() { + } + } + + // Debug: check what's in locals_dict BEFORE creating the frame + for varname in varnames.iter() { + if let Some(value) = locals_dict.get_item_opt(*varname, vm)? { + } else { + } + } + + // Create ArgMapping from locals dict + let locals_mapping = crate::function::ArgMapping::from_dict_exact(locals_dict.clone()); + + // Create scope with locals and globals + let scope = Scope::with_builtins(Some(locals_mapping), globals_dict.clone(), vm); + let func = PyFunction::new(code_obj.clone(), globals_dict.clone(), vm)?; + let func_obj = func.into_ref(&vm.ctx).into(); + let frame = crate::frame::Frame::new(code_obj.clone(), scope, vm.builtins.dict(), &[], Some(func_obj), vm) + .into_ref(&vm.ctx); + + // Restore fastlocals from the locals dict + let mut fastlocals = frame.fastlocals.lock(); + for (idx, varname) in varnames.iter().enumerate() { + if let Some(value) = locals_dict.get_item_opt(*varname, vm)? { + fastlocals[idx] = Some(value); + } else { + } + } + drop(fastlocals); + + // Restore the value stack + for stack_item_id in &frame_state.stack { + let stack_obj = objects + .get(*stack_item_id as usize) + .cloned() + .ok_or_else(|| vm.new_runtime_error(format!("checkpoint frame {i} stack item {} missing", stack_item_id)))?; + frame.push_stack_value(stack_obj); + } + + // Restore block stack + for block_state in &frame_state.blocks { + let block = snapshot::convert_block_state_to_block(block_state, &objects, vm)?; + frame.push_block(block); + } + + if frame_state.lasti as usize >= frame.code.instructions.len() { + return Err(vm.new_value_error( + format!("checkpoint frame {i} lasti is out of range for current bytecode"), + )); + } + frame.set_lasti(frame_state.lasti); + frame_refs.push(frame); + } + + + if frame_refs.len() == 1 { + // Simple case: only one frame, just run it + let result = vm.run_frame(frame_refs[0].clone()); + vm.frames.borrow_mut().clear(); + return result.map(drop); + } + + // Multiple frames: need to execute inner frames first, then continue outer frames + // Push all outer frames to VM stack (they are waiting for inner frames to return) + for i in 0..frame_refs.len() - 1 { + vm.frames.borrow_mut().push(frame_refs[i].clone()); + } + + // Run the innermost frame using vm.run_frame + let innermost_frame = frame_refs.last().unwrap().clone(); + let inner_result = vm.run_frame(innermost_frame); + + // If inner frame failed, clean up and return error + let inner_return_val = match inner_result { + Ok(val) => val, + Err(e) => { + vm.frames.borrow_mut().clear(); + return Err(e); + } + }; + + // Push the inner frame's return value to the caller's (outer frame's) stack + let caller_frame = &frame_refs[frame_refs.len() - 2]; + caller_frame.push_stack_value(inner_return_val); + + // Inner frame succeeded. Now continue executing outer frames + // The return value from inner frame should be on the caller's stack already + // We need to continue executing from the outermost frame + for i in (0..frame_refs.len() - 1).rev() { + let frame = frame_refs[i].clone(); + + // Use frame.run() directly since frame is already on VM stack + let result = frame.run(vm); + + match result { + Ok(crate::frame::ExecutionResult::Return(val)) => { + // Frame returned normally + // Pop this frame + vm.frames.borrow_mut().pop(); + + // If there's an outer frame, push the return value to its stack + if i > 0 { + frame_refs[i - 1].push_stack_value(val); + } else { + // This was the outermost frame, we're done + vm.frames.borrow_mut().clear(); + return Ok(()); + } + } + Err(e) => { + // Error occurred + vm.frames.borrow_mut().clear(); + return Err(e); + } + Ok(_other) => { + vm.frames.borrow_mut().clear(); + return Err(vm.new_runtime_error("unexpected execution result (not Return)".to_owned())); + } + } + } + + vm.frames.borrow_mut().clear(); + Ok(()) +} + +#[allow(dead_code)] +fn compute_resume_lasti(vm: &VirtualMachine, frame: &FrameRef) -> PyResult { + let lasti = frame.lasti(); + let next = frame + .code + .instructions + .get(lasti as usize) + .ok_or_else(|| vm.new_runtime_error("checkpoint out of range".to_owned()))?; + if next.op != bytecode::Instruction::PopTop { + return Err(vm.new_value_error( + "checkpoint() must be used as a standalone statement".to_owned(), + )); + } + lasti + .checked_add(1) + .ok_or_else(|| vm.new_runtime_error("checkpoint lasti overflow".to_owned())) +} + +#[allow(dead_code)] +fn validate_frame_for_checkpoint(vm: &VirtualMachine, frame: &FrameRef) -> PyResult<()> { + // Check value stack is empty + let stack = frame.checkpoint_stack(vm)?; + if !stack.is_empty() { + return Err(vm.new_value_error( + "checkpoint requires an empty value stack in all frames".to_owned(), + )); + } + + // Validate instruction pointer + let lasti = frame.lasti(); + let next = frame + .code + .instructions + .get(lasti as usize) + .ok_or_else(|| vm.new_runtime_error("checkpoint out of range".to_owned()))?; + if next.op != bytecode::Instruction::PopTop { + return Err(vm.new_value_error( + "checkpoint() must be used as a standalone statement".to_owned(), + )); + } + + Ok(()) +} + +fn save_checkpoint_bytes_from_frames( + vm: &VirtualMachine, + frames: &[FrameRef], + innermost_resume_lasti: Option, // If provided, use this for the innermost frame +) -> PyResult> { + save_checkpoint_bytes_from_frames_with_stack(vm, frames, innermost_resume_lasti, Vec::new()) +} + +fn save_checkpoint_bytes_from_frames_with_stack( + vm: &VirtualMachine, + frames: &[FrameRef], + innermost_resume_lasti: Option, + innermost_stack: Vec, +) -> PyResult> { + save_checkpoint_bytes_from_frames_with_stack_and_blocks( + vm, + frames, + innermost_resume_lasti, + innermost_stack, + Vec::new() // Empty blocks for compatibility + ) +} + +fn save_checkpoint_bytes_from_frames_with_stack_and_blocks( + vm: &VirtualMachine, + frames: &[FrameRef], + innermost_resume_lasti: Option, + innermost_stack: Vec, + innermost_blocks: Vec, +) -> PyResult> { + save_checkpoint_bytes_from_frames_with_stack_blocks_and_locals( + vm, frames, innermost_resume_lasti, innermost_stack, innermost_blocks, None + ) +} + +fn save_checkpoint_bytes_from_frames_with_stack_blocks_and_locals( + vm: &VirtualMachine, + frames: &[FrameRef], + innermost_resume_lasti: Option, + innermost_stack: Vec, + innermost_blocks: Vec, + innermost_locals: Option, +) -> PyResult> { + if frames.is_empty() { + return Err(vm.new_runtime_error("no frames to checkpoint".to_owned())); + } + + // Get source path from the outermost (first) frame + let source_path = frames[0].code.source_path.as_str(); + + // Build blocks vec: only innermost frame gets blocks, others get empty vec. + // Outer frames are waiting for inner frames to return and their block state + // can be safely reconstructed as empty since they're not in active control flow. + let mut all_blocks = vec![Vec::new(); frames.len()]; + if !frames.is_empty() { + all_blocks[frames.len() - 1] = innermost_blocks; + } + + // Collect frame states + let mut frame_states = Vec::new(); + for (idx, frame) in frames.iter().enumerate() { + // Only the innermost (last) frame needs special handling + let is_innermost = idx == frames.len() - 1; + let resume_lasti = if is_innermost { + // If innermost_resume_lasti is provided, use it (already validated) + // Otherwise compute it (for backward compatibility) + if let Some(lasti) = innermost_resume_lasti { + lasti + } else { + compute_resume_lasti(vm, frame)? + } + } else { + // For non-innermost frames, just use current lasti + frame.lasti() + }; + frame_states.push((frame, resume_lasti)); + } + + snapshot::dump_checkpoint_frames_with_all_blocks_and_locals( + vm, + source_path, + &frame_states, + innermost_stack, + all_blocks, + innermost_locals + ) +} + +#[allow(dead_code)] +fn ensure_supported_frame(vm: &VirtualMachine, frame: &FrameRef) -> PyResult<()> { + if vm.frames.borrow().len() != 1 { + return Err(vm.new_runtime_error( + "checkpoint only supports top-level module frames".to_owned(), + )); + } + validate_frame_for_checkpoint(vm, frame)?; + Ok(()) +} diff --git a/crates/vm/src/vm/compile.rs b/crates/vm/src/vm/compile.rs index b5f10e47aa1..f1ccef84d89 100644 --- a/crates/vm/src/vm/compile.rs +++ b/crates/vm/src/vm/compile.rs @@ -5,6 +5,7 @@ use crate::{ convert::TryFromObject, scope::Scope, }; +use crate::vm::checkpoint; impl VirtualMachine { pub fn compile( @@ -50,6 +51,29 @@ impl VirtualMachine { self.run_any_file(scope, path) } + pub fn run_script_resume(&self, scope: Scope, path: &str, checkpoint_path: &str) -> PyResult<()> { + if get_importer(path, self)?.is_some() { + return Err(self.new_runtime_error( + "checkpoint resume does not support importers".to_owned(), + )); + } + + if !self.state.config.settings.safe_path { + let dir = std::path::Path::new(path) + .parent() + .unwrap() + .to_str() + .unwrap(); + self.insert_sys_path(self.new_pyobj(dir))?; + } + + checkpoint::resume_script_from_checkpoint(self, scope, path, checkpoint_path) + } + + pub fn resume_from_bytes(&self, script_path: &str, data: &[u8]) -> PyResult<()> { + checkpoint::resume_script_from_bytes(self, script_path, data) + } + // = _PyRun_AnyFileObject fn run_any_file(&self, scope: Scope, path: &str) -> PyResult<()> { let path = if path.is_empty() { "???" } else { path }; diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 34092454059..0c856122b80 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -6,6 +6,8 @@ #[cfg(feature = "rustpython-compiler")] mod compile; mod context; +pub(crate) mod checkpoint; +pub(crate) mod snapshot; mod interpreter; mod method; mod setting; @@ -50,7 +52,7 @@ use std::{ pub use context::Context; pub use interpreter::Interpreter; pub(crate) use method::PyMethod; -pub use setting::{CheckHashPycsMode, Paths, PyConfig, Settings}; +pub use setting::{CheckHashPycsMode, ContinuationMode, Paths, PyConfig, Settings}; pub const MAX_MEMORY_SIZE: usize = isize::MAX as usize; @@ -103,6 +105,20 @@ pub struct PyGlobalState { pub after_forkers_parent: PyMutex>, pub int_max_str_digits: AtomicCell, pub switch_interval: AtomicCell, + pub(crate) checkpoint_request: PyMutex>, + pub(crate) checkpoint_result: PyMutex>>, +} + +#[derive(Clone)] +pub(crate) struct CheckpointRequest { + pub target: CheckpointTarget, + pub expected_lasti: u32, +} + +#[derive(Clone)] +pub(crate) enum CheckpointTarget { + File(String), + Bytes, } pub fn process_hash_secret_seed() -> u32 { @@ -187,6 +203,8 @@ impl VirtualMachine { after_forkers_parent: PyMutex::default(), int_max_str_digits, switch_interval: AtomicCell::new(0.005), + checkpoint_request: PyMutex::default(), + checkpoint_result: PyMutex::default(), }), initialized: false, recursion_depth: Cell::new(0), @@ -506,9 +524,17 @@ impl VirtualMachine { pub fn compile_opts(&self) -> crate::compiler::CompileOpts { crate::compiler::CompileOpts { optimize: self.state.config.settings.optimize, + pvm_fsm: matches!( + self.state.config.settings.continuation_mode, + Some(setting::ContinuationMode::Fsm) + ), } } + pub fn take_checkpoint_bytes(&self) -> Option> { + self.state.checkpoint_result.lock().take() + } + // To be called right before raising the recursion depth. fn check_recursive_call(&self, _where: &str) -> PyResult<()> { if self.recursion_depth.get() >= self.recursion_limit.get() { diff --git a/crates/vm/src/vm/setting.rs b/crates/vm/src/vm/setting.rs index 53e2cef1160..19b6f2b0976 100644 --- a/crates/vm/src/vm/setting.rs +++ b/crates/vm/src/vm/setting.rs @@ -70,6 +70,9 @@ pub struct Settings { /// sys.argv pub argv: Vec, + /// RustPython checkpoint resume path + pub resume_path: Option, + // spell-checker:ignore Xfoo /// -Xfoo[=bar] pub xoptions: Vec<(String, Option)>, @@ -138,6 +141,12 @@ pub struct Settings { /// -O optimization switch counter pub optimize: u8, + /// PVM continuation mode (FSM or checkpoint) + pub continuation_mode: Option, + + /// Allow checkpoint to exit the process (CLI default) + pub checkpoint_exit: bool, + /// -E pub ignore_environment: bool, @@ -159,6 +168,13 @@ pub enum CheckHashPycsMode { Never, } +#[derive(Debug, Copy, Clone, Eq, PartialEq, strum_macros::Display, strum_macros::EnumString)] +#[strum(serialize_all = "lowercase")] +pub enum ContinuationMode { + Fsm, + Checkpoint, +} + impl Settings { pub fn with_path(mut self, path: String) -> Self { self.path_list.push(path); @@ -174,6 +190,8 @@ impl Default for Settings { inspect: false, interactive: false, optimize: 0, + continuation_mode: None, + checkpoint_exit: true, install_signal_handlers: true, user_site_directory: true, import_site: true, @@ -190,6 +208,7 @@ impl Default for Settings { warnoptions: vec![], path_list: vec![], argv: vec![], + resume_path: None, hash_seed: None, faulthandler: false, buffered_stdio: true, diff --git a/crates/vm/src/vm/snapshot.rs b/crates/vm/src/vm/snapshot.rs new file mode 100644 index 00000000000..2d8965ab0dd --- /dev/null +++ b/crates/vm/src/vm/snapshot.rs @@ -0,0 +1,3667 @@ +use crate::{ + AsObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, + builtins::{ + PyClassMethod, PyDictRef, PyFloat, PyInt, PyList, PyModule, PyStaticMethod, PyStr, PyTuple, + PyWeak, + code::{PyCode, CodeObject, PyObjBag}, + dict::PyDict, + function::{PyCell, PyFunction}, + set::{PyFrozenSet, PySet}, + type_::PyType, + }, + convert::TryFromObject, + protocol::PyIterReturn, +}; +use rustpython_compiler_core::marshal; +use rustpython_compiler_core::bytecode; +use std::collections::HashMap; + +// Block conversion functions are defined at the end of this file + +pub(crate) type ObjId = u32; + +const SNAPSHOT_VERSION: u32 = 3; + +#[derive(Debug)] +pub(crate) struct CheckpointState { + pub version: u32, + pub source_path: String, + pub frames: Vec, // Frame stack (outermost first) + pub root: ObjId, // Global namespace + pub objects: Vec, +} + +#[derive(Debug)] +pub(crate) struct FrameState { + pub code: Vec, // Marshaled code object + pub lasti: u32, // Instruction pointer + pub locals: ObjId, // Local variables dict + pub stack: Vec, // Value stack (for loop iterators, etc.) + pub blocks: Vec, // Block stack (for loops, try/except) +} + +impl Default for FrameState { + fn default() -> Self { + Self { + code: Vec::new(), + lasti: 0, + locals: 0, + stack: Vec::new(), + blocks: Vec::new(), + } + } +} + +/// Serializable representation of a block stack entry +#[derive(Debug, Clone)] +pub(crate) struct BlockState { + pub typ: BlockTypeState, + pub level: usize, +} + +/// Serializable representation of block types +#[derive(Debug, Clone)] +pub(crate) enum BlockTypeState { + Loop, + TryExcept { handler: u32 }, + Finally { handler: u32 }, + FinallyHandler { + reason: Option, + prev_exc: Option, + }, + ExceptHandler { + prev_exc: Option, + }, +} + +/// Serializable representation of unwind reasons +#[derive(Debug, Clone)] +pub(crate) enum UnwindReasonState { + Returning { value: ObjId }, + Raising { exception: ObjId }, + Break { target: u32 }, + Continue { target: u32 }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +enum ObjTag { + None = 0, + Bool = 1, + Int = 2, + Float = 3, + Str = 4, + Bytes = 5, + List = 6, + Tuple = 7, + Dict = 8, + Set = 9, + FrozenSet = 10, + Module = 11, + Function = 12, + Code = 13, + Type = 14, + BuiltinType = 15, + Instance = 16, + Cell = 17, + BuiltinModule = 18, + BuiltinDict = 19, + BuiltinFunction = 20, + Enumerate = 21, + Zip = 22, + Map = 23, + Filter = 24, + ListIterator = 25, + RangeIterator = 26, + Range = 27, +} + +#[derive(Debug)] +pub(crate) struct ObjectEntry { + tag: ObjTag, + payload: ObjectPayload, +} + +#[derive(Debug)] +enum ObjectPayload { + None, + Bool(bool), + Int(String), + Float(f64), + Str(String), + Bytes(Vec), + List(Vec), + Tuple(Vec), + Dict(Vec<(ObjId, ObjId)>), + Set(Vec), + FrozenSet(Vec), + Module { name: String, dict: ObjId }, + BuiltinModule { name: String }, + BuiltinDict { name: String }, + Function(FunctionPayload), + Enumerate { iterator: ObjId, count: i64 }, + Zip { iterators: Vec }, + Map { function: ObjId, iterator: ObjId }, + Filter { function: ObjId, iterator: ObjId }, + ListIterator { list: ObjId, position: usize }, + RangeIterator { range: ObjId, position: usize }, + Range { start: i64, stop: i64, step: i64 }, + BuiltinFunction(BuiltinFunctionPayload), + Code(Vec), + Type(TypePayload), + BuiltinType { module: String, name: String }, + Instance(InstancePayload), + Cell(Option), +} + +#[derive(Debug)] +struct FunctionPayload { + code: ObjId, + globals: ObjId, + defaults: Option, + kwdefaults: Option, + closure: Option, + name: ObjId, + qualname: ObjId, + annotations: ObjId, + module: ObjId, + doc: ObjId, + type_params: ObjId, +} + +#[derive(Debug)] +struct TypePayload { + name: String, + qualname: String, + bases: Vec, + dict: ObjId, + flags: u64, + basicsize: usize, + itemsize: usize, + member_count: usize, +} + +#[derive(Debug)] +struct InstancePayload { + typ: ObjId, + state: Option, + new_args: Option, + new_kwargs: Option, +} + +#[derive(Debug)] +struct BuiltinFunctionPayload { + name: String, + module: Option, + self_obj: Option, +} + +#[allow(dead_code)] +#[derive(Debug)] +pub(crate) enum SnapshotError { + Message(String), +} + +impl SnapshotError { + fn msg(msg: impl Into) -> Self { + Self::Message(msg.into()) + } +} + +pub(crate) fn dump_checkpoint_frames( + vm: &VirtualMachine, + source_path: &str, + frames: &[(&crate::frame::FrameRef, u32)], // (frame, resume_lasti) +) -> PyResult> { + dump_checkpoint_frames_with_stack(vm, source_path, frames, Vec::new()) +} + +pub(crate) fn dump_checkpoint_frames_with_stack( + vm: &VirtualMachine, + source_path: &str, + frames: &[(&crate::frame::FrameRef, u32)], // (frame, resume_lasti) + innermost_stack: Vec, // Stack of the innermost frame +) -> PyResult> { + dump_checkpoint_frames_with_stack_and_blocks( + vm, + source_path, + frames, + innermost_stack, + Vec::new() // Empty blocks for compatibility + ) +} + +pub(crate) fn dump_checkpoint_frames_with_stack_and_blocks( + vm: &VirtualMachine, + source_path: &str, + frames: &[(&crate::frame::FrameRef, u32)], // (frame, resume_lasti) + innermost_stack: Vec, // Stack of the innermost frame + innermost_blocks: Vec, // Blocks of the innermost frame +) -> PyResult> { + // Build blocks vec with innermost blocks + let mut all_blocks = vec![Vec::new(); frames.len()]; + if !frames.is_empty() { + all_blocks[frames.len() - 1] = innermost_blocks; + } + dump_checkpoint_frames_with_all_blocks(vm, source_path, frames, innermost_stack, all_blocks) +} + +pub(crate) fn dump_checkpoint_frames_with_all_blocks( + vm: &VirtualMachine, + source_path: &str, + frames: &[(&crate::frame::FrameRef, u32)], // (frame, resume_lasti) + innermost_stack: Vec, // Stack of the innermost frame + all_blocks: Vec>, // Blocks for all frames +) -> PyResult> { + dump_checkpoint_frames_with_all_blocks_and_locals( + vm, source_path, frames, innermost_stack, all_blocks, None + ) +} + +pub(crate) fn dump_checkpoint_frames_with_all_blocks_and_locals( + vm: &VirtualMachine, + source_path: &str, + frames: &[(&crate::frame::FrameRef, u32)], // (frame, resume_lasti) + innermost_stack: Vec, // Stack of the innermost frame + all_blocks: Vec>, // Blocks for all frames + innermost_locals: Option, // Pre-prepared locals for innermost frame +) -> PyResult> { + use crate::builtins::PyDictRef; + + // STEP 1: Prepare all locals dicts BEFORE creating SnapshotWriter + let mut locals_dicts = Vec::new(); + for (idx, (frame, _resume_lasti)) in frames.iter().enumerate() { + let is_innermost = idx == frames.len() - 1; + let locals_dict = if idx == 0 { + // For module-level frame (first frame), use globals as locals + // This ensures that module-level variables defined during execution are captured + frame.globals.clone() + } else if is_innermost && innermost_locals.is_some() { + // For innermost frame, use pre-prepared locals to avoid deadlock + PyDictRef::try_from_object(vm, innermost_locals.clone().unwrap())? + } else { + // For other function frames, create a new dict and copy fastlocals + // This is safe because these frames are not actively executing + let locals_dict = vm.ctx.new_dict(); + + // Copy fastlocals into the new dict + let varnames = &frame.code.code.varnames; + let fastlocals = frame.fastlocals.lock(); + for (idx, varname) in varnames.iter().enumerate() { + if let Some(value) = &fastlocals[idx] { + locals_dict.set_item(*varname, value.clone(), vm)?; + } + } + drop(fastlocals); + + // Also copy cell/free vars if any + if !frame.code.code.cellvars.is_empty() || !frame.code.code.freevars.is_empty() { + let all_vars = frame.code.code.cellvars.iter().chain(frame.code.code.freevars.iter()); + for (idx, varname) in all_vars.enumerate() { + if let Some(cell) = frame.cells_frees.get(idx) { + if let Some(value) = cell.get() { + locals_dict.set_item(*varname, value, vm)?; + } + } + } + } + + locals_dict + }; + + locals_dicts.push(locals_dict); + } + + // STEP 2: Collect value stacks from all frames + let mut stack_items = Vec::new(); + for (idx, (_frame, _resume_lasti)) in frames.iter().enumerate() { + let is_innermost = idx == frames.len() - 1; + let stack_result = if is_innermost { + // Use the provided stack for the innermost frame + innermost_stack.clone() + } else { + // For outer frames, use empty stack + // They are waiting for inner frames to return, and their stack state + // will be reconstructed during resume (return value will be pushed) + Vec::new() + }; + stack_items.push(stack_result); + } + + // STEP 3: Create writer and do a SINGLE serialization pass + // Get globals (from the first frame) + let globals = &frames[0].0.globals; + + // STEP 3: Create writer and do a SINGLE serialization pass + // Create a container tuple that holds: globals, all locals dicts, and all stack lists + let mut container_items = vec![globals.clone().into()]; + for locals_dict in locals_dicts.iter() { + container_items.push(locals_dict.clone().into()); + } + for stack in stack_items.iter() { + let stack_list = vm.ctx.new_list(stack.clone()); + container_items.push(stack_list.into()); + } + let container = vm.ctx.new_tuple(container_items); + + // Now serialize the container (this will serialize everything in one pass) + let mut writer = SnapshotWriter::new(vm); + let container_obj = container.into(); + let _container_id = writer.serialize_obj(&container_obj).map_err(|err| { + vm.new_value_error(format!("checkpoint snapshot failed: {err:?}")) + })?; + + // Now get the IDs for globals and each locals dict + let globals_obj = globals.as_object().to_owned(); + let root = writer.get_id(&globals_obj).map_err(|err| { + vm.new_value_error(format!("globals not found: {err:?}")) + })?; + + // Build frame states with correct locals IDs and stack IDs + let mut frame_states = Vec::new(); + for (_idx, (((frame, resume_lasti), locals_dict), stack)) in + frames.iter().zip(locals_dicts.iter()).zip(stack_items.iter()).enumerate() { + let code_bytes = serialize_code_object(&frame.code.code); + + let locals_obj = locals_dict.clone().into(); + let locals_id = writer.get_id(&locals_obj).map_err(|err| { + vm.new_value_error(format!("frame {} locals not found: {err:?}", _idx)) + })?; + + // Get IDs for all stack items + let mut stack_ids = Vec::new(); + for stack_item in stack.iter() { + let item_id = writer.get_id(stack_item).map_err(|err| { + vm.new_value_error(format!("frame {} stack item not found: {err:?}", _idx)) + })?; + stack_ids.push(item_id); + } + + // Get blocks from frame + let blocks = all_blocks.get(_idx).cloned().unwrap_or_else(Vec::new); + + // Convert blocks to BlockState for serialization + let mut block_states = Vec::new(); + for (_block_idx, block) in blocks.iter().enumerate() { + let block_state = convert_block_to_state(block, &writer)?; + block_states.push(block_state); + } + + frame_states.push(FrameState { + code: code_bytes, + lasti: *resume_lasti, + locals: locals_id, + stack: stack_ids, + blocks: block_states, + }); + } + + let state = CheckpointState { + version: SNAPSHOT_VERSION, + source_path: source_path.to_owned(), + frames: frame_states, + root, + objects: writer.objects, + }; + Ok(encode_checkpoint_state(&state)) +} + +// Keep the old function for backward compatibility +pub(crate) fn dump_checkpoint_state( + vm: &VirtualMachine, + source_path: &str, + lasti: u32, + code: &PyCode, + globals: &PyDictRef, +) -> PyResult> { + let mut writer = SnapshotWriter::new(vm); + let root = writer.serialize_obj(&globals.as_object().to_owned()).map_err(|err| { + vm.new_value_error(format!("checkpoint snapshot failed: {err:?}")) + })?; + + let code_bytes = serialize_code_object(&code.code); + // Convert to new format with single frame + let frame_state = FrameState { + code: code_bytes, + lasti, + locals: root, // For module-level, locals == globals + stack: Vec::new(), // Legacy path, assume empty stack + blocks: Vec::new(), // Legacy path, assume empty blocks + }; + + let state = CheckpointState { + version: SNAPSHOT_VERSION, + source_path: source_path.to_owned(), + frames: vec![frame_state], + root, + objects: writer.objects, + }; + Ok(encode_checkpoint_state(&state)) +} + +pub(crate) fn load_checkpoint_state( + vm: &VirtualMachine, + data: &[u8], +) -> PyResult<(CheckpointState, Vec)> { + let state = decode_checkpoint_state(data) + .map_err(|err| vm.new_value_error(format!("checkpoint decode failed: {err:?}")))?; + if state.version != SNAPSHOT_VERSION { + return Err(vm.new_value_error(format!( + "unsupported checkpoint version: {}", + state.version + ))); + } + let reader = SnapshotReader::new(vm, &state.objects, state.root); + let objects = reader + .restore_all() + .map_err(|err| vm.new_value_error(format!("checkpoint restore failed: {err:?}")))?; + Ok((state, objects)) +} + +pub(crate) fn decode_code_object( + vm: &VirtualMachine, + bytes: &[u8], +) -> Result { + deserialize_code_object(vm, bytes) +} + +fn serialize_code_object(code: &CodeObject) -> Vec { + let mut buf = Vec::new(); + marshal::serialize_code(&mut buf, code); + buf +} + +fn deserialize_code_object(vm: &VirtualMachine, bytes: &[u8]) -> Result { + let mut cursor = marshal::Cursor { data: bytes, position: 0 }; + marshal::deserialize_code(&mut cursor, PyObjBag(&vm.ctx)).map_err(|e| { + SnapshotError::msg(format!("failed to deserialize code object: {e:?}")) + }) +} + +struct SnapshotWriter<'a> { + vm: &'a VirtualMachine, + ids: HashMap, + objects: Vec, + held: Vec, + /// Cache for dynamically created Type attribute dicts: type_ptr -> dict_obj + type_attr_dicts: HashMap, + /// Cache for instance newargs/kwargs/state: obj_ptr -> (newargs, newkwargs, state) + instance_data: HashMap, Option, Option)>, +} + +impl<'a> SnapshotWriter<'a> { + fn new(vm: &'a VirtualMachine) -> Self { + Self { + vm, + ids: HashMap::new(), + objects: Vec::new(), + held: Vec::new(), + type_attr_dicts: HashMap::new(), + instance_data: HashMap::new(), + } + } + + /// Two-pass serialization: first assign IDs, then build payloads + fn serialize_obj(&mut self, obj: &PyObjectRef) -> Result { + // Phase 1: Assign IDs to all reachable objects + self.assign_ids_phase(obj)?; + + // Phase 2: Build payloads for all objects in ID order + self.build_payloads_phase()?; + + // Return the root object's ID + let ptr = obj.as_object().as_raw() as usize; + Ok(*self.ids.get(&ptr).unwrap()) + } + + /// Phase 1: Recursively assign IDs to all objects in the graph + fn assign_ids_phase(&mut self, obj: &PyObjectRef) -> Result<(), SnapshotError> { + // Check recursion depth to prevent stack overflow + static MAX_DEPTH: usize = 100000; + if self.held.len() > MAX_DEPTH { + return Err(SnapshotError::msg("recursion depth exceeded")); + } + + let ptr = obj.as_object().as_raw() as usize; + if self.ids.contains_key(&ptr) { + return Ok(()); // Already visited + } + + let id = self.held.len() as ObjId; + self.ids.insert(ptr, id); + self.held.push(obj.clone()); + + // Recursively visit child objects + self.visit_children(obj)?; + Ok(()) + } + + /// Visit all child objects for ID assignment + fn visit_children(&mut self, obj: &PyObjectRef) -> Result<(), SnapshotError> { + let tag = classify_obj(self.vm, obj)?; + + match tag { + ObjTag::None | ObjTag::Bool | ObjTag::Int | ObjTag::Float | + ObjTag::Str | ObjTag::Bytes | ObjTag::Code | ObjTag::BuiltinType | + ObjTag::BuiltinModule | ObjTag::BuiltinDict => { + // No child objects to visit + Ok(()) + } + ObjTag::BuiltinFunction => { + // Visit __self__ if present + if let Some(self_obj) = get_attr_opt(self.vm, obj, "__self__")? { + if !self.vm.is_none(&self_obj) { + self.assign_ids_phase(&self_obj)?; + } + } + Ok(()) + } + ObjTag::List => { + let list = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected list"))?; + for item in list.borrow_vec().iter() { + self.assign_ids_phase(item)?; + } + Ok(()) + } + ObjTag::Tuple => { + let tuple = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected tuple"))?; + for item in tuple.iter() { + self.assign_ids_phase(item)?; + } + Ok(()) + } + ObjTag::Dict => { + let dict = PyDictRef::try_from_object(self.vm, obj.clone()) + .map_err(|_| SnapshotError::msg("expected dict"))?; + for (key, value) in &dict { + self.assign_ids_phase(&key)?; + self.assign_ids_phase(&value)?; + } + Ok(()) + } + ObjTag::Set => { + let set = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected set"))?; + for key in set.elements() { + self.assign_ids_phase(&key)?; + } + Ok(()) + } + ObjTag::FrozenSet => { + let set = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected frozenset"))?; + for key in set.elements() { + self.assign_ids_phase(&key)?; + } + Ok(()) + } + ObjTag::Module => { + let dict = obj.dict().ok_or_else(|| SnapshotError::msg("module missing dict"))?; + self.assign_ids_phase(&dict.into())?; + Ok(()) + } + ObjTag::Function => { + self.assign_ids_phase(&get_attr(self.vm, obj, "__code__")?)?; + self.assign_ids_phase(&get_attr(self.vm, obj, "__globals__")?)?; + + let defaults_obj = get_attr_opt(self.vm, obj, "__defaults__")?.unwrap_or_else(|| self.vm.ctx.none()); + if !self.vm.is_none(&defaults_obj) && defaults_obj.downcast_ref::().is_some() { + self.assign_ids_phase(&defaults_obj)?; + } + + // For kwdefaults and annotations, just visit the original object + // Conversion will be done in phase 2 + let kwdefaults_obj = get_attr_opt(self.vm, obj, "__kwdefaults__")?.unwrap_or_else(|| self.vm.ctx.none()); + if !self.vm.is_none(&kwdefaults_obj) { + self.assign_ids_phase(&kwdefaults_obj)?; + } + + let closure_obj = get_attr(self.vm, obj, "__closure__")?; + if !self.vm.is_none(&closure_obj) && closure_obj.downcast_ref::().is_some() { + self.assign_ids_phase(&closure_obj)?; + } + + self.assign_ids_phase(&get_attr(self.vm, obj, "__name__")?)?; + self.assign_ids_phase(&get_attr(self.vm, obj, "__qualname__")?)?; + self.assign_ids_phase(&get_attr(self.vm, obj, "__annotations__")?)?; + self.assign_ids_phase(&get_attr(self.vm, obj, "__module__")?)?; + self.assign_ids_phase(&get_attr(self.vm, obj, "__doc__")?)?; + + let type_params_obj = get_attr_opt(self.vm, obj, "__type_params__")?.unwrap_or_else(|| self.vm.ctx.empty_tuple.clone().into()); + self.assign_ids_phase(&type_params_obj)?; + Ok(()) + } + ObjTag::Type => { + let typ = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected type"))?; + + for base in typ.bases.read().iter() { + if base.as_object().as_raw() == self.vm.ctx.types.object_type.as_object().as_raw() { + continue; + } + self.assign_ids_phase(&base.to_owned().into())?; + } + + // Create and cache attributes dict in phase 1 + let dict = self.vm.ctx.new_dict(); + for (key, value) in typ.attributes.read().iter() { + if should_skip_type_attr(self.vm, value) { + continue; + } + dict.set_item(key.as_str(), value.clone(), self.vm) + .map_err(|_| SnapshotError::msg("type dict build failed"))?; + self.assign_ids_phase(value)?; + } + let dict_obj: PyObjectRef = dict.into(); + let type_ptr = obj.as_object().as_raw() as usize; + self.type_attr_dicts.insert(type_ptr, dict_obj.clone()); + self.assign_ids_phase(&dict_obj)?; + Ok(()) + } + ObjTag::Instance => { + let typ = obj.class(); + self.assign_ids_phase(&typ.to_owned().into())?; + + let (new_args, new_kwargs) = get_newargs(self.vm, obj)?; + let state = get_state(self.vm, obj)?; + + // Cache for later use in build_payload + let obj_ptr = obj.as_object().as_raw() as usize; + self.instance_data.insert(obj_ptr, (new_args.clone(), new_kwargs.clone(), state.clone())); + + if let Some(ref args) = new_args { + self.assign_ids_phase(args)?; + } + if let Some(ref kwargs) = new_kwargs { + self.assign_ids_phase(kwargs)?; + } + if let Some(ref s) = state { + self.assign_ids_phase(s)?; + } + Ok(()) + } + ObjTag::Cell => { + let cell = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected cell"))?; + if let Some(contents) = cell.get() { + self.assign_ids_phase(&contents)?; + } + Ok(()) + } + ObjTag::Enumerate => { + // Visit the iterator via __reduce__ + // enumerate.__reduce__() returns (type, (iterator, count)) + if let Some(reduce_fn) = get_attr_opt(self.vm, obj, "__reduce__")? { + if let Ok(result) = self.vm.invoke(&reduce_fn, ()) { + if let Some(tuple) = result.downcast_ref::() { + if tuple.len() >= 2 { + // Get the args tuple: (iterator, count) + if let Some(args) = tuple.get(1).and_then(|o| o.downcast_ref::()) { + if let Some(iterator) = args.get(0) { + self.assign_ids_phase(iterator)?; + } + } + } + } + } + } + Ok(()) + } + ObjTag::Zip => { + // Visit all iterators in the zip + if let Some(iterators) = get_attr_opt(self.vm, obj, "__iterators__")? { + if let Some(tuple) = iterators.downcast_ref::() { + for iter in tuple.iter() { + self.assign_ids_phase(iter)?; + } + } + } + Ok(()) + } + ObjTag::Map => { + // Visit the function and iterator + if let Some(func) = get_attr_opt(self.vm, obj, "__func__")? { + self.assign_ids_phase(&func)?; + } + if let Some(iterator) = get_attr_opt(self.vm, obj, "__iterator__")? { + self.assign_ids_phase(&iterator)?; + } + Ok(()) + } + ObjTag::Filter => { + // Visit the predicate and iterator + if let Some(func) = get_attr_opt(self.vm, obj, "__predicate__")? { + self.assign_ids_phase(&func)?; + } + if let Some(iterator) = get_attr_opt(self.vm, obj, "__iterator__")? { + self.assign_ids_phase(&iterator)?; + } + Ok(()) + } + ObjTag::ListIterator => { + // Visit the list via __reduce__ + // list_iterator.__reduce__() returns (iter, (list,), position) + if let Some(reduce_fn) = get_attr_opt(self.vm, obj, "__reduce__")? { + if let Ok(result) = self.vm.invoke(&reduce_fn, ()) { + if let Some(tuple) = result.downcast_ref::() { + if tuple.len() >= 2 { + // Get the args tuple: (list,) + if let Some(args) = tuple.get(1).and_then(|o| o.downcast_ref::()) { + if let Some(list) = args.get(0) { + self.assign_ids_phase(list)?; + } + } + } + } + } + } + Ok(()) + } + ObjTag::Range => { + // range object has start, stop, step which are integers + // No need to assign IDs for these primitive values + Ok(()) + } + ObjTag::RangeIterator => { + // Visit the range via __reduce__ + // range_iterator.__reduce__() returns (iter, (range,), position) + if let Some(reduce_fn) = get_attr_opt(self.vm, obj, "__reduce__")? { + if let Ok(result) = self.vm.invoke(&reduce_fn, ()) { + if let Some(tuple) = result.downcast_ref::() { + if tuple.len() >= 2 { + // Get the args tuple: (range,) + if let Some(args) = tuple.get(1).and_then(|o| o.downcast_ref::()) { + if let Some(range) = args.get(0) { + self.assign_ids_phase(range)?; + } + } + } + } + } + } + Ok(()) + } + } + } + + /// Phase 2: Build payloads for all objects in ID order + fn build_payloads_phase(&mut self) -> Result<(), SnapshotError> { + let count = self.held.len(); + self.objects.reserve(count); + + for idx in 0..count { + let obj = self.held[idx].clone(); // Clone to avoid borrow checker issues + let tag = classify_obj(self.vm, &obj)?; + let payload = self.build_payload(tag, &obj)?; + self.objects.push(ObjectEntry { tag, payload }); + } + + Ok(()) + } + + /// Get the ID of an already-visited object + fn get_id(&self, obj: &PyObjectRef) -> Result { + let ptr = obj.as_object().as_raw() as usize; + self.ids.get(&ptr).copied() + .ok_or_else(|| SnapshotError::msg(format!("object not in ID map: class={}", obj.class().name()))) + } + + /// Get ID or assign new ID if object not yet visited (for dynamically created objects) + fn get_or_assign_id(&mut self, obj: &PyObjectRef) -> Result { + let ptr = obj.as_object().as_raw() as usize; + if let Some(&id) = self.ids.get(&ptr) { + return Ok(id); + } + + // Object not yet visited, assign ID now + let id = self.held.len() as ObjId; + self.ids.insert(ptr, id); + self.held.push(obj.clone()); + + // Build payload immediately + let tag = classify_obj(self.vm, obj)?; + let payload = self.build_payload(tag, obj)?; + self.objects.push(ObjectEntry { tag, payload }); + + Ok(id) + } + + /// Get or create a converted dict object (for kwdefaults/annotations) + /// Returns the same object on subsequent calls with the same source_ptr + fn build_payload(&mut self, tag: ObjTag, obj: &PyObjectRef) -> Result { + match tag { + ObjTag::None => Ok(ObjectPayload::None), + ObjTag::Bool => Ok(ObjectPayload::Bool(obj.clone().is_true(self.vm).unwrap_or(false))), + ObjTag::Int => { + let value = obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("expected int"))?; + Ok(ObjectPayload::Int(value.as_bigint().to_string())) + } + ObjTag::Float => { + let value = obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("expected float"))?; + Ok(ObjectPayload::Float(value.to_f64())) + } + ObjTag::Str => { + let value = obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("expected str"))?; + Ok(ObjectPayload::Str(value.as_str().to_owned())) + } + ObjTag::Bytes => { + let value = obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("expected bytes"))?; + Ok(ObjectPayload::Bytes(value.as_bytes().to_vec())) + } + ObjTag::List => { + let list = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected list"))?; + let items = list + .borrow_vec() + .iter() + .map(|item| self.get_id(item)) + .collect::, _>>()?; + Ok(ObjectPayload::List(items)) + } + ObjTag::Tuple => { + let tuple = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected tuple"))?; + let items = tuple + .iter() + .map(|item| self.get_id(item)) + .collect::, _>>()?; + Ok(ObjectPayload::Tuple(items)) + } + ObjTag::Dict => { + let dict: PyDictRef = obj + .clone() + .downcast() + .map_err(|_| SnapshotError::msg("expected dict"))?; + let mut entries = Vec::new(); + for (key, value) in &dict { + let key_bytes = snapshot_key_bytes(self.vm, &key)?; + let key_id = self.get_id(&key)?; + let value_id = self.get_id(&value)?; + entries.push((key_bytes, key_id, value_id)); + } + entries.sort_by(|(a, _, _), (b, _, _)| cbor_key_cmp(a, b)); + let pairs = entries + .into_iter() + .map(|(_, k, v)| (k, v)) + .collect(); + Ok(ObjectPayload::Dict(pairs)) + } + ObjTag::Set => { + let set = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected set"))?; + let mut entries = Vec::new(); + for key in set.elements() { + let key_bytes = snapshot_key_bytes(self.vm, &key)?; + let key_id = self.get_id(&key)?; + entries.push((key_bytes, key_id)); + } + entries.sort_by(|(a, _), (b, _)| cbor_key_cmp(a, b)); + let ids = entries.into_iter().map(|(_, id)| id).collect(); + Ok(ObjectPayload::Set(ids)) + } + ObjTag::FrozenSet => { + let set = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected frozenset"))?; + let mut entries = Vec::new(); + for key in set.elements() { + let key_bytes = snapshot_key_bytes(self.vm, &key)?; + let key_id = self.get_id(&key)?; + entries.push((key_bytes, key_id)); + } + entries.sort_by(|(a, _), (b, _)| cbor_key_cmp(a, b)); + let ids = entries.into_iter().map(|(_, id)| id).collect(); + Ok(ObjectPayload::FrozenSet(ids)) + } + ObjTag::Module => { + obj.downcast_ref::() + .ok_or_else(|| SnapshotError::msg("expected module"))?; + let dict = obj + .dict() + .ok_or_else(|| SnapshotError::msg("module missing dict"))?; + let name = get_attr_str(self.vm, obj, "__name__")?.unwrap_or_default(); + let dict_id = self.get_id(&dict.into())?; + Ok(ObjectPayload::Module { name, dict: dict_id }) + } + ObjTag::Function => { + obj.downcast_ref::() + .ok_or_else(|| SnapshotError::msg("expected function"))?; + let code_obj = get_attr(self.vm, obj, "__code__")?; + let code = self.get_id(&code_obj)?; + let globals_obj = get_attr(self.vm, obj, "__globals__")?; + let globals = self.get_id(&globals_obj)?; + let defaults_obj = get_attr(self.vm, obj, "__defaults__")?; + let defaults = if self.vm.is_none(&defaults_obj) { + None + } else if defaults_obj.downcast_ref::().is_some() { + Some(self.get_id(&defaults_obj)?) + } else { + None + }; + let kwdefaults_obj = get_attr(self.vm, obj, "__kwdefaults__")?; + let kwdefaults = if self.vm.is_none(&kwdefaults_obj) { + None + } else if PyDictRef::try_from_object(self.vm, kwdefaults_obj.clone()).is_ok() { + Some(self.get_id(&kwdefaults_obj)?) + } else if let Ok(dict) = mapping_to_dict(self.vm, &kwdefaults_obj) { + // Create new dict, assign ID dynamically + let dict_obj: PyObjectRef = dict.into(); + let id = self.held.len() as ObjId; + let ptr = dict_obj.as_object().as_raw() as usize; + self.ids.insert(ptr, id); + self.held.push(dict_obj.clone()); + self.objects.push(ObjectEntry { + tag: ObjTag::Dict, + payload: ObjectPayload::Dict(Vec::new()), // Will be filled later if needed + }); + Some(id) + } else { + None + }; + let closure_obj = get_attr(self.vm, obj, "__closure__")?; + let closure = if self.vm.is_none(&closure_obj) { + None + } else if closure_obj.downcast_ref::().is_some() { + Some(self.get_id(&closure_obj)?) + } else { + None + }; + let name = self.get_id(&get_attr(self.vm, obj, "__name__")?)?; + let qualname = self.get_id(&get_attr(self.vm, obj, "__qualname__")?)?; + let annotations_obj = get_attr(self.vm, obj, "__annotations__")?; + let annotations = if PyDictRef::try_from_object(self.vm, annotations_obj.clone()).is_ok() { + self.get_id(&annotations_obj)? + } else if let Ok(dict) = mapping_to_dict(self.vm, &annotations_obj) { + // Create new dict, assign ID dynamically + let dict_obj: PyObjectRef = dict.into(); + let id = self.held.len() as ObjId; + let ptr = dict_obj.as_object().as_raw() as usize; + self.ids.insert(ptr, id); + self.held.push(dict_obj.clone()); + self.objects.push(ObjectEntry { + tag: ObjTag::Dict, + payload: ObjectPayload::Dict(Vec::new()), + }); + id + } else { + // Create empty dict + let dict_obj: PyObjectRef = self.vm.ctx.new_dict().into(); + let id = self.held.len() as ObjId; + let ptr = dict_obj.as_object().as_raw() as usize; + self.ids.insert(ptr, id); + self.held.push(dict_obj); + self.objects.push(ObjectEntry { + tag: ObjTag::Dict, + payload: ObjectPayload::Dict(Vec::new()), + }); + id + }; + let module = self.get_id(&get_attr(self.vm, obj, "__module__")?)?; + let doc = self.get_id(&get_attr(self.vm, obj, "__doc__")?)?; + let type_params_obj = get_attr_opt(self.vm, obj, "__type_params__")? + .unwrap_or_else(|| self.vm.ctx.empty_tuple.clone().into()); + let type_params = self.get_id(&type_params_obj)?; + Ok(ObjectPayload::Function(FunctionPayload { + code, + globals, + defaults, + kwdefaults, + closure, + name, + qualname, + annotations, + module, + doc, + type_params, + })) + } + ObjTag::Code => { + let code = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected code"))?; + Ok(ObjectPayload::Code(serialize_code_object(&code.code))) + } + ObjTag::Type => { + let typ = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected type"))?; + let bases: Vec = typ + .bases + .read() + .iter() + .filter_map(|base| { + // Skip object type (should not be serialized) + if base.as_object().as_raw() == self.vm.ctx.types.object_type.as_object().as_raw() { + None + } else { + Some(self.get_id(&base.to_owned().into())) + } + }) + .collect::, _>>()?; + + // Retrieve cached attributes dict + let type_ptr = obj.as_object().as_raw() as usize; + let dict_obj = self.type_attr_dicts.get(&type_ptr) + .ok_or_else(|| SnapshotError::msg("type attributes dict not found in cache"))?; + let dict_id = self.get_id(dict_obj)?; + + let qualname_obj = typ.__qualname__(self.vm); + let qualname = qualname_obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("type __qualname__ must be str"))? + .as_str() + .to_owned(); + Ok(ObjectPayload::Type(TypePayload { + name: typ.name().to_owned(), + qualname, + bases, + dict: dict_id, + flags: typ.slots.flags.bits(), + basicsize: typ.slots.basicsize, + itemsize: typ.slots.itemsize, + member_count: typ.slots.member_count, + })) + } + ObjTag::BuiltinType => { + let typ = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected type"))?; + let module = get_attr_str(self.vm, obj, "__module__")? + .unwrap_or_else(|| "builtins".to_owned()); + Ok(ObjectPayload::BuiltinType { + module, + name: typ.name().to_owned(), + }) + } + ObjTag::Instance => { + let typ = obj.class(); + let typ_id = self.get_id(&typ.to_owned().into())?; + + // Retrieve cached instance data + let obj_ptr = obj.as_object().as_raw() as usize; + let (new_args, new_kwargs, state) = self.instance_data.get(&obj_ptr) + .ok_or_else(|| SnapshotError::msg("instance data not found in cache"))?; + + let new_args_id = new_args.as_ref().map(|o| self.get_id(o)).transpose()?; + let new_kwargs_id = new_kwargs.as_ref().map(|o| self.get_id(o)).transpose()?; + let state_id = state.as_ref().map(|o| self.get_id(o)).transpose()?; + Ok(ObjectPayload::Instance(InstancePayload { + typ: typ_id, + state: state_id, + new_args: new_args_id, + new_kwargs: new_kwargs_id, + })) + } + ObjTag::Cell => { + let cell = obj.downcast_ref::().ok_or_else(|| SnapshotError::msg("expected cell"))?; + let contents = cell.get().map(|o| self.get_id(&o)).transpose()?; + Ok(ObjectPayload::Cell(contents)) + } + ObjTag::BuiltinModule => { + let name = get_attr_str(self.vm, obj, "__name__")?.unwrap_or_default(); + Ok(ObjectPayload::BuiltinModule { name }) + } + ObjTag::BuiltinDict => { + Ok(ObjectPayload::BuiltinDict { name: "builtins".to_owned() }) + } + ObjTag::BuiltinFunction => { + let name = get_attr_str(self.vm, obj, "__name__")? + .ok_or_else(|| SnapshotError::msg("builtin function missing __name__"))?; + let module = get_attr_str(self.vm, obj, "__module__")?; + let self_obj = get_attr_opt(self.vm, obj, "__self__")? + .and_then(|value| if self.vm.is_none(&value) { None } else { Some(value) }) + .map(|value| self.get_id(&value)) + .transpose()?; + Ok(ObjectPayload::BuiltinFunction(BuiltinFunctionPayload { + name, + module, + self_obj, + })) + } + ObjTag::Enumerate => { + // Use __reduce__ to get iterator and count + let reduce_fn = get_attr(self.vm, obj, "__reduce__")?; + let result = self.vm.invoke(&reduce_fn, ()) + .map_err(|_| SnapshotError::msg("enumerate __reduce__ failed"))?; + + let tuple = result.downcast_ref::() + .ok_or_else(|| SnapshotError::msg("enumerate __reduce__ didn't return tuple"))?; + + if tuple.len() < 2 { + return Err(SnapshotError::msg("enumerate __reduce__ tuple too short")); + } + + // Get args tuple: (iterator, count) + let args = tuple.get(1) + .and_then(|o| o.downcast_ref::()) + .ok_or_else(|| SnapshotError::msg("enumerate __reduce__ args invalid"))?; + + let iterator = args.get(0) + .ok_or_else(|| SnapshotError::msg("enumerate missing iterator in __reduce__"))? + .clone(); + let iterator_id = self.get_id(&iterator)?; + + let count_bigint = args.get(1) + .and_then(|o| o.downcast_ref::()) + .ok_or_else(|| SnapshotError::msg("enumerate missing count in __reduce__"))?; + + let count = count_bigint.try_to_primitive::(self.vm).unwrap_or(0); + + Ok(ObjectPayload::Enumerate { iterator: iterator_id, count }) + } + ObjTag::Zip => { + // Extract iterators from zip object + let iterators_obj = get_attr_opt(self.vm, obj, "__iterators__")? + .ok_or_else(|| SnapshotError::msg("zip missing __iterators__"))?; + + let iterators = if let Some(tuple) = iterators_obj.downcast_ref::() { + tuple.iter() + .map(|iter| self.get_id(iter)) + .collect::, _>>()? + } else { + Vec::new() + }; + + Ok(ObjectPayload::Zip { iterators }) + } + ObjTag::Map => { + // Extract function and iterator from map object + let function = get_attr_opt(self.vm, obj, "__func__")? + .ok_or_else(|| SnapshotError::msg("map missing __func__"))?; + let function_id = self.get_id(&function)?; + + let iterator = get_attr_opt(self.vm, obj, "__iterator__")? + .ok_or_else(|| SnapshotError::msg("map missing __iterator__"))?; + let iterator_id = self.get_id(&iterator)?; + + Ok(ObjectPayload::Map { function: function_id, iterator: iterator_id }) + } + ObjTag::Filter => { + // Extract predicate and iterator from filter object + let function = get_attr_opt(self.vm, obj, "__predicate__")? + .ok_or_else(|| SnapshotError::msg("filter missing __predicate__"))?; + let function_id = self.get_id(&function)?; + + let iterator = get_attr_opt(self.vm, obj, "__iterator__")? + .ok_or_else(|| SnapshotError::msg("filter missing __iterator__"))?; + let iterator_id = self.get_id(&iterator)?; + + Ok(ObjectPayload::Filter { function: function_id, iterator: iterator_id }) + } + ObjTag::ListIterator => { + // Use __reduce__ to get list and position + // list_iterator.__reduce__() returns (iter, (list,), position) + let reduce_fn = get_attr(self.vm, obj, "__reduce__")?; + let result = self.vm.invoke(&reduce_fn, ()) + .map_err(|_| SnapshotError::msg("list_iterator __reduce__ failed"))?; + + let tuple = result.downcast_ref::() + .ok_or_else(|| SnapshotError::msg("list_iterator __reduce__ didn't return tuple"))?; + + if tuple.len() < 3 { + return Err(SnapshotError::msg("list_iterator __reduce__ tuple too short")); + } + + // Get args tuple: (list,) + let args = tuple.get(1) + .and_then(|o| o.downcast_ref::()) + .ok_or_else(|| SnapshotError::msg("list_iterator __reduce__ args invalid"))?; + + let list = args.get(0) + .ok_or_else(|| SnapshotError::msg("list_iterator missing list in __reduce__"))? + .clone(); + + let list_id = self.get_id(&list)?; + + // Get position (third element of reduce result) + let position = tuple.get(2) + .and_then(|o| o.downcast_ref::()) + .ok_or_else(|| SnapshotError::msg("list_iterator missing position in __reduce__"))? + .try_to_primitive::(self.vm) + .unwrap_or(0); + + Ok(ObjectPayload::ListIterator { list: list_id, position }) + } + ObjTag::Range => { + // Serialize range object by extracting start, stop, step + let start = get_attr(self.vm, obj, "start")? + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("range.start is not int"))? + .try_to_primitive::(self.vm) + .unwrap_or(0); + + let stop = get_attr(self.vm, obj, "stop")? + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("range.stop is not int"))? + .try_to_primitive::(self.vm) + .unwrap_or(0); + + let step = get_attr(self.vm, obj, "step")? + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("range.step is not int"))? + .try_to_primitive::(self.vm) + .unwrap_or(1); + + Ok(ObjectPayload::Range { start, stop, step }) + } + ObjTag::RangeIterator => { + // Use __reduce__ to get range and position + // range_iterator.__reduce__() returns (iter, (range,), position) + let reduce_fn = get_attr(self.vm, obj, "__reduce__")?; + let result = self.vm.invoke(&reduce_fn, ()) + .map_err(|_| SnapshotError::msg("range_iterator __reduce__ failed"))?; + + let tuple = result.downcast_ref::() + .ok_or_else(|| SnapshotError::msg("range_iterator __reduce__ didn't return tuple"))?; + + if tuple.len() < 3 { + return Err(SnapshotError::msg("range_iterator __reduce__ tuple too short")); + } + + // Get args tuple: (range,) + let args = tuple.get(1) + .and_then(|o| o.downcast_ref::()) + .ok_or_else(|| SnapshotError::msg("range_iterator __reduce__ args invalid"))?; + + let range = args.get(0) + .ok_or_else(|| SnapshotError::msg("range_iterator missing range in __reduce__"))? + .clone(); + + // Use get_or_assign_id to handle range objects that may not have been visited yet + let range_id = self.get_or_assign_id(&range)?; + + // Get position (third element of reduce result) + let position = tuple.get(2) + .and_then(|o| o.downcast_ref::()) + .ok_or_else(|| SnapshotError::msg("range_iterator missing position in __reduce__"))? + .try_to_primitive::(self.vm) + .unwrap_or(0); + + Ok(ObjectPayload::RangeIterator { range: range_id, position }) + } + } + } +} + +fn classify_obj(vm: &VirtualMachine, obj: &PyObjectRef) -> Result { + if vm.is_none(obj) { + return Ok(ObjTag::None); + } + if obj.fast_isinstance(vm.ctx.types.bool_type) { + return Ok(ObjTag::Bool); + } + if obj.fast_isinstance(vm.ctx.types.int_type) { + return Ok(ObjTag::Int); + } + if obj.fast_isinstance(vm.ctx.types.float_type) { + return Ok(ObjTag::Float); + } + if obj.fast_isinstance(vm.ctx.types.str_type) { + return Ok(ObjTag::Str); + } + if obj.fast_isinstance(vm.ctx.types.bytes_type) { + return Ok(ObjTag::Bytes); + } + if obj.downcast_ref::().is_some() { + return Ok(ObjTag::List); + } + if obj.downcast_ref::().is_some() { + return Ok(ObjTag::Tuple); + } + if obj.downcast_ref::().is_some() { + if is_builtin_dict(vm, obj) { + return Ok(ObjTag::BuiltinDict); + } + return Ok(ObjTag::Dict); + } + if obj.downcast_ref::().is_some() { + return Ok(ObjTag::Set); + } + if obj.downcast_ref::().is_some() { + return Ok(ObjTag::FrozenSet); + } + if obj.downcast_ref::().is_some() { + if is_builtin_module(vm, obj) { + return Ok(ObjTag::BuiltinModule); + } + return Ok(ObjTag::Module); + } + if obj.downcast_ref::().is_some() { + return Ok(ObjTag::Function); + } + if obj.fast_isinstance(vm.ctx.types.builtin_function_or_method_type) { + return Ok(ObjTag::BuiltinFunction); + } + + // Check for iterator types by class name + let class_name_obj = obj.class().name(); + let class_name = class_name_obj.as_ref(); + match class_name { + "enumerate" => return Ok(ObjTag::Enumerate), + "zip" => return Ok(ObjTag::Zip), + "map" => return Ok(ObjTag::Map), + "filter" => return Ok(ObjTag::Filter), + "list_iterator" => return Ok(ObjTag::ListIterator), + "range_iterator" => return Ok(ObjTag::RangeIterator), + "range" => return Ok(ObjTag::Range), + _ => {} + } + if obj.downcast_ref::().is_some() { + return Ok(ObjTag::Code); + } + if let Some(typ) = obj.downcast_ref::() { + if typ.slots.flags.has_feature(crate::types::PyTypeFlags::HEAPTYPE) { + return Ok(ObjTag::Type); + } + return Ok(ObjTag::BuiltinType); + } + if obj.downcast_ref::().is_some() { + return Ok(ObjTag::Cell); + } + Ok(ObjTag::Instance) +} + +fn get_attr( + vm: &VirtualMachine, + obj: &PyObjectRef, + name: &'static str, +) -> Result { + get_attr_opt(vm, obj, name)? + .ok_or_else(|| SnapshotError::msg(format!("attribute '{name}' missing"))) +} + +fn get_attr_opt( + vm: &VirtualMachine, + obj: &PyObjectRef, + name: &'static str, +) -> Result, SnapshotError> { + vm.get_attribute_opt(obj.clone(), name) + .map_err(|_| SnapshotError::msg(format!("attribute '{name}' lookup failed"))) +} + +fn get_attr_str( + vm: &VirtualMachine, + obj: &PyObjectRef, + name: &'static str, +) -> Result, SnapshotError> { + let Some(value) = get_attr_opt(vm, obj, name)? else { + return Ok(None); + }; + if vm.is_none(&value) { + return Ok(None); + } + let value = value + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg(format!("attribute '{name}' must be str")))?; + Ok(Some(value.as_str().to_owned())) +} + +fn is_builtin_module(vm: &VirtualMachine, obj: &PyObjectRef) -> bool { + let raw = obj.as_object().as_raw(); + if raw == vm.builtins.as_object().as_raw() || raw == vm.sys_module.as_object().as_raw() { + return true; + } + matches!( + get_attr_str(vm, obj, "__name__").ok().flatten().as_deref(), + Some("builtins") | Some("sys") + ) +} + +fn is_builtin_dict(vm: &VirtualMachine, obj: &PyObjectRef) -> bool { + let raw = obj.as_object().as_raw(); + raw == vm.builtins.dict().as_object().as_raw() +} + +fn should_skip_type_attr(vm: &VirtualMachine, value: &PyObjectRef) -> bool { + value.fast_isinstance(vm.ctx.types.getset_type) + || value.fast_isinstance(vm.ctx.types.member_descriptor_type) + || value.fast_isinstance(vm.ctx.types.method_descriptor_type) + || value.fast_isinstance(vm.ctx.types.wrapper_descriptor_type) +} + +fn get_state(vm: &VirtualMachine, obj: &PyObjectRef) -> Result, SnapshotError> { + let class_name = obj.class().name(); + + // Skip __getstate__ for problematic types that cause infinite recursion + let skip_getstate = &*class_name == "_Feature"; + + if !skip_getstate { + if let Some(getstate) = vm.get_attribute_opt(obj.clone(), "__getstate__").map_err(|_| SnapshotError::msg("getstate lookup failed"))? { + let value = getstate + .call((), vm) + .map_err(|_| SnapshotError::msg("__getstate__ failed"))?; + return Ok(Some(value)); + } + } + + if let Some(dict) = obj.dict() { + return Ok(Some(dict.into())); + } + Ok(None) +} + +fn get_newargs( + vm: &VirtualMachine, + obj: &PyObjectRef, +) -> Result<(Option, Option), SnapshotError> { + if obj.fast_isinstance(vm.ctx.types.classmethod_type) + || obj.fast_isinstance(vm.ctx.types.staticmethod_type) + { + let func = get_attr(vm, obj, "__func__")?; + let args = vm.new_tuple(vec![func]).into(); + return Ok((Some(args), None)); + } + if let Some(getnewargs_ex) = vm + .get_attribute_opt(obj.clone(), "__getnewargs_ex__") + .map_err(|_| SnapshotError::msg("getnewargs_ex lookup failed"))? + { + let value = getnewargs_ex + .call((), vm) + .map_err(|_| SnapshotError::msg("__getnewargs_ex__ failed"))?; + let tuple = if let Some(tuple) = value.downcast_ref::() { + tuple + } else if let Some(list) = value.downcast_ref::() { + return Ok((Some(vm.new_tuple(list.borrow_vec().to_vec()).into()), None)); + } else { + return Ok((None, None)); + }; + let args = tuple + .get(0) + .ok_or_else(|| SnapshotError::msg("__getnewargs_ex__ missing args"))? + .clone(); + let kwargs = tuple + .get(1) + .ok_or_else(|| SnapshotError::msg("__getnewargs_ex__ missing kwargs"))? + .clone(); + return Ok((Some(args), Some(kwargs))); + } + if let Some(getnewargs) = vm + .get_attribute_opt(obj.clone(), "__getnewargs__") + .map_err(|_| SnapshotError::msg("getnewargs lookup failed"))? + { + let value = getnewargs + .call((), vm) + .map_err(|_| SnapshotError::msg("__getnewargs__ failed"))?; + if value.downcast_ref::().is_some() { + return Ok((Some(value), None)); + } + if let Some(list) = value.downcast_ref::() { + return Ok((Some(vm.new_tuple(list.borrow_vec().to_vec()).into()), None)); + } + return Ok((None, None)); + } + Ok((None, None)) +} + +fn snapshot_key_bytes(vm: &VirtualMachine, obj: &PyObjectRef) -> Result, SnapshotError> { + let mut encoder = CborWriter::new(); + encode_key(vm, obj, &mut encoder)?; + Ok(encoder.into_bytes()) +} + +fn encode_key(vm: &VirtualMachine, obj: &PyObjectRef, encoder: &mut CborWriter) -> Result<(), SnapshotError> { + const TAG_NONE: u64 = 0; + const TAG_BOOL: u64 = 1; + const TAG_INT: u64 = 2; + const TAG_FLOAT: u64 = 3; + const TAG_STR: u64 = 4; + const TAG_BYTES: u64 = 5; + const TAG_TUPLE: u64 = 6; + const TAG_TYPE: u64 = 7; + const TAG_MODULE: u64 = 8; + const TAG_FUNCTION: u64 = 9; + const TAG_BUILTIN_FUNCTION: u64 = 10; + const TAG_CODE: u64 = 11; + const TAG_FROZENSET: u64 = 12; + const TAG_WEAKREF: u64 = 13; + + if vm.is_none(obj) { + write_tagged_key(encoder, TAG_NONE, |enc| enc.write_null()); + return Ok(()); + } + if obj.fast_isinstance(vm.ctx.types.bool_type) { + let value = obj.clone().is_true(vm).unwrap_or(false); + write_tagged_key(encoder, TAG_BOOL, |enc| enc.write_bool(value)); + return Ok(()); + } + if obj.fast_isinstance(vm.ctx.types.int_type) { + let value = obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("expected int key"))?; + let text = value.as_bigint().to_string(); + write_tagged_key(encoder, TAG_INT, |enc| enc.write_text(&text)); + return Ok(()); + } + if obj.fast_isinstance(vm.ctx.types.float_type) { + let value = obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("expected float key"))?; + let num = value.to_f64(); + write_tagged_key(encoder, TAG_FLOAT, |enc| enc.write_f64(num)); + return Ok(()); + } + if obj.fast_isinstance(vm.ctx.types.str_type) { + let value = obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("expected str key"))?; + let text = value.as_str(); + write_tagged_key(encoder, TAG_STR, |enc| enc.write_text(text)); + return Ok(()); + } + if obj.fast_isinstance(vm.ctx.types.bytes_type) { + let value = obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("expected bytes key"))?; + let bytes = value.as_bytes(); + write_tagged_key(encoder, TAG_BYTES, |enc| enc.write_bytes(bytes)); + return Ok(()); + } + if let Some(tuple) = obj.downcast_ref::() { + encoder.write_array_len(2); + encoder.write_uint(TAG_TUPLE); + encoder.write_array_len(tuple.len()); + for item in tuple.iter() { + encode_key(vm, item, encoder)?; + } + return Ok(()); + } + if let Some(frozen) = obj.downcast_ref::() { + let mut entries = Vec::new(); + for item in frozen.elements() { + let mut item_writer = CborWriter::new(); + encode_key(vm, &item, &mut item_writer)?; + entries.push(item_writer.into_bytes()); + } + entries.sort_by(|a, b| cbor_key_cmp(a, b)); + encoder.write_array_len(2); + encoder.write_uint(TAG_FROZENSET); + encoder.write_array_len(entries.len()); + for item in entries { + encoder.buf.extend_from_slice(&item); + } + return Ok(()); + } + if let Some(weak) = obj.downcast_ref::() { + let Some(target) = weak.upgrade() else { + return Err(SnapshotError::msg("unsupported dict/set key type: weakref (dead)")); + }; + let mut target_writer = CborWriter::new(); + encode_key(vm, &target, &mut target_writer)?; + encoder.write_array_len(2); + encoder.write_uint(TAG_WEAKREF); + encoder.buf.extend_from_slice(&target_writer.into_bytes()); + return Ok(()); + } + if let Some(typ) = obj.downcast_ref::() { + let module = get_attr_str(vm, obj, "__module__")? + .unwrap_or_else(|| "builtins".to_owned()); + let qualname = get_attr_str(vm, obj, "__qualname__")? + .unwrap_or_else(|| typ.name().to_owned()); + encoder.write_array_len(2); + encoder.write_uint(TAG_TYPE); + encoder.write_array_len(2); + encoder.write_text(&module); + encoder.write_text(&qualname); + return Ok(()); + } + if obj.downcast_ref::().is_some() { + let name = get_attr_str(vm, obj, "__name__")?.unwrap_or_default(); + write_tagged_key(encoder, TAG_MODULE, |enc| enc.write_text(&name)); + return Ok(()); + } + if obj.downcast_ref::().is_some() { + let module = get_attr_str(vm, obj, "__module__")?.unwrap_or_default(); + let qualname = get_attr_str(vm, obj, "__qualname__")? + .or_else(|| get_attr_str(vm, obj, "__name__").ok().flatten()) + .unwrap_or_default(); + encoder.write_array_len(2); + encoder.write_uint(TAG_FUNCTION); + encoder.write_array_len(2); + encoder.write_text(&module); + encoder.write_text(&qualname); + return Ok(()); + } + if obj.fast_isinstance(vm.ctx.types.builtin_function_or_method_type) { + let module = get_attr_str(vm, obj, "__module__")? + .unwrap_or_else(|| "builtins".to_owned()); + let qualname = get_attr_str(vm, obj, "__qualname__")? + .or_else(|| get_attr_str(vm, obj, "__name__").ok().flatten()) + .unwrap_or_default(); + let self_obj = get_attr_opt(vm, obj, "__self__")? + .and_then(|value| if vm.is_none(&value) { None } else { Some(value) }); + encoder.write_array_len(2); + encoder.write_uint(TAG_BUILTIN_FUNCTION); + if let Some(self_obj) = self_obj { + encoder.write_array_len(3); + encoder.write_text(&module); + encoder.write_text(&qualname); + let mut self_writer = CborWriter::new(); + encode_key(vm, &self_obj, &mut self_writer)?; + encoder.buf.extend_from_slice(&self_writer.into_bytes()); + } else { + encoder.write_array_len(2); + encoder.write_text(&module); + encoder.write_text(&qualname); + } + return Ok(()); + } + if let Some(code) = obj.downcast_ref::() { + let filename = code.code.source_path.as_str(); + let name = code.code.obj_name.as_str(); + let first_line = code.code.first_line_number.map_or(0, |n| n.get()); + encoder.write_array_len(2); + encoder.write_uint(TAG_CODE); + encoder.write_array_len(3); + encoder.write_text(filename); + encoder.write_text(name); + encoder.write_uint(first_line as u64); + return Ok(()); + } + let type_name = obj.class().name(); + Err(SnapshotError::msg(format!( + "unsupported dict/set key type: {type_name}" + ))) +} + +fn write_tagged_key(encoder: &mut CborWriter, tag: u64, f: impl FnOnce(&mut CborWriter)) { + encoder.write_array_len(2); + encoder.write_uint(tag); + f(encoder); +} + +fn cbor_key_cmp(a: &[u8], b: &[u8]) -> std::cmp::Ordering { + a.len().cmp(&b.len()).then_with(|| a.cmp(b)) +} + +struct SnapshotReader<'a> { + vm: &'a VirtualMachine, + entries: &'a [ObjectEntry], + root: ObjId, + objects: Vec>, + filled: Vec, + /// Track which objects are currently being restored to detect cycles + restoring: Vec, +} + +impl<'a> SnapshotReader<'a> { + fn new(vm: &'a VirtualMachine, entries: &'a [ObjectEntry], root: ObjId) -> Self { + Self { + vm, + entries, + root, + objects: vec![None; entries.len()], + filled: vec![false; entries.len()], + restoring: vec![false; entries.len()], + } + } + + fn restore_all(mut self) -> Result, SnapshotError> { + for idx in 0..self.entries.len() { + self.restore_entry(idx)?; + } + for idx in 0..self.entries.len() { + self.fill_container(idx)?; + } + for idx in 0..self.entries.len() { + self.apply_instance_state(idx)?; + } + Ok(self.objects.into_iter().map(|o| o.unwrap()).collect()) + } + + fn restore_entry(&mut self, idx: usize) -> Result<(), SnapshotError> { + // Already restored + if self.objects[idx].is_some() { + return Ok(()); + } + + // Cycle detection: if we're already restoring this object, we have a cycle + if self.restoring[idx] { + let entry = &self.entries[idx]; + // For cycles, we'll create a placeholder and handle it later + // This shouldn't happen with the two-phase serialization, but check anyway + return Err(SnapshotError::msg(format!("cycle detected while restoring object {} (tag={:?})", idx, entry.tag))); + } + + self.restoring[idx] = true; + let entry = &self.entries[idx]; + let obj = match &entry.payload { + ObjectPayload::None => self.vm.ctx.none(), + ObjectPayload::Bool(value) => self.vm.ctx.new_bool(*value).into(), + ObjectPayload::Int(value) => { + let int = value + .parse::() + .map_err(|_| SnapshotError::msg("invalid int"))?; + self.vm.ctx.new_int(int).into() + } + ObjectPayload::Float(value) => self.vm.ctx.new_float(*value).into(), + ObjectPayload::Str(value) => self.vm.ctx.new_str(value.clone()).into(), + ObjectPayload::Bytes(value) => self.vm.ctx.new_bytes(value.clone()).into(), + ObjectPayload::List(_) => self.vm.ctx.new_list(Vec::new()).into(), + ObjectPayload::Dict(_) => self.vm.ctx.new_dict().into(), + ObjectPayload::Set(_) => PySet::default().into_ref(&self.vm.ctx).into(), + ObjectPayload::FrozenSet(items) => { + let values = items + .iter() + .map(|id| self.get_obj(*id)) + .collect::, _>>()?; + let frozen = PyFrozenSet::from_iter(self.vm, values) + .map_err(|_| SnapshotError::msg("frozenset build failed"))?; + frozen.into_ref(&self.vm.ctx).into() + } + ObjectPayload::Tuple(items) => { + let values = items + .iter() + .map(|id| self.get_obj(*id)) + .collect::, _>>()?; + self.vm.new_tuple(values).into() + } + ObjectPayload::Module { name, dict } => { + let dict = self.get_obj(*dict)?; + let dict = PyDictRef::try_from_object(self.vm, dict) + .map_err(|_| SnapshotError::msg("module dict invalid"))?; + self.vm.new_module(name, dict.clone(), None).into() + } + ObjectPayload::BuiltinModule { name } => lookup_module(self.vm, name)?, + ObjectPayload::BuiltinDict { name } => { + let module = lookup_module(self.vm, name)?; + let dict = module + .dict() + .ok_or_else(|| SnapshotError::msg("builtin module missing dict"))?; + dict.into() + } + ObjectPayload::Function(payload) => { + let code_obj = self.get_obj(payload.code)?; + let code = code_obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("function code invalid"))? + .to_owned(); + let globals_obj = self.get_obj(payload.globals)?; + let globals = self.resolve_globals(globals_obj, Some(payload.module))?; + let mut func = PyFunction::new(code, globals.clone(), self.vm) + .map_err(|_| SnapshotError::msg("function create failed"))?; + if let Some(defaults) = payload.defaults { + let obj = self.get_obj(defaults)?; + func + .set_function_attribute(crate::bytecode::MakeFunctionFlags::DEFAULTS, obj, self.vm) + .map_err(|_| SnapshotError::msg("defaults invalid"))?; + } + if let Some(kwdefaults) = payload.kwdefaults { + let obj = self.get_obj(kwdefaults)?; + if let Ok(dict) = PyDictRef::try_from_object(self.vm, obj.clone()) { + func + .set_function_attribute( + crate::bytecode::MakeFunctionFlags::KW_ONLY_DEFAULTS, + dict.into(), + self.vm, + ) + .map_err(|_| SnapshotError::msg("kwdefaults invalid"))?; + } else if let Ok(dict) = mapping_to_dict(self.vm, &obj) { + func + .set_function_attribute( + crate::bytecode::MakeFunctionFlags::KW_ONLY_DEFAULTS, + dict.into(), + self.vm, + ) + .map_err(|_| SnapshotError::msg("kwdefaults invalid"))?; + } + } + if let Some(closure_id) = payload.closure { + let obj = self.get_obj(closure_id)?; + func + .set_function_attribute(crate::bytecode::MakeFunctionFlags::CLOSURE, obj, self.vm) + .map_err(|_| SnapshotError::msg("closure invalid"))?; + } + let annotations_obj = self.get_obj(payload.annotations)?; + let annotations_obj = match PyDictRef::try_from_object(self.vm, annotations_obj.clone()) { + Ok(dict) => dict.into(), + Err(_) => match mapping_to_dict(self.vm, &annotations_obj) { + Ok(dict) => dict.into(), + Err(_) => annotations_obj, + }, + }; + func + .set_function_attribute(crate::bytecode::MakeFunctionFlags::ANNOTATIONS, annotations_obj, self.vm) + .map_err(|_| SnapshotError::msg("annotations invalid"))?; + let type_params_obj = self.get_obj(payload.type_params)?; + func + .set_function_attribute(crate::bytecode::MakeFunctionFlags::TYPE_PARAMS, type_params_obj, self.vm) + .map_err(|_| SnapshotError::msg("type params invalid"))?; + let func_ref = func.into_ref(&self.vm.ctx); + let func_obj: PyObjectRef = func_ref.clone().into(); + let name = self.get_obj(payload.name)?; + func_obj + .set_attr("__name__", name, self.vm) + .map_err(|_| SnapshotError::msg("name invalid"))?; + let qualname = self.get_obj(payload.qualname)?; + func_obj + .set_attr("__qualname__", qualname, self.vm) + .map_err(|_| SnapshotError::msg("qualname invalid"))?; + let module = self.get_obj(payload.module)?; + func_obj + .set_attr("__module__", module, self.vm) + .map_err(|_| SnapshotError::msg("module invalid"))?; + let doc = self.get_obj(payload.doc)?; + func_obj + .set_attr("__doc__", doc, self.vm) + .map_err(|_| SnapshotError::msg("doc invalid"))?; + func_obj + } + ObjectPayload::BuiltinFunction(payload) => { + if let Some(self_id) = payload.self_obj { + let target = self.get_obj(self_id)?; + let attr = self.vm.ctx.intern_str(payload.name.as_str()); + target + .get_attr(attr, self.vm) + .map_err(|_| SnapshotError::msg("builtin method lookup failed"))? + } else { + let module_name = payload.module.as_deref().unwrap_or("builtins"); + + // Special case: maketrans is actually str.maketrans, not builtins.maketrans + if module_name == "builtins" && payload.name == "maketrans" { + let attr = self.vm.ctx.intern_str("maketrans"); + self.vm.ctx.types.str_type.as_object().get_attr(attr, self.vm) + .map_err(|_| SnapshotError::msg("str.maketrans not found"))? + } else if module_name == "builtins" && payload.name.ends_with("_errors") { + let name = payload.name.trim_end_matches("_errors"); + self.vm + .state + .codec_registry + .lookup_error(name, self.vm) + .map_err(|_| { + SnapshotError::msg(format!( + "builtin function lookup failed: {}.{}", + module_name, payload.name + )) + })? + } else { + let module = lookup_module(self.vm, module_name)?; + let attr = self.vm.ctx.intern_str(payload.name.as_str()); + module + .get_attr(attr, self.vm) + .map_err(|e| { + SnapshotError::msg(format!("builtin function lookup failed: {}.{}", module_name, payload.name)) + })? + } + } + } + ObjectPayload::Code(bytes) => { + let code = deserialize_code_object(self.vm, bytes)?; + let code_ref: crate::PyRef = self.vm.ctx.new_pyref(PyCode::new(code)); + code_ref.into() + } + ObjectPayload::Type(payload) => { + // Phase 1: Create type with object as base and empty attributes to avoid cycles + // Real bases and attributes will be set in fill_container phase + let temp_bases = vec![self.vm.ctx.types.object_type.to_owned()]; + let empty_attrs = crate::builtins::type_::PyAttributes::default(); + let mut slots = crate::types::PyTypeSlots::heap_default(); + slots.flags = crate::types::PyTypeFlags::from_bits_truncate(payload.flags); + slots.basicsize = payload.basicsize; + slots.itemsize = payload.itemsize; + slots.member_count = payload.member_count; + let metatype = self.vm.ctx.types.type_type.to_owned(); + let typ = crate::builtins::type_::PyType::new_heap( + payload.name.as_str(), + temp_bases, + empty_attrs, + slots, + metatype, + &self.vm.ctx, + ) + .map_err(|e| SnapshotError::msg(format!("type create failed: {e}")))?; + let typ_obj: PyObjectRef = typ.clone().into(); + if payload.qualname != payload.name { + typ_obj + .set_attr("__qualname__", self.vm.ctx.new_str(payload.qualname.clone()), self.vm) + .map_err(|_| SnapshotError::msg("type qualname invalid"))?; + } + typ_obj + } + ObjectPayload::BuiltinType { module, name } => { + // Some builtin types (iterators, views, generators, descriptors, wrappers, etc.) cannot be properly restored + // Use the type class itself instead + if name.ends_with("_iterator") + || name.ends_with("iterator") + || name.ends_with("_descriptor") // wrapper_descriptor, method_descriptor, etc. + || name.ends_with("-wrapper") // method-wrapper, etc. + || name.starts_with("dict_") // dict_keys, dict_values, dict_items + || name.contains("_wrapper") // slot_wrapper, etc. + || name == "generator" + || name == "coroutine" + || name == "async_generator" { + self.vm.ctx.types.type_type.to_owned().into() + } else { + // Handle module and type name aliases + let (actual_module, actual_name) = match (module.as_str(), name.as_str()) { + ("thread", "lock") => ("_thread", "LockType"), + ("builtins", "weakref") => ("weakref", "ref"), + ("builtins", "weakproxy") => ("weakref", "ProxyType"), + ("builtins", "code") => ("types", "CodeType"), + ("builtins", "EllipsisType") => ("types", "EllipsisType"), + ("builtins", "function") => ("types", "FunctionType"), + ("builtins", "mappingproxy") => ("types", "MappingProxyType"), + ("builtins", "cell") => ("types", "CellType"), + ("builtins", "method") => ("types", "MethodType"), + ("builtins", "builtin_function_or_method") => ("types", "BuiltinMethodType"), + ("builtins", "builtin_method") => ("types", "BuiltinMethodType"), + ("builtins", "module") => ("types", "ModuleType"), + ("builtins", "traceback") => ("types", "TracebackType"), + ("builtins", "frame") => ("types", "FrameType"), + ("builtins", "NoneType") => ("types", "NoneType"), + ("builtins", "NotImplementedType") => ("types", "NotImplementedType"), + ("_json", "Scanner") => ("_json", "make_scanner"), + _ => (module.as_str(), name.as_str()), + }; + + let module_obj = lookup_module(self.vm, actual_module)?; + let attr = self.vm.ctx.intern_str(actual_name); + module_obj + .get_attr(attr, self.vm) + .map_err(|e| { + SnapshotError::msg(format!("builtin type not found: {}.{}", module, name)) + })? + } + } + ObjectPayload::Instance(payload) => { + let typ_obj = self.get_obj(payload.typ)?; + let typ = match typ_obj.clone().downcast::() { + Ok(typ) => typ, + Err(obj) => obj.class().to_owned(), + }; + let type_name = typ.name().to_owned(); + + // Special case: if this is a type instance, it should not be created via __new__ + // Use the type itself + if type_name == "type" { + typ.clone().into() + } else + + // Some types cannot be properly restored (weakref, iterators, methods, slices, etc.) + // Return None for these cases + if type_name == "weakref" + || type_name == "weakproxy" + || type_name == "method" // bound methods need specific object binding + || type_name == "builtin_method" + || type_name == "slice" // slice objects need specific start/stop/step + || type_name.ends_with("_iterator") + || type_name.ends_with("iterator") { + self.vm.ctx.none() + } else { + let args_obj = payload + .new_args + .map(|id| self.get_obj(id)) + .transpose()?; + let kwargs_obj = payload + .new_kwargs + .map(|id| self.get_obj(id)) + .transpose()?; + let args_obj = args_obj.unwrap_or_else(|| self.vm.ctx.empty_tuple.clone().into()); + let args = if let Some(tuple) = args_obj.downcast_ref::() { + tuple.to_owned() + } else if let Some(list) = args_obj.downcast_ref::() { + self.vm.new_tuple(list.borrow_vec().to_vec()) + } else { + self.vm.ctx.empty_tuple.clone() + }; + if typ.is(self.vm.ctx.types.classmethod_type) + || typ.is(self.vm.ctx.types.staticmethod_type) + { + let func = args + .get(0) + .cloned() + .unwrap_or_else(|| self.vm.ctx.none().into()); + if typ.is(self.vm.ctx.types.classmethod_type) { + let obj: PyObjectRef = PyClassMethod::from(func) + .into_ref_with_type(self.vm, typ.clone()) + .map_err(|_| SnapshotError::msg("classmethod create failed"))? + .into(); + obj + } else { + let obj: PyObjectRef = PyStaticMethod::new(func) + .into_ref_with_type(self.vm, typ.clone()) + .map_err(|_| SnapshotError::msg("staticmethod create failed"))? + .into(); + obj + } + } else { + let new_func = self + .vm + .get_attribute_opt(typ.clone().into(), "__new__") + .map_err(|_| SnapshotError::msg("__new__ lookup failed"))?; + let kwargs_obj = kwargs_obj.unwrap_or_else(|| self.vm.ctx.new_dict().into()); + let kwargs = if let Ok(dict) = PyDictRef::try_from_object(self.vm, kwargs_obj.clone()) { + dict + } else if let Ok(dict) = mapping_to_dict(self.vm, &kwargs_obj) { + dict + } else { + self.vm.ctx.new_dict() + }; + let mut call_args = Vec::with_capacity(args.len() + 1); + call_args.push(typ.clone().into()); + call_args.extend(args.iter().cloned()); + let kwargs = kwargs_from_dict(kwargs)?; + let instance = if let Some(new_func) = new_func { + match new_func.call(crate::function::FuncArgs::new(call_args.clone(), kwargs.clone()), self.vm) { + Ok(value) => value, + Err(_) => { + self + .vm + .call_method(self.vm.ctx.types.object_type.as_object(), "__new__", (typ.clone(),)) + .map_err(|_| { + SnapshotError::msg(format!("__new__ failed for {type_name}")) + })? + } + } + } else { + self.vm + .call_method(self.vm.ctx.types.object_type.as_object(), "__new__", (typ.clone(),)) + .map_err(|_| SnapshotError::msg(format!("__new__ missing for {type_name}")))? + }; + instance + } + } + } + ObjectPayload::Cell(contents) => { + let value = contents + .map(|id| self.get_obj(id)) + .transpose()?; + let cell = PyCell::new(value); + let cell_ref: crate::PyRef = self.vm.ctx.new_pyref(cell); + cell_ref.into() + } + ObjectPayload::Enumerate { iterator, count } => { + // Restore enumerate object + // Important: get_obj will trigger list_iterator restoration with __setstate__ + let iter_obj = self.get_obj(*iterator) + .map_err(|e| SnapshotError::msg(format!("enumerate: failed to get iterator {}: {:?}", iterator, e)))?; + + // Call enumerate(iter, start=count) to recreate + // Note: iter_obj should already be at the correct position after get_obj + let enumerate_fn = self.vm.builtins.get_attr("enumerate", self.vm) + .map_err(|e| SnapshotError::msg(format!("enumerate: builtin not found: {:?}", e)))?; + let count_obj = self.vm.ctx.new_int(*count); + + // Create kwargs with "start" parameter + use crate::function::{FuncArgs, KwArgs}; + use indexmap::IndexMap; + let mut kwargs_map = IndexMap::new(); + kwargs_map.insert("start".to_string(), count_obj.into()); + let kwargs = KwArgs::new(kwargs_map); + // Use iter_obj directly (don't clone, as it's already the restored iterator) + let args = FuncArgs::new(vec![iter_obj], kwargs); + + self.vm.invoke(&enumerate_fn, args) + .map_err(|e| SnapshotError::msg(format!("enumerate(iterator={}, start={}) failed: {:?}", iterator, count, e)))? + } + ObjectPayload::Zip { iterators } => { + // Restore zip object + let iter_objs: Result, _> = iterators.iter() + .map(|id| self.get_obj(*id)) + .collect(); + let iter_objs = iter_objs?; + let zip_fn = self.vm.builtins.get_attr("zip", self.vm) + .map_err(|_| SnapshotError::msg("zip not found"))?; + self.vm.invoke(&zip_fn, iter_objs) + .map_err(|_| SnapshotError::msg("zip restore failed"))? + } + ObjectPayload::Map { function, iterator } => { + // Restore map object + let func_obj = self.get_obj(*function)?; + let iter_obj = self.get_obj(*iterator)?; + let map_fn = self.vm.builtins.get_attr("map", self.vm) + .map_err(|_| SnapshotError::msg("map not found"))?; + self.vm.invoke(&map_fn, (func_obj, iter_obj)) + .map_err(|_| SnapshotError::msg("map restore failed"))? + } + ObjectPayload::Filter { function, iterator } => { + // Restore filter object + let func_obj = self.get_obj(*function)?; + let iter_obj = self.get_obj(*iterator)?; + let filter_fn = self.vm.builtins.get_attr("filter", self.vm) + .map_err(|_| SnapshotError::msg("filter not found"))?; + self.vm.invoke(&filter_fn, (func_obj, iter_obj)) + .map_err(|_| SnapshotError::msg("filter restore failed"))? + } + ObjectPayload::ListIterator { list, position } => { + // Restore list_iterator object + // 1. Get the list object and fill it + let list_obj = self.get_obj(*list)?; + + // IMPORTANT: fill_container must be called to populate the list's elements + // Otherwise the list will be empty! + let list_idx = *list as usize; + self.fill_container(list_idx)?; + + // 2. Create a new iterator from the list + let iter_fn = self.vm.builtins.get_attr("iter", self.vm) + .map_err(|_| SnapshotError::msg("iter not found"))?; + let new_iter = self.vm.invoke(&iter_fn, (list_obj.clone(),)) + .map_err(|e| SnapshotError::msg(format!("iter() failed: {:?}", e)))?; + + // 3. Advance the iterator to the saved position by calling __next__ + for _ in 0..*position { + match self.vm.call_method(&new_iter, "__next__", ()) { + Ok(_) => { + // Successfully advanced, continue + } + Err(e) => { + // Check if it's StopIteration (iterator exhausted early) + let class_name = e.class().name(); + if &*class_name == "StopIteration" { + // Iterator exhausted before reaching target position, break + break; + } else { + // Other error, propagate + return Err(SnapshotError::msg(format!("list_iterator advance failed: {:?}", e))); + } + } + } + } + + new_iter + } + ObjectPayload::Range { start, stop, step } => { + // Restore range object by calling range(start, stop, step) + let range_fn = self.vm.builtins.get_attr("range", self.vm) + .map_err(|_| SnapshotError::msg("range not found"))?; + let start_obj = self.vm.ctx.new_int(*start); + let stop_obj = self.vm.ctx.new_int(*stop); + let step_obj = self.vm.ctx.new_int(*step); + self.vm.invoke(&range_fn, (start_obj, stop_obj, step_obj)) + .map_err(|e| SnapshotError::msg(format!("range({}, {}, {}) failed: {:?}", start, stop, step, e)))? + } + ObjectPayload::RangeIterator { range, position } => { + // Restore range_iterator object + // 1. Get the range object + let range_obj = self.get_obj(*range)?; + + // 2. Create a new iterator from the range + let iter_fn = self.vm.builtins.get_attr("iter", self.vm) + .map_err(|_| SnapshotError::msg("iter not found"))?; + let new_iter = self.vm.invoke(&iter_fn, (range_obj.clone(),)) + .map_err(|e| SnapshotError::msg(format!("iter(range) failed: {:?}", e)))?; + + // 3. Advance the iterator to the saved position by calling __next__ + for _ in 0..*position { + match self.vm.call_method(&new_iter, "__next__", ()) { + Ok(_) => { + // Successfully advanced, continue + } + Err(e) => { + // Check if it's StopIteration (iterator exhausted early) + let class_name = e.class().name(); + if &*class_name == "StopIteration" { + // Iterator exhausted before reaching target position, break + break; + } else { + // Other error, propagate + return Err(SnapshotError::msg(format!("range_iterator advance failed: {:?}", e))); + } + } + } + } + + new_iter + } + }; + self.objects[idx] = Some(obj); + self.restoring[idx] = false; + Ok(()) + } + + fn fill_container(&mut self, idx: usize) -> Result<(), SnapshotError> { + if self.filled[idx] { + return Ok(()); + } + let entry = &self.entries[idx]; + let Some(obj) = self.objects[idx].clone() else { + return Ok(()); + }; + match &entry.payload { + ObjectPayload::List(items) => { + let list = obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("list fill type error"))?; + let mut data = list.borrow_vec_mut(); + for id in items { + data.push(self.get_obj(*id)?); + } + } + ObjectPayload::Dict(items) => { + let dict = PyDictRef::try_from_object(self.vm, obj) + .map_err(|_| SnapshotError::msg("dict fill type error"))?; + for (k, v) in items { + let key = self.get_obj(*k)?; + let value = self.get_obj(*v)?; + dict.set_item(&*key, value, self.vm) + .map_err(|_| SnapshotError::msg("dict fill failed"))?; + } + } + ObjectPayload::Set(items) => { + let set = obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("set fill type error"))?; + for id in items { + set.add(self.get_obj(*id)?, self.vm) + .map_err(|_| SnapshotError::msg("set add failed"))?; + } + } + ObjectPayload::Type(payload) => { + // Fill in the real bases and attributes for Type objects + let typ = obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("type fill type error"))?; + + // Fill bases + if !payload.bases.is_empty() { + let mut bases = Vec::new(); + for base_id in &payload.bases { + let base_obj = self.get_obj(*base_id)?; + let base_type = base_obj.downcast::() + .map_err(|_| SnapshotError::msg("type base invalid"))?; + bases.push(base_type); + } + // Update the bases + *typ.bases.write() = bases; + } + + // Fill attributes + let attrs = build_type_attributes(self, payload.dict, idx as ObjId)?; + for (key, value) in attrs.iter() { + typ.attributes.write().insert(key.clone(), value.clone()); + } + + // Apply deferred attributes + apply_deferred_type_attrs(self, obj.clone(), payload.dict, idx as ObjId)?; + } + _ => {} + } + self.filled[idx] = true; + Ok(()) + } + + fn apply_instance_state(&mut self, idx: usize) -> Result<(), SnapshotError> { + let entry = &self.entries[idx]; + let ObjectPayload::Instance(payload) = &entry.payload else { + return Ok(()); + }; + let Some(instance) = self.objects[idx].clone() else { + return Ok(()); + }; + let Some(state_id) = payload.state else { + return Ok(()); + }; + let state = self.get_obj(state_id)?; + if let Some(setstate) = self + .vm + .get_attribute_opt(instance.clone(), "__setstate__") + .map_err(|_| SnapshotError::msg("__setstate__ lookup failed"))? + { + setstate + .call((state,), self.vm) + .map_err(|_| SnapshotError::msg("__setstate__ failed"))?; + return Ok(()); + } + if let Some(dict) = instance.dict() { + // state can be None for some objects + if self.vm.is_none(&state) { + return Ok(()); + } + let state_dict = PyDictRef::try_from_object(self.vm, state.clone()) + .map_err(|_| { + SnapshotError::msg("state must be dict") + })?; + for (key, value) in &state_dict { + dict.set_item(&*key, value, self.vm) + .map_err(|_| SnapshotError::msg("state set failed"))?; + } + } + Ok(()) + } + + fn get_obj(&mut self, id: ObjId) -> Result { + let idx = id as usize; + if self.objects.get(idx).and_then(|o| o.as_ref()).is_none() { + self.restore_entry(idx)?; + } + Ok(self.objects[idx].clone().unwrap()) + } + + fn resolve_globals( + &mut self, + globals_obj: PyObjectRef, + module_id: Option, + ) -> Result { + if let Ok(dict) = PyDictRef::try_from_object(self.vm, globals_obj.clone()) { + return Ok(dict); + } + if let Some(dict) = globals_obj.dict() { + return Ok(dict); + } + if let Some(module_id) = module_id { + let module_obj = self.get_obj(module_id)?; + if let Ok(dict) = PyDictRef::try_from_object(self.vm, module_obj.clone()) { + return Ok(dict); + } + if let Some(dict) = module_obj.dict() { + return Ok(dict); + } + if let Some(name) = module_obj + .downcast_ref::() + .map(|s| s.as_str().to_owned()) + { + if let Ok(module) = lookup_module(self.vm, &name) { + if let Some(dict) = module.dict() { + return Ok(dict); + } + } + } + } + if let Ok(dict) = mapping_to_dict(self.vm, &globals_obj) { + return Ok(dict); + } + let root_obj = self.get_obj(self.root)?; + if let Ok(dict) = PyDictRef::try_from_object(self.vm, root_obj.clone()) { + return Ok(dict); + } + if let Some(dict) = root_obj.dict() { + return Ok(dict); + } + Err(SnapshotError::msg(format!( + "function globals invalid: {}", + globals_obj.class().name() + ))) + } +} + +fn lookup_module(vm: &VirtualMachine, name: &str) -> Result { + if name == "builtins" { + return Ok(vm.builtins.clone().into()); + } + if name == "sys" { + return Ok(vm.sys_module.clone().into()); + } + + // Handle module name aliases (Python 2 -> Python 3) + let actual_name = match name { + "thread" => "_thread", + "_os" => "posix", // _os is typically mapped to posix or nt + _ => name, + }; + + // Try to get from sys.modules first + let sys_modules = vm.sys_module + .get_attr("modules", vm) + .map_err(|_| SnapshotError::msg("sys.modules unavailable"))?; + + if let Ok(module) = sys_modules.get_item(actual_name, vm) { + return Ok(module); + } + + // If not found, try to import it + let import_func = vm.builtins + .get_attr("__import__", vm) + .map_err(|_| SnapshotError::msg("__import__ not found"))?; + + match import_func.call((actual_name,), vm) { + Ok(module) => { + Ok(module) + } + Err(e) => { + let msg = e + .as_object() + .str(vm) + .map(|s| s.to_string()) + .unwrap_or_else(|_| "unknown import error".to_owned()); + Err(SnapshotError::msg(format!( + "failed to import module: {name} ({msg})" + ))) + } + } +} + +fn build_type_attributes( + reader: &mut SnapshotReader<'_>, + dict_id: ObjId, + type_id: ObjId, +) -> Result { + if dict_id == type_id { + return Ok(crate::builtins::type_::PyAttributes::default()); + } + let entry = reader + .entries + .get(dict_id as usize) + .ok_or_else(|| SnapshotError::msg("type dict missing"))?; + let items = match &entry.payload { + ObjectPayload::Dict(items) => items.clone(), + ObjectPayload::BuiltinDict { name } => { + let module = lookup_module(reader.vm, name)?; + let dict = module + .dict() + .ok_or_else(|| SnapshotError::msg("builtin module missing dict"))?; + return build_type_attributes_from_dict(reader, dict, type_id); + } + ObjectPayload::Module { dict, .. } => { + let dict_obj = reader.get_obj(*dict)?; + let dict = PyDictRef::try_from_object(reader.vm, dict_obj) + .map_err(|_| SnapshotError::msg("module dict invalid"))?; + return build_type_attributes_from_dict(reader, dict, type_id); + } + _ => { + let dict_obj = reader.get_obj(dict_id)?; + if let Ok(dict) = PyDictRef::try_from_object(reader.vm, dict_obj.clone()) { + return build_type_attributes_from_dict(reader, dict, type_id); + } + if let Ok(dict) = mapping_to_dict(reader.vm, &dict_obj) { + return build_type_attributes_from_dict(reader, dict, type_id); + } + return Ok(crate::builtins::type_::PyAttributes::default()); + } + }; + let mut attrs = crate::builtins::type_::PyAttributes::default(); + for (key_id, val_id) in items { + if key_id == type_id || val_id == type_id { + continue; + } + let key_obj = reader.get_obj(key_id)?; + let key = key_obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("type dict key must be str"))?; + let value = reader.get_obj(val_id)?; + let interned = reader.vm.ctx.intern_str(key.as_str()); + attrs.insert(interned, value); + } + Ok(attrs) +} + +fn build_type_attributes_from_dict( + reader: &mut SnapshotReader<'_>, + dict: PyDictRef, + type_id: ObjId, +) -> Result { + let mut attrs = crate::builtins::type_::PyAttributes::default(); + for (key, value) in &dict { + let _ = type_id; + let key = key + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("type dict key must be str"))?; + let interned = reader.vm.ctx.intern_str(key.as_str()); + attrs.insert(interned, value); + } + Ok(attrs) +} + +fn apply_deferred_type_attrs( + reader: &mut SnapshotReader<'_>, + typ_obj: PyObjectRef, + dict_id: ObjId, + type_id: ObjId, +) -> Result<(), SnapshotError> { + let entry = reader + .entries + .get(dict_id as usize) + .ok_or_else(|| SnapshotError::msg("type dict missing"))?; + let items = match &entry.payload { + ObjectPayload::Dict(items) => items.clone(), + _ => return Ok(()), + }; + for (key_id, val_id) in items { + if key_id != type_id && val_id != type_id { + continue; + } + let key_obj = reader.get_obj(key_id)?; + let key = key_obj + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("type dict key must be str"))?; + let value = reader.get_obj(val_id)?; + let key_interned = reader.vm.ctx.intern_str(key.as_str()); + typ_obj + .set_attr(key_interned, value, reader.vm) + .map_err(|_| SnapshotError::msg("type attribute set failed"))?; + } + Ok(()) +} + +fn kwargs_from_dict(dict: PyDictRef) -> Result { + let mut map = indexmap::IndexMap::new(); + for (key, value) in &dict { + let key = key + .downcast_ref::() + .ok_or_else(|| SnapshotError::msg("kwargs key must be str"))?; + map.insert(key.as_str().to_owned(), value); + } + Ok(crate::function::KwArgs::new(map)) +} + +fn mapping_to_dict(vm: &VirtualMachine, mapping: &PyObjectRef) -> Result { + let items = vm + .call_method(mapping.as_object(), "items", ()) + .map_err(|_| SnapshotError::msg("globals items() failed"))?; + let iter = items + .get_iter(vm) + .map_err(|_| SnapshotError::msg("globals items() not iterable"))?; + let dict = vm.ctx.new_dict(); + loop { + let next = iter + .next(vm) + .map_err(|_| SnapshotError::msg("globals items() iteration failed"))?; + let PyIterReturn::Return(item) = next else { + break; + }; + let (key, value) = if let Some(pair) = item.downcast_ref::() { + if pair.len() != 2 { + return Err(SnapshotError::msg("globals item must be (key, value)")); + } + ( + pair.get(0).unwrap().clone(), + pair.get(1).unwrap().clone(), + ) + } else if let Some(pair) = item.downcast_ref::() { + if pair.borrow_vec().len() != 2 { + return Err(SnapshotError::msg("globals item must be [key, value]")); + } + let values = pair.borrow_vec(); + (values[0].clone(), values[1].clone()) + } else { + return Err(SnapshotError::msg("globals item must be tuple/list")); + }; + dict.set_item(&*key, value, vm) + .map_err(|_| SnapshotError::msg("globals item set failed"))?; + } + Ok(dict) +} + +#[derive(Debug, Clone)] +struct CborWriter { + buf: Vec, +} + +impl CborWriter { + fn new() -> Self { + Self { buf: Vec::new() } + } + + fn into_bytes(self) -> Vec { + self.buf + } + + fn write_uint(&mut self, value: u64) { + write_uint_major(&mut self.buf, 0, value); + } + + fn write_bytes(&mut self, value: &[u8]) { + write_uint_major(&mut self.buf, 2, value.len() as u64); + self.buf.extend_from_slice(value); + } + + fn write_text(&mut self, value: &str) { + write_uint_major(&mut self.buf, 3, value.len() as u64); + self.buf.extend_from_slice(value.as_bytes()); + } + + fn write_array_len(&mut self, len: usize) { + write_uint_major(&mut self.buf, 4, len as u64); + } + + fn write_map_len(&mut self, len: usize) { + write_uint_major(&mut self.buf, 5, len as u64); + } + + fn write_bool(&mut self, value: bool) { + self.buf.push(if value { 0xf5 } else { 0xf4 }); + } + + fn write_null(&mut self) { + self.buf.push(0xf6); + } + + fn write_f64(&mut self, value: f64) { + self.buf.push(0xfb); + self.buf.extend_from_slice(&value.to_be_bytes()); + } +} + +fn write_uint_major(buf: &mut Vec, major: u8, value: u64) { + if value < 24 { + buf.push((major << 5) | value as u8); + } else if value <= u8::MAX as u64 { + buf.push((major << 5) | 24); + buf.push(value as u8); + } else if value <= u16::MAX as u64 { + buf.push((major << 5) | 25); + buf.extend_from_slice(&(value as u16).to_be_bytes()); + } else if value <= u32::MAX as u64 { + buf.push((major << 5) | 26); + buf.extend_from_slice(&(value as u32).to_be_bytes()); + } else { + buf.push((major << 5) | 27); + buf.extend_from_slice(&value.to_be_bytes()); + } +} + +#[derive(Debug)] +struct CborReader<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> CborReader<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + fn read_u8(&mut self) -> Result { + let b = *self.data.get(self.pos).ok_or_else(|| SnapshotError::msg("cbor eof"))?; + self.pos += 1; + Ok(b) + } + + fn read_exact(&mut self, len: usize) -> Result<&'a [u8], SnapshotError> { + let end = self.pos + len; + let slice = self.data.get(self.pos..end).ok_or_else(|| SnapshotError::msg("cbor eof"))?; + self.pos = end; + Ok(slice) + } + + fn read_uint(&mut self, info: u8) -> Result { + match info { + 0..=23 => Ok(info as u64), + 24 => Ok(self.read_u8()? as u64), + 25 => Ok(u16::from_be_bytes(self.read_exact(2)?.try_into().unwrap()) as u64), + 26 => Ok(u32::from_be_bytes(self.read_exact(4)?.try_into().unwrap()) as u64), + 27 => Ok(u64::from_be_bytes(self.read_exact(8)?.try_into().unwrap())), + _ => Err(SnapshotError::msg("unsupported uint")), + } + } + + fn read_value(&mut self) -> Result { + let head = self.read_u8()?; + let major = head >> 5; + let info = head & 0x1f; + match major { + 0 => Ok(CborValue::Uint(self.read_uint(info)?)), + 1 => Ok(CborValue::Nint(self.read_uint(info)?)), + 2 => { + let len = self.read_uint(info)? as usize; + let bytes = self.read_exact(len)?.to_vec(); + Ok(CborValue::Bytes(bytes)) + } + 3 => { + let len = self.read_uint(info)? as usize; + let bytes = self.read_exact(len)?.to_vec(); + let text = String::from_utf8(bytes).map_err(|_| SnapshotError::msg("utf8 error"))?; + Ok(CborValue::Text(text)) + } + 4 => { + let len = self.read_uint(info)? as usize; + let mut items = Vec::with_capacity(len); + for _ in 0..len { + items.push(self.read_value()?); + } + Ok(CborValue::Array(items)) + } + 5 => { + let len = self.read_uint(info)? as usize; + let mut items = Vec::with_capacity(len); + for _ in 0..len { + let key = self.read_value()?; + let val = self.read_value()?; + items.push((key, val)); + } + Ok(CborValue::Map(items)) + } + 7 => match info { + 20 => Ok(CborValue::Bool(false)), + 21 => Ok(CborValue::Bool(true)), + 22 => Ok(CborValue::Null), + 27 => { + let bytes = self.read_exact(8)?; + Ok(CborValue::Float(f64::from_be_bytes(bytes.try_into().unwrap()))) + } + _ => Err(SnapshotError::msg("unsupported simple")), + }, + _ => Err(SnapshotError::msg("unsupported major")), + } + } +} + +#[derive(Debug, Clone)] +enum CborValue { + Uint(u64), + Nint(u64), + Bytes(Vec), + Text(String), + Array(Vec), + Map(Vec<(CborValue, CborValue)>), + Bool(bool), + Null, + Float(f64), +} + +fn encode_checkpoint_state(state: &CheckpointState) -> Vec { + let mut writer = CborWriter::new(); + let mut fields = Vec::new(); + fields.push(("version", CborValue::Uint(state.version as u64))); + fields.push(("source_path", CborValue::Text(state.source_path.clone()))); + + // Encode frames array + let frames_array = state.frames.iter().map(|frame_state| { + let stack_array = frame_state.stack.iter() + .map(|obj_id| CborValue::Uint(*obj_id as u64)) + .collect::>(); + + // Encode blocks array + let blocks_array = frame_state.blocks.iter().map(|block_state| { + encode_block_state(block_state) + }).collect::>(); + + CborValue::Map(vec![ + (CborValue::Text("code".to_owned()), CborValue::Bytes(frame_state.code.clone())), + (CborValue::Text("lasti".to_owned()), CborValue::Uint(frame_state.lasti as u64)), + (CborValue::Text("locals".to_owned()), CborValue::Uint(frame_state.locals as u64)), + (CborValue::Text("stack".to_owned()), CborValue::Array(stack_array)), + (CborValue::Text("blocks".to_owned()), CborValue::Array(blocks_array)), + ]) + }).collect::>(); + fields.push(("frames", CborValue::Array(frames_array))); + + fields.push(("root", CborValue::Uint(state.root as u64))); + let objects = state + .objects + .iter() + .map(encode_object_entry) + .collect::>(); + fields.push(("objects", CborValue::Array(objects))); + write_cbor_map(&mut writer, fields); + writer.into_bytes() +} + +fn decode_checkpoint_state(data: &[u8]) -> Result { + let mut reader = CborReader::new(data); + let value = reader.read_value()?; + let map = match value { + CborValue::Map(map) => map, + _ => return Err(SnapshotError::msg("checkpoint is not map")), + }; + let mut version = None; + let mut source_path = None; + let mut frames_data = None; + // Old format fields (for backward compatibility) + let mut lasti = None; + let mut code = None; + let mut root = None; + let mut objects = None; + for (key, val) in map { + let key = match key { + CborValue::Text(text) => text, + _ => return Err(SnapshotError::msg("invalid map key")), + }; + match key.as_str() { + "version" => version = Some(expect_uint(val)? as u32), + "source_path" => source_path = Some(expect_text(val)?), + "frames" => { + let arr = expect_array(val)?; + let mut frame_states = Vec::new(); + for frame_val in arr { + let frame_map = expect_map(frame_val)?; + let mut f_code = None; + let mut f_lasti = None; + let mut f_locals = None; + let mut f_stack = None; + let mut f_blocks = None; + for (k, v) in frame_map { + let k = expect_text(k)?; + match k.as_str() { + "code" => f_code = Some(expect_bytes(v)?), + "lasti" => f_lasti = Some(expect_uint(v)? as u32), + "locals" => f_locals = Some(expect_uint(v)? as ObjId), + "stack" => { + let arr = expect_array(v)?; + let mut stack_ids = Vec::new(); + for item in arr { + stack_ids.push(expect_uint(item)? as ObjId); + } + f_stack = Some(stack_ids); + } + "blocks" => { + let arr = expect_array(v)?; + let mut blocks = Vec::new(); + for block_val in arr { + blocks.push(decode_block_state(block_val)?); + } + f_blocks = Some(blocks); + } + _ => {} + } + } + frame_states.push(FrameState { + code: f_code.ok_or_else(|| SnapshotError::msg("missing frame code"))?, + lasti: f_lasti.ok_or_else(|| SnapshotError::msg("missing frame lasti"))?, + locals: f_locals.ok_or_else(|| SnapshotError::msg("missing frame locals"))?, + stack: f_stack.unwrap_or_else(Vec::new), // For backward compatibility + blocks: f_blocks.unwrap_or_else(Vec::new), // For backward compatibility + }); + } + frames_data = Some(frame_states); + } + // Old format fields + "lasti" => lasti = Some(expect_uint(val)? as u32), + "code" => code = Some(expect_bytes(val)?), + "root" => root = Some(expect_uint(val)? as ObjId), + "objects" => { + let arr = expect_array(val)?; + let mut entries = Vec::new(); + for item in arr { + entries.push(decode_object_entry(item)?); + } + objects = Some(entries); + } + _ => {} + } + } + + let root = root.ok_or_else(|| SnapshotError::msg("missing root"))?; + + // Handle backward compatibility: if 'frames' field doesn't exist, convert old format + let frames = if let Some(frames_data) = frames_data { + frames_data + } else { + // Old format: single frame + let lasti = lasti.ok_or_else(|| SnapshotError::msg("missing lasti"))?; + let code = code.ok_or_else(|| SnapshotError::msg("missing code"))?; + vec![FrameState { + code, + lasti, + locals: root, // Old format: locals == globals for module-level + stack: Vec::new(), // Old format: assume empty stack + blocks: Vec::new(), // Old format: assume empty blocks + }] + }; + + Ok(CheckpointState { + version: version.ok_or_else(|| SnapshotError::msg("missing version"))?, + source_path: source_path.ok_or_else(|| SnapshotError::msg("missing source_path"))?, + frames, + root, + objects: objects.ok_or_else(|| SnapshotError::msg("missing objects"))?, + }) +} + +fn encode_object_entry(entry: &ObjectEntry) -> CborValue { + let payload = match &entry.payload { + ObjectPayload::None => CborValue::Null, + ObjectPayload::Bool(value) => CborValue::Bool(*value), + ObjectPayload::Int(value) => CborValue::Text(value.clone()), + ObjectPayload::Float(value) => CborValue::Float(*value), + ObjectPayload::Str(value) => CborValue::Text(value.clone()), + ObjectPayload::Bytes(value) => CborValue::Bytes(value.clone()), + ObjectPayload::List(items) => CborValue::Array(items.iter().map(|id| CborValue::Uint(*id as u64)).collect()), + ObjectPayload::Tuple(items) => CborValue::Array(items.iter().map(|id| CborValue::Uint(*id as u64)).collect()), + ObjectPayload::Dict(items) => CborValue::Array(items.iter().map(|(k, v)| { + CborValue::Array(vec![CborValue::Uint(*k as u64), CborValue::Uint(*v as u64)]) + }).collect()), + ObjectPayload::Set(items) => CborValue::Array(items.iter().map(|id| CborValue::Uint(*id as u64)).collect()), + ObjectPayload::FrozenSet(items) => CborValue::Array(items.iter().map(|id| CborValue::Uint(*id as u64)).collect()), + ObjectPayload::Module { name, dict } => CborValue::Map(vec![ + (CborValue::Text("name".to_owned()), CborValue::Text(name.clone())), + (CborValue::Text("dict".to_owned()), CborValue::Uint(*dict as u64)), + ]), + ObjectPayload::BuiltinModule { name } => CborValue::Map(vec![ + (CborValue::Text("name".to_owned()), CborValue::Text(name.clone())), + ]), + ObjectPayload::BuiltinDict { name } => CborValue::Map(vec![ + (CborValue::Text("name".to_owned()), CborValue::Text(name.clone())), + ]), + ObjectPayload::Function(func) => CborValue::Map(vec![ + (CborValue::Text("code".to_owned()), CborValue::Uint(func.code as u64)), + (CborValue::Text("globals".to_owned()), CborValue::Uint(func.globals as u64)), + (CborValue::Text("defaults".to_owned()), opt_id(func.defaults)), + (CborValue::Text("kwdefaults".to_owned()), opt_id(func.kwdefaults)), + (CborValue::Text("closure".to_owned()), opt_id(func.closure)), + (CborValue::Text("name".to_owned()), CborValue::Uint(func.name as u64)), + (CborValue::Text("qualname".to_owned()), CborValue::Uint(func.qualname as u64)), + (CborValue::Text("annotations".to_owned()), CborValue::Uint(func.annotations as u64)), + (CborValue::Text("module".to_owned()), CborValue::Uint(func.module as u64)), + (CborValue::Text("doc".to_owned()), CborValue::Uint(func.doc as u64)), + (CborValue::Text("type_params".to_owned()), CborValue::Uint(func.type_params as u64)), + ]), + ObjectPayload::BuiltinFunction(func) => CborValue::Map(vec![ + (CborValue::Text("name".to_owned()), CborValue::Text(func.name.clone())), + ( + CborValue::Text("module".to_owned()), + func.module + .as_ref() + .map(|m| CborValue::Text(m.clone())) + .unwrap_or(CborValue::Null), + ), + (CborValue::Text("self".to_owned()), opt_id(func.self_obj)), + ]), + ObjectPayload::Code(bytes) => CborValue::Bytes(bytes.clone()), + ObjectPayload::Type(typ) => CborValue::Map(vec![ + (CborValue::Text("name".to_owned()), CborValue::Text(typ.name.clone())), + (CborValue::Text("qualname".to_owned()), CborValue::Text(typ.qualname.clone())), + (CborValue::Text("bases".to_owned()), CborValue::Array(typ.bases.iter().map(|id| CborValue::Uint(*id as u64)).collect())), + (CborValue::Text("dict".to_owned()), CborValue::Uint(typ.dict as u64)), + (CborValue::Text("flags".to_owned()), CborValue::Uint(typ.flags)), + (CborValue::Text("basicsize".to_owned()), CborValue::Uint(typ.basicsize as u64)), + (CborValue::Text("itemsize".to_owned()), CborValue::Uint(typ.itemsize as u64)), + (CborValue::Text("member_count".to_owned()), CborValue::Uint(typ.member_count as u64)), + ]), + ObjectPayload::BuiltinType { module, name } => CborValue::Map(vec![ + (CborValue::Text("module".to_owned()), CborValue::Text(module.clone())), + (CborValue::Text("name".to_owned()), CborValue::Text(name.clone())), + ]), + ObjectPayload::Instance(inst) => CborValue::Map(vec![ + (CborValue::Text("type".to_owned()), CborValue::Uint(inst.typ as u64)), + (CborValue::Text("state".to_owned()), opt_id(inst.state)), + (CborValue::Text("new_args".to_owned()), opt_id(inst.new_args)), + (CborValue::Text("new_kwargs".to_owned()), opt_id(inst.new_kwargs)), + ]), + ObjectPayload::Cell(value) => opt_id(*value), + ObjectPayload::Enumerate { iterator, count } => CborValue::Map(vec![ + (CborValue::Text("iterator".to_owned()), CborValue::Uint(*iterator as u64)), + (CborValue::Text("count".to_owned()), if *count >= 0 { + CborValue::Uint(*count as u64) + } else { + CborValue::Nint((-*count - 1) as u64) + }), + ]), + ObjectPayload::Zip { iterators } => CborValue::Array( + iterators.iter().map(|id| CborValue::Uint(*id as u64)).collect() + ), + ObjectPayload::Map { function, iterator } => CborValue::Map(vec![ + (CborValue::Text("function".to_owned()), CborValue::Uint(*function as u64)), + (CborValue::Text("iterator".to_owned()), CborValue::Uint(*iterator as u64)), + ]), + ObjectPayload::Filter { function, iterator } => CborValue::Map(vec![ + (CborValue::Text("function".to_owned()), CborValue::Uint(*function as u64)), + (CborValue::Text("iterator".to_owned()), CborValue::Uint(*iterator as u64)), + ]), + ObjectPayload::ListIterator { list, position } => CborValue::Map(vec![ + (CborValue::Text("list".to_owned()), CborValue::Uint(*list as u64)), + (CborValue::Text("position".to_owned()), CborValue::Uint(*position as u64)), + ]), + ObjectPayload::RangeIterator { range, position } => CborValue::Map(vec![ + (CborValue::Text("range".to_owned()), CborValue::Uint(*range as u64)), + (CborValue::Text("position".to_owned()), CborValue::Uint(*position as u64)), + ]), + ObjectPayload::Range { start, stop, step } => CborValue::Map(vec![ + (CborValue::Text("start".to_owned()), CborValue::Text(start.to_string())), + (CborValue::Text("stop".to_owned()), CborValue::Text(stop.to_string())), + (CborValue::Text("step".to_owned()), CborValue::Text(step.to_string())), + ]), + }; + CborValue::Array(vec![CborValue::Uint(entry.tag as u64), payload]) +} + +fn decode_object_entry(value: CborValue) -> Result { + let arr = expect_array(value)?; + if arr.len() != 2 { + return Err(SnapshotError::msg("invalid object entry")); + } + let tag = expect_uint(arr[0].clone())? as u8; + let tag = match tag { + 0 => ObjTag::None, + 1 => ObjTag::Bool, + 2 => ObjTag::Int, + 3 => ObjTag::Float, + 4 => ObjTag::Str, + 5 => ObjTag::Bytes, + 6 => ObjTag::List, + 7 => ObjTag::Tuple, + 8 => ObjTag::Dict, + 9 => ObjTag::Set, + 10 => ObjTag::FrozenSet, + 11 => ObjTag::Module, + 12 => ObjTag::Function, + 13 => ObjTag::Code, + 14 => ObjTag::Type, + 15 => ObjTag::BuiltinType, + 16 => ObjTag::Instance, + 17 => ObjTag::Cell, + 18 => ObjTag::BuiltinModule, + 19 => ObjTag::BuiltinDict, + 20 => ObjTag::BuiltinFunction, + 21 => ObjTag::Enumerate, + 22 => ObjTag::Zip, + 23 => ObjTag::Map, + 24 => ObjTag::Filter, + 25 => ObjTag::ListIterator, + 26 => ObjTag::RangeIterator, + 27 => ObjTag::Range, + _ => return Err(SnapshotError::msg("unknown tag")), + }; + let payload = decode_payload(tag, arr[1].clone())?; + Ok(ObjectEntry { tag, payload }) +} + +fn decode_payload(tag: ObjTag, value: CborValue) -> Result { + match tag { + ObjTag::None => Ok(ObjectPayload::None), + ObjTag::Bool => Ok(ObjectPayload::Bool(expect_bool(value)?)), + ObjTag::Int => Ok(ObjectPayload::Int(expect_text(value)?)), + ObjTag::Float => Ok(ObjectPayload::Float(expect_float(value)?)), + ObjTag::Str => Ok(ObjectPayload::Str(expect_text(value)?)), + ObjTag::Bytes => Ok(ObjectPayload::Bytes(expect_bytes(value)?)), + ObjTag::List => Ok(ObjectPayload::List(expect_id_list(value)?)), + ObjTag::Tuple => Ok(ObjectPayload::Tuple(expect_id_list(value)?)), + ObjTag::Dict => { + let arr = expect_array(value)?; + let mut items = Vec::new(); + for item in arr { + let pair = expect_array(item)?; + if pair.len() != 2 { + return Err(SnapshotError::msg("dict entry invalid")); + } + items.push((expect_uint(pair[0].clone())? as ObjId, expect_uint(pair[1].clone())? as ObjId)); + } + Ok(ObjectPayload::Dict(items)) + } + ObjTag::Set => Ok(ObjectPayload::Set(expect_id_list(value)?)), + ObjTag::FrozenSet => Ok(ObjectPayload::FrozenSet(expect_id_list(value)?)), + ObjTag::Module => { + let map = expect_map(value)?; + let name = expect_text(map_get(&map, "name")?)?; + let dict = expect_uint(map_get(&map, "dict")?)? as ObjId; + Ok(ObjectPayload::Module { name, dict }) + } + ObjTag::BuiltinModule => { + let map = expect_map(value)?; + let name = expect_text(map_get(&map, "name")?)?; + Ok(ObjectPayload::BuiltinModule { name }) + } + ObjTag::BuiltinDict => { + let map = expect_map(value)?; + let name = expect_text(map_get(&map, "name")?)?; + Ok(ObjectPayload::BuiltinDict { name }) + } + ObjTag::Function => { + let map = expect_map(value)?; + Ok(ObjectPayload::Function(FunctionPayload { + code: expect_uint(map_get(&map, "code")?)? as ObjId, + globals: expect_uint(map_get(&map, "globals")?)? as ObjId, + defaults: opt_id_decode(&map_get(&map, "defaults")?), + kwdefaults: opt_id_decode(&map_get(&map, "kwdefaults")?), + closure: opt_id_decode(&map_get(&map, "closure")?), + name: expect_uint(map_get(&map, "name")?)? as ObjId, + qualname: expect_uint(map_get(&map, "qualname")?)? as ObjId, + annotations: expect_uint(map_get(&map, "annotations")?)? as ObjId, + module: expect_uint(map_get(&map, "module")?)? as ObjId, + doc: expect_uint(map_get(&map, "doc")?)? as ObjId, + type_params: expect_uint(map_get(&map, "type_params")?)? as ObjId, + })) + } + ObjTag::BuiltinFunction => { + let map = expect_map(value)?; + let name = expect_text(map_get(&map, "name")?)?; + let module = match map_get(&map, "module")? { + CborValue::Null => None, + CborValue::Text(text) => Some(text.clone()), + _ => return Err(SnapshotError::msg("builtin function module invalid")), + }; + let self_obj = opt_id_decode(&map_get(&map, "self")?); + Ok(ObjectPayload::BuiltinFunction(BuiltinFunctionPayload { + name, + module, + self_obj, + })) + } + ObjTag::Code => Ok(ObjectPayload::Code(expect_bytes(value)?)), + ObjTag::Type => { + let map = expect_map(value)?; + Ok(ObjectPayload::Type(TypePayload { + name: expect_text(map_get(&map, "name")?)?, + qualname: expect_text(map_get(&map, "qualname")?)?, + bases: expect_id_list(map_get(&map, "bases")?)?, + dict: expect_uint(map_get(&map, "dict")?)? as ObjId, + flags: expect_uint(map_get(&map, "flags")?)?, + basicsize: expect_uint(map_get(&map, "basicsize")?)? as usize, + itemsize: expect_uint(map_get(&map, "itemsize")?)? as usize, + member_count: expect_uint(map_get(&map, "member_count")?)? as usize, + })) + } + ObjTag::BuiltinType => { + let map = expect_map(value)?; + Ok(ObjectPayload::BuiltinType { + module: expect_text(map_get(&map, "module")?)?, + name: expect_text(map_get(&map, "name")?)?, + }) + } + ObjTag::Instance => { + let map = expect_map(value)?; + Ok(ObjectPayload::Instance(InstancePayload { + typ: expect_uint(map_get(&map, "type")?)? as ObjId, + state: opt_id_decode(&map_get(&map, "state")?), + new_args: opt_id_decode(&map_get(&map, "new_args")?), + new_kwargs: opt_id_decode(&map_get(&map, "new_kwargs")?), + })) + } + ObjTag::Cell => Ok(ObjectPayload::Cell(opt_id_decode(&value))), + ObjTag::Enumerate => { + let map = expect_map(value)?; + Ok(ObjectPayload::Enumerate { + iterator: expect_uint(map_get(&map, "iterator")?)? as ObjId, + count: expect_int(map_get(&map, "count")?)?, + }) + } + ObjTag::Zip => { + let arr = expect_array(value)?; + let iterators = arr.iter() + .map(|v| expect_uint(v.clone()).map(|id| id as ObjId)) + .collect::, _>>()?; + Ok(ObjectPayload::Zip { iterators }) + } + ObjTag::Map => { + let map = expect_map(value)?; + Ok(ObjectPayload::Map { + function: expect_uint(map_get(&map, "function")?)? as ObjId, + iterator: expect_uint(map_get(&map, "iterator")?)? as ObjId, + }) + } + ObjTag::Filter => { + let map = expect_map(value)?; + Ok(ObjectPayload::Filter { + function: expect_uint(map_get(&map, "function")?)? as ObjId, + iterator: expect_uint(map_get(&map, "iterator")?)? as ObjId, + }) + } + ObjTag::ListIterator => { + let map = expect_map(value)?; + Ok(ObjectPayload::ListIterator { + list: expect_uint(map_get(&map, "list")?)? as ObjId, + position: expect_uint(map_get(&map, "position")?)? as usize, + }) + } + ObjTag::RangeIterator => { + let map = expect_map(value)?; + Ok(ObjectPayload::RangeIterator { + range: expect_uint(map_get(&map, "range")?)? as ObjId, + position: expect_uint(map_get(&map, "position")?)? as usize, + }) + } + ObjTag::Range => { + let map = expect_map(value)?; + let start_str = expect_text(map_get(&map, "start")?)?; + let stop_str = expect_text(map_get(&map, "stop")?)?; + let step_str = expect_text(map_get(&map, "step")?)?; + Ok(ObjectPayload::Range { + start: start_str.parse::().unwrap_or(0), + stop: stop_str.parse::().unwrap_or(0), + step: step_str.parse::().unwrap_or(1), + }) + } + } +} + +fn opt_id(value: Option) -> CborValue { + match value { + Some(id) => CborValue::Uint(id as u64), + None => CborValue::Null, + } +} + +fn opt_id_decode(value: &CborValue) -> Option { + match value { + CborValue::Null => None, + CborValue::Uint(id) => Some(*id as ObjId), + _ => None, + } +} + +fn write_cbor_map(writer: &mut CborWriter, fields: Vec<(&str, CborValue)>) { + let mut entries: Vec<(Vec, Vec)> = Vec::with_capacity(fields.len()); + for (key, value) in fields { + let mut key_writer = CborWriter::new(); + key_writer.write_text(key); + let key_bytes = key_writer.into_bytes(); + let value_bytes = encode_cbor_value(value); + entries.push((key_bytes, value_bytes)); + } + entries.sort_by(|(a, _), (b, _)| cbor_key_cmp(a, b)); + writer.write_map_len(entries.len()); + for (key, value) in entries { + writer.buf.extend_from_slice(&key); + writer.buf.extend_from_slice(&value); + } +} + +fn encode_cbor_value(value: CborValue) -> Vec { + let mut writer = CborWriter::new(); + write_cbor_value(&mut writer, value); + writer.into_bytes() +} + +fn write_cbor_value(writer: &mut CborWriter, value: CborValue) { + match value { + CborValue::Uint(value) => writer.write_uint(value), + CborValue::Nint(value) => write_uint_major(&mut writer.buf, 1, value), + CborValue::Bytes(value) => writer.write_bytes(&value), + CborValue::Text(value) => writer.write_text(&value), + CborValue::Array(items) => { + writer.write_array_len(items.len()); + for item in items { + write_cbor_value(writer, item); + } + } + CborValue::Map(items) => { + let mut fields = Vec::with_capacity(items.len()); + for (k, v) in items { + let mut key_writer = CborWriter::new(); + write_cbor_value(&mut key_writer, k); + let key_bytes = key_writer.into_bytes(); + let value_bytes = encode_cbor_value(v); + fields.push((key_bytes, value_bytes)); + } + fields.sort_by(|(a, _), (b, _)| cbor_key_cmp(a, b)); + writer.write_map_len(fields.len()); + for (key, value) in fields { + writer.buf.extend_from_slice(&key); + writer.buf.extend_from_slice(&value); + } + } + CborValue::Bool(value) => writer.write_bool(value), + CborValue::Null => writer.write_null(), + CborValue::Float(value) => writer.write_f64(value), + } +} + +fn expect_uint(value: CborValue) -> Result { + match value { + CborValue::Uint(v) => Ok(v), + _ => Err(SnapshotError::msg("expected uint")), + } +} + +fn expect_int(value: CborValue) -> Result { + match value { + CborValue::Uint(v) => Ok(v as i64), + CborValue::Nint(v) => Ok(-(v as i64) - 1), + _ => Err(SnapshotError::msg("expected int")), + } +} + +fn expect_text(value: CborValue) -> Result { + match value { + CborValue::Text(v) => Ok(v), + _ => Err(SnapshotError::msg("expected text")), + } +} + +fn expect_bytes(value: CborValue) -> Result, SnapshotError> { + match value { + CborValue::Bytes(v) => Ok(v), + _ => Err(SnapshotError::msg("expected bytes")), + } +} + +fn expect_array(value: CborValue) -> Result, SnapshotError> { + match value { + CborValue::Array(v) => Ok(v), + _ => Err(SnapshotError::msg("expected array")), + } +} + +fn expect_map(value: CborValue) -> Result, SnapshotError> { + match value { + CborValue::Map(v) => Ok(v), + _ => Err(SnapshotError::msg("expected map")), + } +} + +fn expect_bool(value: CborValue) -> Result { + match value { + CborValue::Bool(v) => Ok(v), + _ => Err(SnapshotError::msg("expected bool")), + } +} + +fn expect_float(value: CborValue) -> Result { + match value { + CborValue::Float(v) => Ok(v), + _ => Err(SnapshotError::msg("expected float")), + } +} + +fn expect_id_list(value: CborValue) -> Result, SnapshotError> { + let arr = expect_array(value)?; + arr.into_iter() + .map(|v| Ok(expect_uint(v)? as ObjId)) + .collect() +} + +fn map_get(map: &[(CborValue, CborValue)], key: &str) -> Result { + for (k, v) in map { + if let CborValue::Text(text) = k { + if text == key { + return Ok(v.clone()); + } + } + } + Err(SnapshotError::msg("missing map key")) +} + +/// Encode a BlockState to CBOR +fn encode_block_state(block_state: &BlockState) -> CborValue { + let mut map = vec![ + (CborValue::Text("level".to_owned()), CborValue::Uint(block_state.level as u64)), + ]; + + let typ_value = match &block_state.typ { + BlockTypeState::Loop => { + CborValue::Text("Loop".to_owned()) + } + BlockTypeState::TryExcept { handler } => { + CborValue::Map(vec![ + (CborValue::Text("type".to_owned()), CborValue::Text("TryExcept".to_owned())), + (CborValue::Text("handler".to_owned()), CborValue::Uint(*handler as u64)), + ]) + } + BlockTypeState::Finally { handler } => { + CborValue::Map(vec![ + (CborValue::Text("type".to_owned()), CborValue::Text("Finally".to_owned())), + (CborValue::Text("handler".to_owned()), CborValue::Uint(*handler as u64)), + ]) + } + BlockTypeState::FinallyHandler { reason, prev_exc } => { + let mut inner_map = vec![ + (CborValue::Text("type".to_owned()), CborValue::Text("FinallyHandler".to_owned())), + ]; + if let Some(reason_state) = reason { + inner_map.push((CborValue::Text("reason".to_owned()), encode_unwind_reason(reason_state))); + } + if let Some(exc_id) = prev_exc { + inner_map.push((CborValue::Text("prev_exc".to_owned()), CborValue::Uint(*exc_id as u64))); + } + CborValue::Map(inner_map) + } + BlockTypeState::ExceptHandler { prev_exc } => { + let mut inner_map = vec![ + (CborValue::Text("type".to_owned()), CborValue::Text("ExceptHandler".to_owned())), + ]; + if let Some(exc_id) = prev_exc { + inner_map.push((CborValue::Text("prev_exc".to_owned()), CborValue::Uint(*exc_id as u64))); + } + CborValue::Map(inner_map) + } + }; + + map.push((CborValue::Text("typ".to_owned()), typ_value)); + CborValue::Map(map) +} + +/// Encode an UnwindReasonState to CBOR +fn encode_unwind_reason(reason: &UnwindReasonState) -> CborValue { + match reason { + UnwindReasonState::Returning { value } => { + CborValue::Map(vec![ + (CborValue::Text("kind".to_owned()), CborValue::Text("Returning".to_owned())), + (CborValue::Text("value".to_owned()), CborValue::Uint(*value as u64)), + ]) + } + UnwindReasonState::Raising { exception } => { + CborValue::Map(vec![ + (CborValue::Text("kind".to_owned()), CborValue::Text("Raising".to_owned())), + (CborValue::Text("exception".to_owned()), CborValue::Uint(*exception as u64)), + ]) + } + UnwindReasonState::Break { target } => { + CborValue::Map(vec![ + (CborValue::Text("kind".to_owned()), CborValue::Text("Break".to_owned())), + (CborValue::Text("target".to_owned()), CborValue::Uint(*target as u64)), + ]) + } + UnwindReasonState::Continue { target } => { + CborValue::Map(vec![ + (CborValue::Text("kind".to_owned()), CborValue::Text("Continue".to_owned())), + (CborValue::Text("target".to_owned()), CborValue::Uint(*target as u64)), + ]) + } + } +} + +/// Decode a BlockState from CBOR +fn decode_block_state(value: CborValue) -> Result { + let map = expect_map(value)?; + let level = expect_uint(map_get(&map, "level")?)? as usize; + let typ_val = map_get(&map, "typ")?; + + let typ = match typ_val { + CborValue::Text(text) => { + if text == "Loop" { + BlockTypeState::Loop + } else { + return Err(SnapshotError::msg(format!("unknown block type: {}", text))); + } + } + CborValue::Map(inner_map) => { + let type_name = expect_text(map_get(&inner_map, "type")?)?; + match type_name.as_str() { + "TryExcept" => { + let handler = expect_uint(map_get(&inner_map, "handler")?)? as u32; + BlockTypeState::TryExcept { handler } + } + "Finally" => { + let handler = expect_uint(map_get(&inner_map, "handler")?)? as u32; + BlockTypeState::Finally { handler } + } + "FinallyHandler" => { + let reason = if let Ok(reason_val) = map_get(&inner_map, "reason") { + Some(decode_unwind_reason(reason_val)?) + } else { + None + }; + let prev_exc = if let Ok(exc_val) = map_get(&inner_map, "prev_exc") { + Some(expect_uint(exc_val)? as ObjId) + } else { + None + }; + BlockTypeState::FinallyHandler { reason, prev_exc } + } + "ExceptHandler" => { + let prev_exc = if let Ok(exc_val) = map_get(&inner_map, "prev_exc") { + Some(expect_uint(exc_val)? as ObjId) + } else { + None + }; + BlockTypeState::ExceptHandler { prev_exc } + } + _ => { + return Err(SnapshotError::msg(format!("unknown block type: {}", type_name))); + } + } + } + _ => { + return Err(SnapshotError::msg("invalid block type format")); + } + }; + + Ok(BlockState { typ, level }) +} + +/// Decode an UnwindReasonState from CBOR +fn decode_unwind_reason(value: CborValue) -> Result { + let map = expect_map(value)?; + let kind = expect_text(map_get(&map, "kind")?)?; + + match kind.as_str() { + "Returning" => { + let value_id = expect_uint(map_get(&map, "value")?)? as ObjId; + Ok(UnwindReasonState::Returning { value: value_id }) + } + "Raising" => { + let exc_id = expect_uint(map_get(&map, "exception")?)? as ObjId; + Ok(UnwindReasonState::Raising { exception: exc_id }) + } + "Break" => { + let target = expect_uint(map_get(&map, "target")?)? as u32; + Ok(UnwindReasonState::Break { target }) + } + "Continue" => { + let target = expect_uint(map_get(&map, "target")?)? as u32; + Ok(UnwindReasonState::Continue { target }) + } + _ => { + Err(SnapshotError::msg(format!("unknown unwind reason: {}", kind))) + } + } +} + +// ============================================================================ +// Block Stack Conversion Functions +// ============================================================================ + +/// Convert frame Block to serializable BlockState +fn convert_block_to_state( + block: &crate::frame::Block, + writer: &SnapshotWriter<'_>, +) -> PyResult { + use crate::frame::{BlockType, UnwindReason}; + + let typ_state = match &block.typ { + BlockType::Loop => BlockTypeState::Loop, + + BlockType::TryExcept { handler } => { + BlockTypeState::TryExcept { + handler: handler.0, + } + } + + BlockType::Finally { handler } => { + BlockTypeState::Finally { + handler: handler.0, + } + } + + BlockType::FinallyHandler { reason, prev_exc } => { + let reason_state = reason.as_ref() + .map(|r| { + let value_id = match r { + UnwindReason::Returning { value } => { + writer.get_id(value).map(|id| UnwindReasonState::Returning { value: id }) + } + UnwindReason::Raising { exception } => { + let exc_obj = exception.as_object().to_owned(); + writer.get_id(&exc_obj).map(|id| UnwindReasonState::Raising { exception: id }) + } + UnwindReason::Break { target } => { + Ok(UnwindReasonState::Break { target: target.0 }) + } + UnwindReason::Continue { target } => { + Ok(UnwindReasonState::Continue { target: target.0 }) + } + }; + value_id.map_err(|e| writer.vm.new_value_error(format!("Failed to serialize unwind reason: {e:?}"))) + }) + .transpose()?; + let prev_exc_id = prev_exc.as_ref() + .map(|exc| { + let exc_obj = exc.as_object().to_owned(); + writer.get_id(&exc_obj).map_err(|e| writer.vm.new_value_error(format!("Failed to get exception ID: {e:?}"))) + }) + .transpose()?; + + BlockTypeState::FinallyHandler { + reason: reason_state, + prev_exc: prev_exc_id, + } + } + + BlockType::ExceptHandler { prev_exc } => { + let prev_exc_id = prev_exc.as_ref() + .map(|exc| { + let exc_obj = exc.as_object().to_owned(); + writer.get_id(&exc_obj).map_err(|e| writer.vm.new_value_error(format!("Failed to get exception ID: {e:?}"))) + }) + .transpose()?; + + BlockTypeState::ExceptHandler { + prev_exc: prev_exc_id, + } + } + }; + + Ok(BlockState { + typ: typ_state, + level: block.level, + }) +} + +/// Convert serializable BlockState to frame Block +pub(super) fn convert_block_state_to_block( + block_state: &BlockState, + objects: &[PyObjectRef], + vm: &VirtualMachine, +) -> PyResult { + use crate::frame::{Block, BlockType, UnwindReason}; + use crate::convert::TryFromObject; + + let typ = match &block_state.typ { + BlockTypeState::Loop => BlockType::Loop, + + BlockTypeState::TryExcept { handler } => { + BlockType::TryExcept { + handler: bytecode::Label(*handler), + } + } + + BlockTypeState::Finally { handler } => { + BlockType::Finally { + handler: bytecode::Label(*handler), + } + } + + BlockTypeState::FinallyHandler { reason, prev_exc } => { + let reason_opt = reason.as_ref() + .map(|r| { + match r { + UnwindReasonState::Returning { value } => { + let value_obj = objects.get(*value as usize) + .cloned() + .ok_or_else(|| vm.new_runtime_error(format!("return value {} not found", value)))?; + Ok(UnwindReason::Returning { value: value_obj }) + } + UnwindReasonState::Raising { exception } => { + let exc_obj = objects.get(*exception as usize) + .cloned() + .ok_or_else(|| vm.new_runtime_error(format!("exception {} not found", exception)))?; + let exc = crate::builtins::PyBaseExceptionRef::try_from_object(vm, exc_obj)?; + Ok(UnwindReason::Raising { exception: exc }) + } + UnwindReasonState::Break { target } => { + Ok(UnwindReason::Break { target: bytecode::Label(*target) }) + } + UnwindReasonState::Continue { target } => { + Ok(UnwindReason::Continue { target: bytecode::Label(*target) }) + } + } + }) + .transpose()?; + let prev_exc_opt = prev_exc.as_ref() + .map(|exc_id| { + let exc_obj = objects.get(*exc_id as usize) + .cloned() + .ok_or_else(|| vm.new_runtime_error(format!("exception {} not found", exc_id)))?; + crate::builtins::PyBaseExceptionRef::try_from_object(vm, exc_obj) + }) + .transpose()?; + + BlockType::FinallyHandler { + reason: reason_opt, + prev_exc: prev_exc_opt, + } + } + + BlockTypeState::ExceptHandler { prev_exc } => { + let prev_exc_opt = prev_exc.as_ref() + .map(|exc_id| { + let exc_obj = objects.get(*exc_id as usize) + .cloned() + .ok_or_else(|| vm.new_runtime_error(format!("exception {} not found", exc_id)))?; + crate::builtins::PyBaseExceptionRef::try_from_object(vm, exc_obj) + }) + .transpose()?; + + BlockType::ExceptHandler { + prev_exc: prev_exc_opt, + } + } + }; + + Ok(Block { + typ, + level: block_state.level, + }) +} diff --git a/crates/vm/src/vm/thread.rs b/crates/vm/src/vm/thread.rs index 2e687d99820..9c9079e26a6 100644 --- a/crates/vm/src/vm/thread.rs +++ b/crates/vm/src/vm/thread.rs @@ -55,6 +55,10 @@ where }) } +pub(crate) fn has_vm() -> bool { + VM_STACK.with(|vms| !vms.borrow().is_empty()) +} + #[must_use = "ThreadedVirtualMachine does nothing unless you move it to another thread and call .run()"] #[cfg(feature = "threading")] pub struct ThreadedVirtualMachine { diff --git a/examples/ast_visualize/README.md b/examples/ast_visualize/README.md new file mode 100644 index 00000000000..46446447023 --- /dev/null +++ b/examples/ast_visualize/README.md @@ -0,0 +1,182 @@ +# AST Visualize Example + +This example shows how to render a Python AST as a structured tree or a +Graphviz DOT file using RustPython's `ast` module. + +## Run + +Tree view (default): + +``` +./target/release/rustpython examples/ast_visualize/ast_view.py --file examples/ast_visualize/sample.py +``` + +Dump view (ast.dump): + +``` +./target/release/rustpython examples/ast_visualize/ast_view.py --file examples/ast_visualize/sample.py --format dump +``` + +Graphviz DOT output: + +``` +./target/release/rustpython examples/ast_visualize/ast_view.py --file examples/ast_visualize/sample.py --format dot --output ast.dot +``` + +## Graphviz 安装与渲染 + +macOS (Homebrew): + +``` +brew install graphviz +``` + +macOS (Conda): + +``` +conda install -c conda-forge graphviz +``` + +Ubuntu/Debian: + +``` +sudo apt-get update +sudo apt-get install graphviz +``` + +Fedora: + +``` +sudo dnf install graphviz +``` + +Arch: + +``` +sudo pacman -S graphviz +``` + +Windows (Chocolatey): + +``` +choco install graphviz +``` + +Windows (Scoop): + +``` +scoop install graphviz +``` + +安装完成后将 DOT 渲染为图片: + +``` +dot -Tpng ast.dot -o ast.png +``` + +打开图片: + +macOS: + +``` +open ast.png +``` + +Linux: + +``` +xdg-open ast.png +``` + +Windows: + +``` +start ast.png +``` + +## Example Output + +Tree view: + +``` +`-- Module + |-- FunctionDef name=add + | |-- arguments + | | |-- arg arg=a + | | `-- arg arg=b + | `-- Return + | `-- BinOp + | |-- Name id=a ctx=Load + | |-- Add + | `-- Name id=b ctx=Load + |-- Assign targets=list[1] + | |-- Name id=result ctx=Store + | `-- Call func=Name + | |-- Name id=add ctx=Load + | |-- Constant value=1 + | `-- Constant value=2 + `-- If + |-- Compare + | |-- Name id=result ctx=Load + | |-- Gt + | `-- Constant value=2 + `-- Expr + `-- Call func=Name + |-- Name id=print ctx=Load + `-- Constant value='ok' +``` + +Dump view (excerpt): + +``` +Module( + body=[ + FunctionDef( + name='add', + args=arguments( + posonlyargs=[], + args=[ + arg(arg='a'), + arg(arg='b')], + kwonlyargs=[], + kw_defaults=[], + defaults=[]), + body=[ + Return( + value=BinOp( + left=Name(id='a', ctx=Load()), + op=Add(), + right=Name(id='b', ctx=Load())))], + decorator_list=[]), + Assign( + targets=[ + Name(id='result', ctx=Store())], + value=Call( + func=Name(id='add', ctx=Load()), + args=[ + Constant(value=1), + Constant(value=2)], + keywords=[])), + If( + test=Compare( + left=Name(id='result', ctx=Load()), + ops=[ + Gt()], + comparators=[ + Constant(value=2)]), + body=[ + Expr( + value=Call( + func=Name(id='print', ctx=Load()), + args=[ + Constant(value='ok')], + keywords=[]))], + orelse=[])], + type_ignores=[]) +``` + +## Notes + +- Use `--code` to pass inline code. +- Use `--attrs` to include line/column info. +- If you render DOT, use Graphviz (dot) to convert it to an image. diff --git a/examples/ast_visualize/ast_view.py b/examples/ast_visualize/ast_view.py new file mode 100644 index 00000000000..b296e731572 --- /dev/null +++ b/examples/ast_visualize/ast_view.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +import argparse +import ast +import sys +from pathlib import Path +from typing import Iterable + + +SUMMARY_FIELDS: dict[type[ast.AST], tuple[str, ...]] = { + ast.FunctionDef: ("name",), + ast.AsyncFunctionDef: ("name",), + ast.ClassDef: ("name",), + ast.Name: ("id", "ctx"), + ast.arg: ("arg",), + ast.Attribute: ("attr", "ctx"), + ast.Constant: ("value",), + ast.Import: ("names",), + ast.ImportFrom: ("module", "names", "level"), + ast.alias: ("name", "asname"), + ast.Assign: ("targets",), + ast.AnnAssign: ("target",), + ast.Call: ("func",), +} + + +def read_source(args: argparse.Namespace) -> tuple[str, str]: + if args.file and args.code: + raise SystemExit("choose either --file or --code") + + if args.file: + path = Path(args.file) + return path.read_text(encoding="utf-8"), str(path) + + if args.code: + return args.code, "" + + return sys.stdin.read(), "" + + +def truncate(text: str, limit: int = 60) -> str: + if len(text) <= limit: + return text + return text[: limit - 3] + "..." + + +def format_value(value: object) -> str: + if isinstance(value, ast.AST): + return type(value).__name__ + if isinstance(value, list): + if value and all(isinstance(item, ast.alias) for item in value): + parts = [] + for item in value: + if item.asname: + parts.append(f"{item.name} as {item.asname}") + else: + parts.append(item.name) + return "[" + ", ".join(parts) + "]" + return f"list[{len(value)}]" + if value is None: + return "None" + return truncate(repr(value)) + + +def node_summary(node: ast.AST, show_attrs: bool) -> str: + fields = SUMMARY_FIELDS.get(type(node), ()) + parts: list[str] = [] + for field in fields: + value = getattr(node, field, None) + if field == "ctx" and isinstance(value, ast.AST): + parts.append(f"{field}={type(value).__name__}") + else: + parts.append(f"{field}={format_value(value)}") + + if show_attrs: + lineno = getattr(node, "lineno", None) + col = getattr(node, "col_offset", None) + if lineno is not None and col is not None: + parts.append(f"@{lineno}:{col}") + + if parts: + return f"{type(node).__name__} " + " ".join(parts) + return type(node).__name__ + + +def render_tree( + node: ast.AST, + lines: list[str], + prefix: str, + is_last: bool, + max_depth: int, + show_attrs: bool, + depth: int = 0, +) -> None: + connector = "`-- " if is_last else "|-- " + lines.append(prefix + connector + node_summary(node, show_attrs)) + + if depth >= max_depth: + if list(ast.iter_child_nodes(node)): + lines.append(prefix + (" " if is_last else "| ") + "`-- ...") + return + + children = list(ast.iter_child_nodes(node)) + for idx, child in enumerate(children): + last = idx == len(children) - 1 + next_prefix = prefix + (" " if is_last else "| ") + render_tree(child, lines, next_prefix, last, max_depth, show_attrs, depth + 1) + + +def to_tree_text(tree: ast.AST, max_depth: int, show_attrs: bool) -> str: + lines: list[str] = [] + render_tree(tree, lines, "", True, max_depth, show_attrs) + return "\n".join(lines) + + +def dump_node(node: object, show_attrs: bool, indent: int, level: int) -> str: + if isinstance(node, ast.AST): + parts: list[str] = [] + for name, value in ast.iter_fields(node): + parts.append(f"{name}={dump_node(value, show_attrs, indent, level + 1)}") + if show_attrs: + for name in getattr(node, "_attributes", ()): + if hasattr(node, name): + value = getattr(node, name) + parts.append(f"{name}={dump_node(value, show_attrs, indent, level + 1)}") + if indent <= 0 or not parts: + inner = ", ".join(parts) + return f"{type(node).__name__}({inner})" + pad = " " * (indent * (level + 1)) + inner = ",\n".join(pad + part for part in parts) + closing = " " * (indent * level) + return f"{type(node).__name__}(\n{inner}\n{closing})" + if isinstance(node, list): + if not node: + return "[]" + if indent <= 0: + inner = ", ".join(dump_node(item, show_attrs, indent, level + 1) for item in node) + return f"[{inner}]" + pad = " " * (indent * (level + 1)) + inner = ",\n".join(pad + dump_node(item, show_attrs, indent, level + 1) for item in node) + closing = " " * (indent * level) + return f"[\n{inner}\n{closing}]" + return repr(node) + + +def to_dump_text(tree: ast.AST, show_attrs: bool) -> str: + return dump_node(tree, show_attrs, indent=2, level=0) + + +def escape_dot_label(text: str) -> str: + return text.replace("\\", "\\\\").replace('"', "\\\"") + + +def to_dot(tree: ast.AST, show_attrs: bool) -> str: + lines = ["digraph AST {", "node [shape=box];"] + counter = 0 + + def add_node(node: ast.AST) -> int: + nonlocal counter + node_id = counter + counter += 1 + label = escape_dot_label(node_summary(node, show_attrs)) + lines.append(f'n{node_id} [label="{label}"];') + for child in ast.iter_child_nodes(node): + child_id = add_node(child) + lines.append(f"n{node_id} -> n{child_id};") + return node_id + + add_node(tree) + lines.append("}") + return "\n".join(lines) + + +def write_output(text: str, output: str | None) -> None: + if output: + Path(output).write_text(text, encoding="utf-8") + else: + sys.stdout.write(text) + if not text.endswith("\n"): + sys.stdout.write("\n") + + +def main() -> int: + parser = argparse.ArgumentParser(description="AST view utility") + parser.add_argument("--file", help="python source file") + parser.add_argument("--code", help="inline python code") + parser.add_argument( + "--mode", + default="exec", + choices=["exec", "eval", "single"], + help="ast.parse mode", + ) + parser.add_argument( + "--format", + default="tree", + choices=["tree", "dump", "dot"], + help="output format", + ) + parser.add_argument("--output", help="output file path") + parser.add_argument("--max-depth", type=int, default=20) + parser.add_argument("--attrs", action="store_true", help="include line/col info") + args = parser.parse_args() + + source, source_name = read_source(args) + tree = ast.parse(source, filename=source_name, mode=args.mode) + + if args.format == "dump": + text = to_dump_text(tree, args.attrs) + elif args.format == "dot": + text = to_dot(tree, args.attrs) + else: + text = to_tree_text(tree, args.max_depth, args.attrs) + + write_output(text, args.output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/ast_visualize/sample.py b/examples/ast_visualize/sample.py new file mode 100644 index 00000000000..5c2957fa2f0 --- /dev/null +++ b/examples/ast_visualize/sample.py @@ -0,0 +1,6 @@ +def add(a, b): + return a + b + +result = add(1, 2) +if result > 2: + print("ok") diff --git a/examples/breakpoint_resume_demo/actor_complex_demo.py b/examples/breakpoint_resume_demo/actor_complex_demo.py new file mode 100644 index 00000000000..97aae071e58 --- /dev/null +++ b/examples/breakpoint_resume_demo/actor_complex_demo.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from pathlib import Path +import os + +import rustpython_checkpoint as rpc # type: ignore + +CHECKPOINT_PATH = str(Path(__file__).with_suffix(".rpsnap")) +SEP = "=" * 68 + +print(SEP) +print("PVM Actor Checkpoint Placement Demo") +print("Flow: run -> #1(function) -> #2(loop) -> #3(if) -> #4(try) -> done") +print("Use --resume to continue after each checkpoint.") +print(SEP) + +actor = { + "actor_id": "actor:alpha", + "balance": 1200.0, + "limits": {"daily": 2000.0, "transfer": 600.0}, + "flags": [], + "history": [], +} + +mailbox = [ + {"type": "deposit", "amount": 1200.0, "meta": {"source": "salary"}}, + {"type": "transfer", "to": "actor:beta", "amount": 350.0, "meta": {"note": "rent"}}, + {"type": "transfer", "to": "actor:gamma", "amount": 650.0, "meta": {"note": "equipment"}}, + {"type": "transfer", "to": "actor:zeta", "amount": 950.0, "meta": {"note": "equipment"}}, + {"type": "adjust", "amount": -50.0, "meta": {"reason": "fee"}}, + {"type": "query", "fields": ["balance", "flags"]}, + {"type": "noop"}, +] + +normalized_mailbox = [ + { + "seq": i, + **msg, + "amount": round(msg.get("amount", 0.0), 2), + "meta": {**msg.get("meta", {}), "batch": "b001"}, + } + for i, msg in enumerate(mailbox, start=1) +] + +summary = { + "mailbox_size": len(normalized_mailbox), + "types": sorted({msg["type"] for msg in normalized_mailbox}), +} +actor["history"].append({"event": "bootstrap", **summary}) + +print(f"[init] actor={actor['actor_id']} balance={actor['balance']}") +print(f"[init] summary={summary}") + + +def stage_function(state: dict[str, object], messages: list[dict[str, object]]) -> None: + state["history"].append({"stage": "function", "count": len(messages)}) + state["flags"].append("function:armed") + print(SEP) + print("[checkpoint #1] inside function") + rpc.checkpoint(CHECKPOINT_PATH) + state["history"].append({"stage": "function", "resume": True}) + print("[resume #1] after function checkpoint") + + +stage_function(actor, normalized_mailbox) + + + +print(SEP) +print("[2/5] loop stage") +for idx, msg in enumerate(normalized_mailbox): + print(f"[loop] {idx} {msg}") + if msg["type"] == "transfer" and not actor.get("loop_checkpoint"): + actor["loop_checkpoint"] = True + actor["history"].append({"stage": "loop", "seq": idx}) + print("[checkpoint #2] inside loop") + rpc.checkpoint(CHECKPOINT_PATH) + print("[resume #2] after loop checkpoint") + + match msg: + case {"type": "deposit", "amount": amt}: + actor["balance"] = round(actor["balance"] + amt, 2) + case {"type": "transfer", "amount": amt, "to": target}: + if actor["balance"] >= amt: + actor["balance"] = round(actor["balance"] - amt, 2) + else: + actor["flags"].append(f"overdraft:{target}") + case {"type": "adjust", "amount": amt}: + actor["balance"] = round(actor["balance"] + amt, 2) + case {"type": "query", "fields": fields}: + snapshot = {field: actor.get(field) for field in fields} + actor["history"].append({"stage": "query", "snapshot": snapshot}) + case {"type": "noop"}: + actor["flags"].append("noop") + case _: + actor["flags"].append("unknown") + +arr = [1,2,3] +for i in enumerate(arr): + print(f"[loop] {i}") + rpc.checkpoint(CHECKPOINT_PATH) + print(f"[loop] {i} resumed") + +print(SEP) +print("[3/5] if stage") +if actor["balance"] >= 0 and not actor.get("if_checkpoint"): + actor["if_checkpoint"] = True + actor["history"].append({"stage": "if", "balance": actor["balance"]}) + print("[checkpoint #3] inside if") + rpc.checkpoint(CHECKPOINT_PATH) + actor["flags"].append("if_resumed") + print("[resume #3] after if checkpoint") + + + +print(SEP) +print("[4/5] try/except stage") +try: + if not actor.get("try_checkpoint"): + actor["try_checkpoint"] = True + actor["history"].append({"stage": "try"}) + print("[checkpoint #4] inside try") + rpc.checkpoint(CHECKPOINT_PATH) + print("[resume #4] after try checkpoint") + raise ValueError("demo") +except ValueError as exc: + actor["flags"].append(f"handled:{exc}") + +print(SEP) +print("[5/5] final report") +report = { + "actor_id": actor["actor_id"], + "balance": actor["balance"], + "flags": actor["flags"], + "history_tail": actor["history"][-4:], +} +print(f" report={report}") + +if os.path.exists(CHECKPOINT_PATH): + os.remove(CHECKPOINT_PATH) +print("[done] checkpoint file removed; next run starts fresh") diff --git a/examples/breakpoint_resume_demo/comprehensive_demo.py b/examples/breakpoint_resume_demo/comprehensive_demo.py new file mode 100644 index 00000000000..a32b6b359f9 --- /dev/null +++ b/examples/breakpoint_resume_demo/comprehensive_demo.py @@ -0,0 +1,373 @@ +""" +PVM Comprehensive Checkpoint/Resume Demo +=========================================== + +This demo showcases the wide variety of Python control flow structures +that PVM can successfully checkpoint and resume, including: + +- Functions (nested calls) +- For loops (list iteration, enumerate, zip, map, filter) +- While loops +- If/elif/else statements +- Try/except/finally blocks +- Match statements (pattern matching) +- List comprehensions +- Dictionary and set operations +- Nested control structures + +Note: This demo avoids using range() due to a known issue with +range_iterator restoration in loop contexts. Use list iteration +or while loops as alternatives. +""" + +from __future__ import annotations +from pathlib import Path +import os + +import rustpython_checkpoint as rpc # type: ignore + +CHECKPOINT_PATH = str(Path(__file__).with_suffix(".rpsnap")) +SEP = "=" * 70 + +# Global state for tracking progress +state = { + "checkpoints_passed": [], + "test_results": [], + "counter": 0, +} + +print(SEP) +print("PVM Comprehensive Checkpoint/Resume Demo") +print(SEP) + +# ============================================================================ +# Test 1: Nested Function Calls +# ============================================================================ +print("\n[Test 1] Nested Function Calls") + +def outer_function(data: dict) -> int: + """Outer function that calls inner functions.""" + data["outer_called"] = True + result = inner_function_a(data) + return result + +def inner_function_a(data: dict) -> int: + """First inner function.""" + data["inner_a_called"] = True + result = inner_function_b(data) + return result + 10 + +def inner_function_b(data: dict) -> int: + """Second inner function with checkpoint.""" + data["inner_b_called"] = True + if "checkpoint_1" not in state["checkpoints_passed"]: + print(" [Checkpoint #1] Inside nested function (depth=3)") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_1") + print(" [Resumed #1] Continuing from nested function") + return 42 + +result = outer_function(state) +state["test_results"].append(("nested_functions", result == 52)) +print(f" Result: {result} (expected 52)") + +# ============================================================================ +# Test 2: For Loop with List Iteration +# ============================================================================ +print(f"\n[Test 2] For Loop with List Iteration") + +data_list = [10, 20, 30, 40, 50] +sum_val = 0 + +for value in data_list: + sum_val += value + if value == 30 and "checkpoint_2" not in state["checkpoints_passed"]: + print(f" [Checkpoint #2] Inside for loop, value={value}, sum={sum_val}") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_2") + print(f" [Resumed #2] Continuing for loop") + +state["test_results"].append(("for_list", sum_val == 150)) +print(f" Final sum: {sum_val} (expected 150)") + +# ============================================================================ +# Test 3: Enumerate Loop +# ============================================================================ +print(f"\n[Test 3] Enumerate Loop") + +fruits = ["apple", "banana", "cherry", "date"] +enum_results = [] + +for idx, fruit in enumerate(fruits): + enum_results.append((idx, fruit)) + if idx == 2 and "checkpoint_3" not in state["checkpoints_passed"]: + print(f" [Checkpoint #3] In enumerate loop, idx={idx}, fruit={fruit}") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_3") + print(f" [Resumed #3] Continuing enumerate loop") + +state["test_results"].append(("enumerate_loop", len(enum_results) == 4)) +print(f" Enumerated {len(enum_results)} items") + +# ============================================================================ +# Test 4: While Loop +# ============================================================================ +print(f"\n[Test 4] While Loop") + +counter = 0 +while_sum = 0 + +while counter < 5: + while_sum += counter * 2 + counter += 1 + if counter == 3 and "checkpoint_4" not in state["checkpoints_passed"]: + print(f" [Checkpoint #4] In while loop, counter={counter}, sum={while_sum}") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_4") + print(f" [Resumed #4] Continuing while loop") + +state["test_results"].append(("while_loop", while_sum == 20)) +print(f" Final sum: {while_sum} (expected 20)") + +# ============================================================================ +# Test 5: If/Elif/Else Chains +# ============================================================================ +print(f"\n[Test 5] If/Elif/Else Chains") + +test_value = 75 +category = "" + +if test_value < 0: + category = "negative" +elif test_value < 50: + category = "low" +elif test_value < 100: + # Checkpoint in elif branch + if "checkpoint_5" not in state["checkpoints_passed"]: + print(f" [Checkpoint #5] In elif branch, value={test_value}") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_5") + print(f" [Resumed #5] Continuing from elif") + category = "medium" +else: + category = "high" + +state["test_results"].append(("if_elif_else", category == "medium")) +print(f" Category: {category} (expected medium)") + +# ============================================================================ +# Test 6: Try/Except/Finally +# ============================================================================ +print(f"\n[Test 6] Try/Except/Finally") + +try_result = {"attempted": False, "caught": False, "finalized": False} + +try: + try_result["attempted"] = True + if "checkpoint_6" not in state["checkpoints_passed"]: + print(f" [Checkpoint #6] Inside try block") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_6") + print(f" [Resumed #6] Continuing from try") + # Raise an exception to test except + if "checkpoint_7" not in state["checkpoints_passed"]: + raise ValueError("test_exception") +except ValueError as e: + try_result["caught"] = True + if "checkpoint_7" not in state["checkpoints_passed"]: + print(f" [Checkpoint #7] Inside except block, caught: {e}") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_7") + print(f" [Resumed #7] Continuing from except") +finally: + try_result["finalized"] = True + +state["test_results"].append(("try_except", all(try_result.values()))) +print(f" Try/Except/Finally: {try_result}") + +# ============================================================================ +# Test 7: Nested Loops +# ============================================================================ +print(f"\n[Test 7] Nested Loops") + +matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] +nested_sum = 0 + +for row_idx, row in enumerate(matrix): + for col_idx, value in enumerate(row): + nested_sum += value + if row_idx == 1 and col_idx == 1 and "checkpoint_8" not in state["checkpoints_passed"]: + print(f" [Checkpoint #8] In nested loop, pos=({row_idx},{col_idx}), value={value}") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_8") + print(f" [Resumed #8] Continuing nested loops") + +state["test_results"].append(("nested_loops", nested_sum == 45)) +print(f" Matrix sum: {nested_sum} (expected 45)") + +# ============================================================================ +# Test 8: Match Statement (Pattern Matching) +# ============================================================================ +print(f"\n[Test 8] Match Statement") + +test_data = {"type": "transfer", "amount": 100, "to": "account_123"} +match_result = "" + +match test_data: + case {"type": "deposit", "amount": amt}: + match_result = f"deposit_{amt}" + case {"type": "transfer", "amount": amt, "to": target}: + if "checkpoint_9" not in state["checkpoints_passed"]: + print(f" [Checkpoint #9] In match case, transfer to {target}") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_9") + print(f" [Resumed #9] Continuing from match") + match_result = f"transfer_{amt}_to_{target}" + case _: + match_result = "unknown" + +state["test_results"].append(("match_stmt", "transfer" in match_result)) +print(f" Match result: {match_result}") + +# ============================================================================ +# Test 9: List Comprehension with Checkpoint After +# ============================================================================ +print(f"\n[Test 9] List Comprehension") + +numbers = [1, 2, 3, 4, 5] +squares = [x * x for x in numbers] + +if "checkpoint_10" not in state["checkpoints_passed"]: + print(f" [Checkpoint #10] After list comprehension") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_10") + print(f" [Resumed #10] Continuing after comprehension") + +state["test_results"].append(("list_comp", squares == [1, 4, 9, 16, 25])) +print(f" Squares: {squares}") + +# ============================================================================ +# Test 10: Dictionary and Set Operations +# ============================================================================ +print(f"\n[Test 10] Dictionary and Set Operations") + +test_dict = {"a": 1, "b": 2, "c": 3} +test_set = {1, 2, 3, 4, 5} + +# Iterate over dictionary +dict_sum = 0 +for key, value in test_dict.items(): + dict_sum += value + +# Set operations +set_result = test_set.union({6, 7}) + +# Checkpoint after dict/set operations +if "checkpoint_11" not in state["checkpoints_passed"]: + print(f" [Checkpoint #11] After dict/set operations") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_11") + print(f" [Resumed #11] Continuing after dict/set") + +state["test_results"].append(("dict_set", dict_sum == 6 and 7 in set_result)) +print(f" Dict sum: {dict_sum}, Set size: {len(set_result)}") + +# ============================================================================ +# Test 11: Zip and Multiple Iterators +# ============================================================================ +print(f"\n[Test 11] Zip with Multiple Iterators") + +list_a = [10, 20, 30] +list_b = ["x", "y", "z"] +zip_results = [] + +for num, letter in zip(list_a, list_b): + zip_results.append((num, letter)) + +# Checkpoint after zip operation +if "checkpoint_12" not in state["checkpoints_passed"]: + print(f" [Checkpoint #12] After zip loop") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_12") + print(f" [Resumed #12] Continuing after zip") + +state["test_results"].append(("zip_loop", len(zip_results) == 3)) +print(f" Zip pairs: {zip_results}") + +# ============================================================================ +# Test 12: Map and Filter +# ============================================================================ +print(f"\n[Test 12] Map and Filter") + +def double(x): + return x * 2 + +def is_even(x): + return x % 2 == 0 + +numbers_list = [1, 2, 3, 4, 5, 6] +doubled = list(map(double, numbers_list)) +evens = list(filter(is_even, numbers_list)) + +if "checkpoint_13" not in state["checkpoints_passed"]: + print(f" [Checkpoint #13] After map/filter operations") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_13") + print(f" [Resumed #13] Continuing after map/filter") + +state["test_results"].append(("map_filter", len(doubled) == 6 and len(evens) == 3)) +print(f" Doubled: {doubled}, Evens: {evens}") + +# ============================================================================ +# Test 13: Nested Function with Closure +# ============================================================================ +print(f"\n[Test 13] Nested Function with Closure") + +def outer_with_closure(x): + """Function that returns a closure.""" + def inner(y): + return x + y + return inner + +closure_func = outer_with_closure(100) +closure_result = closure_func(23) + +# Checkpoint after closure operation +if "checkpoint_14" not in state["checkpoints_passed"]: + print(f" [Checkpoint #14] After closure execution") + rpc.checkpoint(CHECKPOINT_PATH) + state["checkpoints_passed"].append("checkpoint_14") + print(f" [Resumed #14] Continuing after closure") + +state["test_results"].append(("closure", closure_result == 123)) +print(f" Closure result: {closure_result} (expected 123)") + +# ============================================================================ +# Final Report +# ============================================================================ +print(f"\n{SEP}") +print("FINAL REPORT") +print(SEP) + +passed_count = sum(1 for _, passed in state["test_results"] if passed) +total_count = len(state["test_results"]) + +print(f"\nCheckpoints passed: {len(state['checkpoints_passed'])}") +print(f"Tests passed: {passed_count}/{total_count}") +print(f"\nDetailed results:") +for test_name, passed in state["test_results"]: + status = "✓ PASS" if passed else "✗ FAIL" + print(f" {status}: {test_name}") + +if passed_count == total_count: + print(f"\n🎉 All tests passed!") +else: + print(f"\n⚠️ Some tests failed") + +# Cleanup +if os.path.exists(CHECKPOINT_PATH): + os.remove(CHECKPOINT_PATH) + print(f"\nCheckpoint file removed; next run starts fresh") + +print(SEP) + diff --git a/examples/breakpoint_resume_demo/demo.py b/examples/breakpoint_resume_demo/demo.py new file mode 100644 index 00000000000..19efb0ae77e --- /dev/null +++ b/examples/breakpoint_resume_demo/demo.py @@ -0,0 +1,142 @@ +from __future__ import annotations + +from pathlib import Path + +import rustpython_checkpoint as rpc # type: ignore +import os + +# Checkpoint file path as a string to keep it serializable. +CHECKPOINT_PATH = str(Path(__file__).with_suffix(".rpsnap")) +SEP = "-" * 60 + +# Phase 1: prepare input data and a minimal run log. +print("=" * 60) +print(" PVM Breakpoint/Resume Demo") +print(" Idea: VM saves state at checkpoints and exits.") +print(" Run flow: 1) run -> stop #1, 2) resume -> stop #2, 3) resume -> finish") +print("=" * 60) +print("[1/3] build input state (trading day snapshot)") +print(" action: load orders, prices, and risk limits; precompute exposure summary") +orders = [ + {"id": "ord-001", "symbol": "AAPL", "side": "BUY", "qty": 120, "limit": 192.10}, + {"id": "ord-002", "symbol": "MSFT", "side": "SELL", "qty": 80, "limit": 411.50}, + {"id": "ord-003", "symbol": "NVDA", "side": "BUY", "qty": 60, "limit": 122.30}, +] +prices = {"AAPL": 192.25, "MSFT": 411.10, "NVDA": 122.60} +max_order_notional = 25000.0 +slippage_limit = 0.35 +run_log = ["loaded orders", f"slippage_limit={slippage_limit}"] +summary = { + "order_count": len(orders), + "total_qty": sum(item["qty"] for item in orders), + "symbols": sorted({item["symbol"] for item in orders}), +} +for item in orders: + print( + " order {id} {side} {qty} {symbol} limit={limit}".format(**item) + ) +print(f" prices={prices}") +print(f" max_order_notional={max_order_notional} slippage_limit={slippage_limit}") +print(f" summary={summary}") + +# Breakpoint 1: must be a standalone statement. +print(SEP) +print("[checkpoint #1] VM snapshot saved; process exits now") +print(" note: next run with --resume continues from the next line") +print(SEP) +rpc.checkpoint(CHECKPOINT_PATH) + +# Re-import after resume so the next checkpoint works. +# import rustpython_checkpoint as rpc # type: ignore + +# Phase 2: derive alerts and billing info from restored state. +print("[2/3] resumed after checkpoint #1") +print(" action: simulate fills, compute slippage and notional, flag risk, append run log") +fills = [] +risk_flags = [] +for item in orders: + mkt = prices[item["symbol"]] + slip = round(abs(mkt - item["limit"]), 2) + notional = round(mkt * item["qty"], 2) + fills.append( + { + "id": item["id"], + "symbol": item["symbol"], + "side": item["side"], + "qty": item["qty"], + "fill": mkt, + "slippage": slip, + "notional": notional, + } + ) + if slip > slippage_limit or notional > max_order_notional: + risk_flags.append(item["id"]) +total_notional = round(sum(item["notional"] for item in fills), 2) +run_log.append(f"risk_flags={risk_flags}") +run_log.append(f"total_notional={total_notional}") +print(" fills:") +for item in fills: + print( + " - {id} {symbol} {side} qty={qty} fill={fill} " + "slip={slippage} notional={notional}".format(**item) + ) +print(" risk_flags:") +if risk_flags: + for item_id in risk_flags: + print(f" - {item_id}") +else: + print(" - none") +print(f" total_notional={total_notional}") +print(" log:") +for entry in run_log: + print(f" - {entry}") + +# Breakpoint 2: save state again and exit; next run continues below. +print(SEP) +print("[checkpoint #2] VM snapshot saved; process exits now") +print(" note: next run with --resume continues from the next line") +print(SEP) +rpc.checkpoint(CHECKPOINT_PATH) + +# After resume, prepare cleanup utilities. +# import os + +# Phase 3: produce a final report and clean up the checkpoint file. +print(SEP) +print("[3/3] resumed after checkpoint #2") +print(" action: settle trades, build ledger, emit final report, and cleanup snapshot") +ledger = [ + { + "symbol": item["symbol"], + "net_qty": item["qty"] if item["side"] == "BUY" else -item["qty"], + "avg_price": item["fill"], + } + for item in fills +] +report = { + "summary": summary, + "risk_flags": risk_flags, + "ledger": ledger, + "total_notional": total_notional, + "status": "ready", +} +run_log.append("report_ready") +print(" report:") +print(f" summary={report['summary']}") +print(f" risk_flags={report['risk_flags']}") +print(" ledger:") +for item in report["ledger"]: + print( + " - {symbol} net_qty={net_qty} avg_price={avg_price}".format(**item) + ) +print(f" total_notional={report['total_notional']}") +print(f" status={report['status']}") +print(" log:") +for entry in run_log: + print(f" - {entry}") +print(SEP) + +# Clean up the checkpoint file so the next run starts fresh. +if os.path.exists(CHECKPOINT_PATH): + os.remove(CHECKPOINT_PATH) +print("[done] checkpoint file removed; next run starts fresh") diff --git a/examples/breakpoint_resume_demo/demo_en.py b/examples/breakpoint_resume_demo/demo_en.py new file mode 100644 index 00000000000..05a20a7941d --- /dev/null +++ b/examples/breakpoint_resume_demo/demo_en.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from pathlib import Path + +import rustpython_checkpoint as rpc # type: ignore + +# Checkpoint file path as a string to keep it serializable. +CHECKPOINT_PATH = str(Path(__file__).with_suffix(".rpsnap")) + + +def section(title: str) -> None: + print("\n" + "=" * 60) + print(title) + print("=" * 60) + + +section("PVM Breakpoint/Resume Showcase") +print("[run] phase=init") +customer = "Acme Corp" +order_id = "ORD-2049" +items = [f"sku_{i:02d}" for i in range(3)] +score = 0.87 +notes = ["session started", "items captured"] +print(f"[run] customer={customer} order_id={order_id}") +print(f"[run] items={items} score={score}") +print(f"[run] notes={notes}") + +# Breakpoint 1: must be a standalone statement. +# RustPython saves VM state here and exits the process. +rpc.checkpoint(CHECKPOINT_PATH) + +# Re-import after resume so the next checkpoint works. +import rustpython_checkpoint as rpc # type: ignore + +# Recreate helpers after resume (functions are not checkpoint-serializable). +def section(title: str) -> None: + print("\n" + "=" * 60) + print(title) + print("=" * 60) + +section("Resume #1: state restored") +print("[run] phase=after_checkpoint_1") +priced = [f"{item}:$99" for item in items] +total = 99 * len(priced) +notes.append("pricing complete") +print(f"[run] priced={priced}") +print(f"[run] total={total} notes={notes}") + +# Breakpoint 2: save state again and exit; next run continues below. +rpc.checkpoint(CHECKPOINT_PATH) + +import os + +# Recreate helpers after resume (functions are not checkpoint-serializable). +def section(title: str) -> None: + print("\n" + "=" * 60) + print(title) + print("=" * 60) + +section("Resume #2: finishing up") +print("[run] phase=after_checkpoint_2") +receipt = { + "customer": customer, + "order_id": order_id, + "total": total, + "status": "ok", +} +notes.append("receipt issued") +print(f"[run] receipt={receipt}") +print(f"[run] notes={notes}") + +# Clean up so a fresh run starts from the top. +if os.path.exists(CHECKPOINT_PATH): + os.remove(CHECKPOINT_PATH) +print("[run] done") diff --git a/examples/breakpoint_resume_demo/state_store.py b/examples/breakpoint_resume_demo/state_store.py new file mode 100644 index 00000000000..7e56ce23fc2 --- /dev/null +++ b/examples/breakpoint_resume_demo/state_store.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +BASE_DIR = Path(__file__).parent +DATA_DIR = BASE_DIR / "state" +STATE_FILE = DATA_DIR / "breakpoint_state.json" + + +def load_state() -> dict[str, Any] | None: + # Load checkpoint state file; return None when missing to indicate first run. + if not STATE_FILE.exists(): + return None + return json.loads(STATE_FILE.read_text()) + + +def save_state(state: dict[str, Any]) -> None: + # Write checkpoint state, ensuring the directory exists. + DATA_DIR.mkdir(parents=True, exist_ok=True) + STATE_FILE.write_text(json.dumps(state, sort_keys=True, indent=2)) + + +def clear_state() -> None: + # Clear checkpoint state to allow a fresh start. + if STATE_FILE.exists(): + STATE_FILE.unlink() diff --git a/examples/breakpoint_resume_demo/test_checkpoint.py b/examples/breakpoint_resume_demo/test_checkpoint.py new file mode 100644 index 00000000000..5fe98b0b756 --- /dev/null +++ b/examples/breakpoint_resume_demo/test_checkpoint.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from pathlib import Path + + +def resolve_bin_path(root: Path, bin_override: str | None) -> Path: + if bin_override: + return Path(bin_override) + + bin_path = root / "target" / "release" / "pvm" + if os.name == "nt": + bin_path = bin_path.with_suffix(".exe") + return bin_path + + +def run_cmd(args: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run(args, capture_output=True, text=True, check=False) + + +def combined_output(result: subprocess.CompletedProcess[str]) -> str: + return (result.stdout or "") + (result.stderr or "") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Checkpoint resume functionality test") + parser.add_argument( + "--bin", + help="Path to the rustpython executable, defaults to target/release/rustpython", + ) + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[2] + bin_path = resolve_bin_path(repo_root, args.bin) + demo_path = Path(__file__).with_name("demo.py") + snap_path = demo_path.with_suffix(".rpsnap") + + if not bin_path.exists(): + print(f"[error] rustpython not found: {bin_path}") + return 1 + + if snap_path.exists(): + snap_path.unlink() + + # First run: hit checkpoint and generate snapshot + result1 = run_cmd([str(bin_path), str(demo_path)]) + output1 = combined_output(result1) + if result1.returncode != 0: + print("[error] First run failed") + print("stdout:") + print(result1.stdout) + print("stderr:") + print(result1.stderr) + return 1 + if output1.strip(): + if "phase=init" not in output1: + print("[error] First run output did not match expectation") + print("stdout:") + print(result1.stdout) + print("stderr:") + print(result1.stderr) + return 1 + if not snap_path.exists(): + print("[error] Snapshot file not generated") + return 1 + + # Second run: resume from checkpoint 1 to checkpoint 2 + result2 = run_cmd([str(bin_path), "--resume", str(snap_path), str(demo_path)]) + output2 = combined_output(result2) + if result2.returncode != 0: + print("[error] Second run failed") + print("stdout:") + print(result2.stdout) + print("stderr:") + print(result2.stderr) + return 1 + if output2.strip(): + if "phase=after_checkpoint_1" not in output2: + print("[error] Second run output did not match expectation") + print("stdout:") + print(result2.stdout) + print("stderr:") + print(result2.stderr) + return 1 + if not snap_path.exists(): + print("[error] Snapshot file missing after second run") + return 1 + + # Third run: resume from checkpoint 2 and complete + result3 = run_cmd([str(bin_path), "--resume", str(snap_path), str(demo_path)]) + output3 = combined_output(result3) + if result3.returncode != 0: + print("[error] Third run failed") + print("stdout:") + print(result3.stdout) + print("stderr:") + print(result3.stderr) + return 1 + if output3.strip(): + if "phase=after_checkpoint_2" not in output3 or "done" not in output3: + print("[error] Third run output did not match expectation") + print("stdout:") + print(result3.stdout) + print("stderr:") + print(result3.stderr) + return 1 + if snap_path.exists(): + print("[error] Snapshot file not cleaned up after third run") + return 1 + + print("[ok] Checkpoint resume test passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/dis.rs b/examples/dis.rs index 504b734ca59..21da9c20bd3 100644 --- a/examples/dis.rs +++ b/examples/dis.rs @@ -53,7 +53,10 @@ fn main() -> Result<(), lexopt::Error> { return Err("expected at least one argument".into()); } - let opts = compiler::CompileOpts { optimize }; + let opts = compiler::CompileOpts { + optimize, + pvm_fsm: false, + }; for script in &scripts { if script.exists() && script.is_file() { diff --git a/examples/pvm_actor_transfer_demo/README.md b/examples/pvm_actor_transfer_demo/README.md new file mode 100644 index 00000000000..3c2b001d852 --- /dev/null +++ b/examples/pvm_actor_transfer_demo/README.md @@ -0,0 +1,75 @@ +# PVM Actor Transfer Demo (Filesystem Host) + +This demo shows a minimal actor-style contract with balances and transfers. + +## Files + +- `main.rs`: Example runner. +- `contract.py`: Actor transfer contract. + +## Run + +From the repo root: + +```bash +cargo run --release --example pvm_actor_transfer_demo -- \ + examples/pvm_actor_transfer_demo/contract.py \ + '{"action":"init","params":{"balances":{"alice":1000,"bob":500}}}' +``` + +Mint to a user: + +```bash +cargo run --release --example pvm_actor_transfer_demo -- \ + examples/pvm_actor_transfer_demo/contract.py \ + '{"action":"mint","params":{"to":"carol","amount":200}}' +``` + +Transfer from the sender: + +```bash +cargo run --release --example pvm_actor_transfer_demo -- --sender alice \ + examples/pvm_actor_transfer_demo/contract.py \ + '{"action":"transfer","params":{"to":"bob","amount":150}}' +``` + +Check balance: + +```bash +cargo run --release --example pvm_actor_transfer_demo -- --sender bob \ + examples/pvm_actor_transfer_demo/contract.py \ + '{"action":"balance"}' +``` + +## Alto call example + +Programmatic call using `pvm_alto`: + +```bash +cargo run --release --example pvm_alto_call_demo +``` + +## Output + +The runner prints `output_hex=...`. Decode with: + +```bash +python - <<'PY' +hex_str = "PASTE_OUTPUT_HEX" +print(bytes.fromhex(hex_str).decode("utf-8")) +PY +``` + +## State and events + +- State stored in `tmp/pvm_actor_transfer_state/`. +- Events appended to `tmp/pvm_actor_transfer_events.log`. +- Delete those paths to reset the demo. + +## Actions + +- `init`: initialize balances (`balances` map). +- `mint`: add balance (`to`, `amount`). +- `transfer`: transfer from sender (`to`, `amount`). +- `balance`: read balance (`user`, default sender). +- `info`: dump full state. diff --git a/examples/pvm_actor_transfer_demo/contract.py b/examples/pvm_actor_transfer_demo/contract.py new file mode 100644 index 00000000000..cfcc2c77f47 --- /dev/null +++ b/examples/pvm_actor_transfer_demo/contract.py @@ -0,0 +1,217 @@ +import json + +import pvm_host + +STATE_KEY = b"actor_transfer_state_v1" + +GAS_BASE = 5 +GAS_READ = 5 +GAS_WRITE = 20 + + +def _json_dumps(value): + return json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + ).encode("ascii") + + +def _json_loads(data): + return json.loads(data.decode("utf-8")) + + +def _emit(topic, payload): + pvm_host.emit_event(topic, _json_dumps(payload)) + + +def _ok(payload): + payload["ok"] = True + return _json_dumps(payload) + + +def _err(code, detail=None): + out = {"ok": False, "error": code} + if detail is not None: + out["detail"] = detail + return _json_dumps(out) + + +def _require_int(value, name): + if not isinstance(value, int): + raise ValueError(f"{name} must be int") + return value + + +def _require_positive_int(value, name): + value = _require_int(value, name) + if value <= 0: + raise ValueError(f"{name} must be > 0") + return value + + +def _require_str(value, name): + if not isinstance(value, str) or not value: + raise ValueError(f"{name} must be non-empty string") + return value + + +def _normalize_user(value, name): + if value is None: + raise ValueError(f"{name} required") + if isinstance(value, str): + return _require_str(value, name) + return _require_str(str(value), name) + + +def _ctx_sender(ctx): + sender = ctx.get("sender", b"") + if isinstance(sender, (bytes, bytearray)): + try: + return sender.decode("ascii") + except Exception: + return sender.hex() + return str(sender) + + +def _load_state(): + raw = pvm_host.get_state(STATE_KEY) + if raw is None: + return None + return _json_loads(raw) + + +def _save_state(state): + pvm_host.set_state(STATE_KEY, _json_dumps(state)) + + +def _balance_get(state, user): + return int(state["balances"].get(user, 0)) + + +def _balance_set(state, user, amount): + if amount <= 0: + state["balances"].pop(user, None) + else: + state["balances"][user] = amount + + +def _new_state(): + return {"version": 1, "balances": {}} + + +def _parse_input(input_bytes): + if not input_bytes: + return "info", {} + data = _json_loads(input_bytes) + if not isinstance(data, dict): + raise ValueError("input must be object") + action = data.get("action", "info") + if not isinstance(action, str) or not action: + raise ValueError("action must be non-empty string") + params = data.get("params", {}) + if params is None: + params = {} + if not isinstance(params, dict): + raise ValueError("params must be object") + return action, params + + +def _handle_init(state, params): + pvm_host.charge_gas(GAS_WRITE) + if state is not None: + raise ValueError("already initialized") + balances = params.get("balances", {}) + if balances is None: + balances = {} + if not isinstance(balances, dict): + raise ValueError("balances must be object") + state = _new_state() + for user, amount in balances.items(): + user = _normalize_user(user, "user") + amount = _require_positive_int(amount, "amount") + _balance_set(state, user, amount) + _save_state(state) + _emit("actor.init", {"balances": len(state["balances"])}) + return _ok({"action": "init", "balances": state["balances"]}) + + +def _handle_mint(state, params): + pvm_host.charge_gas(GAS_WRITE) + user = params.get("to") + if user is None: + user = params.get("user") + user = _normalize_user(user, "to") + amount = _require_positive_int(params.get("amount"), "amount") + new_bal = _balance_get(state, user) + amount + _balance_set(state, user, new_bal) + _save_state(state) + _emit("actor.mint", {"to": user, "amount": amount}) + return _ok({"action": "mint", "to": user, "balance": new_bal}) + + +def _handle_transfer(state, params, ctx_sender): + pvm_host.charge_gas(GAS_WRITE) + sender = params.get("from") + if sender is None: + sender = ctx_sender + else: + sender = _normalize_user(sender, "from") + if sender != ctx_sender: + raise ValueError("from must match sender") + to = _normalize_user(params.get("to"), "to") + amount = _require_positive_int(params.get("amount"), "amount") + if sender == to: + raise ValueError("from and to must differ") + sender_bal = _balance_get(state, sender) + if sender_bal < amount: + raise ValueError("insufficient balance") + _balance_set(state, sender, sender_bal - amount) + receiver_bal = _balance_get(state, to) + amount + _balance_set(state, to, receiver_bal) + _save_state(state) + _emit("actor.transfer", {"from": sender, "to": to, "amount": amount}) + return _ok( + {"action": "transfer", "from": sender, "to": to, "amount": amount} + ) + + +def _handle_balance(state, params, ctx_sender): + pvm_host.charge_gas(GAS_READ) + user = params.get("user") + if user is None: + user = ctx_sender + else: + user = _normalize_user(user, "user") + bal = _balance_get(state, user) + return _ok({"action": "balance", "user": user, "balance": bal}) + + +def _handle_info(state): + pvm_host.charge_gas(GAS_READ) + return _ok({"action": "info", "state": state}) + + +def main(input_bytes: bytes) -> bytes: + pvm_host.charge_gas(GAS_BASE) + ctx = pvm_host.context() + ctx_sender = _ctx_sender(ctx) + try: + action, params = _parse_input(input_bytes) + state = _load_state() + if action == "init": + return _handle_init(state, params) + if state is None: + state = _new_state() + if action == "mint": + return _handle_mint(state, params) + if action == "transfer": + return _handle_transfer(state, params, ctx_sender) + if action == "balance": + return _handle_balance(state, params, ctx_sender) + if action == "info": + return _handle_info(state) + raise ValueError("unknown action") + except Exception as exc: + return _err("invalid_request", str(exc)) diff --git a/examples/pvm_actor_transfer_demo/main.rs b/examples/pvm_actor_transfer_demo/main.rs new file mode 100644 index 00000000000..ff31bb720b2 --- /dev/null +++ b/examples/pvm_actor_transfer_demo/main.rs @@ -0,0 +1,141 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +use pvm_alto::{default_options, execute_tx_fs, FsTxConfig}; +use pvm_host::HostContext; + +fn main() -> Result<(), Box> { + let mut args = env::args().skip(1); + let mut script_path: Option = None; + let mut input: Option> = None; + let mut input_file: Option = None; + + let mut sender = b"alice".to_vec(); + let mut actor_addr = b"demo_actor".to_vec(); + let mut state_dir = PathBuf::from("tmp/pvm_actor_transfer_state"); + let mut events_path = PathBuf::from("tmp/pvm_actor_transfer_events.log"); + let mut gas_limit: u64 = 1_000_000; + + while let Some(arg) = args.next() { + match arg.as_str() { + "--sender" => { + let value = args.next().ok_or_else(|| usage())?; + sender = value.into_bytes(); + } + "--actor" => { + let value = args.next().ok_or_else(|| usage())?; + actor_addr = value.into_bytes(); + } + "--state-dir" => { + let value = args.next().ok_or_else(|| usage())?; + state_dir = PathBuf::from(value); + } + "--events-path" => { + let value = args.next().ok_or_else(|| usage())?; + events_path = PathBuf::from(value); + } + "--gas" => { + let value = args.next().ok_or_else(|| usage())?; + gas_limit = value.parse()?; + } + "--input-file" => { + let value = args.next().ok_or_else(|| usage())?; + input_file = Some(value); + } + "--help" | "-h" => { + println!("{}", usage()); + return Ok(()); + } + _ => { + if let Some(value) = arg.strip_prefix("--sender=") { + sender = value.as_bytes().to_vec(); + continue; + } + if let Some(value) = arg.strip_prefix("--actor=") { + actor_addr = value.as_bytes().to_vec(); + continue; + } + if let Some(value) = arg.strip_prefix("--state-dir=") { + state_dir = PathBuf::from(value); + continue; + } + if let Some(value) = arg.strip_prefix("--events-path=") { + events_path = PathBuf::from(value); + continue; + } + if let Some(value) = arg.strip_prefix("--gas=") { + gas_limit = value.parse()?; + continue; + } + if let Some(value) = arg.strip_prefix("--input-file=") { + input_file = Some(value.to_owned()); + continue; + } + + if script_path.is_none() { + script_path = Some(arg); + } else if input.is_none() { + if let Some(path) = arg.strip_prefix('@') { + input_file = Some(path.to_owned()); + } else { + input = Some(arg.into_bytes()); + } + } else { + return Err(usage().into()); + } + } + } + } + + let script_path = script_path.ok_or_else(|| usage())?; + if input.is_some() && input_file.is_some() { + return Err("use --input-file or input string, not both".into()); + } + + let code = fs::read(&script_path)?; + let input = match (input, input_file) { + (Some(bytes), None) => bytes, + (None, Some(path)) => fs::read(path)?, + (None, None) => Vec::new(), + (Some(_), Some(_)) => unreachable!(), + }; + + let ctx = HostContext { + block_height: 1, + block_hash: [0u8; 32], + tx_hash: [1u8; 32], + sender, + timestamp_ms: 1_700_000_000_000, + actor_addr, + msg_id: Vec::new(), + nonce: 0, + }; + + let config = FsTxConfig { + state_dir, + events_path, + gas_limit, + context: ctx, + }; + + let options = default_options().with_source_path(script_path); + let output = execute_tx_fs(&code, &input, config, &options)?; + + println!("output_hex={}", encode_hex(&output)); + Ok(()) +} + +fn encode_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for &byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + +fn usage() -> &'static str { + "usage: pvm_actor_transfer_demo [--sender ] [--actor ] [--state-dir ] [--events-path ] [--gas ] [--input-file ] [input]" +} diff --git a/examples/pvm_alto_call_demo.rs b/examples/pvm_alto_call_demo.rs new file mode 100644 index 00000000000..557f53392ed --- /dev/null +++ b/examples/pvm_alto_call_demo.rs @@ -0,0 +1,82 @@ +use std::fs; +use std::path::PathBuf; + +use pvm_alto::{default_options, execute_tx_fs, FsTxConfig}; +use pvm_host::HostContext; + +fn exec_tx( + code: &[u8], + input: &str, + sender: &[u8], + state_dir: &PathBuf, + events_path: &PathBuf, + script_path: &str, +) -> Result, Box> { + let ctx = HostContext { + block_height: 1, + block_hash: [0u8; 32], + tx_hash: [1u8; 32], + sender: sender.to_vec(), + timestamp_ms: 1_700_000_000_000, + actor_addr: b"demo_actor".to_vec(), + msg_id: Vec::new(), + nonce: 0, + }; + + let config = FsTxConfig { + state_dir: state_dir.clone(), + events_path: events_path.clone(), + gas_limit: 1_000_000, + context: ctx, + }; + + let options = default_options().with_source_path(script_path); + Ok(execute_tx_fs(code, input.as_bytes(), config, &options)?) +} + +fn print_output(label: &str, output: &[u8]) { + println!("{}: {}", label, String::from_utf8_lossy(output)); +} + +fn main() -> Result<(), Box> { + let script_path = "examples/pvm_actor_transfer_demo/contract.py"; + let code = fs::read(script_path)?; + + let state_dir = PathBuf::from("tmp/pvm_alto_call_state"); + let events_path = PathBuf::from("tmp/pvm_alto_call_events.log"); + + let init = r#"{"action":"init","params":{"balances":{"alice":1000,"bob":500}}}"#; + let out = exec_tx( + &code, + init, + b"alice", + &state_dir, + &events_path, + script_path, + )?; + print_output("init", &out); + + let transfer = r#"{"action":"transfer","params":{"to":"bob","amount":150}}"#; + let out = exec_tx( + &code, + transfer, + b"alice", + &state_dir, + &events_path, + script_path, + )?; + print_output("transfer", &out); + + let balance = r#"{"action":"balance","params":{"user":"bob"}}"#; + let out = exec_tx( + &code, + balance, + b"bob", + &state_dir, + &events_path, + script_path, + )?; + print_output("balance", &out); + + Ok(()) +} diff --git a/examples/pvm_dex_demo/README.md b/examples/pvm_dex_demo/README.md new file mode 100644 index 00000000000..909f391222d --- /dev/null +++ b/examples/pvm_dex_demo/README.md @@ -0,0 +1,83 @@ +# PVM DEX Demo (Filesystem Host) + +This demo shows a tiny constant-product DEX contract running in `pvm-runtime` +with a filesystem-backed host. + +## Files + +- `main.rs`: Example runner. +- `contract.py`: DEX contract. + +## Run + +From the repo root: +DYLD_LIBRARY_PATH=/opt/homebrew/opt/libffi/lib \ +```bash +cargo run --release --example pvm_dex_demo -- examples/pvm_dex_demo/contract.py \ + '{"action":"init","params":{"token_a":"USDC","token_b":"ETH","fee_bps":30}}' +``` + +Mint balances (faucet-style): + +```bash +cargo run --release --example pvm_dex_demo -- --sender alice examples/pvm_dex_demo/contract.py \ + '{"action":"mint","params":{"token":"USDC","amount":100000}}' + +cargo run --release --example pvm_dex_demo -- --sender alice examples/pvm_dex_demo/contract.py \ + '{"action":"mint","params":{"token":"ETH","amount":100}}' +``` + +Add liquidity: + +```bash +cargo run --release --example pvm_dex_demo -- --sender alice examples/pvm_dex_demo/contract.py \ + '{"action":"add_liquidity","params":{"amount_a":50000,"amount_b":50}}' +``` + +Swap: + +```bash +cargo run --release --example pvm_dex_demo -- --sender bob examples/pvm_dex_demo/contract.py \ + '{"action":"mint","params":{"token":"USDC","amount":1000}}' + +cargo run --release --example pvm_dex_demo -- --sender bob examples/pvm_dex_demo/contract.py \ + '{"action":"swap","params":{"token_in":"USDC","amount_in":1000,"min_out":1}}' +``` + +Check balances and pool: + +```bash +cargo run --release --example pvm_dex_demo -- --sender bob examples/pvm_dex_demo/contract.py \ + '{"action":"balance"}' + +cargo run --release --example pvm_dex_demo -- examples/pvm_dex_demo/contract.py \ + '{"action":"info"}' +``` + +## Output + +The runner prints `output_hex=...`. Decode with: + +```bash +python - <<'PY' +hex_str = "PASTE_OUTPUT_HEX" +print(bytes.fromhex(hex_str).decode("utf-8")) +PY +``` + +## State and events + +- State stored in `tmp/pvm_dex_state/`. +- Events appended to `tmp/pvm_dex_events.log`. +- Delete those paths to reset the demo. + +## Actions + +- `init`: set `token_a`, `token_b`, `fee_bps`. +- `mint`: faucet-like balance top-up (`token`, `amount`). +- `add_liquidity`: add to pool (`amount_a`, `amount_b`). +- `remove_liquidity`: withdraw (`lp_amount`). +- `swap`: swap token (`token_in`, `amount_in`, `min_out`). +- `quote`: price estimate (`token_in`, `amount_in`). +- `balance`: user balances and LP. +- `info`: pool and config. diff --git a/examples/pvm_dex_demo/contract.py b/examples/pvm_dex_demo/contract.py new file mode 100644 index 00000000000..604e8742e5a --- /dev/null +++ b/examples/pvm_dex_demo/contract.py @@ -0,0 +1,516 @@ +import json + +import pvm_host + +STATE_KEY = b"dex_state_v1" + +GAS_BASE = 5 +GAS_READ = 5 +GAS_WRITE = 20 +GAS_SWAP = 30 + + +def _json_dumps(value): + return json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + ).encode("ascii") + + +def _json_loads(data): + return json.loads(data.decode("utf-8")) + + +def _emit(topic, payload): + pvm_host.emit_event(topic, _json_dumps(payload)) + + +def _sender_id(ctx): + sender = ctx.get("sender", b"") + if isinstance(sender, (bytes, bytearray)): + try: + return sender.decode("ascii") + except Exception: + return sender.hex() + return str(sender) + + +def _load_state(): + raw = pvm_host.get_state(STATE_KEY) + if raw is None: + return None + return _json_loads(raw) + + +def _save_state(state): + pvm_host.set_state(STATE_KEY, _json_dumps(state)) + + +def _require_int(value, name): + if not isinstance(value, int): + raise ValueError(f"{name} must be int") + return value + + +def _require_positive_int(value, name): + value = _require_int(value, name) + if value <= 0: + raise ValueError(f"{name} must be > 0") + return value + + +def _get_balance(state, user, token): + return int(state["balances"].get(user, {}).get(token, 0)) + + +def _set_balance(state, user, token, amount): + balances = state["balances"].setdefault(user, {}) + if amount <= 0: + balances.pop(token, None) + else: + balances[token] = amount + + +def _get_lp(state, user): + return int(state["lp"].get(user, 0)) + + +def _set_lp(state, user, amount): + if amount <= 0: + state["lp"].pop(user, None) + else: + state["lp"][user] = amount + + +def _token_symbol(state, token): + if token is None: + raise ValueError("token is required") + token_str = str(token) + token_upper = token_str.upper() + if token_upper == "A": + return state["token_a"] + if token_upper == "B": + return state["token_b"] + if token_str == state["token_a"] or token_str == state["token_b"]: + return token_str + if token_str.lower() == state["token_a"].lower(): + return state["token_a"] + if token_str.lower() == state["token_b"].lower(): + return state["token_b"] + raise ValueError("unknown token: " + token_str) + + +def _public_state(state): + return { + "token_a": state["token_a"], + "token_b": state["token_b"], + "reserve_a": state["reserve_a"], + "reserve_b": state["reserve_b"], + "fee_bps": state["fee_bps"], + "lp_total": state["lp_total"], + } + + +def _isqrt(n): + if n <= 0: + return 0 + x = n + y = (x + 1) // 2 + while y < x: + x = y + y = (x + n // x) // 2 + return x + + +def _amount_out(amount_in, reserve_in, reserve_out, fee_bps): + if amount_in <= 0: + return 0 + if reserve_in <= 0 or reserve_out <= 0: + return 0 + amount_in_with_fee = amount_in * (10000 - fee_bps) + numerator = amount_in_with_fee * reserve_out + denom = reserve_in * 10000 + amount_in_with_fee + return numerator // denom + + +def _ok(payload): + payload["ok"] = True + return _json_dumps(payload) + + +def _err(code, detail=None): + out = {"ok": False, "error": code} + if detail is not None: + out["detail"] = detail + return _json_dumps(out) + + +def _handle_init(params, sender): + if _load_state() is not None: + raise ValueError("already initialized") + token_a = params.get("token_a", "TOKENA") + token_b = params.get("token_b", "TOKENB") + if not isinstance(token_a, str) or not isinstance(token_b, str): + raise ValueError("token names must be strings") + if token_a == token_b: + raise ValueError("token_a and token_b must differ") + fee_bps = params.get("fee_bps", 30) + fee_bps = _require_int(fee_bps, "fee_bps") + if fee_bps < 0 or fee_bps > 10000: + raise ValueError("fee_bps must be 0..10000") + state = { + "version": 1, + "token_a": token_a, + "token_b": token_b, + "reserve_a": 0, + "reserve_b": 0, + "fee_bps": fee_bps, + "lp_total": 0, + "balances": {}, + "lp": {}, + } + _save_state(state) + _emit( + "dex.init", + { + "sender": sender, + "token_a": token_a, + "token_b": token_b, + "fee_bps": fee_bps, + }, + ) + return _ok({"action": "init", "state": _public_state(state)}) + + +def _handle_mint(state, sender, params): + pvm_host.charge_gas(GAS_WRITE) + token = _token_symbol(state, params.get("token")) + amount = _require_positive_int(params.get("amount"), "amount") + bal = _get_balance(state, sender, token) + new_bal = bal + amount + _set_balance(state, sender, token, new_bal) + _save_state(state) + _emit("dex.mint", {"sender": sender, "token": token, "amount": amount}) + return _ok( + { + "action": "mint", + "sender": sender, + "token": token, + "amount": amount, + "balance": new_bal, + } + ) + + +def _handle_balance(state, sender): + pvm_host.charge_gas(GAS_READ) + balances = state["balances"].get(sender, {}) + lp_balance = _get_lp(state, sender) + return _ok( + { + "action": "balance", + "sender": sender, + "balances": balances, + "lp": lp_balance, + } + ) + + +def _handle_info(state): + pvm_host.charge_gas(GAS_READ) + return _ok({"action": "info", "state": _public_state(state)}) + + +def _handle_quote(state, params): + pvm_host.charge_gas(GAS_READ) + token_in = _token_symbol(state, params.get("token_in")) + amount_in = _require_positive_int(params.get("amount_in"), "amount_in") + token_a = state["token_a"] + token_b = state["token_b"] + if token_in == token_a: + token_out = token_b + reserve_in = state["reserve_a"] + reserve_out = state["reserve_b"] + else: + token_out = token_a + reserve_in = state["reserve_b"] + reserve_out = state["reserve_a"] + amount_out = _amount_out(amount_in, reserve_in, reserve_out, state["fee_bps"]) + return _ok( + { + "action": "quote", + "token_in": token_in, + "token_out": token_out, + "amount_in": amount_in, + "amount_out": amount_out, + } + ) + + +def _handle_add_liquidity(state, sender, params): + pvm_host.charge_gas(GAS_WRITE) + amount_a = _require_positive_int(params.get("amount_a"), "amount_a") + amount_b = _require_positive_int(params.get("amount_b"), "amount_b") + min_lp = params.get("min_lp", 0) + min_lp = _require_int(min_lp, "min_lp") + if min_lp < 0: + raise ValueError("min_lp must be >= 0") + + token_a = state["token_a"] + token_b = state["token_b"] + bal_a = _get_balance(state, sender, token_a) + bal_b = _get_balance(state, sender, token_b) + if bal_a < amount_a or bal_b < amount_b: + raise ValueError("insufficient balance") + + reserve_a = state["reserve_a"] + reserve_b = state["reserve_b"] + lp_total = state["lp_total"] + + if lp_total == 0: + lp_minted = _isqrt(amount_a * amount_b) + if lp_minted <= 0: + raise ValueError("lp_minted is zero") + used_a = amount_a + used_b = amount_b + else: + if reserve_a <= 0 or reserve_b <= 0: + raise ValueError("pool reserves are zero") + lp_from_a = amount_a * lp_total // reserve_a + lp_from_b = amount_b * lp_total // reserve_b + lp_minted = min(lp_from_a, lp_from_b) + if lp_minted <= 0: + raise ValueError("lp_minted is zero") + used_a = lp_minted * reserve_a // lp_total + used_b = lp_minted * reserve_b // lp_total + if used_a <= 0 or used_b <= 0: + raise ValueError("used amount is zero") + + if lp_minted < min_lp: + raise ValueError("lp_minted below min_lp") + + _set_balance(state, sender, token_a, bal_a - used_a) + _set_balance(state, sender, token_b, bal_b - used_b) + + state["reserve_a"] = reserve_a + used_a + state["reserve_b"] = reserve_b + used_b + state["lp_total"] = lp_total + lp_minted + + user_lp = _get_lp(state, sender) + _set_lp(state, sender, user_lp + lp_minted) + + _save_state(state) + _emit( + "dex.add_liquidity", + { + "sender": sender, + "amount_a": used_a, + "amount_b": used_b, + "lp_minted": lp_minted, + }, + ) + return _ok( + { + "action": "add_liquidity", + "sender": sender, + "amount_a": used_a, + "amount_b": used_b, + "lp_minted": lp_minted, + "lp_total": state["lp_total"], + "reserves": {"a": state["reserve_a"], "b": state["reserve_b"]}, + } + ) + + +def _handle_remove_liquidity(state, sender, params): + pvm_host.charge_gas(GAS_WRITE) + lp_amount = _require_positive_int(params.get("lp_amount"), "lp_amount") + min_amount_a = _require_int(params.get("min_amount_a", 0), "min_amount_a") + min_amount_b = _require_int(params.get("min_amount_b", 0), "min_amount_b") + if min_amount_a < 0 or min_amount_b < 0: + raise ValueError("minimums must be >= 0") + + lp_total = state["lp_total"] + if lp_total <= 0: + raise ValueError("no liquidity") + + user_lp = _get_lp(state, sender) + if user_lp < lp_amount: + raise ValueError("insufficient lp") + + reserve_a = state["reserve_a"] + reserve_b = state["reserve_b"] + amount_a = lp_amount * reserve_a // lp_total + amount_b = lp_amount * reserve_b // lp_total + if amount_a <= 0 or amount_b <= 0: + raise ValueError("withdraw amount is zero") + if amount_a < min_amount_a or amount_b < min_amount_b: + raise ValueError("withdrawal below minimum") + + state["reserve_a"] = reserve_a - amount_a + state["reserve_b"] = reserve_b - amount_b + state["lp_total"] = lp_total - lp_amount + + _set_lp(state, sender, user_lp - lp_amount) + + token_a = state["token_a"] + token_b = state["token_b"] + bal_a = _get_balance(state, sender, token_a) + bal_b = _get_balance(state, sender, token_b) + _set_balance(state, sender, token_a, bal_a + amount_a) + _set_balance(state, sender, token_b, bal_b + amount_b) + + _save_state(state) + _emit( + "dex.remove_liquidity", + { + "sender": sender, + "lp_amount": lp_amount, + "amount_a": amount_a, + "amount_b": amount_b, + }, + ) + return _ok( + { + "action": "remove_liquidity", + "sender": sender, + "lp_amount": lp_amount, + "amount_a": amount_a, + "amount_b": amount_b, + "lp_total": state["lp_total"], + "reserves": {"a": state["reserve_a"], "b": state["reserve_b"]}, + } + ) + + +def _handle_swap(state, sender, params): + pvm_host.charge_gas(GAS_SWAP) + token_in = _token_symbol(state, params.get("token_in")) + amount_in = _require_positive_int(params.get("amount_in"), "amount_in") + min_out = _require_int(params.get("min_out", 0), "min_out") + if min_out < 0: + raise ValueError("min_out must be >= 0") + + token_a = state["token_a"] + token_b = state["token_b"] + if token_in == token_a: + token_out = token_b + reserve_in = state["reserve_a"] + reserve_out = state["reserve_b"] + reserve_in_key = "reserve_a" + reserve_out_key = "reserve_b" + else: + token_out = token_a + reserve_in = state["reserve_b"] + reserve_out = state["reserve_a"] + reserve_in_key = "reserve_b" + reserve_out_key = "reserve_a" + + if reserve_in <= 0 or reserve_out <= 0: + raise ValueError("empty pool") + + bal_in = _get_balance(state, sender, token_in) + if bal_in < amount_in: + raise ValueError("insufficient balance") + + amount_out = _amount_out(amount_in, reserve_in, reserve_out, state["fee_bps"]) + if amount_out <= 0: + raise ValueError("amount_out is zero") + if amount_out < min_out: + raise ValueError("amount_out below min_out") + if amount_out >= reserve_out: + raise ValueError("insufficient liquidity") + + _set_balance(state, sender, token_in, bal_in - amount_in) + bal_out = _get_balance(state, sender, token_out) + _set_balance(state, sender, token_out, bal_out + amount_out) + + state[reserve_in_key] = reserve_in + amount_in + state[reserve_out_key] = reserve_out - amount_out + + _save_state(state) + _emit( + "dex.swap", + { + "sender": sender, + "token_in": token_in, + "token_out": token_out, + "amount_in": amount_in, + "amount_out": amount_out, + }, + ) + return _ok( + { + "action": "swap", + "sender": sender, + "token_in": token_in, + "token_out": token_out, + "amount_in": amount_in, + "amount_out": amount_out, + "reserves": {"a": state["reserve_a"], "b": state["reserve_b"]}, + } + ) + + +def main(input_bytes): + pvm_host.charge_gas(GAS_BASE) + if not input_bytes: + return _ok( + { + "message": "pvm dex demo", + "actions": [ + "init", + "mint", + "add_liquidity", + "remove_liquidity", + "swap", + "quote", + "balance", + "info", + ], + } + ) + + try: + request = _json_loads(input_bytes) + except Exception as exc: + return _err("invalid_json", str(exc)) + + action = request.get("action") + params = request.get("params", {}) + if params is None: + params = {} + if not isinstance(params, dict): + return _err("invalid_input", "params must be object") + + ctx = pvm_host.context() + sender = _sender_id(ctx) + + try: + if action == "init": + return _handle_init(params, sender) + + state = _load_state() + if state is None: + return _err("not_initialized") + + if action == "mint": + return _handle_mint(state, sender, params) + if action == "balance": + return _handle_balance(state, sender) + if action == "info": + return _handle_info(state) + if action == "quote": + return _handle_quote(state, params) + if action == "add_liquidity": + return _handle_add_liquidity(state, sender, params) + if action == "remove_liquidity": + return _handle_remove_liquidity(state, sender, params) + if action == "swap": + return _handle_swap(state, sender, params) + except Exception as exc: + return _err("invalid_input", str(exc)) + + return _err("unknown_action", str(action)) diff --git a/examples/pvm_dex_demo/main.rs b/examples/pvm_dex_demo/main.rs new file mode 100644 index 00000000000..97e113fe5f7 --- /dev/null +++ b/examples/pvm_dex_demo/main.rs @@ -0,0 +1,132 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +use pvm_alto::{default_options, execute_tx_fs, FsTxConfig}; +use pvm_host::HostContext; + +fn main() -> Result<(), Box> { + let mut args = env::args().skip(1); + let mut script_path: Option = None; + let mut input: Option> = None; + let mut input_file: Option = None; + + let mut sender = b"alice".to_vec(); + let mut state_dir = PathBuf::from("tmp/pvm_dex_state"); + let mut events_path = PathBuf::from("tmp/pvm_dex_events.log"); + let mut gas_limit: u64 = 1_000_000; + + while let Some(arg) = args.next() { + match arg.as_str() { + "--sender" => { + let value = args.next().ok_or_else(|| usage())?; + sender = value.into_bytes(); + } + "--state-dir" => { + let value = args.next().ok_or_else(|| usage())?; + state_dir = PathBuf::from(value); + } + "--events-path" => { + let value = args.next().ok_or_else(|| usage())?; + events_path = PathBuf::from(value); + } + "--gas" => { + let value = args.next().ok_or_else(|| usage())?; + gas_limit = value.parse()?; + } + "--input-file" => { + let value = args.next().ok_or_else(|| usage())?; + input_file = Some(value); + } + "--help" | "-h" => { + println!("{}", usage()); + return Ok(()); + } + _ => { + if let Some(value) = arg.strip_prefix("--sender=") { + sender = value.as_bytes().to_vec(); + continue; + } + if let Some(value) = arg.strip_prefix("--state-dir=") { + state_dir = PathBuf::from(value); + continue; + } + if let Some(value) = arg.strip_prefix("--events-path=") { + events_path = PathBuf::from(value); + continue; + } + if let Some(value) = arg.strip_prefix("--gas=") { + gas_limit = value.parse()?; + continue; + } + if let Some(value) = arg.strip_prefix("--input-file=") { + input_file = Some(value.to_owned()); + continue; + } + + if script_path.is_none() { + script_path = Some(arg); + } else if input.is_none() { + if let Some(path) = arg.strip_prefix('@') { + input_file = Some(path.to_owned()); + } else { + input = Some(arg.into_bytes()); + } + } else { + return Err(usage().into()); + } + } + } + } + + let script_path = script_path.ok_or_else(|| usage())?; + if input.is_some() && input_file.is_some() { + return Err("use --input-file or input string, not both".into()); + } + + let code = fs::read(&script_path)?; + let input = match (input, input_file) { + (Some(bytes), None) => bytes, + (None, Some(path)) => fs::read(path)?, + (None, None) => Vec::new(), + (Some(_), Some(_)) => unreachable!(), + }; + + let ctx = HostContext { + block_height: 1, + block_hash: [0u8; 32], + tx_hash: [1u8; 32], + sender, + timestamp_ms: 1_700_000_000_000, + actor_addr: b"demo_actor".to_vec(), + msg_id: Vec::new(), + nonce: 0, + }; + + let config = FsTxConfig { + state_dir, + events_path, + gas_limit, + context: ctx, + }; + + let options = default_options().with_source_path(script_path); + let output = execute_tx_fs(&code, &input, config, &options)?; + + println!("output_hex={}", encode_hex(&output)); + Ok(()) +} + +fn encode_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for &byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + +fn usage() -> &'static str { + "usage: pvm_dex_demo [--sender ] [--state-dir ] [--events-path ] [--gas ] [--input-file ] [input]" +} diff --git a/examples/pvm_runtime_chain_demo/README.md b/examples/pvm_runtime_chain_demo/README.md new file mode 100644 index 00000000000..8aa08c8efa2 --- /dev/null +++ b/examples/pvm_runtime_chain_demo/README.md @@ -0,0 +1,113 @@ +# PVM Runtime Chain Demo (Filesystem Host) + +This demo simulates an Alto-style chain host with a filesystem-backed `HostApi`. +It runs a Python contract through `pvm-runtime` and writes state/events to local files. + +## Files + +- `main.rs`: Example runner that loads the Python contract and executes it. +- `contract.py`: Sample contract using `pvm_host`. +- `determinism_demo.py`: Determinism demo (import guard + stdlib shims + host context). +- `escrow_marketplace_demo.py`: Escrow marketplace flow with expiry, funding, and release. +- `batch_payroll_demo.py`: Batch payroll settlement with fees and audit sampling. +- `staking_rewards_demo.py`: Staking rewards distribution with weighted proposer selection. + +## Run + +From the repo root: + +```bash +cargo run --release --example pvm_runtime_chain_demo -- examples/pvm_runtime_chain_demo/contract.py hello +``` + +On macOS with Homebrew libffi, you may need: + +```bash +DYLD_LIBRARY_PATH=/opt/homebrew/opt/libffi/lib \ +cargo run --release --example pvm_runtime_chain_demo -- examples/pvm_runtime_chain_demo/contract.py hello +``` + +## Output and Artifacts + +- `output_hex=...` printed to stdout (hex-encoded bytes returned by the contract). +- State files in `tmp/pvm_state/` (keyed by hex-encoded keys). +- Event log in `tmp/pvm_events.log` (one line per event: `topic:hex_payload`). + +## Determinism Demo + +```bash +DYLD_LIBRARY_PATH=/opt/homebrew/opt/libffi/lib \ +cargo run --release --example pvm_runtime_chain_demo -- examples/pvm_runtime_chain_demo/determinism_demo.py hello +``` + +The contract output is JSON (hex-encoded on stdout) with: + +- Deterministic time and randomness via `time`/`random` stdlib shims. +- Blocked modules and file IO recorded under `blocked`. +- Host context echoed back (hashes and sender as hex). + +Run the command multiple times and compare `output_hex` for identical results. + +## Determinism Check (Multi-run) + +```bash +python examples/pvm_runtime_chain_demo/determinism_check.py --runs 5 --decode +``` + +Use `--keep-state` if you want to keep `tmp/pvm_state` between runs. + +## Business Scenario Demos + +Each demo supports `demo` as input for a built-in batch, or a JSON object with +`action`/`params` or `actions` (list). Outputs include a `state_hash` to +compare determinism. + +```bash +python examples/pvm_runtime_chain_demo/determinism_check.py \ + --runs 5 --decode \ + --script examples/pvm_runtime_chain_demo/escrow_marketplace_demo.py \ + --input demo +``` + +```bash +python examples/pvm_runtime_chain_demo/determinism_check.py \ + --runs 5 --decode \ + --script examples/pvm_runtime_chain_demo/batch_payroll_demo.py \ + --input demo +``` + +```bash +python examples/pvm_runtime_chain_demo/determinism_check.py \ + --runs 5 --decode \ + --script examples/pvm_runtime_chain_demo/staking_rewards_demo.py \ + --input demo +``` + +## Import Trace (Whitelist Generator) + +Generate an import trace (with non-whitelisted imports allowed) and print a +suggested whitelist: + +```bash +python examples/pvm_runtime_chain_demo/determinism_check.py \ + --runs 1 \ + --trace-imports tmp/pvm_import_trace.json \ + --trace-allow-all \ + --print-whitelist +``` + +Or run the binary directly: + +```bash +DYLD_LIBRARY_PATH=/opt/homebrew/opt/libffi/lib \ +cargo run --release --example pvm_runtime_chain_demo -- \ + --trace-imports tmp/pvm_import_trace.json \ + --trace-allow-all \ + examples/pvm_runtime_chain_demo/determinism_demo.py hello +``` + +## Contract Behavior + +- Reads and increments a `counter` state key. +- Emits a `demo` event. +- Returns `b"ok::h="`. diff --git a/examples/pvm_runtime_chain_demo/batch_payroll_demo.py b/examples/pvm_runtime_chain_demo/batch_payroll_demo.py new file mode 100644 index 00000000000..c3048223485 --- /dev/null +++ b/examples/pvm_runtime_chain_demo/batch_payroll_demo.py @@ -0,0 +1,406 @@ +import hashlib +import json +import random + +import pvm_host + +STATE_KEY = b"payroll_state_v1" + +GAS_BASE = 5 +GAS_READ = 5 +GAS_WRITE = 20 + + +def _json_dumps(value): + return json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + ).encode("ascii") + + +def _json_loads(data): + return json.loads(data.decode("utf-8")) + + +def _emit(topic, payload): + pvm_host.emit_event(topic, _json_dumps(payload)) + + +def _state_hash(state): + return hashlib.sha256(_json_dumps(state)).hexdigest() + + +def _ok(payload, state=None): + payload["ok"] = True + if state is not None: + payload["state_hash"] = _state_hash(state) + return _json_dumps(payload) + + +def _err(code, detail=None): + out = {"ok": False, "error": code} + if detail is not None: + out["detail"] = detail + return _json_dumps(out) + + +def _require_int(value, name): + if not isinstance(value, int): + raise ValueError(f"{name} must be int") + return value + + +def _require_positive_int(value, name): + value = _require_int(value, name) + if value <= 0: + raise ValueError(f"{name} must be > 0") + return value + + +def _require_str(value, name): + if not isinstance(value, str) or not value: + raise ValueError(f"{name} must be non-empty string") + return value + + +def _load_state(): + raw = pvm_host.get_state(STATE_KEY) + if raw is None: + return None + return _json_loads(raw) + + +def _save_state(state): + pvm_host.set_state(STATE_KEY, _json_dumps(state)) + + +def _balance_get(state, user): + return int(state["balances"].get(user, 0)) + + +def _balance_set(state, user, amount): + if amount <= 0: + state["balances"].pop(user, None) + else: + state["balances"][user] = amount + + +def _demo_actions(): + return [ + { + "action": "init", + "params": {"fee_bps": 25, "treasury": "treasury"}, + }, + {"action": "credit", "params": {"user": "alice", "amount": 2000}}, + {"action": "credit", "params": {"user": "carol", "amount": 1500}}, + { + "action": "process_batch", + "params": { + "batch_id": "payroll-2024-09", + "transfers": [ + {"from": "alice", "to": "bob", "amount": 300}, + {"from": "alice", "to": "dora", "amount": 250}, + {"from": "carol", "to": "erin", "amount": 400}, + {"from": "carol", "to": "frank", "amount": 200}, + ], + }, + }, + {"action": "snapshot", "params": {}}, + ] + + +def _handle_init(state, params): + pvm_host.charge_gas(GAS_WRITE) + if state is not None: + raise ValueError("already initialized") + fee_bps = _require_int(params.get("fee_bps", 25), "fee_bps") + if fee_bps < 0 or fee_bps > 10000: + raise ValueError("fee_bps must be 0..10000") + treasury = _require_str(params.get("treasury", "treasury"), "treasury") + state = { + "version": 1, + "fee_bps": fee_bps, + "treasury": treasury, + "balances": {}, + "batches": {}, + "ledger": [], + "sequence": 1, + } + _save_state(state) + _emit("payroll.init", {"fee_bps": fee_bps, "treasury": treasury}) + return state, {"action": "init", "fee_bps": fee_bps, "treasury": treasury} + + +def _handle_credit(state, params): + pvm_host.charge_gas(GAS_WRITE) + user = _require_str(params.get("user"), "user") + amount = _require_positive_int(params.get("amount"), "amount") + new_bal = _balance_get(state, user) + amount + _balance_set(state, user, new_bal) + _emit("payroll.credit", {"user": user, "amount": amount}) + return state, {"action": "credit", "user": user, "balance": new_bal} + + +def _handle_process_batch(state, params, ctx): + transfers = params.get("transfers") + if not isinstance(transfers, list) or not transfers: + raise ValueError("transfers must be non-empty list") + pvm_host.charge_gas(GAS_WRITE + len(transfers)) + batch_id = _require_str(params.get("batch_id"), "batch_id") + if batch_id in state["batches"]: + raise ValueError("batch_id already processed") + fee_bps = int(state["fee_bps"]) + required = {} + normalized = [] + total_amount = 0 + total_fee = 0 + for idx, entry in enumerate(transfers): + if not isinstance(entry, dict): + raise ValueError("transfer entry must be object") + sender = _require_str(entry.get("from"), "from") + recipient = _require_str(entry.get("to"), "to") + amount = _require_positive_int(entry.get("amount"), "amount") + fee = amount * fee_bps // 10000 + normalized.append( + { + "index": idx, + "from": sender, + "to": recipient, + "amount": amount, + "fee": fee, + } + ) + required[sender] = required.get(sender, 0) + amount + fee + total_amount += amount + total_fee += fee + + for sender, needed in required.items(): + if _balance_get(state, sender) < needed: + raise ValueError(f"insufficient balance: {sender}") + + digest = hashlib.sha256() + for entry in normalized: + line = ( + f"{entry['index']}:{entry['from']}->{entry['to']}:" + f"{entry['amount']}:{entry['fee']}" + ) + digest.update(line.encode("utf-8")) + digest_hex = digest.hexdigest() + + seed_material = f"{digest_hex}:{ctx.get('block_height', 0)}" + seed_hex = hashlib.sha256(seed_material.encode("utf-8")).hexdigest()[:16] + rng = random.Random(int(seed_hex, 16)) + sample_count = min(2, len(normalized)) + if sample_count: + sample_indices = sorted( + rng.sample(range(len(normalized)), sample_count) + ) + else: + sample_indices = [] + sample_hash = hashlib.sha256() + for idx in sample_indices: + entry = normalized[idx] + line = ( + f"{entry['index']}:{entry['from']}:{entry['to']}:" + f"{entry['amount']}:{entry['fee']}" + ) + sample_hash.update(line.encode("utf-8")) + + treasury = state["treasury"] + ledger = state["ledger"] + sequence = int(state["sequence"]) + for entry in normalized: + sender = entry["from"] + recipient = entry["to"] + amount = entry["amount"] + fee = entry["fee"] + _balance_set( + state, sender, _balance_get(state, sender) - amount - fee + ) + _balance_set( + state, recipient, _balance_get(state, recipient) + amount + ) + _balance_set(state, treasury, _balance_get(state, treasury) + fee) + ledger.append( + { + "id": sequence, + "batch_id": batch_id, + "index": entry["index"], + "from": sender, + "to": recipient, + "amount": amount, + "fee": fee, + } + ) + sequence += 1 + _emit( + "payroll.transfer", + { + "batch_id": batch_id, + "index": entry["index"], + "from": sender, + "to": recipient, + "amount": amount, + "fee": fee, + }, + ) + state["sequence"] = sequence + state["batches"][batch_id] = { + "count": len(normalized), + "total_amount": total_amount, + "total_fee": total_fee, + "digest": digest_hex, + "sample_digest": sample_hash.hexdigest(), + } + _emit( + "payroll.batch", + { + "batch_id": batch_id, + "count": len(normalized), + "total_amount": total_amount, + "total_fee": total_fee, + }, + ) + return state, { + "action": "process_batch", + "batch_id": batch_id, + "count": len(normalized), + "total_amount": total_amount, + "total_fee": total_fee, + "digest": digest_hex, + "audit_sample_indices": sample_indices, + } + + +def _handle_snapshot(state): + pvm_host.charge_gas(GAS_READ) + return state, { + "action": "snapshot", + "balances": state["balances"], + "batch_count": len(state["batches"]), + "ledger_len": len(state["ledger"]), + } + + +def _handle_balance(state, params): + pvm_host.charge_gas(GAS_READ) + user = params.get("user") + if user is None: + return state, {"action": "balance", "balances": state["balances"]} + user = _require_str(user, "user") + return state, {"action": "balance", "user": user, "balance": _balance_get(state, user)} + + +def _handle_batch_info(state, params): + pvm_host.charge_gas(GAS_READ) + batch_id = _require_str(params.get("batch_id"), "batch_id") + info = state["batches"].get(batch_id) + if info is None: + raise ValueError("batch_id not found") + return state, {"action": "batch_info", "batch_id": batch_id, "info": info} + + +def _apply_action(state, action, params, ctx): + if action == "init": + return _handle_init(state, params) + if state is None: + raise ValueError("not_initialized") + if action == "credit": + return _handle_credit(state, params) + if action == "process_batch": + return _handle_process_batch(state, params, ctx) + if action == "snapshot": + return _handle_snapshot(state) + if action == "balance": + return _handle_balance(state, params) + if action == "batch_info": + return _handle_batch_info(state, params) + raise ValueError(f"unknown_action: {action}") + + +def _run_actions(state, actions, ctx): + results = [] + for step in actions: + if not isinstance(step, dict): + raise ValueError("action entry must be object") + action = step.get("action") + params = step.get("params", {}) + if params is None: + params = {} + if not isinstance(params, dict): + raise ValueError("params must be object") + state, summary = _apply_action(state, action, params, ctx) + _save_state(state) + results.append(summary) + return state, results + + +def main(input_bytes): + pvm_host.charge_gas(GAS_BASE) + if not input_bytes: + return _ok( + { + "message": "batch payroll demo", + "actions": [ + "init", + "credit", + "process_batch", + "snapshot", + "balance", + "batch_info", + ], + "hint": "pass 'demo' or a JSON object", + } + ) + + try: + text = input_bytes.decode("utf-8") + except Exception as exc: + return _err("invalid_input", str(exc)) + + ctx = pvm_host.context() + + if text.strip().lower() == "demo": + actions = _demo_actions() + try: + state = _load_state() + state, results = _run_actions(state, actions, ctx) + return _ok({"action": "batch", "results": results}, state) + except Exception as exc: + return _err("invalid_input", str(exc)) + + try: + request = _json_loads(input_bytes) + except Exception as exc: + return _err("invalid_json", str(exc)) + + if not isinstance(request, dict): + return _err("invalid_input", "input must be object") + + if "actions" in request: + actions = request.get("actions") + if not isinstance(actions, list): + return _err("invalid_input", "actions must be list") + try: + state = _load_state() + state, results = _run_actions(state, actions, ctx) + return _ok({"action": "batch", "results": results}, state) + except Exception as exc: + return _err("invalid_input", str(exc)) + + action = request.get("action") + params = request.get("params", {}) + if params is None: + params = {} + if not isinstance(params, dict): + return _err("invalid_input", "params must be object") + + try: + state = _load_state() + state, summary = _apply_action(state, action, params, ctx) + _save_state(state) + return _ok(summary, state) + except Exception as exc: + return _err("invalid_input", str(exc)) diff --git a/examples/pvm_runtime_chain_demo/checkpoint_demo.py b/examples/pvm_runtime_chain_demo/checkpoint_demo.py new file mode 100644 index 00000000000..d318d8c8d87 --- /dev/null +++ b/examples/pvm_runtime_chain_demo/checkpoint_demo.py @@ -0,0 +1,20 @@ +import pvm_host +from pvm_sdk import runner, continuation + + +def run(coro): + return coro.send(None) + + +async def analyze(): + pvm_host.set_state(b"step", b"before") + cid = continuation.new_cid(None, "llm") + pvm_host.set_state(b"cid", cid) + result = await runner.llm("hi") + pvm_host.set_state(b"step", b"after") + pvm_host.set_state(b"result", str(result).encode("utf-8")) + return b"done" + + +def main(_input): + return run(analyze()) diff --git a/examples/pvm_runtime_chain_demo/contract.py b/examples/pvm_runtime_chain_demo/contract.py new file mode 100644 index 00000000000..2d7b2c6107c --- /dev/null +++ b/examples/pvm_runtime_chain_demo/contract.py @@ -0,0 +1,17 @@ +import pvm_host + + +def main(input_bytes: bytes) -> bytes: + ctx = pvm_host.context() + pvm_host.charge_gas(10) + + current = pvm_host.get_state(b"counter") + if current is None: + counter = 1 + else: + counter = int.from_bytes(current, "little") + 1 + pvm_host.set_state(b"counter", counter.to_bytes(8, "little")) + + pvm_host.emit_event("demo", b"ok") + payload = input_bytes if input_bytes else b"empty" + return b"ok:" + payload + b":h=" + str(ctx["block_height"]).encode("ascii") diff --git a/examples/pvm_runtime_chain_demo/determinism_check.py b/examples/pvm_runtime_chain_demo/determinism_check.py new file mode 100644 index 00000000000..29180c9a161 --- /dev/null +++ b/examples/pvm_runtime_chain_demo/determinism_check.py @@ -0,0 +1,203 @@ +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +OUTPUT_RE = re.compile(r"output_hex=([0-9a-fA-F]+)") + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def reset_state(root: Path) -> None: + state_dir = root / "tmp" / "pvm_state" + events_path = root / "tmp" / "pvm_events.log" + if state_dir.exists(): + shutil.rmtree(state_dir) + if events_path.exists(): + events_path.unlink() + + +def ensure_built(root: Path, env: dict) -> None: + cmd = ["cargo", "build", "--release", "--example", "pvm_runtime_chain_demo"] + subprocess.run(cmd, cwd=root, env=env, check=True) + + +def apply_dyld_fallback(env: dict) -> None: + fallback = env.get("DYLD_FALLBACK_LIBRARY_PATH") + if fallback: + paths = fallback.split(os.pathsep) + else: + paths = [] + + conda_prefix = env.get("CONDA_PREFIX") + if conda_prefix: + conda_lib = Path(conda_prefix) / "lib" + if conda_lib.exists(): + paths.append(str(conda_lib)) + + candidates = [ + Path("/opt/homebrew/opt/libffi/lib"), + Path("/usr/local/opt/libffi/lib"), + Path("/opt/homebrew/opt/libiconv/lib"), + Path("/usr/local/opt/libiconv/lib"), + Path("/opt/miniconda3/lib"), + ] + for candidate in candidates: + if candidate.exists() and str(candidate) not in paths: + paths.append(str(candidate)) + + if paths: + env["DYLD_FALLBACK_LIBRARY_PATH"] = os.pathsep.join( + dict.fromkeys(paths) + ) + + +def run_once( + root: Path, exe: Path, script: str, input_text: str, env: dict, extra_args: list[str] +) -> str: + cmd = [str(exe)] + cmd.extend(extra_args) + cmd.append(script) + if input_text: + cmd.append(input_text) + result = subprocess.run( + cmd, + cwd=root, + env=env, + capture_output=True, + text=True, + ) + if result.returncode != 0: + sys.stderr.write(result.stdout) + sys.stderr.write(result.stderr) + raise RuntimeError(f"execution failed with code {result.returncode}") + text = result.stdout + result.stderr + match = OUTPUT_RE.search(text) + if not match: + sys.stderr.write(text) + raise RuntimeError("output_hex not found in output") + return match.group(1) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Check determinism across runs.") + parser.add_argument( + "--script", + default="examples/pvm_runtime_chain_demo/determinism_demo.py", + help="Path to the Python contract (relative to repo root).", + ) + parser.add_argument("--input", default="hello", help="Contract input string.") + parser.add_argument("--runs", type=int, default=3, help="Number of runs to compare.") + parser.add_argument( + "--decode", + action="store_true", + help="Decode output hex as JSON for display.", + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + "--reset-state", + dest="reset_state", + action="store_true", + default=True, + help="Reset tmp state between runs (default).", + ) + group.add_argument( + "--keep-state", + dest="reset_state", + action="store_false", + help="Keep tmp state between runs.", + ) + parser.add_argument( + "--skip-build", + action="store_true", + help="Skip cargo build if the example binary already exists.", + ) + parser.add_argument( + "--trace-imports", + help="Write import trace JSON to this path.", + ) + parser.add_argument( + "--trace-allow-all", + action="store_true", + help="Allow non-whitelisted imports during tracing.", + ) + parser.add_argument( + "--print-whitelist", + action="store_true", + help="Print suggested whitelist from the trace output.", + ) + args = parser.parse_args() + + root = repo_root() + exe = root / "target" / "release" / "examples" / "pvm_runtime_chain_demo" + env = os.environ.copy() + env_build = env.copy() + for key in ("DYLD_LIBRARY_PATH", "DYLD_FALLBACK_LIBRARY_PATH", "DYLD_INSERT_LIBRARIES"): + env_build.pop(key, None) + + if not args.skip_build or not exe.exists(): + ensure_built(root, env_build) + + env_run = env.copy() + apply_dyld_fallback(env_run) + + extra_args = [] + if args.trace_imports: + extra_args.extend(["--trace-imports", args.trace_imports]) + if args.trace_allow_all: + extra_args.append("--trace-allow-all") + + outputs = [] + for idx in range(args.runs): + if args.reset_state: + reset_state(root) + out_hex = run_once( + root, exe, args.script, args.input, env_run, extra_args + ) + outputs.append(out_hex) + print(f"run[{idx}] output_hex={out_hex}") + if args.decode: + try: + decoded = bytes.fromhex(out_hex).decode("utf-8") + parsed = json.loads(decoded) + print(json.dumps(parsed, indent=2, sort_keys=True)) + except Exception as exc: + print(f"decode failed: {exc}") + + first = outputs[0] + mismatches = [idx for idx, value in enumerate(outputs) if value != first] + if mismatches: + print(f"determinism check failed: mismatched runs {mismatches}") + return 1 + + print(f"determinism check ok: {args.runs} runs match") + + if args.trace_imports and args.print_whitelist: + trace_path = Path(args.trace_imports) + if not trace_path.exists(): + print(f"trace file not found: {trace_path}") + return 1 + with trace_path.open("r", encoding="utf-8") as handle: + data = json.load(handle) + missing = data.get("missing", []) + suggested = data.get("whitelist_suggested", []) + print("missing imports (add to whitelist):") + for item in missing: + print(f"- {item}") + print("whitelist_suggested (Rust vec!):") + print("vec![") + for item in suggested: + print(f' "{item}",') + print("]") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/pvm_runtime_chain_demo/determinism_demo.py b/examples/pvm_runtime_chain_demo/determinism_demo.py new file mode 100644 index 00000000000..b6edfb37f90 --- /dev/null +++ b/examples/pvm_runtime_chain_demo/determinism_demo.py @@ -0,0 +1,64 @@ +import hashlib +import json +import random +import struct +import time + +import pvm_host + + +def _hex(b): + return b.hex() if isinstance(b, (bytes, bytearray)) else None + + +def main(input_bytes): + ctx = pvm_host.context() + + # Deterministic state and event usage (idempotent write). + state_key = b"demo:state" + state_value = hashlib.sha256(input_bytes).digest() + pvm_host.set_state(state_key, state_value) + current_state = pvm_host.get_state(state_key) + pvm_host.emit_event("demo", state_value) + + # Deterministic randomness/time from host context. + rand_bytes = random.randbytes(16) + timestamp_ns = time.time_ns() + + # Use whitelisted stdlib modules. + packed_len = struct.pack(" 0") + return value + + +def _require_nonneg_int(value, name): + value = _require_int(value, name) + if value < 0: + raise ValueError(f"{name} must be >= 0") + return value + + +def _require_str(value, name): + if not isinstance(value, str) or not value: + raise ValueError(f"{name} must be non-empty string") + return value + + +def _ctx_sender(ctx): + sender = ctx.get("sender", b"") + if isinstance(sender, (bytes, bytearray)): + try: + return sender.decode("ascii") + except Exception: + return sender.hex() + return str(sender) + + +def _load_state(): + raw = pvm_host.get_state(STATE_KEY) + if raw is None: + return None + return _json_loads(raw) + + +def _save_state(state): + pvm_host.set_state(STATE_KEY, _json_dumps(state)) + + +def _balance_get(state, user): + return int(state["balances"].get(user, 0)) + + +def _balance_set(state, user, amount): + if amount <= 0: + state["balances"].pop(user, None) + else: + state["balances"][user] = amount + + +def _order_list(state): + orders = state["orders"] + out = [] + for key in sorted(orders, key=lambda k: int(k)): + out.append(orders[key]) + return out + + +def _demo_actions(): + return [ + { + "action": "init", + "params": {"fee_bps": 50, "treasury": "treasury"}, + }, + {"action": "deposit", "params": {"user": "alice", "amount": 1000}}, + {"action": "deposit", "params": {"user": "bob", "amount": 800}}, + { + "action": "list", + "params": { + "seller": "alice", + "item": "camera", + "price": 300, + "expires_in": 5, + }, + }, + {"action": "fund", "params": {"order_id": 1, "buyer": "bob"}}, + {"action": "release", "params": {"order_id": 1, "actor": "alice"}}, + { + "action": "list", + "params": { + "seller": "bob", + "item": "bike", + "price": 200, + "expires_in": 0, + }, + }, + {"action": "cancel", "params": {"order_id": 2}}, + {"action": "info", "params": {}}, + ] + + +def _handle_init(params, ctx): + pvm_host.charge_gas(GAS_WRITE) + if _load_state() is not None: + raise ValueError("already initialized") + fee_bps = _require_int(params.get("fee_bps", 30), "fee_bps") + if fee_bps < 0 or fee_bps > 10000: + raise ValueError("fee_bps must be 0..10000") + treasury = _require_str(params.get("treasury", "treasury"), "treasury") + state = { + "version": 1, + "fee_bps": fee_bps, + "treasury": treasury, + "next_order_id": 1, + "balances": {}, + "orders": {}, + } + _save_state(state) + _emit("escrow.init", {"fee_bps": fee_bps, "treasury": treasury}) + return state, { + "action": "init", + "fee_bps": fee_bps, + "treasury": treasury, + } + + +def _handle_deposit(state, params, ctx_sender): + pvm_host.charge_gas(GAS_WRITE) + user = params.get("user") + if user is None: + user = ctx_sender + user = _require_str(user, "user") + amount = _require_positive_int(params.get("amount"), "amount") + new_bal = _balance_get(state, user) + amount + _balance_set(state, user, new_bal) + _emit("escrow.deposit", {"user": user, "amount": amount}) + return state, {"action": "deposit", "user": user, "balance": new_bal} + + +def _handle_list(state, params, ctx_sender, ctx): + pvm_host.charge_gas(GAS_WRITE) + seller = params.get("seller") + if seller is None: + seller = ctx_sender + seller = _require_str(str(seller), "seller") + item = _require_str(str(params.get("item", "item")), "item") + price = _require_positive_int(params.get("price"), "price") + expires_in = _require_nonneg_int(params.get("expires_in", 5), "expires_in") + height = int(ctx.get("block_height", 0)) + order_id = state["next_order_id"] + state["next_order_id"] = order_id + 1 + order = { + "id": order_id, + "item": item, + "seller": seller, + "buyer": None, + "price": price, + "fee": 0, + "escrow": 0, + "status": "listed", + "created_height": height, + "expires_at": height + expires_in, + } + state["orders"][str(order_id)] = order + _emit( + "escrow.list", + {"order_id": order_id, "seller": seller, "price": price}, + ) + return state, {"action": "list", "order": order} + + +def _handle_fund(state, params, ctx_sender, ctx): + pvm_host.charge_gas(GAS_WRITE) + order_id = _require_int(params.get("order_id"), "order_id") + key = str(order_id) + order = state["orders"].get(key) + if order is None: + raise ValueError("unknown order_id") + if order["status"] != "listed": + raise ValueError("order not listed") + height = int(ctx.get("block_height", 0)) + if height >= order["expires_at"]: + raise ValueError("order expired") + buyer = params.get("buyer") + if buyer is None: + buyer = ctx_sender + buyer = _require_str(str(buyer), "buyer") + price = int(order["price"]) + fee = price * int(state["fee_bps"]) // 10000 + escrow_amount = price - fee + if _balance_get(state, buyer) < price: + raise ValueError("insufficient balance") + _balance_set(state, buyer, _balance_get(state, buyer) - price) + treasury = state["treasury"] + _balance_set(state, treasury, _balance_get(state, treasury) + fee) + order["status"] = "funded" + order["buyer"] = buyer + order["fee"] = fee + order["escrow"] = escrow_amount + order["funded_height"] = height + _emit( + "escrow.fund", + {"order_id": order_id, "buyer": buyer, "escrow": escrow_amount}, + ) + return state, {"action": "fund", "order_id": order_id, "buyer": buyer} + + +def _handle_release(state, params, ctx_sender, ctx): + pvm_host.charge_gas(GAS_WRITE) + order_id = _require_int(params.get("order_id"), "order_id") + key = str(order_id) + order = state["orders"].get(key) + if order is None: + raise ValueError("unknown order_id") + if order["status"] != "funded": + raise ValueError("order not funded") + actor = params.get("actor") + if actor is None: + actor = ctx_sender + actor = _require_str(str(actor), "actor") + if actor != order["seller"]: + raise ValueError("only seller can release") + seller = order["seller"] + escrow_amount = int(order["escrow"]) + _balance_set(state, seller, _balance_get(state, seller) + escrow_amount) + order["status"] = "released" + order["released_height"] = int(ctx.get("block_height", 0)) + _emit( + "escrow.release", + {"order_id": order_id, "seller": seller, "amount": escrow_amount}, + ) + return state, {"action": "release", "order_id": order_id, "seller": seller} + + +def _handle_cancel(state, params, ctx): + pvm_host.charge_gas(GAS_WRITE) + order_id = _require_int(params.get("order_id"), "order_id") + key = str(order_id) + order = state["orders"].get(key) + if order is None: + raise ValueError("unknown order_id") + height = int(ctx.get("block_height", 0)) + if height < order["expires_at"]: + raise ValueError("order not expired") + if order["status"] == "listed": + order["status"] = "cancelled" + order["cancelled_height"] = height + _emit("escrow.cancel", {"order_id": order_id}) + return state, {"action": "cancel", "order_id": order_id} + if order["status"] == "funded": + buyer = order.get("buyer") + refund = int(order.get("escrow", 0)) + if buyer: + _balance_set(state, buyer, _balance_get(state, buyer) + refund) + order["status"] = "refunded" + order["refunded_height"] = height + _emit("escrow.refund", {"order_id": order_id, "amount": refund}) + return state, {"action": "refund", "order_id": order_id} + raise ValueError("order not cancellable") + + +def _handle_info(state): + pvm_host.charge_gas(GAS_READ) + return state, { + "action": "info", + "fee_bps": state["fee_bps"], + "treasury": state["treasury"], + "order_count": len(state["orders"]), + "balances": state["balances"], + } + + +def _handle_balance(state, params): + pvm_host.charge_gas(GAS_READ) + user = params.get("user") + if user is None: + return state, {"action": "balance", "balances": state["balances"]} + user = _require_str(user, "user") + return state, { + "action": "balance", + "user": user, + "balance": _balance_get(state, user), + } + + +def _handle_list_orders(state): + pvm_host.charge_gas(GAS_READ) + return state, {"action": "list_orders", "orders": _order_list(state)} + + +def _apply_action(state, action, params, ctx, ctx_sender): + if action == "init": + return _handle_init(params, ctx) + if state is None: + raise ValueError("not_initialized") + if action == "deposit": + return _handle_deposit(state, params, ctx_sender) + if action == "list": + return _handle_list(state, params, ctx_sender, ctx) + if action == "fund": + return _handle_fund(state, params, ctx_sender, ctx) + if action == "release": + return _handle_release(state, params, ctx_sender, ctx) + if action == "cancel": + return _handle_cancel(state, params, ctx) + if action == "info": + return _handle_info(state) + if action == "balance": + return _handle_balance(state, params) + if action == "list_orders": + return _handle_list_orders(state) + raise ValueError(f"unknown_action: {action}") + + +def _run_actions(state, actions, ctx, ctx_sender): + results = [] + for step in actions: + if not isinstance(step, dict): + raise ValueError("action entry must be object") + action = step.get("action") + params = step.get("params", {}) + if params is None: + params = {} + if not isinstance(params, dict): + raise ValueError("params must be object") + state, summary = _apply_action(state, action, params, ctx, ctx_sender) + _save_state(state) + results.append(summary) + return state, results + + +def main(input_bytes): + pvm_host.charge_gas(GAS_BASE) + if not input_bytes: + return _ok( + { + "message": "escrow marketplace demo", + "actions": [ + "init", + "deposit", + "list", + "fund", + "release", + "cancel", + "balance", + "list_orders", + "info", + ], + "hint": "pass 'demo' or a JSON object", + } + ) + + try: + text = input_bytes.decode("utf-8") + except Exception as exc: + return _err("invalid_input", str(exc)) + + ctx = pvm_host.context() + ctx_sender = _ctx_sender(ctx) + + if text.strip().lower() == "demo": + actions = _demo_actions() + try: + state = _load_state() + state, results = _run_actions(state, actions, ctx, ctx_sender) + return _ok({"action": "batch", "results": results}, state) + except Exception as exc: + return _err("invalid_input", str(exc)) + + try: + request = _json_loads(input_bytes) + except Exception as exc: + return _err("invalid_json", str(exc)) + + if not isinstance(request, dict): + return _err("invalid_input", "input must be object") + + if "actions" in request: + actions = request.get("actions") + if not isinstance(actions, list): + return _err("invalid_input", "actions must be list") + try: + state = _load_state() + state, results = _run_actions(state, actions, ctx, ctx_sender) + return _ok({"action": "batch", "results": results}, state) + except Exception as exc: + return _err("invalid_input", str(exc)) + + action = request.get("action") + params = request.get("params", {}) + if params is None: + params = {} + if not isinstance(params, dict): + return _err("invalid_input", "params must be object") + + try: + state = _load_state() + state, summary = _apply_action(state, action, params, ctx, ctx_sender) + _save_state(state) + return _ok(summary, state) + except Exception as exc: + return _err("invalid_input", str(exc)) + diff --git a/examples/pvm_runtime_chain_demo/fsm_demo.py b/examples/pvm_runtime_chain_demo/fsm_demo.py new file mode 100644 index 00000000000..89c62564afc --- /dev/null +++ b/examples/pvm_runtime_chain_demo/fsm_demo.py @@ -0,0 +1,23 @@ +import pvm_host +from pvm_sdk import runner, capture, continuation + + +@runner.continuation +async def analyze(self, msg): + ctx = capture() + ctx.value = await runner.llm("hi") + return ctx.value + + +def main(input_bytes): + if input_bytes == b"start": + cid = continuation.new_cid(None, "analyze") + pvm_host.set_state(b"cid", cid) + analyze(None, {}) + return b"started" + result = input_bytes.decode("utf-8") + cid = continuation.new_cid(None, "analyze") + msg = {"cid": cid, "result": result} + out = analyze__resume(None, msg) + pvm_host.set_state(b"fsm_result", str(out).encode("utf-8")) + return b"done" diff --git a/examples/pvm_runtime_chain_demo/main.rs b/examples/pvm_runtime_chain_demo/main.rs new file mode 100644 index 00000000000..8714393fb23 --- /dev/null +++ b/examples/pvm_runtime_chain_demo/main.rs @@ -0,0 +1,195 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +use pvm_alto::{default_options, execute_tx_fs, FsTxConfig}; +use pvm_host::HostContext; +use pvm_runtime::{ContinuationOptions, DeterminismOptions}; +use rustpython_vm::vm::ContinuationMode; + +fn main() -> Result<(), Box> { + let mut args = env::args().skip(1); + let mut trace_path: Option = None; + let mut trace_allow_all = false; + let mut script_path: Option = None; + let mut input: Option> = None; + let mut deterministic = true; + let mut continuation_mode: Option = None; + let mut resume_bytes: Option> = None; + let mut resume_key: Option> = None; + let mut checkpoint_key: Option> = None; + + while let Some(arg) = args.next() { + match arg.as_str() { + "--trace-imports" => { + let value = args.next().ok_or_else(|| usage())?; + trace_path = Some(value); + } + "--trace-allow-all" => { + trace_allow_all = true; + } + "--nondeterministic" => { + deterministic = false; + } + "--continuation" => { + let value = args.next().ok_or_else(|| usage())?; + continuation_mode = Some(parse_continuation_mode(&value)?); + } + "--resume-bytes" => { + let value = args.next().ok_or_else(|| usage())?; + resume_bytes = Some(parse_hex_arg(&value)?); + } + "--resume-key" => { + let value = args.next().ok_or_else(|| usage())?; + resume_key = Some(parse_hex_arg(&value)?); + } + "--checkpoint-key" => { + let value = args.next().ok_or_else(|| usage())?; + checkpoint_key = Some(parse_hex_arg(&value)?); + } + "--help" | "-h" => { + println!("{}", usage()); + return Ok(()); + } + _ => { + if let Some(value) = arg.strip_prefix("--continuation=") { + continuation_mode = Some(parse_continuation_mode(value)?); + continue; + } + if let Some(value) = arg.strip_prefix("--trace-imports=") { + trace_path = Some(value.to_owned()); + continue; + } + if let Some(value) = arg.strip_prefix("--resume-bytes=") { + resume_bytes = Some(parse_hex_arg(value)?); + continue; + } + if let Some(value) = arg.strip_prefix("--resume-key=") { + resume_key = Some(parse_hex_arg(value)?); + continue; + } + if let Some(value) = arg.strip_prefix("--checkpoint-key=") { + checkpoint_key = Some(parse_hex_arg(value)?); + continue; + } + if script_path.is_none() { + script_path = Some(arg); + } else if input.is_none() { + input = Some(arg.into_bytes()); + } else { + return Err(usage().into()); + } + } + } + } + + if trace_allow_all && trace_path.is_none() { + return Err("--trace-allow-all requires --trace-imports".into()); + } + + let script_path = script_path.ok_or_else(|| usage())?; + let input = input.unwrap_or_default(); + + let code = fs::read(&script_path)?; + + let ctx = HostContext { + block_height: 1, + block_hash: [0u8; 32], + tx_hash: [1u8; 32], + sender: b"alice".to_vec(), + timestamp_ms: 1_700_000_000_000, + actor_addr: b"demo_actor".to_vec(), + msg_id: Vec::new(), + nonce: 0, + }; + + let config = FsTxConfig { + state_dir: PathBuf::from("tmp/pvm_state"), + events_path: PathBuf::from("tmp/pvm_events.log"), + gas_limit: 1_000_000, + context: ctx, + }; + + let mut options = default_options().with_source_path(script_path); + if continuation_mode.is_some() + || resume_bytes.is_some() + || resume_key.is_some() + || checkpoint_key.is_some() + { + let mode = continuation_mode.unwrap_or_else(|| { + if resume_bytes.is_some() || resume_key.is_some() || checkpoint_key.is_some() { + ContinuationMode::Checkpoint + } else { + ContinuationMode::Fsm + } + }); + options.continuation = Some(ContinuationOptions { + mode, + resume_bytes, + resume_key, + checkpoint_key, + }); + } + if !deterministic { + options.deterministic = false; + options.determinism = None; + } + if let Some(path) = trace_path { + let mut det = DeterminismOptions::deterministic(None); + det.trace_imports = true; + det.trace_allow_all = trace_allow_all; + det.trace_path = Some(path); + options = options.with_determinism(det); + } + let output = execute_tx_fs(&code, &input, config, &options)?; + + println!("output_hex={}", encode_hex(&output)); + Ok(()) +} + +fn encode_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for &byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + +fn parse_continuation_mode(value: &str) -> Result { + match value { + "fsm" => Ok(ContinuationMode::Fsm), + "checkpoint" => Ok(ContinuationMode::Checkpoint), + _ => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "continuation mode must be fsm or checkpoint", + )), + } +} + +fn parse_hex_arg(value: &str) -> Result, std::io::Error> { + let value = value.strip_prefix("0x").unwrap_or(value); + if value.len() % 2 != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "hex string must have even length", + )); + } + let mut out = Vec::with_capacity(value.len() / 2); + let bytes = value.as_bytes(); + for idx in (0..bytes.len()).step_by(2) { + let chunk = std::str::from_utf8(&bytes[idx..idx + 2]).map_err(|_| { + std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid hex string") + })?; + let byte = u8::from_str_radix(chunk, 16).map_err(|_| { + std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid hex string") + })?; + out.push(byte); + } + Ok(out) +} + +fn usage() -> &'static str { + "usage: pvm_runtime_chain_demo [--trace-imports ] [--trace-allow-all] [--nondeterministic] [--continuation fsm|checkpoint] [--resume-bytes ] [--resume-key ] [--checkpoint-key ] [input]" +} diff --git a/examples/pvm_runtime_chain_demo/run_checkpoint_demo.sh b/examples/pvm_runtime_chain_demo/run_checkpoint_demo.sh new file mode 100755 index 00000000000..6c21fcc99e7 --- /dev/null +++ b/examples/pvm_runtime_chain_demo/run_checkpoint_demo.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +demo_dir="$repo_root/examples/pvm_runtime_chain_demo" +bin="$repo_root/target/debug/examples/pvm_runtime_chain_demo" +state_dir="$repo_root/tmp/pvm_state" + +cd "$repo_root" + +lib_paths=() +if [ -d "/opt/homebrew/opt/libffi/lib" ]; then + lib_paths+=("/opt/homebrew/opt/libffi/lib") +fi +if [ -d "/opt/homebrew/opt/libiconv/lib" ]; then + lib_paths+=("/opt/homebrew/opt/libiconv/lib") +fi +dyld_prefix="" +if [ "${#lib_paths[@]}" -gt 0 ]; then + dyld_prefix="$(IFS=:; echo "${lib_paths[*]}")" +fi + +mkdir -p "$repo_root/tmp" +ts=$(date +%s) +if [ -e "$repo_root/tmp/pvm_state" ]; then + mv "$repo_root/tmp/pvm_state" "$repo_root/tmp/pvm_state.bak.$ts" +fi +if [ -e "$repo_root/tmp/pvm_events.log" ]; then + mv "$repo_root/tmp/pvm_events.log" "$repo_root/tmp/pvm_events.log.bak.$ts" +fi + +cargo build --example pvm_runtime_chain_demo + +if [ -n "$dyld_prefix" ]; then + DYLD_LIBRARY_PATH="${dyld_prefix}${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" \ + "$bin" --continuation checkpoint --checkpoint-key 636865636b706f696e74 "$demo_dir/checkpoint_demo.py" +else + "$bin" --continuation checkpoint --checkpoint-key 636865636b706f696e74 "$demo_dir/checkpoint_demo.py" +fi + +export PVM_STATE_DIR="$state_dir" +python - <<'PY' +import json +import os +from pathlib import Path + +state_dir = Path(os.environ["PVM_STATE_DIR"]) +cid = (state_dir / "636964").read_bytes() +key = b"__runner_result:" + cid +payload = {"result": "ok"} +raw = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("ascii") +(state_dir / key.hex()).write_bytes(raw) +PY + +if [ -n "$dyld_prefix" ]; then + DYLD_LIBRARY_PATH="${dyld_prefix}${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" \ + "$bin" --resume-key 636865636b706f696e74 "$demo_dir/checkpoint_demo.py" +else + "$bin" --resume-key 636865636b706f696e74 "$demo_dir/checkpoint_demo.py" +fi + +python - <<'PY' +import os +from pathlib import Path + +state_dir = Path(os.environ["PVM_STATE_DIR"]) +step = (state_dir / "73746570").read_bytes() +result = (state_dir / "726573756c74").read_bytes() +print("step=", step) +print("result=", result) +PY diff --git a/examples/pvm_runtime_chain_demo/run_fsm_demo.sh b/examples/pvm_runtime_chain_demo/run_fsm_demo.sh new file mode 100755 index 00000000000..780bc4ad401 --- /dev/null +++ b/examples/pvm_runtime_chain_demo/run_fsm_demo.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +demo_dir="$repo_root/examples/pvm_runtime_chain_demo" +bin="$repo_root/target/debug/examples/pvm_runtime_chain_demo" + +cd "$repo_root" + +lib_paths=() +if [ -d "/opt/homebrew/opt/libffi/lib" ]; then + lib_paths+=("/opt/homebrew/opt/libffi/lib") +fi +if [ -d "/opt/homebrew/opt/libiconv/lib" ]; then + lib_paths+=("/opt/homebrew/opt/libiconv/lib") +fi +dyld_prefix="" +if [ "${#lib_paths[@]}" -gt 0 ]; then + dyld_prefix="$(IFS=:; echo "${lib_paths[*]}")" +fi + +mkdir -p "$repo_root/tmp" +ts=$(date +%s) +if [ -e "$repo_root/tmp/pvm_state" ]; then + mv "$repo_root/tmp/pvm_state" "$repo_root/tmp/pvm_state.bak.$ts" +fi +if [ -e "$repo_root/tmp/pvm_events.log" ]; then + mv "$repo_root/tmp/pvm_events.log" "$repo_root/tmp/pvm_events.log.bak.$ts" +fi + +cargo build --example pvm_runtime_chain_demo + +if [ -n "$dyld_prefix" ]; then + DYLD_LIBRARY_PATH="${dyld_prefix}${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" \ + "$bin" --continuation fsm "$demo_dir/fsm_demo.py" start + DYLD_LIBRARY_PATH="${dyld_prefix}${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" \ + "$bin" --continuation fsm "$demo_dir/fsm_demo.py" ok +else + "$bin" --continuation fsm "$demo_dir/fsm_demo.py" start + "$bin" --continuation fsm "$demo_dir/fsm_demo.py" ok +fi diff --git a/examples/pvm_runtime_chain_demo/staking_rewards_demo.py b/examples/pvm_runtime_chain_demo/staking_rewards_demo.py new file mode 100644 index 00000000000..87f8a07c474 --- /dev/null +++ b/examples/pvm_runtime_chain_demo/staking_rewards_demo.py @@ -0,0 +1,453 @@ +import hashlib +import json +import random + +import pvm_host + +STATE_KEY = b"staking_state_v1" + +GAS_BASE = 5 +GAS_READ = 5 +GAS_WRITE = 20 + + +def _json_dumps(value): + return json.dumps( + value, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + ).encode("ascii") + + +def _json_loads(data): + return json.loads(data.decode("utf-8")) + + +def _emit(topic, payload): + pvm_host.emit_event(topic, _json_dumps(payload)) + + +def _state_hash(state): + return hashlib.sha256(_json_dumps(state)).hexdigest() + + +def _ok(payload, state=None): + payload["ok"] = True + if state is not None: + payload["state_hash"] = _state_hash(state) + return _json_dumps(payload) + + +def _err(code, detail=None): + out = {"ok": False, "error": code} + if detail is not None: + out["detail"] = detail + return _json_dumps(out) + + +def _require_int(value, name): + if not isinstance(value, int): + raise ValueError(f"{name} must be int") + return value + + +def _require_positive_int(value, name): + value = _require_int(value, name) + if value <= 0: + raise ValueError(f"{name} must be > 0") + return value + + +def _require_nonneg_int(value, name): + value = _require_int(value, name) + if value < 0: + raise ValueError(f"{name} must be >= 0") + return value + + +def _require_str(value, name): + if not isinstance(value, str) or not value: + raise ValueError(f"{name} must be non-empty string") + return value + + +def _load_state(): + raw = pvm_host.get_state(STATE_KEY) + if raw is None: + return None + return _json_loads(raw) + + +def _save_state(state): + pvm_host.set_state(STATE_KEY, _json_dumps(state)) + + +def _add_reward(state, addr, amount): + if amount <= 0: + return + rewards = state["rewards"] + rewards[addr] = int(rewards.get(addr, 0)) + int(amount) + + +def _delegations_by_validator(state): + delegations = state["delegations"] + by_validator = {} + for delegator in sorted(delegations): + entries = delegations[delegator] + if not isinstance(entries, dict): + continue + for validator in sorted(entries): + amount = int(entries.get(validator, 0)) + if amount <= 0: + continue + by_validator.setdefault(validator, []).append((delegator, amount)) + return by_validator + + +def _validator_weights(state): + validators = state["validators"] + weights = {} + for name in sorted(validators): + weights[name] = int(validators[name].get("self_stake", 0)) + by_validator = _delegations_by_validator(state) + for validator, entries in by_validator.items(): + total = sum(amount for _, amount in entries) + weights[validator] = weights.get(validator, 0) + total + return weights + + +def _pick_proposer(weights, seed_int): + total_weight = sum(weights.values()) + if total_weight <= 0: + return None + target = seed_int % total_weight + running = 0 + for name in sorted(weights): + running += int(weights[name]) + if target < running: + return name + return sorted(weights)[-1] + + +def _seed_from_ctx(ctx, epoch): + parts = [] + block_hash = ctx.get("block_hash") + if isinstance(block_hash, (bytes, bytearray)): + parts.append(block_hash) + else: + parts.append(str(block_hash).encode("utf-8")) + parts.append(str(ctx.get("block_height", 0)).encode("ascii")) + parts.append(str(epoch).encode("ascii")) + digest = hashlib.sha256(b"|".join(parts)).digest() + return int.from_bytes(digest[:8], "big") + + +def _demo_actions(): + return [ + {"action": "init", "params": {"inflation": 1200}}, + { + "action": "register_validator", + "params": {"validator": "val1", "stake": 500, "commission_bps": 500}, + }, + { + "action": "register_validator", + "params": {"validator": "val2", "stake": 350, "commission_bps": 300}, + }, + { + "action": "delegate", + "params": {"delegator": "alice", "validator": "val1", "amount": 400}, + }, + { + "action": "delegate", + "params": {"delegator": "bob", "validator": "val2", "amount": 250}, + }, + {"action": "distribute", "params": {}}, + {"action": "distribute", "params": {}}, + {"action": "info", "params": {}}, + ] + + +def _handle_init(state, params): + pvm_host.charge_gas(GAS_WRITE) + if state is not None: + raise ValueError("already initialized") + inflation = _require_nonneg_int(params.get("inflation", 1000), "inflation") + state = { + "version": 1, + "epoch": 0, + "inflation": inflation, + "validators": {}, + "delegations": {}, + "rewards": {}, + "last_proposer": None, + } + _save_state(state) + _emit("staking.init", {"inflation": inflation}) + return state, {"action": "init", "inflation": inflation} + + +def _handle_register(state, params): + pvm_host.charge_gas(GAS_WRITE) + validator = _require_str(params.get("validator"), "validator") + stake = _require_positive_int(params.get("stake"), "stake") + commission_bps = _require_int(params.get("commission_bps", 0), "commission_bps") + if commission_bps < 0 or commission_bps > 10000: + raise ValueError("commission_bps must be 0..10000") + entry = state["validators"].get(validator) + if entry is None: + entry = {"self_stake": 0, "commission_bps": commission_bps, "active": True} + else: + entry["commission_bps"] = commission_bps + entry["self_stake"] = int(entry.get("self_stake", 0)) + stake + state["validators"][validator] = entry + _emit( + "staking.register", + {"validator": validator, "stake": stake, "commission_bps": commission_bps}, + ) + return state, { + "action": "register_validator", + "validator": validator, + "self_stake": entry["self_stake"], + } + + +def _handle_delegate(state, params): + pvm_host.charge_gas(GAS_WRITE) + delegator = _require_str(params.get("delegator"), "delegator") + validator = _require_str(params.get("validator"), "validator") + amount = _require_positive_int(params.get("amount"), "amount") + if validator not in state["validators"]: + raise ValueError("validator not found") + entries = state["delegations"].setdefault(delegator, {}) + entries[validator] = int(entries.get(validator, 0)) + amount + _emit( + "staking.delegate", + {"delegator": delegator, "validator": validator, "amount": amount}, + ) + return state, { + "action": "delegate", + "delegator": delegator, + "validator": validator, + "amount": amount, + } + + +def _handle_undelegate(state, params): + pvm_host.charge_gas(GAS_WRITE) + delegator = _require_str(params.get("delegator"), "delegator") + validator = _require_str(params.get("validator"), "validator") + amount = _require_positive_int(params.get("amount"), "amount") + entries = state["delegations"].get(delegator) + if not entries or int(entries.get(validator, 0)) < amount: + raise ValueError("insufficient delegation") + new_amount = int(entries.get(validator, 0)) - amount + if new_amount <= 0: + entries.pop(validator, None) + else: + entries[validator] = new_amount + if not entries: + state["delegations"].pop(delegator, None) + _emit( + "staking.undelegate", + {"delegator": delegator, "validator": validator, "amount": amount}, + ) + return state, { + "action": "undelegate", + "delegator": delegator, + "validator": validator, + "amount": amount, + } + + +def _handle_distribute(state, ctx): + pvm_host.charge_gas(GAS_WRITE) + epoch = int(state["epoch"]) + weights = _validator_weights(state) + total_weight = sum(weights.values()) + if total_weight <= 0: + raise ValueError("no stake available") + seed_int = _seed_from_ctx(ctx, epoch) + proposer = _pick_proposer(weights, seed_int) + inflation = int(state["inflation"]) + reward_map = {} + distributed = 0 + for name in sorted(weights): + reward = inflation * int(weights[name]) // total_weight + reward_map[name] = reward + distributed += reward + leftover = inflation - distributed + if proposer is not None: + reward_map[proposer] = reward_map.get(proposer, 0) + leftover + + delegations = _delegations_by_validator(state) + for name in sorted(reward_map): + reward = int(reward_map[name]) + if reward <= 0: + continue + validator_info = state["validators"].get(name, {}) + commission_bps = int(validator_info.get("commission_bps", 0)) + commission = reward * commission_bps // 10000 + _add_reward(state, name, commission) + remainder = reward - commission + entries = delegations.get(name, []) + if not entries or remainder <= 0: + _add_reward(state, name, remainder) + continue + total_delegation = sum(amount for _, amount in entries) + if total_delegation <= 0: + _add_reward(state, name, remainder) + continue + allocated = 0 + for delegator, amount in entries: + share = remainder * amount // total_delegation + allocated += share + _add_reward(state, delegator, share) + remainder_left = remainder - allocated + if remainder_left: + _add_reward(state, name, remainder_left) + + validator_names = sorted(weights) + rng = random.Random(seed_int) + sample_count = min(2, len(validator_names)) + if sample_count: + sample_validators = sorted(rng.sample(validator_names, sample_count)) + else: + sample_validators = [] + + reward_list = [ + {"validator": name, "reward": reward_map.get(name, 0)} + for name in sorted(reward_map) + ] + + state["epoch"] = epoch + 1 + state["last_proposer"] = proposer + _emit( + "staking.distribute", + {"epoch": epoch, "proposer": proposer, "inflation": inflation}, + ) + return state, { + "action": "distribute", + "epoch": epoch, + "proposer": proposer, + "total_weight": total_weight, + "validator_rewards": reward_list, + "sample_validators": sample_validators, + } + + +def _handle_info(state): + pvm_host.charge_gas(GAS_READ) + return state, { + "action": "info", + "epoch": state["epoch"], + "inflation": state["inflation"], + "validators": state["validators"], + "delegations": state["delegations"], + "rewards": state["rewards"], + "last_proposer": state["last_proposer"], + } + + +def _apply_action(state, action, params, ctx): + if action == "init": + return _handle_init(state, params) + if state is None: + raise ValueError("not_initialized") + if action == "register_validator": + return _handle_register(state, params) + if action == "delegate": + return _handle_delegate(state, params) + if action == "undelegate": + return _handle_undelegate(state, params) + if action == "distribute": + return _handle_distribute(state, ctx) + if action == "info": + return _handle_info(state) + raise ValueError(f"unknown_action: {action}") + + +def _run_actions(state, actions, ctx): + results = [] + for step in actions: + if not isinstance(step, dict): + raise ValueError("action entry must be object") + action = step.get("action") + params = step.get("params", {}) + if params is None: + params = {} + if not isinstance(params, dict): + raise ValueError("params must be object") + state, summary = _apply_action(state, action, params, ctx) + _save_state(state) + results.append(summary) + return state, results + + +def main(input_bytes): + pvm_host.charge_gas(GAS_BASE) + if not input_bytes: + return _ok( + { + "message": "staking rewards demo", + "actions": [ + "init", + "register_validator", + "delegate", + "undelegate", + "distribute", + "info", + ], + "hint": "pass 'demo' or a JSON object", + } + ) + + try: + text = input_bytes.decode("utf-8") + except Exception as exc: + return _err("invalid_input", str(exc)) + + ctx = pvm_host.context() + + if text.strip().lower() == "demo": + actions = _demo_actions() + try: + state = _load_state() + state, results = _run_actions(state, actions, ctx) + return _ok({"action": "batch", "results": results}, state) + except Exception as exc: + return _err("invalid_input", str(exc)) + + try: + request = _json_loads(input_bytes) + except Exception as exc: + return _err("invalid_json", str(exc)) + + if not isinstance(request, dict): + return _err("invalid_input", "input must be object") + + if "actions" in request: + actions = request.get("actions") + if not isinstance(actions, list): + return _err("invalid_input", "actions must be list") + try: + state = _load_state() + state, results = _run_actions(state, actions, ctx) + return _ok({"action": "batch", "results": results}, state) + except Exception as exc: + return _err("invalid_input", str(exc)) + + action = request.get("action") + params = request.get("params", {}) + if params is None: + params = {} + if not isinstance(params, dict): + return _err("invalid_input", "params must be object") + + try: + state = _load_state() + state, summary = _apply_action(state, action, params, ctx) + _save_state(state) + return _ok(summary, state) + except Exception as exc: + return _err("invalid_input", str(exc)) diff --git a/examples/pvm_runtime_chain_demo_contract.py b/examples/pvm_runtime_chain_demo_contract.py new file mode 100644 index 00000000000..184812b13bd --- /dev/null +++ b/examples/pvm_runtime_chain_demo_contract.py @@ -0,0 +1,18 @@ +import pvm_host + + +def main(input_bytes: bytes) -> bytes: + ctx = pvm_host.context() + pvm_host.charge_gas(10) + + current = pvm_host.get_state(b"counter") + if current is None: + counter = 1 + else: + counter = int.from_bytes(current, "little") + 1 + pvm_host.set_state(b"counter", counter.to_bytes(8, "little")) + + pvm_host.emit_event("demo", b"ok") + payload = input_bytes if input_bytes else b"empty" + return b"ok:" + payload + b":h=" + str(ctx["block_height"]).encode("ascii") + diff --git a/src/lib.rs b/src/lib.rs index 8ee22d4eb5a..6505235edef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -237,7 +237,11 @@ fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<()> { RunMode::Script(script_path) => { // pymain_run_file debug!("Running script {}", &script_path); - vm.run_script(scope.clone(), &script_path) + if let Some(resume_path) = vm.state.config.settings.resume_path.as_deref() { + vm.run_script_resume(scope.clone(), &script_path, resume_path) + } else { + vm.run_script(scope.clone(), &script_path) + } } RunMode::Repl => Ok(()), }; diff --git a/src/settings.rs b/src/settings.rs index a63f1a07ccc..ceffd2ee163 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,6 +1,6 @@ use lexopt::Arg::*; use lexopt::ValueExt; -use rustpython_vm::{Settings, vm::CheckHashPycsMode}; +use rustpython_vm::{Settings, vm::{CheckHashPycsMode, ContinuationMode}}; use std::str::FromStr; use std::{cmp, env}; @@ -52,6 +52,8 @@ struct CliArgs { warning_control: Vec, implementation_option: Vec, check_hash_based_pycs: CheckHashPycsMode, + resume_path: Option, + continuation_mode: Option, #[cfg(feature = "flame-it")] profile_output: Option, @@ -100,6 +102,8 @@ Options (and corresponding environment variables): --help-all: print complete help information and exit RustPython extensions: +--resume path : resume execution from a checkpoint file +--continuation mode : continuation mode (fsm or checkpoint) Arguments: @@ -150,6 +154,13 @@ fn parse_args() -> Result<(CliArgs, RunMode, Vec), lexopt::Error> { Long("check-hash-based-pycs") => { args.check_hash_based_pycs = parser.value()?.parse()? } + Long("resume") => { + args.resume_path = Some(parser.value()?.string()?); + } + Long("continuation") => { + let mode: ContinuationMode = parser.value()?.parse()?; + args.continuation_mode = Some(mode); + } // TODO: make these more specific Long("help-env") => help(parser), @@ -326,6 +337,8 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { }; settings.argv = argv; + settings.resume_path = args.resume_path; + settings.continuation_mode = args.continuation_mode; #[cfg(feature = "flame-it")] {