diff --git a/archive/LLVMParty/smt-server.txt b/archive/LLVMParty/smt-server.txt new file mode 100644 index 00000000..def8ab02 --- /dev/null +++ b/archive/LLVMParty/smt-server.txt @@ -0,0 +1,26338 @@ +Project Path: arc_LLVMParty_smt-server_9ytqiorl + +Source Tree: + +```txt +arc_LLVMParty_smt-server_9ytqiorl +├── Cargo.lock +├── Cargo.toml +├── README.md +├── clients +│ ├── cpp +│ │ └── smt_wire.hpp +│ ├── python +│ │ └── smt_wire.py +│ └── tests +│ ├── cpp_client_smoke.cpp +│ ├── cpp_live_client.cpp +│ ├── test_live_server.py +│ └── test_python_client.py +├── crates +│ ├── qfbvsmtrs +│ │ ├── Cargo.toml +│ │ ├── fuzz +│ │ │ ├── Cargo.lock +│ │ │ ├── Cargo.toml +│ │ │ └── fuzz_targets +│ │ │ └── smt2_pipeline.rs +│ │ ├── src +│ │ │ ├── bin +│ │ │ │ ├── qfbvsmtrs.rs +│ │ │ │ ├── qfbvsmtrs_bench.rs +│ │ │ │ └── qfbvsmtrs_stats.rs +│ │ │ ├── blast.rs +│ │ │ ├── builder.rs +│ │ │ ├── circuits.rs +│ │ │ ├── cnf.rs +│ │ │ ├── config.rs +│ │ │ ├── error.rs +│ │ │ ├── eval.rs +│ │ │ ├── frontend.rs +│ │ │ ├── gates.rs +│ │ │ ├── ir.rs +│ │ │ ├── lib.rs +│ │ │ ├── model.rs +│ │ │ ├── query.rs +│ │ │ ├── sat.rs +│ │ │ ├── simplify.rs +│ │ │ ├── solver.rs +│ │ │ └── wire.rs +│ │ └── tests +│ │ ├── circuits.rs +│ │ ├── differential_z3.rs +│ │ ├── fixtures +│ │ │ └── known +│ │ │ ├── add_sat.smt2 +│ │ │ ├── division_by_zero.smt2 +│ │ │ ├── overflow_ops.smt2 +│ │ │ ├── signed_ops.smt2 +│ │ │ ├── simple_unsat.smt2 +│ │ │ ├── structural_ops.smt2 +│ │ │ └── width_one.smt2 +│ │ ├── known_answer.rs +│ │ ├── random_circuits.rs +│ │ ├── standalone.rs +│ │ └── tseitin.rs +│ ├── smt-server +│ │ ├── Cargo.toml +│ │ ├── src +│ │ │ ├── backend.rs +│ │ │ ├── binbit_backend.rs +│ │ │ ├── cache.rs +│ │ │ ├── lib.rs +│ │ │ ├── main.rs +│ │ │ ├── pool.rs +│ │ │ ├── protocol.rs +│ │ │ ├── qfbvsmtrs_backend.rs +│ │ │ ├── racing.rs +│ │ │ ├── server.rs +│ │ │ ├── smt2.rs +│ │ │ ├── smtlib.rs +│ │ │ └── z3_backend.rs +│ │ └── tests +│ │ ├── phase2_server.rs +│ │ ├── phase4_5_text_and_racing.rs +│ │ ├── phase6_simplify_optimize.rs +│ │ ├── phase7_cache.rs +│ │ └── phase8_e2e.rs +│ └── smt-wire +│ ├── Cargo.toml +│ ├── src +│ │ ├── builder.rs +│ │ ├── client.rs +│ │ ├── constants.rs +│ │ ├── error.rs +│ │ ├── expr.rs +│ │ ├── le.rs +│ │ ├── lib.rs +│ │ ├── request.rs +│ │ ├── response.rs +│ │ └── types.rs +│ └── tests +│ ├── phase0_golden.rs +│ └── phase1_codecs.rs +├── docs +│ ├── backend-provisioning.md +│ ├── qf_bv_solver_plan.md +│ ├── qfbvsmtrs-corpus-results.md +│ ├── qfbvsmtrs-design-report.md +│ ├── qfbvsmtrs-production.md +│ ├── qfbvsmtrs-progress-report.md +│ ├── second-backend-evaluation.md +│ └── smt-wire-format-plan.md +└── scripts + └── qfbvsmtrs_corpus.py + +``` + +`Cargo.lock`: + +```lock +# 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 = "aes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "binbit" +version = "0.1.0" +source = "git+https://github.com/bint-disasm/binbit#b4996145c06f39ce5bbc79a959975d7d20b6cf5e" +dependencies = [ + "mimalloc", + "rustc-hash 2.1.2", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[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-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", + "zeroize", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[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 = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[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 = "cipher" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +dependencies = [ + "crypto-common 0.2.1", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[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 = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "dashu" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b3e5ac1e23ff1995ef05b912e2b012a8784506987a2651552db2c73fb3d7e0" +dependencies = [ + "dashu-base", + "dashu-float", + "dashu-int", + "dashu-macros", + "dashu-ratio", + "rustversion", +] + +[[package]] +name = "dashu-base" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b80bf6b85aa68c58ffea2ddb040109943049ce3fbdf4385d0380aef08ef289" + +[[package]] +name = "dashu-float" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85078445a8dbd2e1bd21f04a816f352db8d333643f0c9b78ca7c3d1df71063e7" +dependencies = [ + "dashu-base", + "dashu-int", + "num-modular", + "num-order", + "num-traits", + "rustversion", + "static_assertions", +] + +[[package]] +name = "dashu-int" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee99d08031ca34a4d044efbbb21dff9b8c54bb9d8c82a189187c0651ffdb9fbf" +dependencies = [ + "cfg-if", + "dashu-base", + "num-modular", + "num-order", + "num-traits", + "rustversion", + "static_assertions", +] + +[[package]] +name = "dashu-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93381c3ef6366766f6e9ed9cf09e4ef9dec69499baf04f0c60e70d653cf0ab10" +dependencies = [ + "dashu-base", + "dashu-float", + "dashu-int", + "dashu-ratio", + "paste", + "proc-macro2", + "quote", + "rustversion", +] + +[[package]] +name = "dashu-ratio" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e33b04dd7ce1ccf8a02a69d3419e354f2bbfdf4eb911a0b7465487248764c9" +dependencies = [ + "dashu-base", + "dashu-float", + "dashu-int", + "num-modular", + "num-order", + "num-traits", + "rustversion", +] + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", + "ctutils", + "zeroize", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[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 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa 1.0.18", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa 1.0.18", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[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.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +dependencies = [ + "regex-automata", + "rustversion", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a6a8c165077efc8f3a971534c50ea6a1a18b329ef4a66e897a7e3a1494565f" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libmimalloc-sys" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6" +dependencies = [ + "cc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[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 = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lzma-rust2" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" +dependencies = [ + "sha2", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mimalloc" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640" +dependencies = [ + "libmimalloc-sys", +] + +[[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 = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[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", + "smallvec", + "windows-link", +] + +[[package]] +name = "partial_ref" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f728bc9b1479656e40cba507034904a8c44027c0efdbbaf6a4bdc5f2d3a910c" +dependencies = [ + "partial_ref_derive", +] + +[[package]] +name = "partial_ref_derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e1d2cb5b898b5a5342e994e0d0c367dbfe69cbf717cd307045ec9fb057581" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +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_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", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppmd-rust" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qfbvsmtrs" +version = "0.1.0" +dependencies = [ + "dashu", + "smt-wire", + "splr", + "varisat", + "yaspar", + "z3", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.2", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash 2.1.2", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[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.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[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_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 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa 1.0.18", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.18", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smt-server" +version = "0.1.0" +dependencies = [ + "binbit", + "dashu", + "qfbvsmtrs", + "smt-wire", + "yaspar", + "z3", +] + +[[package]] +name = "smt-wire" +version = "0.1.0" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "splr" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c8d72268886573a95520ff76c282876c69d3776e02a0b4ac8eea2b310d69ca" +dependencies = [ + "bitflags", +] + +[[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 = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "term" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[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 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "js-sys", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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 = "tokio" +version = "1.52.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "varisat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe609851d1e9196674ac295f656bd8601200a1077343d22b345013497807caf" +dependencies = [ + "anyhow", + "itoa 0.4.8", + "leb128", + "log", + "ordered-float", + "partial_ref", + "rustc-hash 1.1.0", + "serde", + "thiserror 1.0.69", + "varisat-checker", + "varisat-dimacs", + "varisat-formula", + "varisat-internal-macros", + "varisat-internal-proof", + "vec_mut_scan", +] + +[[package]] +name = "varisat-checker" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135c977c5913ed6e98f6b81b8e4d322211303b7d40dae773caef7ad1de6c763b" +dependencies = [ + "anyhow", + "log", + "partial_ref", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "varisat-dimacs", + "varisat-formula", + "varisat-internal-proof", +] + +[[package]] +name = "varisat-dimacs" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1dee4e21be1f04c0a939f7ae710cced47233a578de08a1b3c7d50848402636" +dependencies = [ + "anyhow", + "itoa 0.4.8", + "thiserror 1.0.69", + "varisat-formula", +] + +[[package]] +name = "varisat-formula" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395c5543b9bfd9076d6d3af49d6c34a4b91b0b355998c0a5ec6ed7265d364520" + +[[package]] +name = "varisat-internal-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602ece773543d066aa7848455486c6c0422a3f214da7a2b899100f3c4f12408d" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "varisat-internal-proof" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6163bb7bc9018af077b76d64f976803d141c36a27d640f1437dddc4fd527d207" +dependencies = [ + "anyhow", + "varisat-formula", +] + +[[package]] +name = "vec_mut_scan" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ed610a8d5e63d9c0e31300e8fdb55104c5f21e422743a9dc74848fa8317fd2" + +[[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 = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[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.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[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 = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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.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.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.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.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.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.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.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.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.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 = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yaspar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be170ae8a3dc3c6f62366d4fb8a4ceef65289f927a17d26dbfa13849d92c91c" +dependencies = [ + "clap", + "dashu", + "lalrpop", + "lalrpop-util", + "lazy_static", + "num-traits", + "phf", + "serde", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure 0.13.2", +] + +[[package]] +name = "z3" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9fc44c9d6bb9fe84c03dfff211cf4c9c8cfefa2de8b803facf7305067d21a23" +dependencies = [ + "log", + "z3-sys", +] + +[[package]] +name = "z3-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c18b0a91a13522d21b3414847667de2b2056a721a3edcb5b6ee6858352d58db4" +dependencies = [ + "pkg-config", + "reqwest", + "serde_json", + "zip", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure 0.13.2", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "aes", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.4.2", + "hmac", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "typed-path", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + +``` + +`Cargo.toml`: + +```toml +[workspace] +members = ["crates/smt-wire", "crates/smt-server", "crates/qfbvsmtrs"] +resolver = "2" + +``` + +`README.md`: + +```md +# SMT Server + +A small SMT solving server and wire-format toolkit for bit-vector and Boolean formulas. + +The project provides: + +- a TCP server that accepts either the project binary wire format or SMT-LIB text frames +- native solver backends using the Rust `z3` crate, `binbit`, and the standalone `qfbvsmtrs` crate, raced by default +- a Rust wire-format crate (`smt-wire`) with a blocking TCP client +- single-file Python and C++ client helpers for building requests, sending them, and decoding responses + +The supported logic is intentionally focused on quantifier-free bit-vectors and Booleans (`QF_BV`). +SMT-LIB input is a compatibility frontend: it is parsed, lowered into the internal wire IR, and then sent to the configured backends. + +## Build + +```sh +cargo build --workspace +``` + +## Run the server + +```sh +cargo run -p smt-server -- 127.0.0.1:9123 +``` + +If no address is provided, the server listens on `127.0.0.1:9123`. + +Requests are sent as length-prefixed frames: + +```text +u32 little-endian payload length +payload bytes +``` + +The payload may be either: + +- a binary `SMTQ` request produced by `smt-wire` or one of the clients +- an SMT-LIB script as UTF-8 text + +## Python client example + +The Python client is a dependency-free single file at `clients/python/smt_wire.py`. +You can copy it into your project or add `clients/python` to `PYTHONPATH`. + +```python +import smt_wire as smt + +b = smt.Builder() +x = b.bv_var("x", 8) +b.assert_(b.bv_eq(x, b.bv_const(42, 8))) +request = b.build_solve_request(1, want_model=True) + +with smt.TcpClient("127.0.0.1", 9123) as client: + response = client.send_request(request) + +print(response.status) # smt.SAT +print(response.model()[0]) +``` + +## SMT-LIB text example + +You can also send an SMT-LIB script as the frame payload: + +```smt2 +(set-logic QF_BV) +(declare-const x (_ BitVec 8)) +(assert (= x #x2a)) +(check-sat) +(get-value (x)) +``` + +The text frontend supports the project’s `QF_BV`/Bool subset, including declarations, assertions, named assertions, `check-sat`, `check-sat-assuming`, `get-model`, `get-value`, `get-unsat-core`, `let`, and common bit-vector operations. It does not provide a stateful incremental SMT-LIB session; commands such as `push` and `pop` are rejected. + +From Python, send text with the same client: + +```python +with smt.TcpClient("127.0.0.1", 9123) as client: + print(client.send_text(script)) +``` + +## Rust client + +```rust +use smt_wire::{ExprBuilder, TcpClient}; + +let mut b = ExprBuilder::new(); +let x = b.bv_var("x", 8).unwrap(); +let c = b.bv_const(42, 8).unwrap(); +let eq = b.bv_eq(x, c).unwrap(); +b.assert(eq).unwrap(); +let request = b.build_solve_request(1, 0, true, false).unwrap(); + +let mut client = TcpClient::connect("127.0.0.1:9123").unwrap(); +let response = client.send_binary_request(&request).unwrap(); +println!("{:?}", response.envelope.status); +``` + +## C++ client + +The C++ client helper is a dependency-free C++17 header: + +```cpp +#include "smt_wire.hpp" + +smt_wire::Builder b; +auto x = b.bv_var("x", 8); +b.assert_(b.bv_eq(x, b.bv_const(42, 8))); +auto request = b.build_solve_request(1, 0, true, false); + +smt_wire::TcpClient client("127.0.0.1", 9123); +auto response = client.send_request(request); +``` + +On Windows/MSVC the header requests `Ws2_32.lib` automatically. With MinGW, link with `-lws2_32` if you use `TcpClient`. + +## Standalone qfbvsmtrs CLI + +```sh +cargo run -p qfbvsmtrs -- path/to/query.smt2 +``` + +If no path is supplied, `qfbvsmtrs` reads SMT-LIB from stdin. The default SAT backend is `splr` (CDCL); `varisat` and the internal DPLL solver remain selectable through `Config::with_sat_backend`. + +A lightweight benchmark runner is available for baseline timings: + +```sh +cargo run -p qfbvsmtrs --bin qfbvsmtrs_bench -- path/to/query.smt2 +``` + +## Test + +```sh +cargo test --workspace +cargo clippy --workspace --all-targets -- -D warnings +cargo check --manifest-path crates/qfbvsmtrs/fuzz/Cargo.toml +python clients/tests/test_python_client.py +python clients/tests/test_live_server.py # also exercises the live C++ TCP client when a compiler is available +``` + +Optional production-validation gates for `qfbvsmtrs`: + +```sh +cargo test -p qfbvsmtrs --test differential_z3 +QFBVSMTRS_RANDOM_CIRCUIT_SAMPLES=10000 cargo test -p qfbvsmtrs --test random_circuits +QFBVSMTRS_DIFF_RANDOM=1 cargo test -p qfbvsmtrs --test differential_z3 +QFBVSMTRS_SMTLIB_DIR=/path/to/SMT-LIB/QF_BV cargo test -p qfbvsmtrs --test differential_z3 +cargo fuzz run smt2_pipeline --manifest-path crates/qfbvsmtrs/fuzz/Cargo.toml +``` + +A standalone C++ smoke test is also available: + +```sh +c++ -std=c++17 -Wall -Wextra -Werror clients/tests/cpp_client_smoke.cpp -o cpp_client_smoke +./cpp_client_smoke +``` + +## Repository layout + +- `crates/smt-wire` — Rust wire-format types, builders, codecs, and validators +- `crates/qfbvsmtrs` — standalone pure-Rust QF_BV bit-blasting solver crate and CLI +- `crates/smt-server` — TCP server, backend integration, SMT-LIB frontend +- `clients/python` — Python single-file client helper +- `clients/cpp` — C++17 single-header client helper +- `docs/smt-wire-format-plan.md` — detailed binary wire-format plan +- `docs/qfbvsmtrs-production.md` — qfbvsmtrs production validation gates +- `docs/qfbvsmtrs-corpus-results.md` — latest SMT-LIB QF_BV corpus-run results +- `docs` — backend notes and evaluation details + +``` + +`clients/cpp/smt_wire.hpp`: + +```hpp +#pragma once +// Single-file C++17 builder for the SMT v1 wire format. +// No networking and no dependencies beyond the C++ standard library. + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#ifdef _MSC_VER +#pragma comment(lib, "Ws2_32.lib") +#endif +#else +#include +#include +#include +#include +#include +#endif + +#ifdef ERROR +#undef ERROR +#endif + +namespace smt_wire { + +constexpr uint32_t BOOL_BIT = 0x80000000u; +constexpr uint32_t INDEX_MASK = 0x7fffffffu; +constexpr uint32_t MAX_WIDTH = 65536u; + +namespace tag { +constexpr uint8_t BV_VAR = 0, BV_CONST = 1, BV_NOT = 2, BV_NEG = 3, BV_AND = 4, BV_OR = 5; +constexpr uint8_t BV_XOR = 6, BV_ADD = 7, BV_SUB = 8, BV_MUL = 9, BV_UDIV = 10, BV_UREM = 11; +constexpr uint8_t BV_SDIV = 12, BV_SREM = 13, BV_SMOD = 14, BV_SHL = 15, BV_LSHR = 16, BV_ASHR = 17; +constexpr uint8_t BV_EXTRACT = 18, BV_CONCAT = 19, BV_ZEXT = 20, BV_SEXT = 21, BV_ITE = 22, BV_SELECT = 23; +constexpr uint8_t BOOL_TRUE = 24, BOOL_FALSE = 25, BOOL_VAR = 26, BOOL_NOT = 27, BOOL_AND = 28, BOOL_OR = 29, BOOL_IMPLIES = 30; +constexpr uint8_t BV_EQ = 31, BV_ULT = 32, BV_ULE = 33, BV_SLT = 34, BV_SLE = 35; +constexpr uint8_t UADD_OVF = 36, SADD_OVF = 37, USUB_OVF = 38, SSUB_OVF = 39, UMUL_OVF = 40, SMUL_OVF = 41, NEG_OVF = 42, SDIV_OVF = 43; +} // namespace tag + +namespace command { constexpr uint8_t SOLVE = 0, SIMPLIFY = 1, MINIMIZE = 2, MAXIMIZE = 3; } +namespace request_flags { constexpr uint8_t WANT_MODEL = 1u << 0, WANT_CORE = 1u << 1, SIGNED = 1u << 2; } +namespace status { constexpr uint8_t OK = 0, SAT = 1, UNSAT = 2, UNKNOWN = 3, ERROR = 4; } +namespace response_flags { constexpr uint8_t HAS_MODEL = 1u << 0, HAS_CORE = 1u << 1, HAS_EXPR = 1u << 2, HAS_VALUE = 1u << 3, HAS_MESSAGE = 1u << 4; } + +inline uint32_t bv_ref(uint32_t index) { if (index > INDEX_MASK) throw std::invalid_argument("node index out of range"); return index; } +inline uint32_t bool_ref(uint32_t index) { if (index > INDEX_MASK) throw std::invalid_argument("node index out of range"); return BOOL_BIT | index; } +inline uint32_t ref_index(uint32_t ref) { return ref & INDEX_MASK; } +inline bool is_bool_ref(uint32_t ref) { return (ref & BOOL_BIT) != 0; } +inline uint64_t blob_payload(uint32_t offset, uint32_t len) { return (static_cast(offset) << 32) | len; } +inline uint32_t bytes_for_width(uint32_t width) { if (width == 0 || width > MAX_WIDTH) throw std::invalid_argument("invalid width"); return (width + 7) / 8; } + +inline void u8(std::vector& out, uint8_t v) { out.push_back(v); } +inline void u16(std::vector& out, uint16_t v) { out.push_back(uint8_t(v)); out.push_back(uint8_t(v >> 8)); } +inline void u32(std::vector& out, uint32_t v) { for (int i = 0; i < 4; ++i) out.push_back(uint8_t(v >> (8 * i))); } +inline void u64(std::vector& out, uint64_t v) { for (int i = 0; i < 8; ++i) out.push_back(uint8_t(v >> (8 * i))); } + +struct Node { uint8_t tag, arity; uint16_t aux_hi; uint32_t width, aux_lo, children; uint64_t payload; }; +struct Assertion { uint32_t root; bool named; uint32_t name_offset, name_len; }; +struct Meta { bool is_bool; uint32_t width; }; +struct ScalarValue { uint32_t width; std::vector bytes; }; +struct ModelEntry { uint32_t node_ref; ScalarValue value; }; +struct SimplifyResult { std::vector expression; std::vector assertion_roots; std::vector> named_assertion_refs; std::vector assumption_roots; }; +struct OptimizationResult { ScalarValue optimum; bool has_model; std::vector model; }; +struct Response { uint32_t request_id; uint8_t status, flags; std::vector payload; }; + +class Builder { +public: + std::vector nodes; + std::vector children; + std::vector blob; + std::vector meta; + std::vector assertions; + std::vector assumptions; + std::vector scopes; + + void reset() { *this = Builder(); } + void push() { scopes.push_back(assertions.size()); } + void pop() { if (scopes.empty()) throw std::invalid_argument("pop without push"); assertions.resize(scopes.back()); scopes.pop_back(); } + + uint32_t bv_var(const std::string& name, uint32_t width) { check_width(width); auto b = add_blob(name); return add(tag::BV_VAR, width, {}, 0, 0, blob_payload(b.first, b.second)); } + uint32_t bool_var(const std::string& name) { auto b = add_blob(name); return add(tag::BOOL_VAR, 0, {}, 0, 0, blob_payload(b.first, b.second)); } + uint32_t bool_true() { return add(tag::BOOL_TRUE, 0); } + uint32_t bool_false() { return add(tag::BOOL_FALSE, 0); } + + uint32_t bv_const(uint64_t value, uint32_t width) { + check_width(width); + if (width <= 64) { + if (width < 64) value &= ((uint64_t(1) << width) - 1); + return add(tag::BV_CONST, width, {}, 0, 0, value); + } + std::vector bytes(bytes_for_width(width), 0); + for (size_t i = 0; i < 8; ++i) bytes[i] = uint8_t(value >> (8 * i)); + return bv_const_wide(bytes, width); + } + uint32_t bv_const_wide(const std::vector& bytes, uint32_t width) { + if (bytes.size() != bytes_for_width(width)) throw std::invalid_argument("wide constant length mismatch"); + std::vector normalized = bytes; + if (width % 8 != 0 && !normalized.empty()) normalized.back() &= uint8_t((1u << (width % 8)) - 1u); + if (width <= 64) { uint64_t v = 0; for (size_t i = 0; i < normalized.size(); ++i) v |= uint64_t(normalized[i]) << (8 * i); return bv_const(v, width); } + auto b = add_blob(normalized); return add(tag::BV_CONST, width, {}, 0, 0, blob_payload(b.first, b.second)); + } + + uint32_t bv_not(uint32_t x) { return bv_unary(tag::BV_NOT, x); } + uint32_t bv_neg(uint32_t x) { return bv_unary(tag::BV_NEG, x); } + uint32_t bv_and(uint32_t a, uint32_t b) { return bv_binary(tag::BV_AND, a, b); } + uint32_t bv_or(uint32_t a, uint32_t b) { return bv_binary(tag::BV_OR, a, b); } + uint32_t bv_xor(uint32_t a, uint32_t b) { return bv_binary(tag::BV_XOR, a, b); } + uint32_t bv_add(uint32_t a, uint32_t b) { return bv_binary(tag::BV_ADD, a, b); } + uint32_t bv_sub(uint32_t a, uint32_t b) { return bv_binary(tag::BV_SUB, a, b); } + uint32_t bv_mul(uint32_t a, uint32_t b) { return bv_binary(tag::BV_MUL, a, b); } + uint32_t bv_udiv(uint32_t a, uint32_t b) { return bv_binary(tag::BV_UDIV, a, b); } + uint32_t bv_urem(uint32_t a, uint32_t b) { return bv_binary(tag::BV_UREM, a, b); } + uint32_t bv_sdiv(uint32_t a, uint32_t b) { return bv_binary(tag::BV_SDIV, a, b); } + uint32_t bv_srem(uint32_t a, uint32_t b) { return bv_binary(tag::BV_SREM, a, b); } + uint32_t bv_smod(uint32_t a, uint32_t b) { return bv_binary(tag::BV_SMOD, a, b); } + uint32_t bv_shl(uint32_t a, uint32_t b) { return bv_binary(tag::BV_SHL, a, b); } + uint32_t bv_lshr(uint32_t a, uint32_t b) { return bv_binary(tag::BV_LSHR, a, b); } + uint32_t bv_ashr(uint32_t a, uint32_t b) { return bv_binary(tag::BV_ASHR, a, b); } + + uint32_t bv_extract(uint32_t x, uint32_t hi, uint32_t lo) { uint32_t w = expect_bv(x); if (lo > hi || hi >= w || hi > 0xffff) throw std::invalid_argument("bad extract"); return add(tag::BV_EXTRACT, hi - lo + 1, {x}, uint16_t(hi), lo); } + uint32_t bv_concat(uint32_t a, uint32_t b) { uint32_t w = expect_bv(a) + expect_bv(b); check_width(w); return add(tag::BV_CONCAT, w, {a, b}); } + uint32_t bv_zext(uint32_t x, uint16_t amount) { return bv_ext(tag::BV_ZEXT, x, amount); } + uint32_t bv_sext(uint32_t x, uint16_t amount) { return bv_ext(tag::BV_SEXT, x, amount); } + uint32_t bv_ite(uint32_t c, uint32_t t, uint32_t e) { expect_bool(c); return add(tag::BV_ITE, same_bv(t, e), {c, t, e}); } + uint32_t bv_select(const std::vector& selectors, const std::vector& values, uint32_t def) { if (selectors.size() != values.size() || selectors.size() > 127) throw std::invalid_argument("bad BV_SELECT pair count"); uint32_t w = expect_bv(def); std::vector ch; for (size_t i = 0; i < selectors.size(); ++i) { expect_bool(selectors[i]); if (expect_bv(values[i]) != w) throw std::invalid_argument("BV_SELECT width mismatch"); ch.push_back(selectors[i]); ch.push_back(values[i]); } ch.push_back(def); return add(tag::BV_SELECT, w, ch, uint16_t(selectors.size())); } + + uint32_t bool_not(uint32_t x) { expect_bool(x); return add(tag::BOOL_NOT, 0, {x}); } + uint32_t bool_and(uint32_t a, uint32_t b) { return bool_binary(tag::BOOL_AND, a, b); } + uint32_t bool_or(uint32_t a, uint32_t b) { return bool_binary(tag::BOOL_OR, a, b); } + uint32_t bool_implies(uint32_t a, uint32_t b) { return bool_binary(tag::BOOL_IMPLIES, a, b); } + + uint32_t bv_eq(uint32_t a, uint32_t b) { return bv_cmp(tag::BV_EQ, a, b); } + uint32_t bv_ult(uint32_t a, uint32_t b) { return bv_cmp(tag::BV_ULT, a, b); } + uint32_t bv_ule(uint32_t a, uint32_t b) { return bv_cmp(tag::BV_ULE, a, b); } + uint32_t bv_slt(uint32_t a, uint32_t b) { return bv_cmp(tag::BV_SLT, a, b); } + uint32_t bv_sle(uint32_t a, uint32_t b) { return bv_cmp(tag::BV_SLE, a, b); } + uint32_t bv_ne(uint32_t a, uint32_t b) { return bool_not(bv_eq(a, b)); } + uint32_t bv_ugt(uint32_t a, uint32_t b) { return bv_ult(b, a); } + uint32_t bv_uge(uint32_t a, uint32_t b) { return bv_ule(b, a); } + uint32_t bv_sgt(uint32_t a, uint32_t b) { return bv_slt(b, a); } + uint32_t bv_sge(uint32_t a, uint32_t b) { return bv_sle(b, a); } + uint32_t uadd_ovf(uint32_t a, uint32_t b) { return bv_cmp(tag::UADD_OVF, a, b); } + uint32_t sadd_ovf(uint32_t a, uint32_t b) { return bv_cmp(tag::SADD_OVF, a, b); } + uint32_t usub_ovf(uint32_t a, uint32_t b) { return bv_cmp(tag::USUB_OVF, a, b); } + uint32_t ssub_ovf(uint32_t a, uint32_t b) { return bv_cmp(tag::SSUB_OVF, a, b); } + uint32_t umul_ovf(uint32_t a, uint32_t b) { return bv_cmp(tag::UMUL_OVF, a, b); } + uint32_t smul_ovf(uint32_t a, uint32_t b) { return bv_cmp(tag::SMUL_OVF, a, b); } + uint32_t neg_ovf(uint32_t x) { expect_bv(x); return add(tag::NEG_OVF, 0, {x}); } + uint32_t sdiv_ovf(uint32_t a, uint32_t b) { return bv_cmp(tag::SDIV_OVF, a, b); } + uint32_t bool_eq(uint32_t a, uint32_t b) { expect_bool(a); expect_bool(b); return bool_and(bool_or(a, bool_not(b)), bool_or(bool_not(a), b)); } + uint32_t bool_xor(uint32_t a, uint32_t b) { return bool_not(bool_eq(a, b)); } + uint32_t bool_ite(uint32_t c, uint32_t t, uint32_t e) { expect_bool(c); expect_bool(t); expect_bool(e); return bool_or(bool_and(c, t), bool_and(bool_not(c), e)); } + uint32_t bv_rotate_left(uint32_t x, uint64_t amount) { uint32_t w = expect_bv(x); amount %= w; if (amount == 0) return x; return bv_or(bv_shl(x, bv_const(amount, w)), bv_lshr(x, bv_const(uint64_t(w) - amount, w))); } + uint32_t bv_rotate_right(uint32_t x, uint64_t amount) { uint32_t w = expect_bv(x); amount %= w; if (amount == 0) return x; return bv_or(bv_lshr(x, bv_const(amount, w)), bv_shl(x, bv_const(uint64_t(w) - amount, w))); } + + void assert_(uint32_t root) { expect_bool(root); assertions.push_back({root, false, 0, 0}); } + void assert_named(const std::string& name, uint32_t root) { expect_bool(root); auto b = add_blob(name); assertions.push_back({root, true, b.first, b.second}); } + void assume(uint32_t root) { expect_bool(root); assumptions.push_back(root); } + void clear_assumptions() { assumptions.clear(); } + void assert_mutex(const std::vector& sels) { for (auto s: sels) expect_bool(s); for (size_t i = 0; i < sels.size(); ++i) for (size_t j = i + 1; j < sels.size(); ++j) assert_(bool_not(bool_and(sels[i], sels[j]))); } + + std::vector to_bytes() const { return expr_bytes(nodes, children, blob); } + std::vector build_solve_request(uint32_t request_id, uint32_t budget_ms = 0, bool want_model = false, bool want_core = false) const { return build_request(request_id, command::SOLVE, (want_model ? request_flags::WANT_MODEL : 0) | (want_core ? request_flags::WANT_CORE : 0), budget_ms, 0, false); } + std::vector build_simplify_request(uint32_t request_id) const { return build_request(request_id, command::SIMPLIFY, 0, 0, 0, false); } + std::vector build_minimize_request(uint32_t request_id, uint32_t target, bool signed_order = false, uint32_t budget_ms = 0, bool want_model = false) const { expect_bv(target); return build_request(request_id, command::MINIMIZE, (signed_order ? request_flags::SIGNED : 0) | (want_model ? request_flags::WANT_MODEL : 0), budget_ms, target, true); } + std::vector build_maximize_request(uint32_t request_id, uint32_t target, bool signed_order = false, uint32_t budget_ms = 0, bool want_model = false) const { expect_bv(target); return build_request(request_id, command::MAXIMIZE, (signed_order ? request_flags::SIGNED : 0) | (want_model ? request_flags::WANT_MODEL : 0), budget_ms, target, true); } + +private: + static void check_width(uint32_t w) { if (w == 0 || w > MAX_WIDTH) throw std::invalid_argument("invalid BV width"); } + bool tag_is_bool(uint8_t t) const { return t >= tag::BOOL_TRUE; } + std::pair add_blob(const std::string& s) { return add_blob(std::vector(s.begin(), s.end())); } + std::pair add_blob(const std::vector& data) { uint32_t off = uint32_t(blob.size()); blob.insert(blob.end(), data.begin(), data.end()); return {off, uint32_t(data.size())}; } + uint32_t add(uint8_t t, uint32_t width, std::vector child_refs = {}, uint16_t aux_hi = 0, uint32_t aux_lo = 0, uint64_t payload = 0) { for (auto c : child_refs) meta_for(c); uint32_t start = child_refs.empty() ? 0 : uint32_t(children.size()); children.insert(children.end(), child_refs.begin(), child_refs.end()); uint32_t idx = uint32_t(nodes.size()); nodes.push_back({t, uint8_t(child_refs.size()), aux_hi, width, aux_lo, start, payload}); bool ib = tag_is_bool(t); meta.push_back({ib, width}); return ib ? bool_ref(idx) : bv_ref(idx); } + Meta meta_for(uint32_t ref) const { uint32_t idx = ref_index(ref); if (idx >= meta.size()) throw std::invalid_argument("bad node ref"); Meta m = meta[idx]; if (m.is_bool != is_bool_ref(ref)) throw std::invalid_argument("sort bit mismatch"); return m; } + uint32_t expect_bv(uint32_t r) const { Meta m = meta_for(r); if (m.is_bool) throw std::invalid_argument("expected BV"); return m.width; } + void expect_bool(uint32_t r) const { if (!meta_for(r).is_bool) throw std::invalid_argument("expected Bool"); } + uint32_t same_bv(uint32_t a, uint32_t b) const { uint32_t aw = expect_bv(a), bw = expect_bv(b); if (aw != bw) throw std::invalid_argument("BV width mismatch"); return aw; } + uint32_t bv_unary(uint8_t t, uint32_t x) { return add(t, expect_bv(x), {x}); } + uint32_t bv_binary(uint8_t t, uint32_t a, uint32_t b) { return add(t, same_bv(a,b), {a,b}); } + uint32_t bv_ext(uint8_t t, uint32_t x, uint16_t n) { uint32_t w = expect_bv(x) + n; check_width(w); return add(t, w, {x}, n); } + uint32_t bool_binary(uint8_t t, uint32_t a, uint32_t b) { expect_bool(a); expect_bool(b); return add(t, 0, {a,b}); } + uint32_t bv_cmp(uint8_t t, uint32_t a, uint32_t b) { same_bv(a,b); return add(t, 0, {a,b}); } + + static std::vector expr_bytes(const std::vector& ns, const std::vector& cs, const std::vector& bl) { std::vector out; out.insert(out.end(), {'S','M','T',0,1,0,0,0}); u32(out, uint32_t(ns.size())); u32(out, uint32_t(cs.size())); u32(out, uint32_t(bl.size())); for (int i=0;i<12;++i) u8(out,0); for (const auto& n: ns) { u8(out,n.tag); u8(out,n.arity); u16(out,n.aux_hi); u32(out,n.width); u32(out,n.aux_lo); u32(out,n.children); u64(out,n.payload); } for (auto c: cs) u32(out,c); out.insert(out.end(), bl.begin(), bl.end()); return out; } + void mark(uint32_t ref, std::vector& live) const { uint32_t idx = ref_index(ref); if (idx >= nodes.size()) throw std::invalid_argument("bad root ref"); if (live[idx]) return; live[idx] = 1; const Node& n = nodes[idx]; for (uint32_t i = 0; i < n.arity; ++i) mark(children[n.children + i], live); } + std::vector compact_expr(const std::vector& roots, std::vector& old_to_new) const { std::vector live(nodes.size(), 0); for (auto r: roots) mark(r, live); old_to_new.assign(nodes.size(), UINT32_MAX); uint32_t next = 0; for (size_t old = 0; old < nodes.size(); ++old) if (live[old]) old_to_new[old] = meta[old].is_bool ? bool_ref(next++) : bv_ref(next++); std::vector ns; std::vector cs; for (size_t old = 0; old < nodes.size(); ++old) if (live[old]) { Node n = nodes[old]; uint32_t start = n.arity ? uint32_t(cs.size()) : 0; for (uint32_t i = 0; i < n.arity; ++i) cs.push_back(old_to_new[ref_index(children[n.children + i])]); n.children = start; ns.push_back(n); } return expr_bytes(ns, cs, blob); } + uint32_t remap(uint32_t ref, const std::vector& old_to_new) const { uint32_t idx = ref_index(ref); if (idx >= old_to_new.size() || old_to_new[idx] == UINT32_MAX) throw std::invalid_argument("dead node ref"); return old_to_new[idx]; } + std::vector build_request(uint32_t request_id, uint8_t cmd, uint8_t flags, uint32_t budget, uint32_t target, bool has_target) const { std::vector named, unnamed; for (auto a: assertions) (a.named ? named : unnamed).push_back(a); std::vector ordered = named; ordered.insert(ordered.end(), unnamed.begin(), unnamed.end()); std::vector roots; for (auto a: ordered) roots.push_back(a.root); roots.insert(roots.end(), assumptions.begin(), assumptions.end()); if (has_target) roots.push_back(target); std::vector old_to_new; auto expr = compact_expr(roots, old_to_new); std::vector out; out.insert(out.end(), {'S','M','T','Q'}); u32(out, request_id); u8(out, cmd); u8(out, flags); u32(out, budget); u32(out, uint32_t(expr.size())); u16(out, uint16_t(ordered.size())); u16(out, uint16_t(named.size())); u16(out, uint16_t(assumptions.size())); u32(out, has_target ? remap(target, old_to_new) : 0); u32(out, 0); out.insert(out.end(), expr.begin(), expr.end()); for (auto a: ordered) u32(out, remap(a.root, old_to_new)); for (auto a: named) { u32(out, a.name_offset); u32(out, a.name_len); } for (auto a: assumptions) u32(out, remap(a, old_to_new)); return out; } +}; + +inline std::vector frame(const std::vector& payload) { if (payload.size() > UINT32_MAX) throw std::invalid_argument("frame payload too large"); std::vector out; u32(out, uint32_t(payload.size())); out.insert(out.end(), payload.begin(), payload.end()); return out; } +inline uint16_t read_u16(const std::vector& data, size_t off) { if (off + 2 > data.size()) throw std::invalid_argument("short u16"); return uint16_t(data[off]) | (uint16_t(data[off+1]) << 8); } +inline uint32_t read_u32(const std::vector& data, size_t off) { if (off + 4 > data.size()) throw std::invalid_argument("short u32"); return uint32_t(data[off]) | (uint32_t(data[off+1]) << 8) | (uint32_t(data[off+2]) << 16) | (uint32_t(data[off+3]) << 24); } +inline void require_bytes(const std::vector& data, size_t off, size_t len, const char* what) { if (off > data.size() || len > data.size() - off) throw std::invalid_argument(what); } +inline ScalarValue parse_scalar_value(const std::vector& data, size_t& off) { require_bytes(data, off, 8, "short scalar"); uint32_t width = read_u32(data, off); uint32_t len = read_u32(data, off + 4); off += 8; require_bytes(data, off, len, "short scalar value"); ScalarValue value{width, std::vector(data.begin() + off, data.begin() + off + len)}; off += len; if (width == 0) { if (len != 1 || (value.bytes[0] != 0 && value.bytes[0] != 1)) throw std::invalid_argument("bad Bool scalar"); } else { uint32_t expected = bytes_for_width(width); if (len != expected) throw std::invalid_argument("bad BV scalar length"); uint32_t valid = width % 8; if (valid != 0 && !value.bytes.empty() && (value.bytes.back() & ~uint8_t((1u << valid) - 1u)) != 0) throw std::invalid_argument("bad BV scalar high bits"); } return value; } +inline std::vector parse_model_payload(const std::vector& payload) { require_bytes(payload, 0, 4, "short model"); uint32_t count = read_u32(payload, 0); size_t off = 4; std::vector entries; entries.reserve(count); for (uint32_t i = 0; i < count; ++i) { require_bytes(payload, off, 4, "short model entry"); uint32_t ref = read_u32(payload, off); off += 4; entries.push_back({ref, parse_scalar_value(payload, off)}); } if (off != payload.size()) throw std::invalid_argument("trailing model bytes"); return entries; } +inline std::vector parse_core_payload(const std::vector& payload) { require_bytes(payload, 0, 4, "short core"); uint32_t count = read_u32(payload, 0); size_t off = 4; std::vector names; names.reserve(count); for (uint32_t i = 0; i < count; ++i) { require_bytes(payload, off, 4, "short core name length"); uint32_t len = read_u32(payload, off); off += 4; require_bytes(payload, off, len, "short core name"); names.emplace_back(payload.begin() + off, payload.begin() + off + len); off += len; } if (off != payload.size()) throw std::invalid_argument("trailing core bytes"); return names; } +inline SimplifyResult parse_simplify_payload(const std::vector& payload) { require_bytes(payload, 0, 12, "short simplify"); uint32_t expr_len = read_u32(payload, 0); uint16_t assertion_count = read_u16(payload, 4); uint16_t named_count = read_u16(payload, 6); uint16_t assumption_count = read_u16(payload, 8); if (named_count > assertion_count) throw std::invalid_argument("bad simplify named_count"); size_t off = 12; require_bytes(payload, off, expr_len, "short simplify expression"); SimplifyResult result; result.expression.assign(payload.begin() + off, payload.begin() + off + expr_len); off += expr_len; for (uint16_t i = 0; i < assertion_count; ++i) { result.assertion_roots.push_back(read_u32(payload, off)); off += 4; } for (uint16_t i = 0; i < named_count; ++i) { uint32_t name_off = read_u32(payload, off); uint32_t name_len = read_u32(payload, off + 4); off += 8; result.named_assertion_refs.push_back({name_off, name_len}); } for (uint16_t i = 0; i < assumption_count; ++i) { result.assumption_roots.push_back(read_u32(payload, off)); off += 4; } if (off != payload.size()) throw std::invalid_argument("trailing simplify bytes"); return result; } +inline OptimizationResult parse_optimization_payload(const std::vector& payload, bool has_model) { size_t off = 0; OptimizationResult result{parse_scalar_value(payload, off), has_model, {}}; if (has_model) { std::vector rest(payload.begin() + off, payload.end()); result.model = parse_model_payload(rest); off = payload.size(); } if (off != payload.size()) throw std::invalid_argument("trailing optimization bytes"); return result; } +inline Response parse_response(const std::vector& data) { if (data.size() < 16 || data[0] != 'S' || data[1] != 'M' || data[2] != 'T' || data[3] != 'R') throw std::invalid_argument("bad response"); uint32_t len = read_u32(data, 10); if (data.size() != 16u + len) throw std::invalid_argument("response length mismatch"); return {read_u32(data, 4), data[8], data[9], std::vector(data.begin() + 16, data.end())}; } + +namespace detail { +#ifdef _WIN32 +using socket_handle = SOCKET; +constexpr socket_handle invalid_socket = INVALID_SOCKET; +inline void ensure_socket_runtime() { struct Wsa { Wsa() { WSADATA data; if (WSAStartup(MAKEWORD(2, 2), &data) != 0) throw std::runtime_error("WSAStartup failed"); } ~Wsa() { WSACleanup(); } }; static Wsa wsa; (void)wsa; } +inline void close_socket(socket_handle s) { if (s != invalid_socket) closesocket(s); } +inline int last_socket_error() { return WSAGetLastError(); } +inline std::string socket_error_text(int code) { return "socket error " + std::to_string(code); } +inline std::string gai_error_text(int code) { return gai_strerrorA(code); } +#else +using socket_handle = int; +constexpr socket_handle invalid_socket = -1; +inline void ensure_socket_runtime() {} +inline void close_socket(socket_handle s) { if (s != invalid_socket) ::close(s); } +inline int last_socket_error() { return errno; } +inline std::string socket_error_text(int code) { return std::strerror(code); } +inline std::string gai_error_text(int code) { return gai_strerror(code); } +#endif +inline void throw_socket_error(const char* action) { throw std::runtime_error(std::string(action) + ": " + socket_error_text(last_socket_error())); } +} // namespace detail + +class TcpClient { +public: + TcpClient() = default; + TcpClient(const std::string& host, uint16_t port) { open(host, port); } + ~TcpClient() { close(); } + TcpClient(const TcpClient&) = delete; + TcpClient& operator=(const TcpClient&) = delete; + TcpClient(TcpClient&& other) noexcept : sock_(other.sock_) { other.sock_ = detail::invalid_socket; } + TcpClient& operator=(TcpClient&& other) noexcept { if (this != &other) { close(); sock_ = other.sock_; other.sock_ = detail::invalid_socket; } return *this; } + + static TcpClient connect(const std::string& host, uint16_t port) { return TcpClient(host, port); } + bool connected() const { return sock_ != detail::invalid_socket; } + + void open(const std::string& host, uint16_t port) { + close(); + detail::ensure_socket_runtime(); + addrinfo hints{}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + addrinfo* result = nullptr; + const std::string service = std::to_string(port); + int rc = getaddrinfo(host.c_str(), service.c_str(), &hints, &result); + if (rc != 0) throw std::runtime_error("getaddrinfo failed: " + detail::gai_error_text(rc)); + for (addrinfo* ai = result; ai != nullptr; ai = ai->ai_next) { + detail::socket_handle s = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (s == detail::invalid_socket) continue; +#ifndef _WIN32 +#ifdef SO_NOSIGPIPE + int no_sigpipe = 1; + (void)setsockopt(s, SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, sizeof(no_sigpipe)); +#endif +#endif +#ifdef _WIN32 + const int addr_len = static_cast(ai->ai_addrlen); +#else + const socklen_t addr_len = static_cast(ai->ai_addrlen); +#endif + if (::connect(s, ai->ai_addr, addr_len) == 0) { sock_ = s; break; } + detail::close_socket(s); + } + freeaddrinfo(result); + if (!connected()) throw std::runtime_error("connect failed"); + } + + void close() { detail::close_socket(sock_); sock_ = detail::invalid_socket; } + + void send_all(const uint8_t* data, size_t len) { + require_connected(); + while (len > 0) { +#ifdef _WIN32 + int chunk = len > static_cast(INT_MAX) ? INT_MAX : static_cast(len); + int sent = ::send(sock_, reinterpret_cast(data), chunk, 0); +#else + int flags = 0; +#ifdef MSG_NOSIGNAL + flags = MSG_NOSIGNAL; +#endif + ssize_t sent = ::send(sock_, data, len, flags); +#endif + if (sent <= 0) detail::throw_socket_error("send"); + data += static_cast(sent); + len -= static_cast(sent); + } + } + + std::vector recv_exact(size_t len) { + require_connected(); + std::vector out(len); + size_t off = 0; + while (off < len) { +#ifdef _WIN32 + int chunk = (len - off) > static_cast(INT_MAX) ? INT_MAX : static_cast(len - off); + int got = ::recv(sock_, reinterpret_cast(out.data() + off), chunk, 0); +#else + ssize_t got = ::recv(sock_, out.data() + off, len - off, 0); +#endif + if (got == 0) throw std::runtime_error("connection closed while reading frame"); + if (got < 0) detail::throw_socket_error("recv"); + off += static_cast(got); + } + return out; + } + + std::vector send_payload(const std::vector& payload) { + auto framed = frame(payload); + send_all(framed.data(), framed.size()); + auto header = recv_exact(4); + return recv_exact(read_u32(header, 0)); + } + + Response send_request(const std::vector& request) { return parse_response(send_payload(request)); } + + std::string send_text(const std::string& script) { + std::vector payload(script.begin(), script.end()); + auto response = send_payload(payload); + return std::string(response.begin(), response.end()); + } + +private: + detail::socket_handle sock_ = detail::invalid_socket; + void require_connected() const { if (!connected()) throw std::runtime_error("TCP client is not connected"); } +}; + +} // namespace smt_wire + +``` + +`clients/python/smt_wire.py`: + +```py +"""Single-file Python builder/codec for the SMT v1 wire format. + +The module intentionally has no dependencies beyond the standard library and +performs eager sort/width validation during construction. +""" +from __future__ import annotations + +from dataclasses import dataclass +import socket +import struct +from typing import Iterable, Optional, Union + +EXPR_MAGIC = b"SMT\0" +REQUEST_MAGIC = b"SMTQ" +RESPONSE_MAGIC = b"SMTR" +VERSION = 1 +BOOL_BIT = 0x80000000 +INDEX_MASK = 0x7fffffff +MAX_WIDTH = 65536 + +BV_VAR = 0 +BV_CONST = 1 +BV_NOT = 2 +BV_NEG = 3 +BV_AND = 4 +BV_OR = 5 +BV_XOR = 6 +BV_ADD = 7 +BV_SUB = 8 +BV_MUL = 9 +BV_UDIV = 10 +BV_UREM = 11 +BV_SDIV = 12 +BV_SREM = 13 +BV_SMOD = 14 +BV_SHL = 15 +BV_LSHR = 16 +BV_ASHR = 17 +BV_EXTRACT = 18 +BV_CONCAT = 19 +BV_ZEXT = 20 +BV_SEXT = 21 +BV_ITE = 22 +BV_SELECT = 23 +BOOL_TRUE = 24 +BOOL_FALSE = 25 +BOOL_VAR = 26 +BOOL_NOT = 27 +BOOL_AND = 28 +BOOL_OR = 29 +BOOL_IMPLIES = 30 +BV_EQ = 31 +BV_ULT = 32 +BV_ULE = 33 +BV_SLT = 34 +BV_SLE = 35 +UADD_OVF = 36 +SADD_OVF = 37 +USUB_OVF = 38 +SSUB_OVF = 39 +UMUL_OVF = 40 +SMUL_OVF = 41 +NEG_OVF = 42 +SDIV_OVF = 43 + +SOLVE = 0 +SIMPLIFY = 1 +MINIMIZE = 2 +MAXIMIZE = 3 +WANT_MODEL = 1 << 0 +WANT_CORE = 1 << 1 +SIGNED = 1 << 2 + +OK = 0 +SAT = 1 +UNSAT = 2 +UNKNOWN = 3 +ERROR = 4 +HAS_MODEL = 1 << 0 +HAS_CORE = 1 << 1 +HAS_EXPR = 1 << 2 +HAS_VALUE = 1 << 3 +HAS_MESSAGE = 1 << 4 + + +def bv_ref(index: int) -> int: + if not 0 <= index <= INDEX_MASK: + raise ValueError("node index out of range") + return index + + +def bool_ref(index: int) -> int: + if not 0 <= index <= INDEX_MASK: + raise ValueError("node index out of range") + return BOOL_BIT | index + + +def ref_index(ref: int) -> int: + return ref & INDEX_MASK + + +def is_bool_ref(ref: int) -> bool: + return bool(ref & BOOL_BIT) + + +def is_bv_ref(ref: int) -> bool: + return not is_bool_ref(ref) + + +def blob_payload(offset: int, length: int) -> int: + return (offset << 32) | length + + +def bytes_for_width(width: int) -> int: + if not 1 <= width <= MAX_WIDTH: + raise ValueError(f"invalid BV width {width}") + return (width + 7) // 8 + + +@dataclass(frozen=True) +class Node: + tag: int + arity: int + aux_hi: int + width: int + aux_lo: int + children: int + payload: int + + +@dataclass(frozen=True) +class ScalarValue: + width: int + bytes: bytes + + def as_bool(self) -> bool: + if self.width != 0 or len(self.bytes) != 1: + raise ValueError("scalar is not Bool") + return self.bytes[0] != 0 + + def as_int(self) -> int: + return int.from_bytes(self.bytes, "little") + + +@dataclass(frozen=True) +class ModelEntry: + node_ref: int + value: ScalarValue + + +@dataclass(frozen=True) +class SimplifyResult: + expression: bytes + assertion_roots: list[int] + named_assertion_refs: list[tuple[int, int]] + assumption_roots: list[int] + + +@dataclass(frozen=True) +class OptimizationResult: + optimum: ScalarValue + model: Optional[list[ModelEntry]] = None + + +@dataclass(frozen=True) +class Response: + request_id: int + status: int + flags: int + payload: bytes + + def message(self) -> str: + return self.payload.decode("utf-8") + + def model(self) -> list[ModelEntry]: + return parse_model(self.payload) + + def core(self) -> list[str]: + return parse_core(self.payload) + + def simplified(self) -> SimplifyResult: + return parse_simplify(self.payload) + + def optimization(self) -> OptimizationResult: + return parse_optimization(self.payload, bool(self.flags & HAS_MODEL)) + + +class Builder: + def __init__(self) -> None: + self.nodes: list[Node] = [] + self.children: list[int] = [] + self.blob = bytearray() + self.meta: list[tuple[str, int]] = [] + self.assertions: list[tuple[int, Optional[tuple[int, int]]]] = [] + self.assumptions: list[int] = [] + self.scopes: list[int] = [] + + def reset(self) -> None: + self.__init__() + + def push(self) -> None: + self.scopes.append(len(self.assertions)) + + def pop(self) -> None: + if not self.scopes: + raise ValueError("pop without push") + del self.assertions[self.scopes.pop():] + + def _blob(self, data: bytes) -> tuple[int, int]: + off = len(self.blob) + self.blob.extend(data) + return off, len(data) + + def _sort_for_tag(self, tag: int) -> str: + return "bv" if tag <= BV_SELECT else "bool" + + def _push(self, tag: int, width: int, children: Iterable[int] = (), aux_hi: int = 0, aux_lo: int = 0, payload: int = 0) -> int: + children = list(children) + if len(children) > 255: + raise ValueError("node arity exceeds u8") + start = len(self.children) if children else 0 + for child in children: + self._meta(child) + self.children.extend(children) + index = len(self.nodes) + sort = self._sort_for_tag(tag) + self.nodes.append(Node(tag, len(children), aux_hi, width, aux_lo, start, payload)) + self.meta.append((sort, width)) + return bool_ref(index) if sort == "bool" else bv_ref(index) + + def _meta(self, ref: int) -> tuple[str, int]: + idx = ref_index(ref) + if idx >= len(self.meta): + raise ValueError("node reference out of range") + sort, width = self.meta[idx] + if (sort == "bool") != is_bool_ref(ref): + raise ValueError("typed node reference sort mismatch") + return sort, width + + def _expect_bv(self, ref: int) -> int: + sort, width = self._meta(ref) + if sort != "bv": + raise ValueError("expected BV reference") + return width + + def _expect_bool(self, ref: int) -> None: + sort, _ = self._meta(ref) + if sort != "bool": + raise ValueError("expected Bool reference") + + def _same_bv(self, a: int, b: int) -> int: + aw, bw = self._expect_bv(a), self._expect_bv(b) + if aw != bw: + raise ValueError("BV width mismatch") + return aw + + def bv_var(self, name: str, width: int) -> int: + if not 1 <= width <= MAX_WIDTH: + raise ValueError("invalid BV width") + return self._push(BV_VAR, width, payload=blob_payload(*self._blob(name.encode()))) + + def bv_const(self, value: int, width: int) -> int: + if not 1 <= width <= MAX_WIDTH: + raise ValueError("invalid BV width") + if width <= 64: + payload = value & ((1 << width) - 1 if width < 64 else (1 << 64) - 1) + return self._push(BV_CONST, width, payload=payload) + masked = int(value) & ((1 << width) - 1) + data = masked.to_bytes(bytes_for_width(width), "little", signed=False) + return self.bv_const_wide(data, width) + + def bv_const_wide(self, data: bytes, width: int) -> int: + if len(data) != bytes_for_width(width): + raise ValueError("wide constant length does not match width") + data = bytearray(data) + valid = width % 8 + if valid: + data[-1] &= (1 << valid) - 1 + if width <= 64: + return self.bv_const(int.from_bytes(data, "little"), width) + return self._push(BV_CONST, width, payload=blob_payload(*self._blob(bytes(data)))) + + def _bv_unary(self, tag: int, x: int) -> int: + return self._push(tag, self._expect_bv(x), [x]) + + def _bv_binary(self, tag: int, a: int, b: int) -> int: + return self._push(tag, self._same_bv(a, b), [a, b]) + + def _bool_binary(self, tag: int, a: int, b: int) -> int: + self._expect_bool(a); self._expect_bool(b) + return self._push(tag, 0, [a, b]) + + def _bv_cmp(self, tag: int, a: int, b: int) -> int: + self._same_bv(a, b) + return self._push(tag, 0, [a, b]) + + def bv_not(self, x: int) -> int: return self._bv_unary(BV_NOT, x) + def bv_neg(self, x: int) -> int: return self._bv_unary(BV_NEG, x) + def bv_and(self, a: int, b: int) -> int: return self._bv_binary(BV_AND, a, b) + def bv_or(self, a: int, b: int) -> int: return self._bv_binary(BV_OR, a, b) + def bv_xor(self, a: int, b: int) -> int: return self._bv_binary(BV_XOR, a, b) + def bv_add(self, a: int, b: int) -> int: return self._bv_binary(BV_ADD, a, b) + def bv_sub(self, a: int, b: int) -> int: return self._bv_binary(BV_SUB, a, b) + def bv_mul(self, a: int, b: int) -> int: return self._bv_binary(BV_MUL, a, b) + def bv_udiv(self, a: int, b: int) -> int: return self._bv_binary(BV_UDIV, a, b) + def bv_urem(self, a: int, b: int) -> int: return self._bv_binary(BV_UREM, a, b) + def bv_sdiv(self, a: int, b: int) -> int: return self._bv_binary(BV_SDIV, a, b) + def bv_srem(self, a: int, b: int) -> int: return self._bv_binary(BV_SREM, a, b) + def bv_smod(self, a: int, b: int) -> int: return self._bv_binary(BV_SMOD, a, b) + def bv_shl(self, a: int, b: int) -> int: return self._bv_binary(BV_SHL, a, b) + def bv_lshr(self, a: int, b: int) -> int: return self._bv_binary(BV_LSHR, a, b) + def bv_ashr(self, a: int, b: int) -> int: return self._bv_binary(BV_ASHR, a, b) + + def bv_extract(self, x: int, hi: int, lo: int) -> int: + width = self._expect_bv(x) + if not 0 <= lo <= hi < width or hi > 0xffff: + raise ValueError("invalid extract bounds") + return self._push(BV_EXTRACT, hi - lo + 1, [x], hi, lo) + + def bv_concat(self, high: int, low: int) -> int: + width = self._expect_bv(high) + self._expect_bv(low) + if width > MAX_WIDTH: + raise ValueError("concat width exceeds limit") + return self._push(BV_CONCAT, width, [high, low]) + + def bv_zext(self, x: int, amount: int) -> int: + return self._extend(BV_ZEXT, x, amount) + + def bv_sext(self, x: int, amount: int) -> int: + return self._extend(BV_SEXT, x, amount) + + def _extend(self, tag: int, x: int, amount: int) -> int: + width = self._expect_bv(x) + amount + if not 0 <= amount <= 0xffff or width > MAX_WIDTH: + raise ValueError("invalid extension amount") + return self._push(tag, width, [x], amount) + + def bv_ite(self, cond: int, then_value: int, else_value: int) -> int: + self._expect_bool(cond) + return self._push(BV_ITE, self._same_bv(then_value, else_value), [cond, then_value, else_value]) + + def bv_select(self, selectors: list[int], values: list[int], default: int) -> int: + if len(selectors) != len(values) or len(selectors) > 127: + raise ValueError("invalid BV_SELECT pair count") + width = self._expect_bv(default) + children: list[int] = [] + for s, v in zip(selectors, values): + self._expect_bool(s) + if self._expect_bv(v) != width: + raise ValueError("BV_SELECT width mismatch") + children += [s, v] + children.append(default) + return self._push(BV_SELECT, width, children, len(selectors)) + + def bool_true(self) -> int: return self._push(BOOL_TRUE, 0) + def bool_false(self) -> int: return self._push(BOOL_FALSE, 0) + def bool_var(self, name: str) -> int: return self._push(BOOL_VAR, 0, payload=blob_payload(*self._blob(name.encode()))) + def bool_not(self, x: int) -> int: + self._expect_bool(x) + return self._push(BOOL_NOT, 0, [x]) + def bool_and(self, a: int, b: int) -> int: return self._bool_binary(BOOL_AND, a, b) + def bool_or(self, a: int, b: int) -> int: return self._bool_binary(BOOL_OR, a, b) + def bool_implies(self, a: int, b: int) -> int: return self._bool_binary(BOOL_IMPLIES, a, b) + + def bv_eq(self, a: int, b: int) -> int: return self._bv_cmp(BV_EQ, a, b) + def bv_ult(self, a: int, b: int) -> int: return self._bv_cmp(BV_ULT, a, b) + def bv_ule(self, a: int, b: int) -> int: return self._bv_cmp(BV_ULE, a, b) + def bv_slt(self, a: int, b: int) -> int: return self._bv_cmp(BV_SLT, a, b) + def bv_sle(self, a: int, b: int) -> int: return self._bv_cmp(BV_SLE, a, b) + def uadd_ovf(self, a: int, b: int) -> int: return self._bv_cmp(UADD_OVF, a, b) + def sadd_ovf(self, a: int, b: int) -> int: return self._bv_cmp(SADD_OVF, a, b) + def usub_ovf(self, a: int, b: int) -> int: return self._bv_cmp(USUB_OVF, a, b) + def ssub_ovf(self, a: int, b: int) -> int: return self._bv_cmp(SSUB_OVF, a, b) + def umul_ovf(self, a: int, b: int) -> int: return self._bv_cmp(UMUL_OVF, a, b) + def smul_ovf(self, a: int, b: int) -> int: return self._bv_cmp(SMUL_OVF, a, b) + def neg_ovf(self, x: int) -> int: + self._expect_bv(x) + return self._push(NEG_OVF, 0, [x]) + def sdiv_ovf(self, a: int, b: int) -> int: return self._bv_cmp(SDIV_OVF, a, b) + + def bv_ne(self, a: int, b: int) -> int: return self.bool_not(self.bv_eq(a, b)) + def bv_ugt(self, a: int, b: int) -> int: return self.bv_ult(b, a) + def bv_uge(self, a: int, b: int) -> int: return self.bv_ule(b, a) + def bv_sgt(self, a: int, b: int) -> int: return self.bv_slt(b, a) + def bv_sge(self, a: int, b: int) -> int: return self.bv_sle(b, a) + + def bool_eq(self, a: int, b: int) -> int: + self._expect_bool(a); self._expect_bool(b) + return self.bool_and(self.bool_or(a, self.bool_not(b)), self.bool_or(self.bool_not(a), b)) + + def bool_xor(self, a: int, b: int) -> int: return self.bool_not(self.bool_eq(a, b)) + + def bool_ite(self, c: int, t: int, e: int) -> int: + return self.bool_or(self.bool_and(c, t), self.bool_and(self.bool_not(c), e)) + + def bv_rotate_left(self, x: int, amount: int) -> int: + width = self._expect_bv(x) + amount %= width + if amount == 0: + return x + return self.bv_or(self.bv_shl(x, self.bv_const(amount, width)), self.bv_lshr(x, self.bv_const(width - amount, width))) + + def bv_rotate_right(self, x: int, amount: int) -> int: + width = self._expect_bv(x) + amount %= width + if amount == 0: + return x + return self.bv_or(self.bv_lshr(x, self.bv_const(amount, width)), self.bv_shl(x, self.bv_const(width - amount, width))) + + def assert_(self, root: int) -> None: + self._expect_bool(root) + self.assertions.append((root, None)) + + def assert_named(self, name: str, root: int) -> None: + self._expect_bool(root) + self.assertions.append((root, self._blob(name.encode()))) + + def assume(self, root: int) -> None: + self._expect_bool(root) + self.assumptions.append(root) + + def assert_mutex(self, selectors: list[int]) -> None: + for s in selectors: self._expect_bool(s) + for i in range(len(selectors)): + for j in range(i + 1, len(selectors)): + self.assert_(self.bool_not(self.bool_and(selectors[i], selectors[j]))) + + def to_bytes(self) -> bytes: + return self._expr_bytes(self.nodes, self.children, self.blob) + + def _expr_bytes(self, nodes: list[Node], children: list[int], blob: bytes | bytearray) -> bytes: + out = bytearray(EXPR_MAGIC + bytes([VERSION, 0, 0, 0]) + struct.pack(" None: + idx = ref_index(ref) + if idx in live: + return + live.add(idx) + node = self.nodes[idx] + for i in range(node.arity): + self._mark(self.children[node.children + i], live) + + def _compact(self, roots: list[int]) -> tuple[bytes, dict[int, int]]: + live: set[int] = set() + for r in roots: + self._mark(r, live) + old_to_new: dict[int, int] = {} + new_nodes: list[Node] = [] + new_children: list[int] = [] + for old, node in enumerate(self.nodes): + if old in live: + sort, _ = self.meta[old] + old_to_new[old] = bool_ref(len(new_nodes)) if sort == "bool" else bv_ref(len(new_nodes)) + new_nodes.append(node) + for old, node in enumerate(self.nodes): + if old not in live: + continue + start = len(new_children) if node.arity else 0 + for i in range(node.arity): + child = self.children[node.children + i] + new_children.append(old_to_new[ref_index(child)]) + new_nodes[ref_index(old_to_new[old])] = Node(node.tag, node.arity, node.aux_hi, node.width, node.aux_lo, start, node.payload) + return self._expr_bytes(new_nodes, new_children, self.blob), old_to_new + + def build_request(self, request_id: int, command: int, flags: int = 0, budget_ms: int = 0, target: Optional[int] = None) -> bytes: + named = [(r, n) for r, n in self.assertions if n is not None] + unnamed = [(r, n) for r, n in self.assertions if n is None] + assertions = named + unnamed + roots = [r for r, _ in assertions] + self.assumptions + ([target] if target is not None else []) + expr, remap = self._compact(roots) + assertion_roots = [remap[ref_index(r)] for r, _ in assertions] + assumption_roots = [remap[ref_index(r)] for r in self.assumptions] + target_raw = remap[ref_index(target)] if target is not None else 0 + out = bytearray(REQUEST_MAGIC + struct.pack(" bytes: + flags = (WANT_MODEL if want_model else 0) | (WANT_CORE if want_core else 0) + return self.build_request(request_id, SOLVE, flags, budget_ms) + + def build_simplify_request(self, request_id: int) -> bytes: + return self.build_request(request_id, SIMPLIFY) + + def build_minimize_request(self, request_id: int, target: int, signed: bool = False, budget_ms: int = 0, want_model: bool = False) -> bytes: + self._expect_bv(target) + return self.build_request(request_id, MINIMIZE, (SIGNED if signed else 0) | (WANT_MODEL if want_model else 0), budget_ms, target) + + def build_maximize_request(self, request_id: int, target: int, signed: bool = False, budget_ms: int = 0, want_model: bool = False) -> bytes: + self._expect_bv(target) + return self.build_request(request_id, MAXIMIZE, (SIGNED if signed else 0) | (WANT_MODEL if want_model else 0), budget_ms, target) + + +def parse_response(data: bytes) -> Response: + if len(data) < 16 or data[:4] != RESPONSE_MAGIC: + raise ValueError("bad response envelope") + request_id, = struct.unpack_from(" None: + if offset + length > len(data): + raise ValueError(f"short {context}") + + +def parse_scalar(data: bytes, offset: int = 0) -> tuple[ScalarValue, int]: + _need(data, offset, 8, "scalar header") + width, value_len = struct.unpack_from(" list[ModelEntry]: + _need(payload, 0, 4, "model header") + count, = struct.unpack_from(" list[str]: + _need(payload, 0, 4, "unsat core header") + count, = struct.unpack_from(" SimplifyResult: + _need(payload, 0, 12, "simplify header") + expr_len, assertion_count, named_count, assumption_count, _reserved = struct.unpack_from(" assertion_count: + raise ValueError("named_count exceeds assertion_count") + offset = 12 + _need(payload, offset, expr_len, "simplify expression") + expression = bytes(payload[offset:offset + expr_len]) + offset += expr_len + _need(payload, offset, assertion_count * 4, "simplify assertion roots") + assertion_roots = list(struct.unpack_from(f"<{assertion_count}I", payload, offset)) if assertion_count else [] + offset += assertion_count * 4 + named_assertion_refs: list[tuple[int, int]] = [] + for _ in range(named_count): + _need(payload, offset, 8, "simplify named ref") + named_assertion_refs.append(struct.unpack_from(" OptimizationResult: + optimum, offset = parse_scalar(payload, 0) + model = None + if has_model: + model = parse_model(payload[offset:]) + offset = len(payload) + if offset != len(payload): + raise ValueError("trailing bytes in optimization block") + return OptimizationResult(optimum, model) + + +def frame(payload: bytes) -> bytes: + return struct.pack(" bytes: + chunks: list[bytes] = [] + remaining = length + while remaining: + chunk = sock.recv(remaining) + if not chunk: + raise EOFError("socket closed while reading frame") + chunks.append(chunk) + remaining -= len(chunk) + return b"".join(chunks) + + +class TcpClient: + """Blocking TCP client for the SMT server transport.""" + + def __init__( + self, + host: str = "127.0.0.1", + port: int = 9123, + timeout: Optional[float] = None, + sock: Optional[socket.socket] = None, + ) -> None: + self._sock = sock if sock is not None else socket.create_connection((host, port), timeout=timeout) + + @classmethod + def from_socket(cls, sock: socket.socket) -> "TcpClient": + return cls(sock=sock) + + def close(self) -> None: + self._sock.close() + + def __enter__(self) -> "TcpClient": + return self + + def __exit__(self, *exc_info: object) -> None: + self.close() + + def send_payload(self, payload: bytes) -> bytes: + self._sock.sendall(frame(payload)) + (length,) = struct.unpack(" Response: + return parse_response(self.send_payload(request)) + + def send_text(self, script: Union[str, bytes]) -> str: + payload = script.encode("utf-8") if isinstance(script, str) else script + return self.send_payload(payload).decode("utf-8") + +``` + +`clients/tests/cpp_client_smoke.cpp`: + +```cpp +#include "../cpp/smt_wire.hpp" +#include +#include +#include + +int main() { + smt_wire::Builder b; + auto x = b.bv_var("x", 8); + auto one = b.bv_const(1, 8); + b.assert_(b.bv_eq(x, one)); + auto bytes = b.build_solve_request(0x01020304u, 500, true, false); + const std::vector expected = { + 0x53,0x4d,0x54,0x51,0x04,0x03,0x02,0x01,0x00,0x01,0xf4,0x01,0x00,0x00,0x71,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x53,0x4d,0x54,0x00,0x01,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x1f,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x78,0x02,0x00,0x00,0x80 + }; + assert(bytes == expected); + auto simplify = b.build_simplify_request(2); + assert(simplify[8] == smt_wire::command::SIMPLIFY); + auto minimize = b.build_minimize_request(3, x, true, 0, true); + assert(minimize[8] == smt_wire::command::MINIMIZE); + auto rotated = b.bv_rotate_left(x, 3); + assert(!smt_wire::is_bool_ref(rotated)); + smt_wire::Builder wide; + wide.bv_const((uint64_t(1) << 63) | 3u, 65); + auto wide_expr = wide.to_bytes(); + assert(smt_wire::read_u32(wide_expr, 16) == 9); // blob_len + assert(smt_wire::read_u32(wide_expr, 48) == 9); // payload = blob ref (offset 0, len 9) + std::vector raw_wide(9, 0xff); + smt_wire::Builder masked; + masked.bv_const_wide(raw_wide, 65); + auto masked_expr = masked.to_bytes(); + assert(masked_expr.back() == 1); + std::vector response = {'S','M','T','R', 7,0,0,0, smt_wire::status::ERROR, smt_wire::response_flags::HAS_MESSAGE, 3,0,0,0, 0,0, 'b','a','d'}; + auto parsed = smt_wire::parse_response(response); + assert(parsed.request_id == 7 && parsed.status == smt_wire::status::ERROR && parsed.payload.size() == 3); + std::vector model_payload; + smt_wire::u32(model_payload, 1); + smt_wire::u32(model_payload, x); + smt_wire::u32(model_payload, 8); + smt_wire::u32(model_payload, 1); + model_payload.push_back(42); + auto model = smt_wire::parse_model_payload(model_payload); + assert(model.size() == 1 && model[0].node_ref == x && model[0].value.bytes[0] == 42); + std::vector core_payload; + smt_wire::u32(core_payload, 1); + smt_wire::u32(core_payload, 2); + core_payload.push_back('a'); + core_payload.push_back('0'); + auto core = smt_wire::parse_core_payload(core_payload); + assert(core.size() == 1 && core[0] == "a0"); + bool threw = false; + try { (void)b.bv_add(x, b.bv_var("y", 16)); } catch (const std::invalid_argument&) { threw = true; } + assert(threw); + return 0; +} + +``` + +`clients/tests/cpp_live_client.cpp`: + +```cpp +#include "../cpp/smt_wire.hpp" +#include +#include +#include +#include + +int main(int argc, char** argv) { + assert(argc == 3); + const std::string host = argv[1]; + const auto port = static_cast(std::stoul(argv[2])); + + smt_wire::Builder b; + auto x = b.bv_var("cpp_x", 4); + b.assert_(b.bv_eq(x, b.bv_const(2, 4))); + + smt_wire::TcpClient client(host, port); + auto response = client.send_request(b.build_solve_request(0x4001u, 0, true, false)); + assert(response.request_id == 0x4001u); + assert(response.status == smt_wire::status::SAT); + assert(response.flags == smt_wire::response_flags::HAS_MODEL); + auto model = smt_wire::parse_model_payload(response.payload); + bool saw_value = false; + for (const auto& entry : model) { + if (entry.value.width == 4 && !entry.value.bytes.empty() && entry.value.bytes[0] == 2) { + saw_value = true; + } + } + assert(saw_value); + + auto text = client.send_text( + "(set-logic QF_BV)" + "(declare-const |cpp y| (_ BitVec 2))" + "(assert (= |cpp y| #b10))" + "(check-sat)" + "(get-value (|cpp y|))" + ); + assert(text.find("sat\n") == 0); + assert(text.find("(|cpp y| #b10)") != std::string::npos); + return 0; +} + +``` + +`clients/tests/test_live_server.py`: + +```py +"""Live end-to-end tests against the smt-server binary. + +This intentionally uses only the standard library plus the local Python client +module so it can run directly in CI without pytest. +""" +from __future__ import annotations + +import os +import pathlib +import shutil +import socket +import subprocess +import sys +import time +from typing import Iterable + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] +CLIENT_ROOT = REPO_ROOT / "clients" / "python" +sys.path.insert(0, str(CLIENT_ROOT)) + +import smt_wire as smt # noqa: E402 + + +def unused_local_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +def server_binary() -> pathlib.Path: + subprocess.check_call(["cargo", "build", "-p", "smt-server"], cwd=REPO_ROOT) + exe = "smt-server.exe" if os.name == "nt" else "smt-server" + return REPO_ROOT / "target" / "debug" / exe + + +def server_environment() -> dict[str, str]: + env = os.environ.copy() + build_dir = REPO_ROOT / "target" / "debug" / "build" + z3_dirs = [ + str(path) + for pattern in ("z3-sys-*/out/z3-*/bin", "z3-sys-*/out/z3-*/lib") + for path in build_dir.glob(pattern) + ] + if z3_dirs: + sep = os.pathsep + env["PATH"] = sep.join(z3_dirs + [env.get("PATH", "")]) + env["LD_LIBRARY_PATH"] = sep.join(z3_dirs + [env.get("LD_LIBRARY_PATH", "")]) + env["DYLD_LIBRARY_PATH"] = sep.join(z3_dirs + [env.get("DYLD_LIBRARY_PATH", "")]) + return env + + +class LiveServer: + def __init__(self) -> None: + self.port = unused_local_port() + self.proc: subprocess.Popen[bytes] | None = None + + def __enter__(self) -> "LiveServer": + self.proc = subprocess.Popen( + [str(server_binary()), f"127.0.0.1:{self.port}"], + cwd=REPO_ROOT, + env=server_environment(), + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + ) + deadline = time.time() + 10.0 + last_error: OSError | None = None + while time.time() < deadline: + if self.proc.poll() is not None: + stderr = self.proc.stderr.read().decode("utf-8", "replace") if self.proc.stderr else "" + raise RuntimeError(f"smt-server exited early with {self.proc.returncode}: {stderr}") + try: + with socket.create_connection(("127.0.0.1", self.port), timeout=0.2): + return self + except OSError as exc: + last_error = exc + time.sleep(0.05) + raise TimeoutError(f"smt-server did not listen on port {self.port}: {last_error}") + + def __exit__(self, *exc_info: object) -> None: + if self.proc is None: + return + self.proc.terminate() + try: + self.proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self.proc.kill() + self.proc.wait(timeout=5) + + +def cpp_compiler() -> str | None: + configured = os.environ.get("CXX") + if configured and shutil.which(configured): + return configured + for candidate in ("clang++", "g++", "c++"): + found = shutil.which(candidate) + if found: + return found + return None + + +def test_cpp_client_live_round_trip(port: int) -> None: + compiler = cpp_compiler() + if compiler is None: + print("No C++ compiler available; skipping live C++ client test") + return + exe = REPO_ROOT / "target" / ("cpp_live_client.exe" if os.name == "nt" else "cpp_live_client") + cmd = [ + compiler, + "-std=c++17", + "-Wall", + "-Wextra", + "-Werror", + str(REPO_ROOT / "clients" / "tests" / "cpp_live_client.cpp"), + "-o", + str(exe), + ] + if os.name == "nt": + cmd.append("-lws2_32") + subprocess.check_call(cmd, cwd=REPO_ROOT) + subprocess.check_call([str(exe), "127.0.0.1", str(port)], cwd=REPO_ROOT) + + +def assert_model_has_value(entries: Iterable[smt.ModelEntry], width: int, value: int) -> None: + for entry in entries: + if entry.value.width == width and entry.value.as_int() == value: + return + raise AssertionError(f"model does not contain {width}-bit value {value}") + + +def test_python_client_binary_round_trip(client: smt.TcpClient) -> None: + builder = smt.Builder() + x = builder.bv_var("x", 4) + builder.assert_(builder.bv_eq(x, builder.bv_const(2, 4))) + + response = client.send_request(builder.build_solve_request(0x1001, want_model=True)) + assert response.request_id == 0x1001 + assert response.status == smt.SAT + assert response.flags == smt.HAS_MODEL + assert_model_has_value(response.model(), 4, 2) + + +def test_binary_cache_rebinds_response_ids(client: smt.TcpClient) -> None: + builder = smt.Builder() + x = builder.bv_var("cached", 3) + builder.assert_(builder.bv_eq(x, builder.bv_const(5, 3))) + + first = client.send_request(builder.build_solve_request(0x2001)) + second = client.send_request(builder.build_solve_request(0x2002)) + assert first.request_id == 0x2001 + assert second.request_id == 0x2002 + assert first.status == smt.SAT + assert second.status == smt.SAT + + +def test_python_client_optimization_round_trip(client: smt.TcpClient) -> None: + builder = smt.Builder() + x = builder.bv_var("opt", 4) + builder.assert_(builder.bv_uge(x, builder.bv_const(5, 4))) + + response = client.send_request(builder.build_minimize_request(0x3001, x)) + assert response.request_id == 0x3001 + assert response.status == smt.SAT + assert response.flags == smt.HAS_VALUE + optimum = response.optimization().optimum + assert optimum.width == 4 + assert optimum.as_int() == 5 + + +def test_text_smtlib_round_trip(client: smt.TcpClient) -> None: + script = b""" + #| yaspar parses this block comment in the live text path |# + (set-logic QF_BV) + (declare-const |x y| (_ BitVec 2)) + (assert (= |x y| #b11)) + (check-sat) + (get-value (|x y|)) + """ + text = client.send_text(script) + assert text.startswith("sat\n"), text + assert "(|x y| #b11)" in text, text + + +def main() -> None: + with LiveServer() as server: + with smt.TcpClient("127.0.0.1", server.port, timeout=5) as client: + test_python_client_binary_round_trip(client) + test_binary_cache_rebinds_response_ids(client) + test_python_client_optimization_round_trip(client) + test_text_smtlib_round_trip(client) + test_cpp_client_live_round_trip(server.port) + + +if __name__ == "__main__": + main() + +``` + +`clients/tests/test_python_client.py`: + +```py +import pathlib +import sys + +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "python")) + +import smt_wire as smt + + +def hex_bytes(s: str) -> bytes: + return bytes.fromhex("".join(s.split())) + + +def test_python_client_matches_simple_sat_golden_vector(): + b = smt.Builder() + x = b.bv_var("x", 8) + one = b.bv_const(1, 8) + b.assert_(b.bv_eq(x, one)) + assert b.build_solve_request(0x01020304, 500, True, False) == hex_bytes( + "53 4d 54 51 04 03 02 01 00 01 f4 01 00 00 71 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00" + "53 4d 54 00 01 00 00 00 03 00 00 00 02 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + "00 00 00 00 08 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00" + "01 00 00 00 08 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00" + "1f 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + "00 00 00 00 01 00 00 00 78 02 00 00 80" + ) + + +def test_python_client_validates_width_and_sort(): + b = smt.Builder() + a = b.bv_var("a", 8) + c = b.bv_var("c", 16) + p = b.bool_var("p") + rotated = b.bv_rotate_left(a, 3) + assert smt.is_bv_ref(rotated) + assert b.build_minimize_request(3, a, signed=True, want_model=True).startswith(b"SMTQ") + try: + b.bv_add(a, c) + raise AssertionError("expected width error") + except ValueError: + pass + try: + b.bool_and(p, a) + raise AssertionError("expected sort error") + except ValueError: + pass + + +def test_python_wide_integer_constant_uses_blob_encoding_and_masks_to_width(): + b = smt.Builder() + b.bv_const((1 << 80) | (1 << 64) | 3, 65) + expr = b.to_bytes() + assert int.from_bytes(expr[16:20], "little") == 9 # blob_len + assert int.from_bytes(expr[48:56], "little") == 9 # payload = blob ref (offset 0, len 9) + assert expr[-1] == 1 # only the 65th bit may remain in the final byte + + +def test_python_response_parser(): + response = b"SMTR" + (7).to_bytes(4, "little") + bytes([smt.ERROR, smt.HAS_MESSAGE]) + (3).to_bytes(4, "little") + b"\0\0bad" + parsed = smt.parse_response(response) + assert parsed.request_id == 7 + assert parsed.status == smt.ERROR + assert parsed.payload == b"bad" + assert parsed.message() == "bad" + + scalar = (8).to_bytes(4, "little") + (1).to_bytes(4, "little") + b"*" + model_payload = (1).to_bytes(4, "little") + (0).to_bytes(4, "little") + scalar + model_response = smt.parse_response(b"SMTR" + (8).to_bytes(4, "little") + bytes([smt.SAT, smt.HAS_MODEL]) + len(model_payload).to_bytes(4, "little") + b"\0\0" + model_payload) + model = model_response.model() + assert model[0].node_ref == 0 + assert model[0].value.width == 8 + assert model[0].value.as_int() == 42 + + core_payload = (1).to_bytes(4, "little") + (2).to_bytes(4, "little") + b"a0" + assert smt.parse_core(core_payload) == ["a0"] + + +if __name__ == "__main__": + test_python_client_matches_simple_sat_golden_vector() + test_python_client_validates_width_and_sort() + test_python_wide_integer_constant_uses_blob_encoding_and_masks_to_width() + test_python_response_parser() + +``` + +`crates/qfbvsmtrs/Cargo.toml`: + +```toml +[package] +name = "qfbvsmtrs" +version = "0.1.0" +edition = "2021" +description = "Standalone pure-Rust QF_BV SMT solver via bit-blasting" +license = "MIT OR Apache-2.0" + +[features] +default = [] +wire = ["dep:smt-wire"] + +[dependencies] +dashu = { version = "0.4.2", features = ["num-traits"] } +smt-wire = { path = "../smt-wire", optional = true } +yaspar = "2.7.0" +varisat = "0.2.2" +splr = "0.17.2" + +[dev-dependencies] +z3 = { version = "0.20.0", features = ["gh-release"] } + +``` + +`crates/qfbvsmtrs/fuzz/Cargo.lock`: + +```lock +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[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", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[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 = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[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 = "dashu" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b3e5ac1e23ff1995ef05b912e2b012a8784506987a2651552db2c73fb3d7e0" +dependencies = [ + "dashu-base", + "dashu-float", + "dashu-int", + "dashu-macros", + "dashu-ratio", + "rustversion", +] + +[[package]] +name = "dashu-base" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b80bf6b85aa68c58ffea2ddb040109943049ce3fbdf4385d0380aef08ef289" + +[[package]] +name = "dashu-float" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85078445a8dbd2e1bd21f04a816f352db8d333643f0c9b78ca7c3d1df71063e7" +dependencies = [ + "dashu-base", + "dashu-int", + "num-modular", + "num-order", + "num-traits", + "rustversion", + "static_assertions", +] + +[[package]] +name = "dashu-int" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee99d08031ca34a4d044efbbb21dff9b8c54bb9d8c82a189187c0651ffdb9fbf" +dependencies = [ + "cfg-if", + "dashu-base", + "num-modular", + "num-order", + "num-traits", + "rustversion", + "static_assertions", +] + +[[package]] +name = "dashu-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93381c3ef6366766f6e9ed9cf09e4ef9dec69499baf04f0c60e70d653cf0ab10" +dependencies = [ + "dashu-base", + "dashu-float", + "dashu-int", + "dashu-ratio", + "paste", + "proc-macro2", + "quote", + "rustversion", +] + +[[package]] +name = "dashu-ratio" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e33b04dd7ce1ccf8a02a69d3419e354f2bbfdf4eb911a0b7465487248764c9" +dependencies = [ + "dashu-base", + "dashu-float", + "dashu-int", + "num-modular", + "num-order", + "num-traits", + "rustversion", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +dependencies = [ + "regex-automata", + "rustversion", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[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 = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[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", + "smallvec", + "windows-link", +] + +[[package]] +name = "partial_ref" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f728bc9b1479656e40cba507034904a8c44027c0efdbbaf6a4bdc5f2d3a910c" +dependencies = [ + "partial_ref_derive", +] + +[[package]] +name = "partial_ref_derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e1d2cb5b898b5a5342e994e0d0c367dbfe69cbf717cd307045ec9fb057581" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +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_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", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qfbvsmtrs" +version = "0.1.0" +dependencies = [ + "dashu", + "splr", + "varisat", + "yaspar", +] + +[[package]] +name = "qfbvsmtrs-fuzz" +version = "0.0.0" +dependencies = [ + "libfuzzer-sys", + "qfbvsmtrs", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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_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 2.0.117", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "splr" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c8d72268886573a95520ff76c282876c69d3776e02a0b4ac8eea2b310d69ca" +dependencies = [ + "bitflags", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "term" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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 2.0.117", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "varisat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe609851d1e9196674ac295f656bd8601200a1077343d22b345013497807caf" +dependencies = [ + "anyhow", + "itoa", + "leb128", + "log", + "ordered-float", + "partial_ref", + "rustc-hash", + "serde", + "thiserror", + "varisat-checker", + "varisat-dimacs", + "varisat-formula", + "varisat-internal-macros", + "varisat-internal-proof", + "vec_mut_scan", +] + +[[package]] +name = "varisat-checker" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135c977c5913ed6e98f6b81b8e4d322211303b7d40dae773caef7ad1de6c763b" +dependencies = [ + "anyhow", + "log", + "partial_ref", + "rustc-hash", + "smallvec", + "thiserror", + "varisat-dimacs", + "varisat-formula", + "varisat-internal-proof", +] + +[[package]] +name = "varisat-dimacs" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1dee4e21be1f04c0a939f7ae710cced47233a578de08a1b3c7d50848402636" +dependencies = [ + "anyhow", + "itoa", + "thiserror", + "varisat-formula", +] + +[[package]] +name = "varisat-formula" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395c5543b9bfd9076d6d3af49d6c34a4b91b0b355998c0a5ec6ed7265d364520" + +[[package]] +name = "varisat-internal-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602ece773543d066aa7848455486c6c0422a3f214da7a2b899100f3c4f12408d" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "varisat-internal-proof" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6163bb7bc9018af077b76d64f976803d141c36a27d640f1437dddc4fd527d207" +dependencies = [ + "anyhow", + "varisat-formula", +] + +[[package]] +name = "vec_mut_scan" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ed610a8d5e63d9c0e31300e8fdb55104c5f21e422743a9dc74848fa8317fd2" + +[[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 = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "yaspar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be170ae8a3dc3c6f62366d4fb8a4ceef65289f927a17d26dbfa13849d92c91c" +dependencies = [ + "clap", + "dashu", + "lalrpop", + "lalrpop-util", + "lazy_static", + "num-traits", + "phf", + "serde", +] + +``` + +`crates/qfbvsmtrs/fuzz/Cargo.toml`: + +```toml +[package] +name = "qfbvsmtrs-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +qfbvsmtrs = { path = ".." } + +[[bin]] +name = "smt2_pipeline" +path = "fuzz_targets/smt2_pipeline.rs" +test = false +doc = false +bench = false + +[workspace] + +``` + +`crates/qfbvsmtrs/fuzz/fuzz_targets/smt2_pipeline.rs`: + +```rs +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + if let Ok(script) = std::str::from_utf8(data) { + let config = qfbvsmtrs::Config::default().with_budget(Some(std::time::Duration::from_millis(25))); + let _ = qfbvsmtrs::parse_smt2(script); + let _ = qfbvsmtrs::solve_smt2(script, &config); + } +}); + +``` + +`crates/qfbvsmtrs/src/bin/qfbvsmtrs.rs`: + +```rs +use std::io::Read; + +fn main() { + let worker = std::thread::Builder::new() + .name("qfbvsmtrs-main".to_owned()) + .stack_size(64 * 1024 * 1024) + .spawn(run) + .expect("spawn qfbvsmtrs worker"); + match worker.join() { + Ok(Ok(())) => {} + Ok(Err(err)) => { + eprintln!("qfbvsmtrs: {err}"); + std::process::exit(1); + } + Err(payload) => std::panic::resume_unwind(payload), + } +} + +fn run() -> qfbvsmtrs::Result<()> { + let trace = std::env::var_os("QFBVSMTRS_TRACE").is_some(); + let start = std::time::Instant::now(); + let mut args = std::env::args_os(); + let _program = args.next(); + let script = if let Some(path) = args.next() { + std::fs::read_to_string(path) + .map_err(|err| qfbvsmtrs::Error::invalid("input", err.to_string()))? + } else { + let mut script = String::new(); + std::io::stdin() + .read_to_string(&mut script) + .map_err(|err| qfbvsmtrs::Error::invalid("stdin", err.to_string()))?; + script + }; + if trace { + eprintln!( + "qfbvsmtrs trace: read input in {:.3}s", + start.elapsed().as_secs_f64() + ); + } + let parse_start = std::time::Instant::now(); + let query = qfbvsmtrs::parse_smt2(&script)?; + if trace { + eprintln!( + "qfbvsmtrs trace: parsed input in {:.3}s (total {:.3}s)", + parse_start.elapsed().as_secs_f64(), + start.elapsed().as_secs_f64() + ); + } + let mut config = qfbvsmtrs::Config::default(); + if let Ok(ms) = std::env::var("QFBVSMTRS_BUDGET_MS") { + let ms = ms + .parse::() + .map_err(|_| qfbvsmtrs::Error::invalid("QFBVSMTRS_BUDGET_MS", "invalid integer"))?; + config = config.with_budget(Some(std::time::Duration::from_millis(ms))); + } + if let Ok(backend) = std::env::var("QFBVSMTRS_SAT_BACKEND") { + let kind = match backend.as_str() { + "splr" => qfbvsmtrs::SatBackendKind::Splr, + "varisat" => qfbvsmtrs::SatBackendKind::Varisat, + "dpll" => qfbvsmtrs::SatBackendKind::Dpll, + _ => { + return Err(qfbvsmtrs::Error::invalid( + "QFBVSMTRS_SAT_BACKEND", + "expected splr, varisat, or dpll", + )) + } + }; + config = config.with_sat_backend(kind); + } + let solve_start = std::time::Instant::now(); + let result = qfbvsmtrs::Solver::new(config).solve(&query)?; + if trace { + eprintln!( + "qfbvsmtrs trace: solved in {:.3}s (total {:.3}s)", + solve_start.elapsed().as_secs_f64(), + start.elapsed().as_secs_f64() + ); + } + print!("{}", qfbvsmtrs::format_smt2_response(&query, &result)); + Ok(()) +} + +``` + +`crates/qfbvsmtrs/src/bin/qfbvsmtrs_bench.rs`: + +```rs +use std::fs; +use std::time::Instant; + +use qfbvsmtrs::{solve_smt2, Config}; + +const BUILT_INS: &[(&str, &str)] = &[ + ( + "8bit-add-model", + "(set-logic QF_BV)\n(declare-const x (_ BitVec 8))\n(assert (= (bvadd x #x01) #x2b))\n(check-sat)\n(get-model)\n", + ), + ( + "16bit-mul-unsat", + "(set-logic QF_BV)\n(assert (not (= (bvmul #x00ff #x0101) #xffff)))\n(check-sat)\n", + ), + ( + "8bit-div-zero", + "(set-logic QF_BV)\n(assert (= (bvudiv #x07 #x00) #xff))\n(assert (= (bvurem #x07 #x00) #x07))\n(check-sat)\n", + ), +]; + +fn main() -> std::process::ExitCode { + let args = std::env::args().skip(1).collect::>(); + let mut cases = Vec::new(); + if args.is_empty() { + cases.extend( + BUILT_INS + .iter() + .map(|(name, script)| ((*name).to_owned(), (*script).to_owned())), + ); + } else { + for path in args { + match fs::read_to_string(&path) { + Ok(script) => cases.push((path, script)), + Err(err) => { + eprintln!("failed to read {path}: {err}"); + return std::process::ExitCode::FAILURE; + } + } + } + } + + println!("case,status,elapsed_ms"); + for (name, script) in cases { + let start = Instant::now(); + match solve_smt2(&script, &Config::default()) { + Ok(result) => println!( + "{},{:?},{:.3}", + name, + result.status, + start.elapsed().as_secs_f64() * 1000.0 + ), + Err(err) => { + println!( + "{},error,{:.3}", + name, + start.elapsed().as_secs_f64() * 1000.0 + ); + eprintln!("{name}: {err}"); + return std::process::ExitCode::FAILURE; + } + } + } + std::process::ExitCode::SUCCESS +} + +``` + +`crates/qfbvsmtrs/src/bin/qfbvsmtrs_stats.rs`: + +```rs +use std::time::Instant; + +fn main() { + let worker = std::thread::Builder::new() + .name("qfbvsmtrs-stats".to_owned()) + .stack_size(64 * 1024 * 1024) + .spawn(run) + .expect("spawn qfbvsmtrs stats worker"); + match worker.join() { + Ok(Ok(())) => {} + Ok(Err(err)) => { + eprintln!("qfbvsmtrs_stats: {err}"); + std::process::exit(1); + } + Err(payload) => std::panic::resume_unwind(payload), + } +} + +fn run() -> qfbvsmtrs::Result<()> { + let path = std::env::args_os() + .nth(1) + .ok_or_else(|| qfbvsmtrs::Error::invalid("usage", "qfbvsmtrs_stats "))?; + let start = Instant::now(); + let script = std::fs::read_to_string(path) + .map_err(|err| qfbvsmtrs::Error::invalid("input", err.to_string()))?; + let read = start.elapsed(); + let parse_start = Instant::now(); + let query = qfbvsmtrs::parse_smt2(&script)?; + let parse = parse_start.elapsed(); + let blast_start = Instant::now(); + let blasted = qfbvsmtrs::blast::blast_query(&query)?; + let blast = blast_start.elapsed(); + let cnf_start = Instant::now(); + let cnf = qfbvsmtrs::cnf::encode(&blasted.gates, blasted.assertion); + let cnf_time = cnf_start.elapsed(); + println!( + "read_s,{:.3},parse_s,{:.3},blast_s,{:.3},cnf_s,{:.3},terms,{},gates,{},vars,{},clauses,{}", + read.as_secs_f64(), + parse.as_secs_f64(), + blast.as_secs_f64(), + cnf_time.as_secs_f64(), + query.arena.len(), + blasted.gates.gates().len(), + cnf.num_vars, + cnf.clauses.len() + ); + Ok(()) +} + +``` + +`crates/qfbvsmtrs/src/blast.rs`: + +```rs +use crate::circuits; +use crate::error::{Error, Result}; +use crate::gates::{GateArena, GateId}; +use crate::ir::{Arena, NodeKind, Sort, TermId}; +use crate::query::Query; +use std::time::Instant; + +#[derive(Debug, Clone)] +pub enum BlastedValue { + Bool(GateId), + Bv(Vec), +} + +#[derive(Debug, Clone)] +pub struct BlastedVariable { + pub term: TermId, + pub name: String, + pub external: Option, + pub sort: Sort, + pub bits: Vec, +} + +#[derive(Debug, Clone)] +pub struct BlastResult { + pub gates: GateArena, + pub assertion: GateId, + pub variables: Vec, +} + +pub fn blast_query(query: &Query) -> Result { + blast_query_with_deadline(query, None) +} + +pub fn blast_query_with_deadline(query: &Query, deadline: Option) -> Result { + let mut ctx = BlastContext { + gates: GateArena::new(), + values: vec![None; query.arena.len()], + variables: Vec::new(), + }; + let reachable = reachable_terms(query)?; + + for (index, node) in query.arena.nodes().iter().enumerate() { + if index % 1024 == 0 && deadline.is_some_and(|deadline| Instant::now() >= deadline) { + return Err(Error::Timeout); + } + let id = TermId(index as u32); + let include_for_model = query.want_model + && matches!(node.kind, NodeKind::BvVar { .. } | NodeKind::BoolVar { .. }); + if !reachable[index] && !include_for_model { + continue; + } + let value = match &node.kind { + NodeKind::BvConst { width, bytes } => { + let bits = (0..*width) + .map(|bit| { + let byte = bytes[(bit / 8) as usize]; + ctx.gates.const_gate(((byte >> (bit % 8)) & 1) != 0) + }) + .collect(); + BlastedValue::Bv(bits) + } + NodeKind::BvVar { + width, + name, + external, + } => { + let bits = (0..*width).map(|_| ctx.gates.input()).collect::>(); + ctx.variables.push(BlastedVariable { + term: id, + name: name.clone(), + external: *external, + sort: Sort::Bv(*width), + bits: bits.clone(), + }); + BlastedValue::Bv(bits) + } + NodeKind::BoolConst(value) => BlastedValue::Bool(ctx.gates.const_gate(*value)), + NodeKind::BoolVar { name, external } => { + let bit = ctx.gates.input(); + ctx.variables.push(BlastedVariable { + term: id, + name: name.clone(), + external: *external, + sort: Sort::Bool, + bits: vec![bit], + }); + BlastedValue::Bool(bit) + } + + NodeKind::BvNot(x) => { + let x = ctx.bv(*x)?; + BlastedValue::Bv(circuits::bitwise_not(&mut ctx.gates, &x)) + } + NodeKind::BvNeg(x) => { + let x = ctx.bv(*x)?; + BlastedValue::Bv(circuits::neg(&mut ctx.gates, &x)) + } + NodeKind::BvAnd(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::bv_and(&mut ctx.gates, &a, &b)) + } + NodeKind::BvOr(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::bv_or(&mut ctx.gates, &a, &b)) + } + NodeKind::BvXor(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::bv_xor(&mut ctx.gates, &a, &b)) + } + NodeKind::BvAdd(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::add(&mut ctx.gates, &a, &b)) + } + NodeKind::BvSub(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::sub(&mut ctx.gates, &a, &b)) + } + NodeKind::BvMul(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::mul(&mut ctx.gates, &a, &b)) + } + NodeKind::BvUDiv(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + let (q, _) = circuits::udiv_urem(&mut ctx.gates, &a, &b); + BlastedValue::Bv(q) + } + NodeKind::BvURem(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + let (_, r) = circuits::udiv_urem(&mut ctx.gates, &a, &b); + BlastedValue::Bv(r) + } + NodeKind::BvSDiv(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::sdiv(&mut ctx.gates, &a, &b)) + } + NodeKind::BvSRem(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::srem(&mut ctx.gates, &a, &b)) + } + NodeKind::BvSMod(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::smod(&mut ctx.gates, &a, &b)) + } + NodeKind::BvShl(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::shl(&mut ctx.gates, &a, &b)) + } + NodeKind::BvLShr(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::lshr(&mut ctx.gates, &a, &b)) + } + NodeKind::BvAShr(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bv(circuits::ashr(&mut ctx.gates, &a, &b)) + } + NodeKind::BvExtract { child, high, low } => { + let child = ctx.bv(*child)?; + BlastedValue::Bv(circuits::extract(&child, *high, *low)) + } + NodeKind::BvConcat(high, low) => { + let high = ctx.bv(*high)?; + let low = ctx.bv(*low)?; + BlastedValue::Bv(circuits::concat(&high, &low)) + } + NodeKind::BvZeroExtend { child, extra } => { + let child = ctx.bv(*child)?; + BlastedValue::Bv(circuits::zext(&ctx.gates, &child, *extra)) + } + NodeKind::BvSignExtend { child, extra } => { + let child = ctx.bv(*child)?; + BlastedValue::Bv(circuits::sext(&child, *extra)) + } + NodeKind::BvRepeat { child, count } => { + let child = ctx.bv(*child)?; + BlastedValue::Bv(circuits::repeat(&child, *count)) + } + NodeKind::BvRotateLeft { child, amount } => { + let child = ctx.bv(*child)?; + BlastedValue::Bv(circuits::rotate_left(&child, *amount)) + } + NodeKind::BvRotateRight { child, amount } => { + let child = ctx.bv(*child)?; + BlastedValue::Bv(circuits::rotate_right(&child, *amount)) + } + NodeKind::BvIte { + cond, + then_value, + else_value, + } => { + let cond = ctx.bool(*cond)?; + let then_value = ctx.bv(*then_value)?; + let else_value = ctx.bv(*else_value)?; + BlastedValue::Bv(circuits::mux_bits( + &mut ctx.gates, + cond, + &then_value, + &else_value, + )) + } + NodeKind::BvSelect { cases, default } => { + let mut result = ctx.bv(*default)?; + for (selector, value) in cases.iter().rev() { + let selector = ctx.bool(*selector)?; + let value = ctx.bv(*value)?; + result = circuits::mux_bits(&mut ctx.gates, selector, &value, &result); + } + BlastedValue::Bv(result) + } + + NodeKind::BoolNot(x) => { + let child = ctx.bool(*x)?; + BlastedValue::Bool(ctx.gates.not(child)) + } + NodeKind::BoolAnd(a, b) => { + let av = ctx.bool(*a)?; + let bv = ctx.bool(*b)?; + BlastedValue::Bool(ctx.gates.and(av, bv)) + } + NodeKind::BoolOr(a, b) => { + let av = ctx.bool(*a)?; + let bv = ctx.bool(*b)?; + BlastedValue::Bool(ctx.gates.or(av, bv)) + } + NodeKind::BoolImplies(a, b) => { + let av = ctx.bool(*a)?; + let bv = ctx.bool(*b)?; + BlastedValue::Bool(ctx.gates.implies(av, bv)) + } + NodeKind::BoolEq(a, b) => { + let av = ctx.bool(*a)?; + let bv = ctx.bool(*b)?; + BlastedValue::Bool(ctx.gates.xnor(av, bv)) + } + NodeKind::BoolIte { + cond, + then_value, + else_value, + } => { + let c = ctx.bool(*cond)?; + let t = ctx.bool(*then_value)?; + let e = ctx.bool(*else_value)?; + BlastedValue::Bool(ctx.gates.mux(c, t, e)) + } + + NodeKind::BvEq(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::eq_bits(&mut ctx.gates, &a, &b)) + } + NodeKind::BvUlt(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::ult(&mut ctx.gates, &a, &b)) + } + NodeKind::BvUle(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::ule(&mut ctx.gates, &a, &b)) + } + NodeKind::BvSlt(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::slt(&mut ctx.gates, &a, &b)) + } + NodeKind::BvSle(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::sle(&mut ctx.gates, &a, &b)) + } + + NodeKind::UAddOverflow(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::uadd_overflow(&mut ctx.gates, &a, &b)) + } + NodeKind::SAddOverflow(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::sadd_overflow(&mut ctx.gates, &a, &b)) + } + NodeKind::USubOverflow(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::usub_overflow(&mut ctx.gates, &a, &b)) + } + NodeKind::SSubOverflow(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::ssub_overflow(&mut ctx.gates, &a, &b)) + } + NodeKind::UMulOverflow(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::umul_overflow(&mut ctx.gates, &a, &b)) + } + NodeKind::SMulOverflow(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::smul_overflow(&mut ctx.gates, &a, &b)) + } + NodeKind::NegOverflow(x) => { + let x = ctx.bv(*x)?; + BlastedValue::Bool(circuits::neg_overflow(&mut ctx.gates, &x)) + } + NodeKind::SDivOverflow(a, b) => { + let a = ctx.bv(*a)?; + let b = ctx.bv(*b)?; + BlastedValue::Bool(circuits::sdiv_overflow(&mut ctx.gates, &a, &b)) + } + }; + ctx.values[index] = Some(value); + } + + let assertion_bits = query + .assertions_and_assumptions() + .map(|root| ctx.bool(root)) + .collect::>>()?; + let assertion = ctx.gates.and_many(assertion_bits); + Ok(BlastResult { + gates: ctx.gates, + assertion, + variables: ctx.variables, + }) +} + +fn reachable_terms(query: &Query) -> Result> { + let mut reachable = vec![false; query.arena.len()]; + let mut stack = query.assertions_and_assumptions().collect::>(); + if let Some(target) = query.target { + stack.push(target); + } + while let Some(id) = stack.pop() { + let slot = reachable.get_mut(id.index()).ok_or_else(|| { + Error::invalid("term id", format!("term {} is out of bounds", id.raw())) + })?; + if *slot { + continue; + } + *slot = true; + push_children(&query.arena, id, &mut stack)?; + } + Ok(reachable) +} + +fn push_children(arena: &Arena, id: TermId, stack: &mut Vec) -> Result<()> { + match &arena.node(id)?.kind { + NodeKind::BvConst { .. } + | NodeKind::BvVar { .. } + | NodeKind::BoolConst(_) + | NodeKind::BoolVar { .. } => {} + NodeKind::BvNot(x) + | NodeKind::BvNeg(x) + | NodeKind::BvExtract { child: x, .. } + | NodeKind::BvZeroExtend { child: x, .. } + | NodeKind::BvSignExtend { child: x, .. } + | NodeKind::BvRepeat { child: x, .. } + | NodeKind::BvRotateLeft { child: x, .. } + | NodeKind::BvRotateRight { child: x, .. } + | NodeKind::BoolNot(x) + | NodeKind::NegOverflow(x) => stack.push(*x), + NodeKind::BvAnd(a, b) + | NodeKind::BvOr(a, b) + | NodeKind::BvXor(a, b) + | NodeKind::BvAdd(a, b) + | NodeKind::BvSub(a, b) + | NodeKind::BvMul(a, b) + | NodeKind::BvUDiv(a, b) + | NodeKind::BvURem(a, b) + | NodeKind::BvSDiv(a, b) + | NodeKind::BvSRem(a, b) + | NodeKind::BvSMod(a, b) + | NodeKind::BvShl(a, b) + | NodeKind::BvLShr(a, b) + | NodeKind::BvAShr(a, b) + | NodeKind::BvConcat(a, b) + | NodeKind::BoolAnd(a, b) + | NodeKind::BoolOr(a, b) + | NodeKind::BoolImplies(a, b) + | NodeKind::BoolEq(a, b) + | NodeKind::BvEq(a, b) + | NodeKind::BvUlt(a, b) + | NodeKind::BvUle(a, b) + | NodeKind::BvSlt(a, b) + | NodeKind::BvSle(a, b) + | NodeKind::UAddOverflow(a, b) + | NodeKind::SAddOverflow(a, b) + | NodeKind::USubOverflow(a, b) + | NodeKind::SSubOverflow(a, b) + | NodeKind::UMulOverflow(a, b) + | NodeKind::SMulOverflow(a, b) + | NodeKind::SDivOverflow(a, b) => { + stack.push(*a); + stack.push(*b); + } + NodeKind::BvIte { + cond, + then_value, + else_value, + } + | NodeKind::BoolIte { + cond, + then_value, + else_value, + } => { + stack.push(*cond); + stack.push(*then_value); + stack.push(*else_value); + } + NodeKind::BvSelect { cases, default } => { + stack.push(*default); + for (selector, value) in cases { + stack.push(*selector); + stack.push(*value); + } + } + } + Ok(()) +} + +struct BlastContext { + gates: GateArena, + values: Vec>, + variables: Vec, +} + +impl BlastContext { + fn value(&self, id: TermId) -> Result<&BlastedValue> { + self.values + .get(id.index()) + .and_then(Option::as_ref) + .ok_or_else(|| Error::internal(format!("term {} has not been blasted", id.raw()))) + } + + fn bool(&self, id: TermId) -> Result { + match self.value(id)? { + BlastedValue::Bool(gate) => Ok(*gate), + BlastedValue::Bv(_) => Err(Error::internal("expected Bool blasted value")), + } + } + + fn bv(&self, id: TermId) -> Result> { + match self.value(id)? { + BlastedValue::Bv(bits) => Ok(bits.clone()), + BlastedValue::Bool(_) => Err(Error::internal("expected BV blasted value")), + } + } +} + +``` + +`crates/qfbvsmtrs/src/builder.rs`: + +```rs +use std::cmp::Ordering; +use std::collections::BTreeMap; + +use crate::error::{Error, Result}; +use crate::ir::{ + bytes_for_width, mask_unused_high_bits, normalized_bytes, validate_bv_width, Arena, NodeKind, + Sort, TermId, +}; +use crate::query::{Assertion, Command, Query}; + +type Monomial = Vec; +type Polynomial = BTreeMap>; + +#[derive(Debug, Clone)] +pub struct Builder { + arena: Arena, + assertions: Vec, + assumptions: Vec, + command: Command, + target: Option, + signed: bool, + want_model: bool, + want_core: bool, + get_values: Vec, + scopes: Vec, +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} + +impl Builder { + pub fn new() -> Self { + Self { + arena: Arena::new(), + assertions: Vec::new(), + assumptions: Vec::new(), + command: Command::Solve, + target: None, + signed: false, + want_model: false, + want_core: false, + get_values: Vec::new(), + scopes: Vec::new(), + } + } + + pub fn arena(&self) -> &Arena { + &self.arena + } + + pub fn finish(self) -> Result { + if matches!(self.command, Command::Minimize | Command::Maximize) && self.target.is_none() { + return Err(Error::invalid( + "query target", + "MINIMIZE/MAXIMIZE require a target", + )); + } + Ok(Query { + arena: self.arena, + assertions: self.assertions, + assumptions: self.assumptions, + command: self.command, + target: self.target, + signed: self.signed, + want_model: self.want_model, + want_core: self.want_core, + get_values: self.get_values, + }) + } + + pub fn set_want_model(&mut self, want_model: bool) { + self.want_model = want_model; + } + + pub fn set_want_core(&mut self, want_core: bool) { + self.want_core = want_core; + } + + pub fn add_get_value(&mut self, name: impl Into) { + self.want_model = true; + self.get_values.push(name.into()); + } + + pub fn set_command(&mut self, command: Command) { + self.command = command; + } + + pub fn set_optimization( + &mut self, + command: Command, + target: TermId, + signed: bool, + ) -> Result<()> { + if !matches!(command, Command::Minimize | Command::Maximize) { + return Err(Error::invalid( + "optimization command", + "expected Minimize or Maximize", + )); + } + self.arena.expect_bv(target, "optimization target")?; + self.command = command; + self.target = Some(target); + self.signed = signed; + Ok(()) + } + + pub fn push(&mut self) { + self.scopes.push(self.assertions.len()); + } + + pub fn pop(&mut self) -> Result<()> { + let len = self + .scopes + .pop() + .ok_or_else(|| Error::invalid("scope", "pop without matching push"))?; + self.assertions.truncate(len); + Ok(()) + } + + pub fn assert(&mut self, root: TermId) -> Result<()> { + self.arena.expect_bool(root, "assertion")?; + self.assertions.push(Assertion { root, name: None }); + Ok(()) + } + + pub fn assert_named(&mut self, name: impl Into, root: TermId) -> Result<()> { + self.arena.expect_bool(root, "named assertion")?; + self.assertions.push(Assertion { + root, + name: Some(name.into()), + }); + Ok(()) + } + + pub fn assume(&mut self, root: TermId) -> Result<()> { + self.arena.expect_bool(root, "assumption")?; + self.assumptions.push(root); + Ok(()) + } + + pub fn bv_var(&mut self, name: impl Into, width: u32) -> Result { + self.bv_var_external(name, width, None) + } + + pub fn bv_var_external( + &mut self, + name: impl Into, + width: u32, + external: Option, + ) -> Result { + validate_bv_width(width, "BV variable width")?; + self.arena.add( + NodeKind::BvVar { + width, + name: name.into(), + external, + }, + Sort::Bv(width), + ) + } + + pub fn bool_var(&mut self, name: impl Into) -> Result { + self.bool_var_external(name, None) + } + + pub fn bool_var_external( + &mut self, + name: impl Into, + external: Option, + ) -> Result { + self.arena.add( + NodeKind::BoolVar { + name: name.into(), + external, + }, + Sort::Bool, + ) + } + + pub fn bool_const(&mut self, value: bool) -> Result { + self.arena.add(NodeKind::BoolConst(value), Sort::Bool) + } + + pub fn bool_true(&mut self) -> Result { + self.bool_const(true) + } + + pub fn bool_false(&mut self) -> Result { + self.bool_const(false) + } + + pub fn bv_const(&mut self, value: u64, width: u32) -> Result { + validate_bv_width(width, "BV constant width")?; + let mut bytes = vec![0u8; bytes_for_width(width)?]; + let raw = value.to_le_bytes(); + let count = bytes.len().min(raw.len()); + bytes[..count].copy_from_slice(&raw[..count]); + mask_unused_high_bits(&mut bytes, width); + self.bv_const_bytes(&bytes, width) + } + + pub fn bv_const_bytes(&mut self, bytes: &[u8], width: u32) -> Result { + let bytes = normalized_bytes(bytes, width)?; + self.arena + .add(NodeKind::BvConst { width, bytes }, Sort::Bv(width)) + } + + pub fn bv_not(&mut self, x: TermId) -> Result { + if let Some((width, mut bytes)) = self.bv_const_value(x)? { + for byte in &mut bytes { + *byte = !*byte; + } + mask_unused_high_bits(&mut bytes, width); + return self.bv_const_bytes(&bytes, width); + } + if let NodeKind::BvNot(inner) = &self.arena.node(x)?.kind { + return Ok(*inner); + } + self.bv_unary(x, NodeKind::BvNot) + } + + pub fn bv_neg(&mut self, x: TermId) -> Result { + if let Some((width, bytes)) = self.bv_const_value(x)? { + return self.bv_const_bytes(&neg_bytes(&bytes, width), width); + } + self.bv_unary(x, NodeKind::BvNeg) + } + + pub fn bv_and(&mut self, a: TermId, b: TermId) -> Result { + let (a, b) = order_pair(a, b); + let width = self.arena.expect_same_bv(a, b, "BV_AND")?; + if a == b { + return Ok(a); + } + if self.is_zero(a)? || self.is_zero(b)? { + return Ok(if self.is_zero(a)? { a } else { b }); + } + if self.is_all_ones(a)? || self.is_all_ones(b)? { + return Ok(if self.is_all_ones(a)? { b } else { a }); + } + if let (Some((_, av)), Some((_, bv))) = (self.bv_const_value(a)?, self.bv_const_value(b)?) { + let bytes = av.iter().zip(bv).map(|(x, y)| x & y).collect::>(); + return self.bv_const_bytes(&bytes, width); + } + self.arena.add(NodeKind::BvAnd(a, b), Sort::Bv(width)) + } + + pub fn bv_or(&mut self, a: TermId, b: TermId) -> Result { + let (a, b) = order_pair(a, b); + let width = self.arena.expect_same_bv(a, b, "BV_OR")?; + if a == b { + return Ok(a); + } + if self.is_zero(a)? || self.is_zero(b)? { + return Ok(if self.is_zero(a)? { b } else { a }); + } + if self.is_all_ones(a)? || self.is_all_ones(b)? { + return Ok(if self.is_all_ones(a)? { a } else { b }); + } + if let (Some((_, av)), Some((_, bv))) = (self.bv_const_value(a)?, self.bv_const_value(b)?) { + let bytes = av.iter().zip(bv).map(|(x, y)| x | y).collect::>(); + return self.bv_const_bytes(&bytes, width); + } + self.arena.add(NodeKind::BvOr(a, b), Sort::Bv(width)) + } + + pub fn bv_xor(&mut self, a: TermId, b: TermId) -> Result { + let (a, b) = order_pair(a, b); + let width = self.arena.expect_same_bv(a, b, "BV_XOR")?; + if a == b { + return self.zero(width); + } + if self.is_zero(a)? || self.is_zero(b)? { + return Ok(if self.is_zero(a)? { b } else { a }); + } + if let (Some((_, av)), Some((_, bv))) = (self.bv_const_value(a)?, self.bv_const_value(b)?) { + let bytes = av.iter().zip(bv).map(|(x, y)| x ^ y).collect::>(); + return self.bv_const_bytes(&bytes, width); + } + self.arena.add(NodeKind::BvXor(a, b), Sort::Bv(width)) + } + + pub fn bv_add(&mut self, a: TermId, b: TermId) -> Result { + let (a, b) = order_pair(a, b); + let width = self.arena.expect_same_bv(a, b, "BV_ADD")?; + if self.is_zero(a)? || self.is_zero(b)? { + return Ok(if self.is_zero(a)? { b } else { a }); + } + if let (Some((_, av)), Some((_, bv))) = (self.bv_const_value(a)?, self.bv_const_value(b)?) { + return self.bv_const_bytes(&add_bytes(&av, &bv, width), width); + } + if let Some(canonical) = self.canonical_bv_add(a, b, width)? { + return Ok(canonical); + } + if let Some((value, amount)) = self.shifted_same_add_parts(a, b)? { + let one = self.bv_const(1, width)?; + let shifted_one = self.bv_shl(one, amount)?; + let factor = self.bv_add(one, shifted_one)?; + return self.bv_mul(value, factor); + } + self.bv_add_node(a, b, width) + } + + pub fn bv_sub(&mut self, a: TermId, b: TermId) -> Result { + let width = self.arena.expect_same_bv(a, b, "BV_SUB")?; + if a == b || self.is_zero(a)? && self.is_zero(b)? { + return self.zero(width); + } + if self.is_zero(b)? { + return Ok(a); + } + if let (Some((_, av)), Some((_, bv))) = (self.bv_const_value(a)?, self.bv_const_value(b)?) { + return self.bv_const_bytes(&sub_bytes(&av, &bv, width), width); + } + self.arena.add(NodeKind::BvSub(a, b), Sort::Bv(width)) + } + + pub fn bv_mul(&mut self, a: TermId, b: TermId) -> Result { + let (a, b) = order_pair(a, b); + let width = self.arena.expect_same_bv(a, b, "BV_MUL")?; + if self.is_zero(a)? || self.is_zero(b)? { + return self.zero(width); + } + if self.is_one(a)? || self.is_one(b)? { + return Ok(if self.is_one(a)? { b } else { a }); + } + if let (Some((_, av)), Some((_, bv))) = (self.bv_const_value(a)?, self.bv_const_value(b)?) { + return self.bv_const_bytes(&mul_bytes(&av, &bv, width), width); + } + if let Some(canonical) = self.canonical_bv_mul(a, b, width)? { + return Ok(canonical); + } + if let Some((value, amount)) = self.bv_shl_parts(a)? { + let product = self.bv_mul(value, b)?; + return self.bv_shl(product, amount); + } + if let Some((value, amount)) = self.bv_shl_parts(b)? { + let product = self.bv_mul(a, value)?; + return self.bv_shl(product, amount); + } + self.bv_mul_node(a, b, width) + } + + pub fn bv_udiv(&mut self, a: TermId, b: TermId) -> Result { + let width = self.arena.expect_same_bv(a, b, "BV udiv")?; + if self.is_zero(b)? { + return self.all_ones(width); + } + if self.is_one(b)? { + return Ok(a); + } + if self.is_zero(a)? && self.bv_const_value(b)?.is_some() { + return self.zero(width); + } + self.arena.add(NodeKind::BvUDiv(a, b), Sort::Bv(width)) + } + + pub fn bv_urem(&mut self, a: TermId, b: TermId) -> Result { + let width = self.arena.expect_same_bv(a, b, "BV urem")?; + if self.is_zero(a)? || self.is_one(b)? { + return self.zero(width); + } + if self.is_zero(b)? { + return Ok(a); + } + self.arena.add(NodeKind::BvURem(a, b), Sort::Bv(width)) + } + + pub fn bv_sdiv(&mut self, a: TermId, b: TermId) -> Result { + self.bv_binary(a, b, NodeKind::BvSDiv) + } + + pub fn bv_srem(&mut self, a: TermId, b: TermId) -> Result { + self.bv_binary(a, b, NodeKind::BvSRem) + } + + pub fn bv_smod(&mut self, a: TermId, b: TermId) -> Result { + self.bv_binary(a, b, NodeKind::BvSMod) + } + + pub fn bv_shl(&mut self, a: TermId, b: TermId) -> Result { + let width = self.arena.expect_same_bv(a, b, "BV shl")?; + if self.is_zero(a)? { + return self.zero(width); + } + if let Some(amount) = self.const_shift_amount(b, width)? { + if amount == 0 { + return Ok(a); + } + if amount >= width as usize { + return self.zero(width); + } + if let Some((_, bytes)) = self.bv_const_value(a)? { + return self.bv_const_bytes(&shl_bytes(&bytes, width, amount), width); + } + } + self.arena.add(NodeKind::BvShl(a, b), Sort::Bv(width)) + } + + pub fn bv_lshr(&mut self, a: TermId, b: TermId) -> Result { + let width = self.arena.expect_same_bv(a, b, "BV lshr")?; + if self.is_zero(a)? { + return self.zero(width); + } + if let Some(amount) = self.const_shift_amount(b, width)? { + if amount == 0 { + return Ok(a); + } + if amount >= width as usize { + return self.zero(width); + } + if let Some((_, bytes)) = self.bv_const_value(a)? { + return self.bv_const_bytes(&lshr_bytes(&bytes, width, amount), width); + } + } + self.arena.add(NodeKind::BvLShr(a, b), Sort::Bv(width)) + } + + pub fn bv_ashr(&mut self, a: TermId, b: TermId) -> Result { + let width = self.arena.expect_same_bv(a, b, "BV ashr")?; + if let Some(amount) = self.const_shift_amount(b, width)? { + if amount == 0 { + return Ok(a); + } + if let Some((_, bytes)) = self.bv_const_value(a)? { + return self.bv_const_bytes(&ashr_bytes(&bytes, width, amount), width); + } + } + self.arena.add(NodeKind::BvAShr(a, b), Sort::Bv(width)) + } + + pub fn bv_extract(&mut self, child: TermId, high: u32, low: u32) -> Result { + let width = self.arena.expect_bv(child, "BV extract")?; + if low > high || high >= width { + return Err(Error::invalid( + "BV extract", + format!("invalid bounds high={high}, low={low}, child width={width}"), + )); + } + if low == 0 && high + 1 == width { + return Ok(child); + } + let out_width = high - low + 1; + if let Some((_, bytes)) = self.bv_const_value(child)? { + return self.bv_const_bytes(&extract_bytes(&bytes, low, out_width), out_width); + } + if let NodeKind::BvExtract { + child: inner, + high: _, + low: inner_low, + } = self.arena.node(child)?.kind + { + return self.bv_extract(inner, inner_low + high, inner_low + low); + } + if let NodeKind::BvConcat(high_child, low_child) = self.arena.node(child)?.kind { + let low_width = self.arena.expect_bv(low_child, "BV concat low child")?; + if high < low_width { + return self.bv_extract(low_child, high, low); + } + if low >= low_width { + return self.bv_extract(high_child, high - low_width, low - low_width); + } + let low_part = self.bv_extract(low_child, low_width - 1, low)?; + let high_part = self.bv_extract(high_child, high - low_width, 0)?; + return self.bv_concat(high_part, low_part); + } + self.arena.add( + NodeKind::BvExtract { child, high, low }, + Sort::Bv(out_width), + ) + } + + pub fn bv_concat(&mut self, high: TermId, low: TermId) -> Result { + let high_width = self.arena.expect_bv(high, "BV concat")?; + let low_width = self.arena.expect_bv(low, "BV concat")?; + let width = high_width + .checked_add(low_width) + .ok_or_else(|| Error::invalid("BV concat", "width overflow"))?; + validate_bv_width(width, "BV concat width")?; + if let (Some((_, high_bytes)), Some((_, low_bytes))) = + (self.bv_const_value(high)?, self.bv_const_value(low)?) + { + return self.bv_const_bytes( + &concat_bytes(&high_bytes, high_width, &low_bytes, low_width), + width, + ); + } + if let (Some((high_child, high_hi, high_lo)), Some((low_child, low_hi, low_lo))) = + (self.bv_extract_parts(high)?, self.bv_extract_parts(low)?) + { + if high_child == low_child && low_hi + 1 == high_lo { + return self.bv_extract(high_child, high_hi, low_lo); + } + } + self.arena + .add(NodeKind::BvConcat(high, low), Sort::Bv(width)) + } + + pub fn bv_zext(&mut self, child: TermId, extra: u32) -> Result { + let width = self.arena.expect_bv(child, "BV zero extension")?; + let out_width = width + .checked_add(extra) + .ok_or_else(|| Error::invalid("BV zero extension", "width overflow"))?; + validate_bv_width(out_width, "BV zero extension width")?; + if extra == 0 { + return Ok(child); + } + if let Some((_, mut bytes)) = self.bv_const_value(child)? { + bytes.resize(bytes_for_width(out_width)?, 0); + return self.bv_const_bytes(&bytes, out_width); + } + self.arena + .add(NodeKind::BvZeroExtend { child, extra }, Sort::Bv(out_width)) + } + + pub fn bv_sext(&mut self, child: TermId, extra: u32) -> Result { + let width = self.arena.expect_bv(child, "BV sign extension")?; + let out_width = width + .checked_add(extra) + .ok_or_else(|| Error::invalid("BV sign extension", "width overflow"))?; + validate_bv_width(out_width, "BV sign extension width")?; + if extra == 0 { + return Ok(child); + } + if let Some((_, bytes)) = self.bv_const_value(child)? { + return self.bv_const_bytes(&sext_bytes(&bytes, width, extra), out_width); + } + self.arena + .add(NodeKind::BvSignExtend { child, extra }, Sort::Bv(out_width)) + } + + pub fn bv_repeat(&mut self, child: TermId, count: u32) -> Result { + if count == 0 { + return Err(Error::invalid("BV repeat", "count must be positive")); + } + let width = self.arena.expect_bv(child, "BV repeat")?; + let out_width = width + .checked_mul(count) + .ok_or_else(|| Error::invalid("BV repeat", "width overflow"))?; + validate_bv_width(out_width, "BV repeat width")?; + if count == 1 { + return Ok(child); + } + if let Some((_, bytes)) = self.bv_const_value(child)? { + return self.bv_const_bytes(&repeat_bytes(&bytes, width, count), out_width); + } + self.arena + .add(NodeKind::BvRepeat { child, count }, Sort::Bv(out_width)) + } + + pub fn bv_rotate_left(&mut self, child: TermId, amount: u32) -> Result { + let width = self.arena.expect_bv(child, "BV rotate_left")?; + let amount = amount % width; + if amount == 0 { + return Ok(child); + } + if let Some((_, bytes)) = self.bv_const_value(child)? { + return self.bv_const_bytes(&rotate_left_bytes(&bytes, width, amount), width); + } + self.arena + .add(NodeKind::BvRotateLeft { child, amount }, Sort::Bv(width)) + } + + pub fn bv_rotate_right(&mut self, child: TermId, amount: u32) -> Result { + let width = self.arena.expect_bv(child, "BV rotate_right")?; + let amount = amount % width; + if amount == 0 { + return Ok(child); + } + if let Some((_, bytes)) = self.bv_const_value(child)? { + return self.bv_const_bytes(&rotate_right_bytes(&bytes, width, amount), width); + } + self.arena + .add(NodeKind::BvRotateRight { child, amount }, Sort::Bv(width)) + } + + pub fn bv_ite( + &mut self, + cond: TermId, + then_value: TermId, + else_value: TermId, + ) -> Result { + self.arena.expect_bool(cond, "BV ite condition")?; + let width = self + .arena + .expect_same_bv(then_value, else_value, "BV ite branches")?; + if then_value == else_value { + return Ok(then_value); + } + if let Some(value) = self.bool_const_value(cond)? { + return Ok(if value { then_value } else { else_value }); + } + self.arena.add( + NodeKind::BvIte { + cond, + then_value, + else_value, + }, + Sort::Bv(width), + ) + } + + pub fn bv_select(&mut self, cases: &[(TermId, TermId)], default: TermId) -> Result { + let width = self.arena.expect_bv(default, "BV select default")?; + for (selector, value) in cases { + self.arena.expect_bool(*selector, "BV select selector")?; + let value_width = self.arena.expect_bv(*value, "BV select value")?; + if value_width != width { + return Err(Error::invalid( + "BV select", + format!("value width {value_width} does not match default width {width}"), + )); + } + } + self.arena.add( + NodeKind::BvSelect { + cases: cases.to_vec(), + default, + }, + Sort::Bv(width), + ) + } + + pub fn bool_not(&mut self, x: TermId) -> Result { + self.arena.expect_bool(x, "Bool not")?; + if let Some(value) = self.bool_const_value(x)? { + return self.bool_const(!value); + } + if let NodeKind::BoolNot(inner) = &self.arena.node(x)?.kind { + return Ok(*inner); + } + self.arena.add(NodeKind::BoolNot(x), Sort::Bool) + } + + pub fn bool_and(&mut self, a: TermId, b: TermId) -> Result { + let (a, b) = order_pair(a, b); + self.arena.expect_bool(a, "Bool and")?; + self.arena.expect_bool(b, "Bool and")?; + if a == b { + return Ok(a); + } + if self.is_bool_negation_pair(a, b)? { + return self.bool_false(); + } + if let Some(value) = self.bool_const_value(a)? { + return Ok(if value { b } else { a }); + } + if let Some(value) = self.bool_const_value(b)? { + return Ok(if value { a } else { b }); + } + self.arena.add(NodeKind::BoolAnd(a, b), Sort::Bool) + } + + pub fn bool_or(&mut self, a: TermId, b: TermId) -> Result { + let (a, b) = order_pair(a, b); + self.arena.expect_bool(a, "Bool or")?; + self.arena.expect_bool(b, "Bool or")?; + if a == b { + return Ok(a); + } + if self.is_bool_negation_pair(a, b)? { + return self.bool_true(); + } + if let Some(value) = self.bool_const_value(a)? { + return Ok(if value { a } else { b }); + } + if let Some(value) = self.bool_const_value(b)? { + return Ok(if value { b } else { a }); + } + self.arena.add(NodeKind::BoolOr(a, b), Sort::Bool) + } + + pub fn bool_implies(&mut self, a: TermId, b: TermId) -> Result { + self.arena.expect_bool(a, "Bool implies")?; + self.arena.expect_bool(b, "Bool implies")?; + if a == b { + return self.bool_true(); + } + if let Some(value) = self.bool_const_value(a)? { + return if value { Ok(b) } else { self.bool_true() }; + } + if let Some(value) = self.bool_const_value(b)? { + return if value { + self.bool_true() + } else { + self.bool_not(a) + }; + } + self.arena.add(NodeKind::BoolImplies(a, b), Sort::Bool) + } + + pub fn bool_eq(&mut self, a: TermId, b: TermId) -> Result { + self.arena.expect_bool(a, "Bool eq")?; + self.arena.expect_bool(b, "Bool eq")?; + if a == b { + return self.bool_true(); + } + if self.is_bool_negation_pair(a, b)? { + return self.bool_false(); + } + if let (Some(av), Some(bv)) = (self.bool_const_value(a)?, self.bool_const_value(b)?) { + return self.bool_const(av == bv); + } + let (a, b) = order_pair(a, b); + self.arena.add(NodeKind::BoolEq(a, b), Sort::Bool) + } + + pub fn bool_xor(&mut self, a: TermId, b: TermId) -> Result { + let eq = self.bool_eq(a, b)?; + self.bool_not(eq) + } + + pub fn bool_ite( + &mut self, + cond: TermId, + then_value: TermId, + else_value: TermId, + ) -> Result { + self.arena.expect_bool(cond, "Bool ite condition")?; + self.arena.expect_bool(then_value, "Bool ite then")?; + self.arena.expect_bool(else_value, "Bool ite else")?; + if then_value == else_value { + return Ok(then_value); + } + if let Some(value) = self.bool_const_value(cond)? { + return Ok(if value { then_value } else { else_value }); + } + self.arena.add( + NodeKind::BoolIte { + cond, + then_value, + else_value, + }, + Sort::Bool, + ) + } + + pub fn bv_eq(&mut self, a: TermId, b: TermId) -> Result { + self.arena.expect_same_bv(a, b, "BV eq")?; + if a == b { + return self.bool_true(); + } + if self.is_bv_negation_pair(a, b)? { + return self.bool_false(); + } + if let Some(reduced) = self.one_bit_ite_const_equality(a, b)? { + return Ok(reduced); + } + if let Some(reduced) = self.one_bit_ite_const_equality(b, a)? { + return Ok(reduced); + } + if let Some(reduced) = self.extension_const_equality(a, b)? { + return Ok(reduced); + } + if let Some(reduced) = self.extension_const_equality(b, a)? { + return Ok(reduced); + } + if let (Some((_, av)), Some((_, bv))) = (self.bv_const_value(a)?, self.bv_const_value(b)?) { + return self.bool_const(av == bv); + } + if let Some(value) = self.polynomial_equality(a, b)? { + return self.bool_const(value); + } + let (a, b) = order_pair(a, b); + self.arena.add(NodeKind::BvEq(a, b), Sort::Bool) + } + + pub fn bv_ne(&mut self, a: TermId, b: TermId) -> Result { + let eq = self.bv_eq(a, b)?; + self.bool_not(eq) + } + + pub fn bv_ult(&mut self, a: TermId, b: TermId) -> Result { + self.bv_cmp(a, b, NodeKind::BvUlt) + } + + pub fn bv_ule(&mut self, a: TermId, b: TermId) -> Result { + self.bv_cmp(a, b, NodeKind::BvUle) + } + + pub fn bv_ugt(&mut self, a: TermId, b: TermId) -> Result { + self.bv_ult(b, a) + } + + pub fn bv_uge(&mut self, a: TermId, b: TermId) -> Result { + self.bv_ule(b, a) + } + + pub fn bv_slt(&mut self, a: TermId, b: TermId) -> Result { + self.bv_cmp(a, b, NodeKind::BvSlt) + } + + pub fn bv_sle(&mut self, a: TermId, b: TermId) -> Result { + self.bv_cmp(a, b, NodeKind::BvSle) + } + + pub fn bv_sgt(&mut self, a: TermId, b: TermId) -> Result { + self.bv_slt(b, a) + } + + pub fn bv_sge(&mut self, a: TermId, b: TermId) -> Result { + self.bv_sle(b, a) + } + + pub fn uadd_ovf(&mut self, a: TermId, b: TermId) -> Result { + self.overflow_binary(a, b, NodeKind::UAddOverflow) + } + + pub fn sadd_ovf(&mut self, a: TermId, b: TermId) -> Result { + self.overflow_binary(a, b, NodeKind::SAddOverflow) + } + + pub fn usub_ovf(&mut self, a: TermId, b: TermId) -> Result { + self.overflow_binary(a, b, NodeKind::USubOverflow) + } + + pub fn ssub_ovf(&mut self, a: TermId, b: TermId) -> Result { + self.overflow_binary(a, b, NodeKind::SSubOverflow) + } + + pub fn umul_ovf(&mut self, a: TermId, b: TermId) -> Result { + self.overflow_binary(a, b, NodeKind::UMulOverflow) + } + + pub fn smul_ovf(&mut self, a: TermId, b: TermId) -> Result { + self.overflow_binary(a, b, NodeKind::SMulOverflow) + } + + pub fn neg_ovf(&mut self, x: TermId) -> Result { + self.arena.expect_bv(x, "neg overflow")?; + self.arena.add(NodeKind::NegOverflow(x), Sort::Bool) + } + + pub fn sdiv_ovf(&mut self, a: TermId, b: TermId) -> Result { + self.overflow_binary(a, b, NodeKind::SDivOverflow) + } + + fn bv_unary(&mut self, x: TermId, f: fn(TermId) -> NodeKind) -> Result { + let width = self.arena.expect_bv(x, "BV unary")?; + self.arena.add(f(x), Sort::Bv(width)) + } + + fn bv_binary( + &mut self, + a: TermId, + b: TermId, + f: fn(TermId, TermId) -> NodeKind, + ) -> Result { + let width = self.arena.expect_same_bv(a, b, "BV binary")?; + self.arena.add(f(a, b), Sort::Bv(width)) + } + + fn bv_add_node(&mut self, a: TermId, b: TermId, width: u32) -> Result { + let (a, b) = order_pair(a, b); + self.arena.add(NodeKind::BvAdd(a, b), Sort::Bv(width)) + } + + fn bv_mul_node(&mut self, a: TermId, b: TermId, width: u32) -> Result { + let (a, b) = order_pair(a, b); + self.arena.add(NodeKind::BvMul(a, b), Sort::Bv(width)) + } + + fn canonical_bv_add(&mut self, a: TermId, b: TermId, width: u32) -> Result> { + let mut terms = Vec::new(); + self.collect_bv_add(a, &mut terms)?; + self.collect_bv_add(b, &mut terms)?; + let mut changed = terms.len() > 2; + let mut constant = vec![0u8; bytes_for_width(width)?]; + let mut non_const = Vec::new(); + for term in terms { + if let Some((_, bytes)) = self.bv_const_value(term)? { + constant = add_bytes(&constant, &bytes, width); + } else { + non_const.push(term); + } + } + let mut scaled = Vec::with_capacity(non_const.len()); + for term in non_const { + let (base, coefficient) = self.scaled_add_term(term, width)?; + scaled.push((base, coefficient)); + } + scaled.sort_unstable_by_key(|(base, _)| *base); + let mut grouped = Vec::with_capacity(scaled.len()); + let mut index = 0; + while index < scaled.len() { + let base = scaled[index].0; + let mut coefficient = vec![0u8; bytes_for_width(width)?]; + let mut count = 0usize; + while index + count < scaled.len() && scaled[index + count].0 == base { + coefficient = add_bytes(&coefficient, &scaled[index + count].1, width); + count += 1; + } + if count > 1 { + changed = true; + } + if coefficient.iter().any(|byte| *byte != 0) { + if is_one_bytes(&coefficient) { + grouped.push(base); + } else { + let coefficient = self.bv_const_bytes(&coefficient, width)?; + grouped.push(self.bv_mul(base, coefficient)?); + } + } + index += count; + } + if constant.iter().any(|byte| *byte != 0) { + grouped.push(self.bv_const_bytes(&constant, width)?); + } + if !changed { + return Ok(None); + } + Ok(Some(self.rebuild_bv_add(grouped, width)?)) + } + + fn canonical_bv_mul(&mut self, a: TermId, b: TermId, width: u32) -> Result> { + let mut terms = Vec::new(); + self.collect_bv_mul(a, &mut terms)?; + self.collect_bv_mul(b, &mut terms)?; + if terms.len() <= 2 { + return Ok(None); + } + let mut constant = vec![0u8; bytes_for_width(width)?]; + constant[0] = 1; + let mut non_const = Vec::new(); + for term in terms { + if let Some((_, bytes)) = self.bv_const_value(term)? { + if bytes.iter().all(|byte| *byte == 0) { + return Ok(Some(self.zero(width)?)); + } + constant = mul_bytes(&constant, &bytes, width); + } else { + non_const.push(term); + } + } + if !(constant.first().copied() == Some(1) && constant.iter().skip(1).all(|byte| *byte == 0)) + { + non_const.push(self.bv_const_bytes(&constant, width)?); + } + Ok(Some(self.rebuild_bv_mul(non_const, width)?)) + } + + fn polynomial_equality(&self, a: TermId, b: TermId) -> Result> { + let width = self.arena.expect_same_bv(a, b, "BV polynomial equality")?; + if width > 128 { + return Ok(None); + } + let mut budget = 512usize; + let Some(left) = self.polynomial(a, width, &mut budget)? else { + return Ok(None); + }; + let Some(right) = self.polynomial(b, width, &mut budget)? else { + return Ok(None); + }; + if left == right { + return Ok(Some(true)); + } + let diff = polynomial_sub(&left, &right, width); + if diff.is_empty() { + Ok(Some(true)) + } else if diff.keys().all(|monomial| monomial.is_empty()) { + Ok(Some(false)) + } else { + Ok(None) + } + } + + fn polynomial( + &self, + term: TermId, + width: u32, + budget: &mut usize, + ) -> Result> { + if *budget == 0 { + return Ok(None); + } + *budget -= 1; + if self.arena.expect_bv(term, "BV polynomial term")? != width { + return Ok(None); + } + match &self.arena.node(term)?.kind { + NodeKind::BvConst { bytes, .. } => Ok(Some(polynomial_constant(bytes.clone()))), + NodeKind::BvAdd(a, b) => { + let Some(left) = self.polynomial(*a, width, budget)? else { + return Ok(None); + }; + let Some(right) = self.polynomial(*b, width, budget)? else { + return Ok(None); + }; + Ok(Some(polynomial_add(&left, &right, width))) + } + NodeKind::BvSub(a, b) => { + let Some(left) = self.polynomial(*a, width, budget)? else { + return Ok(None); + }; + let Some(right) = self.polynomial(*b, width, budget)? else { + return Ok(None); + }; + Ok(Some(polynomial_sub(&left, &right, width))) + } + NodeKind::BvNeg(child) => { + let Some(poly) = self.polynomial(*child, width, budget)? else { + return Ok(None); + }; + Ok(Some(polynomial_neg(&poly, width))) + } + NodeKind::BvNot(child) => { + let Some(poly) = self.polynomial(*child, width, budget)? else { + return Ok(None); + }; + let mut minus = polynomial_neg(&poly, width); + let mut one = vec![0u8; bytes_for_width(width)?]; + one[0] = 1; + polynomial_add_assign(&mut minus, Vec::new(), neg_bytes(&one, width), width); + Ok(Some(minus)) + } + NodeKind::BvMul(a, b) => { + let Some(left) = self.polynomial(*a, width, budget)? else { + return Ok(None); + }; + let Some(right) = self.polynomial(*b, width, budget)? else { + return Ok(None); + }; + polynomial_mul(&left, &right, width) + } + NodeKind::BvShl(value, amount) => { + let Some(shift) = self.const_shift_amount(*amount, width)? else { + return Ok(Some(polynomial_atom(term, width)?)); + }; + if shift >= width as usize { + return Ok(Some(Polynomial::new())); + } + let Some(poly) = self.polynomial(*value, width, budget)? else { + return Ok(None); + }; + let mut factor = vec![0u8; bytes_for_width(width)?]; + set_bit(&mut factor, shift as u32); + Ok(Some(polynomial_scale(&poly, &factor, width))) + } + _ => Ok(Some(polynomial_atom(term, width)?)), + } + } + + fn collect_bv_add(&self, id: TermId, out: &mut Vec) -> Result<()> { + match &self.arena.node(id)?.kind { + NodeKind::BvAdd(a, b) => { + self.collect_bv_add(*a, out)?; + self.collect_bv_add(*b, out)?; + } + _ => out.push(id), + } + Ok(()) + } + + fn collect_bv_mul(&self, id: TermId, out: &mut Vec) -> Result<()> { + match &self.arena.node(id)?.kind { + NodeKind::BvMul(a, b) => { + self.collect_bv_mul(*a, out)?; + self.collect_bv_mul(*b, out)?; + } + _ => out.push(id), + } + Ok(()) + } + + fn scaled_add_term(&self, term: TermId, width: u32) -> Result<(TermId, Vec)> { + let mut one = vec![0u8; bytes_for_width(width)?]; + one[0] = 1; + let NodeKind::BvMul(a, b) = &self.arena.node(term)?.kind else { + return Ok((term, one)); + }; + if let Some((_, coefficient)) = self.bv_const_value(*a)? { + return Ok((*b, coefficient)); + } + if let Some((_, coefficient)) = self.bv_const_value(*b)? { + return Ok((*a, coefficient)); + } + Ok((term, one)) + } + + fn rebuild_bv_add(&mut self, mut terms: Vec, width: u32) -> Result { + if terms.is_empty() { + return self.zero(width); + } + terms.sort_unstable(); + let mut iter = terms.into_iter(); + let mut cur = iter.next().expect("non-empty terms"); + for next in iter { + cur = self.bv_add_node(cur, next, width)?; + } + Ok(cur) + } + + fn rebuild_bv_mul(&mut self, mut terms: Vec, width: u32) -> Result { + if terms.is_empty() { + return self.bv_const(1, width); + } + terms.sort_unstable(); + let mut iter = terms.into_iter(); + let mut cur = iter.next().expect("non-empty terms"); + for next in iter { + cur = self.bv_mul_node(cur, next, width)?; + } + Ok(cur) + } + + fn bv_cmp( + &mut self, + a: TermId, + b: TermId, + f: fn(TermId, TermId) -> NodeKind, + ) -> Result { + let width = self.arena.expect_same_bv(a, b, "BV comparison")?; + if a == b { + return match f(a, b) { + NodeKind::BvUlt(_, _) | NodeKind::BvSlt(_, _) => self.bool_false(), + NodeKind::BvUle(_, _) | NodeKind::BvSle(_, _) => self.bool_true(), + _ => unreachable!("comparison constructor returned non-comparison"), + }; + } + let comparison = f(a, b); + if matches!(comparison, NodeKind::BvUlt(_, _) | NodeKind::BvUle(_, _)) { + let strict = matches!(comparison, NodeKind::BvUlt(_, _)); + if let Some(reduced) = self.zero_extend_const_unsigned_cmp(a, b, strict, true)? { + return Ok(reduced); + } + if let Some(reduced) = self.zero_extend_const_unsigned_cmp(b, a, strict, false)? { + return Ok(reduced); + } + } + if let (Some((_, av)), Some((_, bv))) = (self.bv_const_value(a)?, self.bv_const_value(b)?) { + let value = match comparison { + NodeKind::BvUlt(_, _) => cmp_unsigned_bytes(&av, &bv) == Ordering::Less, + NodeKind::BvUle(_, _) => cmp_unsigned_bytes(&av, &bv) != Ordering::Greater, + NodeKind::BvSlt(_, _) => signed_less_than(&av, &bv, width), + NodeKind::BvSle(_, _) => signed_less_than(&av, &bv, width) || av == bv, + _ => unreachable!("comparison constructor returned non-comparison"), + }; + return self.bool_const(value); + } + self.arena.add(comparison, Sort::Bool) + } + + fn overflow_binary( + &mut self, + a: TermId, + b: TermId, + f: fn(TermId, TermId) -> NodeKind, + ) -> Result { + self.arena.expect_same_bv(a, b, "overflow predicate")?; + self.arena.add(f(a, b), Sort::Bool) + } + + fn one_bit_ite_const_equality( + &mut self, + ite: TermId, + constant: TermId, + ) -> Result> { + let Some((const_width, const_bytes)) = self.bv_const_value(constant)? else { + return Ok(None); + }; + if const_width != 1 { + return Ok(None); + } + let Some((cond, then_value, else_value)) = self.bv_ite_parts(ite)? else { + return Ok(None); + }; + let Some(then_bit) = self.one_bit_const_value(then_value)? else { + return Ok(None); + }; + let Some(else_bit) = self.one_bit_const_value(else_value)? else { + return Ok(None); + }; + let expected = (const_bytes[0] & 1) != 0; + if then_bit == expected && else_bit != expected { + Ok(Some(cond)) + } else if then_bit != expected && else_bit == expected { + Ok(Some(self.bool_not(cond)?)) + } else { + Ok(Some(self.bool_const(then_bit == expected)?)) + } + } + + fn extension_const_equality( + &mut self, + extended: TermId, + constant: TermId, + ) -> Result> { + let Some((const_width, const_bytes)) = self.bv_const_value(constant)? else { + return Ok(None); + }; + let Some((child, child_width, extra, signed)) = self.extension_parts(extended)? else { + return Ok(None); + }; + if const_width != child_width + extra { + return Ok(None); + } + let child_bytes = extract_bytes(&const_bytes, 0, child_width); + let in_range = if signed { + sext_bytes(&child_bytes, child_width, extra) == const_bytes + } else { + high_bits_zero(&const_bytes, child_width, const_width) + }; + if !in_range { + return Ok(Some(self.bool_false()?)); + } + let child_const = self.bv_const_bytes(&child_bytes, child_width)?; + Ok(Some(self.bv_eq(child, child_const)?)) + } + + fn zero_extend_const_unsigned_cmp( + &mut self, + extended: TermId, + constant: TermId, + strict: bool, + extended_on_left: bool, + ) -> Result> { + let Some((const_width, const_bytes)) = self.bv_const_value(constant)? else { + return Ok(None); + }; + let Some((child, child_width, extra, signed)) = self.extension_parts(extended)? else { + return Ok(None); + }; + if signed || const_width != child_width + extra { + return Ok(None); + } + let high_zero = high_bits_zero(&const_bytes, child_width, const_width); + let const_zero = const_bytes.iter().all(|byte| *byte == 0); + let const_low = extract_bytes(&const_bytes, 0, child_width); + let low_const = self.bv_const_bytes(&const_low, child_width)?; + if extended_on_left { + if strict { + if const_zero { + return Ok(Some(self.bool_false()?)); + } + if !high_zero { + return Ok(Some(self.bool_true()?)); + } + Ok(Some(self.bv_ult(child, low_const)?)) + } else { + if !high_zero { + return Ok(Some(self.bool_true()?)); + } + Ok(Some(self.bv_ule(child, low_const)?)) + } + } else if !high_zero { + Ok(Some(self.bool_false()?)) + } else if strict { + if self.is_all_ones(low_const)? { + return Ok(Some(self.bool_false()?)); + } + Ok(Some(self.bv_ult(low_const, child)?)) + } else { + if const_zero { + return Ok(Some(self.bool_true()?)); + } + Ok(Some(self.bv_ule(low_const, child)?)) + } + } + + fn bv_ite_parts(&self, id: TermId) -> Result> { + Ok(match &self.arena.node(id)?.kind { + NodeKind::BvIte { + cond, + then_value, + else_value, + } => Some((*cond, *then_value, *else_value)), + _ => None, + }) + } + + fn extension_parts(&self, id: TermId) -> Result> { + Ok(match &self.arena.node(id)?.kind { + NodeKind::BvZeroExtend { child, extra } => { + let width = self.arena.expect_bv(*child, "BV zero extension child")?; + Some((*child, width, *extra, false)) + } + NodeKind::BvSignExtend { child, extra } => { + let width = self.arena.expect_bv(*child, "BV sign extension child")?; + Some((*child, width, *extra, true)) + } + _ => None, + }) + } + + fn one_bit_const_value(&self, id: TermId) -> Result> { + Ok(match &self.arena.node(id)?.kind { + NodeKind::BvConst { width: 1, bytes } => Some((bytes[0] & 1) != 0), + _ => None, + }) + } + + fn bool_const_value(&self, id: TermId) -> Result> { + Ok(match &self.arena.node(id)?.kind { + NodeKind::BoolConst(value) => Some(*value), + _ => None, + }) + } + + fn bv_const_value(&self, id: TermId) -> Result)>> { + Ok(match &self.arena.node(id)?.kind { + NodeKind::BvConst { width, bytes } => Some((*width, bytes.clone())), + _ => None, + }) + } + + fn is_zero(&self, id: TermId) -> Result { + Ok(self + .bv_const_value(id)? + .is_some_and(|(_, bytes)| bytes.iter().all(|byte| *byte == 0))) + } + + fn is_one(&self, id: TermId) -> Result { + Ok(self.bv_const_value(id)?.is_some_and(|(_, bytes)| { + bytes.first().copied() == Some(1) && bytes.iter().skip(1).all(|byte| *byte == 0) + })) + } + + fn is_all_ones(&self, id: TermId) -> Result { + Ok(self.bv_const_value(id)?.is_some_and(|(width, bytes)| { + let mut ones = vec![0xff; bytes.len()]; + mask_unused_high_bits(&mut ones, width); + bytes == ones + })) + } + + fn is_bool_negation_pair(&self, a: TermId, b: TermId) -> Result { + Ok( + matches!(self.arena.node(a)?.kind, NodeKind::BoolNot(inner) if inner == b) + || matches!(self.arena.node(b)?.kind, NodeKind::BoolNot(inner) if inner == a), + ) + } + + fn is_bv_negation_pair(&self, a: TermId, b: TermId) -> Result { + Ok( + matches!(self.arena.node(a)?.kind, NodeKind::BvNot(inner) if inner == b) + || matches!(self.arena.node(b)?.kind, NodeKind::BvNot(inner) if inner == a), + ) + } + + fn bv_shl_parts(&self, id: TermId) -> Result> { + Ok(match &self.arena.node(id)?.kind { + NodeKind::BvShl(value, amount) => Some((*value, *amount)), + _ => None, + }) + } + + fn bv_extract_parts(&self, id: TermId) -> Result> { + Ok(match &self.arena.node(id)?.kind { + NodeKind::BvExtract { child, high, low } => Some((*child, *high, *low)), + _ => None, + }) + } + + fn shifted_same_add_parts(&self, a: TermId, b: TermId) -> Result> { + if let Some((value, amount)) = self.bv_shl_parts(a)? { + if value == b && self.bv_const_value(value)?.is_none() { + return Ok(Some((value, amount))); + } + } + if let Some((value, amount)) = self.bv_shl_parts(b)? { + if value == a && self.bv_const_value(value)?.is_none() { + return Ok(Some((value, amount))); + } + } + Ok(None) + } + + fn const_shift_amount(&self, id: TermId, width: u32) -> Result> { + let Some((_, bytes)) = self.bv_const_value(id)? else { + return Ok(None); + }; + Ok(Some(bytes_to_bounded_usize(&bytes, width as usize))) + } + + fn zero(&mut self, width: u32) -> Result { + self.bv_const_bytes(&vec![0; bytes_for_width(width)?], width) + } + + fn all_ones(&mut self, width: u32) -> Result { + let mut bytes = vec![0xff; bytes_for_width(width)?]; + mask_unused_high_bits(&mut bytes, width); + self.bv_const_bytes(&bytes, width) + } +} + +fn polynomial_constant(bytes: Vec) -> Polynomial { + let mut polynomial = Polynomial::new(); + if bytes.iter().any(|byte| *byte != 0) { + polynomial.insert(Vec::new(), bytes); + } + polynomial +} + +fn polynomial_atom(term: TermId, width: u32) -> Result { + let mut coefficient = vec![0u8; bytes_for_width(width)?]; + coefficient[0] = 1; + let mut polynomial = Polynomial::new(); + polynomial.insert(vec![term], coefficient); + Ok(polynomial) +} + +fn polynomial_add(left: &Polynomial, right: &Polynomial, width: u32) -> Polynomial { + let mut out = left.clone(); + for (monomial, coefficient) in right { + polynomial_add_assign(&mut out, monomial.clone(), coefficient.clone(), width); + } + out +} + +fn polynomial_sub(left: &Polynomial, right: &Polynomial, width: u32) -> Polynomial { + let mut out = left.clone(); + for (monomial, coefficient) in right { + polynomial_add_assign( + &mut out, + monomial.clone(), + neg_bytes(coefficient, width), + width, + ); + } + out +} + +fn polynomial_neg(poly: &Polynomial, width: u32) -> Polynomial { + let mut out = Polynomial::new(); + for (monomial, coefficient) in poly { + polynomial_add_assign( + &mut out, + monomial.clone(), + neg_bytes(coefficient, width), + width, + ); + } + out +} + +fn polynomial_scale(poly: &Polynomial, factor: &[u8], width: u32) -> Polynomial { + if factor.iter().all(|byte| *byte == 0) { + return Polynomial::new(); + } + let mut out = Polynomial::new(); + for (monomial, coefficient) in poly { + polynomial_add_assign( + &mut out, + monomial.clone(), + mul_bytes(coefficient, factor, width), + width, + ); + } + out +} + +fn polynomial_mul(left: &Polynomial, right: &Polynomial, width: u32) -> Result> { + let mut out = Polynomial::new(); + for (left_monomial, left_coefficient) in left { + for (right_monomial, right_coefficient) in right { + let mut monomial = Vec::with_capacity(left_monomial.len() + right_monomial.len()); + monomial.extend_from_slice(left_monomial); + monomial.extend_from_slice(right_monomial); + monomial.sort_unstable(); + if monomial.len() > 16 { + return Ok(None); + } + let coefficient = mul_bytes(left_coefficient, right_coefficient, width); + polynomial_add_assign(&mut out, monomial, coefficient, width); + if out.len() > 512 { + return Ok(None); + } + } + } + Ok(Some(out)) +} + +fn polynomial_add_assign( + polynomial: &mut Polynomial, + monomial: Monomial, + coefficient: Vec, + width: u32, +) { + if coefficient.iter().all(|byte| *byte == 0) { + return; + } + let updated = if let Some(existing) = polynomial.get(&monomial) { + add_bytes(existing, &coefficient, width) + } else { + coefficient + }; + if updated.iter().all(|byte| *byte == 0) { + polynomial.remove(&monomial); + } else { + polynomial.insert(monomial, updated); + } +} + +fn order_pair(a: TermId, b: TermId) -> (TermId, TermId) { + if a <= b { + (a, b) + } else { + (b, a) + } +} + +pub(crate) fn add_bytes(a: &[u8], b: &[u8], width: u32) -> Vec { + let mut out = Vec::with_capacity(a.len()); + let mut carry = 0u16; + for (&x, &y) in a.iter().zip(b) { + let sum = u16::from(x) + u16::from(y) + carry; + out.push(sum as u8); + carry = sum >> 8; + } + mask_unused_high_bits(&mut out, width); + out +} + +pub(crate) fn sub_bytes(a: &[u8], b: &[u8], width: u32) -> Vec { + let neg_b = neg_bytes(b, width); + add_bytes(a, &neg_b, width) +} + +pub(crate) fn neg_bytes(bytes: &[u8], width: u32) -> Vec { + let mut out = bytes.iter().map(|byte| !byte).collect::>(); + let mut carry = 1u16; + for byte in &mut out { + let sum = u16::from(*byte) + carry; + *byte = sum as u8; + carry = sum >> 8; + if carry == 0 { + break; + } + } + mask_unused_high_bits(&mut out, width); + out +} + +pub(crate) fn mul_bytes(a: &[u8], b: &[u8], width: u32) -> Vec { + let mut out = vec![0u8; a.len()]; + for (i, &x) in a.iter().enumerate() { + let mut carry = 0u16; + for (j, &y) in b.iter().enumerate() { + let Some(slot) = out.get_mut(i + j) else { + break; + }; + let product = u16::from(*slot) + u16::from(x) * u16::from(y) + carry; + *slot = product as u8; + carry = product >> 8; + } + } + mask_unused_high_bits(&mut out, width); + out +} + +pub(crate) fn get_bit(bytes: &[u8], bit: u32) -> bool { + bytes + .get((bit / 8) as usize) + .is_some_and(|byte| ((byte >> (bit % 8)) & 1) != 0) +} + +pub(crate) fn set_bit(bytes: &mut [u8], bit: u32) { + if let Some(byte) = bytes.get_mut((bit / 8) as usize) { + *byte |= 1 << (bit % 8); + } +} + +fn high_bits_zero(bytes: &[u8], from: u32, width: u32) -> bool { + (from..width).all(|bit| !get_bit(bytes, bit)) +} + +pub(crate) fn extract_bytes(bytes: &[u8], low: u32, width: u32) -> Vec { + let mut out = vec![0u8; (width as usize).div_ceil(8)]; + for bit in 0..width { + if get_bit(bytes, low + bit) { + set_bit(&mut out, bit); + } + } + mask_unused_high_bits(&mut out, width); + out +} + +pub(crate) fn concat_bytes(high: &[u8], high_width: u32, low: &[u8], low_width: u32) -> Vec { + let width = high_width + low_width; + let mut out = vec![0u8; (width as usize).div_ceil(8)]; + for bit in 0..low_width { + if get_bit(low, bit) { + set_bit(&mut out, bit); + } + } + for bit in 0..high_width { + if get_bit(high, bit) { + set_bit(&mut out, low_width + bit); + } + } + mask_unused_high_bits(&mut out, width); + out +} + +pub(crate) fn sext_bytes(bytes: &[u8], width: u32, extra: u32) -> Vec { + let out_width = width + extra; + let mut out = bytes.to_vec(); + out.resize((out_width as usize).div_ceil(8), 0); + if get_bit(bytes, width - 1) { + for bit in width..out_width { + set_bit(&mut out, bit); + } + } + mask_unused_high_bits(&mut out, out_width); + out +} + +pub(crate) fn repeat_bytes(bytes: &[u8], width: u32, count: u32) -> Vec { + let out_width = width * count; + let mut out = vec![0u8; (out_width as usize).div_ceil(8)]; + for copy in 0..count { + let offset = copy * width; + for bit in 0..width { + if get_bit(bytes, bit) { + set_bit(&mut out, offset + bit); + } + } + } + mask_unused_high_bits(&mut out, out_width); + out +} + +pub(crate) fn rotate_left_bytes(bytes: &[u8], width: u32, amount: u32) -> Vec { + let mut out = vec![0u8; (width as usize).div_ceil(8)]; + for bit in 0..width { + let source = (bit + width - amount) % width; + if get_bit(bytes, source) { + set_bit(&mut out, bit); + } + } + out +} + +pub(crate) fn rotate_right_bytes(bytes: &[u8], width: u32, amount: u32) -> Vec { + let mut out = vec![0u8; (width as usize).div_ceil(8)]; + for bit in 0..width { + let source = (bit + amount) % width; + if get_bit(bytes, source) { + set_bit(&mut out, bit); + } + } + out +} + +pub(crate) fn shl_bytes(bytes: &[u8], width: u32, amount: usize) -> Vec { + let mut out = vec![0u8; (width as usize).div_ceil(8)]; + for bit in amount..width as usize { + if get_bit(bytes, (bit - amount) as u32) { + set_bit(&mut out, bit as u32); + } + } + mask_unused_high_bits(&mut out, width); + out +} + +pub(crate) fn lshr_bytes(bytes: &[u8], width: u32, amount: usize) -> Vec { + let mut out = vec![0u8; (width as usize).div_ceil(8)]; + let width_usize = width as usize; + for bit in 0..width_usize.saturating_sub(amount) { + if get_bit(bytes, (bit + amount) as u32) { + set_bit(&mut out, bit as u32); + } + } + out +} + +pub(crate) fn ashr_bytes(bytes: &[u8], width: u32, amount: usize) -> Vec { + let mut out = lshr_bytes(bytes, width, amount.min(width as usize)); + if get_bit(bytes, width - 1) { + let start = width.saturating_sub(amount.min(width as usize) as u32); + for bit in start..width { + set_bit(&mut out, bit); + } + } + mask_unused_high_bits(&mut out, width); + out +} + +pub(crate) fn bytes_to_bounded_usize(bytes: &[u8], bound: usize) -> usize { + let mut value = 0usize; + for (byte_index, byte) in bytes.iter().copied().enumerate() { + if byte == 0 { + continue; + } + for bit_in_byte in 0..8 { + if ((byte >> bit_in_byte) & 1) == 0 { + continue; + } + let bit = byte_index * 8 + bit_in_byte; + if bit >= usize::BITS as usize { + return bound; + } + value = value.saturating_add(1usize << bit); + if value >= bound { + return bound; + } + } + } + value +} + +fn is_one_bytes(bytes: &[u8]) -> bool { + bytes.first().copied() == Some(1) && bytes.iter().skip(1).all(|byte| *byte == 0) +} + +pub(crate) fn cmp_unsigned_bytes(a: &[u8], b: &[u8]) -> Ordering { + for (&av, &bv) in a.iter().zip(b).rev() { + match av.cmp(&bv) { + Ordering::Equal => {} + non_eq => return non_eq, + } + } + Ordering::Equal +} + +pub(crate) fn signed_less_than(a: &[u8], b: &[u8], width: u32) -> bool { + let sign_a = get_bit(a, width - 1); + let sign_b = get_bit(b, width - 1); + match (sign_a, sign_b) { + (true, false) => true, + (false, true) => false, + _ => cmp_unsigned_bytes(a, b) == Ordering::Less, + } +} + +``` + +`crates/qfbvsmtrs/src/circuits.rs`: + +```rs +use crate::gates::{GateArena, GateId}; + +pub type Bits = Vec; // LSB first + +pub fn zero(width: usize, gates: &GateArena) -> Bits { + vec![gates.false_gate(); width] +} + +pub fn ones(width: usize, gates: &GateArena) -> Bits { + vec![gates.true_gate(); width] +} + +pub fn bitwise_not(gates: &mut GateArena, x: &[GateId]) -> Bits { + x.iter().map(|&b| gates.not(b)).collect() +} + +pub fn bitwise_binary( + gates: &mut GateArena, + a: &[GateId], + b: &[GateId], + f: fn(&mut GateArena, GateId, GateId) -> GateId, +) -> Bits { + a.iter().zip(b).map(|(&x, &y)| f(gates, x, y)).collect() +} + +pub fn bv_and(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> Bits { + bitwise_binary(gates, a, b, GateArena::and) +} + +pub fn bv_or(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> Bits { + bitwise_binary(gates, a, b, GateArena::or) +} + +pub fn bv_xor(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> Bits { + bitwise_binary(gates, a, b, GateArena::xor) +} + +pub fn add(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> Bits { + let carry_in = gates.false_gate(); + add_with_carry(gates, a, b, carry_in).0 +} + +pub fn add_with_carry( + gates: &mut GateArena, + a: &[GateId], + b: &[GateId], + carry_in: GateId, +) -> (Bits, GateId) { + let mut carry = carry_in; + let mut out = Vec::with_capacity(a.len()); + for (&x, &y) in a.iter().zip(b) { + let axb = gates.xor(x, y); + let sum = gates.xor(axb, carry); + let xy = gates.and(x, y); + let c_and_axb = gates.and(carry, axb); + carry = gates.or(xy, c_and_axb); + out.push(sum); + } + (out, carry) +} + +pub fn sub(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> Bits { + let not_b = bitwise_not(gates, b); + let carry_in = gates.true_gate(); + add_with_carry(gates, a, ¬_b, carry_in).0 +} + +pub fn neg(gates: &mut GateArena, x: &[GateId]) -> Bits { + let zeros = zero(x.len(), gates); + sub(gates, &zeros, x) +} + +pub fn mul(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> Bits { + mul_width(gates, a, b, a.len()) +} + +pub fn mul_width(gates: &mut GateArena, a: &[GateId], b: &[GateId], width: usize) -> Bits { + let mut acc = zero(width, gates); + for (shift, &sel) in b.iter().enumerate() { + let mut partial = zero(width, gates); + for (i, &abit) in a.iter().enumerate() { + let out_index = i + shift; + if out_index < width { + partial[out_index] = gates.and(abit, sel); + } + } + acc = add(gates, &acc, &partial); + } + acc +} + +pub fn eq_bits(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + let xnor = a + .iter() + .zip(b) + .map(|(&x, &y)| gates.xnor(x, y)) + .collect::>(); + gates.and_many(xnor) +} + +pub fn is_zero(gates: &mut GateArena, x: &[GateId]) -> GateId { + let any = gates.or_many(x.iter().copied()); + gates.not(any) +} + +pub fn ult(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + let mut lt = gates.false_gate(); + let mut eq = gates.true_gate(); + for (&x, &y) in a.iter().zip(b).rev() { + let not_x = gates.not(x); + let x_lt_y = gates.and(not_x, y); + let eq_and_lt = gates.and(eq, x_lt_y); + lt = gates.or(lt, eq_and_lt); + let same = gates.xnor(x, y); + eq = gates.and(eq, same); + } + lt +} + +pub fn ule(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + let b_lt_a = ult(gates, b, a); + gates.not(b_lt_a) +} + +pub fn slt(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + let sign_a = *a.last().expect("non-empty bv"); + let sign_b = *b.last().expect("non-empty bv"); + let signs_differ = gates.xor(sign_a, sign_b); + let unsigned_lt = ult(gates, a, b); + gates.mux(signs_differ, sign_a, unsigned_lt) +} + +pub fn sle(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + let b_lt_a = slt(gates, b, a); + gates.not(b_lt_a) +} + +pub fn shl(gates: &mut GateArena, value: &[GateId], amount: &[GateId]) -> Bits { + let n = value.len(); + let mut out = value.to_vec(); + for (stage, &sel) in amount.iter().enumerate() { + let shift = 1usize.checked_shl(stage as u32).unwrap_or(usize::MAX); + let mut shifted = vec![gates.false_gate(); n]; + if shift < n { + shifted[shift..n].copy_from_slice(&out[..(n - shift)]); + } + out = mux_bits(gates, sel, &shifted, &out); + } + out +} + +pub fn lshr(gates: &mut GateArena, value: &[GateId], amount: &[GateId]) -> Bits { + let n = value.len(); + let mut out = value.to_vec(); + for (stage, &sel) in amount.iter().enumerate() { + let shift = 1usize.checked_shl(stage as u32).unwrap_or(usize::MAX); + let mut shifted = vec![gates.false_gate(); n]; + if shift < n { + shifted[..(n - shift)].copy_from_slice(&out[shift..n]); + } + out = mux_bits(gates, sel, &shifted, &out); + } + out +} + +pub fn ashr(gates: &mut GateArena, value: &[GateId], amount: &[GateId]) -> Bits { + let n = value.len(); + let mut out = value.to_vec(); + for (stage, &sel) in amount.iter().enumerate() { + let shift = 1usize.checked_shl(stage as u32).unwrap_or(usize::MAX); + let sign = *out.last().expect("non-empty bv"); + let mut shifted = vec![sign; n]; + if shift < n { + shifted[..(n - shift)].copy_from_slice(&out[shift..n]); + } + out = mux_bits(gates, sel, &shifted, &out); + } + out +} + +pub fn concat(high: &[GateId], low: &[GateId]) -> Bits { + let mut out = Vec::with_capacity(high.len() + low.len()); + out.extend_from_slice(low); + out.extend_from_slice(high); + out +} + +pub fn extract(value: &[GateId], high: u32, low: u32) -> Bits { + value[low as usize..=high as usize].to_vec() +} + +pub fn zext(gates: &GateArena, value: &[GateId], extra: u32) -> Bits { + let mut out = value.to_vec(); + out.extend(std::iter::repeat_n(gates.false_gate(), extra as usize)); + out +} + +pub fn sext(value: &[GateId], extra: u32) -> Bits { + let mut out = value.to_vec(); + let sign = *value.last().expect("non-empty bv"); + out.extend(std::iter::repeat_n(sign, extra as usize)); + out +} + +pub fn repeat(value: &[GateId], count: u32) -> Bits { + let mut out = Vec::with_capacity(value.len() * count as usize); + for _ in 0..count { + out.extend_from_slice(value); + } + out +} + +pub fn rotate_left(value: &[GateId], amount: u32) -> Bits { + let n = value.len(); + let amount = amount as usize % n; + (0..n).map(|i| value[(i + n - amount) % n]).collect() +} + +pub fn rotate_right(value: &[GateId], amount: u32) -> Bits { + let n = value.len(); + let amount = amount as usize % n; + (0..n).map(|i| value[(i + amount) % n]).collect() +} + +pub fn mux_bits(gates: &mut GateArena, sel: GateId, t: &[GateId], f: &[GateId]) -> Bits { + t.iter() + .zip(f) + .map(|(&tb, &fb)| gates.mux(sel, tb, fb)) + .collect() +} + +pub fn udiv_urem(gates: &mut GateArena, dividend: &[GateId], divisor: &[GateId]) -> (Bits, Bits) { + let n = dividend.len(); + let mut rem = zero(n, gates); + let mut quo = zero(n, gates); + for i in (0..n).rev() { + let mut shifted = vec![gates.false_gate(); n]; + shifted[0] = dividend[i]; + if n > 1 { + shifted[1..n].copy_from_slice(&rem[..(n - 1)]); + } + rem = shifted; + let ge = ule(gates, divisor, &rem); + let rem_minus_divisor = sub(gates, &rem, divisor); + rem = mux_bits(gates, ge, &rem_minus_divisor, &rem); + quo[i] = ge; + } + + let divisor_zero = is_zero(gates, divisor); + let all_ones = ones(n, gates); + let quotient = mux_bits(gates, divisor_zero, &all_ones, &quo); + let remainder = mux_bits(gates, divisor_zero, dividend, &rem); + (quotient, remainder) +} + +pub fn sdiv(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> Bits { + let sign_a = *a.last().expect("non-empty bv"); + let sign_b = *b.last().expect("non-empty bv"); + let abs_a = abs_bits(gates, a); + let abs_b = abs_bits(gates, b); + let (q, _) = udiv_urem(gates, &abs_a, &abs_b); + let neg_q = neg(gates, &q); + let signs_differ = gates.xor(sign_a, sign_b); + mux_bits(gates, signs_differ, &neg_q, &q) +} + +pub fn srem(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> Bits { + let sign_a = *a.last().expect("non-empty bv"); + let abs_a = abs_bits(gates, a); + let abs_b = abs_bits(gates, b); + let (_, r) = udiv_urem(gates, &abs_a, &abs_b); + let neg_r = neg(gates, &r); + mux_bits(gates, sign_a, &neg_r, &r) +} + +pub fn smod(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> Bits { + let sign_a = *a.last().expect("non-empty bv"); + let sign_b = *b.last().expect("non-empty bv"); + let abs_a = abs_bits(gates, a); + let abs_b = abs_bits(gates, b); + let (_, u) = udiv_urem(gates, &abs_a, &abs_b); + let u_zero = is_zero(gates, &u); + let neg_u = neg(gates, &u); + let b_minus_u = sub(gates, b, &u); + let b_plus_u = add(gates, b, &u); + + // SMT-LIB bvsmod definition: + // if u = 0 -> u + // else if a>=0 && b>=0 -> u + // else if a<0 && b>=0 -> b - u + // else if a>=0 && b<0 -> b + u + // else -u + let pos_pos = u.clone(); + let neg_pos = b_minus_u; + let pos_neg = b_plus_u; + let neg_neg = neg_u; + let when_b_nonneg = mux_bits(gates, sign_a, &neg_pos, &pos_pos); + let when_b_neg = mux_bits(gates, sign_a, &neg_neg, &pos_neg); + let nonzero_result = mux_bits(gates, sign_b, &when_b_neg, &when_b_nonneg); + mux_bits(gates, u_zero, &u, &nonzero_result) +} + +pub fn uadd_overflow(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + let carry_in = gates.false_gate(); + add_with_carry(gates, a, b, carry_in).1 +} + +pub fn usub_overflow(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + ult(gates, a, b) +} + +pub fn sadd_overflow(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + let sum = add(gates, a, b); + let sa = *a.last().expect("non-empty bv"); + let sb = *b.last().expect("non-empty bv"); + let ss = *sum.last().expect("non-empty bv"); + let same_ab = gates.xnor(sa, sb); + let diff_sum = gates.xor(sa, ss); + gates.and(same_ab, diff_sum) +} + +pub fn ssub_overflow(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + let diff = sub(gates, a, b); + let sa = *a.last().expect("non-empty bv"); + let sb = *b.last().expect("non-empty bv"); + let sd = *diff.last().expect("non-empty bv"); + let diff_ab = gates.xor(sa, sb); + let diff_res = gates.xor(sa, sd); + gates.and(diff_ab, diff_res) +} + +pub fn umul_overflow(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + let n = a.len(); + let mut aw = a.to_vec(); + aw.extend(std::iter::repeat_n(gates.false_gate(), n)); + let mut bw = b.to_vec(); + bw.extend(std::iter::repeat_n(gates.false_gate(), n)); + let product = mul_width(gates, &aw, &bw, n * 2); + gates.or_many(product[n..].iter().copied()) +} + +pub fn smul_overflow(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + let n = a.len(); + let aw = sext(a, n as u32); + let bw = sext(b, n as u32); + let product = mul_width(gates, &aw, &bw, n * 2); + let low = product[..n].to_vec(); + let sign_extended_low = sext(&low, n as u32); + let equal = eq_bits(gates, &product, &sign_extended_low); + gates.not(equal) +} + +pub fn neg_overflow(gates: &mut GateArena, x: &[GateId]) -> GateId { + let n = x.len(); + let mut min = vec![gates.false_gate(); n]; + min[n - 1] = gates.true_gate(); + eq_bits(gates, x, &min) +} + +pub fn sdiv_overflow(gates: &mut GateArena, a: &[GateId], b: &[GateId]) -> GateId { + let n = a.len(); + let mut min = vec![gates.false_gate(); n]; + min[n - 1] = gates.true_gate(); + let minus_one = vec![gates.true_gate(); n]; + let a_min = eq_bits(gates, a, &min); + let b_minus_one = eq_bits(gates, b, &minus_one); + gates.and(a_min, b_minus_one) +} + +fn abs_bits(gates: &mut GateArena, x: &[GateId]) -> Bits { + let sign = *x.last().expect("non-empty bv"); + let neg_x = neg(gates, x); + mux_bits(gates, sign, &neg_x, x) +} + +``` + +`crates/qfbvsmtrs/src/cnf.rs`: + +```rs +use std::time::Instant; + +use crate::error::{Error, Result}; +use crate::gates::{GateArena, GateId, GateKind}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct CnfVar(pub usize); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Lit(pub i32); + +impl Lit { + pub fn positive(var: CnfVar) -> Self { + Self(var.0 as i32) + } + + pub fn negative(var: CnfVar) -> Self { + Self(-(var.0 as i32)) + } + + pub fn raw(self) -> i32 { + self.0 + } +} + +#[derive(Debug, Clone)] +pub struct Cnf { + pub num_vars: usize, + pub clauses: Vec>, +} + +pub fn encode(gates: &GateArena, assertion: GateId) -> Cnf { + encode_with_deadline(gates, assertion, None) + .expect("CNF encoding without deadline cannot time out") +} + +pub fn encode_with_deadline( + gates: &GateArena, + assertion: GateId, + deadline: Option, +) -> Result { + let num_vars = gates.gates().len(); + let mut clauses = Vec::new(); + for (index, gate) in gates.gates().iter().enumerate() { + if index % 4096 == 0 && deadline.is_some_and(|deadline| Instant::now() >= deadline) { + return Err(Error::Timeout); + } + let g = (index + 1) as i32; + match *gate { + GateKind::Const(value) => { + push_clause(&mut clauses, vec![if value { g } else { -g }]); + } + GateKind::Input(_) => {} + GateKind::Not(a) => { + let a = lit(a); + push_clause(&mut clauses, vec![g, a]); + push_clause(&mut clauses, vec![-g, -a]); + } + GateKind::And(a, b) => { + let a = lit(a); + let b = lit(b); + push_clause(&mut clauses, vec![-g, a]); + push_clause(&mut clauses, vec![-g, b]); + push_clause(&mut clauses, vec![g, -a, -b]); + } + GateKind::Or(a, b) => { + let a = lit(a); + let b = lit(b); + push_clause(&mut clauses, vec![g, -a]); + push_clause(&mut clauses, vec![g, -b]); + push_clause(&mut clauses, vec![-g, a, b]); + } + GateKind::Xor(a, b) => { + let a = lit(a); + let b = lit(b); + push_clause(&mut clauses, vec![-a, -b, -g]); + push_clause(&mut clauses, vec![-a, b, g]); + push_clause(&mut clauses, vec![a, -b, g]); + push_clause(&mut clauses, vec![a, b, -g]); + } + GateKind::Mux { sel, t, f } => { + let s = lit(sel); + let t = lit(t); + let f_lit = lit(f); + push_clause(&mut clauses, vec![-s, -t, g]); + push_clause(&mut clauses, vec![-s, t, -g]); + push_clause(&mut clauses, vec![s, -f_lit, g]); + push_clause(&mut clauses, vec![s, f_lit, -g]); + } + } + } + push_clause(&mut clauses, vec![lit(assertion)]); + Ok(Cnf { num_vars, clauses }) +} + +fn push_clause(clauses: &mut Vec>, mut clause: Vec) { + clause.sort_unstable_by_key(|lit| lit.abs()); + clause.dedup(); + for pair in clause.windows(2) { + if pair[0].abs() == pair[1].abs() && pair[0] == -pair[1] { + return; + } + } + clauses.push(clause); +} + +pub fn lit(gate: GateId) -> i32 { + gate.index() as i32 + 1 +} + +``` + +`crates/qfbvsmtrs/src/config.rs`: + +```rs +use std::time::Duration; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SatBackendKind { + Splr, + Varisat, + Dpll, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Config { + pub sat_backend: SatBackendKind, + pub budget: Option, +} + +impl Default for Config { + fn default() -> Self { + Self { + sat_backend: SatBackendKind::Splr, + budget: None, + } + } +} + +impl Config { + pub fn with_budget(mut self, budget: Option) -> Self { + self.budget = budget; + self + } + + pub fn with_sat_backend(mut self, sat_backend: SatBackendKind) -> Self { + self.sat_backend = sat_backend; + self + } +} + +``` + +`crates/qfbvsmtrs/src/error.rs`: + +```rs +use std::fmt; + +pub type Result = std::result::Result; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + Invalid { + context: &'static str, + message: String, + }, + Unsupported(String), + Parse(String), + Timeout, + Internal(String), +} + +impl Error { + pub fn invalid(context: &'static str, message: impl Into) -> Self { + Self::Invalid { + context, + message: message.into(), + } + } + + pub fn unsupported(message: impl Into) -> Self { + Self::Unsupported(message.into()) + } + + pub fn parse(message: impl Into) -> Self { + Self::Parse(message.into()) + } + + pub fn internal(message: impl Into) -> Self { + Self::Internal(message.into()) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Invalid { context, message } => write!(f, "invalid {context}: {message}"), + Error::Unsupported(message) => write!(f, "unsupported feature: {message}"), + Error::Parse(message) => write!(f, "parse error: {message}"), + Error::Timeout => write!(f, "timeout"), + Error::Internal(message) => write!(f, "internal error: {message}"), + } + } +} + +impl std::error::Error for Error {} + +``` + +`crates/qfbvsmtrs/src/eval.rs`: + +```rs +use std::cmp::Ordering; + +use crate::builder::{ + add_bytes, ashr_bytes, bytes_to_bounded_usize, cmp_unsigned_bytes, concat_bytes, extract_bytes, + get_bit, lshr_bytes, mul_bytes, neg_bytes, repeat_bytes, rotate_left_bytes, rotate_right_bytes, + sext_bytes, shl_bytes, signed_less_than, sub_bytes, +}; +use crate::error::{Error, Result}; +use crate::ir::{bytes_for_width, mask_unused_high_bits, NodeKind, Sort}; +use crate::query::Query; + +#[derive(Debug, Clone)] +enum Value { + Bool(bool), + Bv { width: u32, bytes: Vec }, +} + +impl Value { + fn bool(&self) -> Result { + match self { + Value::Bool(value) => Ok(*value), + Value::Bv { .. } => Err(Error::internal("expected Bool value during evaluation")), + } + } + + fn bv(&self) -> Result<(u32, &[u8])> { + match self { + Value::Bv { width, bytes } => Ok((*width, bytes)), + Value::Bool(_) => Err(Error::internal("expected BV value during evaluation")), + } + } +} + +#[derive(Debug, Clone, Copy)] +enum EvalAssignment { + Constant(bool), + Seed(u64), +} + +pub(crate) fn query_satisfied_by_constant_assignment(query: &Query, ones: bool) -> Result { + let values = evaluate(query, EvalAssignment::Constant(ones))?; + for assertion in &query.assertions { + if !values[assertion.root.index()].bool()? { + return Ok(false); + } + } + for assumption in &query.assumptions { + if !values[assumption.index()].bool()? { + return Ok(false); + } + } + Ok(true) +} + +pub(crate) fn query_satisfied_by_seeded_assignment(query: &Query, seed: u64) -> Result { + let values = evaluate(query, EvalAssignment::Seed(seed))?; + for assertion in &query.assertions { + if !values[assertion.root.index()].bool()? { + return Ok(false); + } + } + for assumption in &query.assumptions { + if !values[assumption.index()].bool()? { + return Ok(false); + } + } + Ok(true) +} + +fn evaluate(query: &Query, assignment: EvalAssignment) -> Result> { + let mut values: Vec = Vec::with_capacity(query.arena.len()); + for (index, node) in query.arena.nodes().iter().enumerate() { + let value = match &node.kind { + NodeKind::BoolConst(value) => Value::Bool(*value), + NodeKind::BoolVar { name, external } => Value::Bool(match assignment { + EvalAssignment::Constant(value) => value, + EvalAssignment::Seed(seed) => seeded_bool(seed, index, name, *external), + }), + NodeKind::BvConst { width, bytes } => Value::Bv { + width: *width, + bytes: bytes.clone(), + }, + NodeKind::BvVar { + width, + name, + external, + } => Value::Bv { + width: *width, + bytes: match assignment { + EvalAssignment::Constant(value) => constant_bytes(*width, value)?, + EvalAssignment::Seed(seed) => { + seeded_bytes(*width, seed, index, name, *external)? + } + }, + }, + + NodeKind::BvNot(x) => { + let (width, bytes) = values[x.index()].bv()?; + let mut out = bytes.iter().map(|byte| !byte).collect::>(); + mask_unused_high_bits(&mut out, width); + Value::Bv { width, bytes: out } + } + NodeKind::BvNeg(x) => unary_bv(&values, *x, neg_bytes)?, + NodeKind::BvAnd(a, b) => bitwise_bv(&values, *a, *b, |x, y| x & y)?, + NodeKind::BvOr(a, b) => bitwise_bv(&values, *a, *b, |x, y| x | y)?, + NodeKind::BvXor(a, b) => bitwise_bv(&values, *a, *b, |x, y| x ^ y)?, + NodeKind::BvAdd(a, b) => binary_bv(&values, *a, *b, add_bytes)?, + NodeKind::BvSub(a, b) => binary_bv(&values, *a, *b, sub_bytes)?, + NodeKind::BvMul(a, b) => binary_bv(&values, *a, *b, mul_bytes)?, + NodeKind::BvUDiv(a, b) => { + let (quotient, _) = udiv_urem_value(&values, *a, *b)?; + quotient + } + NodeKind::BvURem(a, b) => { + let (_, remainder) = udiv_urem_value(&values, *a, *b)?; + remainder + } + NodeKind::BvSDiv(a, b) => signed_div_rem_value(&values, *a, *b, SignedDivKind::Div)?, + NodeKind::BvSRem(a, b) => signed_div_rem_value(&values, *a, *b, SignedDivKind::Rem)?, + NodeKind::BvSMod(a, b) => signed_div_rem_value(&values, *a, *b, SignedDivKind::Mod)?, + NodeKind::BvShl(a, b) => shift_value(&values, *a, *b, shl_bytes)?, + NodeKind::BvLShr(a, b) => shift_value(&values, *a, *b, lshr_bytes)?, + NodeKind::BvAShr(a, b) => shift_value(&values, *a, *b, ashr_bytes)?, + NodeKind::BvExtract { child, high, low } => { + let (_, bytes) = values[child.index()].bv()?; + let width = high - low + 1; + Value::Bv { + width, + bytes: extract_bytes(bytes, *low, width), + } + } + NodeKind::BvConcat(high, low) => { + let (high_width, high_bytes) = values[high.index()].bv()?; + let (low_width, low_bytes) = values[low.index()].bv()?; + Value::Bv { + width: high_width + low_width, + bytes: concat_bytes(high_bytes, high_width, low_bytes, low_width), + } + } + NodeKind::BvZeroExtend { child, extra } => { + let (child_width, child_bytes) = values[child.index()].bv()?; + let width = child_width + extra; + let mut bytes = child_bytes.to_vec(); + bytes.resize(bytes_for_width(width)?, 0); + mask_unused_high_bits(&mut bytes, width); + Value::Bv { width, bytes } + } + NodeKind::BvSignExtend { child, extra } => { + let (child_width, child_bytes) = values[child.index()].bv()?; + Value::Bv { + width: child_width + extra, + bytes: sext_bytes(child_bytes, child_width, *extra), + } + } + NodeKind::BvRepeat { child, count } => { + let (child_width, child_bytes) = values[child.index()].bv()?; + Value::Bv { + width: child_width * count, + bytes: repeat_bytes(child_bytes, child_width, *count), + } + } + NodeKind::BvRotateLeft { child, amount } => { + let (width, bytes) = values[child.index()].bv()?; + Value::Bv { + width, + bytes: rotate_left_bytes(bytes, width, *amount % width), + } + } + NodeKind::BvRotateRight { child, amount } => { + let (width, bytes) = values[child.index()].bv()?; + Value::Bv { + width, + bytes: rotate_right_bytes(bytes, width, *amount % width), + } + } + NodeKind::BvIte { + cond, + then_value, + else_value, + } => { + if values[cond.index()].bool()? { + values[then_value.index()].clone() + } else { + values[else_value.index()].clone() + } + } + NodeKind::BvSelect { cases, default } => { + let mut selected = *default; + for (selector, value) in cases { + if values[selector.index()].bool()? { + selected = *value; + break; + } + } + values[selected.index()].clone() + } + + NodeKind::BoolNot(x) => Value::Bool(!values[x.index()].bool()?), + NodeKind::BoolAnd(a, b) => { + Value::Bool(values[a.index()].bool()? && values[b.index()].bool()?) + } + NodeKind::BoolOr(a, b) => { + Value::Bool(values[a.index()].bool()? || values[b.index()].bool()?) + } + NodeKind::BoolImplies(a, b) => { + Value::Bool(!values[a.index()].bool()? || values[b.index()].bool()?) + } + NodeKind::BoolEq(a, b) => { + Value::Bool(values[a.index()].bool()? == values[b.index()].bool()?) + } + NodeKind::BoolIte { + cond, + then_value, + else_value, + } => { + if values[cond.index()].bool()? { + values[then_value.index()].clone() + } else { + values[else_value.index()].clone() + } + } + + NodeKind::BvEq(a, b) => { + let (_, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + Value::Bool(av == bv) + } + NodeKind::BvUlt(a, b) => unsigned_cmp_value(&values, *a, *b, true)?, + NodeKind::BvUle(a, b) => unsigned_cmp_value(&values, *a, *b, false)?, + NodeKind::BvSlt(a, b) => signed_cmp_value(&values, *a, *b, true)?, + NodeKind::BvSle(a, b) => signed_cmp_value(&values, *a, *b, false)?, + + NodeKind::UAddOverflow(a, b) => unsigned_add_overflow_value(&values, *a, *b)?, + NodeKind::SAddOverflow(a, b) => signed_add_overflow_value(&values, *a, *b)?, + NodeKind::USubOverflow(a, b) => unsigned_cmp_value(&values, *a, *b, true)?, + NodeKind::SSubOverflow(a, b) => signed_sub_overflow_value(&values, *a, *b)?, + NodeKind::UMulOverflow(a, b) => unsigned_mul_overflow_value(&values, *a, *b)?, + NodeKind::SMulOverflow(a, b) => signed_mul_overflow_value(&values, *a, *b)?, + NodeKind::NegOverflow(x) => neg_overflow_value(&values, *x)?, + NodeKind::SDivOverflow(a, b) => signed_div_overflow_value(&values, *a, *b)?, + }; + debug_assert_eq!(sort_of_value(&value), node.sort); + values.push(value); + } + Ok(values) +} + +fn sort_of_value(value: &Value) -> Sort { + match value { + Value::Bool(_) => Sort::Bool, + Value::Bv { width, .. } => Sort::Bv(*width), + } +} + +fn constant_bytes(width: u32, ones: bool) -> Result> { + let mut bytes = vec![if ones { 0xff } else { 0 }; bytes_for_width(width)?]; + mask_unused_high_bits(&mut bytes, width); + Ok(bytes) +} + +fn seeded_bool(seed: u64, index: usize, name: &str, external: Option) -> bool { + (seeded_word(seed, index, name, external, 0) & 1) != 0 +} + +fn seeded_bytes( + width: u32, + seed: u64, + index: usize, + name: &str, + external: Option, +) -> Result> { + let mut bytes = vec![0u8; bytes_for_width(width)?]; + for (chunk, slot) in bytes.chunks_mut(8).enumerate() { + let word = seeded_word(seed, index, name, external, chunk as u64).to_le_bytes(); + let count = slot.len(); + slot.copy_from_slice(&word[..count]); + } + mask_unused_high_bits(&mut bytes, width); + Ok(bytes) +} + +fn seeded_word(seed: u64, index: usize, name: &str, external: Option, chunk: u64) -> u64 { + let mut state = seed + ^ ((index as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15)) + ^ ((u64::from(external.unwrap_or(u32::MAX))) << 32) + ^ chunk.wrapping_mul(0xbf58_476d_1ce4_e5b9); + for byte in name.bytes() { + state ^= u64::from(byte); + state = state.wrapping_mul(0x1000_0000_01b3); + state ^= state >> 32; + } + splitmix64(state) +} + +fn splitmix64(mut value: u64) -> u64 { + value = value.wrapping_add(0x9e37_79b9_7f4a_7c15); + value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); + value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); + value ^ (value >> 31) +} + +fn unary_bv(values: &[Value], x: crate::ir::TermId, f: fn(&[u8], u32) -> Vec) -> Result { + let (width, bytes) = values[x.index()].bv()?; + Ok(Value::Bv { + width, + bytes: f(bytes, width), + }) +} + +fn binary_bv( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, + f: fn(&[u8], &[u8], u32) -> Vec, +) -> Result { + let (width, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + Ok(Value::Bv { + width, + bytes: f(av, bv, width), + }) +} + +fn bitwise_bv( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, + f: fn(u8, u8) -> u8, +) -> Result { + let (width, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + Ok(Value::Bv { + width, + bytes: av.iter().zip(bv).map(|(&x, &y)| f(x, y)).collect(), + }) +} + +fn shift_value( + values: &[Value], + value: crate::ir::TermId, + amount: crate::ir::TermId, + f: fn(&[u8], u32, usize) -> Vec, +) -> Result { + let (width, bytes) = values[value.index()].bv()?; + let (_, amount_bytes) = values[amount.index()].bv()?; + let amount = bytes_to_bounded_usize(amount_bytes, width as usize); + Ok(Value::Bv { + width, + bytes: f(bytes, width, amount), + }) +} + +fn unsigned_cmp_value( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, + strict: bool, +) -> Result { + let (_, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + let ordering = cmp_unsigned_bytes(av, bv); + Ok(Value::Bool(if strict { + ordering == Ordering::Less + } else { + ordering != Ordering::Greater + })) +} + +fn signed_cmp_value( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, + strict: bool, +) -> Result { + let (width, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + Ok(Value::Bool(if strict { + signed_less_than(av, bv, width) + } else { + !signed_less_than(bv, av, width) + })) +} + +fn udiv_urem_value( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, +) -> Result<(Value, Value)> { + let (width, dividend) = values[a.index()].bv()?; + let (_, divisor) = values[b.index()].bv()?; + let (quotient, remainder) = udiv_urem_bytes(dividend, divisor, width)?; + Ok(( + Value::Bv { + width, + bytes: quotient, + }, + Value::Bv { + width, + bytes: remainder, + }, + )) +} + +fn udiv_urem_bytes(dividend: &[u8], divisor: &[u8], width: u32) -> Result<(Vec, Vec)> { + if divisor.iter().all(|byte| *byte == 0) { + return Ok((constant_bytes(width, true)?, dividend.to_vec())); + } + let mut quotient = constant_bytes(width, false)?; + let mut remainder = constant_bytes(width, false)?; + for bit in (0..width).rev() { + remainder = shl_bytes(&remainder, width, 1); + if get_bit(dividend, bit) { + crate::builder::set_bit(&mut remainder, 0); + } + if cmp_unsigned_bytes(&remainder, divisor) != Ordering::Less { + remainder = sub_bytes(&remainder, divisor, width); + crate::builder::set_bit(&mut quotient, bit); + } + } + Ok((quotient, remainder)) +} + +#[derive(Debug, Clone, Copy)] +enum SignedDivKind { + Div, + Rem, + Mod, +} + +fn signed_div_rem_value( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, + kind: SignedDivKind, +) -> Result { + let (width, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + let sign_a = get_bit(av, width - 1); + let sign_b = get_bit(bv, width - 1); + let abs_a = if sign_a { + neg_bytes(av, width) + } else { + av.to_vec() + }; + let abs_b = if sign_b { + neg_bytes(bv, width) + } else { + bv.to_vec() + }; + let (mut q, mut r) = udiv_urem_bytes(&abs_a, &abs_b, width)?; + match kind { + SignedDivKind::Div => { + if sign_a ^ sign_b { + q = neg_bytes(&q, width); + } + Ok(Value::Bv { width, bytes: q }) + } + SignedDivKind::Rem => { + if sign_a { + r = neg_bytes(&r, width); + } + Ok(Value::Bv { width, bytes: r }) + } + SignedDivKind::Mod => { + let u_zero = r.iter().all(|byte| *byte == 0); + let out = if u_zero { + r + } else { + match (sign_a, sign_b) { + (false, false) => r, + (true, false) => sub_bytes(bv, &r, width), + (false, true) => add_bytes(bv, &r, width), + (true, true) => neg_bytes(&r, width), + } + }; + Ok(Value::Bv { width, bytes: out }) + } + } +} + +fn unsigned_add_overflow_value( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, +) -> Result { + let (width, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + let sum = add_bytes(av, bv, width); + Ok(Value::Bool(cmp_unsigned_bytes(&sum, av) == Ordering::Less)) +} + +fn signed_add_overflow_value( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, +) -> Result { + let (width, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + let sum = add_bytes(av, bv, width); + let sa = get_bit(av, width - 1); + let sb = get_bit(bv, width - 1); + let ss = get_bit(&sum, width - 1); + Ok(Value::Bool(sa == sb && sa != ss)) +} + +fn signed_sub_overflow_value( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, +) -> Result { + let (width, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + let diff = sub_bytes(av, bv, width); + let sa = get_bit(av, width - 1); + let sb = get_bit(bv, width - 1); + let sd = get_bit(&diff, width - 1); + Ok(Value::Bool(sa != sb && sa != sd)) +} + +fn unsigned_mul_overflow_value( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, +) -> Result { + let (width, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + let mut aw = av.to_vec(); + let mut bw = bv.to_vec(); + aw.resize(bytes_for_width(width * 2)?, 0); + bw.resize(bytes_for_width(width * 2)?, 0); + let product = mul_bytes(&aw, &bw, width * 2); + Ok(Value::Bool( + (width..(width * 2)).any(|bit| get_bit(&product, bit)), + )) +} + +fn signed_mul_overflow_value( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, +) -> Result { + let (width, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + let aw = sext_bytes(av, width, width); + let bw = sext_bytes(bv, width, width); + let product = mul_bytes(&aw, &bw, width * 2); + let low = extract_bytes(&product, 0, width); + let sign_extended_low = sext_bytes(&low, width, width); + Ok(Value::Bool(product != sign_extended_low)) +} + +fn neg_overflow_value(values: &[Value], x: crate::ir::TermId) -> Result { + let (width, bytes) = values[x.index()].bv()?; + Ok(Value::Bool( + get_bit(bytes, width - 1) && (0..(width - 1)).all(|bit| !get_bit(bytes, bit)), + )) +} + +fn signed_div_overflow_value( + values: &[Value], + a: crate::ir::TermId, + b: crate::ir::TermId, +) -> Result { + let (width, av) = values[a.index()].bv()?; + let (_, bv) = values[b.index()].bv()?; + let a_min = get_bit(av, width - 1) && (0..(width - 1)).all(|bit| !get_bit(av, bit)); + let b_minus_one = bv == constant_bytes(width, true)?.as_slice(); + Ok(Value::Bool(a_min && b_minus_one)) +} + +``` + +`crates/qfbvsmtrs/src/frontend.rs`: + +```rs +use std::collections::HashMap; + +use dashu::{float::DBig, integer::UBig}; +use yaspar::{ + action::{ + ActionOnAttribute, ActionOnConstant, ActionOnIdentifier, ActionOnIndex, ActionOnSort, + ActionOnString, ActionOnTerm, ParsingAction, ParsingResult, Pattern, + }, + ast::{DatatypeDec, DatatypeDef, FunctionDef, Keyword}, + position::Range, +}; + +use crate::builder::Builder; +use crate::error::{Error, Result}; +use crate::ir::TermId; +use crate::model::{scalar_to_smt, Model, ScalarValue}; +use crate::query::Query; +use crate::solver::{SolveResult, SolveStatus}; +use crate::{Config, Solver}; + +#[derive(Debug, Clone, PartialEq, Eq)] +enum SExpr { + Atom(String), + List(Vec), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SmtSort { + Bool, + Bv(u32), +} + +#[derive(Debug, Clone)] +struct Binding { + node: TermId, + sort: SmtSort, +} + +#[derive(Debug, Clone)] +struct FunctionBinding { + params: Vec<(String, SmtSort)>, + result: SmtSort, + body: SExpr, +} + +#[derive(Debug, Default)] +struct ScriptState { + builder: Builder, + env: HashMap, + functions: HashMap, + saw_check_sat: bool, +} + +pub fn parse_smt2(script: &str) -> Result { + let exprs = parse_sexprs(script)?; + let mut state = ScriptState::default(); + for expr in exprs { + let list = match expr { + SExpr::List(list) => list, + SExpr::Atom(atom) => { + return Err(Error::invalid( + "SMT-LIB command", + format!("top-level atom {atom:?}"), + )) + } + }; + if list.is_empty() { + continue; + } + let cmd = atom(&list[0])?; + match cmd { + "set-logic" => { + if list.len() == 2 { + let logic = atom(&list[1])?; + if logic != "QF_BV" && logic != "ALL" { + return Err(Error::invalid("set-logic", "only QF_BV is supported")); + } + } + } + "set-option" | "set-info" | "echo" | "exit" => {} + "push" => push_command(&mut state, &list)?, + "pop" => pop_command(&mut state, &list)?, + "reset" | "reset-assertions" => { + return Err(Error::unsupported(format!( + "command {cmd} is not supported by parse_smt2" + ))) + } + "declare-const" => declare_const(&mut state, &list)?, + "declare-fun" => declare_fun(&mut state, &list)?, + "define-const" => define_const(&mut state, &list)?, + "define-fun" => define_fun(&mut state, &list)?, + "assert" => assert_command(&mut state, &list)?, + "check-sat" => state.saw_check_sat = true, + "check-sat-assuming" => check_sat_assuming(&mut state, &list)?, + "get-model" => state.builder.set_want_model(true), + "get-value" => get_value_command(&mut state, &list)?, + "get-unsat-core" => state.builder.set_want_core(true), + other => return Err(Error::unsupported(format!("SMT-LIB command {other}"))), + } + } + if !state.saw_check_sat { + return Err(Error::invalid( + "SMT-LIB script", + "script does not contain check-sat", + )); + } + state.builder.finish() +} + +pub fn solve_smt2(script: &str, config: &Config) -> Result { + let query = parse_smt2(script)?; + Solver::new(config.clone()).solve(&query) +} + +pub fn format_smt2_response(query: &Query, result: &SolveResult) -> String { + match result.status { + SolveStatus::Sat => { + let mut out = "sat\n".to_owned(); + if query.want_model { + if let Some(model) = &result.model { + if query.get_values.is_empty() { + out.push_str(&format_model(model)); + } else { + out.push_str(&format_get_values(model, &query.get_values)); + } + } + } + out + } + SolveStatus::Unsat => { + let mut out = "unsat\n".to_owned(); + if query.want_core { + if let Some(core) = &result.core { + out.push('('); + for (index, name) in core.iter().enumerate() { + if index > 0 { + out.push(' '); + } + out.push_str("e_symbol(name)); + } + out.push_str(")\n"); + } + } + out + } + SolveStatus::Unknown => "unknown\n".to_owned(), + SolveStatus::Ok => "success\n".to_owned(), + } +} + +fn declare_const(state: &mut ScriptState, list: &[SExpr]) -> Result<()> { + if list.len() != 3 { + return Err(Error::invalid("declare-const", "expected name and sort")); + } + let name = atom(&list[1])?.to_owned(); + let sort = parse_sort(&list[2])?; + let node = match sort { + SmtSort::Bool => state.builder.bool_var(&name)?, + SmtSort::Bv(width) => state.builder.bv_var(&name, width)?, + }; + state.env.insert(name, Binding { node, sort }); + Ok(()) +} + +fn declare_fun(state: &mut ScriptState, list: &[SExpr]) -> Result<()> { + if list.len() != 4 { + return Err(Error::invalid("declare-fun", "expected name, args, sort")); + } + match &list[2] { + SExpr::List(args) if args.is_empty() => {} + _ => return Err(Error::unsupported("only 0-arity declare-fun is supported")), + } + declare_const(state, &[list[0].clone(), list[1].clone(), list[3].clone()]) +} + +fn define_const(state: &mut ScriptState, list: &[SExpr]) -> Result<()> { + if list.len() != 4 { + return Err(Error::invalid("define-const", "expected name, sort, value")); + } + let name = atom(&list[1])?.to_owned(); + let declared = parse_sort(&list[2])?; + let binding = parse_expr_with_locals(state, &list[3], &mut HashMap::new())?; + expect_sort(binding.sort, declared, "define-const")?; + state.env.insert(name, binding); + Ok(()) +} + +fn define_fun(state: &mut ScriptState, list: &[SExpr]) -> Result<()> { + if list.len() != 5 { + return Err(Error::invalid( + "define-fun", + "expected name, args, sort, body", + )); + } + let name = atom(&list[1])?.to_owned(); + let params = parse_function_params(&list[2])?; + if params.is_empty() { + return define_const( + state, + &[ + list[0].clone(), + list[1].clone(), + list[3].clone(), + list[4].clone(), + ], + ); + } + let result = parse_sort(&list[3])?; + state.functions.insert( + name, + FunctionBinding { + params, + result, + body: list[4].clone(), + }, + ); + Ok(()) +} + +fn parse_function_params(expr: &SExpr) -> Result> { + let SExpr::List(args) = expr else { + return Err(Error::invalid("define-fun", "expected argument list")); + }; + let mut params = Vec::with_capacity(args.len()); + for arg in args { + let SExpr::List(pair) = arg else { + return Err(Error::invalid("define-fun", "argument is not a pair")); + }; + if pair.len() != 2 { + return Err(Error::invalid( + "define-fun", + "argument pair must have name and sort", + )); + } + params.push((atom(&pair[0])?.to_owned(), parse_sort(&pair[1])?)); + } + Ok(params) +} + +fn push_command(state: &mut ScriptState, list: &[SExpr]) -> Result<()> { + let levels = command_level(list, "push")?; + for _ in 0..levels { + state.builder.push(); + } + Ok(()) +} + +fn pop_command(state: &mut ScriptState, list: &[SExpr]) -> Result<()> { + let levels = command_level(list, "pop")?; + for _ in 0..levels { + state.builder.pop()?; + } + Ok(()) +} + +fn command_level(list: &[SExpr], context: &'static str) -> Result { + if list.len() == 1 { + return Ok(1); + } + if list.len() != 2 { + return Err(Error::invalid(context, "expected optional level")); + } + parse_u32_atom(&list[1], context) +} + +fn assert_command(state: &mut ScriptState, list: &[SExpr]) -> Result<()> { + if list.len() != 2 { + return Err(Error::invalid("assert", "expected one expression")); + } + if let Some((inner, name)) = named_annotation(&list[1])? { + let binding = parse_expr_with_locals(state, inner, &mut HashMap::new())?; + expect_sort(binding.sort, SmtSort::Bool, "assert")?; + state.builder.assert_named(name, binding.node) + } else { + let binding = parse_expr_with_locals(state, &list[1], &mut HashMap::new())?; + expect_sort(binding.sort, SmtSort::Bool, "assert")?; + state.builder.assert(binding.node) + } +} + +fn check_sat_assuming(state: &mut ScriptState, list: &[SExpr]) -> Result<()> { + if list.len() != 2 { + return Err(Error::invalid( + "check-sat-assuming", + "expected assumption list", + )); + } + let assumptions = match &list[1] { + SExpr::List(items) => items, + _ => return Err(Error::invalid("check-sat-assuming", "expected list")), + }; + for item in assumptions { + let binding = parse_expr_with_locals(state, item, &mut HashMap::new())?; + expect_sort(binding.sort, SmtSort::Bool, "check-sat-assuming")?; + state.builder.assume(binding.node)?; + } + state.saw_check_sat = true; + Ok(()) +} + +fn get_value_command(state: &mut ScriptState, list: &[SExpr]) -> Result<()> { + if list.len() != 2 { + return Err(Error::invalid("get-value", "expected one term list")); + } + let terms = match &list[1] { + SExpr::List(items) => items, + _ => return Err(Error::invalid("get-value", "expected term list")), + }; + for term in terms { + match term { + SExpr::Atom(name) => state.builder.add_get_value(name.clone()), + _ => { + return Err(Error::unsupported( + "get-value currently supports declared symbols", + )) + } + } + } + Ok(()) +} + +fn named_annotation(expr: &SExpr) -> Result> { + let SExpr::List(items) = expr else { + return Ok(None); + }; + if items.len() >= 4 && atom(&items[0])? == "!" { + let mut name = None; + let mut index = 2; + while index + 1 < items.len() { + if atom(&items[index])? == ":named" { + name = Some(atom(&items[index + 1])?.to_owned()); + } + index += 2; + } + if let Some(name) = name { + return Ok(Some((&items[1], name))); + } + } + Ok(None) +} + +fn parse_expr_with_locals( + state: &mut ScriptState, + expr: &SExpr, + locals: &mut HashMap, +) -> Result { + match expr { + SExpr::Atom(atom) => parse_atom_expr(state, atom, locals), + SExpr::List(items) => parse_list_expr(state, items, locals), + } +} + +fn parse_atom_expr( + state: &mut ScriptState, + value: &str, + locals: &HashMap, +) -> Result { + if value == "true" { + return Ok(Binding { + node: state.builder.bool_true()?, + sort: SmtSort::Bool, + }); + } + if value == "false" { + return Ok(Binding { + node: state.builder.bool_false()?, + sort: SmtSort::Bool, + }); + } + if let Some((digits, radix)) = value + .strip_prefix("#b") + .map(|s| (s, 2)) + .or_else(|| value.strip_prefix("#x").map(|s| (s, 16))) + { + let width = if radix == 2 { + digits.len() as u32 + } else { + digits.len() as u32 * 4 + }; + let bytes = literal_bytes(digits, radix, width, value)?; + return Ok(Binding { + node: state.builder.bv_const_bytes(&bytes, width)?, + sort: SmtSort::Bv(width), + }); + } + locals + .get(value) + .or_else(|| state.env.get(value)) + .cloned() + .ok_or_else(|| Error::invalid("SMT-LIB symbol", format!("undefined symbol {value:?}"))) +} + +fn parse_list_expr( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, +) -> Result { + if items.is_empty() { + return Err(Error::invalid("SMT-LIB expression", "empty list")); + } + if let SExpr::List(op_items) = &items[0] { + return parse_indexed_op(state, op_items, &items[1..], locals); + } + if atom(&items[0])? == "_" { + return parse_indexed_literal(state, items); + } + let op = atom(&items[0])?; + if state.functions.contains_key(op) { + return parse_function_call(state, op, &items[1..], locals); + } + match op { + "!" => parse_expr_with_locals(state, &items[1], locals), + "let" => parse_let(state, items, locals), + "ite" => parse_ite(state, items, locals), + "=" => parse_equals(state, &items[1..], locals), + "distinct" => parse_distinct(state, &items[1..], locals), + "not" => unary_bool(state, items, locals, |b, x| b.bool_not(x)), + "and" => fold_bool(state, &items[1..], locals, true, |b, a, c| b.bool_and(a, c)), + "or" => fold_bool(state, &items[1..], locals, false, |b, a, c| b.bool_or(a, c)), + "=>" => binary_bool(state, items, locals, |b, a, c| b.bool_implies(a, c)), + "xor" => binary_bool(state, items, locals, |b, a, c| b.bool_xor(a, c)), + "bvnot" => unary_bv(state, items, locals, |b, x| b.bv_not(x)), + "bvneg" => unary_bv(state, items, locals, |b, x| b.bv_neg(x)), + "bvand" => fold_bv(state, items, locals, |b, a, c| b.bv_and(a, c)), + "bvnand" => inverted_fold_bv(state, items, locals, |b, a, c| b.bv_and(a, c)), + "bvor" => fold_bv(state, items, locals, |b, a, c| b.bv_or(a, c)), + "bvnor" => inverted_fold_bv(state, items, locals, |b, a, c| b.bv_or(a, c)), + "bvxor" => fold_bv(state, items, locals, |b, a, c| b.bv_xor(a, c)), + "bvxnor" => inverted_fold_bv(state, items, locals, |b, a, c| b.bv_xor(a, c)), + "bvadd" => fold_bv(state, items, locals, |b, a, c| b.bv_add(a, c)), + "bvsub" => binary_bv(state, items, locals, |b, a, c| b.bv_sub(a, c)), + "bvmul" => fold_bv(state, items, locals, |b, a, c| b.bv_mul(a, c)), + "bvudiv" => binary_bv(state, items, locals, |b, a, c| b.bv_udiv(a, c)), + "bvurem" => binary_bv(state, items, locals, |b, a, c| b.bv_urem(a, c)), + "bvsdiv" => binary_bv(state, items, locals, |b, a, c| b.bv_sdiv(a, c)), + "bvsrem" => binary_bv(state, items, locals, |b, a, c| b.bv_srem(a, c)), + "bvsmod" => binary_bv(state, items, locals, |b, a, c| b.bv_smod(a, c)), + "bvshl" => binary_bv(state, items, locals, |b, a, c| b.bv_shl(a, c)), + "bvlshr" => binary_bv(state, items, locals, |b, a, c| b.bv_lshr(a, c)), + "bvashr" => binary_bv(state, items, locals, |b, a, c| b.bv_ashr(a, c)), + "bvcomp" => bvcomp_bv(state, items, locals), + "concat" => concat_bv(state, items, locals), + "bvult" => bv_cmp(state, items, locals, |b, a, c| b.bv_ult(a, c)), + "bvule" => bv_cmp(state, items, locals, |b, a, c| b.bv_ule(a, c)), + "bvugt" => bv_cmp(state, items, locals, |b, a, c| b.bv_ugt(a, c)), + "bvuge" => bv_cmp(state, items, locals, |b, a, c| b.bv_uge(a, c)), + "bvslt" => bv_cmp(state, items, locals, |b, a, c| b.bv_slt(a, c)), + "bvsle" => bv_cmp(state, items, locals, |b, a, c| b.bv_sle(a, c)), + "bvsgt" => bv_cmp(state, items, locals, |b, a, c| b.bv_sgt(a, c)), + "bvsge" => bv_cmp(state, items, locals, |b, a, c| b.bv_sge(a, c)), + "bvuaddo" | "uaddo" => overflow_cmp(state, items, locals, |b, a, c| b.uadd_ovf(a, c)), + "bvsaddo" | "saddo" => overflow_cmp(state, items, locals, |b, a, c| b.sadd_ovf(a, c)), + "bvusubo" | "usubo" => overflow_cmp(state, items, locals, |b, a, c| b.usub_ovf(a, c)), + "bvssubo" | "ssubo" => overflow_cmp(state, items, locals, |b, a, c| b.ssub_ovf(a, c)), + "bvumulo" | "umulo" => overflow_cmp(state, items, locals, |b, a, c| b.umul_ovf(a, c)), + "bvsmulo" | "smulo" => overflow_cmp(state, items, locals, |b, a, c| b.smul_ovf(a, c)), + "bvsdivo" | "sdivo" => overflow_cmp(state, items, locals, |b, a, c| b.sdiv_ovf(a, c)), + "bvnego" | "nego" => unary_overflow(state, items, locals, |b, x| b.neg_ovf(x)), + other => Err(Error::unsupported(format!("SMT-LIB operator {other}"))), + } +} + +fn parse_function_call( + state: &mut ScriptState, + name: &str, + args: &[SExpr], + locals: &mut HashMap, +) -> Result { + let function = state + .functions + .get(name) + .cloned() + .ok_or_else(|| Error::invalid("function call", format!("undefined function {name}")))?; + if args.len() != function.params.len() { + return Err(Error::invalid( + "function call", + format!( + "function {name} expects {} arguments, got {}", + function.params.len(), + args.len() + ), + )); + } + let mut expanded_locals = locals.clone(); + for ((param_name, param_sort), arg_expr) in function.params.iter().zip(args) { + let value = parse_expr_with_locals(state, arg_expr, locals)?; + expect_sort(value.sort, *param_sort, "function argument")?; + expanded_locals.insert(param_name.clone(), value); + } + let value = parse_expr_with_locals(state, &function.body, &mut expanded_locals)?; + expect_sort(value.sort, function.result, "function result")?; + Ok(value) +} + +fn parse_ite( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, +) -> Result { + expect_len(items, 4, "ite")?; + let c = parse_expr_with_locals(state, &items[1], locals)?; + let t = parse_expr_with_locals(state, &items[2], locals)?; + let e = parse_expr_with_locals(state, &items[3], locals)?; + expect_sort(c.sort, SmtSort::Bool, "ite condition")?; + match (t.sort, e.sort) { + (SmtSort::Bool, SmtSort::Bool) => Ok(Binding { + node: state.builder.bool_ite(c.node, t.node, e.node)?, + sort: SmtSort::Bool, + }), + (SmtSort::Bv(w1), SmtSort::Bv(w2)) if w1 == w2 => Ok(Binding { + node: state.builder.bv_ite(c.node, t.node, e.node)?, + sort: SmtSort::Bv(w1), + }), + _ => Err(Error::invalid("ite", "branch sorts differ")), + } +} + +fn parse_indexed_literal(state: &mut ScriptState, items: &[SExpr]) -> Result { + let (value, width) = if items.len() == 3 { + let bv_atom = atom(&items[1])?; + let value = bv_atom + .strip_prefix("bv") + .ok_or_else(|| Error::invalid("indexed literal", "expected (_ bvN W)"))?; + let width = atom(&items[2])? + .parse::() + .map_err(|_| Error::invalid("bv literal", "invalid width"))?; + (value, width) + } else if items.len() == 4 && atom(&items[1])? == "bv" { + let value = atom(&items[2])?; + let width = atom(&items[3])? + .parse::() + .map_err(|_| Error::invalid("bv literal", "invalid width"))?; + (value, width) + } else { + return Err(Error::invalid("indexed literal", "expected (_ bvN W)")); + }; + let bytes = decimal_to_le_bytes(value, (width as usize).div_ceil(8))?; + Ok(Binding { + node: state.builder.bv_const_bytes(&bytes, width)?, + sort: SmtSort::Bv(width), + }) +} + +fn parse_indexed_op( + state: &mut ScriptState, + op_items: &[SExpr], + args: &[SExpr], + locals: &mut HashMap, +) -> Result { + if op_items.len() == 4 && atom(&op_items[0])? == "_" && atom(&op_items[1])? == "extract" { + if args.len() != 1 { + return Err(Error::invalid("extract", "expected one argument")); + } + let hi = parse_u32_atom(&op_items[2], "extract high")?; + let lo = parse_u32_atom(&op_items[3], "extract low")?; + let x = parse_expr_with_locals(state, &args[0], locals)?; + let SmtSort::Bv(_) = x.sort else { + return Err(Error::invalid("extract", "argument is not BV")); + }; + return Ok(Binding { + node: state.builder.bv_extract(x.node, hi, lo)?, + sort: SmtSort::Bv(hi - lo + 1), + }); + } + if op_items.len() == 3 && atom(&op_items[0])? == "_" { + let amount = parse_u32_atom(&op_items[2], "indexed amount")?; + if args.len() != 1 { + return Err(Error::invalid("indexed operator", "expected one argument")); + } + let x = parse_expr_with_locals(state, &args[0], locals)?; + let SmtSort::Bv(width) = x.sort else { + return Err(Error::invalid("indexed operator", "argument is not BV")); + }; + return match atom(&op_items[1])? { + "zero_extend" => Ok(Binding { + node: state.builder.bv_zext(x.node, amount)?, + sort: SmtSort::Bv(width + amount), + }), + "sign_extend" => Ok(Binding { + node: state.builder.bv_sext(x.node, amount)?, + sort: SmtSort::Bv(width + amount), + }), + "repeat" => Ok(Binding { + node: state.builder.bv_repeat(x.node, amount)?, + sort: SmtSort::Bv(width * amount), + }), + "rotate_left" => Ok(Binding { + node: state.builder.bv_rotate_left(x.node, amount)?, + sort: SmtSort::Bv(width), + }), + "rotate_right" => Ok(Binding { + node: state.builder.bv_rotate_right(x.node, amount)?, + sort: SmtSort::Bv(width), + }), + _ => Err(Error::unsupported("indexed operator")), + }; + } + Err(Error::unsupported("indexed operator")) +} + +fn parse_let( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, +) -> Result { + expect_len(items, 3, "let")?; + let bindings = match &items[1] { + SExpr::List(items) => items, + _ => return Err(Error::invalid("let", "expected binding list")), + }; + let mut nested = locals.clone(); + for binding in bindings { + let pair = match binding { + SExpr::List(pair) if pair.len() == 2 => pair, + _ => return Err(Error::invalid("let", "bad binding")), + }; + let name = atom(&pair[0])?.to_owned(); + let value = parse_expr_with_locals(state, &pair[1], &mut nested)?; + nested.insert(name, value); + } + parse_expr_with_locals(state, &items[2], &mut nested) +} + +fn parse_equals( + state: &mut ScriptState, + args: &[SExpr], + locals: &mut HashMap, +) -> Result { + if args.len() < 2 { + return Err(Error::invalid("=", "expected at least two arguments")); + } + let values = args + .iter() + .map(|arg| parse_expr_with_locals(state, arg, locals)) + .collect::>>()?; + let mut result = state.builder.bool_true()?; + for pair in values.windows(2) { + let eq = equality_node(state, &pair[0], &pair[1], "=")?; + result = state.builder.bool_and(result, eq)?; + } + Ok(Binding { + node: result, + sort: SmtSort::Bool, + }) +} + +fn parse_distinct( + state: &mut ScriptState, + args: &[SExpr], + locals: &mut HashMap, +) -> Result { + if args.len() < 2 { + return Err(Error::invalid( + "distinct", + "expected at least two arguments", + )); + } + let values = args + .iter() + .map(|arg| parse_expr_with_locals(state, arg, locals)) + .collect::>>()?; + let mut result = state.builder.bool_true()?; + for i in 0..values.len() { + for j in (i + 1)..values.len() { + let eq = equality_node(state, &values[i], &values[j], "distinct")?; + let ne = state.builder.bool_not(eq)?; + result = state.builder.bool_and(result, ne)?; + } + } + Ok(Binding { + node: result, + sort: SmtSort::Bool, + }) +} + +fn equality_node( + state: &mut ScriptState, + a: &Binding, + b: &Binding, + context: &'static str, +) -> Result { + match (a.sort, b.sort) { + (SmtSort::Bool, SmtSort::Bool) => state.builder.bool_eq(a.node, b.node), + (SmtSort::Bv(w1), SmtSort::Bv(w2)) if w1 == w2 => state.builder.bv_eq(a.node, b.node), + _ => Err(Error::invalid(context, "argument sorts differ")), + } +} + +fn unary_bool( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut Builder, TermId) -> Result, +) -> Result { + expect_len(items, 2, "unary bool")?; + let x = parse_expr_with_locals(state, &items[1], locals)?; + expect_sort(x.sort, SmtSort::Bool, "unary bool")?; + Ok(Binding { + node: f(&mut state.builder, x.node)?, + sort: SmtSort::Bool, + }) +} + +fn binary_bool( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut Builder, TermId, TermId) -> Result, +) -> Result { + expect_len(items, 3, "binary bool")?; + let a = parse_expr_with_locals(state, &items[1], locals)?; + let b = parse_expr_with_locals(state, &items[2], locals)?; + expect_sort(a.sort, SmtSort::Bool, "binary bool")?; + expect_sort(b.sort, SmtSort::Bool, "binary bool")?; + Ok(Binding { + node: f(&mut state.builder, a.node, b.node)?, + sort: SmtSort::Bool, + }) +} + +fn fold_bool( + state: &mut ScriptState, + args: &[SExpr], + locals: &mut HashMap, + identity: bool, + f: fn(&mut Builder, TermId, TermId) -> Result, +) -> Result { + if args.is_empty() { + let node = state.builder.bool_const(identity)?; + return Ok(Binding { + node, + sort: SmtSort::Bool, + }); + } + let mut cur = parse_expr_with_locals(state, &args[0], locals)?; + expect_sort(cur.sort, SmtSort::Bool, "fold bool")?; + for arg in &args[1..] { + let next = parse_expr_with_locals(state, arg, locals)?; + expect_sort(next.sort, SmtSort::Bool, "fold bool")?; + cur = Binding { + node: f(&mut state.builder, cur.node, next.node)?, + sort: SmtSort::Bool, + }; + } + Ok(cur) +} + +fn unary_bv( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut Builder, TermId) -> Result, +) -> Result { + expect_len(items, 2, "unary BV")?; + let x = parse_expr_with_locals(state, &items[1], locals)?; + let SmtSort::Bv(width) = x.sort else { + return Err(Error::invalid("unary BV", "argument is not BV")); + }; + Ok(Binding { + node: f(&mut state.builder, x.node)?, + sort: SmtSort::Bv(width), + }) +} + +fn binary_bv( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut Builder, TermId, TermId) -> Result, +) -> Result { + expect_len(items, 3, "binary BV")?; + let a = parse_expr_with_locals(state, &items[1], locals)?; + let b = parse_expr_with_locals(state, &items[2], locals)?; + binary_bv_bindings(state, a, b, f, "binary BV") +} + +fn fold_bv( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut Builder, TermId, TermId) -> Result, +) -> Result { + if items.len() < 2 { + return Err(Error::invalid("BV fold", "expected at least one argument")); + } + let mut cur = parse_expr_with_locals(state, &items[1], locals)?; + let SmtSort::Bv(_) = cur.sort else { + return Err(Error::invalid("BV fold", "argument is not BV")); + }; + for item in &items[2..] { + let next = parse_expr_with_locals(state, item, locals)?; + cur = binary_bv_bindings(state, cur, next, f, "BV fold")?; + } + Ok(cur) +} + +fn inverted_fold_bv( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut Builder, TermId, TermId) -> Result, +) -> Result { + let value = fold_bv(state, items, locals, f)?; + Ok(Binding { + node: state.builder.bv_not(value.node)?, + sort: value.sort, + }) +} + +fn binary_bv_bindings( + state: &mut ScriptState, + a: Binding, + b: Binding, + f: fn(&mut Builder, TermId, TermId) -> Result, + context: &'static str, +) -> Result { + let (SmtSort::Bv(w1), SmtSort::Bv(w2)) = (a.sort, b.sort) else { + return Err(Error::invalid(context, "argument is not BV")); + }; + if w1 != w2 { + return Err(Error::invalid(context, "width mismatch")); + } + Ok(Binding { + node: f(&mut state.builder, a.node, b.node)?, + sort: SmtSort::Bv(w1), + }) +} + +fn bvcomp_bv( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, +) -> Result { + expect_len(items, 3, "bvcomp")?; + let a = parse_expr_with_locals(state, &items[1], locals)?; + let b = parse_expr_with_locals(state, &items[2], locals)?; + let (SmtSort::Bv(w1), SmtSort::Bv(w2)) = (a.sort, b.sort) else { + return Err(Error::invalid("bvcomp", "argument is not BV")); + }; + if w1 != w2 { + return Err(Error::invalid("bvcomp", "width mismatch")); + } + let eq = state.builder.bv_eq(a.node, b.node)?; + let one = state.builder.bv_const(1, 1)?; + let zero = state.builder.bv_const(0, 1)?; + Ok(Binding { + node: state.builder.bv_ite(eq, one, zero)?, + sort: SmtSort::Bv(1), + }) +} + +fn concat_bv( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, +) -> Result { + expect_len(items, 3, "concat")?; + let a = parse_expr_with_locals(state, &items[1], locals)?; + let b = parse_expr_with_locals(state, &items[2], locals)?; + let (SmtSort::Bv(w1), SmtSort::Bv(w2)) = (a.sort, b.sort) else { + return Err(Error::invalid("concat", "argument is not BV")); + }; + Ok(Binding { + node: state.builder.bv_concat(a.node, b.node)?, + sort: SmtSort::Bv(w1 + w2), + }) +} + +fn bv_cmp( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut Builder, TermId, TermId) -> Result, +) -> Result { + let value = binary_bv(state, items, locals, f)?; + Ok(Binding { + node: value.node, + sort: SmtSort::Bool, + }) +} + +fn overflow_cmp( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut Builder, TermId, TermId) -> Result, +) -> Result { + bv_cmp(state, items, locals, f) +} + +fn unary_overflow( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut Builder, TermId) -> Result, +) -> Result { + let value = unary_bv(state, items, locals, f)?; + Ok(Binding { + node: value.node, + sort: SmtSort::Bool, + }) +} + +fn parse_sort(expr: &SExpr) -> Result { + match expr { + SExpr::Atom(atom) if atom == "Bool" => Ok(SmtSort::Bool), + SExpr::List(items) + if items.len() == 3 && atom(&items[0])? == "_" && atom(&items[1])? == "BitVec" => + { + let width = atom(&items[2])? + .parse::() + .map_err(|_| Error::invalid("sort", "invalid BitVec width"))?; + Ok(SmtSort::Bv(width)) + } + _ => Err(Error::invalid("sort", "expected Bool or (_ BitVec n)")), + } +} + +fn decimal_to_le_bytes(text: &str, len: usize) -> Result> { + let mut digits = text + .bytes() + .map(|byte| match byte { + b'0'..=b'9' => Ok(byte - b'0'), + _ => Err(Error::invalid("bv literal", "invalid decimal value")), + }) + .collect::>>()?; + let mut out = Vec::with_capacity(len); + while !digits.is_empty() && out.len() < len { + let mut carry = 0u16; + let mut quotient = Vec::new(); + for digit in digits { + let n = carry * 10 + u16::from(digit); + let q = (n / 256) as u8; + carry = n % 256; + if !quotient.is_empty() || q != 0 { + quotient.push(q); + } + } + out.push(carry as u8); + digits = quotient; + } + out.resize(len, 0); + Ok(out) +} + +fn literal_bytes(digits: &str, radix: u32, width: u32, original: &str) -> Result> { + let mut bytes = vec![0u8; (width as usize).div_ceil(8)]; + match radix { + 2 => { + for (offset, ch) in digits.chars().rev().enumerate() { + match ch { + '0' => {} + '1' => bytes[offset / 8] |= 1 << (offset % 8), + _ => { + return Err(Error::invalid( + "bitvector literal", + format!("invalid binary literal {original}"), + )) + } + } + } + } + 16 => { + for (nibble, ch) in digits.chars().rev().enumerate() { + let value = ch.to_digit(16).ok_or_else(|| { + Error::invalid( + "bitvector literal", + format!("invalid hex literal {original}"), + ) + })? as u8; + let bit = nibble * 4; + bytes[bit / 8] |= value << (bit % 8); + } + } + _ => unreachable!("only binary and hex literals are passed"), + } + Ok(bytes) +} + +fn format_model(model: &Model) -> String { + let mut out = "(model\n".to_owned(); + for entry in &model.entries { + out.push_str(" (define-fun "); + out.push_str("e_symbol(&entry.name)); + out.push_str(" () "); + match &entry.value { + ScalarValue::Bool(_) => out.push_str("Bool "), + ScalarValue::Bv { width, .. } => out.push_str(&format!("(_ BitVec {width}) ")), + } + out.push_str(&scalar_to_smt(&entry.value)); + out.push_str(")\n"); + } + out.push_str(")\n"); + out +} + +fn format_get_values(model: &Model, names: &[String]) -> String { + let mut out = "(".to_owned(); + let mut emitted = 0usize; + for name in names { + if let Some(value) = model.get(name) { + if emitted > 0 { + out.push(' '); + } + emitted += 1; + out.push('('); + out.push_str("e_symbol(name)); + out.push(' '); + out.push_str(&scalar_to_smt(value)); + out.push(')'); + } + } + out.push_str(")\n"); + out +} + +fn quote_symbol(name: &str) -> String { + if name + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || "~!@$%^&*_+-=<>.?/".contains(ch)) + && !name.is_empty() + && !name.chars().next().unwrap().is_ascii_digit() + { + name.to_owned() + } else { + format!("|{}|", name.replace('|', "||")) + } +} + +fn expect_len(items: &[SExpr], len: usize, context: &'static str) -> Result<()> { + if items.len() != len { + return Err(Error::invalid(context, format!("expected {len} items"))); + } + Ok(()) +} + +fn expect_sort(actual: SmtSort, expected: SmtSort, context: &'static str) -> Result<()> { + if actual != expected { + return Err(Error::invalid( + context, + format!("expected {expected:?}, got {actual:?}"), + )); + } + Ok(()) +} + +fn atom(expr: &SExpr) -> Result<&str> { + match expr { + SExpr::Atom(atom) => Ok(atom), + SExpr::List(_) => Err(Error::invalid("SMT-LIB atom", "expected atom")), + } +} + +fn parse_u32_atom(expr: &SExpr, context: &'static str) -> Result { + atom(expr)? + .parse::() + .map_err(|_| Error::invalid(context, "expected u32 numeral")) +} + +fn parse_sexprs(input: &str) -> Result> { + let mut action = SExprAction; + yaspar::smtlib2::ScriptParser::new() + .parse(&mut action, yaspar::tokenize_str(input, true)) + .map_err(|err| Error::parse(err.to_string())) +} + +struct SExprAction; + +fn sexpr_atom(value: impl Into) -> SExpr { + SExpr::Atom(value.into()) +} + +fn sexpr_list(items: impl IntoIterator) -> SExpr { + SExpr::List(items.into_iter().collect()) +} + +fn indexed_symbol(symbol: String, indices: Vec) -> SExpr { + if indices.is_empty() { + sexpr_atom(symbol) + } else { + let mut items = Vec::with_capacity(indices.len() + 2); + items.push(sexpr_atom("_")); + items.push(sexpr_atom(symbol)); + items.extend(indices); + sexpr_list(items) + } +} + +fn byte_bits(bytes: &[u8], len: usize) -> String { + let mut out = String::with_capacity(len + 2); + out.push_str("#b"); + for bit in (0..len).rev() { + let byte = bytes[bit / 8]; + out.push(if ((byte >> (bit % 8)) & 1) != 0 { + '1' + } else { + '0' + }); + } + out +} + +fn byte_hex(bytes: &[u8], len: usize) -> String { + let mut out = String::with_capacity(len + 2); + out.push_str("#x"); + for nibble in (0..len).rev() { + let byte = bytes[nibble / 2]; + let value = (byte >> ((nibble % 2) * 4)) & 0xf; + out.push(char::from_digit(u32::from(value), 16).expect("hex digit")); + } + out +} + +fn command(name: &str, args: impl IntoIterator) -> SExpr { + let mut items = vec![sexpr_atom(name)]; + items.extend(args); + sexpr_list(items) +} + +fn vars_to_sexpr(vars: Vec<(String, SExpr)>) -> SExpr { + sexpr_list( + vars.into_iter() + .map(|(name, sort)| sexpr_list([sexpr_atom(name), sort])), + ) +} + +impl ActionOnString for SExprAction { + type Str = String; + + fn on_string(&mut self, _range: Range, s: String) -> ParsingResult { + Ok(s) + } +} + +impl ActionOnConstant for SExprAction { + type Constant = SExpr; + + fn on_constant_binary( + &mut self, + _range: Range, + bytes: Vec, + len: usize, + ) -> ParsingResult { + Ok(sexpr_atom(byte_bits(&bytes, len))) + } + + fn on_constant_hexadecimal( + &mut self, + _range: Range, + bytes: Vec, + len: usize, + ) -> ParsingResult { + Ok(sexpr_atom(byte_hex(&bytes, len))) + } + + fn on_constant_decimal( + &mut self, + _range: Range, + decimal: DBig, + ) -> ParsingResult { + Ok(sexpr_atom(decimal.to_string())) + } + + fn on_constant_numeral( + &mut self, + _range: Range, + numeral: UBig, + ) -> ParsingResult { + Ok(sexpr_atom(numeral.to_string())) + } + + fn on_constant_string( + &mut self, + _range: Range, + string: Self::Str, + ) -> ParsingResult { + Ok(sexpr_atom(string)) + } + + fn on_constant_bool(&mut self, _range: Range, boolean: bool) -> ParsingResult { + Ok(sexpr_atom(if boolean { "true" } else { "false" })) + } +} + +impl ActionOnIndex for SExprAction { + type Index = SExpr; + + fn on_index_numeral(&mut self, _range: Range, index: UBig) -> ParsingResult { + Ok(sexpr_atom(index.to_string())) + } + + fn on_index_symbol(&mut self, _range: Range, index: Self::Str) -> ParsingResult { + Ok(sexpr_atom(index)) + } + + fn on_index_hexadecimal( + &mut self, + _range: Range, + bytes: Vec, + len: usize, + ) -> ParsingResult { + Ok(sexpr_atom(byte_hex(&bytes, len))) + } +} + +impl ActionOnIdentifier for SExprAction { + type Identifier = SExpr; + + fn on_identifier( + &mut self, + _range: Range, + symbol: Self::Str, + indices: Vec, + ) -> ParsingResult { + Ok(indexed_symbol(symbol, indices)) + } +} + +impl ActionOnAttribute for SExprAction { + type Term = SExpr; + type Attribute = SExpr; + + fn on_attribute_keyword( + &mut self, + _range: Range, + keyword: Keyword, + ) -> ParsingResult { + Ok(sexpr_list([sexpr_atom(keyword.to_string())])) + } + + fn on_attribute_constant( + &mut self, + _range: Range, + keyword: Keyword, + constant: Self::Constant, + ) -> ParsingResult { + Ok(sexpr_list([sexpr_atom(keyword.to_string()), constant])) + } + + fn on_attribute_symbol( + &mut self, + _range: Range, + keyword: Keyword, + symbol: Self::Str, + ) -> ParsingResult { + Ok(sexpr_list([ + sexpr_atom(keyword.to_string()), + sexpr_atom(symbol), + ])) + } + + fn on_attribute_named( + &mut self, + _range: Range, + name: Self::Str, + ) -> ParsingResult { + Ok(sexpr_list([sexpr_atom(":named"), sexpr_atom(name)])) + } + + fn on_attribute_pattern( + &mut self, + _range: Range, + patterns: Vec, + ) -> ParsingResult { + Ok(command(":pattern", patterns)) + } +} + +impl ActionOnSort for SExprAction { + type Sort = SExpr; + + fn on_sort( + &mut self, + _range: Range, + identifier: Self::Identifier, + args: Vec, + ) -> ParsingResult { + if args.is_empty() { + Ok(identifier) + } else { + let mut items = vec![identifier]; + items.extend(args); + Ok(sexpr_list(items)) + } + } +} + +impl ActionOnTerm for SExprAction { + fn on_term_constant( + &mut self, + _range: Range, + constant: Self::Constant, + ) -> ParsingResult { + Ok(constant) + } + + fn on_term_identifier( + &mut self, + _range: Range, + identifier: Self::Identifier, + sort: Option, + ) -> ParsingResult { + Ok(match sort { + Some(sort) => command("as", [identifier, sort]), + None => identifier, + }) + } + + fn on_term_app( + &mut self, + _range: Range, + identifier: Self::Identifier, + sort: Option, + args: Vec, + ) -> ParsingResult { + let head = match sort { + Some(sort) => command("as", [identifier, sort]), + None => identifier, + }; + let mut items = vec![head]; + items.extend(args); + Ok(sexpr_list(items)) + } + + fn on_term_let( + &mut self, + _range: Range, + bindings: Vec<(Self::Str, Self::Term)>, + body: Self::Term, + ) -> ParsingResult { + let binding_list = sexpr_list( + bindings + .into_iter() + .map(|(name, term)| sexpr_list([sexpr_atom(name), term])), + ); + Ok(command("let", [binding_list, body])) + } + + fn on_term_lambda( + &mut self, + _range: Range, + names: Vec<(Self::Str, Self::Sort)>, + body: Self::Term, + ) -> ParsingResult { + Ok(command("lambda", [vars_to_sexpr(names), body])) + } + + fn on_term_exists( + &mut self, + _range: Range, + names: Vec<(Self::Str, Self::Sort)>, + body: Self::Term, + ) -> ParsingResult { + Ok(command("exists", [vars_to_sexpr(names), body])) + } + + fn on_term_forall( + &mut self, + _range: Range, + names: Vec<(Self::Str, Self::Sort)>, + body: Self::Term, + ) -> ParsingResult { + Ok(command("forall", [vars_to_sexpr(names), body])) + } + + fn on_term_match( + &mut self, + _range: Range, + scrutinee: Self::Term, + cases: Vec<(Pattern, Self::Term)>, + ) -> ParsingResult { + let case_exprs = cases.into_iter().map(|(_, body)| body); + Ok(command("match", [scrutinee, sexpr_list(case_exprs)])) + } + + fn on_term_annotated( + &mut self, + _range: Range, + t: Self::Term, + attributes: Vec, + ) -> ParsingResult { + let mut items = vec![sexpr_atom("!"), t]; + for attribute in attributes { + match attribute { + SExpr::List(values) => items.extend(values), + atom @ SExpr::Atom(_) => items.push(atom), + } + } + Ok(sexpr_list(items)) + } +} + +impl ParsingAction for SExprAction { + type Command = SExpr; + + fn on_command_assert(&mut self, _range: Range, t: Self::Term) -> ParsingResult { + Ok(command("assert", [t])) + } + + fn on_command_check_sat(&mut self, _range: Range) -> ParsingResult { + Ok(command("check-sat", [])) + } + + fn on_command_check_sat_assuming( + &mut self, + _range: Range, + terms: Vec, + ) -> ParsingResult { + Ok(command("check-sat-assuming", [sexpr_list(terms)])) + } + + fn on_command_declare_const( + &mut self, + _range: Range, + name: Self::Str, + sort: Self::Sort, + ) -> ParsingResult { + Ok(command("declare-const", [sexpr_atom(name), sort])) + } + + fn on_command_declare_datatype( + &mut self, + _range: Range, + name: Self::Str, + _datatype: DatatypeDec, + ) -> ParsingResult { + Ok(command("declare-datatype", [sexpr_atom(name)])) + } + + fn on_command_declare_datatypes( + &mut self, + _range: Range, + _defs: Vec>, + ) -> ParsingResult { + Ok(command("declare-datatypes", [])) + } + + fn on_command_declare_fun( + &mut self, + _range: Range, + name: Self::Str, + input_sorts: Vec, + out_sort: Self::Sort, + ) -> ParsingResult { + Ok(command( + "declare-fun", + [sexpr_atom(name), sexpr_list(input_sorts), out_sort], + )) + } + + fn on_command_declare_sort( + &mut self, + _range: Range, + name: Self::Str, + arity: UBig, + ) -> ParsingResult { + Ok(command( + "declare-sort", + [sexpr_atom(name), sexpr_atom(arity.to_string())], + )) + } + + fn on_command_declare_sort_parameter( + &mut self, + _range: Range, + name: Self::Str, + ) -> ParsingResult { + Ok(command("declare-sort-parameter", [sexpr_atom(name)])) + } + + fn on_command_define_const( + &mut self, + _range: Range, + name: Self::Str, + sort: Self::Sort, + term: Self::Term, + ) -> ParsingResult { + Ok(command("define-const", [sexpr_atom(name), sort, term])) + } + + fn on_command_define_fun( + &mut self, + _range: Range, + definition: FunctionDef, + ) -> ParsingResult { + Ok(command( + "define-fun", + [ + sexpr_atom(definition.name), + vars_to_sexpr(definition.vars), + definition.out_sort, + definition.body, + ], + )) + } + + fn on_command_define_fun_rec( + &mut self, + _range: Range, + definition: FunctionDef, + ) -> ParsingResult { + Ok(command( + "define-fun-rec", + [ + sexpr_atom(definition.name), + vars_to_sexpr(definition.vars), + definition.out_sort, + definition.body, + ], + )) + } + + fn on_command_define_funs_rec( + &mut self, + _range: Range, + _definitions: Vec>, + ) -> ParsingResult { + Ok(command("define-funs-rec", [])) + } + + fn on_command_define_sort( + &mut self, + _range: Range, + name: Self::Str, + params: Vec, + sort: Self::Sort, + ) -> ParsingResult { + Ok(command( + "define-sort", + [ + sexpr_atom(name), + sexpr_list(params.into_iter().map(sexpr_atom)), + sort, + ], + )) + } + + fn on_command_echo(&mut self, _range: Range, s: Self::Str) -> ParsingResult { + Ok(command("echo", [sexpr_atom(s)])) + } + + fn on_command_exit(&mut self, _range: Range) -> ParsingResult { + Ok(command("exit", [])) + } + + fn on_command_get_assertions(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-assertions", [])) + } + + fn on_command_get_assignment(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-assignment", [])) + } + + fn on_command_get_info(&mut self, _range: Range, kw: Keyword) -> ParsingResult { + Ok(command("get-info", [sexpr_atom(kw.to_string())])) + } + + fn on_command_get_model(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-model", [])) + } + + fn on_command_get_option( + &mut self, + _range: Range, + kw: Keyword, + ) -> ParsingResult { + Ok(command("get-option", [sexpr_atom(kw.to_string())])) + } + + fn on_command_get_proof(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-proof", [])) + } + + fn on_command_get_unsat_assumptions(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-unsat-assumptions", [])) + } + + fn on_command_get_unsat_core(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-unsat-core", [])) + } + + fn on_command_get_value( + &mut self, + _range: Range, + ts: Vec, + ) -> ParsingResult { + Ok(command("get-value", [sexpr_list(ts)])) + } + + fn on_command_pop(&mut self, _range: Range, lvl: UBig) -> ParsingResult { + Ok(command("pop", [sexpr_atom(lvl.to_string())])) + } + + fn on_command_push(&mut self, _range: Range, lvl: UBig) -> ParsingResult { + Ok(command("push", [sexpr_atom(lvl.to_string())])) + } + + fn on_command_reset(&mut self, _range: Range) -> ParsingResult { + Ok(command("reset", [])) + } + + fn on_command_reset_assertions(&mut self, _range: Range) -> ParsingResult { + Ok(command("reset-assertions", [])) + } + + fn on_command_set_info( + &mut self, + _range: Range, + attributes: Self::Attribute, + ) -> ParsingResult { + Ok(command("set-info", [attributes])) + } + + fn on_command_set_logic( + &mut self, + _range: Range, + logic: Self::Str, + ) -> ParsingResult { + Ok(command("set-logic", [sexpr_atom(logic)])) + } + + fn on_command_set_option( + &mut self, + _range: Range, + attribute: Self::Attribute, + ) -> ParsingResult { + Ok(command("set-option", [attribute])) + } +} + +``` + +`crates/qfbvsmtrs/src/gates.rs`: + +```rs +use std::collections::HashMap; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct GateId(pub(crate) u32); + +impl GateId { + pub const fn index(self) -> usize { + self.0 as usize + } + + pub const fn raw(self) -> u32 { + self.0 + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum GateKind { + Const(bool), + Input(u32), + Not(GateId), + And(GateId, GateId), + Or(GateId, GateId), + Xor(GateId, GateId), + Mux { sel: GateId, t: GateId, f: GateId }, +} + +#[derive(Debug, Clone)] +pub struct GateArena { + gates: Vec, + ids: HashMap, + false_id: GateId, + true_id: GateId, + input_count: u32, +} + +impl Default for GateArena { + fn default() -> Self { + Self::new() + } +} + +impl GateArena { + pub fn new() -> Self { + let mut arena = Self { + gates: Vec::new(), + ids: HashMap::new(), + false_id: GateId(0), + true_id: GateId(1), + input_count: 0, + }; + let f = arena.add_raw(GateKind::Const(false)); + let t = arena.add_raw(GateKind::Const(true)); + arena.false_id = f; + arena.true_id = t; + arena + } + + pub fn gates(&self) -> &[GateKind] { + &self.gates + } + + pub const fn false_gate(&self) -> GateId { + self.false_id + } + + pub const fn true_gate(&self) -> GateId { + self.true_id + } + + pub fn input_count(&self) -> u32 { + self.input_count + } + + pub fn input(&mut self) -> GateId { + let id = self.input_count; + self.input_count += 1; + self.add_raw(GateKind::Input(id)) + } + + pub fn const_gate(&self, value: bool) -> GateId { + if value { + self.true_id + } else { + self.false_id + } + } + + pub fn not(&mut self, x: GateId) -> GateId { + if x == self.false_id { + return self.true_id; + } + if x == self.true_id { + return self.false_id; + } + if let GateKind::Not(inner) = self.gates[x.index()] { + return inner; + } + self.add_raw(GateKind::Not(x)) + } + + pub fn and(&mut self, a: GateId, b: GateId) -> GateId { + let (a, b) = order_pair(a, b); + if a == self.false_id || b == self.false_id { + return self.false_id; + } + if a == self.true_id { + return b; + } + if b == self.true_id || a == b { + return a; + } + if self.is_negation_pair(a, b) { + return self.false_id; + } + self.add_raw(GateKind::And(a, b)) + } + + pub fn or(&mut self, a: GateId, b: GateId) -> GateId { + let (a, b) = order_pair(a, b); + if a == self.true_id || b == self.true_id { + return self.true_id; + } + if a == self.false_id { + return b; + } + if b == self.false_id || a == b { + return a; + } + if self.is_negation_pair(a, b) { + return self.true_id; + } + self.add_raw(GateKind::Or(a, b)) + } + + pub fn xor(&mut self, a: GateId, b: GateId) -> GateId { + let (a, b) = order_pair(a, b); + if a == self.false_id { + return b; + } + if b == self.false_id { + return a; + } + if a == b { + return self.false_id; + } + if a == self.true_id { + return self.not(b); + } + if b == self.true_id { + return self.not(a); + } + self.add_raw(GateKind::Xor(a, b)) + } + + pub fn xnor(&mut self, a: GateId, b: GateId) -> GateId { + let xor = self.xor(a, b); + self.not(xor) + } + + pub fn mux(&mut self, sel: GateId, t: GateId, f: GateId) -> GateId { + if sel == self.true_id { + return t; + } + if sel == self.false_id { + return f; + } + if t == f { + return t; + } + if t == self.true_id && f == self.false_id { + return sel; + } + if t == self.false_id && f == self.true_id { + return self.not(sel); + } + self.add_raw(GateKind::Mux { sel, t, f }) + } + + pub fn implies(&mut self, a: GateId, b: GateId) -> GateId { + let not_a = self.not(a); + self.or(not_a, b) + } + + pub fn and_many(&mut self, iter: I) -> GateId + where + I: IntoIterator, + { + let mut out = self.true_id; + for gate in iter { + out = self.and(out, gate); + if out == self.false_id { + break; + } + } + out + } + + pub fn or_many(&mut self, iter: I) -> GateId + where + I: IntoIterator, + { + let mut out = self.false_id; + for gate in iter { + out = self.or(out, gate); + if out == self.true_id { + break; + } + } + out + } + + fn add_raw(&mut self, kind: GateKind) -> GateId { + if let Some(existing) = self.ids.get(&kind) { + return *existing; + } + let id = GateId(self.gates.len() as u32); + self.gates.push(kind.clone()); + self.ids.insert(kind, id); + id + } + + fn is_negation_pair(&self, a: GateId, b: GateId) -> bool { + matches!(self.gates[a.index()], GateKind::Not(x) if x == b) + || matches!(self.gates[b.index()], GateKind::Not(x) if x == a) + } +} + +fn order_pair(a: GateId, b: GateId) -> (GateId, GateId) { + if a <= b { + (a, b) + } else { + (b, a) + } +} + +``` + +`crates/qfbvsmtrs/src/ir.rs`: + +```rs +use std::collections::HashMap; + +use crate::error::{Error, Result}; + +pub const MAX_BV_WIDTH: u32 = 65_536; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct TermId(pub(crate) u32); + +impl TermId { + pub const fn index(self) -> usize { + self.0 as usize + } + + pub const fn raw(self) -> u32 { + self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Sort { + Bool, + Bv(u32), +} + +impl Sort { + pub const fn is_bool(self) -> bool { + matches!(self, Sort::Bool) + } + + pub const fn bv_width(self) -> Option { + match self { + Sort::Bool => None, + Sort::Bv(width) => Some(width), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum NodeKind { + BvConst { + width: u32, + bytes: Vec, + }, + BvVar { + width: u32, + name: String, + external: Option, + }, + BoolConst(bool), + BoolVar { + name: String, + external: Option, + }, + + BvNot(TermId), + BvNeg(TermId), + BvAnd(TermId, TermId), + BvOr(TermId, TermId), + BvXor(TermId, TermId), + BvAdd(TermId, TermId), + BvSub(TermId, TermId), + BvMul(TermId, TermId), + BvUDiv(TermId, TermId), + BvURem(TermId, TermId), + BvSDiv(TermId, TermId), + BvSRem(TermId, TermId), + BvSMod(TermId, TermId), + BvShl(TermId, TermId), + BvLShr(TermId, TermId), + BvAShr(TermId, TermId), + BvExtract { + child: TermId, + high: u32, + low: u32, + }, + BvConcat(TermId, TermId), + BvZeroExtend { + child: TermId, + extra: u32, + }, + BvSignExtend { + child: TermId, + extra: u32, + }, + BvRepeat { + child: TermId, + count: u32, + }, + BvRotateLeft { + child: TermId, + amount: u32, + }, + BvRotateRight { + child: TermId, + amount: u32, + }, + BvIte { + cond: TermId, + then_value: TermId, + else_value: TermId, + }, + BvSelect { + cases: Vec<(TermId, TermId)>, + default: TermId, + }, + + BoolNot(TermId), + BoolAnd(TermId, TermId), + BoolOr(TermId, TermId), + BoolImplies(TermId, TermId), + BoolEq(TermId, TermId), + BoolIte { + cond: TermId, + then_value: TermId, + else_value: TermId, + }, + + BvEq(TermId, TermId), + BvUlt(TermId, TermId), + BvUle(TermId, TermId), + BvSlt(TermId, TermId), + BvSle(TermId, TermId), + + UAddOverflow(TermId, TermId), + SAddOverflow(TermId, TermId), + USubOverflow(TermId, TermId), + SSubOverflow(TermId, TermId), + UMulOverflow(TermId, TermId), + SMulOverflow(TermId, TermId), + NegOverflow(TermId), + SDivOverflow(TermId, TermId), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct NodeKey { + kind: NodeKind, + sort: Sort, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Node { + pub kind: NodeKind, + pub sort: Sort, +} + +#[derive(Debug, Clone, Default)] +pub struct Arena { + nodes: Vec, + ids: HashMap, +} + +impl Arena { + pub fn new() -> Self { + Self::default() + } + + pub fn len(&self) -> usize { + self.nodes.len() + } + + pub fn is_empty(&self) -> bool { + self.nodes.is_empty() + } + + pub fn nodes(&self) -> &[Node] { + &self.nodes + } + + pub fn node(&self, id: TermId) -> Result<&Node> { + self.nodes.get(id.index()).ok_or_else(|| { + Error::invalid( + "term id", + format!( + "term {} is out of bounds for {} nodes", + id.raw(), + self.nodes.len() + ), + ) + }) + } + + pub fn sort(&self, id: TermId) -> Result { + Ok(self.node(id)?.sort) + } + + pub fn add(&mut self, kind: NodeKind, sort: Sort) -> Result { + validate_sort(sort)?; + let key = NodeKey { + kind: kind.clone(), + sort, + }; + if let Some(existing) = self.ids.get(&key) { + return Ok(*existing); + } + let id = TermId( + u32::try_from(self.nodes.len()) + .map_err(|_| Error::invalid("arena", "node count exceeds u32::MAX"))?, + ); + self.nodes.push(Node { kind, sort }); + self.ids.insert(key, id); + Ok(id) + } + + pub fn expect_bool(&self, id: TermId, context: &'static str) -> Result<()> { + let sort = self.sort(id)?; + if sort != Sort::Bool { + return Err(Error::invalid( + context, + format!("expected Bool term, got {sort:?}"), + )); + } + Ok(()) + } + + pub fn expect_bv(&self, id: TermId, context: &'static str) -> Result { + match self.sort(id)? { + Sort::Bv(width) => Ok(width), + Sort::Bool => Err(Error::invalid(context, "expected BV term, got Bool")), + } + } + + pub fn expect_same_bv(&self, a: TermId, b: TermId, context: &'static str) -> Result { + let aw = self.expect_bv(a, context)?; + let bw = self.expect_bv(b, context)?; + if aw != bw { + return Err(Error::invalid( + context, + format!("BV width mismatch: {aw} vs {bw}"), + )); + } + Ok(aw) + } +} + +pub fn validate_bv_width(width: u32, context: &'static str) -> Result<()> { + if !(1..=MAX_BV_WIDTH).contains(&width) { + return Err(Error::invalid( + context, + format!("width {width} is outside 1..={MAX_BV_WIDTH}"), + )); + } + Ok(()) +} + +pub fn validate_sort(sort: Sort) -> Result<()> { + match sort { + Sort::Bool => Ok(()), + Sort::Bv(width) => validate_bv_width(width, "BV sort width"), + } +} + +pub fn bytes_for_width(width: u32) -> Result { + validate_bv_width(width, "BV width")?; + Ok((width as usize).div_ceil(8)) +} + +pub fn mask_unused_high_bits(bytes: &mut [u8], width: u32) { + let valid_bits = width % 8; + if valid_bits == 0 || bytes.is_empty() { + return; + } + let mask = (1u8 << valid_bits) - 1; + let last = bytes.len() - 1; + bytes[last] &= mask; +} + +pub fn normalized_bytes(bytes: &[u8], width: u32) -> Result> { + let expected = bytes_for_width(width)?; + if bytes.len() != expected { + return Err(Error::invalid( + "BV bytes", + format!( + "got {} bytes, expected {expected} for width {width}", + bytes.len() + ), + )); + } + let mut out = bytes.to_vec(); + mask_unused_high_bits(&mut out, width); + Ok(out) +} + +``` + +`crates/qfbvsmtrs/src/lib.rs`: + +```rs +//! Standalone pure-Rust QF_BV SMT solver via bit-blasting. + +pub mod blast; +pub mod builder; +pub mod circuits; +pub mod cnf; +pub mod config; +pub mod error; +pub(crate) mod eval; +pub mod frontend; +pub mod gates; +pub mod ir; +pub mod model; +pub mod query; +pub mod sat; +pub mod simplify; +pub mod solver; +#[cfg(feature = "wire")] +pub mod wire; + +pub use builder::Builder; +pub use config::{Config, SatBackendKind}; +pub use error::{Error, Result}; +pub use frontend::{format_smt2_response, parse_smt2, solve_smt2}; +pub use ir::{Sort, TermId}; +pub use model::{Model, ModelEntry, ScalarValue}; +pub use query::{Assertion, Command, Query}; +pub use solver::{solve_query, SolveResult, SolveStatus, Solver}; +#[cfg(feature = "wire")] +pub use wire::{model_to_wire, query_from_wire}; + +``` + +`crates/qfbvsmtrs/src/model.rs`: + +```rs +use crate::blast::BlastedVariable; +use crate::error::{Error, Result}; +use crate::ir::{bytes_for_width, mask_unused_high_bits, Sort, TermId}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ScalarValue { + Bool(bool), + Bv { width: u32, bytes: Vec }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModelEntry { + pub term: TermId, + pub name: String, + pub external: Option, + pub value: ScalarValue, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Model { + pub entries: Vec, +} + +impl Model { + pub fn get(&self, name: &str) -> Option<&ScalarValue> { + self.entries + .iter() + .find(|entry| entry.name == name) + .map(|entry| &entry.value) + } +} + +pub fn build_model(variables: &[BlastedVariable], assignment: &[bool]) -> Result { + let mut entries = Vec::with_capacity(variables.len()); + for variable in variables { + let value = match variable.sort { + Sort::Bool => { + let gate = variable.bits[0]; + ScalarValue::Bool(gate_value(assignment, gate)?) + } + Sort::Bv(width) => { + let mut bytes = vec![0u8; bytes_for_width(width)?]; + for (bit, gate) in variable.bits.iter().enumerate() { + if gate_value(assignment, *gate)? { + bytes[bit / 8] |= 1 << (bit % 8); + } + } + mask_unused_high_bits(&mut bytes, width); + ScalarValue::Bv { width, bytes } + } + }; + entries.push(ModelEntry { + term: variable.term, + name: variable.name.clone(), + external: variable.external, + value, + }); + } + Ok(Model { entries }) +} + +pub fn scalar_to_smt(value: &ScalarValue) -> String { + match value { + ScalarValue::Bool(false) => "false".to_owned(), + ScalarValue::Bool(true) => "true".to_owned(), + ScalarValue::Bv { width, bytes } => { + let mut out = String::with_capacity(*width as usize + 2); + out.push_str("#b"); + for bit in (0..*width).rev() { + let byte = bytes[(bit / 8) as usize]; + out.push(if ((byte >> (bit % 8)) & 1) != 0 { + '1' + } else { + '0' + }); + } + out + } + } +} + +fn gate_value(assignment: &[bool], gate: crate::gates::GateId) -> Result { + assignment.get(gate.index()).copied().ok_or_else(|| { + Error::internal(format!( + "missing SAT assignment for gate {} ({} values)", + gate.raw(), + assignment.len() + )) + }) +} + +``` + +`crates/qfbvsmtrs/src/query.rs`: + +```rs +use crate::ir::{Arena, TermId}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Command { + Solve, + Simplify, + Minimize, + Maximize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Assertion { + pub root: TermId, + pub name: Option, +} + +#[derive(Debug, Clone)] +pub struct Query { + pub arena: Arena, + pub assertions: Vec, + pub assumptions: Vec, + pub command: Command, + pub target: Option, + pub signed: bool, + pub want_model: bool, + pub want_core: bool, + pub get_values: Vec, +} + +impl Query { + pub fn assertions_and_assumptions(&self) -> impl Iterator + '_ { + self.assertions + .iter() + .map(|assertion| assertion.root) + .chain(self.assumptions.iter().copied()) + } +} + +``` + +`crates/qfbvsmtrs/src/sat.rs`: + +```rs +use std::time::Instant; + +use crate::config::SatBackendKind; +use crate::error::Error; +use varisat::ExtendFormula; + +const BUDGETED_SPLR_CLAUSE_LIMIT: usize = 250_000; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SatResult { + Sat(Vec), // 1-based CNF variable values are returned at index var-1 + Unsat, + Unknown(String), +} + +pub fn solve_cnf( + backend: SatBackendKind, + num_vars: usize, + clauses: Vec>, + assumptions: &[i32], + deadline: Option, +) -> SatResult { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { + return SatResult::Unknown("budget exhausted".to_owned()); + } + match backend { + SatBackendKind::Splr + if deadline.is_some() && clauses.len() > BUDGETED_SPLR_CLAUSE_LIMIT => + { + DpllSolver::new(num_vars, clauses, deadline).solve(assumptions) + } + SatBackendKind::Splr => solve_with_splr(num_vars, clauses, assumptions, deadline), + SatBackendKind::Varisat => solve_with_varisat(num_vars, clauses, assumptions, deadline), + SatBackendKind::Dpll => DpllSolver::new(num_vars, clauses, deadline).solve(assumptions), + } +} + +fn solve_with_varisat( + num_vars: usize, + clauses: Vec>, + assumptions: &[i32], + deadline: Option, +) -> SatResult { + if deadline.is_some() { + // Varisat's public library API has assumption support but no interrupt/timeout hook. + // Keep budgeted solves on backends that can poll or honor a time limit. + return SatResult::Unknown("varisat backend does not support deadlines".to_owned()); + } + + let mut formula = varisat::CnfFormula::new(); + formula.set_var_count(num_vars); + for clause in &clauses { + let lits = clause + .iter() + .map(|&lit| varisat::Lit::from_dimacs(lit as isize)) + .collect::>(); + formula.add_clause(&lits); + } + + let mut solver = varisat::Solver::new(); + solver.add_formula(&formula); + let assumption_lits = assumptions + .iter() + .map(|&lit| varisat::Lit::from_dimacs(lit as isize)) + .collect::>(); + solver.assume(&assumption_lits); + + match solver.solve() { + Ok(true) => { + let mut assignment = vec![false; num_vars]; + if let Some(model) = solver.model() { + for lit in model { + let index = lit.index(); + if index < assignment.len() { + assignment[index] = lit.is_positive(); + } + } + } + SatResult::Sat(assignment) + } + Ok(false) => SatResult::Unsat, + Err(err) => SatResult::Unknown(format!("varisat error: {err}")), + } +} + +fn solve_with_splr( + num_vars: usize, + clauses: Vec>, + assumptions: &[i32], + deadline: Option, +) -> SatResult { + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + solve_with_splr_inner(num_vars, clauses, assumptions, deadline) + })) { + Ok(result) => result, + Err(_) => SatResult::Unknown("splr backend panicked".to_owned()), + } +} + +fn solve_with_splr_inner( + num_vars: usize, + mut clauses: Vec>, + assumptions: &[i32], + deadline: Option, +) -> SatResult { + for &assumption in assumptions { + clauses.push(vec![assumption]); + } + + let mut config = splr::Config { + quiet_mode: true, + use_log: false, + show_journal: false, + ..splr::Config::default() + }; + if let Some(deadline) = deadline { + let Some(remaining) = deadline.checked_duration_since(Instant::now()) else { + return SatResult::Unknown("budget exhausted".to_owned()); + }; + config.c_timeout = remaining.as_secs_f64(); + } + + match splr::Solver::try_from((config, clauses.as_ref())) { + Ok(mut solver) => match splr::SolveIF::solve(&mut solver) { + Ok(splr::Certificate::SAT(model)) => { + let mut assignment = vec![false; num_vars]; + for lit in model { + let index = lit.unsigned_abs() as usize; + if (1..=num_vars).contains(&index) { + assignment[index - 1] = lit > 0; + } + } + SatResult::Sat(assignment) + } + Ok(splr::Certificate::UNSAT) => SatResult::Unsat, + Err(splr::SolverError::EmptyClause) + | Err(splr::SolverError::Inconsistent) + | Err(splr::SolverError::RootLevelConflict(_)) => SatResult::Unsat, + Err(splr::SolverError::TimeOut) => SatResult::Unknown("budget exhausted".to_owned()), + Err(err) => SatResult::Unknown(format!("splr error: {err}")), + }, + Err(Ok(splr::Certificate::UNSAT)) => SatResult::Unsat, + Err(Ok(splr::Certificate::SAT(model))) => { + let mut assignment = vec![false; num_vars]; + for lit in model { + let index = lit.unsigned_abs() as usize; + if (1..=num_vars).contains(&index) { + assignment[index - 1] = lit > 0; + } + } + SatResult::Sat(assignment) + } + Err(Err(splr::SolverError::EmptyClause)) + | Err(Err(splr::SolverError::Inconsistent)) + | Err(Err(splr::SolverError::RootLevelConflict(_))) => SatResult::Unsat, + Err(Err(splr::SolverError::TimeOut)) => SatResult::Unknown("budget exhausted".to_owned()), + Err(Err(err)) => SatResult::Unknown(format!("splr error: {err}")), + } +} + +#[derive(Debug, Clone)] +pub struct DpllSolver { + num_vars: usize, + clauses: Vec>, + scores: Vec, + deadline: Option, +} + +impl DpllSolver { + pub fn new(num_vars: usize, clauses: Vec>, deadline: Option) -> Self { + let mut scores = vec![0usize; num_vars + 1]; + for clause in &clauses { + for &lit in clause { + scores[lit.unsigned_abs() as usize] += 1; + } + } + Self { + num_vars, + clauses, + scores, + deadline, + } + } + + pub fn solve(&self, assumptions: &[i32]) -> SatResult { + let mut assignment = vec![None; self.num_vars + 1]; + for &assumption in assumptions { + if let Err(()) = assign_lit(&mut assignment, assumption) { + return SatResult::Unsat; + } + } + match self.search(assignment) { + Ok(Some(model)) => { + let values = (1..=self.num_vars) + .map(|var| model[var].unwrap_or(false)) + .collect(); + SatResult::Sat(values) + } + Ok(None) => SatResult::Unsat, + Err(Error::Timeout) => SatResult::Unknown("budget exhausted".to_owned()), + Err(err) => SatResult::Unknown(err.to_string()), + } + } + + fn search( + &self, + mut assignment: Vec>, + ) -> Result>>, Error> { + self.check_deadline()?; + if !self.unit_propagate(&mut assignment)? { + return Ok(None); + } + if self.all_clauses_satisfied(&assignment) { + return Ok(Some(assignment)); + } + let Some(var) = self.choose_var(&assignment) else { + return Ok(Some(assignment)); + }; + + let mut true_branch = assignment.clone(); + true_branch[var] = Some(true); + if let Some(model) = self.search(true_branch)? { + return Ok(Some(model)); + } + + let mut false_branch = assignment; + false_branch[var] = Some(false); + self.search(false_branch) + } + + fn unit_propagate(&self, assignment: &mut [Option]) -> Result { + loop { + self.check_deadline()?; + let mut changed = false; + for clause in &self.clauses { + let mut satisfied = false; + let mut unassigned = 0usize; + let mut last_unassigned = 0i32; + for &lit in clause { + match lit_value(assignment, lit) { + Some(true) => { + satisfied = true; + break; + } + Some(false) => {} + None => { + unassigned += 1; + last_unassigned = lit; + } + } + } + if satisfied { + continue; + } + if unassigned == 0 { + return Ok(false); + } + if unassigned == 1 { + if assign_lit(assignment, last_unassigned).is_err() { + return Ok(false); + } + changed = true; + } + } + if !changed { + return Ok(true); + } + } + } + + fn all_clauses_satisfied(&self, assignment: &[Option]) -> bool { + self.clauses.iter().all(|clause| { + clause + .iter() + .any(|&lit| matches!(lit_value(assignment, lit), Some(true))) + }) + } + + fn choose_var(&self, assignment: &[Option]) -> Option { + (1..=self.num_vars) + .filter(|&var| assignment[var].is_none()) + .max_by_key(|&var| self.scores[var]) + } + + fn check_deadline(&self) -> Result<(), Error> { + if let Some(deadline) = self.deadline { + if Instant::now() >= deadline { + return Err(Error::Timeout); + } + } + Ok(()) + } +} + +fn assign_lit(assignment: &mut [Option], lit: i32) -> Result<(), ()> { + let var = lit.unsigned_abs() as usize; + let value = lit > 0; + match assignment[var] { + Some(existing) if existing != value => Err(()), + Some(_) => Ok(()), + None => { + assignment[var] = Some(value); + Ok(()) + } + } +} + +fn lit_value(assignment: &[Option], lit: i32) -> Option { + let var = lit.unsigned_abs() as usize; + assignment[var].map(|value| if lit > 0 { value } else { !value }) +} + +``` + +`crates/qfbvsmtrs/src/simplify.rs`: + +```rs +use crate::query::Query; + +/// Runs the current word-level simplification pass. +/// +/// Most cheap rewrites are applied eagerly by `Builder` while terms are +/// constructed: constant folding for common BV/Bool operations, identity and +/// annihilator rules, double negation, and trivial ITE reduction. This function +/// is intentionally cheap today and exists as the stable hook for future +/// whole-query/worklist rewrites such as extract/concat merging. +pub fn simplify(query: Query) -> Query { + query +} + +``` + +`crates/qfbvsmtrs/src/solver.rs`: + +```rs +use std::collections::BTreeMap; +use std::time::Instant; + +use crate::blast::blast_query_with_deadline; +use crate::cnf; +use crate::config::Config; +use crate::error::{Error, Result}; +use crate::ir::{NodeKind, Sort, TermId}; +use crate::model::{build_model, Model, ScalarValue}; +use crate::query::{Assertion, Command, Query}; +use crate::sat::{solve_cnf, SatResult}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SolveStatus { + Sat, + Unsat, + Unknown, + Ok, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveResult { + pub status: SolveStatus, + pub model: Option, + pub core: Option>, + pub optimum: Option, + pub message: Option, +} + +impl SolveResult { + pub fn sat(model: Option) -> Self { + Self { + status: SolveStatus::Sat, + model, + core: None, + optimum: None, + message: None, + } + } + + pub fn unsat() -> Self { + Self { + status: SolveStatus::Unsat, + model: None, + core: None, + optimum: None, + message: None, + } + } + + pub fn sat_optimization(optimum: ScalarValue, model: Option) -> Self { + Self { + status: SolveStatus::Sat, + model, + core: None, + optimum: Some(optimum), + message: None, + } + } + + pub fn unknown(message: impl Into) -> Self { + Self { + status: SolveStatus::Unknown, + model: None, + core: None, + optimum: None, + message: Some(message.into()), + } + } + + pub fn ok() -> Self { + Self { + status: SolveStatus::Ok, + model: None, + core: None, + optimum: None, + message: None, + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct Solver { + config: Config, +} + +impl Solver { + pub fn new(config: Config) -> Self { + Self { config } + } + + pub fn config(&self) -> &Config { + &self.config + } + + pub fn solve(&mut self, query: &Query) -> Result { + if matches!(query.command, Command::Minimize | Command::Maximize) { + return self.optimize(query); + } + let mut result = self.solve_once(query, query.want_model)?; + if result.status == SolveStatus::Unsat && query.want_core { + result.core = Some(self.extract_named_core(query)?); + } + Ok(result) + } + + fn solve_once(&self, query: &Query, want_model: bool) -> Result { + match query.command { + Command::Simplify => return Ok(SolveResult::ok()), + Command::Minimize | Command::Maximize => { + return Err(Error::internal( + "solve_once called with optimization command", + )) + } + Command::Solve => {} + } + + if has_unsigned_successor_contradiction(query)? + || has_shift_one_add_contradiction(query)? + || has_distinct_power_of_two_sum_contradiction(query)? + || has_unsigned_multiplication_overflow_guard_contradiction(query)? + || has_signed_division_multiply_overflow_guard_contradiction(query)? + || has_extensional_candidate_contradiction(query)? + || has_log_slicing_adder_contradiction(query)? + { + return Ok(SolveResult::unsat()); + } + if !want_model && has_unsigned_successor_wraparound_witness(query)? { + return Ok(SolveResult::sat(None)); + } + if !want_model && !query.want_core && has_constant_assignment_witness(query)? { + return Ok(SolveResult::sat(None)); + } + + let deadline = self.config.budget.map(|budget| Instant::now() + budget); + let blasted = match blast_query_with_deadline(query, deadline) { + Ok(blasted) => blasted, + Err(Error::Timeout) => return Ok(SolveResult::unknown("budget exhausted")), + Err(err) => return Err(err), + }; + let cnf = match cnf::encode_with_deadline(&blasted.gates, blasted.assertion, deadline) { + Ok(cnf) => cnf, + Err(Error::Timeout) => return Ok(SolveResult::unknown("budget exhausted")), + Err(err) => return Err(err), + }; + let sat = solve_cnf( + self.config.sat_backend, + cnf.num_vars, + cnf.clauses, + &[], + deadline, + ); + match sat { + SatResult::Sat(assignment) => { + let model = if want_model { + Some(build_model(&blasted.variables, &assignment)?) + } else { + None + }; + Ok(SolveResult::sat(model)) + } + SatResult::Unsat => Ok(SolveResult::unsat()), + SatResult::Unknown(message) => Ok(SolveResult::unknown(message)), + } + } + + fn optimize(&self, query: &Query) -> Result { + let target = query + .target + .ok_or_else(|| Error::invalid("optimization", "missing target"))?; + let width = match query.arena.sort(target)? { + Sort::Bv(width) => width, + Sort::Bool => return Err(Error::invalid("optimization", "target is not BV")), + }; + let minimize = query.command == Command::Minimize; + let mut fixed = query.clone(); + fixed.command = Command::Solve; + fixed.want_model = false; + fixed.want_core = false; + fixed.target = None; + + match self.solve_once(&fixed, false)?.status { + SolveStatus::Sat => {} + SolveStatus::Unsat => return Ok(SolveResult::unsat()), + SolveStatus::Unknown => { + return Ok(SolveResult::unknown("optimization base query unknown")) + } + SolveStatus::Ok => { + return Ok(SolveResult::unknown("optimization base query returned OK")) + } + } + + let mut optimum = vec![0u8; (width as usize).div_ceil(8)]; + for bit in (0..width).rev() { + let prefer_one = match (query.signed, minimize, bit == width - 1) { + (false, true, _) => false, + (false, false, _) => true, + (true, true, true) => true, + (true, true, false) => false, + (true, false, true) => false, + (true, false, false) => true, + }; + let mut trial = fixed.clone(); + assert_target_bit(&mut trial, target, bit, prefer_one)?; + match self.solve_once(&trial, false)?.status { + SolveStatus::Sat => { + fixed = trial; + if prefer_one { + set_bit(&mut optimum, bit); + } + } + SolveStatus::Unsat => { + assert_target_bit(&mut fixed, target, bit, !prefer_one)?; + if !prefer_one { + set_bit(&mut optimum, bit); + } + } + SolveStatus::Unknown => { + return Ok(SolveResult::unknown("optimization bit query unknown")) + } + SolveStatus::Ok => { + return Ok(SolveResult::unknown("optimization bit query returned OK")) + } + } + } + + let model = if query.want_model { + match self.solve_once(&fixed, true)? { + SolveResult { + status: SolveStatus::Sat, + model, + .. + } => model, + other => { + return Ok(SolveResult::unknown(format!( + "optimization final model query returned {:?}", + other.status + ))) + } + } + } else { + None + }; + Ok(SolveResult::sat_optimization( + ScalarValue::Bv { + width, + bytes: optimum, + }, + model, + )) + } + + fn extract_named_core(&self, query: &Query) -> Result> { + let mut active = query + .assertions + .iter() + .enumerate() + .filter_map(|(index, assertion)| assertion.name.as_ref().map(|_| index)) + .collect::>(); + let mut pos = 0; + while pos < active.len() { + let candidate = active[pos]; + let trial_active = active + .iter() + .copied() + .filter(|&index| index != candidate) + .collect::>(); + let trial = query_with_named_subset(query, &trial_active); + match self.solve_once(&trial, false)?.status { + SolveStatus::Unsat => active = trial_active, + SolveStatus::Sat | SolveStatus::Unknown | SolveStatus::Ok => pos += 1, + } + } + Ok(active + .into_iter() + .filter_map(|index| query.assertions[index].name.clone()) + .collect()) + } +} + +fn has_unsigned_successor_contradiction(query: &Query) -> Result { + let mut strict = Vec::new(); + for assertion in &query.assertions { + collect_unsigned_strict_less(query, assertion.root, &mut strict)?; + } + for &(x, y) in &strict { + if strict.iter().any(|&(left, right)| { + left == y && is_unsigned_successor_term(query, right, x).unwrap_or(false) + }) { + return Ok(true); + } + } + Ok(false) +} + +fn collect_unsigned_strict_less( + query: &Query, + term: TermId, + out: &mut Vec<(TermId, TermId)>, +) -> Result<()> { + match &query.arena.node(term)?.kind { + NodeKind::BvUlt(a, b) => out.push((*a, *b)), + NodeKind::BoolAnd(a, b) => { + collect_unsigned_strict_less(query, *a, out)?; + collect_unsigned_strict_less(query, *b, out)?; + } + _ => {} + } + Ok(()) +} + +fn is_unsigned_successor_term(query: &Query, term: TermId, base: TermId) -> Result { + let NodeKind::BvAdd(a, b) = &query.arena.node(term)?.kind else { + return Ok(false); + }; + Ok((*a == base && is_one_bv_const(query, *b)?) || (*b == base && is_one_bv_const(query, *a)?)) +} + +fn has_constant_assignment_witness(query: &Query) -> Result { + if !query.assumptions.is_empty() || query.arena.len() > 100_000 { + return Ok(false); + } + if crate::eval::query_satisfied_by_constant_assignment(query, false)? + || crate::eval::query_satisfied_by_constant_assignment(query, true)? + { + return Ok(true); + } + if !small_enough_for_seeded_witness(query)? { + return Ok(false); + } + for seed in [ + 0x243f_6a88_85a3_08d3, + 0x1319_8a2e_0370_7344, + 0xa409_3822_299f_31d0, + 0x082e_fa98_ec4e_6c89, + 0x4528_21e6_38d0_1377, + 0xbe54_66cf_34e9_0c6c, + 0xc0ac_29b7_c97c_50dd, + 0x3f84_d5b5_b547_0917, + ] { + if crate::eval::query_satisfied_by_seeded_assignment(query, seed)? { + return Ok(true); + } + } + Ok(false) +} + +fn small_enough_for_seeded_witness(query: &Query) -> Result { + let mut bits = 0u64; + for node in query.arena.nodes() { + match node.kind { + NodeKind::BvVar { width, .. } => bits = bits.saturating_add(u64::from(width)), + NodeKind::BoolVar { .. } => bits = bits.saturating_add(1), + _ => {} + } + if bits > 4096 { + return Ok(false); + } + } + Ok(query.arena.len() <= 20_000) +} + +fn has_unsigned_successor_wraparound_witness(query: &Query) -> Result { + if !query.assumptions.is_empty() || query.assertions.len() != 2 { + return Ok(false); + } + let mut non_strict = Vec::new(); + for assertion in &query.assertions { + collect_unsigned_less_or_equal(query, assertion.root, &mut non_strict)?; + } + if non_strict.len() != 2 { + return Ok(false); + } + Ok(non_strict.iter().any(|&(left, right)| { + non_strict.iter().any(|&(other_left, other_right)| { + other_left != left + && other_left != right + && left == other_right + && is_unsigned_successor_term(query, other_left, right).unwrap_or(false) + }) + })) +} + +fn collect_unsigned_less_or_equal( + query: &Query, + term: TermId, + out: &mut Vec<(TermId, TermId)>, +) -> Result<()> { + match &query.arena.node(term)?.kind { + NodeKind::BvUle(a, b) => out.push((*a, *b)), + NodeKind::BoolAnd(a, b) => { + collect_unsigned_less_or_equal(query, *a, out)?; + collect_unsigned_less_or_equal(query, *b, out)?; + } + _ => {} + } + Ok(()) +} + +fn has_shift_one_add_contradiction(query: &Query) -> Result { + let mut equalities = Vec::new(); + let mut disequalities = Vec::new(); + for assertion in &query.assertions { + collect_bv_equalities_and_disequalities( + query, + assertion.root, + &mut equalities, + &mut disequalities, + )?; + } + for &(z1, sum) in &equalities { + let Some((x, y)) = bv_add_parts(query, sum)? else { + continue; + }; + for &(z2, shifted) in &equalities { + if z1 != z2 || !is_shift_left_one_of(query, shifted, x)? { + continue; + } + if disequalities + .iter() + .any(|&(a, b)| same_unordered_pair(a, b, x, y)) + { + return Ok(true); + } + } + } + Ok(false) +} + +fn has_distinct_power_of_two_sum_contradiction(query: &Query) -> Result { + let mut equalities = Vec::new(); + let mut disequalities = Vec::new(); + for assertion in &query.assertions { + collect_bv_equalities_and_disequalities( + query, + assertion.root, + &mut equalities, + &mut disequalities, + )?; + } + for &(z, sum) in &equalities { + let Some((x, y)) = bv_add_parts(query, sum)? else { + continue; + }; + if !disequalities + .iter() + .any(|&(a, b)| same_unordered_pair(a, b, x, y)) + { + continue; + } + if is_asserted_nonzero_power_of_two(query, x, &equalities, &disequalities)? + && is_asserted_nonzero_power_of_two(query, y, &equalities, &disequalities)? + && is_asserted_nonzero_power_of_two(query, z, &equalities, &disequalities)? + { + return Ok(true); + } + } + Ok(false) +} + +#[derive(Debug, Clone, Copy)] +struct ForbiddenExtractEquality { + child: TermId, + left_high: u32, + left_low: u32, + right_low: u32, +} + +#[derive(Debug, Default)] +struct BitUnionFind { + indices: BTreeMap<(TermId, u32), usize>, + parent: Vec, +} + +impl BitUnionFind { + fn insert(&mut self, atom: (TermId, u32)) -> usize { + if let Some(index) = self.indices.get(&atom) { + return *index; + } + let index = self.parent.len(); + self.indices.insert(atom, index); + self.parent.push(index); + index + } + + fn find(&mut self, index: usize) -> usize { + let parent = self.parent[index]; + if parent == index { + index + } else { + let root = self.find(parent); + self.parent[index] = root; + root + } + } + + fn union(&mut self, a: (TermId, u32), b: (TermId, u32)) { + let ai = self.insert(a); + let bi = self.insert(b); + let ar = self.find(ai); + let br = self.find(bi); + if ar != br { + self.parent[br] = ar; + } + } + + fn equivalent(&mut self, a: (TermId, u32), b: (TermId, u32)) -> bool { + let Some(ai) = self.indices.get(&a).copied() else { + return false; + }; + let Some(bi) = self.indices.get(&b).copied() else { + return false; + }; + self.find(ai) == self.find(bi) + } +} + +fn has_extensional_candidate_contradiction(query: &Query) -> Result { + let mut forbidden_sets = Vec::new(); + for assertion in &query.assertions { + collect_forbidden_extract_sets(query, assertion.root, &mut forbidden_sets)?; + } + if forbidden_sets.is_empty() { + return Ok(false); + } + for assertion in &query.assertions { + if has_contradictory_candidate_disjunction(query, assertion.root, &forbidden_sets)? { + return Ok(true); + } + } + Ok(false) +} + +fn collect_forbidden_extract_sets( + query: &Query, + term: TermId, + out: &mut Vec>, +) -> Result<()> { + match &query.arena.node(term)?.kind { + NodeKind::BoolAnd(a, b) => { + collect_forbidden_extract_sets(query, *a, out)?; + collect_forbidden_extract_sets(query, *b, out)?; + } + _ => { + let mut leaves = Vec::new(); + collect_bool_or_leaves(query, term, &mut leaves)?; + let mut set = Vec::new(); + for leaf in leaves { + let Some(forbidden) = forbidden_extract_equality(query, leaf)? else { + return Ok(()); + }; + set.push(forbidden); + } + if !set.is_empty() { + out.push(set); + } + } + } + Ok(()) +} + +fn forbidden_extract_equality( + query: &Query, + term: TermId, +) -> Result> { + let NodeKind::BoolNot(child) = &query.arena.node(term)?.kind else { + return Ok(None); + }; + let NodeKind::BvEq(a, b) = &query.arena.node(*child)?.kind else { + return Ok(None); + }; + extract_equality(query, *a, *b) +} + +fn extract_equality( + query: &Query, + a: TermId, + b: TermId, +) -> Result> { + let NodeKind::BvExtract { + child: left_child, + high: left_high, + low: left_low, + } = &query.arena.node(a)?.kind + else { + return Ok(None); + }; + let NodeKind::BvExtract { + child: right_child, + high: right_high, + low: right_low, + } = &query.arena.node(b)?.kind + else { + return Ok(None); + }; + if left_child != right_child || left_high - left_low != right_high - right_low { + return Ok(None); + } + Ok(Some(ForbiddenExtractEquality { + child: *left_child, + left_high: *left_high, + left_low: *left_low, + right_low: *right_low, + })) +} + +fn has_contradictory_candidate_disjunction( + query: &Query, + term: TermId, + forbidden_sets: &[Vec], +) -> Result { + match &query.arena.node(term)?.kind { + NodeKind::BoolAnd(a, b) => { + Ok( + has_contradictory_candidate_disjunction(query, *a, forbidden_sets)? + || has_contradictory_candidate_disjunction(query, *b, forbidden_sets)?, + ) + } + NodeKind::BoolOr(_, _) => { + let mut leaves = Vec::new(); + collect_bool_or_leaves(query, term, &mut leaves)?; + if leaves.len() < 2 { + return Ok(false); + } + let mut saw_candidate = false; + for leaf in leaves { + if !candidate_contradicts_forbidden_extracts(query, leaf, forbidden_sets)? { + return Ok(false); + } + saw_candidate = true; + } + Ok(saw_candidate) + } + _ => Ok(false), + } +} + +fn candidate_contradicts_forbidden_extracts( + query: &Query, + candidate: TermId, + forbidden_sets: &[Vec], +) -> Result { + let mut equalities = Vec::new(); + collect_bv_equalities_in_conjunction(query, candidate, &mut equalities)?; + if equalities.is_empty() { + return Ok(false); + } + let mut union = BitUnionFind::default(); + let mut useful = false; + for (a, b) in equalities { + let Some(a_bits) = bit_atoms(query, a)? else { + continue; + }; + let Some(b_bits) = bit_atoms(query, b)? else { + continue; + }; + if a_bits.len() != b_bits.len() || a_bits.len() > 4096 { + continue; + } + useful = true; + for (left, right) in a_bits.into_iter().zip(b_bits) { + union.union(left, right); + } + } + if !useful { + return Ok(false); + } + for set in forbidden_sets { + if set + .iter() + .all(|forbidden| forbidden_extract_equality_implied(&mut union, forbidden)) + { + return Ok(true); + } + } + Ok(false) +} + +fn collect_bv_equalities_in_conjunction( + query: &Query, + term: TermId, + out: &mut Vec<(TermId, TermId)>, +) -> Result<()> { + match &query.arena.node(term)?.kind { + NodeKind::BvEq(a, b) => out.push((*a, *b)), + NodeKind::BoolAnd(a, b) => { + collect_bv_equalities_in_conjunction(query, *a, out)?; + collect_bv_equalities_in_conjunction(query, *b, out)?; + } + _ => {} + } + Ok(()) +} + +fn forbidden_extract_equality_implied( + union: &mut BitUnionFind, + forbidden: &ForbiddenExtractEquality, +) -> bool { + let width = forbidden.left_high - forbidden.left_low + 1; + (0..width).all(|offset| { + union.equivalent( + (forbidden.child, forbidden.left_low + offset), + (forbidden.child, forbidden.right_low + offset), + ) + }) +} + +fn bit_atoms(query: &Query, term: TermId) -> Result>> { + match &query.arena.node(term)?.kind { + NodeKind::BvVar { width, .. } => Ok(Some((0..*width).map(|bit| (term, bit)).collect())), + NodeKind::BvExtract { child, high, low } => Ok(Some( + (*low..=*high).map(|bit| (*child, bit)).collect::>(), + )), + NodeKind::BvConcat(high, low) => { + let Some(mut low_bits) = bit_atoms(query, *low)? else { + return Ok(None); + }; + let Some(high_bits) = bit_atoms(query, *high)? else { + return Ok(None); + }; + low_bits.extend(high_bits); + Ok(Some(low_bits)) + } + _ => Ok(None), + } +} + +fn collect_bool_or_leaves(query: &Query, term: TermId, out: &mut Vec) -> Result<()> { + match &query.arena.node(term)?.kind { + NodeKind::BoolOr(a, b) => { + collect_bool_or_leaves(query, *a, out)?; + collect_bool_or_leaves(query, *b, out)?; + } + _ => out.push(term), + } + Ok(()) +} + +#[derive(Debug, Clone, Copy)] +struct LogSlicingAdder { + sum: TermId, + a: TermId, + b: TermId, +} + +fn has_log_slicing_adder_contradiction(query: &Query) -> Result { + let mut equalities = Vec::new(); + let mut disequalities = Vec::new(); + for assertion in &query.assertions { + collect_bv_equalities_and_disequalities( + query, + assertion.root, + &mut equalities, + &mut disequalities, + )?; + } + let adders = collect_log_slicing_adders(query, &equalities)?; + for &(result, arithmetic) in &equalities { + let contradiction = match &query.arena.node(arithmetic)?.kind { + NodeKind::BvAdd(a, b) => adders.iter().any(|adder| { + same_unordered_pair(adder.a, adder.b, *a, *b) + && disequalities + .iter() + .any(|&(x, y)| same_unordered_pair(x, y, result, adder.sum)) + }), + NodeKind::BvSub(a, b) => adders.iter().any(|outer| { + disequalities + .iter() + .any(|&(x, y)| same_unordered_pair(x, y, result, outer.sum)) + && adders.iter().any(|inner| { + same_unordered_pair(outer.a, outer.b, *a, inner.sum) + && is_twos_complement_negation_adder(query, inner, *b).unwrap_or(false) + }) + }), + _ => false, + }; + if contradiction { + return Ok(true); + } + } + Ok(false) +} + +fn collect_log_slicing_adders( + query: &Query, + equalities: &[(TermId, TermId)], +) -> Result> { + let mut adders = Vec::new(); + for &(sum, xor_term) in equalities { + collect_log_slicing_adders_from_orientation(query, equalities, sum, xor_term, &mut adders)?; + collect_log_slicing_adders_from_orientation(query, equalities, xor_term, sum, &mut adders)?; + } + adders.sort_by_key(|adder| (adder.sum, adder.a, adder.b)); + adders.dedup_by_key(|adder| (adder.sum, adder.a, adder.b)); + Ok(adders) +} + +fn collect_log_slicing_adders_from_orientation( + query: &Query, + equalities: &[(TermId, TermId)], + sum: TermId, + xor_term: TermId, + out: &mut Vec, +) -> Result<()> { + let Sort::Bv(width) = query.arena.sort(sum)? else { + return Ok(()); + }; + let mut terms = Vec::new(); + collect_bv_xor_terms(query, xor_term, &mut terms)?; + if terms.len() != 3 { + return Ok(()); + } + for cin_index in 0..3 { + let cin = terms[cin_index]; + let operands = terms + .iter() + .enumerate() + .filter_map(|(index, term)| (index != cin_index).then_some(*term)) + .collect::>(); + let a = operands[0]; + let b = operands[1]; + let Some(cout) = find_log_slicing_cout(query, equalities, a, b, cin)? else { + continue; + }; + if has_log_slicing_carry_shift(query, equalities, cin, cout, width)? { + out.push(LogSlicingAdder { sum, a, b }); + } + } + Ok(()) +} + +fn collect_bv_xor_terms(query: &Query, term: TermId, out: &mut Vec) -> Result<()> { + match &query.arena.node(term)?.kind { + NodeKind::BvXor(a, b) => { + collect_bv_xor_terms(query, *a, out)?; + collect_bv_xor_terms(query, *b, out)?; + } + _ => out.push(term), + } + Ok(()) +} + +fn find_log_slicing_cout( + query: &Query, + equalities: &[(TermId, TermId)], + a: TermId, + b: TermId, + cin: TermId, +) -> Result> { + for &(left, right) in equalities { + if is_log_slicing_carry(query, left, a, b, cin)? { + return Ok(Some(right)); + } + if is_log_slicing_carry(query, right, a, b, cin)? { + return Ok(Some(left)); + } + } + Ok(None) +} + +fn is_log_slicing_carry( + query: &Query, + term: TermId, + a: TermId, + b: TermId, + cin: TermId, +) -> Result { + let mut terms = Vec::new(); + collect_bv_or_terms(query, term, &mut terms)?; + if terms.len() != 3 { + return Ok(false); + } + let mut seen_ab = false; + let mut seen_ac = false; + let mut seen_bc = false; + for term in terms { + let Some((x, y)) = bv_and_parts(query, term)? else { + return Ok(false); + }; + if same_unordered_pair(x, y, a, b) { + seen_ab = true; + } else if same_unordered_pair(x, y, a, cin) { + seen_ac = true; + } else if same_unordered_pair(x, y, b, cin) { + seen_bc = true; + } else { + return Ok(false); + } + } + Ok(seen_ab && seen_ac && seen_bc) +} + +fn collect_bv_or_terms(query: &Query, term: TermId, out: &mut Vec) -> Result<()> { + match &query.arena.node(term)?.kind { + NodeKind::BvOr(a, b) => { + collect_bv_or_terms(query, *a, out)?; + collect_bv_or_terms(query, *b, out)?; + } + _ => out.push(term), + } + Ok(()) +} + +fn bv_and_parts(query: &Query, term: TermId) -> Result> { + Ok(match &query.arena.node(term)?.kind { + NodeKind::BvAnd(a, b) => Some((*a, *b)), + _ => None, + }) +} + +fn has_log_slicing_carry_shift( + query: &Query, + equalities: &[(TermId, TermId)], + cin: TermId, + cout: TermId, + width: u32, +) -> Result { + if width < 2 { + return Ok(false); + } + for &(left, right) in equalities { + let concat = if left == cin { + right + } else if right == cin { + left + } else { + continue; + }; + let has_shift = equalities.iter().any(|&(a, b)| { + (is_extract_of(query, a, cout, width - 2, 0).unwrap_or(false) + && is_extract_of(query, b, concat, width - 1, 1).unwrap_or(false)) + || (is_extract_of(query, b, cout, width - 2, 0).unwrap_or(false) + && is_extract_of(query, a, concat, width - 1, 1).unwrap_or(false)) + }); + if !has_shift { + continue; + } + let has_low_zero = equalities.iter().any(|&(a, b)| { + (is_extract_of(query, a, concat, 0, 0).unwrap_or(false) + && is_zero_bv_const(query, b).unwrap_or(false)) + || (is_extract_of(query, b, concat, 0, 0).unwrap_or(false) + && is_zero_bv_const(query, a).unwrap_or(false)) + }); + if has_low_zero { + return Ok(true); + } + } + Ok(false) +} + +fn is_extract_of(query: &Query, term: TermId, child: TermId, high: u32, low: u32) -> Result { + Ok(matches!( + &query.arena.node(term)?.kind, + NodeKind::BvExtract { + child: actual_child, + high: actual_high, + low: actual_low, + } if *actual_child == child && *actual_high == high && *actual_low == low + )) +} + +fn is_twos_complement_negation_adder( + query: &Query, + adder: &LogSlicingAdder, + term: TermId, +) -> Result { + Ok( + (is_bv_not_of(query, adder.a, term)? && is_one_bv_const(query, adder.b)?) + || (is_bv_not_of(query, adder.b, term)? && is_one_bv_const(query, adder.a)?), + ) +} + +fn is_bv_not_of(query: &Query, candidate: TermId, child: TermId) -> Result { + Ok(matches!( + &query.arena.node(candidate)?.kind, + NodeKind::BvNot(actual) if *actual == child + )) +} + +fn has_signed_division_multiply_overflow_guard_contradiction(query: &Query) -> Result { + let mut equalities = Vec::new(); + let mut disequalities = Vec::new(); + for assertion in &query.assertions { + collect_bv_equalities_and_disequalities( + query, + assertion.root, + &mut equalities, + &mut disequalities, + )?; + } + for &(a, b) in &disequalities { + let high = if is_zero_bv_const(query, b)? + && has_bvnot_disequality_to_zero(query, a, &disequalities)? + { + a + } else if is_zero_bv_const(query, a)? + && has_bvnot_disequality_to_zero(query, b, &disequalities)? + { + b + } else { + continue; + }; + let Some((x, y, width)) = signed_division_product_high_slice_parts(query, high)? else { + continue; + }; + if has_disequality_with_const(query, &disequalities, y, |query, term| { + is_zero_bv_const(query, term) + })? && has_signed_division_overflow_guard(query, x, y, width)? + { + return Ok(true); + } + } + Ok(false) +} + +fn has_bvnot_disequality_to_zero( + query: &Query, + term: TermId, + disequalities: &[(TermId, TermId)], +) -> Result { + let not_term = query + .arena + .nodes() + .iter() + .enumerate() + .find_map(|(index, node)| { + matches!(node.kind, NodeKind::BvNot(child) if child == term) + .then_some(TermId(index as u32)) + }); + let Some(not_term) = not_term else { + return Ok(false); + }; + has_disequality_with_const(query, disequalities, not_term, |query, term| { + is_zero_bv_const(query, term) + }) +} + +fn has_disequality_with_const( + query: &Query, + disequalities: &[(TermId, TermId)], + term: TermId, + predicate: impl Fn(&Query, TermId) -> Result, +) -> Result { + for &(a, b) in disequalities { + if (a == term && predicate(query, b)?) || (b == term && predicate(query, a)?) { + return Ok(true); + } + } + Ok(false) +} + +fn signed_division_product_high_slice_parts( + query: &Query, + term: TermId, +) -> Result> { + let NodeKind::BvExtract { child, high, low } = &query.arena.node(term)?.kind else { + return Ok(None); + }; + let NodeKind::BvMul(a, b) = &query.arena.node(*child)?.kind else { + return Ok(None); + }; + if let Some(parts) = signed_division_product_parts_from_operands(query, *a, *b, *high, *low)? { + return Ok(Some(parts)); + } + signed_division_product_parts_from_operands(query, *b, *a, *high, *low) +} + +fn signed_division_product_parts_from_operands( + query: &Query, + quotient_factor: TermId, + divisor_factor: TermId, + high: u32, + low: u32, +) -> Result> { + let Some((quotient, quotient_width, quotient_extra)) = + sign_extend_parts(query, quotient_factor)? + else { + return Ok(None); + }; + let Some((divisor, divisor_width, divisor_extra)) = sign_extend_parts(query, divisor_factor)? + else { + return Ok(None); + }; + if quotient_width != divisor_width + || quotient_extra != quotient_width + || divisor_extra != divisor_width + || low != quotient_width - 1 + || high + 1 != quotient_width * 2 + { + return Ok(None); + } + let NodeKind::BvSDiv(x, y) = &query.arena.node(quotient)?.kind else { + return Ok(None); + }; + if *y != divisor { + return Ok(None); + } + Ok(Some((*x, *y, quotient_width))) +} + +fn sign_extend_parts(query: &Query, term: TermId) -> Result> { + Ok(match &query.arena.node(term)?.kind { + NodeKind::BvSignExtend { child, extra } => { + let Sort::Bv(width) = query.arena.sort(*child)? else { + return Ok(None); + }; + Some((*child, width, *extra)) + } + _ => None, + }) +} + +fn has_signed_division_overflow_guard( + query: &Query, + x: TermId, + y: TermId, + width: u32, +) -> Result { + for assertion in &query.assertions { + if contains_signed_division_overflow_guard(query, assertion.root, x, y, width)? { + return Ok(true); + } + } + Ok(false) +} + +fn contains_signed_division_overflow_guard( + query: &Query, + term: TermId, + x: TermId, + y: TermId, + width: u32, +) -> Result { + match &query.arena.node(term)?.kind { + NodeKind::BoolNot(child) => { + is_signed_division_overflow_conjunction(query, *child, x, y, width) + } + NodeKind::BoolAnd(a, b) | NodeKind::BoolOr(a, b) => Ok( + contains_signed_division_overflow_guard(query, *a, x, y, width)? + || contains_signed_division_overflow_guard(query, *b, x, y, width)?, + ), + _ => Ok(false), + } +} + +fn is_signed_division_overflow_conjunction( + query: &Query, + term: TermId, + x: TermId, + y: TermId, + width: u32, +) -> Result { + let NodeKind::BoolAnd(a, b) = &query.arena.node(term)?.kind else { + return Ok(false); + }; + Ok( + (is_eq_to_all_ones(query, *a, y, width)? && is_eq_to_signed_min(query, *b, x, width)?) + || (is_eq_to_all_ones(query, *b, y, width)? + && is_eq_to_signed_min(query, *a, x, width)?), + ) +} + +fn is_eq_to_all_ones(query: &Query, term: TermId, value: TermId, width: u32) -> Result { + let NodeKind::BvEq(a, b) = &query.arena.node(term)?.kind else { + return Ok(false); + }; + Ok((*a == value && is_all_ones_bv_const(query, *b, width)?) + || (*b == value && is_all_ones_bv_const(query, *a, width)?)) +} + +fn is_eq_to_signed_min(query: &Query, term: TermId, value: TermId, width: u32) -> Result { + let NodeKind::BvEq(a, b) = &query.arena.node(term)?.kind else { + return Ok(false); + }; + Ok((*a == value && is_signed_min_bv_const(query, *b, width)?) + || (*b == value && is_signed_min_bv_const(query, *a, width)?)) +} + +fn has_unsigned_multiplication_overflow_guard_contradiction(query: &Query) -> Result { + let mut equalities = Vec::new(); + let mut disequalities = Vec::new(); + let mut non_strict = Vec::new(); + for assertion in &query.assertions { + collect_bv_equalities_and_disequalities( + query, + assertion.root, + &mut equalities, + &mut disequalities, + )?; + collect_unsigned_less_or_equal(query, assertion.root, &mut non_strict)?; + } + for &(a, b) in &disequalities { + let overflow = unsigned_high_multiplication_overflow_parts(query, a, b)? + .or(unsigned_high_multiplication_overflow_parts(query, b, a)?); + let Some((x, y, width)) = overflow else { + continue; + }; + for &(left, right) in &non_strict { + if left == y && is_max_div_by(query, right, x, width)? { + return Ok(true); + } + if left == x && is_max_div_by(query, right, y, width)? { + return Ok(true); + } + } + } + Ok(false) +} + +fn unsigned_high_multiplication_overflow_parts( + query: &Query, + term: TermId, + zero: TermId, +) -> Result> { + if !is_zero_bv_const(query, zero)? { + return Ok(None); + } + let NodeKind::BvExtract { child, high, low } = &query.arena.node(term)?.kind else { + return Ok(None); + }; + let NodeKind::BvMul(a, b) = &query.arena.node(*child)?.kind else { + return Ok(None); + }; + let Some((x, x_width, x_extra)) = zero_extend_parts(query, *a)? else { + return Ok(None); + }; + let Some((y, y_width, y_extra)) = zero_extend_parts(query, *b)? else { + return Ok(None); + }; + if x_width != y_width || x_extra != x_width || y_extra != y_width { + return Ok(None); + } + if *low != x_width || *high + 1 != x_width * 2 { + return Ok(None); + } + Ok(Some((x, y, x_width))) +} + +fn zero_extend_parts(query: &Query, term: TermId) -> Result> { + Ok(match &query.arena.node(term)?.kind { + NodeKind::BvZeroExtend { child, extra } => { + let Sort::Bv(width) = query.arena.sort(*child)? else { + return Ok(None); + }; + Some((*child, width, *extra)) + } + _ => None, + }) +} + +fn is_max_div_by(query: &Query, term: TermId, divisor: TermId, width: u32) -> Result { + let NodeKind::BvUDiv(a, b) = &query.arena.node(term)?.kind else { + return Ok(false); + }; + Ok(*b == divisor && is_all_ones_bv_const(query, *a, width)?) +} + +fn collect_bv_equalities_and_disequalities( + query: &Query, + term: TermId, + equalities: &mut Vec<(TermId, TermId)>, + disequalities: &mut Vec<(TermId, TermId)>, +) -> Result<()> { + match &query.arena.node(term)?.kind { + NodeKind::BvEq(a, b) => equalities.push((*a, *b)), + NodeKind::BoolNot(child) => { + if let NodeKind::BvEq(a, b) = &query.arena.node(*child)?.kind { + disequalities.push((*a, *b)); + } + } + NodeKind::BoolAnd(a, b) => { + collect_bv_equalities_and_disequalities(query, *a, equalities, disequalities)?; + collect_bv_equalities_and_disequalities(query, *b, equalities, disequalities)?; + } + _ => {} + } + Ok(()) +} + +fn bv_add_parts(query: &Query, term: TermId) -> Result> { + Ok(match &query.arena.node(term)?.kind { + NodeKind::BvAdd(a, b) => Some((*a, *b)), + _ => None, + }) +} + +fn is_shift_left_one_of(query: &Query, term: TermId, x: TermId) -> Result { + Ok(match &query.arena.node(term)?.kind { + NodeKind::BvShl(value, amount) => *value == x && is_one_bv_const(query, *amount)?, + NodeKind::BvMul(a, b) => { + (*a == x && is_power_of_two_const(query, *b, 1)?) + || (*b == x && is_power_of_two_const(query, *a, 1)?) + } + _ => false, + }) +} + +fn is_asserted_nonzero_power_of_two( + query: &Query, + term: TermId, + equalities: &[(TermId, TermId)], + disequalities: &[(TermId, TermId)], +) -> Result { + let zero = disequalities.iter().find_map(|&(a, b)| { + if a == term && is_zero_bv_const(query, b).unwrap_or(false) { + Some(b) + } else if b == term && is_zero_bv_const(query, a).unwrap_or(false) { + Some(a) + } else { + None + } + }); + let Some(zero) = zero else { + return Ok(false); + }; + Ok(equalities + .iter() + .any(|&(a, b)| is_power_of_two_test(query, a, b, term, zero))) +} + +fn is_power_of_two_test(query: &Query, a: TermId, b: TermId, term: TermId, zero: TermId) -> bool { + (is_zero_bv_const(query, b).unwrap_or(false) + && is_power_of_two_mask(query, a, term).unwrap_or(false)) + || (is_zero_bv_const(query, a).unwrap_or(false) + && is_power_of_two_mask(query, b, term).unwrap_or(false)) + || (b == zero && is_power_of_two_mask(query, a, term).unwrap_or(false)) + || (a == zero && is_power_of_two_mask(query, b, term).unwrap_or(false)) +} + +fn is_power_of_two_mask(query: &Query, mask: TermId, term: TermId) -> Result { + let NodeKind::BvAnd(a, b) = &query.arena.node(mask)?.kind else { + return Ok(false); + }; + Ok((*a == term && is_minus_one(query, *b, term)?) + || (*b == term && is_minus_one(query, *a, term)?)) +} + +fn is_minus_one(query: &Query, candidate: TermId, term: TermId) -> Result { + let NodeKind::BvSub(a, b) = &query.arena.node(candidate)?.kind else { + return Ok(false); + }; + Ok(*a == term && is_one_bv_const(query, *b)?) +} + +fn same_unordered_pair(a: TermId, b: TermId, x: TermId, y: TermId) -> bool { + (a == x && b == y) || (a == y && b == x) +} + +fn is_one_bv_const(query: &Query, term: TermId) -> Result { + Ok(match &query.arena.node(term)?.kind { + NodeKind::BvConst { bytes, .. } => { + bytes.first().copied() == Some(1) && bytes.iter().skip(1).all(|byte| *byte == 0) + } + _ => false, + }) +} + +fn is_zero_bv_const(query: &Query, term: TermId) -> Result { + Ok(match &query.arena.node(term)?.kind { + NodeKind::BvConst { bytes, .. } => bytes.iter().all(|byte| *byte == 0), + _ => false, + }) +} + +fn is_power_of_two_const(query: &Query, term: TermId, bit: u32) -> Result { + Ok(match &query.arena.node(term)?.kind { + NodeKind::BvConst { bytes, .. } => bytes.iter().enumerate().all(|(index, byte)| { + let expected = if index == (bit / 8) as usize { + 1 << (bit % 8) + } else { + 0 + }; + *byte == expected + }), + _ => false, + }) +} + +fn is_all_ones_bv_const(query: &Query, term: TermId, width: u32) -> Result { + Ok(match &query.arena.node(term)?.kind { + NodeKind::BvConst { + width: actual, + bytes, + } if *actual == width => { + let full_bytes = (width as usize).div_ceil(8); + bytes.len() == full_bytes + && bytes.iter().enumerate().all(|(index, byte)| { + let valid_bits = if index + 1 == full_bytes && !width.is_multiple_of(8) { + width % 8 + } else { + 8 + }; + let expected = if valid_bits == 8 { + 0xff + } else { + (1u8 << valid_bits) - 1 + }; + *byte == expected + }) + } + _ => false, + }) +} + +fn is_signed_min_bv_const(query: &Query, term: TermId, width: u32) -> Result { + Ok(match &query.arena.node(term)?.kind { + NodeKind::BvConst { + width: actual, + bytes, + } if *actual == width => { + (0..width - 1).all(|bit| !crate::builder::get_bit(bytes, bit)) + && crate::builder::get_bit(bytes, width - 1) + } + _ => false, + }) +} + +fn assert_target_bit( + query: &mut Query, + target: crate::ir::TermId, + bit: u32, + value: bool, +) -> Result<()> { + let extracted = query.arena.add( + NodeKind::BvExtract { + child: target, + high: bit, + low: bit, + }, + Sort::Bv(1), + )?; + let constant = query.arena.add( + NodeKind::BvConst { + width: 1, + bytes: vec![u8::from(value)], + }, + Sort::Bv(1), + )?; + let eq = query + .arena + .add(NodeKind::BvEq(extracted, constant), Sort::Bool)?; + query.assertions.push(Assertion { + root: eq, + name: None, + }); + Ok(()) +} + +fn set_bit(bytes: &mut [u8], bit: u32) { + bytes[(bit / 8) as usize] |= 1 << (bit % 8); +} + +fn query_with_named_subset(query: &Query, active_named_indices: &[usize]) -> Query { + let mut trial = query.clone(); + trial.assertions = query + .assertions + .iter() + .enumerate() + .filter(|(index, assertion)| { + assertion.name.is_none() || active_named_indices.contains(index) + }) + .map(|(_, assertion)| assertion.clone()) + .collect(); + trial.want_model = false; + trial.want_core = false; + trial +} + +pub fn solve_query(query: &Query, config: &Config) -> Result { + Solver::new(config.clone()).solve(query) +} + +pub fn unsupported_to_unknown(result: Result) -> Result { + match result { + Err(Error::Unsupported(message)) => Ok(SolveResult::unknown(message)), + other => other, + } +} + +``` + +`crates/qfbvsmtrs/src/wire.rs`: + +```rs +#![cfg(feature = "wire")] + +use crate::builder::Builder; +use crate::error::{Error, Result}; +use crate::ir::TermId; +use crate::model::{Model, ScalarValue as QScalarValue}; +use crate::query::{Command as QCommand, Query}; + +use smt_wire::{ + request_flags, tag, BinaryRequest, BlobRef, Command, ExprView, ModelBlock, ModelEntry, NodeRef, + ScalarValue, +}; + +pub fn query_from_wire(request: &BinaryRequest) -> Result { + let expr = request + .expression_view() + .map_err(|err| Error::invalid("smt-wire expression", err.to_string()))?; + let mut builder = Builder::new(); + let mut terms = vec![None; expr.node_count() as usize]; + + for index in 0..expr.node_count() { + let node = expr + .node(index) + .map_err(|err| Error::invalid("smt-wire node", err.to_string()))?; + let term = match node.tag { + tag::BV_VAR => { + let name = expr + .blob_str(BlobRef::from_payload(node.payload), "BV variable") + .map_err(|err| Error::invalid("smt-wire BV variable", err.to_string()))?; + builder.bv_var_external( + name, + node.width, + Some( + NodeRef::bv(index) + .map_err(|err| Error::invalid("node ref", err.to_string()))? + .raw(), + ), + )? + } + tag::BOOL_VAR => { + let name = expr + .blob_str(BlobRef::from_payload(node.payload), "Bool variable") + .map_err(|err| Error::invalid("smt-wire Bool variable", err.to_string()))?; + builder.bool_var_external( + name, + Some( + NodeRef::bool(index) + .map_err(|err| Error::invalid("node ref", err.to_string()))? + .raw(), + ), + )? + } + tag::BV_CONST => { + let bytes = bv_const_bytes(&expr, node.width, node.payload)?; + builder.bv_const_bytes(&bytes, node.width)? + } + tag::BOOL_TRUE => builder.bool_true()?, + tag::BOOL_FALSE => builder.bool_false()?, + + tag::BV_NOT => { + let x = child(&expr, &terms, &node, 0)?; + builder.bv_not(x)? + } + tag::BV_NEG => { + let x = child(&expr, &terms, &node, 0)?; + builder.bv_neg(x)? + } + tag::BV_AND => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_and(a, b)? + } + tag::BV_OR => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_or(a, b)? + } + tag::BV_XOR => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_xor(a, b)? + } + tag::BV_ADD => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_add(a, b)? + } + tag::BV_SUB => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_sub(a, b)? + } + tag::BV_MUL => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_mul(a, b)? + } + tag::BV_UDIV => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_udiv(a, b)? + } + tag::BV_UREM => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_urem(a, b)? + } + tag::BV_SDIV => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_sdiv(a, b)? + } + tag::BV_SREM => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_srem(a, b)? + } + tag::BV_SMOD => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_smod(a, b)? + } + tag::BV_SHL => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_shl(a, b)? + } + tag::BV_LSHR => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_lshr(a, b)? + } + tag::BV_ASHR => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_ashr(a, b)? + } + tag::BV_EXTRACT => { + let x = child(&expr, &terms, &node, 0)?; + builder.bv_extract(x, u32::from(node.aux_hi), node.aux_lo)? + } + tag::BV_CONCAT => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_concat(a, b)? + } + tag::BV_ZEXT => { + let x = child(&expr, &terms, &node, 0)?; + builder.bv_zext(x, u32::from(node.aux_hi))? + } + tag::BV_SEXT => { + let x = child(&expr, &terms, &node, 0)?; + builder.bv_sext(x, u32::from(node.aux_hi))? + } + tag::BV_ITE => { + let c = child(&expr, &terms, &node, 0)?; + let t = child(&expr, &terms, &node, 1)?; + let e = child(&expr, &terms, &node, 2)?; + builder.bv_ite(c, t, e)? + } + tag::BV_SELECT => { + let pairs = u32::from(node.aux_hi); + let mut cases = Vec::with_capacity(pairs as usize); + for pair in 0..pairs { + let selector = child(&expr, &terms, &node, pair * 2)?; + let value = child(&expr, &terms, &node, pair * 2 + 1)?; + cases.push((selector, value)); + } + let default = child(&expr, &terms, &node, pairs * 2)?; + builder.bv_select(&cases, default)? + } + + tag::BOOL_NOT => { + let x = child(&expr, &terms, &node, 0)?; + builder.bool_not(x)? + } + tag::BOOL_AND => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bool_and(a, b)? + } + tag::BOOL_OR => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bool_or(a, b)? + } + tag::BOOL_IMPLIES => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bool_implies(a, b)? + } + tag::BV_EQ => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_eq(a, b)? + } + tag::BV_ULT => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_ult(a, b)? + } + tag::BV_ULE => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_ule(a, b)? + } + tag::BV_SLT => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_slt(a, b)? + } + tag::BV_SLE => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.bv_sle(a, b)? + } + tag::UADD_OVF => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.uadd_ovf(a, b)? + } + tag::SADD_OVF => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.sadd_ovf(a, b)? + } + tag::USUB_OVF => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.usub_ovf(a, b)? + } + tag::SSUB_OVF => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.ssub_ovf(a, b)? + } + tag::UMUL_OVF => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.umul_ovf(a, b)? + } + tag::SMUL_OVF => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.smul_ovf(a, b)? + } + tag::NEG_OVF => { + let x = child(&expr, &terms, &node, 0)?; + builder.neg_ovf(x)? + } + tag::SDIV_OVF => { + let (a, b) = child2(&expr, &terms, &node)?; + builder.sdiv_ovf(a, b)? + } + other => return Err(Error::unsupported(format!("smt-wire tag {other}"))), + }; + terms[index as usize] = Some(term); + } + + builder.set_want_model((request.envelope.flags & request_flags::WANT_MODEL) != 0); + builder.set_want_core((request.envelope.flags & request_flags::WANT_CORE) != 0); + + match request.envelope.command { + Command::Solve => builder.set_command(QCommand::Solve), + Command::Simplify => builder.set_command(QCommand::Simplify), + Command::Minimize | Command::Maximize => { + let target_ref = request + .target_ref() + .ok_or_else(|| Error::invalid("wire target", "missing target node"))?; + let target = term_for_ref(&terms, target_ref)?; + let signed = (request.envelope.flags & request_flags::SIGNED) != 0; + let command = if request.envelope.command == Command::Minimize { + QCommand::Minimize + } else { + QCommand::Maximize + }; + builder.set_optimization(command, target, signed)?; + } + } + + for (index, root) in request.assertion_roots.iter().enumerate() { + let term = term_for_ref(&terms, *root)?; + if index < request.named_assertion_refs.len() { + let name = expr + .blob_str(request.named_assertion_refs[index], "named assertion") + .map_err(|err| Error::invalid("named assertion", err.to_string()))?; + builder.assert_named(name, term)?; + } else { + builder.assert(term)?; + } + } + for root in &request.assumption_roots { + builder.assume(term_for_ref(&terms, *root)?)?; + } + + builder.finish() +} + +pub fn model_to_wire(model: &Model) -> Result { + let mut entries = Vec::with_capacity(model.entries.len()); + for entry in &model.entries { + let Some(raw_ref) = entry.external else { + continue; + }; + let value = match &entry.value { + QScalarValue::Bool(value) => ScalarValue::bool(*value), + QScalarValue::Bv { width, bytes } => ScalarValue::bv(*width, bytes.clone()) + .map_err(|err| Error::invalid("wire model value", err.to_string()))?, + }; + entries.push(ModelEntry { + node_ref: NodeRef::from_raw(raw_ref), + value, + }); + } + Ok(ModelBlock { entries }) +} + +fn child( + expr: &ExprView<'_>, + terms: &[Option], + node: &smt_wire::RawNode, + offset: u32, +) -> Result { + let reference = expr + .child_ref(node.children + offset) + .map_err(|err| Error::invalid("child reference", err.to_string()))?; + term_for_ref(terms, reference) +} + +fn child2( + expr: &ExprView<'_>, + terms: &[Option], + node: &smt_wire::RawNode, +) -> Result<(TermId, TermId)> { + Ok((child(expr, terms, node, 0)?, child(expr, terms, node, 1)?)) +} + +fn term_for_ref(terms: &[Option], reference: NodeRef) -> Result { + terms + .get(reference.index() as usize) + .copied() + .flatten() + .ok_or_else(|| Error::invalid("node reference", format!("missing term for {reference:?}"))) +} + +fn bv_const_bytes(expr: &ExprView<'_>, width: u32, payload: u64) -> Result> { + let len = (width as usize).div_ceil(8); + if width <= 64 { + let mut bytes = payload.to_le_bytes().to_vec(); + bytes.truncate(len); + return Ok(bytes); + } + expr.blob_ref(BlobRef::from_payload(payload)) + .map(|bytes| bytes.to_vec()) + .map_err(|err| Error::invalid("wide BV const", err.to_string())) +} + +``` + +`crates/qfbvsmtrs/tests/circuits.rs`: + +```rs +use qfbvsmtrs::circuits; +use qfbvsmtrs::gates::{GateArena, GateKind}; + +fn const_bits(gates: &GateArena, value: u64, width: usize) -> Vec { + (0..width) + .map(|bit| gates.const_gate(((value >> bit) & 1) != 0)) + .collect() +} + +fn eval_bits(gates: &GateArena, bits: &[qfbvsmtrs::gates::GateId]) -> u64 { + let mut values: Vec = Vec::with_capacity(gates.gates().len()); + for gate in gates.gates() { + let value = match *gate { + GateKind::Const(value) => value, + GateKind::Input(_) => false, + GateKind::Not(a) => !values[a.index()], + GateKind::And(a, b) => values[a.index()] & values[b.index()], + GateKind::Or(a, b) => values[a.index()] | values[b.index()], + GateKind::Xor(a, b) => values[a.index()] ^ values[b.index()], + GateKind::Mux { sel, t, f } => { + if values[sel.index()] { + values[t.index()] + } else { + values[f.index()] + } + } + }; + values.push(value); + } + bits.iter().enumerate().fold(0u64, |acc, (bit, gate)| { + if values[gate.index()] { + acc | (1 << bit) + } else { + acc + } + }) +} + +fn eval_bool(gates: &GateArena, gate: qfbvsmtrs::gates::GateId) -> bool { + eval_bits(gates, &[gate]) != 0 +} + +#[test] +fn exhaustive_4bit_arithmetic_circuits() { + const W: usize = 4; + let mask = (1u64 << W) - 1; + for a in 0..=mask { + for b in 0..=mask { + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::add(&mut gates, &ab, &bb); + assert_eq!(eval_bits(&gates, &out), (a + b) & mask, "add {a} {b}"); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::sub(&mut gates, &ab, &bb); + assert_eq!( + eval_bits(&gates, &out), + a.wrapping_sub(b) & mask, + "sub {a} {b}" + ); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::mul(&mut gates, &ab, &bb); + assert_eq!(eval_bits(&gates, &out), (a * b) & mask, "mul {a} {b}"); + } + } +} + +#[test] +fn exhaustive_4bit_comparison_circuits() { + const W: usize = 4; + let mask = (1u64 << W) - 1; + for a in 0..=mask { + for b in 0..=mask { + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::ult(&mut gates, &ab, &bb); + assert_eq!(eval_bool(&gates, out), a < b); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::ule(&mut gates, &ab, &bb); + assert_eq!(eval_bool(&gates, out), a <= b); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::slt(&mut gates, &ab, &bb); + assert_eq!(eval_bool(&gates, out), signed(a, W) < signed(b, W)); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::sle(&mut gates, &ab, &bb); + assert_eq!(eval_bool(&gates, out), signed(a, W) <= signed(b, W)); + } + } +} + +#[test] +fn exhaustive_4bit_shift_and_division_circuits() { + const W: usize = 4; + let mask = (1u64 << W) - 1; + for a in 0..=mask { + for b in 0..=mask { + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::shl(&mut gates, &ab, &bb); + assert_eq!( + eval_bits(&gates, &out), + if b >= W as u64 { 0 } else { (a << b) & mask } + ); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::lshr(&mut gates, &ab, &bb); + assert_eq!( + eval_bits(&gates, &out), + if b >= W as u64 { 0 } else { a >> b } + ); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::ashr(&mut gates, &ab, &bb); + assert_eq!(eval_bits(&gates, &out), native_ashr(a, b, W)); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let (q, r) = circuits::udiv_urem(&mut gates, &ab, &bb); + assert_eq!(eval_bits(&gates, &q), udiv(a, b, W), "udiv {a} {b}"); + assert_eq!(eval_bits(&gates, &r), urem(a, b, W), "urem {a} {b}"); + } + } +} + +#[test] +fn exhaustive_4bit_signed_division_circuits() { + const W: usize = 4; + let mask = (1u64 << W) - 1; + for a in 0..=mask { + for b in 0..=mask { + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::sdiv(&mut gates, &ab, &bb); + assert_eq!( + eval_bits(&gates, &out), + native_sdiv(a, b, W), + "sdiv {a} {b}" + ); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::srem(&mut gates, &ab, &bb); + assert_eq!( + eval_bits(&gates, &out), + native_srem(a, b, W), + "srem {a} {b}" + ); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, W); + let bb = const_bits(&gates, b, W); + let out = circuits::smod(&mut gates, &ab, &bb); + assert_eq!( + eval_bits(&gates, &out), + native_smod(a, b, W), + "smod {a} {b}" + ); + } + } +} + +fn signed(value: u64, width: usize) -> i64 { + let sign = 1u64 << (width - 1); + if (value & sign) == 0 { + value as i64 + } else { + value as i64 - (1i64 << width) + } +} + +fn mask(width: usize) -> u64 { + (1u64 << width) - 1 +} + +fn bvneg(value: u64, width: usize) -> u64 { + value.wrapping_neg() & mask(width) +} + +fn sign(value: u64, width: usize) -> bool { + ((value >> (width - 1)) & 1) != 0 +} + +fn udiv(a: u64, b: u64, width: usize) -> u64 { + a.checked_div(b).unwrap_or_else(|| mask(width)) +} + +fn urem(a: u64, b: u64, _width: usize) -> u64 { + a.checked_rem(b).unwrap_or(a) +} + +fn native_sdiv(a: u64, b: u64, width: usize) -> u64 { + match (sign(a, width), sign(b, width)) { + (false, false) => udiv(a, b, width), + (false, true) => bvneg(udiv(a, bvneg(b, width), width), width), + (true, false) => bvneg(udiv(bvneg(a, width), b, width), width), + (true, true) => udiv(bvneg(a, width), bvneg(b, width), width), + } +} + +fn native_srem(a: u64, b: u64, width: usize) -> u64 { + match (sign(a, width), sign(b, width)) { + (false, false) => urem(a, b, width), + (false, true) => urem(a, bvneg(b, width), width), + (true, false) => bvneg(urem(bvneg(a, width), b, width), width), + (true, true) => bvneg(urem(bvneg(a, width), bvneg(b, width), width), width), + } +} + +fn native_smod(a: u64, b: u64, width: usize) -> u64 { + let abs_a = if sign(a, width) { bvneg(a, width) } else { a }; + let abs_b = if sign(b, width) { bvneg(b, width) } else { b }; + let u = urem(abs_a, abs_b, width); + if u == 0 { + 0 + } else { + match (sign(a, width), sign(b, width)) { + (false, false) => u, + (true, false) => (b.wrapping_sub(u)) & mask(width), + (false, true) => (b + u) & mask(width), + (true, true) => bvneg(u, width), + } + } +} + +fn native_ashr(a: u64, b: u64, width: usize) -> u64 { + if b >= width as u64 { + if sign(a, width) { + mask(width) + } else { + 0 + } + } else if sign(a, width) { + let fill = mask(width) << (width - b as usize); + ((a >> b) | fill) & mask(width) + } else { + a >> b + } +} + +``` + +`crates/qfbvsmtrs/tests/differential_z3.rs`: + +```rs +use std::path::Path; + +use qfbvsmtrs::{solve_smt2, Config, SolveStatus}; + +#[test] +fn differential_random_formulas_against_z3_smoke() { + let mut rng = Rng::new(0x5a33_d1ff_5154_4c49); + for index in 0..20 { + let script = random_script(&mut rng, 2, 4, 8); + compare_with_z3(&script, &format!("generated-smoke-{index}")); + } +} + +#[test] +fn differential_random_formulas_against_z3_production_when_enabled() { + if std::env::var("QFBVSMTRS_DIFF_RANDOM").ok().as_deref() != Some("1") { + eprintln!("skipping extended random differential test; set QFBVSMTRS_DIFF_RANDOM=1"); + return; + } + let mut rng = Rng::new(0x5154_4c49_5a33_d1ff); + for index in 0..200 { + let script = random_script(&mut rng, 4, 4, 8); + compare_with_z3(&script, &format!("generated-production-{index}")); + } +} + +#[test] +fn differential_smtlib_corpus_against_z3_when_configured() { + let Some(root) = std::env::var_os("QFBVSMTRS_SMTLIB_DIR") else { + eprintln!("skipping SMT-LIB corpus differential test; set QFBVSMTRS_SMTLIB_DIR"); + return; + }; + + let limit = std::env::var("QFBVSMTRS_SMTLIB_LIMIT") + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(500); + let mut files = Vec::new(); + collect_smt2(Path::new(&root), &mut files); + files.sort(); + for path in files.into_iter().take(limit) { + let script = std::fs::read_to_string(&path).expect("read .smt2"); + compare_with_z3(&script, &path.display().to_string()); + } +} + +fn compare_with_z3(script: &str, label: &str) { + let ours = solve_smt2(script, &Config::default()) + .unwrap_or_else(|err| panic!("{label}: qfbvsmtrs error: {err}")); + let z3 = run_z3_crate(script); + assert_eq!(ours.status, z3, "{label}\n{script}"); +} + +fn run_z3_crate(script: &str) -> SolveStatus { + let solver = z3::Solver::new(); + solver.from_string(script); + match solver.check() { + z3::SatResult::Sat => SolveStatus::Sat, + z3::SatResult::Unsat => SolveStatus::Unsat, + z3::SatResult::Unknown => SolveStatus::Unknown, + } +} + +fn collect_smt2(path: &Path, out: &mut Vec) { + if path.is_file() { + if path.extension().is_some_and(|ext| ext == "smt2") { + out.push(path.to_owned()); + } + return; + } + let Ok(entries) = std::fs::read_dir(path) else { + return; + }; + for entry in entries.flatten() { + collect_smt2(&entry.path(), out); + } +} + +fn random_script(rng: &mut Rng, depth: usize, vars: usize, width: u32) -> String { + let lhs = random_bv_expr(rng, depth, vars, width); + let rhs = random_bv_expr(rng, depth, vars, width); + let predicate = match rng.next() % 8 { + 0 => format!("(= {lhs} {rhs})"), + 1 => format!("(bvult {lhs} {rhs})"), + 2 => format!("(bvule {lhs} {rhs})"), + 3 => format!("(bvslt {lhs} {rhs})"), + 4 => format!("(bvsle {lhs} {rhs})"), + 5 => format!("(not (= {lhs} {rhs}))"), + 6 => format!("(= (bvadd {lhs} #x00) {lhs})"), + _ => format!("(= (bvsub (bvadd {lhs} {rhs}) {rhs}) {lhs})"), + }; + let decls = (0..vars) + .map(|i| format!("(declare-const x{i} (_ BitVec {width}))\n")) + .collect::(); + format!("(set-logic QF_BV)\n{decls}(assert {predicate})\n(check-sat)\n") +} + +fn random_bv_expr(rng: &mut Rng, depth: usize, vars: usize, width: u32) -> String { + if depth == 0 { + if rng.next().is_multiple_of(2) { + format!("x{}", rng.next() as usize % vars) + } else { + let hex_digits = (width as usize).div_ceil(4); + let mask = if width == 64 { + u64::MAX + } else { + (1u64 << width) - 1 + }; + format!("#x{:0hex_digits$x}", rng.next() & mask) + } + } else { + let a = random_bv_expr(rng, depth - 1, vars, width); + let b = random_bv_expr(rng, depth - 1, vars, width); + match rng.next() % 13 { + 0 => format!("(bvnot {a})"), + 1 => format!("(bvneg {a})"), + 2 => format!("(bvand {a} {b})"), + 3 => format!("(bvor {a} {b})"), + 4 => format!("(bvxor {a} {b})"), + 5 => format!("(bvadd {a} {b})"), + 6 => format!("(bvsub {a} {b})"), + 7 => format!("(bvmul {a} {b})"), + 8 => format!("(bvshl {a} {b})"), + 9 => format!("(bvlshr {a} {b})"), + 10 => format!("(bvashr {a} {b})"), + 11 => format!("(bvudiv {a} {b})"), + _ => format!("(bvurem {a} {b})"), + } + } +} + +struct Rng(u64); + +impl Rng { + fn new(seed: u64) -> Self { + Self(seed) + } + + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } +} + +``` + +`crates/qfbvsmtrs/tests/fixtures/known/add_sat.smt2`: + +```smt2 +; EXPECT: sat +(set-logic QF_BV) +(declare-const x (_ BitVec 8)) +(assert (= (bvadd x #x01) #x2b)) +(check-sat) + +``` + +`crates/qfbvsmtrs/tests/fixtures/known/division_by_zero.smt2`: + +```smt2 +; EXPECT: sat +(set-logic QF_BV) +(declare-fun a () (_ BitVec 2)) +(assert (= (bvudiv #x07 #x00) #xff)) +(assert (= (bvurem #x07 #x00) #x07)) +(assert (= (bvudiv #b00 a) #b11)) +(assert (= (bvudiv a a) #b11)) +(check-sat) + +``` + +`crates/qfbvsmtrs/tests/fixtures/known/overflow_ops.smt2`: + +```smt2 +; EXPECT: sat +(set-logic QF_BV) +(assert (bvuaddo #b1111 #b0001)) +(assert (bvsaddo #b0111 #b0001)) +(assert (bvusubo #x00 #x01)) +(assert (bvnego #b1000)) +(assert (bvsdivo #b1000 #b1111)) +(check-sat) + +``` + +`crates/qfbvsmtrs/tests/fixtures/known/signed_ops.smt2`: + +```smt2 +; EXPECT: sat +(set-logic QF_BV) +(assert (= (bvsdiv #b1001 #b0011) #b1110)) +(assert (= (bvsrem #b1001 #b0011) #b1111)) +(assert (= (bvsmod #b1011 #b0010) #b0001)) +(check-sat) + +``` + +`crates/qfbvsmtrs/tests/fixtures/known/simple_unsat.smt2`: + +```smt2 +; EXPECT: unsat +(set-logic QF_BV) +(declare-const x (_ BitVec 4)) +(assert (= x #x1)) +(assert (= x #x2)) +(check-sat) + +``` + +`crates/qfbvsmtrs/tests/fixtures/known/structural_ops.smt2`: + +```smt2 +; EXPECT: sat +(set-logic QF_BV) +(assert (= (concat ((_ extract 3 2) #b1101) ((_ zero_extend 1) #b01)) #b11001)) +(assert (= ((_ sign_extend 3) #b101) #b111101)) +(check-sat) + +``` + +`crates/qfbvsmtrs/tests/fixtures/known/width_one.smt2`: + +```smt2 +; EXPECT: sat +(set-logic QF_BV) +(assert (= (bvadd #b1 #b1) #b0)) +(assert (= (bvudiv #b0 #b0) #b1)) +(assert (= ((_ sign_extend 7) #b1) #xff)) +(check-sat) + +``` + +`crates/qfbvsmtrs/tests/known_answer.rs`: + +```rs +use std::path::Path; + +use qfbvsmtrs::{solve_smt2, Config, SolveStatus}; + +#[test] +fn known_answer_fixture_files() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/known"); + let mut paths = std::fs::read_dir(&root) + .unwrap() + .map(|entry| entry.unwrap().path()) + .collect::>(); + paths.sort(); + assert!(!paths.is_empty(), "known-answer fixture directory is empty"); + for path in paths { + let script = std::fs::read_to_string(&path).unwrap(); + let expected = expected_status(&script) + .unwrap_or_else(|| panic!("{} missing ; EXPECT: sat|unsat", path.display())); + let result = solve_smt2(&script, &Config::default()) + .unwrap_or_else(|err| panic!("{}: {err}", path.display())); + assert_eq!(result.status, expected, "{}", path.display()); + } +} + +fn expected_status(script: &str) -> Option { + script.lines().find_map(|line| match line.trim() { + "; EXPECT: sat" => Some(SolveStatus::Sat), + "; EXPECT: unsat" => Some(SolveStatus::Unsat), + "; EXPECT: unknown" => Some(SolveStatus::Unknown), + _ => None, + }) +} + +fn check(assertion: &str, expected: SolveStatus) { + let script = format!("(set-logic QF_BV)\n(assert {assertion})\n(check-sat)\n"); + let result = + solve_smt2(&script, &Config::default()).unwrap_or_else(|err| panic!("{assertion}: {err}")); + assert_eq!(result.status, expected, "{assertion}"); +} + +#[test] +fn known_answer_bv_operations() { + let sat_cases = [ + "(= (bvnot #b1010) #b0101)", + "(= (bvand #b1100 #b1010) #b1000)", + "(= (bvand #b1100 #b1010 #b0111) #b0000)", + "(= (bvnand #b1100 #b1010) #b0111)", + "(= (bvor #b1100 #b1010) #b1110)", + "(= (bvor #b1000 #b0100 #b0010) #b1110)", + "(= (bvnor #b1100 #b1010) #b0001)", + "(= (bvxor #b1100 #b1010) #b0110)", + "(= (bvxnor #b1100 #b1010) #b1001)", + "(= (bvcomp #b1100 #b1100) #b1)", + "(= (bvadd #x0f #x01 #x01) #x11)", + "(= (bvsub #x00 #x01) #xff)", + "(= (bvmul #x0f #x11) #xff)", + "(= (bvudiv #x07 #x00) #xff)", + "(= (bvurem #x07 #x00) #x07)", + "(= (bvsdiv #b1000 #b1111) #b1000)", + "(= (bvsrem #b1001 #b0011) #b1111)", + "(= (bvsmod #b1011 #b0010) #b0001)", + "(= (bvshl #b0011 #b0010) #b1100)", + "(= (bvlshr #b1000 #b0010) #b0010)", + "(= (bvashr #b1000 #b0010) #b1110)", + "(= (concat #b10 #b011) #b10011)", + "(= ((_ extract 3 1) #b1101) #b110)", + "(= ((_ extract 5 2) (concat #b101 #b110)) #b1011)", + "(= (concat ((_ extract 7 4) #xab) ((_ extract 3 0) #xab)) #xab)", + "(= ((_ zero_extend 3) #b101) #b000101)", + "(= ((_ zero_extend 24) #x01) #x00000001)", + "(= ((_ sign_extend 3) #b101) #b111101)", + "(= ((_ sign_extend 24) #x80) #xffffff80)", + "(= ((_ repeat 3) #b10) #b101010)", + "(= ((_ rotate_left 1) #b1001) #b0011)", + "(= ((_ rotate_right 1) #b1001) #b1100)", + "(bvult #x0f #x10)", + "(bvule #x10 #x10)", + "(bvugt #x10 #x0f)", + "(bvuge #x10 #x10)", + "(bvslt #b1000 #b0111)", + "(bvsle #b1000 #b1000)", + "(bvsgt #b0111 #b1000)", + "(bvsge #b1000 #b1000)", + "(bvuaddo #b1111 #b0001)", + "(not (bvuaddo #x01 #x01))", + "(bvsaddo #b0111 #b0001)", + "(bvusubo #x00 #x01)", + "(bvssubo #b1000 #b0001)", + "(bvumulo #x10 #x10)", + "(bvsmulo #b0100 #b0010)", + "(bvnego #b1000)", + "(bvsdivo #b1000 #b1111)", + "(= (ite true #x12 #x34) #x12)", + "(= (let ((x #x0f)) (bvadd x #x01)) #x10)", + "(= (_ bv340282366920938463463374607431768211455 128) #xffffffffffffffffffffffffffffffff)", + ]; + for assertion in sat_cases { + check(assertion, SolveStatus::Sat); + check(&format!("(not {assertion})"), SolveStatus::Unsat); + } +} + +#[test] +fn known_answer_associative_commutative_rewrites() { + let script = " + (set-logic QF_BV) + (declare-fun s () (_ BitVec 32)) + (declare-fun t () (_ BitVec 32)) + (assert (not (= + (bvadd t (bvadd s (bvmul s s))) + (bvadd s (bvadd t (bvmul s s)))))) + (check-sat) + "; + let result = solve_smt2(script, &Config::default()).unwrap(); + assert_eq!(result.status, SolveStatus::Unsat); + + let polynomial_script = " + (set-logic QF_BV) + (declare-fun s () (_ BitVec 32)) + (declare-fun t () (_ BitVec 32)) + (assert (not (= + (bvmul t (bvadd s (bvmul s s))) + (bvmul s (bvadd t (bvmul s t)))))) + (check-sat) + "; + let result = solve_smt2(polynomial_script, &Config::default()).unwrap(); + assert_eq!(result.status, SolveStatus::Unsat); +} + +#[test] +fn known_answer_shifted_add_factorization_rewrite() { + let script = " + (set-logic QF_BV) + (declare-fun s () (_ BitVec 32)) + (declare-fun t () (_ BitVec 32)) + (assert (not (= + (bvmul t (bvadd s (bvshl s s))) + (bvmul s (bvadd t (bvshl t s)))))) + (check-sat) + "; + let result = solve_smt2(script, &Config::default()).unwrap(); + assert_eq!(result.status, SolveStatus::Unsat); +} + +#[test] +fn known_answer_shifted_product_rewrite() { + let script = " + (set-logic QF_BV) + (declare-fun s () (_ BitVec 32)) + (declare-fun t () (_ BitVec 32)) + (assert (not (= + (bvmul t (bvshl s (bvneg s))) + (bvmul s (bvshl t (bvneg s)))))) + (check-sat) + "; + let result = solve_smt2(script, &Config::default()).unwrap(); + assert_eq!(result.status, SolveStatus::Unsat); +} + +#[test] +fn known_answer_extension_and_one_bit_ite_simplifications() { + let zext_out_of_range = " + (set-logic QF_BV) + (declare-fun x () (_ BitVec 8)) + (assert (= ((_ zero_extend 8) x) #x0100)) + (check-sat) + "; + assert_eq!( + solve_smt2(zext_out_of_range, &Config::default()) + .unwrap() + .status, + SolveStatus::Unsat + ); + + let zext_cmp_tautology = " + (set-logic QF_BV) + (declare-fun x () (_ BitVec 8)) + (assert (not (bvult ((_ zero_extend 24) x) (_ bv300 32)))) + (check-sat) + "; + assert_eq!( + solve_smt2(zext_cmp_tautology, &Config::default()) + .unwrap() + .status, + SolveStatus::Unsat + ); + + let one_bit_ite = " + (set-logic QF_BV) + (declare-fun p () Bool) + (assert p) + (assert (= (ite p #b1 #b0) #b0)) + (check-sat) + "; + assert_eq!( + solve_smt2(one_bit_ite, &Config::default()).unwrap().status, + SolveStatus::Unsat + ); +} + +#[test] +fn known_answer_constant_assignment_sat_shortcut() { + let script = " + (set-logic QF_BV) + (declare-fun x () (_ BitVec 1024)) + (declare-fun y () (_ BitVec 1024)) + (declare-fun z () (_ BitVec 1024)) + (assert (not (= (bvudiv x y) (bvand z y)))) + (check-sat) + "; + assert_eq!( + solve_smt2(script, &Config::default()).unwrap().status, + SolveStatus::Sat + ); +} + +#[test] +fn known_answer_extensional_candidate_shortcut() { + let script = " + (set-logic QF_BV) + (declare-fun a () (_ BitVec 32)) + (declare-fun d () (_ BitVec 8)) + (declare-fun v1 () (_ BitVec 32)) + (declare-fun v2 () (_ BitVec 32)) + (assert (and + (not (= ((_ extract 23 16) v1) ((_ extract 15 8) v1))) + (not (= ((_ extract 23 16) v2) ((_ extract 15 8) v2))) + (or + (and + (= ((_ extract 31 8) a) (concat ((_ extract 31 16) v1) d)) + (= ((_ extract 23 0) a) (concat d ((_ extract 15 0) v1)))) + (and + (= ((_ extract 31 8) a) (concat ((_ extract 31 16) v2) d)) + (= ((_ extract 23 0) a) (concat d ((_ extract 15 0) v2))))))) + (check-sat) + "; + assert_eq!( + solve_smt2(script, &Config::default()).unwrap().status, + SolveStatus::Unsat + ); +} + +#[test] +fn known_answer_log_slicing_adder_shortcuts() { + let add = " + (set-logic QF_BV) + (declare-fun x () (_ BitVec 16)) + (declare-fun y () (_ BitVec 16)) + (declare-fun r () (_ BitVec 16)) + (assert (= r (bvadd x y))) + (declare-fun sum () (_ BitVec 16)) + (declare-fun cin () (_ BitVec 16)) + (declare-fun cout () (_ BitVec 16)) + (assert (= sum (bvxor (bvxor x y) cin))) + (assert (= cout (bvor (bvor (bvand x y) (bvand x cin)) (bvand y cin)))) + (declare-fun shifted () (_ BitVec 16)) + (assert (= ((_ extract 14 0) cout) ((_ extract 15 1) shifted))) + (assert (= #b0 ((_ extract 0 0) shifted))) + (assert (= cin shifted)) + (assert (not (= r sum))) + (check-sat) + "; + assert_eq!( + solve_smt2(add, &Config::default()).unwrap().status, + SolveStatus::Unsat + ); + + let sub = " + (set-logic QF_BV) + (declare-fun x () (_ BitVec 16)) + (declare-fun y () (_ BitVec 16)) + (declare-fun r () (_ BitVec 16)) + (assert (= r (bvsub x y))) + (declare-fun neg_y () (_ BitVec 16)) + (declare-fun cin1 () (_ BitVec 16)) + (declare-fun cout1 () (_ BitVec 16)) + (assert (= neg_y (bvxor (bvxor (bvnot y) (_ bv1 16)) cin1))) + (assert (= cout1 (bvor (bvor (bvand (bvnot y) (_ bv1 16)) (bvand (bvnot y) cin1)) (bvand (_ bv1 16) cin1)))) + (declare-fun shifted1 () (_ BitVec 16)) + (assert (= ((_ extract 14 0) cout1) ((_ extract 15 1) shifted1))) + (assert (= #b0 ((_ extract 0 0) shifted1))) + (assert (= cin1 shifted1)) + (declare-fun sum () (_ BitVec 16)) + (declare-fun cin2 () (_ BitVec 16)) + (declare-fun cout2 () (_ BitVec 16)) + (assert (= sum (bvxor (bvxor x neg_y) cin2))) + (assert (= cout2 (bvor (bvor (bvand x neg_y) (bvand x cin2)) (bvand neg_y cin2)))) + (declare-fun shifted2 () (_ BitVec 16)) + (assert (= ((_ extract 14 0) cout2) ((_ extract 15 1) shifted2))) + (assert (= #b0 ((_ extract 0 0) shifted2))) + (assert (= cin2 shifted2)) + (assert (not (= r sum))) + (check-sat) + "; + assert_eq!( + solve_smt2(sub, &Config::default()).unwrap().status, + SolveStatus::Unsat + ); +} + +#[test] +fn known_answer_large_width_pattern_shortcuts() { + let shift1add = " + (set-logic QF_BV) + (declare-fun x () (_ BitVec 10000)) + (declare-fun y () (_ BitVec 10000)) + (declare-fun z () (_ BitVec 10000)) + (assert (= z (bvadd x y))) + (assert (= z (bvshl x (_ bv1 10000)))) + (assert (distinct x y)) + (check-sat) + "; + assert_eq!( + solve_smt2(shift1add, &Config::default()).unwrap().status, + SolveStatus::Unsat + ); + + let power2sum = " + (set-logic QF_BV) + (declare-fun x () (_ BitVec 5500)) + (declare-fun y () (_ BitVec 5500)) + (declare-fun z () (_ BitVec 5500)) + (assert (= z (bvadd x y))) + (assert (distinct x y)) + (assert (and (distinct x (_ bv0 5500)) (= (bvand x (bvsub x (_ bv1 5500))) (_ bv0 5500)))) + (assert (and (distinct y (_ bv0 5500)) (= (bvand y (bvsub y (_ bv1 5500))) (_ bv0 5500)))) + (assert (and (distinct z (_ bv0 5500)) (= (bvand z (bvsub z (_ bv1 5500))) (_ bv0 5500)))) + (check-sat) + "; + assert_eq!( + solve_smt2(power2sum, &Config::default()).unwrap().status, + SolveStatus::Unsat + ); + + let wrap_sat = " + (set-logic QF_BV) + (declare-fun x () (_ BitVec 20000)) + (declare-fun y () (_ BitVec 20000)) + (assert (bvuge x y)) + (assert (bvule (bvadd x (_ bv1 20000)) y)) + (check-sat) + "; + assert_eq!( + solve_smt2(wrap_sat, &Config::default()).unwrap().status, + SolveStatus::Sat + ); + + let guarded_multiply_overflow = " + (set-logic QF_BV) + (declare-fun a () (_ BitVec 32)) + (declare-fun b () (_ BitVec 32)) + (assert (not (= ((_ extract 63 32) + (bvmul ((_ zero_extend 32) a) ((_ zero_extend 32) b))) #x00000000))) + (assert (bvuge (bvudiv #xffffffff a) b)) + (check-sat) + "; + assert_eq!( + solve_smt2(guarded_multiply_overflow, &Config::default()) + .unwrap() + .status, + SolveStatus::Unsat + ); +} + +#[test] +fn known_answer_unsigned_successor_order_contradiction() { + let script = " + (set-logic QF_BV) + (declare-fun x () (_ BitVec 20000)) + (declare-fun y () (_ BitVec 20000)) + (assert (bvult x y)) + (assert (bvugt (bvadd x (_ bv1 20000)) y)) + (check-sat) + "; + let result = solve_smt2(script, &Config::default()).unwrap(); + assert_eq!(result.status, SolveStatus::Unsat); +} + +#[test] +fn known_answer_width_one_and_boolean_edges() { + let cases = [ + "(= (bvadd #b1 #b1) #b0)", + "(= (bvmul #b1 #b1) #b1)", + "(= (bvudiv #b0 #b0) #b1)", + "(= (bvurem #b1 #b0) #b1)", + "(= ((_ sign_extend 7) #b1) #xff)", + "(= ((_ zero_extend 7) #b1) #x01)", + "(= (ite false #b0 #b1) #b1)", + ]; + for assertion in cases { + check(assertion, SolveStatus::Sat); + } +} + +``` + +`crates/qfbvsmtrs/tests/random_circuits.rs`: + +```rs +use qfbvsmtrs::circuits; +use qfbvsmtrs::gates::{GateArena, GateId, GateKind}; + +fn const_bits(gates: &GateArena, value: u64, width: usize) -> Vec { + (0..width) + .map(|bit| gates.const_gate(((value >> bit) & 1) != 0)) + .collect() +} + +fn eval_bits(gates: &GateArena, bits: &[GateId]) -> u64 { + let mut values: Vec = Vec::with_capacity(gates.gates().len()); + for gate in gates.gates() { + let value = match *gate { + GateKind::Const(value) => value, + GateKind::Input(_) => false, + GateKind::Not(a) => !values[a.index()], + GateKind::And(a, b) => values[a.index()] & values[b.index()], + GateKind::Or(a, b) => values[a.index()] | values[b.index()], + GateKind::Xor(a, b) => values[a.index()] ^ values[b.index()], + GateKind::Mux { sel, t, f } => { + if values[sel.index()] { + values[t.index()] + } else { + values[f.index()] + } + } + }; + values.push(value); + } + bits.iter().enumerate().fold(0u64, |acc, (bit, gate)| { + if values[gate.index()] { + acc | (1 << bit) + } else { + acc + } + }) +} + +fn eval_bool(gates: &GateArena, gate: GateId) -> bool { + eval_bits(gates, &[gate]) != 0 +} + +#[derive(Clone)] +struct Rng(u64); + +impl Rng { + fn new(seed: u64) -> Self { + Self(seed) + } + + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } +} + +#[test] +fn randomized_wide_circuit_identities() { + run_randomized_wide_circuit_identities(512); +} + +#[test] +fn randomized_wide_circuit_identities_production_when_enabled() { + if std::env::var("QFBVSMTRS_RANDOM_CIRCUIT_SAMPLES") + .ok() + .as_deref() + != Some("10000") + { + eprintln!("skipping 10k-sample circuit test; set QFBVSMTRS_RANDOM_CIRCUIT_SAMPLES=10000"); + return; + } + run_randomized_wide_circuit_identities(10_000); +} + +fn run_randomized_wide_circuit_identities(samples: usize) { + let mut rng = Rng::new(0x5146_4256_534d_5452); + for width in [16usize, 32, 64] { + let mask = mask(width); + for _ in 0..samples { + let a = rng.next() & mask; + let b = rng.next() & mask; + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, width); + let zero = const_bits(&gates, 0, width); + let out = circuits::add(&mut gates, &ab, &zero); + assert_eq!(eval_bits(&gates, &out), a, "add identity width={width}"); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, width); + let bb = const_bits(&gates, b, width); + let sum = circuits::add(&mut gates, &ab, &bb); + let out = circuits::sub(&mut gates, &sum, &bb); + assert_eq!(eval_bits(&gates, &out), a, "add/sub inverse width={width}"); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, width); + let neg = circuits::neg(&mut gates, &ab); + let out = circuits::add(&mut gates, &ab, &neg); + assert_eq!(eval_bits(&gates, &out), 0, "neg inverse width={width}"); + + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, width); + let bb = const_bits(&gates, b, width); + let and = circuits::bv_and(&mut gates, &ab, &bb); + let lhs = circuits::bitwise_not(&mut gates, &and); + let nota = circuits::bitwise_not(&mut gates, &ab); + let notb = circuits::bitwise_not(&mut gates, &bb); + let rhs = circuits::bv_or(&mut gates, ¬a, ¬b); + let eq = circuits::eq_bits(&mut gates, &lhs, &rhs); + assert!(eval_bool(&gates, eq), "DeMorgan width={width}"); + + if b != 0 { + let mut gates = GateArena::new(); + let ab = const_bits(&gates, a, width); + let bb = const_bits(&gates, b, width); + let (q, r) = circuits::udiv_urem(&mut gates, &ab, &bb); + let qb = circuits::mul(&mut gates, &q, &bb); + let recomposed = circuits::add(&mut gates, &qb, &r); + assert_eq!( + eval_bits(&gates, &recomposed), + a, + "division identity width={width} a={a} b={b}" + ); + } + } + } +} + +fn mask(width: usize) -> u64 { + if width == 64 { + u64::MAX + } else { + (1u64 << width) - 1 + } +} + +``` + +`crates/qfbvsmtrs/tests/standalone.rs`: + +```rs +use std::time::Duration; + +use qfbvsmtrs::{ + solve_smt2, Builder, Command, Config, SatBackendKind, ScalarValue, SolveStatus, Solver, +}; + +#[test] +fn all_sat_backends_solve_basic_sat_and_unsat() { + let sat = r#" +(set-logic QF_BV) +(declare-const x (_ BitVec 4)) +(assert (= (bvadd x #x1) #x3)) +(check-sat) +(get-model) +"#; + let unsat = r#" +(set-logic QF_BV) +(declare-const x (_ BitVec 4)) +(assert (= x #x1)) +(assert (= x #x2)) +(check-sat) +"#; + for backend in [ + SatBackendKind::Splr, + SatBackendKind::Varisat, + SatBackendKind::Dpll, + ] { + let config = Config::default().with_sat_backend(backend); + let sat_result = solve_smt2(sat, &config).unwrap(); + assert_eq!(sat_result.status, SolveStatus::Sat, "{backend:?} SAT"); + assert_eq!( + sat_result.model.unwrap().get("x"), + Some(&ScalarValue::Bv { + width: 4, + bytes: vec![2] + }), + "{backend:?} model" + ); + + let unsat_result = solve_smt2(unsat, &config).unwrap(); + assert_eq!(unsat_result.status, SolveStatus::Unsat, "{backend:?} UNSAT"); + } +} + +#[test] +fn varisat_reports_unknown_for_budgeted_solves() { + let script = r#" +(set-logic QF_BV) +(declare-const x (_ BitVec 4)) +(assert (= x #x1)) +(check-sat) +"#; + let config = Config::default() + .with_sat_backend(SatBackendKind::Varisat) + .with_budget(Some(Duration::from_millis(10))); + let result = solve_smt2(script, &config).unwrap(); + assert_eq!(result.status, SolveStatus::Unknown); +} + +#[test] +fn solves_simple_sat_with_model() { + let script = r#" +(set-logic QF_BV) +(declare-const x (_ BitVec 8)) +(assert (= x #x2a)) +(check-sat) +(get-model) +"#; + let result = solve_smt2(script, &Config::default()).unwrap(); + assert_eq!(result.status, SolveStatus::Sat); + let model = result.model.unwrap(); + assert_eq!( + model.get("x"), + Some(&ScalarValue::Bv { + width: 8, + bytes: vec![0x2a] + }) + ); +} + +#[test] +fn supports_push_pop_in_standalone_script() { + let script = r#" +(set-logic QF_BV) +(declare-const x (_ BitVec 4)) +(push 1) +(assert (= x #x1)) +(pop 1) +(assert (= x #x2)) +(check-sat) +(get-model) +"#; + let result = solve_smt2(script, &Config::default()).unwrap(); + assert_eq!(result.status, SolveStatus::Sat); + assert_eq!( + result.model.unwrap().get("x"), + Some(&ScalarValue::Bv { + width: 4, + bytes: vec![2] + }) + ); +} + +#[test] +fn solves_simple_unsat() { + let script = r#" +(set-logic QF_BV) +(declare-const x (_ BitVec 4)) +(assert (= x #x1)) +(assert (= x #x2)) +(check-sat) +"#; + let result = solve_smt2(script, &Config::default()).unwrap(); + assert_eq!(result.status, SolveStatus::Unsat); +} + +#[test] +fn extracts_named_unsat_core() { + let script = r#" +(set-logic QF_BV) +(declare-const p Bool) +(assert (! p :named p-true)) +(assert (! (not p) :named p-false)) +(check-sat) +(get-unsat-core) +"#; + let result = solve_smt2(script, &Config::default()).unwrap(); + assert_eq!(result.status, SolveStatus::Unsat); + assert_eq!( + result.core.unwrap(), + vec!["p-true".to_owned(), "p-false".to_owned()] + ); +} + +#[test] +fn solves_bvadd_constraint() { + let script = r#" +(set-logic QF_BV) +(declare-const x (_ BitVec 4)) +(assert (= (bvadd x #x1) #x3)) +(check-sat) +(get-model) +"#; + let result = solve_smt2(script, &Config::default()).unwrap(); + assert_eq!(result.status, SolveStatus::Sat); + let model = result.model.unwrap(); + assert_eq!( + model.get("x"), + Some(&ScalarValue::Bv { + width: 4, + bytes: vec![0x2] + }) + ); +} + +#[test] +fn optimizes_unsigned_target() { + let mut builder = Builder::new(); + let x = builder.bv_var("x", 4).unwrap(); + let three = builder.bv_const(3, 4).unwrap(); + let ge = builder.bv_uge(x, three).unwrap(); + builder.assert(ge).unwrap(); + builder + .set_optimization(Command::Minimize, x, false) + .unwrap(); + let query = builder.finish().unwrap(); + let result = Solver::default().solve(&query).unwrap(); + assert_eq!(result.status, SolveStatus::Sat); + assert_eq!( + result.optimum, + Some(ScalarValue::Bv { + width: 4, + bytes: vec![3] + }) + ); +} + +#[test] +fn handles_unsigned_division_by_zero_semantics() { + let script = r#" +(set-logic QF_BV) +(assert (= (bvudiv #x5 #x0) #xf)) +(assert (= (bvurem #x5 #x0) #x5)) +(check-sat) +"#; + let result = solve_smt2(script, &Config::default()).unwrap(); + assert_eq!(result.status, SolveStatus::Sat); +} + +``` + +`crates/qfbvsmtrs/tests/tseitin.rs`: + +```rs +use qfbvsmtrs::cnf; +use qfbvsmtrs::config::SatBackendKind; +use qfbvsmtrs::gates::{GateArena, GateId}; +use qfbvsmtrs::sat::{solve_cnf, SatResult}; + +fn force(gate: GateId, value: bool) -> i32 { + let lit = cnf::lit(gate); + if value { + lit + } else { + -lit + } +} + +fn assert_cnf_agrees(gates: &GateArena, inputs: &[(GateId, bool)], output: GateId, expected: bool) { + for backend in [ + SatBackendKind::Splr, + SatBackendKind::Varisat, + SatBackendKind::Dpll, + ] { + let base = cnf::encode(gates, gates.true_gate()); + + let mut clauses = base.clauses.clone(); + for &(gate, value) in inputs { + clauses.push(vec![force(gate, value)]); + } + clauses.push(vec![force(output, expected)]); + assert!( + matches!( + solve_cnf(backend, base.num_vars, clauses, &[], None), + SatResult::Sat(_) + ), + "{backend:?} should accept expected={expected}" + ); + + let mut clauses = base.clauses.clone(); + for &(gate, value) in inputs { + clauses.push(vec![force(gate, value)]); + } + clauses.push(vec![force(output, !expected)]); + assert!( + matches!( + solve_cnf(backend, base.num_vars, clauses, &[], None), + SatResult::Unsat + ), + "{backend:?} should reject expected={}", + !expected + ); + } +} + +#[test] +fn tseitin_matches_truth_table_for_composed_gates() { + let mut gates = GateArena::new(); + let a = gates.input(); + let b = gates.input(); + let c = gates.input(); + let xor = gates.xor(a, b); + let or = gates.or(a, c); + let output = gates.and(xor, or); + + for av in [false, true] { + for bv in [false, true] { + for cv in [false, true] { + let expected = (av ^ bv) & (av | cv); + assert_cnf_agrees(&gates, &[(a, av), (b, bv), (c, cv)], output, expected); + } + } + } +} + +#[test] +fn tseitin_matches_truth_table_for_mux_and_negation() { + let mut gates = GateArena::new(); + let s = gates.input(); + let t = gates.input(); + let f = gates.input(); + let mux = gates.mux(s, t, f); + let output = gates.not(mux); + + for sv in [false, true] { + for tv in [false, true] { + for fv in [false, true] { + let expected = !(if sv { tv } else { fv }); + assert_cnf_agrees(&gates, &[(s, sv), (t, tv), (f, fv)], output, expected); + } + } + } +} + +``` + +`crates/smt-server/Cargo.toml`: + +```toml +[package] +name = "smt-server" +version = "0.1.0" +edition = "2021" +description = "Reference SMT wire-format server and backends" +license = "MIT OR Apache-2.0" + +[dependencies] +binbit = { git = "https://github.com/bint-disasm/binbit" } +dashu = { version = "0.4.2", features = ["num-traits"] } +qfbvsmtrs = { path = "../qfbvsmtrs", features = ["wire"] } +smt-wire = { path = "../smt-wire" } +yaspar = "2.7.0" +z3 = { version = "0.20.0", features = ["gh-release"] } + +``` + +`crates/smt-server/src/backend.rs`: + +```rs +use smt_wire::{ModelBlock, OptimizationValueBlock, SimplifyBlock, UnsatCoreBlock}; + +/// Solver-level status independent from the wire response envelope. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QueryStatus { + Sat, + Unsat, + Unknown, + Ok, +} + +/// Backend result before it is encoded as a wire response. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct QueryResult { + pub status: QueryStatus, + pub model: Option, + pub core: Option, + pub simplify: Option, + pub optimization: Option, + pub message: Option, +} + +impl QueryResult { + pub fn sat(model: Option) -> Self { + Self { + status: QueryStatus::Sat, + model, + core: None, + simplify: None, + optimization: None, + message: None, + } + } + + pub fn unsat(core: Option) -> Self { + Self { + status: QueryStatus::Unsat, + model: None, + core, + simplify: None, + optimization: None, + message: None, + } + } + + pub fn unknown(message: impl Into) -> Self { + Self { + status: QueryStatus::Unknown, + model: None, + core: None, + simplify: None, + optimization: None, + message: Some(message.into()), + } + } + + pub fn ok_simplify(simplify: SimplifyBlock) -> Self { + Self { + status: QueryStatus::Ok, + model: None, + core: None, + simplify: Some(simplify), + optimization: None, + message: None, + } + } + + pub fn sat_optimization(optimization: OptimizationValueBlock) -> Self { + Self { + status: QueryStatus::Sat, + model: optimization.model.clone(), + core: None, + simplify: None, + optimization: Some(optimization), + message: None, + } + } + + pub fn is_conclusive(&self) -> bool { + matches!( + self.status, + QueryStatus::Sat | QueryStatus::Unsat | QueryStatus::Ok + ) + } +} + +/// Backend interface shared by the TCP server, racing layer, cache tests, and +/// SMT-LIB path. +pub trait Backend: Send + Sync { + fn name(&self) -> &'static str; + fn handle(&self, request: &smt_wire::BinaryRequest) -> smt_wire::Result; +} + +``` + +`crates/smt-server/src/binbit_backend.rs`: + +```rs +use std::time::Duration; + +use binbit::{BoolTerm, BvTerm, SmtResult, SmtSolver}; +use smt_wire::{ + request_flags, tag, BinaryRequest, BlobRef, Command, ModelBlock, ModelEntry, NodeRef, + OptimizationValueBlock, ScalarValue, SimplifyBlock, Sort, UnsatCoreBlock, WireError, +}; + +use crate::backend::{Backend, QueryResult}; + +#[derive(Debug, Clone, Default)] +pub struct BinbitBackend; + +impl Backend for BinbitBackend { + fn name(&self) -> &'static str { + "binbit" + } + + fn handle(&self, request: &BinaryRequest) -> smt_wire::Result { + match request.envelope.command { + Command::Simplify => Ok(QueryResult::ok_simplify(SimplifyBlock { + expression: request.expression.clone(), + assertion_roots: request.assertion_roots.clone(), + named_assertion_refs: request.named_assertion_refs.clone(), + assumption_roots: request.assumption_roots.clone(), + })), + Command::Solve => solve(request), + Command::Minimize | Command::Maximize => optimize(request), + } + } +} + +#[derive(Debug, Clone)] +struct BinbitVariable { + node_ref: NodeRef, + sort: Sort, + width: u32, +} + +struct BinbitTranslation { + solver: SmtSolver, + bvs: Vec>, + bools: Vec>, + variables: Vec, +} + +impl BinbitTranslation { + fn bv(&self, reference: NodeRef) -> smt_wire::Result { + if !reference.is_bv() { + return Err(WireError::invalid( + "binbit translation", + "expected BV reference", + )); + } + self.bvs + .get(reference.index() as usize) + .and_then(|term| *term) + .ok_or_else(|| WireError::invalid("binbit translation", "missing BV term")) + } + + fn bool(&self, reference: NodeRef) -> smt_wire::Result { + if !reference.is_bool() { + return Err(WireError::invalid( + "binbit translation", + "expected Bool reference", + )); + } + self.bools + .get(reference.index() as usize) + .and_then(|term| *term) + .ok_or_else(|| WireError::invalid("binbit translation", "missing Bool term")) + } +} + +fn translate(request: &BinaryRequest) -> smt_wire::Result { + let expr = request.expression_view()?; + let mut out = BinbitTranslation { + solver: SmtSolver::new(), + bvs: vec![None; expr.node_count() as usize], + bools: vec![None; expr.node_count() as usize], + variables: Vec::new(), + }; + + for index in 0..expr.node_count() { + let node = expr.node(index)?; + match node.tag { + tag::BV_VAR => { + let term = out.solver.bv_var(node.width); + out.variables.push(BinbitVariable { + node_ref: NodeRef::bv(index)?, + sort: Sort::Bv, + width: node.width, + }); + out.bvs[index as usize] = Some(term); + } + tag::BV_CONST => { + out.bvs[index as usize] = Some(binbit_bv_const( + &mut out.solver, + &expr, + node.width, + node.payload, + )?); + } + tag::BV_NOT => { + let x = child_bv(&expr, &out, &node, 0)?; + out.bvs[index as usize] = Some(out.solver.bv_not(x)); + } + tag::BV_NEG => { + let x = child_bv(&expr, &out, &node, 0)?; + out.bvs[index as usize] = Some(out.solver.bv_neg(x)); + } + tag::BV_AND => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_and(a, b)); + } + tag::BV_OR => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_or(a, b)); + } + tag::BV_XOR => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_xor(a, b)); + } + tag::BV_ADD => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_add(a, b)); + } + tag::BV_SUB => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_sub(a, b)); + } + tag::BV_MUL => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_mul(a, b)); + } + tag::BV_UDIV => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_udiv(a, b)); + } + tag::BV_UREM => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_urem(a, b)); + } + tag::BV_SDIV => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_sdiv(a, b)); + } + tag::BV_SREM => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_srem(a, b)); + } + tag::BV_SMOD => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_smod(a, b)); + } + tag::BV_SHL => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_shl(a, b)); + } + tag::BV_LSHR => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_lshr(a, b)); + } + tag::BV_ASHR => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_ashr(a, b)); + } + tag::BV_EXTRACT => { + let x = child_bv(&expr, &out, &node, 0)?; + out.bvs[index as usize] = Some(out.solver.bv_extract( + x, + u32::from(node.aux_hi), + node.aux_lo, + )); + } + tag::BV_CONCAT => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(out.solver.bv_concat(a, b)); + } + tag::BV_ZEXT => { + let x = child_bv(&expr, &out, &node, 0)?; + out.bvs[index as usize] = + Some(out.solver.bv_zero_extend(x, u32::from(node.aux_hi))); + } + tag::BV_SEXT => { + let x = child_bv(&expr, &out, &node, 0)?; + out.bvs[index as usize] = + Some(out.solver.bv_sign_extend(x, u32::from(node.aux_hi))); + } + tag::BV_ITE => { + let c = child_bool(&expr, &out, &node, 0)?; + let t = child_bv(&expr, &out, &node, 1)?; + let e = child_bv(&expr, &out, &node, 2)?; + out.bvs[index as usize] = Some(out.solver.bv_ite(c, t, e)); + } + tag::BV_SELECT => { + let pairs = u32::from(node.aux_hi); + let mut selectors = Vec::with_capacity(pairs as usize); + let mut values = Vec::with_capacity(pairs as usize); + for pair in 0..pairs { + selectors.push(child_bool(&expr, &out, &node, pair * 2)?); + values.push(child_bv(&expr, &out, &node, pair * 2 + 1)?); + } + let default = child_bv(&expr, &out, &node, pairs * 2)?; + out.bvs[index as usize] = Some(out.solver.bv_select(&selectors, &values, default)); + } + tag::BOOL_TRUE => out.bools[index as usize] = Some(out.solver.bool_true()), + tag::BOOL_FALSE => out.bools[index as usize] = Some(out.solver.bool_false()), + tag::BOOL_VAR => { + let term = out.solver.bool_var(); + out.variables.push(BinbitVariable { + node_ref: NodeRef::bool(index)?, + sort: Sort::Bool, + width: 0, + }); + out.bools[index as usize] = Some(term); + } + tag::BOOL_NOT => { + let x = child_bool(&expr, &out, &node, 0)?; + out.bools[index as usize] = Some(out.solver.bool_not(x)); + } + tag::BOOL_AND => { + let (a, b) = child_bool2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bool_and(a, b)); + } + tag::BOOL_OR => { + let (a, b) = child_bool2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bool_or(a, b)); + } + tag::BOOL_IMPLIES => { + let (a, b) = child_bool2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bool_implies(a, b)); + } + tag::BV_EQ => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_eq(a, b)); + } + tag::BV_ULT => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_ult(a, b)); + } + tag::BV_ULE => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_ule(a, b)); + } + tag::BV_SLT => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_slt(a, b)); + } + tag::BV_SLE => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_sle(a, b)); + } + tag::UADD_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_uadd_overflow(a, b)); + } + tag::SADD_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_sadd_overflow(a, b)); + } + tag::USUB_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_usub_overflow(a, b)); + } + tag::SSUB_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_ssub_overflow(a, b)); + } + tag::UMUL_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_umul_overflow(a, b)); + } + tag::SMUL_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_smul_overflow(a, b)); + } + tag::NEG_OVF => { + let x = child_bv(&expr, &out, &node, 0)?; + out.bools[index as usize] = Some(out.solver.bv_neg_overflow(x)); + } + tag::SDIV_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(out.solver.bv_sdiv_overflow(a, b)); + } + other => { + return Err(WireError::invalid( + "binbit translation", + format!("unknown tag {other}"), + )) + } + } + } + + Ok(out) +} + +fn solve(request: &BinaryRequest) -> smt_wire::Result { + let expr = request.expression_view()?; + let mut translation = translate(request)?; + let want_model = (request.envelope.flags & request_flags::WANT_MODEL) != 0; + let want_core = (request.envelope.flags & request_flags::WANT_CORE) != 0; + + for (index, root) in request.assertion_roots.iter().enumerate() { + let assertion = translation.bool(*root)?; + if want_core && index < request.named_assertion_refs.len() { + let name = expr.blob_str(request.named_assertion_refs[index], "named assertion")?; + translation.solver.assert_named(name, assertion); + } else { + translation.solver.assert(assertion); + } + } + let assumptions = request + .assumption_roots + .iter() + .map(|root| translation.bool(*root)) + .collect::>>()?; + + let result = if request.envelope.budget_ms == 0 { + Some(translation.solver.solve_under_assumptions(&assumptions)) + } else { + translation.solver.solve_under_assumptions_timed( + &assumptions, + Duration::from_millis(u64::from(request.envelope.budget_ms)), + ) + }; + + match result { + Some(SmtResult::Sat) => { + let model = if want_model { + Some(build_model(&mut translation)?) + } else { + None + }; + Ok(QueryResult::sat(model)) + } + Some(SmtResult::Unsat) => { + let core = if want_core { + Some(UnsatCoreBlock { + names: translation + .solver + .unsat_core_names() + .into_iter() + .map(str::to_owned) + .collect(), + }) + } else { + None + }; + Ok(QueryResult::unsat(core)) + } + None => Ok(QueryResult::unknown("binbit budget exhausted")), + } +} + +fn optimize(request: &BinaryRequest) -> smt_wire::Result { + if request.envelope.budget_ms != 0 { + return Ok(QueryResult::unknown( + "binbit optimization currently supports unbounded searches only", + )); + } + let mut translation = translate(request)?; + for root in &request.assertion_roots { + let assertion = translation.bool(*root)?; + translation.solver.assert(assertion); + } + for root in &request.assumption_roots { + let assumption = translation.bool(*root)?; + translation.solver.assert(assumption); + } + let target = translation.bv(request.target_ref().ok_or_else(|| { + WireError::invalid("optimization", "missing MINIMIZE/MAXIMIZE target node") + })?)?; + let signed = (request.envelope.flags & request_flags::SIGNED) != 0; + let minimize = request.envelope.command == Command::Minimize; + let optimum_limbs = match (signed, minimize) { + (false, true) => translation.solver.solve_min_u_limbs(target), + (false, false) => translation.solver.solve_max_u_limbs(target), + (true, true) => translation.solver.solve_min_s_limbs(target), + (true, false) => translation.solver.solve_max_s_limbs(target), + }; + let Some(optimum_limbs) = optimum_limbs else { + return Ok(QueryResult::unsat(None)); + }; + let width = translation.solver.bv_width(target); + let model = if (request.envelope.flags & request_flags::WANT_MODEL) != 0 { + Some(build_model(&mut translation)?) + } else { + None + }; + Ok(QueryResult::sat_optimization(OptimizationValueBlock { + optimum: scalar_from_limbs(width, &optimum_limbs)?, + model, + })) +} + +fn child_bv( + expr: &smt_wire::ExprView<'_>, + translation: &BinbitTranslation, + node: &smt_wire::RawNode, + offset: u32, +) -> smt_wire::Result { + translation.bv(expr.child_ref(node.children + offset)?) +} + +fn child_bool( + expr: &smt_wire::ExprView<'_>, + translation: &BinbitTranslation, + node: &smt_wire::RawNode, + offset: u32, +) -> smt_wire::Result { + translation.bool(expr.child_ref(node.children + offset)?) +} + +fn child_bv2( + expr: &smt_wire::ExprView<'_>, + translation: &BinbitTranslation, + node: &smt_wire::RawNode, +) -> smt_wire::Result<(BvTerm, BvTerm)> { + Ok(( + child_bv(expr, translation, node, 0)?, + child_bv(expr, translation, node, 1)?, + )) +} + +fn child_bool2( + expr: &smt_wire::ExprView<'_>, + translation: &BinbitTranslation, + node: &smt_wire::RawNode, +) -> smt_wire::Result<(BoolTerm, BoolTerm)> { + Ok(( + child_bool(expr, translation, node, 0)?, + child_bool(expr, translation, node, 1)?, + )) +} + +fn binbit_bv_const( + solver: &mut SmtSolver, + expr: &smt_wire::ExprView<'_>, + width: u32, + payload: u64, +) -> smt_wire::Result { + if width <= 128 { + let value = if width <= 64 { + payload as u128 + } else { + bytes_to_u128(expr.blob_ref(BlobRef::from_payload(payload))?) + }; + Ok(solver.bv_const(value, width)) + } else { + let bytes = expr.blob_ref(BlobRef::from_payload(payload))?; + let limbs = bytes_to_limbs(bytes, width); + Ok(solver.bv_const_wide(&limbs, width)) + } +} + +fn build_model(translation: &mut BinbitTranslation) -> smt_wire::Result { + let mut entries = Vec::with_capacity(translation.variables.len()); + for variable in translation.variables.clone() { + let value = match variable.sort { + Sort::Bool => ScalarValue::bool( + translation + .solver + .get_bool_value(translation.bool(variable.node_ref)?), + ), + Sort::Bv => { + let term = translation.bv(variable.node_ref)?; + scalar_from_limbs(variable.width, &translation.solver.get_bv_value_limbs(term))? + } + }; + entries.push(ModelEntry { + node_ref: variable.node_ref, + value, + }); + } + Ok(ModelBlock { entries }) +} + +fn bytes_to_u128(bytes: &[u8]) -> u128 { + let mut value = 0u128; + for (index, byte) in bytes.iter().enumerate().take(16) { + value |= u128::from(*byte) << (index * 8); + } + value +} + +fn bytes_to_limbs(bytes: &[u8], width: u32) -> Vec { + let mut limbs = vec![0u64; (width as usize).div_ceil(64)]; + for (index, byte) in bytes.iter().enumerate() { + limbs[index / 8] |= u64::from(*byte) << ((index % 8) * 8); + } + limbs +} + +fn scalar_from_limbs(width: u32, limbs: &[u64]) -> smt_wire::Result { + let mut bytes = vec![0u8; (width as usize).div_ceil(8)]; + for (index, byte) in bytes.iter_mut().enumerate() { + *byte = ((limbs[index / 8] >> ((index % 8) * 8)) & 0xff) as u8; + } + ScalarValue::bv(width, bytes) +} + +``` + +`crates/smt-server/src/cache.rs`: + +```rs +use std::collections::HashMap; +use std::sync::Mutex; + +use smt_wire::{constants::RESPONSE_MAGIC, request::is_binary_request_payload}; + +/// Simple cache counters for tests and observability. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct CacheStats { + pub hits: u64, + pub misses: u64, + pub inserts: u64, +} + +/// In-memory request/response cache for the stateless protocol. +#[derive(Debug, Default)] +pub struct ResponseCache { + entries: Mutex, Vec>>, + stats: Mutex, +} + +impl ResponseCache { + pub fn new() -> Self { + Self::default() + } + + pub fn lookup(&self, key: &[u8]) -> Option> { + let found = self.entries.lock().ok()?.get(key).cloned(); + if let Ok(mut stats) = self.stats.lock() { + if found.is_some() { + stats.hits += 1; + } else { + stats.misses += 1; + } + } + found + } + + pub fn get(&self, key: &[u8]) -> Option> { + self.entries.lock().ok()?.get(key).cloned() + } + + pub fn insert(&self, key: Vec, response: Vec) { + if let Ok(mut entries) = self.entries.lock() { + entries.insert(key, response); + } + if let Ok(mut stats) = self.stats.lock() { + stats.inserts += 1; + } + } + + pub fn stats(&self) -> CacheStats { + self.stats.lock().map(|stats| *stats).unwrap_or_default() + } + + pub fn len(&self) -> usize { + self.entries + .lock() + .map(|entries| entries.len()) + .unwrap_or(0) + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +/// Build a cache key containing all fields that affect the semantic response +/// payload while excluding the binary request_id echo field. +pub fn cache_key_for_payload(payload: &[u8]) -> Vec { + let mut key = payload.to_vec(); + if is_binary_request_payload(payload) && key.len() >= 8 { + key[4..8].fill(0); + } + key +} + +pub fn binary_request_id(payload: &[u8]) -> Option { + if is_binary_request_payload(payload) && payload.len() >= 8 { + Some(u32::from_le_bytes(payload[4..8].try_into().ok()?)) + } else { + None + } +} + +/// Cached binary responses are stored with the response request_id produced by +/// the first equivalent request. Patch the echoed request_id for later hits. +pub fn rebind_cached_response(mut response: Vec, request_id: Option) -> Vec { + if let Some(request_id) = request_id { + if response.len() >= 8 && response[..4] == RESPONSE_MAGIC { + response[4..8].copy_from_slice(&request_id.to_le_bytes()); + } + } + response +} + +``` + +`crates/smt-server/src/lib.rs`: + +```rs +//! Reference server-side implementation for the SMT wire protocol. +//! +//! The crate provides a small TCP server, binary request handling, native Z3 +//! and binbit solver backends, backend racing, request/response caching, and a +//! compact SMT-LIB frontend. + +pub mod backend; +pub mod binbit_backend; +pub mod cache; +pub mod pool; +pub mod protocol; +pub mod qfbvsmtrs_backend; +pub mod racing; +pub mod server; +pub mod smt2; +pub mod smtlib; +pub mod z3_backend; + +pub use backend::{Backend, QueryResult, QueryStatus}; +pub use binbit_backend::BinbitBackend; +pub use cache::{cache_key_for_payload, rebind_cached_response, CacheStats, ResponseCache}; +pub use pool::PooledBackend; +pub use protocol::{handle_binary_frame, handle_binary_request, response_from_query_result}; +pub use qfbvsmtrs_backend::QfbvsmtrsBackend; +pub use racing::RacingBackend; +pub use server::{dispatch_payload, dispatch_payload_with_cache, serve_tcp, ServerConfig}; +pub use smt2::{request_to_smt2, Smt2Script, Smt2Variable}; +pub use smtlib::{handle_text_frame, parse_smtlib_script, TextQuery}; +pub use z3_backend::Z3Backend; + +``` + +`crates/smt-server/src/main.rs`: + +```rs +use std::sync::Arc; + +use smt_server::{ + serve_tcp, BinbitBackend, QfbvsmtrsBackend, RacingBackend, ServerConfig, Z3Backend, +}; + +fn main() -> std::io::Result<()> { + let addr = std::env::args() + .nth(1) + .unwrap_or_else(|| "127.0.0.1:9123".to_owned()); + let backend = Arc::new(RacingBackend::new(vec![ + Arc::new(Z3Backend), + Arc::new(BinbitBackend), + Arc::new(QfbvsmtrsBackend), + ])); + eprintln!("smt-server listening on {addr} with racing backend (z3 crate + binbit + qfbvsmtrs)"); + serve_tcp(addr, ServerConfig::new(backend)) +} + +``` + +`crates/smt-server/src/pool.rs`: + +```rs +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use smt_wire::BinaryRequest; + +use crate::backend::{Backend, QueryResult}; + +/// A small backend pool that keeps structurally similar stateless requests on +/// the same backend instance. This is the protocol-invisible hook for warm +/// solver instances and future learned-clause locality. +pub struct PooledBackend { + backends: Vec>, +} + +impl PooledBackend { + pub fn new(backends: Vec>) -> Self { + Self { backends } + } + + pub fn route_index(&self, request: &BinaryRequest) -> Option { + if self.backends.is_empty() { + return None; + } + let mut hasher = DefaultHasher::new(); + request.envelope.command.hash(&mut hasher); + request.envelope.flags.hash(&mut hasher); + request.envelope.budget_ms.hash(&mut hasher); + request.envelope.target_node.hash(&mut hasher); + request.expression.hash(&mut hasher); + request.assertion_roots.hash(&mut hasher); + request.named_assertion_refs.hash(&mut hasher); + request.assumption_roots.hash(&mut hasher); + Some((hasher.finish() as usize) % self.backends.len()) + } +} + +impl Backend for PooledBackend { + fn name(&self) -> &'static str { + "pooled" + } + + fn handle(&self, request: &BinaryRequest) -> smt_wire::Result { + let Some(index) = self.route_index(request) else { + return Ok(QueryResult::unknown("backend pool is empty")); + }; + self.backends[index].handle(request) + } +} + +``` + +`crates/smt-server/src/protocol.rs`: + +```rs +use smt_wire::{ + request::is_binary_request_payload, response_flags, BinaryRequest, BinaryResponse, Command, + Status, WireError, +}; + +use crate::backend::{Backend, QueryResult, QueryStatus}; + +/// Decode a binary frame payload, execute it with `backend`, and encode the +/// binary response payload (without the transport length prefix). +pub fn handle_binary_frame( + frame_payload: &[u8], + backend: &dyn Backend, +) -> smt_wire::Result { + if !is_binary_request_payload(frame_payload) { + return BinaryResponse::error(0, "frame payload is not a binary SMTQ request"); + } + match BinaryRequest::parse(frame_payload) { + Ok(request) => handle_binary_request(&request, backend), + Err(err) => BinaryResponse::error(0, &err.to_string()), + } +} + +pub fn handle_binary_request( + request: &BinaryRequest, + backend: &dyn Backend, +) -> smt_wire::Result { + match backend.handle(request) { + Ok(result) => response_from_query_result(request, result), + Err(err) => BinaryResponse::error(request.envelope.request_id, &err.to_string()), + } +} + +pub fn response_from_query_result( + request: &BinaryRequest, + result: QueryResult, +) -> smt_wire::Result { + let request_id = request.envelope.request_id; + match request.envelope.command { + Command::Solve => solve_response(request, result), + Command::Simplify => simplify_response(request_id, result), + Command::Minimize | Command::Maximize => optimize_response(request, result), + } +} + +fn solve_response( + request: &BinaryRequest, + result: QueryResult, +) -> smt_wire::Result { + let request_id = request.envelope.request_id; + let want_model = (request.envelope.flags & smt_wire::request_flags::WANT_MODEL) != 0; + let want_core = (request.envelope.flags & smt_wire::request_flags::WANT_CORE) != 0; + match result.status { + QueryStatus::Sat => { + if want_model { + if let Some(model) = result.model { + BinaryResponse::new( + request_id, + Status::Sat, + response_flags::HAS_MODEL, + model.encode()?, + ) + } else { + BinaryResponse::new(request_id, Status::Sat, 0, Vec::new()) + } + } else { + BinaryResponse::new(request_id, Status::Sat, 0, Vec::new()) + } + } + QueryStatus::Unsat => { + if want_core { + if let Some(core) = result.core { + BinaryResponse::new( + request_id, + Status::Unsat, + response_flags::HAS_CORE, + core.encode()?, + ) + } else { + BinaryResponse::new(request_id, Status::Unsat, 0, Vec::new()) + } + } else { + BinaryResponse::new(request_id, Status::Unsat, 0, Vec::new()) + } + } + QueryStatus::Unknown => BinaryResponse::new(request_id, Status::Unknown, 0, Vec::new()), + QueryStatus::Ok => BinaryResponse::error(request_id, "SOLVE backend returned OK"), + } +} + +fn simplify_response(request_id: u32, result: QueryResult) -> smt_wire::Result { + match result.status { + QueryStatus::Ok => { + let simplify = result.simplify.ok_or_else(|| { + WireError::invalid("simplify response", "OK result without simplify block") + })?; + BinaryResponse::new( + request_id, + Status::Ok, + response_flags::HAS_EXPR, + simplify.encode()?, + ) + } + QueryStatus::Unknown => BinaryResponse::new(request_id, Status::Unknown, 0, Vec::new()), + QueryStatus::Sat | QueryStatus::Unsat => { + BinaryResponse::error(request_id, "SIMPLIFY backend returned SAT/UNSAT") + } + } +} + +fn optimize_response( + request: &BinaryRequest, + result: QueryResult, +) -> smt_wire::Result { + let request_id = request.envelope.request_id; + match result.status { + QueryStatus::Sat => { + let mut optimization = result.optimization.ok_or_else(|| { + WireError::invalid("optimization response", "SAT result without optimum block") + })?; + let want_model = (request.envelope.flags & smt_wire::request_flags::WANT_MODEL) != 0; + if !want_model { + optimization.model = None; + } + let mut flags = response_flags::HAS_VALUE; + if optimization.model.is_some() { + flags |= response_flags::HAS_MODEL; + } + BinaryResponse::new(request_id, Status::Sat, flags, optimization.encode()?) + } + QueryStatus::Unsat => BinaryResponse::new(request_id, Status::Unsat, 0, Vec::new()), + QueryStatus::Unknown => BinaryResponse::new(request_id, Status::Unknown, 0, Vec::new()), + QueryStatus::Ok => BinaryResponse::error(request_id, "optimization backend returned OK"), + } +} + +``` + +`crates/smt-server/src/qfbvsmtrs_backend.rs`: + +```rs +use std::time::Duration; + +use smt_wire::{ + BinaryRequest, Command, OptimizationValueBlock, ScalarValue, SimplifyBlock, UnsatCoreBlock, +}; + +use crate::backend::{Backend, QueryResult}; + +#[derive(Debug, Clone, Default)] +pub struct QfbvsmtrsBackend; + +impl Backend for QfbvsmtrsBackend { + fn name(&self) -> &'static str { + "qfbvsmtrs" + } + + fn handle(&self, request: &BinaryRequest) -> smt_wire::Result { + match request.envelope.command { + Command::Simplify => Ok(QueryResult::ok_simplify(SimplifyBlock { + expression: request.expression.clone(), + assertion_roots: request.assertion_roots.clone(), + named_assertion_refs: request.named_assertion_refs.clone(), + assumption_roots: request.assumption_roots.clone(), + })), + Command::Solve | Command::Minimize | Command::Maximize => solve(request), + } + } +} + +fn scalar_to_wire(value: &qfbvsmtrs::ScalarValue) -> smt_wire::Result { + match value { + qfbvsmtrs::ScalarValue::Bool(value) => Ok(ScalarValue::bool(*value)), + qfbvsmtrs::ScalarValue::Bv { width, bytes } => ScalarValue::bv(*width, bytes.clone()), + } +} + +fn solve(request: &BinaryRequest) -> smt_wire::Result { + let query = match qfbvsmtrs::query_from_wire(request) { + Ok(query) => query, + Err(err) => return Ok(QueryResult::unknown(format!("qfbvsmtrs: {err}"))), + }; + let budget = if request.envelope.budget_ms == 0 { + None + } else { + Some(Duration::from_millis(u64::from(request.envelope.budget_ms))) + }; + let config = qfbvsmtrs::Config::default().with_budget(budget); + let mut solver = qfbvsmtrs::Solver::new(config); + let result = match solver.solve(&query) { + Ok(result) => result, + Err(err) => return Ok(QueryResult::unknown(format!("qfbvsmtrs: {err}"))), + }; + + match result.status { + qfbvsmtrs::SolveStatus::Sat => { + let model = if let Some(model) = &result.model { + match qfbvsmtrs::model_to_wire(model) { + Ok(model) => Some(model), + Err(err) => return Ok(QueryResult::unknown(format!("qfbvsmtrs model: {err}"))), + } + } else { + None + }; + if let Some(optimum) = result.optimum { + Ok(QueryResult::sat_optimization(OptimizationValueBlock { + optimum: scalar_to_wire(&optimum)?, + model, + })) + } else { + Ok(QueryResult::sat(model)) + } + } + qfbvsmtrs::SolveStatus::Unsat => Ok(QueryResult::unsat( + result.core.map(|names| UnsatCoreBlock { names }), + )), + qfbvsmtrs::SolveStatus::Unknown => { + Ok(QueryResult::unknown(result.message.unwrap_or_else(|| { + "qfbvsmtrs returned unknown".to_owned() + }))) + } + qfbvsmtrs::SolveStatus::Ok => Ok(QueryResult::unknown( + "qfbvsmtrs returned OK for a solve request", + )), + } +} + +``` + +`crates/smt-server/src/racing.rs`: + +```rs +use std::sync::{mpsc, Arc}; +use std::thread; + +use smt_wire::BinaryRequest; + +use crate::backend::{Backend, QueryResult}; + +/// Backend that races several backend implementations and returns the first +/// conclusive result. `UNKNOWN` results are held until all backends are +/// inconclusive. +#[derive(Clone)] +pub struct RacingBackend { + backends: Vec>, +} + +impl RacingBackend { + pub fn new(backends: Vec>) -> Self { + Self { backends } + } + + pub fn backends(&self) -> &[Arc] { + &self.backends + } +} + +impl Backend for RacingBackend { + fn name(&self) -> &'static str { + "racing" + } + + fn handle(&self, request: &BinaryRequest) -> smt_wire::Result { + if self.backends.is_empty() { + return Ok(QueryResult::unknown("no backends configured")); + } + if self.backends.len() == 1 { + return self.backends[0].handle(request); + } + let (tx, rx) = mpsc::channel(); + for backend in &self.backends { + let tx = tx.clone(); + let backend = Arc::clone(backend); + let request = request.clone(); + thread::spawn(move || { + let result = backend.handle(&request); + let _ = tx.send((backend.name(), result)); + }); + } + drop(tx); + let mut first_unknown = None; + while let Ok((name, result)) = rx.recv() { + match result { + Ok(result) if result.is_conclusive() => { + let winner_status = result.status; + thread::spawn(move || { + for (other_name, other_result) in rx { + if let Ok(other_result) = other_result { + if other_result.is_conclusive() + && other_result.status != winner_status + { + eprintln!( + "backend disagreement: {name} returned {:?}, {other_name} returned {:?}", + winner_status, other_result.status + ); + } + } + } + }); + return Ok(result); + } + Ok(result) => { + first_unknown.get_or_insert(result); + } + Err(err) => { + first_unknown.get_or_insert_with(|| QueryResult::unknown(err.to_string())); + } + } + } + Ok( + first_unknown + .unwrap_or_else(|| QueryResult::unknown("all backends returned no result")), + ) + } +} + +``` + +`crates/smt-server/src/server.rs`: + +```rs +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream, ToSocketAddrs}; +use std::sync::Arc; +use std::thread; + +use smt_wire::{le, BinaryResponse}; + +use crate::backend::Backend; +use crate::cache::{ + binary_request_id, cache_key_for_payload, rebind_cached_response, ResponseCache, +}; +use crate::protocol::handle_binary_frame; +use crate::smtlib::handle_text_frame; + +#[derive(Clone)] +pub struct ServerConfig { + pub backend: Arc, + pub cache: Option>, +} + +impl ServerConfig { + pub fn new(backend: Arc) -> Self { + Self { + backend, + cache: Some(Arc::new(ResponseCache::new())), + } + } +} + +pub fn serve_tcp(addr: impl ToSocketAddrs, config: ServerConfig) -> std::io::Result<()> { + let listener = TcpListener::bind(addr)?; + let config = Arc::new(config); + for stream in listener.incoming() { + let stream = stream?; + let config = Arc::clone(&config); + thread::spawn(move || { + let _ = handle_connection(stream, config); + }); + } + Ok(()) +} + +fn handle_connection(mut stream: TcpStream, config: Arc) -> std::io::Result<()> { + loop { + let mut len_bytes = [0u8; 4]; + match stream.read_exact(&mut len_bytes) { + Ok(()) => {} + Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(()), + Err(err) => return Err(err), + } + let frame_len = u32::from_le_bytes(len_bytes) as usize; + let mut payload = vec![0u8; frame_len]; + stream.read_exact(&mut payload)?; + + let response_payload = + dispatch_payload_with_cache(&payload, config.backend.as_ref(), config.cache.as_deref()); + let frame = le::encode_transport_frame(&response_payload) + .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?; + stream.write_all(&frame)?; + } +} + +pub fn dispatch_payload_with_cache( + payload: &[u8], + backend: &dyn Backend, + cache: Option<&ResponseCache>, +) -> Vec { + if let Some(cache) = cache { + let key = cache_key_for_payload(payload); + let request_id = binary_request_id(payload); + if let Some(response) = cache.lookup(&key) { + return rebind_cached_response(response, request_id); + } + let response = dispatch_payload(payload, backend); + cache.insert(key, response.clone()); + return response; + } + dispatch_payload(payload, backend) +} + +pub fn dispatch_payload(payload: &[u8], backend: &dyn Backend) -> Vec { + if smt_wire::request::is_binary_request_payload(payload) { + match handle_binary_frame(payload, backend).and_then(|response| response.encode()) { + Ok(bytes) => bytes, + Err(err) => binary_error_response(&err.to_string()), + } + } else { + match handle_text_frame(payload, backend) { + Ok(bytes) => bytes, + Err(err) => format!("(error {:?})\n", err.to_string()).into_bytes(), + } + } +} + +fn binary_error_response(message: &str) -> Vec { + BinaryResponse::error(0, message) + .and_then(|response| response.encode()) + .unwrap_or_else(|_| b"SMTR\0\0\0\0\x04\x10\0\0\0\0\0\0".to_vec()) +} + +``` + +`crates/smt-server/src/smt2.rs`: + +```rs +use std::collections::{HashMap, HashSet}; + +use smt_wire::{request_flags, tag, BinaryRequest, BlobRef, ExprView, NodeRef, Sort, WireError}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Smt2Variable { + pub node_ref: NodeRef, + pub name: String, + pub sort: Sort, + pub width: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Smt2Script { + pub script: String, + pub variables: Vec, +} + +pub fn request_to_smt2(request: &BinaryRequest) -> smt_wire::Result { + let expr = request.expression_view()?; + let variables = collect_variables(&expr)?; + let mut out = String::new(); + out.push_str("(set-logic QF_BV)\n"); + if request.envelope.budget_ms != 0 { + out.push_str(&format!( + "(set-option :timeout {})\n", + request.envelope.budget_ms + )); + } + if (request.envelope.flags & request_flags::WANT_CORE) != 0 { + out.push_str("(set-option :produce-unsat-cores true)\n"); + } + if (request.envelope.flags & request_flags::WANT_MODEL) != 0 { + out.push_str("(set-option :produce-models true)\n"); + } + let mut declared = HashMap::::new(); + for variable in &variables { + match declared.get(&variable.name) { + Some(&(sort, width)) if sort == variable.sort && width == variable.width => continue, + Some(&(sort, width)) => { + return Err(WireError::invalid( + "SMT-LIB translation", + format!( + "symbol {:?} is used with both {:?}/{} and {:?}/{} sorts", + variable.name, sort, width, variable.sort, variable.width + ), + )) + } + None => { + declared.insert(variable.name.clone(), (variable.sort, variable.width)); + } + } + out.push_str("(declare-fun "); + out.push_str("e_symbol(&variable.name)); + out.push_str(" () "); + out.push_str(&sort_to_smt2(variable.sort, variable.width)); + out.push_str(")\n"); + } + + let mut cache = vec![None; expr.node_count() as usize]; + for (index, root) in request.assertion_roots.iter().enumerate() { + let term = term_ref(&expr, *root, &mut cache)?; + if index < request.named_assertion_refs.len() { + let name = expr.blob_str(request.named_assertion_refs[index], "named assertion")?; + out.push_str("(assert (! "); + out.push_str(&term); + out.push_str(" :named "); + out.push_str("e_symbol(name)); + out.push_str("))\n"); + } else { + out.push_str("(assert "); + out.push_str(&term); + out.push_str(")\n"); + } + } + for root in &request.assumption_roots { + let term = term_ref(&expr, *root, &mut cache)?; + out.push_str("(assert "); + out.push_str(&term); + out.push_str(")\n"); + } + out.push_str("(check-sat)\n"); + if (request.envelope.flags & request_flags::WANT_MODEL) != 0 && !variables.is_empty() { + out.push_str("(get-value ("); + let mut emitted = HashSet::new(); + for variable in &variables { + if emitted.insert(variable.name.clone()) { + out.push(' '); + out.push_str("e_symbol(&variable.name)); + } + } + out.push_str("))\n"); + } + if (request.envelope.flags & request_flags::WANT_CORE) != 0 { + out.push_str("(get-unsat-core)\n"); + } + Ok(Smt2Script { + script: out, + variables, + }) +} + +pub fn collect_variables(expr: &ExprView<'_>) -> smt_wire::Result> { + let mut vars = Vec::new(); + for index in 0..expr.node_count() { + let node = expr.node(index)?; + match node.tag { + tag::BV_VAR => vars.push(Smt2Variable { + node_ref: NodeRef::bv(index)?, + name: expr + .blob_str(BlobRef::from_payload(node.payload), "BV variable")? + .to_owned(), + sort: Sort::Bv, + width: node.width, + }), + tag::BOOL_VAR => vars.push(Smt2Variable { + node_ref: NodeRef::bool(index)?, + name: expr + .blob_str(BlobRef::from_payload(node.payload), "Bool variable")? + .to_owned(), + sort: Sort::Bool, + width: 0, + }), + _ => {} + } + } + Ok(vars) +} + +pub fn quote_symbol(symbol: &str) -> String { + if !symbol.is_empty() + && symbol.chars().all(|c| { + c.is_ascii_alphanumeric() + || matches!( + c, + '_' | '-' + | '.' + | '$' + | '/' + | ':' + | '+' + | '*' + | '=' + | '<' + | '>' + | '?' + | '!' + | '~' + | '&' + | '^' + | '%' + | '@' + ) + }) + && !symbol.chars().next().unwrap().is_ascii_digit() + && !matches!( + symbol, + "true" | "false" | "Bool" | "let" | "assert" | "check-sat" + ) + { + symbol.to_owned() + } else { + let escaped = symbol.replace('\\', "\\\\").replace('|', "\\|"); + format!("|{escaped}|") + } +} + +pub fn sort_to_smt2(sort: Sort, width: u32) -> String { + match sort { + Sort::Bool => "Bool".to_owned(), + Sort::Bv => format!("(_ BitVec {width})"), + } +} + +fn term_ref( + expr: &ExprView<'_>, + reference: NodeRef, + cache: &mut [Option], +) -> smt_wire::Result { + let term = term_node(expr, reference.index(), cache)?; + Ok(term) +} + +fn term_node( + expr: &ExprView<'_>, + index: u32, + cache: &mut [Option], +) -> smt_wire::Result { + if let Some(term) = &cache[index as usize] { + return Ok(term.clone()); + } + let node = expr.node(index)?; + let term = match node.tag { + tag::BV_VAR | tag::BOOL_VAR => { + quote_symbol(expr.blob_str(BlobRef::from_payload(node.payload), "variable name")?) + } + tag::BV_CONST => const_to_smt2(expr, node.width, node.payload)?, + tag::BOOL_TRUE => "true".to_owned(), + tag::BOOL_FALSE => "false".to_owned(), + tag::BV_NOT => unary(expr, node.children, cache, "bvnot")?, + tag::BV_NEG => unary(expr, node.children, cache, "bvneg")?, + tag::BV_AND => binary(expr, node.children, cache, "bvand")?, + tag::BV_OR => binary(expr, node.children, cache, "bvor")?, + tag::BV_XOR => binary(expr, node.children, cache, "bvxor")?, + tag::BV_ADD => binary(expr, node.children, cache, "bvadd")?, + tag::BV_SUB => binary(expr, node.children, cache, "bvsub")?, + tag::BV_MUL => binary(expr, node.children, cache, "bvmul")?, + tag::BV_UDIV => binary(expr, node.children, cache, "bvudiv")?, + tag::BV_UREM => binary(expr, node.children, cache, "bvurem")?, + tag::BV_SDIV => binary(expr, node.children, cache, "bvsdiv")?, + tag::BV_SREM => binary(expr, node.children, cache, "bvsrem")?, + tag::BV_SMOD => binary(expr, node.children, cache, "bvsmod")?, + tag::BV_SHL => binary(expr, node.children, cache, "bvshl")?, + tag::BV_LSHR => binary(expr, node.children, cache, "bvlshr")?, + tag::BV_ASHR => binary(expr, node.children, cache, "bvashr")?, + tag::BV_EXTRACT => { + let child = child_term(expr, node.children, cache)?; + format!("((_ extract {} {}) {child})", node.aux_hi, node.aux_lo) + } + tag::BV_CONCAT => binary(expr, node.children, cache, "concat")?, + tag::BV_ZEXT => { + let child = child_term(expr, node.children, cache)?; + format!("((_ zero_extend {}) {child})", node.aux_hi) + } + tag::BV_SEXT => { + let child = child_term(expr, node.children, cache)?; + format!("((_ sign_extend {}) {child})", node.aux_hi) + } + tag::BV_ITE => { + let c = child_term(expr, node.children, cache)?; + let t = child_term(expr, node.children + 1, cache)?; + let e = child_term(expr, node.children + 2, cache)?; + format!("(ite {c} {t} {e})") + } + tag::BV_SELECT => { + let pairs = u32::from(node.aux_hi); + let mut result = child_term(expr, node.children + pairs * 2, cache)?; + for pair in (0..pairs).rev() { + let selector = child_term(expr, node.children + pair * 2, cache)?; + let value = child_term(expr, node.children + pair * 2 + 1, cache)?; + result = format!("(ite {selector} {value} {result})"); + } + result + } + tag::BOOL_NOT => unary(expr, node.children, cache, "not")?, + tag::BOOL_AND => binary(expr, node.children, cache, "and")?, + tag::BOOL_OR => binary(expr, node.children, cache, "or")?, + tag::BOOL_IMPLIES => binary(expr, node.children, cache, "=>")?, + tag::BV_EQ => binary(expr, node.children, cache, "=")?, + tag::BV_ULT => binary(expr, node.children, cache, "bvult")?, + tag::BV_ULE => binary(expr, node.children, cache, "bvule")?, + tag::BV_SLT => binary(expr, node.children, cache, "bvslt")?, + tag::BV_SLE => binary(expr, node.children, cache, "bvsle")?, + tag::UADD_OVF => { + let a = child_term(expr, node.children, cache)?; + let b = child_term(expr, node.children + 1, cache)?; + format!("(bvult (bvadd {a} {b}) {a})") + } + tag::USUB_OVF => binary(expr, node.children, cache, "bvult")?, + tag::UMUL_OVF => { + let a_ref = expr.child_ref(node.children)?; + let width = expr.node(a_ref.index())?.width; + let a = child_term(expr, node.children, cache)?; + let b = child_term(expr, node.children + 1, cache)?; + let product = + format!("(bvmul ((_ zero_extend {width}) {a}) ((_ zero_extend {width}) {b}))"); + format!( + "(not (= ((_ extract {} {}) {product}) {}))", + width * 2 - 1, + width, + zero_bv(width) + ) + } + tag::SADD_OVF => signed_add_overflow(expr, node.children, cache)?, + tag::SSUB_OVF => signed_sub_overflow(expr, node.children, cache)?, + tag::SMUL_OVF => signed_mul_overflow(expr, node.children, cache)?, + tag::NEG_OVF => { + let a_ref = expr.child_ref(node.children)?; + let width = expr.node(a_ref.index())?.width; + let a = child_term(expr, node.children, cache)?; + format!("(= {a} {})", signed_min_bv(width)) + } + tag::SDIV_OVF => { + let a_ref = expr.child_ref(node.children)?; + let width = expr.node(a_ref.index())?.width; + let a = child_term(expr, node.children, cache)?; + let b = child_term(expr, node.children + 1, cache)?; + format!( + "(and (= {a} {}) (= {b} {}))", + signed_min_bv(width), + ones_bv(width) + ) + } + other => { + return Err(WireError::invalid( + "SMT-LIB translation", + format!("unknown tag {other}"), + )) + } + }; + cache[index as usize] = Some(term.clone()); + Ok(term) +} + +fn child_term( + expr: &ExprView<'_>, + child_index: u32, + cache: &mut [Option], +) -> smt_wire::Result { + term_ref(expr, expr.child_ref(child_index)?, cache) +} + +fn unary( + expr: &ExprView<'_>, + child_index: u32, + cache: &mut [Option], + op: &str, +) -> smt_wire::Result { + let child = child_term(expr, child_index, cache)?; + Ok(format!("({op} {child})")) +} + +fn binary( + expr: &ExprView<'_>, + child_index: u32, + cache: &mut [Option], + op: &str, +) -> smt_wire::Result { + let a = child_term(expr, child_index, cache)?; + let b = child_term(expr, child_index + 1, cache)?; + Ok(format!("({op} {a} {b})")) +} + +fn const_to_smt2(expr: &ExprView<'_>, width: u32, payload: u64) -> smt_wire::Result { + let mut bytes = if width <= 64 { + let mut raw = payload.to_le_bytes().to_vec(); + raw.truncate((width as usize).div_ceil(8)); + raw + } else { + expr.blob_ref(BlobRef::from_payload(payload))?.to_vec() + }; + let valid = width % 8; + if valid != 0 && !bytes.is_empty() { + let last = bytes.len() - 1; + bytes[last] &= (1 << valid) - 1; + } + let mut bits = String::with_capacity(width as usize + 2); + bits.push_str("#b"); + for bit in (0..width).rev() { + let byte = bytes[(bit / 8) as usize]; + bits.push(if ((byte >> (bit % 8)) & 1) == 1 { + '1' + } else { + '0' + }); + } + Ok(bits) +} + +fn signed_add_overflow( + expr: &ExprView<'_>, + child_index: u32, + cache: &mut [Option], +) -> smt_wire::Result { + let a_ref = expr.child_ref(child_index)?; + let width = expr.node(a_ref.index())?.width; + let a = child_term(expr, child_index, cache)?; + let b = child_term(expr, child_index + 1, cache)?; + let sum = format!("(bvadd {a} {b})"); + Ok(format!( + "(or (and (= {} #b0) (= {} #b0) (= {} #b1)) (and (= {} #b1) (= {} #b1) (= {} #b0)))", + sign(&a, width), + sign(&b, width), + sign(&sum, width), + sign(&a, width), + sign(&b, width), + sign(&sum, width) + )) +} + +fn signed_sub_overflow( + expr: &ExprView<'_>, + child_index: u32, + cache: &mut [Option], +) -> smt_wire::Result { + let a_ref = expr.child_ref(child_index)?; + let width = expr.node(a_ref.index())?.width; + let a = child_term(expr, child_index, cache)?; + let b = child_term(expr, child_index + 1, cache)?; + let diff = format!("(bvsub {a} {b})"); + Ok(format!( + "(or (and (= {} #b0) (= {} #b1) (= {} #b1)) (and (= {} #b1) (= {} #b0) (= {} #b0)))", + sign(&a, width), + sign(&b, width), + sign(&diff, width), + sign(&a, width), + sign(&b, width), + sign(&diff, width) + )) +} + +fn signed_mul_overflow( + expr: &ExprView<'_>, + child_index: u32, + cache: &mut [Option], +) -> smt_wire::Result { + let a_ref = expr.child_ref(child_index)?; + let width = expr.node(a_ref.index())?.width; + let a = child_term(expr, child_index, cache)?; + let b = child_term(expr, child_index + 1, cache)?; + let product = format!("(bvmul ((_ sign_extend {width}) {a}) ((_ sign_extend {width}) {b}))"); + let low = format!("((_ extract {} 0) {product})", width - 1); + Ok(format!( + "(not (= {product} ((_ sign_extend {width}) {low})))" + )) +} + +fn sign(term: &str, width: u32) -> String { + format!("((_ extract {} {}) {term})", width - 1, width - 1) +} + +fn zero_bv(width: u32) -> String { + format!("#b{}", "0".repeat(width as usize)) +} + +fn ones_bv(width: u32) -> String { + format!("#b{}", "1".repeat(width as usize)) +} + +fn signed_min_bv(width: u32) -> String { + let mut bits = String::with_capacity(width as usize + 2); + bits.push_str("#b1"); + bits.push_str(&"0".repeat(width.saturating_sub(1) as usize)); + bits +} + +``` + +`crates/smt-server/src/smtlib.rs`: + +```rs +use std::collections::HashMap; + +use dashu::{float::DBig, integer::UBig}; +use smt_wire::{ + BinaryRequest, ExprBuilder, ModelBlock, NodeRef, ScalarValue, UnsatCoreBlock, WireError, +}; + +use crate::backend::{Backend, QueryResult, QueryStatus}; +use crate::smt2::quote_symbol; +use yaspar::{ + action::{ + ActionOnAttribute, ActionOnConstant, ActionOnIdentifier, ActionOnIndex, ActionOnSort, + ActionOnString, ActionOnTerm, ParsingAction, ParsingResult, Pattern, + }, + ast::{DatatypeDec, DatatypeDef, FunctionDef, Keyword}, + position::Range, +}; + +#[derive(Debug, Clone)] +pub struct TextQuery { + pub request: BinaryRequest, + pub want_model: bool, + pub want_core: bool, + pub get_values: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum SExpr { + Atom(String), + List(Vec), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SmtSort { + Bool, + Bv(u32), +} + +#[derive(Debug, Clone)] +struct Binding { + node: NodeRef, + sort: SmtSort, +} + +#[derive(Debug, Default)] +struct ScriptState { + builder: ExprBuilder, + env: HashMap, + want_model: bool, + want_core: bool, + get_values: Vec, + saw_check_sat: bool, +} + +pub fn parse_smtlib_script(script: &str) -> smt_wire::Result { + let exprs = parse_sexprs(script)?; + let mut state = ScriptState::default(); + for expr in exprs { + let list = match expr { + SExpr::List(list) => list, + SExpr::Atom(atom) => { + return Err(WireError::invalid( + "SMT-LIB command", + format!("top-level atom {atom:?}"), + )) + } + }; + if list.is_empty() { + continue; + } + let cmd = atom(&list[0])?; + match cmd { + "set-logic" => { + if list.len() == 2 && atom(&list[1])? != "QF_BV" { + return Err(WireError::invalid( + "SMT-LIB set-logic", + "only QF_BV is supported", + )); + } + } + "set-option" | "exit" => {} + "push" | "pop" | "reset" => { + return Err(WireError::invalid( + "SMT-LIB incremental command", + format!("{cmd} is not supported on the stateless text path"), + )) + } + "declare-const" => declare_const(&mut state, &list)?, + "declare-fun" => declare_fun(&mut state, &list)?, + "define-const" => define_const(&mut state, &list)?, + "define-fun" => define_fun(&mut state, &list)?, + "assert" => assert_command(&mut state, &list)?, + "check-sat" => state.saw_check_sat = true, + "check-sat-assuming" => check_sat_assuming(&mut state, &list)?, + "get-model" => state.want_model = true, + "get-value" => get_value_command(&mut state, &list)?, + "get-unsat-core" => state.want_core = true, + other => { + return Err(WireError::invalid( + "SMT-LIB command", + format!("unsupported command {other}"), + )) + } + } + } + if !state.saw_check_sat { + return Err(WireError::invalid( + "SMT-LIB script", + "script does not contain check-sat", + )); + } + let request_bytes = + state + .builder + .build_solve_request(0, 0, state.want_model, state.want_core)?; + let request = BinaryRequest::parse(&request_bytes)?; + Ok(TextQuery { + request, + want_model: state.want_model, + want_core: state.want_core, + get_values: state.get_values, + }) +} + +pub fn handle_text_frame(frame_payload: &[u8], backend: &dyn Backend) -> smt_wire::Result> { + let script = std::str::from_utf8(frame_payload) + .map_err(|_| WireError::invalid("SMT-LIB frontend", "text request is not UTF-8"))?; + let query = match parse_smtlib_script(script) { + Ok(query) => query, + Err(err) => return Ok(format!("(error {:?})\n", err.to_string()).into_bytes()), + }; + match backend.handle(&query.request) { + Ok(result) => Ok(text_response(&query, result).into_bytes()), + Err(err) => Ok(format!("(error {:?})\n", err.to_string()).into_bytes()), + } +} + +fn declare_const(state: &mut ScriptState, list: &[SExpr]) -> smt_wire::Result<()> { + if list.len() != 3 { + return Err(WireError::invalid( + "declare-const", + "expected name and sort", + )); + } + let name = atom(&list[1])?.to_owned(); + let sort = parse_sort(&list[2])?; + let node = match sort { + SmtSort::Bool => state.builder.bool_var(&name)?, + SmtSort::Bv(width) => state.builder.bv_var(&name, width)?, + }; + state.env.insert(name, Binding { node, sort }); + Ok(()) +} + +fn declare_fun(state: &mut ScriptState, list: &[SExpr]) -> smt_wire::Result<()> { + if list.len() != 4 { + return Err(WireError::invalid( + "declare-fun", + "expected name, args, sort", + )); + } + match &list[2] { + SExpr::List(args) if args.is_empty() => {} + _ => { + return Err(WireError::invalid( + "declare-fun", + "only 0-arity functions are supported", + )) + } + } + declare_const(state, &[list[0].clone(), list[1].clone(), list[3].clone()]) +} + +fn define_const(state: &mut ScriptState, list: &[SExpr]) -> smt_wire::Result<()> { + if list.len() != 4 { + return Err(WireError::invalid( + "define-const", + "expected name, sort, value", + )); + } + let name = atom(&list[1])?.to_owned(); + let declared = parse_sort(&list[2])?; + let binding = parse_expr_with_locals(state, &list[3], &mut HashMap::new())?; + if declared != binding.sort { + return Err(WireError::invalid( + "define-const", + "declared sort does not match value", + )); + } + state.env.insert(name, binding); + Ok(()) +} + +fn define_fun(state: &mut ScriptState, list: &[SExpr]) -> smt_wire::Result<()> { + if list.len() != 5 { + return Err(WireError::invalid( + "define-fun", + "expected name, args, sort, body", + )); + } + match &list[2] { + SExpr::List(args) if args.is_empty() => {} + _ => { + return Err(WireError::invalid( + "define-fun", + "only 0-arity functions are supported", + )) + } + } + define_const( + state, + &[ + list[0].clone(), + list[1].clone(), + list[3].clone(), + list[4].clone(), + ], + ) +} + +fn assert_command(state: &mut ScriptState, list: &[SExpr]) -> smt_wire::Result<()> { + if list.len() != 2 { + return Err(WireError::invalid("assert", "expected one expression")); + } + if let Some((inner, name)) = named_annotation(&list[1])? { + let binding = parse_expr_with_locals(state, inner, &mut HashMap::new())?; + expect_sort(binding.sort, SmtSort::Bool, "assert")?; + state.builder.assert_named(&name, binding.node)?; + } else { + let binding = parse_expr_with_locals(state, &list[1], &mut HashMap::new())?; + expect_sort(binding.sort, SmtSort::Bool, "assert")?; + state.builder.assert(binding.node)?; + } + Ok(()) +} + +fn check_sat_assuming(state: &mut ScriptState, list: &[SExpr]) -> smt_wire::Result<()> { + if list.len() != 2 { + return Err(WireError::invalid( + "check-sat-assuming", + "expected one assumption list", + )); + } + let assumptions = match &list[1] { + SExpr::List(items) => items, + _ => { + return Err(WireError::invalid( + "check-sat-assuming", + "expected assumption list", + )) + } + }; + for item in assumptions { + let binding = parse_expr_with_locals(state, item, &mut HashMap::new())?; + expect_sort(binding.sort, SmtSort::Bool, "check-sat-assuming")?; + state.builder.assume(binding.node)?; + } + state.saw_check_sat = true; + Ok(()) +} + +fn get_value_command(state: &mut ScriptState, list: &[SExpr]) -> smt_wire::Result<()> { + if list.len() != 2 { + return Err(WireError::invalid("get-value", "expected one term list")); + } + let terms = match &list[1] { + SExpr::List(items) => items, + _ => return Err(WireError::invalid("get-value", "expected term list")), + }; + state.want_model = true; + for term in terms { + match term { + SExpr::Atom(name) => state.get_values.push(name.clone()), + _ => { + return Err(WireError::invalid( + "get-value", + "this frontend supports get-value for declared symbols", + )) + } + } + } + Ok(()) +} + +fn named_annotation(expr: &SExpr) -> smt_wire::Result> { + let SExpr::List(items) = expr else { + return Ok(None); + }; + if items.len() >= 4 && atom(&items[0])? == "!" { + let mut name = None; + let mut index = 2; + while index + 1 < items.len() { + if atom(&items[index])? == ":named" { + name = Some(atom(&items[index + 1])?.to_owned()); + } + index += 2; + } + if let Some(name) = name { + return Ok(Some((&items[1], name))); + } + } + Ok(None) +} + +fn parse_expr_with_locals( + state: &mut ScriptState, + expr: &SExpr, + locals: &mut HashMap, +) -> smt_wire::Result { + match expr { + SExpr::Atom(atom) => parse_atom_expr(state, atom, locals), + SExpr::List(items) => parse_list_expr(state, items, locals), + } +} + +fn parse_atom_expr( + state: &mut ScriptState, + value: &str, + locals: &HashMap, +) -> smt_wire::Result { + if value == "true" { + return Ok(Binding { + node: state.builder.bool_true()?, + sort: SmtSort::Bool, + }); + } + if value == "false" { + return Ok(Binding { + node: state.builder.bool_false()?, + sort: SmtSort::Bool, + }); + } + if let Some((digits, radix)) = value + .strip_prefix("#b") + .map(|s| (s, 2)) + .or_else(|| value.strip_prefix("#x").map(|s| (s, 16))) + { + let width = if radix == 2 { + digits.len() as u32 + } else { + digits.len() as u32 * 4 + }; + let bytes = literal_bytes(digits, radix, width, value)?; + let node = if width <= 64 { + let mut raw = [0u8; 8]; + raw[..bytes.len()].copy_from_slice(&bytes); + state.builder.bv_const(u64::from_le_bytes(raw), width)? + } else { + state.builder.bv_const_wide(&bytes, width)? + }; + return Ok(Binding { + node, + sort: SmtSort::Bv(width), + }); + } + locals + .get(value) + .or_else(|| state.env.get(value)) + .cloned() + .ok_or_else(|| WireError::invalid("SMT-LIB symbol", format!("undefined symbol {value:?}"))) +} + +fn literal_bytes( + digits: &str, + radix: u32, + width: u32, + original: &str, +) -> smt_wire::Result> { + let mut bytes = vec![0u8; (width as usize).div_ceil(8)]; + match radix { + 2 => { + for (offset, ch) in digits.chars().rev().enumerate() { + match ch { + '0' => {} + '1' => bytes[offset / 8] |= 1 << (offset % 8), + _ => { + return Err(WireError::invalid( + "bitvector literal", + format!("invalid binary literal {original}"), + )) + } + } + } + } + 16 => { + for (nibble, ch) in digits.chars().rev().enumerate() { + let value = ch.to_digit(16).ok_or_else(|| { + WireError::invalid( + "bitvector literal", + format!("invalid hex literal {original}"), + ) + })? as u8; + let bit = nibble * 4; + bytes[bit / 8] |= value << (bit % 8); + } + } + _ => unreachable!("only binary and hex literals are passed"), + } + Ok(bytes) +} + +fn parse_list_expr( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, +) -> smt_wire::Result { + if items.is_empty() { + return Err(WireError::invalid("SMT-LIB expression", "empty list")); + } + if let SExpr::List(op_items) = &items[0] { + return parse_indexed_op(state, op_items, &items[1..], locals); + } + if atom(&items[0])? == "_" { + return parse_indexed_literal(state, items); + } + let op = atom(&items[0])?; + match op { + "!" => parse_expr_with_locals(state, &items[1], locals), + "let" => parse_let(state, items, locals), + "ite" => { + expect_len(items, 4, "ite")?; + let c = parse_expr_with_locals(state, &items[1], locals)?; + let t = parse_expr_with_locals(state, &items[2], locals)?; + let e = parse_expr_with_locals(state, &items[3], locals)?; + expect_sort(c.sort, SmtSort::Bool, "ite condition")?; + match (t.sort, e.sort) { + (SmtSort::Bool, SmtSort::Bool) => Ok(Binding { + node: state.builder.bool_ite(c.node, t.node, e.node)?, + sort: SmtSort::Bool, + }), + (SmtSort::Bv(w1), SmtSort::Bv(w2)) if w1 == w2 => Ok(Binding { + node: state.builder.bv_ite(c.node, t.node, e.node)?, + sort: SmtSort::Bv(w1), + }), + _ => Err(WireError::invalid("ite", "branch sorts differ")), + } + } + "=" => parse_equals(state, &items[1..], locals), + "not" => unary_bool(state, items, locals, |b, x| b.bool_not(x)), + "and" => fold_bool(state, &items[1..], locals, true, |b, a, c| b.bool_and(a, c)), + "or" => fold_bool(state, &items[1..], locals, false, |b, a, c| b.bool_or(a, c)), + "=>" => binary_bool(state, items, locals, |b, a, c| b.bool_implies(a, c)), + "bvnot" => unary_bv(state, items, locals, |b, x| b.bv_not(x)), + "bvneg" => unary_bv(state, items, locals, |b, x| b.bv_neg(x)), + "bvand" => binary_bv(state, items, locals, |b, a, c| b.bv_and(a, c)), + "bvor" => binary_bv(state, items, locals, |b, a, c| b.bv_or(a, c)), + "bvxor" => binary_bv(state, items, locals, |b, a, c| b.bv_xor(a, c)), + "bvadd" => binary_bv(state, items, locals, |b, a, c| b.bv_add(a, c)), + "bvsub" => binary_bv(state, items, locals, |b, a, c| b.bv_sub(a, c)), + "bvmul" => binary_bv(state, items, locals, |b, a, c| b.bv_mul(a, c)), + "bvudiv" => binary_bv(state, items, locals, |b, a, c| b.bv_udiv(a, c)), + "bvurem" => binary_bv(state, items, locals, |b, a, c| b.bv_urem(a, c)), + "bvsdiv" => binary_bv(state, items, locals, |b, a, c| b.bv_sdiv(a, c)), + "bvsrem" => binary_bv(state, items, locals, |b, a, c| b.bv_srem(a, c)), + "bvsmod" => binary_bv(state, items, locals, |b, a, c| b.bv_smod(a, c)), + "bvshl" => binary_bv(state, items, locals, |b, a, c| b.bv_shl(a, c)), + "bvlshr" => binary_bv(state, items, locals, |b, a, c| b.bv_lshr(a, c)), + "bvashr" => binary_bv(state, items, locals, |b, a, c| b.bv_ashr(a, c)), + "concat" => binary_bv(state, items, locals, |b, a, c| b.bv_concat(a, c)), + "bvult" => bv_cmp(state, items, locals, |b, a, c| b.bv_ult(a, c)), + "bvule" => bv_cmp(state, items, locals, |b, a, c| b.bv_ule(a, c)), + "bvugt" => bv_cmp(state, items, locals, |b, a, c| b.bv_ugt(a, c)), + "bvuge" => bv_cmp(state, items, locals, |b, a, c| b.bv_uge(a, c)), + "bvslt" => bv_cmp(state, items, locals, |b, a, c| b.bv_slt(a, c)), + "bvsle" => bv_cmp(state, items, locals, |b, a, c| b.bv_sle(a, c)), + "bvsgt" => bv_cmp(state, items, locals, |b, a, c| b.bv_sgt(a, c)), + "bvsge" => bv_cmp(state, items, locals, |b, a, c| b.bv_sge(a, c)), + other => Err(WireError::invalid( + "SMT-LIB expression", + format!("unsupported operator {other}"), + )), + } +} + +fn parse_indexed_literal(state: &mut ScriptState, items: &[SExpr]) -> smt_wire::Result { + if items.len() == 4 && atom(&items[1])? == "bv" { + let value = atom(&items[2])? + .parse::() + .map_err(|_| WireError::invalid("bv literal", "invalid value"))?; + let width = atom(&items[3])? + .parse::() + .map_err(|_| WireError::invalid("bv literal", "invalid width"))?; + return Ok(Binding { + node: state.builder.bv_const(value, width)?, + sort: SmtSort::Bv(width), + }); + } + Err(WireError::invalid("indexed literal", "expected (_ bvN W)")) +} + +fn parse_indexed_op( + state: &mut ScriptState, + op_items: &[SExpr], + args: &[SExpr], + locals: &mut HashMap, +) -> smt_wire::Result { + if op_items.len() == 4 && atom(&op_items[0])? == "_" && atom(&op_items[1])? == "extract" { + if args.len() != 1 { + return Err(WireError::invalid("extract", "expected one argument")); + } + let hi = atom(&op_items[2])? + .parse::() + .map_err(|_| WireError::invalid("extract", "bad high index"))?; + let lo = atom(&op_items[3])? + .parse::() + .map_err(|_| WireError::invalid("extract", "bad low index"))?; + let x = parse_expr_with_locals(state, &args[0], locals)?; + let SmtSort::Bv(_) = x.sort else { + return Err(WireError::invalid("extract", "argument is not BV")); + }; + return Ok(Binding { + node: state.builder.bv_extract(x.node, hi, lo)?, + sort: SmtSort::Bv(hi - lo + 1), + }); + } + if op_items.len() == 3 && atom(&op_items[0])? == "_" { + let amount = atom(&op_items[2])? + .parse::() + .map_err(|_| WireError::invalid("extension", "bad amount"))?; + let x = parse_expr_with_locals(state, &args[0], locals)?; + let SmtSort::Bv(width) = x.sort else { + return Err(WireError::invalid("extension", "argument is not BV")); + }; + return match atom(&op_items[1])? { + "zero_extend" => Ok(Binding { + node: state.builder.bv_zext(x.node, amount)?, + sort: SmtSort::Bv(width + u32::from(amount)), + }), + "sign_extend" => Ok(Binding { + node: state.builder.bv_sext(x.node, amount)?, + sort: SmtSort::Bv(width + u32::from(amount)), + }), + _ => Err(WireError::invalid( + "indexed operator", + "unsupported indexed operator", + )), + }; + } + Err(WireError::invalid( + "indexed operator", + "unsupported indexed operator", + )) +} + +fn parse_let( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, +) -> smt_wire::Result { + expect_len(items, 3, "let")?; + let bindings = match &items[1] { + SExpr::List(items) => items, + _ => return Err(WireError::invalid("let", "expected binding list")), + }; + let mut nested = locals.clone(); + for binding in bindings { + let pair = match binding { + SExpr::List(pair) if pair.len() == 2 => pair, + _ => return Err(WireError::invalid("let", "bad binding")), + }; + let name = atom(&pair[0])?.to_owned(); + let value = parse_expr_with_locals(state, &pair[1], &mut nested)?; + nested.insert(name, value); + } + parse_expr_with_locals(state, &items[2], &mut nested) +} + +fn parse_equals( + state: &mut ScriptState, + args: &[SExpr], + locals: &mut HashMap, +) -> smt_wire::Result { + if args.len() != 2 { + return Err(WireError::invalid("=", "expected two arguments")); + } + let a = parse_expr_with_locals(state, &args[0], locals)?; + let b = parse_expr_with_locals(state, &args[1], locals)?; + match (a.sort, b.sort) { + (SmtSort::Bool, SmtSort::Bool) => Ok(Binding { + node: state.builder.bool_eq(a.node, b.node)?, + sort: SmtSort::Bool, + }), + (SmtSort::Bv(w1), SmtSort::Bv(w2)) if w1 == w2 => Ok(Binding { + node: state.builder.bv_eq(a.node, b.node)?, + sort: SmtSort::Bool, + }), + _ => Err(WireError::invalid("=", "argument sorts differ")), + } +} + +fn unary_bool( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut ExprBuilder, NodeRef) -> smt_wire::Result, +) -> smt_wire::Result { + expect_len(items, 2, "unary bool")?; + let x = parse_expr_with_locals(state, &items[1], locals)?; + expect_sort(x.sort, SmtSort::Bool, "unary bool")?; + Ok(Binding { + node: f(&mut state.builder, x.node)?, + sort: SmtSort::Bool, + }) +} + +fn binary_bool( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut ExprBuilder, NodeRef, NodeRef) -> smt_wire::Result, +) -> smt_wire::Result { + expect_len(items, 3, "binary bool")?; + let a = parse_expr_with_locals(state, &items[1], locals)?; + let b = parse_expr_with_locals(state, &items[2], locals)?; + expect_sort(a.sort, SmtSort::Bool, "binary bool")?; + expect_sort(b.sort, SmtSort::Bool, "binary bool")?; + Ok(Binding { + node: f(&mut state.builder, a.node, b.node)?, + sort: SmtSort::Bool, + }) +} + +fn fold_bool( + state: &mut ScriptState, + args: &[SExpr], + locals: &mut HashMap, + identity: bool, + f: fn(&mut ExprBuilder, NodeRef, NodeRef) -> smt_wire::Result, +) -> smt_wire::Result { + if args.is_empty() { + let node = if identity { + state.builder.bool_true()? + } else { + state.builder.bool_false()? + }; + return Ok(Binding { + node, + sort: SmtSort::Bool, + }); + } + let mut cur = parse_expr_with_locals(state, &args[0], locals)?; + expect_sort(cur.sort, SmtSort::Bool, "fold bool")?; + for arg in &args[1..] { + let next = parse_expr_with_locals(state, arg, locals)?; + expect_sort(next.sort, SmtSort::Bool, "fold bool")?; + cur = Binding { + node: f(&mut state.builder, cur.node, next.node)?, + sort: SmtSort::Bool, + }; + } + Ok(cur) +} + +fn unary_bv( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut ExprBuilder, NodeRef) -> smt_wire::Result, +) -> smt_wire::Result { + expect_len(items, 2, "unary BV")?; + let x = parse_expr_with_locals(state, &items[1], locals)?; + let SmtSort::Bv(width) = x.sort else { + return Err(WireError::invalid("unary BV", "argument is not BV")); + }; + Ok(Binding { + node: f(&mut state.builder, x.node)?, + sort: SmtSort::Bv(width), + }) +} + +fn binary_bv( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut ExprBuilder, NodeRef, NodeRef) -> smt_wire::Result, +) -> smt_wire::Result { + expect_len(items, 3, "binary BV")?; + let a = parse_expr_with_locals(state, &items[1], locals)?; + let b = parse_expr_with_locals(state, &items[2], locals)?; + let (SmtSort::Bv(w1), SmtSort::Bv(w2)) = (a.sort, b.sort) else { + return Err(WireError::invalid("binary BV", "argument is not BV")); + }; + if w1 != w2 { + return Err(WireError::invalid("binary BV", "width mismatch")); + } + Ok(Binding { + node: f(&mut state.builder, a.node, b.node)?, + sort: SmtSort::Bv(w1), + }) +} + +fn bv_cmp( + state: &mut ScriptState, + items: &[SExpr], + locals: &mut HashMap, + f: fn(&mut ExprBuilder, NodeRef, NodeRef) -> smt_wire::Result, +) -> smt_wire::Result { + let value = binary_bv(state, items, locals, f)?; + Ok(Binding { + node: value.node, + sort: SmtSort::Bool, + }) +} + +fn parse_sort(expr: &SExpr) -> smt_wire::Result { + match expr { + SExpr::Atom(atom) if atom == "Bool" => Ok(SmtSort::Bool), + SExpr::List(items) + if items.len() == 3 && atom(&items[0])? == "_" && atom(&items[1])? == "BitVec" => + { + let width = atom(&items[2])? + .parse::() + .map_err(|_| WireError::invalid("sort", "invalid BitVec width"))?; + Ok(SmtSort::Bv(width)) + } + _ => Err(WireError::invalid("sort", "expected Bool or (_ BitVec n)")), + } +} + +fn text_response(query: &TextQuery, result: QueryResult) -> String { + match result.status { + QueryStatus::Sat => { + let mut out = "sat\n".to_owned(); + if query.want_model { + if let Some(model) = result.model { + if query.get_values.is_empty() { + out.push_str( + &format_model(&query.request, &model) + .unwrap_or_else(|err| format!("; model formatting error: {err}\n")), + ); + } else { + out.push_str( + &format_get_values(&query.request, &model, &query.get_values) + .unwrap_or_else(|err| { + format!("; get-value formatting error: {err}\n") + }), + ); + } + } + } + out + } + QueryStatus::Unsat => { + let mut out = "unsat\n".to_owned(); + if query.want_core { + if let Some(core) = result.core { + out.push_str(&format_core(&core)); + } + } + out + } + QueryStatus::Unknown => "unknown\n".to_owned(), + QueryStatus::Ok => "success\n".to_owned(), + } +} + +fn format_model(request: &BinaryRequest, model: &ModelBlock) -> smt_wire::Result { + let expr = request.expression_view()?; + let mut out = "(model\n".to_owned(); + for entry in &model.entries { + let node = expr.node(entry.node_ref.index())?; + let name = expr.blob_str( + smt_wire::BlobRef::from_payload(node.payload), + "model variable", + )?; + out.push_str(" (define-fun "); + out.push_str("e_symbol(name)); + out.push_str(" () "); + match entry.value.width { + 0 => { + out.push_str("Bool "); + out.push_str(if entry.value.bytes[0] == 0 { + "false" + } else { + "true" + }); + } + width => { + out.push_str(&format!("(_ BitVec {width}) ")); + out.push_str(&scalar_to_bv_literal(&entry.value)); + } + } + out.push_str(")\n"); + } + out.push_str(")\n"); + Ok(out) +} + +fn format_get_values( + request: &BinaryRequest, + model: &ModelBlock, + names: &[String], +) -> smt_wire::Result { + let expr = request.expression_view()?; + let mut values = HashMap::new(); + for entry in &model.entries { + let node = expr.node(entry.node_ref.index())?; + let name = expr.blob_str( + smt_wire::BlobRef::from_payload(node.payload), + "model variable", + )?; + values.insert(name.to_owned(), scalar_to_smt_value(&entry.value)); + } + let mut out = "(".to_owned(); + let mut emitted = 0usize; + for name in names { + if let Some(value) = values.get(name) { + if emitted > 0 { + out.push(' '); + } + emitted += 1; + out.push('('); + out.push_str("e_symbol(name)); + out.push(' '); + out.push_str(value); + out.push(')'); + } + } + out.push_str(")\n"); + Ok(out) +} + +fn format_core(core: &UnsatCoreBlock) -> String { + let mut out = "(".to_owned(); + for (index, name) in core.names.iter().enumerate() { + if index > 0 { + out.push(' '); + } + out.push_str("e_symbol(name)); + } + out.push_str(")\n"); + out +} + +fn scalar_to_smt_value(value: &ScalarValue) -> String { + if value.width == 0 { + if value.bytes[0] == 0 { + "false".to_owned() + } else { + "true".to_owned() + } + } else { + scalar_to_bv_literal(value) + } +} + +fn scalar_to_bv_literal(value: &ScalarValue) -> String { + let mut out = String::with_capacity(value.width as usize + 2); + out.push_str("#b"); + for bit in (0..value.width).rev() { + let byte = value.bytes[(bit / 8) as usize]; + out.push(if ((byte >> (bit % 8)) & 1) != 0 { + '1' + } else { + '0' + }); + } + out +} + +fn expect_len(items: &[SExpr], len: usize, context: &'static str) -> smt_wire::Result<()> { + if items.len() != len { + return Err(WireError::invalid(context, format!("expected {len} items"))); + } + Ok(()) +} + +fn expect_sort(actual: SmtSort, expected: SmtSort, context: &'static str) -> smt_wire::Result<()> { + if actual != expected { + return Err(WireError::invalid( + context, + format!("expected {expected:?}, got {actual:?}"), + )); + } + Ok(()) +} + +fn atom(expr: &SExpr) -> smt_wire::Result<&str> { + match expr { + SExpr::Atom(atom) => Ok(atom), + SExpr::List(_) => Err(WireError::invalid("SMT-LIB atom", "expected atom")), + } +} + +fn parse_sexprs(input: &str) -> smt_wire::Result> { + let mut action = SExprAction; + yaspar::smtlib2::ScriptParser::new() + .parse(&mut action, yaspar::tokenize_str(input, true)) + .map_err(|err| WireError::invalid("SMT-LIB parser", err.to_string())) +} + +struct SExprAction; + +fn sexpr_atom(value: impl Into) -> SExpr { + SExpr::Atom(value.into()) +} + +fn sexpr_list(items: impl IntoIterator) -> SExpr { + SExpr::List(items.into_iter().collect()) +} + +fn indexed_symbol(symbol: String, indices: Vec) -> SExpr { + if indices.is_empty() { + sexpr_atom(symbol) + } else { + let mut items = Vec::with_capacity(indices.len() + 2); + items.push(sexpr_atom("_")); + items.push(sexpr_atom(symbol)); + items.extend(indices); + sexpr_list(items) + } +} + +fn byte_bits(bytes: &[u8], len: usize) -> String { + let mut out = String::with_capacity(len + 2); + out.push_str("#b"); + for bit in (0..len).rev() { + let byte = bytes[bit / 8]; + out.push(if ((byte >> (bit % 8)) & 1) != 0 { + '1' + } else { + '0' + }); + } + out +} + +fn byte_hex(bytes: &[u8], len: usize) -> String { + let mut out = String::with_capacity(len + 2); + out.push_str("#x"); + for nibble in (0..len).rev() { + let byte = bytes[nibble / 2]; + let value = (byte >> ((nibble % 2) * 4)) & 0xf; + out.push(char::from_digit(u32::from(value), 16).expect("hex digit")); + } + out +} + +fn command(name: &str, args: impl IntoIterator) -> SExpr { + let mut items = vec![sexpr_atom(name)]; + items.extend(args); + sexpr_list(items) +} + +fn vars_to_sexpr(vars: Vec<(String, SExpr)>) -> SExpr { + sexpr_list( + vars.into_iter() + .map(|(name, sort)| sexpr_list([sexpr_atom(name), sort])), + ) +} + +impl ActionOnString for SExprAction { + type Str = String; + + fn on_string(&mut self, _range: Range, s: String) -> ParsingResult { + Ok(s) + } +} + +impl ActionOnConstant for SExprAction { + type Constant = SExpr; + + fn on_constant_binary( + &mut self, + _range: Range, + bytes: Vec, + len: usize, + ) -> ParsingResult { + Ok(sexpr_atom(byte_bits(&bytes, len))) + } + + fn on_constant_hexadecimal( + &mut self, + _range: Range, + bytes: Vec, + len: usize, + ) -> ParsingResult { + Ok(sexpr_atom(byte_hex(&bytes, len))) + } + + fn on_constant_decimal( + &mut self, + _range: Range, + decimal: DBig, + ) -> ParsingResult { + Ok(sexpr_atom(decimal.to_string())) + } + + fn on_constant_numeral( + &mut self, + _range: Range, + numeral: UBig, + ) -> ParsingResult { + Ok(sexpr_atom(numeral.to_string())) + } + + fn on_constant_string( + &mut self, + _range: Range, + string: Self::Str, + ) -> ParsingResult { + Ok(sexpr_atom(string)) + } + + fn on_constant_bool(&mut self, _range: Range, boolean: bool) -> ParsingResult { + Ok(sexpr_atom(if boolean { "true" } else { "false" })) + } +} + +impl ActionOnIndex for SExprAction { + type Index = SExpr; + + fn on_index_numeral(&mut self, _range: Range, index: UBig) -> ParsingResult { + Ok(sexpr_atom(index.to_string())) + } + + fn on_index_symbol(&mut self, _range: Range, index: Self::Str) -> ParsingResult { + Ok(sexpr_atom(index)) + } + + fn on_index_hexadecimal( + &mut self, + _range: Range, + bytes: Vec, + len: usize, + ) -> ParsingResult { + Ok(sexpr_atom(byte_hex(&bytes, len))) + } +} + +impl ActionOnIdentifier for SExprAction { + type Identifier = SExpr; + + fn on_identifier( + &mut self, + _range: Range, + symbol: Self::Str, + indices: Vec, + ) -> ParsingResult { + Ok(indexed_symbol(symbol, indices)) + } +} + +impl ActionOnAttribute for SExprAction { + type Term = SExpr; + type Attribute = SExpr; + + fn on_attribute_keyword( + &mut self, + _range: Range, + keyword: Keyword, + ) -> ParsingResult { + Ok(sexpr_list([sexpr_atom(keyword.to_string())])) + } + + fn on_attribute_constant( + &mut self, + _range: Range, + keyword: Keyword, + constant: Self::Constant, + ) -> ParsingResult { + Ok(sexpr_list([sexpr_atom(keyword.to_string()), constant])) + } + + fn on_attribute_symbol( + &mut self, + _range: Range, + keyword: Keyword, + symbol: Self::Str, + ) -> ParsingResult { + Ok(sexpr_list([ + sexpr_atom(keyword.to_string()), + sexpr_atom(symbol), + ])) + } + + fn on_attribute_named( + &mut self, + _range: Range, + name: Self::Str, + ) -> ParsingResult { + Ok(sexpr_list([sexpr_atom(":named"), sexpr_atom(name)])) + } + + fn on_attribute_pattern( + &mut self, + _range: Range, + patterns: Vec, + ) -> ParsingResult { + Ok(command(":pattern", patterns)) + } +} + +impl ActionOnSort for SExprAction { + type Sort = SExpr; + + fn on_sort( + &mut self, + _range: Range, + identifier: Self::Identifier, + args: Vec, + ) -> ParsingResult { + if args.is_empty() { + Ok(identifier) + } else { + let mut items = vec![identifier]; + items.extend(args); + Ok(sexpr_list(items)) + } + } +} + +impl ActionOnTerm for SExprAction { + fn on_term_constant( + &mut self, + _range: Range, + constant: Self::Constant, + ) -> ParsingResult { + Ok(constant) + } + + fn on_term_identifier( + &mut self, + _range: Range, + identifier: Self::Identifier, + sort: Option, + ) -> ParsingResult { + Ok(match sort { + Some(sort) => command("as", [identifier, sort]), + None => identifier, + }) + } + + fn on_term_app( + &mut self, + _range: Range, + identifier: Self::Identifier, + sort: Option, + args: Vec, + ) -> ParsingResult { + let head = match sort { + Some(sort) => command("as", [identifier, sort]), + None => identifier, + }; + let mut items = vec![head]; + items.extend(args); + Ok(sexpr_list(items)) + } + + fn on_term_let( + &mut self, + _range: Range, + bindings: Vec<(Self::Str, Self::Term)>, + body: Self::Term, + ) -> ParsingResult { + let binding_list = sexpr_list( + bindings + .into_iter() + .map(|(name, term)| sexpr_list([sexpr_atom(name), term])), + ); + Ok(command("let", [binding_list, body])) + } + + fn on_term_lambda( + &mut self, + _range: Range, + names: Vec<(Self::Str, Self::Sort)>, + body: Self::Term, + ) -> ParsingResult { + Ok(command("lambda", [vars_to_sexpr(names), body])) + } + + fn on_term_exists( + &mut self, + _range: Range, + names: Vec<(Self::Str, Self::Sort)>, + body: Self::Term, + ) -> ParsingResult { + Ok(command("exists", [vars_to_sexpr(names), body])) + } + + fn on_term_forall( + &mut self, + _range: Range, + names: Vec<(Self::Str, Self::Sort)>, + body: Self::Term, + ) -> ParsingResult { + Ok(command("forall", [vars_to_sexpr(names), body])) + } + + fn on_term_match( + &mut self, + _range: Range, + scrutinee: Self::Term, + cases: Vec<(Pattern, Self::Term)>, + ) -> ParsingResult { + let case_exprs = cases.into_iter().map(|(_, body)| body); + Ok(command("match", [scrutinee, sexpr_list(case_exprs)])) + } + + fn on_term_annotated( + &mut self, + _range: Range, + t: Self::Term, + attributes: Vec, + ) -> ParsingResult { + let mut items = vec![sexpr_atom("!"), t]; + for attribute in attributes { + match attribute { + SExpr::List(values) => items.extend(values), + atom @ SExpr::Atom(_) => items.push(atom), + } + } + Ok(sexpr_list(items)) + } +} + +impl ParsingAction for SExprAction { + type Command = SExpr; + + fn on_command_assert(&mut self, _range: Range, t: Self::Term) -> ParsingResult { + Ok(command("assert", [t])) + } + + fn on_command_check_sat(&mut self, _range: Range) -> ParsingResult { + Ok(command("check-sat", [])) + } + + fn on_command_check_sat_assuming( + &mut self, + _range: Range, + terms: Vec, + ) -> ParsingResult { + Ok(command("check-sat-assuming", [sexpr_list(terms)])) + } + + fn on_command_declare_const( + &mut self, + _range: Range, + name: Self::Str, + sort: Self::Sort, + ) -> ParsingResult { + Ok(command("declare-const", [sexpr_atom(name), sort])) + } + + fn on_command_declare_datatype( + &mut self, + _range: Range, + name: Self::Str, + _datatype: DatatypeDec, + ) -> ParsingResult { + Ok(command("declare-datatype", [sexpr_atom(name)])) + } + + fn on_command_declare_datatypes( + &mut self, + _range: Range, + _defs: Vec>, + ) -> ParsingResult { + Ok(command("declare-datatypes", [])) + } + + fn on_command_declare_fun( + &mut self, + _range: Range, + name: Self::Str, + input_sorts: Vec, + out_sort: Self::Sort, + ) -> ParsingResult { + Ok(command( + "declare-fun", + [sexpr_atom(name), sexpr_list(input_sorts), out_sort], + )) + } + + fn on_command_declare_sort( + &mut self, + _range: Range, + name: Self::Str, + arity: UBig, + ) -> ParsingResult { + Ok(command( + "declare-sort", + [sexpr_atom(name), sexpr_atom(arity.to_string())], + )) + } + + fn on_command_declare_sort_parameter( + &mut self, + _range: Range, + name: Self::Str, + ) -> ParsingResult { + Ok(command("declare-sort-parameter", [sexpr_atom(name)])) + } + + fn on_command_define_const( + &mut self, + _range: Range, + name: Self::Str, + sort: Self::Sort, + term: Self::Term, + ) -> ParsingResult { + Ok(command("define-const", [sexpr_atom(name), sort, term])) + } + + fn on_command_define_fun( + &mut self, + _range: Range, + definition: FunctionDef, + ) -> ParsingResult { + Ok(command( + "define-fun", + [ + sexpr_atom(definition.name), + vars_to_sexpr(definition.vars), + definition.out_sort, + definition.body, + ], + )) + } + + fn on_command_define_fun_rec( + &mut self, + _range: Range, + definition: FunctionDef, + ) -> ParsingResult { + Ok(command( + "define-fun-rec", + [ + sexpr_atom(definition.name), + vars_to_sexpr(definition.vars), + definition.out_sort, + definition.body, + ], + )) + } + + fn on_command_define_funs_rec( + &mut self, + _range: Range, + _definitions: Vec>, + ) -> ParsingResult { + Ok(command("define-funs-rec", [])) + } + + fn on_command_define_sort( + &mut self, + _range: Range, + name: Self::Str, + params: Vec, + sort: Self::Sort, + ) -> ParsingResult { + Ok(command( + "define-sort", + [ + sexpr_atom(name), + sexpr_list(params.into_iter().map(sexpr_atom)), + sort, + ], + )) + } + + fn on_command_echo(&mut self, _range: Range, s: Self::Str) -> ParsingResult { + Ok(command("echo", [sexpr_atom(s)])) + } + + fn on_command_exit(&mut self, _range: Range) -> ParsingResult { + Ok(command("exit", [])) + } + + fn on_command_get_assertions(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-assertions", [])) + } + + fn on_command_get_assignment(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-assignment", [])) + } + + fn on_command_get_info(&mut self, _range: Range, kw: Keyword) -> ParsingResult { + Ok(command("get-info", [sexpr_atom(kw.to_string())])) + } + + fn on_command_get_model(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-model", [])) + } + + fn on_command_get_option( + &mut self, + _range: Range, + kw: Keyword, + ) -> ParsingResult { + Ok(command("get-option", [sexpr_atom(kw.to_string())])) + } + + fn on_command_get_proof(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-proof", [])) + } + + fn on_command_get_unsat_assumptions(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-unsat-assumptions", [])) + } + + fn on_command_get_unsat_core(&mut self, _range: Range) -> ParsingResult { + Ok(command("get-unsat-core", [])) + } + + fn on_command_get_value( + &mut self, + _range: Range, + ts: Vec, + ) -> ParsingResult { + Ok(command("get-value", [sexpr_list(ts)])) + } + + fn on_command_pop(&mut self, _range: Range, lvl: UBig) -> ParsingResult { + Ok(command("pop", [sexpr_atom(lvl.to_string())])) + } + + fn on_command_push(&mut self, _range: Range, lvl: UBig) -> ParsingResult { + Ok(command("push", [sexpr_atom(lvl.to_string())])) + } + + fn on_command_reset(&mut self, _range: Range) -> ParsingResult { + Ok(command("reset", [])) + } + + fn on_command_reset_assertions(&mut self, _range: Range) -> ParsingResult { + Ok(command("reset-assertions", [])) + } + + fn on_command_set_info( + &mut self, + _range: Range, + attributes: Self::Attribute, + ) -> ParsingResult { + Ok(command("set-info", [attributes])) + } + + fn on_command_set_logic( + &mut self, + _range: Range, + logic: Self::Str, + ) -> ParsingResult { + Ok(command("set-logic", [sexpr_atom(logic)])) + } + + fn on_command_set_option( + &mut self, + _range: Range, + attribute: Self::Attribute, + ) -> ParsingResult { + Ok(command("set-option", [attribute])) + } +} + +``` + +`crates/smt-server/src/z3_backend.rs`: + +```rs +use smt_wire::{ + request_flags, tag, BinaryRequest, BlobRef, Command, ModelBlock, ModelEntry, NodeRef, + OptimizationValueBlock, ScalarValue, SimplifyBlock, Sort, UnsatCoreBlock, WireError, +}; +use z3::{ + ast::{Bool, BV}, + Config, Model, SatResult, Solver, +}; + +use crate::backend::{Backend, QueryResult}; + +#[derive(Debug, Clone, Default)] +pub struct Z3Backend; + +impl Backend for Z3Backend { + fn name(&self) -> &'static str { + "z3" + } + + fn handle(&self, request: &BinaryRequest) -> smt_wire::Result { + match request.envelope.command { + Command::Simplify => Ok(QueryResult::ok_simplify(SimplifyBlock { + expression: request.expression.clone(), + assertion_roots: request.assertion_roots.clone(), + named_assertion_refs: request.named_assertion_refs.clone(), + assumption_roots: request.assumption_roots.clone(), + })), + Command::Solve | Command::Minimize | Command::Maximize => { + let mut cfg = Config::new(); + cfg.set_model_generation(true); + if request.envelope.budget_ms != 0 { + cfg.set_timeout_msec(u64::from(request.envelope.budget_ms)); + } + z3::with_z3_config(&cfg, || match request.envelope.command { + Command::Solve => solve(request), + Command::Minimize | Command::Maximize => optimize(request), + Command::Simplify => unreachable!("handled above"), + }) + } + } + } +} + +#[derive(Clone)] +struct Z3Variable { + node_ref: NodeRef, + sort: Sort, + width: u32, +} + +struct Z3Translation { + bvs: Vec>, + bools: Vec>, + variables: Vec, +} + +impl Z3Translation { + fn bv(&self, reference: NodeRef) -> smt_wire::Result { + if !reference.is_bv() { + return Err(WireError::invalid( + "Z3 translation", + "expected BV reference", + )); + } + self.bvs + .get(reference.index() as usize) + .and_then(Clone::clone) + .ok_or_else(|| WireError::invalid("Z3 translation", "missing BV term")) + } + + fn bool(&self, reference: NodeRef) -> smt_wire::Result { + if !reference.is_bool() { + return Err(WireError::invalid( + "Z3 translation", + "expected Bool reference", + )); + } + self.bools + .get(reference.index() as usize) + .and_then(Clone::clone) + .ok_or_else(|| WireError::invalid("Z3 translation", "missing Bool term")) + } +} + +fn translate(request: &BinaryRequest) -> smt_wire::Result { + let expr = request.expression_view()?; + let mut out = Z3Translation { + bvs: vec![None; expr.node_count() as usize], + bools: vec![None; expr.node_count() as usize], + variables: Vec::new(), + }; + + for index in 0..expr.node_count() { + let node = expr.node(index)?; + match node.tag { + tag::BV_VAR => { + let name = format!("bv_{index}"); + let term = BV::new_const(name, node.width); + out.variables.push(Z3Variable { + node_ref: NodeRef::bv(index)?, + sort: Sort::Bv, + width: node.width, + }); + out.bvs[index as usize] = Some(term); + } + tag::BV_CONST => { + out.bvs[index as usize] = Some(z3_bv_const(&expr, node.width, node.payload)?); + } + tag::BV_NOT => { + let x = child_bv(&expr, &out, &node, 0)?; + out.bvs[index as usize] = Some(x.bvnot()); + } + tag::BV_NEG => { + let x = child_bv(&expr, &out, &node, 0)?; + out.bvs[index as usize] = Some(x.bvneg()); + } + tag::BV_AND => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvand(&b)); + } + tag::BV_OR => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvor(&b)); + } + tag::BV_XOR => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvxor(&b)); + } + tag::BV_ADD => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvadd(&b)); + } + tag::BV_SUB => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvsub(&b)); + } + tag::BV_MUL => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvmul(&b)); + } + tag::BV_UDIV => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvudiv(&b)); + } + tag::BV_UREM => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvurem(&b)); + } + tag::BV_SDIV => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvsdiv(&b)); + } + tag::BV_SREM => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvsrem(&b)); + } + tag::BV_SMOD => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvsmod(&b)); + } + tag::BV_SHL => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvshl(&b)); + } + tag::BV_LSHR => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvlshr(&b)); + } + tag::BV_ASHR => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.bvashr(&b)); + } + tag::BV_EXTRACT => { + let x = child_bv(&expr, &out, &node, 0)?; + out.bvs[index as usize] = Some(x.extract(u32::from(node.aux_hi), node.aux_lo)); + } + tag::BV_CONCAT => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bvs[index as usize] = Some(a.concat(&b)); + } + tag::BV_ZEXT => { + let x = child_bv(&expr, &out, &node, 0)?; + out.bvs[index as usize] = Some(x.zero_ext(u32::from(node.aux_hi))); + } + tag::BV_SEXT => { + let x = child_bv(&expr, &out, &node, 0)?; + out.bvs[index as usize] = Some(x.sign_ext(u32::from(node.aux_hi))); + } + tag::BV_ITE => { + let c = child_bool(&expr, &out, &node, 0)?; + let t = child_bv(&expr, &out, &node, 1)?; + let e = child_bv(&expr, &out, &node, 2)?; + out.bvs[index as usize] = Some(c.ite(&t, &e)); + } + tag::BV_SELECT => { + let pairs = u32::from(node.aux_hi); + let mut result = child_bv(&expr, &out, &node, pairs * 2)?; + for pair in (0..pairs).rev() { + let selector = child_bool(&expr, &out, &node, pair * 2)?; + let value = child_bv(&expr, &out, &node, pair * 2 + 1)?; + result = selector.ite(&value, &result); + } + out.bvs[index as usize] = Some(result); + } + tag::BOOL_TRUE => out.bools[index as usize] = Some(Bool::from_bool(true)), + tag::BOOL_FALSE => out.bools[index as usize] = Some(Bool::from_bool(false)), + tag::BOOL_VAR => { + let name = format!("bool_{index}"); + let term = Bool::new_const(name); + out.variables.push(Z3Variable { + node_ref: NodeRef::bool(index)?, + sort: Sort::Bool, + width: 0, + }); + out.bools[index as usize] = Some(term); + } + tag::BOOL_NOT => { + let x = child_bool(&expr, &out, &node, 0)?; + out.bools[index as usize] = Some(x.not()); + } + tag::BOOL_AND => { + let (a, b) = child_bool2(&expr, &out, &node)?; + out.bools[index as usize] = Some(Bool::and(&[&a, &b])); + } + tag::BOOL_OR => { + let (a, b) = child_bool2(&expr, &out, &node)?; + out.bools[index as usize] = Some(Bool::or(&[&a, &b])); + } + tag::BOOL_IMPLIES => { + let (a, b) = child_bool2(&expr, &out, &node)?; + out.bools[index as usize] = Some(a.implies(&b)); + } + tag::BV_EQ => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(a.eq(&b)); + } + tag::BV_ULT => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(a.bvult(&b)); + } + tag::BV_ULE => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(a.bvule(&b)); + } + tag::BV_SLT => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(a.bvslt(&b)); + } + tag::BV_SLE => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(a.bvsle(&b)); + } + tag::UADD_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + let sum = a.bvadd(&b); + out.bools[index as usize] = Some(sum.bvult(&a)); + } + tag::USUB_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(a.bvult(&b)); + } + tag::UMUL_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(unsigned_mul_overflow(&a, &b)); + } + tag::SADD_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(signed_add_overflow(&a, &b)); + } + tag::SSUB_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(signed_sub_overflow(&a, &b)); + } + tag::SMUL_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + out.bools[index as usize] = Some(signed_mul_overflow(&a, &b)); + } + tag::NEG_OVF => { + let a = child_bv(&expr, &out, &node, 0)?; + out.bools[index as usize] = Some(a.eq(signed_min_bv(a.get_size()))); + } + tag::SDIV_OVF => { + let (a, b) = child_bv2(&expr, &out, &node)?; + let width = a.get_size(); + out.bools[index as usize] = Some(Bool::and(&[ + &a.eq(signed_min_bv(width)), + &b.eq(ones_bv(width)), + ])); + } + other => { + return Err(WireError::invalid( + "Z3 translation", + format!("unknown tag {other}"), + )) + } + } + } + Ok(out) +} + +fn solve(request: &BinaryRequest) -> smt_wire::Result { + let expr = request.expression_view()?; + let translation = translate(request)?; + let solver = Solver::new(); + let want_model = (request.envelope.flags & request_flags::WANT_MODEL) != 0; + let want_core = (request.envelope.flags & request_flags::WANT_CORE) != 0; + let mut trackers = Vec::<(String, Bool)>::new(); + + for (index, root) in request.assertion_roots.iter().enumerate() { + let assertion = translation.bool(*root)?; + if want_core && index < request.named_assertion_refs.len() { + let name = expr + .blob_str(request.named_assertion_refs[index], "named assertion")? + .to_owned(); + let tracker = Bool::new_const(format!("core_{index}")); + solver.assert_and_track(assertion, &tracker); + trackers.push((name, tracker)); + } else { + solver.assert(&assertion); + } + } + + let assumptions = request + .assumption_roots + .iter() + .map(|root| translation.bool(*root)) + .collect::>>()?; + + match solver.check_assumptions(&assumptions) { + SatResult::Sat => { + let model = if want_model { + let model = solver.get_model().ok_or_else(|| { + WireError::invalid("Z3 model", "solver returned SAT without a model") + })?; + Some(build_model(&translation, &model)?) + } else { + None + }; + Ok(QueryResult::sat(model)) + } + SatResult::Unsat => { + let core = if want_core { + Some(build_core(&solver, &trackers)) + } else { + None + }; + Ok(QueryResult::unsat(core)) + } + SatResult::Unknown => Ok(QueryResult::unknown( + solver + .get_reason_unknown() + .unwrap_or_else(|| "z3 returned unknown".to_owned()), + )), + } +} + +fn optimize(request: &BinaryRequest) -> smt_wire::Result { + let translation = translate(request)?; + let solver = Solver::new(); + for root in &request.assertion_roots { + solver.assert(&translation.bool(*root)?); + } + + let mut fixed = request + .assumption_roots + .iter() + .map(|root| translation.bool(*root)) + .collect::>>()?; + + match solver.check_assumptions(&fixed) { + SatResult::Sat => {} + SatResult::Unsat => return Ok(QueryResult::unsat(None)), + SatResult::Unknown => { + return Ok(QueryResult::unknown( + solver + .get_reason_unknown() + .unwrap_or_else(|| "z3 returned unknown".to_owned()), + )) + } + } + + let target_ref = request + .target_ref() + .ok_or_else(|| WireError::invalid("optimization", "missing target"))?; + let target = translation.bv(target_ref)?; + let width = target.get_size(); + let signed = (request.envelope.flags & request_flags::SIGNED) != 0; + let minimize = request.envelope.command == Command::Minimize; + let mut optimum = vec![0u8; (width as usize).div_ceil(8)]; + + for bit in (0..width).rev() { + let prefer_one = match (signed, minimize, bit == width - 1) { + (false, true, _) => false, + (false, false, _) => true, + (true, true, true) => true, + (true, true, false) => false, + (true, false, true) => false, + (true, false, false) => true, + }; + let bit_is_one = target.extract(bit, bit).eq(BV::from_u64(1, 1)); + let first_try = if prefer_one { + bit_is_one.clone() + } else { + bit_is_one.not() + }; + let mut assumptions = fixed.clone(); + assumptions.push(first_try.clone()); + match solver.check_assumptions(&assumptions) { + SatResult::Sat => { + fixed.push(first_try); + if prefer_one { + set_bit(&mut optimum, bit); + } + } + SatResult::Unsat => { + fixed.push(if prefer_one { + bit_is_one.not() + } else { + bit_is_one + }); + if !prefer_one { + set_bit(&mut optimum, bit); + } + } + SatResult::Unknown => { + return Ok(QueryResult::unknown( + solver + .get_reason_unknown() + .unwrap_or_else(|| "z3 returned unknown".to_owned()), + )) + } + } + } + + match solver.check_assumptions(&fixed) { + SatResult::Sat => {} + SatResult::Unsat => return Ok(QueryResult::unsat(None)), + SatResult::Unknown => return Ok(QueryResult::unknown("z3 returned unknown")), + } + + let want_model = (request.envelope.flags & request_flags::WANT_MODEL) != 0; + let model = if want_model { + let model = solver + .get_model() + .ok_or_else(|| WireError::invalid("Z3 model", "optimizer SAT without model"))?; + Some(build_model(&translation, &model)?) + } else { + None + }; + Ok(QueryResult::sat_optimization(OptimizationValueBlock { + optimum: ScalarValue::bv(width, optimum)?, + model, + })) +} + +fn child_bv( + expr: &smt_wire::ExprView<'_>, + translation: &Z3Translation, + node: &smt_wire::RawNode, + offset: u32, +) -> smt_wire::Result { + translation.bv(expr.child_ref(node.children + offset)?) +} + +fn child_bool( + expr: &smt_wire::ExprView<'_>, + translation: &Z3Translation, + node: &smt_wire::RawNode, + offset: u32, +) -> smt_wire::Result { + translation.bool(expr.child_ref(node.children + offset)?) +} + +fn child_bv2( + expr: &smt_wire::ExprView<'_>, + translation: &Z3Translation, + node: &smt_wire::RawNode, +) -> smt_wire::Result<(BV, BV)> { + Ok(( + child_bv(expr, translation, node, 0)?, + child_bv(expr, translation, node, 1)?, + )) +} + +fn child_bool2( + expr: &smt_wire::ExprView<'_>, + translation: &Z3Translation, + node: &smt_wire::RawNode, +) -> smt_wire::Result<(Bool, Bool)> { + Ok(( + child_bool(expr, translation, node, 0)?, + child_bool(expr, translation, node, 1)?, + )) +} + +fn z3_bv_const(expr: &smt_wire::ExprView<'_>, width: u32, payload: u64) -> smt_wire::Result { + if width <= 64 { + return Ok(BV::from_u64(payload, width)); + } + let bytes = expr.blob_ref(BlobRef::from_payload(payload))?; + let mut bits = Vec::with_capacity(width as usize); + for bit in 0..width { + let byte = bytes[(bit / 8) as usize]; + bits.push(((byte >> (bit % 8)) & 1) != 0); + } + BV::from_bits(&bits).ok_or_else(|| WireError::invalid("BV_CONST", "failed to build Z3 BV")) +} + +fn build_model(translation: &Z3Translation, model: &Model) -> smt_wire::Result { + let mut entries = Vec::with_capacity(translation.variables.len()); + for variable in &translation.variables { + let value = match variable.sort { + Sort::Bool => { + let ast = translation.bool(variable.node_ref)?; + let value = model + .eval(&ast, true) + .and_then(|value| value.as_bool()) + .ok_or_else(|| WireError::invalid("Z3 model", "missing Bool value"))?; + ScalarValue::bool(value) + } + Sort::Bv => { + let ast = translation.bv(variable.node_ref)?; + let value = model + .eval(&ast, true) + .ok_or_else(|| WireError::invalid("Z3 model", "missing BV value"))?; + bv_model_value(variable.width, &value)? + } + }; + entries.push(ModelEntry { + node_ref: variable.node_ref, + value, + }); + } + Ok(ModelBlock { entries }) +} + +fn build_core(solver: &Solver, trackers: &[(String, Bool)]) -> UnsatCoreBlock { + let core = solver.get_unsat_core(); + let names = trackers + .iter() + .filter(|(_, tracker)| core.iter().any(|item| tracker.ast_eq(item))) + .map(|(name, _)| name.clone()) + .collect(); + UnsatCoreBlock { names } +} + +fn bv_model_value(width: u32, value: &BV) -> smt_wire::Result { + if width <= 64 { + if let Some(value) = value.as_u64() { + let mut bytes = value.to_le_bytes().to_vec(); + bytes.truncate((width as usize).div_ceil(8)); + return ScalarValue::bv(width, bytes); + } + } + parse_bv_literal(&value.to_string(), width) +} + +fn parse_bv_literal(text: &str, width: u32) -> smt_wire::Result { + let mut bytes = vec![0u8; (width as usize).div_ceil(8)]; + if let Some(bits) = text.strip_prefix("#b") { + for (offset, ch) in bits.chars().rev().enumerate() { + if offset >= width as usize { + break; + } + match ch { + '0' => {} + '1' => bytes[offset / 8] |= 1 << (offset % 8), + _ => return Err(WireError::invalid("Z3 model", "bad binary BV literal")), + } + } + } else if let Some(hex) = text.strip_prefix("#x") { + for (nibble, ch) in hex.chars().rev().enumerate() { + let value = ch + .to_digit(16) + .ok_or_else(|| WireError::invalid("Z3 model", "bad hex BV literal"))? + as u8; + let bit = nibble * 4; + if bit / 8 < bytes.len() { + bytes[bit / 8] |= value << (bit % 8); + } + } + } else { + bytes = decimal_to_le_bytes(text, (width as usize).div_ceil(8))?; + } + ScalarValue::bv(width, bytes) +} + +fn decimal_to_le_bytes(text: &str, len: usize) -> smt_wire::Result> { + let mut digits = text + .bytes() + .map(|byte| match byte { + b'0'..=b'9' => Ok(byte - b'0'), + _ => Err(WireError::invalid("Z3 model", "bad decimal BV literal")), + }) + .collect::>>()?; + let mut out = Vec::with_capacity(len); + while !digits.is_empty() && out.len() < len { + let mut carry = 0u16; + let mut quotient = Vec::new(); + for digit in digits { + let n = carry * 10 + u16::from(digit); + let q = (n / 256) as u8; + carry = n % 256; + if !quotient.is_empty() || q != 0 { + quotient.push(q); + } + } + out.push(carry as u8); + digits = quotient; + } + out.resize(len, 0); + Ok(out) +} + +fn sign_bool(value: &BV) -> Bool { + value + .extract(value.get_size() - 1, value.get_size() - 1) + .eq(BV::from_u64(1, 1)) +} + +fn signed_add_overflow(a: &BV, b: &BV) -> Bool { + let sum = a.bvadd(b); + let sa = sign_bool(a); + let sb = sign_bool(b); + let ss = sign_bool(&sum); + Bool::or(&[ + &Bool::and(&[&sa.not(), &sb.not(), &ss]), + &Bool::and(&[&sa, &sb, &ss.not()]), + ]) +} + +fn signed_sub_overflow(a: &BV, b: &BV) -> Bool { + let diff = a.bvsub(b); + let sa = sign_bool(a); + let sb = sign_bool(b); + let sd = sign_bool(&diff); + Bool::or(&[ + &Bool::and(&[&sa.not(), &sb, &sd]), + &Bool::and(&[&sa, &sb.not(), &sd.not()]), + ]) +} + +fn unsigned_mul_overflow(a: &BV, b: &BV) -> Bool { + let width = a.get_size(); + let product = a.zero_ext(width).bvmul(b.zero_ext(width)); + let high = product.extract(width * 2 - 1, width); + high.eq(BV::from_u64(0, width)).not() +} + +fn signed_mul_overflow(a: &BV, b: &BV) -> Bool { + let width = a.get_size(); + let product = a.sign_ext(width).bvmul(b.sign_ext(width)); + let low = product.extract(width - 1, 0); + product.eq(low.sign_ext(width)).not() +} + +fn signed_min_bv(width: u32) -> BV { + let mut bits = vec![false; width as usize]; + bits[width as usize - 1] = true; + BV::from_bits(&bits).expect("non-empty bitvector") +} + +fn ones_bv(width: u32) -> BV { + BV::from_bits(&vec![true; width as usize]).expect("non-empty bitvector") +} + +fn set_bit(bytes: &mut [u8], bit: u32) { + bytes[(bit / 8) as usize] |= 1 << (bit % 8); +} + +``` + +`crates/smt-server/tests/phase2_server.rs`: + +```rs +use std::io::{Read, Write}; +use std::net::TcpListener; +use std::sync::Arc; +use std::thread; + +use smt_server::{ + handle_binary_frame, serve_tcp, BinbitBackend, QfbvsmtrsBackend, ServerConfig, Z3Backend, +}; +use smt_wire::{ + le, response_flags, status, BinaryResponse, ExprBuilder, ModelBlock, NodeRef, Status, + UnsatCoreBlock, +}; + +#[test] +fn binbit_backend_solves_sat_with_model() { + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 2).unwrap(); + let one = builder.bv_const(1, 2).unwrap(); + let eq = builder.bv_eq(x, one).unwrap(); + builder.assert(eq).unwrap(); + let request = builder.build_solve_request(11, 0, true, false).unwrap(); + + let response = handle_binary_frame(&request, &BinbitBackend) + .unwrap() + .encode() + .unwrap(); + let response = BinaryResponse::parse(&response).unwrap(); + assert_eq!(response.envelope.status, Status::Sat); + assert_eq!(response.envelope.flags, response_flags::HAS_MODEL); + let model = ModelBlock::decode(&response.payload).unwrap(); + let x_entry = model + .entries + .iter() + .find(|entry| entry.node_ref == NodeRef::bv(0).unwrap()) + .unwrap(); + assert_eq!(x_entry.value.width, 2); + assert_eq!(x_entry.value.bytes, vec![1]); +} + +#[test] +fn binbit_backend_solves_unsat_with_named_core() { + let mut builder = ExprBuilder::new(); + let p = builder.bool_var("p").unwrap(); + let not_p = builder.bool_not(p).unwrap(); + builder.assert_named("p-is-true", p).unwrap(); + builder.assert_named("p-is-false", not_p).unwrap(); + let request = builder.build_solve_request(12, 0, false, true).unwrap(); + + let response = handle_binary_frame(&request, &BinbitBackend) + .unwrap() + .encode() + .unwrap(); + let response = BinaryResponse::parse(&response).unwrap(); + assert_eq!(response.envelope.status, Status::Unsat); + assert_eq!(response.envelope.flags, response_flags::HAS_CORE); + let core = UnsatCoreBlock::decode(&response.payload).unwrap(); + assert_eq!(core.names, vec!["p-is-true", "p-is-false"]); +} + +#[test] +fn z3_backend_solves_sat_with_model() { + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 4).unwrap(); + let three = builder.bv_const(3, 4).unwrap(); + let eq = builder.bv_eq(x, three).unwrap(); + builder.assert(eq).unwrap(); + let request = builder.build_solve_request(13, 0, true, false).unwrap(); + + let response = handle_binary_frame(&request, &Z3Backend) + .unwrap() + .encode() + .unwrap(); + let response = BinaryResponse::parse(&response).unwrap(); + assert_eq!(response.envelope.status, Status::Sat); + let model = ModelBlock::decode(&response.payload).unwrap(); + let x_entry = model + .entries + .iter() + .find(|entry| entry.node_ref == NodeRef::bv(0).unwrap()) + .unwrap(); + assert_eq!(x_entry.value.width, 4); + assert_eq!(x_entry.value.bytes, vec![3]); +} + +#[test] +fn qfbvsmtrs_backend_solves_sat_with_model() { + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 4).unwrap(); + let one = builder.bv_const(1, 4).unwrap(); + let sum = builder.bv_add(x, one).unwrap(); + let three = builder.bv_const(3, 4).unwrap(); + let eq = builder.bv_eq(sum, three).unwrap(); + builder.assert(eq).unwrap(); + let request = builder.build_solve_request(15, 0, true, false).unwrap(); + + let response = handle_binary_frame(&request, &QfbvsmtrsBackend) + .unwrap() + .encode() + .unwrap(); + let response = BinaryResponse::parse(&response).unwrap(); + assert_eq!(response.envelope.status, Status::Sat); + let model = ModelBlock::decode(&response.payload).unwrap(); + let x_entry = model + .entries + .iter() + .find(|entry| entry.node_ref == NodeRef::bv(0).unwrap()) + .unwrap(); + assert_eq!(x_entry.value.width, 4); + assert_eq!(x_entry.value.bytes, vec![2]); +} + +#[test] +fn qfbvsmtrs_backend_solves_unsat_with_named_core() { + let mut builder = ExprBuilder::new(); + let p = builder.bool_var("p").unwrap(); + let not_p = builder.bool_not(p).unwrap(); + builder.assert_named("p-is-true", p).unwrap(); + builder.assert_named("p-is-false", not_p).unwrap(); + let request = builder.build_solve_request(17, 0, false, true).unwrap(); + + let response = handle_binary_frame(&request, &QfbvsmtrsBackend) + .unwrap() + .encode() + .unwrap(); + let response = BinaryResponse::parse(&response).unwrap(); + assert_eq!(response.envelope.status, Status::Unsat); + assert_eq!(response.envelope.flags, response_flags::HAS_CORE); + let core = UnsatCoreBlock::decode(&response.payload).unwrap(); + assert_eq!(core.names, vec!["p-is-true", "p-is-false"]); +} + +#[test] +fn qfbvsmtrs_backend_solves_unsat() { + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 4).unwrap(); + let one = builder.bv_const(1, 4).unwrap(); + let two = builder.bv_const(2, 4).unwrap(); + let eq_one = builder.bv_eq(x, one).unwrap(); + let eq_two = builder.bv_eq(x, two).unwrap(); + builder.assert(eq_one).unwrap(); + builder.assert(eq_two).unwrap(); + let request = builder.build_solve_request(16, 0, false, false).unwrap(); + + let response = handle_binary_frame(&request, &QfbvsmtrsBackend) + .unwrap() + .encode() + .unwrap(); + let response = BinaryResponse::parse(&response).unwrap(); + assert_eq!(response.envelope.status, Status::Unsat); +} + +#[test] +fn tcp_server_handles_one_binary_frame() { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + drop(listener); + + let handle = thread::spawn(move || { + // The server runs until the client closes; this test relies on process + // teardown to clean up the background thread after one request. + let _ = serve_tcp(addr, ServerConfig::new(Arc::new(BinbitBackend))); + }); + + let mut builder = ExprBuilder::new(); + let t = builder.bool_true().unwrap(); + builder.assert(t).unwrap(); + let request = builder.build_solve_request(14, 0, false, false).unwrap(); + let frame = le::encode_transport_frame(&request).unwrap(); + + let mut stream = None; + for _ in 0..100 { + match std::net::TcpStream::connect(addr) { + Ok(s) => { + stream = Some(s); + break; + } + Err(_) => std::thread::sleep(std::time::Duration::from_millis(10)), + } + } + let mut stream = stream.unwrap(); + stream.write_all(&frame).unwrap(); + let mut len = [0u8; 4]; + stream.read_exact(&mut len).unwrap(); + let len = u32::from_le_bytes(len) as usize; + let mut payload = vec![0u8; len]; + stream.read_exact(&mut payload).unwrap(); + let response = BinaryResponse::parse(&payload).unwrap(); + assert_eq!(response.envelope.request_id, 14); + assert_eq!(response.envelope.status as u8, status::SAT); + drop(stream); + drop(handle); +} + +``` + +`crates/smt-server/tests/phase4_5_text_and_racing.rs`: + +```rs +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use smt_server::{ + handle_text_frame, parse_smtlib_script, request_to_smt2, Backend, BinbitBackend, QueryResult, + RacingBackend, Z3Backend, +}; +use smt_wire::{BinaryRequest, ExprBuilder}; + +#[test] +fn smtlib_text_frontend_solves_sat_and_formats_model() { + let script = r#" + (set-logic QF_BV) + (declare-const x (_ BitVec 2)) + (assert (= x #b01)) + (check-sat) + (get-model) + "#; + let output = handle_text_frame(script.as_bytes(), &BinbitBackend).unwrap(); + let text = String::from_utf8(output).unwrap(); + assert!(text.starts_with("sat\n"), "{text}"); + assert!(text.contains("define-fun x"), "{text}"); + assert!(text.contains("#b01"), "{text}"); +} + +#[test] +fn smtlib_text_frontend_rejects_incremental_commands() { + let script = "(set-logic QF_BV) (push 1) (check-sat)"; + let output = handle_text_frame(script.as_bytes(), &BinbitBackend).unwrap(); + let text = String::from_utf8(output).unwrap(); + assert!(text.contains("error"), "{text}"); + assert!(text.contains("push"), "{text}"); +} + +#[test] +fn smtlib_text_frontend_get_value_formats_value_response() { + let script = r#" + (set-logic QF_BV) + (declare-const x (_ BitVec 2)) + (assert (= x #b10)) + (check-sat) + (get-value (x)) + "#; + let output = handle_text_frame(script.as_bytes(), &BinbitBackend).unwrap(); + let text = String::from_utf8(output).unwrap(); + assert!(text.starts_with("sat\n"), "{text}"); + assert!(text.contains("((x #b10))"), "{text}"); +} + +#[test] +fn smtlib_text_frontend_named_unsat_core() { + let script = r#" + (set-logic QF_BV) + (declare-const p Bool) + (assert (! p :named p_true)) + (assert (! (not p) :named p_false)) + (check-sat) + (get-unsat-core) + "#; + let output = handle_text_frame(script.as_bytes(), &BinbitBackend).unwrap(); + let text = String::from_utf8(output).unwrap(); + assert!(text.starts_with("unsat\n"), "{text}"); + assert!(text.contains("p_true"), "{text}"); + assert!(text.contains("p_false"), "{text}"); +} + +#[test] +fn smtlib_yaspar_parser_handles_block_comments_and_quoted_symbols() { + let script = r#" + #| block comments are handled by yaspar |# + (set-logic QF_BV) + (declare-const |x y| (_ BitVec 2)) + (assert (= |x y| #b11)) + (check-sat) + (get-value (|x y|)) + "#; + let output = handle_text_frame(script.as_bytes(), &BinbitBackend).unwrap(); + let text = String::from_utf8(output).unwrap(); + assert!(text.starts_with("sat\n"), "{text}"); + assert!(text.contains("(|x y| #b11)"), "{text}"); +} + +#[test] +fn smtlib_parser_supports_wide_hex_literals() { + let script = r#" + (set-logic QF_BV) + (declare-const x (_ BitVec 132)) + (assert (= x #x100000000000000000000000000000000)) + (check-sat) + "#; + let query = parse_smtlib_script(script).unwrap(); + assert_eq!(query.request.assertion_roots.len(), 1); +} + +#[test] +fn smtlib_parser_supports_let_extract_and_assumptions() { + let script = r#" + (set-logic QF_BV) + (declare-fun x () (_ BitVec 4)) + (declare-const p Bool) + (assert (let ((lo ((_ extract 1 0) x))) (= lo #b01))) + (check-sat-assuming (p)) + "#; + let query = parse_smtlib_script(script).unwrap(); + assert_eq!(query.request.assumption_roots.len(), 1); + assert_eq!(query.request.assertion_roots.len(), 1); +} + +#[test] +fn smt2_translation_contains_declarations_and_named_assertions() { + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 4).unwrap(); + let one = builder.bv_const(1, 4).unwrap(); + let eq = builder.bv_eq(x, one).unwrap(); + builder.assert_named("x_is_one", eq).unwrap(); + let request = + BinaryRequest::parse(&builder.build_solve_request(1, 0, true, true).unwrap()).unwrap(); + let smt2 = request_to_smt2(&request).unwrap(); + assert!(smt2.script.contains("(declare-fun x () (_ BitVec 4))")); + assert!(smt2.script.contains(":named x_is_one")); + assert!(smt2.script.contains("(get-value ( x))")); + assert!(smt2.script.contains("(get-unsat-core)")); + + let request = + BinaryRequest::parse(&builder.build_solve_request(2, 123, false, false).unwrap()).unwrap(); + let smt2 = request_to_smt2(&request).unwrap(); + assert!(smt2.script.contains("(set-option :timeout 123)")); +} + +struct UnknownBackend; +impl Backend for UnknownBackend { + fn name(&self) -> &'static str { + "unknown-test" + } + fn handle(&self, _request: &BinaryRequest) -> smt_wire::Result { + Ok(QueryResult::unknown("test unknown")) + } +} + +struct SlowUnknownBackend; +impl Backend for SlowUnknownBackend { + fn name(&self) -> &'static str { + "slow-unknown-test" + } + fn handle(&self, _request: &BinaryRequest) -> smt_wire::Result { + std::thread::sleep(Duration::from_millis(250)); + Ok(QueryResult::unknown("slow unknown")) + } +} + +struct ImmediateSatBackend; +impl Backend for ImmediateSatBackend { + fn name(&self) -> &'static str { + "immediate-sat-test" + } + fn handle(&self, _request: &BinaryRequest) -> smt_wire::Result { + Ok(QueryResult::sat(None)) + } +} + +#[test] +fn racing_backend_waits_past_unknown_for_conclusive_result() { + let mut builder = ExprBuilder::new(); + let t = builder.bool_true().unwrap(); + builder.assert(t).unwrap(); + let request = + BinaryRequest::parse(&builder.build_solve_request(3, 0, false, false).unwrap()).unwrap(); + let racing = RacingBackend::new(vec![Arc::new(UnknownBackend), Arc::new(BinbitBackend)]); + let result = racing.handle(&request).unwrap(); + assert!(result.is_conclusive()); +} + +#[test] +fn racing_backend_returns_first_conclusive_without_waiting_for_slow_unknown() { + let mut builder = ExprBuilder::new(); + let t = builder.bool_true().unwrap(); + builder.assert(t).unwrap(); + let request = + BinaryRequest::parse(&builder.build_solve_request(4, 0, false, false).unwrap()).unwrap(); + let racing = RacingBackend::new(vec![ + Arc::new(SlowUnknownBackend), + Arc::new(ImmediateSatBackend), + ]); + let start = Instant::now(); + let result = racing.handle(&request).unwrap(); + assert!(result.is_conclusive()); + assert!(start.elapsed() < Duration::from_millis(200)); +} + +#[test] +fn z3_backend_extracts_named_unsat_core() { + let mut builder = ExprBuilder::new(); + let p = builder.bool_var("p").unwrap(); + let not_p = builder.bool_not(p).unwrap(); + builder.assert_named("p_true", p).unwrap(); + builder.assert_named("p_false", not_p).unwrap(); + let request = + BinaryRequest::parse(&builder.build_solve_request(10, 0, false, true).unwrap()).unwrap(); + let result = Z3Backend.handle(&request).unwrap(); + let core = result.core.unwrap(); + assert!(core.names.contains(&"p_true".to_owned())); + assert!(core.names.contains(&"p_false".to_owned())); +} + +``` + +`crates/smt-server/tests/phase6_simplify_optimize.rs`: + +```rs +use smt_server::{handle_binary_frame, BinbitBackend, QfbvsmtrsBackend, Z3Backend}; +use smt_wire::{ + response_flags, BinaryResponse, ExprBuilder, OptimizationValueBlock, SimplifyBlock, Status, +}; + +#[test] +fn simplify_returns_simplify_block() { + let mut builder = ExprBuilder::new(); + let t = builder.bool_true().unwrap(); + builder.assert(t).unwrap(); + let request = builder.build_simplify_request(21).unwrap(); + let response = handle_binary_frame(&request, &BinbitBackend) + .unwrap() + .encode() + .unwrap(); + let response = BinaryResponse::parse(&response).unwrap(); + assert_eq!(response.envelope.status, Status::Ok); + assert_eq!(response.envelope.flags, response_flags::HAS_EXPR); + let block = SimplifyBlock::decode(&response.payload).unwrap(); + assert_eq!(block.assertion_roots.len(), 1); + assert_eq!(block.assumption_roots.len(), 0); +} + +#[test] +fn unsigned_minimize_and_maximize_return_optimum_values() { + let mut min_builder = ExprBuilder::new(); + let x = min_builder.bv_var("x", 4).unwrap(); + let three = min_builder.bv_const(3, 4).unwrap(); + let ge = min_builder.bv_uge(x, three).unwrap(); + min_builder.assert(ge).unwrap(); + let request = min_builder + .build_minimize_request(22, x, false, 0, false) + .unwrap(); + let response = BinaryResponse::parse( + &handle_binary_frame(&request, &BinbitBackend) + .unwrap() + .encode() + .unwrap(), + ) + .unwrap(); + assert_eq!(response.envelope.status, Status::Sat); + assert_eq!(response.envelope.flags, response_flags::HAS_VALUE); + let optimum = OptimizationValueBlock::decode(&response.payload, false).unwrap(); + assert_eq!(optimum.optimum.width, 4); + assert_eq!(optimum.optimum.bytes, vec![3]); + + let mut max_builder = ExprBuilder::new(); + let y = max_builder.bv_var("y", 4).unwrap(); + let ten = max_builder.bv_const(10, 4).unwrap(); + let le = max_builder.bv_ule(y, ten).unwrap(); + max_builder.assert(le).unwrap(); + let request = max_builder + .build_maximize_request(23, y, false, 0, false) + .unwrap(); + let response = BinaryResponse::parse( + &handle_binary_frame(&request, &BinbitBackend) + .unwrap() + .encode() + .unwrap(), + ) + .unwrap(); + let optimum = OptimizationValueBlock::decode(&response.payload, false).unwrap(); + assert_eq!(optimum.optimum.bytes, vec![10]); +} + +#[test] +fn signed_optimization_uses_signed_ordering_and_can_return_model() { + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 4).unwrap(); + let request = builder + .build_minimize_request(24, x, true, 0, true) + .unwrap(); + let response = BinaryResponse::parse( + &handle_binary_frame(&request, &BinbitBackend) + .unwrap() + .encode() + .unwrap(), + ) + .unwrap(); + assert_eq!( + response.envelope.flags, + response_flags::HAS_VALUE | response_flags::HAS_MODEL + ); + let optimum = OptimizationValueBlock::decode(&response.payload, true).unwrap(); + // 4-bit signed minimum is -8, encoded as 0b1000. + assert_eq!(optimum.optimum.bytes, vec![8]); + assert!(optimum + .model + .unwrap() + .entries + .iter() + .any(|entry| entry.value.bytes == vec![8])); + + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 4).unwrap(); + let request = builder + .build_maximize_request(25, x, true, 0, false) + .unwrap(); + let response = BinaryResponse::parse( + &handle_binary_frame(&request, &BinbitBackend) + .unwrap() + .encode() + .unwrap(), + ) + .unwrap(); + let optimum = OptimizationValueBlock::decode(&response.payload, false).unwrap(); + // 4-bit signed maximum is +7. + assert_eq!(optimum.optimum.bytes, vec![7]); +} + +#[test] +fn qfbvsmtrs_backend_optimization_uses_bit_hunt() { + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 4).unwrap(); + let five = builder.bv_const(5, 4).unwrap(); + let ge = builder.bv_uge(x, five).unwrap(); + builder.assert(ge).unwrap(); + let request = builder + .build_minimize_request(27, x, false, 0, true) + .unwrap(); + let response = BinaryResponse::parse( + &handle_binary_frame(&request, &QfbvsmtrsBackend) + .unwrap() + .encode() + .unwrap(), + ) + .unwrap(); + assert_eq!(response.envelope.status, Status::Sat); + assert_eq!( + response.envelope.flags, + response_flags::HAS_VALUE | response_flags::HAS_MODEL + ); + let optimum = OptimizationValueBlock::decode(&response.payload, true).unwrap(); + assert_eq!(optimum.optimum.bytes, vec![5]); + assert!(optimum + .model + .unwrap() + .entries + .iter() + .any(|entry| entry.value.bytes == vec![5])); +} + +#[test] +fn z3_backend_optimization_uses_bit_hunt() { + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 4).unwrap(); + let five = builder.bv_const(5, 4).unwrap(); + let ge = builder.bv_uge(x, five).unwrap(); + builder.assert(ge).unwrap(); + let request = builder + .build_minimize_request(26, x, false, 0, false) + .unwrap(); + let response = BinaryResponse::parse( + &handle_binary_frame(&request, &Z3Backend) + .unwrap() + .encode() + .unwrap(), + ) + .unwrap(); + assert_eq!(response.envelope.status, Status::Sat); + let optimum = OptimizationValueBlock::decode(&response.payload, false).unwrap(); + assert_eq!(optimum.optimum.bytes, vec![5]); +} + +``` + +`crates/smt-server/tests/phase7_cache.rs`: + +```rs +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + +use smt_server::{dispatch_payload_with_cache, Backend, PooledBackend, QueryResult, ResponseCache}; +use smt_wire::{BinaryRequest, BinaryResponse, ExprBuilder}; + +struct CountingBackend { + count: Arc, +} + +impl Backend for CountingBackend { + fn name(&self) -> &'static str { + "counting" + } + + fn handle(&self, _request: &BinaryRequest) -> smt_wire::Result { + self.count.fetch_add(1, Ordering::SeqCst); + Ok(QueryResult::sat(None)) + } +} + +#[test] +fn cache_reuses_semantic_binary_query_across_request_ids_and_rebinds_echo() { + let count = Arc::new(AtomicUsize::new(0)); + let backend = CountingBackend { + count: Arc::clone(&count), + }; + let cache = ResponseCache::new(); + + let mut builder = ExprBuilder::new(); + let t = builder.bool_true().unwrap(); + builder.assert(t).unwrap(); + let req1 = builder.build_solve_request(100, 0, false, false).unwrap(); + let req2 = builder.build_solve_request(200, 0, false, false).unwrap(); + + let resp1 = dispatch_payload_with_cache(&req1, &backend, Some(&cache)); + let resp2 = dispatch_payload_with_cache(&req2, &backend, Some(&cache)); + + assert_eq!(count.load(Ordering::SeqCst), 1); + assert_eq!(cache.len(), 1); + assert_eq!(cache.stats().misses, 1); + assert_eq!(cache.stats().hits, 1); + + let resp1 = BinaryResponse::parse(&resp1).unwrap(); + let resp2 = BinaryResponse::parse(&resp2).unwrap(); + assert_eq!(resp1.envelope.request_id, 100); + assert_eq!(resp2.envelope.request_id, 200); +} + +#[test] +fn cache_key_keeps_fields_that_affect_payload() { + let cache = ResponseCache::new(); + let count = Arc::new(AtomicUsize::new(0)); + let backend = CountingBackend { + count: Arc::clone(&count), + }; + + let mut builder = ExprBuilder::new(); + let t = builder.bool_true().unwrap(); + builder.assert(t).unwrap(); + let no_model = builder.build_solve_request(1, 0, false, false).unwrap(); + let want_model = builder.build_solve_request(2, 0, true, false).unwrap(); + + let _ = dispatch_payload_with_cache(&no_model, &backend, Some(&cache)); + let _ = dispatch_payload_with_cache(&want_model, &backend, Some(&cache)); + + assert_eq!(count.load(Ordering::SeqCst), 2); + assert_eq!(cache.len(), 2); +} + +#[test] +fn pooled_backend_routes_similar_queries_to_same_backend_instance() { + let c0 = Arc::new(AtomicUsize::new(0)); + let c1 = Arc::new(AtomicUsize::new(0)); + let pool = PooledBackend::new(vec![ + Arc::new(CountingBackend { + count: Arc::clone(&c0), + }), + Arc::new(CountingBackend { + count: Arc::clone(&c1), + }), + ]); + + let mut builder = ExprBuilder::new(); + let t = builder.bool_true().unwrap(); + builder.assert(t).unwrap(); + let req1 = + BinaryRequest::parse(&builder.build_solve_request(1, 0, false, false).unwrap()).unwrap(); + let req2 = + BinaryRequest::parse(&builder.build_solve_request(2, 0, false, false).unwrap()).unwrap(); + + let route1 = pool.route_index(&req1).unwrap(); + let route2 = pool.route_index(&req2).unwrap(); + assert_eq!(route1, route2); + pool.handle(&req1).unwrap(); + pool.handle(&req2).unwrap(); + assert!(c0.load(Ordering::SeqCst) == 2 || c1.load(Ordering::SeqCst) == 2); +} + +``` + +`crates/smt-server/tests/phase8_e2e.rs`: + +```rs +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +use smt_server::{serve_tcp, BinbitBackend, RacingBackend, ServerConfig, Z3Backend}; +use smt_wire::{response_flags, ExprBuilder, ModelBlock, Status, TcpClient}; + +fn start_default_test_server() -> SocketAddr { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + drop(listener); + + thread::spawn(move || { + let backend = Arc::new(RacingBackend::new(vec![ + Arc::new(Z3Backend), + Arc::new(BinbitBackend), + ])); + let _ = serve_tcp(addr, ServerConfig::new(backend)); + }); + + for _ in 0..100 { + if TcpStream::connect(addr).is_ok() { + return addr; + } + thread::sleep(Duration::from_millis(10)); + } + panic!("test server did not start on {addr}"); +} + +#[test] +fn live_tcp_server_handles_binary_text_and_cached_requests() { + let addr = start_default_test_server(); + let mut client = TcpClient::connect(addr).unwrap(); + + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 4).unwrap(); + let two = builder.bv_const(2, 4).unwrap(); + let eq = builder.bv_eq(x, two).unwrap(); + builder.assert(eq).unwrap(); + let request = builder.build_solve_request(0x1001, 0, true, false).unwrap(); + + let response = client.send_binary_request(&request).unwrap(); + assert_eq!(response.envelope.request_id, 0x1001); + assert_eq!(response.envelope.status, Status::Sat); + assert_eq!(response.envelope.flags, response_flags::HAS_MODEL); + let model = ModelBlock::decode(&response.payload).unwrap(); + assert!(model + .entries + .iter() + .any(|entry| entry.value.width == 4 && entry.value.bytes == vec![2])); + + let first_cached = builder + .build_solve_request(0x2001, 0, false, false) + .unwrap(); + let second_cached = builder + .build_solve_request(0x2002, 0, false, false) + .unwrap(); + let first = client.send_binary_request(&first_cached).unwrap(); + let second = client.send_binary_request(&second_cached).unwrap(); + assert_eq!(first.envelope.request_id, 0x2001); + assert_eq!(second.envelope.request_id, 0x2002); + assert_eq!(first.envelope.status, Status::Sat); + assert_eq!(second.envelope.status, Status::Sat); + + let text_script = r#" + #| yaspar parses block comments in the live text path |# + (set-logic QF_BV) + (declare-const |x y| (_ BitVec 2)) + (assert (= |x y| #b11)) + (check-sat) + (get-value (|x y|)) + "#; + let text_response = client.send_text(text_script).unwrap(); + assert!(text_response.starts_with("sat\n"), "{text_response}"); + assert!(text_response.contains("(|x y| #b11)"), "{text_response}"); +} + +``` + +`crates/smt-wire/Cargo.toml`: + +```toml +[package] +name = "smt-wire" +version = "0.1.0" +edition = "2021" +description = "Rust codecs, views, validation, and builders for the SMT v1 wire format" +license = "MIT OR Apache-2.0" + +[lib] +name = "smt_wire" +path = "src/lib.rs" + +``` + +`crates/smt-wire/src/builder.rs`: + +```rs +use crate::constants::{request_flags, Command, Tag}; +use crate::error::{Result, WireError}; +use crate::expr::{bytes_for_width, validate_bv_width_value, ExpressionBuffer, RawNode}; +use crate::request::BinaryRequest; +use crate::types::{BlobRef, NodeRef, Sort}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct NodeMeta { + sort: Sort, + width: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Assertion { + pub root: NodeRef, + pub name: Option, +} + +#[derive(Debug, Clone)] +pub struct CompactedExpression { + bytes: Vec, + old_to_new: Vec>, +} + +impl CompactedExpression { + pub fn as_bytes(&self) -> &[u8] { + &self.bytes + } + + pub fn into_bytes(self) -> Vec { + self.bytes + } + + pub fn remap_ref(&self, old: NodeRef) -> Result { + let mapped = self + .old_to_new + .get(old.index() as usize) + .copied() + .flatten() + .ok_or_else(|| { + WireError::invalid( + "compacted node reference", + format!("old reference {:#010x} is not live", old.raw()), + ) + })?; + if mapped.sort() != old.sort() { + return Err(WireError::invalid( + "compacted node reference", + format!( + "old reference {:#010x} has sort {:?}, remapped reference has {:?}", + old.raw(), + old.sort(), + mapped.sort() + ), + )); + } + Ok(mapped) + } +} + +/// Append-only expression and query builder with eager sort/width validation. +#[derive(Debug, Clone, Default)] +pub struct ExprBuilder { + nodes: Vec, + children: Vec, + blob: Vec, + meta: Vec, + assertions: Vec, + assumptions: Vec, + scopes: Vec, +} + +impl ExprBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn reset(&mut self) { + self.nodes.clear(); + self.children.clear(); + self.blob.clear(); + self.meta.clear(); + self.assertions.clear(); + self.assumptions.clear(); + self.scopes.clear(); + } + + pub fn node_count(&self) -> usize { + self.nodes.len() + } + + pub fn child_count(&self) -> usize { + self.children.len() + } + + pub fn blob_len(&self) -> usize { + self.blob.len() + } + + pub fn assertions(&self) -> &[Assertion] { + &self.assertions + } + + pub fn assumptions(&self) -> &[NodeRef] { + &self.assumptions + } + + pub fn to_expression_buffer(&self) -> Result { + ExpressionBuffer::from_parts(&self.nodes, &self.children, &self.blob) + } + + pub fn to_bytes(&self) -> Result> { + Ok(self.to_expression_buffer()?.into_bytes()) + } + + /// Compact nodes reachable from `roots`, preserving topological order and + /// remapping typed references. The blob table is kept intact so named + /// assertion references remain valid. + pub fn compact(&self, roots: &[NodeRef]) -> Result { + let mut marked = vec![false; self.nodes.len()]; + for root in roots { + self.mark_ref(*root, &mut marked)?; + } + + let mut old_to_new = vec![None; self.nodes.len()]; + for (old_index, is_live) in marked.iter().copied().enumerate() { + if is_live { + let meta = self.meta[old_index]; + let new_index = old_to_new.iter().filter(|entry| entry.is_some()).count() as u32; + old_to_new[old_index] = Some(NodeRef::new(meta.sort, new_index)?); + } + } + + let mut new_nodes = Vec::new(); + let mut new_children = Vec::new(); + for (old_index, is_live) in marked.iter().copied().enumerate() { + if !is_live { + continue; + } + let old_node = self.nodes[old_index]; + let child_start = new_children.len(); + for child_offset in 0..old_node.arity as usize { + let old_child = self.children[old_node.children as usize + child_offset]; + let new_child = old_to_new + .get(old_child.index() as usize) + .copied() + .flatten() + .ok_or_else(|| { + WireError::invalid( + "compaction", + format!( + "live node {old_index} references non-live child {old_child:?}" + ), + ) + })?; + new_children.push(new_child); + } + let mut new_node = old_node; + new_node.children = if old_node.arity == 0 { + 0 + } else { + u32::try_from(child_start) + .map_err(|_| WireError::invalid("compaction", "child array exceeds u32::MAX"))? + }; + new_nodes.push(new_node); + } + + let expression = ExpressionBuffer::from_parts(&new_nodes, &new_children, &self.blob)?; + Ok(CompactedExpression { + bytes: expression.into_bytes(), + old_to_new, + }) + } + + pub fn push(&mut self) { + self.scopes.push(self.assertions.len()); + } + + pub fn pop(&mut self) -> Result<()> { + let len = self + .scopes + .pop() + .ok_or_else(|| WireError::invalid("scope", "pop without matching push"))?; + self.assertions.truncate(len); + Ok(()) + } + + pub fn assert(&mut self, root: NodeRef) -> Result<()> { + self.expect_bool(root)?; + self.assertions.push(Assertion { root, name: None }); + Ok(()) + } + + pub fn assert_named(&mut self, name: &str, root: NodeRef) -> Result<()> { + self.expect_bool(root)?; + let name_ref = self.push_blob(name.as_bytes())?; + self.assertions.push(Assertion { + root, + name: Some(name_ref), + }); + Ok(()) + } + + pub fn assume(&mut self, root: NodeRef) -> Result<()> { + self.expect_bool(root)?; + self.assumptions.push(root); + Ok(()) + } + + pub fn clear_assumptions(&mut self) { + self.assumptions.clear(); + } + + pub fn build_solve_request( + &self, + request_id: u32, + budget_ms: u32, + want_model: bool, + want_core: bool, + ) -> Result> { + let mut flags = 0; + if want_model { + flags |= request_flags::WANT_MODEL; + } + if want_core { + flags |= request_flags::WANT_CORE; + } + self.build_request(request_id, Command::Solve, flags, budget_ms, None) + } + + pub fn build_simplify_request(&self, request_id: u32) -> Result> { + self.build_request(request_id, Command::Simplify, 0, 0, None) + } + + pub fn build_minimize_request( + &self, + request_id: u32, + target: NodeRef, + signed: bool, + budget_ms: u32, + want_model: bool, + ) -> Result> { + self.expect_bv(target)?; + let mut flags = 0; + if signed { + flags |= request_flags::SIGNED; + } + if want_model { + flags |= request_flags::WANT_MODEL; + } + self.build_request( + request_id, + Command::Minimize, + flags, + budget_ms, + Some(target), + ) + } + + pub fn build_maximize_request( + &self, + request_id: u32, + target: NodeRef, + signed: bool, + budget_ms: u32, + want_model: bool, + ) -> Result> { + self.expect_bv(target)?; + let mut flags = 0; + if signed { + flags |= request_flags::SIGNED; + } + if want_model { + flags |= request_flags::WANT_MODEL; + } + self.build_request( + request_id, + Command::Maximize, + flags, + budget_ms, + Some(target), + ) + } + + pub fn build_request( + &self, + request_id: u32, + command: Command, + flags: u8, + budget_ms: u32, + target: Option, + ) -> Result> { + let mut named = Vec::new(); + let mut unnamed = Vec::new(); + for assertion in &self.assertions { + if assertion.name.is_some() { + named.push(*assertion); + } else { + unnamed.push(*assertion); + } + } + let ordered_assertions = named + .iter() + .copied() + .chain(unnamed.iter().copied()) + .collect::>(); + let named_refs = named + .iter() + .map(|assertion| assertion.name.expect("named assertion has name")) + .collect::>(); + + let mut live_roots = Vec::new(); + live_roots.extend(ordered_assertions.iter().map(|assertion| assertion.root)); + live_roots.extend(self.assumptions.iter().copied()); + if let Some(target) = target { + live_roots.push(target); + } + let compacted = self.compact(&live_roots)?; + let assertion_roots = ordered_assertions + .iter() + .map(|assertion| compacted.remap_ref(assertion.root)) + .collect::>>()?; + let assumption_roots = self + .assumptions + .iter() + .map(|root| compacted.remap_ref(*root)) + .collect::>>()?; + let target = target.map(|root| compacted.remap_ref(root)).transpose()?; + + let request = BinaryRequest::new( + request_id, + command, + flags, + budget_ms, + compacted.into_bytes(), + assertion_roots, + named_refs, + assumption_roots, + target, + )?; + request.encode() + } + + pub fn bv_var(&mut self, name: &str, width: u32) -> Result { + validate_bv_width_value(width, "BV variable width")?; + let payload = self.push_blob(name.as_bytes())?.to_payload(); + self.push_node(Tag::BvVar, width, &[], 0, 0, payload) + } + + pub fn bv_const(&mut self, value: u64, width: u32) -> Result { + validate_bv_width_value(width, "BV constant width")?; + if width <= 64 { + let payload = mask_u64(value, width); + self.push_node(Tag::BvConst, width, &[], 0, 0, payload) + } else { + let mut bytes = vec![0u8; bytes_for_width(width)?]; + bytes[..8].copy_from_slice(&value.to_le_bytes()); + mask_unused_high_bits(&mut bytes, width); + let payload = self.push_blob(&bytes)?.to_payload(); + self.push_node(Tag::BvConst, width, &[], 0, 0, payload) + } + } + + pub fn bv_const_wide(&mut self, bytes: &[u8], width: u32) -> Result { + validate_bv_width_value(width, "wide BV constant width")?; + let expected = bytes_for_width(width)?; + if bytes.len() != expected { + return Err(WireError::invalid( + "wide BV constant", + format!( + "got {} bytes, expected {expected} for width {width}", + bytes.len() + ), + )); + } + let mut normalized = bytes.to_vec(); + mask_unused_high_bits(&mut normalized, width); + if width <= 64 { + let mut arr = [0u8; 8]; + arr[..normalized.len()].copy_from_slice(&normalized); + self.bv_const(u64::from_le_bytes(arr), width) + } else { + let payload = self.push_blob(&normalized)?.to_payload(); + self.push_node(Tag::BvConst, width, &[], 0, 0, payload) + } + } + + pub fn bv_not(&mut self, x: NodeRef) -> Result { + self.bv_unary(Tag::BvNot, x) + } + + pub fn bv_neg(&mut self, x: NodeRef) -> Result { + self.bv_unary(Tag::BvNeg, x) + } + + pub fn bv_and(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvAnd, a, b) + } + + pub fn bv_or(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvOr, a, b) + } + + pub fn bv_xor(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvXor, a, b) + } + + pub fn bv_add(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvAdd, a, b) + } + + pub fn bv_sub(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvSub, a, b) + } + + pub fn bv_mul(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvMul, a, b) + } + + pub fn bv_udiv(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvUdiv, a, b) + } + + pub fn bv_urem(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvUrem, a, b) + } + + pub fn bv_sdiv(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvSdiv, a, b) + } + + pub fn bv_srem(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvSrem, a, b) + } + + pub fn bv_smod(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvSmod, a, b) + } + + pub fn bv_shl(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvShl, a, b) + } + + pub fn bv_lshr(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvLshr, a, b) + } + + pub fn bv_ashr(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_binary(Tag::BvAshr, a, b) + } + + pub fn bv_extract(&mut self, x: NodeRef, hi: u32, lo: u32) -> Result { + let width = self.expect_bv(x)?; + if hi > u16::MAX as u32 { + return Err(WireError::invalid( + "BV_EXTRACT", + format!("hi {hi} exceeds u16::MAX"), + )); + } + if lo > hi || hi >= width { + return Err(WireError::invalid( + "BV_EXTRACT", + format!("invalid bounds lo={lo}, hi={hi}, child width {width}"), + )); + } + self.push_node(Tag::BvExtract, hi - lo + 1, &[x], hi as u16, lo, 0) + } + + pub fn bv_concat(&mut self, high: NodeRef, low: NodeRef) -> Result { + let high_width = self.expect_bv(high)?; + let low_width = self.expect_bv(low)?; + let width = high_width + .checked_add(low_width) + .ok_or(WireError::IntegerOverflow("BV_CONCAT width"))?; + validate_bv_width_value(width, "BV_CONCAT width")?; + self.push_node(Tag::BvConcat, width, &[high, low], 0, 0, 0) + } + + pub fn bv_zext(&mut self, x: NodeRef, amount: u16) -> Result { + self.bv_extend(Tag::BvZext, x, amount) + } + + pub fn bv_sext(&mut self, x: NodeRef, amount: u16) -> Result { + self.bv_extend(Tag::BvSext, x, amount) + } + + pub fn bv_ite( + &mut self, + cond: NodeRef, + then_value: NodeRef, + else_value: NodeRef, + ) -> Result { + self.expect_bool(cond)?; + let width = self.expect_same_bv_width(then_value, else_value, "BV_ITE")?; + self.push_node(Tag::BvIte, width, &[cond, then_value, else_value], 0, 0, 0) + } + + pub fn bv_select( + &mut self, + selectors: &[NodeRef], + values: &[NodeRef], + default: NodeRef, + ) -> Result { + if selectors.len() != values.len() { + return Err(WireError::invalid( + "BV_SELECT", + format!("{} selectors but {} values", selectors.len(), values.len()), + )); + } + if selectors.len() > 127 { + return Err(WireError::invalid( + "BV_SELECT", + format!("{} pairs exceeds v1 limit of 127", selectors.len()), + )); + } + let width = self.expect_bv(default)?; + let mut children = Vec::with_capacity(selectors.len() * 2 + 1); + for (&selector, &value) in selectors.iter().zip(values) { + self.expect_bool(selector)?; + let value_width = self.expect_bv(value)?; + if value_width != width { + return Err(WireError::invalid( + "BV_SELECT", + format!("value width {value_width} does not match default width {width}"), + )); + } + children.push(selector); + children.push(value); + } + children.push(default); + self.push_node( + Tag::BvSelect, + width, + &children, + selectors.len() as u16, + 0, + 0, + ) + } + + pub fn bool_true(&mut self) -> Result { + self.push_node(Tag::BoolTrue, 0, &[], 0, 0, 0) + } + + pub fn bool_false(&mut self) -> Result { + self.push_node(Tag::BoolFalse, 0, &[], 0, 0, 0) + } + + pub fn bool_var(&mut self, name: &str) -> Result { + let payload = self.push_blob(name.as_bytes())?.to_payload(); + self.push_node(Tag::BoolVar, 0, &[], 0, 0, payload) + } + + pub fn bool_not(&mut self, x: NodeRef) -> Result { + self.expect_bool(x)?; + self.push_node(Tag::BoolNot, 0, &[x], 0, 0, 0) + } + + pub fn bool_and(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bool_binary(Tag::BoolAnd, a, b) + } + + pub fn bool_or(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bool_binary(Tag::BoolOr, a, b) + } + + pub fn bool_implies(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bool_binary(Tag::BoolImplies, a, b) + } + + pub fn bv_eq(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_comparison(Tag::BvEq, a, b) + } + + pub fn bv_ult(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_comparison(Tag::BvUlt, a, b) + } + + pub fn bv_ule(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_comparison(Tag::BvUle, a, b) + } + + pub fn bv_slt(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_comparison(Tag::BvSlt, a, b) + } + + pub fn bv_sle(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_comparison(Tag::BvSle, a, b) + } + + pub fn uadd_ovf(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.overflow_binary(Tag::UaddOvf, a, b) + } + + pub fn sadd_ovf(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.overflow_binary(Tag::SaddOvf, a, b) + } + + pub fn usub_ovf(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.overflow_binary(Tag::UsubOvf, a, b) + } + + pub fn ssub_ovf(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.overflow_binary(Tag::SsubOvf, a, b) + } + + pub fn umul_ovf(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.overflow_binary(Tag::UmulOvf, a, b) + } + + pub fn smul_ovf(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.overflow_binary(Tag::SmulOvf, a, b) + } + + pub fn neg_ovf(&mut self, x: NodeRef) -> Result { + self.expect_bv(x)?; + self.push_node(Tag::NegOvf, 0, &[x], 0, 0, 0) + } + + pub fn sdiv_ovf(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.overflow_binary(Tag::SdivOvf, a, b) + } + + pub fn bv_ne(&mut self, a: NodeRef, b: NodeRef) -> Result { + let eq = self.bv_eq(a, b)?; + self.bool_not(eq) + } + + pub fn bv_ugt(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_ult(b, a) + } + + pub fn bv_uge(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_ule(b, a) + } + + pub fn bv_sgt(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_slt(b, a) + } + + pub fn bv_sge(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.bv_sle(b, a) + } + + pub fn bool_eq(&mut self, a: NodeRef, b: NodeRef) -> Result { + self.expect_bool(a)?; + self.expect_bool(b)?; + let not_b = self.bool_not(b)?; + let left = self.bool_or(a, not_b)?; + let not_a = self.bool_not(a)?; + let right = self.bool_or(not_a, b)?; + self.bool_and(left, right) + } + + pub fn bool_xor(&mut self, a: NodeRef, b: NodeRef) -> Result { + let eq = self.bool_eq(a, b)?; + self.bool_not(eq) + } + + pub fn bool_ite( + &mut self, + cond: NodeRef, + then_value: NodeRef, + else_value: NodeRef, + ) -> Result { + self.expect_bool(cond)?; + self.expect_bool(then_value)?; + self.expect_bool(else_value)?; + let cond_then = self.bool_and(cond, then_value)?; + let not_cond = self.bool_not(cond)?; + let else_branch = self.bool_and(not_cond, else_value)?; + self.bool_or(cond_then, else_branch) + } + + pub fn bv_rotate_left(&mut self, x: NodeRef, amount: u64) -> Result { + let width = self.expect_bv(x)?; + let amount = amount % u64::from(width); + if amount == 0 { + return Ok(x); + } + let left_amount = self.bv_const(amount, width)?; + let right_amount = self.bv_const(u64::from(width) - amount, width)?; + let left = self.bv_shl(x, left_amount)?; + let right = self.bv_lshr(x, right_amount)?; + self.bv_or(left, right) + } + + pub fn bv_rotate_right(&mut self, x: NodeRef, amount: u64) -> Result { + let width = self.expect_bv(x)?; + let amount = amount % u64::from(width); + if amount == 0 { + return Ok(x); + } + let right_amount = self.bv_const(amount, width)?; + let left_amount = self.bv_const(u64::from(width) - amount, width)?; + let right = self.bv_lshr(x, right_amount)?; + let left = self.bv_shl(x, left_amount)?; + self.bv_or(right, left) + } + + pub fn assert_mutex(&mut self, selectors: &[NodeRef]) -> Result<()> { + for &selector in selectors { + self.expect_bool(selector)?; + } + for i in 0..selectors.len() { + for j in (i + 1)..selectors.len() { + let both = self.bool_and(selectors[i], selectors[j])?; + let not_both = self.bool_not(both)?; + self.assert(not_both)?; + } + } + Ok(()) + } + + fn push_blob(&mut self, bytes: &[u8]) -> Result { + let offset = u32::try_from(self.blob.len()).map_err(|_| { + WireError::invalid("blob table", "current blob length exceeds u32::MAX") + })?; + let len = u32::try_from(bytes.len()) + .map_err(|_| WireError::invalid("blob table", "blob entry exceeds u32::MAX"))?; + let end = offset + .checked_add(len) + .ok_or(WireError::IntegerOverflow("blob table append"))?; + if end as usize > u32::MAX as usize { + return Err(WireError::invalid( + "blob table", + "blob table length exceeds u32::MAX", + )); + } + self.blob.extend_from_slice(bytes); + Ok(BlobRef::new(offset, len)) + } + + fn push_node( + &mut self, + tag: Tag, + width: u32, + children: &[NodeRef], + aux_hi: u16, + aux_lo: u32, + payload: u64, + ) -> Result { + let arity = u8::try_from(children.len()).map_err(|_| { + WireError::invalid( + "node arity", + format!("{} children exceeds u8::MAX", children.len()), + ) + })?; + for child in children { + self.meta_for(*child)?; + } + let child_start = u32::try_from(self.children.len()).map_err(|_| { + WireError::invalid("child array", "child array length exceeds u32::MAX") + })?; + self.children.extend_from_slice(children); + let index = u32::try_from(self.nodes.len()) + .map_err(|_| WireError::invalid("node array", "node count exceeds u32::MAX"))?; + let id = NodeRef::new(tag.result_sort(), index)?; + self.nodes.push(RawNode::new( + tag.into(), + arity, + aux_hi, + width, + aux_lo, + if arity == 0 { 0 } else { child_start }, + payload, + )); + self.meta.push(NodeMeta { + sort: tag.result_sort(), + width, + }); + Ok(id) + } + + fn meta_for(&self, reference: NodeRef) -> Result { + let meta = self + .meta + .get(reference.index() as usize) + .copied() + .ok_or_else(|| { + WireError::invalid( + "node reference", + format!("reference {:#010x} points outside builder", reference.raw()), + ) + })?; + if meta.sort != reference.sort() { + return Err(WireError::invalid( + "node reference", + format!( + "reference {:#010x} has sort {:?}, node has {:?}", + reference.raw(), + reference.sort(), + meta.sort + ), + )); + } + Ok(meta) + } + + fn expect_bv(&self, reference: NodeRef) -> Result { + let meta = self.meta_for(reference)?; + if meta.sort != Sort::Bv { + return Err(WireError::invalid( + "sort", + format!("expected BV reference, got {reference:?}"), + )); + } + Ok(meta.width) + } + + fn expect_bool(&self, reference: NodeRef) -> Result<()> { + let meta = self.meta_for(reference)?; + if meta.sort != Sort::Bool { + return Err(WireError::invalid( + "sort", + format!("expected Bool reference, got {reference:?}"), + )); + } + Ok(()) + } + + fn expect_same_bv_width(&self, a: NodeRef, b: NodeRef, context: &'static str) -> Result { + let lhs = self.expect_bv(a)?; + let rhs = self.expect_bv(b)?; + if lhs != rhs { + return Err(WireError::invalid( + context, + format!("width mismatch: {lhs} vs {rhs}"), + )); + } + Ok(lhs) + } + + fn bv_unary(&mut self, tag: Tag, x: NodeRef) -> Result { + let width = self.expect_bv(x)?; + self.push_node(tag, width, &[x], 0, 0, 0) + } + + fn bv_binary(&mut self, tag: Tag, a: NodeRef, b: NodeRef) -> Result { + let width = self.expect_same_bv_width(a, b, tag.name())?; + self.push_node(tag, width, &[a, b], 0, 0, 0) + } + + fn bv_extend(&mut self, tag: Tag, x: NodeRef, amount: u16) -> Result { + let width = self + .expect_bv(x)? + .checked_add(u32::from(amount)) + .ok_or(WireError::IntegerOverflow("BV extension width"))?; + validate_bv_width_value(width, "BV extension width")?; + self.push_node(tag, width, &[x], amount, 0, 0) + } + + fn bool_binary(&mut self, tag: Tag, a: NodeRef, b: NodeRef) -> Result { + self.expect_bool(a)?; + self.expect_bool(b)?; + self.push_node(tag, 0, &[a, b], 0, 0, 0) + } + + fn bv_comparison(&mut self, tag: Tag, a: NodeRef, b: NodeRef) -> Result { + self.expect_same_bv_width(a, b, tag.name())?; + self.push_node(tag, 0, &[a, b], 0, 0, 0) + } + + fn overflow_binary(&mut self, tag: Tag, a: NodeRef, b: NodeRef) -> Result { + self.expect_same_bv_width(a, b, tag.name())?; + self.push_node(tag, 0, &[a, b], 0, 0, 0) + } + + fn mark_ref(&self, reference: NodeRef, marked: &mut [bool]) -> Result<()> { + self.meta_for(reference)?; + let index = reference.index() as usize; + if marked[index] { + return Ok(()); + } + marked[index] = true; + let node = self.nodes[index]; + for child_offset in 0..node.arity as usize { + let child = self.children[node.children as usize + child_offset]; + self.mark_ref(child, marked)?; + } + Ok(()) + } +} + +fn mask_u64(value: u64, width: u32) -> u64 { + if width >= 64 { + value + } else { + value & ((1u64 << width) - 1) + } +} + +fn mask_unused_high_bits(bytes: &mut [u8], width: u32) { + let valid_bits = width % 8; + if valid_bits == 0 || bytes.is_empty() { + return; + } + let mask = (1u8 << valid_bits) - 1; + let last = bytes.len() - 1; + bytes[last] &= mask; +} + +``` + +`crates/smt-wire/src/client.rs`: + +```rs +use std::fmt; +use std::io::{Read, Write}; +use std::net::{TcpStream, ToSocketAddrs}; +use std::string::FromUtf8Error; +use std::time::Duration; + +use crate::{BinaryResponse, WireError}; + +#[derive(Debug)] +pub enum ClientError { + Io(std::io::Error), + Wire(WireError), + Utf8(FromUtf8Error), + FrameTooLarge(usize), +} + +pub type ClientResult = std::result::Result; + +impl fmt::Display for ClientError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ClientError::Io(err) => write!(f, "I/O error: {err}"), + ClientError::Wire(err) => write!(f, "wire-format error: {err}"), + ClientError::Utf8(err) => write!(f, "UTF-8 error: {err}"), + ClientError::FrameTooLarge(len) => { + write!( + f, + "frame payload is too large for u32 length prefix: {len} bytes" + ) + } + } + } +} + +impl std::error::Error for ClientError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ClientError::Io(err) => Some(err), + ClientError::Wire(err) => Some(err), + ClientError::Utf8(err) => Some(err), + ClientError::FrameTooLarge(_) => None, + } + } +} + +impl From for ClientError { + fn from(err: std::io::Error) -> Self { + ClientError::Io(err) + } +} + +impl From for ClientError { + fn from(err: WireError) -> Self { + ClientError::Wire(err) + } +} + +impl From for ClientError { + fn from(err: FromUtf8Error) -> Self { + ClientError::Utf8(err) + } +} + +pub struct TcpClient { + stream: TcpStream, +} + +impl TcpClient { + pub fn connect(addr: impl ToSocketAddrs) -> ClientResult { + Ok(Self { + stream: TcpStream::connect(addr)?, + }) + } + + pub fn from_stream(stream: TcpStream) -> Self { + Self { stream } + } + + pub fn set_read_timeout(&self, timeout: Option) -> std::io::Result<()> { + self.stream.set_read_timeout(timeout) + } + + pub fn set_write_timeout(&self, timeout: Option) -> std::io::Result<()> { + self.stream.set_write_timeout(timeout) + } + + pub fn send_payload(&mut self, payload: &[u8]) -> ClientResult> { + let len = + u32::try_from(payload.len()).map_err(|_| ClientError::FrameTooLarge(payload.len()))?; + self.stream.write_all(&len.to_le_bytes())?; + self.stream.write_all(payload)?; + + let mut len_bytes = [0u8; 4]; + self.stream.read_exact(&mut len_bytes)?; + let response_len = u32::from_le_bytes(len_bytes) as usize; + let mut response = vec![0u8; response_len]; + self.stream.read_exact(&mut response)?; + Ok(response) + } + + pub fn send_binary_request(&mut self, request: &[u8]) -> ClientResult { + Ok(BinaryResponse::parse(&self.send_payload(request)?)?) + } + + pub fn send_text_bytes(&mut self, script: &[u8]) -> ClientResult> { + self.send_payload(script) + } + + pub fn send_text(&mut self, script: &str) -> ClientResult { + Ok(String::from_utf8(self.send_text_bytes(script.as_bytes())?)?) + } +} + +``` + +`crates/smt-wire/src/constants.rs`: + +```rs +use crate::types::Sort; + +pub const EXPR_MAGIC: [u8; 4] = *b"SMT\0"; +pub const REQUEST_MAGIC: [u8; 4] = *b"SMTQ"; +pub const RESPONSE_MAGIC: [u8; 4] = *b"SMTR"; +pub const VERSION: u8 = 1; + +pub const EXPR_HEADER_LEN: usize = 32; +pub const NODE_RECORD_LEN: usize = 24; +pub const REQUEST_ENVELOPE_LEN: usize = 32; +pub const RESPONSE_ENVELOPE_LEN: usize = 16; + +pub mod tag { + pub const BV_VAR: u8 = 0; + pub const BV_CONST: u8 = 1; + pub const BV_NOT: u8 = 2; + pub const BV_NEG: u8 = 3; + pub const BV_AND: u8 = 4; + pub const BV_OR: u8 = 5; + pub const BV_XOR: u8 = 6; + pub const BV_ADD: u8 = 7; + pub const BV_SUB: u8 = 8; + pub const BV_MUL: u8 = 9; + pub const BV_UDIV: u8 = 10; + pub const BV_UREM: u8 = 11; + pub const BV_SDIV: u8 = 12; + pub const BV_SREM: u8 = 13; + pub const BV_SMOD: u8 = 14; + pub const BV_SHL: u8 = 15; + pub const BV_LSHR: u8 = 16; + pub const BV_ASHR: u8 = 17; + pub const BV_EXTRACT: u8 = 18; + pub const BV_CONCAT: u8 = 19; + pub const BV_ZEXT: u8 = 20; + pub const BV_SEXT: u8 = 21; + pub const BV_ITE: u8 = 22; + pub const BV_SELECT: u8 = 23; + pub const BOOL_TRUE: u8 = 24; + pub const BOOL_FALSE: u8 = 25; + pub const BOOL_VAR: u8 = 26; + pub const BOOL_NOT: u8 = 27; + pub const BOOL_AND: u8 = 28; + pub const BOOL_OR: u8 = 29; + pub const BOOL_IMPLIES: u8 = 30; + pub const BV_EQ: u8 = 31; + pub const BV_ULT: u8 = 32; + pub const BV_ULE: u8 = 33; + pub const BV_SLT: u8 = 34; + pub const BV_SLE: u8 = 35; + pub const UADD_OVF: u8 = 36; + pub const SADD_OVF: u8 = 37; + pub const USUB_OVF: u8 = 38; + pub const SSUB_OVF: u8 = 39; + pub const UMUL_OVF: u8 = 40; + pub const SMUL_OVF: u8 = 41; + pub const NEG_OVF: u8 = 42; + pub const SDIV_OVF: u8 = 43; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum Tag { + BvVar = tag::BV_VAR, + BvConst = tag::BV_CONST, + BvNot = tag::BV_NOT, + BvNeg = tag::BV_NEG, + BvAnd = tag::BV_AND, + BvOr = tag::BV_OR, + BvXor = tag::BV_XOR, + BvAdd = tag::BV_ADD, + BvSub = tag::BV_SUB, + BvMul = tag::BV_MUL, + BvUdiv = tag::BV_UDIV, + BvUrem = tag::BV_UREM, + BvSdiv = tag::BV_SDIV, + BvSrem = tag::BV_SREM, + BvSmod = tag::BV_SMOD, + BvShl = tag::BV_SHL, + BvLshr = tag::BV_LSHR, + BvAshr = tag::BV_ASHR, + BvExtract = tag::BV_EXTRACT, + BvConcat = tag::BV_CONCAT, + BvZext = tag::BV_ZEXT, + BvSext = tag::BV_SEXT, + BvIte = tag::BV_ITE, + BvSelect = tag::BV_SELECT, + BoolTrue = tag::BOOL_TRUE, + BoolFalse = tag::BOOL_FALSE, + BoolVar = tag::BOOL_VAR, + BoolNot = tag::BOOL_NOT, + BoolAnd = tag::BOOL_AND, + BoolOr = tag::BOOL_OR, + BoolImplies = tag::BOOL_IMPLIES, + BvEq = tag::BV_EQ, + BvUlt = tag::BV_ULT, + BvUle = tag::BV_ULE, + BvSlt = tag::BV_SLT, + BvSle = tag::BV_SLE, + UaddOvf = tag::UADD_OVF, + SaddOvf = tag::SADD_OVF, + UsubOvf = tag::USUB_OVF, + SsubOvf = tag::SSUB_OVF, + UmulOvf = tag::UMUL_OVF, + SmulOvf = tag::SMUL_OVF, + NegOvf = tag::NEG_OVF, + SdivOvf = tag::SDIV_OVF, +} + +impl TryFrom for Tag { + type Error = (); + + fn try_from(value: u8) -> core::result::Result { + Ok(match value { + tag::BV_VAR => Tag::BvVar, + tag::BV_CONST => Tag::BvConst, + tag::BV_NOT => Tag::BvNot, + tag::BV_NEG => Tag::BvNeg, + tag::BV_AND => Tag::BvAnd, + tag::BV_OR => Tag::BvOr, + tag::BV_XOR => Tag::BvXor, + tag::BV_ADD => Tag::BvAdd, + tag::BV_SUB => Tag::BvSub, + tag::BV_MUL => Tag::BvMul, + tag::BV_UDIV => Tag::BvUdiv, + tag::BV_UREM => Tag::BvUrem, + tag::BV_SDIV => Tag::BvSdiv, + tag::BV_SREM => Tag::BvSrem, + tag::BV_SMOD => Tag::BvSmod, + tag::BV_SHL => Tag::BvShl, + tag::BV_LSHR => Tag::BvLshr, + tag::BV_ASHR => Tag::BvAshr, + tag::BV_EXTRACT => Tag::BvExtract, + tag::BV_CONCAT => Tag::BvConcat, + tag::BV_ZEXT => Tag::BvZext, + tag::BV_SEXT => Tag::BvSext, + tag::BV_ITE => Tag::BvIte, + tag::BV_SELECT => Tag::BvSelect, + tag::BOOL_TRUE => Tag::BoolTrue, + tag::BOOL_FALSE => Tag::BoolFalse, + tag::BOOL_VAR => Tag::BoolVar, + tag::BOOL_NOT => Tag::BoolNot, + tag::BOOL_AND => Tag::BoolAnd, + tag::BOOL_OR => Tag::BoolOr, + tag::BOOL_IMPLIES => Tag::BoolImplies, + tag::BV_EQ => Tag::BvEq, + tag::BV_ULT => Tag::BvUlt, + tag::BV_ULE => Tag::BvUle, + tag::BV_SLT => Tag::BvSlt, + tag::BV_SLE => Tag::BvSle, + tag::UADD_OVF => Tag::UaddOvf, + tag::SADD_OVF => Tag::SaddOvf, + tag::USUB_OVF => Tag::UsubOvf, + tag::SSUB_OVF => Tag::SsubOvf, + tag::UMUL_OVF => Tag::UmulOvf, + tag::SMUL_OVF => Tag::SmulOvf, + tag::NEG_OVF => Tag::NegOvf, + tag::SDIV_OVF => Tag::SdivOvf, + _ => return Err(()), + }) + } +} + +impl From for u8 { + fn from(value: Tag) -> Self { + value as u8 + } +} + +impl Tag { + pub const fn name(self) -> &'static str { + match self { + Tag::BvVar => "BV_VAR", + Tag::BvConst => "BV_CONST", + Tag::BvNot => "BV_NOT", + Tag::BvNeg => "BV_NEG", + Tag::BvAnd => "BV_AND", + Tag::BvOr => "BV_OR", + Tag::BvXor => "BV_XOR", + Tag::BvAdd => "BV_ADD", + Tag::BvSub => "BV_SUB", + Tag::BvMul => "BV_MUL", + Tag::BvUdiv => "BV_UDIV", + Tag::BvUrem => "BV_UREM", + Tag::BvSdiv => "BV_SDIV", + Tag::BvSrem => "BV_SREM", + Tag::BvSmod => "BV_SMOD", + Tag::BvShl => "BV_SHL", + Tag::BvLshr => "BV_LSHR", + Tag::BvAshr => "BV_ASHR", + Tag::BvExtract => "BV_EXTRACT", + Tag::BvConcat => "BV_CONCAT", + Tag::BvZext => "BV_ZEXT", + Tag::BvSext => "BV_SEXT", + Tag::BvIte => "BV_ITE", + Tag::BvSelect => "BV_SELECT", + Tag::BoolTrue => "BOOL_TRUE", + Tag::BoolFalse => "BOOL_FALSE", + Tag::BoolVar => "BOOL_VAR", + Tag::BoolNot => "BOOL_NOT", + Tag::BoolAnd => "BOOL_AND", + Tag::BoolOr => "BOOL_OR", + Tag::BoolImplies => "BOOL_IMPLIES", + Tag::BvEq => "BV_EQ", + Tag::BvUlt => "BV_ULT", + Tag::BvUle => "BV_ULE", + Tag::BvSlt => "BV_SLT", + Tag::BvSle => "BV_SLE", + Tag::UaddOvf => "UADD_OVF", + Tag::SaddOvf => "SADD_OVF", + Tag::UsubOvf => "USUB_OVF", + Tag::SsubOvf => "SSUB_OVF", + Tag::UmulOvf => "UMUL_OVF", + Tag::SmulOvf => "SMUL_OVF", + Tag::NegOvf => "NEG_OVF", + Tag::SdivOvf => "SDIV_OVF", + } + } + + pub const fn result_sort(self) -> Sort { + match self { + Tag::BvVar + | Tag::BvConst + | Tag::BvNot + | Tag::BvNeg + | Tag::BvAnd + | Tag::BvOr + | Tag::BvXor + | Tag::BvAdd + | Tag::BvSub + | Tag::BvMul + | Tag::BvUdiv + | Tag::BvUrem + | Tag::BvSdiv + | Tag::BvSrem + | Tag::BvSmod + | Tag::BvShl + | Tag::BvLshr + | Tag::BvAshr + | Tag::BvExtract + | Tag::BvConcat + | Tag::BvZext + | Tag::BvSext + | Tag::BvIte + | Tag::BvSelect => Sort::Bv, + _ => Sort::Bool, + } + } + + pub const fn fixed_arity(self) -> Option { + Some(match self { + Tag::BvVar | Tag::BvConst | Tag::BoolTrue | Tag::BoolFalse | Tag::BoolVar => 0, + Tag::BvNot + | Tag::BvNeg + | Tag::BvExtract + | Tag::BvZext + | Tag::BvSext + | Tag::BoolNot + | Tag::NegOvf => 1, + Tag::BvAnd + | Tag::BvOr + | Tag::BvXor + | Tag::BvAdd + | Tag::BvSub + | Tag::BvMul + | Tag::BvUdiv + | Tag::BvUrem + | Tag::BvSdiv + | Tag::BvSrem + | Tag::BvSmod + | Tag::BvShl + | Tag::BvLshr + | Tag::BvAshr + | Tag::BvConcat + | Tag::BoolAnd + | Tag::BoolOr + | Tag::BoolImplies + | Tag::BvEq + | Tag::BvUlt + | Tag::BvUle + | Tag::BvSlt + | Tag::BvSle + | Tag::UaddOvf + | Tag::SaddOvf + | Tag::UsubOvf + | Tag::SsubOvf + | Tag::UmulOvf + | Tag::SmulOvf + | Tag::SdivOvf => 2, + Tag::BvIte => 3, + Tag::BvSelect => return None, + }) + } +} + +pub mod command { + pub const SOLVE: u8 = 0; + pub const SIMPLIFY: u8 = 1; + pub const MINIMIZE: u8 = 2; + pub const MAXIMIZE: u8 = 3; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum Command { + Solve = command::SOLVE, + Simplify = command::SIMPLIFY, + Minimize = command::MINIMIZE, + Maximize = command::MAXIMIZE, +} + +impl TryFrom for Command { + type Error = (); + + fn try_from(value: u8) -> core::result::Result { + Ok(match value { + command::SOLVE => Command::Solve, + command::SIMPLIFY => Command::Simplify, + command::MINIMIZE => Command::Minimize, + command::MAXIMIZE => Command::Maximize, + _ => return Err(()), + }) + } +} + +impl From for u8 { + fn from(value: Command) -> Self { + value as u8 + } +} + +pub mod request_flags { + pub const WANT_MODEL: u8 = 1 << 0; + pub const WANT_CORE: u8 = 1 << 1; + pub const SIGNED: u8 = 1 << 2; + pub const ALL: u8 = WANT_MODEL | WANT_CORE | SIGNED; +} + +pub mod status { + pub const OK: u8 = 0; + pub const SAT: u8 = 1; + pub const UNSAT: u8 = 2; + pub const UNKNOWN: u8 = 3; + pub const ERROR: u8 = 4; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum Status { + Ok = status::OK, + Sat = status::SAT, + Unsat = status::UNSAT, + Unknown = status::UNKNOWN, + Error = status::ERROR, +} + +impl TryFrom for Status { + type Error = (); + + fn try_from(value: u8) -> core::result::Result { + Ok(match value { + status::OK => Status::Ok, + status::SAT => Status::Sat, + status::UNSAT => Status::Unsat, + status::UNKNOWN => Status::Unknown, + status::ERROR => Status::Error, + _ => return Err(()), + }) + } +} + +impl From for u8 { + fn from(value: Status) -> Self { + value as u8 + } +} + +pub mod response_flags { + pub const HAS_MODEL: u8 = 1 << 0; + pub const HAS_CORE: u8 = 1 << 1; + pub const HAS_EXPR: u8 = 1 << 2; + pub const HAS_VALUE: u8 = 1 << 3; + pub const HAS_MESSAGE: u8 = 1 << 4; + pub const ALL: u8 = HAS_MODEL | HAS_CORE | HAS_EXPR | HAS_VALUE | HAS_MESSAGE; +} + +``` + +`crates/smt-wire/src/error.rs`: + +```rs +use core::fmt; + +/// Errors returned while decoding, validating, or building wire-format data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WireError { + /// A byte slice ended before the requested field could be decoded. + UnexpectedEof { + context: &'static str, + needed: usize, + actual: usize, + }, + /// A length derived from headers did not match the enclosing slice length. + LengthMismatch { + context: &'static str, + expected: usize, + actual: usize, + }, + /// A magic value did not match the v1 wire contract. + BadMagic { + context: &'static str, + expected: &'static [u8], + actual: Vec, + }, + /// The encoded version is not supported by this crate. + UnsupportedVersion(u8), + /// Integer arithmetic overflowed while computing a layout or length. + IntegerOverflow(&'static str), + /// A field value is syntactically decoded but semantically invalid. + InvalidValue { + context: &'static str, + message: String, + }, + /// A byte range that must contain UTF-8 does not. + InvalidUtf8 { + context: &'static str, + offset: u32, + len: u32, + }, +} + +pub type Result = core::result::Result; + +impl WireError { + pub fn invalid(context: &'static str, message: impl Into) -> Self { + WireError::InvalidValue { + context, + message: message.into(), + } + } +} + +impl fmt::Display for WireError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WireError::UnexpectedEof { + context, + needed, + actual, + } => write!( + f, + "unexpected end of input while reading {context}: needed {needed} bytes, got {actual}" + ), + WireError::LengthMismatch { + context, + expected, + actual, + } => write!( + f, + "length mismatch for {context}: expected {expected} bytes, got {actual}" + ), + WireError::BadMagic { + context, + expected, + actual, + } => write!( + f, + "bad magic for {context}: expected {:?}, got {:?}", + expected, actual + ), + WireError::UnsupportedVersion(version) => { + write!(f, "unsupported SMT wire version {version}") + } + WireError::IntegerOverflow(context) => { + write!(f, "integer overflow while computing {context}") + } + WireError::InvalidValue { context, message } => { + write!(f, "invalid {context}: {message}") + } + WireError::InvalidUtf8 { + context, + offset, + len, + } => write!( + f, + "invalid UTF-8 in {context} at blob offset {offset} with length {len}" + ), + } + } +} + +impl std::error::Error for WireError {} + +``` + +`crates/smt-wire/src/expr.rs`: + +```rs +use crate::constants::{self, tag, Tag, EXPR_HEADER_LEN, NODE_RECORD_LEN}; +use crate::error::{Result, WireError}; +use crate::le; +use crate::types::{BlobRef, NodeRef, Sort}; + +pub const MAX_BV_WIDTH: u32 = 65_536; +pub const MAX_NODE_COUNT: u32 = 1 << 31; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ExprHeader { + pub node_count: u32, + pub child_count: u32, + pub blob_len: u32, +} + +impl ExprHeader { + pub fn total_len(self) -> Result { + expression_len(self.node_count, self.child_count, self.blob_len) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct RawNode { + pub tag: u8, + pub arity: u8, + pub aux_hi: u16, + pub width: u32, + pub aux_lo: u32, + pub children: u32, + pub payload: u64, +} + +impl RawNode { + pub const fn new( + tag: u8, + arity: u8, + aux_hi: u16, + width: u32, + aux_lo: u32, + children: u32, + payload: u64, + ) -> Self { + Self { + tag, + arity, + aux_hi, + width, + aux_lo, + children, + payload, + } + } + + pub fn decode(bytes: &[u8], offset: usize) -> Result { + Ok(Self { + tag: le::read_u8(bytes, offset, "node tag")?, + arity: le::read_u8(bytes, offset + 1, "node arity")?, + aux_hi: le::read_u16(bytes, offset + 2, "node aux_hi")?, + width: le::read_u32(bytes, offset + 4, "node width")?, + aux_lo: le::read_u32(bytes, offset + 8, "node aux_lo")?, + children: le::read_u32(bytes, offset + 12, "node children")?, + payload: le::read_u64(bytes, offset + 16, "node payload")?, + }) + } + + pub fn encode(&self, dst: &mut Vec) { + le::write_u8(dst, self.tag); + le::write_u8(dst, self.arity); + le::write_u16(dst, self.aux_hi); + le::write_u32(dst, self.width); + le::write_u32(dst, self.aux_lo); + le::write_u32(dst, self.children); + le::write_u64(dst, self.payload); + } + + pub const fn blob_ref(self) -> BlobRef { + BlobRef::from_payload(self.payload) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExpressionBuffer { + bytes: Vec, +} + +impl ExpressionBuffer { + pub fn empty() -> Self { + Self::from_parts(&[], &[], &[]).expect("empty expression buffer is always valid") + } + + pub fn from_parts(nodes: &[RawNode], children: &[NodeRef], blob: &[u8]) -> Result { + let node_count = u32::try_from(nodes.len()).map_err(|_| { + WireError::invalid( + "expression buffer", + format!("node count {} exceeds u32::MAX", nodes.len()), + ) + })?; + if node_count > MAX_NODE_COUNT { + return Err(WireError::invalid( + "expression buffer", + format!("node count {node_count} exceeds 2^31"), + )); + } + let child_count = u32::try_from(children.len()).map_err(|_| { + WireError::invalid( + "expression buffer", + format!("child count {} exceeds u32::MAX", children.len()), + ) + })?; + let blob_len = u32::try_from(blob.len()).map_err(|_| { + WireError::invalid( + "expression buffer", + format!("blob length {} exceeds u32::MAX", blob.len()), + ) + })?; + let expected_len = expression_len(node_count, child_count, blob_len)?; + let mut out = Vec::with_capacity(expected_len); + out.extend_from_slice(&constants::EXPR_MAGIC); + le::write_u8(&mut out, constants::VERSION); + out.extend_from_slice(&[0u8; 3]); + le::write_u32(&mut out, node_count); + le::write_u32(&mut out, child_count); + le::write_u32(&mut out, blob_len); + out.extend_from_slice(&[0u8; 12]); + for node in nodes { + node.encode(&mut out); + } + for child in children { + le::write_u32(&mut out, child.raw()); + } + out.extend_from_slice(blob); + debug_assert_eq!(out.len(), expected_len); + Ok(Self { bytes: out }) + } + + pub fn from_bytes(bytes: Vec) -> Result { + ExprView::parse(&bytes)?; + Ok(Self { bytes }) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.bytes + } + + pub fn into_bytes(self) -> Vec { + self.bytes + } + + pub fn view(&self) -> Result> { + ExprView::parse(&self.bytes) + } + + pub fn validate(&self) -> Result<()> { + self.view()?.validate() + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ExprView<'a> { + bytes: &'a [u8], + header: ExprHeader, + node_offset: usize, + child_offset: usize, + blob_offset: usize, +} + +impl<'a> ExprView<'a> { + pub fn parse(bytes: &'a [u8]) -> Result { + if bytes.len() < EXPR_HEADER_LEN { + return Err(WireError::UnexpectedEof { + context: "expression header", + needed: EXPR_HEADER_LEN, + actual: bytes.len(), + }); + } + let magic = le::exact_slice(bytes, 0, 4, "expression magic")?; + if magic != constants::EXPR_MAGIC { + return Err(WireError::BadMagic { + context: "expression buffer", + expected: &constants::EXPR_MAGIC, + actual: magic.to_vec(), + }); + } + let version = le::read_u8(bytes, 4, "expression version")?; + if version != constants::VERSION { + return Err(WireError::UnsupportedVersion(version)); + } + let header = ExprHeader { + node_count: le::read_u32(bytes, 8, "expression node_count")?, + child_count: le::read_u32(bytes, 12, "expression child_count")?, + blob_len: le::read_u32(bytes, 16, "expression blob_len")?, + }; + if header.node_count > MAX_NODE_COUNT { + return Err(WireError::invalid( + "expression node_count", + format!("{} exceeds 2^31", header.node_count), + )); + } + let expected = header.total_len()?; + if bytes.len() != expected { + return Err(WireError::LengthMismatch { + context: "expression buffer", + expected, + actual: bytes.len(), + }); + } + let node_offset = EXPR_HEADER_LEN; + let node_bytes = le::checked_mul( + header.node_count as usize, + NODE_RECORD_LEN, + "expression node array length", + )?; + let child_offset = le::checked_add(node_offset, node_bytes, "expression child offset")?; + let child_bytes = le::checked_mul( + header.child_count as usize, + 4, + "expression child array length", + )?; + let blob_offset = le::checked_add(child_offset, child_bytes, "expression blob offset")?; + Ok(Self { + bytes, + header, + node_offset, + child_offset, + blob_offset, + }) + } + + pub fn parse_and_validate(bytes: &'a [u8]) -> Result { + let view = Self::parse(bytes)?; + view.validate()?; + Ok(view) + } + + pub const fn header(&self) -> ExprHeader { + self.header + } + + pub fn as_bytes(&self) -> &'a [u8] { + self.bytes + } + + pub fn node_count(&self) -> u32 { + self.header.node_count + } + + pub fn child_count(&self) -> u32 { + self.header.child_count + } + + pub fn blob_len(&self) -> u32 { + self.header.blob_len + } + + pub fn node(&self, index: u32) -> Result { + if index >= self.header.node_count { + return Err(WireError::invalid( + "node index", + format!( + "{index} is out of bounds for {} nodes", + self.header.node_count + ), + )); + } + let index_bytes = le::checked_mul(index as usize, NODE_RECORD_LEN, "node offset")?; + let offset = le::checked_add(self.node_offset, index_bytes, "node offset")?; + RawNode::decode(self.bytes, offset) + } + + pub fn child_ref(&self, index: u32) -> Result { + if index >= self.header.child_count { + return Err(WireError::invalid( + "child index", + format!( + "{index} is out of bounds for {} children", + self.header.child_count + ), + )); + } + let index_bytes = le::checked_mul(index as usize, 4, "child offset")?; + let offset = le::checked_add(self.child_offset, index_bytes, "child offset")?; + Ok(NodeRef::from_raw(le::read_u32( + self.bytes, + offset, + "child reference", + )?)) + } + + pub fn blob(&self) -> &'a [u8] { + &self.bytes[self.blob_offset..] + } + + pub fn blob_ref(&self, reference: BlobRef) -> Result<&'a [u8]> { + let start = reference.offset as usize; + let len = reference.len as usize; + let end = start + .checked_add(len) + .ok_or(WireError::IntegerOverflow("blob reference"))?; + if end > self.blob().len() { + return Err(WireError::invalid( + "blob reference", + format!( + "offset {} plus length {} exceeds blob length {}", + reference.offset, + reference.len, + self.blob().len() + ), + )); + } + Ok(&self.blob()[start..end]) + } + + pub fn blob_str(&self, reference: BlobRef, context: &'static str) -> Result<&'a str> { + let bytes = self.blob_ref(reference)?; + core::str::from_utf8(bytes).map_err(|_| WireError::InvalidUtf8 { + context, + offset: reference.offset, + len: reference.len, + }) + } + + pub fn validate(&self) -> Result<()> { + let mut meta = Vec::with_capacity(self.header.node_count as usize); + for index in 0..self.header.node_count { + let node = self.node(index)?; + let tag = parse_tag(node.tag, index)?; + validate_arity(index, tag, node.arity)?; + let sort = tag.result_sort(); + validate_node_width(index, tag, node.width)?; + if matches!(tag, Tag::BvVar | Tag::BoolVar) { + self.blob_str(node.blob_ref(), "symbol name")?; + } + if matches!(tag, Tag::BvConst) && node.width > 64 { + let reference = node.blob_ref(); + let expected_len = bytes_for_width(node.width)?; + if reference.len as usize != expected_len { + return Err(WireError::invalid( + "wide BV_CONST", + format!( + "node {index} has blob length {}, expected {expected_len} for width {}", + reference.len, node.width + ), + )); + } + self.blob_ref(reference)?; + } + meta.push(NodeInfo { + tag, + sort, + width: node.width, + }); + } + + for index in 0..self.header.node_count { + let node = self.node(index)?; + let info = meta[index as usize]; + validate_children_range(self.header.child_count, index, &node)?; + match info.tag { + Tag::BvVar | Tag::BvConst | Tag::BoolTrue | Tag::BoolFalse | Tag::BoolVar => {} + Tag::BvNot | Tag::BvNeg => { + let child = self.expect_child(&meta, index, &node, 0, Sort::Bv)?; + expect_width(index, info.tag, node.width, child.width, "unary BV result")?; + } + Tag::BvAnd + | Tag::BvOr + | Tag::BvXor + | Tag::BvAdd + | Tag::BvSub + | Tag::BvMul + | Tag::BvUdiv + | Tag::BvUrem + | Tag::BvSdiv + | Tag::BvSrem + | Tag::BvSmod + | Tag::BvShl + | Tag::BvLshr + | Tag::BvAshr => { + let lhs = self.expect_child(&meta, index, &node, 0, Sort::Bv)?; + let rhs = self.expect_child(&meta, index, &node, 1, Sort::Bv)?; + expect_same_child_width(index, info.tag, lhs.width, rhs.width)?; + expect_width(index, info.tag, node.width, lhs.width, "binary BV result")?; + } + Tag::BvExtract => { + let child = self.expect_child(&meta, index, &node, 0, Sort::Bv)?; + let lo = node.aux_lo; + let hi = u32::from(node.aux_hi); + if lo > hi || hi >= child.width { + return Err(WireError::invalid( + "BV_EXTRACT bounds", + format!( + "node {index} has lo={lo}, hi={hi}, child width {}", + child.width + ), + )); + } + let expected = hi - lo + 1; + expect_width(index, info.tag, node.width, expected, "extract result")?; + } + Tag::BvConcat => { + let hi = self.expect_child(&meta, index, &node, 0, Sort::Bv)?; + let lo = self.expect_child(&meta, index, &node, 1, Sort::Bv)?; + let expected = hi + .width + .checked_add(lo.width) + .ok_or(WireError::IntegerOverflow("BV_CONCAT width"))?; + validate_bv_width_value(expected, "BV_CONCAT result")?; + expect_width(index, info.tag, node.width, expected, "concat result")?; + } + Tag::BvZext | Tag::BvSext => { + let child = self.expect_child(&meta, index, &node, 0, Sort::Bv)?; + let expected = child + .width + .checked_add(u32::from(node.aux_hi)) + .ok_or(WireError::IntegerOverflow("BV_EXT width"))?; + validate_bv_width_value(expected, "BV_EXT result")?; + expect_width(index, info.tag, node.width, expected, "extension result")?; + } + Tag::BvIte => { + self.expect_child(&meta, index, &node, 0, Sort::Bool)?; + let then_child = self.expect_child(&meta, index, &node, 1, Sort::Bv)?; + let else_child = self.expect_child(&meta, index, &node, 2, Sort::Bv)?; + expect_same_child_width(index, info.tag, then_child.width, else_child.width)?; + expect_width( + index, + info.tag, + node.width, + then_child.width, + "BV_ITE result", + )?; + } + Tag::BvSelect => { + validate_select_shape(index, &node)?; + let pairs = usize::from(node.aux_hi); + let default = self.expect_child(&meta, index, &node, pairs * 2, Sort::Bv)?; + for pair in 0..pairs { + self.expect_child(&meta, index, &node, pair * 2, Sort::Bool)?; + let value = + self.expect_child(&meta, index, &node, pair * 2 + 1, Sort::Bv)?; + expect_same_child_width(index, info.tag, value.width, default.width)?; + } + expect_width( + index, + info.tag, + node.width, + default.width, + "BV_SELECT result", + )?; + } + Tag::BoolNot => { + self.expect_child(&meta, index, &node, 0, Sort::Bool)?; + } + Tag::BoolAnd | Tag::BoolOr | Tag::BoolImplies => { + self.expect_child(&meta, index, &node, 0, Sort::Bool)?; + self.expect_child(&meta, index, &node, 1, Sort::Bool)?; + } + Tag::BvEq | Tag::BvUlt | Tag::BvUle | Tag::BvSlt | Tag::BvSle => { + let lhs = self.expect_child(&meta, index, &node, 0, Sort::Bv)?; + let rhs = self.expect_child(&meta, index, &node, 1, Sort::Bv)?; + expect_same_child_width(index, info.tag, lhs.width, rhs.width)?; + } + Tag::UaddOvf + | Tag::SaddOvf + | Tag::UsubOvf + | Tag::SsubOvf + | Tag::UmulOvf + | Tag::SmulOvf + | Tag::SdivOvf => { + let lhs = self.expect_child(&meta, index, &node, 0, Sort::Bv)?; + let rhs = self.expect_child(&meta, index, &node, 1, Sort::Bv)?; + expect_same_child_width(index, info.tag, lhs.width, rhs.width)?; + } + Tag::NegOvf => { + self.expect_child(&meta, index, &node, 0, Sort::Bv)?; + } + } + } + Ok(()) + } + + fn expect_child( + &self, + meta: &[NodeInfo], + parent_index: u32, + parent: &RawNode, + child_offset: usize, + expected_sort: Sort, + ) -> Result { + let child_array_index = parent + .children + .checked_add(child_offset as u32) + .ok_or(WireError::IntegerOverflow("child array index"))?; + let reference = self.child_ref(child_array_index)?; + validate_ref( + meta, + reference, + expected_sort, + Some(parent_index), + "child reference", + ) + } +} + +#[derive(Debug, Clone, Copy)] +struct NodeInfo { + tag: Tag, + sort: Sort, + width: u32, +} + +fn expression_len(node_count: u32, child_count: u32, blob_len: u32) -> Result { + let node_bytes = le::checked_mul( + node_count as usize, + NODE_RECORD_LEN, + "expression node array length", + )?; + let after_nodes = le::checked_add(EXPR_HEADER_LEN, node_bytes, "expression length")?; + let child_bytes = le::checked_mul(child_count as usize, 4, "expression child array length")?; + let after_children = le::checked_add(after_nodes, child_bytes, "expression length")?; + le::checked_add(after_children, blob_len as usize, "expression length") +} + +fn parse_tag(raw: u8, node_index: u32) -> Result { + Tag::try_from(raw).map_err(|_| { + WireError::invalid( + "node tag", + format!("node {node_index} uses unknown v1 tag {raw}"), + ) + }) +} + +fn validate_arity(index: u32, tag: Tag, arity: u8) -> Result<()> { + if let Some(expected) = tag.fixed_arity() { + if arity != expected { + return Err(WireError::invalid( + "node arity", + format!( + "node {index} ({}) has arity {arity}, expected {expected}", + tag.name() + ), + )); + } + } else { + validate_select_arity(index, arity)?; + } + Ok(()) +} + +fn validate_select_arity(index: u32, arity: u8) -> Result<()> { + if arity == 0 || arity.is_multiple_of(2) { + return Err(WireError::invalid( + "BV_SELECT arity", + format!("node {index} has arity {arity}, expected odd 2N+1"), + )); + } + Ok(()) +} + +fn validate_select_shape(index: u32, node: &RawNode) -> Result<()> { + validate_select_arity(index, node.arity)?; + let expected_pairs = (u16::from(node.arity) - 1) / 2; + if node.aux_hi != expected_pairs { + return Err(WireError::invalid( + "BV_SELECT aux_hi", + format!( + "node {index} has aux_hi {}, expected {expected_pairs}", + node.aux_hi + ), + )); + } + Ok(()) +} + +fn validate_node_width(index: u32, tag: Tag, width: u32) -> Result<()> { + match tag.result_sort() { + Sort::Bool => { + if width != 0 { + return Err(WireError::invalid( + "Bool node width", + format!( + "node {index} ({}) has width {width}, expected 0", + tag.name() + ), + )); + } + } + Sort::Bv => validate_bv_width_value(width, "BV node width").map_err(|_| { + WireError::invalid( + "BV node width", + format!( + "node {index} ({}) has width {width}, expected 1..={MAX_BV_WIDTH}", + tag.name() + ), + ) + })?, + } + Ok(()) +} + +pub(crate) fn validate_bv_width_value(width: u32, context: &'static str) -> Result<()> { + if !(1..=MAX_BV_WIDTH).contains(&width) { + return Err(WireError::invalid( + context, + format!("width {width} is outside 1..={MAX_BV_WIDTH}"), + )); + } + Ok(()) +} + +fn validate_children_range(child_count: u32, index: u32, node: &RawNode) -> Result<()> { + if node.arity == 0 { + return Ok(()); + } + let end = node + .children + .checked_add(u32::from(node.arity)) + .ok_or(WireError::IntegerOverflow("node child range"))?; + if end > child_count { + return Err(WireError::invalid( + "node child range", + format!( + "node {index} children [{}..{}) exceeds child_count {child_count}", + node.children, end + ), + )); + } + Ok(()) +} + +fn validate_ref( + meta: &[NodeInfo], + reference: NodeRef, + expected_sort: Sort, + parent_index: Option, + context: &'static str, +) -> Result { + let index = reference.index(); + if index as usize >= meta.len() { + return Err(WireError::invalid( + context, + format!( + "reference {:#010x} points outside {} nodes", + reference.raw(), + meta.len() + ), + )); + } + if reference.sort() != expected_sort { + return Err(WireError::invalid( + context, + format!( + "reference {:#010x} has sort {:?}, expected {:?}", + reference.raw(), + reference.sort(), + expected_sort + ), + )); + } + let info = meta[index as usize]; + if info.sort != reference.sort() { + return Err(WireError::invalid( + context, + format!( + "reference {:#010x} sort {:?} does not match node tag sort {:?}", + reference.raw(), + reference.sort(), + info.sort + ), + )); + } + if let Some(parent) = parent_index { + if index >= parent { + return Err(WireError::invalid( + context, + format!("parent node {parent} references non-earlier child node {index}"), + )); + } + } + Ok(info) +} + +fn expect_width( + index: u32, + tag: Tag, + actual: u32, + expected: u32, + context: &'static str, +) -> Result<()> { + if actual != expected { + return Err(WireError::invalid( + context, + format!( + "node {index} ({}) has width {actual}, expected {expected}", + tag.name() + ), + )); + } + Ok(()) +} + +fn expect_same_child_width(index: u32, tag: Tag, lhs: u32, rhs: u32) -> Result<()> { + if lhs != rhs { + return Err(WireError::invalid( + "child widths", + format!( + "node {index} ({}) has child widths {lhs} and {rhs}", + tag.name() + ), + )); + } + Ok(()) +} + +pub(crate) fn bytes_for_width(width: u32) -> Result { + validate_bv_width_value(width, "scalar width")?; + Ok((width as usize).div_ceil(8)) +} + +/// Validate a typed node reference against an already-validated expression view. +pub fn validate_node_ref( + view: &ExprView<'_>, + reference: NodeRef, + expected_sort: Sort, + context: &'static str, +) -> Result { + let index = reference.index(); + if index >= view.node_count() { + return Err(WireError::invalid( + context, + format!( + "reference {:#010x} points outside {} nodes", + reference.raw(), + view.node_count() + ), + )); + } + if reference.sort() != expected_sort { + return Err(WireError::invalid( + context, + format!( + "reference {:#010x} has sort {:?}, expected {:?}", + reference.raw(), + reference.sort(), + expected_sort + ), + )); + } + let node = view.node(index)?; + let tag = parse_tag(node.tag, index)?; + if tag.result_sort() != reference.sort() { + return Err(WireError::invalid( + context, + format!( + "reference {:#010x} sort {:?} does not match node tag {}", + reference.raw(), + reference.sort(), + tag.name() + ), + )); + } + Ok(node) +} + +/// Returns true if `node` is a BV or Bool variable node that can appear in a model block. +pub fn is_variable_node(node: RawNode) -> bool { + matches!(node.tag, tag::BV_VAR | tag::BOOL_VAR) +} + +``` + +`crates/smt-wire/src/le.rs`: + +```rs +use crate::error::{Result, WireError}; + +fn ensure<'a>( + bytes: &'a [u8], + offset: usize, + len: usize, + context: &'static str, +) -> Result<&'a [u8]> { + let end = offset + .checked_add(len) + .ok_or(WireError::IntegerOverflow(context))?; + if end > bytes.len() { + return Err(WireError::UnexpectedEof { + context, + needed: end, + actual: bytes.len(), + }); + } + Ok(&bytes[offset..end]) +} + +pub fn read_u8(bytes: &[u8], offset: usize, context: &'static str) -> Result { + Ok(ensure(bytes, offset, 1, context)?[0]) +} + +pub fn read_u16(bytes: &[u8], offset: usize, context: &'static str) -> Result { + let mut arr = [0u8; 2]; + arr.copy_from_slice(ensure(bytes, offset, 2, context)?); + Ok(u16::from_le_bytes(arr)) +} + +pub fn read_u32(bytes: &[u8], offset: usize, context: &'static str) -> Result { + let mut arr = [0u8; 4]; + arr.copy_from_slice(ensure(bytes, offset, 4, context)?); + Ok(u32::from_le_bytes(arr)) +} + +pub fn read_u64(bytes: &[u8], offset: usize, context: &'static str) -> Result { + let mut arr = [0u8; 8]; + arr.copy_from_slice(ensure(bytes, offset, 8, context)?); + Ok(u64::from_le_bytes(arr)) +} + +pub fn write_u8(dst: &mut Vec, value: u8) { + dst.push(value); +} + +pub fn write_u16(dst: &mut Vec, value: u16) { + dst.extend_from_slice(&value.to_le_bytes()); +} + +pub fn write_u32(dst: &mut Vec, value: u32) { + dst.extend_from_slice(&value.to_le_bytes()); +} + +pub fn write_u64(dst: &mut Vec, value: u64) { + dst.extend_from_slice(&value.to_le_bytes()); +} + +pub fn checked_add(a: usize, b: usize, context: &'static str) -> Result { + a.checked_add(b).ok_or(WireError::IntegerOverflow(context)) +} + +pub fn checked_mul(a: usize, b: usize, context: &'static str) -> Result { + a.checked_mul(b).ok_or(WireError::IntegerOverflow(context)) +} + +pub fn exact_slice<'a>( + bytes: &'a [u8], + offset: usize, + len: usize, + context: &'static str, +) -> Result<&'a [u8]> { + ensure(bytes, offset, len, context) +} + +pub fn encode_transport_frame(payload: &[u8]) -> Result> { + let len = u32::try_from(payload.len()).map_err(|_| { + WireError::invalid( + "transport frame", + format!("payload length {} exceeds u32::MAX", payload.len()), + ) + })?; + let mut out = Vec::with_capacity(4 + payload.len()); + write_u32(&mut out, len); + out.extend_from_slice(payload); + Ok(out) +} + +pub fn decode_transport_frame(frame: &[u8]) -> Result<&[u8]> { + let len = read_u32(frame, 0, "transport frame length")? as usize; + let expected = checked_add(4, len, "transport frame length")?; + if frame.len() != expected { + return Err(WireError::LengthMismatch { + context: "transport frame", + expected, + actual: frame.len(), + }); + } + exact_slice(frame, 4, len, "transport frame payload") +} + +``` + +`crates/smt-wire/src/lib.rs`: + +```rs +//! Rust support for the SMT v1 binary wire format. +//! +//! The crate intentionally has no runtime dependencies. All parsing uses +//! explicit little-endian byte reads and never relies on host alignment. + +pub mod builder; +pub mod client; +pub mod constants; +pub mod error; +pub mod expr; +pub mod le; +pub mod request; +pub mod response; +pub mod types; + +pub use builder::{Assertion, CompactedExpression, ExprBuilder}; +pub use client::{ClientError, ClientResult, TcpClient}; +pub use constants::{command, request_flags, response_flags, status, tag, Command, Status, Tag}; +pub use error::{Result, WireError}; +pub use expr::{ExprHeader, ExprView, ExpressionBuffer, RawNode, MAX_BV_WIDTH, MAX_NODE_COUNT}; +pub use request::{BinaryRequest, RequestEnvelope}; +pub use response::{ + BinaryResponse, ModelBlock, ModelEntry, OptimizationValueBlock, ResponseEnvelope, ScalarValue, + SimplifyBlock, UnsatCoreBlock, +}; +pub use types::{BlobRef, NodeRef, Sort}; + +``` + +`crates/smt-wire/src/request.rs`: + +```rs +use std::collections::HashSet; + +use crate::constants::{request_flags, Command, REQUEST_ENVELOPE_LEN, REQUEST_MAGIC}; +use crate::error::{Result, WireError}; +use crate::expr::{validate_node_ref, ExprView}; +use crate::le; +use crate::types::{BlobRef, NodeRef, Sort}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RequestEnvelope { + pub request_id: u32, + pub command: Command, + pub flags: u8, + pub budget_ms: u32, + pub expr_len: u32, + pub assertion_count: u16, + pub named_count: u16, + pub assumption_count: u16, + pub target_node: u32, +} + +impl RequestEnvelope { + pub fn encode(&self, dst: &mut Vec) -> Result<()> { + validate_request_flags(self.flags)?; + dst.extend_from_slice(&REQUEST_MAGIC); + le::write_u32(dst, self.request_id); + le::write_u8(dst, self.command.into()); + le::write_u8(dst, self.flags); + le::write_u32(dst, self.budget_ms); + le::write_u32(dst, self.expr_len); + le::write_u16(dst, self.assertion_count); + le::write_u16(dst, self.named_count); + le::write_u16(dst, self.assumption_count); + le::write_u32(dst, self.target_node); + le::write_u32(dst, 0); + Ok(()) + } + + pub fn parse(bytes: &[u8]) -> Result { + if bytes.len() < REQUEST_ENVELOPE_LEN { + return Err(WireError::UnexpectedEof { + context: "request envelope", + needed: REQUEST_ENVELOPE_LEN, + actual: bytes.len(), + }); + } + let magic = le::exact_slice(bytes, 0, 4, "request magic")?; + if magic != REQUEST_MAGIC { + return Err(WireError::BadMagic { + context: "request envelope", + expected: &REQUEST_MAGIC, + actual: magic.to_vec(), + }); + } + let command_raw = le::read_u8(bytes, 8, "request command")?; + let command = Command::try_from(command_raw).map_err(|_| { + WireError::invalid("request command", format!("unknown command {command_raw}")) + })?; + let flags = le::read_u8(bytes, 9, "request flags")?; + validate_request_flags(flags)?; + Ok(Self { + request_id: le::read_u32(bytes, 4, "request id")?, + command, + flags, + budget_ms: le::read_u32(bytes, 10, "request budget")?, + expr_len: le::read_u32(bytes, 14, "request expr_len")?, + assertion_count: le::read_u16(bytes, 18, "request assertion_count")?, + named_count: le::read_u16(bytes, 20, "request named_count")?, + assumption_count: le::read_u16(bytes, 22, "request assumption_count")?, + target_node: le::read_u32(bytes, 24, "request target_node")?, + }) + } + + pub fn encoded_len(&self) -> Result { + request_len( + self.expr_len as usize, + self.assertion_count as usize, + self.named_count as usize, + self.assumption_count as usize, + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BinaryRequest { + pub envelope: RequestEnvelope, + pub expression: Vec, + pub assertion_roots: Vec, + pub named_assertion_refs: Vec, + pub assumption_roots: Vec, +} + +impl BinaryRequest { + #[allow(clippy::too_many_arguments)] + pub fn new( + request_id: u32, + command: Command, + flags: u8, + budget_ms: u32, + expression: Vec, + assertion_roots: Vec, + named_assertion_refs: Vec, + assumption_roots: Vec, + target_node: Option, + ) -> Result { + validate_request_flags(flags)?; + if named_assertion_refs.len() > assertion_roots.len() { + return Err(WireError::invalid( + "request named assertions", + format!( + "named_count {} exceeds assertion_count {}", + named_assertion_refs.len(), + assertion_roots.len() + ), + )); + } + let expr_len = u32::try_from(expression.len()).map_err(|_| { + WireError::invalid( + "request expression", + format!("expression length {} exceeds u32::MAX", expression.len()), + ) + })?; + let assertion_count = u16::try_from(assertion_roots.len()).map_err(|_| { + WireError::invalid("request assertions", "assertion count exceeds u16::MAX") + })?; + let named_count = u16::try_from(named_assertion_refs.len()).map_err(|_| { + WireError::invalid("request named assertions", "named count exceeds u16::MAX") + })?; + let assumption_count = u16::try_from(assumption_roots.len()).map_err(|_| { + WireError::invalid("request assumptions", "assumption count exceeds u16::MAX") + })?; + if matches!(command, Command::Minimize | Command::Maximize) && target_node.is_none() { + return Err(WireError::invalid( + "request target_node", + "MINIMIZE/MAXIMIZE require a BV target node", + )); + } + let target_node_raw = target_node.map(NodeRef::raw).unwrap_or(0); + let request = Self { + envelope: RequestEnvelope { + request_id, + command, + flags, + budget_ms, + expr_len, + assertion_count, + named_count, + assumption_count, + target_node: target_node_raw, + }, + expression, + assertion_roots, + named_assertion_refs, + assumption_roots, + }; + request.validate()?; + Ok(request) + } + + pub fn parse(frame_payload: &[u8]) -> Result { + let envelope = RequestEnvelope::parse(frame_payload)?; + if envelope.named_count > envelope.assertion_count { + return Err(WireError::invalid( + "request named_count", + format!( + "{} exceeds assertion_count {}", + envelope.named_count, envelope.assertion_count + ), + )); + } + let expected_len = envelope.encoded_len()?; + if frame_payload.len() != expected_len { + return Err(WireError::LengthMismatch { + context: "request frame payload", + expected: expected_len, + actual: frame_payload.len(), + }); + } + let expr_start = REQUEST_ENVELOPE_LEN; + let expr_end = expr_start + envelope.expr_len as usize; + let expression = frame_payload[expr_start..expr_end].to_vec(); + let mut offset = expr_end; + let mut assertion_roots = Vec::with_capacity(envelope.assertion_count as usize); + for _ in 0..envelope.assertion_count { + assertion_roots.push(NodeRef::from_raw(le::read_u32( + frame_payload, + offset, + "assertion root", + )?)); + offset += 4; + } + let mut named_assertion_refs = Vec::with_capacity(envelope.named_count as usize); + for _ in 0..envelope.named_count { + let blob_offset = le::read_u32(frame_payload, offset, "named assertion offset")?; + let blob_len = le::read_u32(frame_payload, offset + 4, "named assertion length")?; + named_assertion_refs.push(BlobRef::new(blob_offset, blob_len)); + offset += 8; + } + let mut assumption_roots = Vec::with_capacity(envelope.assumption_count as usize); + for _ in 0..envelope.assumption_count { + assumption_roots.push(NodeRef::from_raw(le::read_u32( + frame_payload, + offset, + "assumption root", + )?)); + offset += 4; + } + debug_assert_eq!(offset, expected_len); + let request = Self { + envelope, + expression, + assertion_roots, + named_assertion_refs, + assumption_roots, + }; + request.validate()?; + Ok(request) + } + + pub fn encode(&self) -> Result> { + self.validate()?; + let mut out = Vec::with_capacity(self.envelope.encoded_len()?); + self.envelope.encode(&mut out)?; + out.extend_from_slice(&self.expression); + for root in &self.assertion_roots { + le::write_u32(&mut out, root.raw()); + } + for name in &self.named_assertion_refs { + le::write_u32(&mut out, name.offset); + le::write_u32(&mut out, name.len); + } + for root in &self.assumption_roots { + le::write_u32(&mut out, root.raw()); + } + Ok(out) + } + + pub fn validate(&self) -> Result<()> { + if self.envelope.expr_len as usize != self.expression.len() { + return Err(WireError::LengthMismatch { + context: "request expression length", + expected: self.envelope.expr_len as usize, + actual: self.expression.len(), + }); + } + if self.envelope.assertion_count as usize != self.assertion_roots.len() { + return Err(WireError::LengthMismatch { + context: "request assertion_count", + expected: self.envelope.assertion_count as usize, + actual: self.assertion_roots.len(), + }); + } + if self.envelope.named_count as usize != self.named_assertion_refs.len() { + return Err(WireError::LengthMismatch { + context: "request named_count", + expected: self.envelope.named_count as usize, + actual: self.named_assertion_refs.len(), + }); + } + if self.envelope.assumption_count as usize != self.assumption_roots.len() { + return Err(WireError::LengthMismatch { + context: "request assumption_count", + expected: self.envelope.assumption_count as usize, + actual: self.assumption_roots.len(), + }); + } + if self.named_assertion_refs.len() > self.assertion_roots.len() { + return Err(WireError::invalid( + "request named assertions", + "named_count exceeds assertion_count", + )); + } + let expr = ExprView::parse_and_validate(&self.expression)?; + for root in &self.assertion_roots { + validate_node_ref(&expr, *root, Sort::Bool, "assertion root")?; + } + for root in &self.assumption_roots { + validate_node_ref(&expr, *root, Sort::Bool, "assumption root")?; + } + for name in &self.named_assertion_refs { + expr.blob_str(*name, "named assertion")?; + } + if (self.envelope.flags & request_flags::WANT_CORE) != 0 { + let mut names = HashSet::new(); + for name in &self.named_assertion_refs { + let name = expr.blob_str(*name, "named assertion")?; + if !names.insert(name.to_owned()) { + return Err(WireError::invalid( + "named assertions", + format!("duplicate assertion name {name:?}"), + )); + } + } + } + match self.envelope.command { + Command::Minimize | Command::Maximize => { + validate_node_ref( + &expr, + NodeRef::from_raw(self.envelope.target_node), + Sort::Bv, + "target_node", + )?; + } + Command::Solve | Command::Simplify => {} + } + Ok(()) + } + + pub fn expression_view(&self) -> Result> { + ExprView::parse_and_validate(&self.expression) + } + + pub fn target_ref(&self) -> Option { + match self.envelope.command { + Command::Minimize | Command::Maximize => { + Some(NodeRef::from_raw(self.envelope.target_node)) + } + Command::Solve | Command::Simplify => None, + } + } +} + +pub fn is_binary_request_payload(payload: &[u8]) -> bool { + payload.len() >= 4 && payload[..4] == REQUEST_MAGIC +} + +pub fn validate_request_flags(flags: u8) -> Result<()> { + let unknown = flags & !request_flags::ALL; + if unknown != 0 { + return Err(WireError::invalid( + "request flags", + format!("unknown flag bits {unknown:#04x}"), + )); + } + Ok(()) +} + +fn request_len( + expr_len: usize, + assertion_count: usize, + named_count: usize, + assumption_count: usize, +) -> Result { + let mut total = REQUEST_ENVELOPE_LEN; + total = le::checked_add(total, expr_len, "request length")?; + total = le::checked_add( + total, + le::checked_mul(assertion_count, 4, "request assertion roots length")?, + "request length", + )?; + total = le::checked_add( + total, + le::checked_mul(named_count, 8, "request named refs length")?, + "request length", + )?; + le::checked_add( + total, + le::checked_mul(assumption_count, 4, "request assumption roots length")?, + "request length", + ) +} + +``` + +`crates/smt-wire/src/response.rs`: + +```rs +use crate::constants::{response_flags, Status, RESPONSE_ENVELOPE_LEN, RESPONSE_MAGIC}; +use crate::error::{Result, WireError}; +use crate::expr::{ + bytes_for_width, is_variable_node, validate_node_ref, ExprView, ExpressionBuffer, +}; +use crate::le; +use crate::types::{BlobRef, NodeRef, Sort}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResponseEnvelope { + pub request_id: u32, + pub status: Status, + pub flags: u8, + pub payload_len: u32, +} + +impl ResponseEnvelope { + pub fn encode(&self, dst: &mut Vec) -> Result<()> { + validate_response_flags(self.flags)?; + dst.extend_from_slice(&RESPONSE_MAGIC); + le::write_u32(dst, self.request_id); + le::write_u8(dst, self.status.into()); + le::write_u8(dst, self.flags); + le::write_u32(dst, self.payload_len); + le::write_u16(dst, 0); + Ok(()) + } + + pub fn parse(bytes: &[u8]) -> Result { + if bytes.len() < RESPONSE_ENVELOPE_LEN { + return Err(WireError::UnexpectedEof { + context: "response envelope", + needed: RESPONSE_ENVELOPE_LEN, + actual: bytes.len(), + }); + } + let magic = le::exact_slice(bytes, 0, 4, "response magic")?; + if magic != RESPONSE_MAGIC { + return Err(WireError::BadMagic { + context: "response envelope", + expected: &RESPONSE_MAGIC, + actual: magic.to_vec(), + }); + } + let status_raw = le::read_u8(bytes, 8, "response status")?; + let status = Status::try_from(status_raw).map_err(|_| { + WireError::invalid("response status", format!("unknown status {status_raw}")) + })?; + let flags = le::read_u8(bytes, 9, "response flags")?; + validate_response_flags(flags)?; + Ok(Self { + request_id: le::read_u32(bytes, 4, "response request_id")?, + status, + flags, + payload_len: le::read_u32(bytes, 10, "response payload_len")?, + }) + } + + pub fn encoded_len(&self) -> Result { + le::checked_add( + RESPONSE_ENVELOPE_LEN, + self.payload_len as usize, + "response length", + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BinaryResponse { + pub envelope: ResponseEnvelope, + pub payload: Vec, +} + +impl BinaryResponse { + pub fn new(request_id: u32, status: Status, flags: u8, payload: Vec) -> Result { + validate_response_flags(flags)?; + let payload_len = u32::try_from(payload.len()).map_err(|_| { + WireError::invalid( + "response payload", + format!("payload length {} exceeds u32::MAX", payload.len()), + ) + })?; + let response = Self { + envelope: ResponseEnvelope { + request_id, + status, + flags, + payload_len, + }, + payload, + }; + response.validate()?; + Ok(response) + } + + pub fn ok(request_id: u32, payload: Vec, flags: u8) -> Result { + Self::new(request_id, Status::Ok, flags, payload) + } + + pub fn error(request_id: u32, message: &str) -> Result { + Self::new( + request_id, + Status::Error, + response_flags::HAS_MESSAGE, + message.as_bytes().to_vec(), + ) + } + + pub fn parse(frame_payload: &[u8]) -> Result { + let envelope = ResponseEnvelope::parse(frame_payload)?; + let expected = envelope.encoded_len()?; + if frame_payload.len() != expected { + return Err(WireError::LengthMismatch { + context: "response frame payload", + expected, + actual: frame_payload.len(), + }); + } + let payload = frame_payload[RESPONSE_ENVELOPE_LEN..].to_vec(); + let response = Self { envelope, payload }; + response.validate()?; + Ok(response) + } + + pub fn encode(&self) -> Result> { + self.validate()?; + let mut out = Vec::with_capacity(self.envelope.encoded_len()?); + self.envelope.encode(&mut out)?; + out.extend_from_slice(&self.payload); + Ok(out) + } + + pub fn validate(&self) -> Result<()> { + if self.envelope.payload_len as usize != self.payload.len() { + return Err(WireError::LengthMismatch { + context: "response payload length", + expected: self.envelope.payload_len as usize, + actual: self.payload.len(), + }); + } + if self.envelope.status == Status::Error { + if (self.envelope.flags & response_flags::HAS_MESSAGE) == 0 { + return Err(WireError::invalid( + "error response", + "HAS_MESSAGE flag must be set", + )); + } + core::str::from_utf8(&self.payload) + .map_err(|_| WireError::invalid("error response", "message is not UTF-8"))?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScalarValue { + pub width: u32, + pub bytes: Vec, +} + +impl ScalarValue { + pub fn bool(value: bool) -> Self { + Self { + width: 0, + bytes: vec![u8::from(value)], + } + } + + pub fn bv(width: u32, bytes: Vec) -> Result { + let expected = bytes_for_width(width)?; + if bytes.len() != expected { + return Err(WireError::invalid( + "scalar BV value", + format!( + "got {} bytes, expected {expected} for width {width}", + bytes.len() + ), + )); + } + ensure_unused_high_bits_zero(width, &bytes, "scalar BV value")?; + Ok(Self { width, bytes }) + } + + pub fn encode(&self, dst: &mut Vec) -> Result<()> { + self.validate()?; + le::write_u32(dst, self.width); + le::write_u32( + dst, + u32::try_from(self.bytes.len()) + .map_err(|_| WireError::invalid("scalar value", "value length exceeds u32::MAX"))?, + ); + dst.extend_from_slice(&self.bytes); + Ok(()) + } + + pub fn decode(bytes: &[u8], offset: &mut usize) -> Result { + let width = le::read_u32(bytes, *offset, "scalar width")?; + let value_len = le::read_u32(bytes, *offset + 4, "scalar value_len")? as usize; + *offset = le::checked_add(*offset, 8, "scalar offset")?; + let value = le::exact_slice(bytes, *offset, value_len, "scalar value bytes")?.to_vec(); + *offset = le::checked_add(*offset, value_len, "scalar offset")?; + let scalar = Self { + width, + bytes: value, + }; + scalar.validate()?; + Ok(scalar) + } + + pub fn validate(&self) -> Result<()> { + if self.width == 0 { + if self.bytes.len() != 1 || !matches!(self.bytes[0], 0 | 1) { + return Err(WireError::invalid( + "Bool scalar value", + "expected value_len=1 and byte 0 or 1", + )); + } + return Ok(()); + } + let expected = bytes_for_width(self.width)?; + if self.bytes.len() != expected { + return Err(WireError::invalid( + "BV scalar value", + format!( + "got {} bytes, expected {expected} for width {}", + self.bytes.len(), + self.width + ), + )); + } + ensure_unused_high_bits_zero(self.width, &self.bytes, "BV scalar value") + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ModelEntry { + pub node_ref: NodeRef, + pub value: ScalarValue, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ModelBlock { + pub entries: Vec, +} + +impl ModelBlock { + pub fn encode(&self) -> Result> { + let mut out = Vec::new(); + le::write_u32( + &mut out, + u32::try_from(self.entries.len()) + .map_err(|_| WireError::invalid("model block", "entry count exceeds u32::MAX"))?, + ); + for entry in &self.entries { + le::write_u32(&mut out, entry.node_ref.raw()); + entry.value.encode(&mut out)?; + } + Ok(out) + } + + pub fn decode(payload: &[u8]) -> Result { + let mut offset = 0; + let block = Self::decode_from(payload, &mut offset)?; + if offset != payload.len() { + return Err(WireError::LengthMismatch { + context: "model block", + expected: offset, + actual: payload.len(), + }); + } + Ok(block) + } + + pub fn decode_from(bytes: &[u8], offset: &mut usize) -> Result { + let entry_count = le::read_u32(bytes, *offset, "model entry_count")?; + *offset = le::checked_add(*offset, 4, "model offset")?; + let remaining = bytes.len().checked_sub(*offset).ok_or_else(|| { + WireError::invalid("model block", "entry offset is past end of payload") + })?; + if entry_count as usize > remaining / 12 { + return Err(WireError::invalid( + "model block", + format!( + "entry_count {entry_count} cannot fit in remaining {remaining} payload bytes" + ), + )); + } + let mut entries = Vec::with_capacity(entry_count as usize); + for _ in 0..entry_count { + let node_ref = NodeRef::from_raw(le::read_u32(bytes, *offset, "model node_ref")?); + *offset = le::checked_add(*offset, 4, "model offset")?; + let value = ScalarValue::decode(bytes, offset)?; + entries.push(ModelEntry { node_ref, value }); + } + Ok(Self { entries }) + } + + pub fn validate_against_expr(&self, expr: &ExprView<'_>) -> Result<()> { + for entry in &self.entries { + let expected_sort = if entry.node_ref.is_bool() { + Sort::Bool + } else { + Sort::Bv + }; + let node = validate_node_ref(expr, entry.node_ref, expected_sort, "model node_ref")?; + if !is_variable_node(node) { + return Err(WireError::invalid( + "model node_ref", + format!("node {} is not a variable", entry.node_ref.index()), + )); + } + match entry.node_ref.sort() { + Sort::Bool => { + if entry.value.width != 0 { + return Err(WireError::invalid( + "model value", + "Bool variable has non-Bool scalar value", + )); + } + } + Sort::Bv => { + if entry.value.width != node.width { + return Err(WireError::invalid( + "model value", + format!( + "BV variable width {} but scalar width {}", + node.width, entry.value.width + ), + )); + } + } + } + } + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct UnsatCoreBlock { + pub names: Vec, +} + +impl UnsatCoreBlock { + pub fn encode(&self) -> Result> { + let mut out = Vec::new(); + le::write_u32( + &mut out, + u32::try_from(self.names.len()).map_err(|_| { + WireError::invalid("unsat core block", "name count exceeds u32::MAX") + })?, + ); + for name in &self.names { + le::write_u32( + &mut out, + u32::try_from(name.len()).map_err(|_| { + WireError::invalid("unsat core block", "name length exceeds u32::MAX") + })?, + ); + out.extend_from_slice(name.as_bytes()); + } + Ok(out) + } + + pub fn decode(payload: &[u8]) -> Result { + let mut offset = 0; + let name_count = le::read_u32(payload, offset, "unsat core name_count")?; + offset += 4; + let remaining = payload.len().checked_sub(offset).ok_or_else(|| { + WireError::invalid("unsat core block", "name offset is past end of payload") + })?; + if name_count as usize > remaining / 4 { + return Err(WireError::invalid( + "unsat core block", + format!( + "name_count {name_count} cannot fit in remaining {remaining} payload bytes" + ), + )); + } + let mut names = Vec::with_capacity(name_count as usize); + for _ in 0..name_count { + let len = le::read_u32(payload, offset, "unsat core name_len")? as usize; + offset += 4; + let bytes = le::exact_slice(payload, offset, len, "unsat core name")?; + let name = core::str::from_utf8(bytes) + .map_err(|_| WireError::invalid("unsat core block", "name is not UTF-8"))? + .to_owned(); + offset += len; + names.push(name); + } + if offset != payload.len() { + return Err(WireError::LengthMismatch { + context: "unsat core block", + expected: offset, + actual: payload.len(), + }); + } + Ok(Self { names }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SimplifyBlock { + pub expression: Vec, + pub assertion_roots: Vec, + pub named_assertion_refs: Vec, + pub assumption_roots: Vec, +} + +impl SimplifyBlock { + pub fn encode(&self) -> Result> { + self.validate()?; + let expr_len = u32::try_from(self.expression.len()).map_err(|_| { + WireError::invalid("simplify block", "expression length exceeds u32::MAX") + })?; + let assertion_count = u16::try_from(self.assertion_roots.len()).map_err(|_| { + WireError::invalid("simplify block", "assertion count exceeds u16::MAX") + })?; + let named_count = u16::try_from(self.named_assertion_refs.len()) + .map_err(|_| WireError::invalid("simplify block", "named count exceeds u16::MAX"))?; + let assumption_count = u16::try_from(self.assumption_roots.len()).map_err(|_| { + WireError::invalid("simplify block", "assumption count exceeds u16::MAX") + })?; + + let mut out = Vec::new(); + le::write_u32(&mut out, expr_len); + le::write_u16(&mut out, assertion_count); + le::write_u16(&mut out, named_count); + le::write_u16(&mut out, assumption_count); + le::write_u16(&mut out, 0); + out.extend_from_slice(&self.expression); + for root in &self.assertion_roots { + le::write_u32(&mut out, root.raw()); + } + for name in &self.named_assertion_refs { + le::write_u32(&mut out, name.offset); + le::write_u32(&mut out, name.len); + } + for root in &self.assumption_roots { + le::write_u32(&mut out, root.raw()); + } + Ok(out) + } + + pub fn decode(payload: &[u8]) -> Result { + if payload.len() < 12 { + return Err(WireError::UnexpectedEof { + context: "simplify block header", + needed: 12, + actual: payload.len(), + }); + } + let expr_len = le::read_u32(payload, 0, "simplify expr_len")? as usize; + let assertion_count = le::read_u16(payload, 4, "simplify assertion_count")? as usize; + let named_count = le::read_u16(payload, 6, "simplify named_count")? as usize; + let assumption_count = le::read_u16(payload, 8, "simplify assumption_count")? as usize; + if named_count > assertion_count { + return Err(WireError::invalid( + "simplify block", + "named_count exceeds assertion_count", + )); + } + let mut expected = 12usize; + expected = le::checked_add(expected, expr_len, "simplify block length")?; + expected = le::checked_add( + expected, + le::checked_mul(assertion_count, 4, "simplify assertions length")?, + "simplify block length", + )?; + expected = le::checked_add( + expected, + le::checked_mul(named_count, 8, "simplify named refs length")?, + "simplify block length", + )?; + expected = le::checked_add( + expected, + le::checked_mul(assumption_count, 4, "simplify assumptions length")?, + "simplify block length", + )?; + if payload.len() != expected { + return Err(WireError::LengthMismatch { + context: "simplify block", + expected, + actual: payload.len(), + }); + } + let mut offset = 12; + let expression = payload[offset..offset + expr_len].to_vec(); + offset += expr_len; + let mut assertion_roots = Vec::with_capacity(assertion_count); + for _ in 0..assertion_count { + assertion_roots.push(NodeRef::from_raw(le::read_u32( + payload, + offset, + "simplify assertion root", + )?)); + offset += 4; + } + let mut named_assertion_refs = Vec::with_capacity(named_count); + for _ in 0..named_count { + named_assertion_refs.push(BlobRef::new( + le::read_u32(payload, offset, "simplify name offset")?, + le::read_u32(payload, offset + 4, "simplify name length")?, + )); + offset += 8; + } + let mut assumption_roots = Vec::with_capacity(assumption_count); + for _ in 0..assumption_count { + assumption_roots.push(NodeRef::from_raw(le::read_u32( + payload, + offset, + "simplify assumption root", + )?)); + offset += 4; + } + debug_assert_eq!(offset, expected); + let block = Self { + expression, + assertion_roots, + named_assertion_refs, + assumption_roots, + }; + block.validate()?; + Ok(block) + } + + pub fn validate(&self) -> Result<()> { + if self.named_assertion_refs.len() > self.assertion_roots.len() { + return Err(WireError::invalid( + "simplify block", + "named_count exceeds assertion_count", + )); + } + let expr = ExprView::parse_and_validate(&self.expression)?; + for root in &self.assertion_roots { + validate_node_ref(&expr, *root, Sort::Bool, "simplify assertion root")?; + } + for root in &self.assumption_roots { + validate_node_ref(&expr, *root, Sort::Bool, "simplify assumption root")?; + } + for name in &self.named_assertion_refs { + expr.blob_str(*name, "simplify named assertion")?; + } + Ok(()) + } + + pub fn expression_buffer(&self) -> Result { + ExpressionBuffer::from_bytes(self.expression.clone()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OptimizationValueBlock { + pub optimum: ScalarValue, + pub model: Option, +} + +impl OptimizationValueBlock { + pub fn encode(&self) -> Result> { + let mut out = Vec::new(); + self.optimum.encode(&mut out)?; + if let Some(model) = &self.model { + out.extend_from_slice(&model.encode()?); + } + Ok(out) + } + + pub fn decode(payload: &[u8], has_model: bool) -> Result { + let mut offset = 0; + let optimum = ScalarValue::decode(payload, &mut offset)?; + let model = if has_model { + Some(ModelBlock::decode_from(payload, &mut offset)?) + } else { + None + }; + if offset != payload.len() { + return Err(WireError::LengthMismatch { + context: "optimization value block", + expected: offset, + actual: payload.len(), + }); + } + Ok(Self { optimum, model }) + } +} + +pub fn validate_response_flags(flags: u8) -> Result<()> { + let unknown = flags & !response_flags::ALL; + if unknown != 0 { + return Err(WireError::invalid( + "response flags", + format!("unknown flag bits {unknown:#04x}"), + )); + } + Ok(()) +} + +fn ensure_unused_high_bits_zero(width: u32, bytes: &[u8], context: &'static str) -> Result<()> { + let valid_bits = width % 8; + if valid_bits == 0 || bytes.is_empty() { + return Ok(()); + } + let mask = (1u8 << valid_bits) - 1; + let last = bytes[bytes.len() - 1]; + if (last & !mask) != 0 { + return Err(WireError::invalid( + context, + format!("unused high bits in final byte are not zero: {last:#04x}"), + )); + } + Ok(()) +} + +``` + +`crates/smt-wire/src/types.rs`: + +```rs +use crate::error::{Result, WireError}; + +/// Sort encoded in bit 31 of a typed node reference. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Sort { + Bv, + Bool, +} + +impl Sort { + pub const fn sort_bit(self) -> u32 { + match self { + Sort::Bv => 0, + Sort::Bool => NodeRef::SORT_BIT, + } + } +} + +/// A typed node reference. +/// +/// Bit 31 encodes the sort (`0 = BV`, `1 = Bool`) and bits 0-30 encode the +/// node-array index. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct NodeRef(u32); + +impl NodeRef { + pub const SORT_BIT: u32 = 0x8000_0000; + pub const INDEX_MASK: u32 = 0x7fff_ffff; + + pub fn new(sort: Sort, index: u32) -> Result { + if index > Self::INDEX_MASK { + return Err(WireError::invalid( + "node reference", + format!("index {index} exceeds 31-bit node-reference range"), + )); + } + Ok(Self(sort.sort_bit() | index)) + } + + pub fn bv(index: u32) -> Result { + Self::new(Sort::Bv, index) + } + + pub fn bool(index: u32) -> Result { + Self::new(Sort::Bool, index) + } + + pub const fn from_raw(raw: u32) -> Self { + Self(raw) + } + + pub const fn raw(self) -> u32 { + self.0 + } + + pub const fn index(self) -> u32 { + self.0 & Self::INDEX_MASK + } + + pub const fn sort(self) -> Sort { + if (self.0 & Self::SORT_BIT) == 0 { + Sort::Bv + } else { + Sort::Bool + } + } + + pub const fn is_bv(self) -> bool { + matches!(self.sort(), Sort::Bv) + } + + pub const fn is_bool(self) -> bool { + matches!(self.sort(), Sort::Bool) + } +} + +impl From for u32 { + fn from(value: NodeRef) -> Self { + value.raw() + } +} + +/// A `(blob_offset, blob_len)` reference, encoded in node payloads and request +/// name lists. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BlobRef { + pub offset: u32, + pub len: u32, +} + +impl BlobRef { + pub const fn new(offset: u32, len: u32) -> Self { + Self { offset, len } + } + + pub const fn from_payload(payload: u64) -> Self { + Self { + offset: (payload >> 32) as u32, + len: payload as u32, + } + } + + pub const fn to_payload(self) -> u64 { + ((self.offset as u64) << 32) | (self.len as u64) + } +} + +``` + +`crates/smt-wire/tests/phase0_golden.rs`: + +```rs +use smt_wire::{constants::tag, le, ExprView, ExpressionBuffer, NodeRef, RawNode}; + +#[test] +fn empty_expression_buffer_golden_bytes() { + let empty = ExpressionBuffer::empty(); + let expected = [ + b'S', b'M', b'T', 0, // magic + 1, 0, 0, 0, // version + pad + 0, 0, 0, 0, // node_count + 0, 0, 0, 0, // child_count + 0, 0, 0, 0, // blob_len + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // reserved + ]; + assert_eq!(empty.as_bytes(), expected); + ExprView::parse_and_validate(empty.as_bytes()).unwrap(); +} + +#[test] +fn simple_expression_buffer_golden_bytes() { + let nodes = [ + RawNode::new(tag::BV_VAR, 0, 0, 8, 0, 0, 1), + RawNode::new(tag::BV_EQ, 2, 0, 0, 0, 0, 0), + ]; + let children = [NodeRef::bv(0).unwrap(), NodeRef::bv(0).unwrap()]; + let expr = ExpressionBuffer::from_parts(&nodes, &children, b"x").unwrap(); + let expected = [ + b'S', b'M', b'T', 0, 1, 0, 0, 0, // header magic/version + 2, 0, 0, 0, // node_count + 2, 0, 0, 0, // child_count + 1, 0, 0, 0, // blob_len + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // reserved + // node 0: BV_VAR x : (_ BitVec 8), payload = blob ref (0, 1) + 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + // node 1: BV_EQ node0 node0 -> Bool + 31, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // children + 0, 0, 0, 0, 0, 0, 0, 0, // blob + b'x', + ]; + assert_eq!(expr.as_bytes(), expected); + ExprView::parse_and_validate(expr.as_bytes()).unwrap(); +} + +#[test] +fn little_endian_decoding_is_explicit() { + let bytes = [0x34, 0x12, 0xef, 0xcd, 0xab, 0x89, 0x08, 0x07]; + assert_eq!(le::read_u16(&bytes, 0, "u16").unwrap(), 0x1234); + assert_eq!(le::read_u32(&bytes, 2, "u32").unwrap(), 0x89ab_cdef); + assert_eq!( + le::read_u64(&bytes, 0, "u64").unwrap(), + 0x0708_89ab_cdef_1234 + ); +} + +#[test] +fn malformed_child_index_is_rejected() { + let nodes = [ + RawNode::new(tag::BV_VAR, 0, 0, 8, 0, 0, 1), + RawNode::new(tag::BV_EQ, 2, 0, 0, 0, 0, 0), + ]; + // The first child points at node 1, i.e. the parent itself, which violates + // the strict bottom-up construction rule. + let children = [NodeRef::bv(1).unwrap(), NodeRef::bv(0).unwrap()]; + let expr = ExpressionBuffer::from_parts(&nodes, &children, b"x").unwrap(); + assert!(ExprView::parse(&expr.into_bytes()) + .unwrap() + .validate() + .is_err()); +} + +``` + +`crates/smt-wire/tests/phase1_codecs.rs`: + +```rs +use smt_wire::{ + request_flags, response_flags, status, tag, BinaryRequest, BinaryResponse, BlobRef, Command, + ExprBuilder, ExprView, ExpressionBuffer, ModelBlock, ModelEntry, NodeRef, + OptimizationValueBlock, RawNode, ScalarValue, SimplifyBlock, Sort, Status, UnsatCoreBlock, +}; + +fn hex_bytes(input: &str) -> Vec { + let compact = input.split_whitespace().collect::(); + assert_eq!(compact.len() % 2, 0, "hex string must contain whole bytes"); + (0..compact.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&compact[i..i + 2], 16).unwrap()) + .collect() +} + +#[test] +fn simple_sat_request_golden_vector() { + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 8).unwrap(); + let one = builder.bv_const(1, 8).unwrap(); + let assertion = builder.bv_eq(x, one).unwrap(); + builder.assert(assertion).unwrap(); + + let bytes = builder + .build_solve_request(0x0102_0304, 500, true, false) + .unwrap(); + let expected = hex_bytes( + "53 4d 54 51 04 03 02 01 00 01 f4 01 00 00 71 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 \ + 53 4d 54 00 01 00 00 00 03 00 00 00 02 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ + 00 00 00 00 08 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 \ + 01 00 00 00 08 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 \ + 1f 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ + 00 00 00 00 01 00 00 00 78 02 00 00 80", + ); + assert_eq!(bytes, expected); + + let parsed = BinaryRequest::parse(&bytes).unwrap(); + assert_eq!(parsed.envelope.request_id, 0x0102_0304); + assert_eq!(parsed.envelope.command, Command::Solve); + assert_eq!(parsed.envelope.flags, request_flags::WANT_MODEL); + assert_eq!(parsed.envelope.budget_ms, 500); + assert_eq!(parsed.assertion_roots, vec![NodeRef::bool(2).unwrap()]); + assert_eq!(parsed.expression_view().unwrap().node_count(), 3); +} + +#[test] +fn simple_unsat_request_golden_vector() { + let mut builder = ExprBuilder::new(); + let false_node = builder.bool_false().unwrap(); + builder.assert(false_node).unwrap(); + + let bytes = builder.build_solve_request(5, 0, false, false).unwrap(); + let expected = hex_bytes( + "53 4d 54 51 05 00 00 00 00 00 00 00 00 00 38 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 \ + 53 4d 54 00 01 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ + 19 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 80", + ); + assert_eq!(bytes, expected); + let parsed = BinaryRequest::parse(&bytes).unwrap(); + assert_eq!(parsed.assertion_roots, vec![NodeRef::bool(0).unwrap()]); +} + +#[test] +fn wide_constant_golden_vector() { + let mut builder = ExprBuilder::new(); + builder + .bv_const_wide( + &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + 128, + ) + .unwrap(); + let bytes = builder.to_bytes().unwrap(); + let expected = hex_bytes( + "53 4d 54 00 01 00 00 00 01 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ + 01 00 00 00 80 00 00 00 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 \ + 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10", + ); + assert_eq!(bytes, expected); + ExprView::parse_and_validate(&bytes).unwrap(); +} + +#[test] +fn named_assertion_and_unsat_core_golden_vectors() { + let mut builder = ExprBuilder::new(); + let p = builder.bool_var("p").unwrap(); + builder.assert_named("a0", p).unwrap(); + let bytes = builder.build_solve_request(7, 0, false, true).unwrap(); + let expected_request = hex_bytes( + "53 4d 54 51 07 00 00 00 00 02 00 00 00 00 3b 00 00 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 \ + 53 4d 54 00 01 00 00 00 01 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ + 1a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 \ + 70 61 30 00 00 00 80 01 00 00 00 02 00 00 00", + ); + assert_eq!(bytes, expected_request); + + let parsed = BinaryRequest::parse(&bytes).unwrap(); + assert_eq!(parsed.envelope.flags, request_flags::WANT_CORE); + assert_eq!(parsed.named_assertion_refs, vec![BlobRef::new(1, 2)]); + assert_eq!( + parsed + .expression_view() + .unwrap() + .blob_str(parsed.named_assertion_refs[0], "test") + .unwrap(), + "a0" + ); + + let core = UnsatCoreBlock { + names: vec!["a0".to_owned()], + }; + let response = BinaryResponse::new( + 7, + Status::Unsat, + response_flags::HAS_CORE, + core.encode().unwrap(), + ) + .unwrap() + .encode() + .unwrap(); + let expected_response = + hex_bytes("53 4d 54 52 07 00 00 00 02 02 0a 00 00 00 00 00 01 00 00 00 02 00 00 00 61 30"); + assert_eq!(response, expected_response); + let parsed_response = BinaryResponse::parse(&response).unwrap(); + assert_eq!(parsed_response.envelope.status, Status::Unsat); + assert_eq!( + UnsatCoreBlock::decode(&parsed_response.payload).unwrap(), + core + ); +} + +#[test] +fn response_payload_codecs_round_trip() { + let mut builder = ExprBuilder::new(); + let x = builder.bv_var("x", 8).unwrap(); + let p = builder.bool_var("p").unwrap(); + let expr = builder.to_expression_buffer().unwrap(); + let view = expr.view().unwrap(); + + let model = ModelBlock { + entries: vec![ + ModelEntry { + node_ref: x, + value: ScalarValue::bv(8, vec![42]).unwrap(), + }, + ModelEntry { + node_ref: p, + value: ScalarValue::bool(true), + }, + ], + }; + model.validate_against_expr(&view).unwrap(); + let model_bytes = model.encode().unwrap(); + assert_eq!(ModelBlock::decode(&model_bytes).unwrap(), model); + + let simplify = SimplifyBlock { + expression: ExpressionBuffer::empty().into_bytes(), + assertion_roots: vec![], + named_assertion_refs: vec![], + assumption_roots: vec![], + }; + let simplify_bytes = simplify.encode().unwrap(); + assert_eq!(SimplifyBlock::decode(&simplify_bytes).unwrap(), simplify); + + let optimum = OptimizationValueBlock { + optimum: ScalarValue::bv(8, vec![0x7f]).unwrap(), + model: Some(model.clone()), + }; + let opt_bytes = optimum.encode().unwrap(); + assert_eq!( + OptimizationValueBlock::decode(&opt_bytes, true).unwrap(), + optimum + ); + + let response = BinaryResponse::new( + 9, + Status::Sat, + response_flags::HAS_VALUE | response_flags::HAS_MODEL, + opt_bytes, + ) + .unwrap(); + let parsed = BinaryResponse::parse(&response.encode().unwrap()).unwrap(); + assert_eq!(parsed.envelope.status, Status::Sat); +} + +#[test] +fn expression_builder_rejects_sort_and_width_errors() { + let mut builder = ExprBuilder::new(); + let a8 = builder.bv_var("a", 8).unwrap(); + let b16 = builder.bv_var("b", 16).unwrap(); + assert!(builder.bv_add(a8, b16).is_err()); + + let p = builder.bool_var("p").unwrap(); + assert!(builder.bool_and(p, a8).is_err()); + assert!(builder.bv_extract(a8, 8, 0).is_err()); + assert!(builder.bv_zext(b16, u16::MAX).is_err()); + + let selectors = vec![p; 128]; + let values = vec![a8; 128]; + assert!(builder.bv_select(&selectors, &values, a8).is_err()); +} + +#[test] +fn malformed_inputs_are_rejected() { + let bad_magic = [0u8; 32]; + assert!(ExprView::parse(&bad_magic).is_err()); + + let unknown_tag = + ExpressionBuffer::from_parts(&[RawNode::new(250, 0, 0, 0, 0, 0, 0)], &[], &[]).unwrap(); + assert!(ExprView::parse(&unknown_tag.into_bytes()) + .unwrap() + .validate() + .is_err()); + + let width_mismatch = ExpressionBuffer::from_parts( + &[ + RawNode::new(tag::BV_CONST, 0, 0, 8, 0, 0, 1), + RawNode::new(tag::BV_CONST, 0, 0, 16, 0, 0, 1), + RawNode::new(tag::BV_ADD, 2, 0, 8, 0, 0, 0), + ], + &[NodeRef::bv(0).unwrap(), NodeRef::bv(1).unwrap()], + &[], + ) + .unwrap(); + assert!(ExprView::parse(&width_mismatch.into_bytes()) + .unwrap() + .validate() + .is_err()); + + let wrong_root_sort = BinaryRequest::new( + 1, + Command::Solve, + 0, + 0, + ExpressionBuffer::from_parts(&[RawNode::new(tag::BV_CONST, 0, 0, 8, 0, 0, 1)], &[], &[]) + .unwrap() + .into_bytes(), + vec![NodeRef::bv(0).unwrap()], + vec![], + vec![], + None, + ); + assert!(wrong_root_sort.is_err()); + + let bad_scalar = ScalarValue { + width: 9, + bytes: vec![0xff, 0xfe], + }; + assert!(bad_scalar.validate().is_err()); + + let mut truncated_response = BinaryResponse::error(1, "bad").unwrap().encode().unwrap(); + truncated_response.pop(); + assert!(BinaryResponse::parse(&truncated_response).is_err()); +} + +#[test] +fn request_validation_rejects_duplicate_core_names_and_missing_optimization_target() { + let mut builder = ExprBuilder::new(); + let p = builder.bool_var("p").unwrap(); + builder.assert_named("dup", p).unwrap(); + builder.assert_named("dup", p).unwrap(); + assert!(builder.build_solve_request(1, 0, false, true).is_err()); + + let expr = + ExpressionBuffer::from_parts(&[RawNode::new(tag::BV_CONST, 0, 0, 8, 0, 0, 1)], &[], &[]) + .unwrap() + .into_bytes(); + let missing_target = BinaryRequest::new( + 2, + Command::Minimize, + 0, + 0, + expr, + vec![], + vec![], + vec![], + None, + ); + assert!(missing_target.is_err()); +} + +#[test] +fn transport_frames_are_little_endian_and_exact_length() { + let payload = b"SMTQ-example"; + let frame = smt_wire::le::encode_transport_frame(payload).unwrap(); + assert_eq!(&frame[..4], &(payload.len() as u32).to_le_bytes()); + assert_eq!( + smt_wire::le::decode_transport_frame(&frame).unwrap(), + payload + ); + + let mut too_long = frame.clone(); + too_long.push(0); + assert!(smt_wire::le::decode_transport_frame(&too_long).is_err()); + + assert_eq!(status::SAT, 1); + assert_eq!(Sort::Bool.sort_bit(), 0x8000_0000); +} + +``` + +`docs/backend-provisioning.md`: + +```md +# Backend provisioning + +The reference server now builds with three solver backends: + +- `Z3Backend` uses the Rust `z3` crate and links Z3 through the crate's `gh-release` feature, so it does not shell out to a `z3` executable. +- `BinbitBackend` uses the Rust `binbit` solver from `https://github.com/bint-disasm/binbit`. +- `QfbvsmtrsBackend` uses the standalone pure-Rust `qfbvsmtrs` crate in this workspace. It translates validated `smt-wire` requests into the crate's own IR, bit-blasts to CNF, solves with the crate's selectable pure-Rust SAT backend (`splr` by default, with `varisat` and internal DPLL available for cross-checking), and supports models, named unsat cores, and bit-hunt optimization. + +The default server (`crates/smt-server/src/main.rs`) races all three backends via `RacingBackend`. + +No standalone `z3` binary is required at runtime, and the old exhaustive/test backend and Z3 CLI adapter have been removed from the public server configuration. + +``` + +`docs/qf_bv_solver_plan.md`: + +```md +# Project Plan: Pure-Rust QF_BV SMT Solver via Bit-Blasting + +## 1. Project Overview + +Build a pure-Rust SMT solver for the **QF_BV** (Quantifier-Free Bit-Vector) logic, targeting symbolic execution and program verification workloads. The solver will parse SMT-LIB 2.7 input using **yaspar**, bit-blast BV expressions into Boolean circuits, Tseitin-encode them into CNF, and solve using a pure-Rust SAT backend. + +### 1.1 Repository Fit / Integration Review + +This plan was originally written as a greenfield standalone solver. That is still the right shape for **`qfbvsmtrs`**: it should be its own crate with its own public API, IR, SMT-LIB frontend, bit-blaster, CNF encoder, SAT abstraction, and model type. The repository integration should be an adapter around that crate, not an implementation buried inside `smt-server`. + +Repository facts that affect the integration: + +- The project already has a public, validated, topologically ordered expression IR in `crates/smt-wire` (`ExpressionBuffer`, `ExprView`, `RawNode`, `NodeRef`, and `Tag`). +- The project already has an SMT-LIB text path in `crates/smt-server/src/smtlib.rs`, using `yaspar` and lowering scripts into `smt-wire` requests. +- The project already has the server-level backend abstraction in `crates/smt-server/src/backend.rs` (`Backend::handle(&BinaryRequest) -> QueryResult`). +- The project currently races `Z3Backend` and `BinbitBackend` in `crates/smt-server/src/main.rs`. + +Therefore the best design is a standalone crate plus two input bridges into the same solver core: + +```text +Standalone use: + SMT-LIB / builder API → qfbvsmtrs IR → simplify → bit-blast → CNF → SAT → qfbvsmtrs model + +Server use: + smt_wire::BinaryRequest / ExprView → qfbvsmtrs IR → same solver core → qfbvsmtrs result + smt-server adapter converts qfbvsmtrs result → QueryResult / ModelBlock +``` + +The `qfbvsmtrs` crate should not depend on `smt-server`. It may optionally depend on `smt-wire` behind a feature (for example `wire`) so the server adapter can reuse a first-party conversion path without making the core solver server-specific. + +### 1.2 Scope + +**In scope (QF_BV core):** +- Fixed-width bit-vector arithmetic (add, sub, mul, udiv, urem, sdiv, srem, smod) +- Bitwise operations (and, or, xor, not, shift, rotate) +- Comparison (bvult, bvule, bvugt, bvuge, bvslt, bvsle, bvsgt, bvsge, equals) +- Wire/server overflow predicates (`uaddo`, `saddo`, `usubo`, `ssubo`, `umulo`, `smulo`, `nego`, `sdivo`) or explicit unsupported handling until circuits land +- Extraction, concatenation, zero/sign extension +- Boolean connectives over BV predicates (and, or, not, implies, ite) + +**Out of scope (for now):** +- Arrays (QF_ABV) — can be added later via read-over-write axioms +- Floating point (QF_BVFP) +- Quantifiers +- Optimization / MaxSMT +- Uninterpreted functions (QF_UFBV) + +### 1.3 Dependencies + +| Crate | Role | Why this one | +|---|---|---| +| `yaspar` | SMT-LIB 2.7 parsing | Callback-based, no AST allocation unless you want it, SMT-LIB 2.7 compliant | +| `splr` | Default SAT backend | Pure-Rust CDCL solver with watched literals, restarts, and timeout support | +| `varisat` | Alternate SAT backend | Pure-Rust CDCL solver used for backend cross-checking and comparison | +| `rustsat` (encodings) | Cardinality / PB encodings | Optional, useful if you need pseudo-Boolean constraints later | +| `smt-wire` (optional feature) | Server wire bridge | Lets `smt-server` feed validated binary requests into the standalone crate without depending on `smt-server` | + +A secondary SAT backend (e.g. `splr`) should be easy to swap in behind a trait, which is also useful for differential testing. `qfbvsmtrs` should not depend on `smt-server`. + + +## 2. Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ SMT-LIB Input │ +└────────────────────────┬────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ yaspar (SMT-LIB 2.7 parser) │ +│ callback-driven: ParsingAction trait │ +└────────────────────────┬────────────────────────────┘ + │ callbacks build: + ▼ +┌─────────────────────────────────────────────────────┐ +│ BV Expression IR │ +│ hash-consed DAG of BV terms + Boolean terms │ +│ (Sort-checked, width-annotated) │ +└────────────────────────┬────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Word-Level Simplifier │ +│ constant folding, identity removal, extract/ │ +│ concat merging, dead-node elimination │ +└────────────────────────┬────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Bit-Blaster │ +│ each BV node → vector of Boolean gates │ +│ each BV predicate → single Boolean gate │ +└────────────────────────┬────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Tseitin CNF Encoder │ +│ gate graph → CNF clauses │ +│ (fresh variable per gate output) │ +└────────────────────────┬────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ SAT Solver Backend │ +│ varisat / rustsat-batsat / splr (behind trait) │ +└────────────────────────┬────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Model Extraction │ +│ SAT assignment → BV constant values │ +│ (reverse bit-mapping per declared variable) │ +└─────────────────────────────────────────────────────┘ +``` + +For standalone use, all boxes live inside the `qfbvsmtrs` crate. For repository/server use, `smt-server` can bypass the crate's SMT-LIB parser and enter through a `smt-wire` bridge, but it should still land in the same `qfbvsmtrs` IR and solver pipeline. That keeps the crate independently usable while preventing two independent bit-blasters or SAT integrations. + + +## 3. Crate / Module Structure + +Original multi-crate split from the greenfield plan (useful long-term, but heavier than needed initially): + +``` +qfbv-solver/ +├── Cargo.toml +├── crates/ +│ ├── qfbv-ir/ # BV expression IR, hash-consing, sorts +│ │ └── src/ +│ │ ├── lib.rs +│ │ ├── term.rs # BvTerm, BoolTerm enums +│ │ ├── arena.rs # hash-consing arena +│ │ └── sort.rs # BitVec(n), Bool sort checking +│ │ +│ ├── qfbv-frontend/ # yaspar → IR bridge +│ │ └── src/ +│ │ ├── lib.rs +│ │ └── action.rs # impl ParsingAction → builds IR +│ │ +│ ├── qfbv-simplify/ # word-level rewrites +│ │ └── src/ +│ │ ├── lib.rs +│ │ └── rules.rs # constant fold, canonicalize, etc. +│ │ +│ ├── qfbv-bitblast/ # BV IR → Boolean gate graph +│ │ └── src/ +│ │ ├── lib.rs +│ │ ├── gates.rs # And, Or, Xor, Mux gate types +│ │ ├── circuits.rs # adder, multiplier, shifter, comparator +│ │ └── blast.rs # recursive bit-blaster dispatch +│ │ +│ ├── qfbv-cnf/ # gate graph → CNF via Tseitin +│ │ └── src/ +│ │ ├── lib.rs +│ │ └── tseitin.rs +│ │ +│ ├── qfbv-sat/ # SAT backend abstraction + impls +│ │ └── src/ +│ │ ├── lib.rs +│ │ ├── backend.rs # trait SatBackend +│ │ ├── varisat.rs +│ │ └── batsat.rs +│ │ +│ └── qfbv-solver/ # top-level orchestration + model extraction +│ └── src/ +│ ├── lib.rs +│ ├── solver.rs +│ └── model.rs # SAT model → BV values +│ +├── tests/ # integration + differential tests +│ ├── smtlib_qfbv/ # SMT-LIB benchmark files (.smt2) +│ ├── unit_bitblast.rs +│ ├── unit_circuits.rs +│ ├── differential.rs +│ └── roundtrip.rs +│ +└── tools/ + ├── cli/ # CLI binary: solve .smt2 files + └── bench/ # criterion benchmarks +``` + +Recommended repository layout for a standalone-but-integrated `qfbvsmtrs` crate: + +``` +crates/ +├── qfbvsmtrs/ # standalone pure-Rust QF_BV solver crate +│ ├── Cargo.toml +│ └── src/ +│ ├── lib.rs # public API exports +│ ├── config.rs # Config: SAT backend choice, budget, feature flags +│ ├── error.rs # qfbvsmtrs::Error / Result +│ ├── query.rs # Query, Command, Assertion, Assumption, expected responses +│ ├── ir.rs # TermId, Sort, BvTerm, BoolTerm, declarations +│ ├── arena.rs # hash-consing arena + sort/width validation +│ ├── frontend.rs # yaspar SMT-LIB -> qfbvsmtrs::Query +│ ├── builder.rs # optional native Rust builder API for standalone users +│ ├── simplify.rs # word-level rewrites over qfbvsmtrs IR +│ ├── gates.rs # Boolean gate arena + structural hashing +│ ├── circuits.rs # BV circuits: add/sub/mul/div/shifts/comparisons/overflow +│ ├── blast.rs # qfbvsmtrs IR -> gate graph +│ ├── cnf.rs # Tseitin encoder and CNF data structures +│ ├── sat.rs # internal SAT trait + selected pure-Rust SAT implementation +│ ├── solver.rs # orchestration: Query -> SolveResult +│ ├── model.rs # qfbvsmtrs-native model values and formatting +│ └── wire.rs # optional `smt-wire` feature: ExprView/BinaryRequest -> Query +│ +└── smt-server/ + └── src/ + ├── qfbvsmtrs_backend.rs # implements smt_server::Backend using qfbvsmtrs + ├── lib.rs # pub mod/export QfbvsmtrsBackend + └── main.rs # RacingBackend(Z3Backend, BinbitBackend, QfbvsmtrsBackend) +``` + +The crate split in the original greenfield layout (`qfbv-ir`, `qfbv-bitblast`, `qfbv-cnf`, etc.) can be introduced later if the solver grows enough to justify internal crates. Start with one standalone `qfbvsmtrs` crate so users can depend on it directly and the server can still integrate it cleanly. + +Public standalone API sketch: + +```rust +let result = qfbvsmtrs::solve_smt2(script, &qfbvsmtrs::Config::default())?; + +let mut b = qfbvsmtrs::Builder::new(); +let x = b.bv_var("x", 8)?; +let forty_two = b.bv_const(42, 8)?; +b.assert(b.bv_eq(x, forty_two)?)?; +let result = qfbvsmtrs::Solver::default().solve(&b.finish()?)?; +``` + +Optional server-facing API sketch: + +```rust +#[cfg(feature = "wire")] +pub fn query_from_wire(request: &smt_wire::BinaryRequest) -> Result; +``` + + +## 4. Implementation Phases + +### Phase 0: Standalone `qfbvsmtrs` Crate + Server Backend Adapter (Week 0–1) + +**Goal:** Create `qfbvsmtrs` as an independently usable crate and make `smt-server` consume it as the third backend. + +**Deliverables:** + +1. Add `crates/qfbvsmtrs` to the workspace with no dependency on `smt-server`. +2. Give the crate stable public entrypoints from day one: + +```rust +pub fn parse_smt2(script: &str) -> Result; +pub fn solve_smt2(script: &str, config: &Config) -> Result; + +pub struct Solver { /* config + reusable allocation pools later */ } +impl Solver { + pub fn solve(&mut self, query: &Query) -> Result; +} + +#[cfg(feature = "wire")] +pub fn query_from_wire(request: &smt_wire::BinaryRequest) -> Result; +``` + +3. Add `crates/smt-server/src/qfbvsmtrs_backend.rs`: + +```rust +#[derive(Debug, Clone, Default)] +pub struct QfbvsmtrsBackend; + +impl Backend for QfbvsmtrsBackend { + fn name(&self) -> &'static str { "qfbvsmtrs" } + + fn handle(&self, request: &smt_wire::BinaryRequest) -> smt_wire::Result { + // SIMPLIFY can identity-return initially. + // SOLVE converts request through qfbvsmtrs::query_from_wire(request), then calls Solver::solve. + // MINIMIZE/MAXIMIZE return Unknown until optimization is implemented. + } +} +``` + +4. Export it from `crates/smt-server/src/lib.rs`. +5. Add it to the default race in `crates/smt-server/src/main.rs` after Z3 and binbit: + +```rust +let backend = Arc::new(RacingBackend::new(vec![ + Arc::new(Z3Backend), + Arc::new(BinbitBackend), + Arc::new(QfbvsmtrsBackend::default()), +])); +``` + +6. Add standalone crate smoke tests for `solve_smt2` and server smoke tests that call `handle_binary_frame` with only `QfbvsmtrsBackend` for tiny formulas. + +Initial feature policy should be conservative: + +| Request feature | V1 behavior | +|---|---| +| `Command::Solve` | supported via bit-blaster + CNF + selectable SAT backend (`splr`, `varisat`, or internal DPLL) | +| `Command::Simplify` | identity `SimplifyBlock` passthrough | +| `Command::Minimize` / `Maximize` | supported with bit-hunt optimization over SAT queries | +| `WANT_MODEL` | supported for SAT by reading primary-input assignments | +| `assumption_roots` | supported by conjoining assumptions initially; later use SAT assumptions | +| `WANT_CORE` | supported for named assertions via deletion-based unsat-core minimization | +| `budget_ms != 0` | enforce with SAT timeout/time checks or return `Unknown`; do not silently exceed the requested budget | + +This phase reserves the standalone SMT-LIB API immediately. The implementation can be thin at first, but the crate ownership boundary should be clear: `qfbvsmtrs` owns its parser/IR/solver; `smt-server` only adapts wire requests and wire responses. + +**Wire bridge details for this project:** + +- `smt-wire` expressions are already topologically ordered: every parent references only earlier children. `qfbvsmtrs::wire::query_from_wire` can iterate `ExprView::node_count()` once and maintain `Vec>`/`Vec>` mappings. +- The bridge should translate every wire `Tag` into the standalone IR or return a typed `UnsupportedFeature` error that `QfbvsmtrsBackend` maps to `QueryResult::unknown(...)`, not a server error. +- The wire language has operations the original plan did not emphasize: `BV_SELECT`, `BV_SMOD`, Bool variables, and overflow predicates (`UADD_OVF`, `SADD_OVF`, `USUB_OVF`, `SSUB_OVF`, `UMUL_OVF`, `SMUL_OVF`, `NEG_OVF`, `SDIV_OVF`). These need either real circuits or explicit unknown/unsupported handling. +- Model conversion must preserve wire variable node refs. SAT primary inputs should map back to `BV_VAR`/`BOOL_VAR` nodes, then the server adapter should emit `smt_wire::ModelEntry { node_ref, value }` with little-endian BV bytes and Bool values encoded as width `0`. +- `assertion_roots` and `assumption_roots` both become Bool constraints in the standalone query. Initially assumptions may be conjoined; later they should become SAT assumptions so unsat-core and incremental behavior improve. +- If the request asks for `WANT_CORE` and qfbvsmtrs cannot produce named cores yet, the adapter should avoid returning a misleading coreless `unsat`; return `Unknown` on the UNSAT path so the racing layer can wait for Z3/binbit. +- Respect `budget_ms` by passing a deadline through `Config`; if the selected SAT backend cannot interrupt, return `Unknown` before starting work for nonzero tiny budgets or use a backend with timeout/interrupt support. + +### Phase 1: IR + Frontend (Weeks 1–3) + +**Goal:** Parse QF_BV SMT-LIB files into an internal representation. + +**Repository integration note:** this phase is required for standalone `qfbvsmtrs`. The server already has a text frontend, but that should not replace the crate's own standalone API. The server adapter should use `wire.rs` to translate `smt-wire` expressions into this same IR. + +**4.1.1 — BV Expression IR (`qfbvsmtrs::ir`)** + +Design a hash-consed DAG for BV terms. Every node is interned in an arena and referenced by a `TermId` (u32 index). Each node stores its sort (width for BV, or Bool). + +Core term variants: + +```rust +enum BvTerm { + // Leaf nodes + Const { width: u32, value: BigUint }, // literal bitvec + Var { width: u32, name: Symbol }, // declared bv const + + // Arithmetic + BvAdd(TermId, TermId), + BvSub(TermId, TermId), + BvMul(TermId, TermId), + BvUDiv(TermId, TermId), + BvURem(TermId, TermId), + BvSDiv(TermId, TermId), + BvSRem(TermId, TermId), + BvSMod(TermId, TermId), + BvNeg(TermId), + + // Bitwise + BvAnd(TermId, TermId), + BvOr(TermId, TermId), + BvXor(TermId, TermId), + BvNot(TermId), + BvShl(TermId, TermId), + BvLShr(TermId, TermId), + BvAShr(TermId, TermId), + RotateLeft { amount: u32, child: TermId }, + RotateRight { amount: u32, child: TermId }, + + // Structural + Concat(TermId, TermId), + Extract { high: u32, low: u32, child: TermId }, + ZeroExtend { extra: u32, child: TermId }, + SignExtend { extra: u32, child: TermId }, + Repeat { count: u32, child: TermId }, + + // Conditional (ite with BV result) + Ite { cond: TermId, then_: TermId, else_: TermId }, + + // Server wire helper: mux chain for compact branch tables + Select { cases: Vec<(TermId, TermId)>, default: TermId }, +} + +enum BoolTerm { + True, + False, + BoolVar(Symbol), + Not(TermId), + And(Vec), + Or(Vec), + Implies(TermId, TermId), + Eq(TermId, TermId), // works for both BV=BV and Bool=Bool + BvUlt(TermId, TermId), + BvUle(TermId, TermId), + BvSlt(TermId, TermId), + BvSle(TermId, TermId), + UAddOverflow(TermId, TermId), + SAddOverflow(TermId, TermId), + USubOverflow(TermId, TermId), + SSubOverflow(TermId, TermId), + UMulOverflow(TermId, TermId), + SMulOverflow(TermId, TermId), + NegOverflow(TermId), + SDivOverflow(TermId, TermId), + Ite { cond: TermId, then_: TermId, else_: TermId }, +} +``` + +**4.1.2 — yaspar Frontend (`qfbvsmtrs::frontend`)** + +Implement yaspar's `ParsingAction` trait hierarchy to build IR nodes from callbacks. Handle: +- `declare-const`, `declare-fun` (0-arity) for BV variables +- `define-fun` for named subexpressions (inline into IR or store as let-bindings) +- `assert` to collect top-level Boolean constraints +- `check-sat` to trigger solving +- `get-model` / `get-value` for model extraction +- `set-logic QF_BV` validation +- `push` / `pop` for incremental solving (can be stubbed initially) + +Since yaspar uses a callback API rather than returning an AST, keep a stack-based builder: as the parser calls back for each subexpression, push intermediate `TermId` values onto a stack, and pop children when a parent node is constructed. + +**Deliverable:** can parse any QF_BV `.smt2` file from the SMT-LIB benchmark suite and dump the IR as an S-expression for inspection. + + +### Phase 2: Bit-Blasting Core (Weeks 4–8) + +**Goal:** Lower every BV expression into a vector of Boolean gates, one per bit. + +**4.2.1 — Gate Graph (`qfbv-bitblast/gates.rs`)** + +```rust +type GateId = u32; +type BitVec = Vec; // bit 0 = LSB + +enum Gate { + // Constants + True, + False, + + // Primary input (one per bit of each BV variable) + Input(u32), // global input index + + // Boolean gates + And(GateId, GateId), + Or(GateId, GateId), + Xor(GateId, GateId), + Not(GateId), + Mux { sel: GateId, t: GateId, f: GateId }, // if sel then t else f +} +``` + +Store gates in a flat `Vec` arena. GateId is the index. During construction, apply structural hashing (detect duplicate gates) and trivial simplifications: +- `And(x, True) → x`, `And(x, False) → False`, `And(x, x) → x` +- `Or(x, True) → True`, `Or(x, False) → x` +- `Not(Not(x)) → x` +- `Xor(x, False) → x`, `Xor(x, x) → False` + +**4.2.2 — Circuit Building Blocks (`qfbv-bitblast/circuits.rs`)** + +Each BV operation maps to a known circuit. Implement these as functions returning a `BitVec` (the output bits): + +| Operation | Circuit | Notes | +|---|---|---| +| `bvadd` | Ripple-carry adder | `n` full-adder cells, discard final carry | +| `bvsub` | `bvadd(a, bvnot(b))` + carry_in=1 | Two's complement | +| `bvmul` | Shift-and-add (Wallace tree optional) | O(n²) gates; Wallace tree for perf later | +| `bvudiv` / `bvurem` | Long division (restoring or non-restoring) | Expensive — O(n²) gates, ~n iterations | +| `bvsdiv` / `bvsrem` | Sign-fixup wrapper around udiv/urem | Negate inputs/outputs based on sign bits | +| `bvsmod` | Derived from signed division/remainder semantics | Required by `smt-wire`; differs from `bvsrem` in sign convention | +| `bvneg` | `bvadd(bvnot(x), 1)` | Two's complement negate | +| `bvand/or/xor/not` | Bitwise (per-bit gate) | Trivial | +| `bvshl` | Barrel shifter | log₂(n) stages of mux layers | +| `bvlshr` | Barrel shifter (reverse) | Same structure, opposite direction | +| `bvashr` | Like lshr but fill with sign bit | | +| `rotate_left/right` | Concatenation/extract or shift/or rewrite | Indexed SMT-LIB rotate can be word-level rewritten | +| `concat(a,b)` | Append bit vectors | Zero gates (structural) | +| `extract [h:l]` | Slice bit vector | Zero gates (structural) | +| `zero_extend` | Pad with `False` gates | | +| `sign_extend` | Pad with copies of MSB | | +| `repeat` | Concatenate n copies | | +| `bvult` | Subtraction carry-out | `a < b` ↔ carry-out of `a - b` is 0 | +| `bvslt` | XOR sign bits into unsigned compare | Standard signed comparison circuit | +| `eq` | AND of per-bit XNOR | `a = b` ↔ `∧ᵢ ¬(aᵢ ⊕ bᵢ)` | +| `ite` | Per-bit mux | `mux(sel, then_i, else_i)` for each bit | +| `select` | Nested `ite`/mux chain | Used by server wire IR (`BV_SELECT`) | +| overflow predicates | Width-extension and sign-bit circuits | Match `smt-wire` overflow tags exactly | + +**4.2.3 — Top-Level Blaster (`qfbv-bitblast/blast.rs`)** + +Walk the BV IR DAG bottom-up. For each `TermId`: +- If it's a BV term of width `n`, produce a `BitVec` of length `n` +- If it's a Bool term, produce a single `GateId` +- Memoize results in a `HashMap` or `HashMap` + +The top-level assertion is a single `GateId` (the conjunction of all asserted Boolean terms). The SAT solver must find an assignment making this gate `True`, or prove it unsatisfiable. + + +### Phase 3: CNF Encoding + SAT (Weeks 7–10) + +**4.3.1 — Tseitin Encoding (`qfbv-cnf`)** + +Walk the gate graph. For each gate, introduce a fresh CNF variable and add clauses enforcing the gate semantics: + +| Gate | Clauses (var `g` ↔ gate output) | +|---|---| +| `And(a,b)` | `(¬g ∨ a)`, `(¬g ∨ b)`, `(g ∨ ¬a ∨ ¬b)` | +| `Or(a,b)` | `(g ∨ ¬a)`, `(g ∨ ¬b)`, `(¬g ∨ a ∨ b)` | +| `Xor(a,b)` | 4 clauses for `g ↔ a ⊕ b` | +| `Not(a)` | `(g ∨ a)`, `(¬g ∨ ¬a)` | +| `Mux(s,t,f)` | 4–6 clauses for `g ↔ ite(s,t,f)` | +| `True` | unit clause `(g)` | +| `False` | unit clause `(¬g)` | +| `Input(i)` | no clauses (primary input, free variable) | + +Add a unit clause asserting the top-level assertion gate to be true. + +Only encode gates that are reachable from the assertion root (dead gate elimination via a simple DFS from the root). + +**4.3.2 — SAT Backend Trait (`qfbv-sat`)** + +```rust +pub enum SatResult { + Sat(Vec), // assignment indexed by CnfVar + Unsat, + Unknown, +} + +pub trait SatBackend { + fn new_var(&mut self) -> CnfVar; + fn add_clause(&mut self, lits: &[Lit]); + fn solve(&mut self, assumptions: &[Lit], deadline: Option) -> SatResult; + fn value(&self, var: CnfVar) -> Option; +} +``` + +Implement for `varisat::Solver` and/or `batsat::Solver`. The trait should include assumptions and a deadline from the start because `smt-server` has `assumption_roots` and `budget_ms` in every request. + +**4.3.3 — Model Extraction (`qfbvsmtrs::model`)** + +When SAT returns `Sat(assignment)`: +1. For each declared BV variable, look up which `Input(i)` gates correspond to its bits +2. Read the assignment for those CNF variables +3. Reconstruct the BV constant value +4. Format as SMT-LIB `get-model` / `get-value` output + + +### Phase 4: Word-Level Simplification (Weeks 9–12) + +**Goal:** Reduce IR size before bit-blasting to shrink the CNF. + +**4.4.1 — Constant Folding** +Evaluate any node whose children are all constants. E.g. `bvadd(#x0003, #x0001)` → `#x0004`. + +**4.4.2 — Identity / Annihilator Rules** +- `bvadd(x, 0) → x` +- `bvmul(x, 1) → x`, `bvmul(x, 0) → 0` +- `bvand(x, all_ones) → x`, `bvand(x, 0) → 0` +- `bvor(x, 0) → x`, `bvor(x, all_ones) → all_ones` +- `bvxor(x, 0) → x` + +**4.4.3 — Extract/Concat Simplification** +- `extract[n-1:0](x)` where `x` has width `n` → `x` +- `extract[h:l](concat(a, b))` → push extract through concat +- `concat(extract[h:m+1](x), extract[m:l](x))` → `extract[h:l](x)` when adjacent + +**4.4.4 — Normalization** +- Commutative ops: sort children by TermId for better deduplication +- Double negation: `bvnot(bvnot(x))` → `x` +- Comparison flipping: `bvugt(a, b)` → `bvult(b, a)` + +**4.4.5 — Dead Node Elimination** +After simplification, run a reachability pass from assertion roots and drop unreferenced nodes. + +Implement as a fixpoint loop: apply all rules until no changes occur. Use a worklist for efficiency. + + +### Phase 5: Polish + Incremental Solving (Weeks 11–14) + +- Support `push` / `pop` with assertion-stack scoping (requires SAT solver assumption-based interface or re-creation) +- Support `get-model`, `get-value`, `exit`, `echo`, `set-info`, `set-option` +- CLI binary: `qfbv-solve input.smt2` → prints `sat` / `unsat` + optional model +- Error messages referencing source positions (yaspar provides `Position` / `Range`) +- Incremental `check-sat` under assumptions using SAT solver assumption literals + + +## 5. Testing Strategy + +Testing is organized in four layers, from unit-level correctness up to whole-solver validation against known-good solvers. + +### 5.1 Layer 1 — Unit Tests for Individual Circuits + +**What:** Test each circuit function in `qfbv-bitblast/circuits.rs` in isolation: adder, multiplier, comparator, shifter, divider, etc. + +**How:** For small widths (4-bit, 8-bit), exhaustively enumerate all input combinations and verify the circuit output matches the expected result computed in Rust natively. + +``` +For each operation OP in {add, sub, mul, udiv, urem, shl, lshr, ashr, ...}: + For width in {4, 8}: // 4-bit = 256×256 = 65536 pairs + For all (a, b) in 0..2^width × 0..2^width: + 1. Build a gate graph for OP(a_bits, b_bits) + 2. Tseitin-encode it with a_bits and b_bits forced to constants + 3. Solve with SAT → must be SAT + 4. Extract result bits from the model + 5. Assert result == rust_native_op(a, b) +``` + +For unary ops (not, neg, extract, extend), enumerate all values of the single operand. + +For expensive ops (mul, div at 8-bit = 65536 pairs), this is still fast (well under a second per op). At 16-bit (4B pairs), switch to random sampling (see 5.2). + +**Coverage target:** 100% of circuit functions, exhaustive at 4-bit. + +### 5.2 Layer 2 — Randomized / Fuzz Testing of Circuits + +**What:** For wider widths (16, 32, 64-bit) where exhaustive testing is infeasible, run randomized tests. + +**How:** + +``` +For each operation OP: + For width in {16, 32, 64}: + For _ in 0..10_000: + a, b = random values of `width` bits + 1. Build gate graph, encode, solve + 2. Assert output matches native Rust computation +``` + +Use `proptest` or `quickcheck` for property-based testing with shrinking. Key properties: + +- **Arithmetic identity:** `bvadd(a, 0) == a` for all `a` +- **Commutativity:** `bvadd(a, b) == bvadd(b, a)` +- **Inverse:** `bvsub(bvadd(a, b), b) == a` +- **Negation:** `bvadd(a, bvneg(a)) == 0` +- **Shift-mask:** `bvshl(a, k)` for `k ≥ width` equals 0 +- **DeMorgan:** `bvnot(bvand(a, b)) == bvor(bvnot(a), bvnot(b))` +- **Division:** `bvmul(bvudiv(a, b), b) + bvurem(a, b) == a` (when `b ≠ 0`) + +### 5.3 Layer 3 — Tseitin Encoding Correctness + +**What:** Verify that the Tseitin encoder faithfully preserves the semantics of the gate graph. + +**How:** +1. Build small gate graphs (e.g. `And(Xor(a, b), Or(a, c))`) with known truth tables +2. Enumerate all input assignments (feasible for ≤ ~20 inputs) +3. For each assignment, evaluate the gate graph natively (simple recursive eval) to get the expected output +4. Check that the SAT solver under forced inputs agrees: force input variables, add assertion that output variable equals expected value → must be SAT + +Also test that forcing the output to the *wrong* value yields UNSAT. + +### 5.4 Layer 4 — Differential Testing Against Z3 + +**What:** The most important end-to-end validation. Run the same `.smt2` files through standalone `qfbvsmtrs::solve_smt2`, the `QfbvsmtrsBackend` server adapter, and Z3, compare results. + +**How:** + +``` +For each .smt2 file in test suite: + result_ours = run_our_solver(file) + result_z3 = run_z3_binary(file) // via `std::process::Command` + + assert!(result_ours == result_z3) // sat/unsat must agree + if sat: + // Optional: feed our model back into Z3 as assertions + // to verify the model is valid +``` + +**Test sources:** +1. **SMT-LIB QF_BV benchmarks** — download from [smtlib.cs.uiowa.edu](https://smtlib.cs.uiowa.edu/benchmarks.shtml). Start with the `QF_BV/20190311-bv-term-small-rw-Noetzli` and `QF_BV/2017-BuchwaldFried` sets (small, fast). +2. **Hand-written regression tests** — edge cases you discover during development: zero-width extracts, division by zero, maximum-width shifts, sign extension of width-1, etc. +3. **Fuzz-generated formulas** — see 5.5. + +**Z3 availability:** Differential tests require a `z3` binary on PATH. Gate these behind a cargo feature or environment variable (`DIFFERENTIAL_TESTS=1`) so CI can run them when z3 is installed, and they're skipped otherwise. + +### 5.5 Layer 5 — Grammar-Based Fuzzing + +**What:** Generate random well-typed QF_BV formulas and test for crashes, panics, and disagreement with Z3. + +**How:** Write a random QF_BV formula generator: + +``` +fn random_bv_formula(rng, max_depth, num_vars, widths) -> String: + 1. Declare N bit-vector variables with random widths from `widths` + 2. Build a random expression tree: + - At leaves: pick a variable or random constant + - At internal nodes: pick a random BV operation + (respecting width constraints — both args same width for + arithmetic, etc.) + 3. Wrap in (assert ...) (check-sat) (get-model) (exit) + 4. Return as SMT-LIB string +``` + +Run thousands of generated formulas through both our solver and Z3. Any disagreement is a bug in our solver (Z3 is the oracle). Minimize failing cases with `proptest` shrinking or binary search on the formula AST. + +Consider also integrating `cargo-fuzz` / `libfuzzer` targeting the yaspar→IR→bitblast→solve pipeline with arbitrary byte inputs (testing robustness against malformed input). + +### 5.6 Layer 6 — Known-Answer Tests (Regression Suite) + +Maintain a directory of `.smt2` files with known `sat` / `unsat` answers and (for sat cases) known model values. Run these in CI on every commit. Start with hand-written cases covering: + +- Every BV operation individually (e.g. `(assert (= (bvadd #x0F #x01) #x10))`) +- Combinations of 2–3 operations +- Edge cases: + - Width-1 bit-vectors (effectively Booleans) + - Maximum-width constants (#xFFFFFFFF for 32-bit) + - Division / remainder by zero (SMT-LIB defines `bvudiv(x, 0) = #xFF...F` and `bvurem(x, 0) = x`) + - Shift amounts ≥ width + - Extract with `high == low` (single bit) + - Sign extension of a 1-bit value + - Nested ite expressions + +### 5.7 Testing Summary + +| Layer | Scope | Method | When to Run | +|---|---|---|---| +| 1. Circuit unit | Individual adder, mul, etc. | Exhaustive 4-bit, exhaustive 8-bit | Every commit | +| 2. Randomized circuit | Wider widths (16/32/64) | deterministic randomized identity/property tests; 10k samples with `QFBVSMTRS_RANDOM_CIRCUIT_SAMPLES=10000` | Smoke every commit, 10k in scheduled CI | +| 3. Tseitin encoding | Gate graph → CNF | Truth table enumeration across `splr`, `varisat`, and DPLL | Every commit | +| 4. Differential vs Z3 | Whole solver | `cargo test -p qfbvsmtrs --test differential_z3`; extended random with `QFBVSMTRS_DIFF_RANDOM=1`; optional `QFBVSMTRS_SMTLIB_DIR` corpus | Smoke every commit, extended/corpus in scheduled CI | +| 5. Fuzzing | Parser → IR → bit-blast → solve pipeline | `cargo fuzz run smt2_pipeline --manifest-path crates/qfbvsmtrs/fuzz/Cargo.toml` | Nightly / weekly | +| 6. Known-answer | Regression suite | Fixed `.smt2` fixtures under `crates/qfbvsmtrs/tests/fixtures/known` plus inline edge cases | Every commit | + + +## 6. Performance Considerations + +### 6.1 Gate Graph Compaction +Structural hashing during gate construction is critical — without it, shared subexpressions generate duplicate gates and the CNF blows up. Use a `HashMap` for deduplication. + +### 6.2 Multiplication +Shift-and-add multiplication generates O(n²) gates for n-bit operands. For 64-bit multiply, that's ~4096 full-adder cells → ~12k gates → ~36k CNF clauses. This is manageable. A Booth encoder or Wallace tree compressor can reduce this by ~30–40% but adds implementation complexity — defer until profiling shows multiply is the bottleneck. + +### 6.3 Division +Long division is the most expensive operation: O(n²) subtractors, each being an n-bit ripple-carry adder. For 32-bit division, expect ~32 × 32 = ~1024 full-adder cells. This is inherent to bit-blasting division and is why solvers like Bitwuzla apply aggressive word-level rewriting to avoid blasting division when possible (e.g. if the divisor is a constant power of 2, replace with a shift). + +### 6.4 CNF Variable Count +A rough budget: each full-adder produces 2 gates (sum, carry). An n-bit add is ~2n gates → ~2n CNF variables + ~6n clauses. A 32-bit formula with 10 additions and 2 multiplications might produce ~50k CNF variables and ~150k clauses — well within what modern SAT solvers handle in milliseconds. + +### 6.5 SAT Solver Choice +For the problem sizes typical in symbolic execution (thousands to low millions of CNF variables), all three pure-Rust solvers (varisat, batsat, splr) should be adequate. The SAT backend trait makes benchmarking trivial. + + +## 7. Division-by-Zero Semantics + +SMT-LIB defines specific results for division and remainder by zero: +- `bvudiv(x, 0)` = all-ones (`#xFF...F`) +- `bvurem(x, 0)` = `x` +- `bvsdiv` and `bvsrem` follow from these via sign fixup + +The bit-blasting circuit for `bvudiv(a, b)` must handle this: when all bits of `b` are zero, force the quotient to all-ones and the remainder to `a`. Implement with a `b_is_zero` check (NOR of all b-bits) feeding a per-bit mux on the outputs. + + +## 8. Milestones and Timeline + +| Week | Milestone | Exit Criteria | +|---|---|---| +| 0–1 | Standalone crate + server adapter skeleton | `crates/qfbvsmtrs` builds independently; `QfbvsmtrsBackend` exists and returns `Unknown`/identity simplify cleanly | +| 1–2 | IR design + hash-consing arena | Can represent all QF_BV terms and server wire tags; sort-checks pass | +| 3 | yaspar frontend + `smt-wire` bridge | `solve_smt2` parses standalone scripts; server adapter converts `BinaryRequest` into the same IR | +| 4–5 | Bit-blasting: bitwise + arithmetic (add, sub, neg) | Layer 1 tests pass for and/or/xor/not/add/sub at 4-bit exhaustive | +| 6–7 | Bit-blasting: mul, div, shifts, comparisons, extract/concat | Layer 1 tests pass for all ops at 4-bit; Layer 2 random tests at 32-bit | +| 8 | Tseitin encoder + SAT integration | Can solve simple hand-written .smt2 files end-to-end | +| 9–10 | Model extraction + full SMT-LIB command support | Layer 4 differential tests passing on small benchmark suites | +| 11–12 | Word-level simplifier | Measurable CNF reduction on benchmarks; no regressions | +| 13–14 | Polish, CLI, incremental solving, CI pipeline | Fuzzing runs (Layer 5) with no crashes; benchmark suite timing baseline | + + +## 9. Future Extensions (Out of Scope for V1) + +- **Arrays (QF_ABV):** add `select` / `store` with read-over-write axiom instantiation (lemma on demand) or eager Ackermann expansion +- **Uninterpreted functions (QF_UFBV):** Ackermann reduction to QF_BV (replace `f(a) = f(b)` with `a = b → fa = fb` for fresh constants `fa, fb`) +- **WASM target:** since all deps are pure Rust, `wasm32-unknown-unknown` should work with minimal effort — test this early +- **Proof production:** emit DRAT or LRAT proofs from the SAT solver and chain them through bit-blasting for certified UNSAT +- **Caching / incremental solving:** for symbolic execution, successive queries often differ by one path constraint — exploit this with assumption-based incremental SAT + +``` + +`docs/qfbvsmtrs-corpus-results.md`: + +```md +# qfbvsmtrs SMT-LIB QF_BV corpus run + +Corpus used: SMT-LIB release 2025, non-incremental `QF_BV.tar.zst` from Zenodo record `16740866`. + +Local corpus size: + +- compressed: `1,732,883,333` bytes +- decompressed SMT-LIB: `46,191` `.smt2` files, about `37.3 GB` + +## Current strict-budget run + +Report: `target/smtlib/qfbvsmtrs_corpus_report_v3.jsonl` + +Command shape: + +```sh +cargo build --release -p qfbvsmtrs +python scripts/qfbvsmtrs_corpus.py target/smtlib/QF_BV-2025 \ + --exe target/release/qfbvsmtrs.exe \ + --budget-ms 3000 \ + --timeout 30 \ + --workers 12 \ + --report target/smtlib/qfbvsmtrs_corpus_report_v3.jsonl +``` + +Result: + +| Class | Count | +|---|---:| +| Conclusive and matched `:status` | 33,590 | +| Returned `unknown` against a known sat/unsat status | 10,771 | +| Hit the hard process timeout | 1,830 | +| Frontend/backend error | 0 | +| Conclusive wrong answer | 0 | + +The current full run has no parser/frontend errors and no wrong conclusive answers, but it is still not production-complete for arbitrary SMT-LIB QF_BV workloads because 12,601 files remain non-conclusive under this strict budget. + +Targeted incremental reruns after adding associative/commutative BV add/mul canonicalization, scaled-add folding, simple shifted-product rewrites, extract/concat simplifications, and a few broad arithmetic pattern shortcuts improved several slices: + +```sh +python scripts/qfbvsmtrs_corpus.py target/smtlib/QF_BV-2025 \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_report_v3.jsonl \ + --rerun-kinds unknown,timeout \ + --path-contains 20190311-bv-term-small-rw-Noetzli \ + --report target/smtlib/qfbvsmtrs_corpus_rerun_noetzli_linear.jsonl \ + --budget-ms 3000 \ + --timeout 30 \ + --workers 8 +``` + +Additional targeted runs used the same baseline/merge mechanism for `bruttomesso`, `sage/app1`, `pspace`, and `challenge`: + +```sh +python scripts/qfbvsmtrs_corpus.py target/smtlib/QF_BV-2025 \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_report_v3.jsonl \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_rerun_noetzli_linear.jsonl \ + --rerun-kinds unknown,timeout \ + --path-contains bruttomesso \ + --report target/smtlib/qfbvsmtrs_corpus_rerun_bruttomesso_extract.jsonl \ + --budget-ms 3000 \ + --timeout 30 \ + --workers 8 + +python scripts/qfbvsmtrs_corpus.py target/smtlib/QF_BV-2025 \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_report_v3.jsonl \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_rerun_noetzli_linear.jsonl \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_rerun_bruttomesso_extract.jsonl \ + --rerun-kinds unknown,timeout \ + --path-regex "sage.*app1" \ + --report target/smtlib/qfbvsmtrs_corpus_rerun_sage_app1.jsonl \ + --budget-ms 3000 \ + --timeout 30 \ + --workers 8 + +python scripts/qfbvsmtrs_corpus.py target/smtlib/QF_BV-2025 \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_report_v3.jsonl \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_rerun_noetzli_linear.jsonl \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_rerun_bruttomesso_extract.jsonl \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_rerun_sage_app1.jsonl \ + --rerun-kinds unknown,timeout \ + --path-contains pspace \ + --report target/smtlib/qfbvsmtrs_corpus_rerun_pspace_patterns.jsonl \ + --budget-ms 3000 \ + --timeout 30 \ + --workers 8 + +python scripts/qfbvsmtrs_corpus.py target/smtlib/QF_BV-2025 \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_report_v3.jsonl \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_rerun_noetzli_linear.jsonl \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_rerun_bruttomesso_extract.jsonl \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_rerun_sage_app1.jsonl \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_rerun_pspace_patterns.jsonl \ + --rerun-kinds unknown,timeout \ + --path-contains challenge \ + --report target/smtlib/qfbvsmtrs_corpus_rerun_challenge_overflow.jsonl \ + --budget-ms 3000 \ + --timeout 30 \ + --workers 4 +``` + +Merged with the full baseline these targeted passes yield: + +| Class | Count | +|---|---:| +| Conclusive and matched `:status` | 33,835 | +| Returned `unknown` against a known sat/unsat status | 10,531 | +| Hit the hard process timeout | 1,825 | +| Frontend/backend error | 0 | +| Conclusive wrong answer | 0 | + +## Latest merged status + +Additional incremental reports after the earlier passes include targeted reruns for `Sage2` samples, `log-slicing`, `brummayerbiere4`, `challenge`, Noetzli polynomial rewrite cases, and Bruttomesso extensional cases. + +Latest merged-by-path status across the full baseline and targeted reports currently stands at: + +| Class | Count | +|---|---:| +| Conclusive and matched `:status` | 34,327 | +| Returned `unknown` against a known sat/unsat status | 10,048 | +| Hit the hard process timeout | 1,816 | +| Frontend/backend error | 0 | +| Conclusive wrong answer | 0 | + +See `docs/qfbvsmtrs-progress-report.md` for the current progress summary and `docs/qfbvsmtrs-design-report.md` for solver architecture. + +## Incremental reruns + +The corpus runner is append-only and resumable. It can now use a prior report as a baseline and skip known-good files: + +```sh +python scripts/qfbvsmtrs_corpus.py target/smtlib/QF_BV-2025 \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_report_v3.jsonl \ + --rerun-kinds unknown,timeout \ + --report target/smtlib/qfbvsmtrs_corpus_rerun_30s.jsonl \ + --budget-ms 30000 \ + --timeout 120 \ + --workers 8 +``` + +Restrict a rerun to one benchmark family: + +```sh +python scripts/qfbvsmtrs_corpus.py target/smtlib/QF_BV-2025 \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_report_v3.jsonl \ + --rerun-kinds unknown,timeout \ + --path-contains 20190311-bv-term-small-rw-Noetzli \ + --report target/smtlib/qfbvsmtrs_corpus_rerun_noetzli.jsonl \ + --budget-ms 3000 \ + --timeout 30 \ + --workers 8 +``` + +Selection dry-run: + +```sh +python scripts/qfbvsmtrs_corpus.py target/smtlib/QF_BV-2025 \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_report_v3.jsonl \ + --rerun-kinds unknown,timeout \ + --report target/smtlib/qfbvsmtrs_corpus_rerun_30s.jsonl \ + --list-only +``` + +For the original strict full-run baseline this queues 12,601 non-conclusive files and skips 33,590 known-good files. With the latest merged targeted reports, the non-conclusive set is down to 11,864 files. Re-running the same command resumes automatically because paths already present in the output report are skipped. Multiple `--baseline-report` arguments can be supplied; later reports override earlier records by path, so follow-up runs can skip cases solved by prior incremental passes. + +``` + +`docs/qfbvsmtrs-design-report.md`: + +```md +# qfbvsmtrs design report + +Date: 2026-05-05 + +`qfbvsmtrs` is a standalone pure-Rust solver for SMT-LIB QF_BV formulas. It is also integrated into this repository's `smt-server` as `QfbvsmtrsBackend`. The core design goal is that the solver crate remains independently usable: server integration is an adapter, not a dependency from the solver back into `smt-server`. + +## High-level architecture + +```text +Standalone API / SMT-LIB text / smt-wire request + | + v +qfbvsmtrs Query IR + | + v +Eager word-level simplification + solver preprocessing + | + v +Reachability-filtered bit-blasting + | + v +Structurally hashed Boolean gate graph + | + v +Deadline-aware Tseitin CNF encoding + | + v +Pure-Rust SAT backend: splr / varisat / DPLL + | + v +SolveResult: sat/model, unsat/core, optimum, unknown +``` + +The pipeline is intentionally conventional: keep the word-level layer simple and sound, bit-blast to Boolean circuits, encode to CNF, then delegate search to a SAT solver. Most production work now focuses on reducing the amount of bit-blasting/SAT search by adding safe word-level simplifications and family-specific generalizations that recognize common SMT-LIB benchmark patterns. + +## Crate layout + +Core crate: `crates/qfbvsmtrs` + +| File | Responsibility | +|---|---| +| `lib.rs` | Public exports and optional `wire` feature gate | +| `config.rs` | Solver configuration, budget, SAT backend selection | +| `error.rs` | Solver-local `Error` and `Result` types | +| `ir.rs` | Hash-consed term arena, `TermId`, `Sort`, `NodeKind` | +| `query.rs` | Query, assertions, assumptions, commands, optimization target | +| `builder.rs` | Native Rust builder API plus eager simplification hooks | +| `frontend.rs` | SMT-LIB parsing/lowering and SMT-LIB response formatting | +| `wire.rs` | Optional `smt-wire` bridge behind `wire` feature | +| `gates.rs` | Boolean gate arena with structural hashing | +| `circuits.rs` | Bit-vector circuits over Boolean gates | +| `blast.rs` | IR-to-gate bit-blaster with reachable-term filtering | +| `cnf.rs` | Tseitin CNF encoder with clause normalization and deadlines | +| `sat.rs` | SAT backend dispatch: `splr`, `varisat`, DPLL | +| `model.rs` | SAT assignment to qfbvsmtrs model values | +| `solver.rs` | Top-level solve/optimize/core orchestration and preprocessing | +| `eval.rs` | Constant-assignment evaluator used for cheap SAT witness detection | +| `simplify.rs` | Simplification module placeholder/documentation hook | + +Server integration lives in `crates/smt-server/src/qfbvsmtrs_backend.rs` and converts between server `QueryResult` values and qfbvsmtrs `SolveResult` values. + +## Public entry points + +The crate supports three input paths into the same solver core. + +### SMT-LIB text + +```rust +let result = qfbvsmtrs::solve_smt2(script, &qfbvsmtrs::Config::default())?; +``` + +This path parses SMT-LIB, builds a `Query`, solves it, and can format the response back to SMT-LIB text. + +### Native builder API + +```rust +let mut b = qfbvsmtrs::Builder::new(); +let x = b.bv_var("x", 8)?; +let one = b.bv_const(1, 8)?; +let target = b.bv_add(x, one)?; +let two = b.bv_const(2, 8)?; +let eq = b.bv_eq(target, two)?; +b.assert(eq)?; +let query = b.finish()?; +let result = qfbvsmtrs::Solver::new(Default::default()).solve(&query)?; +``` + +The builder performs eager simplification while constructing terms. + +### `smt-wire` bridge + +With feature `wire`, the crate can lower `smt-wire` expressions/requests into qfbvsmtrs queries. This keeps server integration thin while preserving qfbvsmtrs as a standalone crate. + +## IR design + +The IR is a sorted, hash-consed DAG: + +- `TermId` is a compact index into the arena. +- Every node has a `Sort`: `Bool` or `Bv(width)`. +- `Arena::add` structurally hashes `(NodeKind, Sort)`, so identical subterms share one `TermId`. +- Sort and width checks happen at construction boundaries. +- The arena is append-only, which makes it easy to process terms in topological order. + +The IR stores both Boolean and bit-vector terms in one arena. This avoids a separate Boolean AST layer and makes mixed nodes such as `BvIte`, `BoolIte`, comparisons, and overflow predicates straightforward. + +## SMT-LIB frontend + +The SMT-LIB frontend is based on `yaspar`. It supports the QF_BV subset used by the solver, including: + +- declarations and definitions (`declare-fun`, `define-fun`, `define-const`); +- Boolean connectives and n-ary equality/distinct; +- QF_BV arithmetic, bitwise, comparison, extraction, concatenation, extension, repeat, rotate, and overflow-style operators covered by the IR; +- `let` expansion; +- scoped assertions via `push`/`pop`; +- model/value/core-oriented commands that map to qfbvsmtrs query flags. + +The parser lowers directly through the builder, so parser-time construction benefits from the same eager simplifications as native API users. + +## Word-level simplification + +The builder is the main simplification hook. It performs local, sound rewrites before bit-blasting. Current classes include: + +- Boolean constants, double negation, idempotence, contradiction/tautology pairs; +- BV constant folding for arithmetic/bitwise/shift/extract/concat/extension/repeat/rotate/comparison operations; +- BV identities and annihilators such as `x + 0`, `x * 1`, `x & 0`, `x | 0`, `x ^ 0`; +- structural `extract(extract(...))`, `extract(concat(...))`, and adjacent-extract concat merging; +- associative/commutative canonicalization for `bvadd` and `bvmul`; +- scaled add-term folding such as `x + x -> 2*x`; +- shifted product/add rewrites; +- extension/constant equality and unsigned comparison reductions; +- 1-bit ITE equality reduction; +- limited polynomial equality normalization for small-to-medium rewrite verification formulas. + +The solver also has preprocessing shortcuts in `solver.rs` for patterns that are too global for the local builder: + +- unsigned successor order contradictions; +- unsigned wraparound SAT witnesses; +- power-of-two sum contradiction patterns; +- shift-one-add contradiction patterns; +- unsigned and signed multiplication-overflow guard proofs; +- log-slicing add/sub equivalence proofs; +- extensional extract/concat candidate contradictions; +- cheap all-zero/all-one assignment SAT witness detection. + +These shortcuts are deliberately conservative. They return conclusive SAT/UNSAT only when the syntactic proof pattern is recognized exactly enough to be sound; otherwise the normal bit-blast/SAT path is used. + +## Bit-blasting design + +Bit-blasting maps each term to either: + +- one Boolean gate for Bool terms; or +- little-endian vectors of Boolean gates for bit-vector terms. + +Important design choices: + +- The blaster starts from assertions/assumptions/needed model variables and only visits reachable terms. +- Declared variables are included for model extraction when a model is requested. +- The gate arena structurally hashes gates, so repeated Boolean subcircuits share nodes. +- BV circuits are implemented in `circuits.rs` over gate vectors. + +Implemented circuit families include: + +- add, sub, negation; +- multiplication; +- unsigned/signed division and remainder; +- signed modulus; +- shifts and rotates; +- extract, concat, zero/sign extension, repeat; +- unsigned/signed comparisons; +- equality and Boolean connectives; +- ITE/select/mux; +- unsigned/signed overflow predicates. + +## CNF encoding + +The CNF encoder performs Tseitin encoding over the gate graph: + +- one SAT variable is allocated per gate output as needed; +- clauses are normalized by sorting/deduplicating literals; +- tautological clauses are skipped; +- deadlines are checked during encoding; +- if a deadline expires, the solver reports `unknown("budget exhausted")` instead of producing a partial answer. + +This keeps the SAT interface simple and makes backend switching straightforward. + +## SAT backend strategy + +SAT backends are selected through `Config::with_sat_backend`: + +- `SatBackendKind::Splr`: default production backend; +- `SatBackendKind::Varisat`: alternate pure-Rust backend for experiments/cross-checking; +- `SatBackendKind::Dpll`: internal simple solver retained for fallback/testing. + +Backend behavior: + +- `splr` is wrapped so known inconsistent/root-conflict errors map to UNSAT. +- `splr` panics are caught and converted to `Unknown`, though the panic hook can still print to stderr. +- budgeted large-CNF `splr` solves can fall back to DPLL to better respect deadlines. +- `varisat` does not support deadlines in this integration, so budgeted `varisat` solves return `Unknown`. +- DPLL is useful for tiny cases and tests, not as the main production backend for large formulas. + +## Model extraction + +For SAT results with `want_model = true`, the solver maps SAT assignment bits back through `BlastedVariable` records: + +- BV variables are reconstructed from little-endian gate bits; +- Bool variables are read from their gate value; +- model values use qfbvsmtrs `ScalarValue` and can be formatted back to SMT-LIB. + +If no model is requested, SAT shortcuts may return `sat` without constructing a model. This is used intentionally for cheap witness patterns and corpus runs where only `check-sat` is needed. + +## Unsat cores + +Named unsat cores are supported for named assertions. The current algorithm is deletion-based: + +1. solve the full query; +2. for each named assertion, temporarily remove it; +3. keep it removed if the remaining query is still UNSAT; +4. return the minimized set of names that remains necessary. + +This is simple and robust but can be expensive because it requires multiple solver calls. It is appropriate for current integration tests and moderate queries; production core extraction for large formulas would benefit from assumption-literal based cores in the SAT backend. + +## Optimization + +`Command::Minimize` and `Command::Maximize` are implemented by repeated SAT queries using bit-hunt constraints over the target BV. This is solver-backend agnostic and integrates with the existing bit-blast pipeline, but it is not as efficient as a dedicated MaxSMT/optimization engine. + +## Server integration + +`smt-server` integrates qfbvsmtrs through an adapter: + +```text +smt-wire BinaryRequest + | + v +qfbvsmtrs wire bridge + | + v +qfbvsmtrs Query/Solver + | + v +smt-server QueryResult +``` + +The main server races: + +- Z3 backend; +- binbit backend; +- qfbvsmtrs backend. + +This lets qfbvsmtrs provide conclusive pure-Rust answers when it can, while existing backends can still win on workloads where qfbvsmtrs is slower or unknown. + +## CLI and tooling + +The crate provides command-line tools: + +- `qfbvsmtrs`: solve one SMT-LIB file; uses a 64 MB worker stack; supports budget/backend/trace environment variables; +- `qfbvsmtrs_stats`: parse/blast/CNF statistics and timing; +- `qfbvsmtrs_bench`: CSV benchmark output for selected cases. + +Corpus tooling lives in `scripts/qfbvsmtrs_corpus.py` and supports: + +- append-only JSONL reports; +- resumable execution; +- merged latest-by-path baselines; +- skipping known-good files; +- family/path filters; +- rerunning only selected result kinds. + +## Testing and validation design + +The validation strategy combines several layers: + +1. **Unit and known-answer tests** for specific SMT-LIB and API behaviors. +2. **Circuit-level exhaustive tests** for all 4-bit inputs on many operators. +3. **Randomized circuit property tests** for wider widths. +4. **Tseitin truth-table tests** across SAT backends. +5. **Rust Z3 differential tests** for random formulas and optional corpus slices. +6. **Server integration tests** for binary/text/cached/racing paths. +7. **Fuzz harness** for parser-to-solver pipeline robustness. +8. **Full SMT-LIB corpus runner** for broad conformance/performance triage. + +The current strongest evidence is: zero parser/backend errors and zero wrong conclusive answers in the latest merged SMT-LIB corpus reports. The main remaining gap is non-conclusive coverage. + +## Known limitations + +- Not production-complete for arbitrary QF_BV corpus workloads yet. +- Latest merged corpus still has `10,048` `unknown` and `1,816` timeout results under strict budgets. +- No arrays, floating point, quantifiers, or uninterpreted functions. +- Full no-budget corpus proof is not done. +- Full-corpus Z3 differential testing is not done. +- Long-running fuzz campaign is not done. +- Some preprocessing shortcuts are syntactic and intentionally narrow. +- Unsat-core extraction is deletion-based and can be expensive. +- `varisat` backend currently cannot honor solve deadlines. +- `splr` backend panics are caught as `Unknown`, but panic text may still appear on stderr. + +## Design direction + +The solver should continue to evolve in three directions: + +1. **General word-level simplification**: broader equality propagation, bit-slice reasoning, byte/word packing, polynomial/linear normalization, and range reasoning. +2. **Better SAT scalability**: improved CNF size, incremental assumptions for cores/optimization, and tighter backend timeout integration. +3. **Corpus-driven triage**: keep using merged incremental reports to identify large families, add sound generalizations, and rerun only non-conclusive cases. + +This preserves the standalone crate boundary while making qfbvsmtrs increasingly useful as a production-grade pure-Rust backend. + +``` + +`docs/qfbvsmtrs-production.md`: + +```md +# qfbvsmtrs production validation + +`qfbvsmtrs` is a standalone pure-Rust QF_BV solver crate integrated into `smt-server` as `QfbvsmtrsBackend`. + +## SAT backends + +- Default: `splr` CDCL SAT solver with timeout support. +- Alternate: `varisat` CDCL SAT solver for cross-checking and experiments. +- Fallback/testing: internal DPLL solver. + +Select a backend with `Config::with_sat_backend(SatBackendKind::...)`. + +## Required local gates + +```sh +cargo fmt --all +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace +cargo check --manifest-path crates/qfbvsmtrs/fuzz/Cargo.toml +``` + +## Extended validation gates + +```sh +QFBVSMTRS_RANDOM_CIRCUIT_SAMPLES=10000 cargo test -p qfbvsmtrs --test random_circuits +QFBVSMTRS_DIFF_RANDOM=1 cargo test -p qfbvsmtrs --test differential_z3 +QFBVSMTRS_SMTLIB_DIR=/path/to/SMT-LIB/QF_BV cargo test -p qfbvsmtrs --test differential_z3 +cargo fuzz run smt2_pipeline --manifest-path crates/qfbvsmtrs/fuzz/Cargo.toml +cargo run -p qfbvsmtrs --bin qfbvsmtrs_bench -- path/to/query.smt2 +``` + +The checked-in default suite includes exhaustive 4-bit circuit tests, randomized 16/32/64-bit circuit properties, Tseitin truth-table tests across all SAT backends, known-answer SMT-LIB fixtures, Z3 differential smoke tests through the Rust `z3` crate, standalone solver tests, and server integration tests. + +See also: + +- `docs/qfbvsmtrs-progress-report.md` for the current implementation/corpus status. +- `docs/qfbvsmtrs-design-report.md` for the solver architecture and design details. +- `docs/qfbvsmtrs-corpus-results.md` for SMT-LIB QF_BV corpus command history. + +The corpus runs found no wrong conclusive answers, but many `unknown`/timeout cases remain under strict budgets, so the solver is not yet production-complete for arbitrary corpus workloads. + +``` + +`docs/qfbvsmtrs-progress-report.md`: + +```md +# qfbvsmtrs progress report + +Date: 2026-05-05 + +`qfbvsmtrs` is now implemented as a standalone pure-Rust QF_BV solver crate and is integrated into `smt-server` as a third backend. The solver is functional, broadly tested, and has produced no wrong conclusive answers on the SMT-LIB 2025 QF_BV corpus runs performed so far. It is **not yet production-complete for arbitrary QF_BV workloads** because a large tail of corpus cases still returns `unknown` or hits timeout under strict budgets. + +## Current status + +| Area | Status | +|---|---| +| Standalone crate | Implemented in `crates/qfbvsmtrs` | +| Native Rust builder API | Implemented via `qfbvsmtrs::Builder` | +| SMT-LIB frontend | Implemented via `parse_smt2`, `solve_smt2`, and response formatting | +| SAT backends | `splr` default, `varisat` selectable, internal DPLL fallback/testing | +| Bit-blasting pipeline | Implemented for broad QF_BV Bool/BV operator coverage | +| Model extraction | Implemented for SAT results when requested | +| Named unsat cores | Implemented by deletion-based minimization | +| Optimization | Implemented for `minimize`/`maximize` by bit-hunt | +| Server integration | Implemented as `QfbvsmtrsBackend`, raced with Z3 and binbit | +| Corpus runner | Implemented as resumable append-only JSONL runner | +| Fuzz target | Added and compile-checked | + +## Validation performed + +Checked-in validation includes: + +- exhaustive 4-bit arithmetic/comparison/shift/division circuit tests; +- randomized 16/32/64-bit circuit property tests; +- Tseitin truth-table tests across SAT backends; +- known-answer SMT-LIB fixtures; +- Rust `z3` differential smoke tests; +- all-backend SAT/UNSAT tests for `splr`, `varisat`, and DPLL; +- standalone solver tests for SAT, UNSAT, model extraction, push/pop, cores, optimization, and division-by-zero semantics; +- `smt-server` integration tests for qfbvsmtrs SAT, UNSAT, model, core, and optimization paths; +- fuzz harness compile check. + +Recent full workspace gates after the latest solver and documentation changes passed: + +```sh +cargo fmt --all +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace +cargo check --manifest-path crates/qfbvsmtrs/fuzz/Cargo.toml +``` + +These gates should still be rerun before any release or production-ready claim, especially after further corpus-driven solver changes. + +## SMT-LIB QF_BV corpus status + +Corpus: SMT-LIB 2025 non-incremental QF_BV archive from Zenodo record `16740866`. + +Local corpus: + +- `46,191` `.smt2` files; +- about `37.3 GB` decompressed; +- reports stored under `target/smtlib/` and not checked into the repository. + +### Full strict-budget baseline + +Report: `target/smtlib/qfbvsmtrs_corpus_report_v3.jsonl` + +Budget: `QFBVSMTRS_BUDGET_MS=3000`, process timeout `30s`. + +| Class | Count | +|---|---:| +| Conclusive and matched `:status` | 33,590 | +| Returned `unknown` against known sat/unsat status | 10,771 | +| Hit hard process timeout | 1,830 | +| Frontend/backend error | 0 | +| Conclusive wrong answer | 0 | + +### Latest merged incremental status + +Merged reports: + +- `qfbvsmtrs_corpus_report_v3.jsonl` +- `qfbvsmtrs_corpus_rerun_noetzli_linear.jsonl` +- `qfbvsmtrs_corpus_rerun_bruttomesso_extract.jsonl` +- `qfbvsmtrs_corpus_rerun_sage_app1.jsonl` +- `qfbvsmtrs_corpus_rerun_pspace_patterns.jsonl` +- `qfbvsmtrs_corpus_rerun_challenge_overflow.jsonl` +- `qfbvsmtrs_corpus_rerun_sage2_extcmp_sample.jsonl` +- `qfbvsmtrs_corpus_rerun_logslicing_addsub.jsonl` +- `qfbvsmtrs_corpus_rerun_brummayerbiere4_consteval.jsonl` +- `qfbvsmtrs_corpus_rerun_challenge_signed_overflow.jsonl` +- `qfbvsmtrs_corpus_rerun_noetzli_polynomial.jsonl` +- `qfbvsmtrs_corpus_rerun_bruttomesso_extensional.jsonl` + +| Class | Count | +|---|---:| +| Conclusive and matched `:status` | 34,327 | +| Returned `unknown` against known sat/unsat status | 10,048 | +| Hit hard process timeout | 1,816 | +| Frontend/backend error | 0 | +| Conclusive wrong answer | 0 | + +This is an improvement of 737 additional conclusive corpus answers over the strict baseline, while preserving zero wrong/error results in the merged reports. + +## Recent solver improvements + +Recent targeted improvements include: + +- solver preprocessing shortcuts for wide `pspace` arithmetic-order patterns; +- SAT witness shortcut by evaluating all-zero/all-one assignments for satisfiable formulas when no model/core is requested; +- arithmetic pattern detection for unsigned and signed multiplication-overflow guard proofs; +- log-slicing adder/subtractor equivalence recognition; +- extensional extract/concat candidate contradiction detection for Bruttomesso `ext_con` cases; +- limited polynomial equality normalization for 32/64-bit rewrite verification cases; +- additional builder simplifications for extension/constant comparisons and 1-bit ITE equality. + +Targeted corpus effects observed: + +| Family/pass | Result | +|---|---:| +| `pspace` successor/power-of-two/shift-add patterns | 86 / 86 rerun cases solved | +| `brummayerbiere4` constant-assignment SAT witnesses | 10 / 10 rerun cases solved | +| `challenge` unsigned/signed overflow guards | 2 / 2 cases solved after combined passes | +| `log-slicing` add/sub patterns | 42 / 208 rerun cases solved | +| `20190311-bv-term-small-rw-Noetzli` polynomial/linear rewrites | unresolved reduced to 37 cases | +| `bruttomesso` extensional cases | unresolved reduced from 672 to 258 cases | + +## Largest remaining non-conclusive groups + +Latest merged unresolved counts under strict budgets: + +| Family | Non-conclusive | +|---|---:| +| `Sage2` | 5,005 | +| `sage` | 2,488 | +| `spear` | 1,689 | +| `asp` | 465 | +| `uclid` | 407 | +| `20210219-Sydr` | 344 | +| `bruttomesso` | 258 | +| `20210312-Bouvier` | 200 | +| `float` | 191 | +| `log-slicing` | 166 | +| `mcm` | 131 | +| `20230221-oisc-gurtner` | 112 | + +## Production-readiness assessment + +`qfbvsmtrs` is ready for continued integration testing and controlled experimentation as a third backend in `smt-server`. It is not yet ready to claim production completeness for arbitrary SMT-LIB QF_BV because: + +- latest merged corpus still has `10,048` `unknown` cases and `1,816` timeouts under strict budgets; +- full no-budget/no-timeout corpus completion has not been demonstrated; +- full-corpus Z3 differential testing has not been completed; +- a long-running fuzzing campaign has not been completed; +- some important families (`Sage2`, `sage`, `spear`, `asp`, `uclid`) still need major performance/simplification work. + +## Recommended next steps + +1. Run a full workspace gate after the latest solver changes. +2. Continue merged incremental corpus reruns, always skipping known-good `ok` cases. +3. Prioritize `Sage2`, `sage`, and `spear`, which dominate the remaining non-conclusive set. +4. Add stronger word-level preprocessing for symbolic execution path constraints, byte extraction/concatenation, and equality propagation. +5. Extend log-slicing recognition beyond add/sub to shifts, comparisons, multiplication, and division where safe. +6. Run broader Z3 differential tests over the corpus using the Rust `z3` crate. +7. Run an actual fuzzing campaign, not just a compile check. +8. Reassess production readiness only after the remaining corpus tail is either solved or bounded by documented, accepted limits. + +``` + +`docs/second-backend-evaluation.md`: + +```md +# Solver backend integration + +The server currently integrates three real solver backends behind the shared `Backend` trait: + +1. `Z3Backend` (`crates/smt-server/src/z3_backend.rs`) translates validated wire IR directly to Rust `z3` crate ASTs, uses `Solver::check_assumptions`, extracts models and named unsat cores, and implements optimization with a server-side bit-hunt over Z3 assumptions. +2. `BinbitBackend` (`crates/smt-server/src/binbit_backend.rs`) translates validated wire IR directly to `binbit::SmtSolver`, including assumptions, named unsat cores, model extraction, and binbit's native min/max helpers. +3. `QfbvsmtrsBackend` (`crates/smt-server/src/qfbvsmtrs_backend.rs`) adapts validated wire IR into the standalone `qfbvsmtrs` crate, which owns its own IR, SMT-LIB frontend, bit-blaster, CNF encoder, selectable pure-Rust SAT backends (`splr`, `varisat`, internal DPLL), model extraction, bit-hunt optimization, and deletion-based named unsat-core extraction. + +`RacingBackend` can race Z3, binbit, and qfbvsmtrs and returns the first conclusive result while continuing to log later backend disagreements. + +The previous dependency-free `ExhaustiveBackend` and external-process `Z3CliBackend` were removed from the active server API/configuration. The default executable now starts `RacingBackend(Z3Backend, BinbitBackend, QfbvsmtrsBackend)`. + +``` + +`docs/smt-wire-format-plan.md`: + +```md +# SMT Wire Format & Protocol — Design Plan + +## Overview + +A binary format for representing QF_BV + Bool SMT expressions that requires no deserialization. Clients construct expressions in-place in a flat buffer and send it over the wire to a solver server. The server translates to backend-specific ASTs/terms (Z3, binbit, Bitwuzla, or another backend), races configured backends, and returns results in the same format. A secondary text path accepts standard SMT-LIB 2.6 input for tooling compatibility. + +Design priorities: client simplicity (single-file libraries in C++/Python/Rust), server cacheability (stateless request/response), and zero-copy reads (validate a byte buffer, then traverse it without materializing a separate AST). + +--- + +## 1. Architecture + +### System topology + +Clients are lightweight, header-only or single-file libraries. They build a flat expression DAG, collect assertions and assumptions, serialize to a contiguous byte buffer, and send it. They do not link against any solver. The server is a shared service that owns the solver backends. + +### Client responsibilities + +- DAG construction (append-only buffer building) +- Push/pop scope tracking (snapshot and restore assertion list length) +- Hash-consing during construction (optional but recommended; `(tag, child0, child1) → NodeId` map) +- Client-side lowering of convenience operations (see §4) +- Mutual exclusivity assertions (emit pairwise `not(and(si, sj))` — pure DAG construction) +- Variable aliasing (rewrite references at construction time, never hits the wire) +- Constant folding and concretization probes (`try_const_value` is a client-side check) +- DAG compaction before send (reachability walk, renumber, emit only live nodes) + +### Server responsibilities + +- Binary format decoding (validate and traverse the buffer — zero-copy where the implementation language permits it safely) +- SMT-LIB text parsing (build the same internal IR) +- Backend translation (walk node array bottom-up, build backend-specific ASTs/terms) +- Parallel backend dispatch (race configured backends, return first conclusive result) +- Caching (`hash(request) → response` for deduplication and replay) +- Model extraction and response formatting + +### Stateless protocol + +Every request is self-contained. No sessions, no server-side state between requests. Push/pop is client-side bookkeeping over the assertion list. This enables: + +- Request-level caching (pure function from query to answer) +- Deduplication of concurrent identical queries +- Horizontal scaling (any server instance can handle any request) +- Backend comparison and correctness oracles +- Replay and regression testing + +The tradeoff is that learned clauses don't persist across requests. The server can mitigate this internally (solver pooling, warm-start heuristics) without exposing it in the protocol. + +--- + +## 2. Buffer Layout + +A request's expression buffer is four contiguous sections: + +``` +┌────────────────┬──────────────────┬───────────────────┬────────────────┐ +│ Header (32B) │ Node array │ Child array │ Blob table │ +│ │ N × 24B │ M × 4B │ variable │ +└────────────────┴──────────────────┴───────────────────┴────────────────┘ +``` + +### Header (32 bytes) + +| Field | Size | Description | +|------------------|------|--------------------------------------------------| +| `magic` | 4B | `"SMT\0"` | +| `version` | 1B | Format version, currently `1` | +| `_pad` | 3B | Reserved; encoders should write zero, decoders ignore | +| `node_count` | 4B | Number of nodes in the node array | +| `child_count` | 4B | Number of entries in the child array | +| `blob_len` | 4B | Byte length of the blob table | +| `_reserved` | 12B | Future use; encoders should write zero, decoders ignore | + +All multi-byte fields are little-endian. This applies to expression buffers, request/response envelopes, transport frame lengths, and response payloads. + +### Node record (24 bytes) + +| Field | Size | Description | +|-------------|------|--------------------------------------------------------------------| +| `tag` | 1B | Node kind (see §3) | +| `arity` | 1B | Number of children in the child array | +| `aux_hi` | 2B | Tag-specific: extract high bound, extend amount, select pair count | +| `width` | 4B | Bitvector width (0 for Bool nodes) | +| `aux_lo` | 4B | Tag-specific: extract low bound | +| `children` | 4B | Start index into the child array | +| `payload` | 8B | Literal value (≤64-bit) or blob reference `((offset:u32) << 32) \| len:u32` | + +Nodes are laid out in construction order (topological / bottom-up). Node `i` is at byte offset `32 + i × 24`. Children of any node are at indices `[children .. children + arity)` in the child array. For strict validation, every child reference must point to a node index lower than the parent node index. + +### Child array + +Flat array of `uint32_t` node references. Each reference is a typed ID: bit 31 encodes sort (0 = BV, 1 = Bool), bits 0–30 encode the node index. This provides structural sort-checking without a separate sort table. + +### Blob table + +Contiguous byte region storing variable-length data: symbol names (UTF-8) and wide literal values (little-endian bytes). Referenced from node payloads via offset and length. + +--- + +## 3. Node Tags + +44 tags total: 24 BV-producing tags, 7 Bool leaf/connective tags, 5 BV comparison tags, and 8 overflow-predicate tags. Every tag maps directly to the server's backend translation layer. Backend adapters should use direct backend API calls where available; if a backend lacks a feature needed by a request, that backend is not eligible for that request. + +### Bitvector leaves + +| Tag | Arity | Aux | Payload | +|----------------|-------|----------------------|----------------------------------------------------| +| `BV_VAR` | 0 | — | Blob ref to symbol name | +| `BV_CONST` | 0 | — | Inline value if width ≤ 64; blob ref otherwise | + +### Bitvector unary + +| Tag | Arity | +|-----------|-------| +| `BV_NOT` | 1 | +| `BV_NEG` | 1 | + +### Bitvector binary (operands must be same width) + +| Tag | Arity | Notes | +|------------|-------|--------------------------------------| +| `BV_AND` | 2 | | +| `BV_OR` | 2 | | +| `BV_XOR` | 2 | | +| `BV_ADD` | 2 | | +| `BV_SUB` | 2 | | +| `BV_MUL` | 2 | | +| `BV_UDIV` | 2 | div-by-zero = all ones | +| `BV_UREM` | 2 | rem-by-zero = dividend | +| `BV_SDIV` | 2 | toward zero | +| `BV_SREM` | 2 | sign follows dividend | +| `BV_SMOD` | 2 | sign follows divisor | +| `BV_SHL` | 2 | shift amount same width as operand | +| `BV_LSHR` | 2 | logical shift right | +| `BV_ASHR` | 2 | arithmetic shift right, sign-fills | + +### Bitvector structural + +| Tag | Arity | Aux | +|--------------|-------|--------------------------------------------| +| `BV_EXTRACT` | 1 | `aux_hi` = high bit, `aux_lo` = low bit | +| `BV_CONCAT` | 2 | first child is high bits, second is low | +| `BV_ZEXT` | 1 | `aux_hi` = number of bits to extend by | +| `BV_SEXT` | 1 | `aux_hi` = number of bits to extend by | + +### Bitvector conditional + +| Tag | Arity | Aux | Children layout | +|--------------|----------|-------------------------|------------------------------------------------| +| `BV_ITE` | 3 | — | `[condition(Bool), then(BV), else(BV)]` | +| `BV_SELECT` | 2N + 1 | `aux_hi` = N (pairs) | `[s0, v0, s1, v1, …, sN-1, vN-1, default]` | + +`BV_SELECT` has first-match semantics: value of the first true selector wins, otherwise default. Selectors are Bool refs, values and default are BV refs of matching width. + +### Bool leaves + +| Tag | Arity | Payload | +|--------------|-------|----------------------------| +| `BOOL_TRUE` | 0 | — | +| `BOOL_FALSE` | 0 | — | +| `BOOL_VAR` | 0 | Blob ref to symbol name | + +### Bool connectives + +| Tag | Arity | +|----------------|-------| +| `BOOL_NOT` | 1 | +| `BOOL_AND` | 2 | +| `BOOL_OR` | 2 | +| `BOOL_IMPLIES` | 2 | + +### Comparisons (BV × BV → Bool) + +| Tag | Arity | +|----------|-------| +| `BV_EQ` | 2 | +| `BV_ULT` | 2 | +| `BV_ULE` | 2 | +| `BV_SLT` | 2 | +| `BV_SLE` | 2 | + +### Overflow predicates (→ Bool) + +| Tag | Arity | Notes | +|--------------|-------|-------------------------------| +| `UADD_OVF` | 2 | | +| `SADD_OVF` | 2 | | +| `USUB_OVF` | 2 | | +| `SSUB_OVF` | 2 | | +| `UMUL_OVF` | 2 | | +| `SMUL_OVF` | 2 | | +| `NEG_OVF` | 1 | | +| `SDIV_OVF` | 2 | true iff INT_MIN / -1 | + +### Tag numbering + +Tags are assigned sequentially starting from 0. New tags are appended at the end. v1 assigns: + +| Value | Tag | +|------:|----------------| +| 0 | `BV_VAR` | +| 1 | `BV_CONST` | +| 2 | `BV_NOT` | +| 3 | `BV_NEG` | +| 4 | `BV_AND` | +| 5 | `BV_OR` | +| 6 | `BV_XOR` | +| 7 | `BV_ADD` | +| 8 | `BV_SUB` | +| 9 | `BV_MUL` | +| 10 | `BV_UDIV` | +| 11 | `BV_UREM` | +| 12 | `BV_SDIV` | +| 13 | `BV_SREM` | +| 14 | `BV_SMOD` | +| 15 | `BV_SHL` | +| 16 | `BV_LSHR` | +| 17 | `BV_ASHR` | +| 18 | `BV_EXTRACT` | +| 19 | `BV_CONCAT` | +| 20 | `BV_ZEXT` | +| 21 | `BV_SEXT` | +| 22 | `BV_ITE` | +| 23 | `BV_SELECT` | +| 24 | `BOOL_TRUE` | +| 25 | `BOOL_FALSE` | +| 26 | `BOOL_VAR` | +| 27 | `BOOL_NOT` | +| 28 | `BOOL_AND` | +| 29 | `BOOL_OR` | +| 30 | `BOOL_IMPLIES` | +| 31 | `BV_EQ` | +| 32 | `BV_ULT` | +| 33 | `BV_ULE` | +| 34 | `BV_SLT` | +| 35 | `BV_SLE` | +| 36 | `UADD_OVF` | +| 37 | `SADD_OVF` | +| 38 | `USUB_OVF` | +| 39 | `SSUB_OVF` | +| 40 | `UMUL_OVF` | +| 41 | `SMUL_OVF` | +| 42 | `NEG_OVF` | +| 43 | `SDIV_OVF` | + +A client that encounters a future unknown tag can skip it using `arity` and `children` — it knows how many children to skip even without understanding the semantics. A v1 server that must solve or simplify the formula rejects unknown tags with `ERROR`. + +--- + +## 4. Client-Side Lowering + +The following operations are common in SMT-LIB and binary analysis but are **not** format tags. Clients lower them to primitives during construction. This keeps the tag set small and the server simple. + +| Surface operation | Client lowering | +|----------------------------|--------------------------------------------| +| `bv_ne(a, b)` | `bool_not(bv_eq(a, b))` | +| `bv_ugt(a, b)` | `bv_ult(b, a)` (swap operands) | +| `bv_uge(a, b)` | `bv_ule(b, a)` | +| `bv_sgt(a, b)` | `bv_slt(b, a)` | +| `bv_sge(a, b)` | `bv_sle(b, a)` | +| `bv_rotate_left(x, k)` | `bv_or(bv_shl(x, k), bv_lshr(x, w-k))` | +| `bv_rotate_right(x, k)` | `bv_or(bv_lshr(x, k), bv_shl(x, w-k))` | +| `bool_xor(a, b)` | `bool_not(bool_eq(a, b))` or `and`/`or`/`not` | +| `bool_ite(c, t, e)` | `bool_or(bool_and(c, t), bool_and(bool_not(c), e))` | +| `bool_eq(a, b)` | XNOR: `bool_and(bool_or(a, bool_not(b)), bool_or(bool_not(a), b))` | +| `assert_mutex(sels)` | pairwise `assert(bool_not(bool_and(si, sj)))` | + +--- + +## 5. Typed Node References + +Node IDs are `uint32_t` with the high bit encoding the sort: + +| Bit 31 | Meaning | Index range | +|--------|-----------|-----------------| +| 0 | BV node | 0 – 2^31 - 1 | +| 1 | Bool node | 0 – 2^31 - 1 | + +All entries in the child array use this encoding. This provides structural sort-checking at construction time and during server validation without a separate sort table or per-node sort field. The underlying node array is shared — the sort bit is metadata on the *reference*, not the node. + +--- + +## 6. Constants and Wide Literals + +Constants are encoded with a width-dependent strategy: + +- **Width ≤ 64 bits**: value is stored inline in the 8-byte `payload` field. No blob table access needed. The value is interpreted little-endian as an unsigned integer and masked to `width` bits. Encoders should zero bits above `width` for deterministic hashing. + +- **Width > 64 bits**: `payload` encodes a blob table reference as `(offset << 32) | byte_length`. The blob table stores the value as little-endian bytes, exactly `ceil(width / 8)` bytes long. This handles SSE (128-bit), AVX (256-bit), AVX-512 (512-bit), and arbitrary-precision constants uniformly. Encoders should zero unused high bits in the final byte when `width` is not a multiple of 8. + +The client API exposes this as two constructors: a common-case one taking a machine integer, and a wide one taking a byte/limb array. The branching is internal. + +--- + +## 7. Widths + +Bitvector widths are `uint32_t`. Valid range: `1..65536`. Width 0 indicates a Bool node (which doesn't use the width field meaningfully). Widths are not restricted to powers of two — `extract` routinely produces odd widths (7-bit flag slices, 3-bit ARM immediates, etc.). + +Width constraints are validated by the client during construction. The server re-validates on decode but should not need to reject well-formed clients. + +--- + +## 8. Wire Protocol + +### Transport + +TCP or Unix domain socket. Every request and response is carried inside a single length-prefixed transport frame: + +``` +u32_le frame_len +u8[frame_len] frame_payload +``` + +For a binary request, `frame_payload` begins with the 32-byte request envelope whose first four bytes are `"SMTQ"`. For a binary response, `frame_payload` begins with the 16-byte response envelope whose first four bytes are `"SMTR"`. For a text request, `frame_payload` is the complete SMT-LIB script bytes and therefore does not begin with `"SMTQ"`. + +The protocol is request/response with pipelining via `request_id`. Responses may arrive out of order, but each response is still exactly one length-prefixed frame. + +### Request envelope + +| Field | Size | Description | +|--------------------|------|--------------------------------------------------------------| +| `magic` | 4B | `"SMTQ"` | +| `request_id` | 4B | Client-assigned, echoed in response | +| `command` | 1B | `SOLVE`, `SIMPLIFY`, `MINIMIZE`, `MAXIMIZE` | +| `flags` | 1B | `WANT_MODEL`, `WANT_CORE`, `SIGNED` (for min/max) | +| `budget` | 4B | Millisecond timeout; 0 = unbounded | +| `expr_len` | 4B | Byte length of the expression buffer | +| `assertion_count` | 2B | Number of assertion root IDs | +| `named_count` | 2B | How many of the first assertions are named | +| `assumption_count` | 2B | Number of assumption root IDs | +| `target_node` | 4B | For MINIMIZE/MAXIMIZE: the BV node to optimize; 0 otherwise | +| `_pad` | 4B | Reserved; encoders should write zero, decoders ignore | + +Total: 32 bytes. + +Following the header: + +1. Expression buffer (`expr_len` bytes) — self-contained: header + nodes + children + blob table +2. Assertion root IDs (`assertion_count × 4B`) — Bool node refs into the expression buffer +3. Named assertion string refs (`named_count × 8B`) — each is `(blob_offset: u32, blob_len: u32)` pointing into the expression buffer's blob table +4. Assumption root IDs (`assumption_count × 4B`) — Bool node refs, temporary for this solve call only + +The binary request frame length must equal `32 + expr_len + assertion_count*4 + named_count*8 + assumption_count*4`. `named_count` must be less than or equal to `assertion_count`; the first `named_count` assertion roots are the named assertions. `target_node` is ignored for `SOLVE` and `SIMPLIFY`; for `MINIMIZE`/`MAXIMIZE`, it must be a valid BV node ref. + +### Commands + +| Command | Description | +|--------------|----------------------------------------------------------------| +| `SOLVE` | Check satisfiability of assertions under assumptions | +| `SIMPLIFY` | Return a simplified expression buffer | +| `MINIMIZE` | Find minimum value of `target_node` under constraints | +| `MAXIMIZE` | Find maximum value of `target_node` under constraints | + +Command values are fixed for v1: + +| Value | Command | +|------:|------------| +| 0 | `SOLVE` | +| 1 | `SIMPLIFY` | +| 2 | `MINIMIZE` | +| 3 | `MAXIMIZE` | + +Request flag bits are fixed for v1: + +| Bit | Flag | Meaning | +|----:|--------------|----------------------------------------------| +| 0 | `WANT_MODEL` | Return a model when the result is `SAT` | +| 1 | `WANT_CORE` | Return an unsat core when the result is `UNSAT` | +| 2 | `SIGNED` | Use signed comparison for min/max | + +`MINIMIZE`/`MAXIMIZE` perform the bit-hunt optimization loop server-side, avoiding multiple round-trips. The `SIGNED` flag controls whether the optimization uses unsigned or signed comparison. + +In v1, `budget` is always a millisecond timeout. `budget = 0` means unbounded. The timeout is applied to backend solving after successful request decode/validation and IR translation. For backend racing, each backend receives the same timeout budget. If the timeout is exhausted before a conclusive answer, the response is `UNKNOWN`. Conflict-count/resource budgets are deferred to a future protocol extension rather than multiplexed through this field. + +### Response envelope + +| Field | Size | Description | +|----------------|------|------------------------------------------------------| +| `magic` | 4B | `"SMTR"` | +| `request_id` | 4B | Echoed from request | +| `status` | 1B | `OK`, `SAT`, `UNSAT`, `UNKNOWN`, `ERROR` | +| `flags` | 1B | Response flags | +| `payload_len` | 4B | Byte length of the payload | +| `_pad` | 2B | Reserved; encoders should write zero, decoders ignore | + +Total: 16 bytes. The binary response frame length must equal `16 + payload_len`. + +Status values are fixed for v1: + +| Value | Status | +|------:|-----------| +| 0 | `OK` | +| 1 | `SAT` | +| 2 | `UNSAT` | +| 3 | `UNKNOWN` | +| 4 | `ERROR` | + +Response flag bits are fixed for v1: + +| Bit | Flag | Meaning | +|----:|---------------|----------------------------------------------------| +| 0 | `HAS_MODEL` | Payload contains a model block | +| 1 | `HAS_CORE` | Payload contains an unsat-core block | +| 2 | `HAS_EXPR` | Payload contains a simplified expression block | +| 3 | `HAS_VALUE` | Payload contains an optimization value block | +| 4 | `HAS_MESSAGE` | Payload contains a UTF-8 diagnostic message | + +`OK` is used for successful non-satisfiability commands such as `SIMPLIFY`. `UNKNOWN` means the millisecond timeout was exhausted or the backend returned an inconclusive result. This is distinct from `UNSAT` — the client must treat it as inconclusive. A `UNKNOWN` result from a bounded solve is safe to retry with a larger timeout. + +### Response payloads + +All payload integers are little-endian. Payload fields are tightly packed with no alignment padding. Unless stated otherwise, `OK`/`SAT`/`UNSAT`/`UNKNOWN` responses with no requested data have `payload_len = 0`. + +A response with `status = ERROR` always uses the error payload, independent of command: + +``` +u8[payload_len] message_utf8 +``` + +`HAS_MESSAGE` must be set. The message is not NUL-terminated. + +#### Scalar value block + +Used by model entries and optimization results. + +``` +u32 width +u32 value_len +u8[value_len] value_bytes +``` + +Rules: + +- Bool values use `width = 0`, `value_len = 1`, and `value_bytes[0]` is `0` or `1`. +- BV values use `width > 0`, `value_len = ceil(width / 8)`, and little-endian bytes. +- For BV widths not divisible by 8, unused high bits in the final byte must be zero. + +#### Model block + +Returned for `SAT` when `WANT_MODEL` is set. `HAS_MODEL` must be set. + +``` +u32 entry_count +entry[entry_count] + +entry: + u32 node_ref + ScalarValue value +``` + +`node_ref` is the typed node reference of a `BV_VAR` or `BOOL_VAR` node from the request expression buffer. Bool variables are encoded with the Bool sort bit set. BV variables use the BV sort bit. The scalar value width must match the referenced variable's sort and width. The server may return only variables relevant to the formula; clients must treat absent variables as unconstrained. + +#### Unsat core block + +Returned for `UNSAT` when `WANT_CORE` is set. `HAS_CORE` must be set. + +``` +u32 name_count +name[name_count] + +name: + u32 name_len + u8[name_len] name_utf8 +``` + +Names are copied into the response, so the payload is self-contained. Each name must match one of the request's named assertions. Named assertion names should be unique in the request when `WANT_CORE` is set; if duplicates are present, the server may reject the request with `ERROR`. + +#### Simplify block + +Returned by `SIMPLIFY` on success. `status = OK` means the simplification completed successfully, and `HAS_EXPR` must be set. + +``` +u32 expr_len +u16 assertion_count +u16 named_count +u16 assumption_count +u16 reserved +u8[expr_len] expression_buffer +u32 assertion_roots[assertion_count] +named_ref named_assertion_refs[named_count] +u32 assumption_roots[assumption_count] + +named_ref: + u32 blob_offset + u32 blob_len +``` + +The expression buffer is a complete expression buffer using the same layout as requests. Root IDs and named assertion refs point into the returned expression buffer. `named_count` names the first `named_count` assertions, matching the request layout. This block intentionally uses a compact header rather than reusing the request envelope; clients can reuse the expression-buffer parser, but must parse the simplify block's root/name lists separately. + +#### Optimization value block + +Returned by `MINIMIZE`/`MAXIMIZE` when the constraints are satisfiable. `status = SAT` and `HAS_VALUE` must be set. + +``` +ScalarValue optimum +[ModelBlock model] // present only when WANT_MODEL was set +``` + +The returned value is the BV bit pattern of the optimum and its width must match `target_node`. The request's `SIGNED` flag only controls the ordering used to find that optimum. If `WANT_MODEL` is set, `HAS_MODEL` must also be set and the model block immediately follows the scalar value block. + +#### Command/status matrix + +| Command | Status | Payload | +|---------|-----------|--------------------------------------------------------------------------| +| `SOLVE` | `SAT` | empty, or `ModelBlock` if `WANT_MODEL` | +| `SOLVE` | `UNSAT` | empty, or `UnsatCoreBlock` if `WANT_CORE` | +| `SOLVE` | `UNKNOWN` | empty | +| `SIMPLIFY` | `OK` | `SimplifyBlock` | +| `SIMPLIFY` | `ERROR`/`UNKNOWN` | error payload for `ERROR`, empty for `UNKNOWN` | +| `MINIMIZE`/`MAXIMIZE` | `SAT` | `OptimizationValueBlock` | +| `MINIMIZE`/`MAXIMIZE` | `UNSAT` | empty | +| `MINIMIZE`/`MAXIMIZE` | `UNKNOWN` | empty | +| any | `ERROR` | error payload | + +### Pipelining + +The `request_id` field allows multiple in-flight requests over a single connection. Responses may arrive out of order. This is important for symbolic execution workloads that fire hundreds of small feasibility probes. + +--- + +## 9. SMT-LIB Text Support + +### Routing + +The server accepts both binary and text on the same port. The transport frame is always length-prefixed. After reading a frame, the first 4 bytes of the frame payload distinguish the format: if they match `"SMTQ"`, parse a binary request; otherwise, treat the whole frame payload as one complete SMT-LIB text script (which normally starts with `(`, `;`, or whitespace). + +### Scope + +The text frontend accepts a single, self-contained SMT-LIB 2.6 script per request. The script is parsed in its entirety, translated to the internal IR, and executed as one query. There is no multi-command session — `push`/`pop` and incremental interaction are not supported on the text path. If a script contains `push`/`pop`, the server returns an error. This keeps the text path fully stateless, identical to the binary path. + +Supported commands: + +- `set-logic` — accepted, validated as QF_BV, otherwise no-op +- `declare-const`, `declare-fun` (0-arity only) +- `define-const`, `define-fun` (0-arity only, inline-expanded) +- `assert`, including `(! expr :named name)` for named assertions +- `check-sat`, `check-sat-assuming` +- `get-model`, `get-value` +- `get-unsat-core` +- `exit` +- `let` bindings (inline-expanded during parsing) + +Not supported: `push`, `pop`, arrays, quantifiers, non-zero-arity `define-fun`, `reset`, uninterpreted sorts/functions. + +### Internal pipeline + +The SMT-LIB parser builds the same internal IR (node array + child array + blob table) that the binary decoder produces. All downstream code — caching, backend translation, response formatting — is format-agnostic. The parser collects all assertions from the script, identifies the `check-sat` command and its variant, and produces a single self-contained query — structurally identical to what a binary client would send. + +### Response format + +Text requests get text responses (standard SMT-LIB output: `sat`/`unsat`, `(model ...)`, etc.). Text response frames use the same transport framing: `u32_le frame_len` followed by the UTF-8 response bytes. Binary responses to text requests are deferred until a concrete text-request option is specified; the default is symmetric: text in, text out. + +--- + +## 10. Server Internals + +### Cache layer + +The cache sits between the frontend decoders and the backend dispatch. Cache implementation is deferred until after response semantics are stable. A cache key must include every field that can affect the returned payload, including command, flags, expression buffer bytes, assertion roots, named assertion refs when cores can be returned, assumption roots, target node for optimization, and the cache policy for bounded timeouts. On cache hit, the response is returned immediately without invoking any solver. Cache entries are keyed per-format initially (binary and text queries cache independently). Cross-format dedup (hashing the parsed IR) is a future optimization that requires canonicalization. + +### Backend translation + +A single bottom-up pass over the node array, index 0 to N-1. For each node, build the corresponding backend term using already-translated children. The bottom-up construction order in the buffer guarantees children are always translated before parents. + +Backend adapters are pluggable. A backend is eligible for a request only if it supports the requested command and response features: solving QF_BV + Bool, assumptions, timeout handling, model extraction for `WANT_MODEL`, unsat cores for `WANT_CORE`, simplification for `SIMPLIFY`, and optimization for `MINIMIZE`/`MAXIMIZE` as applicable. Phase 2 only requires Z3 support for `SOLVE`. Later phases can add binbit, Bitwuzla, or another backend behind the same adapter interface. + +### Parallel dispatch + +Both backends receive the translated formula simultaneously. The first to return wins. On `UNKNOWN` from one backend (timeout exhausted) the server waits for the other. On disagreement between backends (should not happen for correct implementations), log and flag for investigation. + +### Solver pooling (optional optimization) + +The server can maintain a pool of warm solver instances. When a new request arrives, it can be routed to a solver that already has structurally similar clauses loaded. This is invisible to the protocol — it's a performance optimization that doesn't affect correctness or the API contract. + +--- + +## 11. Client Library Design + +### Principles + +- **Single file per language.** One header (C++), one `.py` file, one `.rs` file. No build system integration, no dependencies beyond the standard library. +- **No code generation.** The tag enum and node struct are hand-written. Adding a new operation is one enum variant and one builder function. +- **No networking.** The library produces and consumes byte buffers. The caller handles transport (`send`, `recv`, `write`, `read`). This keeps the library OS-agnostic and avoids pulling in socket, TLS, or async dependencies. +- **No allocator coupling.** C++ uses `std::vector` or a user-provided allocator. Rust uses `Vec`. Python uses `bytearray`. Override points are available but not required. + +### Builder API surface + +Every client library exposes the same logical API: + +**Buffer management**: `new`, `reset`, `to_bytes` (serialize), `from_bytes` (zero-copy view). + +**BV leaf constructors**: `bv_var(name, width) → NodeId`, `bv_const(value, width) → NodeId`, `bv_const_wide(limbs, width) → NodeId`. + +**BV operations**: one function per tag — `bv_add(a, b)`, `bv_extract(x, hi, lo)`, `bv_ite(c, t, e)`, `bv_select(selectors, values, default)`, etc. Each returns a `NodeId`. Width is inferred from operands, validated, and stored in the node. `bv_select` must reject more than 127 selector/value pairs in v1 with a clear client-side error. + +**Bool leaf constructors**: `bool_true()`, `bool_false()`, `bool_var(name)`. + +**Bool operations**: `bool_not(a)`, `bool_and(a, b)`, `bool_or(a, b)`, `bool_implies(a, b)`. + +**Comparisons and overflow**: `bv_eq(a, b)`, `bv_ult(a, b)`, `uadd_ovf(a, b)`, etc. Each returns a Bool `NodeId`. + +**Convenience operations (lowered client-side)**: `bv_ne`, `bv_ugt`, `bv_uge`, `bv_sgt`, `bv_sge`, `bv_rotate_left`, `bv_rotate_right`, `assert_mutex`. + +**Scope management**: `push()`, `pop()`, `assert(node)`, `assert_named(name, node)`, `assume(node)`. + +**Request building**: `build_solve_request(budget_ms) → bytes`, `build_simplify_request() → bytes`, `build_minimize_request(target, signed, budget_ms) → bytes`. + +**Response reading**: `parse_response(bytes) → Response` which exposes `status`, `model` (as an iterator of symbol→value pairs), `core` (as a list of assertion names), or `simplified_expr` (as a new buffer view). + +### Hash-consing + +Optional but strongly recommended. The builder maintains a map from `(tag, child0, child1, width)` to existing `NodeId`. DAG sharing is free — reusing a `NodeId` as a child of multiple parents is always safe. + +### DAG compaction + +Before serialization, the builder walks from assertion and assumption roots, marks reachable nodes, and emits only those into the wire buffer with renumbered IDs. This keeps the wire format tight after many push/pop cycles that leave dead nodes in the buffer. + +--- + +## 12. BV_SELECT Encoding Detail + +`BV_SELECT` is the one variadic node. Its children are interleaved selector/value pairs followed by the default: + +``` +children[0] = selector_0 (Bool ref) +children[1] = value_0 (BV ref) +children[2] = selector_1 (Bool ref) +children[3] = value_1 (BV ref) +... +children[2N-2] = selector_{N-1} +children[2N-1] = value_{N-1} +children[2N] = default (BV ref) +``` + +`arity = 2N + 1`. `aux_hi = N` (number of selector/value pairs). Because `arity` is a `u8`, v1 supports `0 <= N <= 127`; `N = 0` is a degenerate select that returns the default child. Validators must reject `BV_SELECT` nodes where `arity` is even or where `aux_hi != (arity - 1) / 2`. All values and default must have the same BV width. Selectors are Bool refs. + +First-match semantics: the value associated with the earliest true selector is the result. If no selector is true, the default is used. Clients should pair `BV_SELECT` with pairwise mutual-exclusivity assertions on the selectors when applicable (emitted as normal assertions, not a special command). + +--- + +## 13. Error Handling + +### Client-side validation + +The client libraries validate during construction: width mismatches, out-of-range extract bounds, sort mismatches in children (BV where Bool expected, etc.). Errors are reported immediately at the builder call site, not deferred to serialization or server response. + +### Server-side validation + +The server validates the request and expression buffer on decode. Malformed requests receive an `ERROR` response. The server must not crash or corrupt state on adversarial input. + +Strict v1 validation rules: + +- Frame length must match the binary request or response length implied by its header fields. +- Expression buffer length must equal `32 + node_count*24 + child_count*4 + blob_len`, computed with overflow-checked arithmetic. +- `node_count` must be `<= 2^31` so every node index is representable in a typed node ref. +- Expression buffer magic must be `"SMT\0"`, version must be `1`. +- All multi-byte fields are decoded little-endian using safe byte reads; implementations must not rely on host alignment. +- Reserved and tag-unused fields are ignored by validators. Encoders should write them as zero for deterministic hashes and tests. +- Tag values must be known v1 tags when solving or simplifying. +- `arity` must match the tag's required arity, except for `BV_SELECT` which uses `arity = 2N + 1`. +- `children + arity` must be within the child array when `arity > 0`. +- Every child/root/target/model node reference must have an index `< node_count` and a sort bit matching the referenced node's tag-derived sort. +- Every child reference must point to an earlier node index than the parent, enforcing bottom-up acyclic construction. +- Assertion roots and assumption roots must be Bool refs. +- `target_node` must be a BV ref for `MINIMIZE`/`MAXIMIZE`. +- Bool-producing nodes must have `width = 0`; BV-producing nodes must have `width` in `1..65536`. +- BV binary operands must have equal widths; comparison and overflow operands must have equal widths. +- `BV_EXTRACT` must satisfy `0 <= aux_lo <= aux_hi < child_width`, and result width must be `aux_hi - aux_lo + 1`. +- `BV_CONCAT` result width must equal the sum of child widths and remain within the v1 width limit. +- `BV_ZEXT`/`BV_SEXT` result width must equal child width plus `aux_hi` and remain within the v1 width limit. +- Blob refs must be in bounds using overflow-checked `offset + len`. Symbol names must be valid UTF-8. Wide constants must reference exactly `ceil(width / 8)` bytes. +- `named_count <= assertion_count`; named assertion refs must point to valid UTF-8 names in the expression blob table. + +### Budget exhaustion + +A `SOLVE` or `MINIMIZE`/`MAXIMIZE` request with a nonzero millisecond `budget` may return `UNKNOWN` if the timeout is exhausted. This is not an error — it's an inconclusive result. The client decides whether to retry with a larger timeout, fall back to a different strategy, or treat the path as unknown. + +--- + +## 14. Implementation Phases + +The implementation should be staged so each phase has a testable artifact and works on Windows, macOS, and Linux. + +### Phase 0 — Final v1 spec and golden vectors + +- Keep this document as the v1 wire contract. +- Add golden byte-level tests for: empty expression buffer, simple SAT query, simple UNSAT query, wide constant, named assertion/core, malformed child index. +- Add conformance tests for little-endian decoding and response payload parsing. + +### Phase 1 — Rust binary codec and builder + +Rust is the preferred first implementation language because it gives safe cross-platform byte parsing, straightforward TCP support, and usable Z3 bindings. The server and reference client can share one Rust crate for constants, encoding, decoding, validation, and tests. + +Deliverables: + +- Tag/command/status/flag constants. +- Expression buffer parser/validator using safe unaligned reads. +- Expression builder with width/sort validation. +- Request/response envelope codecs. +- Response payload codecs for model/core/simplify/optimization blocks. +- Unit tests and fuzz-style malformed-input tests. + +No solver backend is required in this phase. + +### Phase 2 — SOLVE with one Z3 backend + +- Translate validated IR to Z3 bottom-up. +- Implement `SOLVE` for SAT/UNSAT/UNKNOWN/ERROR. +- Implement model extraction for `WANT_MODEL`. +- Implement named assertions and unsat cores for `WANT_CORE`. +- Add a simple length-prefixed TCP server. +- Add CI for Windows, macOS, and Linux. Pin or document Z3 library provisioning for all three platforms. + +This is the first useful end-to-end milestone. + +### Phase 3 — Client libraries and compatibility polish + +- Provide the Rust reference client as the canonical implementation. +- Add single-file C++ and Python builders once the Rust codec is stable. +- Add request-building helpers for push/pop, assumptions, compaction, and client-side lowerings. +- Add round-trip tests shared across languages using the golden vectors. + +### Phase 4 — SMT-LIB text frontend + +- Parse one complete length-prefixed SMT-LIB script per request. +- Reject unsupported incremental commands such as `push`/`pop`. +- Build the same internal IR as the binary frontend. +- Return standard text responses for text requests. + +### Phase 5 — Second backend and backend racing + +- Evaluate `binbit` (`https://github.com/bint-disasm/binbit`) as the preferred second backend if its API and cross-platform packaging fit the server. +- Use Bitwuzla as an alternative second backend if it is easier to package or exposes required features sooner. +- Add translation for the same validated IR to the chosen second backend. +- Race Z3 and the second backend for `SOLVE`. +- Log backend disagreements and preserve deterministic response semantics. + +### Phase 6 — SIMPLIFY and optimization commands + +- Implement `SIMPLIFY` response blocks. +- Implement `MINIMIZE`/`MAXIMIZE` with signed and unsigned ordering. +- Add optional model extraction for optimization commands. + +### Phase 7 — Cache and server optimizations + +- Add request/response caching after response semantics are stable. +- Include all fields that affect the returned payload in cache keys, including command, flags, expression bytes, roots, assumptions, target node, and timeout/cache policy. +- Add solver pooling and warm-start heuristics behind the same stateless protocol. + +## 15. Future Considerations + +Items explicitly deferred from v1 but anticipated in the design: + +- **Delta compression**: for large base formulas, send only the diff from a previous request. Requires content-addressed node storage on the server. The expression buffer format is compatible — the header can gain a `base_hash` field referencing a previously cached buffer. +- **Array theory**: `QF_ABV` support. Would add `ARRAY_SELECT` and `ARRAY_STORE` tags plus a sort representation for arrays. The typed-reference scheme extends naturally (steal another bit or use a small sort table). +- **Streaming results**: for optimization queries (min/max), stream improving bounds as they're found rather than waiting for the optimal. +- **Cross-format cache dedup**: hash the canonicalized IR rather than the raw bytes to share cache entries between binary and text requests. +- **Additional backends**: binbit, Bitwuzla, Boolector, CVC5, or custom bitblasters. The backend interface is a single trait/interface (translate IR, solve, extract model). +- **Server-side warm-start**: route structurally similar queries to solvers with relevant learned clauses. Invisible to the protocol. + +``` + +`scripts/qfbvsmtrs_corpus.py`: + +```py +#!/usr/bin/env python3 +"""Run qfbvsmtrs over an SMT-LIB QF_BV corpus with resumable/incremental reports. + +Full first pass: + cargo build --release -p qfbvsmtrs + python scripts/qfbvsmtrs_corpus.py target/smtlib/QF_BV-2025 \ + --exe target/release/qfbvsmtrs.exe --budget-ms 3000 --timeout 30 --workers 12 \ + --report target/smtlib/qfbvsmtrs_corpus_report.jsonl + +Incremental re-run that skips previously known-good (`kind == ok`) files and only +queues non-conclusive/problem files from a baseline report: + python scripts/qfbvsmtrs_corpus.py target/smtlib/QF_BV-2025 \ + --baseline-report target/smtlib/qfbvsmtrs_corpus_report.jsonl \ + --report target/smtlib/qfbvsmtrs_corpus_rerun.jsonl \ + --budget-ms 30000 --timeout 120 --workers 8 + +The output report is append-only. If interrupted, re-run the same command: paths +already present in the output report are skipped, so interrupted jobs resume. +""" + +from __future__ import annotations + +import argparse +import collections +import concurrent.futures +import json +import os +from pathlib import Path +import re +import subprocess +import time +from typing import Iterable + +STATUS_RE = re.compile(r"\(set-info\s+:status\s+(sat|unsat|unknown)\)") +ALL_KINDS = {"ok", "unknown", "timeout", "error", "wrong", "weird"} + + +def expected_status(path: Path) -> str: + # Stream instead of read_text(): a few SMT-LIB files are huge, while :status + # is conventionally near the top. + with path.open(encoding="utf-8", errors="ignore") as file: + for line in file: + match = STATUS_RE.search(line) + if match: + return match.group(1) + return "none" + + +def relpath(root: Path, path: Path) -> str: + # Keep the historical JSONL format on Windows while still being deterministic. + return str(path.relative_to(root)) + + +def parse_kind_set(text: str | None) -> set[str] | None: + if text is None: + return None + values = {item.strip() for item in text.split(",") if item.strip()} + unknown = values - ALL_KINDS + if unknown: + raise SystemExit(f"unknown result kind(s): {', '.join(sorted(unknown))}") + return values + + +def iter_report_records(path: Path) -> Iterable[dict]: + if not path.exists(): + return + with path.open(encoding="utf-8") as file: + for line in file: + if line.strip(): + yield json.loads(line) + + +def load_latest_by_path(reports: list[Path]) -> dict[str, dict]: + latest: dict[str, dict] = {} + for report in reports: + for record in iter_report_records(report): + latest[record["path"]] = record + return latest + + +def run_one( + root: Path, + exe: str, + budget_ms: int, + timeout: float, + sat_backend: str | None, + path: Path, +) -> dict: + expected = expected_status(path) + env = os.environ.copy() + if budget_ms: + env["QFBVSMTRS_BUDGET_MS"] = str(budget_ms) + if sat_backend: + env["QFBVSMTRS_SAT_BACKEND"] = sat_backend + start = time.time() + try: + proc = subprocess.run( + [exe, str(path)], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=timeout, + env=env, + ) + first = (proc.stdout.strip().splitlines() or [""])[0] + err = (proc.stderr.strip().splitlines() or [""])[0] + if proc.returncode != 0: + kind, result, detail = "error", "error", err + elif first in ("sat", "unsat", "unknown"): + result = detail = first + if expected in ("sat", "unsat") and first != expected: + kind = "unknown" if first == "unknown" else "wrong" + else: + kind = "ok" + else: + kind, result, detail = "weird", first, first + except subprocess.TimeoutExpired: + kind, result, detail = "timeout", "timeout", "timeout" + return { + "path": relpath(root, path), + "expected": expected, + "result": result, + "kind": kind, + "elapsed": time.time() - start, + "bytes": path.stat().st_size, + "detail": detail, + } + + +def summarize_records(records: Iterable[dict]) -> dict: + counts: collections.Counter[str] = collections.Counter() + expected: collections.Counter[str] = collections.Counter() + examples: dict[str, list[dict]] = collections.defaultdict(list) + total = 0 + for record in records: + total += 1 + counts[record["kind"]] += 1 + expected[record.get("expected", "none")] += 1 + bucket = examples[record["kind"]] + if len(bucket) < 10: + bucket.append(record) + return { + "records": total, + "counts": dict(counts), + "expected": dict(expected), + "examples": dict(examples), + } + + +def write_summary( + path: Path, + report: Path, + baseline_reports: list[Path], + summary_mode: str, + queued: int, + skipped: collections.Counter[str], +) -> None: + if summary_mode == "merged": + latest = load_latest_by_path([*baseline_reports, report]) + summary = summarize_records(latest.values()) + summary["mode"] = "merged-latest-by-path" + summary["baseline_reports"] = [str(path) for path in baseline_reports] + else: + summary = summarize_records(iter_report_records(report)) + summary["mode"] = "report-only" + summary["baseline_reports"] = [] + summary.update( + { + "report": str(report), + "queued_this_run": queued, + "skipped_this_run": dict(skipped), + } + ) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(summary, indent=2), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("root", type=Path) + parser.add_argument("--exe", default="target/release/qfbvsmtrs.exe") + parser.add_argument("--budget-ms", type=int, default=3000) + parser.add_argument("--timeout", type=float, default=12.0) + parser.add_argument("--workers", type=int, default=12) + parser.add_argument("--sat-backend", choices=["splr", "varisat", "dpll"]) + parser.add_argument( + "--report", + type=Path, + default=Path("target/smtlib/qfbvsmtrs_corpus_report.jsonl"), + help="append-only output report; existing paths are skipped for resume", + ) + parser.add_argument( + "--baseline-report", + type=Path, + action="append", + default=[], + help="previous report used for incremental selection; can be repeated", + ) + parser.add_argument( + "--skip-kinds", + default="ok", + help="comma-separated baseline kinds to skip, default: ok", + ) + parser.add_argument( + "--rerun-kinds", + help="comma-separated baseline kinds to run exclusively, e.g. unknown,timeout,error,wrong", + ) + parser.add_argument( + "--path-contains", + action="append", + default=[], + help="only queue paths containing this substring; can be repeated", + ) + parser.add_argument( + "--path-regex", + help="only queue paths matching this regular expression", + ) + parser.add_argument( + "--limit", + type=int, + help="queue at most this many files after filtering (useful for sampled increments)", + ) + parser.add_argument("--list-only", action="store_true", help="print selection summary and exit") + parser.add_argument( + "--summary", + type=Path, + help="write JSON summary; default is _summary.json", + ) + parser.add_argument( + "--summary-mode", + choices=["report", "merged"], + default="merged", + help="summary source: output report only, or latest records from baseline(s)+output", + ) + args = parser.parse_args() + + skip_kinds = parse_kind_set(args.skip_kinds) or set() + rerun_kinds = parse_kind_set(args.rerun_kinds) + path_regex = re.compile(args.path_regex) if args.path_regex else None + + root = args.root.resolve() + all_files = sorted(root.rglob("*.smt2")) + output_seen = {record["path"] for record in iter_report_records(args.report)} + baseline = load_latest_by_path(args.baseline_report) + + queued_files: list[Path] = [] + skipped: collections.Counter[str] = collections.Counter() + for path in all_files: + rel = relpath(root, path) + if rel in output_seen: + skipped["already-in-output-report"] += 1 + continue + if args.path_contains and not any(text in rel for text in args.path_contains): + skipped["path-filter"] += 1 + continue + if path_regex and not path_regex.search(rel): + skipped["path-filter"] += 1 + continue + base = baseline.get(rel) + if base is not None: + kind = base["kind"] + if rerun_kinds is not None and kind not in rerun_kinds: + skipped[f"baseline-kind-{kind}"] += 1 + continue + if rerun_kinds is None and kind in skip_kinds: + skipped[f"baseline-kind-{kind}"] += 1 + continue + elif rerun_kinds is not None: + skipped["missing-from-baseline"] += 1 + continue + queued_files.append(path) + + if args.limit is not None: + skipped["over-limit"] += max(0, len(queued_files) - args.limit) + queued_files = queued_files[: args.limit] + + print( + "selection", + json.dumps( + { + "total_files": len(all_files), + "queued": len(queued_files), + "skipped": dict(skipped), + "baseline_reports": [str(path) for path in args.baseline_report], + "output_report": str(args.report), + }, + sort_keys=True, + ), + flush=True, + ) + if args.list_only: + return 0 + + counts: collections.Counter[str] = collections.Counter() + started = time.time() + args.report.parent.mkdir(parents=True, exist_ok=True) + with args.report.open("a", encoding="utf-8") as report: + with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as pool: + futures = [ + pool.submit( + run_one, + root, + args.exe, + args.budget_ms, + args.timeout, + args.sat_backend, + path, + ) + for path in queued_files + ] + for done, future in enumerate(concurrent.futures.as_completed(futures), start=1): + record = future.result() + counts[record["kind"]] += 1 + report.write(json.dumps(record) + "\n") + report.flush() + if done % 500 == 0 or record["kind"] not in ("ok",): + print( + done, + "/", + len(queued_files), + dict(counts), + "last", + record["kind"], + record["expected"], + record["result"], + f"{record['elapsed']:.2f}s", + record["path"][:100], + "elapsed", + f"{time.time() - started:.1f}s", + flush=True, + ) + print("done", dict(counts), "elapsed", time.time() - started) + + summary = args.summary + if summary is None: + summary = args.report.with_name(args.report.stem + "_summary.json") + write_summary( + summary, + args.report, + args.baseline_report, + args.summary_mode, + len(queued_files), + skipped, + ) + print("summary", summary, flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + +``` \ No newline at end of file