From eb6ba6273935fbaa9e4df1ac93c08ea565c0ecdd Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 14:04:28 -0400 Subject: [PATCH 01/20] wasm32-pins: land the 32-bit execution leg with today's boundaries pinned red MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit just wasm-check proves before compiles for a 32-bit target; nothing in the tree executed there, so 32-bit-only seams could pass every native suite while misbehaving on wasm32. This leg is the instrument: a detached guest/harness workspace (the fuzzfit idiom) whose guest drives the public byte decode doors at the exact boundary sizes under a real 32-bit usize, and whose tests pin each boundary's behavior as found — red first, per instrument discipline; the cures flip the same pins to correct-value assertions. The pinned bad baselines, verified by execution on wasm32: - 67108864 bytes (exactly 2^29 bits): a valid Version encoding is SILENTLY rejected as Truncated — bitvec's element guard admits the slice and the span encoding's len << 3 shifts the length out of a 32-bit usize, constructing an empty view. Silent wrong result, the worst class. (The recorded finding expected a panic here; the panic starts one byte up.) - 67108865 bytes: bitvec's element guard panics — a valid encoding refused by panic. - 536870912 bytes (512 MiB): panic; bitvec's cap masks the Bits::len() usize multiply overflow that begins at this size. - rank exp = 2^32 - 8: panic inside dashu — from_be_bytes sizes its buffer from input bytes (leading zeros included) and overruns the backend's 32-bit MAX_CAPACITY even though the numerator value fits. A newly found seam below the recorded Shl seam, partially masking it. - rank exp = 2^32: panic at Base's Shl usize::try_from, the recorded seam, at the smallest honest trigger (~604 MB: the format counts fraction depth from bits actually read, so no smaller stream reaches it). Green adjacency witnesses ride beside each red pin (67108863 bytes; exp = 2^32 - 32) so each flip is attributable to its seam, plus a small round-trip/typed-reject liveness pin so a dead leg cannot read green. The guest builds with overflow-checks on: the audited failure class includes silent release wraps, and the checks turn exactly those into observable traps. Wired into the gate's wasm stream (measured: ~3 s of test wall time, all eight pins). Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/wasm32-pins/Cargo.lock | 1175 +++++++++++++++++ crates/before/wasm32-pins/Cargo.toml | 32 + crates/before/wasm32-pins/guest/Cargo.toml | 15 + crates/before/wasm32-pins/guest/src/lib.rs | 200 +++ crates/before/wasm32-pins/harness/Cargo.toml | 17 + crates/before/wasm32-pins/harness/src/lib.rs | 98 ++ .../before/wasm32-pins/harness/tests/pins.rs | 127 ++ justfile | 42 +- 8 files changed, 1705 insertions(+), 1 deletion(-) create mode 100644 crates/before/wasm32-pins/Cargo.lock create mode 100644 crates/before/wasm32-pins/Cargo.toml create mode 100644 crates/before/wasm32-pins/guest/Cargo.toml create mode 100644 crates/before/wasm32-pins/guest/src/lib.rs create mode 100644 crates/before/wasm32-pins/harness/Cargo.toml create mode 100644 crates/before/wasm32-pins/harness/src/lib.rs create mode 100644 crates/before/wasm32-pins/harness/tests/pins.rs diff --git a/crates/before/wasm32-pins/Cargo.lock b/crates/before/wasm32-pins/Cargo.lock new file mode 100644 index 00000000..cb88ea68 --- /dev/null +++ b/crates/before/wasm32-pins/Cargo.lock @@ -0,0 +1,1175 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "before" +version = "0.1.0" +dependencies = [ + "bitvec", + "bytes", + "dashu-int", + "dsi-bitstream", + "serde_json", + "static_assertions", + "suanpan", + "thiserror", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "cpp_demangle" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d552bd33b7a56dc70aeb1e1c960e51a218fa0db50f23873b500a310379450b2d" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e80e4c222279e3330f6aa1a256ca77ddf156d4453166a9f09defedc4594dd" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "820ce15d4ad3562d613c31a67b6d0434d403e7091a68d1349903842f7d31737e" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61bca563d4b86d285928d9e27f97f27039bb33a0fc524fa130d7d0c106bf8ab3" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709f4b7c0fb57d952658d5b5c07fbdc4149acd7b7f0de9678ae754b9c981949a" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.1", + "libm", + "log", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903dc8915af62aad1d9d0f39a5968d33fa80b9aa899ed6f56e47f40ca4512e1e" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0522d74c227e49f3fd49ab055311486ae4f09083262b66705bed676952491469" + +[[package]] +name = "cranelift-control" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66560ea1c5cef72e170b18e46d263dba2d3169c9d39e8cafdab2173c6362cc1a" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ef5b17cc814d27e96b66a5b46da0e4ce2b8ac55a6d478048bb99d04b05526" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a87e0aaa39dbf70693b348a221e45904111704ee8f9fef140498471005f9842d" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.1", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf79003ebfa1eed5e87f3b84446ad5236f540268289960e14f387dc9b28e40b7" + +[[package]] +name = "cranelift-native" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05bf4f235743c81e67ee4db617c5a4a0b65d58d3f0cfc575ee0f1a4e0cd58273" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.134.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6977c2a71ab1e0d1e62f966b411a498aa04c4dce47d93d52f8a360a06058922" + +[[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 = "dashu-base" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64b04cdfc4c8533100fe00304eb9687173bda47b1f1dac8af12ba13712ed49d" + +[[package]] +name = "dashu-int" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6ee98721d5d223e5b64b642dd9588b79d9ef415554b13720308b77d628c3be6" +dependencies = [ + "cfg-if", + "dashu-base", + "num-modular", + "rustversion", + "static_assertions", +] + +[[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 = "dsi-bitstream" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f3e8a7ab0f1148cf7a792807d330f1e417dda3f4f122526e609a83925b4823" +dependencies = [ + "num-primitive", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "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 = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[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 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "num-modular" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd8e500409e6cd603b03e477c26a6caecdc27ac58979a53e881c75eafc079f44" + +[[package]] +name = "num-primitive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0178502a58a2514f927965f80216c0ea2533b5364061544ea8e09b79343d4183" + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown 0.17.1", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulley-interpreter" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5c8c21ea032e4efbdf2d067dc45171779dbe0c8ecf20ef4a57efa7474f2b0a" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f10925455d5dde962e3604eade797ba5488644c8ee14a44191e2f2b58980574" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[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 = "suanpan" +version = "0.1.0" +dependencies = [ + "dashu-int", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-encoder" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm32-pins-guest" +version = "0.0.0" +dependencies = [ + "before", +] + +[[package]] +name = "wasm32-pins-harness" +version = "0.0.0" +dependencies = [ + "wasmtime", +] + +[[package]] +name = "wasmparser" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" +dependencies = [ + "bitflags", + "hashbrown 0.17.1", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmprinter" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7142797de29b35ab8dbf15c00f55fda75d409da4c423a8ab8bd6b667a785824b" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser", +] + +[[package]] +name = "wasmtime" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c80ca6098e0d4d06886d91d7f2cc3cb6623eb583c4c0ab3c89cbfb6098c8586c" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "futures", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "windows-sys", +] + +[[package]] +name = "wasmtime-environ" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134f9d136d29c76f6c1b4c9b468e97a2efc38c7d49fd188e39b64870b2fea701" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.1", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder", + "wasmparser", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc8a678149885cae00289f806fbe74c7863084cf74cace0b8dc73602279400e1" + +[[package]] +name = "wasmtime-internal-core" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0092c4b9d070ac5e278b6d0db10f5e71214190f0347ca57502b4692f796321" +dependencies = [ + "hashbrown 0.17.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6851ebc9e03cab23d9821d2b2505d380bdfe38f35ccec945247b05274d85c98b" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror", + "wasmparser", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b26da6d5f60d4c438da70bba3553fe810a840533a64156be287dca2081c6991" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed621ba25d7bf78b7edd7b4749abbb27c2e5cdba836c9504a424ee74b6c23149" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5684ba160951baad06a725696f3c590e2fb0e8067c2aebee27bf7f9259058e85" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112eead527bffa8ff0646a11fb4339a9d52ddd1da2b9a6fce4aff84c815dd94f" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "47.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153592e0bed824fc13c6696203fa1bd7bd20eb355316475e39a55f081ef80eca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/before/wasm32-pins/Cargo.toml b/crates/before/wasm32-pins/Cargo.toml new file mode 100644 index 00000000..aabc9f51 --- /dev/null +++ b/crates/before/wasm32-pins/Cargo.toml @@ -0,0 +1,32 @@ +# Standalone 32-bit boundary-pin workspace. The empty `[workspace]` table +# detaches it from the parent `rumors` workspace, so the ordinary gate never +# builds it (the harness carries wasmtime, a heavy tool-side dependency that +# must stay out of the production crates' graph; the guest builds only for +# wasm32-unknown-unknown). Build/run through the `just wasm32-pins*` recipes. +# +# Why this exists: `just wasm-check` only proves `before` *compiles* for a +# 32-bit target; nothing in the tree *executes* there, so a 32-bit-only +# arithmetic seam (a `usize` wrap, a pointer-width-capped dependency limit) +# can pass every native suite while decoding wrongly on wasm32. This +# workspace is the execution leg: the guest drives `before`'s public byte +# decode doors at the exact sizes where 32-bit position arithmetic has +# boundaries, and the harness pins each boundary's behavior — red first +# (the trap or wrong value as found), then green when the seam is engineered +# around. +# +# Two members: +# guest the wasm kernel: boundary-size decode cases over `before`'s +# public surface, compiled to wasm32-unknown-unknown and executed +# under wasmtime with a full 4 GiB 32-bit address space +# harness the native side: the wasmtime driver plus the pin tests +[workspace] +members = ["guest", "harness"] +resolver = "2" + +# The pins run at release speed (the boundary cases walk hundreds of +# megabytes), but with overflow checks kept ON: the 32-bit failure class +# under audit includes silent release-mode wraps, and the checks turn +# exactly those wraps into observable traps instead of wrong values the +# guest would have to detect after the fact. +[profile.release] +overflow-checks = true diff --git a/crates/before/wasm32-pins/guest/Cargo.toml b/crates/before/wasm32-pins/guest/Cargo.toml new file mode 100644 index 00000000..a3cc17bd --- /dev/null +++ b/crates/before/wasm32-pins/guest/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "wasm32-pins-guest" +version = "0.0.0" +publish = false +edition = "2021" + +# A cdylib so `cargo build --target wasm32-unknown-unknown` emits a plain wasm +# module (no wasm-bindgen: the ABI is hand-written i64 exports, one per pinned +# boundary case, and the interesting inputs are synthesized inside the guest — +# nothing bulk ever crosses the host boundary). +[lib] +crate-type = ["cdylib"] + +[dependencies] +before = { path = "../.." } diff --git a/crates/before/wasm32-pins/guest/src/lib.rs b/crates/before/wasm32-pins/guest/src/lib.rs new file mode 100644 index 00000000..777b43f8 --- /dev/null +++ b/crates/before/wasm32-pins/guest/src/lib.rs @@ -0,0 +1,200 @@ +//! The 32-bit boundary-pin guest: `before`'s public byte decode doors driven +//! at the sizes where 32-bit position arithmetic has boundaries, compiled to +//! wasm32-unknown-unknown so every pin executes under a genuinely 32-bit +//! `usize` with a full 4 GiB address space. +//! +//! The host (the `wasm32-pins-harness` crate) instantiates this module under +//! wasmtime and calls one export per pinned case. The contract: +//! +//! - Every interesting input is synthesized *inside* the guest (hundreds of +//! megabytes at the deepest pins), so nothing bulk crosses the host +//! boundary and the host-side test stays a one-call driver. +//! - An export returns a nonnegative observation on success — the decoded +//! value's own bit length where one exists, `0` for a plain pass — and a +//! negative code naming the first failed observation. +//! - A panic anywhere in `before` or its dependencies aborts the guest, +//! which wasmtime surfaces as a trap. The traps ARE the pinned bad +//! baselines, so trap-versus-value is the harness's red/green axis. +//! +//! The workspace builds this guest with `overflow-checks = true`: the 32-bit +//! failure class under audit includes silent release-mode wraps, and the +//! checks turn exactly those wraps into observable traps instead of wrong +//! values downstream code would have to detect after the fact. + +use before::{Rank, Version}; + +/// The canonical encoding of a valid single-leaf `Version` padded to exactly +/// `n` bytes: one leaf flag, then the Elias-gamma code of the leaf height +/// `2^k - 1` with `k = 4n - 5`, then the marker byte. +/// +/// The layout, bit by bit (positions are MSB-first over the buffer): +/// +/// - bit `0`: `1`, the leaf flag ending an empty topology run; +/// - bits `1 ..= k`: the gamma code's `k`-zero prefix; +/// - bit `k + 1`: the mantissa's leading `1` (the value is `m = 2^k`); +/// - bits `k + 2 ..= 2k + 1`: the mantissa's remaining `k` zeros; +/// - bit `2k + 2 = 8n - 8`: the padding marker, alone in the final byte. +/// +/// The stream is canonical (a lone leaf has no sibling to collapse with, and +/// its absolute height is a natural), so `Version::decode` must accept it at +/// any `n` the target's memory admits, and the decoded value's +/// `encoded_bits` is exactly `8n - 8`. +fn synth_version(n: usize) -> Vec { + assert!( + n >= 18, + "the single-wide-leaf layout needs k = 4n - 5 >= 64" + ); + let k = 4 * n - 5; + let mut bytes = vec![0u8; n]; + bytes[0] |= 0x80; // the leaf flag + bytes[(k + 1) / 8] |= 0x80 >> ((k + 1) % 8); // the mantissa's leading 1 + bytes[n - 1] |= 0x80; // the padding marker + bytes +} + +/// Sanity and liveness: a small synthesized version decodes to the expected +/// bit length, its bytes round-trip, and two mutilations reject with typed +/// errors (never a panic). Green at every commit; a red here means the +/// harness or the synthesis is broken, not that a boundary moved. +#[no_mangle] +pub extern "C" fn pin_version_small() -> i64 { + let bytes = synth_version(64); + let v = match Version::decode(&bytes[..]) { + Ok(v) => v, + Err(_) => return -1, + }; + if v.encoded_bits() != 8 * 64 - 8 { + return -2; + } + if v.as_bytes() != &bytes[..] { + return -3; + } + // A truncated input must reject as a typed error. + if Version::decode(&bytes[..63]).is_ok() { + return -4; + } + // A zeroed final byte has no padding marker: typed reject. + let mut unmarked = bytes; + unmarked[63] = 0; + if Version::decode(&unmarked[..]).is_ok() { + return -5; + } + 0 +} + +/// Decode a valid `n`-byte synthesized version and return its +/// `encoded_bits` (always `8n - 8` for the synthesized layout), checking +/// that the stored bytes round-trip the input exactly. +/// +/// The harness aims this at the 32-bit boundaries: the last size below +/// `bitvec`'s borrowed-view cap, the first size at it, and the 512 MiB +/// bit-length boundary of `usize` position arithmetic. +#[no_mangle] +pub extern "C" fn pin_version_decode(n_bytes: u64) -> i64 { + let n = match usize::try_from(n_bytes) { + Ok(n) => n, + Err(_) => return -100, + }; + let bytes = synth_version(n); + let v = match Version::decode(&bytes[..]) { + Ok(v) => v, + Err(_) => return -1, + }; + if v.as_bytes() != &bytes[..] { + return -2; + } + i64::try_from(v.encoded_bits()).unwrap_or(-3) +} + +/// The canonical encoding of the valid rank `(2^(exp - 65) + 1) / 2^exp`: +/// integral part zero, a fraction exactly `exp` expansion bits deep whose +/// set bits are expansion positions `65` and `exp`. +/// +/// `exp` must be a positive multiple of 8 (so the final fraction group is +/// flush and the pad is zero) and at least 128. The layout: one `0` header +/// bit (the inverted-delta code of integral `0`), then `exp / 8` fraction +/// groups of nine bits (a `1` continuation, then eight expansion bits), +/// then the closing `0` bit and zero padding to the byte boundary. +/// +/// Every byte of this stream is input the decoder must actually read — the +/// fraction's depth is deliberately in-band (counted from bits read, never +/// from a header's claim), so ~`9/64` bytes per expansion bit is the +/// smallest honest trigger for any `exp`-boundary behavior: no crafted +/// stream can reach the exponent seam without materializing this length. +fn synth_rank(exp: u64) -> Vec { + assert!( + exp >= 128 && exp.is_multiple_of(8), + "the layout wants flush groups" + ); + let groups = exp / 8; + let total_bits = 9 * groups + 2; // header + groups + close bit + let total_bytes = usize::try_from(total_bits.div_ceil(8)).expect("the stream is addressable"); + + // The group region is a 72-bit-periodic stream (eight 9-bit groups per + // period) offset one bit by the header: emit it bytewise as the tile + // shifted right one bit with carry, then clear everything at and past + // the close bit and patch the two set expansion bits. + const TILE: [u8; 9] = [0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01, 0x00]; + let mut bytes = vec![0u8; total_bytes]; + for (b, byte) in bytes.iter_mut().enumerate() { + let hi = if b == 0 { 0 } else { TILE[(b - 1) % 9] }; + *byte = (hi << 7) | (TILE[b % 9] >> 1); + } + // Clear the close bit and the padding after it (the tile pattern would + // keep opening groups past the fraction's end). + let close = 1 + 9 * groups; + let close_byte = usize::try_from(close / 8).expect("the stream is addressable"); + let offset = (close % 8) as u32; + bytes[close_byte] = if offset == 0 { + 0 + } else { + bytes[close_byte] & (0xFFu8 << (8 - offset)) + }; + for byte in bytes.iter_mut().skip(close_byte + 1) { + *byte = 0; + } + // Set expansion bits 65 and `exp` (1-based from the binary point): + // expansion bit `e` lives at stream position `1 + 9·((e-1)/8) + 1 + + // ((e-1)%8)` — its group's continuation bit, then its offset in the + // group. + for e in [65, exp] { + let p = 1 + 9 * ((e - 1) / 8) + 1 + ((e - 1) % 8); + let byte = usize::try_from(p / 8).expect("the stream is addressable"); + bytes[byte] |= 0x80 >> (p % 8); + } + bytes +} + +/// Decode a valid synthesized rank whose fraction is exactly `exp` +/// expansion bits deep, then check three exact-order observations against +/// reference ranks: the value sits strictly between zero and one, and +/// equals its own clone. +/// +/// The harness aims this at the `u64 -> usize` exponent seam: `exp` just +/// below `2^32` (in `usize` range on wasm32) and at `2^32` (past it). The +/// input is ~`9/64 · exp` bytes — about 604 MB at the seam — which is the +/// smallest honest trigger (`synth_rank` documents why no smaller stream +/// can reach it). +#[no_mangle] +pub extern "C" fn pin_rank_decode(exp: u64) -> i64 { + let bytes = synth_rank(exp); + let r = match Rank::decode(&bytes[..]) { + Ok(r) => r, + Err(_) => return -1, + }; + drop(bytes); + if r <= Rank::ZERO { + return -2; + } + let one = match Version::try_from(1) { + Ok(v) => v.rank(), + Err(_) => return -3, + }; + if r >= one { + return -4; + } + if r != r.clone() { + return -5; + } + 0 +} diff --git a/crates/before/wasm32-pins/harness/Cargo.toml b/crates/before/wasm32-pins/harness/Cargo.toml new file mode 100644 index 00000000..75f9c4c2 --- /dev/null +++ b/crates/before/wasm32-pins/harness/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wasm32-pins-harness" +version = "0.0.0" +publish = false +edition = "2021" + +[dependencies] +# Executes the guest on a genuinely 32-bit `usize`. Tool-side only: this +# workspace is detached from the parent, so wasmtime never enters the +# production crates' graph. No fuel and no pooling (unlike the fuzz-fit +# harness): the pins assert outcomes, not instruction counts, and each +# needs a full 4 GiB 32-bit address space rather than a pooled slot's +# small ceiling. +wasmtime = { version = "47", default-features = false, features = [ + "cranelift", + "runtime", +] } diff --git a/crates/before/wasm32-pins/harness/src/lib.rs b/crates/before/wasm32-pins/harness/src/lib.rs new file mode 100644 index 00000000..7a3590ff --- /dev/null +++ b/crates/before/wasm32-pins/harness/src/lib.rs @@ -0,0 +1,98 @@ +//! The wasmtime driver for the 32-bit boundary pins: loads the compiled +//! guest, instantiates it fresh per call, and reports each export's outcome +//! as value-or-trap. +//! +//! A fresh instance per call keeps the pins independent: wasm linear memory +//! only ever grows, and one pin's multi-gigabyte peak must not become the +//! next pin's baseline. The engine and compiled module are process-wide +//! (compilation is the expensive part). No fuel metering and no pooling +//! allocator: the pins assert outcomes, not instruction counts, and each +//! wants the full 4 GiB 32-bit address space a pooled slot would cap. + +use std::path::PathBuf; +use std::sync::OnceLock; + +use wasmtime::{Engine, Instance, Module, Store, Trap}; + +/// One pin call's outcome: the export's return value, or the trap that +/// aborted it. +/// +/// A trap is a first-class outcome, not a driver failure: the pinned bad +/// baselines ARE panics in `before` (surfaced as `unreachable` traps under +/// `panic = abort`), so the pins assert on this axis directly. +#[derive(Debug, PartialEq, Eq)] +pub enum Outcome { + /// The export returned: nonnegative is its observation, negative names + /// the first failed in-guest check (the guest's doc comments key them). + Value(i64), + /// The export trapped; `Trap::UnreachableCodeReached` is a guest panic. + Trapped(Trap), +} + +/// Locate the compiled guest module. +/// +/// Precedence: the `WASM32_PINS_GUEST_WASM` environment variable (explicit +/// provenance, what the `just wasm32-pins` recipe passes), then the +/// workspace-relative target dir the recipe builds into. +pub fn guest_wasm_path() -> PathBuf { + if let Ok(path) = std::env::var("WASM32_PINS_GUEST_WASM") { + return PathBuf::from(path); + } + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "../../../../../target/wasm32-pins/wasm32-unknown-unknown/release/wasm32_pins_guest.wasm", + ) +} + +/// The process-wide engine and compiled module. +fn engine_and_module() -> &'static (Engine, Module) { + static SHARED: OnceLock<(Engine, Module)> = OnceLock::new(); + SHARED.get_or_init(|| { + let engine = Engine::default(); + let path = guest_wasm_path(); + let module = Module::from_file(&engine, &path).unwrap_or_else(|error| { + panic!( + "wasm32-pins guest not loadable from {} (build it first: `just wasm32-pins-build`): {error}", + path.display() + ) + }); + (engine, module) + }) +} + +/// Call a nullary pin export in a fresh instance. +pub fn call0(export: &str) -> Outcome { + call(export, None) +} + +/// Call a one-argument pin export in a fresh instance. +pub fn call1(export: &str, arg: u64) -> Outcome { + call(export, Some(arg)) +} + +fn call(export: &str, arg: Option) -> Outcome { + let (engine, module) = engine_and_module(); + let mut store = Store::new(engine, ()); + let instance = + Instance::new(&mut store, module, &[]).expect("the guest instantiates without imports"); + let result = match arg { + None => { + let func = instance + .get_typed_func::<(), i64>(&mut store, export) + .expect("the export exists with the pinned signature"); + func.call(&mut store, ()) + } + Some(arg) => { + let func = instance + .get_typed_func::(&mut store, export) + .expect("the export exists with the pinned signature"); + func.call(&mut store, arg) + } + }; + match result { + Ok(value) => Outcome::Value(value), + Err(error) => match error.downcast_ref::() { + Some(&trap) => Outcome::Trapped(trap), + None => panic!("pin export {export} failed outside wasm: {error}"), + }, + } +} diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs new file mode 100644 index 00000000..adea2fb6 --- /dev/null +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -0,0 +1,127 @@ +//! The 32-bit boundary pins: each test drives one guest export at one +//! boundary size and pins its behavior on wasm32. +//! +//! Red-first discipline: a boundary found misbehaving is first pinned AS +//! FOUND — the assertion names the trap and the doc comment names the wrong +//! behavior it stands for — and the commit that engineers the seam around +//! flips the same test to the correct-value assertion. A pinned trap is +//! therefore never an accepted behavior: it is the committed bad baseline +//! the cure must move. + +use wasm32_pins_harness::{call0, call1, Outcome}; +use wasmtime::Trap; + +/// A guest panic surfaces as the `unreachable` trap (`panic = abort` on +/// wasm32-unknown-unknown). +const PANICKED: Outcome = Outcome::Trapped(Trap::UnreachableCodeReached); + +/// Liveness: a small valid synthesized version decodes on wasm32 with the +/// exact expected bit length, its stored bytes round-trip the input, and +/// truncated or marker-stripped mutations reject with typed errors, never a +/// panic. Green at every commit — a red here impeaches the leg itself, not +/// a boundary. +#[test] +fn version_small_roundtrips_and_rejects_typed() { + assert_eq!(call0("pin_version_small"), Outcome::Value(0)); +} + +/// Adjacency, green side: the largest input whose whole-buffer bit view +/// still fits `bitvec`'s borrowed-view length cap on wasm32 +/// (`usize::MAX >> 3` = 2^29 - 1 bits, so 67108863 bytes) decodes +/// correctly, returning its exact live bit length. This witnesses that the +/// red pin one byte up is the cap itself, not general large-input +/// breakage. +#[test] +fn version_decode_below_view_cap() { + assert_eq!( + call1("pin_version_decode", 67_108_863), + Outcome::Value(8 * 67_108_863 - 8), + ); +} + +/// BAD BASELINE, pinned red: a valid 64 MiB (67108864-byte) version +/// encoding is SILENTLY rejected as a typed decode error on wasm32 — the +/// worst failure class, a wrong result with no alarm. The decode door +/// views the whole buffer as one bit slice; at exactly 2^29 bits, +/// `bitvec`'s element-count guard still admits the slice, and the span +/// encoding's `len << 3` then shifts the length's one set bit clean out +/// of a 32-bit `usize`, constructing an EMPTY view. The validator walks +/// zero bits and reports `Truncated` — a typed rejection, reserved for +/// invalid inputs, delivered for a valid one. The guest observes it as +/// its `-1` (decode returned `Err`) code. The door engineering flips +/// this to `Value(8 * 67_108_864 - 8)`. +#[test] +fn version_decode_at_view_cap() { + assert_eq!(call1("pin_version_decode", 67_108_864), Outcome::Value(-1)); +} + +/// BAD BASELINE, pinned red: one byte past the silent-empty boundary +/// (67108865 bytes, 2^29 + 8 bits), `bitvec`'s element-count guard fires +/// and the decode door panics — a valid encoding that fits the target's +/// memory, refused by panic. Together with the silent-empty pin one byte +/// below, this witnesses both failure genres the borrowed-view cap +/// produces on wasm32. The door engineering flips this to +/// `Value(8 * 67_108_865 - 8)`. +#[test] +fn version_decode_past_view_cap() { + assert_eq!(call1("pin_version_decode", 67_108_865), PANICKED); +} + +/// BAD BASELINE, pinned red: a valid 512 MiB (2^29-byte) version encoding +/// panics on wasm32. Two seams stack here: today the trap fires in +/// `bitvec`'s view cap (the 64 MiB seam masks everything above it), and +/// once that is engineered around, `Bits::len`'s `bytes.len() * 8` — a +/// `usize` multiply — overflows at exactly this size (2^32 bits). This +/// build runs with overflow checks, so the wrap is this same trap; in an +/// unchecked build it wraps silently, coincidentally correct at exactly +/// 2^29 bytes and wrong (a slice length 2^32 short) for anything larger. +/// The len fix flips this to `Value(8 * 536_870_912 - 8)` — the largest +/// stored stream a 32-bit `usize` can denominate bit positions for. +#[test] +fn version_decode_at_bit_length_boundary() { + assert_eq!(call1("pin_version_decode", 536_870_912), PANICKED); +} + +/// Adjacency, green side: a valid rank whose fraction is 2^32 - 32 +/// expansion bits deep — the deepest flush-group exponent whose fraction +/// bytes still fit the big-integer backend's 32-bit buffer capacity — +/// decodes correctly today, and the decoded value sits strictly between +/// zero and one. ~604 MB of input: the smallest honest trigger, since the +/// fraction's depth is deliberately counted from bits actually read, +/// never from a header's claim. +#[test] +fn rank_decode_below_backend_capacity() { + assert_eq!( + call1("pin_rank_decode", (1u64 << 32) - 32), + Outcome::Value(0), + ); +} + +/// BAD BASELINE, pinned red: a valid rank whose fraction is 2^32 - 8 +/// expansion bits deep panics on wasm32 INSIDE the big-integer backend — +/// `dashu`'s `from_be_bytes` sizes its buffer from the input's byte count +/// (leading zero bytes included), and 536870911 fraction bytes need one +/// word more than the backend's 32-bit `MAX_CAPACITY`, even though the +/// numerator VALUE (which starts with 64 zero expansion bits) fits the +/// backend comfortably. This seam sits below the decoder's own +/// `usize::try_from` shift seam and partially masks it. The byte-assembly +/// fix strips leading zero bytes before materializing, flipping this to +/// `Value(0)`. +#[test] +fn rank_decode_at_backend_byte_capacity() { + assert_eq!(call1("pin_rank_decode", (1u64 << 32) - 8), PANICKED); +} + +/// BAD BASELINE, pinned red: a valid rank whose fraction is exactly 2^32 +/// expansion bits deep panics on wasm32 at `Base`'s `Shl` — the +/// decoder rebuilds the numerator as `integral << exp`, and the shift +/// amount's `usize::try_from` fails for `exp >= 2^32` on a 32-bit target +/// even though the input (~604 MB) and the decoded numerator (~512 MiB) +/// both fit the 4 GiB address space. Under the ruled contract this valid +/// encoding must decode; the byte-assembly fix (concatenate the +/// integral's bytes with the fraction groups instead of shifting, leading +/// zeros stripped) flips this to `Value(0)`. +#[test] +fn rank_decode_at_usize_exp_boundary() { + assert_eq!(call1("pin_rank_decode", 1u64 << 32), PANICKED); +} diff --git a/justfile b/justfile index 8d11636d..a783fd1d 100644 --- a/justfile +++ b/justfile @@ -69,6 +69,14 @@ fuzz_smoke_secs := "20" fuzzfit_target := justfile_directory() + "/target/fuzzfit" fuzzfit_guest_wasm := fuzzfit_target + "/wasm32-unknown-unknown/release/fuzzfit_guest.wasm" +# The 32-bit boundary-pin guest's wasm: produced by `wasm32-pins-build`, +# read by the `wasm32-pins` harness run. Named and passed explicitly for +# the same reason as the fuzz-fit guest above: an ambient CARGO_TARGET_DIR +# must not separate the producer from the consumer. + +wasm32pins_target := justfile_directory() + "/target/wasm32-pins" +wasm32pins_guest_wasm := wasm32pins_target + "/wasm32-unknown-unknown/release/wasm32_pins_guest.wasm" + # Criterion's output root: the bench-judge recipes save baselines and # denominator sidecars here, honoring CARGO_TARGET_DIR so a fresh or # redirected target directory keeps the baselines and the sidecars together. @@ -431,7 +439,7 @@ gate-streams: start_stream workspace 0 clippy clippy-default docs test-all citecheck start_stream doctest 10 doctest start_stream board 10 amp-board-acceptance worst-cases-pin - start_stream wasm 10 fuzzfit fuelscape-test + start_stream wasm 10 fuzzfit fuelscape-test wasm32-pins start_stream fuzz 10 fuzz-build start_stream surface 10 surface-totality start_stream internal-docs 10 docs-internal @@ -574,6 +582,38 @@ fuzzfit: fuzzfit-build fuzzfit-calibrate: fuzzfit-build FUZZFIT_GUEST_WASM={{ fuzzfit_guest_wasm }} cargo run --release -p fuzzfit-harness --bin calibrate +# The 32-bit boundary pins live in their own detached workspace +# (crates/before/wasm32-pins, the fuzz-fit idiom: workspace-wide builds +# never compile it, and wasmtime stays out of the production crates' +# graph); the gate reaches it only through these recipes by name. +# `wasm-check` proves before *compiles* for a 32-bit target; this leg is +# the tree's one place 32-bit code *executes*: the guest drives the public +# byte decode doors at the exact sizes where 32-bit position arithmetic +# has boundaries (bitvec's borrowed-view cap, the usize bit-length +# boundary at 512 MiB, the rank exponent's usize seam), and the harness +# pins each boundary's behavior — the red-first bad baselines and the +# green boundaries beside them. The guest builds with overflow checks on, +# so a 32-bit wrap is an observable trap, never a silently wrong value. + +# Build the 32-bit boundary-pin wasm guest and its harness (both halves). +[working-directory("crates/before/wasm32-pins")] +wasm32-pins-build: + cargo build -p wasm32-pins-guest --release --target wasm32-unknown-unknown --target-dir {{ wasm32pins_target }} + cargo build -p wasm32-pins-harness --tests --release + +# Run the 32-bit boundary pins under wasmtime. The deep pins walk +# hundreds of megabytes inside a 32-bit guest, so this leg costs minutes +# of wall time and peaks at a few GiB of host memory across nextest's +# parallel workers. The fmt/clippy lines are the detached workspace's own +# lint leg (the root `cargo fmt --all`/clippy cannot reach a detached +# workspace, so without them its source rots invisibly through green +# gates — the fuzzfit recipes carry the same discipline). +[working-directory("crates/before/wasm32-pins")] +wasm32-pins: wasm32-pins-build + cargo fmt --check + cargo clippy --all-targets -- -D warnings + WASM32_PINS_GUEST_WASM={{ wasm32pins_guest_wasm }} cargo nextest run --cargo-profile release + # The population atlas lives in its own detached workspace # (crates/before-fuelscape, the fuzz-fit idiom: workspace-wide builds never # compile it, and its wasmtime/plotters tooling stays out of the From 78c6537172429178798d7bfc4c577af634f16126 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 14:27:39 -0400 Subject: [PATCH 02/20] before: walk the byte decode doors over raw bytes, off the borrowed view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four byte decode doors (Party, Version, Clock, Span) and the borsh ReaderCursor built one borrowed bit view over the whole input buffer. bitvec's span encoding steals three usize bits for the head-bit offset, so on a 32-bit target the view caps at usize::MAX >> 3 bits: at exactly 2^29 bits (a valid 64 MiB encoding) the length field wraps and the view constructs EMPTY — the validator walks zero bits and silently rejects a valid input as Truncated — and one byte past that, view construction panics. The ruled contract says a valid encoding that fits the target's memory decodes correctly, so the seam is engineered around, not error-wrapped: - DsiCursor now runs on u64 positions over raw (body, masked-tail) parts, with byte-door constructors (over_bytes, over_bytes_live) and a u64 end-position read; the usize conversions that remain are checked, each carrying the derivation for why an honest walk cannot trip it. - The grammar walks grew byte-door entries (parse_id_bytes, validate_prefix_bytes, validate_dominating_bytes over a raw meet) returning u64 ends; the marker-padding judge got a raw-byte form and the slice form, now caller-less, dissolves into it. - The borsh ReaderCursor buffers at u64 positions, windows gamma codes over its raw bytes, and finish() now hands back the field's canonical marker-padded bytes, adopted via from_canonical with no bitvec round-trip (one copy and one re-seal fewer than before). - Bits::from_canonical asserts the storable bound (bit count fits usize): on 32-bit that is 512 MiB of buffer, where usize bit positions themselves run out — the documented seam the len() fix addresses next — and on 64-bit the bound is 2^61 bytes, dead by allocation. Bits' deref documents the borrowed view's remaining cap: decoded values past 64 MiB on 32-bit carry a correct byte surface (equality, hashing, canonical bytes, re-encode), while their walk surface still reads through the one bitvec borrow and fails loudly there; widening it means a crate-owned view type, proposed separately. The wasm32 pins flip: both view-cap pins (64 MiB silent-empty, 64 MiB + 1 panic) now read the exact decoded bit lengths, and the 512 MiB pin's trap moved from bitvec's cap to the len() multiply it was masking — re-pinned red at the unmasked seam, cured next. Native suite: 686/686 unchanged. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/src/borsh_impls.rs | 73 ++++++----- crates/before/src/clock.rs | 19 +-- crates/before/src/codec.rs | 22 +++- crates/before/src/codec/bits.rs | 86 +++++++++++-- crates/before/src/codec/dsi.rs | 119 ++++++++++++++---- crates/before/src/codec/gamma.rs | 63 ++++++---- crates/before/src/codec/scan.rs | 12 ++ crates/before/src/codec/tree.rs | 27 +++- crates/before/src/party.rs | 9 +- crates/before/src/span/wire.rs | 29 +++-- crates/before/src/version.rs | 9 +- crates/before/src/version/skyline.rs | 8 +- crates/before/src/version/skyline/admit.rs | 35 +++++- crates/before/src/version/skyline/overlay.rs | 14 +++ crates/before/src/version/skyline/validate.rs | 18 +-- .../before/wasm32-pins/harness/tests/pins.rs | 59 ++++----- 16 files changed, 450 insertions(+), 152 deletions(-) diff --git a/crates/before/src/borsh_impls.rs b/crates/before/src/borsh_impls.rs index 4b1b2a1a..30a8a4fb 100644 --- a/crates/before/src/borsh_impls.rs +++ b/crates/before/src/borsh_impls.rs @@ -14,7 +14,7 @@ use borsh::io::{Error, ErrorKind, Read, Write}; use borsh::{BorshDeserialize, BorshSerialize}; use crate::{ - codec::{self, BitCursor, BitsMut}, + codec::{self, BitCursor}, error::Decode, span::Span, version::decode_rank_stream, @@ -45,9 +45,13 @@ struct ReaderCursor<'a, R> { /// but not yet consumed by the parse; they are the only bits the /// [`read_int`] window may prove a code from. /// + /// `u64`, not `usize`: a field's byte count is bounded only by what the + /// reader yields, and `8 · bytes.len()` outgrows a 32-bit `usize` from + /// 512 MiB of field — exactly representable here. + /// /// [`read_bit`]: BitCursor::read_bit /// [`read_int`]: BitCursor::read_int - position: usize, + position: u64, } impl<'a, R: Read> ReaderCursor<'a, R> { @@ -59,13 +63,18 @@ impl<'a, R: Read> ReaderCursor<'a, R> { } } - fn finish(mut self) -> Result { - // Consume the tree's padding — one `1` marker, then zeros to the - // byte boundary — through the same on-demand reads as the parse: - // when the live bits end flush against a byte boundary, the - // padding is a whole `1000_0000` byte the parse never pulled, and - // leaving it unread would hand its bits to the next borsh field. - let end = self.position; + /// Consume the tree's padding and hand back the field's canonical bytes. + /// + /// The padding — one `1` marker, then zeros to the byte boundary — is + /// consumed through the same on-demand reads as the parse: when the live + /// bits end flush against a byte boundary, the padding is a whole + /// `1000_0000` byte the parse never pulled, and leaving it unread would + /// hand its bits to the next borsh field. After it, the buffered bytes + /// are exactly the tree's marker-padded canonical spelling — the at-rest + /// storage form, adopted without a copy or a borrowed bit view (whose + /// encoding caps below the field sizes this reader admits on 32-bit + /// targets). + fn finish(mut self) -> Result, Decode> { if !self.read_bit()? { return Err(Decode::TrailingBits); } @@ -74,9 +83,7 @@ impl<'a, R: Read> ReaderCursor<'a, R> { return Err(Decode::TrailingBits); } } - let mut bits = BitsMut::from_vec(self.bytes); - bits.truncate(end); - Ok(bits) + Ok(self.bytes) } } @@ -87,18 +94,23 @@ impl BitCursor for ReaderCursor<'_, R> { type Error = Decode; fn read_bit(&mut self) -> Result { - if self.position == 8 * self.bytes.len() { + if self.position == self.bytes.len() as u64 * 8 { let mut byte = [0]; self.reader.read_exact(&mut byte).map_err(Decode::Io)?; self.bytes.push(byte[0]); } - let bit = self.bytes[self.position / 8] & (0x80 >> (self.position % 8)) != 0; + // An in-range byte index fits `usize`: it indexes the buffer. + let bit = self.bytes[(self.position / 8) as usize] & (0x80 >> (self.position % 8)) != 0; self.position += 1; Ok(bit) } fn position(&self) -> usize { - self.position + // The grammar walks read no positions on this cursor (they return + // `()` or are read after `finish`); the conversion stays checked so + // a future position-reading walk over a past-usize field fails + // loudly instead of truncating. + usize::try_from(self.position).expect("a read position fits usize") } fn read_int(&mut self) -> Result { @@ -110,9 +122,7 @@ impl BitCursor for ReaderCursor<'_, R> { // construction. It fires when earlier refills left enough unconsumed // bits buffered; everything else, every reject included, is decided // by the per-bit loop below, refilling byte by byte on demand. - if let Some((n, next)) = - codec::decode_int_window(codec::bytes_as_bits(&self.bytes), self.position) - { + if let Some((n, next)) = codec::decode_int_window_bytes(&self.bytes, self.position) { self.position = next; return Ok(codec::Int::Small(n)); } @@ -120,15 +130,17 @@ impl BitCursor for ReaderCursor<'_, R> { } } -/// Read and validate one byte-aligned canonical id tree. -fn deserialize_id(reader: &mut R) -> borsh::io::Result { +/// Read and validate one byte-aligned canonical id tree, returning its +/// canonical marker-padded bytes. +fn deserialize_id(reader: &mut R) -> borsh::io::Result> { let mut cursor = ReaderCursor::new(reader); - codec::parse_id_from(&mut cursor).map_err(decode_error)?; + codec::parse_id_core(&mut cursor).map_err(decode_error)?; cursor.finish().map_err(decode_error) } -/// Read and validate one byte-aligned canonical skyline event stream. -fn deserialize_event(reader: &mut R) -> borsh::io::Result { +/// Read and validate one byte-aligned canonical skyline event stream, +/// returning its canonical marker-padded bytes. +fn deserialize_event(reader: &mut R) -> borsh::io::Result> { let mut cursor = ReaderCursor::new(reader); crate::version::skyline::validate_from(&mut cursor).map_err(decode_error)?; cursor.finish().map_err(decode_error) @@ -152,8 +164,10 @@ impl BorshDeserialize for Party { // The id grammar has no empty production (a starved reader rejects // inside the parse), so the parsed id is a nonzero share — the // standalone-party invariant (paper §3: `i ≠ 0`) holds structurally. - let bits = deserialize_id(reader)?; - Ok(Party::from_bits(bits)) + let bytes = deserialize_id(reader)?; + Ok(Party::from_frozen(codec::Bits::from_canonical( + bytes.into(), + ))) } } @@ -165,7 +179,8 @@ impl BorshSerialize for Version { impl BorshDeserialize for Version { fn deserialize_reader(reader: &mut R) -> borsh::io::Result { - deserialize_event(reader).map(Version::from_bits) + deserialize_event(reader) + .map(|bytes| Version::from_frozen(codec::Bits::from_canonical(bytes.into()))) } } @@ -270,16 +285,16 @@ impl BorshDeserialize for Span<'static> { .map_err(decode_error)?; // The final byte's padding check outranks the pair verdict, // exactly as the byte-slice decode orders them. - let bits = cursor.finish().map_err(decode_error)?; + let bytes = cursor.finish().map_err(decode_error)?; let hi = match admission { Admission::Refuted => return Err(decode_error(Decode::NotCanonical)), // The coincident span stores one buffer twice: the admission // walk proved the second stream byte-equal to the first, so // the join is the meet's clone — an `O(1)` refcount bump the - // ptr_eq fast paths then recognize — and the parsed bits are + // ptr_eq fast paths then recognize — and the parsed bytes are // dropped unstored. Admission::Equal => lo.clone(), - Admission::Dominates => Version::from_bits(bits), + Admission::Dominates => Version::from_frozen(codec::Bits::from_canonical(bytes.into())), }; Ok(Span::owned(lo, hi)) } diff --git a/crates/before/src/clock.rs b/crates/before/src/clock.rs index ef63ad29..d02bf25f 100644 --- a/crates/before/src/clock.rs +++ b/crates/before/src/clock.rs @@ -755,22 +755,27 @@ impl Clock { // exhausted input. Both parts then adopt slices of the ONE read buffer // as their storage: no per-component copy, and the id is parsed once // where handing byte ranges to the component decoders re-parsed it. + // Both components validate over the raw bytes (never a borrowed bit + // view, whose encoding caps below the buffer sizes this door admits + // on 32-bit targets); each walk's input is its component's whole + // byte range as bits, padding included, judged by its marker check. let id_bytes = { - let bits = codec::bytes_as_bits(&buf); - let id_end = codec::parse_id(bits, 0)?; + let id_end = codec::parse_id_bytes(&buf)?; // The party's padding marker rides in its final byte — which an // input cut right after a flush id tree lacks. That cut is // missing required data (the marker byte, and the whole version // after it): the truncation genre, exactly as a byte-starved // reader reports the same boundary. let id_bytes = (id_end + 1).div_ceil(8); - if 8 * id_bytes > bits.len() { + if id_bytes > buf.len() as u64 { return Err(Decode::Truncated); } - codec::require_marker_padding(&bits[..8 * id_bytes], id_end)?; - let tail = &bits[8 * id_bytes..]; - let v_end = crate::version::skyline::validate_prefix(tail)?; - codec::require_marker_padding(tail, v_end)?; + let id_bytes = + usize::try_from(id_bytes).expect("the id prefix ends within the read buffer"); + codec::require_marker_padding_bytes(&buf[..id_bytes], id_end)?; + let tail = &buf[id_bytes..]; + let v_end = crate::version::skyline::validate_prefix_bytes(tail)?; + codec::require_marker_padding_bytes(tail, v_end)?; id_bytes }; let buf = bytes::Bytes::from(buf); diff --git a/crates/before/src/codec.rs b/crates/before/src/codec.rs index 4aad8809..d9497bde 100644 --- a/crates/before/src/codec.rs +++ b/crates/before/src/codec.rs @@ -39,9 +39,14 @@ mod tests; pub(crate) use base::limb_meter; pub use base::Base; pub(crate) use bits::{ - byte_view, bytes_as_bits, canonical_eq, canonical_hash, padding_is_canonical, - require_marker_padding, slice_ptr_eq, + byte_view, canonical_eq, canonical_hash, padding_is_canonical, require_marker_padding_bytes, + slice_ptr_eq, }; +// Whole-buffer borrowed views serve only the meter surface and the test +// suites: the decode doors walk raw bytes instead (the view encoding caps +// below the buffer sizes the doors admit on 32-bit targets). +#[cfg(any(test, feature = "meter"))] +pub(crate) use bits::bytes_as_bits; // Production streams seal at the freeze seam (`Bits::freeze`); the // standalone form serves the buffers that stay build-side, all of them // meter/test instruments producing decodable bytes (the generators' @@ -62,13 +67,22 @@ pub(crate) use int::Int; // `ReaderCursor` (`borsh_impls`), the one consumer outside this module; the // cfg keeps the re-export from dangling when `borsh` is off. #[cfg(feature = "borsh")] -pub(crate) use gamma::decode_int_window; +// The raw-byte window serves the wire-side (borsh) reader, whose buffered +// bytes have no borrowed bit view once they outgrow the view encoding's cap; +// the slice-form window stays inside the codec (the slice cursor's fast +// path reaches it by module path). +#[cfg(any(test, feature = "borsh"))] +pub(crate) use gamma::decode_int_window_bytes; pub(crate) use literal::{id_is_empty, id_leaf, id_node}; pub(crate) use stack::{BitStack, PopStack}; pub(crate) use text::{parse_clock_str, parse_id_str}; -pub(crate) use tree::{parse_id, validate_id}; +pub(crate) use tree::{parse_id_bytes, validate_id}; // The mid-stream parser entry is consumed only by the borsh wire format // (everything else parses whole streams); gating the re-export keeps default // builds warning-free for downstream consumers. #[cfg(feature = "borsh")] +pub(crate) use tree::parse_id_core; +// The generic-position parse serves the wire-side (borsh) test suite; the +// grammar body above is what production readers drive. +#[cfg(test)] pub(crate) use tree::parse_id_from; diff --git a/crates/before/src/codec/bits.rs b/crates/before/src/codec/bits.rs index e9521064..a7d14bbd 100644 --- a/crates/before/src/codec/bits.rs +++ b/crates/before/src/codec/bits.rs @@ -139,7 +139,23 @@ impl Bits { /// the byte boundary; empty for the empty stream — which is what /// `require_marker_padding` accepts. Debug builds assert it; release builds /// trust the validator. + /// + /// # Panics + /// + /// Panics when the buffer's bit count exceeds `usize` — reachable only on + /// targets narrower than 64 bits, from 512 MiB of buffer. Stored bit + /// positions and lengths are `usize`-denominated throughout the crate, so + /// a stream past that bound has no in-memory form here: adopting it would + /// make [`len`](Self::len) and every walk incorrect, and this check keeps + /// the failure at the door instead. On 64-bit targets the bound is 2^61 + /// bytes, which no allocator can hand over, so the check is dead there. pub(crate) fn from_canonical(bytes: Bytes) -> Self { + assert!( + bytes.len() as u128 <= (usize::MAX as u128 + 1) / 8, + "stored streams denominate bit positions in usize: \ + a {}-byte buffer's bits do not fit this target's usize", + bytes.len(), + ); let bits = Bits { bytes }; debug_assert!( padding_is_canonical(&bits), @@ -206,6 +222,21 @@ impl Bits { /// Read the frozen stream as live bits — the padding stays behind the view: /// every cursor and walk consumes [`Bits`] through this slice, exactly as it /// consumes a [`BitsMut`]. +/// +/// # Panics +/// +/// Panics inside `bitvec` when the buffer's bit count exceeds the borrowed +/// view's encoding limit, `usize::MAX >> 3` bits — on a 32-bit target, any +/// stored stream past 64 MiB. Such streams exist there: the byte decode +/// doors admit valid encodings up to the `usize` bit-position bound (512 +/// MiB), and every byte-level operation on them (equality, hashing, the +/// canonical bytes, re-encoding, the length) is exact — but the walk surface +/// reads through this one borrowed view, whose fat-pointer length field is +/// three bits narrower than `usize`, so a walk over such a value fails loudly +/// here rather than reading a mis-lengthed view. Widening the walk surface +/// past the borrowed-view encoding requires a crate-owned view type in place +/// of the `bitvec` borrow. On 64-bit targets the limit is 2^58 bytes, +/// unreachable by allocation. impl Deref for Bits { type Target = BitsSlice; fn deref(&self) -> &BitsSlice { @@ -225,7 +256,10 @@ impl PartialEq for Bits { impl Eq for Bits {} /// Borrow bytes as an MSB-first bit stream without first copying them into a -/// [`BitsMut`]. +/// [`BitsMut`]: the meter surface's and the test suites' view — production +/// decode walks raw bytes instead, since this view's encoding caps below the +/// buffer sizes the decode doors admit on 32-bit targets. +#[cfg(any(test, feature = "meter"))] pub(crate) fn bytes_as_bits(bytes: &[u8]) -> &BitsSlice { bytes.view_bits::() } @@ -342,14 +376,19 @@ pub(crate) fn byte_view(bits: &BitsSlice) -> Option<(&[u8], Option)> { } } -/// Require that the bits from `pos` onward are exactly the canonical padding: -/// one `1` marker bit, then zeros to the byte boundary. +/// Require that the bits from `pos` to the buffer's end are exactly the +/// canonical padding: one `1` marker bit, then zeros to the byte boundary. +/// The byte decode doors' padding judge, over the raw stream bytes. /// -/// A canonical encoding pads with a single marker and at most 7 zeros, all -/// inside the final byte, so an intact remainder here is 1 to 8 bits: a `1`, -/// then zeros. (A stream whose live bits end flush against a byte boundary -/// carries its marker in a whole final `1000_0000` byte.) The rejections -/// split by genre: +/// Positions are `u64` because a door walks the whole byte buffer as bits and +/// `8·bytes.len()` itself can exceed a 32-bit `usize`; the raw-byte form is +/// also what lets the doors admit buffers past the borrowed bit view's +/// encoding cap (see [`Bits`]'s deref). A canonical encoding pads with a +/// single marker and at most 7 zeros, all inside the final byte, so an intact +/// remainder here is 1 to 8 bits — a `1`, then zeros — decided by one mask +/// compare on the final byte. (A stream whose live bits end flush against a +/// byte boundary carries its marker in a whole final `1000_0000` byte.) The +/// rejections split by genre: /// /// - An empty remainder is [`Decode::Truncated`]: the input ends where the /// padding should begin — a flush stream cut before its whole marker byte — @@ -362,10 +401,35 @@ pub(crate) fn byte_view(bits: &BitsSlice) -> Option<(&[u8], Option)> { /// The marker plus the length bound are what make `decode` injective on /// bytes: every stream has exactly one padded spelling, and no byte string /// spells two streams. -pub(crate) fn require_marker_padding(bits: &BitsSlice, pos: usize) -> Result<(), Decode> { - match bits.len() - pos { +/// +/// # Panics +/// +/// `pos` must be at or before the buffer's end in bits (it is a walk's end +/// position over this very buffer). +pub(crate) fn require_marker_padding_bytes(bytes: &[u8], pos: u64) -> Result<(), Decode> { + let total = bytes.len() as u64 * 8; + assert!( + pos <= total, + "padding checked at a position inside the buffer" + ); + let remainder = total - pos; + match remainder { 0 => Err(Decode::Truncated), - 1..=8 if bits[pos] && !bits[pos + 1..].any() => Ok(()), + 1..=8 => { + // The remainder lives entirely in the final byte: its low + // `remainder` bits must be a `1` followed by zeros. + let last = bytes[bytes.len() - 1]; + let mask = if remainder == 8 { + 0xFF + } else { + (1u8 << remainder) - 1 + }; + if last & mask == 1 << (remainder - 1) { + Ok(()) + } else { + Err(Decode::TrailingBits) + } + } _ => Err(Decode::TrailingBits), } } diff --git a/crates/before/src/codec/dsi.rs b/crates/before/src/codec/dsi.rs index efa8b1f5..8e9358aa 100644 --- a/crates/before/src/codec/dsi.rs +++ b/crates/before/src/codec/dsi.rs @@ -52,9 +52,15 @@ use super::{Base, BitCursor, BitsSlice, Int}; pub(crate) struct DsiCursor<'a> { reader: BufBitReader>, /// The position immediately after the last live bit read. - position: usize, - /// The stream's live bit length. - len: usize, + /// + /// `u64`, not `usize`: the byte decode doors walk a whole input buffer + /// as bits, and `8 · bytes.len()` can exceed a 32-bit `usize` (a 600 + /// MiB buffer holds 2^32+ bit positions) while remaining exactly + /// representable here. Slice-backed walks never leave `usize` range — + /// the borrowed view's own encoding bounds them. + position: u64, + /// The stream's live bit length, in the same `u64` denomination. + len: u64, } impl<'a> DsiCursor<'a> { @@ -84,6 +90,53 @@ impl<'a> DsiCursor<'a> { let Some((body, tail)) = super::byte_view(bits) else { unreachable!("stored streams start on a byte boundary") }; + Self::from_parts(body, tail, bits.len() as u64, pos) + } + + /// Open a cursor over the whole bit view of raw stream bytes — all + /// `8 · bytes.len()` of them, padding included: the byte decode doors' + /// entry, which must walk buffers whose borrowed bit view the `bitvec` + /// encoding cannot represent (64 MiB and up on a 32-bit target). + /// + /// The door semantics are exactly the slice walk's over a whole padded + /// buffer: padding bits are data to the walk, and the door's marker + /// check afterwards judges the remainder. + pub(crate) fn over_bytes(bytes: &'a [u8]) -> Self { + Self::from_parts(bytes, None, bytes.len() as u64 * 8, 0) + } + + /// Open a cursor over the first `live` bits of raw stream bytes: the + /// byte decode doors' form of a validated-prefix walk (the span door + /// re-walks the meet it just validated), for buffers past the borrowed + /// view's encoding cap. + /// + /// The partial tail byte's dead bits are masked to zero, exactly as the + /// borrowed view's domain masks them: the reader's phantom bits past + /// the live length must read zero so an apparent unary run can only + /// lengthen past `len` (and reject there), never terminate early. + /// + /// # Panics + /// + /// `live` must be at most `8 · bytes.len()`. + pub(crate) fn over_bytes_live(bytes: &'a [u8], live: usize) -> Self { + assert!( + live as u64 <= bytes.len() as u64 * 8, + "live bits within the buffer" + ); + let (body, tail) = if live.is_multiple_of(8) { + (&bytes[..live / 8], None) + } else { + ( + &bytes[..live / 8], + Some(bytes[live / 8] & !(0xFF >> (live % 8))), + ) + }; + Self::from_parts(body, tail, live as u64, 0) + } + + /// The one constructor body: a word source over `(body, tail)` starting + /// at `pos`, bounded by `len` live bits. + fn from_parts(body: &'a [u8], tail: Option, len: u64, pos: usize) -> Self { let mut reader = BufBitReader::new(ByteWords::new(body, tail, pos / 8)); let skip = pos % 8; if skip != 0 { @@ -93,11 +146,18 @@ impl<'a> DsiCursor<'a> { } DsiCursor { reader, - position: pos, - len: bits.len(), + position: pos as u64, + len, } } + /// The position immediately after the last bit read, in the cursor's own + /// `u64` denomination: the byte decode doors' end-position read, exact + /// even where the walked buffer holds more bit positions than `usize`. + pub(crate) fn position_u64(&self) -> u64 { + self.position + } + /// Read the unary prefix at the cursor without recording a successful run: /// the count of `0` bits before (and consuming) the terminating `1`. /// @@ -106,11 +166,13 @@ impl<'a> DsiCursor<'a> { /// byte's dead bits and zero-fills past the stream) can only /// lengthen an apparent prefix past `len`, never terminate one /// early. - fn unary_raw(&mut self) -> Result { + fn unary_raw(&mut self) -> Result { match self.reader.read_unary() { Err(_) => Err(self.truncated()), Ok(k) => { - let k = k as usize; + // No overflow: position and len are at most 8 · a buffer's + // byte count and k is bounded by the word source's total + // bits, all far below 2^64. if self.position + k + 1 > self.len { return Err(self.truncated()); } @@ -128,7 +190,7 @@ impl<'a> DsiCursor<'a> { /// cursor parks at the live length, where the per-bit loop's failing read /// leaves its own cursor. fn truncated(&mut self) -> Truncated { - super::scan::record_bits(self.len - self.position); + super::scan::record_bits_u64(self.len - self.position); self.position = self.len; Truncated } @@ -149,13 +211,13 @@ impl<'a> DsiCursor<'a> { } let mut remaining = k; while remaining > 0 { - let chunk = remaining.min(u64::BITS as usize); + let chunk = remaining.min(u64::from(u64::BITS)); self.reader - .skip_bits(chunk) + .skip_bits(chunk as usize) .expect("the mantissa was proven to fit the live length"); remaining -= chunk; } - super::scan::record_bits(code_len); + super::scan::record_bits_u64(code_len); self.position += code_len; Ok(()) } @@ -181,14 +243,25 @@ impl BitCursor for DsiCursor<'_> { } fn position(&self) -> usize { - self.position + // Exact for every walk that reads positions through the trait: + // slice-backed cursors are bounded by the borrowed view (below + // usize bits by its encoding), and the byte decode doors read + // their end positions through `position_u64` instead. A byte-door + // walk parking past usize (its truncation reject on a buffer past + // the storable bound) is never followed by a trait position read. + usize::try_from(self.position).expect("a walked position fits usize") } fn read_unary(&mut self) -> Result { let k = self.unary_raw()?; - super::scan::record_bits(k + 1); + super::scan::record_bits_u64(k + 1); self.position += k + 1; - Ok(k) + // A run at or past usize bits (possible only on a 32-bit target, + // inside a multi-part door buffer past 512 MiB) can only belong to + // a stream past the storable bound — its part could never freeze — + // so the checked conversion fails loudly there instead of handing + // the walk a truncated count. + Ok(usize::try_from(k).expect("a unary run fits usize")) } /// Read one Elias-gamma-coded integer: accepting and rejecting on exactly @@ -215,10 +288,10 @@ impl BitCursor for DsiCursor<'_> { /// the per-bit loop does. fn read_int(&mut self) -> Result { // Table tier: only when the peeked 9 bits are all live. - if self.len - self.position >= gamma_tables::READ_BITS { + if self.len - self.position >= gamma_tables::READ_BITS as u64 { if let Some((value, used)) = gamma_tables::read_table_be(&mut self.reader) { super::scan::record_bits(used); - self.position += used; + self.position += used as u64; return Ok(Int::Small(value)); } } @@ -233,19 +306,23 @@ impl BitCursor for DsiCursor<'_> { self.truncated(); return Err(Decode::Truncated); } - if k < u64::BITS as usize { + if k < u64::from(u64::BITS) { let rest = self .reader - .read_bits(k) + .read_bits(k as usize) .expect("the mantissa was proven to fit the live length"); let m = (1u64 << k) | rest; - super::scan::record_bits(code_len); + super::scan::record_bits_u64(code_len); self.position += code_len; return Ok(Int::Small(m - 1)); } // Wide arm: the mantissa's top bit is at position `k`; the next `k` // stream bits fill positions `k - 1 ..= 0`, most-significant first, - // read in machine-word chunks. + // read in machine-word chunks. The bit-index conversion is checked: + // a mantissa at or past usize bits (a 32-bit target, a code deeper + // than the storable bound) fails loudly instead of aliasing a low + // bit index — and the value's backend caps below that width anyway. + let k = usize::try_from(k).expect("a mantissa width fits usize"); let mut m = UBig::ZERO; m.set_bit(k); let mut remaining = k; @@ -266,7 +343,7 @@ impl BitCursor for DsiCursor<'_> { // wide fallback records. #[cfg(feature = "limb-meter")] super::limb_meter::record_wide(&m); - super::scan::record_bits(code_len); + super::scan::record_bits_u64(code_len); self.position += code_len; Ok(Int::from_base(Base::from(m - 1u32))) } diff --git a/crates/before/src/codec/gamma.rs b/crates/before/src/codec/gamma.rs index 5f0ab28a..a33fdba7 100644 --- a/crates/before/src/codec/gamma.rs +++ b/crates/before/src/codec/gamma.rs @@ -121,7 +121,7 @@ pub(crate) fn decode_int(bits: &BitsSlice, pos: usize) -> Result<(Base, usize), } /// The number of bits a [`decode_int_window`] window holds. -const WINDOW_BITS: usize = u64::BITS as usize; +const WINDOW_BITS: u64 = u64::BITS as u64; /// One-window fast path of the gamma decoder: the value and end position of the /// code at `pos`, when a single 64-bit window proves the whole code. @@ -150,10 +150,29 @@ const WINDOW_BITS: usize = u64::BITS as usize; /// the apparent prefix — pushing `2k+1` past the proven bits and into the /// fallback — never shortens it into a bogus accept. pub(crate) fn decode_int_window(bits: &BitsSlice, pos: usize) -> Option<(u64, usize)> { + let (body, tail) = super::byte_view(bits)?; + let (n, next) = window_int(body, tail, bits.len() as u64, pos as u64)?; + // The code ends inside the slice, whose own length bounds the position. + Some((n, next as usize)) +} + +/// [`decode_int_window`] over raw stream bytes: the wire-side reader's form +/// (its buffered bytes have no borrowed bit view once they outgrow the view +/// encoding's cap), windowing the whole `8 · bytes.len()`-bit view at the +/// reader's own `u64` position width. +#[cfg(any(test, feature = "borsh"))] +pub(crate) fn decode_int_window_bytes(bytes: &[u8], pos: u64) -> Option<(u64, u64)> { + window_int(bytes, None, bytes.len() as u64 * 8, pos) +} + +/// The one-window decoder's body over raw parts: `len` live bits across +/// `body` plus the masked partial `tail` byte, positions in `u64` (a byte +/// door's whole-buffer view holds more bit positions than a 32-bit `usize`). +fn window_int(body: &[u8], tail: Option, len: u64, pos: u64) -> Option<(u64, u64)> { // Bits of real stream between `pos` and the window's end. - let proven = bits.len().checked_sub(pos)?.min(WINDOW_BITS); - let window = load_window(bits, pos)?; - let k = window.leading_zeros() as usize; + let proven = len.checked_sub(pos)?.min(WINDOW_BITS); + let window = load_window(body, tail, pos); + let k = u64::from(window.leading_zeros()); let code_len = 2 * k + 1; if code_len > proven { return None; @@ -164,39 +183,37 @@ pub(crate) fn decode_int_window(bits: &BitsSlice, pos: usize) -> Option<(u64, us Some((m - 1, pos + code_len)) } -/// Load a 64-bit big-endian window of `bits` starting at bit `pos`: bit `pos` -/// of the stream in the most significant position, zero past the stream's end. -/// -/// `None` when the slice does not begin on a byte boundary of its own backing -/// store, the one shape with no direct byte view. Every decode surface hands in -/// a whole stored stream (offsets travel as `pos`), so this fallback is latent, -/// kept for correctness rather than reached in practice. -fn load_window(bits: &BitsSlice, pos: usize) -> Option { - let (body, tail) = super::byte_view(bits)?; - let byte = pos / 8; - let shift = pos % 8; +/// Load a 64-bit big-endian window starting at bit `pos` of the stream held +/// as `body` plus the masked partial `tail` byte: bit `pos` in the most +/// significant position, zero past the stream's end. +fn load_window(body: &[u8], tail: Option, pos: u64) -> u64 { + // In-range byte indices fit `usize` (they index an allocated buffer); + // the clamps below keep every computed index in range. + let byte = usize::try_from(pos / 8).unwrap_or(usize::MAX); + let shift = (pos % 8) as usize; // Gather the (up to) 9 bytes covering bits `pos..pos + 64`: 8 whole bytes // plus the partial ninth that a mid-byte `pos` shifts in. Bytes past the - // stream stay zero — `load_value` masks the tail byte's dead bits, and the - // buffer zero-fills past the last byte — so phantom bits are always zero. + // stream stay zero — the tail byte arrives with its dead bits masked, and + // the buffer zero-fills past the last byte — so phantom bits are always + // zero. let mut buf = [0u8; 9]; let start = byte.min(body.len()); - let end = (byte + buf.len()).min(body.len()); + let end = byte.saturating_add(buf.len()).min(body.len()); buf[..end - start].copy_from_slice(&body[start..end]); if let Some(t) = tail { - // `pos <= bits.len()` (checked by the caller) puts `byte` at or before - // the tail byte, so the index never underflows. + // The callers bound `pos` by the live length, which puts `byte` at or + // before the tail byte, so the index never underflows. let tail_at = body.len(); - if tail_at < byte + buf.len() { + if tail_at < byte.saturating_add(buf.len()) { buf[tail_at - byte] = t; } } let word = u64::from_be_bytes(buf[..8].try_into().expect("buf holds 8 whole bytes")); - Some(if shift == 0 { + if shift == 0 { word } else { (word << shift) | (u64::from(buf[8]) >> (8 - shift)) - }) + } } /// Read one Elias-gamma-coded integer from a sequential bit cursor. diff --git a/crates/before/src/codec/scan.rs b/crates/before/src/codec/scan.rs index ac1a5142..3a8c32e8 100644 --- a/crates/before/src/codec/scan.rs +++ b/crates/before/src/codec/scan.rs @@ -57,3 +57,15 @@ pub(crate) fn record_bits(n: usize) { #[cfg(not(feature = "scan-meter"))] let _ = n; } + +/// [`record_bits`] at the counter's own width, for the word-parallel cursor: +/// a byte decode door's walk spans up to `8 · bytes.len()` bit positions, +/// which exceeds a 32-bit `usize` on the largest buffers a door admits, so +/// its run and tail records stay `u64` end to end. +#[inline(always)] +pub(crate) fn record_bits_u64(n: u64) { + #[cfg(feature = "scan-meter")] + counter::record(n); + #[cfg(not(feature = "scan-meter"))] + let _ = n; +} diff --git a/crates/before/src/codec/tree.rs b/crates/before/src/codec/tree.rs index 71af3cd7..fe7776fa 100644 --- a/crates/before/src/codec/tree.rs +++ b/crates/before/src/codec/tree.rs @@ -35,8 +35,31 @@ pub(crate) fn parse_id(bits: &BitsSlice, pos: usize) -> Result { parse_id_from(&mut cursor) } -/// Parse and validate one id tree from a sequential bit cursor. +/// [`parse_id`] over raw stream bytes, walking the whole `8 · bytes.len()` +/// bit view: the byte decode doors' form, for buffers past the borrowed bit +/// view's encoding cap (64 MiB and up on a 32-bit target), returning the +/// tree's end position at the walk's own `u64` width. +pub(crate) fn parse_id_bytes(bytes: &[u8]) -> Result { + let mut cursor = super::DsiCursor::over_bytes(bytes); + parse_id_core(&mut cursor)?; + Ok(cursor.position_u64()) +} + +/// Parse and validate one id tree from a sequential bit cursor, returning the +/// position just past it. pub(crate) fn parse_id_from(cursor: &mut C) -> Result +where + Decode: From, +{ + parse_id_core(cursor)?; + Ok(cursor.position()) +} + +/// Parse and validate one id tree from a sequential bit cursor: the one +/// grammar body, leaving the end position to the caller's own read (the +/// byte decode doors read it at `u64` width, everything else through +/// [`BitCursor::position`]). +pub(crate) fn parse_id_core(cursor: &mut C) -> Result<(), Decode> where Decode: From, { @@ -62,7 +85,7 @@ where // Attach the completed subtree to its parent, possibly completing it too. loop { match stack.pop() { - None => return Ok(cursor.position()), // the root is complete + None => return Ok(()), // the root is complete Some(IdFrame::BothNeedLeft) => { stack.push(IdFrame::BothNeedRight { left_terminal: summary, diff --git a/crates/before/src/party.rs b/crates/before/src/party.rs index 861abf1e..f75c6f04 100644 --- a/crates/before/src/party.rs +++ b/crates/before/src/party.rs @@ -578,10 +578,13 @@ impl Party { pub fn decode(mut reader: R) -> Result { let mut buf = Vec::new(); reader.read_to_end(&mut buf).map_err(Decode::Io)?; + // Validate over the raw bytes (never a borrowed bit view, whose + // encoding caps below the buffer sizes this door admits on 32-bit + // targets): the whole `8 · buf.len()`-bit view is the walk's input, + // padding bits included, and the marker check judges the remainder. { - let bits = codec::bytes_as_bits(&buf); - let end = codec::parse_id(bits, 0)?; - codec::require_marker_padding(bits, end)?; + let end = codec::parse_id_bytes(&buf)?; + codec::require_marker_padding_bytes(&buf, end)?; } // Adopt the read buffer as the result's backing store without // copying: the padding check proved the buffer is the stream's one diff --git a/crates/before/src/span/wire.rs b/crates/before/src/span/wire.rs index c925d7bc..8047ad8f 100644 --- a/crates/before/src/span/wire.rs +++ b/crates/before/src/span/wire.rs @@ -128,24 +128,35 @@ impl<'a> Span<'a> { // The pair verdict is pronounced last, after the padding check, so a // composite defective several ways rejects by its structural genre // first, exactly as decoding the components would. + // Both components validate over the raw bytes (never a borrowed bit + // view, whose encoding caps below the buffer sizes this door admits + // on 32-bit targets); each walk's input is its component's whole + // byte range as bits, padding included, judged by its marker check. let (lo_bytes, admission) = { - let bits = codec::bytes_as_bits(&buf); - let lo_end = skyline::validate_prefix(bits)?; + let lo_end = skyline::validate_prefix_bytes(&buf)?; // The meet's padding marker sits in its final byte — which an // input cut right after a flush stream lacks. That cut is // missing required data (the marker byte, and the whole join // after it): the truncation genre, exactly as a byte-starved // reader reports the same boundary. let lo_bytes = (lo_end + 1).div_ceil(8); - if 8 * lo_bytes > bits.len() { + if lo_bytes > buf.len() as u64 { return Err(Decode::Truncated); } - codec::require_marker_padding(&bits[..8 * lo_bytes], lo_end)?; - let tail = &bits[8 * lo_bytes..]; - let mut cursor = codec::DsiCursor::new(tail); - let admission = skyline::validate_dominating_from(&bits[..lo_end], &mut cursor)?; - let hi_end = codec::BitCursor::position(&cursor); - codec::require_marker_padding(tail, hi_end)?; + let lo_bytes = + usize::try_from(lo_bytes).expect("the meet's prefix ends within the read buffer"); + codec::require_marker_padding_bytes(&buf[..lo_bytes], lo_end)?; + // The meet re-walks beside the join's parse; its live length is + // its own validated end, below `usize` wherever its stream is + // storable at all (the adopting container's bit positions are + // usize-denominated). + let lo_live = usize::try_from(lo_end).expect("a storable meet's bit length fits usize"); + let tail = &buf[lo_bytes..]; + let mut cursor = codec::DsiCursor::over_bytes(tail); + let admission = + skyline::validate_dominating_bytes(&buf[..lo_bytes], lo_live, &mut cursor)?; + let hi_end = cursor.position_u64(); + codec::require_marker_padding_bytes(tail, hi_end)?; if admission == skyline::Admission::Refuted { return Err(Decode::NotCanonical); } diff --git a/crates/before/src/version.rs b/crates/before/src/version.rs index d4224157..a8a3d1bc 100644 --- a/crates/before/src/version.rs +++ b/crates/before/src/version.rs @@ -1062,10 +1062,13 @@ impl Version { pub fn decode(mut reader: R) -> Result { let mut buf = Vec::new(); reader.read_to_end(&mut buf).map_err(Decode::Io)?; + // Validate over the raw bytes (never a borrowed bit view, whose + // encoding caps below the buffer sizes this door admits on 32-bit + // targets): the whole `8 · buf.len()`-bit view is the walk's input, + // padding bits included, and the marker check judges the remainder. { - let bits = codec::bytes_as_bits(&buf); - let end = skyline::validate_prefix(bits)?; - codec::require_marker_padding(bits, end)?; + let end = skyline::validate_prefix_bytes(&buf)?; + codec::require_marker_padding_bytes(&buf, end)?; } // Adopt the read buffer as the result's backing store without // copying: the padding check proved the buffer is the stream's one diff --git a/crates/before/src/version/skyline.rs b/crates/before/src/version/skyline.rs index 76bd3692..6f2f611a 100644 --- a/crates/before/src/version/skyline.rs +++ b/crates/before/src/version/skyline.rs @@ -204,14 +204,18 @@ pub(crate) mod web_traffic; #[cfg(test)] mod tests; -pub(crate) use admit::{validate_dominating_from, Admission}; +pub(crate) use admit::{validate_dominating_bytes, Admission}; +// The slice-form admission walk serves the wire-side (borsh) span reader, +// whose meet is an already-stored version read through its borrowed view. +#[cfg(any(test, feature = "borsh"))] +pub(crate) use admit::validate_dominating_from; #[cfg(any(test, feature = "meter"))] pub(crate) use decode::decode_bits; #[cfg(any(test, feature = "meter"))] pub(crate) use encode::encode_bits; #[cfg(any(test, feature = "meter"))] pub(crate) use validate::validate_bits; -pub(crate) use validate::validate_prefix; +pub(crate) use validate::validate_prefix_bytes; // The wire decoder's from-cursor entry: reached from the borsh event leg. #[cfg(feature = "borsh")] pub(crate) use validate::validate_from; diff --git a/crates/before/src/version/skyline/admit.rs b/crates/before/src/version/skyline/admit.rs index e9e718f8..655963d9 100644 --- a/crates/before/src/version/skyline/admit.rs +++ b/crates/before/src/version/skyline/admit.rs @@ -50,7 +50,9 @@ use core::cmp::Ordering; use suanpan::Accumulator; -use crate::codec::{BitCursor, BitsMut, BitsSlice, Int}; +#[cfg(any(test, feature = "borsh"))] +use crate::codec::BitsSlice; +use crate::codec::{BitCursor, BitsMut, Int}; use crate::error::Decode; use super::overlay::{fold, LeafCursor, PlateauCursor, Side, Step}; @@ -273,6 +275,7 @@ pub(crate) enum Admission { /// `lo` must be a canonical skyline stream — its cursor is the pair sweep's and /// shares [`causal_cmp`](super::sweep::causal_cmp)'s contract. The parsed /// stream needs no such trust; that is the point. +#[cfg(any(test, feature = "borsh"))] pub(crate) fn validate_dominating_from( lo: &BitsSlice, cursor: &mut C, @@ -280,7 +283,35 @@ pub(crate) fn validate_dominating_from( where Decode: From, { - let (mut lo_cur, lo_first) = LeafCursor::open(lo); + let (lo_cur, lo_first) = LeafCursor::open(lo); + validate_dominating_over(lo_cur, lo_first, cursor) +} + +/// [`validate_dominating_from`] with the canonical `lo` given as raw stream +/// bytes and its live bit length: the byte decode door's form, for meets +/// past the borrowed bit view's encoding cap (64 MiB and up on a 32-bit +/// target). Same contract in full. +pub(crate) fn validate_dominating_bytes( + lo: &[u8], + lo_live: usize, + cursor: &mut C, +) -> Result +where + Decode: From, +{ + let (lo_cur, lo_first) = LeafCursor::open_bytes(lo, lo_live); + validate_dominating_over(lo_cur, lo_first, cursor) +} + +/// The admission walk's body, generic over how `lo`'s cursor was opened. +fn validate_dominating_over( + mut lo_cur: LeafCursor<'_>, + lo_first: Int, + cursor: &mut C, +) -> Result +where + Decode: From, +{ let (mut hi_cur, hi_first) = CheckedCursor::open(cursor)?; // D = height_lo − height_hi: dominance survives while no elementary // interval reads D > 0 (the pair sweep's `lo <= hi` direction, `lo` as the diff --git a/crates/before/src/version/skyline/overlay.rs b/crates/before/src/version/skyline/overlay.rs index 79092578..47db9264 100644 --- a/crates/before/src/version/skyline/overlay.rs +++ b/crates/before/src/version/skyline/overlay.rs @@ -340,6 +340,20 @@ impl<'a> LeafCursor<'a> { (this, first) } + /// [`open`](Self::open) over raw stream bytes and a live bit length: the + /// byte decode door's form, for canonical streams past the borrowed bit + /// view's encoding cap (64 MiB and up on a 32-bit target). Same contract, + /// canonicality trust included. + pub(super) fn open_bytes(bytes: &'a [u8], live: usize) -> (Self, Int) { + let mut this = LeafCursor { + cursor: DsiCursor::over_bytes_live(bytes, live), + path: BitStack::new(), + len: live, + }; + let first = this.descend(); + (this, first) + } + /// The flip level the next step would close to, read without moving. /// /// The path's trailing right-branch run popped and the deepest left branch diff --git a/crates/before/src/version/skyline/validate.rs b/crates/before/src/version/skyline/validate.rs index 0c6bebd2..f3d2a5d3 100644 --- a/crates/before/src/version/skyline/validate.rs +++ b/crates/before/src/version/skyline/validate.rs @@ -24,7 +24,9 @@ use core::cmp::Ordering; use suanpan::Accumulator; -use crate::codec::{BitCursor, BitsMut, BitsSlice, DsiCursor}; +#[cfg(any(test, feature = "meter"))] +use crate::codec::BitsSlice; +use crate::codec::{BitCursor, BitsMut, DsiCursor}; use crate::error::Decode; use super::signed::{fold_signed_int, unzigzag, Sign}; @@ -47,16 +49,18 @@ pub(crate) fn validate_bits(bits: &BitsSlice) -> Result<(), Decode> { Ok(()) } -/// Strictly validate one skyline tree at the head of a bit stream, -/// returning the position just past it. +/// Strictly validate one skyline tree at the head of a raw byte buffer's +/// whole `8 · bytes.len()`-bit view, returning the position just past it. /// /// The wire decoder's entry: a version's skyline stream is bit-self-delimiting /// (one complete tree), so the returned end position is where any zero padding -/// must begin. -pub(crate) fn validate_prefix(bits: &BitsSlice) -> Result { - let mut cursor = DsiCursor::new(bits); +/// must begin. Raw bytes and a `u64` end, never a borrowed bit view: the +/// doors admit buffers past the view encoding's cap (64 MiB and up on a +/// 32-bit target), whose bit positions outgrow a 32-bit `usize`. +pub(crate) fn validate_prefix_bytes(bytes: &[u8]) -> Result { + let mut cursor = DsiCursor::over_bytes(bytes); validate_from(&mut cursor)?; - Ok(cursor.position()) + Ok(cursor.position_u64()) } /// Validate one skyline tree from a sequential bit cursor. diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs index adea2fb6..ffc3c492 100644 --- a/crates/before/wasm32-pins/harness/tests/pins.rs +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -39,44 +39,45 @@ fn version_decode_below_view_cap() { ); } -/// BAD BASELINE, pinned red: a valid 64 MiB (67108864-byte) version -/// encoding is SILENTLY rejected as a typed decode error on wasm32 — the -/// worst failure class, a wrong result with no alarm. The decode door -/// views the whole buffer as one bit slice; at exactly 2^29 bits, -/// `bitvec`'s element-count guard still admits the slice, and the span -/// encoding's `len << 3` then shifts the length's one set bit clean out -/// of a 32-bit `usize`, constructing an EMPTY view. The validator walks -/// zero bits and reports `Truncated` — a typed rejection, reserved for -/// invalid inputs, delivered for a valid one. The guest observes it as -/// its `-1` (decode returned `Err`) code. The door engineering flips -/// this to `Value(8 * 67_108_864 - 8)`. +/// A valid 64 MiB (67108864-byte) version encoding — exactly 2^29 bits, +/// the size whose whole-buffer borrowed bit view `bitvec`'s span encoding +/// silently constructs EMPTY on wasm32 — decodes correctly through the +/// byte-backed door, returning its exact live bit length. (The silent +/// empty view was this pin's committed bad baseline: the validator walked +/// zero bits and reported `Truncated` for a valid input, a wrong result +/// with no alarm.) #[test] fn version_decode_at_view_cap() { - assert_eq!(call1("pin_version_decode", 67_108_864), Outcome::Value(-1)); + assert_eq!( + call1("pin_version_decode", 67_108_864), + Outcome::Value(8 * 67_108_864 - 8), + ); } -/// BAD BASELINE, pinned red: one byte past the silent-empty boundary -/// (67108865 bytes, 2^29 + 8 bits), `bitvec`'s element-count guard fires -/// and the decode door panics — a valid encoding that fits the target's -/// memory, refused by panic. Together with the silent-empty pin one byte -/// below, this witnesses both failure genres the borrowed-view cap -/// produces on wasm32. The door engineering flips this to -/// `Value(8 * 67_108_865 - 8)`. +/// A valid 67108865-byte version encoding — one byte past the borrowed +/// view's silent-empty boundary, where `bitvec`'s element-count guard used +/// to panic the decode door on wasm32 — decodes correctly through the +/// byte-backed door, returning its exact live bit length. Together with +/// the pin one byte below, this witnesses both failure genres the +/// borrowed-view cap produced are gone from the doors. #[test] fn version_decode_past_view_cap() { - assert_eq!(call1("pin_version_decode", 67_108_865), PANICKED); + assert_eq!( + call1("pin_version_decode", 67_108_865), + Outcome::Value(8 * 67_108_865 - 8), + ); } /// BAD BASELINE, pinned red: a valid 512 MiB (2^29-byte) version encoding -/// panics on wasm32. Two seams stack here: today the trap fires in -/// `bitvec`'s view cap (the 64 MiB seam masks everything above it), and -/// once that is engineered around, `Bits::len`'s `bytes.len() * 8` — a -/// `usize` multiply — overflows at exactly this size (2^32 bits). This -/// build runs with overflow checks, so the wrap is this same trap; in an -/// unchecked build it wraps silently, coincidentally correct at exactly -/// 2^29 bytes and wrong (a slice length 2^32 short) for anything larger. -/// The len fix flips this to `Value(8 * 536_870_912 - 8)` — the largest -/// stored stream a 32-bit `usize` can denominate bit positions for. +/// panics on wasm32 — now unmasked to the seam this size owns. The +/// byte-backed door walks and validates the stream correctly, and the trap +/// fires at `Bits::len`'s `bytes.len() * 8`: a `usize` multiply that +/// overflows at exactly this size (2^32 bits). This build runs with +/// overflow checks, so the wrap is this trap; in an unchecked build it +/// wraps silently, coincidentally correct at exactly 2^29 bytes and wrong +/// (a length 2^32 short) for anything larger. The len fix flips this to +/// `Value(8 * 536_870_912 - 8)` — the largest stored stream a 32-bit +/// `usize` can denominate bit positions for. #[test] fn version_decode_at_bit_length_boundary() { assert_eq!(call1("pin_version_decode", 536_870_912), PANICKED); From 1c47d7c8648917947e59957824dd7e716aaad2e1 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 14:31:51 -0400 Subject: [PATCH 03/20] before: compute Bits::len at u64 width, checked back to usize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bytes.len() * 8 is a usize multiply: on a 32-bit target it overflows at exactly 512 MiB of buffer (2^32 bits) — a debug panic, and in release a silent wrap that is coincidentally correct at exactly 2^29 bytes and wrong (a length 2^32 short) for anything larger. The live length itself still fits usize at that boundary, because the marker spends at least one of the buffer's bits: 2^32 - 1 - trailing_zeros. So the arithmetic runs at u64 width and converts back checked, with the doc carrying the derivation for why the conversion cannot fail on a constructed stream (from_canonical asserts the storable bound; freeze's build buffer is bounded far below it). The wasm32 pin flips: a valid 512 MiB version encoding — the largest stored stream a 32-bit usize can denominate bit positions for — now decodes on wasm32 with its exact live bit length, one marker byte shy of usize::MAX. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/src/codec/bits.rs | 12 ++++++++- .../before/wasm32-pins/harness/tests/pins.rs | 25 +++++++++++-------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/before/src/codec/bits.rs b/crates/before/src/codec/bits.rs index a7d14bbd..f217f057 100644 --- a/crates/before/src/codec/bits.rs +++ b/crates/before/src/codec/bits.rs @@ -170,12 +170,22 @@ impl Bits { /// `trailing_zeros` over the final byte: `O(1)`, no walk. The one storage /// invariant this rests on — a nonempty buffer's final byte is nonzero — is /// exactly what the freeze and decode doors establish. + /// + /// The arithmetic runs at `u64` width: `bytes.len() * 8` wraps a 32-bit + /// `usize` from 512 MiB of buffer, while the live length itself still + /// fits at exactly that boundary (the marker spends at least one of the + /// buffer's bits). The final conversion is checked, not truncating, and + /// cannot fail on a constructed stream: every door bounds its buffer so + /// the live length fits `usize` ([`from_canonical`](Self::from_canonical) + /// asserts it; [`freeze`](Self::freeze)'s build buffer is bounded far + /// below it by the borrowed view's own encoding). pub fn len(&self) -> usize { match self.bytes.last() { None => 0, Some(&last) => { debug_assert!(last != 0, "stored stream missing its padding marker"); - self.bytes.len() * 8 - 1 - last.trailing_zeros() as usize + let bits = self.bytes.len() as u64 * 8 - 1 - u64::from(last.trailing_zeros()); + usize::try_from(bits).expect("a constructed stream's live length fits usize") } } } diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs index ffc3c492..41022c09 100644 --- a/crates/before/wasm32-pins/harness/tests/pins.rs +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -68,19 +68,22 @@ fn version_decode_past_view_cap() { ); } -/// BAD BASELINE, pinned red: a valid 512 MiB (2^29-byte) version encoding -/// panics on wasm32 — now unmasked to the seam this size owns. The -/// byte-backed door walks and validates the stream correctly, and the trap -/// fires at `Bits::len`'s `bytes.len() * 8`: a `usize` multiply that -/// overflows at exactly this size (2^32 bits). This build runs with -/// overflow checks, so the wrap is this trap; in an unchecked build it -/// wraps silently, coincidentally correct at exactly 2^29 bytes and wrong -/// (a length 2^32 short) for anything larger. The len fix flips this to -/// `Value(8 * 536_870_912 - 8)` — the largest stored stream a 32-bit -/// `usize` can denominate bit positions for. +/// A valid 512 MiB (2^29-byte) version encoding — the largest stored +/// stream a 32-bit `usize` can denominate bit positions for — decodes +/// correctly on wasm32, returning its exact live bit length (2^32 - 8, +/// within one marker byte of `usize::MAX`). This was the `Bits::len` +/// seam's pin: `bytes.len() * 8` as a `usize` multiply overflowed at +/// exactly this size — a trap under this build's overflow checks, a +/// silent wrap coincidentally correct at 2^29 bytes and wrong above it +/// in unchecked builds — masked by the borrowed-view cap until the doors +/// went byte-backed, and cured by `u64` arithmetic with a checked final +/// conversion. #[test] fn version_decode_at_bit_length_boundary() { - assert_eq!(call1("pin_version_decode", 536_870_912), PANICKED); + assert_eq!( + call1("pin_version_decode", 536_870_912), + Outcome::Value(8 * 536_870_912 - 8), + ); } /// Adjacency, green side: a valid rank whose fraction is 2^32 - 32 From 65eaf9f162ede3328bdf7a2c99b247c04b59a9f1 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 14:38:54 -0400 Subject: [PATCH 04/20] before: assemble the rank decoder's numerator from bytes, never a shift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The decoder rebuilt the numerator as (integral << exp) | (groups >> pad). Two 32-bit seams lived in that line, both reachable from valid ~604 MB inputs the address space holds: Base's Shl converts the shift amount to the backend's usize, which fails at exp >= 2^32; and below it, dashu's from_be_bytes sizes its buffer from the input's byte count — leading zero bytes included — so 536870911 fraction bytes overran the backend's 32-bit MAX_CAPACITY by one word even when the numerator's value fits. The cure assembles bytes: num · 2^pad = integral · 2^(8·groups) + G, so the numerator is the concatenation of the integral's minimal big-endian bytes with the fraction groups, shifted right by the sub-byte pad (< 8, in usize range on every target), leading zero bytes stripped so the backend's buffer pays for value, not for zeros. No value-width shift exists on the decode path at all. The encode side gets the dual guard: the biased integral is decided by width (exp >= bits(num) means zero) before any shift is materialized, so re-encoding a fraction-heavy rank works at the same scales its decode admits. Base's u64 shift forms are re-derived honestly: Shr is now total (an amount at or past the value's width yields zero, so amounts past usize clamp, value-preserving); Shl keeps its checked conversion with the corrected rationale — on 32-bit it fires exactly where the shifted result exceeds the backend's representable width, the same values the backend's own capacity assert bounds, so both name an unrepresentable result loudly (the earlier could-not-be-allocated claim was false: a 512 MiB numerator allocates fine in a 4 GiB space). Base::to_be_bytes lands as from_be_bytes' width-metered dual. The rank Add/checked_sub alignment shifts and sum_ranks' accumulator shifts carry their reachability derivations inline: honest exponents stay orders of magnitude below every remaining bound. The wasm32 pins flip: both rank boundary pins (exp = 2^32 - 8 at the backend's byte-capacity seam, exp = 2^32 at the shift seam) now decode and order exactly; the adjacency pin below the backend cap stays green. Native rank suites: 40/40 unchanged. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/src/codec/base.rs | 36 ++++++++++--- crates/before/src/version/rank.rs | 54 +++++++++++++++++-- .../before/wasm32-pins/harness/tests/pins.rs | 46 ++++++++-------- 3 files changed, 106 insertions(+), 30 deletions(-) diff --git a/crates/before/src/codec/base.rs b/crates/before/src/codec/base.rs index f14e361b..274da3af 100644 --- a/crates/before/src/codec/base.rs +++ b/crates/before/src/codec/base.rs @@ -131,6 +131,19 @@ impl Base { self.0.to_le_bytes().into_vec() } + /// The magnitude's minimal big-endian bytes: empty for zero, no leading + /// zero byte otherwise. + /// + /// The materialization dual of [`from_be_bytes`](Self::from_be_bytes), + /// for byte-assembled values (the rank decoder concatenates an integral's + /// bytes with fraction groups instead of shifting by an exponent a 32-bit + /// `usize` cannot hold). Width-scale work, so the limb meter records the + /// operand's width. + pub(crate) fn to_be_bytes(&self) -> Vec { + meter_limbs_solo(self); + self.0.to_be_bytes().into_vec() + } + /// Assemble a magnitude from big-endian bytes. /// /// The materialization point for values parsed out of a bit stream @@ -430,17 +443,26 @@ impl Shr for Base { } // The u64 shift forms serve exponent-denominated callers (a `Rank`'s -// exponent is u64). A shift amount is realizable only when the shifted -// value fits the address space, so the conversion to the backend's -// usize is checked, not truncating: an amount past usize denotes a -// value that could not be allocated anyway. +// exponent is u64). The two directions part on totality. A left shift's +// checked conversion fails only for amounts at or past usize bits: never +// on 64-bit targets (the shifted value would dwarf the address space +// first), and on 32-bit targets only where the shifted result exceeds +// the backend's representable width anyway (its buffer caps at +// usize::MAX / word-bits words), so the expect and the backend's own +// capacity assert bound the same values — results the dependency cannot +// hold, failing loudly by name instead of wrapping. A right shift is +// total: an amount at or past the value's width yields zero, and on a +// 32-bit target an amount past usize can only name that case (the +// value's width is capped below usize::MAX bits by the same backend +// bound), so the conversion clamps, value-preserving. impl Shl for Base { type Output = Base; fn shl(self, rhs: u64) -> Base { meter_limbs_shl(&self, rhs); - let rhs = usize::try_from(rhs).expect("shift amount fits the address space"); + let rhs = usize::try_from(rhs) + .expect("a left shift this wide exceeds the backend's representable width"); Base(self.0 << rhs) } } @@ -450,7 +472,9 @@ impl Shr for Base { fn shr(self, rhs: u64) -> Base { meter_limbs1(&self); - let rhs = usize::try_from(rhs).expect("shift amount fits the address space"); + // The clamp is exact, never a truncation: any amount at or past the + // value's width — everything past usize included — yields zero. + let rhs = usize::try_from(rhs).unwrap_or(usize::MAX); Base(self.0 >> rhs) } } diff --git a/crates/before/src/version/rank.rs b/crates/before/src/version/rank.rs index a16894d8..7d0233b3 100644 --- a/crates/before/src/version/rank.rs +++ b/crates/before/src/version/rank.rs @@ -293,6 +293,9 @@ impl Rank { Ordering::Less => None, Ordering::Equal => Some(Rank::ZERO), Ordering::Greater => { + // The alignment shifts are width-bounded on any addressable + // input (`Add`'s note carries the derivation), so the + // checked conversions inside `<<` stay dead. let e = self.exp.max(other.exp); let a = self.num.clone() << (e - self.exp); let b = other.num.clone() << (e - other.exp); @@ -497,8 +500,17 @@ impl Rank { /// no walk beyond the fold's own. pub(crate) fn encode_parts(num: &Base, exp: u64) -> Vec { // The integral part, biased so zero has a (smallest) codeword: - // m = ⌊r⌋ + 1, w = bits(m), ρ = bits(w) − 1. - let biased = (num.clone() >> exp) + 1u32; + // m = ⌊r⌋ + 1, w = bits(m), ρ = bits(w) − 1. Decided by width when the + // exponent covers the numerator whole — `⌊r⌋ = 0` with no shift + // materialized — because a fraction-heavy rank's `exp` outruns a 32-bit + // `usize` (from ~604 MB of decoded input) while the rank itself is + // representable; the shift arm's amount is then always below the + // numerator's width, which the backend bounds within `usize`. + let biased = if exp >= num.bits() { + Base::from(1u8) + } else { + (num.clone() >> exp) + 1u32 + }; let w = biased.bits(); let rho = u64::from(63 - w.leading_zeros()); let groups = exp.div_ceil(FRACTION_GROUP_BITS); @@ -659,7 +671,23 @@ pub(crate) fn decode_stream(next_byte: impl FnMut() -> Result) -> Re let num = if frac_len == 0 { integral } else { - (integral << exp) | (Base::from_be_bytes(&groups) >> pad) + // The numerator by byte assembly, never by a value-width shift: + // `num · 2^pad = integral · 2^(8·groups) + G` with `G` the groups' + // big-endian value, and the `pad` low bits shifted out are exactly + // the final group's trailing zeros — so `num` is the concatenated + // image's value shifted right by the sub-byte pad. The + // `integral << exp` spelling is not available at every scale this + // decoder accepts: on a 32-bit target `exp` outruns `usize` from + // ~604 MB of input while the numerator itself still fits the + // backend. Leading zero bytes are stripped before materializing + // because the backend sizes its buffer from the image's byte count, + // and a fraction opening with zero expansion bits would otherwise + // pay capacity for value it does not carry. + let mut image = integral.to_be_bytes(); + image.extend_from_slice(&groups); + drop(groups); + let lead = image.iter().take_while(|&&byte| byte == 0).count(); + Base::from_be_bytes(&image[lead..]) >> pad }; debug_assert!( exp == 0 || num.bit(0), @@ -781,6 +809,17 @@ impl PartialOrd for Rank { impl Add<&Rank> for &Rank { type Output = Rank; fn add(self, rhs: &Rank) -> Rank { + // The alignment shift materializes `num · 2^gap`, so its width is + // the aligned numerator's own: on a 32-bit target a gap at or past + // `usize` (2^32) names a result past the big-integer backend's + // representable width — the shift's checked conversion and the + // backend's capacity assert bound the same values and both fail + // loudly, never silently — while every gap below that fits the + // conversion. Exponents themselves stay far smaller on honest + // inputs: a decoded rank's exponent is counted from fraction bits + // actually read (under 2^35 from a whole 32-bit address space of + // input), and a version-derived exponent is bounded by its tree's + // stored bit length. let e = self.exp.max(rhs.exp); let a = self.num.clone() << (e - self.exp); let b = rhs.num.clone() << (e - rhs.exp); @@ -847,6 +886,15 @@ impl<'a> Sum<&'a Rank> for Rank { /// element, and the result is the identical [`Rank`] the pairwise fold produces /// (one exact value, one shared normalization at the end). fn sum_ranks, I: Iterator>(iter: I) -> Rank { + // The accumulator's shifted entry points document a panic at digit + // positions past `usize` (`shift / 32 > usize::MAX`, so from + // `shift = 2^37` on a 32-bit target). The exponent gaps fed here stay + // orders of magnitude below it on any addressable input: a decoded + // rank's exponent is counted from fraction bits actually read — under + // 2^35 even if a whole 32-bit address space were one fraction — and a + // version-derived exponent is bounded by its tree's stored bit length + // (under 2^32, the storage bound), so the documented panic is + // unreachable from this fold. let mut acc = Accumulator::new(); let mut exp = 0u64; for rank in iter { diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs index 41022c09..8607adfe 100644 --- a/crates/before/wasm32-pins/harness/tests/pins.rs +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -101,31 +101,35 @@ fn rank_decode_below_backend_capacity() { ); } -/// BAD BASELINE, pinned red: a valid rank whose fraction is 2^32 - 8 -/// expansion bits deep panics on wasm32 INSIDE the big-integer backend — -/// `dashu`'s `from_be_bytes` sizes its buffer from the input's byte count -/// (leading zero bytes included), and 536870911 fraction bytes need one -/// word more than the backend's 32-bit `MAX_CAPACITY`, even though the -/// numerator VALUE (which starts with 64 zero expansion bits) fits the -/// backend comfortably. This seam sits below the decoder's own -/// `usize::try_from` shift seam and partially masks it. The byte-assembly -/// fix strips leading zero bytes before materializing, flipping this to -/// `Value(0)`. +/// A valid rank whose fraction is 2^32 - 8 expansion bits deep decodes +/// correctly on wasm32 and orders exactly against reference ranks. This +/// pin's committed bad baseline was a panic INSIDE the big-integer +/// backend: `dashu`'s `from_be_bytes` sizes its buffer from the input's +/// byte count (leading zero bytes included), and 536870911 fraction bytes +/// needed one word more than the backend's 32-bit `MAX_CAPACITY` even +/// though the numerator VALUE (its fraction opens with 64 zero bits) fits +/// comfortably — a seam below the decoder's own shift seam, partially +/// masking it. The byte-assembly cure strips leading zero bytes before +/// materializing. #[test] fn rank_decode_at_backend_byte_capacity() { - assert_eq!(call1("pin_rank_decode", (1u64 << 32) - 8), PANICKED); + assert_eq!( + call1("pin_rank_decode", (1u64 << 32) - 8), + Outcome::Value(0), + ); } -/// BAD BASELINE, pinned red: a valid rank whose fraction is exactly 2^32 -/// expansion bits deep panics on wasm32 at `Base`'s `Shl` — the -/// decoder rebuilds the numerator as `integral << exp`, and the shift -/// amount's `usize::try_from` fails for `exp >= 2^32` on a 32-bit target -/// even though the input (~604 MB) and the decoded numerator (~512 MiB) -/// both fit the 4 GiB address space. Under the ruled contract this valid -/// encoding must decode; the byte-assembly fix (concatenate the -/// integral's bytes with the fraction groups instead of shifting, leading -/// zeros stripped) flips this to `Value(0)`. +/// A valid rank whose fraction is exactly 2^32 expansion bits deep — the +/// exponent one past wasm32's `usize` — decodes correctly and orders +/// exactly against reference ranks. This pin's committed bad baseline was +/// a panic at `Base`'s `Shl`: the decoder rebuilt the numerator as +/// `integral << exp`, whose shift amount's `usize::try_from` fails for +/// `exp >= 2^32` on a 32-bit target even though the input (~604 MB) and +/// the decoded numerator (~512 MiB) both fit the 4 GiB address space. The +/// cure assembles the numerator from bytes — the integral's bytes +/// concatenated with the fraction groups, leading zeros stripped — so no +/// value-width shift exists on the decode path at all. #[test] fn rank_decode_at_usize_exp_boundary() { - assert_eq!(call1("pin_rank_decode", 1u64 << 32), PANICKED); + assert_eq!(call1("pin_rank_decode", 1u64 << 32), Outcome::Value(0)); } From 9d38eef086e84c89dcd6abbcf1456985d57b37d1 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 14:41:44 -0400 Subject: [PATCH 05/20] wasm32-pins: state pin invariants in the present tense Test docs and driver prose narrated each pin's prior red state (the baseline it used to trap on, what cured it). Provenance lives in git: each doc now states the boundary the pin guards and the failure mode it forecloses, as counterfactuals about the present code, with the pins file's header naming the red-first discipline once. The trap-outcome plumbing stays (a future seam pins red through it); its unused constant goes. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/wasm32-pins/guest/src/lib.rs | 5 +- crates/before/wasm32-pins/harness/src/lib.rs | 7 +- .../before/wasm32-pins/harness/tests/pins.rs | 115 ++++++++---------- justfile | 6 +- 4 files changed, 64 insertions(+), 69 deletions(-) diff --git a/crates/before/wasm32-pins/guest/src/lib.rs b/crates/before/wasm32-pins/guest/src/lib.rs index 777b43f8..a849bebd 100644 --- a/crates/before/wasm32-pins/guest/src/lib.rs +++ b/crates/before/wasm32-pins/guest/src/lib.rs @@ -13,8 +13,9 @@ //! value's own bit length where one exists, `0` for a plain pass — and a //! negative code naming the first failed observation. //! - A panic anywhere in `before` or its dependencies aborts the guest, -//! which wasmtime surfaces as a trap. The traps ARE the pinned bad -//! baselines, so trap-versus-value is the harness's red/green axis. +//! which wasmtime surfaces as a trap: a first-class outcome the harness +//! pins directly, so trap-versus-value is the red/green axis whenever a +//! boundary misbehaves. //! //! The workspace builds this guest with `overflow-checks = true`: the 32-bit //! failure class under audit includes silent release-mode wraps, and the diff --git a/crates/before/wasm32-pins/harness/src/lib.rs b/crates/before/wasm32-pins/harness/src/lib.rs index 7a3590ff..2673ea29 100644 --- a/crates/before/wasm32-pins/harness/src/lib.rs +++ b/crates/before/wasm32-pins/harness/src/lib.rs @@ -17,9 +17,10 @@ use wasmtime::{Engine, Instance, Module, Store, Trap}; /// One pin call's outcome: the export's return value, or the trap that /// aborted it. /// -/// A trap is a first-class outcome, not a driver failure: the pinned bad -/// baselines ARE panics in `before` (surfaced as `unreachable` traps under -/// `panic = abort`), so the pins assert on this axis directly. +/// A trap is a first-class outcome, not a driver failure: a guest panic +/// surfaces as the `unreachable` trap under `panic = abort`, and a boundary +/// found panicking is pinned as exactly that trap until its cure lands, so +/// the pins assert on this axis directly. #[derive(Debug, PartialEq, Eq)] pub enum Outcome { /// The export returned: nonnegative is its observation, negative names diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs index 8607adfe..449ade4c 100644 --- a/crates/before/wasm32-pins/harness/tests/pins.rs +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -1,19 +1,15 @@ //! The 32-bit boundary pins: each test drives one guest export at one -//! boundary size and pins its behavior on wasm32. +//! boundary size and pins its exact behavior on wasm32. //! -//! Red-first discipline: a boundary found misbehaving is first pinned AS -//! FOUND — the assertion names the trap and the doc comment names the wrong -//! behavior it stands for — and the commit that engineers the seam around -//! flips the same test to the correct-value assertion. A pinned trap is -//! therefore never an accepted behavior: it is the committed bad baseline -//! the cure must move. +//! Red-first discipline: a boundary found misbehaving is pinned AS FOUND — +//! the assertion names the trap or wrong value, and the doc comment names +//! the wrong behavior it stands for — and the commit that engineers the +//! seam around flips the same test to the correct-value assertion. A pinned +//! trap is therefore never an accepted behavior: it is a committed bad +//! baseline its cure must move. Each pin's own history of red and green +//! lives in this file's git log. use wasm32_pins_harness::{call0, call1, Outcome}; -use wasmtime::Trap; - -/// A guest panic surfaces as the `unreachable` trap (`panic = abort` on -/// wasm32-unknown-unknown). -const PANICKED: Outcome = Outcome::Trapped(Trap::UnreachableCodeReached); /// Liveness: a small valid synthesized version decodes on wasm32 with the /// exact expected bit length, its stored bytes round-trip the input, and @@ -25,12 +21,12 @@ fn version_small_roundtrips_and_rejects_typed() { assert_eq!(call0("pin_version_small"), Outcome::Value(0)); } -/// Adjacency, green side: the largest input whose whole-buffer bit view -/// still fits `bitvec`'s borrowed-view length cap on wasm32 -/// (`usize::MAX >> 3` = 2^29 - 1 bits, so 67108863 bytes) decodes -/// correctly, returning its exact live bit length. This witnesses that the -/// red pin one byte up is the cap itself, not general large-input -/// breakage. +/// The largest input whose whole-buffer bit view still fits `bitvec`'s +/// borrowed-view length cap on wasm32 (`usize::MAX >> 3` = 2^29 - 1 bits, +/// so 67108863 bytes) decodes correctly, returning its exact live bit +/// length: the boundary's lower adjacency witness, so a failure at the +/// sizes just above is attributable to the cap, never to general +/// large-input handling. #[test] fn version_decode_below_view_cap() { assert_eq!( @@ -39,13 +35,13 @@ fn version_decode_below_view_cap() { ); } -/// A valid 64 MiB (67108864-byte) version encoding — exactly 2^29 bits, -/// the size whose whole-buffer borrowed bit view `bitvec`'s span encoding -/// silently constructs EMPTY on wasm32 — decodes correctly through the -/// byte-backed door, returning its exact live bit length. (The silent -/// empty view was this pin's committed bad baseline: the validator walked -/// zero bits and reported `Truncated` for a valid input, a wrong result -/// with no alarm.) +/// A valid 64 MiB (67108864-byte) version encoding decodes correctly on +/// wasm32, returning its exact live bit length. The size is exactly 2^29 +/// bits: a whole-buffer borrowed bit view of it is unconstructible — +/// `bitvec`'s span encoding silently produces an EMPTY view here, whose +/// walk would reject a valid input as `Truncated` with no alarm — so this +/// pin holds the doors to their byte-backed walk, on which no such view +/// exists. #[test] fn version_decode_at_view_cap() { assert_eq!( @@ -55,11 +51,11 @@ fn version_decode_at_view_cap() { } /// A valid 67108865-byte version encoding — one byte past the borrowed -/// view's silent-empty boundary, where `bitvec`'s element-count guard used -/// to panic the decode door on wasm32 — decodes correctly through the -/// byte-backed door, returning its exact live bit length. Together with -/// the pin one byte below, this witnesses both failure genres the -/// borrowed-view cap produced are gone from the doors. +/// view's silent-empty boundary, the first size whose view construction +/// `bitvec`'s element-count guard refuses by panic — decodes correctly on +/// wasm32, returning its exact live bit length. Together with the pin one +/// byte below, this holds the doors clear of both failure genres the +/// borrowed-view cap produces (the silent empty view, then the panic). #[test] fn version_decode_past_view_cap() { assert_eq!( @@ -71,13 +67,12 @@ fn version_decode_past_view_cap() { /// A valid 512 MiB (2^29-byte) version encoding — the largest stored /// stream a 32-bit `usize` can denominate bit positions for — decodes /// correctly on wasm32, returning its exact live bit length (2^32 - 8, -/// within one marker byte of `usize::MAX`). This was the `Bits::len` -/// seam's pin: `bytes.len() * 8` as a `usize` multiply overflowed at -/// exactly this size — a trap under this build's overflow checks, a -/// silent wrap coincidentally correct at 2^29 bytes and wrong above it -/// in unchecked builds — masked by the borrowed-view cap until the doors -/// went byte-backed, and cured by `u64` arithmetic with a checked final -/// conversion. +/// within one marker byte of `usize::MAX`). The size is where a `usize` +/// spelling of the stored length arithmetic (`bytes.len() * 8`) wraps a +/// 32-bit target — a wrap that is coincidentally correct at exactly 2^29 +/// bytes and short by 2^32 for anything larger — so this pin holds +/// `Bits::len`'s wider arithmetic exact at the boundary, under a build +/// whose overflow checks would surface any wrap as a trap. #[test] fn version_decode_at_bit_length_boundary() { assert_eq!( @@ -86,13 +81,14 @@ fn version_decode_at_bit_length_boundary() { ); } -/// Adjacency, green side: a valid rank whose fraction is 2^32 - 32 -/// expansion bits deep — the deepest flush-group exponent whose fraction -/// bytes still fit the big-integer backend's 32-bit buffer capacity — -/// decodes correctly today, and the decoded value sits strictly between -/// zero and one. ~604 MB of input: the smallest honest trigger, since the -/// fraction's depth is deliberately counted from bits actually read, -/// never from a header's claim. +/// A valid rank whose fraction is 2^32 - 32 expansion bits deep — the +/// deepest flush-group exponent whose whole fraction image fits the +/// big-integer backend's 32-bit buffer capacity without any stripping — +/// decodes correctly, and the decoded value sits strictly between zero +/// and one: the backend-capacity boundary's lower adjacency witness. +/// ~604 MB of input: the smallest honest trigger, since the fraction's +/// depth is deliberately counted from bits actually read, never from a +/// header's claim. #[test] fn rank_decode_below_backend_capacity() { assert_eq!( @@ -102,15 +98,13 @@ fn rank_decode_below_backend_capacity() { } /// A valid rank whose fraction is 2^32 - 8 expansion bits deep decodes -/// correctly on wasm32 and orders exactly against reference ranks. This -/// pin's committed bad baseline was a panic INSIDE the big-integer -/// backend: `dashu`'s `from_be_bytes` sizes its buffer from the input's -/// byte count (leading zero bytes included), and 536870911 fraction bytes -/// needed one word more than the backend's 32-bit `MAX_CAPACITY` even -/// though the numerator VALUE (its fraction opens with 64 zero bits) fits -/// comfortably — a seam below the decoder's own shift seam, partially -/// masking it. The byte-assembly cure strips leading zero bytes before -/// materializing. +/// correctly on wasm32 and orders exactly against reference ranks. At +/// this size an unstripped fraction image overruns the big-integer +/// backend's 32-bit buffer capacity by one word — `dashu` sizes buffers +/// from an image's byte count, leading zero bytes included — while the +/// numerator's value (its fraction opens with 64 zero bits) fits the +/// backend comfortably, so this pin holds the decoder to materializing +/// value, never zeros. #[test] fn rank_decode_at_backend_byte_capacity() { assert_eq!( @@ -120,15 +114,12 @@ fn rank_decode_at_backend_byte_capacity() { } /// A valid rank whose fraction is exactly 2^32 expansion bits deep — the -/// exponent one past wasm32's `usize` — decodes correctly and orders -/// exactly against reference ranks. This pin's committed bad baseline was -/// a panic at `Base`'s `Shl`: the decoder rebuilt the numerator as -/// `integral << exp`, whose shift amount's `usize::try_from` fails for -/// `exp >= 2^32` on a 32-bit target even though the input (~604 MB) and -/// the decoded numerator (~512 MiB) both fit the 4 GiB address space. The -/// cure assembles the numerator from bytes — the integral's bytes -/// concatenated with the fraction groups, leading zeros stripped — so no -/// value-width shift exists on the decode path at all. +/// exponent one past wasm32's `usize`, from an input (~604 MB) whose +/// decoded numerator (~512 MiB) fits the 4 GiB address space — decodes +/// correctly and orders exactly against reference ranks. An exponent this +/// size does not fit any backend shift amount on a 32-bit target, so this +/// pin holds the decode path to its byte-assembled numerator, on which no +/// value-width shift exists at all. #[test] fn rank_decode_at_usize_exp_boundary() { assert_eq!(call1("pin_rank_decode", 1u64 << 32), Outcome::Value(0)); diff --git a/justfile b/justfile index a783fd1d..8ad50c35 100644 --- a/justfile +++ b/justfile @@ -591,8 +591,10 @@ fuzzfit-calibrate: fuzzfit-build # byte decode doors at the exact sizes where 32-bit position arithmetic # has boundaries (bitvec's borrowed-view cap, the usize bit-length # boundary at 512 MiB, the rank exponent's usize seam), and the harness -# pins each boundary's behavior — the red-first bad baselines and the -# green boundaries beside them. The guest builds with overflow checks on, +# pins each boundary's exact outcome, with adjacency witnesses beside +# each boundary so a failure is attributable to its seam. Pins land +# red-first when a seam is found; each pin's history lives in git. +# The guest builds with overflow checks on, # so a 32-bit wrap is an observable trap, never a silently wrong value. # Build the 32-bit boundary-pin wasm guest and its harness (both halves). From 7ea3df58355df92fb3c3dcd956e560d9781110fb Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 14:50:03 -0400 Subject: [PATCH 06/20] before: classify the remaining 32-bit widening sites inline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The audit's ledger, written where the code is, each site carrying the invariant that keeps its arithmetic exact on 32-bit targets: - PackedBuilder's committed prefix states the width bound its four byte-to-bit position sites (len, patch_bit, read_bits, bit_at) rest on: build-side outputs are emitter-bounded over storage-capped operands, orders of magnitude below a usize wrap. - BitStack::len states the same for its word widening: walk transients price depth in bits, and depth is bounded by the walked stream's capped length. - The per-bit gamma decoder's prefix-overflow reject states what its arm names on each target: a value past the big-integer backend's representable width (32-bit), an unallocatable input (64-bit). - The accumulator-shift call sites in the integral and web folds state why the shifted entry points' documented 32-bit panic (digit position past usize, shift 2^37) is unreachable from them: every shift is bounded by stored-stream content under 2^32. - Packed::as_bits states why the instrument surface may keep its borrowed view where the decode doors walk raw bytes. Two seams in this class are genuine 32-bit defects and get cures, not comments: Party::encoded_bits reads the stored form's O(1) length instead of constructing a borrowed view (which caps below the stored sizes the doors admit), and Clock::encoded_bits sums its components at u64 width with a checked conversion — each component's length fits usize on 32-bit, but their byte-rounded sum need not, and a wrap there was a silently wrong public answer. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/src/clock.rs | 9 ++++++++- crates/before/src/codec/build.rs | 10 ++++++++++ crates/before/src/codec/gamma.rs | 7 +++++++ crates/before/src/codec/stack.rs | 6 ++++++ crates/before/src/meter.rs | 6 ++++++ crates/before/src/party.rs | 6 +++++- crates/before/src/version/skyline/query/integral.rs | 8 ++++++++ crates/before/src/version/skyline/query/web.rs | 6 ++++++ 8 files changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/before/src/clock.rs b/crates/before/src/clock.rs index d02bf25f..91c81f7f 100644 --- a/crates/before/src/clock.rs +++ b/crates/before/src/clock.rs @@ -806,7 +806,14 @@ impl Clock { /// assert_eq!(clock.encode().len(), (clock.encoded_bits() + 1).div_ceil(8)); /// ``` pub fn encoded_bits(&self) -> usize { - 8 * (self.party().encoded_bits() + 1).div_ceil(8) + self.version().encoded_bits() + // u64 width: on a 32-bit target each component's bit length fits + // usize (the storable bound), but the byte-rounded party plus the + // version can exceed it — a clock is two independently bounded + // streams. The conversion is checked, so a composite too long for + // this target's usize fails loudly by name instead of wrapping. + let party = 8 * (self.party().encoded_bits() as u64 + 1).div_ceil(8); + let bits = party + self.version().encoded_bits() as u64; + usize::try_from(bits).expect("the clock's combined bit length fits usize") } /// Duplicates this clock, producing a second handle to the same clock: an diff --git a/crates/before/src/codec/build.rs b/crates/before/src/codec/build.rs index dcf0d1b1..d5c02c82 100644 --- a/crates/before/src/codec/build.rs +++ b/crates/before/src/codec/build.rs @@ -41,6 +41,16 @@ use super::{BitsMut, BitsSlice, Code}; /// buffer, the primitive moves, and the write metering. pub(crate) struct PackedBuilder { /// The committed prefix: whole bytes, most-significant bit first. + /// + /// `bytes.len() * 8` fits `usize` on every target — the width the + /// position arithmetic below (`len`, `patch_bit`, `read_bits`, + /// `bit_at`) rests on. The bound: every builder's output is a stream + /// destined for the build-side buffer, whose borrowed-view encoding + /// caps at `usize::MAX >> 3` bits, and the emitters write at most a + /// small constant per input node over operands that are themselves + /// stored streams under the same cap — so the committed prefix stays + /// multiple binary orders of magnitude below any `usize` wrap, even + /// on 32-bit targets. bytes: Vec, /// The trailing not-yet-committed bits, value-packed at the low end /// (the stream's next bit is the register's most significant live diff --git a/crates/before/src/codec/gamma.rs b/crates/before/src/codec/gamma.rs index a33fdba7..cc7dd722 100644 --- a/crates/before/src/codec/gamma.rs +++ b/crates/before/src/codec/gamma.rs @@ -226,6 +226,13 @@ where // The match (rather than `ok_or`) keeps the error value — `Decode` has // drop glue — from being constructed and dropped on every iteration of // this per-bit loop; see `codec::cursor::Truncated`. + // + // The overflow arm names a value no target can hold: a prefix of + // usize::MAX zeros declares a mantissa past the big-integer + // backend's representable width (the backend caps below usize::MAX + // bits), so the reject genre is the value's, not the machine's. On + // 64-bit targets the arm is dead — reading 2^64 bits first needs + // an unallocatable input. k = match k.checked_add(1) { Some(k) => k, None => return Err(Decode::NotCanonical), diff --git a/crates/before/src/codec/stack.rs b/crates/before/src/codec/stack.rs index 42456160..7a2c78f4 100644 --- a/crates/before/src/codec/stack.rs +++ b/crates/before/src/codec/stack.rs @@ -35,6 +35,12 @@ impl BitStack { } /// The stack's height in bits. + /// + /// `words.len() * 64` fits `usize` on every target: a walk holds a few + /// bits per open ancestor here, depth is bounded by the walked stream's + /// bit length, and stored streams cap at `usize::MAX >> 3` bits (the + /// borrowed-view encoding) — so the height stays multiple binary orders + /// of magnitude below any `usize` wrap, even on 32-bit targets. pub(crate) fn len(&self) -> usize { self.words.len() * 64 + self.top_len as usize } diff --git a/crates/before/src/meter.rs b/crates/before/src/meter.rs index 8e0d8f05..ef0d0bf6 100644 --- a/crates/before/src/meter.rs +++ b/crates/before/src/meter.rs @@ -109,6 +109,12 @@ impl Packed { } /// The generator's live bits, borrowed. + /// + /// A borrowed bit view, sized for the instrument surface: the shape + /// generators emit parameter-scale streams far below the view + /// encoding's cap (`usize::MAX >> 3` bits), where the byte decode + /// doors — which admit larger buffers on 32-bit targets — walk raw + /// bytes instead. pub fn as_bits(&self) -> &codec::BitsSlice { &codec::bytes_as_bits(&self.bytes)[..self.bits] } diff --git a/crates/before/src/party.rs b/crates/before/src/party.rs index f75c6f04..3c690993 100644 --- a/crates/before/src/party.rs +++ b/crates/before/src/party.rs @@ -551,7 +551,11 @@ impl Party { /// assert_eq!(before::Party::seed().encoded_bits(), 2); /// ``` pub fn encoded_bits(&self) -> usize { - self.as_bits().len() + // The stored form's O(1) length, never the borrowed bit view: the + // view's encoding caps below the stored sizes the decode doors admit + // on 32-bit targets, while the length itself is exact at every + // storable size. + self.0.len() } /// Decodes a [`Party`] from a reader of canonical bytes, strictly rejecting diff --git a/crates/before/src/version/skyline/query/integral.rs b/crates/before/src/version/skyline/query/integral.rs index 312eef6d..75d3a44e 100644 --- a/crates/before/src/version/skyline/query/integral.rs +++ b/crates/before/src/version/skyline/query/integral.rs @@ -461,6 +461,14 @@ pub(super) fn charge_digits( factor: &Base, digits: &[(u64, i64)], ) { + // Every accumulator shift in this module — the `32 * index` digit + // routings here, the interval weights and segment scales below — is + // bounded by the walked stream's own content: digit indexes by a + // value's width over 32, weights by the tree's depth, both under the + // stored stream's bit length, which the storage caps below 2^32. The + // shifted entry points' documented panic (a digit position past + // `usize`, from shift 2^37 on a 32-bit target) therefore sits multiple + // binary orders of magnitude beyond anything this fold can feed it. // Total with no identity fast path: an empty digit run yields no // clusters, so the loop is the no-op it should be, and both callers // (the segment settles and the aggregate merges) already skip diff --git a/crates/before/src/version/skyline/query/web.rs b/crates/before/src/version/skyline/query/web.rs index 8806485c..f1a9ca06 100644 --- a/crates/before/src/version/skyline/query/web.rs +++ b/crates/before/src/version/skyline/query/web.rs @@ -133,6 +133,12 @@ pub(super) fn mul_into( if *factor == Base::ZERO { return; } + // The shifts routed below are digit positions of walked-value widths + // (a width over 32, plus the caller's scale): bounded by the stored + // stream's bit length, which the storage caps below 2^32 — multiple + // binary orders of magnitude under the accumulator entry points' + // documented panic bound (a digit position past `usize`, from shift + // 2^37 on a 32-bit target). let mut carry = 0u64; let mut add_term = |digit: u64, sign: Sign, shift: u64| { if digit == 0 { From 456dc0f4544d3a46a356c3916edd1d1684d2ff72 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 14:53:29 -0400 Subject: [PATCH 07/20] docs: keep the new byte-door and pin doc summaries under the doclint cap Each flagged doc's first paragraph shortens to its one-sentence summary, with the detail moved below the paragraph break; no content changes. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/src/codec/bits.rs | 7 +- crates/before/src/codec/dsi.rs | 8 +- crates/before/src/codec/gamma.rs | 9 +- crates/before/src/codec/scan.rs | 5 +- crates/before/src/codec/tree.rs | 15 +-- crates/before/src/version/skyline/admit.rs | 7 +- crates/before/src/version/skyline/overlay.rs | 9 +- crates/before/wasm32-pins/guest/src/lib.rs | 14 +-- .../before/wasm32-pins/harness/tests/pins.rs | 102 ++++++++++-------- 9 files changed, 99 insertions(+), 77 deletions(-) diff --git a/crates/before/src/codec/bits.rs b/crates/before/src/codec/bits.rs index f217f057..6b6a82b4 100644 --- a/crates/before/src/codec/bits.rs +++ b/crates/before/src/codec/bits.rs @@ -266,9 +266,10 @@ impl PartialEq for Bits { impl Eq for Bits {} /// Borrow bytes as an MSB-first bit stream without first copying them into a -/// [`BitsMut`]: the meter surface's and the test suites' view — production -/// decode walks raw bytes instead, since this view's encoding caps below the -/// buffer sizes the decode doors admit on 32-bit targets. +/// [`BitsMut`]: the meter surface's and the test suites' view. +/// +/// Production decode walks raw bytes instead, since this view's encoding +/// caps below the buffer sizes the decode doors admit on 32-bit targets. #[cfg(any(test, feature = "meter"))] pub(crate) fn bytes_as_bits(bytes: &[u8]) -> &BitsSlice { bytes.view_bits::() diff --git a/crates/before/src/codec/dsi.rs b/crates/before/src/codec/dsi.rs index 8e9358aa..0ece8709 100644 --- a/crates/before/src/codec/dsi.rs +++ b/crates/before/src/codec/dsi.rs @@ -94,9 +94,11 @@ impl<'a> DsiCursor<'a> { } /// Open a cursor over the whole bit view of raw stream bytes — all - /// `8 · bytes.len()` of them, padding included: the byte decode doors' - /// entry, which must walk buffers whose borrowed bit view the `bitvec` - /// encoding cannot represent (64 MiB and up on a 32-bit target). + /// `8 · bytes.len()` of them, padding included. + /// + /// The byte decode doors' entry, which must walk buffers whose borrowed + /// bit view the `bitvec` encoding cannot represent (64 MiB and up on a + /// 32-bit target). /// /// The door semantics are exactly the slice walk's over a whole padded /// buffer: padding bits are data to the walk, and the door's marker diff --git a/crates/before/src/codec/gamma.rs b/crates/before/src/codec/gamma.rs index cc7dd722..42cd7ce5 100644 --- a/crates/before/src/codec/gamma.rs +++ b/crates/before/src/codec/gamma.rs @@ -156,10 +156,11 @@ pub(crate) fn decode_int_window(bits: &BitsSlice, pos: usize) -> Option<(u64, us Some((n, next as usize)) } -/// [`decode_int_window`] over raw stream bytes: the wire-side reader's form -/// (its buffered bytes have no borrowed bit view once they outgrow the view -/// encoding's cap), windowing the whole `8 · bytes.len()`-bit view at the -/// reader's own `u64` position width. +/// [`decode_int_window`] over raw stream bytes: the wire-side reader's form. +/// +/// The reader's buffered bytes have no borrowed bit view once they outgrow +/// the view encoding's cap, so this windows the whole `8 · bytes.len()`-bit +/// view at the reader's own `u64` position width. #[cfg(any(test, feature = "borsh"))] pub(crate) fn decode_int_window_bytes(bytes: &[u8], pos: u64) -> Option<(u64, u64)> { window_int(bytes, None, bytes.len() as u64 * 8, pos) diff --git a/crates/before/src/codec/scan.rs b/crates/before/src/codec/scan.rs index 3a8c32e8..2748f42d 100644 --- a/crates/before/src/codec/scan.rs +++ b/crates/before/src/codec/scan.rs @@ -58,8 +58,9 @@ pub(crate) fn record_bits(n: usize) { let _ = n; } -/// [`record_bits`] at the counter's own width, for the word-parallel cursor: -/// a byte decode door's walk spans up to `8 · bytes.len()` bit positions, +/// [`record_bits`] at the counter's own width, for the word-parallel cursor. +/// +/// A byte decode door's walk spans up to `8 · bytes.len()` bit positions, /// which exceeds a 32-bit `usize` on the largest buffers a door admits, so /// its run and tail records stay `u64` end to end. #[inline(always)] diff --git a/crates/before/src/codec/tree.rs b/crates/before/src/codec/tree.rs index fe7776fa..11c1ebdd 100644 --- a/crates/before/src/codec/tree.rs +++ b/crates/before/src/codec/tree.rs @@ -36,9 +36,10 @@ pub(crate) fn parse_id(bits: &BitsSlice, pos: usize) -> Result { } /// [`parse_id`] over raw stream bytes, walking the whole `8 · bytes.len()` -/// bit view: the byte decode doors' form, for buffers past the borrowed bit -/// view's encoding cap (64 MiB and up on a 32-bit target), returning the -/// tree's end position at the walk's own `u64` width. +/// bit view and returning the tree's end at the walk's own `u64` width. +/// +/// The byte decode doors' form, for buffers past the borrowed bit view's +/// encoding cap (64 MiB and up on a 32-bit target). pub(crate) fn parse_id_bytes(bytes: &[u8]) -> Result { let mut cursor = super::DsiCursor::over_bytes(bytes); parse_id_core(&mut cursor)?; @@ -56,9 +57,11 @@ where } /// Parse and validate one id tree from a sequential bit cursor: the one -/// grammar body, leaving the end position to the caller's own read (the -/// byte decode doors read it at `u64` width, everything else through -/// [`BitCursor::position`]). +/// grammar body. +/// +/// The end position is left to the caller's own read: the byte decode doors +/// read it at `u64` width, everything else through +/// [`BitCursor::position`]. pub(crate) fn parse_id_core(cursor: &mut C) -> Result<(), Decode> where Decode: From, diff --git a/crates/before/src/version/skyline/admit.rs b/crates/before/src/version/skyline/admit.rs index 655963d9..fc713a8a 100644 --- a/crates/before/src/version/skyline/admit.rs +++ b/crates/before/src/version/skyline/admit.rs @@ -288,9 +288,10 @@ where } /// [`validate_dominating_from`] with the canonical `lo` given as raw stream -/// bytes and its live bit length: the byte decode door's form, for meets -/// past the borrowed bit view's encoding cap (64 MiB and up on a 32-bit -/// target). Same contract in full. +/// bytes and its live bit length. Same contract in full. +/// +/// The byte decode door's form, for meets past the borrowed bit view's +/// encoding cap (64 MiB and up on a 32-bit target). pub(crate) fn validate_dominating_bytes( lo: &[u8], lo_live: usize, diff --git a/crates/before/src/version/skyline/overlay.rs b/crates/before/src/version/skyline/overlay.rs index 47db9264..d8b6cd7a 100644 --- a/crates/before/src/version/skyline/overlay.rs +++ b/crates/before/src/version/skyline/overlay.rs @@ -340,10 +340,11 @@ impl<'a> LeafCursor<'a> { (this, first) } - /// [`open`](Self::open) over raw stream bytes and a live bit length: the - /// byte decode door's form, for canonical streams past the borrowed bit - /// view's encoding cap (64 MiB and up on a 32-bit target). Same contract, - /// canonicality trust included. + /// [`open`](Self::open) over raw stream bytes and a live bit length. + /// + /// The byte decode door's form, for canonical streams past the borrowed + /// bit view's encoding cap (64 MiB and up on a 32-bit target). Same + /// contract, canonicality trust included. pub(super) fn open_bytes(bytes: &'a [u8], live: usize) -> (Self, Int) { let mut this = LeafCursor { cursor: DsiCursor::over_bytes_live(bytes, live), diff --git a/crates/before/wasm32-pins/guest/src/lib.rs b/crates/before/wasm32-pins/guest/src/lib.rs index a849bebd..27ee57a3 100644 --- a/crates/before/wasm32-pins/guest/src/lib.rs +++ b/crates/before/wasm32-pins/guest/src/lib.rs @@ -54,9 +54,10 @@ fn synth_version(n: usize) -> Vec { } /// Sanity and liveness: a small synthesized version decodes to the expected -/// bit length, its bytes round-trip, and two mutilations reject with typed -/// errors (never a panic). Green at every commit; a red here means the -/// harness or the synthesis is broken, not that a boundary moved. +/// bit length, round-trips, and rejects two mutilations with typed errors. +/// +/// Green at every commit; a red here means the harness or the synthesis is +/// broken, not that a boundary moved. #[no_mangle] pub extern "C" fn pin_version_small() -> i64 { let bytes = synth_version(64); @@ -167,9 +168,10 @@ fn synth_rank(exp: u64) -> Vec { } /// Decode a valid synthesized rank whose fraction is exactly `exp` -/// expansion bits deep, then check three exact-order observations against -/// reference ranks: the value sits strictly between zero and one, and -/// equals its own clone. +/// expansion bits deep, then check exact-order observations. +/// +/// The observations, against reference ranks: the value sits strictly +/// between zero and one, and equals its own clone. /// /// The harness aims this at the `u64 -> usize` exponent seam: `exp` just /// below `2^32` (in `usize` range on wasm32) and at `2^32` (past it). The diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs index 449ade4c..b42942c7 100644 --- a/crates/before/wasm32-pins/harness/tests/pins.rs +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -11,22 +11,23 @@ use wasm32_pins_harness::{call0, call1, Outcome}; -/// Liveness: a small valid synthesized version decodes on wasm32 with the -/// exact expected bit length, its stored bytes round-trip the input, and -/// truncated or marker-stripped mutations reject with typed errors, never a -/// panic. Green at every commit — a red here impeaches the leg itself, not -/// a boundary. +/// Liveness: a small valid version decodes on wasm32 with the exact bit +/// length, round-trips, and rejects mutilations with typed errors. +/// +/// Green at every commit — a red here impeaches the leg itself, not a +/// boundary. #[test] fn version_small_roundtrips_and_rejects_typed() { assert_eq!(call0("pin_version_small"), Outcome::Value(0)); } /// The largest input whose whole-buffer bit view still fits `bitvec`'s -/// borrowed-view length cap on wasm32 (`usize::MAX >> 3` = 2^29 - 1 bits, -/// so 67108863 bytes) decodes correctly, returning its exact live bit -/// length: the boundary's lower adjacency witness, so a failure at the -/// sizes just above is attributable to the cap, never to general -/// large-input handling. +/// borrowed-view length cap on wasm32 decodes with its exact bit length. +/// +/// The cap is `usize::MAX >> 3` = 2^29 - 1 bits, so 67108863 bytes: the +/// boundary's lower adjacency witness, so a failure at the sizes just +/// above is attributable to the cap, never to general large-input +/// handling. #[test] fn version_decode_below_view_cap() { assert_eq!( @@ -36,12 +37,13 @@ fn version_decode_below_view_cap() { } /// A valid 64 MiB (67108864-byte) version encoding decodes correctly on -/// wasm32, returning its exact live bit length. The size is exactly 2^29 -/// bits: a whole-buffer borrowed bit view of it is unconstructible — -/// `bitvec`'s span encoding silently produces an EMPTY view here, whose -/// walk would reject a valid input as `Truncated` with no alarm — so this -/// pin holds the doors to their byte-backed walk, on which no such view -/// exists. +/// wasm32, returning its exact live bit length. +/// +/// The size is exactly 2^29 bits: a whole-buffer borrowed bit view of it +/// is unconstructible — `bitvec`'s span encoding silently produces an +/// EMPTY view here, whose walk would reject a valid input as `Truncated` +/// with no alarm — so this pin holds the doors to their byte-backed walk, +/// on which no such view exists. #[test] fn version_decode_at_view_cap() { assert_eq!( @@ -50,12 +52,14 @@ fn version_decode_at_view_cap() { ); } -/// A valid 67108865-byte version encoding — one byte past the borrowed -/// view's silent-empty boundary, the first size whose view construction -/// `bitvec`'s element-count guard refuses by panic — decodes correctly on -/// wasm32, returning its exact live bit length. Together with the pin one -/// byte below, this holds the doors clear of both failure genres the -/// borrowed-view cap produces (the silent empty view, then the panic). +/// A valid 67108865-byte version encoding decodes correctly on wasm32, +/// returning its exact live bit length. +/// +/// One byte past the borrowed view's silent-empty boundary, this is the +/// first size whose view construction `bitvec`'s element-count guard +/// refuses by panic. Together with the pin one byte below, this holds the +/// doors clear of both failure genres the borrowed-view cap produces (the +/// silent empty view, then the panic). #[test] fn version_decode_past_view_cap() { assert_eq!( @@ -66,13 +70,15 @@ fn version_decode_past_view_cap() { /// A valid 512 MiB (2^29-byte) version encoding — the largest stored /// stream a 32-bit `usize` can denominate bit positions for — decodes -/// correctly on wasm32, returning its exact live bit length (2^32 - 8, -/// within one marker byte of `usize::MAX`). The size is where a `usize` -/// spelling of the stored length arithmetic (`bytes.len() * 8`) wraps a -/// 32-bit target — a wrap that is coincidentally correct at exactly 2^29 -/// bytes and short by 2^32 for anything larger — so this pin holds -/// `Bits::len`'s wider arithmetic exact at the boundary, under a build -/// whose overflow checks would surface any wrap as a trap. +/// correctly on wasm32 with its exact live bit length. +/// +/// The returned length is 2^32 - 8, within one marker byte of +/// `usize::MAX`. The size is where a `usize` spelling of the stored +/// length arithmetic (`bytes.len() * 8`) wraps a 32-bit target — a wrap +/// coincidentally correct at exactly 2^29 bytes and short by 2^32 for +/// anything larger — so this pin holds `Bits::len`'s wider arithmetic +/// exact at the boundary, under a build whose overflow checks would +/// surface any wrap as a trap. #[test] fn version_decode_at_bit_length_boundary() { assert_eq!( @@ -81,14 +87,15 @@ fn version_decode_at_bit_length_boundary() { ); } -/// A valid rank whose fraction is 2^32 - 32 expansion bits deep — the -/// deepest flush-group exponent whose whole fraction image fits the -/// big-integer backend's 32-bit buffer capacity without any stripping — -/// decodes correctly, and the decoded value sits strictly between zero -/// and one: the backend-capacity boundary's lower adjacency witness. -/// ~604 MB of input: the smallest honest trigger, since the fraction's -/// depth is deliberately counted from bits actually read, never from a -/// header's claim. +/// A valid rank whose fraction is 2^32 - 32 expansion bits deep decodes +/// correctly, and the decoded value sits strictly between zero and one. +/// +/// The deepest flush-group exponent whose whole fraction image fits the +/// big-integer backend's 32-bit buffer capacity without any stripping: +/// the backend-capacity boundary's lower adjacency witness. ~604 MB of +/// input is the smallest honest trigger, since the fraction's depth is +/// deliberately counted from bits actually read, never from a header's +/// claim. #[test] fn rank_decode_below_backend_capacity() { assert_eq!( @@ -98,8 +105,9 @@ fn rank_decode_below_backend_capacity() { } /// A valid rank whose fraction is 2^32 - 8 expansion bits deep decodes -/// correctly on wasm32 and orders exactly against reference ranks. At -/// this size an unstripped fraction image overruns the big-integer +/// correctly on wasm32 and orders exactly against reference ranks. +/// +/// At this size an unstripped fraction image overruns the big-integer /// backend's 32-bit buffer capacity by one word — `dashu` sizes buffers /// from an image's byte count, leading zero bytes included — while the /// numerator's value (its fraction opens with 64 zero bits) fits the @@ -113,13 +121,15 @@ fn rank_decode_at_backend_byte_capacity() { ); } -/// A valid rank whose fraction is exactly 2^32 expansion bits deep — the -/// exponent one past wasm32's `usize`, from an input (~604 MB) whose -/// decoded numerator (~512 MiB) fits the 4 GiB address space — decodes -/// correctly and orders exactly against reference ranks. An exponent this -/// size does not fit any backend shift amount on a 32-bit target, so this -/// pin holds the decode path to its byte-assembled numerator, on which no -/// value-width shift exists at all. +/// A valid rank whose fraction is exactly 2^32 expansion bits deep — +/// the exponent one past wasm32's `usize` — decodes correctly and orders +/// exactly against reference ranks. +/// +/// The input (~604 MB) and its decoded numerator (~512 MiB) fit the +/// 4 GiB address space, while an exponent this size fits no backend +/// shift amount on a 32-bit target — so this pin holds the decode path +/// to its byte-assembled numerator, on which no value-width shift exists +/// at all. #[test] fn rank_decode_at_usize_exp_boundary() { assert_eq!(call1("pin_rank_decode", 1u64 << 32), Outcome::Value(0)); From cfa7c7ed0d754b97e44e0319b98d6f61a17806c5 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 15:02:14 -0400 Subject: [PATCH 08/20] before: restore the encode shift's metered spelling; tighten the byte-door cfgs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three gate verdicts, three cures: - The rank encode's integral extraction returns to (num >> exp) + 1 whole: Shr is total (it clamps past the numerator's width), so the width-guard arm was a second spelling of the same boundary — and one that skipped the shift's width-scale limb record, reading the amp board's rank_encode limb floors from below (the floor was right: the walk genuinely reads the numerator; the record rides the shift). - validate_bits' doc names validate_prefix_bytes, the production entry beside it. - The byte-door companions gated to their one consumer's exact cfg (feature = borsh, not any(test, ...)): the default-feature test build compiles cfg(test) items without the borsh module that uses them, and clippy-default exists precisely to catch that dangling surface. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/src/codec.rs | 4 ++-- crates/before/src/codec/gamma.rs | 2 +- crates/before/src/version/rank.rs | 17 ++++++----------- crates/before/src/version/skyline.rs | 2 +- crates/before/src/version/skyline/admit.rs | 4 ++-- crates/before/src/version/skyline/validate.rs | 2 +- 6 files changed, 13 insertions(+), 18 deletions(-) diff --git a/crates/before/src/codec.rs b/crates/before/src/codec.rs index d9497bde..cfcbc1c2 100644 --- a/crates/before/src/codec.rs +++ b/crates/before/src/codec.rs @@ -71,7 +71,7 @@ pub(crate) use int::Int; // bytes have no borrowed bit view once they outgrow the view encoding's cap; // the slice-form window stays inside the codec (the slice cursor's fast // path reaches it by module path). -#[cfg(any(test, feature = "borsh"))] +#[cfg(feature = "borsh")] pub(crate) use gamma::decode_int_window_bytes; pub(crate) use literal::{id_is_empty, id_leaf, id_node}; pub(crate) use stack::{BitStack, PopStack}; @@ -84,5 +84,5 @@ pub(crate) use tree::{parse_id_bytes, validate_id}; pub(crate) use tree::parse_id_core; // The generic-position parse serves the wire-side (borsh) test suite; the // grammar body above is what production readers drive. -#[cfg(test)] +#[cfg(all(test, feature = "borsh"))] pub(crate) use tree::parse_id_from; diff --git a/crates/before/src/codec/gamma.rs b/crates/before/src/codec/gamma.rs index 42cd7ce5..c9834b20 100644 --- a/crates/before/src/codec/gamma.rs +++ b/crates/before/src/codec/gamma.rs @@ -161,7 +161,7 @@ pub(crate) fn decode_int_window(bits: &BitsSlice, pos: usize) -> Option<(u64, us /// The reader's buffered bytes have no borrowed bit view once they outgrow /// the view encoding's cap, so this windows the whole `8 · bytes.len()`-bit /// view at the reader's own `u64` position width. -#[cfg(any(test, feature = "borsh"))] +#[cfg(feature = "borsh")] pub(crate) fn decode_int_window_bytes(bytes: &[u8], pos: u64) -> Option<(u64, u64)> { window_int(bytes, None, bytes.len() as u64 * 8, pos) } diff --git a/crates/before/src/version/rank.rs b/crates/before/src/version/rank.rs index 7d0233b3..df69a211 100644 --- a/crates/before/src/version/rank.rs +++ b/crates/before/src/version/rank.rs @@ -500,17 +500,12 @@ impl Rank { /// no walk beyond the fold's own. pub(crate) fn encode_parts(num: &Base, exp: u64) -> Vec { // The integral part, biased so zero has a (smallest) codeword: - // m = ⌊r⌋ + 1, w = bits(m), ρ = bits(w) − 1. Decided by width when the - // exponent covers the numerator whole — `⌊r⌋ = 0` with no shift - // materialized — because a fraction-heavy rank's `exp` outruns a 32-bit - // `usize` (from ~604 MB of decoded input) while the rank itself is - // representable; the shift arm's amount is then always below the - // numerator's width, which the backend bounds within `usize`. - let biased = if exp >= num.bits() { - Base::from(1u8) - } else { - (num.clone() >> exp) + 1u32 - }; + // m = ⌊r⌋ + 1, w = bits(m), ρ = bits(w) − 1. The shift is total at any + // exponent — `Base`'s `Shr` clamps past the numerator's width — so + // a fraction-heavy rank whose `exp` outruns a 32-bit `usize` (from + // ~604 MB of decoded input) floors to zero here exactly as any other + // sub-unit value does. + let biased = (num.clone() >> exp) + 1u32; let w = biased.bits(); let rho = u64::from(63 - w.leading_zeros()); let groups = exp.div_ceil(FRACTION_GROUP_BITS); diff --git a/crates/before/src/version/skyline.rs b/crates/before/src/version/skyline.rs index 6f2f611a..8fcd2562 100644 --- a/crates/before/src/version/skyline.rs +++ b/crates/before/src/version/skyline.rs @@ -207,7 +207,7 @@ mod tests; pub(crate) use admit::{validate_dominating_bytes, Admission}; // The slice-form admission walk serves the wire-side (borsh) span reader, // whose meet is an already-stored version read through its borrowed view. -#[cfg(any(test, feature = "borsh"))] +#[cfg(feature = "borsh")] pub(crate) use admit::validate_dominating_from; #[cfg(any(test, feature = "meter"))] pub(crate) use decode::decode_bits; diff --git a/crates/before/src/version/skyline/admit.rs b/crates/before/src/version/skyline/admit.rs index fc713a8a..5517e99e 100644 --- a/crates/before/src/version/skyline/admit.rs +++ b/crates/before/src/version/skyline/admit.rs @@ -50,7 +50,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; -#[cfg(any(test, feature = "borsh"))] +#[cfg(feature = "borsh")] use crate::codec::BitsSlice; use crate::codec::{BitCursor, BitsMut, Int}; use crate::error::Decode; @@ -275,7 +275,7 @@ pub(crate) enum Admission { /// `lo` must be a canonical skyline stream — its cursor is the pair sweep's and /// shares [`causal_cmp`](super::sweep::causal_cmp)'s contract. The parsed /// stream needs no such trust; that is the point. -#[cfg(any(test, feature = "borsh"))] +#[cfg(feature = "borsh")] pub(crate) fn validate_dominating_from( lo: &BitsSlice, cursor: &mut C, diff --git a/crates/before/src/version/skyline/validate.rs b/crates/before/src/version/skyline/validate.rs index f3d2a5d3..8e425d37 100644 --- a/crates/before/src/version/skyline/validate.rs +++ b/crates/before/src/version/skyline/validate.rs @@ -37,7 +37,7 @@ use super::signed::{fold_signed_int, unzigzag, Sign}; /// the last live bit is [`Decode::TrailingBits`]; everything else is /// [`validate_from`]'s contract. /// -/// Test- and meter-only: the production entries run [`validate_prefix`] and +/// Test- and meter-only: the production entries run [`validate_prefix_bytes`] and /// [`validate_from`], which leave the tail to their callers. #[cfg(any(test, feature = "meter"))] pub(crate) fn validate_bits(bits: &BitsSlice) -> Result<(), Decode> { From f34f4b34f233e5a88134424af0b85835c4857bdc Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 15:10:22 -0400 Subject: [PATCH 09/20] before: re-pin rank_decode's worst heap families after the byte assembly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rank decoder's numerator assembly moved the operation's allocation profile, and the worst-case ranking pin caught the flip. Parent-measured attribution (worktree at the byte-assembly commit's parent, same board, same scales): - freeze-parade (fraction-heavy): 3.00 -> 2.0 B/input-byte at the default scale, 3.67 -> 2.7 at acceptance — the dominant improvement; the assembly never materializes a shifted integral, so the family that paid that transient stops paying it. - plateau-puncture: 2.74 -> 2.6 at default — slightly improved, and now the default-scale worst by overtake. - bigroot (integral-heavy): at most 3.34 -> 3.7 at acceptance — a small constant rise, now the acceptance-scale worst: the byte image pays the integral's own bytes where the shift spelling folded them into its shifted buffer. Exponents hold at 1.00 and every acceptance ceiling stays green (2096/0), so the movement is a constant-factor trade inside the linear envelope, priced against the decode-path correctness the assembly buys at every scale. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/src/meter/board/worst.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/before/src/meter/board/worst.rs b/crates/before/src/meter/board/worst.rs index 60212fed..fa71722d 100644 --- a/crates/before/src/meter/board/worst.rs +++ b/crates/before/src/meter/board/worst.rs @@ -418,7 +418,7 @@ pub(super) const WORST_RANKINGS: &[(&str, &str, [&str; 4])] = &[ ("default", "rank_pair_ops", ["hugeleaf", "concurrent-pair", "-", "-"]), ("default", "rank_sum", ["plateau-puncture", "hugeleaf", "-", "freeze-pos"]), ("default", "rank_encode", ["hugeleaf", "concurrent-pair", "-", "-"]), - ("default", "rank_decode", ["freeze-parade", "benign,concurrent-pair", "-", "-"]), + ("default", "rank_decode", ["plateau-puncture", "benign,concurrent-pair", "-", "-"]), ("default", "version_distance", ["wide-arming", "wide-arming", "promo-rearm", "harmonic"]), ("default", "version_lag", ["wide-arming", "wide-arming", "promo-rearm", "harmonic"]), ("default", "ranked_cmp", ["wide-arming", "wide-arming", "promo-rearm", "harmonic"]), @@ -504,7 +504,7 @@ pub(super) const WORST_RANKINGS: &[(&str, &str, [&str; 4])] = &[ ("acceptance", "rank_pair_ops", ["hugeleaf", "concurrent-pair", "-", "-"]), ("acceptance", "rank_sum", ["bigroot", "hugeleaf", "-", "freeze-pos"]), ("acceptance", "rank_encode", ["hugeleaf", "concurrent-pair", "-", "-"]), - ("acceptance", "rank_decode", ["freeze-parade", "concurrent-pair", "-", "-"]), + ("acceptance", "rank_decode", ["bigroot", "concurrent-pair", "-", "-"]), ("acceptance", "version_distance", ["wide-arming", "wide-arming", "dense-suffix", "harmonic"]), ("acceptance", "version_lag", ["wide-arming", "wide-arming", "dense-suffix", "harmonic"]), ("acceptance", "ranked_cmp", ["wide-arming", "wide-arming", "dense-suffix", "harmonic"]), From 39b237e6bbcc31b06a87978feeaa0bcf383c6421 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 17:29:27 -0400 Subject: [PATCH 10/20] wasm32-pins: red-first pins for five newly scoped 32-bit boundaries The execution leg so far pinned the byte decode doors; five boundaries in the walk, emit, and rank-arithmetic surfaces had no committed baseline. Each now lands as-found (instruments before cures: every later reshaping flips a committed red), with an adjacency witness just inside the boundary so a failure is attributable to its seam: - Composite doors (Ranked byte + borsh, Span borsh): the rank re-derivation and dominance re-walk read the version through the borrowed bit view, so valid keys trap from 67108864 bytes of version component; both view-cap genres pinned, green at 67108863. - Semantic walks (Version PartialOrd, join): stored values the doors admit exactly cannot be compared or joined past the view cap; both genres pinned beside green witnesses, on values the decode pins already prove storable. - Emitter output: two operands under every per-operand bound whose join concatenates trap when the finished stream reaches 67108864 whole bytes (the builder's byte-backed buffer adopts into a bitvec whose length encoding caps at usize::MAX >> 3 bits); the largest emittable output in the family, 536870903 live bits, is the witness. - Rank numerator at the backend word cap: a valid ~604 MB rank whose numerator is 2^32 - 24 value bits traps in the backend's buffer-fill assert though value and working set fit the address space; a numerator of exactly 2^32 - 32 bits (the cap itself) decodes green. - Rank alignment gap: Add/checked_sub trap at an exponent gap of exactly 2^32 (the shift amount's usize conversion), and Add also traps just below the gap boundary when the aligned numerator outgrows the backend (gap 2^32 - 1, rank 1/2); gap 2^32 - 128 with a 64-bit numerator is exact on both arms. The guest gains the two-leaf skyline and integral-rank-stream syntheses these shapes need (everything synthesized in-guest as before), the borsh feature for the streaming doors, and a two-argument export form; the harness gains call2 and re-exports Trap so pins can name the exact trap they assert. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/wasm32-pins/Cargo.lock | 18 + crates/before/wasm32-pins/guest/Cargo.toml | 7 +- crates/before/wasm32-pins/guest/src/lib.rs | 386 +++++++++++++++++- crates/before/wasm32-pins/harness/src/lib.rs | 29 +- .../before/wasm32-pins/harness/tests/pins.rs | 380 ++++++++++++++++- justfile | 17 +- 6 files changed, 816 insertions(+), 21 deletions(-) diff --git a/crates/before/wasm32-pins/Cargo.lock b/crates/before/wasm32-pins/Cargo.lock index cb88ea68..724cf9c2 100644 --- a/crates/before/wasm32-pins/Cargo.lock +++ b/crates/before/wasm32-pins/Cargo.lock @@ -45,6 +45,7 @@ name = "before" version = "0.1.0" dependencies = [ "bitvec", + "borsh", "bytes", "dashu-int", "dsi-bitstream", @@ -81,6 +82,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +dependencies = [ + "bytes", + "cfg_aliases", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -112,6 +123,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "cobs" version = "0.3.0" @@ -928,6 +945,7 @@ name = "wasm32-pins-guest" version = "0.0.0" dependencies = [ "before", + "borsh", ] [[package]] diff --git a/crates/before/wasm32-pins/guest/Cargo.toml b/crates/before/wasm32-pins/guest/Cargo.toml index a3cc17bd..3ab15cb5 100644 --- a/crates/before/wasm32-pins/guest/Cargo.toml +++ b/crates/before/wasm32-pins/guest/Cargo.toml @@ -12,4 +12,9 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -before = { path = "../.." } +# The borsh feature lights the composite streaming doors +# (`Ranked`/`Span::deserialize_reader`), whose walk boundaries the guest +# pins beside the byte doors'; the borsh crate itself names the trait the +# exports drive. +before = { path = "../..", features = ["borsh"] } +borsh = "1" diff --git a/crates/before/wasm32-pins/guest/src/lib.rs b/crates/before/wasm32-pins/guest/src/lib.rs index 27ee57a3..8eadc01f 100644 --- a/crates/before/wasm32-pins/guest/src/lib.rs +++ b/crates/before/wasm32-pins/guest/src/lib.rs @@ -1,7 +1,8 @@ -//! The 32-bit boundary-pin guest: `before`'s public byte decode doors driven -//! at the sizes where 32-bit position arithmetic has boundaries, compiled to -//! wasm32-unknown-unknown so every pin executes under a genuinely 32-bit -//! `usize` with a full 4 GiB address space. +//! The 32-bit boundary-pin guest: `before`'s public surface — the byte and +//! borsh decode doors, the semantic walks and emitters, and rank arithmetic — +//! driven at the sizes where 32-bit position arithmetic has boundaries, +//! compiled to wasm32-unknown-unknown so every pin executes under a genuinely +//! 32-bit `usize` with a full 4 GiB address space. //! //! The host (the `wasm32-pins-harness` crate) instantiates this module under //! wasmtime and calls one export per pinned case. The contract: @@ -22,7 +23,10 @@ //! checks turn exactly those wraps into observable traps instead of wrong //! values downstream code would have to detect after the fact. -use before::{Rank, Version}; +use core::cmp::Ordering; + +use before::{Rank, Ranked, Span, Version}; +use borsh::BorshDeserialize; /// The canonical encoding of a valid single-leaf `Version` padded to exactly /// `n` bytes: one leaf flag, then the Elias-gamma code of the leaf height @@ -201,3 +205,375 @@ pub extern "C" fn pin_rank_decode(exp: u64) -> i64 { } 0 } + +/// Set the stream bit at `pos` (positions are MSB-first over the buffer). +fn set_bit(bytes: &mut [u8], pos: u64) { + let byte = usize::try_from(pos / 8).expect("the stream is addressable"); + bytes[byte] |= 0x80 >> (pos % 8); +} + +/// Set every stream bit in `lo ..= hi` (MSB-first positions). +/// +/// The interior fills byte-at-a-time, so a run of hundreds of megabits is a +/// fill, not a per-bit loop. +fn fill_ones(bytes: &mut [u8], lo: u64, hi: u64) { + assert!(lo <= hi, "a ones run has a nonempty range"); + let lo_byte = usize::try_from(lo / 8).expect("the stream is addressable"); + let hi_byte = usize::try_from(hi / 8).expect("the stream is addressable"); + let lo_mask = 0xFFu8 >> (lo % 8); + let hi_mask = 0xFFu8 << (7 - (hi % 8)); + if lo_byte == hi_byte { + bytes[lo_byte] |= lo_mask & hi_mask; + } else { + bytes[lo_byte] |= lo_mask; + bytes[lo_byte + 1..hi_byte].fill(0xFF); + bytes[hi_byte] |= hi_mask; + } +} + +/// The canonical encoding of the two-leaf version `node(leaf(2^k - 1), +/// leaf(0))`: a tall left plateau over `[0, 1/2)`, height zero over +/// `[1/2, 1)`. +/// +/// The layout, bit by bit (MSB-first): the root's internal flag `0`; the left +/// leaf's flag `1` and its absolute height as gamma(`2^k - 1`) — `k` zeros, +/// then the `k + 1`-bit mantissa `2^k`; the right leaf's flag `1` and its +/// delta `-(2^k - 1)` as zigzag-gamma — the mapped value's successor is +/// `2^(k+1) - 2`, so `k` zeros, then the `k + 1`-bit mantissa `1…10`; the +/// padding marker. Live length `4k + 5` bits. +/// +/// The stream is canonical (the right sibling's delta is nonzero, both +/// heights are naturals), so `Version::decode` accepts it, and joining it +/// with its right-tall dual concatenates rather than collapses. +fn synth_two_leaf_left(k: u64) -> Vec { + assert!(k >= 2, "the layout wants a multi-bit height mantissa"); + let live = 4 * k + 5; + let total_bytes = usize::try_from((live + 1).div_ceil(8)).expect("the stream is addressable"); + let mut bytes = vec![0u8; total_bytes]; + set_bit(&mut bytes, 1); // the left leaf flag + set_bit(&mut bytes, k + 2); // gamma(2^k - 1)'s mantissa lead + set_bit(&mut bytes, 2 * k + 3); // the right leaf flag + fill_ones(&mut bytes, 3 * k + 4, 4 * k + 3); // the delta mantissa's k ones + set_bit(&mut bytes, live); // the padding marker + bytes +} + +/// The canonical encoding of the two-leaf version `node(leaf(0), +/// leaf(2^k - 1 + 2^(j-1)))`: height zero over `[0, 1/2)`, a tall right +/// plateau over `[1/2, 1)`, its height chosen so the join against +/// [`synth_two_leaf_left`]'s value has the exact delta `2^(j-1)`. +/// +/// The layout: the root's `0`; the left leaf's `1` and gamma(0), the single +/// bit `1`; the right leaf's `1` and its delta `+h` as zigzag-gamma — the +/// mapped value's successor is `2^j + 2^(k+1) - 1` (`j >= k + 2` keeps its +/// bit length `j + 1`), so `j` zeros, then the mantissa: a leading `1`, +/// zeros, and `k + 1` trailing ones. Live length `2j + 5` bits. +fn synth_two_leaf_right(k: u64, j: u64) -> Vec { + assert!( + j >= k + 2, + "the delta mantissa must dominate the height's low bits" + ); + let live = 2 * j + 5; + let total_bytes = usize::try_from((live + 1).div_ceil(8)).expect("the stream is addressable"); + let mut bytes = vec![0u8; total_bytes]; + set_bit(&mut bytes, 1); // the left leaf flag + set_bit(&mut bytes, 2); // gamma(0): the single bit `1` + set_bit(&mut bytes, 3); // the right leaf flag + set_bit(&mut bytes, j + 4); // the delta mantissa's leading 1 + fill_ones(&mut bytes, 2 * j + 4 - k, 2 * j + 4); // its k + 1 trailing ones + set_bit(&mut bytes, live); // the padding marker + bytes +} + +/// The canonical rank stream of the integral `2^k - 1` (exponent zero): +/// exactly `Rank::encode` of [`synth_version`]'s decoded value, whose lone +/// root leaf of height `2^k - 1` spans the whole unit interval, so its rank — +/// the area under the skyline — is that height itself. +/// +/// The layout (the inverted-delta integral header, no fraction): for the +/// biased mantissa `m = 2^k`, width `w = k + 1`, and run `rho = bits(w) - 1`: +/// `rho` ones, the terminating zero, the `rho` bits of `w` below its leading +/// bit, the `k` zero mantissa bits below `m`'s leading bit, then the closing +/// `0` and zero padding to the byte boundary. +fn synth_integral_rank(k: u64) -> Vec { + let w = k + 1; + let rho = u64::from(63 - w.leading_zeros()); + let total_bits = 2 * rho + k + 2; + let total_bytes = usize::try_from(total_bits.div_ceil(8)).expect("the stream is addressable"); + let mut bytes = vec![0u8; total_bytes]; + fill_ones(&mut bytes, 0, rho - 1); // the header's rho ones + for i in 0..rho { + // w's bits below its leading bit, MSB-first. + if w >> i & 1 == 1 { + set_bit(&mut bytes, 2 * rho - i); + } + } + bytes +} + +/// The canonical composite key `Ranked::encode` of [`synth_version`]'s +/// `n`-byte value: the rank stream of its integral rank `2^(4n - 5) - 1`, +/// then the version's canonical bytes. Returns the composite and the rank +/// stream's byte length. +fn synth_ranked(n: usize) -> (Vec, usize) { + let k = 4 * n as u64 - 5; + let mut composite = synth_integral_rank(k); + let rank_len = composite.len(); + composite.extend_from_slice(&synth_version(n)); + (composite, rank_len) +} + +/// Decode a valid `n`-byte-version composite key through the byte door +/// `Ranked::decode`, checking the decoded version's bytes round-trip. +/// +/// The door re-derives the version's rank to verify the key's rank component +/// — a whole-stream fold over the version's borrowed bit view — so this +/// export observes the composite door's walk boundary, not the byte doors' +/// (which admit the same version alone up to 512 MiB). +#[no_mangle] +pub extern "C" fn pin_ranked_decode(n_bytes: u64) -> i64 { + let n = match usize::try_from(n_bytes) { + Ok(n) => n, + Err(_) => return -100, + }; + let (composite, rank_len) = synth_ranked(n); + let ranked = match Ranked::decode(&composite[..]) { + Ok(ranked) => ranked, + Err(_) => return -1, + }; + if ranked.version().as_bytes() != &composite[rank_len..] { + return -2; + } + 0 +} + +/// Decode a valid `n`-byte-version composite key through the borsh door +/// `Ranked::deserialize_reader`, checking full consumption and byte +/// round-trip. +/// +/// Same composite and same rank re-derivation as [`pin_ranked_decode`], via +/// the streaming reader the borsh transport uses. +#[no_mangle] +pub extern "C" fn pin_ranked_borsh(n_bytes: u64) -> i64 { + let n = match usize::try_from(n_bytes) { + Ok(n) => n, + Err(_) => return -100, + }; + let (composite, rank_len) = synth_ranked(n); + let mut reader = &composite[..]; + let ranked = match as BorshDeserialize>::deserialize_reader(&mut reader) { + Ok(ranked) => ranked, + Err(_) => return -1, + }; + if !reader.is_empty() { + return -2; + } + if ranked.version().as_bytes() != &composite[rank_len..] { + return -3; + } + 0 +} + +/// Decode a valid coincident span (two byte-equal `n`-byte version streams) +/// through the borsh door `Span::deserialize_reader`, checking full +/// consumption and that both endpoints are the parsed version. +/// +/// The door validates the second stream against the first component's +/// borrowed bit view in one fused admission walk, so this export observes +/// that walk's boundary on the `lo` component; the endpoint checks are byte +/// compares, exact at any storable size. +#[no_mangle] +pub extern "C" fn pin_span_borsh(n_bytes: u64) -> i64 { + let n = match usize::try_from(n_bytes) { + Ok(n) => n, + Err(_) => return -100, + }; + let lo = synth_version(n); + let mut composite = lo.clone(); + composite.extend_from_slice(&lo); + let mut reader = &composite[..]; + let span = match as BorshDeserialize>::deserialize_reader(&mut reader) { + Ok(span) => span, + Err(_) => return -1, + }; + if !reader.is_empty() { + return -2; + } + if span.lo() != span.hi() { + return -3; + } + if span.lo().as_bytes() != &lo[..] { + return -4; + } + 0 +} + +/// Causally compare a valid `n`-byte version against a small one (both lone +/// leaves, so they are comparable), expecting strictly greater both ways +/// around. +/// +/// The comparison sweep reads each operand through its borrowed bit view, so +/// this export observes the walk surface's boundary on ordinary comparison — +/// a decode-free operation pair over already-stored values. +#[no_mangle] +pub extern "C" fn pin_version_cmp(n_bytes: u64) -> i64 { + let n = match usize::try_from(n_bytes) { + Ok(n) => n, + Err(_) => return -100, + }; + let big = match Version::decode(&synth_version(n)[..]) { + Ok(v) => v, + Err(_) => return -1, + }; + let small = match Version::decode(&synth_version(18)[..]) { + Ok(v) => v, + Err(_) => return -2, + }; + if big.partial_cmp(&small) != Some(Ordering::Greater) { + return -3; + } + if small.partial_cmp(&big) != Some(Ordering::Less) { + return -4; + } + 0 +} + +/// Join a valid `n`-byte version with a small one it covers (both lone +/// leaves: pointwise max is the taller), expecting the join to equal the +/// big operand byte for byte. +/// +/// The join walks both operands through their borrowed bit views before its +/// merge emission, so this export observes the walk surface's boundary on +/// the emitting operation class; the result check is a byte compare, exact +/// at any storable size. +#[no_mangle] +pub extern "C" fn pin_version_join_covering(n_bytes: u64) -> i64 { + let n = match usize::try_from(n_bytes) { + Ok(n) => n, + Err(_) => return -100, + }; + let big = match Version::decode(&synth_version(n)[..]) { + Ok(v) => v, + Err(_) => return -1, + }; + let small = match Version::decode(&synth_version(18)[..]) { + Ok(v) => v, + Err(_) => return -2, + }; + let joined = big.join(&small); + if joined != big { + return -3; + } + 0 +} + +/// Join the two-leaf pair [`synth_two_leaf_left`]`(k)` and +/// [`synth_two_leaf_right`]`(k, j)` — each operand well under the walk +/// surface's per-operand bound — and return the joined stream's exact live +/// bit length, `2k + 2j + 5`. +/// +/// The join is `node(leaf(2^k - 1), leaf(2^k - 1 + 2^(j-1)))`: each half of +/// the unit interval takes its taller side, so the output concatenates the +/// left operand's tall code with a fresh `2j + 1`-bit delta code instead of +/// collapsing — the output outgrows both inputs. The harness aims `(k, j)` +/// at the emitter's output-side boundary: the build buffer's own length +/// encoding, not any per-operand bound, is what this export observes. +#[no_mangle] +pub extern "C" fn pin_version_join_emit(k: u64, j: u64) -> i64 { + let a = match Version::decode(&synth_two_leaf_left(k)[..]) { + Ok(v) => v, + Err(_) => return -1, + }; + let b = match Version::decode(&synth_two_leaf_right(k, j)[..]) { + Ok(v) => v, + Err(_) => return -2, + }; + let joined = a.join(&b); + let expected = 2 * k + 2 * j + 5; + if joined.encoded_bits() as u64 != expected { + return -3; + } + i64::try_from(expected).unwrap_or(-4) +} + +/// The small second operand of the rank-arithmetic pins, keyed by its +/// exponent: `0` is the integral rank `1`, `1` is the rank `1/2`, and any +/// larger value is [`synth_rank`]'s fraction at that depth. +fn small_rank(exp: u64) -> Result { + match exp { + 0 => match Version::try_from(1u64) { + Ok(v) => Ok(v.rank()), + Err(_) => Err(-101), + }, + 1 => match "(0, 1, 0)".parse::() { + Ok(v) => Ok(v.rank()), + Err(_) => Err(-102), + }, + exp => Rank::decode(&synth_rank(exp)[..]).map_err(|_| -103), + } +} + +/// Add a rank whose fraction is exactly `2^32` expansion bits deep to a +/// small rank of exponent `small_exp`, checking the sum strictly exceeds +/// both summands. +/// +/// Addition aligns the two numerators to the larger exponent by a left +/// shift of the exponent gap, so the harness aims `small_exp` at the gap +/// boundaries of a 32-bit target: the width the shift amount must fit, and +/// the width the shifted numerator must fit. +#[no_mangle] +pub extern "C" fn pin_rank_add(small_exp: u64) -> i64 { + let bytes = synth_rank(1u64 << 32); + let big = match Rank::decode(&bytes[..]) { + Ok(r) => r, + Err(_) => return -1, + }; + drop(bytes); + let small = match small_rank(small_exp) { + Ok(r) => r, + Err(code) => return code, + }; + let sum = &big + &small; + if sum <= big { + return -2; + } + if sum <= small { + return -3; + } + 0 +} + +/// Subtract a rank whose fraction is exactly `2^32` expansion bits deep +/// from a strictly larger small rank of exponent `small_exp`, checking the +/// difference sits strictly between zero and the minuend. +/// +/// A strictly positive difference aligns both numerators to the larger +/// exponent exactly as addition does, so the harness aims `small_exp` at +/// the same gap boundaries through the subtraction arm. +#[no_mangle] +pub extern "C" fn pin_rank_checked_sub(small_exp: u64) -> i64 { + let bytes = synth_rank(1u64 << 32); + let big = match Rank::decode(&bytes[..]) { + Ok(r) => r, + Err(_) => return -1, + }; + drop(bytes); + let small = match small_rank(small_exp) { + Ok(r) => r, + Err(code) => return code, + }; + // Both small operands exceed `big` (about `2^-65 + 2^-(2^32)`): the + // integral `1` outright, and a synthesized fraction by its earlier + // second set expansion bit at equal magnitude class. + let diff = match small.checked_sub(&big) { + Some(diff) => diff, + None => return -2, + }; + if diff <= Rank::ZERO { + return -3; + } + if diff >= small { + return -4; + } + 0 +} diff --git a/crates/before/wasm32-pins/harness/src/lib.rs b/crates/before/wasm32-pins/harness/src/lib.rs index 2673ea29..e32e2a31 100644 --- a/crates/before/wasm32-pins/harness/src/lib.rs +++ b/crates/before/wasm32-pins/harness/src/lib.rs @@ -12,7 +12,10 @@ use std::path::PathBuf; use std::sync::OnceLock; -use wasmtime::{Engine, Instance, Module, Store, Trap}; +// `Trap` is re-exported so the pin tests can name the exact trap they +// assert without a direct wasmtime dependency edge of their own. +pub use wasmtime::Trap; +use wasmtime::{Engine, Instance, Module, Store}; /// One pin call's outcome: the export's return value, or the trap that /// aborted it. @@ -62,32 +65,44 @@ fn engine_and_module() -> &'static (Engine, Module) { /// Call a nullary pin export in a fresh instance. pub fn call0(export: &str) -> Outcome { - call(export, None) + call(export, &[]) } /// Call a one-argument pin export in a fresh instance. pub fn call1(export: &str, arg: u64) -> Outcome { - call(export, Some(arg)) + call(export, &[arg]) } -fn call(export: &str, arg: Option) -> Outcome { +/// Call a two-argument pin export in a fresh instance. +pub fn call2(export: &str, a: u64, b: u64) -> Outcome { + call(export, &[a, b]) +} + +fn call(export: &str, args: &[u64]) -> Outcome { let (engine, module) = engine_and_module(); let mut store = Store::new(engine, ()); let instance = Instance::new(&mut store, module, &[]).expect("the guest instantiates without imports"); - let result = match arg { - None => { + let result = match *args { + [] => { let func = instance .get_typed_func::<(), i64>(&mut store, export) .expect("the export exists with the pinned signature"); func.call(&mut store, ()) } - Some(arg) => { + [arg] => { let func = instance .get_typed_func::(&mut store, export) .expect("the export exists with the pinned signature"); func.call(&mut store, arg) } + [a, b] => { + let func = instance + .get_typed_func::<(u64, u64), i64>(&mut store, export) + .expect("the export exists with the pinned signature"); + func.call(&mut store, (a, b)) + } + _ => unreachable!("pin exports take at most two arguments"), }; match result { Ok(value) => Outcome::Value(value), diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs index b42942c7..53c2e82f 100644 --- a/crates/before/wasm32-pins/harness/tests/pins.rs +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -9,7 +9,17 @@ //! baseline its cure must move. Each pin's own history of red and green //! lives in this file's git log. -use wasm32_pins_harness::{call0, call1, Outcome}; +use wasm32_pins_harness::{call0, call1, call2, Outcome, Trap}; + +/// The largest buffer byte count whose whole-stream borrowed bit view is +/// constructible on wasm32: `bitvec`'s view length caps at `usize::MAX >> 3` +/// = 2^29 - 1 bits, so 67108863 bytes. +/// +/// The walk surface — comparisons, joins, the composite doors' rank +/// re-derivation and dominance re-walk — reads stored streams through that +/// borrowed view, so this is every walk pin's lower adjacency witness; the +/// byte doors themselves admit streams up to 512 MiB. +const VIEW_CAP_BYTES: u64 = 67_108_863; /// Liveness: a small valid version decodes on wasm32 with the exact bit /// length, round-trips, and rejects mutilations with typed errors. @@ -134,3 +144,371 @@ fn rank_decode_at_backend_byte_capacity() { fn rank_decode_at_usize_exp_boundary() { assert_eq!(call1("pin_rank_decode", 1u64 << 32), Outcome::Value(0)); } + +/// A valid rank whose numerator is exactly the big-integer backend's +/// 32-bit capacity — 2^32 - 32 value bits, from a fraction 2^32 + 32 +/// expansion bits deep opening with 64 zero bits — decodes correctly and +/// orders exactly against reference ranks. +/// +/// The backend caps a magnitude at `usize::MAX / 32` words so bit counts +/// fit `usize`; a numerator of exactly that many bits fills the buffer to +/// its last word. This is the backend-capacity boundary's lower adjacency +/// witness on the numerator's own width (the byte-capacity witness above +/// covers the unstripped image's width). +#[test] +fn rank_decode_at_backend_bit_capacity() { + assert_eq!( + call1("pin_rank_decode", (1u64 << 32) + 32), + Outcome::Value(0), + ); +} + +/// PINNED AS FOUND: a valid rank whose numerator is 2^32 - 24 value bits — +/// one flush group past the backend's 2^32 - 32-bit capacity — traps on +/// wasm32 instead of decoding or rejecting with a typed error. +/// +/// The ~604 MB input and its ~512 MiB numerator both fit the 4 GiB address +/// space with the decode's whole working set beside them, so the boundary +/// is the backend's structural word cap, not memory: the backend clamps +/// the buffer's capacity to its maximum and the fill's release assert +/// panics on the word past it. Unconditional 32-bit correctness requires +/// this input to decode; the cure flips this pin to the correct-value +/// assertion beside its lower witness `rank_decode_at_backend_bit_capacity`. +#[test] +fn rank_decode_past_backend_bit_capacity_traps() { + assert_eq!( + call1("pin_rank_decode", (1u64 << 32) + 40), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// A valid composite key — the rank stream, then the version whose rank it +/// is — decodes through the byte door `Ranked::decode` at the largest +/// version size whose whole-stream bit view is constructible on wasm32. +/// +/// The door re-derives the version's rank to verify the key, and that fold +/// walks the version through its borrowed view: this is the composite +/// door's lower adjacency witness, one byte below the view cap. +#[test] +fn ranked_decode_below_view_cap() { + assert_eq!( + call1("pin_ranked_decode", VIEW_CAP_BYTES), + Outcome::Value(0) + ); +} + +/// PINNED AS FOUND: a valid composite key whose version component is +/// 67108864 bytes — exactly 2^29 bits, the first size past the borrowed +/// view's encoding — traps in the byte door `Ranked::decode` on wasm32 +/// instead of decoding. +/// +/// The rank and version components both parse (the version door's own walk +/// is byte-backed and admits streams to 512 MiB); the door then re-derives +/// the version's rank through its borrowed bit view, which is +/// unconstructible at this size, and the walk panics. Unconditional 32-bit +/// correctness requires every storable key to decode; the cure flips this +/// pin to the correct-value assertion. +#[test] +fn ranked_decode_at_view_cap_traps() { + assert_eq!( + call1("pin_ranked_decode", VIEW_CAP_BYTES + 1), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// PINNED AS FOUND: a valid composite key whose version component is +/// 67108865 bytes — one byte past the view-cap boundary, where the view +/// construction fails by the element-count guard rather than the length +/// encoding — traps in `Ranked::decode` on wasm32 instead of decoding. +/// +/// Together with the pin one byte below, this holds both failure genres of +/// the borrowed-view cap to the same committed baseline the cure must move. +#[test] +fn ranked_decode_past_view_cap_traps() { + assert_eq!( + call1("pin_ranked_decode", VIEW_CAP_BYTES + 2), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// A valid composite key decodes through the borsh door +/// `Ranked::deserialize_reader` at the largest version size whose +/// whole-stream bit view is constructible on wasm32, consuming exactly its +/// own bytes. +/// +/// The streaming door runs the same rank re-derivation as the byte door: +/// this is its lower adjacency witness. +#[test] +fn ranked_borsh_below_view_cap() { + assert_eq!(call1("pin_ranked_borsh", VIEW_CAP_BYTES), Outcome::Value(0)); +} + +/// PINNED AS FOUND: a valid composite key whose version component is +/// 67108864 bytes — exactly 2^29 bits — traps in the borsh door +/// `Ranked::deserialize_reader` on wasm32 instead of deserializing. +/// +/// The streaming reader parses both components byte-backed; the door's +/// rank re-derivation then walks the version through its borrowed bit +/// view, unconstructible at this size. The cure flips this pin to the +/// correct-value assertion. +#[test] +fn ranked_borsh_at_view_cap_traps() { + assert_eq!( + call1("pin_ranked_borsh", VIEW_CAP_BYTES + 1), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// PINNED AS FOUND: a valid composite key whose version component is +/// 67108865 bytes — the view cap's element-count-guard genre — traps in +/// the borsh door `Ranked::deserialize_reader` on wasm32. +/// +/// The second genre's committed baseline beside the pin one byte below. +#[test] +fn ranked_borsh_past_view_cap_traps() { + assert_eq!( + call1("pin_ranked_borsh", VIEW_CAP_BYTES + 2), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// A valid coincident span — two byte-equal version streams — decodes +/// through the borsh door `Span::deserialize_reader` at the largest `lo` +/// size whose whole-stream bit view is constructible on wasm32, consuming +/// exactly its own bytes. +/// +/// The door validates the second stream against `lo`'s borrowed view in +/// one fused admission walk: this is that walk's lower adjacency witness. +/// (The byte door `Span::decode` runs the same admission on raw bytes and +/// is exact to 512 MiB per component; only the streaming door reads +/// through the view.) +#[test] +fn span_borsh_below_view_cap() { + assert_eq!(call1("pin_span_borsh", VIEW_CAP_BYTES), Outcome::Value(0)); +} + +/// PINNED AS FOUND: a valid coincident span whose `lo` component is +/// 67108864 bytes — exactly 2^29 bits — traps in the borsh door +/// `Span::deserialize_reader` on wasm32 instead of deserializing. +/// +/// `lo` itself parses byte-backed; the door then opens `lo`'s borrowed bit +/// view for the dominance re-walk, unconstructible at this size. The byte +/// door decodes this same span exactly, so the boundary is the streaming +/// door's view, not the value. The cure flips this pin to the +/// correct-value assertion. +#[test] +fn span_borsh_at_view_cap_traps() { + assert_eq!( + call1("pin_span_borsh", VIEW_CAP_BYTES + 1), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// PINNED AS FOUND: a valid coincident span whose `lo` component is +/// 67108865 bytes — the view cap's element-count-guard genre — traps in +/// the borsh door `Span::deserialize_reader` on wasm32. +/// +/// The second genre's committed baseline beside the pin one byte below. +#[test] +fn span_borsh_past_view_cap_traps() { + assert_eq!( + call1("pin_span_borsh", VIEW_CAP_BYTES + 2), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// Causal comparison decides a valid stored pair exactly at the largest +/// operand size whose whole-stream bit view is constructible on wasm32: +/// the taller lone leaf reads strictly greater both ways around. +/// +/// The comparison-class walk's lower adjacency witness: ordering reads +/// each operand through its borrowed view, with no decode door in front. +#[test] +fn version_cmp_below_view_cap() { + assert_eq!(call1("pin_version_cmp", VIEW_CAP_BYTES), Outcome::Value(0)); +} + +/// PINNED AS FOUND: causally comparing a valid stored 67108864-byte +/// version — exactly 2^29 bits, the first size past the borrowed view's +/// encoding — traps on wasm32 instead of ordering. +/// +/// The byte doors admit and store this value exactly (its decode is pinned +/// green above), so the boundary is the walk surface's borrowed view, not +/// the value: a stored version this size cannot be compared at all. +/// Unconditional 32-bit correctness requires ordering to be exact at every +/// storable size; the cure flips this pin to the correct-value assertion. +#[test] +fn version_cmp_at_view_cap_traps() { + assert_eq!( + call1("pin_version_cmp", VIEW_CAP_BYTES + 1), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// PINNED AS FOUND: causally comparing a valid stored 67108865-byte +/// version — the view cap's element-count-guard genre — traps on wasm32. +/// +/// The second genre's committed baseline beside the pin one byte below. +#[test] +fn version_cmp_past_view_cap_traps() { + assert_eq!( + call1("pin_version_cmp", VIEW_CAP_BYTES + 2), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// Join emits a covered pair exactly at the largest operand size whose +/// whole-stream bit view is constructible on wasm32: the taller lone leaf +/// joined with a short one reproduces the taller, byte for byte. +/// +/// The join-class walk's lower adjacency witness — and its emission +/// rebuilds the full-size output, so the pin also witnesses the build +/// buffer just below its own cap. +#[test] +fn version_join_below_view_cap() { + assert_eq!( + call1("pin_version_join_covering", VIEW_CAP_BYTES), + Outcome::Value(0), + ); +} + +/// PINNED AS FOUND: joining a valid stored 67108864-byte version — +/// exactly 2^29 bits — with a small one traps on wasm32 instead of +/// emitting the covered result. +/// +/// The join walks both operands through their borrowed views before its +/// merge emission; the big operand's view is unconstructible at this +/// size. The cure flips this pin to the correct-value assertion. +#[test] +fn version_join_at_view_cap_traps() { + assert_eq!( + call1("pin_version_join_covering", VIEW_CAP_BYTES + 1), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// PINNED AS FOUND: joining a valid stored 67108865-byte version — the +/// view cap's element-count-guard genre — with a small one traps on +/// wasm32. +/// +/// The second genre's committed baseline beside the pin one byte below. +#[test] +fn version_join_past_view_cap_traps() { + assert_eq!( + call1("pin_version_join_covering", VIEW_CAP_BYTES + 2), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// Join emits the largest output this operand family can pass through the +/// emitter's finish seam on wasm32 — 536870903 live bits, one bit under +/// the 67108863 whole output bytes the finished stream's bit-vector +/// adoption can represent — from two operands each comfortably under the +/// walk surface's per-operand bound. +/// +/// The operands are complementary two-leaf skylines (~50 MB and ~42 MB) +/// whose join concatenates: the output outgrows both inputs, so this +/// witnesses the emitter's output-side boundary from below, independent of +/// any per-operand cap. The returned observation is the output's exact +/// live bit length. +#[test] +fn version_join_emit_below_build_cap() { + assert_eq!( + call2("pin_version_join_emit", 100_000_000, 168_435_449), + Outcome::Value(536_870_903), + ); +} + +/// PINNED AS FOUND: a join of two valid operands, each comfortably under +/// the walk surface's per-operand bound (~50 MB and ~42 MB), traps on +/// wasm32 when its output's finished byte length reaches 67108864 — +/// 536870905 live bits, the first length whose whole-byte buffer exceeds +/// the bit vector's `usize::MAX >> 3`-bit length encoding when the +/// emitter's byte-backed builder hands the finished stream over — instead +/// of emitting. +/// +/// The boundary is the build-side buffer's length encoding, documented at +/// no public operation, and it sits below the byte doors' 512 MiB storage +/// bound: valid operand pairs admitted and walkable on this target have no +/// emittable join. Unconditional 32-bit correctness requires every +/// storable join to emit; the cure flips this pin to the exact-length +/// assertion beside its witness `version_join_emit_below_build_cap`. +#[test] +fn version_join_emit_at_build_cap_traps() { + assert_eq!( + call2("pin_version_join_emit", 100_000_000, 168_435_450), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// Rank addition is exact just below the 32-bit alignment-gap boundary: a +/// fraction 2^32 expansion bits deep plus one 128 bits deep — an exponent +/// gap of 2^32 - 128, whose aligned numerator still fits the backend — +/// sums to a value strictly above both summands. +/// +/// The gap boundary's lower adjacency witness on the addition arm. +#[test] +fn rank_add_below_gap_boundary() { + assert_eq!(call1("pin_rank_add", 128), Outcome::Value(0)); +} + +/// PINNED AS FOUND: adding the integral rank 1 to a fraction 2^32 +/// expansion bits deep traps on wasm32: the alignment shift's exponent +/// gap is exactly 2^32, one past the widest shift amount a 32-bit target +/// can name. +/// +/// Both operands are honest decoded/derived values and the exact sum is +/// representable in memory; the boundary is the alignment shift's `usize` +/// conversion (loud by construction, documented at the shift). The cure +/// flips this pin to the correct-value assertion. +#[test] +fn rank_add_at_gap_boundary_traps() { + assert_eq!( + call1("pin_rank_add", 0), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// PINNED AS FOUND: adding the rank 1/2 to a fraction 2^32 expansion bits +/// deep traps on wasm32: the exponent gap, 2^32 - 1, fits the shift +/// amount, but the aligned numerator — 2^32 value bits — exceeds the +/// backend's 2^32 - 32-bit capacity. +/// +/// The gap boundary's other genre: just below the shift-amount seam the +/// terminal moves from the shift's conversion to the backend's capacity +/// assert, so no sub-boundary gap is safe unless the aligned width also +/// fits. The cure flips this pin to the correct-value assertion. +#[test] +fn rank_add_below_gap_wide_result_traps() { + assert_eq!( + call1("pin_rank_add", 1), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + +/// Rank subtraction is exact just below the 32-bit alignment-gap +/// boundary: a fraction 128 expansion bits deep minus a smaller one 2^32 +/// bits deep — the same 2^32 - 128 gap as the addition witness — yields a +/// difference strictly between zero and the minuend. +/// +/// The gap boundary's lower adjacency witness on the subtraction arm. +#[test] +fn rank_checked_sub_below_gap_boundary() { + assert_eq!(call1("pin_rank_checked_sub", 128), Outcome::Value(0)); +} + +/// PINNED AS FOUND: subtracting a fraction 2^32 expansion bits deep from +/// the integral rank 1 traps on wasm32: the strictly positive difference +/// aligns the minuend by a shift whose exponent gap is exactly 2^32, one +/// past the widest amount a 32-bit target can name. +/// +/// The ordering pre-check settles sign without alignment, so `None` and +/// zero results stay exact at any gap; only the positive arm reaches the +/// shift. The cure flips this pin to the correct-value assertion. +#[test] +fn rank_checked_sub_at_gap_boundary_traps() { + assert_eq!( + call1("pin_rank_checked_sub", 0), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} diff --git a/justfile b/justfile index 8ad50c35..b6419142 100644 --- a/justfile +++ b/justfile @@ -587,13 +587,16 @@ fuzzfit-calibrate: fuzzfit-build # never compile it, and wasmtime stays out of the production crates' # graph); the gate reaches it only through these recipes by name. # `wasm-check` proves before *compiles* for a 32-bit target; this leg is -# the tree's one place 32-bit code *executes*: the guest drives the public -# byte decode doors at the exact sizes where 32-bit position arithmetic -# has boundaries (bitvec's borrowed-view cap, the usize bit-length -# boundary at 512 MiB, the rank exponent's usize seam), and the harness -# pins each boundary's exact outcome, with adjacency witnesses beside -# each boundary so a failure is attributable to its seam. Pins land -# red-first when a seam is found; each pin's history lives in git. +# the tree's one place 32-bit code *executes*: the guest drives the +# public surface — the byte and borsh decode doors, the semantic walks +# and emitters, and rank arithmetic — at the exact sizes where 32-bit +# position arithmetic has boundaries (bitvec's borrowed-view cap on the +# walk surface, the emitter's build-buffer cap, the usize bit-length +# boundary at 512 MiB, the rank exponent's usize seam, the big-integer +# backend's word cap, the rank alignment-gap seam), and the harness pins +# each boundary's exact outcome, with adjacency witnesses beside each +# boundary so a failure is attributable to its seam. Pins land red-first +# when a seam is found; each pin's history lives in git. # The guest builds with overflow checks on, # so a 32-bit wrap is an observable trap, never a silently wrong value. From 16cb4dfb2b4cfbd5dcaa59ae39a8ab1f77adb240 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 17:35:05 -0400 Subject: [PATCH 11/20] wasm32-pins: keep the new pins' doc summaries under the doclint cap The gate's doclint caps a doc comment's first sentence at 220 rendered characters; fourteen of the new pins' summaries ran long. Each now opens with the short claim and carries its qualifiers in the body, unchanged in content. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/wasm32-pins/guest/src/lib.rs | 31 +++--- .../before/wasm32-pins/harness/tests/pins.rs | 96 ++++++++++--------- 2 files changed, 68 insertions(+), 59 deletions(-) diff --git a/crates/before/wasm32-pins/guest/src/lib.rs b/crates/before/wasm32-pins/guest/src/lib.rs index 8eadc01f..c9f232d5 100644 --- a/crates/before/wasm32-pins/guest/src/lib.rs +++ b/crates/before/wasm32-pins/guest/src/lib.rs @@ -259,9 +259,11 @@ fn synth_two_leaf_left(k: u64) -> Vec { } /// The canonical encoding of the two-leaf version `node(leaf(0), -/// leaf(2^k - 1 + 2^(j-1)))`: height zero over `[0, 1/2)`, a tall right -/// plateau over `[1/2, 1)`, its height chosen so the join against -/// [`synth_two_leaf_left`]'s value has the exact delta `2^(j-1)`. +/// leaf(2^k - 1 + 2^(j-1)))`. +/// +/// Height zero over `[0, 1/2)`, a tall right plateau over `[1/2, 1)`, its +/// height chosen so the join against [`synth_two_leaf_left`]'s value has +/// the exact delta `2^(j-1)`. /// /// The layout: the root's `0`; the left leaf's `1` and gamma(0), the single /// bit `1`; the right leaf's `1` and its delta `+h` as zigzag-gamma — the @@ -285,10 +287,11 @@ fn synth_two_leaf_right(k: u64, j: u64) -> Vec { bytes } -/// The canonical rank stream of the integral `2^k - 1` (exponent zero): -/// exactly `Rank::encode` of [`synth_version`]'s decoded value, whose lone -/// root leaf of height `2^k - 1` spans the whole unit interval, so its rank — -/// the area under the skyline — is that height itself. +/// The canonical rank stream of the integral `2^k - 1` (exponent zero). +/// +/// Exactly `Rank::encode` of [`synth_version`]'s decoded value: the lone +/// root leaf of height `2^k - 1` spans the whole unit interval, so its rank +/// — the area under the skyline — is that height itself. /// /// The layout (the inverted-delta integral header, no fraction): for the /// biased mantissa `m = 2^k`, width `w = k + 1`, and run `rho = bits(w) - 1`: @@ -312,9 +315,11 @@ fn synth_integral_rank(k: u64) -> Vec { } /// The canonical composite key `Ranked::encode` of [`synth_version`]'s -/// `n`-byte value: the rank stream of its integral rank `2^(4n - 5) - 1`, -/// then the version's canonical bytes. Returns the composite and the rank -/// stream's byte length. +/// `n`-byte value. +/// +/// The rank stream of its integral rank `2^(4n - 5) - 1`, then the +/// version's canonical bytes. Returns the composite and the rank stream's +/// byte length. fn synth_ranked(n: usize) -> (Vec, usize) { let k = 4 * n as u64 - 5; let mut composite = synth_integral_rank(k); @@ -468,10 +473,10 @@ pub extern "C" fn pin_version_join_covering(n_bytes: u64) -> i64 { } /// Join the two-leaf pair [`synth_two_leaf_left`]`(k)` and -/// [`synth_two_leaf_right`]`(k, j)` — each operand well under the walk -/// surface's per-operand bound — and return the joined stream's exact live -/// bit length, `2k + 2j + 5`. +/// [`synth_two_leaf_right`]`(k, j)`, returning the joined stream's exact +/// live bit length, `2k + 2j + 5`. /// +/// Each operand sits well under the walk surface's per-operand bound. /// The join is `node(leaf(2^k - 1), leaf(2^k - 1 + 2^(j-1)))`: each half of /// the unit interval takes its taller side, so the output concatenates the /// left operand's tall code with a fresh `2j + 1`-bit delta code instead of diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs index 53c2e82f..e203cb93 100644 --- a/crates/before/wasm32-pins/harness/tests/pins.rs +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -145,11 +145,12 @@ fn rank_decode_at_usize_exp_boundary() { assert_eq!(call1("pin_rank_decode", 1u64 << 32), Outcome::Value(0)); } -/// A valid rank whose numerator is exactly the big-integer backend's -/// 32-bit capacity — 2^32 - 32 value bits, from a fraction 2^32 + 32 -/// expansion bits deep opening with 64 zero bits — decodes correctly and -/// orders exactly against reference ranks. +/// A valid rank whose numerator exactly fills the big-integer backend's +/// 32-bit capacity decodes correctly and orders exactly against reference +/// ranks. /// +/// The numerator is 2^32 - 32 value bits, from a fraction 2^32 + 32 +/// expansion bits deep opening with 64 zero bits. /// The backend caps a magnitude at `usize::MAX / 32` words so bit counts /// fit `usize`; a numerator of exactly that many bits fills the buffer to /// its last word. This is the backend-capacity boundary's lower adjacency @@ -198,10 +199,11 @@ fn ranked_decode_below_view_cap() { } /// PINNED AS FOUND: a valid composite key whose version component is -/// 67108864 bytes — exactly 2^29 bits, the first size past the borrowed -/// view's encoding — traps in the byte door `Ranked::decode` on wasm32 +/// 67108864 bytes traps in the byte door `Ranked::decode` on wasm32 /// instead of decoding. /// +/// The component is exactly 2^29 bits: the first size past the borrowed +/// view's encoding. /// The rank and version components both parse (the version door's own walk /// is byte-backed and admits streams to 512 MiB); the door then re-derives /// the version's rank through its borrowed bit view, which is @@ -217,12 +219,12 @@ fn ranked_decode_at_view_cap_traps() { } /// PINNED AS FOUND: a valid composite key whose version component is -/// 67108865 bytes — one byte past the view-cap boundary, where the view -/// construction fails by the element-count guard rather than the length -/// encoding — traps in `Ranked::decode` on wasm32 instead of decoding. +/// 67108865 bytes traps in `Ranked::decode` on wasm32 instead of decoding. /// -/// Together with the pin one byte below, this holds both failure genres of -/// the borrowed-view cap to the same committed baseline the cure must move. +/// One byte past the view-cap boundary, the view construction fails by the +/// element-count guard rather than the length encoding. Together with the +/// pin one byte below, this holds both failure genres of the borrowed-view +/// cap to the same committed baseline the cure must move. #[test] fn ranked_decode_past_view_cap_traps() { assert_eq!( @@ -274,10 +276,10 @@ fn ranked_borsh_past_view_cap_traps() { /// A valid coincident span — two byte-equal version streams — decodes /// through the borsh door `Span::deserialize_reader` at the largest `lo` -/// size whose whole-stream bit view is constructible on wasm32, consuming -/// exactly its own bytes. +/// size whose whole-stream bit view is constructible on wasm32. /// -/// The door validates the second stream against `lo`'s borrowed view in +/// The door consumes exactly its own bytes. +/// It validates the second stream against `lo`'s borrowed view in /// one fused admission walk: this is that walk's lower adjacency witness. /// (The byte door `Span::decode` runs the same admission on raw bytes and /// is exact to 512 MiB per component; only the streaming door reads @@ -401,16 +403,16 @@ fn version_join_past_view_cap_traps() { } /// Join emits the largest output this operand family can pass through the -/// emitter's finish seam on wasm32 — 536870903 live bits, one bit under -/// the 67108863 whole output bytes the finished stream's bit-vector -/// adoption can represent — from two operands each comfortably under the -/// walk surface's per-operand bound. +/// emitter's finish seam on wasm32: 536870903 live bits. /// -/// The operands are complementary two-leaf skylines (~50 MB and ~42 MB) -/// whose join concatenates: the output outgrows both inputs, so this -/// witnesses the emitter's output-side boundary from below, independent of -/// any per-operand cap. The returned observation is the output's exact -/// live bit length. +/// That is one bit under the 67108863 whole output bytes the finished +/// stream's bit-vector adoption can represent, from two operands each +/// comfortably under the walk surface's per-operand bound. The operands +/// are complementary two-leaf skylines (~50 MB and ~42 MB) whose join +/// concatenates: the output outgrows both inputs, so this witnesses the +/// emitter's output-side boundary from below, independent of any +/// per-operand cap. The returned observation is the output's exact live +/// bit length. #[test] fn version_join_emit_below_build_cap() { assert_eq!( @@ -419,14 +421,14 @@ fn version_join_emit_below_build_cap() { ); } -/// PINNED AS FOUND: a join of two valid operands, each comfortably under -/// the walk surface's per-operand bound (~50 MB and ~42 MB), traps on -/// wasm32 when its output's finished byte length reaches 67108864 — -/// 536870905 live bits, the first length whose whole-byte buffer exceeds -/// the bit vector's `usize::MAX >> 3`-bit length encoding when the -/// emitter's byte-backed builder hands the finished stream over — instead -/// of emitting. +/// PINNED AS FOUND: a join of two valid operands, each under every +/// per-operand bound, traps on wasm32 instead of emitting when its +/// output's finished byte length reaches 67108864. /// +/// That is 536870905 live bits: the first length whose whole-byte buffer +/// exceeds the bit vector's `usize::MAX >> 3`-bit length encoding when the +/// emitter's byte-backed builder hands the finished stream over. The +/// operands are complementary two-leaf skylines, ~50 MB and ~42 MB. /// The boundary is the build-side buffer's length encoding, documented at /// no public operation, and it sits below the byte doors' 512 MiB storage /// bound: valid operand pairs admitted and walkable on this target have no @@ -441,12 +443,12 @@ fn version_join_emit_at_build_cap_traps() { ); } -/// Rank addition is exact just below the 32-bit alignment-gap boundary: a -/// fraction 2^32 expansion bits deep plus one 128 bits deep — an exponent -/// gap of 2^32 - 128, whose aligned numerator still fits the backend — -/// sums to a value strictly above both summands. +/// Rank addition is exact just below the 32-bit alignment-gap boundary. /// -/// The gap boundary's lower adjacency witness on the addition arm. +/// A fraction 2^32 expansion bits deep plus one 128 bits deep — an +/// exponent gap of 2^32 - 128, whose aligned numerator still fits the +/// backend — sums to a value strictly above both summands. The gap +/// boundary's lower adjacency witness on the addition arm. #[test] fn rank_add_below_gap_boundary() { assert_eq!(call1("pin_rank_add", 128), Outcome::Value(0)); @@ -470,10 +472,11 @@ fn rank_add_at_gap_boundary_traps() { } /// PINNED AS FOUND: adding the rank 1/2 to a fraction 2^32 expansion bits -/// deep traps on wasm32: the exponent gap, 2^32 - 1, fits the shift -/// amount, but the aligned numerator — 2^32 value bits — exceeds the -/// backend's 2^32 - 32-bit capacity. +/// deep traps on wasm32. /// +/// The exponent gap, 2^32 - 1, fits the shift amount, but the aligned +/// numerator — 2^32 value bits — exceeds the backend's 2^32 - 32-bit +/// capacity. /// The gap boundary's other genre: just below the shift-amount seam the /// terminal moves from the shift's conversion to the backend's capacity /// assert, so no sub-boundary gap is safe unless the aligned width also @@ -486,22 +489,23 @@ fn rank_add_below_gap_wide_result_traps() { ); } -/// Rank subtraction is exact just below the 32-bit alignment-gap -/// boundary: a fraction 128 expansion bits deep minus a smaller one 2^32 -/// bits deep — the same 2^32 - 128 gap as the addition witness — yields a -/// difference strictly between zero and the minuend. +/// Rank subtraction is exact just below the 32-bit alignment-gap boundary. /// -/// The gap boundary's lower adjacency witness on the subtraction arm. +/// A fraction 128 expansion bits deep minus a smaller one 2^32 bits deep — +/// the same 2^32 - 128 gap as the addition witness — yields a difference +/// strictly between zero and the minuend. The gap boundary's lower +/// adjacency witness on the subtraction arm. #[test] fn rank_checked_sub_below_gap_boundary() { assert_eq!(call1("pin_rank_checked_sub", 128), Outcome::Value(0)); } /// PINNED AS FOUND: subtracting a fraction 2^32 expansion bits deep from -/// the integral rank 1 traps on wasm32: the strictly positive difference -/// aligns the minuend by a shift whose exponent gap is exactly 2^32, one -/// past the widest amount a 32-bit target can name. +/// the integral rank 1 traps on wasm32. /// +/// The strictly positive difference aligns the minuend by a shift whose +/// exponent gap is exactly 2^32, one past the widest amount a 32-bit +/// target can name. /// The ordering pre-check settles sign without alignment, so `None` and /// zero results stay exact at any gap; only the positive arm reaches the /// shift. The cure flips this pin to the correct-value assertion. From 5d167a6367fc7c48fa3261cd572d8113786ad1fe Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 19:37:44 -0400 Subject: [PATCH 12/20] before: replace the borrowed bit view with the crate-owned BitsView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The walk surface reads stored streams and door buffers through BitsView — borrowed bytes beside a u64 live bit length — so every semantic walk (comparisons, joins' input side, admission walks, rank folds, the id operations) is exact at every storable size on every target. Bits's Deref to a borrowed bit slice, whose 32-bit length encoding capped walks at 64 MiB, is deleted rather than widened; Bits::live() is the explicit accessor in its place. Views are byte-aligned by construction, so sub-stream ranges travel as explicit (view, start, end) bit positions at the copy and parse seams. The parallel byte-form duals existed only because the slice form capped; each collapses to one form under the plain name: require_marker_padding_bytes -> require_marker_padding, parse_id_bytes -> parse_id, validate_prefix_bytes -> validate_prefix, validate_dominating_bytes -> validate_dominating_from, decode_int_window_bytes -> decode_int_window, DsiCursor::over_bytes/over_bytes_live -> DsiCursor::new (BitsView::whole / BitsView::new carry the byte doors' forms), LeafCursor::open_bytes -> LeafCursor::open. codec::byte_view dissolves into BitsView::body_tail — views are byte-aligned by type, so its mid-byte None arm has no representable input. The never-a-borrowed-bit-view door comments are excised; prose is restated against the view. The wasm32 pins flip in the same commit, per the pins file's own red-first discipline: the composite-door pins (Ranked byte + borsh, borsh Span) and the comparison pins at the 2^29-bit boundary go from trap-assertions to correct-value assertions, with every adjacency witness unchanged, and four new pins spot-check exactness deep in the storable range (comparison at the 512 MiB storage bound; the composite doors at 128-256 MiB components). The covered-join pins stay pinned as traps with their mechanism re-attributed: the walk is exact at those sizes, and the residual trap is the emitters' build-buffer length encoding at the freeze seam — the same boundary the emitter-output pin holds, cured by the crate-owned build buffer, not by this change. The rank-cap and gap-boundary pins stay as found. The skyline decode wrap now allocates its copy once, exactly sized plus the marker bit (the meter envelopes tighten in the next commit). The mutants roster's one line-pinned recode entry re-aims 457:38 -> 468:38 (same width-tie comparison, same equivalence argument; the counts re-verify under just mutants-list). Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- .cargo/mutants.toml | 2 +- crates/before/src/borsh_impls.rs | 13 +- crates/before/src/borsh_impls/tests.rs | 6 +- crates/before/src/causally/query.rs | 14 +- crates/before/src/clock.rs | 16 +- crates/before/src/codec.rs | 20 +- crates/before/src/codec/bits.rs | 361 +++++++++++++----- crates/before/src/codec/build.rs | 68 ++-- crates/before/src/codec/code.rs | 29 +- crates/before/src/codec/cursor.rs | 33 +- crates/before/src/codec/display.rs | 4 +- crates/before/src/codec/dsi.rs | 98 ++--- crates/before/src/codec/dsi/tests.rs | 35 +- crates/before/src/codec/gamma.rs | 28 +- crates/before/src/codec/literal.rs | 12 +- crates/before/src/codec/stack.rs | 6 +- crates/before/src/codec/tests.rs | 127 +++--- crates/before/src/codec/text.rs | 2 +- crates/before/src/codec/tree.rs | 23 +- crates/before/src/idbits.rs | 35 +- crates/before/src/meter.rs | 10 +- crates/before/src/meter/board/ceilings.rs | 2 +- crates/before/src/meter/board/defect.rs | 26 +- crates/before/src/meter/board/family.rs | 23 +- crates/before/src/meter/board/operand.rs | 34 +- crates/before/src/meter/board/tests.rs | 2 +- crates/before/src/meter/tests.rs | 4 +- crates/before/src/meter/tier2.rs | 8 +- crates/before/src/meter/tier2/tests.rs | 80 +++- crates/before/src/party.rs | 30 +- crates/before/src/party/ops/build.rs | 21 +- crates/before/src/party/ops/diff.rs | 23 +- crates/before/src/party/ops/index.rs | 30 +- crates/before/src/party/ops/split.rs | 35 +- crates/before/src/party/ops/sum.rs | 2 +- crates/before/src/party/ops/sum_split.rs | 64 ++-- crates/before/src/party/tests.rs | 39 +- crates/before/src/span.rs | 24 +- crates/before/src/span/wire.rs | 27 +- crates/before/src/testing/bridge.rs | 18 +- crates/before/src/testing/compactness.rs | 6 +- crates/before/src/testing/snapshots.rs | 20 +- crates/before/src/version.rs | 75 ++-- crates/before/src/version/own.rs | 29 +- crates/before/src/version/ranked.rs | 2 +- crates/before/src/version/skyline.rs | 35 +- crates/before/src/version/skyline/admit.rs | 38 +- crates/before/src/version/skyline/build.rs | 37 +- .../before/src/version/skyline/build/tests.rs | 33 +- crates/before/src/version/skyline/decode.rs | 10 +- crates/before/src/version/skyline/emit.rs | 20 +- .../before/src/version/skyline/emit/tests.rs | 116 ++++-- crates/before/src/version/skyline/encode.rs | 10 +- crates/before/src/version/skyline/fill.rs | 54 +-- .../before/src/version/skyline/fill/fuse.rs | 41 +- .../before/src/version/skyline/fill/memo.rs | 6 +- .../src/version/skyline/fill/prescan.rs | 18 +- .../before/src/version/skyline/fill/tests.rs | 85 +++-- crates/before/src/version/skyline/grow.rs | 93 +++-- .../before/src/version/skyline/grow/tests.rs | 26 +- crates/before/src/version/skyline/literal.rs | 6 +- crates/before/src/version/skyline/masked.rs | 26 +- .../src/version/skyline/masked/tests.rs | 35 +- crates/before/src/version/skyline/overlay.rs | 27 +- crates/before/src/version/skyline/place.rs | 18 +- .../src/version/skyline/place/filter.rs | 12 +- .../before/src/version/skyline/place/tests.rs | 62 +-- crates/before/src/version/skyline/query.rs | 26 +- .../before/src/version/skyline/query/tests.rs | 121 ++++-- crates/before/src/version/skyline/sweep.rs | 16 +- .../before/src/version/skyline/sweep/tests.rs | 43 ++- crates/before/src/version/skyline/tests.rs | 67 +++- crates/before/src/version/skyline/text.rs | 6 +- .../before/src/version/skyline/text/tests.rs | 11 +- crates/before/src/version/skyline/validate.rs | 23 +- crates/before/src/version/tests.rs | 2 +- crates/before/tests/meter.rs | 90 ++--- crates/before/wasm32-pins/guest/src/lib.rs | 36 +- .../before/wasm32-pins/harness/tests/pins.rs | 337 ++++++++-------- tools/mutantcheck-expected.json | 2 +- 80 files changed, 1800 insertions(+), 1354 deletions(-) diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index 1c2661d4..47fa687c 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -139,7 +139,7 @@ exclude_re = [ # Proven equivalent, line-pinned (the match arm's guard shares the # operator pattern and is killed): at equal magnitude the cross and # shrink paths both land on the positive zero zigzag. - "grow\\.rs:457:38: replace < with <= in recode", + "grow\\.rs:468:38: replace < with <= in recode", # --- before::version::skyline::fill / prescan --- diff --git a/crates/before/src/borsh_impls.rs b/crates/before/src/borsh_impls.rs index 30a8a4fb..7e59ccb5 100644 --- a/crates/before/src/borsh_impls.rs +++ b/crates/before/src/borsh_impls.rs @@ -71,9 +71,7 @@ impl<'a, R: Read> ReaderCursor<'a, R> { /// `1000_0000` byte the parse never pulled, and leaving it unread would /// hand its bits to the next borsh field. After it, the buffered bytes /// are exactly the tree's marker-padded canonical spelling — the at-rest - /// storage form, adopted without a copy or a borrowed bit view (whose - /// encoding caps below the field sizes this reader admits on 32-bit - /// targets). + /// storage form, adopted without a copy). fn finish(mut self) -> Result, Decode> { if !self.read_bit()? { return Err(Decode::TrailingBits); @@ -122,7 +120,9 @@ impl BitCursor for ReaderCursor<'_, R> { // construction. It fires when earlier refills left enough unconsumed // bits buffered; everything else, every reject included, is decided // by the per-bit loop below, refilling byte by byte on demand. - if let Some((n, next)) = codec::decode_int_window_bytes(&self.bytes, self.position) { + if let Some((n, next)) = + codec::decode_int_window(codec::BitsView::whole(&self.bytes), self.position) + { self.position = next; return Ok(codec::Int::Small(n)); } @@ -281,8 +281,9 @@ impl BorshDeserialize for Span<'static> { use crate::version::skyline::Admission; let lo = Version::deserialize_reader(reader)?; let mut cursor = ReaderCursor::new(reader); - let admission = crate::version::skyline::validate_dominating_from(lo.view(), &mut cursor) - .map_err(decode_error)?; + let admission = + crate::version::skyline::validate_dominating_from(lo.view().live(), &mut cursor) + .map_err(decode_error)?; // The final byte's padding check outranks the pair verdict, // exactly as the byte-slice decode orders them. let bytes = cursor.finish().map_err(decode_error)?; diff --git a/crates/before/src/borsh_impls/tests.rs b/crates/before/src/borsh_impls/tests.rs index ea72530b..bbc87421 100644 --- a/crates/before/src/borsh_impls/tests.rs +++ b/crates/before/src/borsh_impls/tests.rs @@ -315,7 +315,9 @@ impl BitCursor for BitwiseReaderCursor<'_, R> { if self.position == self.bits.len() { let mut byte = [0]; self.reader.read_exact(&mut byte).map_err(Decode::Io)?; - self.bits.extend_from_bitslice(codec::bytes_as_bits(&byte)); + use bitvec::view::BitView; + self.bits + .extend_from_bitslice(byte.view_bits::()); } let bit = self.bits[self.position]; self.position += 1; @@ -523,7 +525,7 @@ fn reference_span(reader: &mut R) -> Result, Decode> { bits: BitsMut::new(), position: 0, }; - let admission = validate_dominating_from(lo.view(), &mut cursor)?; + let admission = validate_dominating_from((lo.view()).live(), &mut cursor)?; let position = cursor.position; reference_consume_padding(&mut cursor)?; let mut bits = cursor.bits; diff --git a/crates/before/src/causally/query.rs b/crates/before/src/causally/query.rs index 9e890e39..b28a33e2 100644 --- a/crates/before/src/causally/query.rs +++ b/crates/before/src/causally/query.rs @@ -13,7 +13,7 @@ use std::marker::PhantomData; use super::polarity::{Hole, Neutral, Polarity}; use super::{le, Version}; -use crate::codec::BitsSlice; +use crate::codec::BitsView; use crate::span::Span; use crate::version::skyline::place::filter::{self, Demand}; @@ -71,20 +71,20 @@ impl<'a, P: Polarity> Query<'a, P> { /// The stored bounds as (stream, demand) pairs, in the walks' deterministic /// read order: floor, holes in stored order, ceiling. - fn demands(&self) -> impl Iterator { + fn demands(&self) -> impl Iterator, Demand)> { self.floor .as_deref() - .map(|p| (&**p.view(), Demand::After)) + .map(|p| (p.view().live(), Demand::After)) .into_iter() .chain( self.holes .iter() - .map(|hole| (&**hole.at.view(), P::hole_demand(hole.strict))), + .map(|hole| (hole.at.view().live(), P::hole_demand(hole.strict))), ) .chain( self.ceiling .as_deref() - .map(|e| (&**e.view(), Demand::Before)), + .map(|e| (e.view().live(), Demand::Before)), ) } @@ -103,7 +103,7 @@ impl<'a, P: Polarity> Query<'a, P> { #[doc = include_str!(concat!(env!("OUT_DIR"), "/fuelscapes/query_contains_ceiling_hole.html"))] #[doc = include_str!(concat!(env!("OUT_DIR"), "/fuelscapes/query_contains_floor_ceiling_hole.html"))] pub fn contains(&self, version: &Version) -> bool { - filter::admits(version.view(), self.demands()) + filter::admits(version.view().live(), self.demands()) } /// How much of `span` this query admits. @@ -133,7 +133,7 @@ impl<'a, P: Polarity> Query<'a, P> { Coverage::Empty }; } - match filter::coverage(lo.view(), hi.view(), self.demands()) { + match filter::coverage(lo.view().live(), hi.view().live(), self.demands()) { Coverage::Full => Coverage::Full, Coverage::Empty => Coverage::Empty, Coverage::Partial => self.refine_partial(lo, hi), diff --git a/crates/before/src/clock.rs b/crates/before/src/clock.rs index 91c81f7f..498d0cdd 100644 --- a/crates/before/src/clock.rs +++ b/crates/before/src/clock.rs @@ -755,12 +755,10 @@ impl Clock { // exhausted input. Both parts then adopt slices of the ONE read buffer // as their storage: no per-component copy, and the id is parsed once // where handing byte ranges to the component decoders re-parsed it. - // Both components validate over the raw bytes (never a borrowed bit - // view, whose encoding caps below the buffer sizes this door admits - // on 32-bit targets); each walk's input is its component's whole - // byte range as bits, padding included, judged by its marker check. + // Each walk's input is its component's whole byte range as bits, + // padding included, judged by its marker check. let id_bytes = { - let id_end = codec::parse_id_bytes(&buf)?; + let id_end = codec::parse_id(codec::BitsView::whole(&buf), 0)?; // The party's padding marker rides in its final byte — which an // input cut right after a flush id tree lacks. That cut is // missing required data (the marker byte, and the whole version @@ -772,10 +770,10 @@ impl Clock { } let id_bytes = usize::try_from(id_bytes).expect("the id prefix ends within the read buffer"); - codec::require_marker_padding_bytes(&buf[..id_bytes], id_end)?; + codec::require_marker_padding(&buf[..id_bytes], id_end)?; let tail = &buf[id_bytes..]; - let v_end = crate::version::skyline::validate_prefix_bytes(tail)?; - codec::require_marker_padding_bytes(tail, v_end)?; + let v_end = crate::version::skyline::validate_prefix(codec::BitsView::whole(tail))?; + codec::require_marker_padding(tail, v_end)?; id_bytes }; let buf = bytes::Bytes::from(buf); @@ -903,7 +901,7 @@ impl FromStr for Clock { fn from_str(s: &str) -> Result { let (id, ev) = codec::parse_clock_str(s)?; let version: Version = ev.parse()?; - if codec::id_is_empty(&id) { + if codec::id_is_empty(codec::built_view(&id)) { return Err(Parse::Anonymous); } Ok(Clock::from_parts(Party::from_bits(id), version)) diff --git a/crates/before/src/codec.rs b/crates/before/src/codec.rs index cfcbc1c2..273ffa92 100644 --- a/crates/before/src/codec.rs +++ b/crates/before/src/codec.rs @@ -39,14 +39,9 @@ mod tests; pub(crate) use base::limb_meter; pub use base::Base; pub(crate) use bits::{ - byte_view, canonical_eq, canonical_hash, padding_is_canonical, require_marker_padding_bytes, - slice_ptr_eq, + built_view, canonical_eq, canonical_hash, extend_from_view, padding_is_canonical, + require_marker_padding, }; -// Whole-buffer borrowed views serve only the meter surface and the test -// suites: the decode doors walk raw bytes instead (the view encoding caps -// below the buffer sizes the doors admit on 32-bit targets). -#[cfg(any(test, feature = "meter"))] -pub(crate) use bits::bytes_as_bits; // Production streams seal at the freeze seam (`Bits::freeze`); the // standalone form serves the buffers that stay build-side, all of them // meter/test instruments producing decodable bytes (the generators' @@ -55,7 +50,7 @@ pub(crate) use bits::bytes_as_bits; pub(crate) use bits::seal_padding; // The storage forms are `pub` (the enclosing module is not), so the // meter surface can re-export them for the resource-envelope suite. -pub use bits::{Bits, BitsMut, BitsSlice}; +pub use bits::{Bits, BitsMut, BitsView}; pub(crate) use build::PackedBuilder; pub(crate) use code::Code; pub(crate) use cursor::{BitCursor, SliceCursor}; @@ -67,16 +62,11 @@ pub(crate) use int::Int; // `ReaderCursor` (`borsh_impls`), the one consumer outside this module; the // cfg keeps the re-export from dangling when `borsh` is off. #[cfg(feature = "borsh")] -// The raw-byte window serves the wire-side (borsh) reader, whose buffered -// bytes have no borrowed bit view once they outgrow the view encoding's cap; -// the slice-form window stays inside the codec (the slice cursor's fast -// path reaches it by module path). -#[cfg(feature = "borsh")] -pub(crate) use gamma::decode_int_window_bytes; +pub(crate) use gamma::decode_int_window; pub(crate) use literal::{id_is_empty, id_leaf, id_node}; pub(crate) use stack::{BitStack, PopStack}; pub(crate) use text::{parse_clock_str, parse_id_str}; -pub(crate) use tree::{parse_id_bytes, validate_id}; +pub(crate) use tree::{parse_id, validate_id}; // The mid-stream parser entry is consumed only by the borsh wire format // (everything else parses whole streams); gating the re-export keeps default // builds warning-free for downstream consumers. diff --git a/crates/before/src/codec/bits.rs b/crates/before/src/codec/bits.rs index 6b6a82b4..375e7578 100644 --- a/crates/before/src/codec/bits.rs +++ b/crates/before/src/codec/bits.rs @@ -58,9 +58,7 @@ #![allow(rustdoc::private_intra_doc_links)] use core::hash::Hasher; -use core::ops::Deref; -use bitvec::domain::Domain; use bitvec::prelude::*; use bytes::Bytes; @@ -76,9 +74,6 @@ use crate::error::Decode; /// happens, [`Bits`] is the shared, immutable result. pub type BitsMut = BitVec; -/// A borrowed view of a packed bit stream, mutable or frozen. -pub type BitsSlice = BitSlice; - /// The at-rest storage form of a `Party`/`Version`: the canonical packed /// preorder bit stream, marker-padded to a byte boundary, over a refcounted /// byte buffer. @@ -95,11 +90,10 @@ pub type BitsSlice = BitSlice; /// The backing store is [`Bytes`], so [`Clone`] is a refcount bump: two clones /// share one buffer, cost `O(1)`, and that shared identity is observable /// through [`ptr_eq`](Self::ptr_eq) — the fast path the identity-law shortcuts -/// (`x ∨ x`, `cmp(x, x)`, `distance(x, x)`) dispatch on. Reading is still -/// `bitvec`'s: the struct [derefs](core::ops::Deref) to [`BitsSlice`], which -/// exposes exactly the live bits — the padding stays behind the view — so -/// every cursor and walk consumes the frozen form exactly as it consumes a -/// [`BitsMut`]. +/// (`x ∨ x`, `cmp(x, x)`, `distance(x, x)`) dispatch on. Reading is the +/// crate-owned [`BitsView`] ([`live`](Self::live)), which exposes exactly the +/// live bits — the padding stays behind the view — at every storable size on +/// every target. #[derive(Clone)] pub struct Bits { /// The canonical marker-padded bytes: the live bits, one `1`, then @@ -143,12 +137,13 @@ impl Bits { /// # Panics /// /// Panics when the buffer's bit count exceeds `usize` — reachable only on - /// targets narrower than 64 bits, from 512 MiB of buffer. Stored bit - /// positions and lengths are `usize`-denominated throughout the crate, so - /// a stream past that bound has no in-memory form here: adopting it would - /// make [`len`](Self::len) and every walk incorrect, and this check keeps - /// the failure at the door instead. On 64-bit targets the bound is 2^61 - /// bytes, which no allocator can hand over, so the check is dead there. + /// targets narrower than 64 bits, from 512 MiB of buffer. A stored + /// stream's live length is `usize`-denominated ([`len`](Self::len) and + /// the value types' `encoded_bits`), so a stream past that bound has no + /// in-memory form here: adopting it would make [`len`](Self::len) + /// incorrect, and this check keeps the failure at the door instead. On + /// 64-bit targets the bound is 2^61 bytes, which no allocator can hand + /// over, so the check is dead there. pub(crate) fn from_canonical(bytes: Bytes) -> Self { assert!( bytes.len() as u128 <= (usize::MAX as u128 + 1) / 8, @@ -164,7 +159,8 @@ impl Bits { bits } - /// The live bit length of the stream, recovered from the padding. + /// The live bit length of the stream at `u64` width, recovered from the + /// padding. /// /// The marker is the buffer's final set bit, so the length is one /// `trailing_zeros` over the final byte: `O(1)`, no walk. The one storage @@ -174,22 +170,42 @@ impl Bits { /// The arithmetic runs at `u64` width: `bytes.len() * 8` wraps a 32-bit /// `usize` from 512 MiB of buffer, while the live length itself still /// fits at exactly that boundary (the marker spends at least one of the - /// buffer's bits). The final conversion is checked, not truncating, and - /// cannot fail on a constructed stream: every door bounds its buffer so - /// the live length fits `usize` ([`from_canonical`](Self::from_canonical) - /// asserts it; [`freeze`](Self::freeze)'s build buffer is bounded far - /// below it by the borrowed view's own encoding). - pub fn len(&self) -> usize { + /// buffer's bits). + fn live_bits(&self) -> u64 { match self.bytes.last() { None => 0, Some(&last) => { debug_assert!(last != 0, "stored stream missing its padding marker"); - let bits = self.bytes.len() as u64 * 8 - 1 - u64::from(last.trailing_zeros()); - usize::try_from(bits).expect("a constructed stream's live length fits usize") + self.bytes.len() as u64 * 8 - 1 - u64::from(last.trailing_zeros()) } } } + /// The live bit length of the stream, recovered from the padding: + /// [`live_bits`](Self::live_bits) converted to `usize`. + /// + /// The conversion is checked, not truncating, and cannot fail on a + /// constructed stream: every door bounds its buffer so the live length + /// fits `usize` ([`from_canonical`](Self::from_canonical) asserts it; + /// [`freeze`](Self::freeze)'s build buffer is bounded far below it by + /// the build buffer's own encoding). + pub fn len(&self) -> usize { + usize::try_from(self.live_bits()).expect("a constructed stream's live length fits usize") + } + + /// Read the frozen stream as live bits — the padding stays behind the + /// view: every cursor and walk consumes [`Bits`] through this view. + /// + /// Exact at every storable size on every target: the view carries the + /// buffer's bytes beside a `u64` live length, so no 32-bit length + /// encoding narrows what a walk can read below what the doors admit. + pub fn live(&self) -> BitsView<'_> { + BitsView { + bytes: &self.bytes, + live: self.live_bits(), + } + } + /// Whether the stream holds no bits at all. /// /// This is emptiness of the *storage* (the anonymous id), not of the value @@ -229,28 +245,174 @@ impl Bits { } } -/// Read the frozen stream as live bits — the padding stays behind the view: -/// every cursor and walk consumes [`Bits`] through this slice, exactly as it -/// consumes a [`BitsMut`]. +/// A borrowed view of one packed bit stream's live bits: the stream's bytes +/// beside a `u64` live bit length. /// -/// # Panics +/// The walk surface's one read form. Every semantic walk — comparison sweeps, +/// emissions, admission walks, the id operations — consumes stored streams +/// and door buffers through this view, so a walk's reach is exactly the +/// doors' (every storable stream, on every target), never narrowed by a +/// borrowed-view length encoding. +/// +/// A view always starts on byte 0 of its `bytes`: sub-stream *ranges* travel +/// as explicit bit positions beside the view (the copy and parse seams take +/// `(view, start, end)`), never as re-sliced views, so byte alignment of +/// every view is an invariant of the type, not a runtime question. /// -/// Panics inside `bitvec` when the buffer's bit count exceeds the borrowed -/// view's encoding limit, `usize::MAX >> 3` bits — on a 32-bit target, any -/// stored stream past 64 MiB. Such streams exist there: the byte decode -/// doors admit valid encodings up to the `usize` bit-position bound (512 -/// MiB), and every byte-level operation on them (equality, hashing, the -/// canonical bytes, re-encoding, the length) is exact — but the walk surface -/// reads through this one borrowed view, whose fat-pointer length field is -/// three bits narrower than `usize`, so a walk over such a value fails loudly -/// here rather than reading a mis-lengthed view. Widening the walk surface -/// past the borrowed-view encoding requires a crate-owned view type in place -/// of the `bitvec` borrow. On 64-bit targets the limit is 2^58 bytes, -/// unreachable by allocation. -impl Deref for Bits { - type Target = BitsSlice; - fn deref(&self) -> &BitsSlice { - &self.bytes.view_bits::()[..self.len()] +/// Bits at or past `live` — the padding, for a stored stream's buffer — are +/// not part of the view: [`bit`](Self::bit)/[`get`](Self::get) never read +/// them and [`body_tail`](Self::body_tail) masks them. +#[derive(Clone, Copy)] +pub struct BitsView<'a> { + /// The bytes holding the live bits (and possibly padding past them). + bytes: &'a [u8], + /// The live bit length: at most `8 · bytes.len()`. + live: u64, +} + +impl<'a> BitsView<'a> { + /// A view of the first `live` bits of `bytes`. + /// + /// # Panics + /// + /// `live` must be at most `8 · bytes.len()`. + pub(crate) fn new(bytes: &'a [u8], live: u64) -> Self { + assert!( + live <= bytes.len() as u64 * 8, + "live bits within the buffer" + ); + BitsView { bytes, live } + } + + /// The whole buffer as live bits — all `8 · bytes.len()` of them, + /// padding included. + /// + /// The byte decode doors' entry: a door walks its whole input buffer as + /// bits, and its marker check afterwards judges the remainder. + pub(crate) fn whole(bytes: &'a [u8]) -> Self { + BitsView { + bytes, + live: bytes.len() as u64 * 8, + } + } + + /// The empty view: no bits, no bytes. + pub(crate) fn empty() -> Self { + BitsView { + bytes: &[], + live: 0, + } + } + + /// The live bit length. + pub fn len(&self) -> u64 { + self.live + } + + /// Whether the view holds no bits at all. + pub fn is_empty(&self) -> bool { + self.live == 0 + } + + /// The bit at `pos`, or `None` at or past the live length: the + /// sequential cursors' bounded read. + pub(crate) fn get(&self, pos: u64) -> Option { + if pos >= self.live { + return None; + } + // `pos / 8` indexes an allocated buffer, so it fits `usize`. + let byte = self.bytes[(pos / 8) as usize]; + Some(byte >> (7 - pos % 8) & 1 == 1) + } + + /// The bit at `pos`. + /// + /// # Panics + /// + /// `pos` must be below the live length; the trusted-stream walks index + /// positions their own parses established. + pub(crate) fn bit(&self, pos: u64) -> bool { + assert!(pos < self.live, "bit read past the view's live length"); + let byte = self.bytes[(pos / 8) as usize]; + byte >> (7 - pos % 8) & 1 == 1 + } + + /// The `len <= 64` bits at `start`, value-packed at the low end of the + /// result: one gathered big-endian window, no per-bit loop. + /// + /// # Panics + /// + /// Debug-asserted: `start + len` must be within the live length (every + /// read bit is live, so no masking is needed). + pub(crate) fn load_be(&self, start: u64, len: u32) -> u64 { + debug_assert!( + u64::from(len) <= 64 && start + u64::from(len) <= self.live, + "loaded range within the view's live length" + ); + if len == 0 { + return 0; + } + // Gather the (up to) 9 bytes covering bits `start..start + 64`: 8 + // whole bytes plus the partial ninth a mid-byte `start` shifts in. + let byte = (start / 8) as usize; + let shift = (start % 8) as u32; + let mut buf = [0u8; 9]; + let end = (byte + buf.len()).min(self.bytes.len()); + buf[..end - byte].copy_from_slice(&self.bytes[byte..end]); + let word = u64::from_be_bytes(buf[..8].try_into().expect("buf holds 8 whole bytes")); + let window = if shift == 0 { + word + } else { + (word << shift) | (u64::from(buf[8]) >> (8 - shift)) + }; + window >> (64 - len) + } + + /// The view's bytes: whole body bytes, then the masked partial tail + /// byte, if any. + /// + /// The word-source destructuring: dead bits past the live length read + /// zero through the mask, so a reader's phantom bits can only lengthen + /// an apparent unary run past the live length (where its bounds checks + /// reject), never terminate one early. + pub(crate) fn body_tail(&self) -> (&'a [u8], Option) { + let whole = (self.live / 8) as usize; + let rem = (self.live % 8) as u32; + if rem == 0 { + (&self.bytes[..whole], None) + } else { + ( + &self.bytes[..whole], + Some(self.bytes[whole] & !(0xFF >> rem)), + ) + } + } + + /// The underlying bytes, whole: `(live + padding)` bits' worth. The + /// byte-copy seams read body bytes straight out of them. + pub(crate) fn bytes(&self) -> &'a [u8] { + self.bytes + } + + /// The view's bits copied into a build buffer: the test suites' + /// bridge to `bitvec`-vocabulary assertions. + #[cfg(test)] + pub(crate) fn to_bitvec(self) -> BitsMut { + let mut out = BitsMut::with_capacity(self.live as usize); + extend_from_view(&mut out, self, 0, self.live); + out + } + + /// Whether two views read one memory region: [`Bits::ptr_eq`]'s clone + /// identity, observable at the view level the walk kernels consume. + /// + /// Two views of one frozen buffer — a [`Bits`] viewed twice, through any + /// number of `O(1)` clones — carry the same byte pointer and live length, + /// so view identity implies bit-for-bit equality and an identity-law fast + /// path may answer without a walk. Never the converse: equal streams in + /// distinct buffers fall through to the walk that reads them. + pub(crate) fn ptr_eq(&self, other: &BitsView<'_>) -> bool { + self.bytes.as_ptr() == other.bytes.as_ptr() && self.live == other.live } } @@ -265,28 +427,6 @@ impl PartialEq for Bits { impl Eq for Bits {} -/// Borrow bytes as an MSB-first bit stream without first copying them into a -/// [`BitsMut`]: the meter surface's and the test suites' view. -/// -/// Production decode walks raw bytes instead, since this view's encoding -/// caps below the buffer sizes the decode doors admit on 32-bit targets. -#[cfg(any(test, feature = "meter"))] -pub(crate) fn bytes_as_bits(bytes: &[u8]) -> &BitsSlice { - bytes.view_bits::() -} - -/// Whether two bit-slice views read one memory region: [`Bits::ptr_eq`]'s clone -/// identity, observable at the slice level the walk kernels consume. -/// -/// Two views of one frozen buffer — a [`Bits`] deref'd twice, through any -/// number of `O(1)` clones — carry the same bit pointer and length, so view -/// identity implies bit-for-bit equality and an identity-law fast path may -/// answer without a walk. Never the converse: equal streams in distinct buffers -/// fall through to the walk that reads them. -pub(crate) fn slice_ptr_eq(a: &BitsSlice, b: &BitsSlice) -> bool { - a.as_bitptr() == b.as_bitptr() && a.len() == b.len() -} - /// Seal a built stream's canonical padding: one `1` marker bit, then zeroed /// dead bits to the byte boundary. /// @@ -360,30 +500,55 @@ pub(crate) fn padding_is_canonical(bits: &Bits) -> bool { } } -/// The direct byte view of a bit slice that starts on a byte boundary of its -/// backing store: the whole body bytes plus the masked partial tail byte, if -/// any. +/// A build buffer's contents as a [`BitsView`]: the underlying bytes beside +/// the live bit length. /// -/// `None` for the one shape with no direct byte view — a slice whose -/// backing-store offset puts live bits behind a partial head element. Every -/// stored stream starts on a byte boundary (offsets travel as bit positions, -/// never as re-sliced heads), so the `None` arm is a caller policy decision, -/// not a reachable production state: the gamma window loader degrades to its -/// bit-addressed fallback, the dsi cursor treats it as a violated precondition. -/// The destructuring lives here once so its callers cannot drift on the -/// byte-alignment invariant while keeping their deliberately different failure -/// policies. -pub(crate) fn byte_view(bits: &BitsSlice) -> Option<(&[u8], Option)> { - match bits.domain() { - Domain::Region { - head: None, - body, - tail, - } => Some((body, tail.map(|elem| elem.load_value()))), - Domain::Enclave(elem) if elem.head().into_inner() == 0 => { - Some((&[], Some(elem.load_value()))) - } - Domain::Region { head: Some(_), .. } | Domain::Enclave(_) => None, +/// A [`BitsMut`] always starts on byte 0 of its storage and its raw bytes +/// travel with it, so the view is a plain destructuring; bits past the live +/// length (a truncated buffer's shed tail) stay behind the view exactly as a +/// frozen stream's padding does. +pub(crate) fn built_view(bits: &BitsMut) -> BitsView<'_> { + BitsView::new(bits.as_raw_slice(), bits.len() as u64) +} + +/// Extend a build buffer with the bit range `start..end` copied verbatim +/// from a stored stream's view. +/// +/// The build-side copy seam of the operations that assemble their output +/// from input subtrees (the party split/sum families). The source range is +/// walked through bounded chunk views — never one whole-buffer borrowed +/// view, whose length encoding a storable stream can exceed on a 32-bit +/// target — so the copy is exact at every storable source size; the output +/// buffer's own bounds are the build side's, unchanged. +/// +/// # Panics +/// +/// `start..end` must be a range within the view's live length. +pub(crate) fn extend_from_view(out: &mut BitsMut, src: BitsView<'_>, start: u64, end: u64) { + assert!( + start <= end && end <= src.len(), + "copied range within the view's live length" + ); + let mut pos = start; + // Head bits to the source's byte boundary, one at a time (at most 7). + while pos < end && !pos.is_multiple_of(8) { + out.push(src.bit(pos)); + pos += 1; + } + // Whole-byte body, in bounded chunks: each chunk's borrowed bit view is + // far below any length-encoding bound on every target. + const CHUNK_BYTES: usize = 1024; + while end - pos >= 8 { + let at = (pos / 8) as usize; + let take_bytes = (((end - pos) / 8) as usize).min(CHUNK_BYTES); + let chunk = &src.bytes()[at..at + take_bytes]; + out.extend_from_bitslice(chunk.view_bits::()); + pos += take_bytes as u64 * 8; + } + // Tail bits (fewer than 8). + while pos < end { + out.push(src.bit(pos)); + pos += 1; } } @@ -392,14 +557,12 @@ pub(crate) fn byte_view(bits: &BitsSlice) -> Option<(&[u8], Option)> { /// The byte decode doors' padding judge, over the raw stream bytes. /// /// Positions are `u64` because a door walks the whole byte buffer as bits and -/// `8·bytes.len()` itself can exceed a 32-bit `usize`; the raw-byte form is -/// also what lets the doors admit buffers past the borrowed bit view's -/// encoding cap (see [`Bits`]'s deref). A canonical encoding pads with a -/// single marker and at most 7 zeros, all inside the final byte, so an intact -/// remainder here is 1 to 8 bits — a `1`, then zeros — decided by one mask -/// compare on the final byte. (A stream whose live bits end flush against a -/// byte boundary carries its marker in a whole final `1000_0000` byte.) The -/// rejections split by genre: +/// `8·bytes.len()` itself can exceed a 32-bit `usize`. A canonical encoding +/// pads with a single marker and at most 7 zeros, all inside the final byte, +/// so an intact remainder here is 1 to 8 bits — a `1`, then zeros — decided +/// by one mask compare on the final byte. (A stream whose live bits end flush +/// against a byte boundary carries its marker in a whole final `1000_0000` +/// byte.) The rejections split by genre: /// /// - An empty remainder is [`Decode::Truncated`]: the input ends where the /// padding should begin — a flush stream cut before its whole marker byte — @@ -417,7 +580,7 @@ pub(crate) fn byte_view(bits: &BitsSlice) -> Option<(&[u8], Option)> { /// /// `pos` must be at or before the buffer's end in bits (it is a walk's end /// position over this very buffer). -pub(crate) fn require_marker_padding_bytes(bytes: &[u8], pos: u64) -> Result<(), Decode> { +pub(crate) fn require_marker_padding(bytes: &[u8], pos: u64) -> Result<(), Decode> { let total = bytes.len() as u64 * 8; assert!( pos <= total, diff --git a/crates/before/src/codec/build.rs b/crates/before/src/codec/build.rs index d5c02c82..76fd6634 100644 --- a/crates/before/src/codec/build.rs +++ b/crates/before/src/codec/build.rs @@ -33,7 +33,7 @@ //! empty, a two-shift merge per byte otherwise. use super::code::SMALL_CODE_BITS; -use super::{BitsMut, BitsSlice, Code}; +use super::{BitsMut, BitsView, Code}; /// An append-truncate builder over one packed preorder bit stream. /// @@ -44,13 +44,12 @@ pub(crate) struct PackedBuilder { /// /// `bytes.len() * 8` fits `usize` on every target — the width the /// position arithmetic below (`len`, `patch_bit`, `read_bits`, - /// `bit_at`) rests on. The bound: every builder's output is a stream - /// destined for the build-side buffer, whose borrowed-view encoding - /// caps at `usize::MAX >> 3` bits, and the emitters write at most a - /// small constant per input node over operands that are themselves - /// stored streams under the same cap — so the committed prefix stays - /// multiple binary orders of magnitude below any `usize` wrap, even - /// on 32-bit targets. + /// `bit_at`) rests on. The bound: every finished stream hands over + /// through the build buffer, whose own length encoding caps at + /// `usize::MAX >> 3` bits, and the emitters write at most a small + /// constant per input node — so the committed prefix stays multiple + /// binary orders of magnitude below any `usize` wrap, even on 32-bit + /// targets. bytes: Vec, /// The trailing not-yet-committed bits, value-packed at the low end /// (the stream's next bit is the register's most significant live @@ -114,7 +113,10 @@ impl PackedBuilder { self.append_bits(*bits, u32::from(*len)); } // The splice records its own write. - Code::Wide(bits) => self.splice(bits), + Code::Wide(bits) => { + let src = super::bits::built_view(bits); + self.splice(src, 0, src.len()); + } } } @@ -163,24 +165,36 @@ impl PackedBuilder { } } - /// Append a verbatim bit range copied from an already-normal source. - pub(crate) fn splice(&mut self, src: &BitsSlice) { - super::scan::record_bits(src.len()); - // Walk the source up to its backing store's next byte boundary - // (at most seven bits), where the byte view takes over. - let mut src = src; - while !src.is_empty() { - if let Some((body, tail)) = super::bits::byte_view(src) { - self.append_bytes(body); - let rem = (src.len() - body.len() * 8) as u32; - if rem > 0 { - let tail = tail.expect("a partial trailing byte backs the trailing bits"); - self.append_bits(u64::from(tail >> (8 - rem)), rem); - } - return; - } - self.append_bits(u64::from(src[0]), 1); - src = &src[1..]; + /// Append the bit range `start..end` of `src`, copied verbatim from an + /// already-normal source. + /// + /// # Panics + /// + /// `start..end` must be a range within the view's live length. + pub(crate) fn splice(&mut self, src: BitsView<'_>, start: u64, end: u64) { + assert!( + start <= end && end <= src.len(), + "spliced range within the view's live length" + ); + super::scan::record_bits_u64(end - start); + let mut pos = start; + // Walk the source up to its next byte boundary (at most seven + // bits), where the byte copy takes over. + while pos < end && !pos.is_multiple_of(8) { + self.append_bits(u64::from(src.bit(pos)), 1); + pos += 1; + } + let whole = (end - pos) / 8; + if whole > 0 { + let at = (pos / 8) as usize; + self.append_bytes(&src.bytes()[at..at + whole as usize]); + pos += whole * 8; + } + if pos < end { + // Trailing bits (fewer than 8), value-packed from their byte. + let rem = (end - pos) as u32; + let byte = src.bytes()[(pos / 8) as usize]; + self.append_bits(u64::from(byte >> (8 - rem)), rem); } } diff --git a/crates/before/src/codec/code.rs b/crates/before/src/codec/code.rs index e66195ca..db78b276 100644 --- a/crates/before/src/codec/code.rs +++ b/crates/before/src/codec/code.rs @@ -9,8 +9,7 @@ //! the band organic histories occupy — in two machine words, and spills wider //! codes to the buffer form unchanged. -use super::{BitsMut, BitsSlice}; -use bitvec::field::BitField; +use super::{BitsMut, BitsView}; /// One complete payload code, value-packed when it fits a word. pub(crate) enum Code { @@ -34,16 +33,28 @@ impl Code { } } - /// A code copied out of a canonical stream's bit range. - pub(crate) fn from_slice(src: &BitsSlice) -> Code { - debug_assert!(!src.is_empty(), "a payload code is never empty"); - if src.len() <= SMALL_CODE_BITS { + /// A code copied out of the bit range `start..end` of a canonical + /// stream's view. + /// + /// # Panics + /// + /// `start..end` must be a range within the view's live length. + pub(crate) fn from_range(src: BitsView<'_>, start: u64, end: u64) -> Code { + debug_assert!(start < end, "a payload code is never empty"); + assert!( + end <= src.len(), + "copied range within the view's live length" + ); + let len = end - start; + if len <= SMALL_CODE_BITS as u64 { Code::Small { - bits: src.load_be::(), - len: src.len() as u8, + bits: src.load_be(start, len as u32), + len: len as u8, } } else { - Code::Wide(src.to_bitvec()) + let mut out = BitsMut::with_capacity(len as usize); + super::bits::extend_from_view(&mut out, src, start, end); + Code::Wide(out) } } } diff --git a/crates/before/src/codec/cursor.rs b/crates/before/src/codec/cursor.rs index 11285e9b..e2e2a33b 100644 --- a/crates/before/src/codec/cursor.rs +++ b/crates/before/src/codec/cursor.rs @@ -2,7 +2,7 @@ use crate::error::Decode; -use super::{decode_int_from, gamma, BitsSlice, Int}; +use super::{decode_int_from, gamma, BitsView, Int}; /// The bit stream ended before the requested bit. /// @@ -78,16 +78,27 @@ pub(crate) trait BitCursor { } } -/// A sequential cursor over an existing packed bit slice. +/// A sequential cursor over an existing packed bit view. pub(crate) struct SliceCursor<'a> { - bits: &'a BitsSlice, - position: usize, + bits: BitsView<'a>, + /// The position immediately after the last bit read. + /// + /// `u64`, the view's own denomination: a byte decode door's + /// whole-buffer view holds more bit positions than a 32-bit `usize`. + position: u64, } impl<'a> SliceCursor<'a> { - pub(crate) fn new(bits: &'a BitsSlice, position: usize) -> Self { + pub(crate) fn new(bits: BitsView<'a>, position: u64) -> Self { SliceCursor { bits, position } } + + /// The position immediately after the last bit read, in the cursor's own + /// `u64` denomination: exact even where the walked buffer holds more bit + /// positions than `usize`. + pub(crate) fn position_u64(&self) -> u64 { + self.position + } } impl BitCursor for SliceCursor<'_> { @@ -95,7 +106,7 @@ impl BitCursor for SliceCursor<'_> { fn read_bit(&mut self) -> Result { // `ok_or`'s eager argument is fine here: `Truncated` is a ZST. - let bit = *self.bits.get(self.position).ok_or(Truncated)?; + let bit = self.bits.get(self.position).ok_or(Truncated)?; // One live bit scanned: this cursor is the sequential read primitive // under the id-tree parsers and the per-bit gamma decode path, so the // scan meter records here once for both. The skyline kernels read @@ -106,11 +117,15 @@ impl BitCursor for SliceCursor<'_> { } fn position(&self) -> usize { - self.position + // Exact for every walk that reads positions through the trait: a + // stored stream's live length fits `usize` on every target (the + // storage doors bound it), and the byte decode doors read their end + // positions through `position_u64` instead. + usize::try_from(self.position).expect("a walked position fits usize") } fn read_int(&mut self) -> Result { - // Word fast path over the slice; anything the window cannot prove — + // Word fast path over the view; anything the window cannot prove — // every reject included — is decided by the default per-bit loop, so // the two paths accept and reject identically by construction. if let Some((n, next)) = gamma::decode_int_window(self.bits, self.position) { @@ -118,7 +133,7 @@ impl BitCursor for SliceCursor<'_> { // reads one at a time, so it records the same count: the scan meter // prices work by bits examined, not by how the examining path // batches them. - super::scan::record_bits(next - self.position); + super::scan::record_bits_u64(next - self.position); self.position = next; return Ok(Int::Small(n)); } diff --git a/crates/before/src/codec/display.rs b/crates/before/src/codec/display.rs index 40cc8896..b5f57842 100644 --- a/crates/before/src/codec/display.rs +++ b/crates/before/src/codec/display.rs @@ -1,6 +1,6 @@ use crate::idbits::{IdNode, IdReader}; -use super::{BitsMut, BitsSlice}; +use super::{BitsMut, BitsView}; /// While rendering an open id node, which child the walk is inside. /// @@ -22,7 +22,7 @@ const RIGHT_PHASE: bool = false; /// sees the traversal), and its control state is one to two bits per open node /// on a bit stack — a deep id costs bits, never stack frames or grown segments. pub(crate) fn write_id( - bits: &BitsSlice, + bits: BitsView<'_>, f: &mut core::fmt::Formatter<'_>, sep: &str, ) -> core::fmt::Result { diff --git a/crates/before/src/codec/dsi.rs b/crates/before/src/codec/dsi.rs index 0ece8709..b2f498e1 100644 --- a/crates/before/src/codec/dsi.rs +++ b/crates/before/src/codec/dsi.rs @@ -8,7 +8,7 @@ //! [`read_unary`](DsiCursor::read_unary) (a `leading_zeros` over a buffered //! window) and whole payload codes in `O(1)` word operations. The accept/reject //! boundary lives in this wrapper, not the library: `position`/`len` bound -//! every read against the slice's live bit length, so the reader's zero padding +//! every read against the view's live bit length, so the reader's zero padding //! past the live bits is never surfaced as data. //! //! Values are read through the in-house wide arm, never `dsi-bitstream`'s own @@ -39,9 +39,9 @@ use dsi_bitstream::traits::{BitRead, WordRead, BE}; use crate::error::Decode; use super::cursor::Truncated; -use super::{Base, BitCursor, BitsSlice, Int}; +use super::{Base, BitCursor, BitsView, Int}; -/// A word-parallel sequential cursor over an existing packed bit slice. +/// A word-parallel sequential cursor over an existing packed bit view. /// /// The skyline walks' reader: [`read_bit`](BitCursor::read_bit) for interleaved /// single flags, [`read_unary`](BitCursor::read_unary) for topology runs, @@ -56,25 +56,24 @@ pub(crate) struct DsiCursor<'a> { /// `u64`, not `usize`: the byte decode doors walk a whole input buffer /// as bits, and `8 · bytes.len()` can exceed a 32-bit `usize` (a 600 /// MiB buffer holds 2^32+ bit positions) while remaining exactly - /// representable here. Slice-backed walks never leave `usize` range — - /// the borrowed view's own encoding bounds them. + /// representable here. Stored-stream walks never leave `usize` range — + /// the storage doors bound a stream's live length below it. position: u64, /// The stream's live bit length, in the same `u64` denomination. len: u64, } impl<'a> DsiCursor<'a> { - /// Open a cursor at bit 0 of a stored stream. + /// Open a cursor at bit 0 of a stream's view. /// - /// # Panics - /// - /// Panics if the slice does not start on a byte boundary of its backing - /// store; every stored `Version` stream does. - pub(crate) fn new(bits: &'a BitsSlice) -> Self { + /// A stored stream's live view and a byte decode door's whole padded + /// buffer enter identically: padding bits are data to a door's walk, + /// and the door's marker check afterwards judges the remainder. + pub(crate) fn new(bits: BitsView<'a>) -> Self { DsiCursor::new_at(bits, 0) } - /// Open a cursor at bit `pos` of a stored stream. + /// Open a cursor at bit `pos` of a stream's view. /// /// `O(1)`: the word source starts at `pos`'s byte and the cursor discards /// the at most 7 leading bits before `pos` unrecorded (the walk never @@ -82,65 +81,12 @@ impl<'a> DsiCursor<'a> { /// /// # Panics /// - /// Panics if `pos` lies past the stream's end, or if the slice does not - /// start on a byte boundary of its backing store; every stored - /// `Version` stream does. - pub(crate) fn new_at(bits: &'a BitsSlice, pos: usize) -> Self { + /// Panics if `pos` lies past the view's live length. + pub(crate) fn new_at(bits: BitsView<'a>, pos: u64) -> Self { assert!(pos <= bits.len(), "cursor opened past the stream's end"); - let Some((body, tail)) = super::byte_view(bits) else { - unreachable!("stored streams start on a byte boundary") - }; - Self::from_parts(body, tail, bits.len() as u64, pos) - } - - /// Open a cursor over the whole bit view of raw stream bytes — all - /// `8 · bytes.len()` of them, padding included. - /// - /// The byte decode doors' entry, which must walk buffers whose borrowed - /// bit view the `bitvec` encoding cannot represent (64 MiB and up on a - /// 32-bit target). - /// - /// The door semantics are exactly the slice walk's over a whole padded - /// buffer: padding bits are data to the walk, and the door's marker - /// check afterwards judges the remainder. - pub(crate) fn over_bytes(bytes: &'a [u8]) -> Self { - Self::from_parts(bytes, None, bytes.len() as u64 * 8, 0) - } - - /// Open a cursor over the first `live` bits of raw stream bytes: the - /// byte decode doors' form of a validated-prefix walk (the span door - /// re-walks the meet it just validated), for buffers past the borrowed - /// view's encoding cap. - /// - /// The partial tail byte's dead bits are masked to zero, exactly as the - /// borrowed view's domain masks them: the reader's phantom bits past - /// the live length must read zero so an apparent unary run can only - /// lengthen past `len` (and reject there), never terminate early. - /// - /// # Panics - /// - /// `live` must be at most `8 · bytes.len()`. - pub(crate) fn over_bytes_live(bytes: &'a [u8], live: usize) -> Self { - assert!( - live as u64 <= bytes.len() as u64 * 8, - "live bits within the buffer" - ); - let (body, tail) = if live.is_multiple_of(8) { - (&bytes[..live / 8], None) - } else { - ( - &bytes[..live / 8], - Some(bytes[live / 8] & !(0xFF >> (live % 8))), - ) - }; - Self::from_parts(body, tail, live as u64, 0) - } - - /// The one constructor body: a word source over `(body, tail)` starting - /// at `pos`, bounded by `len` live bits. - fn from_parts(body: &'a [u8], tail: Option, len: u64, pos: usize) -> Self { - let mut reader = BufBitReader::new(ByteWords::new(body, tail, pos / 8)); - let skip = pos % 8; + let (body, tail) = bits.body_tail(); + let mut reader = BufBitReader::new(ByteWords::new(body, tail, (pos / 8) as usize)); + let skip = (pos % 8) as usize; if skip != 0 { reader .skip_bits(skip) @@ -148,8 +94,8 @@ impl<'a> DsiCursor<'a> { } DsiCursor { reader, - position: pos as u64, - len, + position: pos, + len: bits.len(), } } @@ -245,10 +191,10 @@ impl BitCursor for DsiCursor<'_> { } fn position(&self) -> usize { - // Exact for every walk that reads positions through the trait: - // slice-backed cursors are bounded by the borrowed view (below - // usize bits by its encoding), and the byte decode doors read - // their end positions through `position_u64` instead. A byte-door + // Exact for every walk that reads positions through the trait: a + // stored stream's live length fits `usize` on every target (the + // storage doors bound it), and the byte decode doors read their + // end positions through `position_u64` instead. A byte-door // walk parking past usize (its truncation reject on a buffer past // the storable bound) is never followed by a trait position read. usize::try_from(self.position).expect("a walked position fits usize") diff --git a/crates/before/src/codec/dsi/tests.rs b/crates/before/src/codec/dsi/tests.rs index ecb74c1a..0ec29832 100644 --- a/crates/before/src/codec/dsi/tests.rs +++ b/crates/before/src/codec/dsi/tests.rs @@ -36,24 +36,24 @@ fn gamma_reader_matches_decoder_across_the_word_seam() { for value in &values { let mut bits = BitsMut::new(); codec::encode_int(&mut bits, value); - let (want, want_end) = - codec::decode_int(&bits, 0).expect("the committed decoder reads its own encoding"); - let mut cursor = DsiCursor::new(&bits); + let (want, want_end) = codec::decode_int(crate::codec::built_view(&bits), 0) + .expect("the committed decoder reads its own encoding"); + let mut cursor = DsiCursor::new(crate::codec::built_view(&bits)); let got = cursor .read_int() .expect("the word-parallel reader reads the same code"); assert_eq!(&got.clone().into_base(), &want, "value diverges at {value}"); assert_eq!( - cursor.position(), + cursor.position_u64(), want_end, "consumed bits diverge at {value}" ); - let mut skipper = DsiCursor::new(&bits); + let mut skipper = DsiCursor::new(crate::codec::built_view(&bits)); skipper .skip_int() .expect("the skip accepts what the read accepts"); assert_eq!( - skipper.position(), + skipper.position_u64(), want_end, "skip width diverges at {value}" ); @@ -84,14 +84,14 @@ fn skip_int_meters_exactly_the_code_width_read_int_pays() { ] { let mut bits = BitsMut::new(); codec::encode_int(&mut bits, &value); - let mut reader = DsiCursor::new(&bits); + let mut reader = DsiCursor::new(crate::codec::built_view(&bits)); crate::meter::reset_scan_bits(); reader .read_int() .expect("the reader reads its own encoding"); let read_record = crate::meter::scan_bits(); let width = reader.position() as u64; - let mut skipper = DsiCursor::new(&bits); + let mut skipper = DsiCursor::new(crate::codec::built_view(&bits)); crate::meter::reset_scan_bits(); skipper .skip_int() @@ -123,7 +123,7 @@ fn truncated_codes_reject_at_every_cut_point() { let mut bits = BitsMut::new(); codec::encode_int(&mut bits, &value); for cut in 0..bits.len() { - let prefix = &bits[..cut]; + let prefix = codec::BitsView::new(bits.as_raw_slice(), cut as u64); assert!( codec::decode_int(prefix, 0).is_err(), "the per-bit loop accepts a truncated code at {cut} of {value}" @@ -157,7 +157,7 @@ fn unary_reads_match_the_per_bit_loop_across_word_seams() { } bits.push(true); bits.push(true); // one trailing live bit so the terminator is interior - let mut cursor = DsiCursor::new(&bits); + let mut cursor = DsiCursor::new(crate::codec::built_view(&bits)); assert_eq!( cursor.read_unary().expect("a terminated run reads"), run, @@ -165,11 +165,11 @@ fn unary_reads_match_the_per_bit_loop_across_word_seams() { ); assert_eq!(cursor.position(), run + 1, "the terminating 1 is consumed"); // The same bits through the default per-bit trait loop. - let mut slice = SliceCursor::new(&bits, 0); + let mut slice = SliceCursor::new(crate::codec::built_view(&bits), 0); assert_eq!(slice.read_unary().expect("a terminated run reads"), run); assert_eq!(slice.position(), run + 1); - let unterminated = &bits[..run]; + let unterminated = codec::BitsView::new(bits.as_raw_slice(), run as u64); let mut cursor = DsiCursor::new(unterminated); assert!( matches!(cursor.read_unary(), Err(Truncated)), @@ -197,8 +197,8 @@ fn mid_stream_opens_read_the_same_suffix() { codec::encode_int(&mut bits, &Base::from(value)); } for pos in 0..=bits.len() { - let mut fresh = DsiCursor::new_at(&bits, pos); - let mut walked = DsiCursor::new(&bits); + let mut fresh = DsiCursor::new_at(crate::codec::built_view(&bits), pos as u64); + let mut walked = DsiCursor::new(crate::codec::built_view(&bits)); let mut consumed = 0usize; while consumed < pos { walked.read_bit().expect("within the live length"); @@ -249,8 +249,8 @@ proptest! { codec::encode_int(&mut bits, &Base::from(*v)); } } - let mut dsi = DsiCursor::new(&bits); - let mut slice = SliceCursor::new(&bits, 0); + let mut dsi = DsiCursor::new(crate::codec::built_view(&bits)); + let mut slice = SliceCursor::new(crate::codec::built_view(&bits), 0); for (unary, _) in &ops { if *unary { let want = slice.read_unary().expect("the stream holds the run"); @@ -258,7 +258,8 @@ proptest! { prop_assert_eq!(got, want); } else { let want = slice.read_int().expect("the stream holds the code"); - let mut skipper = DsiCursor::new_at(&bits, dsi.position()); + let mut skipper = + DsiCursor::new_at(crate::codec::built_view(&bits), dsi.position_u64()); let got = dsi.read_int().expect("the stream holds the code"); prop_assert_eq!(&got, &want); skipper.skip_int().expect("the skip accepts the code"); diff --git a/crates/before/src/codec/gamma.rs b/crates/before/src/codec/gamma.rs index c9834b20..81344b82 100644 --- a/crates/before/src/codec/gamma.rs +++ b/crates/before/src/codec/gamma.rs @@ -21,7 +21,7 @@ use dashu_int::UBig; use crate::error::Decode; use super::code::SMALL_CODE_BITS; -use super::{Base, BitCursor, BitsMut, BitsSlice, Code, SliceCursor}; +use super::{Base, BitCursor, BitsMut, BitsView, Code, SliceCursor}; /// Append `n` as the Elias gamma code of `m = n + 1`: `floor(log2(m))` zero /// bits, then `m` in `floor(log2(m)) + 1` bits, most-significant first. @@ -114,10 +114,10 @@ pub(crate) fn code_int_small(n: u64) -> Code { /// per-bit loop ([`decode_int_from`]), so the two paths accept and reject /// identically by construction (the routing lives in /// [`SliceCursor::read_int`](BitCursor::read_int)). -pub(crate) fn decode_int(bits: &BitsSlice, pos: usize) -> Result<(Base, usize), Decode> { +pub(crate) fn decode_int(bits: BitsView<'_>, pos: u64) -> Result<(Base, u64), Decode> { let mut cursor = SliceCursor::new(bits, pos); let base = cursor.read_int()?.into_base(); - Ok((base, cursor.position())) + Ok((base, cursor.position_u64())) } /// The number of bits a [`decode_int_window`] window holds. @@ -133,8 +133,6 @@ const WINDOW_BITS: u64 = u64::BITS as u64; /// Returns `None` — decode nothing, let the caller run the per-bit loop from /// `pos` instead — whenever the window cannot *prove* a complete code: /// -/// - the slice does not start on a byte boundary of its backing store (no -/// cheap byte view; stored forms always do); /// - `pos` lies past the end of the stream (the bit loop reports `Truncated`); /// - the `2k+1`-bit code overruns the window's proven bits, either because the /// stream ends first (the bit loop reports `Truncated`) or because the code @@ -149,21 +147,13 @@ const WINDOW_BITS: u64 = u64::BITS as u64; /// bits are masked, missing bytes are zero-filled), which only ever *lengthens* /// the apparent prefix — pushing `2k+1` past the proven bits and into the /// fallback — never shortens it into a bogus accept. -pub(crate) fn decode_int_window(bits: &BitsSlice, pos: usize) -> Option<(u64, usize)> { - let (body, tail) = super::byte_view(bits)?; - let (n, next) = window_int(body, tail, bits.len() as u64, pos as u64)?; - // The code ends inside the slice, whose own length bounds the position. - Some((n, next as usize)) -} - -/// [`decode_int_window`] over raw stream bytes: the wire-side reader's form. /// -/// The reader's buffered bytes have no borrowed bit view once they outgrow -/// the view encoding's cap, so this windows the whole `8 · bytes.len()`-bit -/// view at the reader's own `u64` position width. -#[cfg(feature = "borsh")] -pub(crate) fn decode_int_window_bytes(bytes: &[u8], pos: u64) -> Option<(u64, u64)> { - window_int(bytes, None, bytes.len() as u64 * 8, pos) +/// Positions are the view's own `u64`: the wire-side reader windows its +/// buffered bytes ([`BitsView::whole`]) at the same width its own position +/// runs at. +pub(crate) fn decode_int_window(bits: BitsView<'_>, pos: u64) -> Option<(u64, u64)> { + let (body, tail) = bits.body_tail(); + window_int(body, tail, bits.len(), pos) } /// The one-window decoder's body over raw parts: `len` live bits across diff --git a/crates/before/src/codec/literal.rs b/crates/before/src/codec/literal.rs index 2eca2343..0e89d452 100644 --- a/crates/before/src/codec/literal.rs +++ b/crates/before/src/codec/literal.rs @@ -1,6 +1,6 @@ use crate::error::Parse; -use super::{validate_id, BitsMut, BitsSlice}; +use super::{validate_id, BitsMut, BitsView}; /// Whether a normal-form id stream is the anonymous (empty) identity. /// @@ -12,11 +12,11 @@ use super::{validate_id, BitsMut, BitsSlice}; /// arity vs stream length — never a full re-parse: this helper is on every /// decode path's metered hot loop, and asserted work here would make dev builds /// meter a different program than the release board of record. -pub(crate) fn id_is_empty(bits: &BitsSlice) -> bool { +pub(crate) fn id_is_empty(bits: BitsView<'_>) -> bool { debug_assert!( bits.is_empty() || (bits.len() >= 2 - && if bits[..2].any() { + && if bits.bit(0) || bits.bit(1) { // a root with a present child carries at least one more tag bits.len() >= 4 } else { @@ -40,7 +40,7 @@ pub(crate) fn id_leaf(v: bool) -> BitsMut { } /// Whether `bits` is exactly the terminal tag `00` (the `1` leaf). -fn id_is_terminal(bits: &BitsSlice) -> bool { +fn id_is_terminal(bits: &BitsMut) -> bool { bits.len() == 2 && !bits[0] && !bits[1] } @@ -49,7 +49,7 @@ fn id_is_terminal(bits: &BitsSlice) -> bool { /// present. /// /// Rejects a collapsible `(0, 0)` or `(1, 1)`, then validates the result. -pub(crate) fn id_node(l: &BitsSlice, r: &BitsSlice) -> Result { +pub(crate) fn id_node(l: &BitsMut, r: &BitsMut) -> Result { if l.is_empty() && r.is_empty() { return Err(Parse::NotCanonical); // (0, 0) → 0, not a node } @@ -61,6 +61,6 @@ pub(crate) fn id_node(l: &BitsSlice, r: &BitsSlice) -> Result { b.push(!r.is_empty()); // bit 1 = right present b.extend_from_bitslice(l); b.extend_from_bitslice(r); - validate_id(&b)?; + validate_id(super::bits::built_view(&b))?; Ok(b) } diff --git a/crates/before/src/codec/stack.rs b/crates/before/src/codec/stack.rs index 7a2c78f4..72661ac5 100644 --- a/crates/before/src/codec/stack.rs +++ b/crates/before/src/codec/stack.rs @@ -38,9 +38,9 @@ impl BitStack { /// /// `words.len() * 64` fits `usize` on every target: a walk holds a few /// bits per open ancestor here, depth is bounded by the walked stream's - /// bit length, and stored streams cap at `usize::MAX >> 3` bits (the - /// borrowed-view encoding) — so the height stays multiple binary orders - /// of magnitude below any `usize` wrap, even on 32-bit targets. + /// live bit length, and a storable stream's live length itself fits + /// `usize` — so the height stays below any `usize` wrap, even on 32-bit + /// targets. pub(crate) fn len(&self) -> usize { self.words.len() * 64 + self.top_len as usize } diff --git a/crates/before/src/codec/tests.rs b/crates/before/src/codec/tests.rs index afec618d..eecd603f 100644 --- a/crates/before/src/codec/tests.rs +++ b/crates/before/src/codec/tests.rs @@ -13,8 +13,7 @@ use proptest::prelude::*; use proptest::test_runner::TestCaseError; use super::{ - bytes_as_bits, decode_int, decode_int_from, encode_int, Base, BitCursor, BitsMut, BitsSlice, - DsiCursor, SliceCursor, + decode_int, decode_int_from, encode_int, Base, BitsMut, BitsView, DsiCursor, SliceCursor, }; use crate::oracle; use crate::span::Span; @@ -38,9 +37,9 @@ proptest! { let n = Base::from(n); let mut bits = BitsMut::new(); encode_int(&mut bits, &n); - let (decoded, pos) = decode_int(&bits, 0).expect("well-formed"); + let (decoded, pos) = decode_int(crate::codec::built_view(&bits), 0).expect("well-formed"); prop_assert_eq!(decoded, n); - prop_assert_eq!(pos, bits.len()); + prop_assert_eq!(pos, bits.len() as u64); } } @@ -56,9 +55,9 @@ proptest! { } let mut bits = BitsMut::new(); encode_int(&mut bits, &n); - let (decoded, pos) = decode_int(&bits, 0).expect("well-formed"); + let (decoded, pos) = decode_int(crate::codec::built_view(&bits), 0).expect("well-formed"); prop_assert_eq!(decoded, n); - prop_assert_eq!(pos, bits.len()); + prop_assert_eq!(pos, bits.len() as u64); } } @@ -90,10 +89,10 @@ fn gamma_roundtrip_just_above_u64_max() { let n = Base::from(u64::MAX) + Base::from(1u8); let mut bits = BitsMut::new(); encode_int(&mut bits, &n); - let (decoded, pos) = decode_int(&bits, 0).expect("well-formed"); + let (decoded, pos) = decode_int(crate::codec::built_view(&bits), 0).expect("well-formed"); assert_eq!(decoded, n); assert_eq!(decoded.to_string(), "18446744073709551616"); - assert_eq!(pos, bits.len()); + assert_eq!(pos, bits.len() as u64); } /// `decode_int` never panics and reports `Truncated` when the code runs off the @@ -101,9 +100,15 @@ fn gamma_roundtrip_just_above_u64_max() { #[test] fn gamma_truncated() { let empty = BitsMut::new(); - assert!(matches!(decode_int(&empty, 0), Err(Decode::Truncated))); + assert!(matches!( + decode_int(crate::codec::built_view(&empty), 0), + Err(Decode::Truncated) + )); let zeros: BitsMut = bitvec![u8, Msb0; 0, 0, 0, 0, 0]; - assert!(matches!(decode_int(&zeros, 0), Err(Decode::Truncated))); + assert!(matches!( + decode_int(crate::codec::built_view(&zeros), 0), + Err(Decode::Truncated) + )); } // ───────────────────────── frozen storage (Bits) ───────────────────────── @@ -124,7 +129,7 @@ fn freeze_canonicalizes_storage() { assert_eq!(frozen.len(), 3); assert_eq!(frozen.as_raw_slice(), &[0b1111_0000]); assert!(super::padding_is_canonical(&frozen)); - assert_eq!(&*frozen, &buf[..]); + assert_eq!(frozen.live().to_bitvec(), buf); } /// The marker padding makes stored bytes injective on streams: a stream @@ -226,14 +231,14 @@ fn encode_int_bitwise(out: &mut BitsMut, n: &Base) { /// The per-bit reference `skip_int`, the skip-side differential oracle: counts /// the unary prefix, then steps over the mantissa bit by bit. -fn skip_int_bitwise(bits: &BitsSlice, pos: usize) -> Result { - let mut k = 0usize; +fn skip_int_bitwise(bits: BitsView<'_>, pos: u64) -> Result { + let mut k = 0u64; loop { let idx = pos + k; if idx >= bits.len() { return Err(Decode::Truncated); } - if bits[idx] { + if bits.bit(idx) { break; } k += 1; @@ -248,14 +253,14 @@ fn skip_int_bitwise(bits: &BitsSlice, pos: usize) -> Result { /// Assert `decode_int` (windowed) agrees with the pure bit loop at `pos`: same /// accept/reject, same error variant, same value, same end position. -fn assert_decode_matches_bit_loop(bits: &BitsSlice, pos: usize) -> Result<(), TestCaseError> { +fn assert_decode_matches_bit_loop(bits: BitsView<'_>, pos: u64) -> Result<(), TestCaseError> { let subject = decode_int(bits, pos); let mut cursor = SliceCursor::new(bits, pos); let oracle = decode_int_from(&mut cursor); match (subject, oracle) { (Ok((value, end)), Ok(oracle_value)) => { prop_assert_eq!(value, oracle_value); - prop_assert_eq!(end, cursor.position()); + prop_assert_eq!(end, cursor.position_u64()); } (Err(s), Err(o)) => { prop_assert_eq!(std::mem::discriminant(&s), std::mem::discriminant(&o)); @@ -271,10 +276,10 @@ fn assert_decode_matches_bit_loop(bits: &BitsSlice, pos: usize) -> Result<(), Te /// Runs inside the cursor's stated domain — a byte-aligned slice origin and /// `pos` at or inside the live length; every production skip site satisfies /// both (stored streams, positions from the same cursor). -fn assert_skip_matches_bit_loop(bits: &BitsSlice, pos: usize) -> Result<(), TestCaseError> { +fn assert_skip_matches_bit_loop(bits: BitsView<'_>, pos: u64) -> Result<(), TestCaseError> { let mut cursor = DsiCursor::new_at(bits, pos); match (cursor.skip_int(), skip_int_bitwise(bits, pos)) { - (Ok(()), Ok(o)) => prop_assert_eq!(cursor.position(), o), + (Ok(()), Ok(o)) => prop_assert_eq!(cursor.position_u64(), o), (Err(_), Err(o)) => { prop_assert!( matches!(o, Decode::Truncated), @@ -356,9 +361,10 @@ proptest! { prop_assert_eq!(&word, &bit); // Word-decode of the word-encode round-trips value and position. - let (decoded, end) = decode_int(&word, pos).expect("well-formed"); + let (decoded, end) = + decode_int(crate::codec::built_view(&word), pos as u64).expect("well-formed"); prop_assert_eq!(decoded, value); - prop_assert_eq!(end, word.len()); + prop_assert_eq!(end, word.len() as u64); } } @@ -375,23 +381,18 @@ proptest! { (bits, pos) in arb_gamma_stream(), extra in 0usize..3, ) { - assert_decode_matches_bit_loop(&bits, pos)?; - assert_skip_matches_bit_loop(&bits, pos)?; + let view = crate::codec::built_view(&bits); + let pos = pos as u64; + let extra = extra as u64; + assert_decode_matches_bit_loop(view, pos)?; + assert_skip_matches_bit_loop(view, pos)?; // The end of the stream, just before it, and past it (the skip cursor's // domain ends at the live length; `decode_int` alone covers the // past-the-end positions). - assert_decode_matches_bit_loop(&bits, bits.len().saturating_sub(extra))?; - assert_skip_matches_bit_loop(&bits, bits.len().saturating_sub(extra))?; - assert_decode_matches_bit_loop(&bits, bits.len() + extra)?; - - // A slice whose origin is mid-byte in its backing store: the window - // declines and only the per-bit loop runs. The skip cursor's domain - // excludes such slices (stored streams are byte-aligned), so the decode - // pair alone covers them. - if !bits.is_empty() { - assert_decode_matches_bit_loop(&bits[1..], pos.saturating_sub(1))?; - } + assert_decode_matches_bit_loop(view, view.len().saturating_sub(extra))?; + assert_skip_matches_bit_loop(view, view.len().saturating_sub(extra))?; + assert_decode_matches_bit_loop(view, view.len() + extra)?; } } @@ -408,7 +409,8 @@ proptest! { bytes in proptest::collection::vec(any::(), 0..12), pos in 0usize..104, ) { - let bits = bytes_as_bits(&bytes); + let bits = crate::codec::BitsView::whole(&bytes); + let pos = pos as u64; assert_decode_matches_bit_loop(bits, pos)?; assert_skip_matches_bit_loop(bits, pos.min(bits.len()))?; } @@ -430,10 +432,16 @@ fn gamma_window_edge() { let mut bits = BitsMut::new(); encode_int(&mut bits, &Base::from(n)); assert_eq!(bits.len(), 63); - assert_eq!(decode_int_window(&bits, 0), Some((n, 63))); + assert_eq!( + decode_int_window(crate::codec::built_view(&bits), 0), + Some((n, 63)) + ); // The same code cut one bit short: nothing provable, decline. - assert_eq!(decode_int_window(&bits[..62], 0), None); + assert_eq!( + decode_int_window(crate::codec::BitsView::new(bits.as_raw_slice(), 62), 0), + None + ); // k = 32: a 65-bit code straddles the window edge — decline, and the full // decoder still reads it through the loop. @@ -441,10 +449,10 @@ fn gamma_window_edge() { let mut bits = BitsMut::new(); encode_int(&mut bits, &Base::from(n)); assert_eq!(bits.len(), 65); - assert_eq!(decode_int_window(&bits, 0), None); - let (decoded, end) = decode_int(&bits, 0).expect("well-formed"); + assert_eq!(decode_int_window(crate::codec::built_view(&bits), 0), None); + let (decoded, end) = decode_int(crate::codec::built_view(&bits), 0).expect("well-formed"); assert_eq!(decoded, Base::from(n)); - assert_eq!(end, 65); + assert_eq!(end, 65u64); // Junk after a short code must not leak into its mantissa. let mut bits = BitsMut::new(); @@ -453,12 +461,15 @@ fn gamma_window_edge() { for _ in 0..64 { bits.push(true); } - assert_eq!(decode_int_window(&bits, 0), Some((5, code_len))); + assert_eq!( + decode_int_window(crate::codec::built_view(&bits), 0), + Some((5, code_len as u64)) + ); } -/// The window decoder never guesses at unprovable input: a slice whose origin -/// is mid-byte, a position at or past the stream end, and an all-zeros -/// (truncated) stream all decline to the bit loop. +/// The window decoder never guesses at unprovable input: a position at or +/// past the stream end and an all-zeros (truncated) stream both decline to +/// the bit loop. #[test] fn gamma_window_declines_conservatively() { use super::gamma::decode_int_window; @@ -467,18 +478,20 @@ fn gamma_window_declines_conservatively() { bits.push(false); bits.push(true); - // Mid-byte slice origin: no byte view, decline — but the same bit addressed - // as (whole slice, pos) has one, and the fast path fires. - assert_eq!(decode_int_window(&bits[1..], 0), None); - assert_eq!(decode_int_window(&bits, 1), Some((0, 2))); + // A mid-stream position addressed as (whole view, pos) proves its code + // and the fast path fires. + assert_eq!( + decode_int_window(crate::codec::built_view(&bits), 1), + Some((0, 2)) + ); // At and past the end of the stream. - assert_eq!(decode_int_window(&bits, 2), None); - assert_eq!(decode_int_window(&bits, 7), None); + assert_eq!(decode_int_window(crate::codec::built_view(&bits), 2), None); + assert_eq!(decode_int_window(crate::codec::built_view(&bits), 7), None); // All zeros: no terminating 1 in the stream (bit loop: `Truncated`). let zeros = BitsMut::repeat(false, 70); - assert_eq!(decode_int_window(&zeros, 0), None); + assert_eq!(decode_int_window(crate::codec::built_view(&zeros), 0), None); } /// A gamma code wide enough to spill machine-word decoding round-trips exactly @@ -490,9 +503,9 @@ fn gamma_roundtrip_wide_value() { let n = (Base::from(1u8) << 1000u32) + 12345u64; let mut bits = BitsMut::new(); encode_int(&mut bits, &n); - let (decoded, pos) = decode_int(&bits, 0).expect("well-formed"); + let (decoded, pos) = decode_int(crate::codec::built_view(&bits), 0).expect("well-formed"); assert_eq!(decoded, n); - assert_eq!(pos, bits.len()); + assert_eq!(pos, bits.len() as u64); } /// A stream that ends anywhere inside a wide mantissa is `Truncated`: the @@ -506,16 +519,16 @@ fn gamma_truncated_inside_wide_mantissa() { // Cuts inside the unary prefix, at the leading mantissa 1, just after it, // at byte-scale offsets into the mantissa, and one bit short. for cut in [1, 500, 1001, 1002, 1009, 1500, bits.len() - 1] { - let truncated = &bits[..cut]; + let truncated = crate::codec::BitsView::new(bits.as_raw_slice(), cut as u64); assert!( matches!(decode_int(truncated, 0), Err(Decode::Truncated)), "cut at bit {cut} must report Truncated", ); } // The full code still decodes: the cuts, not the value, are the failure. - let (decoded, pos) = decode_int(&bits, 0).expect("well-formed"); + let (decoded, pos) = decode_int(crate::codec::built_view(&bits), 0).expect("well-formed"); assert_eq!(decoded, n); - assert_eq!(pos, bits.len()); + assert_eq!(pos, bits.len() as u64); } // ──────────────────── metered Base equality and hashing ──────────────────── @@ -1250,7 +1263,7 @@ proptest! { // The marker sits at the live bit length; zeros fill the rest of // the final byte. - let used = party.as_bits().len(); + let used = party.as_bits().len() as usize; let total = valid.len() * 8; // Clearing the marker leaves the padding without its delimiter. @@ -1547,7 +1560,7 @@ fn ref_parse_id_str(s: &str) -> Result { if cur.peek().is_some() { return Err(crate::error::Parse::Syntax); } - super::validate_id(&bits)?; + super::validate_id(crate::codec::built_view(&bits))?; Ok(bits) } diff --git a/crates/before/src/codec/text.rs b/crates/before/src/codec/text.rs index 1fe503a2..a720feb3 100644 --- a/crates/before/src/codec/text.rs +++ b/crates/before/src/codec/text.rs @@ -79,7 +79,7 @@ pub(crate) fn parse_id_str(s: &str) -> Result { if cur.peek().is_some() { return Err(Parse::Syntax); // trailing junk } - validate_id(&bits)?; + validate_id(super::built_view(&bits))?; Ok(bits) } diff --git a/crates/before/src/codec/tree.rs b/crates/before/src/codec/tree.rs index 11c1ebdd..d420cbce 100644 --- a/crates/before/src/codec/tree.rs +++ b/crates/before/src/codec/tree.rs @@ -1,6 +1,6 @@ use crate::error::{Decode, Parse}; -use super::{BitCursor, BitsSlice, SliceCursor}; +use super::{BitCursor, BitsView}; /// While building a node bottom-up, what we still need from the stream. /// @@ -21,7 +21,9 @@ enum IdFrame { /// Parse one packed id tree at `pos`, validating id normal form (no node with /// two terminal children, that is `(1, 1)`). /// -/// Returns the position just past the tree. Iterative: depth lives on an +/// Returns the position just past the tree, at the walk's own `u64` width: +/// the byte decode doors walk their whole padded buffer as bits, whose +/// positions can exceed a 32-bit `usize`. Iterative: depth lives on an /// explicit stack, never the call stack. /// /// Each node is a 2-bit presence tag (bit 0 = left child follows, bit 1 = right @@ -30,24 +32,15 @@ enum IdFrame { /// tag, no bits of its own — so the grammar has no empty production: input /// exhausted before a tag completes, the empty input included, is /// [`Decode::Truncated`], exactly as a byte-starved reader reports it. -pub(crate) fn parse_id(bits: &BitsSlice, pos: usize) -> Result { - let mut cursor = SliceCursor::new(bits, pos); - parse_id_from(&mut cursor) -} - -/// [`parse_id`] over raw stream bytes, walking the whole `8 · bytes.len()` -/// bit view and returning the tree's end at the walk's own `u64` width. -/// -/// The byte decode doors' form, for buffers past the borrowed bit view's -/// encoding cap (64 MiB and up on a 32-bit target). -pub(crate) fn parse_id_bytes(bytes: &[u8]) -> Result { - let mut cursor = super::DsiCursor::over_bytes(bytes); +pub(crate) fn parse_id(bits: BitsView<'_>, pos: u64) -> Result { + let mut cursor = super::DsiCursor::new_at(bits, pos); parse_id_core(&mut cursor)?; Ok(cursor.position_u64()) } /// Parse and validate one id tree from a sequential bit cursor, returning the /// position just past it. +#[cfg(all(test, feature = "borsh"))] pub(crate) fn parse_id_from(cursor: &mut C) -> Result where Decode: From, @@ -119,7 +112,7 @@ where /// id is *allowed* is the caller's question, answered at the standalone-value /// gates (`Parse::Anonymous`); the wire grammar never asks it, because no /// encoder spells the anonymous id on the wire. -pub(crate) fn validate_id(bits: &BitsSlice) -> Result<(), Parse> { +pub(crate) fn validate_id(bits: BitsView<'_>) -> Result<(), Parse> { if bits.is_empty() { return Ok(()); } diff --git a/crates/before/src/idbits.rs b/crates/before/src/idbits.rs index af93b2b7..48a7372f 100644 --- a/crates/before/src/idbits.rs +++ b/crates/before/src/idbits.rs @@ -27,7 +27,7 @@ //! decoded node rather than subtree scans. Callers must only pass normal-form //! id bits. -use crate::codec::BitsSlice; +use crate::codec::BitsView; /// A decoded id node: the empty `0` leaf, the full `1` leaf, or an internal /// node tagged with which of its children are present. @@ -70,8 +70,8 @@ pub(crate) enum IdNode { /// cursor from the source per pass. pub(crate) enum IdReader<'a> { At { - bits: &'a BitsSlice, - pos: usize, + bits: BitsView<'a>, + pos: u64, }, /// A synthetic empty `0` leaf (see the type doc); reads as [`IdNode::Empty`] /// and never advances. @@ -81,7 +81,7 @@ pub(crate) enum IdReader<'a> { impl<'a> IdReader<'a> { /// A reader at the root of `bits`. Empty bits are the anonymous `0` id, so /// they read as the synthetic [`Empty`](IdReader::Empty) leaf. - pub(crate) fn root(bits: &'a BitsSlice) -> Self { + pub(crate) fn root(bits: BitsView<'a>) -> Self { if bits.is_empty() { IdReader::Empty } else { @@ -91,16 +91,16 @@ impl<'a> IdReader<'a> { /// A reader at an explicit bit offset, for resuming a scan at a recorded /// subtree position (see `split`'s `build_split`). - pub(crate) fn at(bits: &'a BitsSlice, pos: usize) -> Self { + pub(crate) fn at(bits: BitsView<'a>, pos: u64) -> Self { IdReader::At { bits, pos } } /// Decode the 2-bit tag at `pos`: `(left_present, right_present)`. Neither /// present is the terminal (`Full`); otherwise an internal node. #[inline] - fn tag(bits: &BitsSlice, pos: usize) -> IdNode { - let left = bits[pos]; - let right = bits[pos + 1]; + fn tag(bits: BitsView<'_>, pos: u64) -> IdNode { + let left = bits.bit(pos); + let right = bits.bit(pos + 1); if !left && !right { IdNode::Full } else { @@ -116,7 +116,7 @@ impl<'a> IdReader<'a> { IdReader::Empty => IdNode::Empty, IdReader::At { bits, pos } => { crate::codec::scan::record_bits(2); // one 2-bit tag scanned - let node = Self::tag(bits, *pos); + let node = Self::tag(*bits, *pos); *pos += 2; node } @@ -134,7 +134,7 @@ impl<'a> IdReader<'a> { IdReader::Empty => IdNode::Empty, IdReader::At { bits, pos } => { crate::codec::scan::record_bits(2); // one 2-bit tag scanned - Self::tag(bits, *pos) + Self::tag(*bits, *pos) } } } @@ -149,7 +149,7 @@ impl<'a> IdReader<'a> { // One 2-bit tag scanned per skip step. Children present = // the two tag bits; the tag is 2 bits wide. crate::codec::scan::record_bits(2); - let children = usize::from(bits[at]) + usize::from(bits[at + 1]); + let children = u64::from(bits.bit(at)) + u64::from(bits.bit(at + 1)); (children, at + 2) }); } @@ -174,16 +174,16 @@ impl<'a> IdReader<'a> { /// /// Used for `sum`/`diff` capacity hints, where an anonymous (`0`) operand /// is a synthetic [`Empty`](IdReader::Empty) contributing zero bits. - pub(crate) fn bits(&self) -> &'a BitsSlice { + pub(crate) fn bits(&self) -> BitsView<'a> { match self { - IdReader::At { bits, .. } => bits, - IdReader::Empty => BitsSlice::empty(), + IdReader::At { bits, .. } => *bits, + IdReader::Empty => BitsView::empty(), } } /// This reader's bit offset, for copying a subtree's verbatim bit range or /// recording a branch position. Not called on a synthetic reader. - pub(crate) fn pos(&self) -> usize { + pub(crate) fn pos(&self) -> u64 { match self { IdReader::At { pos, .. } => *pos, IdReader::Empty => { @@ -207,10 +207,7 @@ impl<'a> IdReader<'a> { /// The single shared spelling of this scan: [`IdReader::skip`] runs it on the /// packed id encoding, and the skyline `grow` walks run it to skip event /// subtrees (one topology flag plus one skipped payload code per node). -pub(crate) fn skip_subtree( - mut at: usize, - mut header: impl FnMut(usize) -> (usize, usize), -) -> usize { +pub(crate) fn skip_subtree(mut at: u64, mut header: impl FnMut(u64) -> (u64, u64)) -> u64 { let mut pending: i64 = 1; while pending > 0 { let (children, next) = header(at); diff --git a/crates/before/src/meter.rs b/crates/before/src/meter.rs index ef0d0bf6..c304c54c 100644 --- a/crates/before/src/meter.rs +++ b/crates/before/src/meter.rs @@ -109,14 +109,8 @@ impl Packed { } /// The generator's live bits, borrowed. - /// - /// A borrowed bit view, sized for the instrument surface: the shape - /// generators emit parameter-scale streams far below the view - /// encoding's cap (`usize::MAX >> 3` bits), where the byte decode - /// doors — which admit larger buffers on 32-bit targets — walk raw - /// bytes instead. - pub fn as_bits(&self) -> &codec::BitsSlice { - &codec::bytes_as_bits(&self.bytes)[..self.bits] + pub fn as_bits(&self) -> codec::BitsView<'_> { + codec::BitsView::new(&self.bytes, self.bits as u64) } /// Lift an event-shape generator's output into a stored [`Version`](crate::Version), diff --git a/crates/before/src/meter/board/ceilings.rs b/crates/before/src/meter/board/ceilings.rs index 5c32aa99..087551cc 100644 --- a/crates/before/src/meter/board/ceilings.rs +++ b/crates/before/src/meter/board/ceilings.rs @@ -291,7 +291,7 @@ pub(super) fn both_present_nodes(p: &Party) -> u64 { let mut count = 0u64; let mut i = 0; while i + 1 < bits.len() { - count += u64::from(bits[i] && bits[i + 1]); + count += u64::from(bits.bit(i) && bits.bit(i + 1)); i += 2; } count diff --git a/crates/before/src/meter/board/defect.rs b/crates/before/src/meter/board/defect.rs index 8cdc5901..ef023b82 100644 --- a/crates/before/src/meter/board/defect.rs +++ b/crates/before/src/meter/board/defect.rs @@ -64,16 +64,15 @@ pub(super) fn trailing_bytes(bytes: &[u8]) -> Vec { /// Iterative over the packed form, outside any measurement; the last node of a /// preorder event stream is always a leaf (an internal node's children would /// follow it). -fn last_leaf_flag_pos(v: &Version) -> usize { - let all = codec::bytes_as_bits(v.as_bytes()); - let bits = &all[..v.encoded_bits()]; - let mut pos = 0usize; +fn last_leaf_flag_pos(v: &Version) -> u64 { + let bits = v.as_bits(); + let mut pos = 0u64; let mut pending = 1usize; - let mut last = 0usize; + let mut last = 0u64; while pending > 0 { pending -= 1; let flag = pos; - let internal = !bits[pos]; // skyline flag: 0 internal, 1 leaf + let internal = !bits.bit(pos); // skyline flag: 0 internal, 1 leaf pos += 1; if internal { pending += 2; @@ -93,13 +92,12 @@ fn last_leaf_flag_pos(v: &Version) -> usize { /// validator can only judge at that pair's close, the stream's last position. /// The maximally-deferred [`NotCanonical`](crate::error::Decode) defect. pub(super) fn version_noncanonical_bytes(v: &Version) -> Vec { - let all = codec::bytes_as_bits(v.as_bytes()); - let bits = &all[..v.encoded_bits()]; + let bits = v.as_bits(); let leaf = last_leaf_flag_pos(v); - let mut out = codec::BitsMut::with_capacity(bits.len() + 4); - out.extend_from_bitslice(&bits[..leaf]); + let mut out = codec::BitsMut::with_capacity(bits.len() as usize + 4); + codec::extend_from_view(&mut out, bits, 0, leaf); out.push(false); // the old leaf's position becomes an internal node - out.extend_from_bitslice(&bits[leaf..]); // left child: the old leaf verbatim + codec::extend_from_view(&mut out, bits, leaf, bits.len()); // left child: the old leaf verbatim out.push(true); // right child: a leaf equal to its sibling codec::encode_int(&mut out, &Base::from(0u32)); // zero delta codec::seal_padding(&mut out); @@ -116,11 +114,11 @@ pub(super) fn party_noncanonical_bytes(p: &Party) -> Vec { let bits = p.as_bits(); let end = bits.len(); assert!( - !bits[end - 2] && !bits[end - 1], + !bits.bit(end - 2) && !bits.bit(end - 1), "a preorder id stream ends in a terminal tag" ); - let mut out = codec::BitsMut::with_capacity(end + 4); - out.extend_from_bitslice(&bits[..end - 2]); + let mut out = codec::BitsMut::with_capacity(end as usize + 4); + codec::extend_from_view(&mut out, bits, 0, end - 2); out.push(true); // the last terminal becomes a node with both children out.push(true); for _ in 0..2 { diff --git a/crates/before/src/meter/board/family.rs b/crates/before/src/meter/board/family.rs index 0369d69b..e1d5e09e 100644 --- a/crates/before/src/meter/board/family.rs +++ b/crates/before/src/meter/board/family.rs @@ -1109,10 +1109,11 @@ impl FamilyData { fn disjoint_mounted_pair(id: &[u8]) -> (Vec, Vec) { let shape = decode_party(id); let mount = |left: bool| -> Vec { - let mut bits = codec::BitsMut::with_capacity(shape.as_bits().len() + 2); + let view = shape.as_bits(); + let mut bits = codec::BitsMut::with_capacity(view.len() as usize + 2); bits.push(left); bits.push(!left); - bits.extend_from_bitslice(shape.as_bits()); + codec::extend_from_view(&mut bits, view, 0, view.len()); codec::seal_padding(&mut bits); bits.into_vec() }; @@ -1151,10 +1152,10 @@ pub(super) fn overlap_mounted_pair(id: &[u8]) -> (Vec, Vec) { "the overlap-mount adapter needs a non-terminal shape: a full shape's mount would \ not be normal form" ); - let mut a = codec::BitsMut::with_capacity(bits.len() + 2 * path.len() + 4); + let mut a = codec::BitsMut::with_capacity(bits.len() as usize + 2 * path.len() + 4); a.push(true); // root: both children present a.push(true); - a.extend_from_bitslice(bits); // left: the shape + codec::extend_from_view(&mut a, bits, 0, bits.len()); // left: the shape for &go_right in &path { // right: the marker chain, one single-child node per level a.push(!go_right); @@ -1163,10 +1164,10 @@ pub(super) fn overlap_mounted_pair(id: &[u8]) -> (Vec, Vec) { a.push(false); // the marker's terminal, at the shape's last owned position a.push(false); codec::seal_padding(&mut a); - let mut b = codec::BitsMut::with_capacity(bits.len() + 2); + let mut b = codec::BitsMut::with_capacity(bits.len() as usize + 2); b.push(false); // root: right child only b.push(true); - b.extend_from_bitslice(bits); // right: the shape + codec::extend_from_view(&mut b, bits, 0, bits.len()); // right: the shape codec::seal_padding(&mut b); let (a, b) = (a.into_vec(), b.into_vec()); assert!( @@ -1183,12 +1184,12 @@ pub(super) fn overlap_mounted_pair(id: &[u8]) -> (Vec, Vec) { /// belongs to the node reached by always taking the rightmost present child; /// left subtrees along the way are skipped (each exactly once, so the walk is /// linear). Runs at bundle build, outside any measurement. -fn rightmost_terminal_path(bits: &codec::BitsSlice) -> Vec { - let mut pos = 0usize; +fn rightmost_terminal_path(bits: codec::BitsView<'_>) -> Vec { + let mut pos = 0u64; let mut path = Vec::new(); loop { - let left = bits[pos]; - let right = bits[pos + 1]; + let left = bits.bit(pos); + let right = bits.bit(pos + 1); pos += 2; if !left && !right { return path; // the terminal @@ -1196,7 +1197,7 @@ fn rightmost_terminal_path(bits: &codec::BitsSlice) -> Vec { if right { if left { pos = crate::idbits::skip_subtree(pos, |at| { - let children = usize::from(bits[at]) + usize::from(bits[at + 1]); + let children = u64::from(bits.bit(at)) + u64::from(bits.bit(at + 1)); (children, at + 2) }); } diff --git a/crates/before/src/meter/board/operand.rs b/crates/before/src/meter/board/operand.rs index 48f890b8..5f371e57 100644 --- a/crates/before/src/meter/board/operand.rs +++ b/crates/before/src/meter/board/operand.rs @@ -21,15 +21,14 @@ use super::ceilings::MACHINE_WORD_MAGNITUDE_BITS; /// does (a plateau-heavy stream legitimately reads near zero). Iterative over /// the packed form, outside any measurement. pub(super) fn stored_nonzero_deltas(v: &Version) -> u64 { - let all = codec::bytes_as_bits(v.as_bytes()); - let bits = &all[..v.encoded_bits()]; - let mut pos = 0usize; + let bits = v.as_bits(); + let mut pos = 0u64; let mut pending = 1usize; let mut first = true; let mut nonzero = 0u64; while pending > 0 { pending -= 1; - let internal = !bits[pos]; // skyline flag: 0 internal, 1 leaf + let internal = !bits.bit(pos); // skyline flag: 0 internal, 1 leaf pos += 1; if internal { pending += 2; @@ -56,14 +55,13 @@ pub(super) fn stored_nonzero_deltas(v: &Version) -> u64 { /// that read the stored form as-is. Iterative over the packed form, outside any /// measurement. pub(super) fn mandatory_limbs_stream(v: &Version) -> u64 { - let all = codec::bytes_as_bits(v.as_bytes()); - let bits = &all[..v.encoded_bits()]; - let mut pos = 0usize; + let bits = v.as_bits(); + let mut pos = 0u64; let mut pending = 1usize; let mut limbs = 0u64; while pending > 0 { pending -= 1; - let internal = !bits[pos]; // skyline flag: 0 internal, 1 leaf + let internal = !bits.bit(pos); // skyline flag: 0 internal, 1 leaf pos += 1; if internal { pending += 2; @@ -89,15 +87,14 @@ pub(super) fn mandatory_limbs_stream(v: &Version) -> u64 { /// packed bytes grow only by the unit delta codes over a fixed wide intercept. /// Iterative over the packed form, outside any measurement. pub(super) fn value_content_bytes(v: &Version) -> usize { - let all = codec::bytes_as_bits(v.as_bytes()); - let bits = &all[..v.encoded_bits()]; - let mut pos = 0usize; + let bits = v.as_bits(); + let mut pos = 0u64; let mut pending = 1usize; let mut last: Option = None; let mut content = 0u64; while pending > 0 { pending -= 1; - let internal = !bits[pos]; // skyline flag: 0 internal, 1 leaf + let internal = !bits.bit(pos); // skyline flag: 0 internal, 1 leaf pos += 1; if internal { pending += 2; @@ -157,16 +154,15 @@ pub(super) fn mandatory_limbs_version(v: &Version) -> u64 { /// (absolute leaf heights, bottom-up subtree floors, per-node relative bases), /// entirely outside any measurement. pub(super) fn stored_bases(v: &Version) -> Vec { - let all = codec::bytes_as_bits(v.as_bytes()); - let bits = &all[..v.encoded_bits()]; + let bits = v.as_bits(); // Pass 1: topology flags and absolute leaf heights. - let mut pos = 0usize; + let mut pos = 0u64; let mut topology: Vec = Vec::new(); let mut heights: Vec = Vec::new(); let mut pending = 1usize; while pending > 0 { pending -= 1; - let internal = !bits[pos]; // skyline flag: 0 internal, 1 leaf + let internal = !bits.bit(pos); // skyline flag: 0 internal, 1 leaf pos += 1; topology.push(internal); if internal { @@ -265,13 +261,13 @@ pub(super) fn radix_units_party(p: &Party) -> u64 { if bits.is_empty() { return 1; // the empty id renders one `0` token } - let mut pos = 0usize; + let mut pos = 0u64; let mut pending = 1u64; let mut units = 0u64; while pending > 0 { pending -= 1; - let left = bits[pos]; - let right = bits[pos + 1]; + let left = bits.bit(pos); + let right = bits.bit(pos + 1); pos += 2; if !left && !right { units += 1; // a terminal renders `1` diff --git a/crates/before/src/meter/board/tests.rs b/crates/before/src/meter/board/tests.rs index 23825e69..e3d4c38a 100644 --- a/crates/before/src/meter/board/tests.rs +++ b/crates/before/src/meter/board/tests.rs @@ -388,7 +388,7 @@ fn mandatory_limbs_match_hand_counts() { #[cfg(feature = "scan-meter")] fn bypass_walk(v: &Version) -> usize { let bits = v.as_bits(); - (0..bits.len()).filter(|&i| bits[i]).count() + (0..bits.len()).filter(|&i| bits.bit(i)).count() } /// A body that does its traversal outside the metered primitives reads green diff --git a/crates/before/src/meter/tests.rs b/crates/before/src/meter/tests.rs index 21125db0..73171d2e 100644 --- a/crates/before/src/meter/tests.rs +++ b/crates/before/src/meter/tests.rs @@ -799,9 +799,7 @@ fn masked_hole_decodes_canonically_and_realizes_less() { /// topology walk (payload codes skipped unread). fn leaf_count(v: &Version) -> usize { use codec::BitCursor; - let all = codec::bytes_as_bits(v.as_bytes()); - let bits = &all[..v.encoded_bits()]; - let mut cur = codec::DsiCursor::new(bits); + let mut cur = codec::DsiCursor::new(v.as_bits()); let mut pending = 1usize; let mut leaves = 0usize; while pending > 0 { diff --git a/crates/before/src/meter/tier2.rs b/crates/before/src/meter/tier2.rs index 46ff9c1d..ee2c7045 100644 --- a/crates/before/src/meter/tier2.rs +++ b/crates/before/src/meter/tier2.rs @@ -18,7 +18,7 @@ //! 2|k| - 1` (no negative zero), and each mapped delta is then gamma-coded //! exactly like today's stored bases. -use crate::codec::{self, Base, BitsSlice}; +use crate::codec::{self, Base, BitsView}; /// The Tier 2 encoded bit length of a [`Version`](crate::Version), split into /// the terms the compactness envelope is stated over. @@ -54,8 +54,8 @@ pub struct Tier2Size { /// /// Panics if the packed form does not parse cleanly; callers hand in /// generator-built canonical streams. -pub fn tier2_size(bits: &BitsSlice) -> Tier2Size { - let mut pos = 0usize; +pub fn tier2_size(bits: BitsView<'_>) -> Tier2Size { + let mut pos = 0u64; // Inherited root-to-node path sums for the nodes not yet visited, top of // stack belonging to the next node in the preorder stream. Both children of // an internal node inherit the same sum, and the stream lists the whole @@ -68,7 +68,7 @@ pub fn tier2_size(bits: &BitsSlice) -> Tier2Size { let mut prev_leaf: Option = None; while let Some(offset) = offsets.pop() { - let internal = bits[pos]; + let internal = bits.bit(pos); pos += 1; let (base, next) = codec::decode_int(bits, pos).expect("canonical Version parses cleanly"); pos = next; diff --git a/crates/before/src/meter/tier2/tests.rs b/crates/before/src/meter/tier2/tests.rs index 1782d871..18c066bd 100644 --- a/crates/before/src/meter/tier2/tests.rs +++ b/crates/before/src/meter/tier2/tests.rs @@ -45,11 +45,17 @@ const JOIN_MEET_BOUNDARY_SLACK_BITS: u64 = 4; /// total Tier 2 bits within [`JOIN_MEET_BOUNDARY_SLACK_BITS`] per input leaf of /// the inputs' sum. fn check_join_meet_lipschitz(a: &Version, b: &Version) { - let sa = tier2_size(&packed_bits_of(&to_oracle_version(a))); - let sb = tier2_size(&packed_bits_of(&to_oracle_version(b))); + let sa = tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(a), + ))); + let sb = tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(b), + ))); for (name, emit) in EMITTERS { let out = emit(a, b); - let so = tier2_size(&packed_bits_of(&to_oracle_version(&out))); + let so = tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(&out), + ))); assert!( so.leaves < sa.leaves + sb.leaves, "{name}: {} output leaves reach the input leaf total {} + {}: \ @@ -78,7 +84,9 @@ fn check_join_meet_lipschitz(a: &Version, b: &Version) { #[test] fn empty_version_is_two_bits() { let v = Version::new(); - let size = tier2_size(&packed_bits_of(&to_oracle_version(&v))); + let size = tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(&v), + ))); assert_eq!( size, Tier2Size { @@ -98,7 +106,9 @@ fn empty_version_is_two_bits() { fn single_small_leaf_matches_current_size() { let mut v = Version::new(); v.tick(&Party::seed()); - let size = tier2_size(&packed_bits_of(&to_oracle_version(&v))); + let size = tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(&v), + ))); assert_eq!(size.total_bits, 4); assert_eq!((size.nodes, size.leaves), (1, 1)); assert_eq!(size.total_bits, v.encoded_bits() as u64); @@ -133,7 +143,9 @@ fn one_fork_matches_hand_computation() { )); assert_eq!(packed_bits_of(&to_oracle_version(&v)).len(), 10); assert_eq!(v.encoded_bits(), 11, "the stored coding is Tier 2 itself"); - let size = tier2_size(&packed_bits_of(&to_oracle_version(&v))); + let size = tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(&v), + ))); assert_eq!( size, Tier2Size { @@ -366,11 +378,18 @@ fn operator_meet(a: &Version, b: &Version) -> Version { /// [`tier2_size`] on the result: the pins then price the kernel's own /// output stream, not merely the value it denotes. fn skyline_join(a: &Version, b: &Version) -> Version { - let out = skyline::emit::join(&skyline::encode(a), &skyline::encode(b)); - let decoded = skyline::decode(&out).expect("an emitted join is canonical"); + let out = skyline::emit::join( + crate::codec::built_view(&skyline::encode(a)), + crate::codec::built_view(&skyline::encode(b)), + ); + let decoded = + skyline::decode(crate::codec::built_view(&out)).expect("an emitted join is canonical"); assert_eq!( out.len() as u64, - tier2_size(&packed_bits_of(&to_oracle_version(&decoded))).total_bits, + tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(&decoded) + ))) + .total_bits, "the emitted join stream must be exactly the canonical coded size" ); decoded @@ -378,11 +397,18 @@ fn skyline_join(a: &Version, b: &Version) -> Version { /// The emission kernel's meet, called directly (no short-circuits). fn skyline_meet(a: &Version, b: &Version) -> Version { - let out = skyline::emit::meet(&skyline::encode(a), &skyline::encode(b)); - let decoded = skyline::decode(&out).expect("an emitted meet is canonical"); + let out = skyline::emit::meet( + crate::codec::built_view(&skyline::encode(a)), + crate::codec::built_view(&skyline::encode(b)), + ); + let decoded = + skyline::decode(crate::codec::built_view(&out)).expect("an emitted meet is canonical"); assert_eq!( out.len() as u64, - tier2_size(&packed_bits_of(&to_oracle_version(&decoded))).total_bits, + tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(&decoded) + ))) + .total_bits, "the emitted meet stream must be exactly the canonical coded size" ); decoded @@ -399,9 +425,15 @@ fn check_subadditive( a: &Version, b: &Version, ) { - let sa = tier2_size(&packed_bits_of(&to_oracle_version(a))); - let sb = tier2_size(&packed_bits_of(&to_oracle_version(b))); - let so = tier2_size(&packed_bits_of(&to_oracle_version(&emit(a, b)))); + let sa = tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(a), + ))); + let sb = tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(b), + ))); + let so = tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(&emit(a, b)), + ))); assert!( so.total_bits + JOIN_MEET_SUBADDITIVITY_SAVINGS_BITS <= sa.total_bits + sb.total_bits, "{name}: subadditivity violated: {} output bits > {} + {} input bits - {} pinned savings", @@ -465,11 +497,19 @@ fn magnitude_bits() -> impl Strategy { fn empty_pair_is_the_subadditivity_equality_case() { let (a, b) = (Version::new(), Version::new()); for (name, emit) in EMITTERS { - let so = tier2_size(&packed_bits_of(&to_oracle_version(&emit(&a, &b)))); + let so = tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(&emit(&a, &b)), + ))); assert_eq!( so.total_bits + JOIN_MEET_SUBADDITIVITY_SAVINGS_BITS, - tier2_size(&packed_bits_of(&to_oracle_version(&a))).total_bits - + tier2_size(&packed_bits_of(&to_oracle_version(&b))).total_bits, + tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(&a) + ))) + .total_bits + + tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(&b) + ))) + .total_bits, "{name} of two empty versions must realize the savings margin exactly", ); } @@ -679,7 +719,9 @@ fn cross_boundary_equal_leaves_are_smaller_in_tier2() { oracle::Version::leaf(1u64), )); assert_eq!(packed_bits_of(&to_oracle_version(&v)).len(), 14); - let size = tier2_size(&packed_bits_of(&to_oracle_version(&v))); + let size = tier2_size(crate::codec::built_view(&packed_bits_of( + &to_oracle_version(&v), + ))); assert_eq!( size, Tier2Size { diff --git a/crates/before/src/party.rs b/crates/before/src/party.rs index 3c690993..04ebf6ae 100644 --- a/crates/before/src/party.rs +++ b/crates/before/src/party.rs @@ -18,7 +18,7 @@ use core::fmt::Display; -use crate::codec::{self, BitsSlice}; +use crate::codec::{self, BitsView}; use crate::error::{Decode, Parse}; use crate::idbits::IdReader; use crate::{Ticks, Version}; @@ -457,7 +457,7 @@ impl Party { /// ``` pub fn without(self, other: &Party) -> Option { let bits = self.view().diff(other.view()); - if codec::id_is_empty(&bits) { + if codec::id_is_empty(codec::built_view(&bits)) { None } else { Some(Party::from_bits(bits)) @@ -551,10 +551,7 @@ impl Party { /// assert_eq!(before::Party::seed().encoded_bits(), 2); /// ``` pub fn encoded_bits(&self) -> usize { - // The stored form's O(1) length, never the borrowed bit view: the - // view's encoding caps below the stored sizes the decode doors admit - // on 32-bit targets, while the length itself is exact at every - // storable size. + // The stored form's O(1) length: exact at every storable size. self.0.len() } @@ -582,13 +579,12 @@ impl Party { pub fn decode(mut reader: R) -> Result { let mut buf = Vec::new(); reader.read_to_end(&mut buf).map_err(Decode::Io)?; - // Validate over the raw bytes (never a borrowed bit view, whose - // encoding caps below the buffer sizes this door admits on 32-bit - // targets): the whole `8 · buf.len()`-bit view is the walk's input, - // padding bits included, and the marker check judges the remainder. + // Validate over the whole buffer as bits, padding included: the + // walk's input is the whole `8 · buf.len()`-bit view, and the marker + // check judges the remainder. { - let end = codec::parse_id_bytes(&buf)?; - codec::require_marker_padding_bytes(&buf, end)?; + let end = codec::parse_id(codec::BitsView::whole(&buf), 0)?; + codec::require_marker_padding(&buf, end)?; } // Adopt the read buffer as the result's backing store without // copying: the padding check proved the buffer is the stream's one @@ -615,7 +611,7 @@ impl Party { /// A read-only [`IdReader`] cursor at the root of this party's packed id bits. pub(crate) fn view(&self) -> IdReader<'_> { - IdReader::root(&self.0) + IdReader::root(self.0.live()) } /// Reunites this party with `other` and re-splits the union, in one fused @@ -665,8 +661,8 @@ impl Party { /// The packed preorder bit stream, live bits only (the padding stays /// behind the view). Internal. - pub(crate) fn as_bits(&self) -> &BitsSlice { - &self.0 + pub(crate) fn as_bits(&self) -> BitsView<'_> { + self.0.live() } /// Freeze a normal-form packed bit stream as a `Party`, canonicalizing its @@ -709,7 +705,7 @@ impl Party { /// ``` impl core::fmt::Display for Party { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - codec::write_id(&self.0, f, ", ") + codec::write_id(self.0.live(), f, ", ") } } @@ -752,7 +748,7 @@ impl core::str::FromStr for Party { /// identity. The single gate through which every parsed/built top-level `Party` /// passes. fn finish_id(bits: codec::BitsMut) -> Result { - if codec::id_is_empty(&bits) { + if codec::id_is_empty(codec::built_view(&bits)) { Err(Parse::Anonymous) } else { Ok(Party::from_bits(bits)) diff --git a/crates/before/src/party/ops/build.rs b/crates/before/src/party/ops/build.rs index e3f803e4..07dfaa0c 100644 --- a/crates/before/src/party/ops/build.rs +++ b/crates/before/src/party/ops/build.rs @@ -1,4 +1,4 @@ -use crate::codec::{BitStack, BitsMut, BitsSlice, PackedBuilder, PopStack}; +use crate::codec::{BitStack, BitsMut, BitsView, PackedBuilder, PopStack}; use crate::idbits::{IdNode, IdReader}; /// Single-buffer builder for normalized id output. @@ -91,7 +91,7 @@ impl IdBuilder { src.skip(); // The peek and the skip above record their own reads; the splice // records the write. - self.out.splice(&src.bits()[start..src.pos()]); + self.out.splice(src.bits(), start, src.pos()); if is_terminal { Built::Terminal } else { @@ -99,11 +99,12 @@ impl IdBuilder { } } - /// Append a complete already-normal subtree's bits verbatim (the splice - /// records the write), for a spliced child whose kind the caller reports to - /// [`close_node`](Self::close_node) itself. - pub(super) fn splice(&mut self, src: &BitsSlice) { - self.out.splice(src); + /// Append an already-normal subtree's bits — the range `start..end` of + /// `src` — verbatim (the splice records the write), for a spliced child + /// whose kind the caller reports to [`close_node`](Self::close_node) + /// itself. + pub(super) fn splice(&mut self, src: BitsView<'_>, start: u64, end: u64) { + self.out.splice(src, start, end); } /// Normalize and close the node opened at `node` from what its two children @@ -243,7 +244,7 @@ impl IdSkylineBuilder { /// by plateau would close as (its root has a child that is neither /// both-empty nor both-terminal), so the ancestors' presence patches and /// collapses are unchanged. - pub(super) fn subtree(&mut self, depth: usize, src: &BitsSlice) { + pub(super) fn subtree(&mut self, depth: usize, src: BitsView<'_>, start: u64, end: u64) { debug_assert!( self.root.is_none(), "a subtree arrived after the final plateau: the tiling is complete" @@ -253,7 +254,7 @@ impl IdSkylineBuilder { "a subtree depth above its forced flip level: the input is not one preorder tiling" ); debug_assert!( - src[0] || src[1], + src.bit(start) || src.bit(start + 1), "a spliced block is an internal subtree, never a lone terminal" ); // Open an ancestor per level entered, exactly as a leaf would. @@ -262,7 +263,7 @@ impl IdSkylineBuilder { self.tags.push(at); self.path.push(false); } - self.out.splice(src); + self.out.splice(src, start, end); self.close_up(Built::Node); } diff --git a/crates/before/src/party/ops/diff.rs b/crates/before/src/party/ops/diff.rs index 59cdfe79..702ac564 100644 --- a/crates/before/src/party/ops/diff.rs +++ b/crates/before/src/party/ops/diff.rs @@ -44,7 +44,7 @@ //! the shape on which a structural walk's recursion depth tracks the full tree //! depth — cost bits, not stack frames or grown segments. -use crate::codec::{BitsMut, BitsSlice}; +use crate::codec::{BitsMut, BitsView}; use crate::idbits::IdReader; use crate::version::skyline::overlay::{self, PlateauCursor}; @@ -74,7 +74,8 @@ impl IdReader<'_> { // `self \ other ⊆ self`, but over a full `self` plateau the output // is `other`'s complement, which can be as large as `other`. Both // inputs combined is a safe bound; normalization only shrinks it. - let mut out = IdSkylineBuilder::with_capacity(self.bits().len() + other.bits().len()); + let mut out = + IdSkylineBuilder::with_capacity((self.bits().len() + other.bits().len()) as usize); let (mut a, a_entering) = IdLeafCursor::open(self); let (mut b, b_entering) = IdLeafCursor::open(other); settle_pair(&mut a, &mut b, a_entering, b_entering); @@ -89,7 +90,7 @@ impl IdReader<'_> { b.depth() <= a.depth() && !b.owned(), "a splice is covered by an unowned `other` plateau" ); - out.subtree(a.depth(), &a.bits[start..a.pos]); + out.subtree(a.depth(), a.bits, start, a.pos); } Item::Plateau { owned } => { out.leaf(a.depth().max(b.depth()), owned && !b.owned()); @@ -210,7 +211,7 @@ enum Item { /// A whole `self` subtree consumed as one covered block, to be spliced /// verbatim: `bits[start..pos]`. Only ever formed on the `self` cursor, /// under an unowned `other` plateau. - Splice { start: usize }, + Splice { start: u64 }, } /// What one descent move resolved to (see [`IdLeafCursor::enter`]). @@ -243,11 +244,11 @@ enum Enter { /// ([`consume`](Self::consume)) or walked plateau by plateau /// ([`enter`](Self::enter)/[`descend`](Self::descend)). struct IdLeafCursor<'a> { - bits: &'a BitsSlice, + bits: BitsView<'a>, /// The next unread tag's bit offset. Preorder consumption keeps it at the /// subtree of the next *present* child slot the walk flips into; synthetic /// plateaus consume nothing. - pos: usize, + pos: u64, /// Root-to-item branch directions: `false` inside a left child slot, `true` /// inside a right. path: BitsMut, @@ -273,7 +274,7 @@ impl<'a> IdLeafCursor<'a> { /// (`false`). fn open(src: IdReader<'a>) -> (Self, bool) { let mut this = IdLeafCursor { - bits: BitsSlice::empty(), + bits: BitsView::empty(), pos: 0, path: BitsMut::new(), pending_right: BitsMut::new(), @@ -307,7 +308,7 @@ impl<'a> IdLeafCursor<'a> { /// present ([`Enter::Left`]). fn enter(&mut self) -> Enter { crate::codec::scan::record_bits(2); // one 2-bit tag read - let (left, right) = (self.bits[self.pos], self.bits[self.pos + 1]); + let (left, right) = (self.bits.bit(self.pos), self.bits.bit(self.pos + 1)); self.pos += 2; if !left && !right { // The terminal `1` leaf. @@ -347,18 +348,18 @@ impl<'a> IdLeafCursor<'a> { fn consume(&mut self, splice: bool) { let start = self.pos; crate::codec::scan::record_bits(2); // the subtree top's 2-bit tag - let (left, right) = (self.bits[self.pos], self.bits[self.pos + 1]); + let (left, right) = (self.bits.bit(self.pos), self.bits.bit(self.pos + 1)); self.pos += 2; if !left && !right { self.item = Item::Plateau { owned: true }; return; } let bits = self.bits; - let scan = |at: usize| { + let scan = |at: u64| { // One 2-bit tag scanned per skipped node. Children present = the // two tag bits; the tag is 2 bits wide. crate::codec::scan::record_bits(2); - let children = usize::from(bits[at]) + usize::from(bits[at + 1]); + let children = u64::from(bits.bit(at)) + u64::from(bits.bit(at + 1)); (children, at + 2) }; if left { diff --git a/crates/before/src/party/ops/index.rs b/crates/before/src/party/ops/index.rs index f5824243..1f541b94 100644 --- a/crates/before/src/party/ops/index.rs +++ b/crates/before/src/party/ops/index.rs @@ -32,7 +32,7 @@ //! and it exists *only* because the fold repeats the test against one fixed //! side; single-shot predicates stay on the cursor walk, which needs no table. -use crate::codec::BitsSlice; +use crate::codec::BitsView; use crate::idbits::{IdNode, IdReader}; /// A random-access view of one packed id operand: the operand's bits plus a @@ -44,7 +44,7 @@ use crate::idbits::{IdNode, IdReader}; /// indexed operand — strictly smaller than the operand itself. pub(crate) struct IdIndex<'a> { /// The indexed operand's packed preorder tag stream. - bits: &'a BitsSlice, + bits: BitsView<'a>, /// `rights[i]` is the right child's bit position for the `i`-th /// both-present node in preorder. Only both-present nodes need an entry: a /// right-only node's child follows its tag directly, and a left child @@ -69,16 +69,16 @@ impl<'a> IdIndex<'a> { /// child's) — threading the frames' entry slots through the not-yet-filled /// table itself, so the only transient state beyond the table is one `bool` /// per open frame. - pub(crate) fn build(bits: &'a BitsSlice) -> IdIndex<'a> { - if bits.len() > u32::MAX as usize { + pub(crate) fn build(bits: BitsView<'a>) -> IdIndex<'a> { + if bits.len() > u64::from(u32::MAX) { return IdIndex { bits, rights: None }; } // Pass 1: count. Reads every tag once. - crate::codec::scan::record_bits(bits.len()); + crate::codec::scan::record_bits_u64(bits.len()); let mut count = 0usize; - let mut p = 0; + let mut p = 0u64; while p < bits.len() { - if bits[p] && bits[p + 1] { + if bits.bit(p) && bits.bit(p + 1) { count += 1; } p += 2; @@ -88,7 +88,7 @@ impl<'a> IdIndex<'a> { // next-outer awaiting frame (`u32::MAX` terminates the chain); the real // right-child position overwrites the link the moment the left subtree // completes. - crate::codec::scan::record_bits(bits.len()); + crate::codec::scan::record_bits_u64(bits.len()); let mut rights = vec![0u32; count]; // One bit per open both-present frame, innermost last: `true` while the // frame awaits its left subtree's end, `false` while it awaits its @@ -96,9 +96,9 @@ impl<'a> IdIndex<'a> { let mut awaiting_left: Vec = Vec::new(); let mut chain_head = u32::MAX; let mut next_entry = 0usize; - let mut p = 0; + let mut p = 0u64; while p < bits.len() { - let (left, right) = (bits[p], bits[p + 1]); + let (left, right) = (bits.bit(p), bits.bit(p + 1)); p += 2; if left && right { rights[next_entry] = chain_head; @@ -154,7 +154,7 @@ impl<'a> IdIndex<'a> { /// to hold [`is_disjoint`](IdIndex::is_disjoint)'s unindexed arm to the /// cursor walk's verdict. #[cfg(test)] - pub(crate) fn build_unindexed(bits: &'a BitsSlice) -> IdIndex<'a> { + pub(crate) fn build_unindexed(bits: BitsView<'a>) -> IdIndex<'a> { IdIndex { bits, rights: None } } @@ -178,7 +178,7 @@ impl<'a> IdIndex<'a> { }; // The current pair's indexed side: the bit position of its present // node, or `None` for an absent (empty) region. - let mut node: Option = (!self.bits.is_empty()).then_some(0); + let mut node: Option = (!self.bits.is_empty()).then_some(0); // The table index of the first entry at or after the current node — the // node's own entry whenever it is both-present. let mut entry = 0usize; @@ -186,7 +186,7 @@ impl<'a> IdIndex<'a> { // (`None` = absent child) and its entry bound. `other`'s right children // need no bookkeeping — its cursor reaches each one in stream order, // exactly as in the cursor walk. - let mut pending: Vec<(Option, usize)> = Vec::new(); + let mut pending: Vec<(Option, usize)> = Vec::new(); loop { // One pair per iteration, driven by the input side. let b = other.read(); @@ -198,7 +198,7 @@ impl<'a> IdIndex<'a> { } Some(p) => { crate::codec::scan::record_bits(2); // one 2-bit tag read - let (al, ar) = (self.bits[p], self.bits[p + 1]); + let (al, ar) = (self.bits.bit(p), self.bits.bit(p + 1)); match b { // Only an empty *stream* reads `Empty` here (an absent // child pair is never visited): vacuously disjoint. @@ -227,7 +227,7 @@ impl<'a> IdIndex<'a> { let after_left = entry + 1 + metered_partition_point(&rights[entry + 1..], target); - (Some(target as usize), after_left) + (Some(u64::from(target)), after_left) } else if ar { // Right-only: the child follows the tag. (Some(p + 2), entry) diff --git a/crates/before/src/party/ops/split.rs b/crates/before/src/party/ops/split.rs index 1f2a8888..108d8158 100644 --- a/crates/before/src/party/ops/split.rs +++ b/crates/before/src/party/ops/split.rs @@ -1,4 +1,4 @@ -use crate::codec::{BitsMut, BitsSlice}; +use crate::codec::{extend_from_view, BitsMut, BitsView}; use crate::idbits::{IdNode, IdReader}; impl IdReader<'_> { @@ -42,16 +42,17 @@ enum SpineEnd { /// copy of already-normal bit ranges, normal by construction (the kept child is /// nonempty, so no collapse can arise). Iterative: the spine walk is a loop, so /// deep ids cannot overflow. -fn build_split(bits: &BitsSlice, start: usize) -> (BitsMut, BitsMut) { +fn build_split(bits: BitsView<'_>, start: u64) -> (BitsMut, BitsMut) { let mut pos = start; let (prefix_end, kind) = loop { - match (bits[pos], bits[pos + 1]) { + match (bits.bit(pos), bits.bit(pos + 1)) { (false, false) => break (pos, SpineEnd::Terminal), // the `1` leaf (true, true) => break (pos, SpineEnd::Branch), // both-present branch _ => pos += 2, // unary: descend the single present child (at pos + 2) } }; - let prefix = &bits[start..prefix_end]; + let prefix = start..prefix_end; + let prefix_len = (prefix_end - start) as usize; match kind { SpineEnd::Branch => { @@ -65,22 +66,22 @@ fn build_split(bits: &BitsSlice, start: usize) -> (BitsMut, BitsMut) { bits.len(), "the branch subtree is the spine's tail", ); - let i1 = &bits[left_child..right_child]; - let i2 = &bits[right_child..branch_end]; // Each half keeps one child and drops the other, so its length is // exact: prefix + the 2-bit retagged branch + the kept child. - let mut a = BitsMut::with_capacity(prefix.len() + 2 + i1.len()); - a.extend_from_bitslice(prefix); + let mut a = + BitsMut::with_capacity(prefix_len + 2 + (right_child - left_child) as usize); + extend_from_view(&mut a, bits, prefix.start, prefix.end); a.push(true); // branch → Left-only: keep i1 ... a.push(false); // ... drop i2 - a.extend_from_bitslice(i1); + extend_from_view(&mut a, bits, left_child, right_child); - let mut b = BitsMut::with_capacity(prefix.len() + 2 + i2.len()); - b.extend_from_bitslice(prefix); + let mut b = + BitsMut::with_capacity(prefix_len + 2 + (branch_end - right_child) as usize); + extend_from_view(&mut b, bits, prefix.start, prefix.end); b.push(false); // branch → Right-only: drop i1 ... b.push(true); // ... keep i2 - b.extend_from_bitslice(i2); + extend_from_view(&mut b, bits, right_child, branch_end); (a, b) } @@ -92,15 +93,15 @@ fn build_split(bits: &BitsSlice, start: usize) -> (BitsMut, BitsMut) { bits.len(), "the terminal is the spine's tail", ); - let mut a = BitsMut::with_capacity(prefix.len() + 4); - a.extend_from_bitslice(prefix); + let mut a = BitsMut::with_capacity(prefix_len + 4); + extend_from_view(&mut a, bits, prefix.start, prefix.end); a.push(true); // (1, 0): Left-only ... a.push(false); a.push(false); // ... over a terminal a.push(false); - let mut b = BitsMut::with_capacity(prefix.len() + 4); - b.extend_from_bitslice(prefix); + let mut b = BitsMut::with_capacity(prefix_len + 4); + extend_from_view(&mut b, bits, prefix.start, prefix.end); b.push(false); // (0, 1): Right-only ... b.push(true); b.push(false); // ... over a terminal @@ -113,7 +114,7 @@ fn build_split(bits: &BitsSlice, start: usize) -> (BitsMut, BitsMut) { /// The bit position just past the subtree at `pos` (the shared /// [`skip`](IdReader::skip) scan), for slicing a branch child's verbatim range. -fn subtree_end(bits: &BitsSlice, pos: usize) -> usize { +fn subtree_end(bits: BitsView<'_>, pos: u64) -> u64 { let mut r = IdReader::at(bits, pos); r.skip(); r.pos() diff --git a/crates/before/src/party/ops/sum.rs b/crates/before/src/party/ops/sum.rs index e6a40846..a4766877 100644 --- a/crates/before/src/party/ops/sum.rs +++ b/crates/before/src/party/ops/sum.rs @@ -29,7 +29,7 @@ impl IdReader<'_> { // Conservative: the disjoint union has at most as many bits as both // inputs combined; normalization (collapsing `(v, v)` leaves) only // shrinks it. No tighter bound is cheap without doing the sum. - let mut out = IdBuilder::with_capacity(self.bits().len() + other.bits().len()); + let mut out = IdBuilder::with_capacity((self.bits().len() + other.bits().len()) as usize); let mut frames = Frames::new(); // Whether the current pair's side is a present child (read the real // cursor) or an absent `0` (stand in a synthetic empty). diff --git a/crates/before/src/party/ops/sum_split.rs b/crates/before/src/party/ops/sum_split.rs index 877e28be..47b3403e 100644 --- a/crates/before/src/party/ops/sum_split.rs +++ b/crates/before/src/party/ops/sum_split.rs @@ -1,4 +1,4 @@ -use crate::codec::{BitsMut, BitsSlice}; +use crate::codec::{built_view, extend_from_view, BitsMut, BitsView}; use crate::idbits::{IdNode, IdReader}; impl<'a> IdReader<'a> { @@ -102,7 +102,7 @@ impl<'a> IdReader<'a> { // full, so `split` cuts it exactly where it would cut the // built union's branch. let union = self.sum(other)?; - let (keep_child, give_child) = IdReader::root(&union).split(); + let (keep_child, give_child) = IdReader::root(built_view(&union)).split(); return Some((splice(&spine, &keep_child), splice(&spine, &give_child))); } // The targeted branch: each half keeps the spine, a one-child @@ -111,8 +111,8 @@ impl<'a> IdReader<'a> { other.read(); let (a_left, a_right) = branch_children(&self, al, ar); let (b_left, b_right) = branch_children(&other, bl, br); - let keep_child = union_child(a_left, b_left)?; - let give_child = union_child(a_right, b_right)?; + let keep_child = union_child(self.bits(), other.bits(), a_left, b_left)?; + let give_child = union_child(self.bits(), other.bits(), a_right, b_right)?; let keep = half(&spine, true, false, &keep_child); let give = half(&spine, false, true, &give_child); return Some((keep, give)); @@ -131,17 +131,27 @@ impl<'a> IdReader<'a> { /// One union child's bits at the branch: an operand's subtree verbatim, or a /// freshly merged pair. enum UnionChild<'a> { - /// The child is one operand's subtree alone: its verbatim bit range. - Verbatim(&'a BitsSlice), + /// The child is one operand's subtree alone: its stream and the + /// subtree's verbatim bit range within it. + Verbatim(BitsView<'a>, u64, u64), /// The child is present on both sides: the merged (summed) subtree. Merged(BitsMut), } impl UnionChild<'_> { - fn bits(&self) -> &BitsSlice { + /// The child's bit length, for the halves' exact capacity hints. + fn len(&self) -> usize { match self { - UnionChild::Verbatim(bits) => bits, - UnionChild::Merged(bits) => bits, + UnionChild::Verbatim(_, start, end) => (end - start) as usize, + UnionChild::Merged(bits) => bits.len(), + } + } + + /// Append the child's bits to a half. + fn append_to(&self, out: &mut BitsMut) { + match self { + UnionChild::Verbatim(bits, start, end) => extend_from_view(out, *bits, *start, *end), + UnionChild::Merged(bits) => out.extend_from_bitslice(bits), } } } @@ -153,11 +163,12 @@ impl UnionChild<'_> { /// it consumed only unary tags), so the last present child runs to the stream's /// end and only a both-present operand pays a skip — of its left child, to find /// the boundary between the two. -fn branch_children<'a>( - reader: &IdReader<'a>, +#[allow(clippy::type_complexity)] // two optional bit ranges: an inline pair over a minted name +fn branch_children( + reader: &IdReader<'_>, left: bool, right: bool, -) -> (Option<&'a BitsSlice>, Option<&'a BitsSlice>) { +) -> (Option<(u64, u64)>, Option<(u64, u64)>) { let bits = reader.bits(); let start = reader.pos(); match (left, right) { @@ -165,22 +176,27 @@ fn branch_children<'a>( let mut probe = IdReader::at(bits, start); probe.skip(); let mid = probe.pos(); - (Some(&bits[start..mid]), Some(&bits[mid..])) + (Some((start, mid)), Some((mid, bits.len()))) } - (true, false) => (Some(&bits[start..]), None), - (false, true) => (None, Some(&bits[start..])), + (true, false) => (Some((start, bits.len())), None), + (false, true) => (None, Some((start, bits.len()))), (false, false) => unreachable!("an internal id node has a present child"), } } /// One union child at the branch: the side present alone, verbatim, or the /// merge of both — `None` if the merged subtrees overlap. -fn union_child<'a>(a: Option<&'a BitsSlice>, b: Option<&'a BitsSlice>) -> Option> { +fn union_child<'a>( + a_bits: BitsView<'a>, + b_bits: BitsView<'a>, + a: Option<(u64, u64)>, + b: Option<(u64, u64)>, +) -> Option> { match (a, b) { - (Some(a), None) => Some(UnionChild::Verbatim(a)), - (None, Some(b)) => Some(UnionChild::Verbatim(b)), - (Some(a), Some(b)) => IdReader::root(a) - .sum(IdReader::root(b)) + (Some((start, end)), None) => Some(UnionChild::Verbatim(a_bits, start, end)), + (None, Some((start, end))) => Some(UnionChild::Verbatim(b_bits, start, end)), + (Some(a), Some(b)) => IdReader::at(a_bits, a.0) + .sum(IdReader::at(b_bits, b.0)) .map(UnionChild::Merged), (None, None) => unreachable!("a union branch child is present on some side"), } @@ -188,18 +204,18 @@ fn union_child<'a>(a: Option<&'a BitsSlice>, b: Option<&'a BitsSlice>) -> Option /// Assemble one half: the spine, the branch retagged to its kept side, and the /// kept child's bits. -fn half(spine: &BitsSlice, left: bool, right: bool, child: &UnionChild) -> BitsMut { - let mut out = BitsMut::with_capacity(spine.len() + 2 + child.bits().len()); +fn half(spine: &BitsMut, left: bool, right: bool, child: &UnionChild) -> BitsMut { + let mut out = BitsMut::with_capacity(spine.len() + 2 + child.len()); out.extend_from_bitslice(spine); out.push(left); out.push(right); - out.extend_from_bitslice(child.bits()); + child.append_to(&mut out); out } /// Assemble one delegated-mode half: the spine, then the composition's own half /// of the branch subtree's union. -fn splice(spine: &BitsSlice, tail: &BitsSlice) -> BitsMut { +fn splice(spine: &BitsMut, tail: &BitsMut) -> BitsMut { let mut out = BitsMut::with_capacity(spine.len() + tail.len()); out.extend_from_bitslice(spine); out.extend_from_bitslice(tail); diff --git a/crates/before/src/party/tests.rs b/crates/before/src/party/tests.rs index 1a129430..642aa8cd 100644 --- a/crates/before/src/party/tests.rs +++ b/crates/before/src/party/tests.rs @@ -451,14 +451,17 @@ fn unindexed_fallback_matches_the_walk_on_constructed_pairs() { ]; for (a, b) in &pairs { for (x, y) in [(a, b), (b, a)] { - let walk = IdReader::root(x).is_disjoint(IdReader::root(y)); + let walk = IdReader::root(crate::codec::built_view(x)) + .is_disjoint(IdReader::root(crate::codec::built_view(y))); assert_eq!( - IdIndex::build(x).is_disjoint(IdReader::root(y)), + IdIndex::build(crate::codec::built_view(x)) + .is_disjoint(IdReader::root(crate::codec::built_view(y))), walk, "the built index diverged from the cursor walk" ); assert_eq!( - IdIndex::build_unindexed(x).is_disjoint(IdReader::root(y)), + IdIndex::build_unindexed(crate::codec::built_view(x)) + .is_disjoint(IdReader::root(crate::codec::built_view(y))), walk, "the unindexed fallback diverged from the cursor walk" ); @@ -547,7 +550,7 @@ proptest! { let fused = IdReader::root(ia.as_bits()).sum_split(IdReader::root(ib.as_bits())); let composed = IdReader::root(ia.as_bits()) .sum(IdReader::root(ib.as_bits())) - .map(|union| IdReader::root(&union).split()); + .map(|union| IdReader::root(crate::codec::built_view(&union)).split()); prop_assert_eq!(fused, composed); } } @@ -569,7 +572,7 @@ fn sum_split_collapsed_union_matches_terminal_split() { let union = IdReader::root(keep.as_bits()) .sum(IdReader::root(give.as_bits())) .expect("the seed's halves are disjoint"); - let composed = IdReader::root(&union).split(); + let composed = IdReader::root(crate::codec::built_view(&union)).split(); assert_eq!(fused, composed); assert_eq!(Party::from_bits(fused.0), keep, "the keep half is (1, 0)"); assert_eq!(Party::from_bits(fused.1), give, "the give half is (0, 1)"); @@ -666,10 +669,11 @@ mod sum_split_constructed { /// orders (byte equality, `None` arms included). fn assert_matches_composition(a: &BitsMut, b: &BitsMut) { for (x, y) in [(a, b), (b, a)] { - let fused = IdReader::root(x).sum_split(IdReader::root(y)); - let composed = IdReader::root(x) - .sum(IdReader::root(y)) - .map(|union| IdReader::root(&union).split()); + let fused = IdReader::root(crate::codec::built_view(x)) + .sum_split(IdReader::root(crate::codec::built_view(y))); + let composed = IdReader::root(crate::codec::built_view(x)) + .sum(IdReader::root(crate::codec::built_view(y))) + .map(|union| IdReader::root(crate::codec::built_view(&union)).split()); assert_eq!(fused, composed); } } @@ -753,12 +757,13 @@ mod sum_split_constructed { }; let compare = |name: &str, a: &BitsMut, b: &BitsMut| -> u64 { let fused = scan(&|| { - IdReader::root(a).sum_split(IdReader::root(b)); + IdReader::root(crate::codec::built_view(a)) + .sum_split(IdReader::root(crate::codec::built_view(b))); }); let composed = scan(&|| { - IdReader::root(a) - .sum(IdReader::root(b)) - .map(|u| IdReader::root(&u).split()); + IdReader::root(crate::codec::built_view(a)) + .sum(IdReader::root(crate::codec::built_view(b))) + .map(|u| IdReader::root(crate::codec::built_view(&u)).split()); }); assert!( 0 < fused && fused <= composed, @@ -832,7 +837,8 @@ mod diff_constructed { /// at oracle-reachable scales (`k <= ORACLE_SCALE_MAX`) also equal to the /// recursive oracle's `without`, compared over lowered oracle trees. fn assert_diff(a: &BitsMut, b: &BitsMut, expected: &BitsMut, k: usize) { - let d = IdReader::root(a).diff(IdReader::root(b)); + let d = IdReader::root(crate::codec::built_view(a)) + .diff(IdReader::root(crate::codec::built_view(b))); assert_eq!( &d, expected, "diff diverged from the constructed expectation (k={k})" @@ -925,7 +931,8 @@ mod diff_constructed { const BLOCK_SLACK: u64 = 8; let scan = |a: &BitsMut, b: &BitsMut| -> u64 { crate::codec::scan::reset(); - IdReader::root(a).diff(IdReader::root(b)); + IdReader::root(crate::codec::built_view(a)) + .diff(IdReader::root(crate::codec::built_view(b))); crate::codec::scan::scan_bits() }; for k in [256usize, 4096] { @@ -1010,7 +1017,7 @@ proptest! { ) { let a = from_oracle_party(&oa); let b = from_oracle_party(&ob); - let bit_eq = a.as_bits() == b.as_bits(); + let bit_eq = a.as_bits().to_bitvec() == b.as_bits().to_bitvec(); prop_assert_eq!(a == b, bit_eq); prop_assert_eq!(b == a, bit_eq); if a == b { diff --git a/crates/before/src/span.rs b/crates/before/src/span.rs index 0ee8f0ad..955612bd 100644 --- a/crates/before/src/span.rs +++ b/crates/before/src/span.rs @@ -260,7 +260,11 @@ impl<'a> Span<'a> { None => Placement::Concurrent(Endpoint::Both), }; } - place::span(version.view(), self.lo.view(), self.hi.view()) + place::span( + version.view().live(), + self.lo.view().live(), + self.hi.view().live(), + ) } /// Determines how much of this [`Span`] `version` *dominates*, rendering a @@ -320,7 +324,11 @@ impl<'a> Span<'a> { Dominance::Before }; } - place::dominance(version.view(), self.lo.view(), self.hi.view()) + place::dominance( + version.view().live(), + self.lo.view().live(), + self.hi.view().live(), + ) } /// Determines how much of this [`Span`] `version` *precedes*, rendering a @@ -381,7 +389,11 @@ impl<'a> Span<'a> { Precedence::After }; } - place::precedence(version.view(), self.lo.view(), self.hi.view()) + place::precedence( + version.view().live(), + self.lo.view().live(), + self.hi.view().live(), + ) } /// Whether this [`Span`] contains `other`, in the containment order: @@ -447,7 +459,11 @@ impl<'a> Span<'a> { if self.lo.view().ptr_eq(self.hi.view()) { return codec::canonical_eq(version.view(), self.lo().view()); } - return place::contains(version.view(), self.lo.view(), self.hi.view()); + return place::contains( + version.view().live(), + self.lo.view().live(), + self.hi.view().live(), + ); } // A span is contained iff both its endpoints are: every version // between them lies within `self` by transitivity of the bounds. diff --git a/crates/before/src/span/wire.rs b/crates/before/src/span/wire.rs index 8047ad8f..c72076c5 100644 --- a/crates/before/src/span/wire.rs +++ b/crates/before/src/span/wire.rs @@ -127,13 +127,11 @@ impl<'a> Span<'a> { // // The pair verdict is pronounced last, after the padding check, so a // composite defective several ways rejects by its structural genre - // first, exactly as decoding the components would. - // Both components validate over the raw bytes (never a borrowed bit - // view, whose encoding caps below the buffer sizes this door admits - // on 32-bit targets); each walk's input is its component's whole - // byte range as bits, padding included, judged by its marker check. + // first, exactly as decoding the components would. Each component + // walk's input is its whole byte range as bits, padding included, + // judged by its marker check. let (lo_bytes, admission) = { - let lo_end = skyline::validate_prefix_bytes(&buf)?; + let lo_end = skyline::validate_prefix(codec::BitsView::whole(&buf))?; // The meet's padding marker sits in its final byte — which an // input cut right after a flush stream lacks. That cut is // missing required data (the marker byte, and the whole join @@ -145,18 +143,15 @@ impl<'a> Span<'a> { } let lo_bytes = usize::try_from(lo_bytes).expect("the meet's prefix ends within the read buffer"); - codec::require_marker_padding_bytes(&buf[..lo_bytes], lo_end)?; - // The meet re-walks beside the join's parse; its live length is - // its own validated end, below `usize` wherever its stream is - // storable at all (the adopting container's bit positions are - // usize-denominated). - let lo_live = usize::try_from(lo_end).expect("a storable meet's bit length fits usize"); + codec::require_marker_padding(&buf[..lo_bytes], lo_end)?; + // The meet re-walks beside the join's parse, viewed at its own + // validated live length. + let lo = codec::BitsView::new(&buf[..lo_bytes], lo_end); let tail = &buf[lo_bytes..]; - let mut cursor = codec::DsiCursor::over_bytes(tail); - let admission = - skyline::validate_dominating_bytes(&buf[..lo_bytes], lo_live, &mut cursor)?; + let mut cursor = codec::DsiCursor::new(codec::BitsView::whole(tail)); + let admission = skyline::validate_dominating_from(lo, &mut cursor)?; let hi_end = cursor.position_u64(); - codec::require_marker_padding_bytes(tail, hi_end)?; + codec::require_marker_padding(tail, hi_end)?; if admission == skyline::Admission::Refuted { return Err(Decode::NotCanonical); } diff --git a/crates/before/src/testing/bridge.rs b/crates/before/src/testing/bridge.rs index 68dc746a..103e7e2f 100644 --- a/crates/before/src/testing/bridge.rs +++ b/crates/before/src/testing/bridge.rs @@ -83,7 +83,9 @@ pub(crate) fn from_oracle_party(t: &oracle::Party) -> Party { pub(crate) fn from_oracle_version(t: &oracle::Version) -> Version { let mut bits = BitsMut::new(); emit_ev(&mut bits, t); - Version::from_bits(crate::version::skyline::encode_bits(&bits)) + Version::from_bits(crate::version::skyline::encode_bits( + crate::codec::built_view(&bits), + )) } /// Build the impl `Clock` mirroring an oracle clock. @@ -104,9 +106,9 @@ pub(crate) fn from_oracle_clock(c: &oracle::Clock) -> Clock { // bounded tree (test-only; the impl's own traversals are iterative). Both // forms are normalized, so structural `==` ⇔ semantic equality. -fn read_id(bits: &codec::BitsSlice, pos: usize) -> (oracle::Party, usize) { - let left = bits[pos]; - let right = bits[pos + 1]; +fn read_id(bits: codec::BitsView<'_>, pos: u64) -> (oracle::Party, u64) { + let left = bits.bit(pos); + let right = bits.bit(pos + 1); if !left && !right { return (oracle::Party::Leaf(true), pos + 2); // terminal = `1` } @@ -138,12 +140,12 @@ fn read_id(bits: &codec::BitsSlice, pos: usize) -> (oracle::Party, usize) { /// The oracle base is the arbitrary-precision `Base` (matching the impl), /// so lowering is lossless for any magnitude: no `u64` truncation point. fn read_ev( - bits: &codec::BitsSlice, - pos: usize, + bits: codec::BitsView<'_>, + pos: u64, prev: &mut Option, -) -> (oracle::Version, usize) { +) -> (oracle::Version, u64) { // Skyline topology flag: `0` internal, `1` leaf. - let internal = !bits[pos]; + let internal = !bits.bit(pos); if internal { let (l, after_l) = descend!(0, read_ev(bits, pos + 1, prev)); let (r, after_r) = descend!(0, read_ev(bits, after_l, prev)); diff --git a/crates/before/src/testing/compactness.rs b/crates/before/src/testing/compactness.rs index 783a57d0..bd4e50f5 100644 --- a/crates/before/src/testing/compactness.rs +++ b/crates/before/src/testing/compactness.rs @@ -80,7 +80,7 @@ pub(crate) fn check_sample(version: &Version) -> Sample { // oracle lowering; the stored coding is Tier 2 itself. let packed = crate::testing::bridge::packed_bits_of(&crate::testing::bridge::to_oracle_version(version)); - let tier2 = tier2_size(&packed); + let tier2 = tier2_size(crate::codec::built_view(&packed)); let current_bits = packed.len() as u64; let ratio = tier2.total_bits as f64 / current_bits as f64; @@ -154,7 +154,9 @@ pub(crate) fn comb(m_bits: usize, pairs: usize) -> Version { } // The comb is hand-built in the min-lifted packed construction // language; the transcoding bridge lifts it into the stored coding. - let version = Version::from_bits(crate::version::skyline::encode_bits(&bits)); + let version = Version::from_bits(crate::version::skyline::encode_bits( + crate::codec::built_view(&bits), + )); // Self-check: the built stream is canonical and round-trips the wire. let decoded = Version::decode(version.encode().as_slice()) diff --git a/crates/before/src/testing/snapshots.rs b/crates/before/src/testing/snapshots.rs index 4a0a3875..99cfcbce 100644 --- a/crates/before/src/testing/snapshots.rs +++ b/crates/before/src/testing/snapshots.rs @@ -2,14 +2,16 @@ use insta::assert_snapshot; -use crate::codec::{encode_int, Base, BitsMut, BitsSlice}; +use crate::codec::{encode_int, Base, BitsMut, BitsView}; use crate::error::{Crossed, Decode, Overlap, Parse}; use crate::{Clock, Party, Rank, Version}; /// Render a bit stream most-significant-bit-first as a string of `'0'`/`'1'`, the same /// order `encode_int` and the preorder codec emit. Empty stream renders as `""`. -fn bits_to_string(bits: &BitsSlice) -> String { - bits.iter().map(|b| if *b { '1' } else { '0' }).collect() +fn bits_to_string(bits: BitsView<'_>) -> String { + (0..bits.len()) + .map(|i| if bits.bit(i) { '1' } else { '0' }) + .collect() } /// Render bytes as space-separated two-digit hex, e.g. `[0x80, 0x01]` -> `"80 01"`. @@ -28,7 +30,7 @@ fn gamma_row(n: u64) -> String { format!( "{:>20} -> {} ({} bits)", n, - bits_to_string(&bits), + bits_to_string(crate::codec::built_view(&bits)), bits.len() ) } @@ -70,7 +72,11 @@ fn gamma_bit_layout_table() { let big = Base::from(1u8) << 64u32; // 2^64 encode_int(&mut big_bits, &big); assert_snapshot!( - format!("2^64 -> {} ({} bits)", bits_to_string(&big_bits), big_bits.len()), + format!( + "2^64 -> {} ({} bits)", + bits_to_string(crate::codec::built_view(&big_bits)), + big_bits.len() + ), @"2^64 -> 000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001 (129 bits)" ); } @@ -167,10 +173,10 @@ fn clock_canonical_form() { // with no padding between (padding is added only by `encode`). Rebuild that unpadded // concatenation here to show the boundary between the two halves. let mut bits = c.party().as_bits().to_bitvec(); - bits.extend_from_bitslice(c.version().as_bits()); + bits.extend_from_bitslice(&c.version().as_bits().to_bitvec()); let fields = format!( "display: {c}\ndebug: {c:?}\nbits: {} ({} bits)\nbytes: {}", - bits_to_string(&bits), + bits_to_string(crate::codec::built_view(&bits)), bits.len(), bytes_to_hex(&c.encode()), ); diff --git a/crates/before/src/version.rs b/crates/before/src/version.rs index a8a3d1bc..e2d1d7e0 100644 --- a/crates/before/src/version.rs +++ b/crates/before/src/version.rs @@ -155,7 +155,7 @@ impl Version { // `codec::encode_int`). The stored skyline stream is a unique // representation, so this O(1) bit test is the whole question — no // allocation, no walk. - skyline::is_empty_stream(&self.0) + skyline::is_empty_stream(self.0.live()) } /// Advances this version by one event for `party`. @@ -178,7 +178,7 @@ impl Version { /// assert!(v > Version::new()); // one event: strictly after the empty history /// ``` pub fn tick(&mut self, party: &Party) { - *self = Version::from_bits(skyline::fill::tick(&self.0, party)); + *self = Version::from_bits(skyline::fill::tick(self.0.live(), party)); } /// Advances this version by `k` events for `party`. @@ -214,7 +214,7 @@ impl Version { if k.0.bits() == 0 { return; } - *self = Version::from_bits(skyline::fill::ticks(&self.0, party, &k.0)); + *self = Version::from_bits(skyline::fill::ticks(self.0.live(), party, &k.0)); } /// Tests whether two [`Version`]s are concurrent (incomparable). @@ -276,7 +276,7 @@ impl Version { /// assert_eq!(peaks.min_ticks(), Ticks::from(2u64)); /// ``` pub fn min_ticks(&self) -> Ticks { - Ticks(skyline::query::min_ticks(&self.0)) + Ticks(skyline::query::min_ticks(self.0.live())) } /// This [`Version`]'s exact causal [`Rank`]: `v < w` implies `v.rank() < @@ -307,7 +307,7 @@ impl Version { /// assert!(b.version().rank() < joined.rank()); /// ``` pub fn rank(&self) -> Rank { - skyline::query::rank(&self.0) + skyline::query::rank(self.0.live()) } /// Views this version ordered totally by its causal rank, using its own @@ -391,7 +391,7 @@ impl Version { if codec::canonical_eq(&self.0, &other.0) { return Rank::ZERO; } - skyline::query::distance(&self.0, &other.0) + skyline::query::distance(self.0.live(), other.0.live()) } /// How far `self` lags behind `other`. @@ -432,7 +432,7 @@ impl Version { if codec::canonical_eq(&self.0, &other.0) { return Rank::ZERO; } - skyline::query::lag(&self.0, &other.0) + skyline::query::lag(self.0.live(), other.0.live()) } /// The join (least upper bound) of this [`Version`] and `other`: their @@ -817,17 +817,17 @@ impl Version { if codec::canonical_eq(&self.0, incoming) { return; // a ∨ a = a } - if skyline::is_empty_stream(incoming) { + if skyline::is_empty_stream(incoming.live()) { return; // v ∨ 0 = v: nothing to fold in } - if skyline::is_empty_stream(&self.0) { + if skyline::is_empty_stream(self.0.live()) { // 0 ∨ v = v: adopt the incoming stream wholesale. Both streams are // canonical, so the shared buffer (an `O(1)` refcount clone) equals // the merge byte for byte. *self = Version::from_frozen(incoming.clone()); return; } - *self = Version::from_bits(skyline::emit::join(&self.0, incoming)); + *self = Version::from_bits(skyline::emit::join(self.0.live(), incoming.live())); } /// The borrowed-operands join: `a ∨ b` as a fresh [`Version`], reading both @@ -842,13 +842,13 @@ impl Version { if codec::canonical_eq(&a.0, &b.0) { return a.clone(); // a ∨ a = a } - if skyline::is_empty_stream(&b.0) { + if skyline::is_empty_stream(b.0.live()) { return a.clone(); // v ∨ 0 = v } - if skyline::is_empty_stream(&a.0) { + if skyline::is_empty_stream(a.0.live()) { return b.clone(); // 0 ∨ v = v } - Version::from_bits(skyline::emit::join(&a.0, &b.0)) + Version::from_bits(skyline::emit::join(a.0.live(), b.0.live())) } /// The view-taking meet core, the dual of [`join_view`](Self::join_view): @@ -865,15 +865,15 @@ impl Version { if codec::canonical_eq(&self.0, incoming) { return; // a ∧ a == a } - if skyline::is_empty_stream(&self.0) { + if skyline::is_empty_stream(self.0.live()) { return; // 0 ∧ v = 0: already empty, nothing can shrink it } - if skyline::is_empty_stream(incoming) { + if skyline::is_empty_stream(incoming.live()) { // v ∧ 0 = 0: the result is the empty version, whatever `v` was. *self = Version::new(); return; } - *self = Version::from_bits(skyline::emit::meet(&self.0, incoming)); + *self = Version::from_bits(skyline::emit::meet(self.0.live(), incoming.live())); } /// The borrowed-operands meet: `a ∧ b` as a fresh [`Version`], reading both @@ -887,13 +887,13 @@ impl Version { if codec::canonical_eq(&a.0, &b.0) { return a.clone(); // a ∧ a = a } - if skyline::is_empty_stream(&a.0) { + if skyline::is_empty_stream(a.0.live()) { return a.clone(); // 0 ∧ v = 0: `a` is already the answer } - if skyline::is_empty_stream(&b.0) { + if skyline::is_empty_stream(b.0.live()) { return Version::new(); // v ∧ 0 = 0, whatever `v` was } - Version::from_bits(skyline::emit::meet(&a.0, &b.0)) + Version::from_bits(skyline::emit::meet(a.0.live(), b.0.live())) } /// The borrowed-operands hull: `(a ∧ b, a ∨ b)` as fresh [`Version`]s, @@ -923,17 +923,17 @@ impl Version { hull_traffic::record(Rung::Equal); return (a.clone(), a.clone()); // a ∧ a = a = a ∨ a } - if skyline::is_empty_stream(&a.0) { + if skyline::is_empty_stream(a.0.live()) { // 0 ∧ v = 0 (`a` is already the meet), 0 ∨ v = v. hull_traffic::record(Rung::Empty); return (a.clone(), b.clone()); } - if skyline::is_empty_stream(&b.0) { + if skyline::is_empty_stream(b.0.live()) { // v ∧ 0 = 0, v ∨ 0 = v. hull_traffic::record(Rung::Empty); return (Version::new(), a.clone()); } - match skyline::sweep::causal_cmp(&a.0, &b.0) { + match skyline::sweep::causal_cmp(a.0.live(), b.0.live()) { // The comparable case's answer IS an operand pair. Some(Ordering::Less) => { hull_traffic::record(Rung::Comparable); @@ -949,7 +949,7 @@ impl Version { None => {} } hull_traffic::record(Rung::Concurrent); - let hull = skyline::emit::hull(&a.0, &b.0); + let hull = skyline::emit::hull(a.0.live(), b.0.live()); // The fused walk folds the pair relation beside its emissions (an O(1) // flag pair riding sign reads the walk performs anyway), so the // ladder's classification is cross-checked at the only door that emits. @@ -1062,13 +1062,12 @@ impl Version { pub fn decode(mut reader: R) -> Result { let mut buf = Vec::new(); reader.read_to_end(&mut buf).map_err(Decode::Io)?; - // Validate over the raw bytes (never a borrowed bit view, whose - // encoding caps below the buffer sizes this door admits on 32-bit - // targets): the whole `8 · buf.len()`-bit view is the walk's input, - // padding bits included, and the marker check judges the remainder. + // Validate over the whole buffer as bits, padding included: the + // walk's input is the whole `8 · buf.len()`-bit view, and the marker + // check judges the remainder. { - let end = skyline::validate_prefix_bytes(&buf)?; - codec::require_marker_padding_bytes(&buf, end)?; + let end = skyline::validate_prefix(codec::BitsView::whole(&buf))?; + codec::require_marker_padding(&buf, end)?; } // Adopt the read buffer as the result's backing store without // copying: the padding check proved the buffer is the stream's one @@ -1131,8 +1130,8 @@ impl Version { /// differential bridges read it; production code goes through /// [`Self::as_bytes`] or the crate-internal `view`. #[cfg(any(test, feature = "meter"))] - pub(crate) fn as_bits(&self) -> &codec::BitsSlice { - &self.0 + pub(crate) fn as_bits(&self) -> codec::BitsView<'_> { + self.0.live() } /// Freeze a normal-form skyline bit stream as a `Version`, canonicalizing @@ -1366,7 +1365,7 @@ impl<'a> FromIterator<&'a Version> for Version { /// ``` impl Display for Version { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str(&skyline::text::render(&self.0)) + f.write_str(&skyline::text::render(self.0.live())) } } @@ -1448,7 +1447,11 @@ where fn try_from((n, l, r): (u64, T, S)) -> Result { let l = Version::try_from(l)?; let r = Version::try_from(r)?; - Ok(Version::from_bits(skyline::literal::node(n, &l.0, &r.0)?)) + Ok(Version::from_bits(skyline::literal::node( + n, + l.0.live(), + r.0.live(), + )?)) } } @@ -1670,7 +1673,7 @@ macro_rules! causal_cmp_impls { } impl PartialOrd<$rhs> for $lhs { fn partial_cmp(&self, o: &$rhs) -> Option { - skyline::sweep::causal_cmp(self.view(), o.view()) + skyline::sweep::causal_cmp(self.view().live(), o.view().live()) } } impl PartialEq<$rhs> for &$lhs { @@ -1680,7 +1683,7 @@ macro_rules! causal_cmp_impls { } impl PartialOrd<$rhs> for &$lhs { fn partial_cmp(&self, o: &$rhs) -> Option { - skyline::sweep::causal_cmp(self.view(), o.view()) + skyline::sweep::causal_cmp(self.view().live(), o.view().live()) } } impl PartialEq<&$rhs> for $lhs { @@ -1690,7 +1693,7 @@ macro_rules! causal_cmp_impls { } impl PartialOrd<&$rhs> for $lhs { fn partial_cmp(&self, o: &&$rhs) -> Option { - skyline::sweep::causal_cmp(self.view(), o.view()) + skyline::sweep::causal_cmp(self.view().live(), o.view().live()) } } )* diff --git a/crates/before/src/version/own.rs b/crates/before/src/version/own.rs index a74e01c8..5992e713 100644 --- a/crates/before/src/version/own.rs +++ b/crates/before/src/version/own.rs @@ -82,7 +82,10 @@ impl OwnVersion<'_> { if self.party.is_seed() { return self.version.clone(); } - Version::from_bits(skyline::query::project(self.version.view(), self.party)) + Version::from_bits(skyline::query::project( + self.version.view().live(), + self.party, + )) } } @@ -110,9 +113,9 @@ impl From> for Version { /// The fused three-stream comparison: `(v / p) ⋚ w`, no materialization. fn view_cmp_version(view: &OwnVersion<'_>, w: &Version) -> Option { skyline::masked::causal_cmp( - view.version.view(), + view.version.view().live(), Some(view.party.as_bits()), - w.view(), + w.view().live(), None, ) } @@ -120,9 +123,9 @@ fn view_cmp_version(view: &OwnVersion<'_>, w: &Version) -> Option { /// The fused three-stream equality: `(v / p) == w`, no materialization. fn view_eq_version(view: &OwnVersion<'_>, w: &Version) -> bool { skyline::masked::eq( - view.version.view(), + view.version.view().live(), Some(view.party.as_bits()), - w.view(), + w.view().live(), None, ) } @@ -138,9 +141,9 @@ fn view_eq_version(view: &OwnVersion<'_>, w: &Version) -> bool { /// tests pins against the materialized projection. fn version_cmp_view(w: &Version, view: &OwnVersion<'_>) -> Option { skyline::masked::causal_cmp( - w.view(), + w.view().live(), None, - view.version.view(), + view.version.view().live(), Some(view.party.as_bits()), ) } @@ -148,9 +151,9 @@ fn version_cmp_view(w: &Version, view: &OwnVersion<'_>) -> Option { /// The mirror three-stream equality: `w == (v / p)`, mask on the second side. fn version_eq_view(w: &Version, view: &OwnVersion<'_>) -> bool { skyline::masked::eq( - w.view(), + w.view().live(), None, - view.version.view(), + view.version.view().live(), Some(view.party.as_bits()), ) } @@ -158,9 +161,9 @@ fn version_eq_view(w: &Version, view: &OwnVersion<'_>) -> bool { /// The fused four-stream comparison: `(v₁ / p₁) ⋚ (v₂ / p₂)`. fn view_cmp_view(a: &OwnVersion<'_>, b: &OwnVersion<'_>) -> Option { skyline::masked::causal_cmp( - a.version.view(), + a.version.view().live(), Some(a.party.as_bits()), - b.version.view(), + b.version.view().live(), Some(b.party.as_bits()), ) } @@ -168,9 +171,9 @@ fn view_cmp_view(a: &OwnVersion<'_>, b: &OwnVersion<'_>) -> Option { /// The fused four-stream equality: the projected histories agree. fn view_eq_view(a: &OwnVersion<'_>, b: &OwnVersion<'_>) -> bool { skyline::masked::eq( - a.version.view(), + a.version.view().live(), Some(a.party.as_bits()), - b.version.view(), + b.version.view().live(), Some(b.party.as_bits()), ) } diff --git a/crates/before/src/version/ranked.rs b/crates/before/src/version/ranked.rs index cf75d86d..6f80d006 100644 --- a/crates/before/src/version/ranked.rs +++ b/crates/before/src/version/ranked.rs @@ -342,7 +342,7 @@ fn total_cmp(a: &Ranked<'_>, b: &Ranked<'_>) -> Ordering { if crate::codec::canonical_eq(a.version.view(), b.version.view()) { return Ordering::Equal; } - skyline::query::rank_cmp(a.version.view(), b.version.view()) + skyline::query::rank_cmp(a.version.view().live(), b.version.view().live()) .then_with(|| a.version.as_bytes().cmp(b.version.as_bytes())) } diff --git a/crates/before/src/version/skyline.rs b/crates/before/src/version/skyline.rs index 8fcd2562..a806036d 100644 --- a/crates/before/src/version/skyline.rs +++ b/crates/before/src/version/skyline.rs @@ -157,7 +157,7 @@ use crate::Version; pub use crate::codec::Bits; #[cfg(any(test, feature = "meter"))] pub use crate::codec::BitsMut; -pub use crate::codec::BitsSlice; +pub use crate::codec::BitsView; // The admission walk: the span wire form's fused second-component parse, // consumed by `Span::decode` and the borsh span leg. @@ -204,32 +204,37 @@ pub(crate) mod web_traffic; #[cfg(test)] mod tests; -pub(crate) use admit::{validate_dominating_bytes, Admission}; -// The slice-form admission walk serves the wire-side (borsh) span reader, -// whose meet is an already-stored version read through its borrowed view. -#[cfg(feature = "borsh")] -pub(crate) use admit::validate_dominating_from; +pub(crate) use admit::{validate_dominating_from, Admission}; #[cfg(any(test, feature = "meter"))] pub(crate) use decode::decode_bits; #[cfg(any(test, feature = "meter"))] pub(crate) use encode::encode_bits; #[cfg(any(test, feature = "meter"))] pub(crate) use validate::validate_bits; -pub(crate) use validate::validate_prefix_bytes; +pub(crate) use validate::validate_prefix; // The wire decoder's from-cursor entry: reached from the borsh event leg. #[cfg(feature = "borsh")] pub(crate) use validate::validate_from; +/// A build buffer's contents as a [`BitsView`]: the instrument surface's +/// bridge from [`encode`]'s buffer to the view the walk entries read. +/// +/// Test- and meter-only, like the buffers it views. +#[cfg(any(test, feature = "meter"))] +pub fn view(bits: &BitsMut) -> BitsView<'_> { + crate::codec::built_view(bits) +} + /// A [`Version`]'s canonical skyline stream: the stored form, cloned. /// /// Test- and meter-only: production callers reach the stored stream through /// [`Version::as_bytes`]/[`Version::encode`]. #[cfg(any(test, feature = "meter"))] pub fn encode(version: &Version) -> BitsMut { - let mut bits = version.as_bits().to_bitvec(); - // Live bits only — no padding: consumers walk these as a stream. The - // dead tail is zeroed so the buffer reads deterministically anyway. - bits.set_uninitialized(false); + let view = version.as_bits(); + let mut bits = BitsMut::with_capacity(view.len() as usize); + // Live bits only — no padding: consumers walk these as a stream. + crate::codec::extend_from_view(&mut bits, view, 0, view.len()); bits } @@ -247,7 +252,7 @@ pub fn encode(version: &Version) -> BitsMut { /// ([`Version::decode`], the borsh leg) run the underlying pass through /// `validate_prefix`/`validate_from` directly. #[cfg(any(test, feature = "meter"))] -pub fn validate(bits: &BitsSlice) -> Result<(), Decode> { +pub fn validate(bits: BitsView<'_>) -> Result<(), Decode> { validate_bits(bits) } @@ -261,7 +266,7 @@ pub fn validate(bits: &BitsSlice) -> Result<(), Decode> { /// Test- and meter-only: the production decode ([`Version::decode`]) validates /// the prefix and adopts the buffer without this wrapper. #[cfg(any(test, feature = "meter"))] -pub fn decode(bits: &BitsSlice) -> Result { +pub fn decode(bits: BitsView<'_>) -> Result { decode_bits(bits) } @@ -270,6 +275,6 @@ pub fn decode(bits: &BitsSlice) -> Result { /// The empty version is exactly the 2-bit stream `11` (leaf flag `1`, then /// gamma(0), the single bit `1`). Canonical uniqueness makes this O(1) test the /// whole question. -pub(crate) fn is_empty_stream(bits: &BitsSlice) -> bool { - bits.len() == 2 && bits[0] && bits[1] +pub(crate) fn is_empty_stream(bits: BitsView<'_>) -> bool { + bits.len() == 2 && bits.bit(0) && bits.bit(1) } diff --git a/crates/before/src/version/skyline/admit.rs b/crates/before/src/version/skyline/admit.rs index 5517e99e..7f24ef99 100644 --- a/crates/before/src/version/skyline/admit.rs +++ b/crates/before/src/version/skyline/admit.rs @@ -50,9 +50,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; -#[cfg(feature = "borsh")] -use crate::codec::BitsSlice; -use crate::codec::{BitCursor, BitsMut, Int}; +use crate::codec::{BitCursor, BitsMut, BitsView, Int}; use crate::error::Decode; use super::overlay::{fold, LeafCursor, PlateauCursor, Side, Step}; @@ -275,44 +273,14 @@ pub(crate) enum Admission { /// `lo` must be a canonical skyline stream — its cursor is the pair sweep's and /// shares [`causal_cmp`](super::sweep::causal_cmp)'s contract. The parsed /// stream needs no such trust; that is the point. -#[cfg(feature = "borsh")] pub(crate) fn validate_dominating_from( - lo: &BitsSlice, - cursor: &mut C, -) -> Result -where - Decode: From, -{ - let (lo_cur, lo_first) = LeafCursor::open(lo); - validate_dominating_over(lo_cur, lo_first, cursor) -} - -/// [`validate_dominating_from`] with the canonical `lo` given as raw stream -/// bytes and its live bit length. Same contract in full. -/// -/// The byte decode door's form, for meets past the borrowed bit view's -/// encoding cap (64 MiB and up on a 32-bit target). -pub(crate) fn validate_dominating_bytes( - lo: &[u8], - lo_live: usize, - cursor: &mut C, -) -> Result -where - Decode: From, -{ - let (lo_cur, lo_first) = LeafCursor::open_bytes(lo, lo_live); - validate_dominating_over(lo_cur, lo_first, cursor) -} - -/// The admission walk's body, generic over how `lo`'s cursor was opened. -fn validate_dominating_over( - mut lo_cur: LeafCursor<'_>, - lo_first: Int, + lo: BitsView<'_>, cursor: &mut C, ) -> Result where Decode: From, { + let (mut lo_cur, lo_first) = LeafCursor::open(lo); let (mut hi_cur, hi_first) = CheckedCursor::open(cursor)?; // D = height_lo − height_hi: dominance survives while no elementary // interval reads D > 0 (the pair sweep's `lo <= hi` direction, `lo` as the diff --git a/crates/before/src/version/skyline/build.rs b/crates/before/src/version/skyline/build.rs index ff577a8d..701c48da 100644 --- a/crates/before/src/version/skyline/build.rs +++ b/crates/before/src/version/skyline/build.rs @@ -53,7 +53,7 @@ //! `skyline_join_*` rows) pins the whole emission's transient against these //! bounds. -use crate::codec::{BitStack, BitsMut, BitsSlice, Code, PackedBuilder, PopStack}; +use crate::codec::{BitStack, BitsMut, BitsView, Code, PackedBuilder, PopStack}; /// The 1-bit payload code: `gamma(zigzag(0))`, the zero delta. /// @@ -213,16 +213,16 @@ impl SkylineBuilder { /// debug-asserts it: absorb takes only the held leaf's direct *right* /// sibling, while a subtree's first leaf at positive relative depth lies /// on the subtree's leftmost path and so enters as a *left* child. - /// `rest` is the subtree's stream from just past that leaf's payload - /// code to the subtree's end. Because every consecutive-leaf delta - /// strictly inside a canonical subtree is unchanged by anything outside - /// it, the range is copied in one splice instead of leaf by leaf; only - /// the held-leaf discipline is re-established around it — the last - /// leaf's flag is withheld and its code (`last_code_len` bits, ending - /// the range) becomes the held code. `first_rel_depth` and - /// `last_rel_depth` are the first and last leaves' depths below the - /// subtree root, each at least 1: a single-leaf subtree is fed wholly - /// through [`leaf`](Self::leaf) instead. + /// `start..end` names the subtree's bit range in `src` from just past + /// that leaf's payload code to the subtree's end. Because every + /// consecutive-leaf delta strictly inside a canonical subtree is + /// unchanged by anything outside it, the range is copied in one splice + /// instead of leaf by leaf; only the held-leaf discipline is + /// re-established around it — the last leaf's flag is withheld and its + /// code (`last_code_len` bits, ending the range) becomes the held code. + /// `first_rel_depth` and `last_rel_depth` are the first and last leaves' + /// depths below the subtree root, each at least 1: a single-leaf subtree + /// is fed wholly through [`leaf`](Self::leaf) instead. /// /// The spliced interior levels record no left-sibling-leaf collapse /// coordinates: a canonical subtree's rightmost leaf is never the equal @@ -230,9 +230,12 @@ impl SkylineBuilder { /// the source), so a cascade can never need to re-anchor into the spliced /// range, and the placeholder records only suppress merges canonicity /// already rules out. + #[allow(clippy::too_many_arguments)] // (src, start, end) is one logical range argument pub(super) fn continue_verbatim( &mut self, - rest: &BitsSlice, + src: BitsView<'_>, + start: u64, + end: u64, root_depth: usize, first_rel_depth: usize, last_rel_depth: usize, @@ -249,11 +252,11 @@ impl SkylineBuilder { its subtree root enters as a left child" ); debug_assert!( - rest.len() > last_code_len, + end - start > last_code_len as u64, "the continuation holds at least the last leaf's flag and code" ); - let last_flag = rest.len() - last_code_len - 1; - debug_assert!(rest[last_flag], "the continuation ends with a leaf"); + let last_flag = end - last_code_len as u64 - 1; + debug_assert!(src.bit(last_flag), "the continuation ends with a leaf"); let held = self .held .take() @@ -262,8 +265,8 @@ impl SkylineBuilder { // the last code is withheld as the new held leaf. self.out.push_bit(true); self.out.push_code(&held); - self.out.splice(&rest[..last_flag]); - self.held = Some(Code::from_slice(&rest[last_flag + 1..])); + self.out.splice(src, start, last_flag); + self.held = Some(Code::from_range(src, last_flag + 1, end)); // Re-anchor the per-level stacks from the first leaf's leftmost descent // to the last leaf's rightmost one. The popped levels were pushed by // `descend_to` (left branches, no left-sibling records), and the pushed diff --git a/crates/before/src/version/skyline/build/tests.rs b/crates/before/src/version/skyline/build/tests.rs index a43f2790..c6362666 100644 --- a/crates/before/src/version/skyline/build/tests.rs +++ b/crates/before/src/version/skyline/build/tests.rs @@ -241,7 +241,16 @@ fn continue_verbatim_matches_per_leaf_feeding() { spliced.leaf(3, delta(Sign::Positive, 2)); let (range, first_rel, last_rel, last_len) = continuation(2, 3, &[(3, delta(Sign::Positive, 1))]); - spliced.continue_verbatim(&range, 2, first_rel, last_rel, last_len); + let range_view = crate::codec::built_view(&range); + spliced.continue_verbatim( + range_view, + 0, + range_view.len(), + 2, + first_rel, + last_rel, + last_len, + ); spliced.leaf(1, delta(Sign::Negative, 1)); assert_eq!(spliced.finish(), per_leaf); } @@ -269,7 +278,16 @@ fn continue_verbatim_reanchors_across_levels() { 4, &[(4, delta(Sign::Positive, 3)), (3, delta(Sign::Negative, 1))], ); - spliced.continue_verbatim(&range, 2, first_rel, last_rel, last_len); + let range_view = crate::codec::built_view(&range); + spliced.continue_verbatim( + range_view, + 0, + range_view.len(), + 2, + first_rel, + last_rel, + last_len, + ); spliced.leaf(1, delta(Sign::Positive, 3)); assert_eq!(spliced.finish(), per_leaf); } @@ -293,7 +311,16 @@ fn collapse_after_a_splice_matches_per_leaf_feeding() { spliced.leaf(3, delta(Sign::Positive, 2)); let (range, first_rel, last_rel, last_len) = continuation(2, 3, &[(3, delta(Sign::Positive, 1))]); - spliced.continue_verbatim(&range, 2, first_rel, last_rel, last_len); + let range_view = crate::codec::built_view(&range); + spliced.continue_verbatim( + range_view, + 0, + range_view.len(), + 2, + first_rel, + last_rel, + last_len, + ); spliced.leaf(2, delta(Sign::Positive, 2)); spliced.leaf(2, delta(Sign::Positive, 0)); assert_eq!(spliced.finish(), per_leaf); diff --git a/crates/before/src/version/skyline/decode.rs b/crates/before/src/version/skyline/decode.rs index 122fe16c..0699ce8c 100644 --- a/crates/before/src/version/skyline/decode.rs +++ b/crates/before/src/version/skyline/decode.rs @@ -1,6 +1,6 @@ //! Strict decoding of a skyline stream into a stored [`Version`]. -use crate::codec::BitsSlice; +use crate::codec::BitsView; use crate::error::Decode; use crate::Version; @@ -11,7 +11,11 @@ use super::validate_bits; /// Acceptance is [`validate_bits`]'s, bit for bit; the stream then becomes the /// version's storage directly (the stored form *is* the skyline coding), so /// decoding materializes nothing beyond the copy. -pub(crate) fn decode_bits(bits: &BitsSlice) -> Result { +pub(crate) fn decode_bits(bits: BitsView<'_>) -> Result { validate_bits(bits)?; - Ok(Version::from_bits(bits.to_bitvec())) + // One spare bit of capacity: the storage gate's padding marker lands + // without regrowing (and re-copying) an exactly-sized buffer. + let mut copy = crate::codec::BitsMut::with_capacity(bits.len() as usize + 1); + crate::codec::extend_from_view(&mut copy, bits, 0, bits.len()); + Ok(Version::from_bits(copy)) } diff --git a/crates/before/src/version/skyline/emit.rs b/crates/before/src/version/skyline/emit.rs index 33102525..c50b7644 100644 --- a/crates/before/src/version/skyline/emit.rs +++ b/crates/before/src/version/skyline/emit.rs @@ -80,7 +80,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; -use crate::codec::{BitsMut, BitsSlice, Code, Int}; +use crate::codec::{BitsMut, BitsView, Code, Int}; use super::build::SkylineBuilder; use super::overlay::{advance_diff, OpenedPair, PlateauCursor, Side, Step}; @@ -126,7 +126,7 @@ fn follow_min(sign: Ordering, current: Side) -> Side { /// the walk structurally notices (truncation, malformation) panic; the rest (a /// collapsible sibling pair, a delta driving the running height negative) sweep /// silently, and the output is then unspecified. -pub fn join(a: &BitsSlice, b: &BitsSlice) -> BitsMut { +pub fn join(a: BitsView<'_>, b: BitsView<'_>) -> BitsMut { emit(a, b, follow_max) } @@ -139,7 +139,7 @@ pub fn join(a: &BitsSlice, b: &BitsSlice) -> BitsMut { /// /// [`join`]'s contract exactly: canonical operands required, structural /// violations panic, the rest yield an unspecified output. -pub fn meet(a: &BitsSlice, b: &BitsSlice) -> BitsMut { +pub fn meet(a: BitsView<'_>, b: BitsView<'_>) -> BitsMut { emit(a, b, follow_min) } @@ -187,7 +187,7 @@ pub struct Hull { /// /// [`join`]'s contract exactly: canonical operands required, structural /// violations panic, the rest yield an unspecified output triple. -pub fn hull(a_bits: &BitsSlice, b_bits: &BitsSlice) -> Hull { +pub fn hull(a_bits: BitsView<'_>, b_bits: BitsView<'_>) -> Hull { /// One output of the fused sweep: its side selection (pointwise min or max /// — the only point where the two outputs differ), the side it is currently /// following, and its builder. @@ -220,12 +220,12 @@ pub fn hull(a_bits: &BitsSlice, b_bits: &BitsSlice) -> Hull { Emission { pick: follow_min, side: Side::A, - out: SkylineBuilder::with_capacity(a_bits.len() + b_bits.len()), + out: SkylineBuilder::with_capacity((a_bits.len() + b_bits.len()) as usize), }, Emission { pick: follow_max, side: Side::A, - out: SkylineBuilder::with_capacity(a_bits.len() + b_bits.len()), + out: SkylineBuilder::with_capacity((a_bits.len() + b_bits.len()) as usize), }, ]; for emission in &mut outputs { @@ -276,7 +276,11 @@ pub fn hull(a_bits: &BitsSlice, b_bits: &BitsSlice) -> Hull { /// difference's sign and the current side — the winner by sign, sticky at ties, /// and the only point where join and meet differ (see the module doc's /// side-switch algebra). -fn emit(a_bits: &BitsSlice, b_bits: &BitsSlice, pick: impl Fn(Ordering, Side) -> Side) -> BitsMut { +fn emit( + a_bits: BitsView<'_>, + b_bits: BitsView<'_>, + pick: impl Fn(Ordering, Side) -> Side, +) -> BitsMut { let OpenedPair { a: mut cursor_a, b: mut cursor_b, @@ -296,7 +300,7 @@ fn emit(a_bits: &BitsSlice, b_bits: &BitsSlice, pick: impl Fn(Ordering, Side) -> // arbitrary: at a tie the two first heights are equal, so either side opens // the output identically. let mut side = pick(diff.sign(), Side::A); - let mut out = SkylineBuilder::with_capacity(a_bits.len() + b_bits.len()); + let mut out = SkylineBuilder::with_capacity((a_bits.len() + b_bits.len()) as usize); let first = match side { Side::A => &a_first, Side::B => &b_first, diff --git a/crates/before/src/version/skyline/emit/tests.rs b/crates/before/src/version/skyline/emit/tests.rs index e55f2a1f..b44b0f2c 100644 --- a/crates/before/src/version/skyline/emit/tests.rs +++ b/crates/before/src/version/skyline/emit/tests.rs @@ -20,7 +20,7 @@ use rayon::prelude::*; use suanpan::Accumulator; use crate::codec::Base; -use crate::codec::{BitsMut, BitsSlice}; +use crate::codec::{BitsMut, BitsView}; use crate::meter::registry::Shape; use crate::meter::Packed; use crate::testing::bridge::{from_oracle_version, to_oracle_version}; @@ -51,15 +51,16 @@ fn assert_emits(a: &Version, b: &Version) { let joined = encode(&from_oracle_version(&(ta.clone() | tb.clone()))); let met = encode(&from_oracle_version(&(ta & tb))); for (x, y) in [(&ea, &eb), (&eb, &ea)] { - let out = join(x, y); + let (vx, vy) = (crate::codec::built_view(x), crate::codec::built_view(y)); + let out = join(vx, vy); assert_eq!(out, joined, "join must match the oracle: {a} vs {b}"); - validate(&out).expect("an emitted join is canonical"); - assert_pointwise(x, y, &out, false); - let out = meet(x, y); + validate(crate::codec::built_view(&out)).expect("an emitted join is canonical"); + assert_pointwise(vx, vy, crate::codec::built_view(&out), false); + let out = meet(vx, vy); assert_eq!(out, met, "meet must match the oracle: {a} vs {b}"); - validate(&out).expect("an emitted meet is canonical"); - assert_pointwise(x, y, &out, true); - let hulled = hull(x, y); + validate(crate::codec::built_view(&out)).expect("an emitted meet is canonical"); + assert_pointwise(vx, vy, crate::codec::built_view(&out), true); + let hulled = hull(crate::codec::built_view(x), crate::codec::built_view(y)); assert_eq!( hulled.relation, oracle_relation(&met, x, y), @@ -100,7 +101,7 @@ fn oracle_relation(met: &BitsMut, x: &BitsMut, y: &BitsMut) -> Option, b: BitsView<'_>, out: BitsView<'_>, meet: bool) { let (mut ca, ha) = LeafCursor::open(a); let (mut cb, hb) = LeafCursor::open(b); let (mut co, ho) = LeafCursor::open(out); @@ -248,7 +249,10 @@ fn flat_over_deep_collapses_totally() { let deep = version_of(&Shape::Dense.packed1(512)); let flat = version_of(&Shape::Hugeleaf.packed1(600)); assert_emits(&deep, &flat); - let joined = join(&encode(&deep), &encode(&flat)); + let joined = join( + crate::codec::built_view(&encode(&deep)), + crate::codec::built_view(&encode(&flat)), + ); assert_eq!( joined, encode(&flat), @@ -278,16 +282,16 @@ fn exhaustive_small_scope_emits_identically() { let joined = encode(&from_oracle_version(&(ta.clone() | tb.clone()))); let met = encode(&from_oracle_version(&(ta.clone() & tb.clone()))); assert_eq!( - join(ea, eb), + join(crate::codec::built_view(ea), crate::codec::built_view(eb)), joined, "join must match the oracle: {va} vs {vb}" ); assert_eq!( - meet(ea, eb), + meet(crate::codec::built_view(ea), crate::codec::built_view(eb)), met, "meet must match the oracle: {va} vs {vb}" ); - let hulled = hull(ea, eb); + let hulled = hull(crate::codec::built_view(ea), crate::codec::built_view(eb)); assert_eq!( hulled.relation, oracle_relation(&met, ea, eb), @@ -309,15 +313,39 @@ fn exhaustive_small_scope_emits_identically() { fn family_lattice_laws_hold_on_the_kernel() { let pool: Vec = family_pool().iter().map(encode).collect(); for ea in &pool { - assert_eq!(join(ea, ea), *ea, "join is idempotent"); - assert_eq!(meet(ea, ea), *ea, "meet is idempotent"); + assert_eq!( + join(crate::codec::built_view(ea), crate::codec::built_view(ea)), + *ea, + "join is idempotent" + ); + assert_eq!( + meet(crate::codec::built_view(ea), crate::codec::built_view(ea)), + *ea, + "meet is idempotent" + ); for eb in &pool { - let j = join(ea, eb); - let m = meet(ea, eb); - assert_eq!(j, join(eb, ea), "join commutes"); - assert_eq!(m, meet(eb, ea), "meet commutes"); - assert_eq!(join(ea, &m), *ea, "join absorbs the meet"); - assert_eq!(meet(ea, &j), *ea, "meet absorbs the join"); + let j = join(crate::codec::built_view(ea), crate::codec::built_view(eb)); + let m = meet(crate::codec::built_view(ea), crate::codec::built_view(eb)); + assert_eq!( + j, + join(crate::codec::built_view(eb), crate::codec::built_view(ea)), + "join commutes" + ); + assert_eq!( + m, + meet(crate::codec::built_view(eb), crate::codec::built_view(ea)), + "meet commutes" + ); + assert_eq!( + join(crate::codec::built_view(ea), crate::codec::built_view(&m)), + *ea, + "join absorbs the meet" + ); + assert_eq!( + meet(crate::codec::built_view(ea), crate::codec::built_view(&j)), + *ea, + "meet absorbs the join" + ); } } } @@ -330,13 +358,37 @@ fn family_associativity_holds_on_the_kernel() { for eb in &pool { for ec in &pool { assert_eq!( - join(&join(ea, eb), ec), - join(ea, &join(eb, ec)), + join( + crate::codec::built_view(&join( + crate::codec::built_view(ea), + crate::codec::built_view(eb), + )), + crate::codec::built_view(ec), + ), + join( + crate::codec::built_view(ea), + crate::codec::built_view(&join( + crate::codec::built_view(eb), + crate::codec::built_view(ec), + )), + ), "join associates" ); assert_eq!( - meet(&meet(ea, eb), ec), - meet(ea, &meet(eb, ec)), + meet( + crate::codec::built_view(&meet( + crate::codec::built_view(ea), + crate::codec::built_view(eb), + )), + crate::codec::built_view(ec), + ), + meet( + crate::codec::built_view(ea), + crate::codec::built_view(&meet( + crate::codec::built_view(eb), + crate::codec::built_view(ec), + )), + ), "meet associates" ); } @@ -375,10 +427,10 @@ proptest! { let ea = encode(&from_oracle_version(&a)); let eb = encode(&from_oracle_version(&b)); let ec = encode(&from_oracle_version(&c)); - prop_assert_eq!(join(&join(&ea, &eb), &ec), join(&ea, &join(&eb, &ec))); - prop_assert_eq!(meet(&meet(&ea, &eb), &ec), meet(&ea, &meet(&eb, &ec))); - prop_assert_eq!(join(&ea, &meet(&ea, &eb)), ea.clone()); - prop_assert_eq!(meet(&ea, &join(&ea, &eb)), ea); + prop_assert_eq!(join(crate::codec::built_view(&join(crate::codec::built_view(&ea), crate::codec::built_view(&eb))), crate::codec::built_view(&ec)), join(crate::codec::built_view(&ea), crate::codec::built_view(&join(crate::codec::built_view(&eb), crate::codec::built_view(&ec))))); + prop_assert_eq!(meet(crate::codec::built_view(&meet(crate::codec::built_view(&ea), crate::codec::built_view(&eb))), crate::codec::built_view(&ec)), meet(crate::codec::built_view(&ea), crate::codec::built_view(&meet(crate::codec::built_view(&eb), crate::codec::built_view(&ec))))); + prop_assert_eq!(join(crate::codec::built_view(&ea), crate::codec::built_view(&meet(crate::codec::built_view(&ea), crate::codec::built_view(&eb)))), ea.clone()); + prop_assert_eq!(meet(crate::codec::built_view(&ea), crate::codec::built_view(&join(crate::codec::built_view(&ea), crate::codec::built_view(&eb)))), ea); } /// Every pair of versions produced by one organic fork/tick/send/sync/join @@ -398,16 +450,16 @@ proptest! { let joined = encode(&from_oracle_version(&(ta.clone() | tb.clone()))); let met = encode(&from_oracle_version(&(ta.clone() & tb.clone()))); prop_assert_eq!( - join(ea, eb), + join(crate::codec::built_view(ea), crate::codec::built_view(eb)), joined.clone(), "join must match the oracle: {} vs {}", va, vb ); prop_assert_eq!( - meet(ea, eb), + meet(crate::codec::built_view(ea), crate::codec::built_view(eb)), met.clone(), "meet must match the oracle: {} vs {}", va, vb ); - let hulled = hull(ea, eb); + let hulled = hull(crate::codec::built_view(ea), crate::codec::built_view(eb)); prop_assert_eq!( hulled.relation, oracle_relation(&met, ea, eb), diff --git a/crates/before/src/version/skyline/encode.rs b/crates/before/src/version/skyline/encode.rs index bded6152..cbbf9165 100644 --- a/crates/before/src/version/skyline/encode.rs +++ b/crates/before/src/version/skyline/encode.rs @@ -2,7 +2,7 @@ //! generators' construction language (min-lifted preorder packed streams, one //! gamma-coded base per node) to the stored skyline coding. -use crate::codec::{self, Base, BitsMut, BitsSlice}; +use crate::codec::{self, Base, BitsMut, BitsView}; use super::signed::zigzag; @@ -20,9 +20,9 @@ use super::signed::zigzag; /// /// Panics if the packed form does not parse cleanly; callers hand in /// generator-built canonical streams. -pub(crate) fn encode_bits(bits: &BitsSlice) -> BitsMut { - let mut out = BitsMut::with_capacity(bits.len()); - let mut pos = 0usize; +pub(crate) fn encode_bits(bits: BitsView<'_>) -> BitsMut { + let mut out = BitsMut::with_capacity(bits.len() as usize); + let mut pos = 0u64; // Inherited root-to-node path sums for the nodes not yet visited, top of // stack belonging to the next node in the preorder stream. Both children of // an internal node inherit the same sum, and the stream lists the whole @@ -31,7 +31,7 @@ pub(crate) fn encode_bits(bits: &BitsSlice) -> BitsMut { let mut prev_leaf: Option = None; while let Some(offset) = offsets.pop() { - let internal = bits[pos]; + let internal = bits.bit(pos); pos += 1; let (base, next) = codec::decode_int(bits, pos).expect("canonical Version parses cleanly"); pos = next; diff --git a/crates/before/src/version/skyline/fill.rs b/crates/before/src/version/skyline/fill.rs index 1371c8ac..55a2816c 100644 --- a/crates/before/src/version/skyline/fill.rs +++ b/crates/before/src/version/skyline/fill.rs @@ -153,7 +153,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; -use crate::codec::{self, Base, BitCursor, BitStack, BitsMut, BitsSlice, Int, PopStack}; +use crate::codec::{self, Base, BitCursor, BitStack, BitsMut, BitsView, Int, PopStack}; use crate::idbits::{IdNode, IdReader}; use self::fuse::{decode_cost_component, encode_cost_component, Out, RouteProbe}; @@ -206,7 +206,7 @@ const _: () = assert!( /// at least one region: an empty id leaves `fill` the identity, and the grow /// fallback requires an owning id (debug builds assert it; the result on an /// empty id is unspecified in release builds). -pub fn tick(event: &BitsSlice, id: &crate::Party) -> BitsMut { +pub fn tick(event: BitsView<'_>, id: &crate::Party) -> BitsMut { // `n = 1` performs exactly one fused walk plus at most one splice: // the delta against a direct dispatch is two unmetered width tests // and one non-allocating `Base` construction. The committed @@ -244,11 +244,13 @@ pub fn tick(event: &BitsSlice, id: &crate::Party) -> BitsMut { /// [`validate`](fn@super::validate) first on untrusted bytes. For `n >= 1` the /// id must own at least one region, exactly as [`tick`] (debug builds assert /// it; the result on an empty id is unspecified in release builds). -pub fn ticks(event: &BitsSlice, id: &crate::Party, n: &Base) -> BitsMut { +pub fn ticks(event: BitsView<'_>, id: &crate::Party, n: &Base) -> BitsMut { // Width tests, not value compares: n = 0 has no bits, n = 1 is the // one-bit magnitude, and neither test touches the limb meter. if n.bits() == 0 { - return event.to_bitvec(); + let mut out = BitsMut::with_capacity(event.len() as usize); + codec::extend_from_view(&mut out, event, 0, event.len()); + return out; } match fused_fill(event, id) { FillOutcome::Changed(bits) => { @@ -257,9 +259,9 @@ pub fn ticks(event: &BitsSlice, id: &crate::Party, n: &Base) -> BitsMut { return bits; } let remaining = n.clone() - &Base::from(1u8); - match fused_fill(&bits, id) { + match fused_fill(codec::built_view(&bits), id) { FillOutcome::Unchanged(route) => { - super::grow::emit(&bits, id.as_bits(), &route, &remaining) + super::grow::emit(codec::built_view(&bits), id.as_bits(), &route, &remaining) } FillOutcome::Changed(_) => { unreachable!("fill is idempotent: a filled tree cannot fill again") @@ -288,7 +290,7 @@ pub(super) enum FillOutcome { /// # Panics /// /// Panics if the event operand is not a canonical skyline stream. -pub(super) fn fused_fill(event_bits: &BitsSlice, id: &crate::Party) -> FillOutcome { +pub(super) fn fused_fill(event_bits: BitsView<'_>, id: &crate::Party) -> FillOutcome { let id_bits = id.as_bits(); let mut walk = FillWalk { event: event_bits, @@ -302,7 +304,7 @@ pub(super) fn fused_fill(event_bits: &BitsSlice, id: &crate::Party) -> FillOutco memo: Memo::new(), relation: Relation::None, out: Out::Unstarted, - probe: RouteProbe::new(id_bits.len()), + probe: RouteProbe::new(id_bits.len() as usize), }; let mut reader = IdReader::root(id_bits); walk.web.open(1); @@ -344,7 +346,7 @@ pub(super) fn fused_fill(event_bits: &BitsSlice, id: &crate::Party) -> FillOutco struct FillWalk<'a> { /// The input skyline stream (kept beside the cursor for the unmetered /// single-flag peek and the sub-scans' spawn positions). - event: &'a BitsSlice, + event: BitsView<'a>, /// The input cursor. cursor: codec::DsiCursor<'a>, /// Whether the next payload is the stream's first (coded absolute, not as a @@ -633,8 +635,8 @@ impl FillWalk<'_> { } /// The cursor's bit position: the next node's flag. - fn pos(&self) -> usize { - self.cursor.position() + fn pos(&self) -> u64 { + self.cursor.position_u64() } /// Read one topology flag at the cursor (`true` = leaf), recording the @@ -1068,7 +1070,9 @@ impl FillWalk<'_> { // The region's last leaf is the last emission. self.gap.reset(); self.out.continue_verbatim( - &self.event[rest_start..self.pos()], + self.event, + rest_start, + self.pos(), depth, first_leaf_depth, skip.last_depth, @@ -1095,7 +1099,7 @@ impl FillWalk<'_> { // reproduces the input's topology iff the range is a single leaf — // exactly its first flag bit (`1` = leaf). An unmetered peek of the bit // the scan is about to read as its first flag. - self.range_is_leaf = self.event[self.pos()]; + self.range_is_leaf = self.event.bit(self.pos()); let mut above = Extremum::max(self.web.lease()); let mut walk = LeafWalk::new(); let first_leaf_depth = walk @@ -1156,7 +1160,7 @@ enum Frame { /// delta's width in transient, never a machine word. struct DeltaReg { /// The most recently pushed position (zero before any push). - register: usize, + register: u64, } impl DeltaReg { @@ -1166,20 +1170,20 @@ impl DeltaReg { /// Suspend `position`: its delta against the register goes onto `values`, /// and the register advances to it. - fn push(&mut self, values: &mut PopStack, position: usize) { + fn push(&mut self, values: &mut PopStack, position: u64) { debug_assert!( position >= self.register, "registered positions only advance" ); - values.push((position - self.register) as u64); + values.push(position - self.register); self.register = position; } /// Restore the register to the previous position, returning the popped /// one. - fn pop(&mut self, values: &mut PopStack) -> usize { + fn pop(&mut self, values: &mut PopStack) -> u64 { let position = self.register; - self.register = position - values.pop() as usize; + self.register = position - values.pop(); position } } @@ -1247,7 +1251,7 @@ impl Frames { /// Suspend an ordinary node: key delta on the value stack, control bits /// armed for the left child. - fn push_node(&mut self, key: usize, right: bool) { + fn push_node(&mut self, key: u64, right: bool) { self.keys.push(&mut self.values, key); self.site.push(false); self.phase.push(false); @@ -1255,7 +1259,7 @@ impl Frames { } /// Suspend a consume-site around its sibling walk. - fn push_site(&mut self, key: usize, outermost: bool) { + fn push_site(&mut self, key: u64, outermost: bool) { self.keys.push(&mut self.values, key); self.site.push(true); self.phase.push(false); @@ -1277,7 +1281,7 @@ impl Frames { /// Pop the control bits of the top frame and restore the key register, /// returning the frame's key. - fn pop_key(&mut self) -> usize { + fn pop_key(&mut self) -> u64 { self.site.pop(); self.phase.pop(); self.aux.pop(); @@ -1286,19 +1290,19 @@ impl Frames { /// Close a left-awaiting node whose right side resolved in place: its route /// key. - fn pop_await_left(&mut self) -> usize { + fn pop_await_left(&mut self) -> u64 { self.pop_key() } /// Close a right-awaiting node: its route key and deferred left cost. - fn pop_await_right(&mut self) -> (usize, Cost) { + fn pop_await_right(&mut self) -> (u64, Cost) { let depth = decode_cost_component(self.values.pop()); let expansions = decode_cost_component(self.values.pop()); (self.pop_key(), Cost { expansions, depth }) } /// Close a site frame: its route key and outermost flag. - fn pop_site(&mut self) -> (usize, bool) { + fn pop_site(&mut self) -> (u64, bool) { let outermost = self.aux_top(); (self.pop_key(), outermost) } @@ -1309,7 +1313,7 @@ impl Frames { /// /// The absent-right-sibling raise's argument (`min(fill(0, er)) = min(er)`), /// priced by the scan that reads the range. -fn scan_min_from(event: &BitsSlice, pos: usize) -> Signed { +fn scan_min_from(event: BitsView<'_>, pos: u64) -> Signed { let mut cursor = codec::DsiCursor::new_at(event, pos); let skip = skip_region(&mut cursor); // `min = h_entry + net + (min − h_exit)`. diff --git a/crates/before/src/version/skyline/fill/fuse.rs b/crates/before/src/version/skyline/fill/fuse.rs index 0c5cdcd3..d26acbcc 100644 --- a/crates/before/src/version/skyline/fill/fuse.rs +++ b/crates/before/src/version/skyline/fill/fuse.rs @@ -44,7 +44,7 @@ //! that shifts which leaf is first trips on topology (the replaced range was //! not a single leaf) before any code comparison is reached. -use crate::codec::{BitCursor, BitStack, BitsMut, BitsSlice, Code, PopStack}; +use crate::codec::{BitStack, BitsMut, BitsView, Code, PopStack}; use crate::idbits::{IdNode, IdReader}; use super::super::build::SkylineBuilder; @@ -100,7 +100,7 @@ pub(super) enum Out { Verbatim { /// The input position just past the last matched plateau's code: the /// prefix a divergence materializes. - matched_end: usize, + matched_end: u64, }, /// A plateau diverged (or the walk replayed the prefix): the canonical /// builder holds the real output. @@ -135,7 +135,7 @@ impl Out { /// matched verbatim emission does no output work at all); on a built output /// this is a no-op answering false — emission bodies always run /// post-divergence. - pub(super) fn note_match(&mut self, end: usize) -> bool { + pub(super) fn note_match(&mut self, end: u64) -> bool { match self { Out::Unstarted | Out::Verbatim { .. } => { *self = Out::Verbatim { matched_end: end }; @@ -170,9 +170,12 @@ impl Out { /// # Panics /// /// Panics on a verbatim walk: the splice runs post-divergence. + #[allow(clippy::too_many_arguments)] // (src, start, end) is one logical range argument pub(super) fn continue_verbatim( &mut self, - rest: &BitsSlice, + src: BitsView<'_>, + start: u64, + end: u64, root_depth: usize, first_rel_depth: usize, last_rel_depth: usize, @@ -180,7 +183,9 @@ impl Out { ) { match self { Out::Built(builder) => builder.continue_verbatim( - rest, + src, + start, + end, root_depth, first_rel_depth, last_rel_depth, @@ -203,16 +208,16 @@ impl Out { /// verbatim run; the walk from here on is a direct fill emission. Iterative /// (a path bit stack, no recursion), so prefix depth cannot overflow the /// native stack. - pub(super) fn materialize(&mut self, event: &BitsSlice) { + pub(super) fn materialize(&mut self, event: BitsView<'_>) { let matched_end = match self { Out::Unstarted => 0, Out::Verbatim { matched_end } => *matched_end, Out::Built(_) => return, }; - let mut builder = SkylineBuilder::with_capacity(event.len()); + let mut builder = SkylineBuilder::with_capacity(event.len() as usize); let mut cursor = crate::codec::DsiCursor::new(event); let mut walk = LeafWalk::new(); - while cursor.position() < matched_end { + while cursor.position_u64() < matched_end { // A descent never straddles `matched_end` (a matched prefix ends on // a plateau boundary), and a divergence always leaves its own // consumed range beyond the matched prefix, so the prefix is a @@ -222,12 +227,12 @@ impl Out { let depth = walk .descend(&mut cursor) .expect("a matched prefix is a proper prefix of the tiling"); - let start = cursor.position(); + let start = cursor.position_u64(); cursor.skip_int().expect("canonical skyline bits"); - builder.leaf(depth, Code::from_slice(&event[start..cursor.position()])); + builder.leaf(depth, Code::from_range(event, start, cursor.position_u64())); } debug_assert_eq!( - cursor.position(), + cursor.position_u64(), matched_end, "a matched prefix ends on a plateau boundary" ); @@ -237,7 +242,7 @@ impl Out { /// Finish the walk's output: the built stream when a plateau diverged, or /// `None` for an unchanged walk (every plateau matched; `fill(i, e) = e`, /// byte-exact by canonical uniqueness). - pub(super) fn finish(self, event: &BitsSlice) -> Option { + pub(super) fn finish(self, event: BitsView<'_>) -> Option { let matched_end = match self { Out::Built(builder) => return Some(builder.finish()), Out::Unstarted => 0, @@ -246,7 +251,7 @@ impl Out { debug_assert_eq!( matched_end, event.len(), - "an unchanged walk matches every input plateau" + "an unchanged walk matches every input plateau", ); None } @@ -289,7 +294,7 @@ impl RouteProbe { /// Fold a branch node whose children's costs the walk computed (`grow((il, /// ir), (n, el, er))`: the cheaper child, ties right, cost + 1), recording /// the chosen direction at the branch's id key. - pub(super) fn join(&mut self, key: usize, left: Cost, right: Cost) -> Cost { + pub(super) fn join(&mut self, key: u64, left: Cost, right: Cost) -> Cost { if !self.live { return Cost::MAX; } @@ -310,13 +315,7 @@ impl RouteProbe { /// pays — computing each node's distance to its nearest owned terminal and /// recording the direction toward it (ties right); it advances `id` past /// both children, exactly as the plain skips would. - pub(super) fn expand( - &mut self, - key: usize, - id: &mut IdReader, - left: bool, - right: bool, - ) -> Cost { + pub(super) fn expand(&mut self, key: u64, id: &mut IdReader, left: bool, right: bool) -> Cost { if !self.live { if left { id.skip(); diff --git a/crates/before/src/version/skyline/fill/memo.rs b/crates/before/src/version/skyline/fill/memo.rs index bdcd91a7..37de924d 100644 --- a/crates/before/src/version/skyline/fill/memo.rs +++ b/crates/before/src/version/skyline/fill/memo.rs @@ -82,7 +82,7 @@ pub(super) struct Memo { pub(super) cursor: usize, /// The end position of the current fresh scan's span: sites before it are /// recorded; a site at or past it launches a new scan. - pub(super) covered_until: usize, + pub(super) covered_until: u64, /// Order-sensitive checksum of the recorded sites' positions, matched /// against the consumed ones when the scan drains — O(1) state where a /// position list would bill the heap meter for a debug-only buffer. @@ -98,8 +98,8 @@ const _: () = assert!(core::mem::size_of::>() == core::mem::s /// Fold one position into an order-sensitive checksum (FNV-style). #[cfg(debug_assertions)] -pub(super) fn position_check(check: u64, pos: usize) -> u64 { - (check ^ pos as u64).wrapping_mul(0x0100_0000_01b3) +pub(super) fn position_check(check: u64, pos: u64) -> u64 { + (check ^ pos).wrapping_mul(0x0100_0000_01b3) } impl Memo { diff --git a/crates/before/src/version/skyline/fill/prescan.rs b/crates/before/src/version/skyline/fill/prescan.rs index 77baa14a..045fd6c4 100644 --- a/crates/before/src/version/skyline/fill/prescan.rs +++ b/crates/before/src/version/skyline/fill/prescan.rs @@ -50,7 +50,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; -use crate::codec::{self, BitCursor, BitStack, BitsSlice, PopStack}; +use crate::codec::{self, BitCursor, BitStack, BitsView, PopStack}; use crate::idbits::{IdNode, IdReader}; use super::super::signed::{fold_signed_int, unzigzag, Signed}; @@ -119,7 +119,7 @@ pub(super) struct SuspendedLevel { impl<'a, 'm> PreScan<'a, 'm> { /// A fresh scan entered at `start`: the cursor at the entry, an empty web, /// the entry net alive at zero, no head seeded, the outermost level. - pub(super) fn new(event: &'a BitsSlice, start: usize, memo: &'m mut Memo) -> Self { + pub(super) fn new(event: BitsView<'a>, start: u64, memo: &'m mut Memo) -> Self { PreScan { cursor: codec::DsiCursor::new_at(event, start), web: MinWeb::new(), @@ -152,7 +152,7 @@ impl<'a, 'm> PreScan<'a, 'm> { /// consequence). The site-nesting level is one plus the count of open /// site frames: a site's own range walks at `level + 1`, and its close /// records at `level`. - pub(super) fn run(&mut self, id: &mut IdReader) -> usize { + pub(super) fn run(&mut self, id: &mut IdReader) -> u64 { let mut frames = PreFrames::new(); let mut level: u64 = 1; 'descend: loop { @@ -207,7 +207,7 @@ impl<'a, 'm> PreScan<'a, 'm> { self.web.close(); break; } - let slot = self.reserve(self.cursor.position()); + let slot = self.reserve(self.cursor.position_u64()); frames.push_site(slot); level += 1; self.web.open(1); @@ -227,7 +227,7 @@ impl<'a, 'm> PreScan<'a, 'm> { // Ascend: resume suspended nodes as their children complete. loop { let Some(top) = frames.top() else { - return self.cursor.position(); + return self.cursor.position_u64(); }; self.web.close(); match top { @@ -286,7 +286,7 @@ impl<'a, 'm> PreScan<'a, 'm> { /// Reserve the next consumption-order queue slot for the site whose range /// starts at `pos`. - pub(super) fn reserve(&mut self, pos: usize) -> usize { + pub(super) fn reserve(&mut self, pos: u64) -> usize { let slot = self.memo.queue.len(); self.memo.queue.push(None); #[cfg(debug_assertions)] @@ -660,7 +660,7 @@ impl PreFrames { /// Suspend a left-full site around its sibling walk. fn push_site(&mut self, slot: usize) { - self.slots.push(&mut self.values, slot); + self.slots.push(&mut self.values, slot as u64); self.site.push(true); self.phase.push(false); self.aux.push(false); @@ -688,6 +688,8 @@ impl PreFrames { self.site.pop(); self.phase.pop(); self.aux.pop(); - self.slots.pop(&mut self.values) + // Ledger slots are queue indices, capped far below `u32::MAX` by the + // ledger's own link-storage contract. + self.slots.pop(&mut self.values) as usize } } diff --git a/crates/before/src/version/skyline/fill/tests.rs b/crates/before/src/version/skyline/fill/tests.rs index 4f816ec7..88e45ccb 100644 --- a/crates/before/src/version/skyline/fill/tests.rs +++ b/crates/before/src/version/skyline/fill/tests.rs @@ -55,7 +55,10 @@ fn party_of(p: &Packed) -> Party { /// Whether the fused walk's changed flag tripped on one pair. fn flag_of(v: &Version, p: &Party) -> bool { - matches!(fused_fill(&encode(v), p), FillOutcome::Changed(_)) + matches!( + fused_fill(crate::codec::built_view(&encode(v)), p), + FillOutcome::Changed(_) + ) } /// The two differentials of record on one pair, plus entry agreement and @@ -71,7 +74,7 @@ fn assert_tick(v: &Version, p: &Party) { let enc = encode(v); let filled = from_oracle_version(&to_oracle_version(v).fill_for_test(&to_oracle_party(p))); let changed = filled != *v; - match fused_fill(&enc, p) { + match fused_fill(crate::codec::built_view(&enc), p) { FillOutcome::Changed(bits) => { assert!( changed, @@ -90,8 +93,8 @@ fn assert_tick(v: &Version, p: &Party) { ); } } - let out = tick(&enc, p); - validate(&out).expect("a ticked stream is canonical"); + let out = tick(crate::codec::built_view(&enc), p); + validate(crate::codec::built_view(&out)).expect("a ticked stream is canonical"); let mut oracle = to_oracle_version(v); oracle.tick(&to_oracle_party(p)); assert_eq!( @@ -287,7 +290,7 @@ fn worked_examples_tick_exactly() { let p: Party = party.parse().expect("test party literals parse"); let v: Version = before.parse().expect("test version literals parse"); let expected: Version = after.parse().expect("test version literals parse"); - match fused_fill(&encode(&v), &p) { + match fused_fill(crate::codec::built_view(&encode(&v)), &p) { FillOutcome::Changed(bits) => assert_eq!( bits, encode(&expected), @@ -404,25 +407,25 @@ fn materialize_is_a_noop_once_built() { .parse() .expect("test version literals parse"); let event = encode(&v); - let matched_end = event.len(); + let matched_end = event.len() as u64; let mut once = Out::Verbatim { matched_end }; - once.materialize(&event); + once.materialize(crate::codec::built_view(&event)); let mut twice = Out::Verbatim { matched_end }; - twice.materialize(&event); + twice.materialize(crate::codec::built_view(&event)); assert!( !twice.is_verbatim(), "the first materialize leaves the output built" ); // The second call runs against the built output: the no-op contract. - twice.materialize(&event); + twice.materialize(crate::codec::built_view(&event)); let once_out = once - .finish(&event) + .finish(crate::codec::built_view(&event)) .expect("a built output finishes to a stream"); let twice_out = twice - .finish(&event) + .finish(crate::codec::built_view(&event)) .expect("a built output finishes to a stream"); assert_eq!( once_out, event, @@ -1029,9 +1032,9 @@ fn deep_spines_tick_and_flag_identically() { // (fill idempotence, flag-denominated). let assert_deep_changed = |v: &Version, p: &Party, expected: &Version| { let enc = encode(v); - match fused_fill(&enc, p) { + match fused_fill(crate::codec::built_view(&enc), p) { FillOutcome::Changed(bits) => { - validate(&bits).expect("a filled stream is canonical"); + validate(crate::codec::built_view(&bits)).expect("a filled stream is canonical"); assert_eq!(bits, encode(expected), "the derived closed form"); let again: Version = Version::from_bits(bits.clone()); assert!( @@ -1042,7 +1045,7 @@ fn deep_spines_tick_and_flag_identically() { FillOutcome::Unchanged(_) => panic!("fill moves this pair: the flag must trip"), } assert_eq!( - tick(&enc, p), + tick(crate::codec::built_view(&enc), p), encode(expected), "tick takes the fill branch: the collapse" ); @@ -1055,8 +1058,8 @@ fn deep_spines_tick_and_flag_identically() { let assert_deep_unchanged = |v: &Version, p: &Party, grown: &Version| { let enc = encode(v); assert!(!flag_of(v, p), "fill is the identity: the flag stays clear"); - let out = tick(&enc, p); - validate(&out).expect("a ticked stream is canonical"); + let out = tick(crate::codec::built_view(&enc), p); + validate(crate::codec::built_view(&out)).expect("a ticked stream is canonical"); assert_eq!(out, encode(grown), "the derived grow closed form"); let mut ticked = v.clone(); ticked.tick(p); @@ -1097,7 +1100,7 @@ fn deep_spines_tick_and_flag_identically() { ); let mut grown = matched.clone(); grown.tick(&nested); - validate(&encode(&grown)).expect("a ticked stream is canonical"); + validate(crate::codec::built_view(&encode(&grown))).expect("a ticked stream is canonical"); // The mirror id over the wide-tail spine: a left-full shortcut site at // every one of the 4096 levels — the memoized pre-scan at full depth, every @@ -1199,10 +1202,16 @@ fn tick_splices_fill_and_grow() { // fill simplifies: the collapse is the tick. let v: Version = "(2, 0, 1)".parse().expect("test literals parse"); let p: Party = "(1, 0)".parse().expect("test literals parse"); - assert_eq!(tick(&encode(&v), &p), encode(&"3".parse().unwrap())); + assert_eq!( + tick(crate::codec::built_view(&encode(&v)), &p), + encode(&"3".parse().unwrap()) + ); // fill is the identity: grow registers the event. let v: Version = "(0, 1, 0)".parse().expect("test literals parse"); - assert_eq!(tick(&encode(&v), &p), encode(&"(0, 2, 0)".parse().unwrap())); + assert_eq!( + tick(crate::codec::built_view(&encode(&v)), &p), + encode(&"(0, 2, 0)".parse().unwrap()) + ); assert_tick(&v, &p); } @@ -1267,8 +1276,8 @@ proptest! { let v = from_oracle_version(&ov); if !p.as_bits().is_empty() { let ev = encode(&v); - let out = tick(&ev, &p); - let bound = 2 * ev.len() + 4 * p.as_bits().len() + 32; + let out = tick(crate::codec::built_view(&ev), &p); + let bound = 2 * ev.len() + 4 * p.as_bits().len() as usize + 32; prop_assert!( out.len() <= bound, "tick output {} bits exceeds input envelope {} (event {}, id {})", @@ -1302,13 +1311,13 @@ proptest! { let v = from_oracle_version(&ov); if !p.as_bits().is_empty() { let mut e = encode(&v); - e = tick(&e, &p); + e = tick(crate::codec::built_view(&e), &p); let b1 = e.len(); for k in 2u32..=48 { - e = tick(&e, &p); + e = tick(crate::codec::built_view(&e), &p); let logk = u64::from(32 - (k + 1).leading_zeros()); let bound = b1 as u64 - + 4 * p.as_bits().len() as u64 + + 4 * p.as_bits().len() + 4 * logk + 8; prop_assert!( @@ -1336,10 +1345,10 @@ fn tick_deep_orbits_stay_banded() { let idb = party_of(&Shape::IdSpine.packed_flagged(4, true)); let mut e = encode(&ev); - e = tick(&e, &ida); + e = tick(crate::codec::built_view(&e), &ida); let b1 = e.len(); for k in 2u32..=4096 { - e = tick(&e, &ida); + e = tick(crate::codec::built_view(&e), &ida); let logk = usize::try_from(32 - (k + 1).leading_zeros()).expect("small"); assert!( e.len() <= b1 + 4 * logk + 8, @@ -1349,11 +1358,14 @@ fn tick_deep_orbits_stay_banded() { } let mut e = encode(&ev); - e = tick(&e, &ida); - e = tick(&e, &idb); + e = tick(crate::codec::built_view(&e), &ida); + e = tick(crate::codec::built_view(&e), &idb); let b2 = e.len(); for k in 3u32..=2048 { - e = tick(&e, if k % 2 == 1 { &ida } else { &idb }); + e = tick( + crate::codec::built_view(&e), + if k % 2 == 1 { &ida } else { &idb }, + ); let logk = usize::try_from(32 - (k + 1).leading_zeros()).expect("small"); assert!( e.len() <= b2 + 4 * logk + 8, @@ -1407,7 +1419,7 @@ fn direction_chain(path: &[bool]) -> Party { fn assert_chain_saturation(path: &[bool], ceiling: u64) { let p = direction_chain(path); let bits = p.as_bits(); - let mut probe = RouteProbe::new(bits.len()); + let mut probe = RouteProbe::new(bits.len() as usize); let mut id = IdReader::root(bits); let cost = probe.expand_subtree(&mut id, ceiling); assert_eq!(id.pos(), bits.len(), "the DP consumes exactly the subtree"); @@ -1485,7 +1497,7 @@ proptest! { /// [`ticks`] lifted to the stored-value level, through the same `from_bits` /// gate the public entry commits through. fn ticks_version(v: &Version, id: &Party, n: &Base) -> Version { - Version::from_bits(ticks(&encode(v), id, n)) + Version::from_bits(ticks(crate::codec::built_view(&encode(v)), id, n)) } /// Check `ticks(n)` against the iterated public tick for every `n` in @@ -1562,9 +1574,9 @@ proptest! { ) { let v = from_oracle_version(&ov); let p = from_oracle_party(&op); - if let FillOutcome::Changed(bits) = fused_fill(&encode(&v), &p) { + if let FillOutcome::Changed(bits) = fused_fill(crate::codec::built_view(&encode(&v)), &p) { prop_assert!( - matches!(fused_fill(&bits, &p), FillOutcome::Unchanged(_)), + matches!(fused_fill(crate::codec::built_view(&bits), &p), FillOutcome::Unchanged(_)), "fill moved a tree it had already filled: {} with {}", v, p ); } @@ -1588,7 +1600,7 @@ proptest! { cur.tick(&p); for _ in 0..4 { prop_assert!( - matches!(fused_fill(&encode(&cur), &p), FillOutcome::Unchanged(_)), + matches!(fused_fill(crate::codec::built_view(&encode(&cur)), &p), FillOutcome::Unchanged(_)), "a grow re-opened the fill branch: {} with {}", v, p ); cur.tick(&p); @@ -1668,7 +1680,10 @@ fn ticks_covers_fill_changed_branch() { let v = generators::shape_version(generators::Shape::Bushy, 5); let p = Party::seed(); // the full owner of everything assert!( - matches!(fused_fill(&encode(&v), &p), FillOutcome::Changed(_)), + matches!( + fused_fill(crate::codec::built_view(&encode(&v)), &p), + FillOutcome::Changed(_) + ), "witness must take the fill branch" ); check_ticks_equivalence(&v, &p, &[0, 1, 2, 3, 7, 64, 1000]); diff --git a/crates/before/src/version/skyline/grow.rs b/crates/before/src/version/skyline/grow.rs index eefc2880..fb45d8e4 100644 --- a/crates/before/src/version/skyline/grow.rs +++ b/crates/before/src/version/skyline/grow.rs @@ -90,7 +90,7 @@ use core::ops::Range; -use crate::codec::{self, Base, BitCursor, BitsMut, BitsSlice, Code}; +use crate::codec::{self, Base, BitCursor, BitsMut, BitsView, Code}; use super::build::SkylineBuilder; use super::signed::{gamma_code, gamma_code_signed, unzigzag_base, zigzag_signed, Sign}; @@ -196,19 +196,22 @@ impl Route { /// Record that the cheapest inflation at the branch keyed by `key` /// descends into the left child (`left = true`). - pub(super) fn record(&mut self, key: usize, left: bool) { - self.dirs.set(key, left); + /// + /// Keys are id bit positions, bounded by the direction table's own + /// length, far below `usize` on every target. + pub(super) fn record(&mut self, key: u64, left: bool) { + self.dirs.set(key as usize, left); } /// Whether the cheapest inflation at the branch keyed by `key` /// descends into the left child. - fn descends_left(&self, key: usize) -> bool { - self.dirs[key] + fn descends_left(&self, key: u64) -> bool { + self.dirs[key as usize] } /// The raw direction bits, for the route differential. #[cfg(test)] - pub(super) fn dirs(&self) -> &BitsSlice { + pub(super) fn dirs(&self) -> &BitsMut { &self.dirs } } @@ -222,13 +225,13 @@ impl Route { /// because the walk interleaves with the id stream one node at a time — there /// is no run to batch. struct EvScan<'a> { - bits: &'a BitsSlice, + bits: BitsView<'a>, cursor: codec::DsiCursor<'a>, } impl<'a> EvScan<'a> { /// A cursor at the stream's root. - fn new(bits: &'a BitsSlice) -> Self { + fn new(bits: BitsView<'a>) -> Self { EvScan { bits, cursor: codec::DsiCursor::new(bits), @@ -236,13 +239,13 @@ impl<'a> EvScan<'a> { } /// The cursor's bit position: the next node's flag. - fn pos(&self) -> usize { - self.cursor.position() + fn pos(&self) -> u64 { + self.cursor.position_u64() } /// Move the cursor to `pos` (a node-flag position located by a side scan): /// `O(1)`, nothing is read or recorded. - fn seek(&mut self, pos: usize) { + fn seek(&mut self, pos: u64) { self.cursor = codec::DsiCursor::new_at(self.bits, pos); } @@ -253,14 +256,14 @@ impl<'a> EvScan<'a> { /// # Panics /// /// Panics if the stream is not a canonical skyline encoding. - fn read(&mut self) -> Option> { + fn read(&mut self) -> Option> { let leaf = self.cursor.read_bit().expect("canonical skyline bits"); if !leaf { None } else { - let start = self.cursor.position(); + let start = self.cursor.position_u64(); self.cursor.skip_int().expect("canonical skyline bits"); - Some(start..self.cursor.position()) + Some(start..self.cursor.position_u64()) } } @@ -288,16 +291,16 @@ impl<'a> EvScan<'a> { /// /// Neither present is the full `1` terminal; a canonical id has no `(0, 0)` /// node. `O(1)` random access into the packed id. -fn id_tag(bits: &BitsSlice, pos: usize) -> (bool, bool) { +fn id_tag(bits: BitsView<'_>, pos: u64) -> (bool, bool) { codec::scan::record_bits(2); - (bits[pos], bits[pos + 1]) + (bits.bit(pos), bits.bit(pos + 1)) } /// Position just past the id subtree whose tag sits at `pos`. -fn id_skip(bits: &BitsSlice, pos: usize) -> usize { +fn id_skip(bits: BitsView<'_>, pos: u64) -> u64 { crate::idbits::skip_subtree(pos, |at| { codec::scan::record_bits(2); - let children = usize::from(bits[at]) + usize::from(bits[at + 1]); + let children = u64::from(bits.bit(at)) + u64::from(bits.bit(at + 1)); (children, at + 2) }) } @@ -315,14 +318,14 @@ enum Repair<'a> { /// One complete off-path subtree, located by a forward topology scan. struct Subtree { /// Just past the subtree's last bit. - end: usize, + end: u64, /// The first (leftmost) leaf's payload code range. - first_code: Range, + first_code: Range, /// The first leaf's depth below the subtree root; `0` means the /// subtree is a single leaf. first_rel_depth: usize, /// The last (rightmost) leaf's payload code range. - last_code: Range, + last_code: Range, /// The last leaf's depth below the subtree root. last_rel_depth: usize, } @@ -336,18 +339,18 @@ struct Subtree { /// # Panics /// /// Panics if the stream is not a canonical skyline encoding. -fn scan_subtree(bits: &BitsSlice, start: usize) -> Subtree { +fn scan_subtree(bits: BitsView<'_>, start: u64) -> Subtree { let mut cursor = codec::DsiCursor::new_at(bits, start); // The first leaf's coordinates, recorded once; the last leaf's are whatever // the loop recorded most recently when the walk ends. - let mut first: Option<(Range, usize)> = None; + let mut first: Option<(Range, usize)> = None; let mut last_code = 0..0; let mut last_rel_depth = 0; let mut walk = LeafWalk::new(); while let Some(depth) = walk.descend(&mut cursor) { - let code_start = cursor.position(); + let code_start = cursor.position_u64(); cursor.skip_int().expect("canonical skyline bits"); - last_code = code_start..cursor.position(); + last_code = code_start..cursor.position_u64(); last_rel_depth = depth; if first.is_none() { first = Some((last_code.clone(), last_rel_depth)); @@ -355,7 +358,7 @@ fn scan_subtree(bits: &BitsSlice, start: usize) -> Subtree { } let (first_code, first_rel_depth) = first.expect("a subtree has at least one leaf"); Subtree { - end: cursor.position(), + end: cursor.position_u64(), first_code, first_rel_depth, last_code, @@ -376,21 +379,29 @@ fn feed_subtree( repair: Repair<'_>, ) { let subtree = scan_subtree(event.bits, event.pos()); - let original = &event.bits[subtree.first_code.clone()]; let first_code = match repair { - Repair::None => Code::from_slice(original), + Repair::None => { + Code::from_range(event.bits, subtree.first_code.start, subtree.first_code.end) + } // The successor is never the stream's first leaf (the grown leaf // precedes it), so its code is always a zigzag delta. - Repair::Minus(events) => recode(original, Step::DownDelta, events), + Repair::Minus(events) => recode( + event.bits, + subtree.first_code.clone(), + Step::DownDelta, + events, + ), }; out.leaf(depth + subtree.first_rel_depth, first_code); if subtree.first_rel_depth > 0 { out.continue_verbatim( - &event.bits[subtree.first_code.end..subtree.end], + event.bits, + subtree.first_code.end, + subtree.end, depth, subtree.first_rel_depth, subtree.last_rel_depth, - subtree.last_code.len(), + (subtree.last_code.end - subtree.last_code.start) as usize, ); } event.seek(subtree.end); @@ -419,9 +430,9 @@ enum Step { /// /// One decode, one signed step, one re-encode — `O(the code's own width + the /// width of events)`, the only payload arithmetic in the whole emit. -fn recode(code: &BitsSlice, step: Step, events: &Base) -> Code { - let (value, end) = codec::decode_int(code, 0).expect("canonical skyline bits"); - debug_assert_eq!(end, code.len(), "a payload range is exactly one code"); +fn recode(bits: BitsView<'_>, code: Range, step: Step, events: &Base) -> Code { + let (value, end) = codec::decode_int(bits, code.start).expect("canonical skyline bits"); + debug_assert_eq!(end, code.end, "a payload range is exactly one code"); let increment = match step { Step::UpAbsolute => return gamma_code(&(value + events)), Step::UpDelta => true, @@ -485,8 +496,8 @@ fn recode(code: &BitsSlice, step: Step, events: &Base) -> Code { /// region and `events` must be at least 1; the result otherwise is unspecified /// in release builds (debug builds panic). pub(super) fn emit( - event_bits: &BitsSlice, - id_bits: &BitsSlice, + event_bits: BitsView<'_>, + id_bits: BitsView<'_>, route: &Route, events: &Base, ) -> BitsMut { @@ -501,10 +512,10 @@ pub(super) fn emit( "the splice registers at least one event" ); let mut event = EvScan::new(event_bits); - let mut id_pos = 0usize; + let mut id_pos = 0u64; // Subadditivity of the coding bounds the output by the input plus the // expansion chain's fresh codes, each a few bits per id level. - let mut out = SkylineBuilder::with_capacity(event_bits.len() + id_bits.len() + 64); + let mut out = SkylineBuilder::with_capacity((event_bits.len() + id_bits.len()) as usize + 64); // One bit per chosen-path level: `true` = the branch descended left, so its // right sibling subtree is pending after the inflation point. let mut pending = BitsMut::new(); @@ -607,7 +618,7 @@ pub(super) fn emit( // leaf itself comes first. let path_depth = depth; let chain = chain_dirs.len(); - let original = &event_bits[original_range]; + let original = original_range; debug_assert_eq!( path_depth, pending.len(), @@ -625,7 +636,7 @@ pub(super) fn emit( // height, same predecessor — or the same absolute, when the // chain opens the stream (nothing fed before it means the // original code was the absolute first, and so is this one). - Code::from_slice(original) + Code::from_range(event_bits, original.start, original.end) }; out.leaf(path_depth + level + 1, code); emitted_in_chain = true; @@ -641,7 +652,7 @@ pub(super) fn emit( } else { Step::UpAbsolute }; - recode(original, step, events) + recode(event_bits, original.clone(), step, events) }; out.leaf(path_depth + chain, grown_code); // Fresh sibling leaves that follow the grown leaf, deepest first. diff --git a/crates/before/src/version/skyline/grow/tests.rs b/crates/before/src/version/skyline/grow/tests.rs index 7f4c443f..703e6003 100644 --- a/crates/before/src/version/skyline/grow/tests.rs +++ b/crates/before/src/version/skyline/grow/tests.rs @@ -22,7 +22,7 @@ use proptest::prelude::*; use rayon::prelude::*; use crate::codec::BitsMut; -use crate::codec::BitsSlice; +use crate::codec::BitsView; use crate::meter::registry::Shape; use crate::meter::Packed; use crate::recurse::descend; @@ -81,19 +81,19 @@ fn assert_grow(v: &Version, p: &Party) -> bool { /// this directly and takes its value witnesses from closed forms instead. fn assert_grow_depth_safe(v: &Version, p: &Party) -> Option { let enc = encode(v); - match fused_fill(&enc, p) { + match fused_fill(crate::codec::built_view(&enc), p) { // fill moved the tree: the splice is unreachable for this pair. FillOutcome::Changed(_) => None, FillOutcome::Unchanged(route) => { - let (reference, _) = reference_probe(enc.as_bitslice(), p.as_bits()); + let (reference, _) = reference_probe(crate::codec::built_view(&enc), p.as_bits()); assert_eq!( route.dirs(), reference.dirs(), "the fused walk's route must match the recursive reference bit for bit: \ {v} with {p}" ); - let out = tick(&enc, p); - validate(&out).expect("a grown stream is canonical"); + let out = tick(crate::codec::built_view(&enc), p); + validate(crate::codec::built_view(&out)).expect("a grown stream is canonical"); Some(out) } } @@ -113,10 +113,10 @@ enum RefId { /// Probe the cheapest inflation by direct recursion over the `(id, ev)` shape — /// the transliteration of the recursive walk whose route fold the fused tick /// walk carries, kept as its structural witness. -fn reference_probe(ev_bits: &BitsSlice, id_bits: &BitsSlice) -> (Route, Cost) { - let mut route = Route::new(id_bits.len()); +fn reference_probe(ev_bits: BitsView<'_>, id_bits: BitsView<'_>) -> (Route, Cost) { + let mut route = Route::new(id_bits.len() as usize); let mut ev = EvScan::new(ev_bits); - let mut id_pos = 0usize; + let mut id_pos = 0u64; let root = if id_bits.is_empty() { RefId::Empty } else { @@ -135,8 +135,8 @@ fn reference_probe(ev_bits: &BitsSlice, id_bits: &BitsSlice) -> (Route, Cost) { fn rec( route: &mut Route, ev: &mut EvScan<'_>, - id_bits: &BitsSlice, - id_pos: &mut usize, + id_bits: BitsView<'_>, + id_pos: &mut u64, id: RefId, ev_zero: bool, depth: usize, @@ -189,7 +189,7 @@ fn rec( /// /// Component steps included ([`Cost::deepen`]: infeasibility propagates, /// feasible components saturate strictly below the infeasible sentinel). -fn combine(route: &mut Route, expand: bool, key: usize, left: Cost, right: Cost) -> Cost { +fn combine(route: &mut Route, expand: bool, key: u64, left: Cost, right: Cost) -> Cost { let left_chosen = left < right; route.record(key, left_chosen); let m = if left_chosen { left } else { right }; @@ -368,7 +368,7 @@ fn worked_examples_grow_exactly() { let v: Version = before.parse().expect("test version literals parse"); let expected: Version = after.parse().expect("test version literals parse"); assert_eq!( - tick(&encode(&v), &p), + tick(crate::codec::built_view(&encode(&v)), &p), encode(&expected), "grow of {before} with {party} must yield {after}" ); @@ -452,7 +452,7 @@ proptest! { let (best, _) = best_inflation(&op, &ov).expect("an owning id always inflates"); let minimal = from_oracle_version(&best.normalized_for_test()); prop_assert_eq!( - tick(&encode(&v), &p), + tick(crate::codec::built_view(&encode(&v)), &p), encode(&minimal), "grow must register the brute-force minimal inflation: {} with {}", v, p ); diff --git a/crates/before/src/version/skyline/literal.rs b/crates/before/src/version/skyline/literal.rs index 70cb7f35..27432621 100644 --- a/crates/before/src/version/skyline/literal.rs +++ b/crates/before/src/version/skyline/literal.rs @@ -9,7 +9,7 @@ //! its inner node hoards a liftable minimum, so it is not the normal spelling //! and is refused. -use crate::codec::{self, Base, BitCursor, BitsMut, BitsSlice, DsiCursor}; +use crate::codec::{self, Base, BitCursor, BitsMut, BitsView, DsiCursor}; use crate::error::Parse; use super::signed::{unzigzag_base, zigzag}; @@ -29,7 +29,7 @@ pub(crate) fn leaf(base: u64) -> BitsMut { /// children of equal height, which is just the leaf itself) or when the node /// hoards a liftable minimum (neither child's minimum leaf height is zero — /// normal form stores the shared minimum at the parent). -pub(crate) fn node(base: u64, left: &BitsSlice, right: &BitsSlice) -> Result { +pub(crate) fn node(base: u64, left: BitsView<'_>, right: BitsView<'_>) -> Result { let (left_topology, left_heights) = scan(left); let (right_topology, right_heights) = scan(right); @@ -78,7 +78,7 @@ pub(crate) fn node(base: u64, left: &BitsSlice, right: &BitsSlice) -> Result (BitsMut, Vec) { +fn scan(bits: BitsView<'_>) -> (BitsMut, Vec) { let mut cursor = DsiCursor::new(bits); let mut topology = BitsMut::new(); let mut heights: Vec = Vec::new(); diff --git a/crates/before/src/version/skyline/masked.rs b/crates/before/src/version/skyline/masked.rs index 8daeebf0..79f13666 100644 --- a/crates/before/src/version/skyline/masked.rs +++ b/crates/before/src/version/skyline/masked.rs @@ -79,7 +79,7 @@ use core::ops::ControlFlow; use suanpan::Accumulator; -use crate::codec::BitsSlice; +use crate::codec::BitsView; use super::overlay::{ advance_set, fold, CursorSet, IdLeafCursor, LeafCursor, OpenedPair, PlateauCursor, Side, @@ -102,10 +102,10 @@ use super::sweep::{eq_exit, order_exit, Directions}; /// delta driving the running height negative) sweep silently, and the verdict /// is then unspecified. pub fn causal_cmp( - a: &BitsSlice, - a_mask: Option<&BitsSlice>, - b: &BitsSlice, - b_mask: Option<&BitsSlice>, + a: BitsView<'_>, + a_mask: Option>, + b: BitsView<'_>, + b_mask: Option>, ) -> Option { // At exhaustion every surviving combination is a verdict // ([`Directions::relation`]'s map). @@ -125,10 +125,10 @@ pub fn causal_cmp( /// [`causal_cmp`]'s contract exactly: canonical operands required, structural /// violations panic, the rest yield an unspecified verdict. pub fn eq( - a: &BitsSlice, - a_mask: Option<&BitsSlice>, - b: &BitsSlice, - b_mask: Option<&BitsSlice>, + a: BitsView<'_>, + a_mask: Option>, + b: BitsView<'_>, + b_mask: Option>, ) -> bool { // Surviving to exhaustion is equality: `eq_exit` breaks on any refutation // before the exhaustion check runs, so reaching the finish arm IS the @@ -180,10 +180,10 @@ impl<'a> Walk<'a> { /// Open every operand stream at its first leaf or region and seed the /// integrators with the two absolute first heights. fn open( - a_bits: &'a BitsSlice, - a_mask: Option<&'a BitsSlice>, - b_bits: &'a BitsSlice, - b_mask: Option<&'a BitsSlice>, + a_bits: BitsView<'a>, + a_mask: Option>, + b_bits: BitsView<'a>, + b_mask: Option>, ) -> Walk<'a> { let OpenedPair { a, diff --git a/crates/before/src/version/skyline/masked/tests.rs b/crates/before/src/version/skyline/masked/tests.rs index c7fe6bb9..34ec808b 100644 --- a/crates/before/src/version/skyline/masked/tests.rs +++ b/crates/before/src/version/skyline/masked/tests.rs @@ -32,18 +32,41 @@ fn collapsible_sibling_pair_sweeps_without_panicking() { bad.push(true); // right leaf codec::encode_int(&mut bad, &Base::from(0u64)); // zigzag(0): equal sibling assert!( - matches!(validate_bits(&bad), Err(Decode::NotCanonical)), + matches!( + validate_bits(crate::codec::built_view(&bad)), + Err(Decode::NotCanonical) + ), "the witness must sit outside the contract's canonical-operand precondition" ); // The canonical spelling of the same step function: the single leaf 5. let mut good = BitsMut::new(); good.push(true); codec::encode_int(&mut good, &Base::from(5u64)); - validate_bits(&good).expect("the peer operand is canonical"); + validate_bits(crate::codec::built_view(&good)).expect("the peer operand is canonical"); // Both entry points, both operand positions: each call must return. The // verdicts are unspecified and deliberately unpinned. - let _ = causal_cmp(&bad, None, &good, None); - let _ = causal_cmp(&good, None, &bad, None); - let _ = eq(&bad, None, &good, None); - let _ = eq(&good, None, &bad, None); + let _ = causal_cmp( + crate::codec::built_view(&bad), + None, + crate::codec::built_view(&good), + None, + ); + let _ = causal_cmp( + crate::codec::built_view(&good), + None, + crate::codec::built_view(&bad), + None, + ); + let _ = eq( + crate::codec::built_view(&bad), + None, + crate::codec::built_view(&good), + None, + ); + let _ = eq( + crate::codec::built_view(&good), + None, + crate::codec::built_view(&bad), + None, + ); } diff --git a/crates/before/src/version/skyline/overlay.rs b/crates/before/src/version/skyline/overlay.rs index d8b6cd7a..f90f0e75 100644 --- a/crates/before/src/version/skyline/overlay.rs +++ b/crates/before/src/version/skyline/overlay.rs @@ -89,7 +89,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; -use crate::codec::{BitCursor, BitStack, BitsSlice, DsiCursor, Int, SliceCursor}; +use crate::codec::{BitCursor, BitStack, BitsView, DsiCursor, Int, SliceCursor}; use super::signed::Sign; @@ -316,7 +316,7 @@ pub(super) struct LeafCursor<'a> { path: BitStack, /// The stream's live bit length; the cursor reaching it is /// exhaustion (the current leaf is the stream's last). - len: usize, + len: u64, } impl<'a> LeafCursor<'a> { @@ -330,7 +330,7 @@ impl<'a> LeafCursor<'a> { /// notices — truncation, malformation — panic; the rest walk silently with /// an unspecified result (the contract of /// [`causal_cmp`](super::sweep::causal_cmp), stated once there). - pub(super) fn open(bits: &'a BitsSlice) -> (Self, Int) { + pub(super) fn open(bits: BitsView<'a>) -> (Self, Int) { let mut this = LeafCursor { cursor: DsiCursor::new(bits), path: BitStack::new(), @@ -340,21 +340,6 @@ impl<'a> LeafCursor<'a> { (this, first) } - /// [`open`](Self::open) over raw stream bytes and a live bit length. - /// - /// The byte decode door's form, for canonical streams past the borrowed - /// bit view's encoding cap (64 MiB and up on a 32-bit target). Same - /// contract, canonicality trust included. - pub(super) fn open_bytes(bytes: &'a [u8], live: usize) -> (Self, Int) { - let mut this = LeafCursor { - cursor: DsiCursor::over_bytes_live(bytes, live), - path: BitStack::new(), - len: live, - }; - let first = this.descend(); - (this, first) - } - /// The flip level the next step would close to, read without moving. /// /// The path's trailing right-branch run popped and the deepest left branch @@ -430,7 +415,7 @@ impl PlateauCursor for LeafCursor<'_> { /// Whether the current leaf is the stream's last (its plateau ends at the /// unit interval's right edge). fn done(&self) -> bool { - self.cursor.position() == self.len + self.cursor.position_u64() == self.len } /// Advance past the current leaf to the next: the flip level's depth for @@ -507,7 +492,7 @@ impl<'a> IdLeafCursor<'a> { /// walked tree) walk silently with unspecified ownership readings (the /// mask-operand contract of [`causal_cmp`](super::masked::causal_cmp), /// stated once there). - pub(super) fn open(bits: &'a BitsSlice) -> Self { + pub(super) fn open(bits: BitsView<'a>) -> Self { let mut this = IdLeafCursor { cursor: SliceCursor::new(bits, 0), path: BitStack::new(), @@ -705,7 +690,7 @@ impl<'a> OpenedPair<'a> { /// # Panics /// /// Panics if either stream is not a canonical skyline encoding. - pub(super) fn open(a_bits: &'a BitsSlice, b_bits: &'a BitsSlice) -> OpenedPair<'a> { + pub(super) fn open(a_bits: BitsView<'a>, b_bits: BitsView<'a>) -> OpenedPair<'a> { let (a, a_first) = LeafCursor::open(a_bits); let (b, b_first) = LeafCursor::open(b_bits); let mut diff = Accumulator::new(); diff --git a/crates/before/src/version/skyline/place.rs b/crates/before/src/version/skyline/place.rs index 1e5809fa..4a9bd1ee 100644 --- a/crates/before/src/version/skyline/place.rs +++ b/crates/before/src/version/skyline/place.rs @@ -113,7 +113,7 @@ use core::ops::ControlFlow; use suanpan::Accumulator; -use crate::codec::{BitsSlice, Int}; +use crate::codec::{BitsView, Int}; use crate::span::{Dominance, Endpoint, Placement, Precedence}; use super::overlay::{advance_set, fold, CursorSet, LeafCursor, PlateauCursor, Side}; @@ -147,7 +147,7 @@ impl<'a> BoundSide<'a> { /// # Panics /// /// Panics if the stream is not a canonical skyline encoding. - fn open(bits: &'a BitsSlice, probe_first: &Int) -> BoundSide<'a> { + fn open(bits: BitsView<'a>, probe_first: &Int) -> BoundSide<'a> { let (cursor, first) = LeafCursor::open(bits); let mut diff = Accumulator::new(); super::signed::fold_signed_int(&mut diff, Sign::Positive, probe_first); @@ -188,7 +188,7 @@ impl<'a> BoundSide<'a> { /// /// The canonical-stream contract of [`causal_cmp`](super::sweep::causal_cmp), /// on all three operands. -pub(crate) fn span(probe: &BitsSlice, lo: &BitsSlice, hi: &BitsSlice) -> Placement { +pub(crate) fn span(probe: BitsView<'_>, lo: BitsView<'_>, hi: BitsView<'_>) -> Placement { /// Either endpoint's decided concurrency drops its own cursor while the /// other still sweeps. /// @@ -250,7 +250,7 @@ pub(crate) fn span(probe: &BitsSlice, lo: &BitsSlice, hi: &BitsSlice) -> Placeme /// /// The canonical-stream contract of [`causal_cmp`](super::sweep::causal_cmp), /// on all three operands. -pub(crate) fn dominance(probe: &BitsSlice, lo: &BitsSlice, hi: &BitsSlice) -> Dominance { +pub(crate) fn dominance(probe: BitsView<'_>, lo: BitsView<'_>, hi: BitsView<'_>) -> Dominance { walk( probe, lo, @@ -313,7 +313,7 @@ pub(crate) fn dominance(probe: &BitsSlice, lo: &BitsSlice, hi: &BitsSlice) -> Do /// /// The canonical-stream contract of [`causal_cmp`](super::sweep::causal_cmp), /// on all three operands. -pub(crate) fn precedence(probe: &BitsSlice, lo: &BitsSlice, hi: &BitsSlice) -> Precedence { +pub(crate) fn precedence(probe: BitsView<'_>, lo: BitsView<'_>, hi: BitsView<'_>) -> Precedence { walk( probe, lo, @@ -377,7 +377,7 @@ pub(crate) fn precedence(probe: &BitsSlice, lo: &BitsSlice, hi: &BitsSlice) -> P /// /// The canonical-stream contract of [`causal_cmp`](super::sweep::causal_cmp), /// on all three operands. -pub(crate) fn contains(probe: &BitsSlice, lo: &BitsSlice, hi: &BitsSlice) -> bool { +pub(crate) fn contains(probe: BitsView<'_>, lo: BitsView<'_>, hi: BitsView<'_>) -> bool { walk( probe, lo, @@ -441,9 +441,9 @@ pub(crate) fn contains(probe: &BitsSlice, lo: &BitsSlice, hi: &BitsSlice) -> boo /// flattened `None` and the decided relation give the same answer. Each entry /// point carries the per-verdict argument at its closures. fn walk( - probe: &BitsSlice, - start: &BitsSlice, - end: &BitsSlice, + probe: BitsView<'_>, + start: BitsView<'_>, + end: BitsView<'_>, on_start: impl Fn(Directions, bool) -> ControlFlow, on_end: impl Fn(Directions, bool) -> ControlFlow, finish: impl FnOnce(Option>, Option>) -> V, diff --git a/crates/before/src/version/skyline/place/filter.rs b/crates/before/src/version/skyline/place/filter.rs index 1754c0a4..3c5a3e57 100644 --- a/crates/before/src/version/skyline/place/filter.rs +++ b/crates/before/src/version/skyline/place/filter.rs @@ -52,7 +52,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; use crate::causally::Coverage; -use crate::codec::{BitsSlice, Int}; +use crate::codec::{BitsView, Int}; use super::super::overlay::{advance_set, fold, CursorSet, LeafCursor, PlateauCursor, Side}; use super::super::signed::Sign; @@ -159,8 +159,8 @@ struct BoundSide<'a> { /// exactly: the violations the walk structurally notices panic, the rest sweep /// silently with an unspecified verdict. pub(crate) fn admits<'a>( - probe: &'a BitsSlice, - bounds: impl IntoIterator, + probe: BitsView<'a>, + bounds: impl IntoIterator, Demand)>, ) -> bool { let mut bounds = bounds.into_iter().peekable(); if bounds.peek().is_none() { @@ -322,9 +322,9 @@ struct SpanSide<'a> { /// /// The canonical-stream contract of [`admits`], on all operands. pub(crate) fn coverage<'a>( - lo: &'a BitsSlice, - hi: &'a BitsSlice, - bounds: impl IntoIterator, + lo: BitsView<'a>, + hi: BitsView<'a>, + bounds: impl IntoIterator, Demand)>, ) -> Coverage { let mut bounds = bounds.into_iter().peekable(); if bounds.peek().is_none() { diff --git a/crates/before/src/version/skyline/place/tests.rs b/crates/before/src/version/skyline/place/tests.rs index cebe59a2..c6326fbb 100644 --- a/crates/before/src/version/skyline/place/tests.rs +++ b/crates/before/src/version/skyline/place/tests.rs @@ -11,8 +11,8 @@ use crate::{Clock, Version}; /// The composed two-sweep spelling of the span mode: the nine-state verdict /// transcribed from the raw relations. fn composed_span(probe: &Version, lo: &Version, hi: &Version) -> Placement { - let lo_rel = sweep::causal_cmp(probe.view(), lo.view()); - let hi_rel = sweep::causal_cmp(probe.view(), hi.view()); + let lo_rel = sweep::causal_cmp((probe.view()).live(), (lo.view()).live()); + let hi_rel = sweep::causal_cmp((probe.view()).live(), (hi.view()).live()); match lo_rel { Some(Ordering::Less) => Placement::Before, Some(Ordering::Equal) => match hi_rel { @@ -72,7 +72,7 @@ fn composed_contains(probe: &Version, lo: &Version, hi: &Version) -> bool { /// The composed pairwise spelling of one demand's verdict: the filter walks' /// stream-level oracle, per bound. fn demand_admits(probe: &Version, bound: &Version, demand: Demand) -> bool { - let rel = sweep::causal_cmp(probe.view(), bound.view()); + let rel = sweep::causal_cmp((probe.view()).live(), (bound.view()).live()); let le = matches!(rel, Some(Ordering::Less | Ordering::Equal)); let lt = rel == Some(Ordering::Less); let ge = matches!(rel, Some(Ordering::Greater | Ordering::Equal)); @@ -95,18 +95,22 @@ fn composed_coverage(lo: &Version, hi: &Version, bounds: &[(&Version, Demand)]) for &(bound, demand) in bounds { let le = |p: &Version| { matches!( - sweep::causal_cmp(p.view(), bound.view()), + sweep::causal_cmp((p.view()).live(), (bound.view()).live()), Some(Ordering::Less | Ordering::Equal) ) }; - let lt = |p: &Version| sweep::causal_cmp(p.view(), bound.view()) == Some(Ordering::Less); + let lt = |p: &Version| { + sweep::causal_cmp((p.view()).live(), (bound.view()).live()) == Some(Ordering::Less) + }; let ge = |p: &Version| { matches!( - sweep::causal_cmp(p.view(), bound.view()), + sweep::causal_cmp((p.view()).live(), (bound.view()).live()), Some(Ordering::Greater | Ordering::Equal) ) }; - let gt = |p: &Version| sweep::causal_cmp(p.view(), bound.view()) == Some(Ordering::Greater); + let gt = |p: &Version| { + sweep::causal_cmp((p.view()).live(), (bound.view()).live()) == Some(Ordering::Greater) + }; let (empties, admits_all) = match demand { Demand::After => (!ge(hi), ge(lo)), Demand::Before => (!le(lo), le(hi)), @@ -159,10 +163,10 @@ fn demand_lists<'a>(b: &'a Version, c: &'a Version) -> Vec(bounds: &[(&'a Version, Demand)]) -> Vec<(&'a BitsSlice, Demand)> { +fn streams<'a>(bounds: &[(&'a Version, Demand)]) -> Vec<(BitsView<'a>, Demand)> { bounds .iter() - .map(|&(bound, demand)| (&**bound.view(), demand)) + .map(|&(bound, demand)| (bound.view().live(), demand)) .collect() } @@ -179,8 +183,13 @@ fn span_walk_places_organic_witnesses() { let b1 = bob.tick().clone(); let joined = &a2 | &b1; - let placed = - |probe: &Version, lo: &Version, hi: &Version| span(probe.view(), lo.view(), hi.view()); + let placed = |probe: &Version, lo: &Version, hi: &Version| { + span( + (probe.view()).live(), + (lo.view()).live(), + (hi.view()).live(), + ) + }; // The chain verdicts. assert_eq!(placed(&Version::new(), &a1, &a3), Placement::Before); assert_eq!(placed(&a1, &a1, &a3), Placement::At(Endpoint::Start)); @@ -222,7 +231,11 @@ fn precedence_walk_verdicts_organic_witnesses() { let joined = &a2 | &b1; let preceded = |probe: &Version, lo: &Version, hi: &Version| { - precedence(probe.view(), lo.view(), hi.view()) + precedence( + (probe.view()).live(), + (lo.view()).live(), + (hi.view()).live(), + ) }; // Exhaustion confirmations: the whole span preceded, at and below the // start. @@ -254,8 +267,13 @@ fn contains_walk_verdicts_organic_witnesses() { let a3 = alice.tick().clone(); let b1 = bob.tick().clone(); - let within = - |probe: &Version, lo: &Version, hi: &Version| contains(probe.view(), lo.view(), hi.view()); + let within = |probe: &Version, lo: &Version, hi: &Version| { + contains( + (probe.view()).live(), + (lo.view()).live(), + (hi.view()).live(), + ) + }; // Exhaustion confirmations: both endpoints and the interior. assert!(within(&a1, &a1, &a3)); assert!(within(&a2, &a1, &a3)); @@ -287,7 +305,7 @@ fn filter_admits_organic_witnesses() { let b1 = bob.tick().clone(); let admits = |probe: &Version, bounds: &[(&Version, Demand)]| { - filter::admits(probe.view(), streams(bounds)) + filter::admits((probe.view()).live(), streams(bounds)) }; // The required-direction bail: a floor above the probe. assert!(!admits(&a1, &[(&a2, Demand::After)])); @@ -322,7 +340,7 @@ fn filter_coverage_organic_witnesses() { let b1 = bob.tick().clone(); let coverage = |lo: &Version, hi: &Version, bounds: &[(&Version, Demand)]| { - filter::coverage(lo.view(), hi.view(), streams(bounds)) + filter::coverage((lo.view()).live(), (hi.view()).live(), streams(bounds)) }; // A floor refuting `floor <= hi`: the early Empty, whether above // or concurrent to the whole segment. @@ -392,22 +410,22 @@ proptest! { for (lo, hi) in pairs { for probe in [&a, &b, &c, &meet, &join] { prop_assert_eq!( - span(probe.view(), lo.view(), hi.view()), + span((probe.view()).live(), (lo.view()).live(), (hi.view()).live()), composed_span(probe, lo, hi), "fused span walk vs composed sweeps", ); prop_assert_eq!( - dominance(probe.view(), lo.view(), hi.view()), + dominance((probe.view()).live(), (lo.view()).live(), (hi.view()).live()), composed_dominance(probe, lo, hi), "fused dominance walk vs composed coarsening", ); prop_assert_eq!( - precedence(probe.view(), lo.view(), hi.view()), + precedence((probe.view()).live(), (lo.view()).live(), (hi.view()).live()), composed_precedence(probe, lo, hi), "fused precedence walk vs composed coarsening", ); prop_assert_eq!( - contains(probe.view(), lo.view(), hi.view()), + contains((probe.view()).live(), (lo.view()).live(), (hi.view()).live()), composed_contains(probe, lo, hi), "fused membership walk vs composed coarsening", ); @@ -438,7 +456,7 @@ proptest! { .iter() .all(|&(bound, demand)| demand_admits(probe, bound, demand)); prop_assert_eq!( - filter::admits(probe.view(), streams(&bounds)), + filter::admits((probe.view()).live(), streams(&bounds)), composed, "fused membership walk vs composed sweeps over {:?}", bounds, @@ -478,7 +496,7 @@ proptest! { for (lo, hi) in segments { for bounds in demand_lists(&b, &c) { prop_assert_eq!( - filter::coverage(lo.view(), hi.view(), streams(&bounds)), + filter::coverage((lo.view()).live(), (hi.view()).live(), streams(&bounds)), composed_coverage(lo, hi, &bounds), "fused coverage walk vs composed fold over {:?}", bounds, diff --git a/crates/before/src/version/skyline/query.rs b/crates/before/src/version/skyline/query.rs index 2f832db1..3f964ac7 100644 --- a/crates/before/src/version/skyline/query.rs +++ b/crates/before/src/version/skyline/query.rs @@ -174,7 +174,7 @@ use core::cmp::Ordering; use suanpan::{Accumulator, UBig}; -use crate::codec::{self, Base, BitsMut, BitsSlice, Int}; +use crate::codec::{self, Base, BitsMut, BitsView, Int}; use crate::Rank; use self::integral::{int_digits, Integrator, FREEZE_ALLOWANCE_DIGITS}; @@ -198,7 +198,7 @@ use super::walk::LeafWalk; /// /// Panics if the operand is not a canonical skyline stream — run /// [`validate`](fn@super::validate) first on untrusted bytes. -pub fn rank(bits: &BitsSlice) -> Rank { +pub fn rank(bits: BitsView<'_>) -> Rank { let max_depth = max_depth(bits); // Depth counts levels of a stream held in memory, so it always fits the u64 // rank exponent. @@ -238,7 +238,7 @@ pub fn rank(bits: &BitsSlice) -> Rank { /// # Panics /// /// Panics on a non-canonical operand, exactly as [`rank`](fn@rank) does. -pub fn distance(a: &BitsSlice, b: &BitsSlice) -> Rank { +pub fn distance(a: BitsView<'_>, b: BitsView<'_>) -> Rank { // `∫ |D|`: σ is `sign(D)` itself, so the integrand `σ·D` is `|D|`. pair_integral(a, b, |sign| match sign { Ordering::Greater => 1, @@ -259,7 +259,7 @@ pub fn distance(a: &BitsSlice, b: &BitsSlice) -> Rank { /// /// Panics on a non-canonical operand, exactly as [`rank`](fn@rank) /// does. -pub fn lag(a: &BitsSlice, b: &BitsSlice) -> Rank { +pub fn lag(a: BitsView<'_>, b: BitsView<'_>) -> Rank { // `∫ (−D)⁺`: σ is `−1` exactly where `D < 0`, so the integrand keeps the // history `b` records beyond `a` and nothing else. pair_integral(a, b, |sign| match sign { @@ -283,7 +283,7 @@ pub fn lag(a: &BitsSlice, b: &BitsSlice) -> Rank { /// # Panics /// /// Panics on a non-canonical operand, exactly as [`rank`](fn@rank) does. -pub fn rank_cmp(a: &BitsSlice, b: &BitsSlice) -> Ordering { +pub fn rank_cmp(a: BitsView<'_>, b: BitsView<'_>) -> Ordering { // `∫ D`, signed: σ is constantly `+1`, the total is // `rank(a) − rank(b)`, and only its sign is kept. pair_fold(a, b, |_| 1).0 @@ -292,8 +292,8 @@ pub fn rank_cmp(a: &BitsSlice, b: &BitsSlice) -> Ordering { /// Run the nonnegative pair co-sweep and normalize its raw total into a /// [`Rank`]: the distance/lag entry into [`pair_fold`]. fn pair_integral( - a_bits: &BitsSlice, - b_bits: &BitsSlice, + a_bits: BitsView<'_>, + b_bits: BitsView<'_>, orientation: impl Fn(Ordering) -> i8, ) -> Rank { let (sign, total, scale) = pair_fold(a_bits, b_bits, orientation); @@ -325,8 +325,8 @@ fn pair_integral( /// /// Panics on a non-canonical operand, exactly as [`rank`](fn@rank) does. fn pair_fold( - a_bits: &BitsSlice, - b_bits: &BitsSlice, + a_bits: BitsView<'_>, + b_bits: BitsView<'_>, orientation: impl Fn(Ordering) -> i8, ) -> (Ordering, UBig, u64) { // The overlay's scale: elementary intervals nest inside both operands' @@ -419,7 +419,7 @@ fn pair_fold( /// /// Panics if the operand is not a canonical skyline stream — run /// [`validate`](fn@super::validate) first on untrusted bytes. -pub fn min_ticks(bits: &BitsSlice) -> Base { +pub fn min_ticks(bits: BitsView<'_>) -> Base { let (mut cursor, first) = LeafCursor::open(bits); // The height split: `h = F + L`, with `L` folding every delta and `F` // living entirely in the epoch ledger — one drift per freeze, settled @@ -496,7 +496,7 @@ pub fn min_ticks(bits: &BitsSlice) -> Base { /// # Panics /// /// Panics if the skyline operand is not a canonical stream. -pub fn project(event_bits: &BitsSlice, id: &crate::Party) -> BitsMut { +pub fn project(event_bits: BitsView<'_>, id: &crate::Party) -> BitsMut { let id_bits = id.as_bits(); let (mut event_cursor, first) = LeafCursor::open(event_bits); let mut id_cursor = IdLeafCursor::open(id_bits); @@ -511,7 +511,7 @@ pub fn project(event_bits: &BitsSlice, id: &crate::Party) -> BitsMut { // the allocation benchmark to measure against the shipped pre-size; shipped // builds always take the pre-sized arm. #[cfg(not(before_alloc_ab = "projection_growth"))] - let capacity = event_bits.len() + id_bits.len(); + let capacity = (event_bits.len() + id_bits.len()) as usize; #[cfg(before_alloc_ab = "projection_growth")] let capacity = 0; let mut out = SkylineBuilder::with_capacity(capacity); @@ -633,7 +633,7 @@ fn absolute_height(height: &mut Accumulator) -> Base { /// # Panics /// /// Panics if the stream is not a canonical skyline encoding. -fn max_depth(bits: &BitsSlice) -> usize { +fn max_depth(bits: BitsView<'_>) -> usize { let mut cursor = codec::DsiCursor::new(bits); let mut deepest = 0usize; let mut walk = LeafWalk::new(); diff --git a/crates/before/src/version/skyline/query/tests.rs b/crates/before/src/version/skyline/query/tests.rs index 2a956648..93b65bfb 100644 --- a/crates/before/src/version/skyline/query/tests.rs +++ b/crates/before/src/version/skyline/query/tests.rs @@ -43,12 +43,12 @@ fn assert_single(v: &Version) { let enc = encode(v); let tree = to_oracle_version(v); assert_eq!( - rank(&enc), + rank(crate::codec::built_view(&enc)), tree.rank(), "rank kernel disagrees with the tree-fold oracle: {v}" ); assert_eq!( - crate::Ticks(min_ticks(&enc)), + crate::Ticks(min_ticks(crate::codec::built_view(&enc))), tree.min_ticks(), "min_ticks kernel disagrees with the tree-fold oracle: {v}" ); @@ -60,7 +60,7 @@ fn assert_projection(v: &Version, p: &Party) { let enc = encode(v); let masked = from_oracle_version(&to_oracle_version(v).project(&to_oracle_party(p))); assert_eq!( - project(&enc, p), + project(crate::codec::built_view(&enc), p), encode(&masked), "projection must match the oracle mask: {v} / {p}" ); @@ -90,9 +90,13 @@ fn assert_pair(a: &Version, b: &Version) { let (ea, eb) = (encode(a), encode(b)); let (ta, tb) = (to_oracle_version(a), to_oracle_version(b)); let order = ta.rank().cmp(&tb.rank()); - assert_eq!(rank_cmp(&ea, &eb), order, "rank_cmp: {a} vs {b}"); assert_eq!( - rank_cmp(&eb, &ea), + rank_cmp(crate::codec::built_view(&ea), crate::codec::built_view(&eb)), + order, + "rank_cmp: {a} vs {b}" + ); + assert_eq!( + rank_cmp(crate::codec::built_view(&eb), crate::codec::built_view(&ea)), order.reverse(), "rank_cmp reversed: {b} vs {a}" ); @@ -101,32 +105,54 @@ fn assert_pair(a: &Version, b: &Version) { let dist = join_rank .checked_sub(&meet_rank) .expect("rank is monotone: the meet's rank never exceeds the join's"); - assert_eq!(distance(&ea, &eb), dist, "distance: {a} vs {b}"); - assert_eq!(distance(&eb, &ea), dist, "distance: {b} vs {a}"); + assert_eq!( + distance(crate::codec::built_view(&ea), crate::codec::built_view(&eb)), + dist, + "distance: {a} vs {b}" + ); + assert_eq!( + distance(crate::codec::built_view(&eb), crate::codec::built_view(&ea)), + dist, + "distance: {b} vs {a}" + ); let lag_a = join_rank .checked_sub(&ta.rank()) .expect("rank is monotone: an operand's rank never exceeds the join's"); let lag_b = join_rank .checked_sub(&tb.rank()) .expect("rank is monotone: an operand's rank never exceeds the join's"); - assert_eq!(lag(&ea, &eb), lag_a, "lag: {a} vs {b}"); - assert_eq!(lag(&eb, &ea), lag_b, "lag: {b} vs {a}"); + assert_eq!( + lag(crate::codec::built_view(&ea), crate::codec::built_view(&eb)), + lag_a, + "lag: {a} vs {b}" + ); + assert_eq!( + lag(crate::codec::built_view(&eb), crate::codec::built_view(&ea)), + lag_b, + "lag: {b} vs {a}" + ); // The composed forms, on this crate's own kernels. - let kernel_join = rank(&emit::join(&ea, &eb)); - let kernel_meet = rank(&emit::meet(&ea, &eb)); + let kernel_join = rank(crate::codec::built_view(&emit::join( + crate::codec::built_view(&ea), + crate::codec::built_view(&eb), + ))); + let kernel_meet = rank(crate::codec::built_view(&emit::meet( + crate::codec::built_view(&ea), + crate::codec::built_view(&eb), + ))); let composed_dist = kernel_join .checked_sub(&kernel_meet) .expect("rank is monotone: the meet's rank never exceeds the join's"); assert_eq!( - distance(&ea, &eb), + distance(crate::codec::built_view(&ea), crate::codec::built_view(&eb)), composed_dist, "distance vs the composed rank-of-meet arithmetic: {a} vs {b}" ); let composed_lag_a = kernel_join - .checked_sub(&rank(&ea)) + .checked_sub(&rank(crate::codec::built_view(&ea))) .expect("rank is monotone: an operand's rank never exceeds the join's"); assert_eq!( - lag(&ea, &eb), + lag(crate::codec::built_view(&ea), crate::codec::built_view(&eb)), composed_lag_a, "lag vs the composed rank-of-join arithmetic: {a} vs {b}" ); @@ -401,9 +427,13 @@ fn rank_cmp_agrees_with_the_oracle_in_the_freeze_regime() { let want = to_oracle_version(a) .rank() .cmp(&to_oracle_version(b).rank()); - assert_eq!(rank_cmp(&ea, &eb), want, "rank_cmp: {a} vs {b}"); assert_eq!( - rank_cmp(&eb, &ea), + rank_cmp(crate::codec::built_view(&ea), crate::codec::built_view(&eb)), + want, + "rank_cmp: {a} vs {b}" + ); + assert_eq!( + rank_cmp(crate::codec::built_view(&eb), crate::codec::built_view(&ea)), want.reverse(), "rank_cmp reversed: {a} vs {b}" ); @@ -505,22 +535,32 @@ fn exhaustive_small_scope_pairs_agree() { .map(|t| encode(&from_oracle_version(t))) .collect(); for ea in &events { - let rank_a = rank(ea); + let rank_a = rank(crate::codec::built_view(ea)); for eb in &events { - let join = rank(&emit::join(ea, eb)); - let meet = rank(&emit::meet(ea, eb)); + let join = rank(crate::codec::built_view(&emit::join( + crate::codec::built_view(ea), + crate::codec::built_view(eb), + ))); + let meet = rank(crate::codec::built_view(&emit::meet( + crate::codec::built_view(ea), + crate::codec::built_view(eb), + ))); let composed_dist = join .checked_sub(&meet) .expect("rank is monotone: the meet's rank never exceeds the join's"); assert_eq!( - distance(ea, eb), + distance(crate::codec::built_view(ea), crate::codec::built_view(eb)), composed_dist, "distance at a small-scope pair" ); let composed_lag = join .checked_sub(&rank_a) .expect("rank is monotone: an operand's rank never exceeds the join's"); - assert_eq!(lag(ea, eb), composed_lag, "lag at a small-scope pair"); + assert_eq!( + lag(crate::codec::built_view(ea), crate::codec::built_view(eb)), + composed_lag, + "lag at a small-scope pair" + ); } } } @@ -542,12 +582,12 @@ proptest! { let g = semantic_oracle::ev_res(&ev); prop_assert_eq!( semantic_oracle::rank(&ev, g), - rank(&encode(&a)), + rank(crate::codec::built_view(&encode(&a))), "the Riemann sum disagrees with the rank kernel: {}", a ); prop_assert_eq!( semantic_oracle::min_ticks(&ev, g), - min_ticks(&encode(&a)), + min_ticks(crate::codec::built_view(&encode(&a))), "the semantic tick floor disagrees with the min_ticks kernel: {}", a ); } @@ -650,12 +690,12 @@ proptest! { ); let hits_before = super::integral::FREEZE_HITS.with(|hits| hits.get()); prop_assert_eq!( - rank_cmp(&el, &er), + rank_cmp(crate::codec::built_view(&el), crate::codec::built_view(&er)), core::cmp::Ordering::Equal, "rank_cmp on the mirrored pair: {} vs {}", left, right ); prop_assert_eq!( - rank_cmp(&er, &el), + rank_cmp(crate::codec::built_view(&er), crate::codec::built_view(&el)), core::cmp::Ordering::Equal, "rank_cmp on the mirrored pair reversed: {} vs {}", right, left ); @@ -1358,7 +1398,7 @@ mod adequacy { use suanpan::{touch_meter, Accumulator}; - use crate::codec::{Base, BitsSlice, Int}; + use crate::codec::{Base, BitsView, Int}; use crate::meter::registry::Shape; use crate::version::skyline::encode; use crate::version::skyline::overlay::{fold, LeafCursor, PlateauCursor, Side}; @@ -1379,7 +1419,7 @@ mod adequacy { /// exactly where the tripwire asserts it: freeze `i`'s position read walks /// the accumulator's whole written span, which `FP(k)`'s descending spine /// grows with every block. - fn absolute_position_rank(bits: &BitsSlice) -> Rank { + fn absolute_position_rank(bits: BitsView<'_>) -> Rank { let max_depth = max_depth(bits); let scale = max_depth as u64; let (mut cursor, first) = LeafCursor::open(bits); @@ -1431,7 +1471,7 @@ mod adequacy { let enc = encode(&v); let expected = v.rank(); touch_meter::reset(); - let r = absolute_position_rank(&enc); + let r = absolute_position_rank(crate::codec::built_view(&enc)); let touches = touch_meter::touches(); assert_eq!( r, expected, @@ -1643,7 +1683,7 @@ mod adequacy { /// The rank fold on the span-reading integrator: the shipped /// [`rank`](super::super::rank) loop verbatim, integrator swapped. - fn span_promotion_rank(bits: &BitsSlice) -> Rank { + fn span_promotion_rank(bits: BitsView<'_>) -> Rank { let max_depth = max_depth(bits); let (mut cursor, first) = LeafCursor::open(bits); let mut integral = SpanIntegrator::new(); @@ -1663,7 +1703,7 @@ mod adequacy { /// The distance co-sweep on the span-reading integrator: the shipped pair /// loop verbatim (distance orientation), integrator swapped. - fn span_promotion_distance(a_bits: &BitsSlice, b_bits: &BitsSlice) -> Rank { + fn span_promotion_distance(a_bits: BitsView<'_>, b_bits: BitsView<'_>) -> Rank { let orientation = |sign: Ordering| -> i8 { match sign { Ordering::Greater => 1, @@ -1721,7 +1761,7 @@ mod adequacy { let enc = encode(&v); let expected = v.rank(); touch_meter::reset(); - let r = span_promotion_rank(&enc); + let r = span_promotion_rank(crate::codec::built_view(&enc)); let touches = touch_meter::touches(); assert_eq!( r, expected, @@ -1741,7 +1781,8 @@ mod adequacy { let eb = encode(&b); let expected = a.distance(&b); touch_meter::reset(); - let d = span_promotion_distance(&ea, &eb); + let d = + span_promotion_distance(crate::codec::built_view(&ea), crate::codec::built_view(&eb)); let touches = touch_meter::touches(); assert_eq!( d, expected, @@ -2006,7 +2047,7 @@ mod adequacy { /// The rank fold on the suffix-walk integrator: the shipped /// [`rank`](super::super::rank) loop verbatim, integrator swapped. - fn suffix_walk_rank(bits: &BitsSlice) -> Rank { + fn suffix_walk_rank(bits: BitsView<'_>) -> Rank { let max_depth = max_depth(bits); let (mut cursor, first) = LeafCursor::open(bits); let mut integral = SuffixWalkIntegrator::new(); @@ -2026,7 +2067,7 @@ mod adequacy { /// The distance co-sweep on the suffix-walk integrator: the shipped pair /// loop verbatim (distance orientation), integrator swapped. - fn suffix_walk_distance(a_bits: &BitsSlice, b_bits: &BitsSlice) -> Rank { + fn suffix_walk_distance(a_bits: BitsView<'_>, b_bits: BitsView<'_>) -> Rank { let orientation = |sign: Ordering| -> i8 { match sign { Ordering::Greater => 1, @@ -2084,7 +2125,7 @@ mod adequacy { let enc = encode(&v); let expected = v.rank(); touch_meter::reset(); - let r = suffix_walk_rank(&enc); + let r = suffix_walk_rank(crate::codec::built_view(&enc)); let touches = touch_meter::touches(); assert_eq!( r, expected, @@ -2104,7 +2145,7 @@ mod adequacy { let eb = encode(&b); let expected = a.distance(&b); touch_meter::reset(); - let d = suffix_walk_distance(&ea, &eb); + let d = suffix_walk_distance(crate::codec::built_view(&ea), crate::codec::built_view(&eb)); let touches = touch_meter::touches(); assert_eq!( d, expected, @@ -2313,7 +2354,7 @@ mod adequacy { /// The rank fold on the shipped integrator with the per-digit /// close: the shipped [`rank`](super::super::rank) loop verbatim, /// only the close swapped. - fn per_digit_rank(bits: &BitsSlice) -> Rank { + fn per_digit_rank(bits: BitsView<'_>) -> Rank { let max_depth = max_depth(bits); let (mut cursor, first) = LeafCursor::open(bits); let mut integral = Integrator::new(); @@ -2342,7 +2383,7 @@ mod adequacy { let enc = encode(&v); let expected = v.rank(); reset_limb_ops(); - let r = per_digit_rank(&enc); + let r = per_digit_rank(crate::codec::built_view(&enc)); let limbs = limb_ops(); assert_eq!( r, expected, @@ -2546,7 +2587,7 @@ mod adequacy { /// The rank fold on the shipped integrator with the schoolbook close: the /// shipped [`rank`](super::super::rank) loop verbatim, only the close /// swapped. - fn schoolbook_rank(bits: &BitsSlice) -> Rank { + fn schoolbook_rank(bits: BitsView<'_>) -> Rank { let max_depth = max_depth(bits); let (mut cursor, first) = LeafCursor::open(bits); let mut integral = Integrator::new(); @@ -2572,7 +2613,7 @@ mod adequacy { let expected = v.rank(); touch_meter::reset(); reset_limb_ops(); - let r = schoolbook_rank(&enc); + let r = schoolbook_rank(crate::codec::built_view(&enc)); let touches = touch_meter::touches(); let limbs = limb_ops(); assert_eq!( diff --git a/crates/before/src/version/skyline/sweep.rs b/crates/before/src/version/skyline/sweep.rs index f6aac247..02ab0c40 100644 --- a/crates/before/src/version/skyline/sweep.rs +++ b/crates/before/src/version/skyline/sweep.rs @@ -72,7 +72,7 @@ use core::cmp::Ordering; use core::ops::ControlFlow; -use crate::codec::BitsSlice; +use crate::codec::BitsView; use super::overlay::{advance_diff, OpenedPair, PlateauCursor}; @@ -91,13 +91,13 @@ use super::overlay::{advance_diff, OpenedPair, PlateauCursor}; /// the walk structurally notices (truncation, malformation) panic; the rest (a /// collapsible sibling pair, a delta driving the running height negative) sweep /// silently, and the verdict is then unspecified. -pub fn causal_cmp(a: &BitsSlice, b: &BitsSlice) -> Option { +pub fn causal_cmp(a: BitsView<'_>, b: BitsView<'_>) -> Option { // Clone identity decides reflexivity without a walk: one shared stored // buffer read through two views is bit-for-bit one stream (`Version::clone` // is a refcount bump), and a version compares `Equal` to itself — the // `order_reflexive` law in `crate::laws`. Equal streams in distinct buffers // still take the sweep below. - if crate::codec::slice_ptr_eq(a, b) { + if a.ptr_eq(&b) { return Some(Ordering::Equal); } // At exhaustion every surviving combination is a verdict. @@ -119,7 +119,7 @@ pub fn causal_cmp(a: &BitsSlice, b: &BitsSlice) -> Option { /// Test- and meter-only: production equality is the stored forms' byte equality /// (canonical uniqueness makes them the same test). #[cfg(any(test, feature = "meter"))] -pub fn eq(a: &BitsSlice, b: &BitsSlice) -> bool { +pub fn eq(a: BitsView<'_>, b: BitsView<'_>) -> bool { // Surviving to exhaustion is equality: `eq_exit` breaks on any refutation // before the exhaustion check runs, so reaching the finish arm IS the // verdict. The assertion keeps that control-flow argument loud: an exit or @@ -149,7 +149,7 @@ pub fn eq(a: &BitsSlice, b: &BitsSlice) -> bool { /// Test- and meter-only: production concurrency checks go through /// [`Version::concurrent`](crate::Version::concurrent) over the same sweep. #[cfg(any(test, feature = "meter"))] -pub fn concurrent(a: &BitsSlice, b: &BitsSlice) -> bool { +pub fn concurrent(a: BitsView<'_>, b: BitsView<'_>) -> bool { causal_cmp(a, b).is_none() } @@ -167,7 +167,7 @@ pub fn concurrent(a: &BitsSlice, b: &BitsSlice) -> bool { /// Test- and meter-only: production ordering goes through the `PartialOrd` /// surface over [`causal_cmp`]. #[cfg(any(test, feature = "meter"))] -pub fn le(a: &BitsSlice, b: &BitsSlice) -> bool { +pub fn le(a: BitsView<'_>, b: BitsView<'_>) -> bool { sweep( a, b, @@ -261,8 +261,8 @@ pub(super) fn eq_exit(directions: Directions) -> ControlFlow { /// be stale at an early exit: only the fully-swept directions `finish` maps /// at exhaustion are all decided. fn sweep( - a_bits: &BitsSlice, - b_bits: &BitsSlice, + a_bits: BitsView<'_>, + b_bits: BitsView<'_>, exit: impl Fn(Directions) -> ControlFlow, finish: impl FnOnce(Directions) -> V, ) -> V { diff --git a/crates/before/src/version/skyline/sweep/tests.rs b/crates/before/src/version/skyline/sweep/tests.rs index 36ec245d..b15d2da1 100644 --- a/crates/before/src/version/skyline/sweep/tests.rs +++ b/crates/before/src/version/skyline/sweep/tests.rs @@ -32,7 +32,10 @@ fn version_of(p: &Packed) -> Version { /// The sweep's causal order of two versions, on their stored streams. fn cmp_enc(a: &Version, b: &Version) -> Option { - causal_cmp(&encode(a), &encode(b)) + causal_cmp( + crate::codec::built_view(&encode(a)), + crate::codec::built_view(&encode(b)), + ) } /// Assert all four entry points agree with the recursive oracle's @@ -41,30 +44,38 @@ fn assert_verdicts(a: &Version, b: &Version) { let (ea, eb) = (encode(a), encode(b)); let want = to_oracle_version(a).partial_cmp(&to_oracle_version(b)); assert_eq!( - causal_cmp(&ea, &eb), + causal_cmp(crate::codec::built_view(&ea), crate::codec::built_view(&eb)), want, "causal_cmp disagrees with the recursive oracle: {a} vs {b}" ); assert_eq!( - causal_cmp(&eb, &ea), + causal_cmp(crate::codec::built_view(&eb), crate::codec::built_view(&ea)), want.map(Ordering::reverse), "causal_cmp breaks antisymmetry against the recursive oracle: {b} vs {a}" ); let equal = want == Some(Ordering::Equal); - assert_eq!(eq(&ea, &eb), equal, "eq disagrees: {a} vs {b}"); - assert_eq!(eq(&eb, &ea), equal, "eq disagrees: {b} vs {a}"); assert_eq!( - concurrent(&ea, &eb), + eq(crate::codec::built_view(&ea), crate::codec::built_view(&eb)), + equal, + "eq disagrees: {a} vs {b}" + ); + assert_eq!( + eq(crate::codec::built_view(&eb), crate::codec::built_view(&ea)), + equal, + "eq disagrees: {b} vs {a}" + ); + assert_eq!( + concurrent(crate::codec::built_view(&ea), crate::codec::built_view(&eb)), want.is_none(), "concurrent disagrees: {a} vs {b}" ); assert_eq!( - le(&ea, &eb), + le(crate::codec::built_view(&ea), crate::codec::built_view(&eb)), matches!(want, Some(Ordering::Less | Ordering::Equal)), "le disagrees: {a} vs {b}" ); assert_eq!( - le(&eb, &ea), + le(crate::codec::built_view(&eb), crate::codec::built_view(&ea)), matches!(want, Some(Ordering::Greater | Ordering::Equal)), "le disagrees: {b} vs {a}" ); @@ -192,22 +203,22 @@ fn exhaustive_small_scope_agrees() { for (tb, vb, eb) in &pool { let want = ta.partial_cmp(tb); assert_eq!( - causal_cmp(ea, eb), + causal_cmp(crate::codec::built_view(ea), crate::codec::built_view(eb)), want, "causal_cmp disagrees: {va} vs {vb}" ); assert_eq!( - eq(ea, eb), + eq(crate::codec::built_view(ea), crate::codec::built_view(eb)), want == Some(Ordering::Equal), "eq disagrees: {va} vs {vb}" ); assert_eq!( - concurrent(ea, eb), + concurrent(crate::codec::built_view(ea), crate::codec::built_view(eb)), want.is_none(), "concurrent disagrees: {va} vs {vb}" ); assert_eq!( - le(ea, eb), + le(crate::codec::built_view(ea), crate::codec::built_view(eb)), matches!(want, Some(Ordering::Less | Ordering::Equal)), "le disagrees: {va} vs {vb}" ); @@ -249,19 +260,19 @@ proptest! { for (ta, va, ea) in &pool { for (tb, vb, eb) in &pool { let want = ta.partial_cmp(tb); - prop_assert_eq!(causal_cmp(ea, eb), want, "causal_cmp disagrees: {} vs {}", va, vb); + prop_assert_eq!(causal_cmp(crate::codec::built_view(ea), crate::codec::built_view(eb)), want, "causal_cmp disagrees: {} vs {}", va, vb); prop_assert_eq!( - eq(ea, eb), + eq(crate::codec::built_view(ea), crate::codec::built_view(eb)), want == Some(Ordering::Equal), "eq disagrees: {} vs {}", va, vb ); prop_assert_eq!( - concurrent(ea, eb), + concurrent(crate::codec::built_view(ea), crate::codec::built_view(eb)), want.is_none(), "concurrent disagrees: {} vs {}", va, vb ); prop_assert_eq!( - le(ea, eb), + le(crate::codec::built_view(ea), crate::codec::built_view(eb)), matches!(want, Some(Ordering::Less | Ordering::Equal)), "le disagrees: {} vs {}", va, vb ); diff --git a/crates/before/src/version/skyline/tests.rs b/crates/before/src/version/skyline/tests.rs index b527952c..ece93029 100644 --- a/crates/before/src/version/skyline/tests.rs +++ b/crates/before/src/version/skyline/tests.rs @@ -54,7 +54,10 @@ fn empty_version_is_the_two_bit_stream() { assert_eq!(bits.len(), 2); assert!(bits[0], "a leaf's topology flag is 1"); assert!(bits[1], "gamma(0) is the single bit 1"); - assert_eq!(decode_bits(&bits).expect("canonical"), v); + assert_eq!( + decode_bits(crate::codec::built_view(&bits)).expect("canonical"), + v + ); } /// One fork `(1, 0, 2)` codes as hand-derived: 3 topology bits, `gamma(1)` for @@ -76,7 +79,10 @@ fn one_fork_matches_hand_derivation() { ] .to_vec(); assert_eq!(bits.iter().by_vals().collect::>(), expected); - assert_eq!(decode_bits(&bits).expect("canonical"), v); + assert_eq!( + decode_bits(crate::codec::built_view(&bits)).expect("canonical"), + v + ); } // ─── the strict-reject corpus ─────────────────────────────────────────────── @@ -96,7 +102,10 @@ fn rejects_zero_right_sibling_delta() { bits.push(false); // root: internal push_leaf(&mut bits, 5); // gamma(5): the first leaf, absolute push_leaf(&mut bits, 0); // zigzag(0) = 0 -> gamma(0): equal sibling - assert!(matches!(validate_bits(&bits), Err(Decode::NotCanonical))); + assert!(matches!( + validate_bits(crate::codec::built_view(&bits)), + Err(Decode::NotCanonical) + )); } /// A collapsible sibling pair whose closing ancestor is NOT the root — @@ -115,7 +124,10 @@ fn rejects_non_root_collapsible_pair() { bits.push(false); // right child: internal push_leaf(&mut bits, 0); // zigzag(0): leaf 5 again, non-sibling (legal) push_leaf(&mut bits, 0); // zigzag(0): its equal sibling — the pair - assert!(matches!(validate_bits(&bits), Err(Decode::NotCanonical))); + assert!(matches!( + validate_bits(crate::codec::built_view(&bits)), + Err(Decode::NotCanonical) + )); } /// The `(flag, end)` bit positions of every leaf code in a stored stream: the @@ -133,7 +145,9 @@ fn leaf_code_ranges(bits: &BitsMut) -> Vec<(usize, usize)> { pending += 2; continue; } - let (_, next) = codec::decode_int(bits, pos).expect("a stored stream is canonical"); + let (_, next) = codec::decode_int(crate::codec::built_view(bits), pos as u64) + .expect("a stored stream is canonical"); + let next = next as usize; out.push((pos - 1, next)); pos = next; } @@ -166,7 +180,7 @@ proptest! { planted.extend_from_bitslice(&bits[flag..end]); // left child: the old leaf push_leaf(&mut planted, 0); // right child: zigzag(0), the equal sibling planted.extend_from_bitslice(&bits[end..]); - prop_assert!(matches!(validate_bits(&planted), Err(Decode::NotCanonical))); + prop_assert!(matches!(validate_bits(crate::codec::built_view(&planted)), Err(Decode::NotCanonical))); } } @@ -192,8 +206,11 @@ fn accepts_zero_delta_across_a_subtree_boundary() { push_leaf(&mut bits, 0); // leaf 0: gamma(0), absolute push_leaf(&mut bits, 2); // leaf 1: zigzag(+1) = 2 push_leaf(&mut bits, 0); // leaf 1 again: zigzag(0) = 0, non-sibling - assert!(validate_bits(&bits).is_ok()); - assert_eq!(decode_bits(&bits).expect("canonical"), expected); + assert!(validate_bits(crate::codec::built_view(&bits)).is_ok()); + assert_eq!( + decode_bits(crate::codec::built_view(&bits)).expect("canonical"), + expected + ); assert_eq!(stream_of(&expected), bits); } @@ -207,7 +224,10 @@ fn rejects_negative_running_height() { bits.push(false); // root: internal push_leaf(&mut bits, 1); // first leaf: height 1 push_leaf(&mut bits, 3); // zigzag(-2) = 3: height would be -1 - assert!(matches!(validate_bits(&bits), Err(Decode::NotCanonical))); + assert!(matches!( + validate_bits(crate::codec::built_view(&bits)), + Err(Decode::NotCanonical) + )); } /// A negative excursion is rejected even when later deltas would climb back up: @@ -222,7 +242,10 @@ fn rejects_negative_height_midstream() { bits.push(false); // right child: internal push_leaf(&mut bits, 3); // zigzag(-2) = 3: height -1, invalid here push_leaf(&mut bits, 12); // zigzag(+6) = 12: would recover to 5 - assert!(matches!(validate_bits(&bits), Err(Decode::NotCanonical))); + assert!(matches!( + validate_bits(crate::codec::built_view(&bits)), + Err(Decode::NotCanonical) + )); } /// Every proper prefix of a valid stream is rejected as [`Decode::Truncated`], @@ -241,7 +264,10 @@ fn rejects_every_truncation() { let bits = stream_of(v); for cut in 0..bits.len() { assert!( - matches!(validate_bits(&bits[..cut]), Err(Decode::Truncated)), + matches!( + validate_bits(crate::codec::BitsView::new(bits.as_raw_slice(), cut as u64)), + Err(Decode::Truncated) + ), "a {cut}-bit prefix of a {}-bit stream must read as truncated", bits.len(), ); @@ -258,12 +284,15 @@ fn rejects_trailing_bits() { for extra in [false, true] { let mut bits = clean.clone(); bits.push(extra); - assert!(matches!(validate_bits(&bits), Err(Decode::TrailingBits))); + assert!(matches!( + validate_bits(crate::codec::built_view(&bits)), + Err(Decode::TrailingBits) + )); } let mut two_trees = clean.clone(); two_trees.extend_from_bitslice(&clean); assert!(matches!( - validate_bits(&two_trees), + validate_bits(crate::codec::built_view(&two_trees)), Err(Decode::TrailingBits) )); } @@ -356,7 +385,9 @@ fn flip_topology_flags(bits: &BitsMut, internal: bool) -> BitsMut { pending += 2; continue; } - let (_, next) = codec::decode_int(bits, pos).expect("a payload code per leaf"); + let (_, next) = codec::decode_int(crate::codec::built_view(bits), pos as u64) + .expect("a payload code per leaf"); + let next = next as usize; out.extend_from_bitslice(&bits[pos..next]); pos = next; } @@ -425,7 +456,7 @@ fn assert_mutation_never_aliases(v: &Version, bits: &BitsMut, flip: usize) { let mut mutated = bits.clone(); let old = mutated[flip]; mutated.set(flip, !old); - match decode_bits(&mutated) { + match decode_bits(crate::codec::built_view(&mutated)) { Err(_) => {} Ok(w) => { assert_ne!( @@ -484,7 +515,7 @@ proptest! { fn assert_agreement(v: &Version) { let bits = stream_of(v); let packed = packed_bits_of(&to_oracle_version(v)); - let size = tier2_size(&packed); + let size = tier2_size(crate::codec::built_view(&packed)); assert_eq!( bits.len() as u64, size.total_bits, @@ -492,10 +523,10 @@ fn assert_agreement(v: &Version) { two independent walks is wrong" ); assert!( - validate_bits(&bits).is_ok(), + validate_bits(crate::codec::built_view(&bits)).is_ok(), "the encoder emits canonical streams" ); - let back = decode_bits(&bits).expect("a canonical stream decodes"); + let back = decode_bits(crate::codec::built_view(&bits)).expect("a canonical stream decodes"); assert_eq!( &back, v, "the skyline round-trip reproduces the version exactly" diff --git a/crates/before/src/version/skyline/text.rs b/crates/before/src/version/skyline/text.rs index 2af79f53..2fbb7b67 100644 --- a/crates/before/src/version/skyline/text.rs +++ b/crates/before/src/version/skyline/text.rs @@ -69,7 +69,7 @@ use core::fmt::Write as _; use suanpan::Accumulator; use crate::codec::text::{parse_base, Cur}; -use crate::codec::{Base, BitCursor, BitsMut, BitsSlice, DsiCursor}; +use crate::codec::{Base, BitCursor, BitsMut, BitsView, DsiCursor}; use crate::error::Parse; use super::build::SkylineBuilder; @@ -227,7 +227,7 @@ impl ParkedStack { /// # Panics /// /// Panics if the stream is not a canonical skyline encoding. -pub fn render(bits: &BitsSlice) -> String { +pub fn render(bits: BitsView<'_>) -> String { let mut cursor = DsiCursor::new(bits); // Finalize state. `topology`: per-node internal flags (semantic, not the @@ -307,7 +307,7 @@ pub fn render(bits: &BitsSlice) -> String { } }; assert_eq!( - cursor.position(), + cursor.position_u64(), bits.len(), "a canonical skyline stream is exactly one tree" ); diff --git a/crates/before/src/version/skyline/text/tests.rs b/crates/before/src/version/skyline/text/tests.rs index eb7d585f..ea8a07ee 100644 --- a/crates/before/src/version/skyline/text/tests.rs +++ b/crates/before/src/version/skyline/text/tests.rs @@ -46,7 +46,7 @@ fn version_of(p: &Packed) -> Version { /// pins entry agreement (the public entry routes to the kernel). fn assert_text_kernels_agree(v: &Version) { let enc = super::super::encode(v); - let text = render(&enc); + let text = render(crate::codec::built_view(&enc)); assert_eq!( text, v.to_string(), @@ -127,7 +127,7 @@ fn parse_corpus_pins_the_grammar_decisions() { let v: Version = text.parse().expect("the public entry accepts"); let enc = parse(text).expect("the kernel accepts the corpus's accepted texts"); assert!( - super::super::validate_bits(&enc).is_ok(), + super::super::validate_bits(crate::codec::built_view(&enc)).is_ok(), "an accepted parse must build a canonical skyline stream: {text:?}" ); assert_eq!( @@ -238,7 +238,7 @@ fn mutated_texts_hold_reject_parity_through_the_public_entry() { |mutated| match (mutated.parse::(), parse(mutated)) { (Ok(v), Ok(enc)) => { assert!( - validate_bits(&enc).is_ok(), + validate_bits(crate::codec::built_view(&enc)).is_ok(), "an accepted mutant {mutated:?} must build a canonical skyline stream" ); assert_eq!( @@ -415,7 +415,8 @@ fn parse_schoolbook(s: &str) -> Result { return Err(Parse::NotCanonical); } let bits = builder.finish(); - validate_bits(&bits).expect("a canonical text parse builds a canonical skyline stream"); + validate_bits(crate::codec::built_view(&bits)) + .expect("a canonical text parse builds a canonical skyline stream"); Ok(bits) } @@ -511,7 +512,7 @@ mod schoolbook_contrast { fn run(s: usize, kernel: fn(&str) -> Result) -> (u64, u64) { let v = Shape::WideArming.packed2(s, s).version(); let enc = encode(&v); - let text = render(&enc); + let text = render(crate::codec::built_view(&enc)); let bytes = text.len() as u64; touch_meter::reset(); let parsed = kernel(&text).expect("rendered text parses"); diff --git a/crates/before/src/version/skyline/validate.rs b/crates/before/src/version/skyline/validate.rs index 8e425d37..ca2e77e9 100644 --- a/crates/before/src/version/skyline/validate.rs +++ b/crates/before/src/version/skyline/validate.rs @@ -24,9 +24,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; -#[cfg(any(test, feature = "meter"))] -use crate::codec::BitsSlice; -use crate::codec::{BitCursor, BitsMut, DsiCursor}; +use crate::codec::{BitCursor, BitsMut, BitsView, DsiCursor}; use crate::error::Decode; use super::signed::{fold_signed_int, unzigzag, Sign}; @@ -37,28 +35,27 @@ use super::signed::{fold_signed_int, unzigzag, Sign}; /// the last live bit is [`Decode::TrailingBits`]; everything else is /// [`validate_from`]'s contract. /// -/// Test- and meter-only: the production entries run [`validate_prefix_bytes`] and +/// Test- and meter-only: the production entries run [`validate_prefix`] and /// [`validate_from`], which leave the tail to their callers. #[cfg(any(test, feature = "meter"))] -pub(crate) fn validate_bits(bits: &BitsSlice) -> Result<(), Decode> { +pub(crate) fn validate_bits(bits: BitsView<'_>) -> Result<(), Decode> { let mut cursor = DsiCursor::new(bits); validate_from(&mut cursor)?; - if cursor.position() != bits.len() { + if cursor.position_u64() != bits.len() { return Err(Decode::TrailingBits); } Ok(()) } -/// Strictly validate one skyline tree at the head of a raw byte buffer's -/// whole `8 · bytes.len()`-bit view, returning the position just past it. +/// Strictly validate one skyline tree at the head of a view, returning the +/// position just past it. /// /// The wire decoder's entry: a version's skyline stream is bit-self-delimiting /// (one complete tree), so the returned end position is where any zero padding -/// must begin. Raw bytes and a `u64` end, never a borrowed bit view: the -/// doors admit buffers past the view encoding's cap (64 MiB and up on a -/// 32-bit target), whose bit positions outgrow a 32-bit `usize`. -pub(crate) fn validate_prefix_bytes(bytes: &[u8]) -> Result { - let mut cursor = DsiCursor::over_bytes(bytes); +/// must begin. The end is `u64`, the view's own width: a byte door's +/// whole-buffer view holds more bit positions than a 32-bit `usize`. +pub(crate) fn validate_prefix(bits: BitsView<'_>) -> Result { + let mut cursor = DsiCursor::new(bits); validate_from(&mut cursor)?; Ok(cursor.position_u64()) } diff --git a/crates/before/src/version/tests.rs b/crates/before/src/version/tests.rs index 648a6573..7aa6edfb 100644 --- a/crates/before/src/version/tests.rs +++ b/crates/before/src/version/tests.rs @@ -1895,7 +1895,7 @@ proptest! { ) { let a = from_oracle_version(&oa); let b = from_oracle_version(&ob); - let bit_eq = a.as_bits() == b.as_bits(); + let bit_eq = a.as_bits().to_bitvec() == b.as_bits().to_bitvec(); prop_assert_eq!(a == b, bit_eq); prop_assert_eq!(b == a, bit_eq); if a == b { diff --git a/crates/before/tests/meter.rs b/crates/before/tests/meter.rs index 0b152498..a0d1ebba 100644 --- a/crates/before/tests/meter.rs +++ b/crates/before/tests/meter.rs @@ -1424,7 +1424,7 @@ fn skyline_validate_dense_envelope() { "skyline_validate_dense", enc.as_raw_slice().len(), &envelope::SKYLINE_VALIDATE_DENSE, - || meter::skyline::validate(&enc), + || meter::skyline::validate(meter::skyline::view(&enc)), ); assert!(r.is_ok(), "the transcoded dense spine is canonical"); } @@ -1442,7 +1442,7 @@ fn skyline_validate_cliff_envelope() { "skyline_validate_cliff", enc.as_raw_slice().len(), &envelope::SKYLINE_VALIDATE_CLIFF, - || meter::skyline::validate(&enc), + || meter::skyline::validate(meter::skyline::view(&enc)), ); assert!(r.is_ok(), "the transcoded boundary comb is canonical"); } @@ -1458,7 +1458,7 @@ fn skyline_validate_wide_tooth_envelope() { "skyline_validate_wide_tooth", enc.as_raw_slice().len(), &envelope::SKYLINE_VALIDATE_WIDE_TOOTH, - || meter::skyline::validate(&enc), + || meter::skyline::validate(meter::skyline::view(&enc)), ); assert!(r.is_ok(), "the transcoded wide-tooth comb is canonical"); } @@ -1476,7 +1476,7 @@ fn skyline_validate_hugeleaf_envelope() { "skyline_validate_hugeleaf", enc.as_raw_slice().len(), &envelope::SKYLINE_VALIDATE_HUGELEAF, - || meter::skyline::validate(&enc), + || meter::skyline::validate(meter::skyline::view(&enc)), ); assert!(r.is_ok(), "the transcoded hugeleaf is canonical"); } @@ -1492,7 +1492,7 @@ fn skyline_validate_alt_spine_envelope() { "skyline_validate_alt_spine", enc.as_raw_slice().len(), &envelope::SKYLINE_VALIDATE_ALT_SPINE, - || meter::skyline::validate(&enc), + || meter::skyline::validate(meter::skyline::view(&enc)), ); assert!(r.is_ok(), "the transcoded alternating spine is canonical"); } @@ -1507,7 +1507,7 @@ fn skyline_decode_dense_envelope() { "skyline_decode_dense", enc.as_raw_slice().len(), &envelope::SKYLINE_DECODE_DENSE, - || meter::skyline::decode(&enc).expect("canonical"), + || meter::skyline::decode(meter::skyline::view(&enc)).expect("canonical"), ); assert_eq!(v, version_of(&p), "the transcode round-trips"); } @@ -1525,7 +1525,7 @@ fn skyline_decode_cliff_envelope() { "skyline_decode_cliff", enc.as_raw_slice().len(), &envelope::SKYLINE_DECODE_CLIFF, - || meter::skyline::decode(&enc).expect("canonical"), + || meter::skyline::decode(meter::skyline::view(&enc)).expect("canonical"), ); assert_eq!(v, version_of(&p), "the transcode round-trips"); } @@ -1540,7 +1540,7 @@ fn skyline_decode_wide_tooth_envelope() { "skyline_decode_wide_tooth", enc.as_raw_slice().len(), &envelope::SKYLINE_DECODE_WIDE_TOOTH, - || meter::skyline::decode(&enc).expect("canonical"), + || meter::skyline::decode(meter::skyline::view(&enc)).expect("canonical"), ); assert_eq!(v, version_of(&p), "the transcode round-trips"); } @@ -1555,7 +1555,7 @@ fn skyline_decode_hugeleaf_envelope() { "skyline_decode_hugeleaf", enc.as_raw_slice().len(), &envelope::SKYLINE_DECODE_HUGELEAF, - || meter::skyline::decode(&enc).expect("canonical"), + || meter::skyline::decode(meter::skyline::view(&enc)).expect("canonical"), ); assert_eq!(v, version_of(&p), "the transcode round-trips"); } @@ -1570,7 +1570,7 @@ fn skyline_decode_alt_spine_envelope() { "skyline_decode_alt_spine", enc.as_raw_slice().len(), &envelope::SKYLINE_DECODE_ALT_SPINE, - || meter::skyline::decode(&enc).expect("canonical"), + || meter::skyline::decode(meter::skyline::view(&enc)).expect("canonical"), ); assert_eq!(v, version_of(&p), "the transcode round-trips"); } @@ -1755,7 +1755,7 @@ fn skyline_cmp_dense_envelope() { "skyline_cmp_dense", sweep_input_bytes(&a, &b), &sweep_env::SKYLINE_CMP_DENSE, - || meter::skyline::sweep::causal_cmp(&a, &b), + || meter::skyline::sweep::causal_cmp(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!( r, @@ -1777,7 +1777,7 @@ fn skyline_cmp_dense_self_envelope() { "skyline_cmp_dense_self", sweep_input_bytes(&a, &b), &sweep_env::SKYLINE_CMP_DENSE_SELF, - || meter::skyline::sweep::causal_cmp(&a, &b), + || meter::skyline::sweep::causal_cmp(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!(r, Some(Ordering::Equal), "identical streams read equal"); } @@ -1793,7 +1793,7 @@ fn skyline_cmp_bigroot_envelope() { "skyline_cmp_bigroot", sweep_input_bytes(&a, &b), &sweep_env::SKYLINE_CMP_BIGROOT, - || meter::skyline::sweep::causal_cmp(&a, &b), + || meter::skyline::sweep::causal_cmp(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!( r, @@ -1816,7 +1816,7 @@ fn skyline_cmp_cliff_envelope() { "skyline_cmp_cliff", sweep_input_bytes(&a, &b), &sweep_env::SKYLINE_CMP_CLIFF, - || meter::skyline::sweep::causal_cmp(&a, &b), + || meter::skyline::sweep::causal_cmp(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!( r, @@ -1839,7 +1839,7 @@ fn skyline_cmp_wide_tooth_envelope() { "skyline_cmp_wide_tooth", sweep_input_bytes(&a, &b), &sweep_env::SKYLINE_CMP_WIDE_TOOTH, - || meter::skyline::sweep::causal_cmp(&a, &b), + || meter::skyline::sweep::causal_cmp(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!( r, @@ -1921,7 +1921,7 @@ fn skyline_join_dense_envelope() { "skyline_join_dense", sweep_input_bytes(&a, &b), &emit_env::SKYLINE_JOIN_DENSE, - || meter::skyline::emit::join(&a, &b), + || meter::skyline::emit::join(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!(out, expected, "the emitted join must match the oracle"); } @@ -1943,7 +1943,7 @@ fn skyline_join_absorb_envelope() { "skyline_join_absorb", sweep_input_bytes(&a, &b), &emit_env::SKYLINE_JOIN_ABSORB, - || meter::skyline::emit::join(&a, &b), + || meter::skyline::emit::join(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!(out, expected, "a dominating flat operand is the whole join"); } @@ -1960,7 +1960,7 @@ fn skyline_join_bigroot_envelope() { "skyline_join_bigroot", sweep_input_bytes(&a, &b), &emit_env::SKYLINE_JOIN_BIGROOT, - || meter::skyline::emit::join(&a, &b), + || meter::skyline::emit::join(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!(out, expected, "the emitted join must match the oracle"); } @@ -1978,7 +1978,7 @@ fn skyline_join_cliff_envelope() { "skyline_join_cliff", sweep_input_bytes(&a, &b), &emit_env::SKYLINE_JOIN_CLIFF, - || meter::skyline::emit::join(&a, &b), + || meter::skyline::emit::join(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!(out, expected, "the emitted join must match the oracle"); } @@ -1995,7 +1995,7 @@ fn skyline_join_wide_tooth_envelope() { "skyline_join_wide_tooth", sweep_input_bytes(&a, &b), &emit_env::SKYLINE_JOIN_WIDE_TOOTH, - || meter::skyline::emit::join(&a, &b), + || meter::skyline::emit::join(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!(out, expected, "the emitted join must match the oracle"); } @@ -2015,7 +2015,7 @@ fn skyline_meet_cliff_envelope() { "skyline_meet_cliff", sweep_input_bytes(&a, &b), &emit_env::SKYLINE_MEET_CLIFF, - || meter::skyline::emit::meet(&a, &b), + || meter::skyline::emit::meet(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!(out, expected, "the emitted meet must match the oracle"); } @@ -2035,7 +2035,7 @@ fn skyline_meet_wide_tooth_envelope() { "skyline_meet_wide_tooth", sweep_input_bytes(&a, &b), &emit_env::SKYLINE_MEET_WIDE_TOOTH, - || meter::skyline::emit::meet(&a, &b), + || meter::skyline::emit::meet(meter::skyline::view(&a), meter::skyline::view(&b)), ); assert_eq!(out, expected, "the emitted meet must match the oracle"); } @@ -2170,7 +2170,7 @@ fn skyline_render_dense_envelope() { "skyline_render_dense", a.as_raw_slice().len(), &text_env::SKYLINE_RENDER_DENSE, - || meter::skyline::text::render(&a), + || meter::skyline::text::render(meter::skyline::view(&a)), ); assert_eq!(out, expected, "the kernel must render Display's bytes"); } @@ -2191,7 +2191,7 @@ fn skyline_render_bigroot_envelope() { "skyline_render_bigroot", a.as_raw_slice().len(), &text_env::SKYLINE_RENDER_BIGROOT, - || meter::skyline::text::render(&a), + || meter::skyline::text::render(meter::skyline::view(&a)), ); assert_eq!(out, expected, "the kernel must render Display's bytes"); } @@ -2208,7 +2208,7 @@ fn skyline_render_hugeleaf_envelope() { "skyline_render_hugeleaf", a.as_raw_slice().len(), &text_env::SKYLINE_RENDER_HUGELEAF, - || meter::skyline::text::render(&a), + || meter::skyline::text::render(meter::skyline::view(&a)), ); assert_eq!(out, expected, "the kernel must render Display's bytes"); } @@ -2226,7 +2226,7 @@ fn skyline_render_cliff_envelope() { "skyline_render_cliff", a.as_raw_slice().len(), &text_env::SKYLINE_RENDER_CLIFF, - || meter::skyline::text::render(&a), + || meter::skyline::text::render(meter::skyline::view(&a)), ); assert_eq!(out, expected, "the kernel must render Display's bytes"); } @@ -2368,7 +2368,7 @@ mod skyline_flatness { let enc = meter::skyline::encode(&v); touch_meter::reset(); meter::reset_limb_ops(); - meter::skyline::validate(&enc).expect("the comb stream is canonical"); + meter::skyline::validate(meter::skyline::view(&enc)).expect("the comb stream is canonical"); let run = Run { // 2n + 1 leaves: 2n delta codes follow the first leaf. deltas: 2 * scale as u64, @@ -2443,7 +2443,8 @@ mod skyline_flatness { let b = meter::skyline::encode(&before::Version::new()); touch_meter::reset(); meter::reset_limb_ops(); - let verdict = meter::skyline::sweep::causal_cmp(&a, &b); + let verdict = + meter::skyline::sweep::causal_cmp(meter::skyline::view(&a), meter::skyline::view(&b)); assert_eq!( verdict, Some(std::cmp::Ordering::Greater), @@ -2514,7 +2515,7 @@ mod skyline_flatness { let expected = meter::skyline::encode(&(&v | &one)); touch_meter::reset(); meter::reset_limb_ops(); - let out = meter::skyline::emit::join(&a, &b); + let out = meter::skyline::emit::join(meter::skyline::view(&a), meter::skyline::view(&b)); let run = Run { // 2n + 1 leaves: 2n delta codes follow the first leaf. deltas: 2 * scale as u64, @@ -2653,7 +2654,7 @@ mod skyline_flatness { let v = packed.version(); let enc = meter::skyline::encode(&v); touch_meter::reset(); - let out = meter::skyline::text::render(&enc); + let out = meter::skyline::text::render(meter::skyline::view(&enc)); assert!(!out.is_empty(), "the render does real work"); assert_eq!( touch_meter::touches(), @@ -2697,7 +2698,7 @@ mod skyline_flatness { let enc = meter::skyline::encode(&v); touch_meter::reset(); meter::reset_limb_ops(); - let r = meter::skyline::query::rank(&enc); + let r = meter::skyline::query::rank(meter::skyline::view(&enc)); let run = Run { // Each tooth's two leaves follow the first leaf as deltas. deltas: 2 * n as u64, @@ -2828,7 +2829,7 @@ mod skyline_flatness { let enc = meter::skyline::encode(&v); touch_meter::reset(); meter::reset_limb_ops(); - let r = meter::skyline::query::rank(&enc); + let r = meter::skyline::query::rank(meter::skyline::view(&enc)); let run = Run { // Each tooth's two leaves follow the first leaf as deltas. deltas: 2 * n as u64, @@ -4708,7 +4709,8 @@ mod skyline_flatness { let bytes = (ea.as_raw_slice().len() + eb.as_raw_slice().len()) as u64; touch_meter::reset(); meter::reset_limb_ops(); - let verdict = meter::skyline::sweep::causal_cmp(&ea, &eb); + let verdict = + meter::skyline::sweep::causal_cmp(meter::skyline::view(&ea), meter::skyline::view(&eb)); let run = QueryRun { bytes, touches: touch_meter::touches(), @@ -5037,7 +5039,7 @@ mod eq_early_exit { touch_meter::reset(); #[cfg(feature = "scan-meter")] meter::reset_scan_bits(); - let verdict = meter::skyline::sweep::eq(&a, &b); + let verdict = meter::skyline::sweep::eq(meter::skyline::view(&a), meter::skyline::view(&b)); let run = Run { bytes: (a.as_raw_slice().len() + b.as_raw_slice().len()) as u64, touches: touch_meter::touches(), @@ -5564,7 +5566,7 @@ mod parse_wide_arming { generator does not build the tree this band reasons about" ); let enc = meter::skyline::encode(&v); - let text = meter::skyline::text::render(&enc); + let text = meter::skyline::text::render(meter::skyline::view(&enc)); let bytes = text.len() as u64; touch_meter::reset(); let parsed = meter::skyline::text::parse(&text).expect("rendered text parses"); @@ -6982,7 +6984,7 @@ fn skyline_rank_dense_envelope() { "skyline_rank_dense", enc.as_raw_slice().len(), &query_env::SKYLINE_RANK_DENSE, - || meter::skyline::query::rank(&enc), + || meter::skyline::query::rank(meter::skyline::view(&enc)), ); assert_eq!(r, v.rank(), "the kernel must match the packed rank"); } @@ -7001,7 +7003,7 @@ fn skyline_rank_bigroot_envelope() { "skyline_rank_bigroot", enc.as_raw_slice().len(), &query_env::SKYLINE_RANK_BIGROOT, - || meter::skyline::query::rank(&enc), + || meter::skyline::query::rank(meter::skyline::view(&enc)), ); assert_eq!(r, v.rank(), "the kernel must match the packed rank"); } @@ -7021,7 +7023,7 @@ fn skyline_rank_harmonic_envelope() { "skyline_rank_harmonic", enc.as_raw_slice().len(), &query_env::SKYLINE_RANK_HARMONIC, - || meter::skyline::query::rank(&enc), + || meter::skyline::query::rank(meter::skyline::view(&enc)), ); assert_eq!(r, v.rank(), "the kernel must match the packed rank"); } @@ -7042,7 +7044,7 @@ fn skyline_rank_cliff_envelope() { "skyline_rank_cliff", enc.as_raw_slice().len(), &query_env::SKYLINE_RANK_CLIFF, - || meter::skyline::query::rank(&enc), + || meter::skyline::query::rank(meter::skyline::view(&enc)), ); assert_eq!(r, v.rank(), "the kernel must match the packed rank"); } @@ -7064,7 +7066,7 @@ fn skyline_rank_wide_tooth_envelope() { "skyline_rank_wide_tooth", enc.as_raw_slice().len(), &query_env::SKYLINE_RANK_WIDE_TOOTH, - || meter::skyline::query::rank(&enc), + || meter::skyline::query::rank(meter::skyline::view(&enc)), ); assert_eq!(r, v.rank(), "the kernel must match the packed rank"); } @@ -7081,7 +7083,7 @@ fn skyline_min_ticks_dense_envelope() { "skyline_min_ticks_dense", enc.as_raw_slice().len(), &query_env::SKYLINE_MIN_TICKS_DENSE, - || meter::skyline::query::min_ticks(&enc), + || meter::skyline::query::min_ticks(meter::skyline::view(&enc)), ); assert_eq!( r.to_string(), @@ -7104,7 +7106,7 @@ fn skyline_min_ticks_cliff_envelope() { "skyline_min_ticks_cliff", enc.as_raw_slice().len(), &query_env::SKYLINE_MIN_TICKS_CLIFF, - || meter::skyline::query::min_ticks(&enc), + || meter::skyline::query::min_ticks(meter::skyline::view(&enc)), ); assert!( r.to_string().len() > 20, @@ -7142,7 +7144,7 @@ fn skyline_min_ticks_ascend_envelope() { "skyline_min_ticks_ascend", enc.as_raw_slice().len(), &query_env::SKYLINE_MIN_TICKS_ASCEND, - || meter::skyline::query::min_ticks(&enc), + || meter::skyline::query::min_ticks(meter::skyline::view(&enc)), ); // The family's closed form: k leaves at 2^b + i over spine minima // all zero (the terminal cliff), so min_ticks = k·2^b + k(k+1)/2. @@ -7182,7 +7184,7 @@ fn skyline_project_comb_scatter_envelope() { "skyline_project_comb_scatter", io_bytes_in, &query_env::SKYLINE_PROJECT_COMB_SCATTER, - || meter::skyline::query::project(&enc, &party), + || meter::skyline::query::project(meter::skyline::view(&enc), &party), ); eprintln!( "MEASURED skyline_project_comb_scatter: output_bytes={}", diff --git a/crates/before/wasm32-pins/guest/src/lib.rs b/crates/before/wasm32-pins/guest/src/lib.rs index c9f232d5..f646e324 100644 --- a/crates/before/wasm32-pins/guest/src/lib.rs +++ b/crates/before/wasm32-pins/guest/src/lib.rs @@ -92,9 +92,10 @@ pub extern "C" fn pin_version_small() -> i64 { /// `encoded_bits` (always `8n - 8` for the synthesized layout), checking /// that the stored bytes round-trip the input exactly. /// -/// The harness aims this at the 32-bit boundaries: the last size below -/// `bitvec`'s borrowed-view cap, the first size at it, and the 512 MiB -/// bit-length boundary of `usize` position arithmetic. +/// The harness aims this at the 32-bit boundaries: the sizes straddling +/// 2^29 bits (the 32-bit bit-vector length encoding's cap, which the +/// build side still carries) and the 512 MiB bit-length boundary of +/// `usize` position arithmetic. #[no_mangle] pub extern "C" fn pin_version_decode(n_bytes: u64) -> i64 { let n = match usize::try_from(n_bytes) { @@ -331,10 +332,10 @@ fn synth_ranked(n: usize) -> (Vec, usize) { /// Decode a valid `n`-byte-version composite key through the byte door /// `Ranked::decode`, checking the decoded version's bytes round-trip. /// -/// The door re-derives the version's rank to verify the key's rank component -/// — a whole-stream fold over the version's borrowed bit view — so this -/// export observes the composite door's walk boundary, not the byte doors' -/// (which admit the same version alone up to 512 MiB). +/// The door re-derives the version's rank to verify the key's rank +/// component — a whole-stream fold over the version's stored view — so this +/// export observes the composite door's walk surface at sizes the byte +/// doors admit (up to 512 MiB per stream). #[no_mangle] pub extern "C" fn pin_ranked_decode(n_bytes: u64) -> i64 { let n = match usize::try_from(n_bytes) { @@ -384,9 +385,9 @@ pub extern "C" fn pin_ranked_borsh(n_bytes: u64) -> i64 { /// consumption and that both endpoints are the parsed version. /// /// The door validates the second stream against the first component's -/// borrowed bit view in one fused admission walk, so this export observes -/// that walk's boundary on the `lo` component; the endpoint checks are byte -/// compares, exact at any storable size. +/// stored view in one fused admission walk, so this export observes that +/// walk on the `lo` component; the endpoint checks are byte compares, +/// exact at any storable size. #[no_mangle] pub extern "C" fn pin_span_borsh(n_bytes: u64) -> i64 { let n = match usize::try_from(n_bytes) { @@ -417,9 +418,9 @@ pub extern "C" fn pin_span_borsh(n_bytes: u64) -> i64 { /// leaves, so they are comparable), expecting strictly greater both ways /// around. /// -/// The comparison sweep reads each operand through its borrowed bit view, so -/// this export observes the walk surface's boundary on ordinary comparison — -/// a decode-free operation pair over already-stored values. +/// The comparison sweep reads each operand through its stored view, so +/// this export observes the walk surface on ordinary comparison — a +/// decode-free operation pair over already-stored values. #[no_mangle] pub extern "C" fn pin_version_cmp(n_bytes: u64) -> i64 { let n = match usize::try_from(n_bytes) { @@ -447,10 +448,11 @@ pub extern "C" fn pin_version_cmp(n_bytes: u64) -> i64 { /// leaves: pointwise max is the taller), expecting the join to equal the /// big operand byte for byte. /// -/// The join walks both operands through their borrowed bit views before its -/// merge emission, so this export observes the walk surface's boundary on -/// the emitting operation class; the result check is a byte compare, exact -/// at any storable size. +/// The join walks both operands through their stored views before its +/// merge emission, so this export observes the emitting operation class: +/// the walk surface on its input side, and the build buffer on its output +/// side (a covered join's output reproduces the big operand whole); the +/// result check is a byte compare, exact at any storable size. #[no_mangle] pub extern "C" fn pin_version_join_covering(n_bytes: u64) -> i64 { let n = match usize::try_from(n_bytes) { diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs index e203cb93..d0ce83da 100644 --- a/crates/before/wasm32-pins/harness/tests/pins.rs +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -11,15 +11,16 @@ use wasm32_pins_harness::{call0, call1, call2, Outcome, Trap}; -/// The largest buffer byte count whose whole-stream borrowed bit view is -/// constructible on wasm32: `bitvec`'s view length caps at `usize::MAX >> 3` -/// = 2^29 - 1 bits, so 67108863 bytes. +/// The largest buffer byte count whose whole-buffer bit count stays below +/// 2^29: one byte under the 32-bit bit-vector length encoding's cap +/// (`usize::MAX >> 3` bits), so 67108863 bytes. /// -/// The walk surface — comparisons, joins, the composite doors' rank -/// re-derivation and dominance re-walk — reads stored streams through that -/// borrowed view, so this is every walk pin's lower adjacency witness; the -/// byte doors themselves admit streams up to 512 MiB. -const VIEW_CAP_BYTES: u64 = 67_108_863; +/// The walk surface reads stored streams through the crate-owned view and is +/// exact at every storable size; the emitters' build buffer is the one +/// surface still bounded by this encoding, so the walk pins straddle the +/// boundary (below, at, and past it) to hold the read side clean of it, and +/// the emitting pins meet it on their output side. +const BUILD_CAP_BYTES: u64 = 67_108_863; /// Liveness: a small valid version decodes on wasm32 with the exact bit /// length, round-trips, and rejects mutilations with typed errors. @@ -31,15 +32,15 @@ fn version_small_roundtrips_and_rejects_typed() { assert_eq!(call0("pin_version_small"), Outcome::Value(0)); } -/// The largest input whose whole-buffer bit view still fits `bitvec`'s -/// borrowed-view length cap on wasm32 decodes with its exact bit length. +/// The largest input whose whole-buffer bit count stays below the 32-bit +/// bit-vector length encoding's cap decodes with its exact bit length. /// /// The cap is `usize::MAX >> 3` = 2^29 - 1 bits, so 67108863 bytes: the /// boundary's lower adjacency witness, so a failure at the sizes just -/// above is attributable to the cap, never to general large-input +/// above is attributable to the boundary, never to general large-input /// handling. #[test] -fn version_decode_below_view_cap() { +fn version_decode_below_build_cap() { assert_eq!( call1("pin_version_decode", 67_108_863), Outcome::Value(8 * 67_108_863 - 8), @@ -49,13 +50,11 @@ fn version_decode_below_view_cap() { /// A valid 64 MiB (67108864-byte) version encoding decodes correctly on /// wasm32, returning its exact live bit length. /// -/// The size is exactly 2^29 bits: a whole-buffer borrowed bit view of it -/// is unconstructible — `bitvec`'s span encoding silently produces an -/// EMPTY view here, whose walk would reject a valid input as `Truncated` -/// with no alarm — so this pin holds the doors to their byte-backed walk, -/// on which no such view exists. +/// The size is exactly 2^29 bits: the first size past the 32-bit +/// bit-vector length encoding, so this pin holds the doors to a walk on +/// which no such encoding binds. #[test] -fn version_decode_at_view_cap() { +fn version_decode_at_build_cap() { assert_eq!( call1("pin_version_decode", 67_108_864), Outcome::Value(8 * 67_108_864 - 8), @@ -65,13 +64,12 @@ fn version_decode_at_view_cap() { /// A valid 67108865-byte version encoding decodes correctly on wasm32, /// returning its exact live bit length. /// -/// One byte past the borrowed view's silent-empty boundary, this is the -/// first size whose view construction `bitvec`'s element-count guard -/// refuses by panic. Together with the pin one byte below, this holds the -/// doors clear of both failure genres the borrowed-view cap produces (the -/// silent empty view, then the panic). +/// One byte past the length-encoding boundary's silent-wrap size; together +/// with the pin one byte below, this holds the doors clear of both failure +/// genres a 2^29-bit length encoding produces (a silently empty view, then +/// an element-count guard panic). #[test] -fn version_decode_past_view_cap() { +fn version_decode_past_build_cap() { assert_eq!( call1("pin_version_decode", 67_108_865), Outcome::Value(8 * 67_108_865 - 8), @@ -185,219 +183,251 @@ fn rank_decode_past_backend_bit_capacity_traps() { /// A valid composite key — the rank stream, then the version whose rank it /// is — decodes through the byte door `Ranked::decode` at the largest -/// version size whose whole-stream bit view is constructible on wasm32. +/// version size below the build buffer's length-encoding boundary. /// /// The door re-derives the version's rank to verify the key, and that fold -/// walks the version through its borrowed view: this is the composite -/// door's lower adjacency witness, one byte below the view cap. +/// walks the version through the crate-owned view: the lower adjacency +/// witness of the boundary straddle the two pins above it complete. #[test] -fn ranked_decode_below_view_cap() { +fn ranked_decode_below_build_cap() { assert_eq!( - call1("pin_ranked_decode", VIEW_CAP_BYTES), + call1("pin_ranked_decode", BUILD_CAP_BYTES), Outcome::Value(0) ); } -/// PINNED AS FOUND: a valid composite key whose version component is -/// 67108864 bytes traps in the byte door `Ranked::decode` on wasm32 -/// instead of decoding. +/// A valid composite key whose version component is 67108864 bytes — +/// exactly 2^29 bits — decodes through the byte door `Ranked::decode` on +/// wasm32. /// -/// The component is exactly 2^29 bits: the first size past the borrowed -/// view's encoding. -/// The rank and version components both parse (the version door's own walk -/// is byte-backed and admits streams to 512 MiB); the door then re-derives -/// the version's rank through its borrowed bit view, which is -/// unconstructible at this size, and the walk panics. Unconditional 32-bit -/// correctness requires every storable key to decode; the cure flips this -/// pin to the correct-value assertion. -#[test] -fn ranked_decode_at_view_cap_traps() { +/// The door's rank re-derivation walks the version through the crate-owned +/// view, whose `u64` live length is exact at every storable size, so the +/// composite door admits every storable key: the boundary straddle's +/// middle witness, beside `ranked_decode_below_build_cap`. +#[test] +fn ranked_decode_at_build_cap() { assert_eq!( - call1("pin_ranked_decode", VIEW_CAP_BYTES + 1), - Outcome::Trapped(Trap::UnreachableCodeReached), + call1("pin_ranked_decode", BUILD_CAP_BYTES + 1), + Outcome::Value(0), ); } -/// PINNED AS FOUND: a valid composite key whose version component is -/// 67108865 bytes traps in `Ranked::decode` on wasm32 instead of decoding. -/// -/// One byte past the view-cap boundary, the view construction fails by the -/// element-count guard rather than the length encoding. Together with the -/// pin one byte below, this holds both failure genres of the borrowed-view -/// cap to the same committed baseline the cure must move. +/// A valid composite key whose version component is 67108865 bytes decodes +/// through `Ranked::decode` on wasm32: the boundary straddle's upper +/// witness. #[test] -fn ranked_decode_past_view_cap_traps() { +fn ranked_decode_past_build_cap() { assert_eq!( - call1("pin_ranked_decode", VIEW_CAP_BYTES + 2), - Outcome::Trapped(Trap::UnreachableCodeReached), + call1("pin_ranked_decode", BUILD_CAP_BYTES + 2), + Outcome::Value(0), ); } +/// A valid composite key decodes through the byte door `Ranked::decode` +/// with a 256 MiB version component, checking the decoded version's bytes +/// round-trip. +/// +/// The upward exactness spot-check on the composite door's rank +/// re-derivation: the fold's numerator (~128 MiB of value bits) sits well +/// inside the big-integer backend's 32-bit capacity, so the whole key is +/// priced by its own size, hundreds of megabytes into the storable range. +#[test] +fn ranked_decode_deep_in_storable_range() { + assert_eq!(call1("pin_ranked_decode", 268_435_456), Outcome::Value(0),); +} + /// A valid composite key decodes through the borsh door -/// `Ranked::deserialize_reader` at the largest version size whose -/// whole-stream bit view is constructible on wasm32, consuming exactly its -/// own bytes. +/// `Ranked::deserialize_reader` at the largest version size below the +/// build buffer's length-encoding boundary, consuming exactly its own +/// bytes. /// /// The streaming door runs the same rank re-derivation as the byte door: -/// this is its lower adjacency witness. +/// the boundary straddle's lower witness. #[test] -fn ranked_borsh_below_view_cap() { - assert_eq!(call1("pin_ranked_borsh", VIEW_CAP_BYTES), Outcome::Value(0)); +fn ranked_borsh_below_build_cap() { + assert_eq!( + call1("pin_ranked_borsh", BUILD_CAP_BYTES), + Outcome::Value(0) + ); } -/// PINNED AS FOUND: a valid composite key whose version component is -/// 67108864 bytes — exactly 2^29 bits — traps in the borsh door -/// `Ranked::deserialize_reader` on wasm32 instead of deserializing. +/// A valid composite key whose version component is 67108864 bytes — +/// exactly 2^29 bits — deserializes through the borsh door +/// `Ranked::deserialize_reader` on wasm32, consuming exactly its own +/// bytes. /// -/// The streaming reader parses both components byte-backed; the door's -/// rank re-derivation then walks the version through its borrowed bit -/// view, unconstructible at this size. The cure flips this pin to the -/// correct-value assertion. +/// The streaming reader parses both components byte-backed and the rank +/// re-derivation walks the version through the crate-owned view: the +/// boundary straddle's middle witness. #[test] -fn ranked_borsh_at_view_cap_traps() { +fn ranked_borsh_at_build_cap() { assert_eq!( - call1("pin_ranked_borsh", VIEW_CAP_BYTES + 1), - Outcome::Trapped(Trap::UnreachableCodeReached), + call1("pin_ranked_borsh", BUILD_CAP_BYTES + 1), + Outcome::Value(0), ); } -/// PINNED AS FOUND: a valid composite key whose version component is -/// 67108865 bytes — the view cap's element-count-guard genre — traps in -/// the borsh door `Ranked::deserialize_reader` on wasm32. -/// -/// The second genre's committed baseline beside the pin one byte below. +/// A valid composite key whose version component is 67108865 bytes +/// deserializes through the borsh door on wasm32: the boundary straddle's +/// upper witness. #[test] -fn ranked_borsh_past_view_cap_traps() { +fn ranked_borsh_past_build_cap() { assert_eq!( - call1("pin_ranked_borsh", VIEW_CAP_BYTES + 2), - Outcome::Trapped(Trap::UnreachableCodeReached), + call1("pin_ranked_borsh", BUILD_CAP_BYTES + 2), + Outcome::Value(0), ); } +/// A valid composite key deserializes through the borsh door with a +/// 128 MiB version component: the streaming door's upward exactness +/// spot-check, deep in the storable range. +#[test] +fn ranked_borsh_deep_in_storable_range() { + assert_eq!(call1("pin_ranked_borsh", 134_217_728), Outcome::Value(0),); +} + /// A valid coincident span — two byte-equal version streams — decodes /// through the borsh door `Span::deserialize_reader` at the largest `lo` -/// size whose whole-stream bit view is constructible on wasm32. +/// size below the build buffer's length-encoding boundary. /// /// The door consumes exactly its own bytes. -/// It validates the second stream against `lo`'s borrowed view in -/// one fused admission walk: this is that walk's lower adjacency witness. -/// (The byte door `Span::decode` runs the same admission on raw bytes and -/// is exact to 512 MiB per component; only the streaming door reads -/// through the view.) +/// It validates the second stream against `lo`'s view in one fused +/// admission walk: the boundary straddle's lower witness. (The byte door +/// `Span::decode` runs the same admission and is exact to 512 MiB per +/// component.) #[test] -fn span_borsh_below_view_cap() { - assert_eq!(call1("pin_span_borsh", VIEW_CAP_BYTES), Outcome::Value(0)); +fn span_borsh_below_build_cap() { + assert_eq!(call1("pin_span_borsh", BUILD_CAP_BYTES), Outcome::Value(0)); } -/// PINNED AS FOUND: a valid coincident span whose `lo` component is -/// 67108864 bytes — exactly 2^29 bits — traps in the borsh door -/// `Span::deserialize_reader` on wasm32 instead of deserializing. +/// A valid coincident span whose `lo` component is 67108864 bytes — +/// exactly 2^29 bits — deserializes through the borsh door +/// `Span::deserialize_reader` on wasm32, consuming exactly its own bytes. /// -/// `lo` itself parses byte-backed; the door then opens `lo`'s borrowed bit -/// view for the dominance re-walk, unconstructible at this size. The byte -/// door decodes this same span exactly, so the boundary is the streaming -/// door's view, not the value. The cure flips this pin to the -/// correct-value assertion. +/// The dominance re-walk reads `lo` through the crate-owned view, exact at +/// every storable size: the boundary straddle's middle witness. #[test] -fn span_borsh_at_view_cap_traps() { +fn span_borsh_at_build_cap() { assert_eq!( - call1("pin_span_borsh", VIEW_CAP_BYTES + 1), - Outcome::Trapped(Trap::UnreachableCodeReached), + call1("pin_span_borsh", BUILD_CAP_BYTES + 1), + Outcome::Value(0), ); } -/// PINNED AS FOUND: a valid coincident span whose `lo` component is -/// 67108865 bytes — the view cap's element-count-guard genre — traps in -/// the borsh door `Span::deserialize_reader` on wasm32. -/// -/// The second genre's committed baseline beside the pin one byte below. +/// A valid coincident span whose `lo` component is 67108865 bytes +/// deserializes through the borsh door on wasm32: the boundary straddle's +/// upper witness. #[test] -fn span_borsh_past_view_cap_traps() { +fn span_borsh_past_build_cap() { assert_eq!( - call1("pin_span_borsh", VIEW_CAP_BYTES + 2), - Outcome::Trapped(Trap::UnreachableCodeReached), + call1("pin_span_borsh", BUILD_CAP_BYTES + 2), + Outcome::Value(0), ); } +/// A valid coincident span deserializes through the borsh door with +/// 128 MiB components: the admission walk's upward exactness spot-check, +/// a quarter-gigabyte composite deep in the storable range. +#[test] +fn span_borsh_deep_in_storable_range() { + assert_eq!(call1("pin_span_borsh", 134_217_728), Outcome::Value(0),); +} + /// Causal comparison decides a valid stored pair exactly at the largest -/// operand size whose whole-stream bit view is constructible on wasm32: -/// the taller lone leaf reads strictly greater both ways around. +/// operand size below the build buffer's length-encoding boundary: the +/// taller lone leaf reads strictly greater both ways around. /// -/// The comparison-class walk's lower adjacency witness: ordering reads -/// each operand through its borrowed view, with no decode door in front. +/// The comparison-class walk's boundary straddle, lower witness: ordering +/// reads each operand through the crate-owned view, with no decode door in +/// front. #[test] -fn version_cmp_below_view_cap() { - assert_eq!(call1("pin_version_cmp", VIEW_CAP_BYTES), Outcome::Value(0)); +fn version_cmp_below_build_cap() { + assert_eq!(call1("pin_version_cmp", BUILD_CAP_BYTES), Outcome::Value(0)); } -/// PINNED AS FOUND: causally comparing a valid stored 67108864-byte -/// version — exactly 2^29 bits, the first size past the borrowed view's -/// encoding — traps on wasm32 instead of ordering. +/// Causal comparison decides a valid stored pair exactly at 67108864 +/// bytes — exactly 2^29 bits — on wasm32: the taller lone leaf reads +/// strictly greater both ways around. /// -/// The byte doors admit and store this value exactly (its decode is pinned -/// green above), so the boundary is the walk surface's borrowed view, not -/// the value: a stored version this size cannot be compared at all. -/// Unconditional 32-bit correctness requires ordering to be exact at every -/// storable size; the cure flips this pin to the correct-value assertion. +/// The crate-owned view carries a `u64` live length, so the comparison +/// sweep is exact at every storable size: the boundary straddle's middle +/// witness, beside `version_cmp_below_build_cap`. #[test] -fn version_cmp_at_view_cap_traps() { +fn version_cmp_at_build_cap() { assert_eq!( - call1("pin_version_cmp", VIEW_CAP_BYTES + 1), - Outcome::Trapped(Trap::UnreachableCodeReached), + call1("pin_version_cmp", BUILD_CAP_BYTES + 1), + Outcome::Value(0), ); } -/// PINNED AS FOUND: causally comparing a valid stored 67108865-byte -/// version — the view cap's element-count-guard genre — traps on wasm32. -/// -/// The second genre's committed baseline beside the pin one byte below. +/// Causal comparison decides a valid stored 67108865-byte pair exactly on +/// wasm32: the boundary straddle's upper witness. #[test] -fn version_cmp_past_view_cap_traps() { +fn version_cmp_past_build_cap() { assert_eq!( - call1("pin_version_cmp", VIEW_CAP_BYTES + 2), - Outcome::Trapped(Trap::UnreachableCodeReached), + call1("pin_version_cmp", BUILD_CAP_BYTES + 2), + Outcome::Value(0), ); } -/// Join emits a covered pair exactly at the largest operand size whose -/// whole-stream bit view is constructible on wasm32: the taller lone leaf -/// joined with a short one reproduces the taller, byte for byte. +/// Causal comparison decides a valid stored pair exactly at 512 MiB — the +/// largest storable stream on a 32-bit target — both ways around. +/// +/// The comparison class's upward exactness spot-check at the storage +/// bound itself: the operand's live length (2^32 - 8 bits) is within one +/// marker byte of `usize::MAX`, so this also exercises the view's `u64` +/// length arithmetic at the very top of the storable range. +#[test] +fn version_cmp_at_storage_bound() { + assert_eq!(call1("pin_version_cmp", 536_870_912), Outcome::Value(0),); +} + +/// Join emits a covered pair exactly at the largest operand size below the +/// build buffer's length-encoding boundary: the taller lone leaf joined +/// with a short one reproduces the taller, byte for byte. /// /// The join-class walk's lower adjacency witness — and its emission /// rebuilds the full-size output, so the pin also witnesses the build /// buffer just below its own cap. #[test] -fn version_join_below_view_cap() { +fn version_join_below_build_cap() { assert_eq!( - call1("pin_version_join_covering", VIEW_CAP_BYTES), + call1("pin_version_join_covering", BUILD_CAP_BYTES), Outcome::Value(0), ); } /// PINNED AS FOUND: joining a valid stored 67108864-byte version — -/// exactly 2^29 bits — with a small one traps on wasm32 instead of -/// emitting the covered result. -/// -/// The join walks both operands through their borrowed views before its -/// merge emission; the big operand's view is unconstructible at this -/// size. The cure flips this pin to the correct-value assertion. +/// exactly 2^29 bits — with a small one it covers traps on wasm32 instead +/// of emitting the covered result. +/// +/// The walk itself is exact at this size (the comparison pins beside this +/// one decide the same operands both ways around); the trap is the +/// emission's output side: the covered join reproduces the big operand, a +/// finished stream whose element-rounded bit count exceeds the build +/// buffer's `usize::MAX >> 3`-bit length encoding at the freeze seam — +/// the same boundary `version_join_emit_at_build_cap_traps` pins from its +/// two-operand family. The cure is the crate-owned build buffer, which +/// flips this pin to the correct-value assertion. #[test] -fn version_join_at_view_cap_traps() { +fn version_join_at_build_cap_traps() { assert_eq!( - call1("pin_version_join_covering", VIEW_CAP_BYTES + 1), + call1("pin_version_join_covering", BUILD_CAP_BYTES + 1), Outcome::Trapped(Trap::UnreachableCodeReached), ); } -/// PINNED AS FOUND: joining a valid stored 67108865-byte version — the -/// view cap's element-count-guard genre — with a small one traps on -/// wasm32. +/// PINNED AS FOUND: joining a valid stored 67108865-byte version with a +/// small one it covers traps on wasm32. /// -/// The second genre's committed baseline beside the pin one byte below. +/// The output boundary's second witness, one byte further: the covered +/// output's live bit count itself exceeds the build buffer's length +/// encoding. The cure is the crate-owned build buffer, beside +/// `version_join_at_build_cap_traps`. #[test] -fn version_join_past_view_cap_traps() { +fn version_join_past_build_cap_traps() { assert_eq!( - call1("pin_version_join_covering", VIEW_CAP_BYTES + 2), + call1("pin_version_join_covering", BUILD_CAP_BYTES + 2), Outcome::Trapped(Trap::UnreachableCodeReached), ); } @@ -407,12 +437,11 @@ fn version_join_past_view_cap_traps() { /// /// That is one bit under the 67108863 whole output bytes the finished /// stream's bit-vector adoption can represent, from two operands each -/// comfortably under the walk surface's per-operand bound. The operands -/// are complementary two-leaf skylines (~50 MB and ~42 MB) whose join -/// concatenates: the output outgrows both inputs, so this witnesses the -/// emitter's output-side boundary from below, independent of any -/// per-operand cap. The returned observation is the output's exact live -/// bit length. +/// comfortably under 64 MiB. The operands are complementary two-leaf +/// skylines (~50 MB and ~42 MB) whose join concatenates: the output +/// outgrows both inputs, so this witnesses the emitter's output-side +/// boundary from below, independent of any operand size. The returned +/// observation is the output's exact live bit length. #[test] fn version_join_emit_below_build_cap() { assert_eq!( diff --git a/tools/mutantcheck-expected.json b/tools/mutantcheck-expected.json index 5755dfb7..8b038cc3 100644 --- a/tools/mutantcheck-expected.json +++ b/tools/mutantcheck-expected.json @@ -25,7 +25,7 @@ "listed": 1, "suppressed": 1 }, - "grow\\.rs:457:38: replace < with <= in recode": { + "grow\\.rs:468:38: replace < with <= in recode": { "listed": 1, "suppressed": 1 }, From f6c0a6a4cde6064a895b4868adcffa4523788375 Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 19:38:10 -0400 Subject: [PATCH 13/20] before: tighten the skyline-decode heap envelopes to the once-allocated copy The decode wrap allocates its copy once, exactly sized plus the marker bit, so the peak heap over each decode scenario drops to the copy itself. Parent-measured on the same idle machine, old -> new (bytes, per scenario): dense 98304 -> 49152, cliff 3072 -> 1793, wide tooth 196608 -> 100049, alt spine 98304 -> 49152; hugeleaf is unmoved at 66752 (its peak is the wide payload arithmetic, not the copy). The ceilings re-pin at the same proportional headroom: dense/alt spine 122880 -> 61440, cliff 3840 -> 2250, wide tooth 245760 -> 125100; hugeleaf keeps 83440. Limb, scan, and segment columns are unmoved in every scenario, so those pins and the limb floors stand. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/tests/meter.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/before/tests/meter.rs b/crates/before/tests/meter.rs index a0d1ebba..99ac99c0 100644 --- a/crates/before/tests/meter.rs +++ b/crates/before/tests/meter.rs @@ -293,11 +293,11 @@ mod envelope { // Skyline decoder rows: validation plus the wrap into storage — the // stored coding is the skyline stream itself, so decode materializes // nothing beyond the copy and stays priced by the wire input. - pub const SKYLINE_DECODE_DENSE: SweepEnvelope = sweep_envelope(122_880, 0, 0, 468_758, 0); // decode is validate + wrap - pub const SKYLINE_DECODE_CLIFF: SweepEnvelope = sweep_envelope(3_840, 0, 88, 17_923, 52); // decode is validate + wrap - pub const SKYLINE_DECODE_WIDE_TOOTH: SweepEnvelope = sweep_envelope(245_760, 0, 29_509, 1_000_480, 17_705); // decode is validate + wrap; the wrap's copy prices the wide payloads + pub const SKYLINE_DECODE_DENSE: SweepEnvelope = sweep_envelope(61_440, 0, 0, 468_758, 0); // decode is validate + wrap: the wrap allocates the copy once, exactly sized + pub const SKYLINE_DECODE_CLIFF: SweepEnvelope = sweep_envelope(2_250, 0, 88, 17_923, 52); // decode is validate + wrap: the wrap allocates the copy once, exactly sized + pub const SKYLINE_DECODE_WIDE_TOOTH: SweepEnvelope = sweep_envelope(125_100, 0, 29_509, 1_000_480, 17_705); // decode is validate + wrap; the once-allocated copy prices the wide payloads pub const SKYLINE_DECODE_HUGELEAF: SweepEnvelope = sweep_envelope( 83_440, 0, 2_443, 312_503, 1_465); // decode is validate + wrap - pub const SKYLINE_DECODE_ALT_SPINE: SweepEnvelope = sweep_envelope(122_880, 0, 0, 468_758, 0); // decode is validate + wrap + pub const SKYLINE_DECODE_ALT_SPINE: SweepEnvelope = sweep_envelope(61_440, 0, 0, 468_758, 0); // decode is validate + wrap: the wrap allocates the copy once, exactly sized } // ─── meter liveness canaries ──────────────────────────────────────────────── From b802e1b4dc29934c3c13dc1ad1e12ecf1cc4399d Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 20:08:13 -0400 Subject: [PATCH 14/20] before: disambiguate the skyline encode doc link The internal-docs build denies the bare link as ambiguous against the encode submodule; name the function form explicitly. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before/src/version/skyline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/before/src/version/skyline.rs b/crates/before/src/version/skyline.rs index a806036d..38c0fb98 100644 --- a/crates/before/src/version/skyline.rs +++ b/crates/before/src/version/skyline.rs @@ -217,7 +217,7 @@ pub(crate) use validate::validate_prefix; pub(crate) use validate::validate_from; /// A build buffer's contents as a [`BitsView`]: the instrument surface's -/// bridge from [`encode`]'s buffer to the view the walk entries read. +/// bridge from [`encode()`](fn@encode)'s buffer to the view the walk entries read. /// /// Test- and meter-only, like the buffers it views. #[cfg(any(test, feature = "meter"))] From 83e61b4df9f85a29eee1383f929a3ed6958050ee Mon Sep 17 00:00:00 2001 From: finch Date: Tue, 18 Aug 2026 21:46:31 -0400 Subject: [PATCH 15/20] before: replace the bitvec build buffer with the crate-owned BitsBuf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build side's one remaining 32-bit boundary was the bit vector behind the emitters: its length encoding caps at usize::MAX >> 3 bits, so an emitting operation whose output crossed 2^29 bits (a covered join of a 64 MiB operand, a two-operand join concatenating past it) trapped at the freeze seam on wasm32 while every walk and door was already exact. BitsBuf is the one build buffer now: bytes beside a u64 live length, under two representation invariants every mutation maintains — the byte vector holds exactly the live bits' bytes, and dead bits in the final partial byte are zero (truncation masks the new final byte in O(1)). The byte image is therefore a function of the bit content alone: Eq is one memcmp, sealing appends only the marker, and the freeze seam adopts the allocation whole. PackedBuilder keeps its staged-register internals and word-parallel moves but runs its positions at u64 and hands its finished bytes straight to BitsBuf — no length-encoding conversion binds the hand-off on any target. The storable bound (usize-denominated bit positions in the frozen form) is asserted loudly at the freeze door, matching the decode door's existing contract. The gamma word path emits through BitsBuf::push_bits; bitvec leaves the production dependency graph (it stays a dev-dependency as emit_probe's external baseline). The build-history proptest family pins the invariants as a family: arbitrary interleavings of pushes, word appends, view copies, and truncations (byte-aligned, mid-byte, to-empty, multi-byte sheds) leave the buffer byte-identical to a clean rebuild of the surviving content at every intermediate state, freeze to the identical canonical spelling, and keep Eq and the canonical hash agreeing with bit-level equality; a second leg pins spelling injectivity across distinct contents. A transient no-zeroing mutant of truncate fails both legs (adequacy verified before landing; the mutant is not committed). wasm32-pins: the three output-side trap pins flip green in this same commit, per the pins file's own discipline — the covered joins at and past 2^29 bits now assert byte-identical emission, and the two-operand emit at the coordinate asserts its exact 536870905-bit output. The boundary prose across the pins re-denominates to the straddle coordinate (the bit count where a 32-bit length encoding would bind), which every surface is now exact across. The rank pins stay as found for the later phase that owns them. All 36 wasm32 pins pass; the resource-envelope suite passes with every pinned ceiling and floor untouched. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N --- crates/before-fuelscape/Cargo.lock | 40 -- crates/before/Cargo.toml | 4 +- crates/before/examples/emit_probe.rs | 25 +- crates/before/fuzz/Cargo.lock | 40 -- crates/before/fuzzfit/Cargo.lock | 40 -- crates/before/src/borsh_impls/tests.rs | 24 +- crates/before/src/clock/tests.rs | 6 +- crates/before/src/codec.rs | 14 +- crates/before/src/codec/bits.rs | 146 ++----- crates/before/src/codec/buf.rs | 390 ++++++++++++++++++ crates/before/src/codec/build.rs | 83 ++-- crates/before/src/codec/code.rs | 16 +- crates/before/src/codec/display.rs | 4 +- crates/before/src/codec/dsi.rs | 5 +- crates/before/src/codec/dsi/tests.rs | 20 +- crates/before/src/codec/gamma.rs | 33 +- crates/before/src/codec/literal.rs | 20 +- crates/before/src/codec/tests.rs | 276 ++++++++++--- crates/before/src/codec/text.rs | 14 +- crates/before/src/meter.rs | 174 ++++---- crates/before/src/meter/board/defect.rs | 8 +- crates/before/src/meter/board/family.rs | 14 +- crates/before/src/meter/tier2/tests.rs | 4 +- crates/before/src/party.rs | 26 +- crates/before/src/party/ops/build.rs | 24 +- crates/before/src/party/ops/compare.rs | 6 +- crates/before/src/party/ops/diff.rs | 23 +- crates/before/src/party/ops/split.rs | 20 +- crates/before/src/party/ops/sum.rs | 10 +- crates/before/src/party/ops/sum_split.rs | 30 +- crates/before/src/party/tests.rs | 58 +-- crates/before/src/testing/bridge.rs | 14 +- crates/before/src/testing/compactness.rs | 6 +- crates/before/src/testing/generators.rs | 2 +- crates/before/src/testing/snapshots.rs | 10 +- crates/before/src/version.rs | 2 +- crates/before/src/version/skyline.rs | 8 +- crates/before/src/version/skyline/admit.rs | 18 +- crates/before/src/version/skyline/build.rs | 12 +- .../before/src/version/skyline/build/tests.rs | 14 +- crates/before/src/version/skyline/decode.rs | 2 +- crates/before/src/version/skyline/emit.rs | 28 +- .../before/src/version/skyline/emit/tests.rs | 12 +- crates/before/src/version/skyline/encode.rs | 6 +- crates/before/src/version/skyline/fill.rs | 12 +- .../before/src/version/skyline/fill/fuse.rs | 10 +- .../before/src/version/skyline/fill/tests.rs | 48 +-- crates/before/src/version/skyline/grow.rs | 40 +- .../before/src/version/skyline/grow/tests.rs | 6 +- crates/before/src/version/skyline/literal.rs | 25 +- .../src/version/skyline/masked/tests.rs | 6 +- crates/before/src/version/skyline/query.rs | 6 +- .../before/src/version/skyline/query/tests.rs | 16 +- .../src/version/skyline/signed/tests.rs | 12 +- .../before/src/version/skyline/sweep/tests.rs | 6 +- crates/before/src/version/skyline/tests.rs | 73 ++-- crates/before/src/version/skyline/text.rs | 36 +- .../before/src/version/skyline/text/tests.rs | 10 +- crates/before/src/version/skyline/validate.rs | 4 +- crates/before/src/version/tests.rs | 2 +- crates/before/surfacecheck/Cargo.lock | 40 -- crates/before/tests/meter.rs | 12 +- crates/before/wasm32-pins/Cargo.lock | 40 -- crates/before/wasm32-pins/guest/src/lib.rs | 10 +- .../before/wasm32-pins/harness/tests/pins.rs | 136 +++--- 65 files changed, 1300 insertions(+), 981 deletions(-) create mode 100644 crates/before/src/codec/buf.rs diff --git a/crates/before-fuelscape/Cargo.lock b/crates/before-fuelscape/Cargo.lock index 200afdcc..fb035dc8 100644 --- a/crates/before-fuelscape/Cargo.lock +++ b/crates/before-fuelscape/Cargo.lock @@ -106,7 +106,6 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" name = "before" version = "0.1.0" dependencies = [ - "bitvec", "bytes", "dashu-int", "dsi-bitstream", @@ -160,18 +159,6 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" -[[package]] -name = "bitvec" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -599,12 +586,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures" version = "0.3.33" @@ -1100,12 +1081,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.7" @@ -1405,12 +1380,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "target-lexicon" version = "0.13.5" @@ -1828,15 +1797,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zerocopy" version = "0.8.55" diff --git a/crates/before/Cargo.toml b/crates/before/Cargo.toml index d0434c14..be6a6214 100644 --- a/crates/before/Cargo.toml +++ b/crates/before/Cargo.toml @@ -21,7 +21,6 @@ rustdoc-args = ["--html-in-header", "docs/fuelscape-header.html"] serde_json = "1" [dependencies] -bitvec = "1" bytes = "1" dashu-int = { version = "0.5", default-features = false, features = ["std"] } suanpan = { path = "../suanpan" } @@ -32,6 +31,9 @@ serde = { version = "1", optional = true, default-features = false, features = [ borsh = { version = "1", optional = true } [dev-dependencies] +# The emit_probe example's external comparison baseline: the production +# build buffer is the crate-owned BitsBuf, and nothing shipped links bitvec. +bitvec = "1" proptest = "1" serde_json = "1" criterion = "0.5" diff --git a/crates/before/examples/emit_probe.rs b/crates/before/examples/emit_probe.rs index 3ba72fa7..08576931 100644 --- a/crates/before/examples/emit_probe.rs +++ b/crates/before/examples/emit_probe.rs @@ -1,10 +1,11 @@ //! Primitive-cost probe for the emission machinery. //! -//! Measures the raw output-side primitives the join/tick sweeps spend -//! their cycles in (per-bit `bitvec` pushes, per-leaf heap-allocated -//! code buffers, bit-addressed splices) against word-buffered -//! equivalents, on the same workload shape (~5k leaves, 3-9 bit codes, -//! 75k output bits). +//! Measures candidate output-side primitive shapes (per-bit pushes into a +//! general bit vector — the external `bitvec` baseline — per-leaf +//! heap-allocated code buffers, bit-addressed splices) against the +//! word-buffered equivalents the crate's own builder ships, on the +//! join/tick sweeps' workload shape (~5k leaves, 3-9 bit codes, 75k +//! output bits). //! //! Usage: cargo run -p before --profile bench --example emit_probe @@ -12,7 +13,7 @@ use bitvec::prelude::*; use std::hint::black_box; use std::time::Instant; -type BitsMut = BitVec; +type BaselineBits = BitVec; const LEAVES: usize = 5000; @@ -97,7 +98,7 @@ fn main() { // 1. Per-bit bitvec push: the current PackedBuilder discipline // (1 flag push + per-bit code pushes per leaf). bench("bitvec push per bit", 2000, || { - let mut out: BitsMut = BitVec::with_capacity(total_bits); + let mut out: BaselineBits = BitVec::with_capacity(total_bits); for &(code, len) in &codes { out.push(true); for i in (0..len).rev() { @@ -111,9 +112,9 @@ fn main() { // 2. Current per-leaf heap code + extend_from_bitslice splice: what // gamma_code + SkylineBuilder::leaf actually do. bench("bitvec alloc code + splice per leaf", 2000, || { - let mut out: BitsMut = BitVec::with_capacity(total_bits); + let mut out: BaselineBits = BitVec::with_capacity(total_bits); for &(code, len) in &codes { - let mut c: BitsMut = BitsMut::new(); + let mut c: BaselineBits = BaselineBits::new(); for i in (0..len).rev() { c.push((code >> i) & 1 == 1); } @@ -137,8 +138,8 @@ fn main() { // 4. Bulk copy comparison: one 37.5k-bit misaligned splice, bitvec vs // word shifts (the tick grow-path verbatim copy). - let src: BitsMut = { - let mut v = BitsMut::new(); + let src: BaselineBits = { + let mut v = BaselineBits::new(); let mut state = 7u64; for _ in 0..37_500 { state = state.wrapping_mul(6364136223846793005).wrapping_add(1); @@ -150,7 +151,7 @@ fn main() { "bitvec extend_from_bitslice 37.5kbit misaligned", 2000, || { - let mut out: BitsMut = BitVec::with_capacity(38_000); + let mut out: BaselineBits = BitVec::with_capacity(38_000); out.push(true); // force misalignment out.extend_from_bitslice(&src[3..]); out.len() diff --git a/crates/before/fuzz/Cargo.lock b/crates/before/fuzz/Cargo.lock index cdd2f16f..221345d4 100644 --- a/crates/before/fuzz/Cargo.lock +++ b/crates/before/fuzz/Cargo.lock @@ -12,7 +12,6 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" name = "before" version = "0.1.0" dependencies = [ - "bitvec", "borsh", "bytes", "dashu-int", @@ -36,18 +35,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "borsh" version = "1.8.0" @@ -143,12 +130,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "getrandom" version = "0.3.4" @@ -253,12 +234,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rustversion" version = "1.0.23" @@ -349,12 +324,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "thiserror" version = "2.0.18" @@ -396,15 +365,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zmij" version = "1.0.23" diff --git a/crates/before/fuzzfit/Cargo.lock b/crates/before/fuzzfit/Cargo.lock index 405b3864..d8c9ef03 100644 --- a/crates/before/fuzzfit/Cargo.lock +++ b/crates/before/fuzzfit/Cargo.lock @@ -50,7 +50,6 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" name = "before" version = "0.1.0" dependencies = [ - "bitvec", "bytes", "dashu-int", "dsi-bitstream", @@ -81,18 +80,6 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" -[[package]] -name = "bitvec" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -421,12 +408,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures" version = "0.3.33" @@ -811,12 +792,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.7" @@ -1084,12 +1059,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "target-lexicon" version = "0.13.5" @@ -1424,15 +1393,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zerocopy" version = "0.8.55" diff --git a/crates/before/src/borsh_impls/tests.rs b/crates/before/src/borsh_impls/tests.rs index bbc87421..281bd8b4 100644 --- a/crates/before/src/borsh_impls/tests.rs +++ b/crates/before/src/borsh_impls/tests.rs @@ -4,7 +4,7 @@ use proptest::prelude::*; use proptest::test_runner::TestCaseError; use super::decode_error; -use crate::codec::{self, BitCursor, BitsMut}; +use crate::codec::{self, BitCursor, BitsBuf}; use crate::error::Decode; use crate::span::Span; use crate::testing::bridge::{from_oracle_party, from_oracle_version}; @@ -304,7 +304,7 @@ proptest! { /// byte at a time, per-bit reads only, and the default per-bit `read_int`. struct BitwiseReaderCursor<'a, R> { reader: &'a mut R, - bits: BitsMut, + bits: BitsBuf, position: usize, } @@ -312,14 +312,12 @@ impl BitCursor for BitwiseReaderCursor<'_, R> { type Error = Decode; fn read_bit(&mut self) -> Result { - if self.position == self.bits.len() { + if self.position as u64 == self.bits.len() { let mut byte = [0]; self.reader.read_exact(&mut byte).map_err(Decode::Io)?; - use bitvec::view::BitView; - self.bits - .extend_from_bitslice(byte.view_bits::()); + self.bits.push_bits(u64::from(byte[0]), 8); } - let bit = self.bits[self.position]; + let bit = self.bits.get(self.position as u64); self.position += 1; Ok(bit) } @@ -354,14 +352,14 @@ fn reference_consume_padding( fn reference_version(reader: &mut R) -> Result { let mut cursor = BitwiseReaderCursor { reader, - bits: BitsMut::new(), + bits: BitsBuf::new(), position: 0, }; crate::version::skyline::validate_from(&mut cursor)?; let position = cursor.position; reference_consume_padding(&mut cursor)?; let mut bits = cursor.bits; - bits.truncate(position); + bits.truncate(position as u64); Ok(Version::from_bits(bits)) } @@ -374,14 +372,14 @@ fn reference_version(reader: &mut R) -> Result { fn reference_party(reader: &mut R) -> Result { let mut cursor = BitwiseReaderCursor { reader, - bits: BitsMut::new(), + bits: BitsBuf::new(), position: 0, }; codec::parse_id_from(&mut cursor)?; let position = cursor.position; reference_consume_padding(&mut cursor)?; let mut bits = cursor.bits; - bits.truncate(position); + bits.truncate(position as u64); Ok(Party::from_bits(bits)) } @@ -522,14 +520,14 @@ fn reference_span(reader: &mut R) -> Result, Decode> { let lo = reference_version(reader)?; let mut cursor = BitwiseReaderCursor { reader, - bits: BitsMut::new(), + bits: BitsBuf::new(), position: 0, }; let admission = validate_dominating_from((lo.view()).live(), &mut cursor)?; let position = cursor.position; reference_consume_padding(&mut cursor)?; let mut bits = cursor.bits; - bits.truncate(position); + bits.truncate(position as u64); let hi = match admission { Admission::Refuted => return Err(Decode::NotCanonical), Admission::Equal => lo.clone(), diff --git a/crates/before/src/clock/tests.rs b/crates/before/src/clock/tests.rs index ee3be13b..942814a1 100644 --- a/crates/before/src/clock/tests.rs +++ b/crates/before/src/clock/tests.rs @@ -792,11 +792,11 @@ proptest! { // // `Clock::encode` lays the id directly before the event, so the event begins at // a generally non-byte-aligned bit offset. A `decode` that extracts the event -// with `slice.to_bitvec()` keeps that head offset (rather than shifting to bit -// 0), leaving the recovered `Version`'s packed stream non-canonical: +// as an offset slice of the clock's buffer (rather than copying it down to +// bit 0) leaves the recovered `Version`'s packed stream non-canonical: // `version().encode()` mis-packs it and `Version::decode` then disagrees. // Whole-clock round-trips hide this, because `Clock::encode` re-aligns each -// component via `extend_from_bitslice`; the bug only shows when a component +// component as it copies it in; the bug only shows when a component // extracted from a decoded clock is encoded on its own. /// The seed's id is two bits, so its event starts at a non-byte-aligned offset. diff --git a/crates/before/src/codec.rs b/crates/before/src/codec.rs index 273ffa92..ee214c13 100644 --- a/crates/before/src/codec.rs +++ b/crates/before/src/codec.rs @@ -19,6 +19,7 @@ pub(crate) mod base; mod bits; +mod buf; mod build; mod code; mod cursor; @@ -38,19 +39,20 @@ mod tests; #[cfg(feature = "limb-meter")] pub(crate) use base::limb_meter; pub use base::Base; -pub(crate) use bits::{ - built_view, canonical_eq, canonical_hash, extend_from_view, padding_is_canonical, - require_marker_padding, -}; +pub(crate) use bits::{canonical_eq, canonical_hash, padding_is_canonical, require_marker_padding}; +#[cfg(test)] +pub(crate) use buf::bits_buf; +pub(crate) use buf::{built_view, extend_from_view}; // Production streams seal at the freeze seam (`Bits::freeze`); the // standalone form serves the buffers that stay build-side, all of them // meter/test instruments producing decodable bytes (the generators' // packed outputs, the board's defect shapes, the snapshot corpus). #[cfg(any(test, feature = "meter"))] -pub(crate) use bits::seal_padding; +pub(crate) use buf::seal_padding; // The storage forms are `pub` (the enclosing module is not), so the // meter surface can re-export them for the resource-envelope suite. -pub use bits::{Bits, BitsMut, BitsView}; +pub use bits::{Bits, BitsView}; +pub use buf::BitsBuf; pub(crate) use build::PackedBuilder; pub(crate) use code::Code; pub(crate) use cursor::{BitCursor, SliceCursor}; diff --git a/crates/before/src/codec/bits.rs b/crates/before/src/codec/bits.rs index 375e7578..099510e0 100644 --- a/crates/before/src/codec/bits.rs +++ b/crates/before/src/codec/bits.rs @@ -1,6 +1,9 @@ -//! The packed bit-stream storage forms: the mutable build buffer ([`BitsMut`]), -//! the refcounted frozen at-rest form ([`Bits`]), and the canonicality helpers -//! both rest on. +//! The packed bit-stream storage forms: the refcounted frozen at-rest form +//! ([`Bits`]) and the borrowed live-bits view ([`BitsView`]). +//! +//! The canonicality helpers both forms rest on live here too. The mutable +//! build-side form lives in the sibling `buf` module; [`Bits::freeze`] is +//! the seam between the two. //! //! # The identity fast-path ladder, and where each rung belongs //! @@ -59,21 +62,11 @@ use core::hash::Hasher; -use bitvec::prelude::*; use bytes::Bytes; +use super::buf::{seal_padding, BitsBuf}; use crate::error::Decode; -/// The mutable build-side form of a packed bit stream: a -/// most-significant-bit-first bit vector over bytes. -/// -/// Every emitter and builder writes into one of these (the crate's -/// packed-stream builder wraps one with the metered move set); a finished -/// stream freezes into the at-rest [`Bits`] at the storage seam. The -/// `Bytes`/`BytesMut` naming echo is deliberate: `BitsMut` is where mutation -/// happens, [`Bits`] is the shared, immutable result. -pub type BitsMut = BitVec; - /// The at-rest storage form of a `Party`/`Version`: the canonical packed /// preorder bit stream, marker-padded to a byte boundary, over a refcounted /// byte buffer. @@ -114,15 +107,36 @@ impl Bits { /// the single gate between the mutable build-side world and the shared /// frozen one. /// - /// Seals the padding ([`seal_padding`]: the `1` marker, then zeroed dead - /// bits — see the type docs for what the marker underpins, and - /// [`seal_padding`] for why a tree op can leave the tail dirty), then - /// adopts the buffer without copying: [`BitVec::into_vec`] hands back the - /// underlying allocation and `Bytes::from(vec)` wraps it in place. - pub(crate) fn freeze(mut buf: BitsMut) -> Self { + /// Seals the padding ([`seal_padding`]: the `1` marker completing the + /// canonical `1 0*` tail the build buffer's zeroed-dead-bits invariant + /// already established — see the type docs for what the marker + /// underpins), then adopts the buffer's allocation whole: + /// `Bytes::from(vec)` wraps it in place, no copy. + /// + /// # Panics + /// + /// Panics when the sealed buffer's byte count exceeds what `usize` bit + /// positions can denominate — reachable only on targets narrower than + /// 64 bits, from an emission past 512 MiB of output. The build side + /// itself is exact to allocatable memory (its lengths are `u64`), but a + /// stored stream's live length is `usize`-denominated + /// ([`len`](Self::len) and the value types' `encoded_bits`), so a + /// stream past that bound has no in-memory form here and this check + /// keeps the failure at the door — the same bound the decode door + /// ([`from_canonical`](Self::from_canonical)) holds. On 64-bit targets + /// the bound is 2^61 bytes, which no allocator can hand over, so the + /// check is dead there. + pub(crate) fn freeze(mut buf: BitsBuf) -> Self { seal_padding(&mut buf); + let bytes = buf.into_bytes(); + assert!( + bytes.len() as u128 <= (usize::MAX as u128 + 1) / 8, + "stored streams denominate bit positions in usize: \ + a {}-byte buffer's bits do not fit this target's usize", + bytes.len(), + ); Bits { - bytes: Bytes::from(buf.into_vec()), + bytes: Bytes::from(bytes), } } @@ -185,10 +199,10 @@ impl Bits { /// [`live_bits`](Self::live_bits) converted to `usize`. /// /// The conversion is checked, not truncating, and cannot fail on a - /// constructed stream: every door bounds its buffer so the live length - /// fits `usize` ([`from_canonical`](Self::from_canonical) asserts it; - /// [`freeze`](Self::freeze)'s build buffer is bounded far below it by - /// the build buffer's own encoding). + /// constructed stream: both doors assert the same byte-count bound + /// ([`from_canonical`](Self::from_canonical) on adoption, + /// [`freeze`](Self::freeze) on sealing), under which every live length + /// fits `usize`. pub fn len(&self) -> usize { usize::try_from(self.live_bits()).expect("a constructed stream's live length fits usize") } @@ -395,11 +409,11 @@ impl<'a> BitsView<'a> { } /// The view's bits copied into a build buffer: the test suites' - /// bridge to `bitvec`-vocabulary assertions. + /// bridge to buffer-vocabulary assertions. #[cfg(test)] - pub(crate) fn to_bitvec(self) -> BitsMut { - let mut out = BitsMut::with_capacity(self.live as usize); - extend_from_view(&mut out, self, 0, self.live); + pub(crate) fn to_buf(self) -> BitsBuf { + let mut out = BitsBuf::with_capacity(self.live); + super::buf::extend_from_view(&mut out, self, 0, self.live); out } @@ -427,28 +441,6 @@ impl PartialEq for Bits { impl Eq for Bits {} -/// Seal a built stream's canonical padding: one `1` marker bit, then zeroed -/// dead bits to the byte boundary. -/// -/// Sealing makes the packed bytes ([`BitVec::as_raw_slice`]) the canonical wire -/// spelling — injective, byte-equal if and only if the bit content is equal. -/// -/// The zeroing is load-bearing on its own: the tree builders write into a -/// reused buffer, and a collapsing node (the party `sum`/`diff` ops, via -/// `IdBuilder::close_node`) `truncate`s it, shrinking the live length while -/// leaving the bits it shed in the final partial byte, where `as_raw_slice` -/// would expose them. The marker then pins the live length inside the sealed -/// byte. The empty stream seals to itself: no marker, no bytes. -/// [`Bits::freeze`] applies this at the storage seam; the standalone form seals -/// buffers that stay build-side — all of them meter/test instruments producing -/// decodable bytes. -pub(crate) fn seal_padding(bits: &mut BitsMut) { - if !bits.is_empty() { - bits.push(true); - } - bits.set_uninitialized(false); -} - /// Byte-level equality of two canonical stored streams: equal raw /// bytes, entered through the clone-identity fast path. /// @@ -500,58 +492,6 @@ pub(crate) fn padding_is_canonical(bits: &Bits) -> bool { } } -/// A build buffer's contents as a [`BitsView`]: the underlying bytes beside -/// the live bit length. -/// -/// A [`BitsMut`] always starts on byte 0 of its storage and its raw bytes -/// travel with it, so the view is a plain destructuring; bits past the live -/// length (a truncated buffer's shed tail) stay behind the view exactly as a -/// frozen stream's padding does. -pub(crate) fn built_view(bits: &BitsMut) -> BitsView<'_> { - BitsView::new(bits.as_raw_slice(), bits.len() as u64) -} - -/// Extend a build buffer with the bit range `start..end` copied verbatim -/// from a stored stream's view. -/// -/// The build-side copy seam of the operations that assemble their output -/// from input subtrees (the party split/sum families). The source range is -/// walked through bounded chunk views — never one whole-buffer borrowed -/// view, whose length encoding a storable stream can exceed on a 32-bit -/// target — so the copy is exact at every storable source size; the output -/// buffer's own bounds are the build side's, unchanged. -/// -/// # Panics -/// -/// `start..end` must be a range within the view's live length. -pub(crate) fn extend_from_view(out: &mut BitsMut, src: BitsView<'_>, start: u64, end: u64) { - assert!( - start <= end && end <= src.len(), - "copied range within the view's live length" - ); - let mut pos = start; - // Head bits to the source's byte boundary, one at a time (at most 7). - while pos < end && !pos.is_multiple_of(8) { - out.push(src.bit(pos)); - pos += 1; - } - // Whole-byte body, in bounded chunks: each chunk's borrowed bit view is - // far below any length-encoding bound on every target. - const CHUNK_BYTES: usize = 1024; - while end - pos >= 8 { - let at = (pos / 8) as usize; - let take_bytes = (((end - pos) / 8) as usize).min(CHUNK_BYTES); - let chunk = &src.bytes()[at..at + take_bytes]; - out.extend_from_bitslice(chunk.view_bits::()); - pos += take_bytes as u64 * 8; - } - // Tail bits (fewer than 8). - while pos < end { - out.push(src.bit(pos)); - pos += 1; - } -} - /// Require that the bits from `pos` to the buffer's end are exactly the /// canonical padding: one `1` marker bit, then zeros to the byte boundary. /// The byte decode doors' padding judge, over the raw stream bytes. diff --git a/crates/before/src/codec/buf.rs b/crates/before/src/codec/buf.rs new file mode 100644 index 00000000..94ace6c1 --- /dev/null +++ b/crates/before/src/codec/buf.rs @@ -0,0 +1,390 @@ +//! The mutable build-side form of a packed bit stream: [`BitsBuf`], the one +//! buffer every emitter, parser, and instrument writes into, and the sealing +//! step that hands a finished stream to the frozen storage form. +//! +//! # Representation +//! +//! A [`BitsBuf`] is the stream's bytes beside a `u64` live bit length, under +//! two invariants every mutation maintains: +//! +//! - **Exact bytes**: the byte vector holds exactly the live bits' bytes +//! (`live.div_ceil(8)` of them), never a trailing byte of shed content. +//! - **Zeroed dead bits**: the bits of the final partial byte at and past +//! the live length are zero. Truncation masks the new final byte in +//! `O(1)`; appends write into space the invariant already zeroed. +//! +//! Together they make the byte image a function of the bit content alone: +//! two buffers holding equal bits are byte-for-byte equal (so [`PartialEq`] +//! is one length check and one `memcmp`), and sealing a finished stream +//! ([`seal_padding`]) only appends the marker bit — the padding it completes +//! is already canonical, so the freeze seam adopts the allocation whole, +//! without a repair pass or a copy. +//! +//! # Widths +//! +//! Lengths and positions are `u64` on every target: the buffer is correct up +//! to allocatable memory, and no `usize`-denominated bit count anywhere in +//! the build path can wrap or bind on a 32-bit target. (A 32-bit `usize` +//! spelling of `bytes.len() * 8` wraps from 512 MiB of buffer — sizes a +//! 4 GiB address space allocates comfortably.) The one storable bound is the +//! frozen form's, checked at its door (`Bits::freeze`), never here. +//! +//! Byte *indexes* stay `usize`: an index into an allocated buffer fits the +//! target's address width by construction. + +use super::bits::BitsView; + +/// The mutable build-side form of a packed bit stream: bytes beside a `u64` +/// live bit length, dead bits zeroed. +/// +/// Every emitter and builder writes into one of these (the crate's +/// packed-stream builder wraps one with the metered move set); a finished +/// stream freezes into the at-rest `Bits` at the storage seam. The module doc +/// carries the representation invariants and the width discipline. +/// +/// Equality is bit-content equality, decided bytewise: the zeroed-dead-bits +/// invariant makes the byte image injective on contents. +#[derive(Clone, Default, PartialEq, Eq)] +pub struct BitsBuf { + /// The live bits' bytes, most-significant bit first: + /// `live.div_ceil(8)` bytes exactly, dead bits zero (the module doc's + /// invariants). + bytes: Vec, + /// The live bit length. + /// + /// `u64` increments from an allocatable buffer cannot wrap: every + /// stored bit occupies real memory, so the length is bounded by the + /// address space times eight, orders of magnitude under `u64::MAX`. + live: u64, +} + +impl BitsBuf { + /// An empty buffer: no bits, no bytes, no allocation. + pub(crate) fn new() -> Self { + BitsBuf::default() + } + + /// An empty buffer with room for `bits` bits before reallocation. + /// + /// The capacity is a hint: a request past the target's address space + /// allocates nothing up front, and the buffer still grows to whatever + /// the pushes actually demand. + pub(crate) fn with_capacity(bits: u64) -> Self { + BitsBuf { + bytes: Vec::with_capacity(usize::try_from(bits.div_ceil(8)).unwrap_or(0)), + live: 0, + } + } + + /// A buffer of `len` copies of `bit`. + pub(crate) fn repeat(bit: bool, len: u64) -> Self { + let whole = usize::try_from(len.div_ceil(8)).expect("a repeated buffer is allocatable"); + let mut this = BitsBuf { + bytes: vec![if bit { 0xFF } else { 0x00 }; whole], + live: len, + }; + this.mask_tail(); + this + } + + /// Adopt bytes a builder produced under this type's own invariants: + /// exactly `live.div_ceil(8)` bytes, dead bits zero. Debug-asserted. + pub(super) fn from_raw_parts(bytes: Vec, live: u64) -> Self { + let this = BitsBuf { bytes, live }; + debug_assert_eq!( + this.bytes.len() as u64, + live.div_ceil(8), + "adopted bytes hold exactly the live bits" + ); + debug_assert!(this.tail_is_zeroed(), "adopted dead bits are zero"); + this + } + + /// The live bit length. + pub fn len(&self) -> u64 { + self.live + } + + /// Whether the buffer holds no bits at all. + pub fn is_empty(&self) -> bool { + self.live == 0 + } + + /// The live bits' bytes: dead bits of the final partial byte read zero + /// (the module doc's invariant), so the image is the content's one + /// spelling. + pub fn as_raw_slice(&self) -> &[u8] { + &self.bytes + } + + /// The buffer's bytes, surrendered whole: the freeze seam's `O(1)` + /// hand-off into the frozen storage form. + pub(crate) fn into_bytes(self) -> Vec { + self.bytes + } + + /// The bit at `pos`. + /// + /// # Panics + /// + /// `pos` must be below the live length. + pub(crate) fn get(&self, pos: u64) -> bool { + assert!(pos < self.live, "bit read past the buffer's live length"); + self.bytes[(pos / 8) as usize] >> (7 - pos % 8) & 1 == 1 + } + + /// Overwrite the bit at `pos`. + /// + /// # Panics + /// + /// `pos` must be below the live length. + pub(crate) fn set(&mut self, pos: u64, bit: bool) { + assert!(pos < self.live, "bit write past the buffer's live length"); + let mask = 0x80 >> (pos % 8); + if bit { + self.bytes[(pos / 8) as usize] |= mask; + } else { + self.bytes[(pos / 8) as usize] &= !mask; + } + } + + /// The live bits, oldest first. + pub(crate) fn iter(&self) -> impl Iterator + '_ { + (0..self.live).map(|pos| self.get(pos)) + } + + /// The number of set live bits: a bytewise popcount, exact because the + /// dead bits are zero (the module doc's invariant). + pub(crate) fn count_ones(&self) -> u64 { + self.bytes.iter().map(|b| u64::from(b.count_ones())).sum() + } + + /// Append one bit. + pub(crate) fn push(&mut self, bit: bool) { + let within = (self.live % 8) as u32; + if within == 0 { + self.bytes.push(if bit { 0x80 } else { 0x00 }); + } else if bit { + // The target bit is zero (the invariant), so setting it is one OR. + *self.bytes.last_mut().expect("a partial byte exists") |= 0x80 >> within; + } + self.live += 1; + } + + /// Pop the newest bit. + pub(crate) fn pop(&mut self) -> Option { + let pos = self.live.checked_sub(1)?; + let bit = self.get(pos); + self.truncate(pos); + Some(bit) + } + + /// Append `len <= 64` bits, value-packed at the low end of `value` (bits + /// above `len` must be zero), most-significant first. + pub(crate) fn push_bits(&mut self, value: u64, len: u32) { + debug_assert!(len <= 64, "an append stages at most one machine word"); + debug_assert!( + len == 64 || value >> len == 0, + "append value has bits above its stated width" + ); + if len == 0 { + return; + } + // Reload the partial tail byte's live bits, merge in a double-word + // register, and write back whole bytes plus the new (zero-padded) + // partial byte. + let within = (self.live % 8) as u32; + let staged = if within == 0 { + 0 + } else { + u64::from(self.bytes.pop().expect("a partial byte exists") >> (8 - within)) + }; + let total = within + len; + let acc = (u128::from(staged) << len) | u128::from(value); + let aligned = (acc << (128 - total)).to_be_bytes(); + let whole = (total / 8) as usize; + self.bytes.extend_from_slice(&aligned[..whole]); + if !total.is_multiple_of(8) { + // The next byte carries the remaining bits at its top and zeros + // below: the dead-bits invariant by construction. + self.bytes.push(aligned[whole]); + } + self.live += u64::from(len); + } + + /// Append another buffer's bits, oldest first: + /// [`extend_from_view`] over the other buffer's whole view. + pub(crate) fn extend_from_buf(&mut self, other: &BitsBuf) { + extend_from_view(self, built_view(other), 0, other.len()); + } + + /// Roll the buffer back to `len` bits, discarding everything after: + /// the byte vector sheds the freed bytes and the new final partial + /// byte's dead bits are zeroed, both `O(1)` past the deallocation. + /// + /// # Panics + /// + /// Panics if `len` exceeds the current length: truncation only ever + /// shortens. + pub(crate) fn truncate(&mut self, len: u64) { + assert!( + len <= self.live, + "buffer truncation target {len} exceeds the {} bits held", + self.live, + ); + self.bytes.truncate(len.div_ceil(8) as usize); + self.live = len; + self.mask_tail(); + } + + /// Re-establish the zeroed-dead-bits invariant on the final partial + /// byte, after a truncation exposed formerly live bits as dead. + fn mask_tail(&mut self) { + let within = (self.live % 8) as u32; + if within != 0 { + *self.bytes.last_mut().expect("a partial byte exists") &= 0xFF << (8 - within); + } + } + + /// Whether the final partial byte's dead bits are zero: the invariant, + /// as a probe for the debug asserts. + fn tail_is_zeroed(&self) -> bool { + let within = (self.live % 8) as u32; + within == 0 || self.bytes.last().is_some_and(|b| b & (0xFF >> within) == 0) + } + + /// Append whole bytes: a `memcpy` when the live length is + /// byte-aligned, a two-shift merge per byte otherwise. + fn extend_bytes(&mut self, body: &[u8]) { + let within = (self.live % 8) as u32; + if within == 0 { + self.bytes.extend_from_slice(body); + } else { + self.bytes.reserve(body.len()); + for &b in body { + *self.bytes.last_mut().expect("a partial byte exists") |= b >> within; + // The shift zero-fills below the carried bits: the dead-bits + // invariant by construction. + self.bytes.push(b << (8 - within)); + } + } + self.live += body.len() as u64 * 8; + } +} + +/// Renders the live bits most-significant-first as `0`/`1`, the test +/// suites' failure-message spelling. +impl core::fmt::Debug for BitsBuf { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("BitsBuf[")?; + for pos in 0..self.live { + f.write_str(if self.get(pos) { "1" } else { "0" })?; + } + f.write_str("]") + } +} + +/// Collect bits into a buffer, oldest first: the test generators' +/// construction form. +impl FromIterator for BitsBuf { + fn from_iter>(iter: I) -> Self { + let mut out = BitsBuf::new(); + out.extend(iter); + out + } +} + +/// Append bits oldest-first: [`FromIterator`]'s in-place form. +impl Extend for BitsBuf { + fn extend>(&mut self, iter: I) { + for bit in iter { + self.push(bit); + } + } +} + +/// A [`BitsBuf`] literal for the test suites: `bits_buf![1, 0, 1]` builds +/// from listed bits, `bits_buf![1; 8]` repeats one. +#[cfg(test)] +macro_rules! bits_buf { + ($bit:literal; $n:expr) => { + $crate::codec::BitsBuf::repeat($bit != 0, $n) + }; + ($($bit:literal),+ $(,)?) => { + [$($bit != 0),+] + .into_iter() + .collect::<$crate::codec::BitsBuf>() + }; +} +#[cfg(test)] +pub(crate) use bits_buf; + +/// A build buffer's contents as a [`BitsView`]: the underlying bytes beside +/// the live bit length. +/// +/// A [`BitsBuf`]'s bits always start on byte 0 of its storage and its raw +/// bytes travel with it, so the view is a plain destructuring; the final +/// partial byte's dead bits read zero (the buffer's invariant) and sit +/// behind the view exactly as a frozen stream's padding does. +pub(crate) fn built_view(bits: &BitsBuf) -> BitsView<'_> { + BitsView::new(bits.as_raw_slice(), bits.len()) +} + +/// Extend a build buffer with the bit range `start..end` copied verbatim +/// from a stored stream's view. +/// +/// The build-side copy seam of the operations that assemble their output +/// from input subtrees (the party split/sum families). Byte-parallel past +/// the source's alignment: at most seven leading bits go one at a time, +/// then whole source bytes land as a `memcpy` (aligned output) or a +/// two-shift merge per byte, then the trailing partial byte. +/// +/// # Panics +/// +/// `start..end` must be a range within the view's live length. +pub(crate) fn extend_from_view(out: &mut BitsBuf, src: BitsView<'_>, start: u64, end: u64) { + assert!( + start <= end && end <= src.len(), + "copied range within the view's live length" + ); + let mut pos = start; + // Head bits to the source's byte boundary (at most 7). + while pos < end && !pos.is_multiple_of(8) { + out.push(src.bit(pos)); + pos += 1; + } + // Whole source bytes. + let whole = ((end - pos) / 8) as usize; + if whole > 0 { + let at = (pos / 8) as usize; + out.extend_bytes(&src.bytes()[at..at + whole]); + pos += whole as u64 * 8; + } + // Tail bits (fewer than 8). + while pos < end { + out.push(src.bit(pos)); + pos += 1; + } +} + +/// Seal a built stream's canonical padding: one `1` marker bit appended +/// after the live bits. +/// +/// Sealing makes the buffer's bytes ([`BitsBuf::as_raw_slice`]) the canonical +/// wire spelling — injective, byte-equal if and only if the bit content is +/// equal. The dead bits after the marker are already zero (the buffer's own +/// invariant, held through every truncation), so the marker completes the +/// canonical `1 0*` padding without a zeroing pass; the debug assert holds +/// the invariant at the seam. The empty stream seals to itself: no marker, +/// no bytes. `Bits::freeze` applies this at the storage seam; the standalone +/// form seals buffers that stay build-side — all of them meter/test +/// instruments producing decodable bytes. +pub(crate) fn seal_padding(bits: &mut BitsBuf) { + if !bits.is_empty() { + bits.push(true); + } + debug_assert!( + bits.tail_is_zeroed(), + "a sealed stream's dead bits are zero: the build buffer's invariant" + ); +} diff --git a/crates/before/src/codec/build.rs b/crates/before/src/codec/build.rs index 76fd6634..323c538f 100644 --- a/crates/before/src/codec/build.rs +++ b/crates/before/src/codec/build.rs @@ -33,23 +33,20 @@ //! empty, a two-shift merge per byte otherwise. use super::code::SMALL_CODE_BITS; -use super::{BitsMut, BitsView, Code}; +use super::{BitsBuf, BitsView, Code}; /// An append-truncate builder over one packed preorder bit stream. /// /// The wrapper owning it defines the tree coding; this core owns the /// buffer, the primitive moves, and the write metering. +/// +/// Positions and lengths run at `u64` width on every target, the build +/// side's discipline (the `buf` module doc): the builder is exact to +/// allocatable memory, and byte *indexes* — which fit `usize` because they +/// index an allocated buffer — are converted exactly where a byte is +/// touched. pub(crate) struct PackedBuilder { /// The committed prefix: whole bytes, most-significant bit first. - /// - /// `bytes.len() * 8` fits `usize` on every target — the width the - /// position arithmetic below (`len`, `patch_bit`, `read_bits`, - /// `bit_at`) rests on. The bound: every finished stream hands over - /// through the build buffer, whose own length encoding caps at - /// `usize::MAX >> 3` bits, and the emitters write at most a small - /// constant per input node — so the committed prefix stays multiple - /// binary orders of magnitude below any `usize` wrap, even on 32-bit - /// targets. bytes: Vec, /// The trailing not-yet-committed bits, value-packed at the low end /// (the stream's next bit is the register's most significant live @@ -62,9 +59,13 @@ pub(crate) struct PackedBuilder { impl PackedBuilder { /// Create a builder with room for `capacity` bits before reallocation. - pub(crate) fn with_capacity(capacity: usize) -> Self { + /// + /// The capacity is a hint: a request past the target's address space + /// allocates nothing up front, and the buffer still grows to whatever + /// the appends actually demand. + pub(crate) fn with_capacity(capacity: u64) -> Self { PackedBuilder { - bytes: Vec::with_capacity(capacity / 8 + 1), + bytes: Vec::with_capacity(usize::try_from(capacity / 8 + 1).unwrap_or(0)), staged: 0, staged_len: 0, } @@ -73,8 +74,8 @@ impl PackedBuilder { /// The current output length in bits: the position the next append /// lands at, and the coordinate [`truncate`](Self::truncate) rolls /// back to. - pub(crate) fn len(&self) -> usize { - self.bytes.len() * 8 + self.staged_len as usize + pub(crate) fn len(&self) -> u64 { + self.bytes.len() as u64 * 8 + u64::from(self.staged_len) } /// Append one bit. @@ -89,16 +90,16 @@ impl PackedBuilder { /// Collapse repairs re-anchor a surviving code before truncating the /// region it sits in; this is the read half of that repair (the /// skyline builder's cascade, on the production join/meet path). - pub(crate) fn extract_code(&self, start: usize) -> Code { + pub(crate) fn extract_code(&self, start: u64) -> Code { let n = self.len() - start; - super::scan::record_bits(n); + super::scan::record_bits_u64(n); if n <= SMALL_CODE_BITS { return Code::Small { bits: self.read_bits(start, n as u32), len: n as u8, }; } - let mut out = BitsMut::with_capacity(n); + let mut out = BitsBuf::with_capacity(n); for i in start..start + n { out.push(self.bit_at(i)); } @@ -114,7 +115,7 @@ impl PackedBuilder { } // The splice records its own write. Code::Wide(bits) => { - let src = super::bits::built_view(bits); + let src = super::buf::built_view(bits); self.splice(src, 0, src.len()); } } @@ -123,7 +124,7 @@ impl PackedBuilder { /// Append `width` zero bits as a header slot to be /// [`patch_bit`](Self::patch_bit)ed once the children are known, /// returning the slot's position. - pub(crate) fn reserve(&mut self, width: usize) -> usize { + pub(crate) fn reserve(&mut self, width: usize) -> u64 { super::scan::record_bits(width); let at = self.len(); let mut remaining = width; @@ -140,15 +141,15 @@ impl PackedBuilder { /// # Panics /// /// Panics if `at` is at or past the current output length. - pub(crate) fn patch_bit(&mut self, at: usize, bit: bool) { + pub(crate) fn patch_bit(&mut self, at: u64, bit: bool) { super::scan::record_bits(1); - let committed = self.bytes.len() * 8; + let committed = self.bytes.len() as u64 * 8; if at < committed { let mask = 1u8 << (7 - at % 8); if bit { - self.bytes[at / 8] |= mask; + self.bytes[(at / 8) as usize] |= mask; } else { - self.bytes[at / 8] &= !mask; + self.bytes[(at / 8) as usize] &= !mask; } } else { let offset = (at - committed) as u32; @@ -204,7 +205,7 @@ impl PackedBuilder { /// /// Panics if `len` exceeds the current output length: truncation only /// ever shortens. - pub(crate) fn truncate(&mut self, len: usize) { + pub(crate) fn truncate(&mut self, len: u64) { assert!( len <= self.len(), "builder truncation target {len} exceeds the {} bits written", @@ -212,30 +213,30 @@ impl PackedBuilder { ); let whole = len / 8; let rem = (len % 8) as u32; - if whole < self.bytes.len() { + if whole < self.bytes.len() as u64 { self.staged = if rem > 0 { - u64::from(self.bytes[whole] >> (8 - rem)) + u64::from(self.bytes[whole as usize] >> (8 - rem)) } else { 0 }; self.staged_len = rem; - self.bytes.truncate(whole); + self.bytes.truncate(whole as usize); } else { self.staged >>= self.staged_len - rem; self.staged_len = rem; } } - /// Take the finished stream. - pub(crate) fn finish(self) -> BitsMut { + /// Take the finished stream: the committed bytes adopted whole, the + /// staging register flushed as the final (zero-padded) partial byte — + /// no length-encoding conversion binds the hand-off on any target. + pub(crate) fn finish(self) -> BitsBuf { let bit_len = self.len(); let mut bytes = self.bytes; if self.staged_len > 0 { bytes.push((self.staged << (8 - self.staged_len)) as u8); } - let mut out = BitsMut::from_vec(bytes); - out.truncate(bit_len); - out + BitsBuf::from_raw_parts(bytes, bit_len) } /// Append `len <= 63` bits, value-packed at the low end of `value` @@ -282,9 +283,9 @@ impl PackedBuilder { /// Read `n <= 63` bits at `pos` back out of the output, value-packed /// at the low end of the result. - fn read_bits(&self, pos: usize, n: u32) -> u64 { - debug_assert!(n as usize <= SMALL_CODE_BITS && pos + n as usize <= self.len()); - let committed = self.bytes.len() * 8; + fn read_bits(&self, pos: u64, n: u32) -> u64 { + debug_assert!(u64::from(n) <= SMALL_CODE_BITS && pos + u64::from(n) <= self.len()); + let committed = self.bytes.len() as u64 * 8; let mut acc = 0u64; let mut got = 0u32; let mut p = pos; @@ -292,11 +293,11 @@ impl PackedBuilder { if p < committed { let within = (p % 8) as u32; let take = (8 - within).min(n - got); - let byte = self.bytes[p / 8]; + let byte = self.bytes[(p / 8) as usize]; let chunk = u64::from(byte >> (8 - within - take)) & ((1u64 << take) - 1); acc = (acc << take) | chunk; got += take; - p += take as usize; + p += u64::from(take); } else { let offset = (p - committed) as u32; let take = n - got; @@ -304,7 +305,7 @@ impl PackedBuilder { (self.staged >> (self.staged_len - offset - take)) & ((1u64 << take) - 1); acc = (acc << take) | chunk; got += take; - p += take as usize; + p += u64::from(take); } } acc @@ -312,10 +313,10 @@ impl PackedBuilder { /// The bit at `pos`, read back out of the committed prefix or the /// staging register. - fn bit_at(&self, pos: usize) -> bool { - let committed = self.bytes.len() * 8; + fn bit_at(&self, pos: u64) -> bool { + let committed = self.bytes.len() as u64 * 8; if pos < committed { - self.bytes[pos / 8] >> (7 - pos % 8) & 1 == 1 + self.bytes[(pos / 8) as usize] >> (7 - pos % 8) & 1 == 1 } else { let offset = (pos - committed) as u32; debug_assert!(offset < self.staged_len, "read past the output"); diff --git a/crates/before/src/codec/code.rs b/crates/before/src/codec/code.rs index db78b276..cb4f4184 100644 --- a/crates/before/src/codec/code.rs +++ b/crates/before/src/codec/code.rs @@ -9,7 +9,7 @@ //! the band organic histories occupy — in two machine words, and spills wider //! codes to the buffer form unchanged. -use super::{BitsMut, BitsView}; +use super::{BitsBuf, BitsView}; /// One complete payload code, value-packed when it fits a word. pub(crate) enum Code { @@ -18,17 +18,17 @@ pub(crate) enum Code { /// live bit; bits above `len` are zero). Small { bits: u64, len: u8 }, /// A code wider than 63 bits, as an owned bit buffer. - Wide(BitsMut), + Wide(BitsBuf), } /// The widest code [`Code::Small`] carries. -pub(crate) const SMALL_CODE_BITS: usize = 63; +pub(crate) const SMALL_CODE_BITS: u64 = 63; impl Code { /// The code's length in bits. - pub(crate) fn len(&self) -> usize { + pub(crate) fn len(&self) -> u64 { match self { - Code::Small { len, .. } => usize::from(*len), + Code::Small { len, .. } => u64::from(*len), Code::Wide(bits) => bits.len(), } } @@ -46,14 +46,14 @@ impl Code { "copied range within the view's live length" ); let len = end - start; - if len <= SMALL_CODE_BITS as u64 { + if len <= SMALL_CODE_BITS { Code::Small { bits: src.load_be(start, len as u32), len: len as u8, } } else { - let mut out = BitsMut::with_capacity(len as usize); - super::bits::extend_from_view(&mut out, src, start, end); + let mut out = BitsBuf::with_capacity(len); + super::buf::extend_from_view(&mut out, src, start, end); Code::Wide(out) } } diff --git a/crates/before/src/codec/display.rs b/crates/before/src/codec/display.rs index b5f57842..9c936a5b 100644 --- a/crates/before/src/codec/display.rs +++ b/crates/before/src/codec/display.rs @@ -1,6 +1,6 @@ use crate::idbits::{IdNode, IdReader}; -use super::{BitsMut, BitsView}; +use super::{BitsBuf, BitsView}; /// While rendering an open id node, which child the walk is inside. /// @@ -29,7 +29,7 @@ pub(crate) fn write_id( let mut reader = IdReader::root(bits); // Per open node: a phase bit on top ([`LEFT_PHASE`]/[`RIGHT_PHASE`]); under // a left phase, the right child's presence bit. - let mut pending = BitsMut::new(); + let mut pending = BitsBuf::new(); // Whether the child to render next is present (decode the cursor) or an // absent `0` (the cursor holds no bits for it). let mut present = true; diff --git a/crates/before/src/codec/dsi.rs b/crates/before/src/codec/dsi.rs index b2f498e1..5a99a320 100644 --- a/crates/before/src/codec/dsi.rs +++ b/crates/before/src/codec/dsi.rs @@ -300,8 +300,9 @@ impl BitCursor for DsiCursor<'_> { /// Native-order `u32` words over one stored stream's packed bytes: the word /// source under the buffered reader. /// -/// The final partial word zero-fills past the stream's bytes (the tail byte's -/// dead bits arrive already masked through `bitvec`'s domain view), which +/// The final partial word zero-fills past the stream's bytes (the tail byte +/// arrives with its dead bits already masked, the view's `body_tail` +/// destructuring), which /// parallels the slice cursor's zero-filled decode window: the phantom zeros /// can only lengthen an apparent unary prefix, and the cursor's live-length /// checks keep them from ever surfacing in a decoded value. Reads past the last diff --git a/crates/before/src/codec/dsi/tests.rs b/crates/before/src/codec/dsi/tests.rs index 0ec29832..be2b3981 100644 --- a/crates/before/src/codec/dsi/tests.rs +++ b/crates/before/src/codec/dsi/tests.rs @@ -5,7 +5,7 @@ use proptest::prelude::*; use crate::codec::cursor::Truncated; -use crate::codec::{self, Base, BitCursor, BitsMut, SliceCursor}; +use crate::codec::{self, Base, BitCursor, BitsBuf, SliceCursor}; use super::DsiCursor; @@ -34,7 +34,7 @@ fn gamma_reader_matches_decoder_across_the_word_seam() { Base::from((UBig::ONE << 100usize) + 12345u32), ]; for value in &values { - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); codec::encode_int(&mut bits, value); let (want, want_end) = codec::decode_int(crate::codec::built_view(&bits), 0) .expect("the committed decoder reads its own encoding"); @@ -82,7 +82,7 @@ fn skip_int_meters_exactly_the_code_width_read_int_pays() { Base::from(u64::MAX), // k = 64: the first wide-arm code Base::from(UBig::ONE << 100usize), ] { - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); codec::encode_int(&mut bits, &value); let mut reader = DsiCursor::new(crate::codec::built_view(&bits)); crate::meter::reset_scan_bits(); @@ -120,10 +120,10 @@ fn truncated_codes_reject_at_every_cut_point() { Base::from(u64::MAX), Base::from(UBig::ONE << 100usize), ] { - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); codec::encode_int(&mut bits, &value); for cut in 0..bits.len() { - let prefix = codec::BitsView::new(bits.as_raw_slice(), cut as u64); + let prefix = codec::BitsView::new(bits.as_raw_slice(), cut); assert!( codec::decode_int(prefix, 0).is_err(), "the per-bit loop accepts a truncated code at {cut} of {value}" @@ -151,7 +151,7 @@ fn truncated_codes_reject_at_every_cut_point() { #[test] fn unary_reads_match_the_per_bit_loop_across_word_seams() { for run in [0usize, 1, 7, 8, 31, 32, 33, 63, 64, 65, 200] { - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); for _ in 0..run { bits.push(false); } @@ -183,7 +183,7 @@ fn unary_reads_match_the_per_bit_loop_across_word_seams() { /// stream, at and off byte boundaries. #[test] fn mid_stream_opens_read_the_same_suffix() { - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); // A mixed stream: alternating flags and codes of assorted widths. for (flag, value) in [ (true, 0u64), @@ -197,9 +197,9 @@ fn mid_stream_opens_read_the_same_suffix() { codec::encode_int(&mut bits, &Base::from(value)); } for pos in 0..=bits.len() { - let mut fresh = DsiCursor::new_at(crate::codec::built_view(&bits), pos as u64); + let mut fresh = DsiCursor::new_at(crate::codec::built_view(&bits), pos); let mut walked = DsiCursor::new(crate::codec::built_view(&bits)); - let mut consumed = 0usize; + let mut consumed = 0u64; while consumed < pos { walked.read_bit().expect("within the live length"); consumed += 1; @@ -238,7 +238,7 @@ proptest! { 1..40, ), ) { - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); for (unary, v) in &ops { if *unary { for _ in 0..*v { diff --git a/crates/before/src/codec/gamma.rs b/crates/before/src/codec/gamma.rs index 81344b82..580c39f9 100644 --- a/crates/before/src/codec/gamma.rs +++ b/crates/before/src/codec/gamma.rs @@ -15,13 +15,12 @@ //! ([`decode_int_window`]) and emitted with one store, with per-bit loops as //! the fallback — and, on decode, the sole arbiter of every reject. -use bitvec::field::BitField; use dashu_int::UBig; use crate::error::Decode; use super::code::SMALL_CODE_BITS; -use super::{Base, BitCursor, BitsMut, BitsView, Code, SliceCursor}; +use super::{Base, BitCursor, BitsBuf, BitsView, Code, SliceCursor}; /// Append `n` as the Elias gamma code of `m = n + 1`: `floor(log2(m))` zero /// bits, then `m` in `floor(log2(m)) + 1` bits, most-significant first. @@ -29,20 +28,20 @@ use super::{Base, BitCursor, BitsMut, BitsView, Code, SliceCursor}; /// Cost is `2*floor(log2(n+1)) + 1` bits; `0` costs a single bit. Canonical and /// prefix-free, for an arbitrary-width non-negative `n` (there is no value /// cap). -pub(crate) fn encode_int(out: &mut BitsMut, n: &Base) { +pub(crate) fn encode_int(out: &mut BitsBuf, n: &Base) { let m = n + 1u32; match m.to_u64() { // Word case: the mantissa fits a machine word, so append the whole code - // word-wise — the `2k+1` bits (zeros and all) in one `resize`, then the - // `k+1` mantissa bits in one `store_be` — instead of one `push` per - // bit. Byte-identical to the per-bit emit below. + // word-wise — the `k`-zero prefix in one append, then the `k+1`-bit + // mantissa (`m` value-packed, its leading 1 included) in another — + // instead of one `push` per bit. Byte-identical to the per-bit emit + // below. Some(m) => { // m >= 1, so `leading_zeros < 64` and `k = floor(log2(m))` never - // underflows. - let k = (u64::BITS - 1 - m.leading_zeros()) as usize; - let start = out.len(); - out.resize(start + 2 * k + 1, false); - out[start + k..].store_be::(m); + // underflows; both appends stay within one machine word. + let k = u64::BITS - 1 - m.leading_zeros(); + out.push_bits(0, k); + out.push_bits(m, k + 1); } // Wide case (`n >= u64::MAX`): per-bit emit of the wide mantissa. None => { @@ -69,8 +68,8 @@ pub(crate) fn encode_int(out: &mut BitsMut, n: &Base) { /// is two shifts — and as an owned buffer past that. pub(crate) fn code_int(n: &Base) -> Code { if let Some(m) = n.to_u64().and_then(|n| n.checked_add(1)) { - let k = (u64::BITS - 1 - m.leading_zeros()) as usize; - let len = 2 * k + 1; + let k = u64::BITS - 1 - m.leading_zeros(); + let len = u64::from(2 * k + 1); if len <= SMALL_CODE_BITS { return Code::Small { bits: m, @@ -78,7 +77,7 @@ pub(crate) fn code_int(n: &Base) -> Code { }; } } - let mut out = BitsMut::new(); + let mut out = BitsBuf::new(); encode_int(&mut out, n); Code::Wide(out) } @@ -87,8 +86,8 @@ pub(crate) fn code_int(n: &Base) -> Code { /// machine-word form, with no intermediate [`Base`]. pub(crate) fn code_int_small(n: u64) -> Code { if let Some(m) = n.checked_add(1) { - let k = (u64::BITS - 1 - m.leading_zeros()) as usize; - let len = 2 * k + 1; + let k = u64::BITS - 1 - m.leading_zeros(); + let len = u64::from(2 * k + 1); if len <= SMALL_CODE_BITS { return Code::Small { bits: m, @@ -96,7 +95,7 @@ pub(crate) fn code_int_small(n: u64) -> Code { }; } } - let mut out = BitsMut::new(); + let mut out = BitsBuf::new(); encode_int(&mut out, &Base::from(n)); Code::Wide(out) } diff --git a/crates/before/src/codec/literal.rs b/crates/before/src/codec/literal.rs index 0e89d452..409e7dfa 100644 --- a/crates/before/src/codec/literal.rs +++ b/crates/before/src/codec/literal.rs @@ -1,6 +1,6 @@ use crate::error::Parse; -use super::{validate_id, BitsMut, BitsView}; +use super::{validate_id, BitsBuf, BitsView}; /// Whether a normal-form id stream is the anonymous (empty) identity. /// @@ -30,8 +30,8 @@ pub(crate) fn id_is_empty(bits: BitsView<'_>) -> bool { /// The bits for an id leaf: the empty stream for `0` (absence), the terminal /// tag `00` for `1`. -pub(crate) fn id_leaf(v: bool) -> BitsMut { - let mut b = BitsMut::with_capacity(2); +pub(crate) fn id_leaf(v: bool) -> BitsBuf { + let mut b = BitsBuf::with_capacity(2); if v { b.push(false); // terminal tag `00`: an owned leaf, no children b.push(false); @@ -40,8 +40,8 @@ pub(crate) fn id_leaf(v: bool) -> BitsMut { } /// Whether `bits` is exactly the terminal tag `00` (the `1` leaf). -fn id_is_terminal(bits: &BitsMut) -> bool { - bits.len() == 2 && !bits[0] && !bits[1] +fn id_is_terminal(bits: &BitsBuf) -> bool { + bits.len() == 2 && !bits.get(0) && !bits.get(1) } /// Assemble an id node from two already-normal child streams: a `0` child is @@ -49,18 +49,18 @@ fn id_is_terminal(bits: &BitsMut) -> bool { /// present. /// /// Rejects a collapsible `(0, 0)` or `(1, 1)`, then validates the result. -pub(crate) fn id_node(l: &BitsMut, r: &BitsMut) -> Result { +pub(crate) fn id_node(l: &BitsBuf, r: &BitsBuf) -> Result { if l.is_empty() && r.is_empty() { return Err(Parse::NotCanonical); // (0, 0) → 0, not a node } if id_is_terminal(l) && id_is_terminal(r) { return Err(Parse::NotCanonical); // (1, 1) → 1, not a node } - let mut b = BitsMut::with_capacity(2 + l.len() + r.len()); + let mut b = BitsBuf::with_capacity(2 + l.len() + r.len()); b.push(!l.is_empty()); // bit 0 = left present b.push(!r.is_empty()); // bit 1 = right present - b.extend_from_bitslice(l); - b.extend_from_bitslice(r); - validate_id(super::bits::built_view(&b))?; + b.extend_from_buf(l); + b.extend_from_buf(r); + validate_id(super::buf::built_view(&b))?; Ok(b) } diff --git a/crates/before/src/codec/tests.rs b/crates/before/src/codec/tests.rs index eecd603f..48e9b334 100644 --- a/crates/before/src/codec/tests.rs +++ b/crates/before/src/codec/tests.rs @@ -7,13 +7,13 @@ use std::sync::Arc; -use bitvec::prelude::*; use proptest::prelude::*; use proptest::test_runner::TestCaseError; use super::{ - decode_int, decode_int_from, encode_int, Base, BitsMut, BitsView, DsiCursor, SliceCursor, + bits_buf, decode_int, decode_int_from, encode_int, Base, BitsBuf, BitsView, DsiCursor, + SliceCursor, }; use crate::oracle; use crate::span::Span; @@ -35,11 +35,11 @@ proptest! { #[test] fn gamma_roundtrip(n in 0u64..1_000_000) { let n = Base::from(n); - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); encode_int(&mut bits, &n); let (decoded, pos) = decode_int(crate::codec::built_view(&bits), 0).expect("well-formed"); prop_assert_eq!(decoded, n); - prop_assert_eq!(pos, bits.len() as u64); + prop_assert_eq!(pos, bits.len()); } } @@ -53,11 +53,11 @@ proptest! { for limb in limbs { n = (n << 64) | Base::from(limb); } - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); encode_int(&mut bits, &n); let (decoded, pos) = decode_int(crate::codec::built_view(&bits), 0).expect("well-formed"); prop_assert_eq!(decoded, n); - prop_assert_eq!(pos, bits.len() as u64); + prop_assert_eq!(pos, bits.len()); } } @@ -71,7 +71,7 @@ proptest! { #[test] fn gamma_costs() { let cost = |n: u64| { - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); encode_int(&mut bits, &Base::from(n)); bits.len() }; @@ -87,24 +87,24 @@ fn gamma_costs() { #[test] fn gamma_roundtrip_just_above_u64_max() { let n = Base::from(u64::MAX) + Base::from(1u8); - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); encode_int(&mut bits, &n); let (decoded, pos) = decode_int(crate::codec::built_view(&bits), 0).expect("well-formed"); assert_eq!(decoded, n); assert_eq!(decoded.to_string(), "18446744073709551616"); - assert_eq!(pos, bits.len() as u64); + assert_eq!(pos, bits.len()); } /// `decode_int` never panics and reports `Truncated` when the code runs off the /// end (empty input, or all-zeros with no terminating `1`). #[test] fn gamma_truncated() { - let empty = BitsMut::new(); + let empty = BitsBuf::new(); assert!(matches!( decode_int(crate::codec::built_view(&empty), 0), Err(Decode::Truncated) )); - let zeros: BitsMut = bitvec![u8, Msb0; 0, 0, 0, 0, 0]; + let zeros: BitsBuf = bits_buf![0, 0, 0, 0, 0]; assert!(matches!( decode_int(crate::codec::built_view(&zeros), 0), Err(Decode::Truncated) @@ -115,21 +115,25 @@ fn gamma_truncated() { /// `Bits::freeze` canonicalizes storage. /// -/// A build buffer whose `truncate` left stale bits in the final partial -/// byte freezes to the canonical marker-padded raw slice — live bits, -/// one `1`, zeros to the byte boundary — with the live length and bit -/// content preserved behind the deref view. +/// A build buffer that shed live bits by `truncate` freezes to the +/// canonical marker-padded raw slice — live bits, one `1`, zeros to the +/// byte boundary — with the live length and bit content preserved behind +/// the live view: the buffer zeroes the shed bits at the truncation +/// itself (its representation invariant), so the freeze only appends the +/// marker and the bytes come out canonical. #[test] fn freeze_canonicalizes_storage() { // Write a byte of ones, then truncate to 3 live bits: the shed ones - // linger in the buffer's final byte until the freeze seals over them. - let mut buf: BitsMut = bitvec![u8, Msb0; 1; 8]; + // leave the buffer's final byte at the truncation, so the raw image + // is already `1110_0000` before the freeze appends the marker. + let mut buf: BitsBuf = bits_buf![1; 8]; buf.truncate(3); + assert_eq!(buf.as_raw_slice(), &[0b1110_0000]); let frozen = super::Bits::freeze(buf.clone()); assert_eq!(frozen.len(), 3); assert_eq!(frozen.as_raw_slice(), &[0b1111_0000]); assert!(super::padding_is_canonical(&frozen)); - assert_eq!(frozen.live().to_bitvec(), buf); + assert_eq!(frozen.live().to_buf(), buf); } /// The marker padding makes stored bytes injective on streams: a stream @@ -137,8 +141,8 @@ fn freeze_canonicalizes_storage() { /// distinct raw slices, so the byte compare alone decides equality. #[test] fn marker_padding_separates_length_collisions() { - let a = super::Bits::freeze(bitvec![u8, Msb0; 0, 1]); - let b = super::Bits::freeze(bitvec![u8, Msb0; 0, 1, 0]); + let a = super::Bits::freeze(bits_buf![0, 1]); + let b = super::Bits::freeze(bits_buf![0, 1, 0]); assert_eq!(a.as_raw_slice(), &[0b0110_0000]); assert_eq!(b.as_raw_slice(), &[0b0101_0000]); assert!(!super::canonical_eq(&a, &b)); @@ -149,7 +153,7 @@ fn marker_padding_separates_length_collisions() { /// the live length reads back through it. #[test] fn flush_stream_carries_a_whole_marker_byte() { - let frozen = super::Bits::freeze(bitvec![u8, Msb0; 1; 8]); + let frozen = super::Bits::freeze(bits_buf![1; 8]); assert_eq!(frozen.len(), 8); assert_eq!(frozen.as_raw_slice(), &[0xFF, 0b1000_0000]); assert!(super::padding_is_canonical(&frozen)); @@ -168,7 +172,7 @@ fn flush_stream_carries_a_whole_marker_byte() { /// equality gives it, never a clone-history fact. #[test] fn ptr_eq_implies_equality_with_clones_the_nonempty_source() { - let build = || super::Bits::freeze(bitvec![u8, Msb0; 1, 0, 1, 1, 0]); + let build = || super::Bits::freeze(bits_buf![1, 0, 1, 1, 0]); let a = build(); let clone = a.clone(); assert!(a.ptr_eq(&clone)); @@ -178,8 +182,8 @@ fn ptr_eq_implies_equality_with_clones_the_nonempty_source() { assert!(super::canonical_eq(&a, &b)); // Independently frozen empty streams alias: ptr_eq true with no clone // anywhere — and still value-equal, the only fact a fast path may use. - let e1 = super::Bits::freeze(BitsMut::new()); - let e2 = super::Bits::freeze(BitsMut::new()); + let e1 = super::Bits::freeze(BitsBuf::new()); + let e2 = super::Bits::freeze(BitsBuf::new()); assert!(e1.ptr_eq(&e2)); assert!(super::canonical_eq(&e1, &e2)); } @@ -191,7 +195,7 @@ fn ptr_eq_implies_equality_with_clones_the_nonempty_source() { /// constructor is the freeze of the empty buffer. #[test] fn from_canonical_matches_freeze() { - let frozen = super::Bits::freeze(bitvec![u8, Msb0; 1, 0, 1]); + let frozen = super::Bits::freeze(bits_buf![1, 0, 1]); let adopted = super::Bits::from_canonical(bytes::Bytes::copy_from_slice(frozen.as_raw_slice())); assert!(super::canonical_eq(&frozen, &adopted)); assert!(!frozen.ptr_eq(&adopted)); // distinct buffers, equal content @@ -201,10 +205,184 @@ fn from_canonical_matches_freeze() { assert_eq!(empty.len(), 0); assert!(super::canonical_eq( &empty, - &super::Bits::freeze(BitsMut::new()) + &super::Bits::freeze(BitsBuf::new()) )); } +// ───────────── build-history family (the buffer's invariants) ───────────── +// +// The build buffer promises that its byte image — and therefore the sealed +// spelling the freeze door emits — is a function of the bit *content* alone, +// whatever mutation history produced it. The family below drives arbitrary +// interleavings of the buffer's whole mutating move set (single-bit pushes, +// word-wide appends, verbatim view copies, truncations aimed at byte +// boundaries, mid-byte positions, and empty) and compares every observable +// against a clean rebuild of the surviving content: a history-dependent +// observable — a stale shed bit surviving a truncation, a dead bit a word +// append failed to zero — reads red here before any decoder ever sees it. + +/// One step of an arbitrary build history: the buffer's mutating move set. +#[derive(Debug, Clone)] +enum BuildOp { + /// Append one bit. + Push(bool), + /// Append `len` bits of `value` word-wide (`len <= 64`). + PushBits { value: u64, len: u32 }, + /// Append a verbatim range of a fresh source stream through the view + /// copy seam; the range is `sub`-selected inside the source at + /// application time. + Extend { src: Vec, sub: (u16, u16) }, + /// Roll back: `sel` picks the truncation genre (empty, the deepest + /// byte boundary at or under a chosen position, or an arbitrary + /// mid-byte position), `frac` seeds the position. + Truncate { sel: u8, frac: u16 }, +} + +fn arb_build_op() -> impl Strategy { + prop_oneof![ + any::().prop_map(BuildOp::Push), + (any::(), 0u32..=64).prop_map(|(value, len)| BuildOp::PushBits { + value: if len == 64 { + value + } else { + value & ((1u64 << len) - 1) + }, + len, + }), + ( + proptest::collection::vec(any::(), 0..100), + any::(), + any::() + ) + .prop_map(|(src, a, b)| BuildOp::Extend { src, sub: (a, b) }), + (any::(), any::()).prop_map(|(sel, frac)| BuildOp::Truncate { sel, frac }), + ] +} + +/// Apply one history step to the buffer under test and the `Vec` model +/// in lockstep. +fn apply_build_op(buf: &mut BitsBuf, model: &mut Vec, op: &BuildOp) { + match op { + BuildOp::Push(bit) => { + buf.push(*bit); + model.push(*bit); + } + BuildOp::PushBits { value, len } => { + buf.push_bits(*value, *len); + for i in (0..*len).rev() { + model.push(value >> i & 1 == 1); + } + } + BuildOp::Extend { src, sub } => { + let source: BitsBuf = src.iter().copied().collect(); + let (a, b) = ( + u64::from(sub.0) % (source.len() + 1), + u64::from(sub.1) % (source.len() + 1), + ); + let (start, end) = (a.min(b), a.max(b)); + crate::codec::extend_from_view(buf, crate::codec::built_view(&source), start, end); + model.extend(&src[start as usize..end as usize]); + } + BuildOp::Truncate { sel, frac } => { + let pos = u64::from(*frac) % (buf.len() + 1); + let target = match sel % 3 { + 0 => 0, // truncate-to-empty + 1 => pos / 8 * 8, // the deepest byte boundary at or under `pos` + _ => pos, // arbitrary, usually mid-byte + }; + buf.truncate(target); + model.truncate(usize::try_from(target).expect("test histories are small")); + } + } +} + +/// A clean rebuild of `content`: single-bit pushes only, no truncation — +/// the reference spelling of the surviving content. +fn clean_rebuild(content: &[bool]) -> BitsBuf { + content.iter().copied().collect() +} + +/// The standard `Hash` image of a frozen stream, for the eq/hash agreement +/// leg. +fn hash_of(bits: &super::Bits) -> u64 { + use core::hash::{Hash, Hasher}; + let mut hasher = std::hash::DefaultHasher::new(); + // `Bits` hashes through `canonical_hash` via the value types' derives; + // feed the raw slice exactly as `canonical_hash` does. + bits.as_raw_slice().hash(&mut hasher); + hasher.finish() +} + +proptest! { + /// The buffer's byte image and sealed spelling are functions of the + /// bit content alone, whatever mutation history produced it. + /// + /// Any interleaving of pushes, word appends, view copies, and + /// truncations (byte-aligned, mid-byte, to empty, multi-byte sheds + /// included) leaves the buffer byte-identical to a clean rebuild of + /// the surviving content — at every intermediate state — freezing to + /// the identical canonical spelling, with `Eq` and the canonical hash + /// agreeing with bit-level equality. + #[test] + fn build_history_spelling_is_a_function_of_content( + ops in proptest::collection::vec(arb_build_op(), 0..40), + ) { + let mut buf = BitsBuf::new(); + let mut model: Vec = Vec::new(); + for op in &ops { + apply_build_op(&mut buf, &mut model, op); + // The representation invariants hold at every intermediate + // state, not only at the seal: the byte image already equals + // the clean rebuild's. + let clean = clean_rebuild(&model); + prop_assert_eq!(buf.len(), clean.len()); + prop_assert_eq!(buf.as_raw_slice(), clean.as_raw_slice()); + } + let clean = clean_rebuild(&model); + prop_assert!(buf == clean, "Eq agrees with bit-level equality"); + let frozen = super::Bits::freeze(buf); + let reference = super::Bits::freeze(clean); + prop_assert_eq!( + frozen.as_raw_slice(), + reference.as_raw_slice(), + "one content, one sealed spelling" + ); + prop_assert!(super::canonical_eq(&frozen, &reference)); + prop_assert_eq!(hash_of(&frozen), hash_of(&reference)); + } + + /// Sealed spellings are injective on contents. + /// + /// Two arbitrary build histories freeze to equal spellings exactly + /// when they end holding equal bit sequences, and the canonical hash + /// refines the same partition (equal contents hash equal). + #[test] + fn build_history_spellings_are_injective( + ops_a in proptest::collection::vec(arb_build_op(), 0..25), + ops_b in proptest::collection::vec(arb_build_op(), 0..25), + ) { + let (mut a, mut model_a) = (BitsBuf::new(), Vec::new()); + for op in &ops_a { + apply_build_op(&mut a, &mut model_a, op); + } + let (mut b, mut model_b) = (BitsBuf::new(), Vec::new()); + for op in &ops_b { + apply_build_op(&mut b, &mut model_b, op); + } + prop_assert_eq!(a == b, model_a == model_b, "Eq is bit-content equality"); + let (fa, fb) = (super::Bits::freeze(a), super::Bits::freeze(b)); + prop_assert_eq!( + fa.as_raw_slice() == fb.as_raw_slice(), + model_a == model_b, + "spellings collide exactly on equal contents" + ); + prop_assert_eq!(super::canonical_eq(&fa, &fb), model_a == model_b); + if model_a == model_b { + prop_assert_eq!(hash_of(&fa), hash_of(&fb)); + } + } +} + // ───────────────── word-window fast paths (differential) ───────────────── // // `encode_int` and `decode_int` carry word-wise fast paths riding on @@ -218,7 +396,7 @@ fn from_canonical_matches_freeze() { /// The per-bit reference emitter, the encode-side differential oracle: unary /// prefix then MSB-first mantissa, one push per bit. -fn encode_int_bitwise(out: &mut BitsMut, n: &Base) { +fn encode_int_bitwise(out: &mut BitsBuf, n: &Base) { let m = n + 1u32; let k = m.bits() - 1; for _ in 0..k { @@ -308,7 +486,7 @@ fn arb_boundary_u64() -> impl Strategy { /// `pad` positions the read mid-byte, `zeros` spans prefix lengths across the /// 31/32 window split and the 63/64/65 word widths, and `rest` supplies — or, /// when short, truncates — the mantissa, plus trailing junk. -fn arb_gamma_stream() -> impl Strategy { +fn arb_gamma_stream() -> impl Strategy { ( proptest::collection::vec(any::(), 0..17), prop_oneof![ @@ -323,7 +501,7 @@ fn arb_gamma_stream() -> impl Strategy { ) .prop_map(|(pad, zeros, rest)| { let pos = pad.len(); - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); bits.extend(pad); for _ in 0..zeros { bits.push(false); @@ -350,8 +528,8 @@ proptest! { value = (value << 64) | Base::from(limb); } let pos = prefix.len(); - let mut word = BitsMut::new(); - let mut bit = BitsMut::new(); + let mut word = BitsBuf::new(); + let mut bit = BitsBuf::new(); for b in prefix { word.push(b); bit.push(b); @@ -364,7 +542,7 @@ proptest! { let (decoded, end) = decode_int(crate::codec::built_view(&word), pos as u64).expect("well-formed"); prop_assert_eq!(decoded, value); - prop_assert_eq!(end, word.len() as u64); + prop_assert_eq!(end, word.len()); } } @@ -429,7 +607,7 @@ fn gamma_window_edge() { // k = 31: the widest code a 64-bit window proves. let n = (1u64 << 31) - 1; - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); encode_int(&mut bits, &Base::from(n)); assert_eq!(bits.len(), 63); assert_eq!( @@ -446,7 +624,7 @@ fn gamma_window_edge() { // k = 32: a 65-bit code straddles the window edge — decline, and the full // decoder still reads it through the loop. let n = (1u64 << 32) - 1; - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); encode_int(&mut bits, &Base::from(n)); assert_eq!(bits.len(), 65); assert_eq!(decode_int_window(crate::codec::built_view(&bits), 0), None); @@ -455,7 +633,7 @@ fn gamma_window_edge() { assert_eq!(end, 65u64); // Junk after a short code must not leak into its mantissa. - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); encode_int(&mut bits, &Base::from(5u64)); let code_len = bits.len(); for _ in 0..64 { @@ -463,7 +641,7 @@ fn gamma_window_edge() { } assert_eq!( decode_int_window(crate::codec::built_view(&bits), 0), - Some((5, code_len as u64)) + Some((5, code_len)) ); } @@ -474,7 +652,7 @@ fn gamma_window_edge() { fn gamma_window_declines_conservatively() { use super::gamma::decode_int_window; - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); bits.push(false); bits.push(true); @@ -490,7 +668,7 @@ fn gamma_window_declines_conservatively() { assert_eq!(decode_int_window(crate::codec::built_view(&bits), 7), None); // All zeros: no terminating 1 in the stream (bit loop: `Truncated`). - let zeros = BitsMut::repeat(false, 70); + let zeros = BitsBuf::repeat(false, 70); assert_eq!(decode_int_window(crate::codec::built_view(&zeros), 0), None); } @@ -501,11 +679,11 @@ fn gamma_window_declines_conservatively() { fn gamma_roundtrip_wide_value() { // 2^1000 + 12345: a 1001-bit mantissa with live bits at both ends. let n = (Base::from(1u8) << 1000u32) + 12345u64; - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); encode_int(&mut bits, &n); let (decoded, pos) = decode_int(crate::codec::built_view(&bits), 0).expect("well-formed"); assert_eq!(decoded, n); - assert_eq!(pos, bits.len() as u64); + assert_eq!(pos, bits.len()); } /// A stream that ends anywhere inside a wide mantissa is `Truncated`: the @@ -514,12 +692,12 @@ fn gamma_roundtrip_wide_value() { #[test] fn gamma_truncated_inside_wide_mantissa() { let n = (Base::from(1u8) << 1000u32) + 12345u64; - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); encode_int(&mut bits, &n); // Cuts inside the unary prefix, at the leading mantissa 1, just after it, // at byte-scale offsets into the mantissa, and one bit short. for cut in [1, 500, 1001, 1002, 1009, 1500, bits.len() - 1] { - let truncated = crate::codec::BitsView::new(bits.as_raw_slice(), cut as u64); + let truncated = crate::codec::BitsView::new(bits.as_raw_slice(), cut); assert!( matches!(decode_int(truncated, 0), Err(Decode::Truncated)), "cut at bit {cut} must report Truncated", @@ -528,7 +706,7 @@ fn gamma_truncated_inside_wide_mantissa() { // The full code still decodes: the cuts, not the value, are the failure. let (decoded, pos) = decode_int(crate::codec::built_view(&bits), 0).expect("well-formed"); assert_eq!(decoded, n); - assert_eq!(pos, bits.len() as u64); + assert_eq!(pos, bits.len()); } // ──────────────────── metered Base equality and hashing ──────────────────── @@ -1512,7 +1690,7 @@ impl RefCur<'_> { /// `NotCanonical`) runs only after its closing paren parsed. fn ref_parse_id_node( cur: &mut RefCur, - bits: &mut BitsMut, + bits: &mut BitsBuf, ) -> Result { use crate::error::Parse; match cur.bump() { @@ -1550,12 +1728,12 @@ fn ref_parse_id_node( /// The reference id-string parser: one tree, no trailing input, normal form /// revalidated on the emitted bits — `parse_id_str`'s exact contract. -fn ref_parse_id_str(s: &str) -> Result { +fn ref_parse_id_str(s: &str) -> Result { let mut cur = RefCur { bytes: s.as_bytes(), pos: 0, }; - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); ref_parse_id_node(&mut cur, &mut bits)?; if cur.peek().is_some() { return Err(crate::error::Parse::Syntax); @@ -1660,7 +1838,7 @@ proptest! { assert_id_parse_matches_reference(&rendered)?; assert_id_parse_matches_reference(&spaced)?; let bits = super::parse_id_str(&spaced).expect("a rendered id parses"); - prop_assert_eq!(bits, party.as_bits().to_bitvec()); + prop_assert_eq!(bits, party.as_bits().to_buf()); } } @@ -1719,8 +1897,8 @@ fn id_text_parser_error_precedence_pins() { assert_eq!(super::parse_id_str(""), Err(Parse::Syntax)); assert_eq!(super::parse_id_str("(1 0)"), Err(Parse::Syntax)); assert_eq!(super::parse_id_str("(1, 0"), Err(Parse::Syntax)); - assert_eq!(super::parse_id_str("1"), Ok(bitvec![u8, Msb0; 0, 0])); - assert_eq!(super::parse_id_str("0"), Ok(BitsMut::new())); + assert_eq!(super::parse_id_str("1"), Ok(bits_buf![0, 0])); + assert_eq!(super::parse_id_str("0"), Ok(BitsBuf::new())); let spaced = super::parse_id_str(" ( 1 ,\t( 0 ,\n1 ) )\r").expect("whitespace between tokens"); assert_eq!( spaced, diff --git a/crates/before/src/codec/text.rs b/crates/before/src/codec/text.rs index a720feb3..d745d892 100644 --- a/crates/before/src/codec/text.rs +++ b/crates/before/src/codec/text.rs @@ -1,6 +1,6 @@ use crate::error::Parse; -use super::{validate_id, Base, BitsMut}; +use super::{validate_id, Base, BitsBuf}; /// A whitespace-skipping byte cursor over the input string. The grammar is pure /// ASCII (`(`, `)`, `,`, digits, `0`/`1`), so byte-level scanning is exact. @@ -72,9 +72,9 @@ pub(crate) fn parse_base(cur: &mut Cur) -> Result { /// Iterative, like the packed-tree parsers in [`super::tree`]: depth lives on /// an explicit frame stack, never the call stack, so nesting depth cannot /// overflow. -pub(crate) fn parse_id_str(s: &str) -> Result { +pub(crate) fn parse_id_str(s: &str) -> Result { let mut cur = Cur::new(s); - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); parse_id_tree(&mut cur, &mut bits)?; if cur.peek().is_some() { return Err(Parse::Syntax); // trailing junk @@ -106,13 +106,13 @@ enum IdFrame { /// The next subtree is the node's left child. NeedLeft { /// The node's tag position in the output bits. - tag: usize, + tag: u64, }, /// The left child is parsed and its `,` consumed; the next subtree is the /// node's right child. NeedRight { /// The node's tag position in the output bits. - tag: usize, + tag: u64, /// What the left child was (the presence patch and the collapsible-node /// check both need it). left: IdKind, @@ -126,7 +126,7 @@ enum IdFrame { /// `(0, 0)` / `(1, 1)` once its `)` has parsed (a structural defect outranks /// the canonicality check, exactly the token order of the grammar). One frame /// per unfinished ancestor. -fn parse_id_tree(cur: &mut Cur, bits: &mut BitsMut) -> Result<(), Parse> { +fn parse_id_tree(cur: &mut Cur, bits: &mut BitsBuf) -> Result<(), Parse> { let mut stack: Vec = Vec::new(); loop { // One atom: a leaf token, or a `(` opening the next unfinished node. @@ -182,7 +182,7 @@ fn parse_id_tree(cur: &mut Cur, bits: &mut BitsMut) -> Result<(), Parse> { /// Parse a stamp `(i, e)` into its id bit stream and the event component's /// text. Splits at the top-level (depth-0) comma, parses the id side, and /// returns the event side for the caller's version parser. Iterative. -pub(crate) fn parse_clock_str(s: &str) -> Result<(BitsMut, &str), Parse> { +pub(crate) fn parse_clock_str(s: &str) -> Result<(BitsBuf, &str), Parse> { let t = s.trim(); let bytes = t.as_bytes(); if bytes.first() != Some(&b'(') || bytes.last() != Some(&b')') { diff --git a/crates/before/src/meter.rs b/crates/before/src/meter.rs index c304c54c..bdb835b5 100644 --- a/crates/before/src/meter.rs +++ b/crates/before/src/meter.rs @@ -80,7 +80,7 @@ pub use crate::version::hull_traffic::SpanTraffic; /// readers ([`emit_traffic`]/[`reset_emit_traffic`]). pub use crate::version::skyline::web_traffic::EmitTraffic; -use crate::codec::{self, Base, BitsMut}; +use crate::codec::{self, Base, BitsBuf}; /// A generator's output: canonical packed bytes plus the exact bit length. /// @@ -99,11 +99,11 @@ pub struct Packed { impl Packed { /// Canonicalize a built bit stream: seal the marker padding, keeping /// the live length. - fn from_bits(mut bits: BitsMut) -> Self { - let len = bits.len(); + fn from_bits(mut bits: BitsBuf) -> Self { + let len = usize::try_from(bits.len()).expect("instrument shapes are host-built and small"); codec::seal_padding(&mut bits); Packed { - bytes: bits.into_vec(), + bytes: bits.into_bytes(), bits: len, } } @@ -122,13 +122,13 @@ impl Packed { } /// Append an event leaf with base `n`: flag `0`, then `gamma(n)`. -fn ev_leaf(bits: &mut BitsMut, n: u64) { +fn ev_leaf(bits: &mut BitsBuf, n: u64) { ev_leaf_wide(bits, &Base::from(n)); } /// Append an event leaf with an arbitrary-width stored base: flag `0`, then /// `gamma(base)`. -fn ev_leaf_wide(bits: &mut BitsMut, base: &Base) { +fn ev_leaf_wide(bits: &mut BitsBuf, base: &Base) { bits.push(false); codec::encode_int(bits, base); } @@ -143,7 +143,7 @@ fn ev_leaf_wide(bits: &mut BitsMut, base: &Base) { /// bits), maximizing node count and recursion depth simultaneously. Normal form /// holds everywhere: each internal node's spine child has base 0, and the only /// leaf pair is `(0, 1)`. -fn ev_spine(bits: &mut BitsMut, d: usize) { +fn ev_spine(bits: &mut BitsBuf, d: usize) { for _ in 0..d { bits.push(true); // internal-node flag codec::encode_int(bits, &Base::from(0u8)); // gamma(0) = "1" @@ -166,7 +166,7 @@ fn ev_spine(bits: &mut BitsMut, d: usize) { /// Panics if `d == 0`: the spine needs at least one internal node. fn dense(d: usize) -> Packed { assert!(d >= 1, "dense spine needs at least one internal node"); - let mut bits = BitsMut::with_capacity(4 * d + 4); + let mut bits = BitsBuf::with_capacity((4 * d + 4) as u64); ev_spine(&mut bits, d); Packed::from_bits(bits) } @@ -184,7 +184,7 @@ fn dense(d: usize) -> Packed { fn bigroot(b: usize, d: usize) -> Packed { assert!(b >= 1, "bigroot needs a nonzero root magnitude"); assert!(d >= 1, "bigroot needs a nonzero spine depth"); - let mut bits = BitsMut::with_capacity(2 * b + 4 * d + 8); + let mut bits = BitsBuf::with_capacity((2 * b + 4 * d + 8) as u64); bits.push(true); // root node flag codec::encode_int(&mut bits, &pow2_minus_1(b)); ev_spine(&mut bits, d); // left child: the dense spine (its root has base 0) @@ -203,7 +203,7 @@ fn bigroot(b: usize, d: usize) -> Packed { /// Panics if `b == 0`. fn hugeleaf(b: usize) -> Packed { assert!(b >= 1, "hugeleaf needs a nonzero magnitude"); - let mut bits = BitsMut::with_capacity(2 * b + 2); + let mut bits = BitsBuf::with_capacity((2 * b + 2) as u64); bits.push(false); // leaf flag codec::encode_int(&mut bits, &pow2_minus_1(b)); Packed::from_bits(bits) @@ -236,7 +236,7 @@ fn hugeleaf(b: usize) -> Packed { fn cliff_comb(k: usize, n: usize) -> Packed { assert!(k >= 1, "cliff comb needs a nonzero tooth magnitude"); assert!(n >= 1, "cliff comb needs at least one tooth"); - let mut bits = BitsMut::with_capacity(n * (2 * k + 10) + 2); + let mut bits = BitsBuf::with_capacity((n * (2 * k + 10) + 2) as u64); let tooth = pow2_minus_1(k); for _ in 0..n { bits.push(true); // spine node flag @@ -277,7 +277,7 @@ fn cliff_comb(k: usize, n: usize) -> Packed { fn jump_comb(k: usize, n: usize) -> Packed { assert!(k >= 1, "jump comb needs a nonzero cliff magnitude"); assert!(n >= 2, "jump comb needs a low tooth and a cliff tooth"); - let mut bits = BitsMut::with_capacity((n - 1) * (2 * k + 10) + 14); + let mut bits = BitsBuf::with_capacity(((n - 1) * (2 * k + 10) + 14) as u64); let tooth = pow2_minus_1(k); let one = Base::from(1u8); for i in 0..n { @@ -324,7 +324,7 @@ fn wide_tooth_comb(k: usize, w: usize, n: usize) -> Packed { "wide-tooth comb needs its cliff above its tooth width" ); assert!(n >= 1, "wide-tooth comb needs at least one tooth"); - let mut bits = BitsMut::with_capacity(n * (2 * k + 2 * w + 6) + 2); + let mut bits = BitsBuf::with_capacity((n * (2 * k + 2 * w + 6) + 2) as u64); let tooth_width = pow2(w); let tooth_base = pow2(k) - &tooth_width; for _ in 0..n { @@ -367,7 +367,7 @@ fn wide_tooth_comb(k: usize, w: usize, n: usize) -> Packed { fn cliff_fan(k: usize, n: usize) -> Packed { assert!(k >= 1, "cliff fan needs a nonzero root magnitude"); assert!(n >= 1, "cliff fan needs at least one tooth"); - let mut bits = BitsMut::with_capacity(12 * n + 2 * k + 6); + let mut bits = BitsBuf::with_capacity((12 * n + 2 * k + 6) as u64); bits.push(true); // root node flag codec::encode_int(&mut bits, &pow2_minus_1(k)); let one = Base::from(1u8); @@ -414,7 +414,7 @@ fn cliff_fan(k: usize, n: usize) -> Packed { fn cancelling_chain(k: usize, n: usize) -> Packed { assert!(k >= 1, "cancelling chain needs a nonzero peak magnitude"); assert!(n >= 1, "cancelling chain needs at least one tooth"); - let mut bits = BitsMut::with_capacity(n * (2 * k + 10) + 2); + let mut bits = BitsBuf::with_capacity((n * (2 * k + 10) + 2) as u64); let peak_drop = pow2_minus_1(k); let one = Base::from(1u8); for _ in 0..n { @@ -454,7 +454,7 @@ fn cancelling_chain(k: usize, n: usize) -> Packed { /// Panics if `d == 0`: the spine needs at least one internal node. fn harmonic(d: usize) -> Packed { assert!(d >= 1, "harmonic spine needs at least one internal node"); - let mut bits = BitsMut::with_capacity(6 * d + 2); + let mut bits = BitsBuf::with_capacity((6 * d + 2) as u64); for _ in 0..d { bits.push(true); // internal-node flag codec::encode_int(&mut bits, &Base::ZERO); // gamma(0) = "1" @@ -491,7 +491,7 @@ fn harmonic(d: usize) -> Packed { /// Panics if `d == 0`: the spine needs at least one internal node. fn alt_spine(d: usize) -> Packed { assert!(d >= 1, "alternating spine needs at least one internal node"); - let mut bits = BitsMut::with_capacity(4 * d + 4); + let mut bits = BitsBuf::with_capacity((4 * d + 4) as u64); // Levels 0..d−1 have one internal child each (left at even levels, // right at odd); level d−1 is the bottom node with leaves (0, 1). for level in 0..d { @@ -533,7 +533,7 @@ fn alt_spine(d: usize) -> Packed { /// Panics if `e == 0`. fn scattered_id(e: usize) -> Packed { assert!(e >= 1, "scattered id needs at least one owned fragment"); - let mut bits = BitsMut::with_capacity(6 * e + 2); + let mut bits = BitsBuf::with_capacity((6 * e + 2) as u64); for _ in 0..e { bits.push(true); // fragment node: left child present ... bits.push(true); // ... and the spine continues right @@ -560,7 +560,7 @@ fn scattered_id(e: usize) -> Packed { /// Panics if `d == 0`. fn id_spine(d: usize, divert: bool) -> Packed { assert!(d >= 1, "id spine needs at least one unary node"); - let mut bits = BitsMut::with_capacity(2 * d + 2); + let mut bits = BitsBuf::with_capacity((2 * d + 2) as u64); for _ in 0..d - 1 { bits.push(true); // left child present ... bits.push(false); // ... right child absent @@ -588,7 +588,7 @@ fn id_spine(d: usize, divert: bool) -> Packed { /// Panics if `d == 0`. fn nested_full_id(d: usize) -> Packed { assert!(d >= 1, "nested-full id needs at least one shortcut level"); - let mut bits = BitsMut::with_capacity(4 * d + 4); + let mut bits = BitsBuf::with_capacity((4 * d + 4) as u64); for _ in 0..d { bits.push(true); // left child present (the spine continues) ... bits.push(true); // ... and a right child follows it @@ -624,7 +624,7 @@ fn nested_left_full_id(d: usize) -> Packed { d >= 1, "nested-left-full id needs at least one shortcut level" ); - let mut bits = BitsMut::with_capacity(4 * d + 4); + let mut bits = BitsBuf::with_capacity((4 * d + 4) as u64); for _ in 0..d { bits.push(true); // left child present (the full terminal) ... bits.push(true); // ... and the spine continues right @@ -656,7 +656,7 @@ fn nested_left_full_id(d: usize) -> Packed { fn wide_tail(b: usize, d: usize) -> Packed { assert!(b >= 1, "wide tail needs a nonzero magnitude"); assert!(d >= 1, "wide tail needs a nonzero spine depth"); - let mut bits = BitsMut::with_capacity(4 * d + 2 * b + 3); + let mut bits = BitsBuf::with_capacity((4 * d + 2 * b + 3) as u64); for _ in 0..d { bits.push(true); // spine node flag ... codec::encode_int(&mut bits, &Base::from(0u8)); // ... base 0 @@ -685,7 +685,7 @@ fn wide_tail(b: usize, d: usize) -> Packed { /// Panics if `d == 0`. fn staircase(d: usize) -> Packed { assert!(d >= 1, "the staircase needs at least one internal node"); - let mut bits = BitsMut::with_capacity(5 * d + 8); + let mut bits = BitsBuf::with_capacity((5 * d + 8) as u64); bits.push(true); // the root: base 0 (the whole tree's minimum) codec::encode_int(&mut bits, &Base::from(0u8)); for _ in 1..d { @@ -725,7 +725,7 @@ fn staircase(d: usize) -> Packed { /// floor leaf). `4·lead + 2(m − 1) + Σ_{v=1}^{m} 2·bitlen(v + 1)` bits. /// Min-lifted normal form holds at every node (every subtree bottoms at /// its zero floor), and no sibling leaf pair is equal. -fn hole_region(bits: &mut BitsMut, lead: usize, m: usize) { +fn hole_region(bits: &mut BitsBuf, lead: usize, m: usize) { debug_assert!(lead >= 2, "a hole region's block routing needs depth 2+"); debug_assert!(m >= 1, "a hole region needs at least one descending step"); for _ in 0..lead - 1 { @@ -776,7 +776,7 @@ fn collapse_hole(k: usize, m: usize) -> (Packed, Packed) { "the collapse hole needs an even unit count" ); assert!(m >= 1, "the collapse hole needs a nonzero region size"); - let mut ev = BitsMut::new(); + let mut ev = BitsBuf::new(); for i in 0..k { ev.push(true); // spine node codec::encode_int(&mut ev, &Base::ZERO); @@ -786,7 +786,7 @@ fn collapse_hole(k: usize, m: usize) -> (Packed, Packed) { ev_leaf(&mut ev, 0); // the site's absent-side sibling leaf } ev_leaf(&mut ev, 0); // the spine's trailing no-stake leaf - let mut id = BitsMut::new(); + let mut id = BitsBuf::new(); for i in 0..k { id.push(true); // spine node: the unit hangs left ... id.push(i + 1 < k); // ... and the spine continues (absent at the end) @@ -828,7 +828,7 @@ fn copy_hole(k: usize, m: usize) -> (Packed, Packed) { "the copy hole needs an even unit count" ); assert!(m >= 1, "the copy hole needs a nonzero region size"); - let mut ev = BitsMut::new(); + let mut ev = BitsBuf::new(); ev.push(true); // the root site's node codec::encode_int(&mut ev, &Base::ZERO); ev_leaf(&mut ev, 0); // its collapsed left leaf @@ -838,7 +838,7 @@ fn copy_hole(k: usize, m: usize) -> (Packed, Packed) { hole_region(&mut ev, 2 + (i % 2), m); // the absent-child range } ev_leaf(&mut ev, 0); // the owned tail leaf - let mut id = BitsMut::new(); + let mut id = BitsBuf::new(); id.push(true); // the root site: left full ... id.push(true); // ... over the spine id.push(false); // the full collapsed child @@ -881,7 +881,7 @@ fn raise_hole(k: usize, m: usize) -> (Packed, Packed) { "the raise hole needs an even unit count" ); assert!(m >= 1, "the raise hole needs a nonzero region size"); - let mut ev = BitsMut::new(); + let mut ev = BitsBuf::new(); for _ in 0..k { ev.push(true); // each chain node: its region trails in preorder codec::encode_int(&mut ev, &Base::ZERO); @@ -890,7 +890,7 @@ fn raise_hole(k: usize, m: usize) -> (Packed, Packed) { for i in (0..k).rev() { hole_region(&mut ev, 2 + (i % 2), m); // each node's raised right child } - let mut id = BitsMut::new(); + let mut id = BitsBuf::new(); for _ in 0..k { id.push(true); // chain node: the chain continues left ... id.push(true); // ... and its right child is full @@ -941,7 +941,7 @@ fn site_hole(k: usize, m: usize) -> (Packed, Packed) { "the site hole needs an even unit count" ); assert!(m >= 1, "the site hole needs a nonzero region size"); - let mut ev = BitsMut::new(); + let mut ev = BitsBuf::new(); ev.push(true); // the root site's node codec::encode_int(&mut ev, &Base::ZERO); ev_leaf(&mut ev, 0); // its collapsed left leaf @@ -954,7 +954,7 @@ fn site_hole(k: usize, m: usize) -> (Packed, Packed) { ev_leaf(&mut ev, 0); // the site's absent-side sibling leaf } ev_leaf(&mut ev, 0); // the spine's trailing no-stake leaf - let mut id = BitsMut::new(); + let mut id = BitsBuf::new(); id.push(true); // the root site: left full ... id.push(true); // ... over the spine id.push(false); // the full collapsed child @@ -992,7 +992,7 @@ fn site_hole(k: usize, m: usize) -> (Packed, Packed) { /// Panics if `k == 0`. fn memo_chain(k: usize, distinct: bool) -> Packed { assert!(k >= 1, "the memo chain needs at least one interior site"); - let mut bits = BitsMut::with_capacity(14 * k + 9); + let mut bits = BitsBuf::with_capacity((14 * k + 9) as u64); bits.push(true); // the root: the covering site's node codec::encode_int(&mut bits, &Base::ZERO); ev_leaf(&mut bits, 0); // the covering site's collapsed left leaf @@ -1021,7 +1021,7 @@ fn memo_chain(k: usize, distinct: bool) -> Packed { /// Panics if `k == 0`. fn memo_chain_id(k: usize) -> Packed { assert!(k >= 1, "the memo-chain id needs at least one interior site"); - let mut bits = BitsMut::with_capacity(10 * k + 8); + let mut bits = BitsBuf::with_capacity((10 * k + 8) as u64); bits.push(true); // the root: full left child ... bits.push(true); // ... over the spine bits.push(false); // the full left terminal @@ -1069,7 +1069,7 @@ fn memo_chain_id(k: usize) -> Packed { /// Panics if `d == 0`. fn memo_comb(d: usize) -> Packed { assert!(d >= 1, "the memo comb needs at least one level"); - let mut bits = BitsMut::with_capacity(20 * d + 24); + let mut bits = BitsBuf::with_capacity((20 * d + 24) as u64); bits.push(true); // the root: the outermost covering site's node codec::encode_int(&mut bits, &Base::ZERO); ev_leaf(&mut bits, 0); // its collapsed left leaf @@ -1100,7 +1100,7 @@ fn memo_comb(d: usize) -> Packed { /// Panics if `d == 0`. fn memo_comb_id(d: usize) -> Packed { assert!(d >= 1, "the memo-comb id needs at least one level"); - let mut bits = BitsMut::with_capacity(14 * d + 12); + let mut bits = BitsBuf::with_capacity((14 * d + 12) as u64); bits.push(true); // the root: full left child over the comb bits.push(true); bits.push(false); // the full left terminal @@ -1154,7 +1154,7 @@ fn memo_fanout(k: usize, b: usize) -> Packed { assert!(b >= 1, "the memo fan-out needs a nonzero magnitude"); let wide = pow2_minus_1(b); let below = wide.clone() - &Base::from(1u8); - let mut bits = BitsMut::with_capacity(13 * k + 4 * b + 9); + let mut bits = BitsBuf::with_capacity((13 * k + 4 * b + 9) as u64); bits.push(true); // the root: the covering site's node codec::encode_int(&mut bits, &Base::ZERO); ev_leaf(&mut bits, 0); // the covering site's collapsed left leaf @@ -1188,7 +1188,7 @@ fn memo_oscillating(k: usize, b: usize) -> Packed { assert!(b >= 1, "the oscillating siblings need a nonzero magnitude"); let wide = pow2_minus_1(b); let one = Base::from(1u8); - let mut bits = BitsMut::with_capacity(13 * k + k * b + 9); + let mut bits = BitsBuf::with_capacity((13 * k + k * b + 9) as u64); bits.push(true); // the root: the covering site's node codec::encode_int(&mut bits, &Base::ZERO); ev_leaf(&mut bits, 0); // the covering site's collapsed left leaf @@ -1226,7 +1226,7 @@ fn memo_oscillating(k: usize, b: usize) -> Packed { /// Panics if `d == 0`. fn memo_churn(d: usize) -> Packed { assert!(d >= 1, "the memo churn needs at least one site"); - let mut bits = BitsMut::with_capacity(18 * d + 10 * (2 * d) + 20); + let mut bits = BitsBuf::with_capacity((18 * d + 10 * (2 * d) + 20) as u64); bits.push(true); // the root: the covering site's node codec::encode_int(&mut bits, &Base::ZERO); ev_leaf(&mut bits, 0); // the covering site's collapsed left leaf @@ -1269,7 +1269,7 @@ fn memo_churn(d: usize) -> Packed { /// Panics if `d == 0`. fn memo_churn_id(d: usize) -> Packed { assert!(d >= 1, "the memo-churn id needs at least one site"); - let mut bits = BitsMut::with_capacity(14 * d + 6); + let mut bits = BitsBuf::with_capacity((14 * d + 6) as u64); bits.push(true); // the root: full left child over the carriers bits.push(true); bits.push(false); // the full left terminal @@ -1308,7 +1308,7 @@ fn memo_churn_id(d: usize) -> Packed { /// Panics if `d == 0`. fn descending_raises(d: usize) -> Packed { assert!(d >= 1, "the descending raises need at least one site"); - let mut bits = BitsMut::with_capacity(13 * d + 30); + let mut bits = BitsBuf::with_capacity((13 * d + 30) as u64); bits.push(true); // the root: the covering site's node codec::encode_int(&mut bits, &Base::ZERO); ev_leaf(&mut bits, 0); // the covering site's collapsed left leaf @@ -1340,7 +1340,7 @@ fn descending_raises(d: usize) -> Packed { /// Panics if `d == 0`. fn descending_raises_id(d: usize) -> Packed { assert!(d >= 1, "the descending-raises id needs at least one site"); - let mut bits = BitsMut::with_capacity(10 * d + 10); + let mut bits = BitsBuf::with_capacity((10 * d + 10) as u64); bits.push(true); // the root: full left child over the rest bits.push(true); bits.push(false); // the full left terminal @@ -1394,7 +1394,7 @@ fn reveal_comb(k: usize, b: usize) -> Packed { assert!(b >= 1, "the reveal comb needs a nonzero magnitude"); let wide = pow2(b); let below = pow2_minus_1(b); - let mut bits = BitsMut::with_capacity(k * (4 * b + 8) + 6); + let mut bits = BitsBuf::with_capacity((k * (4 * b + 8) + 6) as u64); bits.push(true); // the root: the covering site's node codec::encode_int(&mut bits, &Base::ZERO); ev_leaf(&mut bits, 0); // the covering site's collapsed left leaf @@ -1432,7 +1432,7 @@ fn reveal_comb_hifloor(k: usize, b: usize) -> Packed { let wide = pow2(b); let below = pow2_minus_1(b); let floor = wide.clone() - &Base::from(2u8); - let mut bits = BitsMut::with_capacity(k * (4 * b + 8) + 2 * b + 4); + let mut bits = BitsBuf::with_capacity((k * (4 * b + 8) + 2 * b + 4) as u64); bits.push(true); // the root: the covering site's node codec::encode_int(&mut bits, &Base::ZERO); ev_leaf(&mut bits, 0); // the covering site's collapsed left leaf @@ -1465,7 +1465,7 @@ fn reveal_comb_hifloor(k: usize, b: usize) -> Packed { /// Panics if `k == 0`. fn reveal_comb_id(k: usize) -> Packed { assert!(k >= 1, "the reveal-comb id needs at least one site"); - let mut bits = BitsMut::with_capacity(10 * k + 4); + let mut bits = BitsBuf::with_capacity((10 * k + 4) as u64); bits.push(true); // the root: full left child ... bits.push(true); // ... over the comb bits.push(false); // the full left terminal @@ -1510,7 +1510,7 @@ fn pure_comb(k: usize, b: usize) -> Packed { assert!(k >= 1, "the pure comb needs at least one level"); assert!(b >= 1, "the pure comb needs a nonzero magnitude"); let wide = pow2(b); - let mut bits = BitsMut::with_capacity(k * (2 * b + 4) + 2); + let mut bits = BitsBuf::with_capacity((k * (2 * b + 4) + 2) as u64); for _ in 0..k { bits.push(true); // comb node a_i, i = k..1 codec::encode_int(&mut bits, &Base::ZERO); @@ -1535,7 +1535,7 @@ fn pure_comb(k: usize, b: usize) -> Packed { /// Panics if `k == 0`. fn pure_comb_id(k: usize) -> Packed { assert!(k >= 1, "the pure-comb id needs at least one level"); - let mut bits = BitsMut::with_capacity(6 * k); + let mut bits = BitsBuf::with_capacity((6 * k) as u64); for _ in 1..k { bits.push(true); // b_i: the deeper comb left ... bits.push(true); // ... and the leaf's id right @@ -1610,7 +1610,7 @@ fn ascend_spine(k: usize, b: usize, ascend: bool) -> Packed { "the ascent must stay inside the width-b code band" ); let wide = pow2(b); - let mut bits = BitsMut::with_capacity(k * (2 * b + 4) + 2); + let mut bits = BitsBuf::with_capacity((k * (2 * b + 4) + 2) as u64); for i in 1..=k { bits.push(true); // spine node S_i, i = 1..=k codec::encode_int(&mut bits, &Base::ZERO); @@ -1661,7 +1661,7 @@ fn freeze_position(k: usize) -> Packed { let unit = suanpan::UBig::ONE; let descent = (&wide + &unit) * suanpan::UBig::from(k as u64); let mut value = (suanpan::UBig::ONE << band) + descent; - let mut bits = BitsMut::with_capacity(4 * k * (band + 2) + 2); + let mut bits = BitsBuf::with_capacity((4 * k * (band + 2) + 2) as u64); for _ in 0..k { for drop in [&wide, &unit] { bits.push(true); // spine node: base 0, leaf left, spine right @@ -1737,7 +1737,7 @@ fn promotion_rearm(p: usize) -> Packed { let settle = pow2(PROMOTION_REARM_SETTLE_BITS); let zero = Base::ZERO; let one = Base::from(1u8); - let mut bits = BitsMut::with_capacity(1972 * p + 4); + let mut bits = BitsBuf::with_capacity((1972 * p + 4) as u64); for level in 0..PROMOTION_REARM_LEVELS_PER_BLOCK * p { bits.push(true); // span-builder node: alternating leaf left codec::encode_int(&mut bits, &zero); @@ -1779,7 +1779,7 @@ fn promotion_rearm_mate(p: usize) -> Packed { // The spine matches PR(p) node for node: 32p span-builder levels // plus the 4p block levels, the alternation running through both. let levels = (PROMOTION_REARM_LEVELS_PER_BLOCK + 4) * p; - let mut bits = BitsMut::with_capacity(180 * p + 4); + let mut bits = BitsBuf::with_capacity((180 * p + 4) as u64); for level in 0..levels { bits.push(true); // spine node: alternating leaf left codec::encode_int(&mut bits, &zero); @@ -1829,7 +1829,7 @@ fn dense_suffix(p: usize, d: usize) -> Packed { let arm = pow2(PROMOTION_REARM_ARM_BITS); let settle = pow2(PROMOTION_REARM_SETTLE_BITS); let one = Base::from(1u8); - let mut bits = BitsMut::with_capacity(134 * d + 1812 * p + 4); + let mut bits = BitsBuf::with_capacity((134 * d + 1812 * p + 4) as u64); let trailing = gap_spine(&mut bits, d); for _ in 0..p { for base in [&arm, &one, &settle, &one] { @@ -1865,7 +1865,7 @@ fn dense_suffix_mate(p: usize, d: usize) -> Packed { assert!(p >= 1, "the dense-suffix mate needs at least one block"); assert!(d >= 1, "the dense-suffix mate needs at least one gap"); let one = Base::from(1u8); - let mut bits = BitsMut::with_capacity(134 * d + 24 * p + 4); + let mut bits = BitsBuf::with_capacity((134 * d + 24 * p + 4) as u64); let trailing = gap_spine(&mut bits, d); for _ in 0..4 * p { bits.push(true); // block node: 0-leaf left, chain right @@ -1914,7 +1914,7 @@ fn wide_arming(w: usize, d: usize) -> Packed { let arm = pow2(32 * w); let settle = pow2(PROMOTION_REARM_SETTLE_BITS); let one = Base::from(1u8); - let mut bits = BitsMut::with_capacity(134 * d + 64 * w + 600); + let mut bits = BitsBuf::with_capacity((134 * d + 64 * w + 600) as u64); let trailing = gap_spine(&mut bits, d); for base in [&arm, &one, &settle, &one] { bits.push(true); // the one block: 0-leaf left, chain right @@ -1969,7 +1969,7 @@ fn hoisted_window(w: usize, d: usize, t: usize) -> Packed { let arm = pow2(32 * w); let settle = pow2(PROMOTION_REARM_SETTLE_BITS); let one = Base::from(1u8); - let mut bits = BitsMut::with_capacity(134 * d + 64 * w + 4 * t + 600); + let mut bits = BitsBuf::with_capacity((134 * d + 64 * w + 4 * t + 600) as u64); let trailing = gap_spine(&mut bits, d); for base in [&arm, &one, &settle, &one] { bits.push(true); // the one block: 0-leaf left, chain right @@ -1989,7 +1989,7 @@ fn hoisted_window(w: usize, d: usize, t: usize) -> Packed { /// A turn's 1-leaf is emitted before the descent; the return value is the count /// of trailing 0-leaf siblings the caller must emit innermost-first after the /// spine's terminal content. -fn gap_spine(bits: &mut BitsMut, d: usize) -> usize { +fn gap_spine(bits: &mut BitsBuf, d: usize) -> usize { let mut trailing = 0usize; for level in 0..DENSE_SUFFIX_DIGIT_STRIDE * d { bits.push(true); // spine node flag @@ -2018,7 +2018,7 @@ fn gap_spine(bits: &mut BitsMut, d: usize) -> usize { /// # Panics /// /// Panics if `s < 2` (the innermost node and the root are distinct). -fn parked_unit_spine(bits: &mut BitsMut, s: usize) { +fn parked_unit_spine(bits: &mut BitsBuf, s: usize) { assert!(s >= 2, "the parked-unit spine needs at least two levels"); for _ in 0..s { bits.push(true); // spine node flag, base 0 @@ -2060,11 +2060,11 @@ fn weight_comb(n: usize) -> Packed { n.is_power_of_two(), "the weight-comb block is one complete subtree" ); - let mut bits = BitsMut::with_capacity(202 * n - 4); + let mut bits = BitsBuf::with_capacity((202 * n - 4) as u64); parked_unit_spine(&mut bits, 32 * n); // The block: a complete subtree over 2n leaves alternating 0 and 2, // every internal base 0. - fn block(bits: &mut BitsMut, width: usize) { + fn block(bits: &mut BitsBuf, width: usize) { bits.push(true); // block node flag, base 0 codec::encode_int(bits, &Base::ZERO); if width == 2 { @@ -2124,10 +2124,10 @@ fn freeze_parade(k: usize) -> Packed { values.push(v.clone()); v -= suanpan::UBig::ONE; } - let mut bits = BitsMut::with_capacity(1546 * k - 2); + let mut bits = BitsBuf::with_capacity((1546 * k - 2) as u64); parked_unit_spine(&mut bits, 64 * k); // The min-lifted complete subtree over the descending run. - fn block(bits: &mut BitsMut, vals: &[suanpan::UBig], parent_min: &suanpan::UBig) { + fn block(bits: &mut BitsBuf, vals: &[suanpan::UBig], parent_min: &suanpan::UBig) { if let [leaf] = vals { ev_leaf_wide(bits, &Base::from(leaf - parent_min)); return; @@ -2201,8 +2201,8 @@ fn lone_freeze(pre: usize, post: usize) -> Packed { "the lone freeze needs a whole-pair low tail" ); let plateau = (suanpan::UBig::ONE << LONE_FREEZE_PLATEAU_BITS) + suanpan::UBig::from(2u8); - let mut bits = BitsMut::with_capacity(580 * pre + 6 * post + 14); - let leaf = |bits: &mut BitsMut, value: suanpan::UBig| { + let mut bits = BitsBuf::with_capacity((580 * pre + 6 * post + 14) as u64); + let leaf = |bits: &mut BitsBuf, value: suanpan::UBig| { bits.push(true); // spine node: base 0, leaf left, spine right codec::encode_int(bits, &Base::ZERO); ev_leaf_wide(bits, &Base::from(value)); @@ -2246,7 +2246,7 @@ fn tooth_tail(g: usize, m: usize) -> (Packed, Packed) { assert!(m >= 2, "the tooth-tail spike rides the second leaf"); let spike = pow2(32 * g); let build = |base_h: u64| -> Packed { - let mut bits = BitsMut::with_capacity(6 * m + 64 * g); + let mut bits = BitsBuf::with_capacity((6 * m + 64 * g) as u64); for i in 0..m { bits.push(true); // chain node: leaf left, chain right, base 0 codec::encode_int(&mut bits, &Base::ZERO); @@ -2304,7 +2304,7 @@ fn puncture_product(x: &suanpan::UBig, y: &suanpan::UBig) -> Packed { let levels = mass.bit_len(); let turns = (0..levels).filter(|&b| mass.bit(b)).count(); let plateau = Base::from(x.clone()); - let mut bits = BitsMut::with_capacity(4 * levels + turns * 2 * (x.bit_len() + 1) + 8); + let mut bits = BitsBuf::with_capacity((4 * levels + turns * 2 * (x.bit_len() + 1) + 8) as u64); let mut trailing = 0usize; for level in 0..levels { bits.push(true); // spine node flag @@ -2493,7 +2493,7 @@ fn arming_train(n: usize, w: usize, g: usize, alternate: bool) -> Packed { // The plateau band's floor plus double-swing headroom: every wide leaf // below stays inside [2^band, 2^(band+1)), one gamma width. let mut plateau = (suanpan::UBig::ONE << band) + (&arm << 1); - let mut bits = BitsMut::with_capacity(n * (g * (2 * band + 132) + 8 * band + 16) + 2); + let mut bits = BitsBuf::with_capacity((n * (g * (2 * band + 132) + 8 * band + 16) + 2) as u64); let mut trailing = 0usize; for b in 0..n { for level in 0..DENSE_SUFFIX_DIGIT_STRIDE * g { @@ -2549,7 +2549,7 @@ fn ascend_cliff_id(k: usize) -> Packed { k >= 1, "the ascending-cliff id needs at least one spine node" ); - let mut bits = BitsMut::with_capacity(2 * k + 4); + let mut bits = BitsBuf::with_capacity((2 * k + 4) as u64); for _ in 0..k { bits.push(false); // S_i's tag: left absent (the wide leaf stays) ... bits.push(true); // ... right present (the descent continues) @@ -2616,7 +2616,7 @@ fn dominated_undercut(k: usize, b: usize) -> Packed { let wide = pow2(b + 2) + pow2(b); // 5 · 2^b let raise = Base::from(DOMINATED_UNDERCUT_RAISE); let rise = Base::from(DOMINATED_UNDERCUT_EXIT_RISE); - let mut bits = BitsMut::with_capacity(k * (2 * b + 26) + 2); + let mut bits = BitsBuf::with_capacity((k * (2 * b + 26) + 2) as u64); for _ in 0..k { bits.push(true); // spine node codec::encode_int(&mut bits, &Base::ZERO); @@ -2650,7 +2650,7 @@ fn dominated_undercut(k: usize, b: usize) -> Packed { /// Panics if `k == 0`. fn dominated_undercut_id(k: usize) -> Packed { assert!(k >= 1, "the dominated-undercut id needs at least one site"); - let mut bits = BitsMut::with_capacity(6 * k + 2); + let mut bits = BitsBuf::with_capacity((6 * k + 2) as u64); for _ in 0..k { bits.push(true); // the spine node: the site ... bits.push(true); // ... then deeper @@ -2738,7 +2738,7 @@ fn seam_plunge(k: usize, r: usize) -> Packed { let rung = seam_rung(); let base = seam_wide(r); let leaf_bits = 64 * r - 58; - let mut bits = BitsMut::with_capacity((k + 1) * (leaf_bits + 2) + 2); + let mut bits = BitsBuf::with_capacity(((k + 1) * (leaf_bits + 2) + 2) as u64); let mut value = base; for _ in 0..=k { value += &rung; @@ -2787,7 +2787,7 @@ fn seam_plunge_control(k: usize, r: usize) -> Packed { "the rung sum must stay within the ascending leaves' shared bit length" ); let rung = seam_rung(); - let mut bits = BitsMut::with_capacity(136 * k + 64 * r + 78); + let mut bits = BitsBuf::with_capacity((136 * k + 64 * r + 78) as u64); bits.push(true); // node 1: base = the first arming's absolute height codec::encode_int(&mut bits, &(seam_wide(r) + &rung)); ev_leaf(&mut bits, 0); @@ -2834,7 +2834,7 @@ fn seam_plunge_control(k: usize, r: usize) -> Packed { /// Panics if `k == 0` or `k > 2^11` (the descending leaves must share one /// bit length and stay strictly positive under the shared base). fn seam_stop(k: usize) -> Packed { - let mut bits = BitsMut::with_capacity(164 * k + 266); + let mut bits = BitsBuf::with_capacity((164 * k + 266) as u64); bits.push(true); // the root, base 0 codec::encode_int(&mut bits, &Base::ZERO); ev_leaf(&mut bits, 0); // the floor leaf: arms the root at 0 @@ -2856,14 +2856,14 @@ fn seam_stop(k: usize) -> Packed { /// /// As [`seam_stop`]. fn seam_stop_control(k: usize) -> Packed { - let mut bits = BitsMut::with_capacity(164 * k + 262); + let mut bits = BitsBuf::with_capacity((164 * k + 262) as u64); seam_stop_descent(&mut bits, k); Packed::from_bits(bits) } /// Append the seam-stop descent subtree: the based node over `k` descending /// three-digit leaves and the rel-0 terminal (the layouts above). -fn seam_stop_descent(bits: &mut BitsMut, k: usize) { +fn seam_stop_descent(bits: &mut BitsBuf, k: usize) { assert!( k >= 1, "the seam stop needs at least one descending residue" @@ -2936,7 +2936,7 @@ fn latent_ladder(w: usize, k: usize) -> Packed { ); let anchor = seam_wide(w); let leaf_bits = 64 * w - 58; - let mut bits = BitsMut::with_capacity(k * (leaf_bits + 2) + 64 * w - 48); + let mut bits = BitsBuf::with_capacity((k * (leaf_bits + 2) + 64 * w - 48) as u64); for _ in 0..k { bits.push(true); // spine node, base 0 codec::encode_int(&mut bits, &Base::ZERO); @@ -3042,7 +3042,7 @@ fn jump_pair_operand(k: usize, m: usize, d: usize, band: bool) -> Packed { let tooth = &pow2(k) + 3u64; let plateau = &pow2(k) + 1u64; let zero = Base::ZERO; - let mut bits = BitsMut::with_capacity(132 * d + m * (2 * k + 14) + 2); + let mut bits = BitsBuf::with_capacity((132 * d + m * (2 * k + 14) + 2) as u64); // The shared descent spine: right turns every 33rd level consume // their 0-leaf before the comb (the freeze-position bits), left // turns queue theirs after it. @@ -3200,10 +3200,10 @@ fn stagger_comb(n: usize, m: usize, i: usize) -> Packed { // level, the unit tooth at the bottom. Depth L + log2(m) is word-scale for // any buildable population, so plain recursion is safe here (the generators // are test-only construction code). - fn path(bits: &mut BitsMut, levels: u32, i: usize, t: u32) { + fn path(bits: &mut BitsBuf, levels: u32, i: usize, t: u32) { bits.push(true); // path node flag codec::encode_int(bits, &Base::ZERO); - let deeper = |bits: &mut BitsMut| { + let deeper = |bits: &mut BitsBuf| { if t + 1 == levels { ev_leaf(bits, 1); // the tooth: operand i's unit height } else { @@ -3218,7 +3218,7 @@ fn stagger_comb(n: usize, m: usize, i: usize) -> Packed { deeper(bits); // ... the slot right } } - fn top(bits: &mut BitsMut, levels: u32, i: usize, m: usize) { + fn top(bits: &mut BitsBuf, levels: u32, i: usize, m: usize) { if m == 1 { path(bits, levels, i, 0); return; @@ -3228,7 +3228,7 @@ fn stagger_comb(n: usize, m: usize, i: usize) -> Packed { top(bits, levels, i, m / 2); top(bits, levels, i, m / 2); } - let mut bits = BitsMut::with_capacity(m * (4 * levels as usize + 6) - 2); + let mut bits = BitsBuf::with_capacity((m * (4 * levels as usize + 6) - 2) as u64); top(&mut bits, levels, i, m); Packed::from_bits(bits) } @@ -3266,7 +3266,7 @@ fn stagger_id(n: usize, m: usize, i: usize) -> Packed { ); assert!(i < n, "the operand index addresses one of the n slots"); let levels = n.trailing_zeros(); - fn path(bits: &mut BitsMut, levels: u32, i: usize, t: u32) { + fn path(bits: &mut BitsBuf, levels: u32, i: usize, t: u32) { if t == levels { bits.push(false); // the owned slot: terminal tag "00" bits.push(false); @@ -3277,7 +3277,7 @@ fn stagger_id(n: usize, m: usize, i: usize) -> Packed { bits.push(right); // right child present iff it sits right path(bits, levels, i, t + 1); } - fn top(bits: &mut BitsMut, levels: u32, i: usize, m: usize) { + fn top(bits: &mut BitsBuf, levels: u32, i: usize, m: usize) { if m == 1 { path(bits, levels, i, 0); return; @@ -3287,7 +3287,7 @@ fn stagger_id(n: usize, m: usize, i: usize) -> Packed { top(bits, levels, i, m / 2); top(bits, levels, i, m / 2); } - let mut bits = BitsMut::with_capacity(m * (2 * levels as usize + 4) - 2); + let mut bits = BitsBuf::with_capacity((m * (2 * levels as usize + 4) - 2) as u64); top(&mut bits, levels, i, m); Packed::from_bits(bits) } @@ -3397,7 +3397,7 @@ fn mask_drift_triple(k: usize, n: usize) -> (Packed, Packed, Packed) { n >= 2 && n.is_multiple_of(2), "the mask-drift triple needs an even tooth count" ); - let mut plateau = BitsMut::with_capacity(2 * k + 2); + let mut plateau = BitsBuf::with_capacity((2 * k + 2) as u64); ev_leaf_wide(&mut plateau, &pow2(k)); ( cliff_comb(k, n), @@ -3451,7 +3451,7 @@ fn mask_drift_quadruple(k: usize, n: usize) -> ((Packed, Packed), (Packed, Packe /// teeth's `(0, 1)`. fn sparse_cliff_comb(k: usize, n: usize) -> Packed { debug_assert!(k >= 1 && n >= 2 && n.is_multiple_of(2)); - let mut bits = BitsMut::with_capacity((n / 2) * (2 * k + 14) + 2); + let mut bits = BitsBuf::with_capacity(((n / 2) * (2 * k + 14) + 2) as u64); let tooth = pow2_minus_1(k); for level in 0..n { bits.push(true); // spine node flag @@ -3481,7 +3481,7 @@ fn sparse_cliff_comb(k: usize, n: usize) -> Packed { /// final gap level) and no node has two absent children. fn scattered_id_offset(e: usize) -> Packed { debug_assert!(e >= 1); - let mut bits = BitsMut::with_capacity(6 * e + 4); + let mut bits = BitsBuf::with_capacity((6 * e + 4) as u64); for _ in 0..e { bits.push(false); // gap node: left child absent ... bits.push(true); // ... the spine continues right @@ -3524,7 +3524,7 @@ fn masked_hole(d: usize, h: usize) -> (Packed, Packed, Packed) { "the masked hole's mask needs a unary run to divert from" ); assert!(d > h, "the masked hole's spine must outrun its mask"); - let mut plateau = BitsMut::with_capacity(4); + let mut plateau = BitsBuf::with_capacity(4_u64); ev_leaf(&mut plateau, 2); // dominates every spine height (they are 0 or 1) (dense(d), id_spine(h, true), Packed::from_bits(plateau)) } diff --git a/crates/before/src/meter/board/defect.rs b/crates/before/src/meter/board/defect.rs index ef023b82..c43f04a8 100644 --- a/crates/before/src/meter/board/defect.rs +++ b/crates/before/src/meter/board/defect.rs @@ -94,14 +94,14 @@ fn last_leaf_flag_pos(v: &Version) -> u64 { pub(super) fn version_noncanonical_bytes(v: &Version) -> Vec { let bits = v.as_bits(); let leaf = last_leaf_flag_pos(v); - let mut out = codec::BitsMut::with_capacity(bits.len() as usize + 4); + let mut out = codec::BitsBuf::with_capacity(bits.len() + 4); codec::extend_from_view(&mut out, bits, 0, leaf); out.push(false); // the old leaf's position becomes an internal node codec::extend_from_view(&mut out, bits, leaf, bits.len()); // left child: the old leaf verbatim out.push(true); // right child: a leaf equal to its sibling codec::encode_int(&mut out, &Base::from(0u32)); // zero delta codec::seal_padding(&mut out); - out.into_vec() + out.into_bytes() } /// `p`'s stream with its preorder-last terminal split into a collapsible @@ -117,7 +117,7 @@ pub(super) fn party_noncanonical_bytes(p: &Party) -> Vec { !bits.bit(end - 2) && !bits.bit(end - 1), "a preorder id stream ends in a terminal tag" ); - let mut out = codec::BitsMut::with_capacity(end as usize + 4); + let mut out = codec::BitsBuf::with_capacity(end + 4); codec::extend_from_view(&mut out, bits, 0, end - 2); out.push(true); // the last terminal becomes a node with both children out.push(true); @@ -126,7 +126,7 @@ pub(super) fn party_noncanonical_bytes(p: &Party) -> Vec { out.push(false); } codec::seal_padding(&mut out); - out.into_vec() + out.into_bytes() } /// `text` with junk appended after the complete valid notation: the parser diff --git a/crates/before/src/meter/board/family.rs b/crates/before/src/meter/board/family.rs index e1d5e09e..aa38c005 100644 --- a/crates/before/src/meter/board/family.rs +++ b/crates/before/src/meter/board/family.rs @@ -1110,12 +1110,12 @@ fn disjoint_mounted_pair(id: &[u8]) -> (Vec, Vec) { let shape = decode_party(id); let mount = |left: bool| -> Vec { let view = shape.as_bits(); - let mut bits = codec::BitsMut::with_capacity(view.len() as usize + 2); + let mut bits = codec::BitsBuf::with_capacity(view.len() + 2); bits.push(left); bits.push(!left); codec::extend_from_view(&mut bits, view, 0, view.len()); codec::seal_padding(&mut bits); - bits.into_vec() + bits.into_bytes() }; let (a, b) = (mount(true), mount(false)); assert!( @@ -1152,7 +1152,7 @@ pub(super) fn overlap_mounted_pair(id: &[u8]) -> (Vec, Vec) { "the overlap-mount adapter needs a non-terminal shape: a full shape's mount would \ not be normal form" ); - let mut a = codec::BitsMut::with_capacity(bits.len() as usize + 2 * path.len() + 4); + let mut a = codec::BitsBuf::with_capacity(bits.len() + 2 * path.len() as u64 + 4); a.push(true); // root: both children present a.push(true); codec::extend_from_view(&mut a, bits, 0, bits.len()); // left: the shape @@ -1164,12 +1164,12 @@ pub(super) fn overlap_mounted_pair(id: &[u8]) -> (Vec, Vec) { a.push(false); // the marker's terminal, at the shape's last owned position a.push(false); codec::seal_padding(&mut a); - let mut b = codec::BitsMut::with_capacity(bits.len() as usize + 2); + let mut b = codec::BitsBuf::with_capacity(bits.len() + 2); b.push(false); // root: right child only b.push(true); codec::extend_from_view(&mut b, bits, 0, bits.len()); // right: the shape codec::seal_padding(&mut b); - let (a, b) = (a.into_vec(), b.into_vec()); + let (a, b) = (a.into_bytes(), b.into_bytes()); assert!( !decode_party(&a).is_disjoint(&decode_party(&b)), "the overlap-mount adapter must mint an overlapping pair" @@ -1219,13 +1219,13 @@ fn rightmost_terminal_path(bits: codec::BitsView<'_>) -> Vec { /// accumulator index answers the same test in O(probe), which is the separation /// the row watches. pub(super) fn overlap_fold_probe() -> Vec { - let mut probe = codec::BitsMut::with_capacity(4); + let mut probe = codec::BitsBuf::with_capacity(4); probe.push(false); // root: right child only probe.push(true); probe.push(false); // the right child: a full leaf probe.push(false); codec::seal_padding(&mut probe); - probe.into_vec() + probe.into_bytes() } /// Decode packed bytes the board itself generated. diff --git a/crates/before/src/meter/tier2/tests.rs b/crates/before/src/meter/tier2/tests.rs index 18c066bd..0420fb10 100644 --- a/crates/before/src/meter/tier2/tests.rs +++ b/crates/before/src/meter/tier2/tests.rs @@ -385,7 +385,7 @@ fn skyline_join(a: &Version, b: &Version) -> Version { let decoded = skyline::decode(crate::codec::built_view(&out)).expect("an emitted join is canonical"); assert_eq!( - out.len() as u64, + out.len(), tier2_size(crate::codec::built_view(&packed_bits_of( &to_oracle_version(&decoded) ))) @@ -404,7 +404,7 @@ fn skyline_meet(a: &Version, b: &Version) -> Version { let decoded = skyline::decode(crate::codec::built_view(&out)).expect("an emitted meet is canonical"); assert_eq!( - out.len() as u64, + out.len(), tier2_size(crate::codec::built_view(&packed_bits_of( &to_oracle_version(&decoded) ))) diff --git a/crates/before/src/party.rs b/crates/before/src/party.rs index 04ebf6ae..c107dab7 100644 --- a/crates/before/src/party.rs +++ b/crates/before/src/party.rs @@ -74,11 +74,11 @@ pub struct Party(codec::Bits); static_assertions::assert_not_impl_any!(Party: Clone, Copy); // Equality and hashing are byte-level over the stored stream's raw bytes plus -// its live length, resting on the canonical-raw-slice invariant: `from_bits` -// zeroes the dead pad bits at every storage seam, so raw-byte equality is -// exactly bit equality (see `codec::canonical_eq` for the argument and the -// measurement). The two impls read the same pair, so `Eq`/`Hash` consistency -// holds by construction. +// its live length, resting on the canonical-raw-slice invariant: the build +// buffer keeps its dead bits zero and `from_bits` seals the marker at every +// storage seam, so raw-byte equality is exactly bit equality (see +// `codec::canonical_eq` for the argument and the measurement). The two impls +// read the same pair, so `Eq`/`Hash` consistency holds by construction. impl PartialEq for Party { fn eq(&self, other: &Self) -> bool { codec::canonical_eq(&self.0, &other.0) @@ -671,9 +671,9 @@ impl Party { /// /// Callers guarantee normal *tree* form (a nonempty, normalized id); /// the freeze seals the marker padding so the stored bytes are - /// canonical — see [`codec::Bits::freeze`] for why a tree op can leave - /// the tail dirty, and what the padding underpins. - pub(crate) fn from_bits(bits: codec::BitsMut) -> Self { + /// canonical — see [`codec::Bits::freeze`] for the seam's contract and + /// what the padding underpins. + pub(crate) fn from_bits(bits: codec::BitsBuf) -> Self { Party(codec::Bits::freeze(bits)) } @@ -747,7 +747,7 @@ impl core::str::FromStr for Party { /// Wrap validated id bits as a `Party`, rejecting the anonymous (empty) /// identity. The single gate through which every parsed/built top-level `Party` /// passes. -fn finish_id(bits: codec::BitsMut) -> Result { +fn finish_id(bits: codec::BitsBuf) -> Result { if codec::id_is_empty(codec::built_view(&bits)) { Err(Parse::Anonymous) } else { @@ -772,11 +772,11 @@ mod sealed { #[doc(hidden)] pub trait PartyLiteral: sealed::Sealed { #[doc(hidden)] - fn into_id_bits(self) -> Result; + fn into_id_bits(self) -> Result; } impl PartyLiteral for u8 { - fn into_id_bits(self) -> Result { + fn into_id_bits(self) -> Result { match self { 0 => Ok(codec::id_leaf(false)), 1 => Ok(codec::id_leaf(true)), @@ -786,13 +786,13 @@ impl PartyLiteral for u8 { } impl PartyLiteral for bool { - fn into_id_bits(self) -> Result { + fn into_id_bits(self) -> Result { Ok(codec::id_leaf(self)) } } impl PartyLiteral for (T, S) { - fn into_id_bits(self) -> Result { + fn into_id_bits(self) -> Result { let l = self.0.into_id_bits()?; let r = self.1.into_id_bits()?; codec::id_node(&l, &r) // assembles + validates normal form diff --git a/crates/before/src/party/ops/build.rs b/crates/before/src/party/ops/build.rs index 07dfaa0c..2373f2c0 100644 --- a/crates/before/src/party/ops/build.rs +++ b/crates/before/src/party/ops/build.rs @@ -1,4 +1,4 @@ -use crate::codec::{BitStack, BitsMut, BitsView, PackedBuilder, PopStack}; +use crate::codec::{BitStack, BitsBuf, BitsView, PackedBuilder, PopStack}; use crate::idbits::{IdNode, IdReader}; /// Single-buffer builder for normalized id output. @@ -37,17 +37,17 @@ pub(super) enum Built { /// borrow checker stops it being reused or dropped silently — so an open with /// no matching close cannot compile. #[must_use = "an opened node must be closed with close_node"] -pub(super) struct Open(usize); +pub(super) struct Open(u64); /// The width of an id node's presence tag: one bit per child. const TAG_BITS: usize = 2; /// The output width of a node whose two children are both terminals: its own /// tag followed by the two terminal tags. -const TERMINAL_PAIR_BITS: usize = 3 * TAG_BITS; +const TERMINAL_PAIR_BITS: u64 = 3 * TAG_BITS as u64; impl IdBuilder { - pub(super) fn with_capacity(capacity: usize) -> Self { + pub(super) fn with_capacity(capacity: u64) -> Self { IdBuilder { out: PackedBuilder::with_capacity(capacity), } @@ -149,7 +149,7 @@ impl IdBuilder { self.terminal() } - pub(super) fn finish(self) -> BitsMut { + pub(super) fn finish(self) -> BitsBuf { self.out.finish() } } @@ -191,7 +191,7 @@ pub(super) struct IdSkylineBuilder { impl IdSkylineBuilder { /// Create a builder with room for `capacity` output bits. - pub(super) fn with_capacity(capacity: usize) -> Self { + pub(super) fn with_capacity(capacity: u64) -> Self { IdSkylineBuilder { out: IdBuilder::with_capacity(capacity), path: BitStack::new(), @@ -268,7 +268,7 @@ impl IdSkylineBuilder { } /// Take the finished canonical stream (empty for a wholly unowned tiling). - pub(super) fn finish(self) -> BitsMut { + pub(super) fn finish(self) -> BitsBuf { debug_assert!( self.root.is_some(), "an id tiling closes its root exactly once" @@ -335,7 +335,7 @@ impl IdSkylineBuilder { /// here the same way it does in the path stacks. struct PosStack { /// The innermost entry's absolute position (0 when empty). - top: usize, + top: u64, /// The entries' deltas from the entry under them, stored off by one /// so the width is nonzero even at delta 0 (the first entry at /// position 0). @@ -351,9 +351,9 @@ impl PosStack { } /// Push a position at or above the current top. - fn push(&mut self, pos: usize) { + fn push(&mut self, pos: u64) { debug_assert!(pos >= self.top, "reserved tag positions never move left"); - self.deltas.push((pos - self.top + 1) as u64); + self.deltas.push(pos - self.top + 1); self.top = pos; } @@ -362,9 +362,9 @@ impl PosStack { /// # Panics /// /// Panics if the stack is empty. - fn pop(&mut self) -> usize { + fn pop(&mut self) -> u64 { let pos = self.top; - self.top -= self.deltas.pop() as usize - 1; + self.top -= self.deltas.pop() - 1; pos } } diff --git a/crates/before/src/party/ops/compare.rs b/crates/before/src/party/ops/compare.rs index 627b01f3..2b274dc2 100644 --- a/crates/before/src/party/ops/compare.rs +++ b/crates/before/src/party/ops/compare.rs @@ -1,6 +1,6 @@ use core::ops::ControlFlow; -use crate::codec::BitsMut; +use crate::codec::BitsBuf; use crate::idbits::{IdNode, IdReader}; impl IdReader<'_> { @@ -112,7 +112,7 @@ fn lockstep_holds(mut a: IdReader, mut b: IdReader, a_settles: impl Fn(IdNode) - /// left pair's), so a unary lockstep chain of any depth keeps the stack empty. struct Lockstep { /// Two presence bits per queued right child pair, innermost on top. - pending: BitsMut, + pending: BitsBuf, /// Whether the current pair's `a` side is a present child (read the real /// cursor) or an absent `0` (stand in a synthetic empty). a_on: bool, @@ -124,7 +124,7 @@ impl Lockstep { /// A walk at its root pair: both sides are the real cursors. fn new() -> Lockstep { Lockstep { - pending: BitsMut::new(), + pending: BitsBuf::new(), a_on: true, b_on: true, } diff --git a/crates/before/src/party/ops/diff.rs b/crates/before/src/party/ops/diff.rs index 702ac564..7649ebc0 100644 --- a/crates/before/src/party/ops/diff.rs +++ b/crates/before/src/party/ops/diff.rs @@ -44,7 +44,7 @@ //! the shape on which a structural walk's recursion depth tracks the full tree //! depth — cost bits, not stack frames or grown segments. -use crate::codec::{BitsMut, BitsView}; +use crate::codec::{BitsBuf, BitsView}; use crate::idbits::IdReader; use crate::version::skyline::overlay::{self, PlateauCursor}; @@ -70,12 +70,11 @@ impl IdReader<'_> { /// `O(|self| + |other|)`: the sweep form of `oracle::Party::without` (the /// module doc), reading each operand's tags at most once and emitting one /// output plateau or covered block per item of the overlay. - pub(crate) fn diff(self, other: IdReader) -> BitsMut { + pub(crate) fn diff(self, other: IdReader) -> BitsBuf { // `self \ other ⊆ self`, but over a full `self` plateau the output // is `other`'s complement, which can be as large as `other`. Both // inputs combined is a safe bound; normalization only shrinks it. - let mut out = - IdSkylineBuilder::with_capacity((self.bits().len() + other.bits().len()) as usize); + let mut out = IdSkylineBuilder::with_capacity(self.bits().len() + other.bits().len()); let (mut a, a_entering) = IdLeafCursor::open(self); let (mut b, b_entering) = IdLeafCursor::open(other); settle_pair(&mut a, &mut b, a_entering, b_entering); @@ -251,11 +250,11 @@ struct IdLeafCursor<'a> { pos: u64, /// Root-to-item branch directions: `false` inside a left child slot, `true` /// inside a right. - path: BitsMut, + path: BitsBuf, /// One bit per open left-branch level, innermost last: whether that /// ancestor's right child is present in the stream (`false` = the right /// slot is a synthetic unowned plateau). - pending_right: BitsMut, + pending_right: BitsBuf, /// Count of left-branch levels in `path`: zero exactly at the final item /// (the all-right path), so [`done`](Self::done) is `O(1)`. open_lefts: usize, @@ -276,8 +275,8 @@ impl<'a> IdLeafCursor<'a> { let mut this = IdLeafCursor { bits: BitsView::empty(), pos: 0, - path: BitsMut::new(), - pending_right: BitsMut::new(), + path: BitsBuf::new(), + pending_right: BitsBuf::new(), open_lefts: 0, item: Item::Plateau { owned: false }, }; @@ -386,8 +385,12 @@ impl PlateauCursor for IdLeafCursor<'_> { type Crossing = bool; /// The current item's depth: its interval has width `2^-depth`. + /// + /// Depths are `usize` across the walk surface: each open ancestor costs + /// at least one bit of the stored id, whose live length fits `usize` on + /// every target (the storage doors' bound). fn depth(&self) -> usize { - self.path.len() + usize::try_from(self.path.len()).expect("depth is bounded by the id's live length") } /// Whether the current item is the tiling's last (it ends at the @@ -418,7 +421,7 @@ impl PlateauCursor for IdLeafCursor<'_> { } self.open_lefts -= 1; self.path.push(true); - let flip = self.path.len(); + let flip = self.depth(); let right_present = self .pending_right .pop() diff --git a/crates/before/src/party/ops/split.rs b/crates/before/src/party/ops/split.rs index 108d8158..0b2631e5 100644 --- a/crates/before/src/party/ops/split.rs +++ b/crates/before/src/party/ops/split.rs @@ -1,4 +1,4 @@ -use crate::codec::{extend_from_view, BitsMut, BitsView}; +use crate::codec::{extend_from_view, BitsBuf, BitsView}; use crate::idbits::{IdNode, IdReader}; impl IdReader<'_> { @@ -17,10 +17,10 @@ impl IdReader<'_> { /// is absence, not a leaf). At a terminal the split is `(1,0)`/`(0,1)`. /// /// The recursive form of `oracle::Party::split` (the paper's `split`). - pub(crate) fn split(self) -> (BitsMut, BitsMut) { + pub(crate) fn split(self) -> (BitsBuf, BitsBuf) { // split(0) = (0, 0): the empty id splits into two empties. if let IdNode::Empty = self.peek() { - return (BitsMut::new(), BitsMut::new()); + return (BitsBuf::new(), BitsBuf::new()); } let start = self.pos(); build_split(self.bits(), start) @@ -42,7 +42,7 @@ enum SpineEnd { /// copy of already-normal bit ranges, normal by construction (the kept child is /// nonempty, so no collapse can arise). Iterative: the spine walk is a loop, so /// deep ids cannot overflow. -fn build_split(bits: BitsView<'_>, start: u64) -> (BitsMut, BitsMut) { +fn build_split(bits: BitsView<'_>, start: u64) -> (BitsBuf, BitsBuf) { let mut pos = start; let (prefix_end, kind) = loop { match (bits.bit(pos), bits.bit(pos + 1)) { @@ -52,7 +52,7 @@ fn build_split(bits: BitsView<'_>, start: u64) -> (BitsMut, BitsMut) { } }; let prefix = start..prefix_end; - let prefix_len = (prefix_end - start) as usize; + let prefix_len = prefix_end - start; match kind { SpineEnd::Branch => { @@ -69,15 +69,13 @@ fn build_split(bits: BitsView<'_>, start: u64) -> (BitsMut, BitsMut) { // Each half keeps one child and drops the other, so its length is // exact: prefix + the 2-bit retagged branch + the kept child. - let mut a = - BitsMut::with_capacity(prefix_len + 2 + (right_child - left_child) as usize); + let mut a = BitsBuf::with_capacity(prefix_len + 2 + (right_child - left_child)); extend_from_view(&mut a, bits, prefix.start, prefix.end); a.push(true); // branch → Left-only: keep i1 ... a.push(false); // ... drop i2 extend_from_view(&mut a, bits, left_child, right_child); - let mut b = - BitsMut::with_capacity(prefix_len + 2 + (branch_end - right_child) as usize); + let mut b = BitsBuf::with_capacity(prefix_len + 2 + (branch_end - right_child)); extend_from_view(&mut b, bits, prefix.start, prefix.end); b.push(false); // branch → Right-only: drop i1 ... b.push(true); // ... keep i2 @@ -93,14 +91,14 @@ fn build_split(bits: BitsView<'_>, start: u64) -> (BitsMut, BitsMut) { bits.len(), "the terminal is the spine's tail", ); - let mut a = BitsMut::with_capacity(prefix_len + 4); + let mut a = BitsBuf::with_capacity(prefix_len + 4); extend_from_view(&mut a, bits, prefix.start, prefix.end); a.push(true); // (1, 0): Left-only ... a.push(false); a.push(false); // ... over a terminal a.push(false); - let mut b = BitsMut::with_capacity(prefix_len + 4); + let mut b = BitsBuf::with_capacity(prefix_len + 4); extend_from_view(&mut b, bits, prefix.start, prefix.end); b.push(false); // (0, 1): Right-only ... b.push(true); diff --git a/crates/before/src/party/ops/sum.rs b/crates/before/src/party/ops/sum.rs index a4766877..f6388bcc 100644 --- a/crates/before/src/party/ops/sum.rs +++ b/crates/before/src/party/ops/sum.rs @@ -1,4 +1,4 @@ -use crate::codec::BitsMut; +use crate::codec::BitsBuf; use crate::idbits::{IdNode, IdReader}; use super::build::{Built, IdBuilder}; @@ -25,11 +25,11 @@ impl IdReader<'_> { /// /// The nodes are [`peek`](IdReader::peek)ed, not read: a copied side must /// stay unconsumed so `copy_reader` can splice its whole subtree. - pub(crate) fn sum(mut self, mut other: IdReader) -> Option { + pub(crate) fn sum(mut self, mut other: IdReader) -> Option { // Conservative: the disjoint union has at most as many bits as both // inputs combined; normalization (collapsing `(v, v)` leaves) only // shrinks it. No tighter bound is cheap without doing the sum. - let mut out = IdBuilder::with_capacity((self.bits().len() + other.bits().len()) as usize); + let mut out = IdBuilder::with_capacity(self.bits().len() + other.bits().len()); let mut frames = Frames::new(); // Whether the current pair's side is a present child (read the real // cursor) or an absent `0` (stand in a synthetic empty). @@ -133,7 +133,7 @@ impl IdReader<'_> { /// retracts a fixed-width suffix of the output /// ([`IdBuilder::collapse_terminal_pair`]). struct Frames { - bits: BitsMut, + bits: BitsBuf, } /// One popped [`Frames`] entry; see the stack's two shapes. @@ -149,7 +149,7 @@ enum Frame { impl Frames { fn new() -> Frames { Frames { - bits: BitsMut::new(), + bits: BitsBuf::new(), } } diff --git a/crates/before/src/party/ops/sum_split.rs b/crates/before/src/party/ops/sum_split.rs index 47b3403e..9ce12f25 100644 --- a/crates/before/src/party/ops/sum_split.rs +++ b/crates/before/src/party/ops/sum_split.rs @@ -1,4 +1,4 @@ -use crate::codec::{built_view, extend_from_view, BitsMut, BitsView}; +use crate::codec::{built_view, extend_from_view, BitsBuf, BitsView}; use crate::idbits::{IdNode, IdReader}; impl<'a> IdReader<'a> { @@ -64,7 +64,7 @@ impl<'a> IdReader<'a> { /// present on one side alone is spliced without reading its nodes, where /// the composition pays two scans of it (`sum`'s copy skip, then `split`'s /// subtree-end scan) plus its bytes in the built union. - pub(crate) fn sum_split(mut self, mut other: IdReader) -> Option<(BitsMut, BitsMut)> { + pub(crate) fn sum_split(mut self, mut other: IdReader) -> Option<(BitsBuf, BitsBuf)> { // An empty operand leaves the union the other operand, whole, so the // halves are its plain split. Only the root can be empty: below it, // presence in the union keeps both cursors live. @@ -75,7 +75,7 @@ impl<'a> IdReader<'a> { return Some(self.split()); } // The union's spine tags, shared by both halves (split's prefix). - let mut spine = BitsMut::new(); + let mut spine = BitsBuf::new(); loop { let (a_node, b_node) = (self.peek(), other.peek()); let (al, ar) = match a_node { @@ -135,23 +135,23 @@ enum UnionChild<'a> { /// subtree's verbatim bit range within it. Verbatim(BitsView<'a>, u64, u64), /// The child is present on both sides: the merged (summed) subtree. - Merged(BitsMut), + Merged(BitsBuf), } impl UnionChild<'_> { /// The child's bit length, for the halves' exact capacity hints. - fn len(&self) -> usize { + fn len(&self) -> u64 { match self { - UnionChild::Verbatim(_, start, end) => (end - start) as usize, + UnionChild::Verbatim(_, start, end) => end - start, UnionChild::Merged(bits) => bits.len(), } } /// Append the child's bits to a half. - fn append_to(&self, out: &mut BitsMut) { + fn append_to(&self, out: &mut BitsBuf) { match self { UnionChild::Verbatim(bits, start, end) => extend_from_view(out, *bits, *start, *end), - UnionChild::Merged(bits) => out.extend_from_bitslice(bits), + UnionChild::Merged(bits) => out.extend_from_buf(bits), } } } @@ -204,9 +204,9 @@ fn union_child<'a>( /// Assemble one half: the spine, the branch retagged to its kept side, and the /// kept child's bits. -fn half(spine: &BitsMut, left: bool, right: bool, child: &UnionChild) -> BitsMut { - let mut out = BitsMut::with_capacity(spine.len() + 2 + child.len()); - out.extend_from_bitslice(spine); +fn half(spine: &BitsBuf, left: bool, right: bool, child: &UnionChild) -> BitsBuf { + let mut out = BitsBuf::with_capacity(spine.len() + 2 + child.len()); + out.extend_from_buf(spine); out.push(left); out.push(right); child.append_to(&mut out); @@ -215,9 +215,9 @@ fn half(spine: &BitsMut, left: bool, right: bool, child: &UnionChild) -> BitsMut /// Assemble one delegated-mode half: the spine, then the composition's own half /// of the branch subtree's union. -fn splice(spine: &BitsMut, tail: &BitsMut) -> BitsMut { - let mut out = BitsMut::with_capacity(spine.len() + tail.len()); - out.extend_from_bitslice(spine); - out.extend_from_bitslice(tail); +fn splice(spine: &BitsBuf, tail: &BitsBuf) -> BitsBuf { + let mut out = BitsBuf::with_capacity(spine.len() + tail.len()); + out.extend_from_buf(spine); + out.extend_from_buf(tail); out } diff --git a/crates/before/src/party/tests.rs b/crates/before/src/party/tests.rs index 642aa8cd..9ae0a96d 100644 --- a/crates/before/src/party/tests.rs +++ b/crates/before/src/party/tests.rs @@ -435,9 +435,9 @@ proptest! { #[test] fn unindexed_fallback_matches_the_walk_on_constructed_pairs() { use self::constructed::{complement_leftmost, full, leftmost, node}; - use crate::codec::BitsMut; - let empty = BitsMut::new(); - let pairs: Vec<(BitsMut, BitsMut)> = vec![ + use crate::codec::BitsBuf; + let empty = BitsBuf::new(); + let pairs: Vec<(BitsBuf, BitsBuf)> = vec![ (leftmost(6), complement_leftmost(6)), (leftmost(6), leftmost(6)), (leftmost(6), leftmost(3)), @@ -584,11 +584,11 @@ fn sum_split_collapsed_union_matches_terminal_split() { /// witnesses and tripwires assemble their operands from, each built tags-first /// in one pass so a deep stream costs one allocation, not one per level. mod constructed { - use crate::codec::BitsMut; + use crate::codec::BitsBuf; /// The full `1` leaf: terminal tag `00`. - pub(super) fn full() -> BitsMut { - let mut b = BitsMut::new(); + pub(super) fn full() -> BitsBuf { + let mut b = BitsBuf::new(); b.push(false); b.push(false); b @@ -596,33 +596,33 @@ mod constructed { /// An internal node over the present children (normal form is the caller's /// obligation: at least one child, never two terminals). - pub(super) fn node(left: Option<&BitsMut>, right: Option<&BitsMut>) -> BitsMut { - let mut b = BitsMut::new(); + pub(super) fn node(left: Option<&BitsBuf>, right: Option<&BitsBuf>) -> BitsBuf { + let mut b = BitsBuf::new(); b.push(left.is_some()); b.push(right.is_some()); if let Some(l) = left { - b.extend_from_bitslice(l); + b.extend_from_buf(l); } if let Some(r) = right { - b.extend_from_bitslice(r); + b.extend_from_buf(r); } b } /// `levels` unary nodes toward `left_side` over `tail` (built tags-first, /// so a deep spine costs one pass, not one per level). - pub(super) fn spine(levels: usize, left_side: bool, tail: BitsMut) -> BitsMut { - let mut b = BitsMut::with_capacity(2 * levels + tail.len()); + pub(super) fn spine(levels: usize, left_side: bool, tail: BitsBuf) -> BitsBuf { + let mut b = BitsBuf::with_capacity(2 * levels as u64 + tail.len()); for _ in 0..levels { b.push(left_side); b.push(!left_side); } - b.extend_from_bitslice(&tail); + b.extend_from_buf(&tail); b } /// The leftmost `2^-k` cell: a `k`-level left-unary spine over `1`. - pub(super) fn leftmost(k: usize) -> BitsMut { + pub(super) fn leftmost(k: usize) -> BitsBuf { spine(k, true, full()) } @@ -632,17 +632,17 @@ mod constructed { /// Built by one preorder pass — `k − 1` both-present nodes whose left child /// continues and whose right child is full, then the deepest right-only /// cell. - pub(super) fn complement_leftmost(k: usize) -> BitsMut { - let mut b = BitsMut::with_capacity(4 * k); + pub(super) fn complement_leftmost(k: usize) -> BitsBuf { + let mut b = BitsBuf::with_capacity(4 * k as u64); for _ in 1..k { b.push(true); b.push(true); } b.push(false); b.push(true); - b.extend_from_bitslice(&full()); + b.extend_from_buf(&full()); for _ in 1..k { - b.extend_from_bitslice(&full()); + b.extend_from_buf(&full()); } b } @@ -663,11 +663,11 @@ mod constructed { mod sum_split_constructed { use super::constructed::{complement_leftmost, full, leftmost, node, spine}; use super::*; - use crate::codec::BitsMut; + use crate::codec::BitsBuf; /// The fused walk against its composition on one id pair, in both operand /// orders (byte equality, `None` arms included). - fn assert_matches_composition(a: &BitsMut, b: &BitsMut) { + fn assert_matches_composition(a: &BitsBuf, b: &BitsBuf) { for (x, y) in [(a, b), (b, a)] { let fused = IdReader::root(crate::codec::built_view(x)) .sum_split(IdReader::root(crate::codec::built_view(y))); @@ -755,7 +755,7 @@ mod sum_split_constructed { f(); crate::codec::scan::scan_bits() }; - let compare = |name: &str, a: &BitsMut, b: &BitsMut| -> u64 { + let compare = |name: &str, a: &BitsBuf, b: &BitsBuf| -> u64 { let fused = scan(&|| { IdReader::root(crate::codec::built_view(a)) .sum_split(IdReader::root(crate::codec::built_view(b))); @@ -801,7 +801,7 @@ mod sum_split_constructed { /// other, and two empties split to empties. #[test] fn root_leaf_and_empty_operands_match_composition() { - let empty = BitsMut::new(); + let empty = BitsBuf::new(); assert_matches_composition(&full(), &leftmost(3)); assert_matches_composition(&full(), &empty); assert_matches_composition(&empty, &empty.clone()); @@ -824,7 +824,7 @@ mod sum_split_constructed { mod diff_constructed { use super::constructed::{complement_leftmost, full, leftmost, node}; use super::*; - use crate::codec::BitsMut; + use crate::codec::BitsBuf; /// The scale ladder: every family runs at each `k`, byte-checked. const SCALES: [usize; 3] = [256, 4096, 100_000]; @@ -836,7 +836,7 @@ mod diff_constructed { /// `self \ other` on one constructed pair: byte-equal to `expected`, and /// at oracle-reachable scales (`k <= ORACLE_SCALE_MAX`) also equal to the /// recursive oracle's `without`, compared over lowered oracle trees. - fn assert_diff(a: &BitsMut, b: &BitsMut, expected: &BitsMut, k: usize) { + fn assert_diff(a: &BitsBuf, b: &BitsBuf, expected: &BitsBuf, k: usize) { let d = IdReader::root(crate::codec::built_view(a)) .diff(IdReader::root(crate::codec::built_view(b))); assert_eq!( @@ -889,7 +889,7 @@ mod diff_constructed { for k in SCALES { let a = leftmost(k); let b = node(Some(&full()), None); - assert_diff(&a, &b, &BitsMut::new(), k); + assert_diff(&a, &b, &BitsBuf::new(), k); } } @@ -929,7 +929,7 @@ mod diff_constructed { /// Constant scan overhead of a settled block beyond its operand reads /// and output write: the root-level tag reservations and patches. const BLOCK_SLACK: u64 = 8; - let scan = |a: &BitsMut, b: &BitsMut| -> u64 { + let scan = |a: &BitsBuf, b: &BitsBuf| -> u64 { crate::codec::scan::reset(); IdReader::root(crate::codec::built_view(a)) .diff(IdReader::root(crate::codec::built_view(b))); @@ -945,8 +945,8 @@ mod diff_constructed { ("owned-cover block", &owned_cover, 0), ] { let blocked = scan(&spine, cover); - let floor = (spine.len() + cover.len()) as u64; - let ceiling = floor + output_len as u64 + BLOCK_SLACK; + let floor = spine.len() + cover.len(); + let ceiling = floor + output_len + BLOCK_SLACK; assert!( floor <= blocked && blocked <= ceiling, "{name} k={k}: scanned {blocked} bits outside \ @@ -1017,7 +1017,7 @@ proptest! { ) { let a = from_oracle_party(&oa); let b = from_oracle_party(&ob); - let bit_eq = a.as_bits().to_bitvec() == b.as_bits().to_bitvec(); + let bit_eq = a.as_bits().to_buf() == b.as_bits().to_buf(); prop_assert_eq!(a == b, bit_eq); prop_assert_eq!(b == a, bit_eq); if a == b { diff --git a/crates/before/src/testing/bridge.rs b/crates/before/src/testing/bridge.rs index 103e7e2f..e8425f62 100644 --- a/crates/before/src/testing/bridge.rs +++ b/crates/before/src/testing/bridge.rs @@ -13,7 +13,7 @@ use std::sync::Arc; -use crate::codec::{self, BitsMut}; +use crate::codec::{self, BitsBuf}; use crate::oracle; use crate::recurse::descend; use crate::{Clock, Party, Version}; @@ -26,7 +26,7 @@ fn id_is_zero(t: &oracle::Party) -> bool { matches!(t, oracle::Party::Leaf(false)) } -fn emit_id(out: &mut BitsMut, t: &oracle::Party) { +fn emit_id(out: &mut BitsBuf, t: &oracle::Party) { match t { oracle::Party::Leaf(false) => {} // `0`: absence, no bits oracle::Party::Leaf(true) => { @@ -44,7 +44,7 @@ fn emit_id(out: &mut BitsMut, t: &oracle::Party) { } } -fn emit_ev(out: &mut BitsMut, t: &oracle::Version) { +fn emit_ev(out: &mut BitsBuf, t: &oracle::Version) { match t { oracle::Version::Leaf(n) => { out.push(false); @@ -61,8 +61,8 @@ fn emit_ev(out: &mut BitsMut, t: &oracle::Version) { /// The min-lifted packed preorder stream of an oracle tree: the /// construction language the generators and the skyline transcoder share. -pub(crate) fn packed_bits_of(t: &oracle::Version) -> BitsMut { - let mut bits = BitsMut::new(); +pub(crate) fn packed_bits_of(t: &oracle::Version) -> BitsBuf { + let mut bits = BitsBuf::new(); emit_ev(&mut bits, t); bits } @@ -70,7 +70,7 @@ pub(crate) fn packed_bits_of(t: &oracle::Version) -> BitsMut { /// Build the impl `Party` whose canonical bits encode `t`. Recursive over a bounded /// oracle tree (test-only; the impl's own traversals are iterative). pub(crate) fn from_oracle_party(t: &oracle::Party) -> Party { - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); emit_id(&mut bits, t); Party::from_bits(bits) } @@ -81,7 +81,7 @@ pub(crate) fn from_oracle_party(t: &oracle::Party) -> Party { /// traversals are iterative): emits the min-lifted packed preorder stream, /// then transcodes it into the skyline coding the version stores. pub(crate) fn from_oracle_version(t: &oracle::Version) -> Version { - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); emit_ev(&mut bits, t); Version::from_bits(crate::version::skyline::encode_bits( crate::codec::built_view(&bits), diff --git a/crates/before/src/testing/compactness.rs b/crates/before/src/testing/compactness.rs index bd4e50f5..ad47baca 100644 --- a/crates/before/src/testing/compactness.rs +++ b/crates/before/src/testing/compactness.rs @@ -21,7 +21,7 @@ use proptest::prelude::*; -use crate::codec::{self, Base, BitsMut}; +use crate::codec::{self, Base, BitsBuf}; use crate::meter::tier2::{tier2_size, Tier2Size}; use crate::Version; @@ -81,7 +81,7 @@ pub(crate) fn check_sample(version: &Version) -> Sample { let packed = crate::testing::bridge::packed_bits_of(&crate::testing::bridge::to_oracle_version(version)); let tier2 = tier2_size(crate::codec::built_view(&packed)); - let current_bits = packed.len() as u64; + let current_bits = packed.len(); let ratio = tier2.total_bits as f64 / current_bits as f64; let envelope = 2.0 * current_bits as f64 + TIER2_NODE_ENVELOPE_BITS * tier2.nodes as f64; @@ -135,7 +135,7 @@ pub(crate) fn comb(m_bits: usize, pairs: usize) -> Version { let m = (Base::from(1u8) << m_bits_u32) - &Base::from(1u8); let pair_bits = 2 * m_bits + 8; - let mut bits = BitsMut::with_capacity(pairs * pair_bits - 2); + let mut bits = BitsBuf::with_capacity((pairs * pair_bits - 2) as u64); // The spine: each node is `1 . gamma(0)`, its left child the next spine // node (the innermost left child is the first pair subtree). for _ in 0..pairs - 1 { diff --git a/crates/before/src/testing/generators.rs b/crates/before/src/testing/generators.rs index df173ac9..23ed320f 100644 --- a/crates/before/src/testing/generators.rs +++ b/crates/before/src/testing/generators.rs @@ -272,7 +272,7 @@ pub(crate) fn shape_party(shape: Shape, scale: usize) -> Party { /// children). Built with a flat loop: no recursion at any depth, in the builder /// or in `Drop` (the packed forms are flat buffers). pub(crate) fn deep_left_spine_party(depth: usize) -> Party { - let mut bits = codec::BitsMut::with_capacity(2 * depth + 2); + let mut bits = codec::BitsBuf::with_capacity(2 * depth as u64 + 2); for _ in 0..depth { bits.push(true); // Left-only tag `10`: left child present ... bits.push(false); // ... right child absent diff --git a/crates/before/src/testing/snapshots.rs b/crates/before/src/testing/snapshots.rs index 99cfcbce..8cedaae5 100644 --- a/crates/before/src/testing/snapshots.rs +++ b/crates/before/src/testing/snapshots.rs @@ -2,7 +2,7 @@ use insta::assert_snapshot; -use crate::codec::{encode_int, Base, BitsMut, BitsView}; +use crate::codec::{encode_int, Base, BitsBuf, BitsView}; use crate::error::{Crossed, Decode, Overlap, Parse}; use crate::{Clock, Party, Rank, Version}; @@ -25,7 +25,7 @@ fn bytes_to_hex(bytes: &[u8]) -> String { /// One Elias-gamma row: `n` then its code as an MSB-first bit string and the bit count. fn gamma_row(n: u64) -> String { - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); encode_int(&mut bits, &Base::from(n)); format!( "{:>20} -> {} ({} bits)", @@ -68,7 +68,7 @@ fn gamma_bit_layout_table() { // Arbitrary-width witness: 2^64 has no u64 representation, but the gamma code (and // therefore an event base of this magnitude) encodes and round-trips regardless. - let mut big_bits = BitsMut::new(); + let mut big_bits = BitsBuf::new(); let big = Base::from(1u8) << 64u32; // 2^64 encode_int(&mut big_bits, &big); assert_snapshot!( @@ -172,8 +172,8 @@ fn clock_canonical_form() { // A `Clock`'s canonical stream is its `Party` bits followed by its `Version` bits, // with no padding between (padding is added only by `encode`). Rebuild that unpadded // concatenation here to show the boundary between the two halves. - let mut bits = c.party().as_bits().to_bitvec(); - bits.extend_from_bitslice(&c.version().as_bits().to_bitvec()); + let mut bits = c.party().as_bits().to_buf(); + bits.extend_from_buf(&c.version().as_bits().to_buf()); let fields = format!( "display: {c}\ndebug: {c:?}\nbits: {} ({} bits)\nbytes: {}", bits_to_string(crate::codec::built_view(&bits)), diff --git a/crates/before/src/version.rs b/crates/before/src/version.rs index e2d1d7e0..8f16c66f 100644 --- a/crates/before/src/version.rs +++ b/crates/before/src/version.rs @@ -1141,7 +1141,7 @@ impl Version { /// Callers guarantee canonical skyline form; the freeze seals the /// marker padding so the stored bytes are canonical (see /// [`codec::Bits::freeze`]). - pub(crate) fn from_bits(bits: codec::BitsMut) -> Self { + pub(crate) fn from_bits(bits: codec::BitsBuf) -> Self { Version(codec::Bits::freeze(bits)) } diff --git a/crates/before/src/version/skyline.rs b/crates/before/src/version/skyline.rs index 38c0fb98..c29a6874 100644 --- a/crates/before/src/version/skyline.rs +++ b/crates/before/src/version/skyline.rs @@ -156,7 +156,7 @@ use crate::Version; #[cfg(any(test, feature = "meter"))] pub use crate::codec::Bits; #[cfg(any(test, feature = "meter"))] -pub use crate::codec::BitsMut; +pub use crate::codec::BitsBuf; pub use crate::codec::BitsView; // The admission walk: the span wire form's fused second-component parse, @@ -221,7 +221,7 @@ pub(crate) use validate::validate_from; /// /// Test- and meter-only, like the buffers it views. #[cfg(any(test, feature = "meter"))] -pub fn view(bits: &BitsMut) -> BitsView<'_> { +pub fn view(bits: &BitsBuf) -> BitsView<'_> { crate::codec::built_view(bits) } @@ -230,9 +230,9 @@ pub fn view(bits: &BitsMut) -> BitsView<'_> { /// Test- and meter-only: production callers reach the stored stream through /// [`Version::as_bytes`]/[`Version::encode`]. #[cfg(any(test, feature = "meter"))] -pub fn encode(version: &Version) -> BitsMut { +pub fn encode(version: &Version) -> BitsBuf { let view = version.as_bits(); - let mut bits = BitsMut::with_capacity(view.len() as usize); + let mut bits = BitsBuf::with_capacity(view.len()); // Live bits only — no padding: consumers walk these as a stream. crate::codec::extend_from_view(&mut bits, view, 0, view.len()); bits diff --git a/crates/before/src/version/skyline/admit.rs b/crates/before/src/version/skyline/admit.rs index 7f24ef99..54b98bcb 100644 --- a/crates/before/src/version/skyline/admit.rs +++ b/crates/before/src/version/skyline/admit.rs @@ -50,7 +50,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; -use crate::codec::{BitCursor, BitsMut, BitsView, Int}; +use crate::codec::{BitCursor, BitsBuf, BitsView, Int}; use crate::error::Decode; use super::overlay::{fold, LeafCursor, PlateauCursor, Side, Step}; @@ -75,10 +75,10 @@ struct CheckedCursor<'a, C> { cursor: &'a mut C, /// Root-to-leaf branch directions, root first (`false`: inside the left /// child, its right sibling still pending in the stream). - path: BitsMut, + path: BitsBuf, /// Per open ancestor: whether its completed left child was a leaf (a /// placeholder `false` until that child completes). - left_was_leaf: BitsMut, + left_was_leaf: BitsBuf, /// The count of `false` bits in `path`: zero exactly when the current /// leaf's plateau ends at the unit interval's right edge — the tree is /// whole and the stream's bits end here. @@ -98,8 +98,8 @@ where fn open(cursor: &'a mut C) -> Result<(Self, Int), Decode> { let mut this = CheckedCursor { cursor, - path: BitsMut::new(), - left_was_leaf: BitsMut::new(), + path: BitsBuf::new(), + left_was_leaf: BitsBuf::new(), open_lefts: 0, last_delta_zero: false, }; @@ -121,8 +121,12 @@ where } /// The current leaf's depth: its plateau has width `2^-depth`. + /// + /// Depths are `usize` across the walk surface: each open ancestor costs + /// at least one bit of the walked stream, whose live length fits `usize` + /// on every target (the storage doors' bound). fn depth(&self) -> usize { - self.path.len() + usize::try_from(self.path.len()).expect("depth is bounded by the stream's live length") } /// Whether the current leaf completes the tree (see `open_lefts`). @@ -187,7 +191,7 @@ where ), } } - let flip = self.path.len(); + let flip = self.depth(); let code = self.descend()?; self.last_delta_zero = code.is_zero(); let (sign, magnitude) = unzigzag(code); diff --git a/crates/before/src/version/skyline/build.rs b/crates/before/src/version/skyline/build.rs index 701c48da..294c5f0e 100644 --- a/crates/before/src/version/skyline/build.rs +++ b/crates/before/src/version/skyline/build.rs @@ -53,7 +53,7 @@ //! `skyline_join_*` rows) pins the whole emission's transient against these //! bounds. -use crate::codec::{BitStack, BitsMut, BitsView, Code, PackedBuilder, PopStack}; +use crate::codec::{BitStack, BitsBuf, BitsView, Code, PackedBuilder, PopStack}; /// The 1-bit payload code: `gamma(zigzag(0))`, the zero delta. /// @@ -68,7 +68,7 @@ use crate::codec::{BitStack, BitsMut, BitsView, Code, PackedBuilder, PopStack}; /// which is the absolute code only while the first leaf is held — and the first /// leaf lies on the leftmost, all-`false` path, so cascade's right-child test /// fails before the length is consulted. -const ZERO_DELTA_CODE_BITS: usize = 1; +const ZERO_DELTA_CODE_BITS: u64 = 1; /// A canonical-skyline stream builder driven by the output leaf sequence. /// @@ -98,7 +98,7 @@ pub(super) struct SkylineBuilder { impl SkylineBuilder { /// Create a builder with room for `capacity` output bits. - pub(super) fn with_capacity(capacity: usize) -> Self { + pub(super) fn with_capacity(capacity: u64) -> Self { SkylineBuilder { out: PackedBuilder::with_capacity(capacity), held: None, @@ -179,7 +179,7 @@ impl SkylineBuilder { self.left_leaf.push(left_is_leaf); if left_is_leaf { debug_assert!(flushed_len > 0, "payload codes are never empty"); - self.lens.push(flushed_len as u64); + self.lens.push(flushed_len); } debug_assert!( depth >= self.path.len(), @@ -291,7 +291,7 @@ impl SkylineBuilder { /// # Panics /// /// Panics if no leaf was ever appended. - pub(super) fn finish(mut self) -> BitsMut { + pub(super) fn finish(mut self) -> BitsBuf { let held = self .held .take() @@ -330,7 +330,7 @@ impl SkylineBuilder { // flag, left leaf code. The merged leaf keeps the left code — same // height, same predecessor — and the pair leaves the stream; each // copied bit is one being truncated. - let code_len = self.lens.pop() as usize; + let code_len = self.lens.pop(); let code = self.out.extract_code(self.out.len() - code_len); self.out.truncate(self.out.len() - code_len - 2); self.path.pop(); diff --git a/crates/before/src/version/skyline/build/tests.rs b/crates/before/src/version/skyline/build/tests.rs index c6362666..657cdae7 100644 --- a/crates/before/src/version/skyline/build/tests.rs +++ b/crates/before/src/version/skyline/build/tests.rs @@ -4,7 +4,7 @@ //! stream, so a bookkeeping error in absorb, re-anchor, or the cascade fails //! against bits a reader can re-derive in the margin. -use crate::codec::{self, Base, BitsMut, Code}; +use crate::codec::{self, Base, BitsBuf, Code}; use crate::version::skyline::signed::{gamma_code_signed, Sign}; use super::SkylineBuilder; @@ -20,7 +20,7 @@ fn delta(sign: Sign, magnitude: u64) -> Code { } /// Drive a builder over `(depth, code)` leaves and return the stream. -fn built(leaves: Vec<(usize, Code)>) -> BitsMut { +fn built(leaves: Vec<(usize, Code)>) -> BitsBuf { let mut builder = SkylineBuilder::with_capacity(64); for (depth, code) in leaves { builder.leaf(depth, code); @@ -29,7 +29,7 @@ fn built(leaves: Vec<(usize, Code)>) -> BitsMut { } /// A stream literal from a `0`/`1` string, whitespace ignored. -fn bits(s: &str) -> BitsMut { +fn bits(s: &str) -> BitsBuf { s.chars() .filter(|c| !c.is_whitespace()) .map(|c| match c { @@ -181,8 +181,8 @@ fn continuation( root_depth: usize, first_depth: usize, leaves: &[(usize, Code)], -) -> (BitsMut, usize, usize, usize) { - let mut range = BitsMut::new(); +) -> (BitsBuf, usize, usize, usize) { + let mut range = BitsBuf::new(); // The within-subtree path to the previous leaf; the subtree's first leaf is // its leftmost, so the path starts all left branches. let mut path = vec![false; first_depth - root_depth]; @@ -207,7 +207,7 @@ fn continuation( range.push(bits >> i & 1 == 1); } } - Code::Wide(code) => range.extend_from_bitslice(code), + Code::Wide(code) => range.extend_from_buf(code), } } let (last_depth, last_code) = leaves.last().expect("a continuation has at least one leaf"); @@ -215,7 +215,7 @@ fn continuation( range, first_depth - root_depth, last_depth - root_depth, - last_code.len(), + usize::try_from(last_code.len()).expect("test codes are small"), ) } diff --git a/crates/before/src/version/skyline/decode.rs b/crates/before/src/version/skyline/decode.rs index 0699ce8c..21aab50c 100644 --- a/crates/before/src/version/skyline/decode.rs +++ b/crates/before/src/version/skyline/decode.rs @@ -15,7 +15,7 @@ pub(crate) fn decode_bits(bits: BitsView<'_>) -> Result { validate_bits(bits)?; // One spare bit of capacity: the storage gate's padding marker lands // without regrowing (and re-copying) an exactly-sized buffer. - let mut copy = crate::codec::BitsMut::with_capacity(bits.len() as usize + 1); + let mut copy = crate::codec::BitsBuf::with_capacity(bits.len() + 1); crate::codec::extend_from_view(&mut copy, bits, 0, bits.len()); Ok(Version::from_bits(copy)) } diff --git a/crates/before/src/version/skyline/emit.rs b/crates/before/src/version/skyline/emit.rs index c50b7644..b1f65ea4 100644 --- a/crates/before/src/version/skyline/emit.rs +++ b/crates/before/src/version/skyline/emit.rs @@ -80,7 +80,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; -use crate::codec::{BitsMut, BitsView, Code, Int}; +use crate::codec::{BitsBuf, BitsView, Code, Int}; use super::build::SkylineBuilder; use super::overlay::{advance_diff, OpenedPair, PlateauCursor, Side, Step}; @@ -116,8 +116,8 @@ fn follow_min(sign: Ordering, current: Side) -> Side { /// /// One merge over the two streams; the module doc carries the emission algebra /// and the cost bounds. The output stream equals the recursive oracle's join -/// bit for bit (the differential suite pins it); its dead pad bits are left as -/// built — zeroing them is the storage gate's job (`Version::from_bits`). +/// bit for bit (the differential suite pins it); the marker padding is the +/// storage gate's job (`Version::from_bits`). /// /// # Panics /// @@ -126,7 +126,7 @@ fn follow_min(sign: Ordering, current: Side) -> Side { /// the walk structurally notices (truncation, malformation) panic; the rest (a /// collapsible sibling pair, a delta driving the running height negative) sweep /// silently, and the output is then unspecified. -pub fn join(a: BitsView<'_>, b: BitsView<'_>) -> BitsMut { +pub fn join(a: BitsView<'_>, b: BitsView<'_>) -> BitsBuf { emit(a, b, follow_max) } @@ -139,7 +139,7 @@ pub fn join(a: BitsView<'_>, b: BitsView<'_>) -> BitsMut { /// /// [`join`]'s contract exactly: canonical operands required, structural /// violations panic, the rest yield an unspecified output. -pub fn meet(a: BitsView<'_>, b: BitsView<'_>) -> BitsMut { +pub fn meet(a: BitsView<'_>, b: BitsView<'_>) -> BitsBuf { emit(a, b, follow_min) } @@ -154,9 +154,9 @@ pub struct Hull { /// pair: the fold sees only signs, never buffers. pub relation: Option, /// The meet (pointwise min) stream. - pub lo: BitsMut, + pub lo: BitsBuf, /// The join (pointwise max) stream. - pub hi: BitsMut, + pub hi: BitsBuf, } /// The hull `(meet, join)` of the versions two skyline streams denote, as @@ -220,12 +220,12 @@ pub fn hull(a_bits: BitsView<'_>, b_bits: BitsView<'_>) -> Hull { Emission { pick: follow_min, side: Side::A, - out: SkylineBuilder::with_capacity((a_bits.len() + b_bits.len()) as usize), + out: SkylineBuilder::with_capacity(a_bits.len() + b_bits.len()), }, Emission { pick: follow_max, side: Side::A, - out: SkylineBuilder::with_capacity((a_bits.len() + b_bits.len()) as usize), + out: SkylineBuilder::with_capacity(a_bits.len() + b_bits.len()), }, ]; for emission in &mut outputs { @@ -280,7 +280,7 @@ fn emit( a_bits: BitsView<'_>, b_bits: BitsView<'_>, pick: impl Fn(Ordering, Side) -> Side, -) -> BitsMut { +) -> BitsBuf { let OpenedPair { a: mut cursor_a, b: mut cursor_b, @@ -300,7 +300,7 @@ fn emit( // arbitrary: at a tie the two first heights are equal, so either side opens // the output identically. let mut side = pick(diff.sign(), Side::A); - let mut out = SkylineBuilder::with_capacity((a_bits.len() + b_bits.len()) as usize); + let mut out = SkylineBuilder::with_capacity(a_bits.len() + b_bits.len()); let first = match side { Side::A => &a_first, Side::B => &b_first, @@ -318,9 +318,9 @@ fn emit( out.leaf(cursor_a.depth().max(cursor_b.depth()), code); } - // Canonicalizing the storage (zeroing dead pad bits) is the job of - // `Version::from_bits`, the single gate a stream passes through when it - // becomes a stored value; intermediate streams stay as built. + // Sealing the marker padding is the job of `Version::from_bits`, the + // single gate a stream passes through when it becomes a stored value; + // intermediate streams stay unsealed. out.finish() } diff --git a/crates/before/src/version/skyline/emit/tests.rs b/crates/before/src/version/skyline/emit/tests.rs index b44b0f2c..6dd00f87 100644 --- a/crates/before/src/version/skyline/emit/tests.rs +++ b/crates/before/src/version/skyline/emit/tests.rs @@ -20,7 +20,7 @@ use rayon::prelude::*; use suanpan::Accumulator; use crate::codec::Base; -use crate::codec::{BitsMut, BitsView}; +use crate::codec::{BitsBuf, BitsView}; use crate::meter::registry::Shape; use crate::meter::Packed; use crate::testing::bridge::{from_oracle_version, to_oracle_version}; @@ -84,7 +84,7 @@ fn assert_emits(a: &Version, b: &Version) { /// Independent of the sweep under test on both faces — the meet comes from the /// recursive oracle, the reading from the order-theoretic definition — so the /// fused verdict differential shares nothing with the fold it checks. -fn oracle_relation(met: &BitsMut, x: &BitsMut, y: &BitsMut) -> Option { +fn oracle_relation(met: &BitsBuf, x: &BitsBuf, y: &BitsBuf) -> Option { match (met == x, met == y) { (true, true) => Some(core::cmp::Ordering::Equal), (true, false) => Some(core::cmp::Ordering::Less), @@ -269,7 +269,7 @@ fn flat_over_deep_collapses_totally() { /// deterministically rather than by sampling. #[test] fn exhaustive_small_scope_emits_identically() { - let pool: Vec<(crate::oracle::Version, Version, BitsMut)> = all_normal_events(EV_SMALL_DEPTH) + let pool: Vec<(crate::oracle::Version, Version, BitsBuf)> = all_normal_events(EV_SMALL_DEPTH) .iter() .map(|t| { let v = from_oracle_version(t); @@ -311,7 +311,7 @@ fn exhaustive_small_scope_emits_identically() { /// equality of canonical streams. #[test] fn family_lattice_laws_hold_on_the_kernel() { - let pool: Vec = family_pool().iter().map(encode).collect(); + let pool: Vec = family_pool().iter().map(encode).collect(); for ea in &pool { assert_eq!( join(crate::codec::built_view(ea), crate::codec::built_view(ea)), @@ -353,7 +353,7 @@ fn family_lattice_laws_hold_on_the_kernel() { /// Associativity holds on the emitted streams over every family triple. #[test] fn family_associativity_holds_on_the_kernel() { - let pool: Vec = family_pool().iter().map(encode).collect(); + let pool: Vec = family_pool().iter().map(encode).collect(); pool.par_iter().for_each(|ea| { for eb in &pool { for ec in &pool { @@ -441,7 +441,7 @@ proptest! { for op in &ops { optrace::step_impl(&mut clocks, op); } - let pool: Vec<(crate::oracle::Version, &Version, BitsMut)> = clocks + let pool: Vec<(crate::oracle::Version, &Version, BitsBuf)> = clocks .iter() .map(|c| (to_oracle_version(c.version()), c.version(), encode(c.version()))) .collect(); diff --git a/crates/before/src/version/skyline/encode.rs b/crates/before/src/version/skyline/encode.rs index cbbf9165..010898a6 100644 --- a/crates/before/src/version/skyline/encode.rs +++ b/crates/before/src/version/skyline/encode.rs @@ -2,7 +2,7 @@ //! generators' construction language (min-lifted preorder packed streams, one //! gamma-coded base per node) to the stored skyline coding. -use crate::codec::{self, Base, BitsMut, BitsView}; +use crate::codec::{self, Base, BitsBuf, BitsView}; use super::signed::zigzag; @@ -20,8 +20,8 @@ use super::signed::zigzag; /// /// Panics if the packed form does not parse cleanly; callers hand in /// generator-built canonical streams. -pub(crate) fn encode_bits(bits: BitsView<'_>) -> BitsMut { - let mut out = BitsMut::with_capacity(bits.len() as usize); +pub(crate) fn encode_bits(bits: BitsView<'_>) -> BitsBuf { + let mut out = BitsBuf::with_capacity(bits.len()); let mut pos = 0u64; // Inherited root-to-node path sums for the nodes not yet visited, top of // stack belonging to the next node in the preorder stream. Both children of diff --git a/crates/before/src/version/skyline/fill.rs b/crates/before/src/version/skyline/fill.rs index 55a2816c..05a9cc6f 100644 --- a/crates/before/src/version/skyline/fill.rs +++ b/crates/before/src/version/skyline/fill.rs @@ -153,7 +153,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; -use crate::codec::{self, Base, BitCursor, BitStack, BitsMut, BitsView, Int, PopStack}; +use crate::codec::{self, Base, BitCursor, BitStack, BitsBuf, BitsView, Int, PopStack}; use crate::idbits::{IdNode, IdReader}; use self::fuse::{decode_cost_component, encode_cost_component, Out, RouteProbe}; @@ -206,7 +206,7 @@ const _: () = assert!( /// at least one region: an empty id leaves `fill` the identity, and the grow /// fallback requires an owning id (debug builds assert it; the result on an /// empty id is unspecified in release builds). -pub fn tick(event: BitsView<'_>, id: &crate::Party) -> BitsMut { +pub fn tick(event: BitsView<'_>, id: &crate::Party) -> BitsBuf { // `n = 1` performs exactly one fused walk plus at most one splice: // the delta against a direct dispatch is two unmetered width tests // and one non-allocating `Base` construction. The committed @@ -244,11 +244,11 @@ pub fn tick(event: BitsView<'_>, id: &crate::Party) -> BitsMut { /// [`validate`](fn@super::validate) first on untrusted bytes. For `n >= 1` the /// id must own at least one region, exactly as [`tick`] (debug builds assert /// it; the result on an empty id is unspecified in release builds). -pub fn ticks(event: BitsView<'_>, id: &crate::Party, n: &Base) -> BitsMut { +pub fn ticks(event: BitsView<'_>, id: &crate::Party, n: &Base) -> BitsBuf { // Width tests, not value compares: n = 0 has no bits, n = 1 is the // one-bit magnitude, and neither test touches the limb meter. if n.bits() == 0 { - let mut out = BitsMut::with_capacity(event.len() as usize); + let mut out = BitsBuf::with_capacity(event.len()); codec::extend_from_view(&mut out, event, 0, event.len()); return out; } @@ -276,7 +276,7 @@ pub fn ticks(event: BitsView<'_>, id: &crate::Party, n: &Base) -> BitsMut { /// tick), or it was the identity and the recorded route drives the grow splice. pub(super) enum FillOutcome { /// `fill(id, e) ≠ e`: the canonical filled stream. - Changed(BitsMut), + Changed(BitsBuf), /// `fill(id, e) = e`: the inflation route for /// [`grow::emit`](super::grow::emit). Unchanged(super::grow::Route), @@ -304,7 +304,7 @@ pub(super) fn fused_fill(event_bits: BitsView<'_>, id: &crate::Party) -> FillOut memo: Memo::new(), relation: Relation::None, out: Out::Unstarted, - probe: RouteProbe::new(id_bits.len() as usize), + probe: RouteProbe::new(id_bits.len()), }; let mut reader = IdReader::root(id_bits); walk.web.open(1); diff --git a/crates/before/src/version/skyline/fill/fuse.rs b/crates/before/src/version/skyline/fill/fuse.rs index d26acbcc..188bdae4 100644 --- a/crates/before/src/version/skyline/fill/fuse.rs +++ b/crates/before/src/version/skyline/fill/fuse.rs @@ -44,7 +44,7 @@ //! that shifts which leaf is first trips on topology (the replaced range was //! not a single leaf) before any code comparison is reached. -use crate::codec::{BitStack, BitsMut, BitsView, Code, PopStack}; +use crate::codec::{BitStack, BitsBuf, BitsView, Code, PopStack}; use crate::idbits::{IdNode, IdReader}; use super::super::build::SkylineBuilder; @@ -214,7 +214,7 @@ impl Out { Out::Verbatim { matched_end } => *matched_end, Out::Built(_) => return, }; - let mut builder = SkylineBuilder::with_capacity(event.len() as usize); + let mut builder = SkylineBuilder::with_capacity(event.len()); let mut cursor = crate::codec::DsiCursor::new(event); let mut walk = LeafWalk::new(); while cursor.position_u64() < matched_end { @@ -242,7 +242,7 @@ impl Out { /// Finish the walk's output: the built stream when a plateau diverged, or /// `None` for an unchanged walk (every plateau matched; `fill(i, e) = e`, /// byte-exact by canonical uniqueness). - pub(super) fn finish(self, event: BitsView<'_>) -> Option { + pub(super) fn finish(self, event: BitsView<'_>) -> Option { let matched_end = match self { Out::Built(builder) => return Some(builder.finish()), Out::Unstarted => 0, @@ -266,7 +266,7 @@ pub(super) struct RouteProbe { /// The id stream's bit length (the route's key space), held so /// [`take_route`](Self::take_route)'s never-recorded fallback can still /// build a well-formed (empty, never-read) route. - id_span: usize, + id_span: u64, /// False once the walk diverges. /// /// Dead, every fold degenerates to the plain skip and no direction is @@ -277,7 +277,7 @@ pub(super) struct RouteProbe { } impl RouteProbe { - pub(super) fn new(id_span: usize) -> Self { + pub(super) fn new(id_span: u64) -> Self { RouteProbe { route: None, id_span, diff --git a/crates/before/src/version/skyline/fill/tests.rs b/crates/before/src/version/skyline/fill/tests.rs index 88e45ccb..feccc279 100644 --- a/crates/before/src/version/skyline/fill/tests.rs +++ b/crates/before/src/version/skyline/fill/tests.rs @@ -407,7 +407,7 @@ fn materialize_is_a_noop_once_built() { .parse() .expect("test version literals parse"); let event = encode(&v); - let matched_end = event.len() as u64; + let matched_end = event.len(); let mut once = Out::Verbatim { matched_end }; once.materialize(crate::codec::built_view(&event)); @@ -1277,7 +1277,7 @@ proptest! { if !p.as_bits().is_empty() { let ev = encode(&v); let out = tick(crate::codec::built_view(&ev), &p); - let bound = 2 * ev.len() + 4 * p.as_bits().len() as usize + 32; + let bound = 2 * ev.len() + 4 * p.as_bits().len() + 32; prop_assert!( out.len() <= bound, "tick output {} bits exceeds input envelope {} (event {}, id {})", @@ -1316,12 +1316,12 @@ proptest! { for k in 2u32..=48 { e = tick(crate::codec::built_view(&e), &p); let logk = u64::from(32 - (k + 1).leading_zeros()); - let bound = b1 as u64 + let bound = b1 + 4 * p.as_bits().len() + 4 * logk + 8; prop_assert!( - e.len() as u64 <= bound, + e.len() <= bound, "orbit size {} bits at tick {k} exceeds the transient-plus-log \ envelope {bound} (first-tick size {b1}, id {} bits)", e.len(), p.as_bits().len(), @@ -1351,7 +1351,7 @@ fn tick_deep_orbits_stay_banded() { e = tick(crate::codec::built_view(&e), &ida); let logk = usize::try_from(32 - (k + 1).leading_zeros()).expect("small"); assert!( - e.len() <= b1 + 4 * logk + 8, + e.len() <= b1 + 4 * logk as u64 + 8, "fixed-id orbit: {} bits at tick {k} (first-tick size {b1})", e.len(), ); @@ -1368,7 +1368,7 @@ fn tick_deep_orbits_stay_banded() { ); let logk = usize::try_from(32 - (k + 1).leading_zeros()).expect("small"); assert!( - e.len() <= b2 + 4 * logk + 8, + e.len() <= b2 + 4 * logk as u64 + 8, "alternating orbit: {} bits at tick {k} (two-tick size {b2})", e.len(), ); @@ -1419,7 +1419,7 @@ fn direction_chain(path: &[bool]) -> Party { fn assert_chain_saturation(path: &[bool], ceiling: u64) { let p = direction_chain(path); let bits = p.as_bits(); - let mut probe = RouteProbe::new(bits.len() as usize); + let mut probe = RouteProbe::new(bits.len()); let mut id = IdReader::root(bits); let cost = probe.expand_subtree(&mut id, ceiling); assert_eq!(id.pos(), bits.len(), "the DP consumes exactly the subtree"); @@ -1440,7 +1440,7 @@ fn assert_chain_saturation(path: &[bool], ceiling: u64) { let route = probe.take_route(); for (level, &left) in path.iter().enumerate() { assert_eq!( - route.dirs()[2 * level], + route.dirs().get(2 * level as u64), left, "the route at level {level} must turn into the present child", ); @@ -1749,22 +1749,22 @@ fn ticks_composes_at_wide_n() { /// `PreScan::max_range`'s entry assertion). mod prescan_raise_shapes { use super::*; - use crate::codec::{self, BitsMut}; + use crate::codec::{self, BitsBuf}; /// Construction-language pushers (the meter shape builders' vocabulary): /// internal node = `1 · gamma(0)`, leaf = `0 · gamma(n)`. - fn nd(ev: &mut BitsMut) { + fn nd(ev: &mut BitsBuf) { ev.push(true); codec::encode_int(ev, &Base::ZERO); } - fn lf(ev: &mut BitsMut, n: u64) { + fn lf(ev: &mut BitsBuf, n: u64) { ev.push(false); codec::encode_int(ev, &Base::from(n)); } /// A deep staircase region (the meter `hole_region`'s lead-2 shape): /// wrapper node, staircase `m..0`, wrapper floor leaf — deep enough to /// route every consuming scan and copy through its block summary. - fn hole(ev: &mut BitsMut, m: u64) { + fn hole(ev: &mut BitsBuf, m: u64) { nd(ev); // wrapper nd(ev); // staircase root lf(ev, m); @@ -1776,13 +1776,13 @@ mod prescan_raise_shapes { lf(ev, 0); // wrapper floor } /// Seal a built construction-language stream (the meter `Packed` form). - fn pk(bits: BitsMut) -> Packed { + fn pk(bits: BitsBuf) -> Packed { let live = bits.len(); let mut sealed = bits; codec::seal_padding(&mut sealed); Packed { - bytes: sealed.into_vec(), - bits: live, + bytes: sealed.into_bytes(), + bits: usize::try_from(live).expect("test streams are small"), } } @@ -1792,12 +1792,12 @@ mod prescan_raise_shapes { /// Wrap an entry range and its id in the covering left-full site that /// launches the fresh pre-scan: a root site over a fully-owned collapse /// leaf, the entry range as its sibling. - fn covered(er: &BitsMut, ir: &[bool]) -> (Version, Party) { - let mut ev = BitsMut::new(); + fn covered(er: &BitsBuf, ir: &[bool]) -> (Version, Party) { + let mut ev = BitsBuf::new(); nd(&mut ev); // the covering site's node lf(&mut ev, 2); // its fully-owned collapse leaf - ev.extend_from_bitslice(er); - let mut id = BitsMut::new(); + ev.extend_from_buf(er); + let mut id = BitsBuf::new(); for b in [T, T, F, F] { id.push(b); // the covering site: internal, full left child } @@ -1814,7 +1814,7 @@ mod prescan_raise_shapes { /// leaf. `k = 0` is the minimum-latency raise: the entry range's root /// raises immediately after its left leaf's single emission. fn chain_raise(k: usize, deep: bool) -> (Version, Party) { - let mut er = BitsMut::new(); + let mut er = BitsBuf::new(); nd(&mut er); // the raising node for _ in 0..k { nd(&mut er); // a site along the chain ... @@ -1863,7 +1863,7 @@ mod prescan_raise_shapes { #[test] fn suspended_and_skipping_site_raises_match_oracle() { // The raise under suspension: site over (collapse leaf, raising node). - let mut er = BitsMut::new(); + let mut er = BitsBuf::new(); nd(&mut er); // the site lf(&mut er, 1); // its collapse (skipped, unemitted) nd(&mut er); // the raising node @@ -1881,7 +1881,7 @@ mod prescan_raise_shapes { assert_tick(&v, &p); // The skip-then-forced-copy dual: raising node over (no-sibling site // with a deep collapse, raise range). - let mut er = BitsMut::new(); + let mut er = BitsBuf::new(); nd(&mut er); // the raising node nd(&mut er); // the no-sibling site hole(&mut er, 4); // its deep collapse (skipped net-only, unemitted) @@ -1906,13 +1906,13 @@ mod prescan_raise_shapes { /// full child's sibling or a child its caller peeked as not-full. #[test] fn full_sibling_of_full_child_is_undecodable() { - let mut id = BitsMut::new(); + let mut id = BitsBuf::new(); for b in [T, T, F, F, F, F] { id.push(b); } codec::seal_padding(&mut id); assert!( - Party::decode(&id.into_vec()[..]).is_err(), + Party::decode(&id.into_bytes()[..]).is_err(), "id (1, 1) must be rejected as non-normal" ); } diff --git a/crates/before/src/version/skyline/grow.rs b/crates/before/src/version/skyline/grow.rs index fb45d8e4..f9898549 100644 --- a/crates/before/src/version/skyline/grow.rs +++ b/crates/before/src/version/skyline/grow.rs @@ -90,7 +90,7 @@ use core::ops::Range; -use crate::codec::{self, Base, BitCursor, BitsMut, BitsView, Code}; +use crate::codec::{self, Base, BitCursor, BitsBuf, BitsView, Code}; use super::build::SkylineBuilder; use super::signed::{gamma_code, gamma_code_signed, unzigzag_base, zigzag_signed, Sign}; @@ -183,35 +183,35 @@ static_assertions::const_assert!(Cost::CEILING < Cost::INFEASIBLE); /// panic, which is why the route differential pins the walk's route against a /// reference recursive probe bit for bit. pub(super) struct Route { - dirs: BitsMut, + dirs: BitsBuf, } impl Route { /// All directions cleared, sized to the id's bit positions. - pub(super) fn new(id_span: usize) -> Self { + pub(super) fn new(id_span: u64) -> Self { Route { - dirs: BitsMut::repeat(false, id_span), + dirs: BitsBuf::repeat(false, id_span), } } /// Record that the cheapest inflation at the branch keyed by `key` /// descends into the left child (`left = true`). /// - /// Keys are id bit positions, bounded by the direction table's own - /// length, far below `usize` on every target. + /// Keys are id bit positions, bounded by the direction table's + /// own length, which its allocation keeps addressable. pub(super) fn record(&mut self, key: u64, left: bool) { - self.dirs.set(key as usize, left); + self.dirs.set(key, left); } /// Whether the cheapest inflation at the branch keyed by `key` /// descends into the left child. fn descends_left(&self, key: u64) -> bool { - self.dirs[key as usize] + self.dirs.get(key) } /// The raw direction bits, for the route differential. #[cfg(test)] - pub(super) fn dirs(&self) -> &BitsMut { + pub(super) fn dirs(&self) -> &BitsBuf { &self.dirs } } @@ -500,7 +500,7 @@ pub(super) fn emit( id_bits: BitsView<'_>, route: &Route, events: &Base, -) -> BitsMut { +) -> BitsBuf { debug_assert!( !id_bits.is_empty(), "grow requires an id owning at least one region" @@ -515,10 +515,10 @@ pub(super) fn emit( let mut id_pos = 0u64; // Subadditivity of the coding bounds the output by the input plus the // expansion chain's fresh codes, each a few bits per id level. - let mut out = SkylineBuilder::with_capacity((event_bits.len() + id_bits.len()) as usize + 64); + let mut out = SkylineBuilder::with_capacity(event_bits.len() + id_bits.len() + 64); // One bit per chosen-path level: `true` = the branch descended left, so its // right sibling subtree is pending after the inflation point. - let mut pending = BitsMut::new(); + let mut pending = BitsBuf::new(); let mut depth = 0usize; // Whether any leaf has entered the output ahead of the grown leaf. The // grown leaf's own code is absolute exactly when none has (Phase 2's @@ -546,7 +546,7 @@ pub(super) fn emit( // collapsed the region and tripped the changed flag), and // incrementing that leaf in place is the inflation. match event.read() { - Some(code) => break (code, BitsMut::new()), + Some(code) => break (code, BitsBuf::new()), None => unreachable!("a full id over an event node collapses under fill"), } } @@ -576,7 +576,7 @@ pub(super) fn emit( // An id node over an event leaf — the chain below is id-only, its // directions collected for the fresh leaves' preorder. Some(code) => { - let mut directions = BitsMut::new(); + let mut directions = BitsBuf::new(); let mut current = (key, left_present, right_present); loop { let (key, left_present, right_present) = current; @@ -617,10 +617,14 @@ pub(super) fn emit( // code (same height, same predecessor) or re-codes it `+k` when the grown // leaf itself comes first. let path_depth = depth; - let chain = chain_dirs.len(); + // Depths are `usize` across the walk surface: a chain level costs at + // least one bit of the stored id, whose live length fits `usize` on + // every target (the storage doors' bound). + let chain = + usize::try_from(chain_dirs.len()).expect("chain depth is bounded by the id's live length"); let original = original_range; debug_assert_eq!( - path_depth, + path_depth as u64, pending.len(), "one pending record per path level" ); @@ -628,7 +632,7 @@ pub(super) fn emit( // Fresh sibling leaves that precede the grown leaf: one per level whose // branch descended right (the sibling is the left child). for level in 0..chain { - if !chain_dirs[level] { + if !chain_dirs.get(level as u64) { let code = if emitted_in_chain { gamma_code(&Base::ZERO) } else { @@ -658,7 +662,7 @@ pub(super) fn emit( // Fresh sibling leaves that follow the grown leaf, deepest first. let mut first_after_grown = true; for level in (0..chain).rev() { - if chain_dirs[level] { + if chain_dirs.get(level as u64) { let code = if first_after_grown { gamma_code_signed(Sign::Negative, events) } else { diff --git a/crates/before/src/version/skyline/grow/tests.rs b/crates/before/src/version/skyline/grow/tests.rs index 703e6003..7a7eef60 100644 --- a/crates/before/src/version/skyline/grow/tests.rs +++ b/crates/before/src/version/skyline/grow/tests.rs @@ -21,7 +21,7 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use proptest::prelude::*; use rayon::prelude::*; -use crate::codec::BitsMut; +use crate::codec::BitsBuf; use crate::codec::BitsView; use crate::meter::registry::Shape; use crate::meter::Packed; @@ -79,7 +79,7 @@ fn assert_grow(v: &Version, p: &Party) -> bool { /// /// The recursive oracle walks on native frames, so the deep-spine test calls /// this directly and takes its value witnesses from closed forms instead. -fn assert_grow_depth_safe(v: &Version, p: &Party) -> Option { +fn assert_grow_depth_safe(v: &Version, p: &Party) -> Option { let enc = encode(v); match fused_fill(crate::codec::built_view(&enc), p) { // fill moved the tree: the splice is unreachable for this pair. @@ -114,7 +114,7 @@ enum RefId { /// the transliteration of the recursive walk whose route fold the fused tick /// walk carries, kept as its structural witness. fn reference_probe(ev_bits: BitsView<'_>, id_bits: BitsView<'_>) -> (Route, Cost) { - let mut route = Route::new(id_bits.len() as usize); + let mut route = Route::new(id_bits.len()); let mut ev = EvScan::new(ev_bits); let mut id_pos = 0u64; let root = if id_bits.is_empty() { diff --git a/crates/before/src/version/skyline/literal.rs b/crates/before/src/version/skyline/literal.rs index 27432621..1002d5b9 100644 --- a/crates/before/src/version/skyline/literal.rs +++ b/crates/before/src/version/skyline/literal.rs @@ -9,14 +9,14 @@ //! its inner node hoards a liftable minimum, so it is not the normal spelling //! and is refused. -use crate::codec::{self, Base, BitCursor, BitsMut, BitsView, DsiCursor}; +use crate::codec::{self, Base, BitCursor, BitsBuf, BitsView, DsiCursor}; use crate::error::Parse; use super::signed::{unzigzag_base, zigzag}; /// The skyline stream of an event leaf with base `base`. -pub(crate) fn leaf(base: u64) -> BitsMut { - let mut bits = BitsMut::new(); +pub(crate) fn leaf(base: u64) -> BitsBuf { + let mut bits = BitsBuf::new(); bits.push(true); // topology: a leaf codec::encode_int(&mut bits, &Base::from(base)); // absolute height bits @@ -29,7 +29,7 @@ pub(crate) fn leaf(base: u64) -> BitsMut { /// children of equal height, which is just the leaf itself) or when the node /// hoards a liftable minimum (neither child's minimum leaf height is zero — /// normal form stores the shared minimum at the parent). -pub(crate) fn node(base: u64, left: BitsView<'_>, right: BitsView<'_>) -> Result { +pub(crate) fn node(base: u64, left: BitsView<'_>, right: BitsView<'_>) -> Result { let (left_topology, left_heights) = scan(left); let (right_topology, right_heights) = scan(right); @@ -50,13 +50,14 @@ pub(crate) fn node(base: u64, left: BitsView<'_>, right: BitsView<'_>) -> Result } let base = Base::from(base); - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); bits.push(false); // topology: this node - bits.extend_from_bitslice(&left_topology); // then the left subtree's topology… - bits.extend_from_bitslice(&right_topology); // …then the right's — but the - // payloads interleave, so re-emit. - let mut out = BitsMut::new(); - let mut flags = bits.iter().by_vals(); + // …then the left subtree's topology, then the right's — but the + // payloads interleave, so re-emit. + bits.extend_from_buf(&left_topology); + bits.extend_from_buf(&right_topology); + let mut out = BitsBuf::new(); + let mut flags = bits.iter(); let mut heights = left_heights.iter().chain(right_heights.iter()); let mut prev: Option = None; for flag in &mut flags { @@ -78,9 +79,9 @@ pub(crate) fn node(base: u64, left: BitsView<'_>, right: BitsView<'_>) -> Result /// Split a canonical stream into its topology flags (wire convention: `0` /// internal, `1` leaf) and absolute leaf heights. -fn scan(bits: BitsView<'_>) -> (BitsMut, Vec) { +fn scan(bits: BitsView<'_>) -> (BitsBuf, Vec) { let mut cursor = DsiCursor::new(bits); - let mut topology = BitsMut::new(); + let mut topology = BitsBuf::new(); let mut heights: Vec = Vec::new(); let mut pending = 1usize; while pending > 0 { diff --git a/crates/before/src/version/skyline/masked/tests.rs b/crates/before/src/version/skyline/masked/tests.rs index 34ec808b..a60b8e6d 100644 --- a/crates/before/src/version/skyline/masked/tests.rs +++ b/crates/before/src/version/skyline/masked/tests.rs @@ -6,7 +6,7 @@ //! lives here is the Panics contract's negative space — the silent sweep over //! the canonicality violations the walk does not structurally notice. -use crate::codec::{self, Base, BitsMut}; +use crate::codec::{self, Base, BitsBuf}; use crate::error::Decode; use crate::version::skyline::validate_bits; @@ -25,7 +25,7 @@ use super::{causal_cmp, eq}; fn collapsible_sibling_pair_sweeps_without_panicking() { // (5, 5): internal root, first leaf absolute gamma(5), then the zero // right-sibling delta — the collapsible pair. - let mut bad = BitsMut::new(); + let mut bad = BitsBuf::new(); bad.push(false); // root: internal bad.push(true); // left leaf codec::encode_int(&mut bad, &Base::from(5u64)); @@ -39,7 +39,7 @@ fn collapsible_sibling_pair_sweeps_without_panicking() { "the witness must sit outside the contract's canonical-operand precondition" ); // The canonical spelling of the same step function: the single leaf 5. - let mut good = BitsMut::new(); + let mut good = BitsBuf::new(); good.push(true); codec::encode_int(&mut good, &Base::from(5u64)); validate_bits(crate::codec::built_view(&good)).expect("the peer operand is canonical"); diff --git a/crates/before/src/version/skyline/query.rs b/crates/before/src/version/skyline/query.rs index 3f964ac7..86b7f993 100644 --- a/crates/before/src/version/skyline/query.rs +++ b/crates/before/src/version/skyline/query.rs @@ -174,7 +174,7 @@ use core::cmp::Ordering; use suanpan::{Accumulator, UBig}; -use crate::codec::{self, Base, BitsMut, BitsView, Int}; +use crate::codec::{self, Base, BitsBuf, BitsView, Int}; use crate::Rank; use self::integral::{int_digits, Integrator, FREEZE_ALLOWANCE_DIGITS}; @@ -496,7 +496,7 @@ pub fn min_ticks(bits: BitsView<'_>) -> Base { /// # Panics /// /// Panics if the skyline operand is not a canonical stream. -pub fn project(event_bits: BitsView<'_>, id: &crate::Party) -> BitsMut { +pub fn project(event_bits: BitsView<'_>, id: &crate::Party) -> BitsBuf { let id_bits = id.as_bits(); let (mut event_cursor, first) = LeafCursor::open(event_bits); let mut id_cursor = IdLeafCursor::open(id_bits); @@ -511,7 +511,7 @@ pub fn project(event_bits: BitsView<'_>, id: &crate::Party) -> BitsMut { // the allocation benchmark to measure against the shipped pre-size; shipped // builds always take the pre-sized arm. #[cfg(not(before_alloc_ab = "projection_growth"))] - let capacity = (event_bits.len() + id_bits.len()) as usize; + let capacity = event_bits.len() + id_bits.len(); #[cfg(before_alloc_ab = "projection_growth")] let capacity = 0; let mut out = SkylineBuilder::with_capacity(capacity); diff --git a/crates/before/src/version/skyline/query/tests.rs b/crates/before/src/version/skyline/query/tests.rs index 93b65bfb..35e36f43 100644 --- a/crates/before/src/version/skyline/query/tests.rs +++ b/crates/before/src/version/skyline/query/tests.rs @@ -530,7 +530,7 @@ fn exhaustive_small_scope_agrees() { /// quadratic pair product stays fast. #[test] fn exhaustive_small_scope_pairs_agree() { - let events: Vec = all_normal_events(EV_SMALL_DEPTH) + let events: Vec = all_normal_events(EV_SMALL_DEPTH) .iter() .map(|t| encode(&from_oracle_version(t))) .collect(); @@ -1478,7 +1478,7 @@ mod adequacy { "the known-bad fold must stay value-exact: a wrong demonstrator \ proves nothing about the family's coverage" ); - (enc.len().div_ceil(8) as u64, touches) + (enc.len().div_ceil(8), touches) } /// `FP(k)` catches the absolute-position accounting red: its per-byte touch @@ -1768,7 +1768,7 @@ mod adequacy { "the known-bad fold must stay value-exact: a wrong demonstrator \ proves nothing about the family's coverage" ); - (enc.len().div_ceil(8) as u64, touches) + (enc.len().div_ceil(8), touches) } /// One pair tripwire run over `(PR(p), PRM(p))`: the pair's packed bytes @@ -1789,7 +1789,7 @@ mod adequacy { "the known-bad co-sweep must stay value-exact: a wrong \ demonstrator proves nothing about the family's coverage" ); - ((ea.len() + eb.len()).div_ceil(8) as u64, touches) + ((ea.len() + eb.len()).div_ceil(8), touches) } /// `PR(p)` catches the span-reading promotion red on the single-stream @@ -2132,7 +2132,7 @@ mod adequacy { "the known-bad fold must stay value-exact: a wrong demonstrator \ proves nothing about the family's coverage" ); - (enc.len().div_ceil(8) as u64, touches) + (enc.len().div_ceil(8), touches) } /// One pair tripwire run over `(DS(p, p), DSM(p, p))`: the pair's packed @@ -2152,7 +2152,7 @@ mod adequacy { "the known-bad co-sweep must stay value-exact: a wrong \ demonstrator proves nothing about the family's coverage" ); - ((ea.len() + eb.len()).div_ceil(8) as u64, touches) + ((ea.len() + eb.len()).div_ceil(8), touches) } /// `DS(p, p)` catches the per-arming suffix walk red on the single-stream @@ -2390,7 +2390,7 @@ mod adequacy { "the known-bad fold must stay value-exact: a wrong demonstrator \ proves nothing about the family's coverage" ); - (enc.len().div_ceil(8) as u64, limbs) + (enc.len().div_ceil(8), limbs) } /// `DS(p, p)` catches the per-digit window absorb red through the combine @@ -2621,7 +2621,7 @@ mod adequacy { "the known-bad fold must stay value-exact: a wrong demonstrator \ proves nothing about the family's coverage" ); - (enc.len().div_ceil(8) as u64, touches, limbs) + (enc.len().div_ceil(8), touches, limbs) } /// `WA(w, w)` catches the schoolbook charge red in both width currencies: diff --git a/crates/before/src/version/skyline/signed/tests.rs b/crates/before/src/version/skyline/signed/tests.rs index 427d36d3..bfa3208e 100644 --- a/crates/before/src/version/skyline/signed/tests.rs +++ b/crates/before/src/version/skyline/signed/tests.rs @@ -16,11 +16,10 @@ //! word-scale value may travel as [`Int::Small`] or parked in [`Int::Wide`], //! and the order must not see the difference. -use bitvec::field::BitField; use dashu_int::IBig; use proptest::prelude::*; -use crate::codec::{self, Base, BitsMut, Code, Int}; +use crate::codec::{self, Base, BitsBuf, Code, Int}; use super::{ gamma_code_signed, gamma_code_signed_int, signed_le, signed_max, zigzag_signed, Sign, Signed, @@ -29,12 +28,11 @@ use super::{ /// Render a payload code into live bits by [`Code`]'s own representation /// contract (a small code sits value-packed at the register's low end, first /// bit most significant) — independent of both coders under comparison. -fn bits_of(code: &Code) -> BitsMut { +fn bits_of(code: &Code) -> BitsBuf { match code { Code::Small { bits, len } => { - let mut out = BitsMut::new(); - out.resize(usize::from(*len), false); - out[..].store_be::(*bits); + let mut out = BitsBuf::new(); + out.push_bits(*bits, u32::from(*len)); out } Code::Wide(bits) => bits.clone(), @@ -44,7 +42,7 @@ fn bits_of(code: &Code) -> BitsMut { /// Assert both fused coders agree with the unfused composition on one signed /// delta, bit for bit. fn assert_fused_matches(sign: Sign, magnitude: &Base) { - let mut reference = BitsMut::new(); + let mut reference = BitsBuf::new(); codec::encode_int(&mut reference, &zigzag_signed(sign, magnitude.clone())); assert_eq!( bits_of(&gamma_code_signed(sign, magnitude)), diff --git a/crates/before/src/version/skyline/sweep/tests.rs b/crates/before/src/version/skyline/sweep/tests.rs index b15d2da1..ba8945e9 100644 --- a/crates/before/src/version/skyline/sweep/tests.rs +++ b/crates/before/src/version/skyline/sweep/tests.rs @@ -14,7 +14,7 @@ use core::cmp::Ordering; use proptest::prelude::*; use rayon::prelude::*; -use crate::codec::BitsMut; +use crate::codec::BitsBuf; use crate::meter::registry::Shape; use crate::meter::Packed; use crate::testing::bridge::{from_oracle_version, to_oracle_version}; @@ -191,7 +191,7 @@ fn family_pairs_agree() { /// consumption, zero deltas across subtree boundaries. #[test] fn exhaustive_small_scope_agrees() { - let pool: Vec<(oracle::Version, Version, BitsMut)> = all_normal_events(EV_SMALL_DEPTH) + let pool: Vec<(oracle::Version, Version, BitsBuf)> = all_normal_events(EV_SMALL_DEPTH) .iter() .map(|t| { let v = from_oracle_version(t); @@ -253,7 +253,7 @@ proptest! { for op in &ops { optrace::step_impl(&mut clocks, op); } - let pool: Vec<(oracle::Version, &Version, BitsMut)> = clocks + let pool: Vec<(oracle::Version, &Version, BitsBuf)> = clocks .iter() .map(|c| (to_oracle_version(c.version()), c.version(), encode(c.version()))) .collect(); diff --git a/crates/before/src/version/skyline/tests.rs b/crates/before/src/version/skyline/tests.rs index ece93029..025d734f 100644 --- a/crates/before/src/version/skyline/tests.rs +++ b/crates/before/src/version/skyline/tests.rs @@ -19,7 +19,7 @@ use std::collections::BTreeSet; use proptest::prelude::*; -use crate::codec::{self, Base, BitsMut}; +use crate::codec::{self, Base, BitsBuf}; use crate::error::Decode; use crate::meter::registry::Shape; use crate::meter::tier2::tier2_size; @@ -39,8 +39,8 @@ fn version_of(p: &Packed) -> Version { } /// The stored skyline stream of a version, as live bits. -fn stream_of(v: &Version) -> BitsMut { - v.as_bits().to_bitvec() +fn stream_of(v: &Version) -> BitsBuf { + v.as_bits().to_buf() } // ─── hand-pinned streams ──────────────────────────────────────────────────── @@ -52,8 +52,8 @@ fn empty_version_is_the_two_bit_stream() { let v = Version::new(); let bits = stream_of(&v); assert_eq!(bits.len(), 2); - assert!(bits[0], "a leaf's topology flag is 1"); - assert!(bits[1], "gamma(0) is the single bit 1"); + assert!(bits.get(0), "a leaf's topology flag is 1"); + assert!(bits.get(1), "gamma(0) is the single bit 1"); assert_eq!( decode_bits(crate::codec::built_view(&bits)).expect("canonical"), v @@ -78,7 +78,7 @@ fn one_fork_matches_hand_derivation() { true, false, false, true, false, true, // right leaf: flag 1, gamma(4) ] .to_vec(); - assert_eq!(bits.iter().by_vals().collect::>(), expected); + assert_eq!(bits.iter().collect::>(), expected); assert_eq!( decode_bits(crate::codec::built_view(&bits)).expect("canonical"), v @@ -88,7 +88,7 @@ fn one_fork_matches_hand_derivation() { // ─── the strict-reject corpus ─────────────────────────────────────────────── /// Append a leaf carrying a raw payload value (the caller pre-zigzags). -fn push_leaf(bits: &mut BitsMut, payload: u64) { +fn push_leaf(bits: &mut BitsBuf, payload: u64) { bits.push(true); codec::encode_int(bits, &Base::from(payload)); } @@ -98,7 +98,7 @@ fn push_leaf(bits: &mut BitsMut, payload: u64) { #[test] fn rejects_zero_right_sibling_delta() { // (5, 5): internal root, leaf height 5, then delta 0. - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); bits.push(false); // root: internal push_leaf(&mut bits, 5); // gamma(5): the first leaf, absolute push_leaf(&mut bits, 0); // zigzag(0) = 0 -> gamma(0): equal sibling @@ -118,7 +118,7 @@ fn rejects_zero_right_sibling_delta() { /// causal-equality identity. #[test] fn rejects_non_root_collapsible_pair() { - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); bits.push(false); // root: internal push_leaf(&mut bits, 5); // gamma(5): the first leaf, absolute bits.push(false); // right child: internal @@ -133,21 +133,20 @@ fn rejects_non_root_collapsible_pair() { /// The `(flag, end)` bit positions of every leaf code in a stored stream: the /// leaf's topology flag and the position just past its payload code, in /// preorder. -fn leaf_code_ranges(bits: &BitsMut) -> Vec<(usize, usize)> { +fn leaf_code_ranges(bits: &BitsBuf) -> Vec<(u64, u64)> { let mut out = Vec::new(); - let mut pos = 0usize; + let mut pos = 0u64; let mut pending = 1usize; while pending > 0 { pending -= 1; - let leaf = bits[pos]; + let leaf = bits.get(pos); pos += 1; if !leaf { pending += 2; continue; } - let (_, next) = codec::decode_int(crate::codec::built_view(bits), pos as u64) + let (_, next) = codec::decode_int(crate::codec::built_view(bits), pos) .expect("a stored stream is canonical"); - let next = next as usize; out.push((pos - 1, next)); pos = next; } @@ -174,12 +173,13 @@ proptest! { let bits = stream_of(&from_oracle_version(&t)); let leaves = leaf_code_ranges(&bits); let (flag, end) = leaves[leaf_seed.index(leaves.len())]; - let mut planted = BitsMut::with_capacity(bits.len() + 4); - planted.extend_from_bitslice(&bits[..flag]); + let mut planted = BitsBuf::with_capacity(bits.len() + 4); + let view = crate::codec::built_view(&bits); + crate::codec::extend_from_view(&mut planted, view, 0, flag); planted.push(false); // the chosen leaf's position becomes internal - planted.extend_from_bitslice(&bits[flag..end]); // left child: the old leaf + crate::codec::extend_from_view(&mut planted, view, flag, end); // left child: the old leaf push_leaf(&mut planted, 0); // right child: zigzag(0), the equal sibling - planted.extend_from_bitslice(&bits[end..]); + crate::codec::extend_from_view(&mut planted, view, end, bits.len()); prop_assert!(matches!(validate_bits(crate::codec::built_view(&planted)), Err(Decode::NotCanonical))); } } @@ -200,7 +200,7 @@ fn accepts_zero_delta_across_a_subtree_boundary() { ), oracle::Version::leaf(1u64), )); - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); bits.push(false); // root: internal bits.push(false); // left child: internal push_leaf(&mut bits, 0); // leaf 0: gamma(0), absolute @@ -220,7 +220,7 @@ fn accepts_zero_delta_across_a_subtree_boundary() { #[test] fn rejects_negative_running_height() { // (1, -1): internal root, leaf height 1, then delta -2. - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); bits.push(false); // root: internal push_leaf(&mut bits, 1); // first leaf: height 1 push_leaf(&mut bits, 3); // zigzag(-2) = 3: height would be -1 @@ -236,7 +236,7 @@ fn rejects_negative_running_height() { fn rejects_negative_height_midstream() { // Root over leaf(1) and (node over leaf(-1), leaf(5)): the middle leaf // dips negative before the last one recovers. - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); bits.push(false); // root: internal push_leaf(&mut bits, 1); // first leaf: height 1 bits.push(false); // right child: internal @@ -265,7 +265,7 @@ fn rejects_every_truncation() { for cut in 0..bits.len() { assert!( matches!( - validate_bits(crate::codec::BitsView::new(bits.as_raw_slice(), cut as u64)), + validate_bits(crate::codec::BitsView::new(bits.as_raw_slice(), cut)), Err(Decode::Truncated) ), "a {cut}-bit prefix of a {}-bit stream must read as truncated", @@ -290,7 +290,7 @@ fn rejects_trailing_bits() { )); } let mut two_trees = clean.clone(); - two_trees.extend_from_bitslice(&clean); + two_trees.extend_from_buf(&clean); assert!(matches!( validate_bits(crate::codec::built_view(&two_trees)), Err(Decode::TrailingBits) @@ -345,8 +345,8 @@ fn zigzag_is_a_bijection_without_negative_zero() { /// Emit the flag-inverted skyline spelling of a normal-form oracle tree: /// per-node preorder flag `1` internal / `0` leaf, payloads exactly the stored /// coding's (first leaf absolute, later leaves zigzag deltas). -fn inverted_flag_stream(t: &oracle::Version) -> BitsMut { - fn walk(t: &oracle::Version, offset: &Base, prev: &mut Option, out: &mut BitsMut) { +fn inverted_flag_stream(t: &oracle::Version) -> BitsBuf { + fn walk(t: &oracle::Version, offset: &Base, prev: &mut Option, out: &mut BitsBuf) { match t { oracle::Version::Leaf(n) => { out.push(false); // leaf flag, inverted spelling @@ -364,7 +364,7 @@ fn inverted_flag_stream(t: &oracle::Version) -> BitsMut { } } } - let mut out = BitsMut::new(); + let mut out = BitsBuf::new(); walk(t, &Base::ZERO, &mut None, &mut out); out } @@ -372,23 +372,22 @@ fn inverted_flag_stream(t: &oracle::Version) -> BitsMut { /// Transcode between the two flag spellings: walk the stream by its own grammar /// (`internal` says which flag value opens two children), invert exactly the /// one flag bit per node, and copy every payload code verbatim. -fn flip_topology_flags(bits: &BitsMut, internal: bool) -> BitsMut { - let mut out = BitsMut::with_capacity(bits.len()); - let mut pos = 0usize; +fn flip_topology_flags(bits: &BitsBuf, internal: bool) -> BitsBuf { + let mut out = BitsBuf::with_capacity(bits.len()); + let mut pos = 0u64; let mut pending = 1usize; while pending > 0 { pending -= 1; - let flag = bits[pos]; + let flag = bits.get(pos); out.push(!flag); pos += 1; if flag == internal { pending += 2; continue; } - let (_, next) = codec::decode_int(crate::codec::built_view(bits), pos as u64) + let (_, next) = codec::decode_int(crate::codec::built_view(bits), pos) .expect("a payload code per leaf"); - let next = next as usize; - out.extend_from_bitslice(&bits[pos..next]); + crate::codec::extend_from_view(&mut out, crate::codec::built_view(bits), pos, next); pos = next; } assert_eq!(pos, bits.len(), "the transcode consumes exactly one tree"); @@ -452,9 +451,9 @@ proptest! { /// the decoded version's own stream against the mutated bytes would hold under /// any validator behavior. Only an independently rebuilt encoding can convict /// a validator that accepted a non-canonical spelling. -fn assert_mutation_never_aliases(v: &Version, bits: &BitsMut, flip: usize) { +fn assert_mutation_never_aliases(v: &Version, bits: &BitsBuf, flip: u64) { let mut mutated = bits.clone(); - let old = mutated[flip]; + let old = mutated.get(flip); mutated.set(flip, !old); match decode_bits(crate::codec::built_view(&mutated)) { Err(_) => {} @@ -499,7 +498,7 @@ proptest! { ) { let v = from_oracle_version(&t); let bits = stream_of(&v); - let flip = flip_seed.index(bits.len()); + let flip = flip_seed.index(usize::try_from(bits.len()).expect("test streams are small")) as u64; assert_mutation_never_aliases(&v, &bits, flip); } } @@ -517,7 +516,7 @@ fn assert_agreement(v: &Version) { let packed = packed_bits_of(&to_oracle_version(v)); let size = tier2_size(crate::codec::built_view(&packed)); assert_eq!( - bits.len() as u64, + bits.len(), size.total_bits, "stored skyline length disagrees with the tier2 sizer: one of the \ two independent walks is wrong" diff --git a/crates/before/src/version/skyline/text.rs b/crates/before/src/version/skyline/text.rs index 2fbb7b67..0928ba83 100644 --- a/crates/before/src/version/skyline/text.rs +++ b/crates/before/src/version/skyline/text.rs @@ -69,7 +69,7 @@ use core::fmt::Write as _; use suanpan::Accumulator; use crate::codec::text::{parse_base, Cur}; -use crate::codec::{Base, BitCursor, BitsMut, BitsView, DsiCursor}; +use crate::codec::{Base, BitCursor, BitsBuf, BitsView, DsiCursor}; use crate::error::Parse; use super::build::SkylineBuilder; @@ -236,8 +236,8 @@ pub fn render(bits: BitsView<'_>) -> String { // left child), `open` its preorder index, and `lefts` the parked left-child // summary of each open node past its left phase — parallel stacks, where an // enum-of-frames layout would pad every open level to its widest variant. - let mut topology = BitsMut::new(); - let mut phase = BitsMut::new(); + let mut topology = BitsBuf::new(); + let mut phase = BitsBuf::new(); let mut open: Vec = Vec::new(); let mut lefts = ParkedStack::new(); // The digit arena: every printed nonzero base, rendered at the merge that @@ -253,10 +253,10 @@ pub fn render(bits: BitsView<'_>) -> String { let internal_nodes = cursor.read_unary().expect("canonical skyline bits"); for _ in 0..internal_nodes { phase.push(LEFT_PHASE); - open.push(topology.len()); + open.push(node_index(topology.len())); topology.push(true); } - let index = topology.len(); + let index = node_index(topology.len()); topology.push(false); // The cursor's own `read_int`: word-parallel payload decode. let code = cursor @@ -326,8 +326,8 @@ pub fn render(bits: BitsView<'_>) -> String { // terminators) plus one `0` per node without an entry, and each internal // node adds its fixed syntax bytes. let exact = (arena.len() - entries.len()) - + (topology.len() - entries.len()) - + topology.count_ones() * INTERNAL_SYNTAX_BYTES; + + (node_index(topology.len()) - entries.len()) + + node_index(topology.count_ones()) * INTERNAL_SYNTAX_BYTES; // The finalize-only stacks are drained; release them before the output // materializes rather than holding their capacity across the emit pass. @@ -352,9 +352,9 @@ pub fn render(bits: BitsView<'_>) -> String { let mut out = String::with_capacity(exact); #[cfg(before_alloc_ab = "display_growth")] let mut out = String::new(); - let mut pending = BitsMut::new(); + let mut pending = BitsBuf::new(); let mut next_entry = 0usize; - for (node, internal) in topology.iter().by_vals().enumerate() { + for (node, internal) in topology.iter().enumerate() { let digits: &str = match entries.get(next_entry) { Some(&(entry_node, start)) if entry_node == node => { next_entry += 1; @@ -401,6 +401,13 @@ pub fn render(bits: BitsView<'_>) -> String { /// entry's length. const ARENA_SEP: char = ';'; +/// A node count or preorder node index as the renderer's `usize` vocabulary: +/// each node costs at least one bit of the rendered stream, whose live length +/// fits `usize` on every target (the storage doors' bound). +fn node_index(count: u64) -> usize { + usize::try_from(count).expect("node indexes are bounded by the stream's live length") +} + /// Render one finalized printed base into the digit arena, keyed by its node's /// preorder index — unless it is zero. /// @@ -481,7 +488,7 @@ fn merge( /// sibling leaves) is checked at each close and reported after the whole syntax /// pass, so syntax errors — including trailing junk — outrank /// [`Parse::NotCanonical`]. -pub fn parse(text: &str) -> Result { +pub fn parse(text: &str) -> Result { /// What a parsed subtree contributes to its parent's normal-form check: its /// written base and whether it is a single leaf. struct Child { @@ -490,7 +497,7 @@ pub fn parse(text: &str) -> Result { } let mut cursor = Cur::new(text); - let mut builder = SkylineBuilder::with_capacity(text.len()); + let mut builder = SkylineBuilder::with_capacity(text.len() as u64); // The open-node stacks, innermost last — the render's parallel-stack // discipline: `phase` holds one bit per open node ([`LEFT_PHASE`] while it // awaits its left child), `bases` the node's own written base, and `lefts` @@ -499,7 +506,7 @@ pub fn parse(text: &str) -> Result { // and a flat `Vec` of frames holds an old and a new buffer at once while it // doubles — on a deep spine that padded coexistence alone is most of the // parse's transient. - let mut phase = BitsMut::new(); + let mut phase = BitsBuf::new(); let mut bases: ParkedStack = ParkedStack::new(); let mut lefts: ParkedStack = ParkedStack::new(); // The signed height movement since the last emitted leaf. @@ -559,7 +566,10 @@ pub fn parse(text: &str) -> Result { debug_assert_ne!(sign, Ordering::Less, "a path sum of naturals is a natural"); gamma_code(&Base::from(magnitude)) }; - builder.leaf(phase.len(), code); + builder.leaf( + usize::try_from(phase.len()).expect("depth is bounded by the parsed text's length"), + code, + ); delta.sub_magnitude(&base); // the leaf's base exits the path // Close every node the leaf completes. diff --git a/crates/before/src/version/skyline/text/tests.rs b/crates/before/src/version/skyline/text/tests.rs index ea8a07ee..23a80176 100644 --- a/crates/before/src/version/skyline/text/tests.rs +++ b/crates/before/src/version/skyline/text/tests.rs @@ -19,7 +19,7 @@ use proptest::prelude::*; use suanpan::Accumulator; use crate::codec::text::{parse_base, Cur}; -use crate::codec::{Base, BitsMut}; +use crate::codec::{Base, BitsBuf}; use crate::error::Parse; use crate::meter::registry::Shape; use crate::meter::Packed; @@ -311,7 +311,7 @@ fn schoolbook_twin_agrees_on_the_mutant_corpus() { /// after each leaf's extraction it re-zeroes the accumulator by subtracting /// the extracted magnitude back, which zeroes the *value* but not the digit /// buffer's top — the high-water walk the wide-arming family prices. -fn parse_schoolbook(s: &str) -> Result { +fn parse_schoolbook(s: &str) -> Result { /// What a parsed subtree contributes to its parent's /// normal-form check. struct Child { @@ -328,7 +328,7 @@ fn parse_schoolbook(s: &str) -> Result { } let mut cur = Cur::new(s); - let mut builder = SkylineBuilder::with_capacity(s.len()); + let mut builder = SkylineBuilder::with_capacity(s.len() as u64); let mut frames: Vec = Vec::new(); let mut delta = Accumulator::new(); let mut emitted_first = false; @@ -499,7 +499,7 @@ proptest! { mod schoolbook_contrast { use suanpan::touch_meter; - use crate::codec::BitsMut; + use crate::codec::BitsBuf; use crate::error::Parse; use crate::meter::registry::Shape; use crate::version::skyline::encode; @@ -509,7 +509,7 @@ mod schoolbook_contrast { /// One kernel run over `Shape::WideArming.packed2(s, s)`'s rendered text: text /// bytes and accumulator touches over the parse body alone, /// value-pinned against the stored stream. - fn run(s: usize, kernel: fn(&str) -> Result) -> (u64, u64) { + fn run(s: usize, kernel: fn(&str) -> Result) -> (u64, u64) { let v = Shape::WideArming.packed2(s, s).version(); let enc = encode(&v); let text = render(crate::codec::built_view(&enc)); diff --git a/crates/before/src/version/skyline/validate.rs b/crates/before/src/version/skyline/validate.rs index ca2e77e9..daf43d7e 100644 --- a/crates/before/src/version/skyline/validate.rs +++ b/crates/before/src/version/skyline/validate.rs @@ -24,7 +24,7 @@ use core::cmp::Ordering; use suanpan::Accumulator; -use crate::codec::{BitCursor, BitsMut, BitsView, DsiCursor}; +use crate::codec::{BitCursor, BitsBuf, BitsView, DsiCursor}; use crate::error::Decode; use super::signed::{fold_signed_int, unzigzag, Sign}; @@ -73,7 +73,7 @@ where // Two bits per open ancestor, pushed [left-complete, left-was-leaf] and // popped in reverse order below. A packed bit stack, so depth costs bits, // not frames. - let mut open: BitsMut = BitsMut::new(); + let mut open: BitsBuf = BitsBuf::new(); // The running leaf height. Only its sign is ever read, and only after a // subtracting delta: an adding delta cannot take a valid height negative, // and the first leaf's absolute payload is a natural. diff --git a/crates/before/src/version/tests.rs b/crates/before/src/version/tests.rs index 7aa6edfb..10229655 100644 --- a/crates/before/src/version/tests.rs +++ b/crates/before/src/version/tests.rs @@ -1895,7 +1895,7 @@ proptest! { ) { let a = from_oracle_version(&oa); let b = from_oracle_version(&ob); - let bit_eq = a.as_bits().to_bitvec() == b.as_bits().to_bitvec(); + let bit_eq = a.as_bits().to_buf() == b.as_bits().to_buf(); prop_assert_eq!(a == b, bit_eq); prop_assert_eq!(b == a, bit_eq); if a == b { diff --git a/crates/before/surfacecheck/Cargo.lock b/crates/before/surfacecheck/Cargo.lock index 2c10dd2d..2ef81986 100644 --- a/crates/before/surfacecheck/Cargo.lock +++ b/crates/before/surfacecheck/Cargo.lock @@ -6,7 +6,6 @@ version = 4 name = "before" version = "0.1.0" dependencies = [ - "bitvec", "bytes", "dashu-int", "dsi-bitstream", @@ -16,18 +15,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "bitvec" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "bytes" version = "1.12.1" @@ -68,12 +55,6 @@ dependencies = [ "num-primitive", ] -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "itoa" version = "1.0.18" @@ -116,12 +97,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rustdoc-types" version = "0.59.0" @@ -213,12 +188,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "thiserror" version = "2.0.19" @@ -245,15 +214,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zmij" version = "1.0.23" diff --git a/crates/before/tests/meter.rs b/crates/before/tests/meter.rs index 99ac99c0..216a2432 100644 --- a/crates/before/tests/meter.rs +++ b/crates/before/tests/meter.rs @@ -1408,7 +1408,7 @@ fn rank_sum_mixed_envelope() { // carries the wire-bit-linear claim). /// The skyline stream of a packed family shape, built outside measurement. -fn skyline_of(p: &meter::Packed) -> meter::skyline::BitsMut { +fn skyline_of(p: &meter::Packed) -> meter::skyline::BitsBuf { meter::skyline::encode(&version_of(p)) } @@ -1733,12 +1733,12 @@ fn sweep_metered( /// The empty version's two-bit skyline stream: the shallow operand of /// the family cmp scenarios. -fn skyline_empty() -> meter::skyline::BitsMut { +fn skyline_empty() -> meter::skyline::BitsBuf { meter::skyline::encode(&Version::new()) } /// The combined operand bytes of a sweep scenario. -fn sweep_input_bytes(a: &meter::skyline::BitsMut, b: &meter::skyline::BitsMut) -> usize { +fn sweep_input_bytes(a: &meter::skyline::BitsBuf, b: &meter::skyline::BitsBuf) -> usize { a.as_raw_slice().len() + b.as_raw_slice().len() } @@ -1888,7 +1888,7 @@ mod emit_env { /// The one-tick version's skyline stream: the shallow operand of the /// family join/meet scenarios, mirroring the packed-form join rows. -fn skyline_one_tick() -> meter::skyline::BitsMut { +fn skyline_one_tick() -> meter::skyline::BitsBuf { let one = Version::try_from(1u64).expect("a one-tick version is valid"); meter::skyline::encode(&one) } @@ -1899,7 +1899,7 @@ fn skyline_one_tick() -> meter::skyline::BitsMut { fn skyline_oracle( p: &meter::Packed, join: bool, -) -> (meter::skyline::BitsMut, meter::skyline::BitsMut) { +) -> (meter::skyline::BitsBuf, meter::skyline::BitsBuf) { let v = version_of(p); let one = Version::try_from(1u64).expect("a one-tick version is valid"); let out = if join { &v | &one } else { &v & &one }; @@ -2075,7 +2075,7 @@ fn left_spike(depth: usize) -> Version { fn tick_expand_spine_envelope() { let mut v = Version::new(); let party = party_of(&Shape::IdSpine.packed_flagged(ID_DEPTH, false)); - let input = meter::skyline::encode(&v).len() / 8 + party.encoded_bits().div_ceil(8); + let input = meter::skyline::encode(&v).len() as usize / 8 + party.encoded_bits().div_ceil(8); query_metered( "tick_expand_spine", input, diff --git a/crates/before/wasm32-pins/Cargo.lock b/crates/before/wasm32-pins/Cargo.lock index 724cf9c2..3693b5f3 100644 --- a/crates/before/wasm32-pins/Cargo.lock +++ b/crates/before/wasm32-pins/Cargo.lock @@ -44,7 +44,6 @@ dependencies = [ name = "before" version = "0.1.0" dependencies = [ - "bitvec", "borsh", "bytes", "dashu-int", @@ -61,18 +60,6 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" -[[package]] -name = "bitvec" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -411,12 +398,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures" version = "0.3.34" @@ -693,12 +674,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "regalloc2" version = "0.15.2" @@ -871,12 +846,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "target-lexicon" version = "0.13.5" @@ -1177,15 +1146,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zmij" version = "1.0.23" diff --git a/crates/before/wasm32-pins/guest/src/lib.rs b/crates/before/wasm32-pins/guest/src/lib.rs index f646e324..aa09e505 100644 --- a/crates/before/wasm32-pins/guest/src/lib.rs +++ b/crates/before/wasm32-pins/guest/src/lib.rs @@ -93,9 +93,9 @@ pub extern "C" fn pin_version_small() -> i64 { /// that the stored bytes round-trip the input exactly. /// /// The harness aims this at the 32-bit boundaries: the sizes straddling -/// 2^29 bits (the 32-bit bit-vector length encoding's cap, which the -/// build side still carries) and the 512 MiB bit-length boundary of -/// `usize` position arithmetic. +/// 2^29 bits (where a `usize >> 3` bit-count encoding would bind on a +/// 32-bit target) and the 512 MiB bit-length boundary of `usize` +/// position arithmetic. #[no_mangle] pub extern "C" fn pin_version_decode(n_bytes: u64) -> i64 { let n = match usize::try_from(n_bytes) { @@ -483,8 +483,8 @@ pub extern "C" fn pin_version_join_covering(n_bytes: u64) -> i64 { /// the unit interval takes its taller side, so the output concatenates the /// left operand's tall code with a fresh `2j + 1`-bit delta code instead of /// collapsing — the output outgrows both inputs. The harness aims `(k, j)` -/// at the emitter's output-side boundary: the build buffer's own length -/// encoding, not any per-operand bound, is what this export observes. +/// at the emitter's output side — the build buffer and its freeze hand-off, +/// not any per-operand bound, are what this export observes. #[no_mangle] pub extern "C" fn pin_version_join_emit(k: u64, j: u64) -> i64 { let a = match Version::decode(&synth_two_leaf_left(k)[..]) { diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs index d0ce83da..c8986f64 100644 --- a/crates/before/wasm32-pins/harness/tests/pins.rs +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -12,14 +12,16 @@ use wasm32_pins_harness::{call0, call1, call2, Outcome, Trap}; /// The largest buffer byte count whose whole-buffer bit count stays below -/// 2^29: one byte under the 32-bit bit-vector length encoding's cap -/// (`usize::MAX >> 3` bits), so 67108863 bytes. +/// 2^29 bits — `usize::MAX >> 3`, the cap a 32-bit bit-vector length +/// encoding imposes — so 67108863 bytes. /// -/// The walk surface reads stored streams through the crate-owned view and is -/// exact at every storable size; the emitters' build buffer is the one -/// surface still bounded by this encoding, so the walk pins straddle the -/// boundary (below, at, and past it) to hold the read side clean of it, and -/// the emitting pins meet it on their output side. +/// This is the coordinate of the boundary class this suite exists to catch: +/// a `usize`-denominated bit count binding or wrapping on a 32-bit target. +/// Every surface is exact across it — the walks and doors read stored +/// streams through the crate-owned view, and the emitters build into the +/// crate-owned buffer, both `u64`-denominated — and the pins straddle it +/// (below, at, and past) on every surface class to hold that exactness +/// pinned. const BUILD_CAP_BYTES: u64 = 67_108_863; /// Liveness: a small valid version decodes on wasm32 with the exact bit @@ -32,11 +34,11 @@ fn version_small_roundtrips_and_rejects_typed() { assert_eq!(call0("pin_version_small"), Outcome::Value(0)); } -/// The largest input whose whole-buffer bit count stays below the 32-bit -/// bit-vector length encoding's cap decodes with its exact bit length. +/// The largest input whose whole-buffer bit count stays below the 2^29-bit +/// straddle coordinate ([`BUILD_CAP_BYTES`]) decodes with its exact bit +/// length. /// -/// The cap is `usize::MAX >> 3` = 2^29 - 1 bits, so 67108863 bytes: the -/// boundary's lower adjacency witness, so a failure at the sizes just +/// The boundary's lower adjacency witness: a failure at the sizes just /// above is attributable to the boundary, never to general large-input /// handling. #[test] @@ -50,9 +52,9 @@ fn version_decode_below_build_cap() { /// A valid 64 MiB (67108864-byte) version encoding decodes correctly on /// wasm32, returning its exact live bit length. /// -/// The size is exactly 2^29 bits: the first size past the 32-bit -/// bit-vector length encoding, so this pin holds the doors to a walk on -/// which no such encoding binds. +/// The size is exactly 2^29 bits: the first size past the straddle +/// coordinate, so this pin holds the doors to a walk on which no 32-bit +/// length encoding binds. #[test] fn version_decode_at_build_cap() { assert_eq!( @@ -64,10 +66,10 @@ fn version_decode_at_build_cap() { /// A valid 67108865-byte version encoding decodes correctly on wasm32, /// returning its exact live bit length. /// -/// One byte past the length-encoding boundary's silent-wrap size; together +/// One byte past the straddle coordinate's silent-wrap size; together /// with the pin one byte below, this holds the doors clear of both failure -/// genres a 2^29-bit length encoding produces (a silently empty view, then -/// an element-count guard panic). +/// genres a 2^29-bit length encoding would produce (a silently empty view, +/// then an element-count guard panic). #[test] fn version_decode_past_build_cap() { assert_eq!( @@ -183,7 +185,7 @@ fn rank_decode_past_backend_bit_capacity_traps() { /// A valid composite key — the rank stream, then the version whose rank it /// is — decodes through the byte door `Ranked::decode` at the largest -/// version size below the build buffer's length-encoding boundary. +/// version size below the straddle coordinate. /// /// The door re-derives the version's rank to verify the key, and that fold /// walks the version through the crate-owned view: the lower adjacency @@ -238,8 +240,7 @@ fn ranked_decode_deep_in_storable_range() { /// A valid composite key decodes through the borsh door /// `Ranked::deserialize_reader` at the largest version size below the -/// build buffer's length-encoding boundary, consuming exactly its own -/// bytes. +/// straddle coordinate, consuming exactly its own bytes. /// /// The streaming door runs the same rank re-derivation as the byte door: /// the boundary straddle's lower witness. @@ -288,7 +289,7 @@ fn ranked_borsh_deep_in_storable_range() { /// A valid coincident span — two byte-equal version streams — decodes /// through the borsh door `Span::deserialize_reader` at the largest `lo` -/// size below the build buffer's length-encoding boundary. +/// size below the straddle coordinate. /// /// The door consumes exactly its own bytes. /// It validates the second stream against `lo`'s view in one fused @@ -334,8 +335,8 @@ fn span_borsh_deep_in_storable_range() { } /// Causal comparison decides a valid stored pair exactly at the largest -/// operand size below the build buffer's length-encoding boundary: the -/// taller lone leaf reads strictly greater both ways around. +/// operand size below the straddle coordinate: the taller lone leaf reads +/// strictly greater both ways around. /// /// The comparison-class walk's boundary straddle, lower witness: ordering /// reads each operand through the crate-owned view, with no decode door in @@ -383,12 +384,12 @@ fn version_cmp_at_storage_bound() { } /// Join emits a covered pair exactly at the largest operand size below the -/// build buffer's length-encoding boundary: the taller lone leaf joined -/// with a short one reproduces the taller, byte for byte. +/// straddle coordinate: the taller lone leaf joined with a short one +/// reproduces the taller, byte for byte. /// /// The join-class walk's lower adjacency witness — and its emission /// rebuilds the full-size output, so the pin also witnesses the build -/// buffer just below its own cap. +/// buffer just below the coordinate. #[test] fn version_join_below_build_cap() { assert_eq!( @@ -397,51 +398,44 @@ fn version_join_below_build_cap() { ); } -/// PINNED AS FOUND: joining a valid stored 67108864-byte version — -/// exactly 2^29 bits — with a small one it covers traps on wasm32 instead -/// of emitting the covered result. +/// Joining a valid stored 67108864-byte version — exactly 2^29 bits — with +/// a small one it covers emits the covered result on wasm32, byte for +/// byte. /// -/// The walk itself is exact at this size (the comparison pins beside this -/// one decide the same operands both ways around); the trap is the -/// emission's output side: the covered join reproduces the big operand, a -/// finished stream whose element-rounded bit count exceeds the build -/// buffer's `usize::MAX >> 3`-bit length encoding at the freeze seam — -/// the same boundary `version_join_emit_at_build_cap_traps` pins from its -/// two-operand family. The cure is the crate-owned build buffer, which -/// flips this pin to the correct-value assertion. +/// The emitting operation class's middle straddle witness, on the output +/// side: the covered join rebuilds the big operand whole, a finished +/// stream whose bit count sits exactly at the straddle coordinate when it +/// crosses the freeze seam — which the crate-owned build buffer carries at +/// `u64` width on every target. #[test] -fn version_join_at_build_cap_traps() { +fn version_join_at_build_cap() { assert_eq!( call1("pin_version_join_covering", BUILD_CAP_BYTES + 1), - Outcome::Trapped(Trap::UnreachableCodeReached), + Outcome::Value(0), ); } -/// PINNED AS FOUND: joining a valid stored 67108865-byte version with a -/// small one it covers traps on wasm32. -/// -/// The output boundary's second witness, one byte further: the covered -/// output's live bit count itself exceeds the build buffer's length -/// encoding. The cure is the crate-owned build buffer, beside -/// `version_join_at_build_cap_traps`. +/// Joining a valid stored 67108865-byte version with a small one it covers +/// emits the covered result on wasm32: the emitting class's upper straddle +/// witness, one byte past the coordinate, beside +/// `version_join_at_build_cap`. #[test] -fn version_join_past_build_cap_traps() { +fn version_join_past_build_cap() { assert_eq!( call1("pin_version_join_covering", BUILD_CAP_BYTES + 2), - Outcome::Trapped(Trap::UnreachableCodeReached), + Outcome::Value(0), ); } -/// Join emits the largest output this operand family can pass through the -/// emitter's finish seam on wasm32: 536870903 live bits. +/// Join emits an output of 536870903 live bits — one bit under 67108863 +/// whole output bytes, the straddle coordinate — from two operands each +/// comfortably under 64 MiB. /// -/// That is one bit under the 67108863 whole output bytes the finished -/// stream's bit-vector adoption can represent, from two operands each -/// comfortably under 64 MiB. The operands are complementary two-leaf -/// skylines (~50 MB and ~42 MB) whose join concatenates: the output -/// outgrows both inputs, so this witnesses the emitter's output-side -/// boundary from below, independent of any operand size. The returned -/// observation is the output's exact live bit length. +/// The operands are complementary two-leaf skylines (~50 MB and ~42 MB) +/// whose join concatenates: the output outgrows both inputs, so this +/// witnesses the emitter's output side just below the coordinate, +/// independent of any operand size. The returned observation is the +/// output's exact live bit length. #[test] fn version_join_emit_below_build_cap() { assert_eq!( @@ -450,25 +444,21 @@ fn version_join_emit_below_build_cap() { ); } -/// PINNED AS FOUND: a join of two valid operands, each under every -/// per-operand bound, traps on wasm32 instead of emitting when its -/// output's finished byte length reaches 67108864. +/// A join of two valid operands, each under every per-operand bound, emits +/// on wasm32 with an output of 536870905 live bits — 67108864 finished +/// bytes, the first byte length past the straddle coordinate at the freeze +/// seam. /// -/// That is 536870905 live bits: the first length whose whole-byte buffer -/// exceeds the bit vector's `usize::MAX >> 3`-bit length encoding when the -/// emitter's byte-backed builder hands the finished stream over. The -/// operands are complementary two-leaf skylines, ~50 MB and ~42 MB. -/// The boundary is the build-side buffer's length encoding, documented at -/// no public operation, and it sits below the byte doors' 512 MiB storage -/// bound: valid operand pairs admitted and walkable on this target have no -/// emittable join. Unconditional 32-bit correctness requires every -/// storable join to emit; the cure flips this pin to the exact-length -/// assertion beside its witness `version_join_emit_below_build_cap`. -#[test] -fn version_join_emit_at_build_cap_traps() { +/// The operands are complementary two-leaf skylines, ~50 MB and ~42 MB, +/// whose join concatenates: the emitting class's upper output-side +/// straddle witness, beside `version_join_emit_below_build_cap` — every +/// storable join emits, whatever its output size, because the build +/// buffer and the freeze hand-off carry `u64` bit counts on every target. +#[test] +fn version_join_emit_at_build_cap() { assert_eq!( call2("pin_version_join_emit", 100_000_000, 168_435_450), - Outcome::Trapped(Trap::UnreachableCodeReached), + Outcome::Value(536_870_905), ); } From 05d87e1b881246b6fab03f0df42f33149311b0ed Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 19 Aug 2026 10:55:05 -0400 Subject: [PATCH 16/20] before: denominate bit streams in u64 end to end; gate encoded_bits meter-side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stored streams, cursors, walks, and depths now speak one width: u64, the denomination the build buffer and the view already carry. Bits::len returns the u64 live length directly; the freeze and decode doors adopt any buffer whose padding validates, imposing no bound of their own; the BitCursor trait's position and read_unary are u64 (the separate u64 position read dissolves into them); BitStack heights, unary runs, tree depths, flip levels, code lengths, and open-range counts widen across the walk surface. On 32-bit targets streams are therefore bounded only by allocatable memory: no usize-derived structural cap survives on the stored, walk, or build path. Byte indexes stay usize (they index allocated buffers), and each remaining u64-to-usize seam carries a checked conversion with its rationale inline. Where a gamma mantissa names a value wider than the big-integer backend can hold on the target, both decode paths now reject with the value's own genre (NotCanonical) at the same width, per-bit and word-parallel alike. Owner ruling (Finch, direct): encoded_bits on Party, Version, and Clock leaves the unconditional public API — every consumer is a meter, a coverage suite, a fuel denominator, or a boundary pin — and re-homes behind the existing meter feature (the instrument surface), returning u64. Consumers routed: laws now implies meter (the representational laws read the observation), the fuzzfit harness and wasm32-pins guest enable it explicitly, and rumors' dev-dependency lights it for the conservation suite, whose invariant is unchanged. The surface roster's rows are unchanged: the extractor scans source text, so the gated fns keep their named rows. The recode mutant exclusion's line pin moves with its expression (rustfmt reflowed the file); same mutant, same rationale, re-pinned. Meter parity: all 300 MEASURED rows are byte-identical to the parent commit's readings. --- .cargo/mutants.toml | 2 +- Cargo.toml | 5 +- crates/before-fuelscape/src/count/tests.rs | 8 +- crates/before-fuelscape/src/sample/tests.rs | 4 +- crates/before/Cargo.toml | 9 +- crates/before/benches/presize.rs | 4 +- crates/before/examples/code_study.rs | 11 ++- crates/before/examples/space_consumption.rs | 2 +- crates/before/fuzzfit/harness/Cargo.toml | 4 +- crates/before/fuzzfit/harness/src/ops.rs | 99 +++++++++---------- crates/before/src/borsh_impls.rs | 8 +- crates/before/src/borsh_impls/tests.rs | 14 +-- crates/before/src/clock.rs | 23 +++-- crates/before/src/clock/tests.rs | 8 +- crates/before/src/codec/bits.rs | 84 +++++----------- crates/before/src/codec/buf.rs | 5 +- crates/before/src/codec/cursor.rs | 28 +++--- crates/before/src/codec/dsi.rs | 56 +++++------ crates/before/src/codec/dsi/tests.rs | 12 +-- crates/before/src/codec/gamma.rs | 34 +++---- crates/before/src/codec/stack.rs | 24 ++--- crates/before/src/codec/stack/tests.rs | 2 +- crates/before/src/codec/tests.rs | 12 +-- crates/before/src/codec/tree.rs | 9 +- crates/before/src/laws.rs | 6 +- crates/before/src/meter/board/operand.rs | 4 +- crates/before/src/meter/board/ops.rs | 30 ++++-- crates/before/src/meter/tests.rs | 8 +- crates/before/src/meter/tier2/tests.rs | 8 +- crates/before/src/party.rs | 11 ++- crates/before/src/party/ops/build.rs | 4 +- crates/before/src/party/ops/diff.rs | 16 +-- crates/before/src/party/tests.rs | 6 +- crates/before/src/span/wire.rs | 3 +- crates/before/src/version.rs | 9 +- crates/before/src/version/skyline/admit.rs | 17 ++-- crates/before/src/version/skyline/build.rs | 20 ++-- .../before/src/version/skyline/build/tests.rs | 18 ++-- .../before/src/version/skyline/emit/tests.rs | 2 +- crates/before/src/version/skyline/fill.rs | 36 +++---- .../before/src/version/skyline/fill/fuse.rs | 20 ++-- .../src/version/skyline/fill/prescan.rs | 4 +- crates/before/src/version/skyline/grow.rs | 47 ++++----- crates/before/src/version/skyline/literal.rs | 4 +- crates/before/src/version/skyline/masked.rs | 6 +- crates/before/src/version/skyline/overlay.rs | 33 ++++--- crates/before/src/version/skyline/place.rs | 8 +- .../src/version/skyline/place/filter.rs | 8 +- crates/before/src/version/skyline/query.rs | 16 +-- .../before/src/version/skyline/query/tests.rs | 32 +++--- .../before/src/version/skyline/query/web.rs | 2 +- crates/before/src/version/skyline/text.rs | 18 ++-- .../before/src/version/skyline/text/tests.rs | 2 +- crates/before/src/version/skyline/validate.rs | 4 +- crates/before/src/version/skyline/walk.rs | 18 ++-- .../before/src/version/skyline/watermark.rs | 14 +-- .../src/version/skyline/watermark/tests.rs | 2 +- crates/before/tests/meter.rs | 7 +- crates/before/wasm32-pins/guest/Cargo.toml | 5 +- tools/mutantcheck-expected.json | 2 +- 60 files changed, 450 insertions(+), 467 deletions(-) diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index 47fa687c..a06cd48e 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -139,7 +139,7 @@ exclude_re = [ # Proven equivalent, line-pinned (the match arm's guard shares the # operator pattern and is killed): at equal magnitude the cross and # shrink paths both land on the positive zero zigzag. - "grow\\.rs:468:38: replace < with <= in recode", + "grow\\.rs:463:38: replace < with <= in recode", # --- before::version::skyline::fill / prescan --- diff --git a/Cargo.toml b/Cargo.toml index 970c2682..d47cfd61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,10 @@ rand = "0.8" [dev-dependencies] rumors = { path = ".", features = ["test-internals", "conformance"] } -before = { path = "crates/before", features = ["borsh"] } +# The meter feature lights before's instrument surface for this crate's +# own tests: the conservation suite reads the exact-bit-length observation +# (`encoded_bits`) it denominates identity conservation in. +before = { path = "crates/before", features = ["borsh", "meter"] } proptest = "1" criterion = { version = "0.5", features = ["html_reports"] } insta = "1.47" diff --git a/crates/before-fuelscape/src/count/tests.rs b/crates/before-fuelscape/src/count/tests.rs index 1583cba2..81e3b994 100644 --- a/crates/before-fuelscape/src/count/tests.rs +++ b/crates/before-fuelscape/src/count/tests.rs @@ -133,7 +133,7 @@ const CENSUS_BITS: usize = 8 * CENSUS_BYTES - 1; /// and tallies acceptances per live bit length. The sweep fans out over /// rayon; per-chunk histograms add commutatively, so the fan-out cannot /// change any count. -fn decoder_census(accept: impl Fn(&[u8]) -> Option + Sync) -> Vec { +fn decoder_census(accept: impl Fn(&[u8]) -> Option + Sync) -> Vec { let empty = || vec![0u64; CENSUS_BITS + 1]; let mut census = empty(); for len in 1..=CENSUS_BYTES { @@ -147,10 +147,12 @@ fn decoder_census(accept: impl Fn(&[u8]) -> Option + Sync) -> Vec { } if let Some(bits) = accept(&buf[..len]) { assert!( - bits < 8 * len, + bits < 8 * len as u64, "decoder reported {bits} live bits from a {len}-byte input" ); - hist[bits] += 1; + // In range per the assert above: census inputs are a few + // bytes, so the bit length indexes the histogram. + hist[bits as usize] += 1; } hist }) diff --git a/crates/before-fuelscape/src/sample/tests.rs b/crates/before-fuelscape/src/sample/tests.rs index d26fe781..3d5149b7 100644 --- a/crates/before-fuelscape/src/sample/tests.rs +++ b/crates/before-fuelscape/src/sample/tests.rs @@ -240,7 +240,7 @@ proptest! { .expect("every byte size down to 1 has canonical versions"); prop_assert_eq!(draw.bytes.len(), size); let version = Version::decode(&draw.bytes[..]).expect("sampler output must be canonical"); - prop_assert_eq!(version.encoded_bits(), draw.bits); + prop_assert_eq!(version.encoded_bits(), draw.bits as u64); prop_assert_eq!(version.encode(), draw.bytes); } @@ -257,7 +257,7 @@ proptest! { .expect("every byte size down to 1 has canonical parties"); prop_assert_eq!(draw.bytes.len(), size); let party = Party::decode(&draw.bytes[..]).expect("sampler output must be canonical"); - prop_assert_eq!(party.encoded_bits(), draw.bits); + prop_assert_eq!(party.encoded_bits(), draw.bits as u64); prop_assert_eq!(party.encode(), draw.bytes); } } diff --git a/crates/before/Cargo.toml b/crates/before/Cargo.toml index be6a6214..fe5a7046 100644 --- a/crates/before/Cargo.toml +++ b/crates/before/Cargo.toml @@ -62,12 +62,15 @@ oracle = [] # worst-case inputs and read the counters they envelope, and the public # operation surface roster (`src/surface.rs`) as `before::surface`, so external # instrument crates can bind their coverage to the rows the surface-coverage -# suite enforces totality over. Bench/test-only. +# suite enforces totality over, and the instrument-side value observations +# (`encoded_bits` on `Party`/`Version`/`Clock`), the exact bit-length +# denomination the meters and boundary pins read. Bench/test-only. meter = [] # Exposes the named algebraic-law predicates (`src/laws.rs`) as `before::laws`, # so the fuzz workspace can drive the same law collection the in-tree proptests -# assert. Test/fuzz-only. -laws = [] +# assert. Implies `meter`: the representational laws read the instrument-side +# observations (`encoded_bits`) that feature exposes. Test/fuzz-only. +laws = ["meter"] # Counts big-integer limb-scale work (operand limbs per `Base` operation: # arithmetic, comparison, equality, and hashing; plus one value-width record per # decoded wide-gamma value) into a process-global counter read via diff --git a/crates/before/benches/presize.rs b/crates/before/benches/presize.rs index c836154d..83c0895a 100644 --- a/crates/before/benches/presize.rs +++ b/crates/before/benches/presize.rs @@ -138,7 +138,7 @@ fn outgrow_family() -> Vec<(u32, Version, Party)> { fn resident_line( site: &str, x: impl std::fmt::Display, - out_bits: usize, + out_bits: u64, build: impl FnOnce() -> T, ) { let arm = common::alloc_arms(); @@ -196,7 +196,7 @@ fn resident_report() { let plan = common::plan(&mut r, n, 1); let v = common::impl_versions(&plan, 1).pop().expect("one group"); let s = v.to_string(); - resident_line("display", n, s.len() * 8, || v.to_string()); + resident_line("display", n, s.len() as u64 * 8, || v.to_string()); resident_line("parse", n, v.encoded_bits(), || { s.parse::().expect("a rendered version parses") }); diff --git a/crates/before/examples/code_study.rs b/crates/before/examples/code_study.rs index 392add6b..e14f7fc1 100644 --- a/crates/before/examples/code_study.rs +++ b/crates/before/examples/code_study.rs @@ -244,8 +244,9 @@ impl Code { // ─── the skyline stream walker ────────────────────────────────────────────── /// Read bit `i` of an Msb0 packed stream. -fn bit(bytes: &[u8], i: usize) -> bool { - (bytes[i / 8] >> (7 - i % 8)) & 1 == 1 +fn bit(bytes: &[u8], i: u64) -> bool { + // An in-range byte index fits `usize`: it indexes an allocated buffer. + (bytes[(i / 8) as usize] >> (7 - i % 8)) & 1 == 1 } /// Walk one stored version stream, recording payload values per class and @@ -253,7 +254,7 @@ fn bit(bytes: &[u8], i: usize) -> bool { fn walk(v: &Version, corpus: &mut Corpus) { let bytes = v.as_bytes(); let bits = v.encoded_bits(); - let mut pos = 0usize; + let mut pos = 0u64; let mut pending = 1u64; // subtrees still owed let mut nodes = 0u128; let mut first = true; @@ -272,7 +273,7 @@ fn walk(v: &Version, corpus: &mut Corpus) { } pending -= 1; // Leaf (`1`) payload: one gamma code. Count the unary prefix. - let mut k = 0usize; + let mut k = 0u64; while !bit(bytes, pos) { pos += 1; k += 1; @@ -301,7 +302,7 @@ fn walk(v: &Version, corpus: &mut Corpus) { } } else { pos += k; // skip the wide mantissa remainder - let l = (k + 1) as u64; + let l = k + 1; hist.record_wide(l); for (slot, (_, code)) in code_bits.iter_mut().zip(CODES) { *slot = match (*slot, code.len_wide(l)) { diff --git a/crates/before/examples/space_consumption.rs b/crates/before/examples/space_consumption.rs index e6b12dc3..c7120a58 100644 --- a/crates/before/examples/space_consumption.rs +++ b/crates/before/examples/space_consumption.rs @@ -334,7 +334,7 @@ fn build_population(n: usize) -> Vec { /// `⌈bits/8⌉` per stamp (the per-stamp ceiling is what biases the byte mean). fn mean_stamp_sizes(clocks: &[Clock]) -> (f64, f64) { let n = clocks.len() as f64; - let bits: usize = clocks.iter().map(Clock::encoded_bits).sum(); + let bits: u64 = clocks.iter().map(Clock::encoded_bits).sum(); let bytes: usize = clocks.iter().map(|c| c.encode().len()).sum(); (bits as f64 / n, bytes as f64 / n) } diff --git a/crates/before/fuzzfit/harness/Cargo.toml b/crates/before/fuzzfit/harness/Cargo.toml index c75e4f0a..2b614d3d 100644 --- a/crates/before/fuzzfit/harness/Cargo.toml +++ b/crates/before/fuzzfit/harness/Cargo.toml @@ -8,7 +8,9 @@ edition = "2021" # The native mirror: the harness executes every generated program twice — # natively (for denominators and expected bytes) and in the wasm guest (for # fuel) — so the same `before` build the guest wraps is a direct dependency. -before = { path = "../.." } +# The meter feature lights the instrument surface: the exact-bit-length +# observation (`encoded_bits`) the fuel denominators read. +before = { path = "../..", features = ["meter"] } # Executes the guest under fuel metering: fuel decrements per executed wasm # instruction, deterministic and host-independent. Tool-side only: this # workspace is detached from the parent, so wasmtime never enters the diff --git a/crates/before/fuzzfit/harness/src/ops.rs b/crates/before/fuzzfit/harness/src/ops.rs index ef1a476e..48f1cd72 100644 --- a/crates/before/fuzzfit/harness/src/ops.rs +++ b/crates/before/fuzzfit/harness/src/ops.rs @@ -456,7 +456,7 @@ impl Mirror { done(SEED_DENOM_BITS, OK) } Op::ClockTick { c } => { - let denom = self.clock(c).ok_or_else(malformed)?.encoded_bits() as u64; + let denom = self.clock(c).ok_or_else(malformed)?.encoded_bits(); match self.regs.get_mut(c as usize) { Some(Some(NVal::C(clock))) => { clock.tick(); @@ -466,7 +466,7 @@ impl Mirror { } } Op::ClockSend { c } => { - let denom = self.clock(c).ok_or_else(malformed)?.encoded_bits() as u64; + let denom = self.clock(c).ok_or_else(malformed)?.encoded_bits(); match self.regs.get_mut(c as usize) { Some(Some(NVal::C(clock))) => { clock.send(); @@ -476,7 +476,7 @@ impl Mirror { } } Op::ClockFork { dst, src } => { - let denom = self.clock(src).ok_or_else(malformed)?.encoded_bits() as u64; + let denom = self.clock(src).ok_or_else(malformed)?.encoded_bits(); let forked = match self.regs.get_mut(src as usize) { Some(Some(NVal::C(clock))) => clock.fork(), _ => return Err(malformed()), @@ -485,9 +485,8 @@ impl Mirror { done(denom, OK) } Op::ClockJoin { a, b } => { - let denom = (self.clock(a).ok_or_else(malformed)?.encoded_bits() - + self.clock(b).ok_or_else(malformed)?.encoded_bits()) - as u64; + let denom = self.clock(a).ok_or_else(malformed)?.encoded_bits() + + self.clock(b).ok_or_else(malformed)?.encoded_bits(); let Some(NVal::C(cb)) = self.take(b) else { return Err(malformed()); }; @@ -503,9 +502,8 @@ impl Mirror { } } Op::ClockRecv { c, v } => { - let denom = (self.clock(c).ok_or_else(malformed)?.encoded_bits() - + self.version(v).ok_or_else(malformed)?.encoded_bits()) - as u64; + let denom = self.clock(c).ok_or_else(malformed)?.encoded_bits() + + self.version(v).ok_or_else(malformed)?.encoded_bits(); let version = self.version(v).ok_or_else(malformed)?.clone(); match self.regs.get_mut(c as usize) { Some(Some(NVal::C(clock))) => { @@ -516,9 +514,8 @@ impl Mirror { } } Op::ClockSync { a, b } => { - let denom = (self.clock(a).ok_or_else(malformed)?.encoded_bits() - + self.clock(b).ok_or_else(malformed)?.encoded_bits()) - as u64; + let denom = self.clock(a).ok_or_else(malformed)?.encoded_bits() + + self.clock(b).ok_or_else(malformed)?.encoded_bits(); // Two mutable borrows out of one file: take, sync, put back. let Some(NVal::C(mut cb)) = self.take(b) else { return Err(malformed()); @@ -532,25 +529,25 @@ impl Mirror { } Op::ClockOwnVersion { dst, src } => { let clock = self.clock(src).ok_or_else(malformed)?; - let input = clock.encoded_bits() as u64; + let input = clock.encoded_bits(); // The explicit materialization: the view itself is O(1) // and prices nothing. let own = clock.own_version().to_version(); // Output-dominated row: input + packed output, output read // from the actual result. - let denom = input + own.encoded_bits() as u64; + let denom = input + own.encoded_bits(); self.put(dst, NVal::V(own)); done(denom, OK) } Op::ClockVersion { dst, src } => { let clock = self.clock(src).ok_or_else(malformed)?; let version = clock.version().clone(); - let denom = clock.encoded_bits() as u64; + let denom = clock.encoded_bits(); self.put(dst, NVal::V(version)); done(denom, OK) } Op::ClockIntoParts { dst_p, dst_v, src } => { - let denom = self.clock(src).ok_or_else(malformed)?.encoded_bits() as u64; + let denom = self.clock(src).ok_or_else(malformed)?.encoded_bits(); let Some(NVal::C(clock)) = self.take(src) else { return Err(malformed()); }; @@ -560,9 +557,8 @@ impl Mirror { done(denom, OK) } Op::ClockFromParts { dst, p, v } => { - let denom = (self.party(p).ok_or_else(malformed)?.encoded_bits() - + self.version(v).ok_or_else(malformed)?.encoded_bits()) - as u64; + let denom = self.party(p).ok_or_else(malformed)?.encoded_bits() + + self.version(v).ok_or_else(malformed)?.encoded_bits(); let Some(NVal::P(party)) = self.take(p) else { return Err(malformed()); }; @@ -574,7 +570,7 @@ impl Mirror { } Op::ClockEncode { src } => { let clock = self.clock(src).ok_or_else(malformed)?; - let denom = clock.encoded_bits() as u64; + let denom = clock.encoded_bits(); self.stage = clock.encode(); done(denom, OK) } @@ -585,9 +581,8 @@ impl Mirror { done(denom, OK) } Op::VersionTick { v, p } => { - let denom = (self.version(v).ok_or_else(malformed)?.encoded_bits() - + self.party(p).ok_or_else(malformed)?.encoded_bits()) - as u64; + let denom = self.version(v).ok_or_else(malformed)?.encoded_bits() + + self.party(p).ok_or_else(malformed)?.encoded_bits(); let Some(NVal::P(party)) = self.take(p) else { return Err(malformed()); }; @@ -606,9 +601,8 @@ impl Mirror { } } Op::VersionJoin { dst, a, b } => { - let denom = (self.version(a).ok_or_else(malformed)?.encoded_bits() - + self.version(b).ok_or_else(malformed)?.encoded_bits()) - as u64; + let denom = self.version(a).ok_or_else(malformed)?.encoded_bits() + + self.version(b).ok_or_else(malformed)?.encoded_bits(); let Some(NVal::V(va)) = self.take(a) else { return Err(malformed()); }; @@ -620,9 +614,8 @@ impl Mirror { done_pair(denom, OK, identity) } Op::VersionMeet { dst, a, b } => { - let denom = (self.version(a).ok_or_else(malformed)?.encoded_bits() - + self.version(b).ok_or_else(malformed)?.encoded_bits()) - as u64; + let denom = self.version(a).ok_or_else(malformed)?.encoded_bits() + + self.version(b).ok_or_else(malformed)?.encoded_bits(); let Some(NVal::V(va)) = self.take(a) else { return Err(malformed()); }; @@ -635,19 +628,19 @@ impl Mirror { Op::VersionProject { dst, v, p } => { let version = self.version(v).ok_or_else(malformed)?; let party = self.party(p).ok_or_else(malformed)?; - let input = (version.encoded_bits() + party.encoded_bits()) as u64; + let input = version.encoded_bits() + party.encoded_bits(); // The explicit materialization: the view itself is O(1) // and prices nothing. let projected = (version / party).to_version(); // Output-dominated row: input + packed output. - let denom = input + projected.encoded_bits() as u64; + let denom = input + projected.encoded_bits(); self.put(dst, NVal::V(projected)); done(denom, OK) } Op::VersionCmp { a, b } => { let va = self.version(a).ok_or_else(malformed)?; let vb = self.version(b).ok_or_else(malformed)?; - let denom = (va.encoded_bits() + vb.encoded_bits()) as u64; + let denom = va.encoded_bits() + vb.encoded_bits(); let expect = match va.partial_cmp(vb) { Some(Ordering::Less) => 0, Some(Ordering::Equal) => 1, @@ -661,13 +654,13 @@ impl Mirror { Op::VersionConcurrent { a, b } => { let va = self.version(a).ok_or_else(malformed)?; let vb = self.version(b).ok_or_else(malformed)?; - let denom = (va.encoded_bits() + vb.encoded_bits()) as u64; + let denom = va.encoded_bits() + vb.encoded_bits(); let identity = version_buffers_alias(va, vb); done_pair(denom, i64::from(va.concurrent(vb)), identity) } Op::VersionRank { dst, src } => { let version = self.version(src).ok_or_else(malformed)?; - let denom = version.encoded_bits() as u64; + let denom = version.encoded_bits(); let rank = version.rank(); self.put(dst, NVal::R(rank)); done(denom, OK) @@ -675,7 +668,7 @@ impl Mirror { Op::VersionDistance { dst, a, b } => { let va = self.version(a).ok_or_else(malformed)?; let vb = self.version(b).ok_or_else(malformed)?; - let denom = (va.encoded_bits() + vb.encoded_bits()) as u64; + let denom = va.encoded_bits() + vb.encoded_bits(); // The metric's rung is canonical equality: equal values // answer zero whichever buffers carry them. let identity = va == vb; @@ -686,7 +679,7 @@ impl Mirror { Op::VersionLag { dst, a, b } => { let va = self.version(a).ok_or_else(malformed)?; let vb = self.version(b).ok_or_else(malformed)?; - let denom = (va.encoded_bits() + vb.encoded_bits()) as u64; + let denom = va.encoded_bits() + vb.encoded_bits(); // The metric's rung is canonical equality, as `distance`. let identity = va == vb; let rank = va.lag(vb); @@ -695,7 +688,7 @@ impl Mirror { } Op::VersionMinTicks { src } => { let version = self.version(src).ok_or_else(malformed)?; - let denom = version.encoded_bits() as u64; + let denom = version.encoded_bits(); // The guest returns the count's decimal digest (the // count is unbounded; the i64 channel carries FNV-1a of // its rendering, computed identically here). @@ -709,7 +702,7 @@ impl Mirror { Some(NVal::V(v)) => v, _ => return Err(malformed()), }; - denom += v.encoded_bits() as u64; + denom += v.encoded_bits(); operands.push(v); } let mut operands = operands.into_iter(); @@ -729,7 +722,7 @@ impl Mirror { Some(NVal::V(v)) => v, _ => return Err(malformed()), }; - denom += v.encoded_bits() as u64; + denom += v.encoded_bits(); operands.push(v); } let mut operands = operands.into_iter(); @@ -743,7 +736,7 @@ impl Mirror { } Op::VersionEncode { src } => { let version = self.version(src).ok_or_else(malformed)?; - let denom = version.encoded_bits() as u64; + let denom = version.encoded_bits(); self.stage = version.encode(); done(denom, OK) } @@ -755,7 +748,7 @@ impl Mirror { } Op::VersionDisplay { src } => { let version = self.version(src).ok_or_else(malformed)?; - let input = version.encoded_bits() as u64; + let input = version.encoded_bits(); let text = version.to_string(); // Text I/O: packed input + text output, output read from // the actual result. @@ -773,7 +766,7 @@ impl Mirror { done(denom, OK) } Op::PartyFork { dst, src } => { - let denom = self.party(src).ok_or_else(malformed)?.encoded_bits() as u64; + let denom = self.party(src).ok_or_else(malformed)?.encoded_bits(); let forked = match self.regs.get_mut(src as usize) { Some(Some(NVal::P(party))) => party.fork(), _ => return Err(malformed()), @@ -782,22 +775,21 @@ impl Mirror { done(denom, OK) } Op::PartyForks { dst, src, n } => { - let input = self.party(src).ok_or_else(malformed)?.encoded_bits() as u64; + let input = self.party(src).ok_or_else(malformed)?.encoded_bits(); let shares = match self.regs.get_mut(src as usize) { Some(Some(NVal::P(party))) => party.forks(u64::from(n)).collect::>(), _ => return Err(malformed()), }; // Share splitting: the output is n packed parties. - let denom = input + shares.iter().map(|s| s.encoded_bits() as u64).sum::(); + let denom = input + shares.iter().map(|s| s.encoded_bits()).sum::(); for (i, share) in shares.into_iter().enumerate() { self.put(dst + i as u32, NVal::P(share)); } done(denom, OK) } Op::PartyJoin { a, b } => { - let denom = (self.party(a).ok_or_else(malformed)?.encoded_bits() - + self.party(b).ok_or_else(malformed)?.encoded_bits()) - as u64; + let denom = self.party(a).ok_or_else(malformed)?.encoded_bits() + + self.party(b).ok_or_else(malformed)?.encoded_bits(); let Some(NVal::P(pb)) = self.take(b) else { return Err(malformed()); }; @@ -815,19 +807,18 @@ impl Mirror { Op::PartyIsDisjoint { a, b } => { let pa = self.party(a).ok_or_else(malformed)?; let pb = self.party(b).ok_or_else(malformed)?; - let denom = (pa.encoded_bits() + pb.encoded_bits()) as u64; + let denom = pa.encoded_bits() + pb.encoded_bits(); done(denom, i64::from(pa.is_disjoint(pb))) } Op::PartyCovers { a, b } => { let pa = self.party(a).ok_or_else(malformed)?; let pb = self.party(b).ok_or_else(malformed)?; - let denom = (pa.encoded_bits() + pb.encoded_bits()) as u64; + let denom = pa.encoded_bits() + pb.encoded_bits(); done(denom, i64::from(pa.covers(pb))) } Op::PartyWithout { dst, a, b } => { - let denom = (self.party(a).ok_or_else(malformed)?.encoded_bits() - + self.party(b).ok_or_else(malformed)?.encoded_bits()) - as u64; + let denom = self.party(a).ok_or_else(malformed)?.encoded_bits() + + self.party(b).ok_or_else(malformed)?.encoded_bits(); let Some(NVal::P(pa)) = self.take(a) else { return Err(malformed()); }; @@ -842,7 +833,7 @@ impl Mirror { } Op::PartyEncode { src } => { let party = self.party(src).ok_or_else(malformed)?; - let denom = party.encoded_bits() as u64; + let denom = party.encoded_bits(); self.stage = party.encode(); done(denom, OK) } @@ -854,7 +845,7 @@ impl Mirror { } Op::PartyDisplay { src } => { let party = self.party(src).ok_or_else(malformed)?; - let input = party.encoded_bits() as u64; + let input = party.encoded_bits(); let text = party.to_string(); let denom = input + (text.len() as u64) * 8; self.stage = text.into_bytes(); diff --git a/crates/before/src/borsh_impls.rs b/crates/before/src/borsh_impls.rs index 7e59ccb5..25076490 100644 --- a/crates/before/src/borsh_impls.rs +++ b/crates/before/src/borsh_impls.rs @@ -103,12 +103,8 @@ impl BitCursor for ReaderCursor<'_, R> { Ok(bit) } - fn position(&self) -> usize { - // The grammar walks read no positions on this cursor (they return - // `()` or are read after `finish`); the conversion stays checked so - // a future position-reading walk over a past-usize field fails - // loudly instead of truncating. - usize::try_from(self.position).expect("a read position fits usize") + fn position(&self) -> u64 { + self.position } fn read_int(&mut self) -> Result { diff --git a/crates/before/src/borsh_impls/tests.rs b/crates/before/src/borsh_impls/tests.rs index 281bd8b4..341f1fc4 100644 --- a/crates/before/src/borsh_impls/tests.rs +++ b/crates/before/src/borsh_impls/tests.rs @@ -305,24 +305,24 @@ proptest! { struct BitwiseReaderCursor<'a, R> { reader: &'a mut R, bits: BitsBuf, - position: usize, + position: u64, } impl BitCursor for BitwiseReaderCursor<'_, R> { type Error = Decode; fn read_bit(&mut self) -> Result { - if self.position as u64 == self.bits.len() { + if self.position == self.bits.len() { let mut byte = [0]; self.reader.read_exact(&mut byte).map_err(Decode::Io)?; self.bits.push_bits(u64::from(byte[0]), 8); } - let bit = self.bits.get(self.position as u64); + let bit = self.bits.get(self.position); self.position += 1; Ok(bit) } - fn position(&self) -> usize { + fn position(&self) -> u64 { self.position } } @@ -359,7 +359,7 @@ fn reference_version(reader: &mut R) -> Result { let position = cursor.position; reference_consume_padding(&mut cursor)?; let mut bits = cursor.bits; - bits.truncate(position as u64); + bits.truncate(position); Ok(Version::from_bits(bits)) } @@ -379,7 +379,7 @@ fn reference_party(reader: &mut R) -> Result { let position = cursor.position; reference_consume_padding(&mut cursor)?; let mut bits = cursor.bits; - bits.truncate(position as u64); + bits.truncate(position); Ok(Party::from_bits(bits)) } @@ -527,7 +527,7 @@ fn reference_span(reader: &mut R) -> Result, Decode> { let position = cursor.position; reference_consume_padding(&mut cursor)?; let mut bits = cursor.bits; - bits.truncate(position as u64); + bits.truncate(position); let hi = match admission { Admission::Refuted => return Err(Decode::NotCanonical), Admission::Equal => lo.clone(), diff --git a/crates/before/src/clock.rs b/crates/before/src/clock.rs index 498d0cdd..dc8932d0 100644 --- a/crates/before/src/clock.rs +++ b/crates/before/src/clock.rs @@ -792,6 +792,11 @@ impl Clock { /// this is the byte-aligned party length plus the version's own bit /// length. /// + /// Instrument surface, public under the `meter` feature: the resource + /// meters, coverage suites, and boundary pins denominate readings in + /// exact encoded bit lengths. Applications measure wire cost as + /// `encode().len()` — the byte length actually shipped. + /// /// # Complexity /// /// `O(1)`. @@ -801,17 +806,15 @@ impl Clock { /// ``` /// use before::Clock; /// let clock = Clock::seed(); - /// assert_eq!(clock.encode().len(), (clock.encoded_bits() + 1).div_ceil(8)); + /// assert_eq!(clock.encode().len() as u64, (clock.encoded_bits() + 1).div_ceil(8)); /// ``` - pub fn encoded_bits(&self) -> usize { - // u64 width: on a 32-bit target each component's bit length fits - // usize (the storable bound), but the byte-rounded party plus the - // version can exceed it — a clock is two independently bounded - // streams. The conversion is checked, so a composite too long for - // this target's usize fails loudly by name instead of wrapping. - let party = 8 * (self.party().encoded_bits() as u64 + 1).div_ceil(8); - let bits = party + self.version().encoded_bits() as u64; - usize::try_from(bits).expect("the clock's combined bit length fits usize") + #[cfg(any(test, feature = "meter"))] + pub fn encoded_bits(&self) -> u64 { + // `u64` throughout, as both components' lengths are: the sum of two + // allocated streams' bit lengths sits orders of magnitude under any + // `u64` wrap on every target. + let party = 8 * (self.party().encoded_bits() + 1).div_ceil(8); + party + self.version().encoded_bits() } /// Duplicates this clock, producing a second handle to the same clock: an diff --git a/crates/before/src/clock/tests.rs b/crates/before/src/clock/tests.rs index 942814a1..60e744de 100644 --- a/crates/before/src/clock/tests.rs +++ b/crates/before/src/clock/tests.rs @@ -1241,7 +1241,7 @@ fn orbit_population(n: usize) -> Vec { /// Max over each octave `[2^i, 2^(i+1))` of a per-round trajectory (`traj[k - /// 1]` is the reading after round `k`), starting at octave `[4, 8)`: the /// resolution the scenario orbits' bands are pinned at. -fn octave_maxima(traj: &[usize]) -> Vec { +fn octave_maxima(traj: &[u64]) -> Vec { let mut maxima = Vec::new(); let mut hi = 8usize; while hi <= traj.len() { @@ -1297,7 +1297,7 @@ fn fork_join_round_trip_orbit_is_byte_stationary() { fn fork_tick_join_orbit_returns_party_and_grows_gamma() { let mut c = Clock::seed(); let seed_party = c.party().encode(); - for k in 1usize..=512 { + for k in 1u64..=512 { let mut child = c.fork(); child.tick(); c.join(child).expect("a clock's own fork is disjoint"); @@ -1308,7 +1308,7 @@ fn fork_tick_join_orbit_returns_party_and_grows_gamma() { ); assert_eq!( c.version().encoded_bits(), - 7 + 2 * k.ilog2() as usize, + 7 + 2 * u64::from(k.ilog2()), "version bits after round {k}" ); } @@ -1464,7 +1464,7 @@ fn static_orbit_ids_freeze_and_versions_grow_log() { let octaves = octave_maxima(&version_max); assert_eq!(octaves[0], 22, "the transient octave [4, 8)"); for (j, &m) in octaves.iter().enumerate().skip(1) { - let i = j + 3; // octave j ends at round 2^(j + 3) + let i = j as u64 + 3; // octave j ends at round 2^(j + 3) assert_eq!( m, 8 * i - 4, diff --git a/crates/before/src/codec/bits.rs b/crates/before/src/codec/bits.rs index 099510e0..07678376 100644 --- a/crates/before/src/codec/bits.rs +++ b/crates/before/src/codec/bits.rs @@ -85,8 +85,9 @@ use crate::error::Decode; /// through [`ptr_eq`](Self::ptr_eq) — the fast path the identity-law shortcuts /// (`x ∨ x`, `cmp(x, x)`, `distance(x, x)`) dispatch on. Reading is the /// crate-owned [`BitsView`] ([`live`](Self::live)), which exposes exactly the -/// live bits — the padding stays behind the view — at every storable size on -/// every target. +/// live bits — the padding stays behind the view — at every size on every +/// target: a stream is bounded only by allocatable memory, never by a +/// length denomination. #[derive(Clone)] pub struct Bits { /// The canonical marker-padded bytes: the live bits, one `1`, then @@ -113,30 +114,13 @@ impl Bits { /// underpins), then adopts the buffer's allocation whole: /// `Bytes::from(vec)` wraps it in place, no copy. /// - /// # Panics - /// - /// Panics when the sealed buffer's byte count exceeds what `usize` bit - /// positions can denominate — reachable only on targets narrower than - /// 64 bits, from an emission past 512 MiB of output. The build side - /// itself is exact to allocatable memory (its lengths are `u64`), but a - /// stored stream's live length is `usize`-denominated - /// ([`len`](Self::len) and the value types' `encoded_bits`), so a - /// stream past that bound has no in-memory form here and this check - /// keeps the failure at the door — the same bound the decode door - /// ([`from_canonical`](Self::from_canonical)) holds. On 64-bit targets - /// the bound is 2^61 bytes, which no allocator can hand over, so the - /// check is dead there. + /// Exact at every size on every target: lengths and positions are `u64` + /// on both sides of this seam, so an emission is storable whenever its + /// buffer is allocatable — the door imposes no bound of its own. pub(crate) fn freeze(mut buf: BitsBuf) -> Self { seal_padding(&mut buf); - let bytes = buf.into_bytes(); - assert!( - bytes.len() as u128 <= (usize::MAX as u128 + 1) / 8, - "stored streams denominate bit positions in usize: \ - a {}-byte buffer's bits do not fit this target's usize", - bytes.len(), - ); Bits { - bytes: Bytes::from(bytes), + bytes: Bytes::from(buf.into_bytes()), } } @@ -148,23 +132,11 @@ impl Bits { /// `require_marker_padding` accepts. Debug builds assert it; release builds /// trust the validator. /// - /// # Panics - /// - /// Panics when the buffer's bit count exceeds `usize` — reachable only on - /// targets narrower than 64 bits, from 512 MiB of buffer. A stored - /// stream's live length is `usize`-denominated ([`len`](Self::len) and - /// the value types' `encoded_bits`), so a stream past that bound has no - /// in-memory form here: adopting it would make [`len`](Self::len) - /// incorrect, and this check keeps the failure at the door instead. On - /// 64-bit targets the bound is 2^61 bytes, which no allocator can hand - /// over, so the check is dead there. + /// Exact at every size on every target: the stored form denominates its + /// bit positions in `u64` ([`len`](Self::len), [`live`](Self::live)), so + /// any buffer the validator admits is adoptable whole — the door imposes + /// no bound of its own. pub(crate) fn from_canonical(bytes: Bytes) -> Self { - assert!( - bytes.len() as u128 <= (usize::MAX as u128 + 1) / 8, - "stored streams denominate bit positions in usize: \ - a {}-byte buffer's bits do not fit this target's usize", - bytes.len(), - ); let bits = Bits { bytes }; debug_assert!( padding_is_canonical(&bits), @@ -182,10 +154,10 @@ impl Bits { /// exactly what the freeze and decode doors establish. /// /// The arithmetic runs at `u64` width: `bytes.len() * 8` wraps a 32-bit - /// `usize` from 512 MiB of buffer, while the live length itself still - /// fits at exactly that boundary (the marker spends at least one of the - /// buffer's bits). - fn live_bits(&self) -> u64 { + /// `usize` from 512 MiB of buffer, while the length itself stays exactly + /// representable up to allocatable memory — the only bound a stored + /// stream has, on any target. + pub fn len(&self) -> u64 { match self.bytes.last() { None => 0, Some(&last) => { @@ -195,28 +167,16 @@ impl Bits { } } - /// The live bit length of the stream, recovered from the padding: - /// [`live_bits`](Self::live_bits) converted to `usize`. - /// - /// The conversion is checked, not truncating, and cannot fail on a - /// constructed stream: both doors assert the same byte-count bound - /// ([`from_canonical`](Self::from_canonical) on adoption, - /// [`freeze`](Self::freeze) on sealing), under which every live length - /// fits `usize`. - pub fn len(&self) -> usize { - usize::try_from(self.live_bits()).expect("a constructed stream's live length fits usize") - } - /// Read the frozen stream as live bits — the padding stays behind the /// view: every cursor and walk consumes [`Bits`] through this view. /// - /// Exact at every storable size on every target: the view carries the - /// buffer's bytes beside a `u64` live length, so no 32-bit length - /// encoding narrows what a walk can read below what the doors admit. + /// Exact at every size on every target: the view carries the buffer's + /// bytes beside a `u64` live length, so no 32-bit length encoding + /// narrows what a walk can read below what memory can hold. pub fn live(&self) -> BitsView<'_> { BitsView { bytes: &self.bytes, - live: self.live_bits(), + live: self.len(), } } @@ -264,9 +224,9 @@ impl Bits { /// /// The walk surface's one read form. Every semantic walk — comparison sweeps, /// emissions, admission walks, the id operations — consumes stored streams -/// and door buffers through this view, so a walk's reach is exactly the -/// doors' (every storable stream, on every target), never narrowed by a -/// borrowed-view length encoding. +/// and door buffers through this view, so a walk's reach is every stream +/// memory can hold, on every target, never narrowed by a borrowed-view +/// length encoding. /// /// A view always starts on byte 0 of its `bytes`: sub-stream *ranges* travel /// as explicit bit positions beside the view (the copy and parse seams take diff --git a/crates/before/src/codec/buf.rs b/crates/before/src/codec/buf.rs index 94ace6c1..072b37f4 100644 --- a/crates/before/src/codec/buf.rs +++ b/crates/before/src/codec/buf.rs @@ -26,8 +26,9 @@ //! to allocatable memory, and no `usize`-denominated bit count anywhere in //! the build path can wrap or bind on a 32-bit target. (A 32-bit `usize` //! spelling of `bytes.len() * 8` wraps from 512 MiB of buffer — sizes a -//! 4 GiB address space allocates comfortably.) The one storable bound is the -//! frozen form's, checked at its door (`Bits::freeze`), never here. +//! 4 GiB address space allocates comfortably.) The frozen form (`Bits`) +//! carries the same `u64` denomination, so the freeze seam is exact too: +//! allocatable memory is the only bound anywhere on the build path. //! //! Byte *indexes* stay `usize`: an index into an allocated buffer fits the //! target's address width by construction. diff --git a/crates/before/src/codec/cursor.rs b/crates/before/src/codec/cursor.rs index e2e2a33b..8082d482 100644 --- a/crates/before/src/codec/cursor.rs +++ b/crates/before/src/codec/cursor.rs @@ -40,7 +40,10 @@ pub(crate) trait BitCursor { fn read_bit(&mut self) -> Result; /// The position immediately after the last bit read. - fn position(&self) -> usize; + /// + /// `u64`, the stream denomination shared by every cursor: a walked + /// buffer holds more bit positions than a 32-bit `usize` from 512 MiB. + fn position(&self) -> u64; /// Read the unary run at the cursor: the count of `false` bits before — and /// consuming — the terminating `true` bit. @@ -51,8 +54,12 @@ pub(crate) trait BitCursor { /// ([`DsiCursor`](super::DsiCursor)) overrides it to take the run from a /// buffered window. Running out of bits mid-run is the per-bit error, at /// the same position either way. - fn read_unary(&mut self) -> Result { - let mut k = 0usize; + /// + /// `u64`, as every bit count here: every counted zero occupies real + /// input (a buffer bit or a byte the reader yielded), so the count is + /// bounded by memory, far below any `u64` wrap. + fn read_unary(&mut self) -> Result { + let mut k = 0u64; while !self.read_bit()? { k += 1; } @@ -92,13 +99,6 @@ impl<'a> SliceCursor<'a> { pub(crate) fn new(bits: BitsView<'a>, position: u64) -> Self { SliceCursor { bits, position } } - - /// The position immediately after the last bit read, in the cursor's own - /// `u64` denomination: exact even where the walked buffer holds more bit - /// positions than `usize`. - pub(crate) fn position_u64(&self) -> u64 { - self.position - } } impl BitCursor for SliceCursor<'_> { @@ -116,12 +116,8 @@ impl BitCursor for SliceCursor<'_> { Ok(bit) } - fn position(&self) -> usize { - // Exact for every walk that reads positions through the trait: a - // stored stream's live length fits `usize` on every target (the - // storage doors bound it), and the byte decode doors read their end - // positions through `position_u64` instead. - usize::try_from(self.position).expect("a walked position fits usize") + fn position(&self) -> u64 { + self.position } fn read_int(&mut self) -> Result { diff --git a/crates/before/src/codec/dsi.rs b/crates/before/src/codec/dsi.rs index 5a99a320..84b2e56e 100644 --- a/crates/before/src/codec/dsi.rs +++ b/crates/before/src/codec/dsi.rs @@ -53,11 +53,10 @@ pub(crate) struct DsiCursor<'a> { reader: BufBitReader>, /// The position immediately after the last live bit read. /// - /// `u64`, not `usize`: the byte decode doors walk a whole input buffer - /// as bits, and `8 · bytes.len()` can exceed a 32-bit `usize` (a 600 - /// MiB buffer holds 2^32+ bit positions) while remaining exactly - /// representable here. Stored-stream walks never leave `usize` range — - /// the storage doors bound a stream's live length below it. + /// `u64`, not `usize`: every walked buffer — a stored stream's or a + /// byte decode door's — holds `8 · bytes.len()` bit positions, which + /// exceeds a 32-bit `usize` from 512 MiB (a size a 4 GiB address space + /// allocates comfortably) while remaining exactly representable here. position: u64, /// The stream's live bit length, in the same `u64` denomination. len: u64, @@ -99,13 +98,6 @@ impl<'a> DsiCursor<'a> { } } - /// The position immediately after the last bit read, in the cursor's own - /// `u64` denomination: the byte decode doors' end-position read, exact - /// even where the walked buffer holds more bit positions than `usize`. - pub(crate) fn position_u64(&self) -> u64 { - self.position - } - /// Read the unary prefix at the cursor without recording a successful run: /// the count of `0` bits before (and consuming) the terminating `1`. /// @@ -190,26 +182,15 @@ impl BitCursor for DsiCursor<'_> { Ok(bit) } - fn position(&self) -> usize { - // Exact for every walk that reads positions through the trait: a - // stored stream's live length fits `usize` on every target (the - // storage doors bound it), and the byte decode doors read their - // end positions through `position_u64` instead. A byte-door - // walk parking past usize (its truncation reject on a buffer past - // the storable bound) is never followed by a trait position read. - usize::try_from(self.position).expect("a walked position fits usize") + fn position(&self) -> u64 { + self.position } - fn read_unary(&mut self) -> Result { + fn read_unary(&mut self) -> Result { let k = self.unary_raw()?; super::scan::record_bits_u64(k + 1); self.position += k + 1; - // A run at or past usize bits (possible only on a 32-bit target, - // inside a multi-part door buffer past 512 MiB) can only belong to - // a stream past the storable bound — its part could never freeze — - // so the checked conversion fails loudly there instead of handing - // the walk a truncated count. - Ok(usize::try_from(k).expect("a unary run fits usize")) + Ok(k) } /// Read one Elias-gamma-coded integer: accepting and rejecting on exactly @@ -266,11 +247,22 @@ impl BitCursor for DsiCursor<'_> { } // Wide arm: the mantissa's top bit is at position `k`; the next `k` // stream bits fill positions `k - 1 ..= 0`, most-significant first, - // read in machine-word chunks. The bit-index conversion is checked: - // a mantissa at or past usize bits (a 32-bit target, a code deeper - // than the storable bound) fails loudly instead of aliasing a low - // bit index — and the value's backend caps below that width anyway. - let k = usize::try_from(k).expect("a mantissa width fits usize"); + // read in machine-word chunks. A mantissa at or past `usize` bits + // names a value the big-integer backend cannot hold on this target + // (it caps magnitudes below `usize::MAX` bits), so the reject genre + // is the value's, not the machine's — the same genre the per-bit + // loop (`decode_int_from`) reports at the same width. + let Ok(k) = usize::try_from(k) else { + // The examined prefix (its terminating 1 included) records + // before the reject surfaces — exactly the bits the per-bit + // loop's own reads have recorded when it rejects at this + // width — and the cursor parks just past it, where the loop's + // cursor stands at the same reject. + let prefix = k + 1; + super::scan::record_bits_u64(prefix); + self.position += prefix; + return Err(Decode::NotCanonical); + }; let mut m = UBig::ZERO; m.set_bit(k); let mut remaining = k; diff --git a/crates/before/src/codec/dsi/tests.rs b/crates/before/src/codec/dsi/tests.rs index be2b3981..a5c7d9b9 100644 --- a/crates/before/src/codec/dsi/tests.rs +++ b/crates/before/src/codec/dsi/tests.rs @@ -44,7 +44,7 @@ fn gamma_reader_matches_decoder_across_the_word_seam() { .expect("the word-parallel reader reads the same code"); assert_eq!(&got.clone().into_base(), &want, "value diverges at {value}"); assert_eq!( - cursor.position_u64(), + cursor.position(), want_end, "consumed bits diverge at {value}" ); @@ -53,7 +53,7 @@ fn gamma_reader_matches_decoder_across_the_word_seam() { .skip_int() .expect("the skip accepts what the read accepts"); assert_eq!( - skipper.position_u64(), + skipper.position(), want_end, "skip width diverges at {value}" ); @@ -90,7 +90,7 @@ fn skip_int_meters_exactly_the_code_width_read_int_pays() { .read_int() .expect("the reader reads its own encoding"); let read_record = crate::meter::scan_bits(); - let width = reader.position() as u64; + let width = reader.position(); let mut skipper = DsiCursor::new(crate::codec::built_view(&bits)); crate::meter::reset_scan_bits(); skipper @@ -150,7 +150,7 @@ fn truncated_codes_reject_at_every_cut_point() { /// never terminate rejects. #[test] fn unary_reads_match_the_per_bit_loop_across_word_seams() { - for run in [0usize, 1, 7, 8, 31, 32, 33, 63, 64, 65, 200] { + for run in [0u64, 1, 7, 8, 31, 32, 33, 63, 64, 65, 200] { let mut bits = BitsBuf::new(); for _ in 0..run { bits.push(false); @@ -169,7 +169,7 @@ fn unary_reads_match_the_per_bit_loop_across_word_seams() { assert_eq!(slice.read_unary().expect("a terminated run reads"), run); assert_eq!(slice.position(), run + 1); - let unterminated = codec::BitsView::new(bits.as_raw_slice(), run as u64); + let unterminated = codec::BitsView::new(bits.as_raw_slice(), run); let mut cursor = DsiCursor::new(unterminated); assert!( matches!(cursor.read_unary(), Err(Truncated)), @@ -259,7 +259,7 @@ proptest! { } else { let want = slice.read_int().expect("the stream holds the code"); let mut skipper = - DsiCursor::new_at(crate::codec::built_view(&bits), dsi.position_u64()); + DsiCursor::new_at(crate::codec::built_view(&bits), dsi.position()); let got = dsi.read_int().expect("the stream holds the code"); prop_assert_eq!(&got, &want); skipper.skip_int().expect("the skip accepts the code"); diff --git a/crates/before/src/codec/gamma.rs b/crates/before/src/codec/gamma.rs index 580c39f9..99dd5469 100644 --- a/crates/before/src/codec/gamma.rs +++ b/crates/before/src/codec/gamma.rs @@ -116,7 +116,7 @@ pub(crate) fn code_int_small(n: u64) -> Code { pub(crate) fn decode_int(bits: BitsView<'_>, pos: u64) -> Result<(Base, u64), Decode> { let mut cursor = SliceCursor::new(bits, pos); let base = cursor.read_int()?.into_base(); - Ok((base, cursor.position_u64())) + Ok((base, cursor.position())) } /// The number of bits a [`decode_int_window`] window holds. @@ -211,26 +211,16 @@ pub(crate) fn decode_int_from(cursor: &mut C) -> Result, { - let mut k = 0usize; + // `u64`, as every bit count here: each counted zero occupies real input + // (a buffer bit or a byte the reader yielded), so the count is bounded + // by memory, far below any `u64` wrap. + let mut k = 0u64; while !cursor.read_bit()? { - // The match (rather than `ok_or`) keeps the error value — `Decode` has - // drop glue — from being constructed and dropped on every iteration of - // this per-bit loop; see `codec::cursor::Truncated`. - // - // The overflow arm names a value no target can hold: a prefix of - // usize::MAX zeros declares a mantissa past the big-integer - // backend's representable width (the backend caps below usize::MAX - // bits), so the reject genre is the value's, not the machine's. On - // 64-bit targets the arm is dead — reading 2^64 bits first needs - // an unallocatable input. - k = match k.checked_add(1) { - Some(k) => k, - None => return Err(Decode::NotCanonical), - }; + k += 1; } // Common case: read small codes into a machine integer, then widen once. - if k < u64::BITS as usize { + if k < u64::from(u64::BITS) { let mut m = 1u64; for _ in 0..k { m <<= 1; @@ -249,6 +239,16 @@ where // the only allocation is the value itself. A truncated stream still fails // at the same `read_bit` position it would reading into an accumulator, so // the accept/reject boundary is unchanged. + // + // A mantissa at or past `usize` bits names a value the big-integer + // backend cannot hold on this target (it caps magnitudes below + // `usize::MAX` bits), so the reject genre is the value's, not the + // machine's — the word-parallel reader (`DsiCursor::read_int`) rejects + // at the same width with the same genre. On 64-bit targets the arm is + // dead: reading 2^64 prefix bits first needs an unallocatable input. + let Ok(k) = usize::try_from(k) else { + return Err(Decode::NotCanonical); + }; let mut m = UBig::ZERO; m.set_bit(k); for i in (0..k).rev() { diff --git a/crates/before/src/codec/stack.rs b/crates/before/src/codec/stack.rs index 72661ac5..d2c7e3b1 100644 --- a/crates/before/src/codec/stack.rs +++ b/crates/before/src/codec/stack.rs @@ -36,13 +36,12 @@ impl BitStack { /// The stack's height in bits. /// - /// `words.len() * 64` fits `usize` on every target: a walk holds a few - /// bits per open ancestor here, depth is bounded by the walked stream's - /// live bit length, and a storable stream's live length itself fits - /// `usize` — so the height stays below any `usize` wrap, even on 32-bit - /// targets. - pub(crate) fn len(&self) -> usize { - self.words.len() * 64 + self.top_len as usize + /// `u64`, the walks' depth denomination: a stack this deep occupies + /// real memory (its words), so the height is bounded by allocatable + /// memory — past a 32-bit `usize` from 512 MiB of stack, and exactly + /// representable here on every target. + pub(crate) fn len(&self) -> u64 { + self.words.len() as u64 * 64 + u64::from(self.top_len) } /// Push one bit. @@ -105,16 +104,17 @@ impl BitStack { /// The exact run of set bits at the top of the stack. /// /// One word read per 64 bits of the run: the cost is the run the caller is - /// about to pop (or has decided not to), never the whole stack. - pub(crate) fn trailing_ones(&self) -> usize { + /// about to pop (or has decided not to), never the whole stack. `u64`, + /// as [`len`](Self::len): the run is bounded by the stack's own height. + pub(crate) fn trailing_ones(&self) -> u64 { let top_run = self.top.trailing_ones().min(self.top_len); if top_run < self.top_len { - return top_run as usize; + return u64::from(top_run); } - let mut run = top_run as usize; + let mut run = u64::from(top_run); for &word in self.words.iter().rev() { let w = word.trailing_ones(); - run += w as usize; + run += u64::from(w); if w < 64 { break; } diff --git a/crates/before/src/codec/stack/tests.rs b/crates/before/src/codec/stack/tests.rs index 340bfe2b..0ff59d20 100644 --- a/crates/before/src/codec/stack/tests.rs +++ b/crates/before/src/codec/stack/tests.rs @@ -35,7 +35,7 @@ proptest! { prop_assert_eq!(stack.pop(), model.pop()); } prop_assert_eq!(stack.last(), model.last().copied()); - prop_assert_eq!(stack.len(), model.len()); + prop_assert_eq!(stack.len(), model.len() as u64); prop_assert_eq!(stack.all_set(), model.iter().all(|&b| b)); } } diff --git a/crates/before/src/codec/tests.rs b/crates/before/src/codec/tests.rs index 48e9b334..c7e51161 100644 --- a/crates/before/src/codec/tests.rs +++ b/crates/before/src/codec/tests.rs @@ -12,8 +12,8 @@ use proptest::prelude::*; use proptest::test_runner::TestCaseError; use super::{ - bits_buf, decode_int, decode_int_from, encode_int, Base, BitsBuf, BitsView, DsiCursor, - SliceCursor, + bits_buf, decode_int, decode_int_from, encode_int, Base, BitCursor, BitsBuf, BitsView, + DsiCursor, SliceCursor, }; use crate::oracle; use crate::span::Span; @@ -438,7 +438,7 @@ fn assert_decode_matches_bit_loop(bits: BitsView<'_>, pos: u64) -> Result<(), Te match (subject, oracle) { (Ok((value, end)), Ok(oracle_value)) => { prop_assert_eq!(value, oracle_value); - prop_assert_eq!(end, cursor.position_u64()); + prop_assert_eq!(end, cursor.position()); } (Err(s), Err(o)) => { prop_assert_eq!(std::mem::discriminant(&s), std::mem::discriminant(&o)); @@ -457,7 +457,7 @@ fn assert_decode_matches_bit_loop(bits: BitsView<'_>, pos: u64) -> Result<(), Te fn assert_skip_matches_bit_loop(bits: BitsView<'_>, pos: u64) -> Result<(), TestCaseError> { let mut cursor = DsiCursor::new_at(bits, pos); match (cursor.skip_int(), skip_int_bitwise(bits, pos)) { - (Ok(()), Ok(o)) => prop_assert_eq!(cursor.position_u64(), o), + (Ok(()), Ok(o)) => prop_assert_eq!(cursor.position(), o), (Err(_), Err(o)) => { prop_assert!( matches!(o, Decode::Truncated), @@ -1536,7 +1536,7 @@ proptest! { ) { let bytes = v.encode(); prop_assert_eq!( - bytes.len() * 8, + bytes.len() as u64 * 8, v.encoded_bits() + 8, "the padding occupies a whole final byte", ); @@ -1583,7 +1583,7 @@ proptest! { fn flush_cut_party_reads_truncated_at_every_door(p in arb_flush_party()) { let bytes = p.encode(); prop_assert_eq!( - bytes.len() * 8, + bytes.len() as u64 * 8, p.encoded_bits() + 8, "the padding occupies a whole final byte", ); diff --git a/crates/before/src/codec/tree.rs b/crates/before/src/codec/tree.rs index d420cbce..4ea5db86 100644 --- a/crates/before/src/codec/tree.rs +++ b/crates/before/src/codec/tree.rs @@ -35,13 +35,13 @@ enum IdFrame { pub(crate) fn parse_id(bits: BitsView<'_>, pos: u64) -> Result { let mut cursor = super::DsiCursor::new_at(bits, pos); parse_id_core(&mut cursor)?; - Ok(cursor.position_u64()) + Ok(cursor.position()) } /// Parse and validate one id tree from a sequential bit cursor, returning the /// position just past it. #[cfg(all(test, feature = "borsh"))] -pub(crate) fn parse_id_from(cursor: &mut C) -> Result +pub(crate) fn parse_id_from(cursor: &mut C) -> Result where Decode: From, { @@ -52,9 +52,8 @@ where /// Parse and validate one id tree from a sequential bit cursor: the one /// grammar body. /// -/// The end position is left to the caller's own read: the byte decode doors -/// read it at `u64` width, everything else through -/// [`BitCursor::position`]. +/// The end position is left to the caller's own [`BitCursor::position`] +/// read. pub(crate) fn parse_id_core(cursor: &mut C) -> Result<(), Decode> where Decode: From, diff --git a/crates/before/src/laws.rs b/crates/before/src/laws.rs index d88c9527..d20a453a 100644 --- a/crates/before/src/laws.rs +++ b/crates/before/src/laws.rs @@ -424,7 +424,7 @@ laws! { /// `encoded_bits` is the pre-padding bit length of `encode`: the byte length /// is the bit length plus the marker, rounded up to whole bytes. fn version_encoded_bits_matches_encode_len { - a.encode().len() == (a.encoded_bits() + 1).div_ceil(8) + a.encode().len() as u64 == (a.encoded_bits() + 1).div_ceil(8) } } @@ -2159,7 +2159,7 @@ laws! { /// `encoded_bits` is the pre-pad bit length of `encode`. fn party_encoded_bits_matches_encode_len { - p.encode().len() == (p.encoded_bits() + 1).div_ceil(8) + p.encode().len() as u64 == (p.encoded_bits() + 1).div_ceil(8) } } @@ -3102,7 +3102,7 @@ laws! { /// `encoded_bits` is the pre-pad bit length of `encode`, at the clock level /// too. fn clock_encoded_bits_matches_encode_len { - c.encode().len() == (c.encoded_bits() + 1).div_ceil(8) + c.encode().len() as u64 == (c.encoded_bits() + 1).div_ceil(8) } } diff --git a/crates/before/src/meter/board/operand.rs b/crates/before/src/meter/board/operand.rs index 5f371e57..d7580852 100644 --- a/crates/before/src/meter/board/operand.rs +++ b/crates/before/src/meter/board/operand.rs @@ -291,5 +291,7 @@ pub(super) fn radix_units_clock(c: &Clock) -> u64 { /// The packed byte size of a version produced by a measured body. pub(super) fn version_output_bytes(v: &Version) -> usize { - v.encoded_bits().div_ceil(8) + // The measured value's stored buffer is allocated on this host, so its + // byte count fits `usize`. + usize::try_from(v.encoded_bits().div_ceil(8)).expect("an allocated buffer's byte count") } diff --git a/crates/before/src/meter/board/ops.rs b/crates/before/src/meter/board/ops.rs index 7977feb5..bcc1268c 100644 --- a/crates/before/src/meter/board/ops.rs +++ b/crates/before/src/meter/board/ops.rs @@ -1313,7 +1313,7 @@ pub(super) fn ops() -> Vec { let child_bytes = { let bytes = f.parties.as_ref().map(|(a, _)| a.clone())?; let mut probe = decode_party(&bytes); - (probe.fork().encoded_bits() / 8) as u64 + probe.fork().encoded_bits() / 8 }; let floors = Floors { heap: if child_bytes == 0 { @@ -1486,7 +1486,10 @@ pub(super) fn ops() -> Vec { output_is_text: false, }; assert_honest_text("party_from_str input", s.len(), spec.radix_units); - let packed = a.encoded_bits().div_ceil(8); + // The operand's stored buffer is allocated on this host, so + // its byte count fits `usize`. + let packed = usize::try_from(a.encoded_bits().div_ceil(8)) + .expect("an allocated buffer's byte count"); let floors = Floors { heap: heap_materializes(packed), limb: na(NA_LIMB_ID_TREE), @@ -1498,10 +1501,13 @@ pub(super) fn ops() -> Vec { s.len(), floors, |r| { - r.downcast_ref::() + let bits = r + .downcast_ref::() .expect("the parse body yields a party") - .encoded_bits() - .div_ceil(8) + .encoded_bits(); + // An output the body materialized: its byte count + // fits this host's `usize`. + usize::try_from(bits.div_ceil(8)).expect("an allocated buffer's byte count") }, spec, move || s.parse::().expect("a displayed party parses back"), @@ -1783,7 +1789,10 @@ pub(super) fn ops() -> Vec { output_is_text: false, }; assert_honest_text("clock_from_str input", s.len(), spec.radix_units); - let packed = clock.encoded_bits().div_ceil(8); + // The operand's stored buffers are allocated on this host, + // so their byte count fits `usize`. + let packed = usize::try_from(clock.encoded_bits().div_ceil(8)) + .expect("an allocated buffer's byte count"); let floors = Floors { heap: heap_materializes(packed), limb: limb_wide(mandatory_limbs_version(clock.version())), @@ -1795,10 +1804,13 @@ pub(super) fn ops() -> Vec { s.len(), floors, |r| { - r.downcast_ref::() + let bits = r + .downcast_ref::() .expect("the parse body yields a clock") - .encoded_bits() - .div_ceil(8) + .encoded_bits(); + // An output the body materialized: its byte count + // fits this host's `usize`. + usize::try_from(bits.div_ceil(8)).expect("an allocated buffer's byte count") }, spec, move || s.parse::().expect("a displayed clock parses back"), diff --git a/crates/before/src/meter/tests.rs b/crates/before/src/meter/tests.rs index 73171d2e..a5597fec 100644 --- a/crates/before/src/meter/tests.rs +++ b/crates/before/src/meter/tests.rs @@ -42,7 +42,7 @@ fn check_version(p: &Packed, bits: usize) { fn check_party(p: &Packed, bits: usize) -> Party { assert_eq!(p.bits, bits, "closed-form bit length"); let id = Party::decode(&p.bytes[..]).expect("generated shape is strict normal form"); - assert_eq!(id.encoded_bits(), p.bits, "decoded live bit length"); + assert_eq!(id.encoded_bits(), p.bits as u64, "decoded live bit length"); assert_eq!(id.encode(), p.bytes, "byte-identical re-encode"); id } @@ -543,7 +543,7 @@ fn scan_meter_counts_deterministically_and_resets() { a.join(b).expect("the divert arms are disjoint"); let joined = super::scan_bits(); assert!( - joined as usize >= a.encoded_bits(), + joined >= a.encoded_bits(), "a join that wrote {} output bits recorded only {joined} scan bits: \ the id builder is not recording its writes: {ISOLATION_NOTE}", a.encoded_bits(), @@ -651,7 +651,7 @@ fn scattered_id_decodes_canonically_at_predicted_length() { Party::decode(&scattered_id(n / 2).bytes[..]).expect("scattered id is strict normal form"); let projected = (&comb / &party).to_version(); assert!( - projected.encoded_bits() >= (n / 2) * k, + projected.encoded_bits() >= ((n / 2) * k) as u64, "projection through the scattered id must keep a wide magnitude per kept tooth \ (got {} bits from {} teeth of {} bits)", projected.encoded_bits(), @@ -1215,7 +1215,7 @@ fn plateau_puncture_decodes_canonically_at_predicted_length() { // the same bound as an inequality). assert_eq!( plateau_puncture(w, d).version().encoded_bits(), - 128 * w + 198 * d + 2, + (128 * w + 198 * d + 2) as u64, "the stored stream must stay linear in the factors' widths" ); let (x, y) = plateau_puncture_factors(w, d); diff --git a/crates/before/src/meter/tier2/tests.rs b/crates/before/src/meter/tier2/tests.rs index 0420fb10..7ccd2b4b 100644 --- a/crates/before/src/meter/tier2/tests.rs +++ b/crates/before/src/meter/tier2/tests.rs @@ -97,7 +97,7 @@ fn empty_version_is_two_bits() { delta_bits: 0, } ); - assert_eq!(size.total_bits, v.encoded_bits() as u64); + assert_eq!(size.total_bits, v.encoded_bits()); } /// A single ticked leaf (value 1) is one topology bit plus `gamma(1) = 3`, 4 @@ -111,7 +111,7 @@ fn single_small_leaf_matches_current_size() { ))); assert_eq!(size.total_bits, 4); assert_eq!((size.nodes, size.leaves), (1, 1)); - assert_eq!(size.total_bits, v.encoded_bits() as u64); + assert_eq!(size.total_bits, v.encoded_bits()); } /// A single huge leaf `2^b - 1` is one topology bit plus `gamma(2^b - 1) = 2b + @@ -125,7 +125,7 @@ fn single_big_leaf_matches_current_size() { assert_eq!(size.total_bits as usize, 2 * b + 2); assert_eq!((size.nodes, size.leaves), (1, 1)); assert_eq!(size.first_leaf_bits as usize, 2 * b + 1); - assert_eq!(size.total_bits, packed.version().encoded_bits() as u64); + assert_eq!(size.total_bits, packed.version().encoded_bits()); } } @@ -176,7 +176,7 @@ fn dense_spine_matches_hand_computation() { delta_bits: 6, } ); - assert_eq!(size.total_bits, packed.version().encoded_bits() as u64); + assert_eq!(size.total_bits, packed.version().encoded_bits()); } /// The boundary comb's Tier 2 size is exactly `10n + 4k + 2` bits against the diff --git a/crates/before/src/party.rs b/crates/before/src/party.rs index c107dab7..28fc5b97 100644 --- a/crates/before/src/party.rs +++ b/crates/before/src/party.rs @@ -540,6 +540,12 @@ impl Party { /// padding — the marker bit and zero-pad to the byte boundary, so /// `encode().len()` is `(encoded_bits() + 1).div_ceil(8)`. /// + /// Instrument surface, public under the `meter` feature: the resource + /// meters, coverage suites, and boundary pins denominate readings in + /// exact encoded bit lengths. Applications measure wire cost as + /// `encode().len()` or [`as_bytes`](Self::as_bytes)`.len()` — the byte + /// length actually shipped. + /// /// # Complexity /// /// `O(1)`. @@ -550,8 +556,9 @@ impl Party { /// // The seed is a single terminal: a 2-bit presence tag (`00`). /// assert_eq!(before::Party::seed().encoded_bits(), 2); /// ``` - pub fn encoded_bits(&self) -> usize { - // The stored form's O(1) length: exact at every storable size. + #[cfg(any(test, feature = "meter"))] + pub fn encoded_bits(&self) -> u64 { + // The stored form's O(1) length: exact at every size memory holds. self.0.len() } diff --git a/crates/before/src/party/ops/build.rs b/crates/before/src/party/ops/build.rs index 2373f2c0..52b34356 100644 --- a/crates/before/src/party/ops/build.rs +++ b/crates/before/src/party/ops/build.rs @@ -207,7 +207,7 @@ impl IdSkylineBuilder { /// The plateau sequence must be the preorder tiling of one dyadic tree: /// each new depth must be reachable from the last by the forced /// flip-and-descend, which the builder debug-asserts. - pub(super) fn leaf(&mut self, depth: usize, owned: bool) { + pub(super) fn leaf(&mut self, depth: u64, owned: bool) { debug_assert!( self.root.is_none(), "a plateau arrived after the final one: the tiling is complete" @@ -244,7 +244,7 @@ impl IdSkylineBuilder { /// by plateau would close as (its root has a child that is neither /// both-empty nor both-terminal), so the ancestors' presence patches and /// collapses are unchanged. - pub(super) fn subtree(&mut self, depth: usize, src: BitsView<'_>, start: u64, end: u64) { + pub(super) fn subtree(&mut self, depth: u64, src: BitsView<'_>, start: u64, end: u64) { debug_assert!( self.root.is_none(), "a subtree arrived after the final plateau: the tiling is complete" diff --git a/crates/before/src/party/ops/diff.rs b/crates/before/src/party/ops/diff.rs index 7649ebc0..17e9e6fa 100644 --- a/crates/before/src/party/ops/diff.rs +++ b/crates/before/src/party/ops/diff.rs @@ -257,7 +257,10 @@ struct IdLeafCursor<'a> { pending_right: BitsBuf, /// Count of left-branch levels in `path`: zero exactly at the final item /// (the all-right path), so [`done`](Self::done) is `O(1)`. - open_lefts: usize, + /// + /// `u64`, as the path height it counts within: each open left branch + /// is one stored path bit. + open_lefts: u64, /// The current item (meaningless while the cursor is unsettled atop a /// just-entered subtree; every unsettled state is resolved before the sweep /// emits). @@ -386,11 +389,10 @@ impl PlateauCursor for IdLeafCursor<'_> { /// The current item's depth: its interval has width `2^-depth`. /// - /// Depths are `usize` across the walk surface: each open ancestor costs - /// at least one bit of the stored id, whose live length fits `usize` on - /// every target (the storage doors' bound). - fn depth(&self) -> usize { - usize::try_from(self.path.len()).expect("depth is bounded by the id's live length") + /// Depths are `u64` across the walk surface, as every stream position + /// is: each open ancestor costs at least one bit of the stored id. + fn depth(&self) -> u64 { + self.path.len() } /// Whether the current item is the tiling's last (it ends at the @@ -409,7 +411,7 @@ impl PlateauCursor for IdLeafCursor<'_> { /// Never called on a final item: a sweep stops when both cursors are done, /// and the skyline sweep module's bookkeeping (which this cursor inherits) /// shows a final item is never the advanced side before then. - fn step(&mut self) -> (usize, bool) { + fn step(&mut self) -> (u64, bool) { loop { match self.path.pop() { Some(true) => continue, // this ancestor closed with the item diff --git a/crates/before/src/party/tests.rs b/crates/before/src/party/tests.rs index 9ae0a96d..ea6b795e 100644 --- a/crates/before/src/party/tests.rs +++ b/crates/before/src/party/tests.rs @@ -1055,7 +1055,7 @@ proptest! { fn fork_chain_orbit_sizes_are_exactly_affine() { let mut p = Party::seed(); assert_eq!(p.encoded_bits(), 2, "the seed is the 2-bit whole region"); - for k in 1usize..=512 { + for k in 1u64..=512 { let q = p.fork(); assert_eq!(p.encoded_bits(), 2 + 2 * k, "keeper id bits after fork {k}"); assert_eq!(q.encoded_bits(), 2 + 2 * k, "mover id bits after fork {k}"); @@ -1078,7 +1078,7 @@ fn fork_chain_orbit_sizes_are_exactly_affine() { fn fork_fan_orbit_grows_affine_and_unwinds_to_seed() { let mut root = Party::seed(); let mut children = Vec::new(); - for k in 1usize..=512 { + for k in 1u64..=512 { let q = root.fork(); assert_eq!( root.encoded_bits(), @@ -1093,7 +1093,7 @@ fn fork_fan_orbit_grows_affine_and_unwinds_to_seed() { .expect("fan children are disjoint from the root"); assert_eq!( root.encoded_bits(), - 2 + 2 * (511 - i), + 2 + 2 * (511 - i as u64), "root id bits after unwind join {i}" ); } diff --git a/crates/before/src/span/wire.rs b/crates/before/src/span/wire.rs index c72076c5..2c50bd19 100644 --- a/crates/before/src/span/wire.rs +++ b/crates/before/src/span/wire.rs @@ -10,6 +10,7 @@ use std::borrow::Cow; use std::io::{self, Read, Write}; use crate::codec; +use crate::codec::BitCursor; use crate::error::Decode; use crate::version::skyline; use crate::Version; @@ -150,7 +151,7 @@ impl<'a> Span<'a> { let tail = &buf[lo_bytes..]; let mut cursor = codec::DsiCursor::new(codec::BitsView::whole(tail)); let admission = skyline::validate_dominating_from(lo, &mut cursor)?; - let hi_end = cursor.position_u64(); + let hi_end = cursor.position(); codec::require_marker_padding(tail, hi_end)?; if admission == skyline::Admission::Refuted { return Err(Decode::NotCanonical); diff --git a/crates/before/src/version.rs b/crates/before/src/version.rs index 8f16c66f..7d06cacd 100644 --- a/crates/before/src/version.rs +++ b/crates/before/src/version.rs @@ -1082,6 +1082,12 @@ impl Version { /// padding — the marker bit and zero-pad to the byte boundary, so /// `encode().len()` is `(encoded_bits() + 1).div_ceil(8)`. /// + /// Instrument surface, public under the `meter` feature: the resource + /// meters, coverage suites, and boundary pins denominate readings in + /// exact encoded bit lengths. Applications measure wire cost as + /// `encode().len()` or [`as_bytes`](Self::as_bytes)`.len()` — the byte + /// length actually shipped. + /// /// # Complexity /// /// `O(1)`. @@ -1093,7 +1099,8 @@ impl Version { /// // The empty version is a single `0` leaf: a flag bit plus a value bit. /// assert_eq!(Version::new().encoded_bits(), 2); /// ``` - pub fn encoded_bits(&self) -> usize { + #[cfg(any(test, feature = "meter"))] + pub fn encoded_bits(&self) -> u64 { self.0.len() } diff --git a/crates/before/src/version/skyline/admit.rs b/crates/before/src/version/skyline/admit.rs index 54b98bcb..7c91d873 100644 --- a/crates/before/src/version/skyline/admit.rs +++ b/crates/before/src/version/skyline/admit.rs @@ -82,7 +82,10 @@ struct CheckedCursor<'a, C> { /// The count of `false` bits in `path`: zero exactly when the current /// leaf's plateau ends at the unit interval's right edge — the tree is /// whole and the stream's bits end here. - open_lefts: usize, + /// + /// `u64`, as the path height it counts within: each open left branch + /// is one stored path bit. + open_lefts: u64, /// Whether the current leaf's payload code was zero — the collapsible-pair /// check's right-child half. Never read for the first leaf (preorder puts /// it leftmost, so it is no ancestor's right child). @@ -122,11 +125,11 @@ where /// The current leaf's depth: its plateau has width `2^-depth`. /// - /// Depths are `usize` across the walk surface: each open ancestor costs - /// at least one bit of the walked stream, whose live length fits `usize` - /// on every target (the storage doors' bound). - fn depth(&self) -> usize { - usize::try_from(self.path.len()).expect("depth is bounded by the stream's live length") + /// Depths are `u64` across the walk surface, as every stream position + /// is: each open ancestor costs at least one bit of the walked stream, + /// whose live length outgrows a 32-bit `usize` from 512 MiB. + fn depth(&self) -> u64 { + self.path.len() } /// Whether the current leaf completes the tree (see `open_lefts`). @@ -162,7 +165,7 @@ where /// [`close_ancestor`](Self::close_ancestor)'s collapsible-pair check. /// /// Never called on a done cursor; the walk asks first. - fn step(&mut self) -> Result<(usize, Step), Decode> { + fn step(&mut self) -> Result<(u64, Step), Decode> { // The consumed leaf completes one subtree per popped right branch; // `is_leaf`/`zero_delta` describe the completed subtree (the leaf // itself on the first iteration). diff --git a/crates/before/src/version/skyline/build.rs b/crates/before/src/version/skyline/build.rs index 294c5f0e..71735796 100644 --- a/crates/before/src/version/skyline/build.rs +++ b/crates/before/src/version/skyline/build.rs @@ -116,7 +116,7 @@ impl SkylineBuilder { /// must be the preorder tiling of one tree: each new depth must be /// reachable from the last by the forced flip-and-descend, which the /// builder debug-asserts. - pub(super) fn leaf(&mut self, depth: usize, code: Code) { + pub(super) fn leaf(&mut self, depth: u64, code: Code) { debug_assert!(code.len() > 0, "a leaf payload code is never empty"); let Some(held) = self.held.take() else { // The first leaf: the leftmost path, one flag per ancestor. @@ -156,7 +156,7 @@ impl SkylineBuilder { // Close the ancestors the flushed leaf completed: pop the trailing // right-branch levels, retiring their left-sibling records, then flip // the deepest left branch to its right child. - let mut popped_rights = 0usize; + let mut popped_rights = 0u64; loop { match self.path.pop() { Some(true) => { @@ -195,7 +195,7 @@ impl SkylineBuilder { /// The predicate behind /// [`continue_verbatim`](Self::continue_verbatim)'s held-first-leaf /// precondition: the splice extends exactly that leaf. - pub(super) fn held_at(&self, depth: usize) -> bool { + pub(super) fn held_at(&self, depth: u64) -> bool { self.held.is_some() && self.path.len() == depth } @@ -236,10 +236,10 @@ impl SkylineBuilder { src: BitsView<'_>, start: u64, end: u64, - root_depth: usize, - first_rel_depth: usize, - last_rel_depth: usize, - last_code_len: usize, + root_depth: u64, + first_rel_depth: u64, + last_rel_depth: u64, + last_code_len: u64, ) { debug_assert!( first_rel_depth >= 1 && last_rel_depth >= 1, @@ -252,10 +252,10 @@ impl SkylineBuilder { its subtree root enters as a left child" ); debug_assert!( - end - start > last_code_len as u64, + end - start > last_code_len, "the continuation holds at least the last leaf's flag and code" ); - let last_flag = end - last_code_len as u64 - 1; + let last_flag = end - last_code_len - 1; debug_assert!(src.bit(last_flag), "the continuation ends with a leaf"); let held = self .held @@ -341,7 +341,7 @@ impl SkylineBuilder { /// Descend left from the current path to a leaf at `depth`, emitting one /// internal-node flag per level entered. - fn descend_to(&mut self, depth: usize) { + fn descend_to(&mut self, depth: u64) { for _ in self.path.len()..depth { self.out.push_bit(false); self.path.push(false); diff --git a/crates/before/src/version/skyline/build/tests.rs b/crates/before/src/version/skyline/build/tests.rs index 657cdae7..0374d55a 100644 --- a/crates/before/src/version/skyline/build/tests.rs +++ b/crates/before/src/version/skyline/build/tests.rs @@ -20,7 +20,7 @@ fn delta(sign: Sign, magnitude: u64) -> Code { } /// Drive a builder over `(depth, code)` leaves and return the stream. -fn built(leaves: Vec<(usize, Code)>) -> BitsBuf { +fn built(leaves: Vec<(u64, Code)>) -> BitsBuf { let mut builder = SkylineBuilder::with_capacity(64); for (depth, code) in leaves { builder.leaf(depth, code); @@ -111,7 +111,7 @@ fn zero_delta_against_internal_sibling_survives() { /// held code is written exactly once. #[test] fn deep_uniform_collapse_holds_the_wide_code() { - const DEPTH: usize = 8; + const DEPTH: u64 = 8; const WIDE: u64 = u64::MAX >> 1; let mut leaves = vec![(DEPTH, gamma(WIDE)), (DEPTH, delta(Sign::Positive, 0))]; for level in (1..DEPTH).rev() { @@ -178,14 +178,14 @@ fn partial_equality_collapses_only_the_equal_pair() { /// leaves' relative depths and the last code's length — the coordinates the /// splice re-anchors the builder around. fn continuation( - root_depth: usize, - first_depth: usize, - leaves: &[(usize, Code)], -) -> (BitsBuf, usize, usize, usize) { + root_depth: u64, + first_depth: u64, + leaves: &[(u64, Code)], +) -> (BitsBuf, u64, u64, u64) { let mut range = BitsBuf::new(); // The within-subtree path to the previous leaf; the subtree's first leaf is // its leftmost, so the path starts all left branches. - let mut path = vec![false; first_depth - root_depth]; + let mut path = vec![false; (first_depth - root_depth) as usize]; for (depth, code) in leaves { // Close the ancestors the previous leaf completed and flip the // deepest left branch, then descend, emitting one internal flag @@ -197,7 +197,7 @@ fn continuation( } } let rel = depth - root_depth; - let entered = rel - path.len(); + let entered = (rel - path.len() as u64) as usize; range.extend(std::iter::repeat_n(false, entered)); path.extend(std::iter::repeat_n(false, entered)); range.push(true); @@ -215,7 +215,7 @@ fn continuation( range, first_depth - root_depth, last_depth - root_depth, - usize::try_from(last_code.len()).expect("test codes are small"), + last_code.len(), ) } diff --git a/crates/before/src/version/skyline/emit/tests.rs b/crates/before/src/version/skyline/emit/tests.rs index 6dd00f87..c92db717 100644 --- a/crates/before/src/version/skyline/emit/tests.rs +++ b/crates/before/src/version/skyline/emit/tests.rs @@ -145,7 +145,7 @@ fn assert_pointwise(a: BitsView<'_>, b: BitsView<'_>, out: BitsView<'_>, meet: b // level ties (the two-cursor sweeps' rule, which extends to three // streams unchanged). let depth = ca.depth().max(cb.depth()).max(co.depth()); - let mut flip = usize::MAX; + let mut flip = u64::MAX; let (mut so, mut sa, mut sb) = (None, None, None); if co.depth() == depth { let (f, step) = co.step(); diff --git a/crates/before/src/version/skyline/fill.rs b/crates/before/src/version/skyline/fill.rs index 05a9cc6f..54dea4aa 100644 --- a/crates/before/src/version/skyline/fill.rs +++ b/crates/before/src/version/skyline/fill.rs @@ -432,22 +432,10 @@ impl FillWalk<'_> { let mut frames = Frames::new(); // Derived state: always equal to `frames.len()` (the assert below), // carried as a word so the hot loop never recounts a bit stack. - // Width: `usize`, justified per target rather than by the mirror - // alone — the mirrored `frames.len()` is itself a `usize` that - // would wrap in step, the frame stacks' own bit lengths included. - // On 64-bit targets, 2^64 open frames is unreachable outright. On - // 32-bit targets (wasm32), the derivation is the load-bearing - // fact and its margin is thin: every open frame has consumed at - // least 3 live input bits (an id tag and an event flag) and holds - // at least 4 transient bits (the site/phase/aux stacks and the - // route-key delta), so 2^32 open frames demand upwards of 3.5 GiB - // across live input and frame stacks alone — nearly the whole - // 4 GiB address space, before the web, memo, and output are - // counted — and allocation fails loudly before this counter or - // any stack's own bit length (which crosses 2^32 at the same - // instant) can wrap. The pre-scan's counters carry their own - // width contract (the `prescan` module doc). - let mut depth = 0usize; + // `u64`, the walk surface's depth denomination: every open frame + // holds transient bits in real memory, so the count is bounded by + // allocatable memory, far below any `u64` wrap on every target. + let mut depth = 0u64; 'descend: loop { debug_assert_eq!(depth, frames.len(), "one frame per open branch level"); // Descend: resolve the subtree at the cursor to a cost, or suspend @@ -636,7 +624,7 @@ impl FillWalk<'_> { /// The cursor's bit position: the next node's flag. fn pos(&self) -> u64 { - self.cursor.position_u64() + self.cursor.position() } /// Read one topology flag at the cursor (`true` = leaf), recording the @@ -687,7 +675,7 @@ impl FillWalk<'_> { /// Consume the queue-front memoized site: resolve its minimum by one fold /// of its ledger link into the live relation, decide the raise, and emit. - fn consume_site(&mut self, above: &Signed, depth: usize) { + fn consume_site(&mut self, above: &Signed, depth: u64) { debug_assert!( self.memo.cursor < self.memo.queue.len(), "a covered site has a recorded entry" @@ -754,7 +742,7 @@ impl FillWalk<'_> { mut relation: Accumulator, link: Option, above: &Signed, - depth: usize, + depth: u64, ) { // The decision is sign((h + above) − m_s) = sign(relation + above − // link); the link stays folded in, so the accumulator then holds h − @@ -861,7 +849,7 @@ impl FillWalk<'_> { /// past its absolute-coded first leaf ([`Out::Built`]'s contract), so the /// body codes a delta unconditionally — from the registers, never from /// the step itself. - fn emit_step(&mut self, depth: usize) { + fn emit_step(&mut self, depth: u64) { self.web.emit_here(); if self.out.note_match(self.pos()) { self.gap.reset(); @@ -901,7 +889,7 @@ impl FillWalk<'_> { /// absolute: output position ≡ input position while the walk is verbatim, /// so a first leaf compares absolute against absolute). A value-reproducing /// raise is a match, never a divergence. - fn emit_offset(&mut self, depth: usize, offset: Signed) { + fn emit_offset(&mut self, depth: u64, offset: Signed) { self.web.emit_offset(&offset); if self.out.is_verbatim() && self.range_is_leaf && offset.is_zero() { // A value-reproducing emission on a verbatim walk always @@ -965,7 +953,7 @@ impl FillWalk<'_> { /// tracked minimum strictly exceeds `h + above`, and `h + above` is the /// consumed range's maximum — at or above every input plateau the emission /// replaces — so the emitted value moved. - fn emit_at_min(&mut self, depth: usize) { + fn emit_at_min(&mut self, depth: u64) { debug_assert!( !self.out.is_unstarted(), "a tracked minimum implies an emission" @@ -1016,7 +1004,7 @@ impl FillWalk<'_> { /// its own depth, deltas passing straight through (the first through the /// divergence gap); the watermark web absorbs each emission in amortized /// O(1). - fn copy_subtree(&mut self, depth: usize) { + fn copy_subtree(&mut self, depth: u64) { // Three regimes, selected in order: (1) a verbatim walk over a // depth-2+ region block-scans it as one matched prefix extension; // (2) post-divergence a depth-2+ region feeds its first leaf @@ -1224,7 +1212,7 @@ impl Frames { } /// Open branch levels (the walk's current depth). - fn len(&self) -> usize { + fn len(&self) -> u64 { self.site.len() } diff --git a/crates/before/src/version/skyline/fill/fuse.rs b/crates/before/src/version/skyline/fill/fuse.rs index 188bdae4..163df1de 100644 --- a/crates/before/src/version/skyline/fill/fuse.rs +++ b/crates/before/src/version/skyline/fill/fuse.rs @@ -44,7 +44,7 @@ //! that shifts which leaf is first trips on topology (the replaced range was //! not a single leaf) before any code comparison is reached. -use crate::codec::{BitStack, BitsBuf, BitsView, Code, PopStack}; +use crate::codec::{BitCursor, BitStack, BitsBuf, BitsView, Code, PopStack}; use crate::idbits::{IdNode, IdReader}; use super::super::build::SkylineBuilder; @@ -151,7 +151,7 @@ impl Out { /// /// Panics on a verbatim walk — unreachable there: matched emissions /// return before their bodies, and diverging ones materialize first. - pub(super) fn leaf(&mut self, depth: usize, code: Code) { + pub(super) fn leaf(&mut self, depth: u64, code: Code) { match self { Out::Built(builder) => builder.leaf(depth, code), Out::Unstarted | Out::Verbatim { .. } => { @@ -176,10 +176,10 @@ impl Out { src: BitsView<'_>, start: u64, end: u64, - root_depth: usize, - first_rel_depth: usize, - last_rel_depth: usize, - last_code_len: usize, + root_depth: u64, + first_rel_depth: u64, + last_rel_depth: u64, + last_code_len: u64, ) { match self { Out::Built(builder) => builder.continue_verbatim( @@ -217,7 +217,7 @@ impl Out { let mut builder = SkylineBuilder::with_capacity(event.len()); let mut cursor = crate::codec::DsiCursor::new(event); let mut walk = LeafWalk::new(); - while cursor.position_u64() < matched_end { + while cursor.position() < matched_end { // A descent never straddles `matched_end` (a matched prefix ends on // a plateau boundary), and a divergence always leaves its own // consumed range beyond the matched prefix, so the prefix is a @@ -227,12 +227,12 @@ impl Out { let depth = walk .descend(&mut cursor) .expect("a matched prefix is a proper prefix of the tiling"); - let start = cursor.position_u64(); + let start = cursor.position(); cursor.skip_int().expect("canonical skyline bits"); - builder.leaf(depth, Code::from_range(event, start, cursor.position_u64())); + builder.leaf(depth, Code::from_range(event, start, cursor.position())); } debug_assert_eq!( - cursor.position_u64(), + cursor.position(), matched_end, "a matched prefix ends on a plateau boundary" ); diff --git a/crates/before/src/version/skyline/fill/prescan.rs b/crates/before/src/version/skyline/fill/prescan.rs index 045fd6c4..a5a959b1 100644 --- a/crates/before/src/version/skyline/fill/prescan.rs +++ b/crates/before/src/version/skyline/fill/prescan.rs @@ -207,7 +207,7 @@ impl<'a, 'm> PreScan<'a, 'm> { self.web.close(); break; } - let slot = self.reserve(self.cursor.position_u64()); + let slot = self.reserve(self.cursor.position()); frames.push_site(slot); level += 1; self.web.open(1); @@ -227,7 +227,7 @@ impl<'a, 'm> PreScan<'a, 'm> { // Ascend: resume suspended nodes as their children complete. loop { let Some(top) = frames.top() else { - return self.cursor.position_u64(); + return self.cursor.position(); }; self.web.close(); match top { diff --git a/crates/before/src/version/skyline/grow.rs b/crates/before/src/version/skyline/grow.rs index f9898549..13575a44 100644 --- a/crates/before/src/version/skyline/grow.rs +++ b/crates/before/src/version/skyline/grow.rs @@ -240,7 +240,7 @@ impl<'a> EvScan<'a> { /// The cursor's bit position: the next node's flag. fn pos(&self) -> u64 { - self.cursor.position_u64() + self.cursor.position() } /// Move the cursor to `pos` (a node-flag position located by a side scan): @@ -261,9 +261,9 @@ impl<'a> EvScan<'a> { if !leaf { None } else { - let start = self.cursor.position_u64(); + let start = self.cursor.position(); self.cursor.skip_int().expect("canonical skyline bits"); - Some(start..self.cursor.position_u64()) + Some(start..self.cursor.position()) } } @@ -278,7 +278,7 @@ impl<'a> EvScan<'a> { fn skip(&mut self) { // One unary read per descent: each internal node in the run opens two // children, and the terminating leaf closes one. - let mut pending = 1usize; + let mut pending = 1u64; while pending > 0 { let internal_nodes = self.cursor.read_unary().expect("canonical skyline bits"); self.cursor.skip_int().expect("canonical skyline bits"); @@ -323,11 +323,11 @@ struct Subtree { first_code: Range, /// The first leaf's depth below the subtree root; `0` means the /// subtree is a single leaf. - first_rel_depth: usize, + first_rel_depth: u64, /// The last (rightmost) leaf's payload code range. last_code: Range, /// The last leaf's depth below the subtree root. - last_rel_depth: usize, + last_rel_depth: u64, } /// Locate the subtree at `start`: its end, and the first/last leaf coordinates @@ -343,14 +343,14 @@ fn scan_subtree(bits: BitsView<'_>, start: u64) -> Subtree { let mut cursor = codec::DsiCursor::new_at(bits, start); // The first leaf's coordinates, recorded once; the last leaf's are whatever // the loop recorded most recently when the walk ends. - let mut first: Option<(Range, usize)> = None; + let mut first: Option<(Range, u64)> = None; let mut last_code = 0..0; let mut last_rel_depth = 0; let mut walk = LeafWalk::new(); while let Some(depth) = walk.descend(&mut cursor) { - let code_start = cursor.position_u64(); + let code_start = cursor.position(); cursor.skip_int().expect("canonical skyline bits"); - last_code = code_start..cursor.position_u64(); + last_code = code_start..cursor.position(); last_rel_depth = depth; if first.is_none() { first = Some((last_code.clone(), last_rel_depth)); @@ -358,7 +358,7 @@ fn scan_subtree(bits: BitsView<'_>, start: u64) -> Subtree { } let (first_code, first_rel_depth) = first.expect("a subtree has at least one leaf"); Subtree { - end: cursor.position_u64(), + end: cursor.position(), first_code, first_rel_depth, last_code, @@ -372,12 +372,7 @@ fn scan_subtree(bits: BitsView<'_>, start: u64) -> Subtree { /// The first leaf goes through the builder's collapse checks (with the /// successor repair when the grown leaf precedes it); the remainder is one /// verbatim splice. -fn feed_subtree( - out: &mut SkylineBuilder, - event: &mut EvScan<'_>, - depth: usize, - repair: Repair<'_>, -) { +fn feed_subtree(out: &mut SkylineBuilder, event: &mut EvScan<'_>, depth: u64, repair: Repair<'_>) { let subtree = scan_subtree(event.bits, event.pos()); let first_code = match repair { Repair::None => { @@ -401,7 +396,7 @@ fn feed_subtree( depth, subtree.first_rel_depth, subtree.last_rel_depth, - (subtree.last_code.end - subtree.last_code.start) as usize, + subtree.last_code.end - subtree.last_code.start, ); } event.seek(subtree.end); @@ -519,7 +514,9 @@ pub(super) fn emit( // One bit per chosen-path level: `true` = the branch descended left, so its // right sibling subtree is pending after the inflation point. let mut pending = BitsBuf::new(); - let mut depth = 0usize; + // `u64`, the walk surface's depth denomination: each level holds one + // pending bit in real memory. + let mut depth = 0u64; // Whether any leaf has entered the output ahead of the grown leaf. The // grown leaf's own code is absolute exactly when none has (Phase 2's // UpAbsolute/UpDelta selection): the decision is the walk's, not recode's, @@ -617,14 +614,12 @@ pub(super) fn emit( // code (same height, same predecessor) or re-codes it `+k` when the grown // leaf itself comes first. let path_depth = depth; - // Depths are `usize` across the walk surface: a chain level costs at - // least one bit of the stored id, whose live length fits `usize` on - // every target (the storage doors' bound). - let chain = - usize::try_from(chain_dirs.len()).expect("chain depth is bounded by the id's live length"); + // Depths are `u64` across the walk surface, as every stream position + // is: a chain level costs at least one bit of the stored id. + let chain = chain_dirs.len(); let original = original_range; debug_assert_eq!( - path_depth as u64, + path_depth, pending.len(), "one pending record per path level" ); @@ -632,7 +627,7 @@ pub(super) fn emit( // Fresh sibling leaves that precede the grown leaf: one per level whose // branch descended right (the sibling is the left child). for level in 0..chain { - if !chain_dirs.get(level as u64) { + if !chain_dirs.get(level) { let code = if emitted_in_chain { gamma_code(&Base::ZERO) } else { @@ -662,7 +657,7 @@ pub(super) fn emit( // Fresh sibling leaves that follow the grown leaf, deepest first. let mut first_after_grown = true; for level in (0..chain).rev() { - if chain_dirs.get(level as u64) { + if chain_dirs.get(level) { let code = if first_after_grown { gamma_code_signed(Sign::Negative, events) } else { diff --git a/crates/before/src/version/skyline/literal.rs b/crates/before/src/version/skyline/literal.rs index 1002d5b9..9b44e345 100644 --- a/crates/before/src/version/skyline/literal.rs +++ b/crates/before/src/version/skyline/literal.rs @@ -83,7 +83,9 @@ fn scan(bits: BitsView<'_>) -> (BitsBuf, Vec) { let mut cursor = DsiCursor::new(bits); let mut topology = BitsBuf::new(); let mut heights: Vec = Vec::new(); - let mut pending = 1usize; + // `u64`, as the unary runs it accumulates: every counted node costs + // at least one stream bit. + let mut pending = 1u64; while pending > 0 { // One whole descent per unary read: the run's internal nodes, then the // leaf whose flag terminates the run. Each internal node opens two diff --git a/crates/before/src/version/skyline/masked.rs b/crates/before/src/version/skyline/masked.rs index 79f13666..7738cafc 100644 --- a/crates/before/src/version/skyline/masked.rs +++ b/crates/before/src/version/skyline/masked.rs @@ -285,7 +285,7 @@ impl<'a> Walk<'a> { /// The deepest current depth among every cursor slot but `slot`: the block /// consume's bound — a boundary whose flip level exceeds it is crossed by /// `slot`'s cursor alone. - fn others_deepest(&self, slot: usize) -> usize { + fn others_deepest(&self, slot: usize) -> u64 { self.priority() .filter(|&other| other != slot) .map(|other| self.depth(other)) @@ -375,7 +375,7 @@ impl CursorSet for Walk<'_> { /// An absent mask reads zero: one all-owned region over the whole /// interval, which never steps. - fn depth(&self, slot: usize) -> usize { + fn depth(&self, slot: usize) -> u64 { match slot { Self::A => self.a.depth(), Self::A_MASK => self.a_mask.as_ref().map_or(0, PlateauCursor::depth), @@ -391,7 +391,7 @@ impl CursorSet for Walk<'_> { /// The watchers are `diff` always, plus the side's height integrator when /// present. A mask crossing carries no delta — ownership is per-region /// state read between boundaries. - fn step(&mut self, slot: usize) -> usize { + fn step(&mut self, slot: usize) -> u64 { match slot { Self::A => { let (flip, step) = self.a.step(); diff --git a/crates/before/src/version/skyline/overlay.rs b/crates/before/src/version/skyline/overlay.rs index f90f0e75..7720e451 100644 --- a/crates/before/src/version/skyline/overlay.rs +++ b/crates/before/src/version/skyline/overlay.rs @@ -110,7 +110,11 @@ pub(crate) trait PlateauCursor { type Crossing; /// The current plateau's depth: its interval has width `2^-depth`. - fn depth(&self) -> usize; + /// + /// Depths are `u64` across the walk surface, as every stream position + /// is: each open ancestor costs at least one bit of the walked stream, + /// whose live length outgrows a 32-bit `usize` from 512 MiB. + fn depth(&self) -> u64; /// Whether the current plateau is the tiling's last (its interval ends at /// the unit interval's right edge). @@ -124,7 +128,7 @@ pub(crate) trait PlateauCursor { /// `2^-flip`, which is what the law's tie test reads — the deeper side's /// plateau end reaches the shallower side's exactly when `flip <= /// other.depth()` (the module doc's bookkeeping). - fn step(&mut self) -> (usize, Self::Crossing); + fn step(&mut self) -> (u64, Self::Crossing); } /// One crossing the overlay law consumed, tagged with the cursor that crossed @@ -236,11 +240,11 @@ pub(crate) trait CursorSet { /// The slot's current plateau depth: its interval has width `2^-depth`. /// An absent or dropped slot reads zero. - fn depth(&self, slot: usize) -> usize; + fn depth(&self, slot: usize) -> u64; /// Step the slot past its plateau, folding its crossing into the walk's /// own algebra; returns the flip level. - fn step(&mut self, slot: usize) -> usize; + fn step(&mut self, slot: usize) -> u64; } /// Advance an overlay walk of N cursors one boundary — the overlay-advance law @@ -263,7 +267,7 @@ pub(crate) trait CursorSet { /// integrals need. pub(crate) fn advance_set(set: &mut impl CursorSet) { let priority = set.priority(); - let mut deepest: Option<(usize, usize)> = None; + let mut deepest: Option<(usize, u64)> = None; for slot in priority.clone() { let depth = set.depth(slot); // Strict: the first slot in priority order achieving the maximum. @@ -345,7 +349,7 @@ impl<'a> LeafCursor<'a> { /// The path's trailing right-branch run popped and the deepest left branch /// flipped. Zero on a final leaf (the all-right path), where no step /// remains — every real flip level is at least one. - pub(super) fn peek_flip(&self) -> usize { + pub(super) fn peek_flip(&self) -> u64 { self.path.len() - self.path.trailing_ones() } @@ -370,7 +374,7 @@ impl<'a> LeafCursor<'a> { /// notices — truncation, malformation — panic; the rest walk silently with /// an unspecified result (the contract of /// [`causal_cmp`](super::sweep::causal_cmp), stated once there). - pub(super) fn skip_deeper(&mut self, bound: usize, net: &mut Accumulator) { + pub(super) fn skip_deeper(&mut self, bound: u64, net: &mut Accumulator) { while self.peek_flip() > bound { let (_, step) = self.step(); super::signed::fold_signed_int(net, step.sign, &step.magnitude); @@ -408,14 +412,14 @@ impl PlateauCursor for LeafCursor<'_> { type Crossing = Step; /// The current leaf's depth: its plateau has width `2^-depth`. - fn depth(&self) -> usize { + fn depth(&self) -> u64 { self.path.len() } /// Whether the current leaf is the stream's last (its plateau ends at the /// unit interval's right edge). fn done(&self) -> bool { - self.cursor.position_u64() == self.len + self.cursor.position() == self.len } /// Advance past the current leaf to the next: the flip level's depth for @@ -435,7 +439,7 @@ impl PlateauCursor for LeafCursor<'_> { /// called on a final leaf: a sweep stops when both cursors are done, and /// the module doc's bookkeeping shows a final leaf is never the advanced /// side before then. - fn step(&mut self) -> (usize, Step) { + fn step(&mut self) -> (u64, Step) { loop { match self.path.pop() { Some(true) => continue, // this ancestor closed with the leaf @@ -472,7 +476,10 @@ pub(super) struct IdLeafCursor<'a> { /// stream (a clear flag is a synthetic unowned leaf). right_present: BitStack, /// Left-branch levels still open; zero exactly at the final leaf. - lefts: usize, + /// + /// `u64`, as the path height it counts within: each open left branch + /// is one stored path bit. + lefts: u64, /// Whether the current leaf's region is owned. owned: bool, } @@ -552,7 +559,7 @@ impl PlateauCursor for IdLeafCursor<'_> { type Crossing = (); /// The current region's depth: its interval has width `2^-depth`. - fn depth(&self) -> usize { + fn depth(&self) -> u64 { self.path.len() } @@ -573,7 +580,7 @@ impl PlateauCursor for IdLeafCursor<'_> { /// mask-operand contract of [`causal_cmp`](super::masked::causal_cmp), /// stated once there). Never called on a final region (the overlay stops /// when both cursors are done). - fn step(&mut self) -> (usize, ()) { + fn step(&mut self) -> (u64, ()) { loop { match self.path.pop() { Some(true) => { diff --git a/crates/before/src/version/skyline/place.rs b/crates/before/src/version/skyline/place.rs index 4a9bd1ee..e79dcd7c 100644 --- a/crates/before/src/version/skyline/place.rs +++ b/crates/before/src/version/skyline/place.rs @@ -171,7 +171,7 @@ impl<'a> BoundSide<'a> { /// Step this bound past its plateau, folding its crossing into its own /// difference as the `B` operand; returns the flip level. - fn step(&mut self) -> usize { + fn step(&mut self) -> u64 { let (flip, step) = self.cursor.step(); fold(&mut self.diff, Side::B, step.sign, &step.magnitude); flip @@ -514,7 +514,7 @@ impl Cursors<'_> { /// Step one bound slot; a dropped side never steps (its depth reads zero, /// and every flip level is at least one). - fn step_bound(side: &mut Option>) -> usize { + fn step_bound(side: &mut Option>) -> u64 { side.as_mut() .expect("a dropped side reads depth zero and never steps") .step() @@ -536,7 +536,7 @@ impl CursorSet for Cursors<'_> { } /// A dropped side reads zero, like the masked walk's absent mask. - fn depth(&self, slot: usize) -> usize { + fn depth(&self, slot: usize) -> u64 { match slot { Self::PROBE => self.probe.depth(), Self::START => self.start.as_ref().map_or(0, |side| side.cursor.depth()), @@ -548,7 +548,7 @@ impl CursorSet for Cursors<'_> { /// The probe's step folds its crossing into every live difference as the /// `A` operand (the probe is every pair's first operand); a bound's step /// folds into its own difference as the `B` operand. - fn step(&mut self, slot: usize) -> usize { + fn step(&mut self, slot: usize) -> u64 { match slot { Self::PROBE => { let (flip, step) = self.probe.step(); diff --git a/crates/before/src/version/skyline/place/filter.rs b/crates/before/src/version/skyline/place/filter.rs index 3c5a3e57..288bb170 100644 --- a/crates/before/src/version/skyline/place/filter.rs +++ b/crates/before/src/version/skyline/place/filter.rs @@ -260,7 +260,7 @@ impl CursorSet for MemberCursors<'_> { } /// A dropped (satisfied-hole) bound reads zero and never steps. - fn depth(&self, slot: usize) -> usize { + fn depth(&self, slot: usize) -> u64 { match slot { Self::PROBE => self.probe.depth(), _ => self.sides[slot - 1] @@ -276,7 +276,7 @@ impl CursorSet for MemberCursors<'_> { /// Every present side's pair reads every interval: membership never /// settles a pair — a satisfied hole drops its whole side — so presence /// is the only gate. - fn step(&mut self, slot: usize) -> usize { + fn step(&mut self, slot: usize) -> u64 { match slot { Self::PROBE => { let (flip, step) = self.probe.step(); @@ -552,7 +552,7 @@ impl CursorSet for SpanCursors<'_> { } /// A settled probe endpoint or dropped bound reads zero and never steps. - fn depth(&self, slot: usize) -> usize { + fn depth(&self, slot: usize) -> u64 { match slot { Self::HI => { if self.hi_live { @@ -577,7 +577,7 @@ impl CursorSet for SpanCursors<'_> { /// An endpoint's step folds its crossing into its own live pairs as the /// `A` operand; a bound's step folds into both its live pairs as the `B` /// operand (settled pairs advance unread). - fn step(&mut self, slot: usize) -> usize { + fn step(&mut self, slot: usize) -> u64 { match slot { Self::HI => { let (flip, step) = self.hi.step(); diff --git a/crates/before/src/version/skyline/query.rs b/crates/before/src/version/skyline/query.rs index 86b7f993..8e6bf038 100644 --- a/crates/before/src/version/skyline/query.rs +++ b/crates/before/src/version/skyline/query.rs @@ -202,7 +202,7 @@ pub fn rank(bits: BitsView<'_>) -> Rank { let max_depth = max_depth(bits); // Depth counts levels of a stream held in memory, so it always fits the u64 // rank exponent. - let scale = max_depth as u64; + let scale = max_depth; let (mut cursor, first) = LeafCursor::open(bits); // The single-stream instance of the anchored-segment integral: the // integrand is the height itself, opened at the first leaf's absolute (the @@ -210,7 +210,7 @@ pub fn rank(bits: BitsView<'_>) -> Rank { let mut integral = Integrator::new(); integral.open(Sign::Positive, &first); loop { - let weight_shift = (max_depth - cursor.depth()) as u64; + let weight_shift = max_depth - cursor.depth(); integral.interval(weight_shift); if cursor.done() { break; @@ -219,7 +219,7 @@ pub fn rank(bits: BitsView<'_>) -> Rank { fold(&mut integral.live, Side::A, step.sign, &step.magnitude); integral.boundary(int_digits(&step.magnitude)); } - let (sign, numerator) = integral.finish(max_depth as u64); + let (sign, numerator) = integral.finish(max_depth); debug_assert_ne!(sign, Ordering::Less, "heights are nonnegative"); Rank::from_raw(Base::from(numerator), scale) } @@ -334,7 +334,7 @@ fn pair_fold( // Depth counts levels of streams held in memory, so it always fits the u64 // rank exponent. let overlay_depth = max_depth(a_bits).max(max_depth(b_bits)); - let scale = overlay_depth as u64; + let scale = overlay_depth; let OpenedPair { a: mut cursor_a, b: mut cursor_b, @@ -357,7 +357,7 @@ fn pair_fold( integral.open(sign, &Int::from_ubig(opening)); } loop { - let weight_shift = (overlay_depth - cursor_a.depth().max(cursor_b.depth())) as u64; + let weight_shift = overlay_depth - cursor_a.depth().max(cursor_b.depth()); integral.interval(weight_shift); if cursor_a.done() && cursor_b.done() { break; @@ -396,7 +396,7 @@ fn pair_fold( .expect("the advance law steps at least one side per boundary"); integral.boundary(funded); } - let (sign, total) = integral.finish(overlay_depth as u64); + let (sign, total) = integral.finish(overlay_depth); (sign, total, scale) } @@ -633,9 +633,9 @@ fn absolute_height(height: &mut Accumulator) -> Base { /// # Panics /// /// Panics if the stream is not a canonical skyline encoding. -fn max_depth(bits: BitsView<'_>) -> usize { +fn max_depth(bits: BitsView<'_>) -> u64 { let mut cursor = codec::DsiCursor::new(bits); - let mut deepest = 0usize; + let mut deepest = 0u64; let mut walk = LeafWalk::new(); while let Some(depth) = walk.descend(&mut cursor) { deepest = deepest.max(depth); diff --git a/crates/before/src/version/skyline/query/tests.rs b/crates/before/src/version/skyline/query/tests.rs index 35e36f43..29bc1a40 100644 --- a/crates/before/src/version/skyline/query/tests.rs +++ b/crates/before/src/version/skyline/query/tests.rs @@ -760,7 +760,7 @@ proptest! { // a stored size nothing checks: a fold could be charged M(|v|) against // an operand secretly as large as the product itself. prop_assert!( - v.encoded_bits() <= 4 * x.bit_len() + 4 * (y.bit_len() + 1) + 64, + v.encoded_bits() <= (4 * x.bit_len() + 4 * (y.bit_len() + 1) + 64) as u64, "the stored stream must stay linear in the factors' widths: \ {} stored bits against bits(x) = {}, bits(y) = {}", v.encoded_bits(), @@ -1421,7 +1421,7 @@ mod adequacy { /// grows with every block. fn absolute_position_rank(bits: BitsView<'_>) -> Rank { let max_depth = max_depth(bits); - let scale = max_depth as u64; + let scale = max_depth; let (mut cursor, first) = LeafCursor::open(bits); let mut total = Accumulator::new(); let mut live_height = Accumulator::new(); @@ -1430,7 +1430,7 @@ mod adequacy { let mut position = Accumulator::new(); let one = Base::from(1u8); loop { - let weight_shift = (max_depth - cursor.depth()) as u64; + let weight_shift = max_depth - cursor.depth(); if !live_height.is_literally_zero() { total.add_accum_shl(&live_height, weight_shift); } @@ -1458,7 +1458,7 @@ mod adequacy { live_height = Accumulator::new(); } } - total.add_accum_shl(&frozen, max_depth as u64); + total.add_accum_shl(&frozen, max_depth); let (sign, num) = total.sign_magnitude(); debug_assert_ne!(sign, Ordering::Less, "heights are nonnegative"); Rank::from_raw(Base::from(num), scale) @@ -1689,7 +1689,7 @@ mod adequacy { let mut integral = SpanIntegrator::new(); integral.open(&first); loop { - let weight_shift = (max_depth - cursor.depth()) as u64; + let weight_shift = max_depth - cursor.depth(); integral.interval(weight_shift); if cursor.done() { break; @@ -1698,7 +1698,7 @@ mod adequacy { fold(&mut integral.live, Side::A, step.sign, &step.magnitude); integral.boundary(super::super::integral::int_digits(&step.magnitude)); } - integral.finish(max_depth as u64) + integral.finish(max_depth) } /// The distance co-sweep on the span-reading integrator: the shipped pair @@ -1724,7 +1724,7 @@ mod adequacy { integral.open(&Int::from_ubig(opening)); } loop { - let weight_shift = (overlay_depth - ca.depth().max(cb.depth())) as u64; + let weight_shift = overlay_depth - ca.depth().max(cb.depth()); integral.interval(weight_shift); if ca.done() && cb.done() { break; @@ -1751,7 +1751,7 @@ mod adequacy { .unwrap_or(1); integral.boundary(funded); } - integral.finish(overlay_depth as u64) + integral.finish(overlay_depth) } /// One rank tripwire run over `PR(p)`: packed bytes and the touch count @@ -2053,7 +2053,7 @@ mod adequacy { let mut integral = SuffixWalkIntegrator::new(); integral.open(&first); loop { - let weight_shift = (max_depth - cursor.depth()) as u64; + let weight_shift = max_depth - cursor.depth(); integral.interval(weight_shift); if cursor.done() { break; @@ -2062,7 +2062,7 @@ mod adequacy { fold(&mut integral.live, Side::A, step.sign, &step.magnitude); integral.boundary(super::super::integral::int_digits(&step.magnitude)); } - integral.finish(max_depth as u64) + integral.finish(max_depth) } /// The distance co-sweep on the suffix-walk integrator: the shipped pair @@ -2088,7 +2088,7 @@ mod adequacy { integral.open(&Int::from_ubig(opening)); } loop { - let weight_shift = (overlay_depth - ca.depth().max(cb.depth())) as u64; + let weight_shift = overlay_depth - ca.depth().max(cb.depth()); integral.interval(weight_shift); if ca.done() && cb.done() { break; @@ -2115,7 +2115,7 @@ mod adequacy { .unwrap_or(1); integral.boundary(funded); } - integral.finish(overlay_depth as u64) + integral.finish(overlay_depth) } /// One rank tripwire run over `DS(p, p)`: packed bytes and the touch count @@ -2360,7 +2360,7 @@ mod adequacy { let mut integral = Integrator::new(); integral.open(Sign::Positive, &first); loop { - let weight_shift = (max_depth - cursor.depth()) as u64; + let weight_shift = max_depth - cursor.depth(); integral.interval(weight_shift); if cursor.done() { break; @@ -2369,7 +2369,7 @@ mod adequacy { fold(&mut integral.live, Side::A, step.sign, &step.magnitude); integral.boundary(super::super::integral::int_digits(&step.magnitude)); } - per_digit_finish(integral, max_depth as u64) + per_digit_finish(integral, max_depth) } /// One tripwire run over `DS(p, p)`: packed bytes and the limb @@ -2593,7 +2593,7 @@ mod adequacy { let mut integral = Integrator::new(); integral.open(Sign::Positive, &first); loop { - let weight_shift = (max_depth - cursor.depth()) as u64; + let weight_shift = max_depth - cursor.depth(); integral.interval(weight_shift); if cursor.done() { break; @@ -2602,7 +2602,7 @@ mod adequacy { fold(&mut integral.live, Side::A, step.sign, &step.magnitude); integral.boundary(super::super::integral::int_digits(&step.magnitude)); } - schoolbook_finish(integral, max_depth as u64) + schoolbook_finish(integral, max_depth) } /// One schoolbook tripwire run: packed bytes and both counters over diff --git a/crates/before/src/version/skyline/query/web.rs b/crates/before/src/version/skyline/query/web.rs index f1a9ca06..68444967 100644 --- a/crates/before/src/version/skyline/query/web.rs +++ b/crates/before/src/version/skyline/query/web.rs @@ -234,7 +234,7 @@ impl ReignWeb { } /// Open `count` ranges: the internal nodes a descent just entered. - pub(super) fn open(&mut self, count: usize) { + pub(super) fn open(&mut self, count: u64) { self.web.open(count); } diff --git a/crates/before/src/version/skyline/text.rs b/crates/before/src/version/skyline/text.rs index 0928ba83..868e0405 100644 --- a/crates/before/src/version/skyline/text.rs +++ b/crates/before/src/version/skyline/text.rs @@ -307,7 +307,7 @@ pub fn render(bits: BitsView<'_>) -> String { } }; assert_eq!( - cursor.position_u64(), + cursor.position(), bits.len(), "a canonical skyline stream is exactly one tree" ); @@ -401,11 +401,14 @@ pub fn render(bits: BitsView<'_>) -> String { /// entry's length. const ARENA_SEP: char = ';'; -/// A node count or preorder node index as the renderer's `usize` vocabulary: -/// each node costs at least one bit of the rendered stream, whose live length -/// fits `usize` on every target (the storage doors' bound). +/// A node count or preorder node index as the renderer's `usize` vocabulary. +/// +/// Checked, and priced in output bytes: the rendered text prints at least +/// one byte per node, so a node count past `usize` names an output no +/// allocation on this target can hold — failing loudly here is the same +/// abort the output's own allocation would deliver. fn node_index(count: u64) -> usize { - usize::try_from(count).expect("node indexes are bounded by the stream's live length") + usize::try_from(count).expect("node indexes are bounded by the rendered text's length") } /// Render one finalized printed base into the digit arena, keyed by its node's @@ -566,10 +569,7 @@ pub fn parse(text: &str) -> Result { debug_assert_ne!(sign, Ordering::Less, "a path sum of naturals is a natural"); gamma_code(&Base::from(magnitude)) }; - builder.leaf( - usize::try_from(phase.len()).expect("depth is bounded by the parsed text's length"), - code, - ); + builder.leaf(phase.len(), code); delta.sub_magnitude(&base); // the leaf's base exits the path // Close every node the leaf completes. diff --git a/crates/before/src/version/skyline/text/tests.rs b/crates/before/src/version/skyline/text/tests.rs index 23a80176..e0ab6571 100644 --- a/crates/before/src/version/skyline/text/tests.rs +++ b/crates/before/src/version/skyline/text/tests.rs @@ -369,7 +369,7 @@ fn parse_schoolbook(s: &str) -> Result { Ordering::Less => delta.add_wide(&magnitude), Ordering::Equal => {} } - builder.leaf(frames.len(), code); + builder.leaf(frames.len() as u64, code); delta.sub_magnitude(&base); let mut summary = Child { diff --git a/crates/before/src/version/skyline/validate.rs b/crates/before/src/version/skyline/validate.rs index daf43d7e..73efd9db 100644 --- a/crates/before/src/version/skyline/validate.rs +++ b/crates/before/src/version/skyline/validate.rs @@ -41,7 +41,7 @@ use super::signed::{fold_signed_int, unzigzag, Sign}; pub(crate) fn validate_bits(bits: BitsView<'_>) -> Result<(), Decode> { let mut cursor = DsiCursor::new(bits); validate_from(&mut cursor)?; - if cursor.position_u64() != bits.len() { + if cursor.position() != bits.len() { return Err(Decode::TrailingBits); } Ok(()) @@ -57,7 +57,7 @@ pub(crate) fn validate_bits(bits: BitsView<'_>) -> Result<(), Decode> { pub(crate) fn validate_prefix(bits: BitsView<'_>) -> Result { let mut cursor = DsiCursor::new(bits); validate_from(&mut cursor)?; - Ok(cursor.position_u64()) + Ok(cursor.position()) } /// Validate one skyline tree from a sequential bit cursor. diff --git a/crates/before/src/version/skyline/walk.rs b/crates/before/src/version/skyline/walk.rs index 36bd3cd3..a653b3b8 100644 --- a/crates/before/src/version/skyline/walk.rs +++ b/crates/before/src/version/skyline/walk.rs @@ -77,7 +77,7 @@ impl LeafWalk { /// notices — truncation, malformation — panic; the rest walk silently /// with an unspecified result (the contract of /// [`causal_cmp`](super::sweep::causal_cmp), stated once there). - pub(super) fn descend(&mut self, cursor: &mut DsiCursor<'_>) -> Option { + pub(super) fn descend(&mut self, cursor: &mut DsiCursor<'_>) -> Option { if self.started { loop { match self.path.pop() { @@ -212,9 +212,13 @@ pub(super) struct RegionSkip { /// leaf's, itself in the minimum's range). pub(super) min_from_exit: Signed, /// The last leaf's depth below the walked subtree's root. - pub(super) last_depth: usize, - /// The last leaf's payload code length in bits. - pub(super) last_code_len: usize, + /// + /// `u64`, as every depth on the walk surface: each level below the + /// root costs at least one bit of the walked stream. + pub(super) last_depth: u64, + /// The last leaf's payload code length in bits, in the stream's own + /// `u64` denomination. + pub(super) last_code_len: u64, } /// Drive `walk` over the remaining leaves of the subtree at the cursor, folding @@ -243,8 +247,8 @@ pub(super) fn fold_region( first: bool, net: &mut Accumulator, extremum: &mut Extremum, - pending: Option, -) -> Option<(usize, usize)> { + pending: Option, +) -> Option<(u64, u64)> { let mut first = first; let mut last = None; let mut pending = pending; @@ -348,7 +352,7 @@ pub(super) fn skip_leaves( walk: &mut LeafWalk, cursor: &mut DsiCursor<'_>, first: bool, - pending: Option, + pending: Option, ) -> Option { let mut net = Accumulator::new(); let mut min = Extremum::min(Accumulator::new()); diff --git a/crates/before/src/version/skyline/watermark.rs b/crates/before/src/version/skyline/watermark.rs index ca4b1438..b69ea21f 100644 --- a/crates/before/src/version/skyline/watermark.rs +++ b/crates/before/src/version/skyline/watermark.rs @@ -161,7 +161,7 @@ enum Boundary { /// One record of the difference stack. enum Entry

{ /// `count` consecutive ranges whose minima equal the next inner range's. - ZeroRun(usize), + ZeroRun(u64), /// A range whose minimum sits `boundary` below the next-inner one, with /// the payload its client rode on that boundary. Diff { boundary: Boundary, payload: P }, @@ -200,10 +200,10 @@ pub(super) struct MinWeb

{ /// runs compressed; last entry = nearest the innermost. diffs: Vec>, /// Open ranges with no emission yet, all inner of every armed one. - pending: usize, + pending: u64, /// Armed ranges (the difference stack carries `armed − 1` range /// records). - armed: usize, + armed: u64, /// Active followers (module doc), tracking `m − X` (anchor-relative while /// the slot's tag is set). The fill walk installs them; the min-ticks /// fold leaves both slots empty. @@ -275,7 +275,7 @@ impl

MinWeb

{ /// Open `count` ranges: `count` more ranges, each unarmed until the /// next emission. - pub(super) fn open(&mut self, count: usize) { + pub(super) fn open(&mut self, count: u64) { self.pending += count; } @@ -632,7 +632,7 @@ impl

MinWeb

{ fn push_boundary( &mut self, offset: Accumulator, - pending: usize, + pending: u64, payload: impl FnOnce() -> P, mut on_die: impl FnMut(P), ) { @@ -698,7 +698,7 @@ impl

MinWeb

{ // minimum now equals the new innermost one's counts here, and one flush // after the loop pushes the merged run — every escape path below // reaches that flush. - let mut zeros = 0usize; + let mut zeros = 0u64; // Loop invariant: `residue > 0` is always the drop still to apply at // the current stack position. Every arm either kills it (the stopping // range absorbs it, or the stack empties — break), consumes a @@ -864,7 +864,7 @@ impl

MinWeb

{ } /// Push `count` zero-difference ranges, merging with a top run. - fn push_zeros(&mut self, count: usize) { + fn push_zeros(&mut self, count: u64) { if count == 0 { return; } diff --git a/crates/before/src/version/skyline/watermark/tests.rs b/crates/before/src/version/skyline/watermark/tests.rs index 3c7d061f..248dee19 100644 --- a/crates/before/src/version/skyline/watermark/tests.rs +++ b/crates/before/src/version/skyline/watermark/tests.rs @@ -395,7 +395,7 @@ proptest! { web.open(1); web.emit_here(); // the outer range arms at v = 0 web.fold_height(Sign::Positive, &Int::Small(7)); // h = 7 - web.open(n); + web.open(n as u64); web.emit_here(); // all n inner ranges arm at v = 7: one boundary, n − 1 zeros for i in 0..n { if i < n - 1 { diff --git a/crates/before/tests/meter.rs b/crates/before/tests/meter.rs index 216a2432..af09a5cb 100644 --- a/crates/before/tests/meter.rs +++ b/crates/before/tests/meter.rs @@ -2075,7 +2075,9 @@ fn left_spike(depth: usize) -> Version { fn tick_expand_spine_envelope() { let mut v = Version::new(); let party = party_of(&Shape::IdSpine.packed_flagged(ID_DEPTH, false)); - let input = meter::skyline::encode(&v).len() as usize / 8 + party.encoded_bits().div_ceil(8); + // Byte sizes of buffers this test just allocated fit `usize`. + let input = + ((meter::skyline::encode(&v).len() / 8) + party.encoded_bits().div_ceil(8)) as usize; query_metered( "tick_expand_spine", input, @@ -2105,7 +2107,8 @@ fn tick_expand_cross_envelope() { let mut v = version_of(&ev); let party = party_of(&Shape::IdSpine.packed_flagged(ID_DEPTH, false)); let expected = &v | &left_spike(ID_DEPTH); - let input = ev.bytes.len() + party.encoded_bits().div_ceil(8); + // Byte sizes of buffers this test just allocated fit `usize`. + let input = ev.bytes.len() + party.encoded_bits().div_ceil(8) as usize; query_metered( "tick_expand_cross", input, diff --git a/crates/before/wasm32-pins/guest/Cargo.toml b/crates/before/wasm32-pins/guest/Cargo.toml index 3ab15cb5..fa5ef7e9 100644 --- a/crates/before/wasm32-pins/guest/Cargo.toml +++ b/crates/before/wasm32-pins/guest/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["cdylib"] # The borsh feature lights the composite streaming doors # (`Ranked`/`Span::deserialize_reader`), whose walk boundaries the guest # pins beside the byte doors'; the borsh crate itself names the trait the -# exports drive. -before = { path = "../..", features = ["borsh"] } +# exports drive. The meter feature lights the instrument surface: the +# exact-bit-length observation (`encoded_bits`) the pins report through. +before = { path = "../..", features = ["borsh", "meter"] } borsh = "1" diff --git a/tools/mutantcheck-expected.json b/tools/mutantcheck-expected.json index 8b038cc3..3e8a868d 100644 --- a/tools/mutantcheck-expected.json +++ b/tools/mutantcheck-expected.json @@ -25,7 +25,7 @@ "listed": 1, "suppressed": 1 }, - "grow\\.rs:468:38: replace < with <= in recode": { + "grow\\.rs:463:38: replace < with <= in recode": { "listed": 1, "suppressed": 1 }, From c75d5022cf968e18b5db73793f3b4803b223ac4e Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 19 Aug 2026 10:55:05 -0400 Subject: [PATCH 17/20] wasm32-pins: pin the memory bound as the only terminal past every coordinate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the doors and walks u64 end to end, the pins hold the new upward reach and name what remains. The decode, comparison, emit, and rank surfaces cross the 2^29-byte coordinate — where a 32-bit usize runs out of bit positions — green: a 512 MiB + 1 byte version decodes with its exact bit length and compares both ways, a join emits a finished stream past 2^32 live bits, and a 768 MiB version decodes deep in the memory-bounded range. A new ladder synthesis and export drive the rank fold on numerators wider than any 32-bit quantity (2684354560 bits, green), reachable only through depth weighting, never through a door. Where the 4 GiB address space runs out, the terminal is pinned as found: allocation-failure aborts — loud, never a silent wrong value — on the ~1 GiB decode, the past-coordinate composite key, and the rank fold at the big-integer backend's capacity coordinate. Probe backtraces attribute each to accumulator buffer growth, so the backend's word cap (2^32 - 32 bits) is unreachable through the version doors and the rank fold on this target: a W-bit value or numerator keeps at least 2W bits of stream alive underneath it, and that working set exhausts memory just below the capacity. The rank wire door, which assembles its numerator from bytes with no fold transients, is where the capacity is reachable and stays pinned. A leaner working set — not a wider denomination — is what would move these terminals outward. --- crates/before/wasm32-pins/guest/src/lib.rs | 105 +++++++++- .../before/wasm32-pins/harness/tests/pins.rs | 184 ++++++++++++++++-- justfile | 14 +- 3 files changed, 270 insertions(+), 33 deletions(-) diff --git a/crates/before/wasm32-pins/guest/src/lib.rs b/crates/before/wasm32-pins/guest/src/lib.rs index aa09e505..6f7c7b53 100644 --- a/crates/before/wasm32-pins/guest/src/lib.rs +++ b/crates/before/wasm32-pins/guest/src/lib.rs @@ -92,10 +92,12 @@ pub extern "C" fn pin_version_small() -> i64 { /// `encoded_bits` (always `8n - 8` for the synthesized layout), checking /// that the stored bytes round-trip the input exactly. /// -/// The harness aims this at the 32-bit boundaries: the sizes straddling +/// The harness aims this at the 32-bit coordinates: the sizes straddling /// 2^29 bits (where a `usize >> 3` bit-count encoding would bind on a -/// 32-bit target) and the 512 MiB bit-length boundary of `usize` -/// position arithmetic. +/// 32-bit target), the 2^29-byte coordinate where a `usize` spelling of +/// bit positions would wrap, and — through the synthesized single wide +/// leaf — the sizes where the decode's working set meets the 4 GiB +/// address space, the doors' one terminal on this target. #[no_mangle] pub extern "C" fn pin_version_decode(n_bytes: u64) -> i64 { let n = match usize::try_from(n_bytes) { @@ -334,8 +336,8 @@ fn synth_ranked(n: usize) -> (Vec, usize) { /// /// The door re-derives the version's rank to verify the key's rank /// component — a whole-stream fold over the version's stored view — so this -/// export observes the composite door's walk surface at sizes the byte -/// doors admit (up to 512 MiB per stream). +/// export observes the composite door's walk surface at any size memory +/// admits. #[no_mangle] pub extern "C" fn pin_ranked_decode(n_bytes: u64) -> i64 { let n = match usize::try_from(n_bytes) { @@ -387,7 +389,7 @@ pub extern "C" fn pin_ranked_borsh(n_bytes: u64) -> i64 { /// The door validates the second stream against the first component's /// stored view in one fused admission walk, so this export observes that /// walk on the `lo` component; the endpoint checks are byte compares, -/// exact at any storable size. +/// exact at any size memory admits. #[no_mangle] pub extern "C" fn pin_span_borsh(n_bytes: u64) -> i64 { let n = match usize::try_from(n_bytes) { @@ -452,7 +454,7 @@ pub extern "C" fn pin_version_cmp(n_bytes: u64) -> i64 { /// merge emission, so this export observes the emitting operation class: /// the walk surface on its input side, and the build buffer on its output /// side (a covered join's output reproduces the big operand whole); the -/// result check is a byte compare, exact at any storable size. +/// result check is a byte compare, exact at any size memory admits. #[no_mangle] pub extern "C" fn pin_version_join_covering(n_bytes: u64) -> i64 { let n = match usize::try_from(n_bytes) { @@ -478,13 +480,12 @@ pub extern "C" fn pin_version_join_covering(n_bytes: u64) -> i64 { /// [`synth_two_leaf_right`]`(k, j)`, returning the joined stream's exact /// live bit length, `2k + 2j + 5`. /// -/// Each operand sits well under the walk surface's per-operand bound. /// The join is `node(leaf(2^k - 1), leaf(2^k - 1 + 2^(j-1)))`: each half of /// the unit interval takes its taller side, so the output concatenates the /// left operand's tall code with a fresh `2j + 1`-bit delta code instead of /// collapsing — the output outgrows both inputs. The harness aims `(k, j)` /// at the emitter's output side — the build buffer and its freeze hand-off, -/// not any per-operand bound, are what this export observes. +/// not the operands, are what this export observes. #[no_mangle] pub extern "C" fn pin_version_join_emit(k: u64, j: u64) -> i64 { let a = match Version::decode(&synth_two_leaf_left(k)[..]) { @@ -497,12 +498,96 @@ pub extern "C" fn pin_version_join_emit(k: u64, j: u64) -> i64 { }; let joined = a.join(&b); let expected = 2 * k + 2 * j + 5; - if joined.encoded_bits() as u64 != expected { + if joined.encoded_bits() != expected { return -3; } i64::try_from(expected).unwrap_or(-4) } +/// The canonical encoding of a depth-`d` right-descending ladder whose +/// first leaf carries the height `2^(b-1)`: the shape whose rank numerator +/// is wider than any value its decode materializes. +/// +/// The tree is `node(leaf(h), node(leaf(h+1), node(leaf(h+2), ... , +/// node(leaf(h+d-1), leaf(h+d))...)))` with `h = 2^(b-1)`: the root's left +/// leaf at depth 1 carries `h` as the stream's absolute first payload (a +/// `2b - 1`-bit gamma code), and every later leaf is one `+1` delta (the +/// 3-bit zigzag-gamma code `011`), descending one level per step to the +/// sibling pair at depth `d`. Canonical: every delta is nonzero and every +/// height a natural. Live length `2b + 5d` bits. +/// +/// The point of the shape: decode materializes only the `b`-bit first +/// height (every delta is machine-word small), while the rank fold weights +/// each leaf by `2^(d - depth)`, so the rank numerator is exactly `b + d` +/// bits wide — depth converts into numerator width that no decoded value +/// ever had, which is what lets the harness aim `b + d` at the fold's own +/// coordinates independently of the doors'. +fn synth_rank_ladder(b: u64, d: u64) -> Vec { + assert!( + b >= 3 && d >= 1, + "the ladder wants a wide height and a level" + ); + let live = 2 * b + 5 * d; + let total_bytes = usize::try_from((live + 1).div_ceil(8)).expect("the stream is addressable"); + let mut bytes = vec![0u8; total_bytes]; + // Root '0' at position 0; the first leaf's flag, then gamma(2^(b-1)): + // b - 1 zeros, then the b-bit mantissa `2^(b-1) + 1` (its ends set). + set_bit(&mut bytes, 1); + set_bit(&mut bytes, b + 1); // the mantissa's leading 1 + set_bit(&mut bytes, 2 * b); // its trailing 1 + let mut p = 2 * b + 1; + // Interior levels: an internal flag, a leaf flag, and the +1 delta + // (zigzag 2, gamma code `011`). + for _ in 1..d { + set_bit(&mut bytes, p + 1); // the leaf flag (after the internal '0') + set_bit(&mut bytes, p + 3); // gamma(2): '0', then '11' + set_bit(&mut bytes, p + 4); + p += 5; + } + // The deepest pair's right leaf: flag and one more +1 delta. + set_bit(&mut bytes, p); // the leaf flag (no internal precedes a right child) + set_bit(&mut bytes, p + 2); + set_bit(&mut bytes, p + 3); + p += 4; + debug_assert_eq!(p, live); + set_bit(&mut bytes, live); // the padding marker + bytes +} + +/// Decode a depth-`d` ladder whose first height is `2^(b-1)` +/// ([`synth_rank_ladder`]), then fold its rank, checking exact-order +/// observations: the rank exceeds the rank of the version `1` and equals +/// its own clone. +/// +/// The harness aims `(b, d)` at the rank fold's numerator width, `b + d` +/// bits: the one quantity on the fold path that outgrows every decoded +/// value (decode materializes `b` bits at most). ~`b/4` bytes of input is +/// the smallest honest trigger: a numerator is at most its widest height +/// times `2^depth`, heights pay their own width in code bits, and depth +/// pays five stream bits per level. +#[no_mangle] +pub extern "C" fn pin_version_rank(b: u64, d: u64) -> i64 { + let bytes = synth_rank_ladder(b, d); + let v = match Version::decode(&bytes[..]) { + Ok(v) => v, + Err(_) => return -1, + }; + drop(bytes); + let r = v.rank(); + drop(v); + let one = match Version::try_from(1) { + Ok(v) => v.rank(), + Err(_) => return -2, + }; + if r <= one { + return -3; + } + if r != r.clone() { + return -4; + } + 0 +} + /// The small second operand of the rank-arithmetic pins, keyed by its /// exponent: `0` is the integral rank `1`, `1` is the rank `1/2`, and any /// larger value is [`synth_rank`]'s fraction at that depth. diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs index c8986f64..b5eea968 100644 --- a/crates/before/wasm32-pins/harness/tests/pins.rs +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -78,25 +78,80 @@ fn version_decode_past_build_cap() { ); } -/// A valid 512 MiB (2^29-byte) version encoding — the largest stored -/// stream a 32-bit `usize` can denominate bit positions for — decodes -/// correctly on wasm32 with its exact live bit length. +/// A valid 512 MiB (2^29-byte) version encoding decodes correctly on +/// wasm32 with its exact live bit length. /// /// The returned length is 2^32 - 8, within one marker byte of /// `usize::MAX`. The size is where a `usize` spelling of the stored /// length arithmetic (`bytes.len() * 8`) wraps a 32-bit target — a wrap /// coincidentally correct at exactly 2^29 bytes and short by 2^32 for -/// anything larger — so this pin holds `Bits::len`'s wider arithmetic -/// exact at the boundary, under a build whose overflow checks would -/// surface any wrap as a trap. +/// anything larger — so this pin holds the stored form's `u64` length +/// arithmetic exact at the coordinate, under a build whose overflow +/// checks would surface any wrap as a trap. #[test] -fn version_decode_at_bit_length_boundary() { +fn version_decode_at_usize_positions_coordinate() { assert_eq!( call1("pin_version_decode", 536_870_912), Outcome::Value(8 * 536_870_912 - 8), ); } +/// A valid 536870913-byte version encoding — one byte past the 2^29-byte +/// coordinate where a 32-bit `usize` runs out of bit positions — decodes +/// correctly on wasm32 with its exact live bit length. +/// +/// Streams are bounded only by allocatable memory: the stored form, the +/// doors, and the walks denominate bit positions in `u64` on every +/// target, so no size below memory has a structural cap to trip. This +/// pin and the deep witnesses below hold that upward exactness. +#[test] +fn version_decode_past_usize_positions_coordinate() { + assert_eq!( + call1("pin_version_decode", 536_870_913), + Outcome::Value(8 * 536_870_913 - 8), + ); +} + +/// A valid 768 MiB version encoding — its single leaf's height a +/// ~3.2-gigabit value, half again as wide as any 32-bit position quantity +/// — decodes correctly on wasm32 with its exact live bit length. +/// +/// The decode doors' deep upward witness: input, materialized height, and +/// the validator's running-height fold are priced only by memory, deep +/// past every 32-bit position coordinate. +#[test] +fn version_decode_deep_in_memory_bounded_range() { + assert_eq!( + call1("pin_version_decode", 805_306_368), + Outcome::Value(8 * 805_306_368 - 8), + ); +} + +/// PINNED AS FOUND: a valid ~1 GiB (1073741817-byte) version encoding +/// aborts on allocation failure — the doors' one terminal here. +/// +/// The memory bound fires as a loud abort, never a silent wrong value. +/// The working set at the abort (the input, its read copy, the ~512 MiB +/// materialized height, and the validator's running-height accumulator) +/// crosses what the 4 GiB address space allocates; the probe backtrace +/// attributes the trap to the accumulator's buffer growth inside the +/// height fold. This size's height is one flush nibble under the +/// big-integer backend's 2^32 - 32-bit capacity, so the capacity itself +/// is unreachable through the doors on this target: a wide value's gamma +/// code alone costs a quarter of the address space, and the decode's +/// working set exhausts memory first. (The rank wire door reaches the +/// same capacity with no fold transients, where +/// `rank_decode_past_backend_bit_capacity_traps` pins it.) A leaner +/// working set — not a wider denomination — is what would move this +/// terminal outward. +#[test] +fn version_decode_memory_terminal_traps() { + assert_eq!( + call1("pin_version_decode", 1_073_741_817), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + /// A valid rank whose fraction is 2^32 - 32 expansion bits deep decodes /// correctly, and the decoded value sits strictly between zero and one. /// @@ -238,6 +293,28 @@ fn ranked_decode_deep_in_storable_range() { assert_eq!(call1("pin_ranked_decode", 268_435_456), Outcome::Value(0),); } +/// PINNED AS FOUND: a valid composite key whose version component is one +/// byte past the 2^29-byte coordinate aborts on allocation failure in the +/// byte door `Ranked::decode`. +/// +/// The component is 536870913 bytes, one past where a 32-bit `usize` +/// runs out of bit positions. No denomination binds here: the door's own straddle pins hold it exact +/// across the 2^29-bit coordinate, and the version door crosses this very +/// coordinate green (`version_decode_past_usize_positions_coordinate`). +/// What fires is the memory bound: the composite's working set — the key, +/// its read copy, and the rank re-derivation's fold and ~2^31-bit +/// numerator — crosses what the 4 GiB address space allocates, and the +/// probe backtrace attributes the trap to the fold accumulator's buffer +/// growth inside the re-derivation. A leaner working set — not a wider +/// denomination — is what would flip this pin to `Value(0)`. +#[test] +fn ranked_decode_memory_terminal_traps() { + assert_eq!( + call1("pin_ranked_decode", 536_870_913), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} + /// A valid composite key decodes through the borsh door /// `Ranked::deserialize_reader` at the largest version size below the /// straddle coordinate, consuming exactly its own bytes. @@ -294,8 +371,8 @@ fn ranked_borsh_deep_in_storable_range() { /// The door consumes exactly its own bytes. /// It validates the second stream against `lo`'s view in one fused /// admission walk: the boundary straddle's lower witness. (The byte door -/// `Span::decode` runs the same admission and is exact to 512 MiB per -/// component.) +/// `Span::decode` runs the same admission, exact at every size memory +/// admits.) #[test] fn span_borsh_below_build_cap() { assert_eq!(call1("pin_span_borsh", BUILD_CAP_BYTES), Outcome::Value(0)); @@ -371,18 +448,29 @@ fn version_cmp_past_build_cap() { ); } -/// Causal comparison decides a valid stored pair exactly at 512 MiB — the -/// largest storable stream on a 32-bit target — both ways around. +/// Causal comparison decides a valid stored pair exactly at 512 MiB both +/// ways around. /// -/// The comparison class's upward exactness spot-check at the storage -/// bound itself: the operand's live length (2^32 - 8 bits) is within one -/// marker byte of `usize::MAX`, so this also exercises the view's `u64` -/// length arithmetic at the very top of the storable range. +/// The comparison class's spot-check at the 2^29-byte coordinate where a +/// 32-bit `usize` runs out of bit positions: the operand's live length +/// (2^32 - 8 bits) is within one marker byte of `usize::MAX`, so this +/// exercises the view's `u64` length arithmetic right at the coordinate. #[test] -fn version_cmp_at_storage_bound() { +fn version_cmp_at_usize_positions_coordinate() { assert_eq!(call1("pin_version_cmp", 536_870_912), Outcome::Value(0),); } +/// Causal comparison decides a valid stored 536870913-byte pair — one +/// byte past the 2^29-byte coordinate — exactly, both ways around. +/// +/// The comparison class's upward witness past the coordinate: stored +/// streams and their walks are bounded only by allocatable memory, so +/// ordering reads stay exact wherever the doors can admit an operand. +#[test] +fn version_cmp_past_usize_positions_coordinate() { + assert_eq!(call1("pin_version_cmp", 536_870_913), Outcome::Value(0),); +} + /// Join emits a covered pair exactly at the largest operand size below the /// straddle coordinate: the taller lone leaf joined with a short one /// reproduces the taller, byte for byte. @@ -462,6 +550,23 @@ fn version_join_emit_at_build_cap() { ); } +/// A join of two valid operands (~25 MB and ~488 MB) emits an output of +/// 4294967299 live bits — 536870913 finished bytes, one byte past the +/// 2^29-byte coordinate where a 32-bit `usize` runs out of bit positions. +/// +/// The emitting class's upward witness past the coordinate: the build +/// buffer, the freeze seam, and the frozen form all carry `u64` bit +/// counts, so an emission is storable whenever its buffer is allocatable — +/// the output's live length itself exceeds 2^32 here, past any `usize` +/// spelling on this target. +#[test] +fn version_join_emit_past_usize_positions_coordinate() { + assert_eq!( + call2("pin_version_join_emit", 100_000_000, 2_047_483_647), + Outcome::Value(4_294_967_299), + ); +} + /// Rank addition is exact just below the 32-bit alignment-gap boundary. /// /// A fraction 2^32 expansion bits deep plus one 128 bits deep — an @@ -535,3 +640,50 @@ fn rank_checked_sub_at_gap_boundary_traps() { Outcome::Trapped(Trap::UnreachableCodeReached), ); } + +/// The rank fold is exact on a numerator wider than any 32-bit quantity, +/// ordering strictly above the rank of the version `1` and equal to its +/// own clone. +/// +/// The ladder version's numerator is exactly 2684354560 bits — five +/// quarters of 2^31, past every `usize` and `u32` coordinate on this +/// target. The fold path's deep upward witness: the ~640 MiB input decodes with +/// only the first height materialized (2684354496 bits), and the fold's +/// depth-weighted numerator crosses 2^31 bits without meeting any +/// denomination — only memory prices it. +#[test] +fn version_rank_deep_in_memory_bounded_range() { + assert_eq!( + call2("pin_version_rank", 2_684_354_496, 64), + Outcome::Value(0), + ); +} + +/// PINNED AS FOUND: folding the rank of a ladder version whose numerator +/// is 2^32 - 32 bits aborts on allocation failure — the fold's one +/// terminal here. +/// +/// The numerator sits exactly at the big-integer backend's 32-bit +/// capacity, and the memory bound fires first, as a loud abort, never a +/// silent wrong value. The fold's working set at the abort (the ~1 GiB stream, the +/// 2^32 - 96-bit first height, the integral's base component, and the +/// close's shifted-add target) crosses what the 4 GiB address space +/// allocates; the probe backtrace attributes the trap to the +/// accumulator's buffer growth inside the integral's close. The backend +/// capacity itself is therefore unreachable through the fold on this +/// target: a numerator of `W` bits needs a stream of at least `2W` bits +/// alive underneath it (heights pay their own width in code bits, depth +/// pays five stream bits per level), and that plus the fold's transients +/// exhausts memory just below the capacity — the rank wire door, which +/// assembles its numerator from bytes with no fold transients, is where +/// the capacity is reachable and pinned +/// (`rank_decode_past_backend_bit_capacity_traps`). A leaner working +/// set — not a wider denomination — is what would move this terminal +/// outward. +#[test] +fn version_rank_memory_terminal_traps() { + assert_eq!( + call2("pin_version_rank", (1u64 << 32) - 96, 64), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} diff --git a/justfile b/justfile index b6419142..7ab401de 100644 --- a/justfile +++ b/justfile @@ -590,13 +590,13 @@ fuzzfit-calibrate: fuzzfit-build # the tree's one place 32-bit code *executes*: the guest drives the # public surface — the byte and borsh decode doors, the semantic walks # and emitters, and rank arithmetic — at the exact sizes where 32-bit -# position arithmetic has boundaries (bitvec's borrowed-view cap on the -# walk surface, the emitter's build-buffer cap, the usize bit-length -# boundary at 512 MiB, the rank exponent's usize seam, the big-integer -# backend's word cap, the rank alignment-gap seam), and the harness pins -# each boundary's exact outcome, with adjacency witnesses beside each -# boundary so a failure is attributable to its seam. Pins land red-first -# when a seam is found; each pin's history lives in git. +# arithmetic has coordinates (the 2^29-bit and 2^29-byte marks where a +# usize spelling of bit counts or positions would bind, the big-integer +# backend's word cap on decoded values and on the rank fold's numerator, +# the rank exponent's usize seam, the rank alignment-gap seam), and the +# harness pins each coordinate's exact outcome, with adjacency witnesses +# beside each boundary so a failure is attributable to its seam. Pins +# land red-first when a seam is found; each pin's history lives in git. # The guest builds with overflow checks on, # so a 32-bit wrap is an observable trap, never a silently wrong value. From 8da5792062b18be1e0fd164c1604a3473da2d640 Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 19 Aug 2026 10:55:05 -0400 Subject: [PATCH 18/20] suanpan: stream limbs past the backend and read totals back out as limbs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Magnitude seam lends a whole UBig, and on a 32-bit target a UBig magnitude caps out near usize::MAX bits — so operands and totals wider than the backend can hold had no way in or out of the accumulator. Two entry points and one readout close that: add_limbs_shl/sub_limbs_shl stream an operand as little-endian 64-bit limbs at any shift (the same apply_limbs kernel and cost model as the wide entries, pinned touch-identical by limb_stream_matches_the_wide_entry), and sign_limbs reads the held value back as minimal limbs through the same carry pass as sign_magnitude (read_magnitude's digit collection is now the shared read_digits core). reserve_digits pre-sizes the digit buffer so a caller folding operands of known final scale pays one exact allocation instead of a growth-doubling transient — near a 32-bit target's memory ceiling that doubling is the difference between fitting and failing. The claims roster and crate-page table gain the new rows; the differential streams gain limb-stream and reservation operations; the metered suite pins the streaming entries' exact touch counts and sign_limbs' carry pass; witnesses pin the limb readout's conversion corners in both tiers and the reservation's value-neutrality. The mutants exclusion roster's read_magnitude pattern follows the moved high-part drains into read_digits, counts unchanged. --- .cargo/mutants.toml | 4 +- crates/suanpan/README.md | 15 +- crates/suanpan/src/accumulator.rs | 135 +++++++++++++++++- crates/suanpan/src/accumulator/tests.rs | 13 ++ .../src/accumulator/tests/differential.rs | 44 ++++++ .../suanpan/src/accumulator/tests/metered.rs | 119 +++++++++++++++ .../src/accumulator/tests/witnesses.rs | 90 ++++++++++++ crates/suanpan/src/claims.rs | 27 ++++ crates/suanpan/src/lib.rs | 15 +- tools/mutantcheck-expected.json | 2 +- 10 files changed, 451 insertions(+), 13 deletions(-) diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index a06cd48e..f5f17e59 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -82,11 +82,11 @@ additional_cargo_args = ["--all-features"] exclude_re = [ # --- suanpan::accumulator --- - # Proven equivalent: read_magnitude's final carry has magnitude at + # Proven equivalent: read_digits' final carry has magnitude at # most 3 (bound derived in the comment at the site), so each high-part # drain emits its whole value in its first digit and the mutated shift # direction has an empty discriminating class. - "accumulator\\.rs.*: replace >>= with <<= in Accumulator::read_magnitude", + "accumulator\\.rs.*: replace >>= with <<= in Accumulator::read_digits", # Performance genre, owner-declared benign fast path: the identity # guard routes cost and representation only (the comment at the site diff --git a/crates/suanpan/README.md b/crates/suanpan/README.md index 232214c8..01252b64 100644 --- a/crates/suanpan/README.md +++ b/crates/suanpan/README.md @@ -217,12 +217,13 @@ worst-case per call. | `add_u64_shl`, `sub_u64_shl` | amortized O(1), independent of the shift | | `add_magnitude`, `sub_magnitude` | word-scale: amortized O(1); wide: amortized O(\|delta\|) | | `add_magnitude_shl`, `sub_magnitude_shl` | as `add_magnitude`/`sub_magnitude`, at any shift | +| `add_limbs_shl`, `sub_limbs_shl` | amortized O(limbs yielded), independent of the shift | | `add_accum`, `sub_accum` | amortized O(\|other\|) | | `add_accum_shl`, `sub_accum_shl` | amortized O(\|other\|), independent of the shift | | `merge_into_wider` | amortized O(min(\|self\|, \|other\|)) | | `sign`, `is_negative`, `sign_dominates_word`, `sign_dominates_at` | amortized O(1) | | `is_literally_zero` (one-sided: `true` means zero, `false` means unknown), `digit_count` | O(1) | -| `shl`, `negate`, `reset`, `sign_magnitude` | O(\|self\|) | +| `shl`, `negate`, `reset`, `sign_magnitude`, `sign_limbs` | O(\|self\|) | | `sign_magnitude_shl` | O(w), w the written span since the last reset | Digit touches are shift-independent; memory is not. A shifted entry point @@ -238,9 +239,17 @@ touched. The `*_magnitude` entry points are generic over `Magnitude`, the seam for a caller's own stored-magnitude type: the operand reports whether it fits a machine word, and the accumulator dispatches to the small or wide path -accordingly. There is no from-value constructor: build with +accordingly. The `*_limbs_shl` entry points and +`sign_limbs` are the same seam past the +backend's reach: on a 32-bit target a `UBig` magnitude caps out near +`usize::MAX` bits while the digit buffer is bounded only by memory, so +operands and totals wider than that stream in and read out as plain +little-endian 64-bit limb sequences with no backend value in between. +There is no from-value constructor: build with `new` (or `Default`) and a single `add_*` call, read out -with `sign_magnitude`. +with `sign_magnitude` (or +`sign_limbs` at widths the backend cannot +hold). ## When not to reach for it diff --git a/crates/suanpan/src/accumulator.rs b/crates/suanpan/src/accumulator.rs index 4948f498..55487037 100644 --- a/crates/suanpan/src/accumulator.rs +++ b/crates/suanpan/src/accumulator.rs @@ -347,6 +347,57 @@ impl Accumulator { self.apply_limbs(Limbs::new(delta), true, shift); } + /// Add a stream of little-endian 64-bit limbs times `2^shift`: + /// amortized O(limbs yielded) digit touches, independent of the + /// shift. + /// + /// The streaming twin of [`add_wide_shl`](Accumulator::add_wide_shl), + /// for operands **wider than the backend can hold**: a [`Magnitude`] + /// lends a whole [`UBig`], and on a 32-bit target a magnitude caps + /// out near `usize::MAX` bits — while a caller's own stored wide + /// value (a limb vector bounded only by memory) has no [`UBig`] to + /// lend. This entry takes the value as its little-endian 64-bit + /// limbs directly, so any representation that can stream its limbs + /// can enter at any width memory admits. High zero limbs are + /// permitted and value-neutral, but each yielded limb costs its + /// touch — stream the minimal form. + /// + /// # Complexity + /// + /// Amortized `O(limbs yielded)` digit touches, independent of the + /// shift; the digit buffer grows to cover the shifted positions. + /// + /// # Panics + /// + /// As [`add_wide_shl`](Accumulator::add_wide_shl): a shifted digit + /// position past `usize` panics. + pub fn add_limbs_shl>(&mut self, limbs: I, shift: u64) { + self.spill(); + self.apply_limbs(limbs.into_iter(), false, shift); + } + + /// Subtract a stream of little-endian 64-bit limbs times `2^shift`: + /// amortized O(limbs yielded) digit touches, independent of the + /// shift. + /// + /// The subtractive twin of + /// [`add_limbs_shl`](Accumulator::add_limbs_shl), with the same + /// wider-than-the-backend rationale and memory note. + /// + /// # Complexity + /// + /// Amortized `O(limbs yielded)` digit touches, independent of the + /// shift; the digit buffer grows to cover the shifted positions. + /// + /// # Panics + /// + /// As [`add_wide_shl`](Accumulator::add_wide_shl): a shifted digit + /// position past `usize` panics. + pub fn sub_limbs_shl>(&mut self, limbs: I, shift: u64) { + self.spill(); + self.apply_limbs(limbs.into_iter(), true, shift); + } + /// Add a stored magnitude times `2^shift`, at the width it is stored /// at. /// @@ -627,6 +678,29 @@ impl Accumulator { self.quick = Some(0); } + /// Pre-size the digit buffer to cover positions `0..digits`: no + /// digit touches, one allocation at most. + /// + /// An allocation-shaping hint, value-neutral: a caller that knows + /// the scale its writes will reach (a fold aligning summands to a + /// known common exponent) reserves once and every later buffer + /// growth is in-place, so the peak transient is the buffer itself — + /// without the hint, incremental growth's doubling can briefly hold + /// twice the final width, which is the difference between fitting + /// and failing near a 32-bit target's memory ceiling. Reserving + /// less than the writes reach costs nothing but the doubling; extra + /// reserved capacity is plain unused memory until + /// [`shl`](Accumulator::shl) on a digit-engine value or a + /// replacement drops the buffer. + /// + /// # Complexity + /// + /// No digit touches; one buffer allocation when capacity grows. + pub fn reserve_digits(&mut self, digits: usize) { + self.digits + .reserve_exact(digits.saturating_sub(self.digits.len())); + } + /// The sign of the held value — `value.cmp(&0)`, so `Less` means /// negative: amortized O(1). /// @@ -934,6 +1008,49 @@ impl Accumulator { (sign, magnitude, 32 * start as u64) } + /// The held value as a sign and normalized little-endian 64-bit + /// limbs: O(held digits). + /// + /// [`sign_magnitude`](Accumulator::sign_magnitude) without the + /// backend magnitude: the readout for totals **wider than the + /// backend can hold** — on a 32-bit target a magnitude caps out + /// near `usize::MAX` bits while the digit buffer, and this limb + /// vector, are bounded only by memory. The limbs are minimal (no + /// high zero limb) and empty exactly when the sign is + /// [`Ordering::Equal`]. The same one low-to-high carry pass as + /// [`sign_magnitude`](Accumulator::sign_magnitude), and like it a + /// read-out, not a drain. + /// + /// # Complexity + /// + /// `O(|self|)` digit touches and a same-order limb allocation. + pub fn sign_limbs(&self) -> (Ordering, Vec) { + if let Some(value) = self.quick { + touch(self.digit_count() as u64); + let magnitude = value.unsigned_abs(); + let mut limbs = vec![magnitude as u64, (magnitude >> 64) as u64]; + while limbs.last() == Some(&0) { + limbs.pop(); + } + return (value.cmp(&0), limbs); + } + let (sign, digits) = self.read_digits(0); + let mut limbs: Vec = digits + .chunks(2) + .map(|pair| u64::from(pair[0]) | (pair.get(1).copied().map_or(0, u64::from) << 32)) + .collect(); + drop(digits); + while limbs.last() == Some(&0) { + limbs.pop(); + } + debug_assert_eq!( + sign == Ordering::Equal, + limbs.is_empty(), + "the readout's limbs are empty exactly at zero" + ); + (sign, limbs) + } + /// Read out `Σ_{i ≥ start} digits[i] · 2^(32·(i − start))` as a sign /// and a normalized magnitude. /// @@ -943,6 +1060,17 @@ impl Accumulator { /// every step of every schedule it drives), so the suffix read is /// the whole value at scale `2^(32·start)`. fn read_magnitude(&self, start: usize) -> (Ordering, UBig) { + let (sign, digits) = self.read_digits(start); + (sign, magnitude_from_digits(digits)) + } + + /// Read out the suffix at or above `start` as a sign and normalized + /// unsigned base-2^32 digits (little-endian, possibly with high + /// zeros): the one carry pass behind every magnitude readout. + /// + /// [`read_magnitude`](Accumulator::read_magnitude)'s soundness + /// condition (every digit below `start` zero) applies verbatim. + fn read_digits(&self, start: usize) -> (Ordering, Vec) { // Low-to-high signed carry: after the pass, the collected unsigned // digits hold `M` with `value = carry · 2^(32·len) + M`, // `0 ≤ M < 2^(32·len)`. The final carry has magnitude at most 3: @@ -984,7 +1112,7 @@ impl Accumulator { high >>= DIGIT_BITS; } // |carry| ≥ 1 makes |value| ≥ 2^(32·len) − M > 0: never zero. - (Ordering::Less, magnitude_from_digits(collected)) + (Ordering::Less, collected) } else { let mut high = carry as u128; while high > 0 { @@ -992,13 +1120,12 @@ impl Accumulator { collected.push((high & u128::from(DIGIT_MASK)) as u32); high >>= DIGIT_BITS; } - let magnitude = magnitude_from_digits(collected); - let sign = if magnitude == UBig::ZERO { + let sign = if collected.iter().all(|&digit| digit == 0) { Ordering::Equal } else { Ordering::Greater }; - (sign, magnitude) + (sign, collected) } } diff --git a/crates/suanpan/src/accumulator/tests.rs b/crates/suanpan/src/accumulator/tests.rs index 7cf3e106..6911c74b 100644 --- a/crates/suanpan/src/accumulator/tests.rs +++ b/crates/suanpan/src/accumulator/tests.rs @@ -67,6 +67,19 @@ fn assert_value(acc: &Accumulator, oracle: &IBig) { _ => IBig::from(magnitude), }; assert_eq!(&rebuilt, oracle, "sign_magnitude magnitude"); + // The limb read is the same value spelled as minimal LE 64-bit limbs. + let (limb_sign, limbs) = acc.sign_limbs(); + assert_eq!(limb_sign, sign, "sign_limbs sign"); + assert_ne!( + limbs.last(), + Some(&0), + "sign_limbs limbs are minimal: no high zero limb" + ); + let rebuilt = match limb_sign { + Ordering::Less => -IBig::from(from_limbs(&limbs)), + _ => IBig::from(from_limbs(&limbs)), + }; + assert_eq!(&rebuilt, oracle, "sign_limbs magnitude"); // The scaled read denotes the same value: ±magnitude · 2^shift. let (shl_sign, shl_magnitude, shift) = acc.sign_magnitude_shl(); assert_eq!(shl_sign, sign, "sign_magnitude_shl sign"); diff --git a/crates/suanpan/src/accumulator/tests/differential.rs b/crates/suanpan/src/accumulator/tests/differential.rs index e3282234..97a0588d 100644 --- a/crates/suanpan/src/accumulator/tests/differential.rs +++ b/crates/suanpan/src/accumulator/tests/differential.rs @@ -32,6 +32,16 @@ enum Op { value: UBig, shift: u64, }, + /// A wide delta entering as a raw little-endian limb stream — the + /// wider-than-the-backend entry — optionally padded with high zero + /// limbs (value-neutral by contract). + LimbsShl { + negative: bool, + limbs: Vec, + shift: u64, + }, + /// An allocation-shaping reservation: value-neutral by contract. + Reserve(usize), } /// Apply one operation to the accumulator and the oracle in lockstep. @@ -64,6 +74,23 @@ fn apply(acc: &mut Accumulator, oracle: &mut IBig, op: &Op) { *oracle += scaled; } } + Op::LimbsShl { + negative, + limbs, + shift, + } => { + let scaled = IBig::from(from_limbs(limbs)) << usize::try_from(*shift).unwrap(); + if *negative { + acc.sub_limbs_shl(limbs.iter().copied(), *shift); + *oracle -= scaled; + } else { + acc.add_limbs_shl(limbs.iter().copied(), *shift); + *oracle += scaled; + } + } + Op::Reserve(digits) => { + acc.reserve_digits(*digits); + } } } @@ -102,6 +129,23 @@ fn arb_op() -> impl Strategy { value: from_limbs(&limbs), shift, }), + 1 => ( + proptest::collection::vec(any::(), 1..=4), + 0usize..3, + any::(), + 0u64..512, + ) + .prop_map(|(mut limbs, zero_pad, negative, shift)| { + // High zero limbs are contractually value-neutral: pad + // some streams so the padding arm stays exercised. + limbs.extend(std::iter::repeat_n(0, zero_pad)); + Op::LimbsShl { + negative, + limbs, + shift, + } + }), + 1 => (0usize..64).prop_map(Op::Reserve), ] } diff --git a/crates/suanpan/src/accumulator/tests/metered.rs b/crates/suanpan/src/accumulator/tests/metered.rs index 405f9a78..6a30c60a 100644 --- a/crates/suanpan/src/accumulator/tests/metered.rs +++ b/crates/suanpan/src/accumulator/tests/metered.rs @@ -429,6 +429,17 @@ fn held_width_rows_cost_the_held_digits() { ); assert_eq!((sign, magnitude), (Ordering::Greater, wide_value.clone())); + touch_meter::reset(); + let (limb_sign, limbs) = acc.sign_limbs(); + assert_eq!( + touch_meter::touches(), + held_digits, + "sign_limbs at {held_digits} held digits: the same one carry \ + pass as sign_magnitude" + ); + assert_eq!(limb_sign, Ordering::Greater); + assert_eq!(from_limbs(&limbs), wide_value.clone()); + touch_meter::reset(); acc.shl(32); assert_eq!( @@ -544,6 +555,114 @@ fn wide_writes_cost_the_operand_at_any_held_width() { } } +/// The streaming limb entry points cost the limbs the stream yields, +/// independent of the shift and the held width: exact totals on the +/// alternating-pair schedule, plus the padded-stream clause. +/// +/// A one-limb stream oscillating at digit `shift/32` costs exactly 5 +/// touches per sub/add pair — the same accounting as the wide rows (2 +/// limb reads + 2 deposits + 1 certificate skip), pinned identical +/// across a shift doubling. The second clause pins the contract's +/// padding sentence exactly: a `[5, 0, 0]` stream costs 4 touches (3 +/// yielded-limb reads + 1 deposit) — high zero limbs are value-neutral +/// but each yielded limb pays its touch. +#[test] +fn limb_stream_writes_cost_the_yielded_limbs() { + for shift in [32_000u64, 64_000] { + let mut acc = Accumulator::new(); + acc.add_limbs_shl([1u64], shift); + touch_meter::reset(); + for _ in 0..1_000 { + acc.sub_limbs_shl([1u64], shift); + acc.add_limbs_shl([1u64], shift); + } + assert_eq!( + touch_meter::touches(), + 5_000, + "1,000 alternating one-limb stream pairs at shift {shift}: \ + 5 touches per pair, whatever the shift" + ); + let (sign, magnitude) = acc.sign_magnitude(); + assert_eq!(sign, Ordering::Greater); + assert_eq!( + magnitude, + UBig::from(1u8) << usize::try_from(shift).unwrap() + ); + } + let mut acc = Accumulator::new(); + acc.add_limbs_shl([5u64, 0, 0], 0); + touch_meter::reset(); + acc.add_limbs_shl([5u64, 0, 0], 0); + assert_eq!( + touch_meter::touches(), + 4, + "a padded [5, 0, 0] stream: 3 yielded-limb reads + 1 deposit — \ + zero limbs are value-neutral but pay their touch" + ); + let (sign, magnitude) = acc.sign_magnitude(); + assert_eq!((sign, magnitude), (Ordering::Greater, UBig::from(10u8))); +} + +/// The streaming limb entries are the wide entries with the backend +/// value elided: identical held values and identical exact touch counts +/// when fed the same minimal limbs, in both signs, at zero and nonzero +/// shifts. +/// +/// This pins the streaming entry as a re-spelling of `add_wide_shl`'s +/// cost model, not a second cost model: whatever evidence prices the +/// wide rows prices these. +#[test] +fn limb_stream_matches_the_wide_entry() { + let shapes: [(&[u64], u64); 4] = [ + (&[3, 5], 0), + (&[3, 5], 32_000), + (&[u64::MAX, 0, 1], 17), + (&[7, 0], 63), + ]; + for (limbs, shift) in shapes { + for negative in [false, true] { + let seed = (UBig::from(1u8) << 4_096usize) - 1u8; + let mut via_wide = Accumulator::new(); + via_wide.add_wide(&seed); + let mut via_stream = Accumulator::new(); + via_stream.add_wide(&seed); + + touch_meter::reset(); + if negative { + via_wide.sub_wide_shl(&from_limbs(limbs), shift); + } else { + via_wide.add_wide_shl(&from_limbs(limbs), shift); + } + let wide_touches = touch_meter::touches(); + + touch_meter::reset(); + // `Limbs` yields minimal limbs; strip the shape's padding so + // both entries are fed the identical stream. + let minimal = limbs + .iter() + .copied() + .take(limbs.len() - limbs.iter().rev().take_while(|&&limb| limb == 0).count()); + if negative { + via_stream.sub_limbs_shl(minimal, shift); + } else { + via_stream.add_limbs_shl(minimal, shift); + } + assert_eq!( + touch_meter::touches(), + wide_touches, + "the streaming entry costs exactly the wide entry's touches" + ); + let (wide_sign, wide_magnitude) = via_wide.sign_magnitude(); + let (stream_sign, stream_magnitude) = via_stream.sign_magnitude(); + assert_eq!( + (wide_sign, wide_magnitude), + (stream_sign, stream_magnitude), + "the streaming entry holds exactly the wide entry's value" + ); + } + } +} + /// The magnitude entry points cost exactly their dispatched path: a /// word-scale operand the small path's touches, a wide operand the /// wide path's, in both signs. diff --git a/crates/suanpan/src/accumulator/tests/witnesses.rs b/crates/suanpan/src/accumulator/tests/witnesses.rs index ceba6e11..f4ccb84f 100644 --- a/crates/suanpan/src/accumulator/tests/witnesses.rs +++ b/crates/suanpan/src/accumulator/tests/witnesses.rs @@ -560,3 +560,93 @@ fn quick_register_extremes_spill_exactly() { assert_value(&acc, &oracle); } } + +/// `sign_limbs` at its conversion-path corners: zero reads empty in +/// both tiers, a register value spanning two limbs splits exactly at +/// the limb seam, and negatives read the magnitude's limbs. +/// +/// The register readout packs an `i128` magnitude into at most two +/// limbs and strips high zeros; the digit-engine readout pairs base-2^32 +/// digits into limbs. Each corner is checked in the register and again +/// after a forced spill, so both tiers pin the same spellings — and the +/// register corner at exactly `2^64` (a low limb of zero under a high +/// limb of one) pins the strip as top-down, never a sweep of interior +/// zeros. +#[test] +fn sign_limbs_conversion_corners() { + // Zero: empty limbs, both tiers. + let mut zero = Accumulator::new(); + assert_eq!(zero.sign_limbs(), (Ordering::Equal, vec![])); + zero.spill(); + assert_eq!(zero.sign_limbs(), (Ordering::Equal, vec![])); + + // (value, expected LE limbs): the u64 ceiling, the limb seam at + // 2^64 (interior zero limb kept), and a two-limb composite. + let corners: [(u128, Vec); 3] = [ + (u128::from(u64::MAX), vec![u64::MAX]), + (1u128 << 64, vec![0, 1]), + ((7u128 << 64) | 5, vec![5, 7]), + ]; + for (value, limbs) in corners { + for negative in [false, true] { + for spill in [false, true] { + // Register-preserving construction: word deposits and + // in-register shifts only (30 + 30 + 4 covers one limb), + // so the unspilled leg genuinely reads the register tier. + let mut acc = Accumulator::new(); + acc.add_u64((value >> 64) as u64); + acc.shl(30); + acc.shl(30); + acc.shl(4); + acc.add_u64(value as u64); + if negative { + acc.negate(); + } + if spill { + acc.spill(); + } else { + assert!(acc.quick.is_some(), "the construction stays registered"); + } + let sign = if negative { + Ordering::Less + } else { + Ordering::Greater + }; + assert_eq!( + acc.sign_limbs(), + (sign, limbs.clone()), + "value {value}, negative {negative}, spilled {spill}" + ); + } + } + } +} + +/// `reserve_digits` is value-neutral in every tier. +/// +/// Reserving on the register leaves it registered, reserving less than +/// the held width changes nothing, and writes after a reservation land +/// exactly as without one. +#[test] +fn reserve_digits_is_value_neutral() { + // On the register: the reservation warms the idle buffer without + // arming the digit engine or touching the held value. + let mut acc = Accumulator::new(); + acc.add_small(7); + acc.reserve_digits(100); + assert!(acc.quick.is_some(), "a reservation never arms the engine"); + let mut oracle = IBig::from(7); + assert_value(&acc, &oracle); + + // In the digit engine, before and after the covered writes — and a + // reservation smaller than the held width is a no-op. + acc.add_wide(&(UBig::from(1u8) << 3_200usize)); + oracle += IBig::from(UBig::from(1u8) << 3_200usize); + acc.reserve_digits(500); + assert_value(&acc, &oracle); + acc.reserve_digits(1); + assert_value(&acc, &oracle); + acc.sub_wide(&(UBig::from(1u8) << 12_800usize)); + oracle -= IBig::from(UBig::from(1u8) << 12_800usize); + assert_value(&acc, &oracle); +} diff --git a/crates/suanpan/src/claims.rs b/crates/suanpan/src/claims.rs index 67a65c0f..fa2969dd 100644 --- a/crates/suanpan/src/claims.rs +++ b/crates/suanpan/src/claims.rs @@ -230,6 +230,23 @@ pub(crate) const CLAIMS: &[Claim] = &[ (OWN, "magnitude_dispatch_costs_its_width_path"), ]), }, + // ──────────────────── streaming limb operands ─────────────────────── + Claim { + op: "Accumulator::add_limbs_shl", + table_cost: Some("amortized O(limbs yielded), independent of the shift"), + evidence: Evidence::Witnessed(&[ + (OWN, "limb_stream_writes_cost_the_yielded_limbs"), + (OWN, "limb_stream_matches_the_wide_entry"), + ]), + }, + Claim { + op: "Accumulator::sub_limbs_shl", + table_cost: Some("amortized O(limbs yielded), independent of the shift"), + evidence: Evidence::Witnessed(&[ + (OWN, "limb_stream_writes_cost_the_yielded_limbs"), + (OWN, "limb_stream_matches_the_wide_entry"), + ]), + }, // ─────────────────────── accumulator operands ─────────────────────── Claim { op: "Accumulator::add_accum", @@ -302,6 +319,11 @@ pub(crate) const CLAIMS: &[Claim] = &[ scan's metering)", ), }, + constant( + "Accumulator::reserve_digits", + "an allocation-shaping hint: it grows the buffer's capacity without reading or \ + writing a digit, so there is no digit-touch axis to measure", + ), // ─────────────────────── held-width operations ────────────────────── Claim { op: "Accumulator::shl", @@ -323,6 +345,11 @@ pub(crate) const CLAIMS: &[Claim] = &[ table_cost: Some(r"O(\|self\|)"), evidence: Evidence::Witnessed(&[(OWN, "held_width_rows_cost_the_held_digits")]), }, + Claim { + op: "Accumulator::sign_limbs", + table_cost: Some(r"O(\|self\|)"), + evidence: Evidence::Witnessed(&[(OWN, "held_width_rows_cost_the_held_digits")]), + }, Claim { op: "Accumulator::sign_magnitude_shl", table_cost: Some("O(w), w the written span since the last reset"), diff --git a/crates/suanpan/src/lib.rs b/crates/suanpan/src/lib.rs index 01409a61..aed8bc1d 100644 --- a/crates/suanpan/src/lib.rs +++ b/crates/suanpan/src/lib.rs @@ -213,12 +213,13 @@ //! | [`add_u64_shl`](Accumulator::add_u64_shl), [`sub_u64_shl`](Accumulator::sub_u64_shl) | amortized O(1), independent of the shift | //! | [`add_magnitude`](Accumulator::add_magnitude), [`sub_magnitude`](Accumulator::sub_magnitude) | word-scale: amortized O(1); wide: amortized O(\|delta\|) | //! | [`add_magnitude_shl`](Accumulator::add_magnitude_shl), [`sub_magnitude_shl`](Accumulator::sub_magnitude_shl) | as [`add_magnitude`](Accumulator::add_magnitude)/[`sub_magnitude`](Accumulator::sub_magnitude), at any shift | +//! | [`add_limbs_shl`](Accumulator::add_limbs_shl), [`sub_limbs_shl`](Accumulator::sub_limbs_shl) | amortized O(limbs yielded), independent of the shift | //! | [`add_accum`](Accumulator::add_accum), [`sub_accum`](Accumulator::sub_accum) | amortized O(\|other\|) | //! | [`add_accum_shl`](Accumulator::add_accum_shl), [`sub_accum_shl`](Accumulator::sub_accum_shl) | amortized O(\|other\|), independent of the shift | //! | [`merge_into_wider`](Accumulator::merge_into_wider) | amortized O(min(\|self\|, \|other\|)) | //! | [`sign`](Accumulator::sign), [`is_negative`](Accumulator::is_negative), [`sign_dominates_word`](Accumulator::sign_dominates_word), [`sign_dominates_at`](Accumulator::sign_dominates_at) | amortized O(1) | //! | [`is_literally_zero`](Accumulator::is_literally_zero) (one-sided: `true` means zero, `false` means unknown), [`digit_count`](Accumulator::digit_count) | O(1) | -//! | [`shl`](Accumulator::shl), [`negate`](Accumulator::negate), [`reset`](Accumulator::reset), [`sign_magnitude`](Accumulator::sign_magnitude) | O(\|self\|) | +//! | [`shl`](Accumulator::shl), [`negate`](Accumulator::negate), [`reset`](Accumulator::reset), [`sign_magnitude`](Accumulator::sign_magnitude), [`sign_limbs`](Accumulator::sign_limbs) | O(\|self\|) | //! | [`sign_magnitude_shl`](Accumulator::sign_magnitude_shl) | O(w), w the written span since the last reset | //! //! Digit touches are shift-independent; memory is not. A shifted entry point @@ -234,9 +235,17 @@ //! The `*_magnitude` entry points are generic over [`Magnitude`], the seam for //! a caller's own stored-magnitude type: the operand reports whether it fits a //! machine word, and the accumulator dispatches to the small or wide path -//! accordingly. There is no from-value constructor: build with +//! accordingly. The `*_limbs_shl` entry points and +//! [`sign_limbs`](Accumulator::sign_limbs) are the same seam past the +//! backend's reach: on a 32-bit target a [`UBig`] magnitude caps out near +//! `usize::MAX` bits while the digit buffer is bounded only by memory, so +//! operands and totals wider than that stream in and read out as plain +//! little-endian 64-bit limb sequences with no backend value in between. +//! There is no from-value constructor: build with //! [`new`](Accumulator::new) (or `Default`) and a single `add_*` call, read out -//! with [`sign_magnitude`](Accumulator::sign_magnitude). +//! with [`sign_magnitude`](Accumulator::sign_magnitude) (or +//! [`sign_limbs`](Accumulator::sign_limbs) at widths the backend cannot +//! hold). //! //! # When not to reach for it //! diff --git a/tools/mutantcheck-expected.json b/tools/mutantcheck-expected.json index 3e8a868d..9d53d8d7 100644 --- a/tools/mutantcheck-expected.json +++ b/tools/mutantcheck-expected.json @@ -1,7 +1,7 @@ { "tool": "cargo-mutants 27.1.0", "patterns": { - "accumulator\\.rs.*: replace >>= with <<= in Accumulator::read_magnitude": { + "accumulator\\.rs.*: replace >>= with <<= in Accumulator::read_digits": { "listed": 2, "suppressed": 2 }, From 79a944abda27200f22f6386bd296e37e50441dcd Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 19 Aug 2026 10:55:05 -0400 Subject: [PATCH 19/20] before: total rank arithmetic to the working-set ceiling on 32-bit targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rank numerator becomes two-arm storage (version/rank/num.rs): the backend magnitude wherever the backend can represent the value, a raw little-endian limb vector past that — canonical by construction (wide iff the width exceeds the backend's capacity), so structural equality and hashing stay value equality and the arm choice is pure routing between two exact implementations. On 64-bit targets the seam sits at 2^64 - 64 bits and never engages; on a 32-bit target it sits at 2^32 - 32 bits, inside the honestly reachable range of the rank wire door. What the wide arm totalizes, each previously a loud backend panic on wasm32 at inputs that fit the address space: - decode: both wire paths to a past-capacity numerator — the fraction image (~604 MB of input) and the integral mantissa (~512 MiB), including the biased transient at exactly the capacity — assemble limbs directly, never a backend value; - Add and checked_sub: a routing predicate keeps the backend's shift-and-combine wherever both aligned operands and the carry fit it (the historical path, cost and metering identical), and routes past that through the streaming accumulator, whose digit buffer is reserved to the aligned width up front — no aligned numerator is ever materialized in the backend, so gaps at or past usize and aligned widths past the capacity are values priced by memory; - Sum: summands enter at the width their arm stores (the wide arm via the streaming limb entry) and the total reads back out as limbs; - encode, Ord, Eq, Hash, Display: the emission, the class-tie window comparison (one shared MSB-window kernel across arms, extracted from Base::msb_cmp), equality, hashing, and exact decimal rendering (schoolbook long division on the wide arm — quadratic, the documented price of exactness past the backend's reach) all operate per arm. The wasm32 pins flip with the cure: the fraction-door decode past the capacity, both rank-addition gap-boundary pins, and the checked_sub gap-boundary pin now assert exact values (as-found: all four trap at f615220f). New adjacency pins hold the integral door below, at, and past the capacity, and a full-width decode-then-re-encode byte-identity pin holds the emission at the seam's far side. The memory-terminal pins are untouched. Host-side, a test-only arm-ceiling override (thread-local, routing only) drives both arms and the seam through the public doors at small sizes: the alignment-order sweep, the RANK_TRIPLE laws, sum-vs-fold, arithmetic against backend-oracle alignment, normalization-family proptests, codec round-trips, Display, and the Ranked composite door all run in the wide regime, plus unit differentials for every wide-arm operation against the backend as oracle. The limb meter's convention extends to the wide arm — the same 64-bit-limb denomination, recorded at the num module's operations — with the base arm's recordings byte-identical to the historical numerator path. --- crates/before/src/codec/base.rs | 87 ++- crates/before/src/codec/base/limb_meter.rs | 8 +- crates/before/src/version/rank.rs | 241 +++++-- crates/before/src/version/rank/num.rs | 650 ++++++++++++++++++ crates/before/src/version/rank/num/tests.rs | 191 +++++ crates/before/src/version/tests.rs | 324 ++++++++- crates/before/wasm32-pins/guest/src/lib.rs | 54 ++ .../before/wasm32-pins/harness/tests/pins.rs | 187 +++-- 8 files changed, 1591 insertions(+), 151 deletions(-) create mode 100644 crates/before/src/version/rank/num.rs create mode 100644 crates/before/src/version/rank/num/tests.rs diff --git a/crates/before/src/codec/base.rs b/crates/before/src/codec/base.rs index 274da3af..a70667cc 100644 --- a/crates/before/src/codec/base.rs +++ b/crates/before/src/codec/base.rs @@ -98,32 +98,17 @@ impl Base { /// `a · 2^x` versus `b · 2^y` whenever the two values share a magnitude /// class (`bits(a) − x == bits(b) − y`). /// - /// Streams 64-bit windows most-significant-first — no alignment shift - /// is ever materialized — and stops at the first differing window, so - /// the cost is O(shared-prefix limbs) with zero allocation. When every - /// shared window agrees, the longer bit string is the larger value: - /// this rides on the caller's normalization invariant that the strings - /// end in a set bit (an odd numerator), so the longer string's - /// extension is nonzero. The limb meter records one limb per streamed - /// window, keeping the metered cost honest about the scan. + /// The stored-magnitude instance of [`msb_cmp_windows`], which carries + /// the streaming argument, the tail rule's normalization premise, and + /// the per-window metering. pub(crate) fn msb_cmp(a: &Base, b: &Base) -> Ordering { - let mut wa = MsbWindows::new(a); - let mut wb = MsbWindows::new(b); - loop { - match (wa.next(), wb.next()) { - (Some(x), Some(y)) => { - #[cfg(feature = "limb-meter")] - limb_meter::record(2); - match x.cmp(&y) { - Ordering::Equal => continue, - decided => return decided, - } - } - (Some(_), None) => return Ordering::Greater, - (None, Some(_)) => return Ordering::Less, - (None, None) => return Ordering::Equal, - } - } + msb_cmp_windows(a.msb_windows(), b.msb_windows()) + } + + /// The MSB-first 64-bit windows of this magnitude's bit string, for + /// [`msb_cmp_windows`]. + pub(crate) fn msb_windows(&self) -> MsbWindows + '_> { + MsbWindows::new(Limbs::new(&self.0).rev(), self.bits()) } #[cfg(test)] @@ -160,16 +145,51 @@ impl Base { } } +/// Compare two MSB-aligned window streams ([`MsbWindows`]): the shared +/// kernel behind [`Base::msb_cmp`] and the rank numerator's cross-arm +/// class-tie comparison. +/// +/// Streams 64-bit windows most-significant-first — no alignment shift is +/// ever materialized — and stops at the first differing window, so the +/// cost is O(shared-prefix limbs) with zero allocation. When every shared +/// window agrees, the longer bit string is the larger value: this rides on +/// the caller's normalization invariant that the strings end in a set bit +/// (an odd numerator), so the longer string's extension is nonzero. The +/// limb meter records one limb per streamed window pair, keeping the +/// metered cost honest about the scan. +pub(crate) fn msb_cmp_windows( + mut a: impl Iterator, + mut b: impl Iterator, +) -> Ordering { + loop { + match (a.next(), b.next()) { + (Some(x), Some(y)) => { + #[cfg(feature = "limb-meter")] + limb_meter::record(2); + match x.cmp(&y) { + Ordering::Equal => continue, + decided => return decided, + } + } + (Some(_), None) => return Ordering::Greater, + (None, Some(_)) => return Ordering::Less, + (None, None) => return Ordering::Equal, + } + } +} + /// The 64-bit windows of a magnitude's bit string, most-significant first. /// /// The first window is the value's top 64 bits left-aligned (the MSB in /// bit 63); the last is zero-padded below the final significant bit. A /// zero value has no windows. Streams the stored limbs top-down with one /// register of carry, so a window costs O(1) and no shifted copy of the -/// value ever exists. -struct MsbWindows<'a> { +/// value ever exists. Generic over the reversed limb source so both +/// numerator arms (the stored magnitude here, the rank's wide limb vector) +/// stream through one implementation. +pub(crate) struct MsbWindows { /// Remaining limbs, top first; exhausted once the tail is consumed. - limbs: core::iter::Rev>, + limbs: I, /// The previously consumed limb, still owed its low bits. held: Option, /// The left-alignment shift: `64 − (bits mod 64)`, zero for a @@ -177,18 +197,19 @@ struct MsbWindows<'a> { shift: u32, } -impl<'a> MsbWindows<'a> { - fn new(value: &'a Base) -> Self { - let bits = value.bits(); +impl> MsbWindows { + /// The windows over `limbs` — the value's 64-bit limbs, **already + /// reversed** (most significant first) — for a value `bits` wide. + pub(crate) fn new(limbs: I, bits: u64) -> Self { MsbWindows { - limbs: Limbs::new(&value.0).rev(), + limbs, held: None, shift: ((64 - bits % 64) % 64) as u32, } } } -impl Iterator for MsbWindows<'_> { +impl> Iterator for MsbWindows { type Item = u64; fn next(&mut self) -> Option { diff --git a/crates/before/src/codec/base/limb_meter.rs b/crates/before/src/codec/base/limb_meter.rs index fc8965fb..200ed003 100644 --- a/crates/before/src/codec/base/limb_meter.rs +++ b/crates/before/src/codec/base/limb_meter.rs @@ -8,7 +8,13 @@ //! they run, and the wide-gamma decode in `codec::gamma` records one //! value-width count per decoded value — so amortized-linear algorithms count //! linearly in packed input bits and magnitude-quadratic ones count -//! quadratically. Relaxed ordering suffices: the metering binaries run one +//! quadratically. The denomination is the value's width in 64-bit limbs, +//! not any particular storage: the rank numerator's wide arm +//! (`version::rank::num`, magnitudes past the backend's capacity on +//! 32-bit targets) records its operations' operand and materialization +//! widths into this same counter under the same unit, so limb-denominated +//! envelopes read continuously across that arm seam. Relaxed ordering +//! suffices: the metering binaries run one //! scenario per process and read the counters only after the metered call //! returns. //! diff --git a/crates/before/src/version/rank.rs b/crates/before/src/version/rank.rs index df69a211..5499d16b 100644 --- a/crates/before/src/version/rank.rs +++ b/crates/before/src/version/rank.rs @@ -141,6 +141,11 @@ use suanpan::Accumulator; use crate::codec::Base; use crate::error::Decode; +mod num; +use num::{arm_ceiling_bits, Num}; +#[cfg(test)] +pub(crate) use num::{ceiling as arm_ceiling, BACKEND_CAPACITY_BITS}; + /// The causal rank of a [`Version`](crate::Version) as an exact dyadic /// rational. /// @@ -240,7 +245,13 @@ use crate::error::Decode; pub struct Rank { /// The numerator. Normalized: odd, or zero with `exp` zero, so each /// value has exactly one representation. - num: Base, + /// + /// Stored on the canonical arm of the two-arm [`Num`] (the backend + /// magnitude up to the backend's capacity, a raw limb vector past + /// it), so numerators wider than the backend can hold on a 32-bit + /// target are exact values, not panics; the [`num`] module doc + /// carries the arm dispatch and its invariants. + num: Num, /// The (binary) exponent of the denominator `2^exp`. Bounded by the /// event tree's depth, since each level halves the interval width. exp: u64, @@ -260,7 +271,7 @@ impl Rank { /// Version::try_from(7).unwrap().rank()); /// ``` pub const ZERO: Rank = Rank { - num: Base::ZERO, + num: Num::ZERO, exp: 0, }; @@ -293,13 +304,27 @@ impl Rank { Ordering::Less => None, Ordering::Equal => Some(Rank::ZERO), Ordering::Greater => { - // The alignment shifts are width-bounded on any addressable - // input (`Add`'s note carries the derivation), so the - // checked conversions inside `<<` stay dead. + // Two exact routes, split by whether the backend can + // materialize the aligned operands (`Add`'s note carries + // the routing argument): the backend's shift-and-subtract + // wherever the aligned widths fit it, the accumulator + // route past that. A strictly positive difference cannot + // carry past its wider operand, so the backend route needs + // no width headroom beyond the operands' own. let e = self.exp.max(other.exp); - let a = self.num.clone() << (e - self.exp); - let b = other.num.clone() << (e - other.exp); - Some(Rank::from_raw(a - &b, e)) + if let (Num::Base(a), Num::Base(b)) = (&self.num, &other.num) { + if backend_alignment_fits(a, self.exp, b, other.exp, e, 0) { + let a = a.clone() << (e - self.exp); + let b = b.clone() << (e - other.exp); + return Some(Rank::from_raw(a - &b, e)); + } + } + let difference = accumulate(self, other, e, true); + debug_assert!( + difference > Rank::ZERO, + "the Greater pre-check promises a strictly positive difference" + ); + Some(difference) } } } @@ -439,6 +464,13 @@ impl Rank { decode_bytes(&buf) } + /// Whether this rank's numerator is stored on the wide arm, for the + /// test suites' canonicity assertions. + #[cfg(test)] + pub(crate) fn numerator_is_wide(&self) -> bool { + self.num.is_wide() + } + /// The rank's value content in bits: `bits(num) + exp`. /// /// The meter denominator of record for `Rank` operands, which have no @@ -461,7 +493,7 @@ impl Rank { /// It is **VERY IMPORTANT** that these not be exposed together, with the /// `from_raw` constructor, as this creates an affordance for constructing /// exponential serialization-size bombs. - pub(crate) fn raw_parts(&self) -> (&Base, u64) { + pub(crate) fn raw_parts(&self) -> (&Num, u64) { (&self.num, self.exp) } @@ -476,15 +508,25 @@ impl Rank { /// `raw_parts` destructor, as this creates an affordance for constructing /// exponential serialization-size bombs. pub(crate) fn from_raw(num: Base, exp: u64) -> Self { + Rank::from_num(Num::from_base(num), exp) + } + + /// [`from_raw`](Rank::from_raw) over either numerator arm. + /// + /// The shared normalization every raw `(numerator, exponent)` + /// producer — the folds, the decoder, the accumulator readout — lands + /// through, which also re-dispatches the stripped numerator onto its + /// canonical arm. + fn from_num(num: Num, exp: u64) -> Self { match num.trailing_zeros() { None => Rank { - num: Base::ZERO, + num: Num::ZERO, exp: 0, }, Some(tz) => { let shift = tz.min(exp); Rank { - num: num >> shift, + num: num.shr(shift), exp: exp - shift, } } @@ -492,20 +534,97 @@ impl Rank { } } +/// Whether the backend can materialize both aligned operands and the +/// result at the common exponent `e`. +/// +/// The routing predicate between the backend's shift-and-combine and the +/// accumulator route, for rank addition and positive subtraction. +/// +/// Three clauses, all width facts: each exponent gap must fit the +/// backend's `usize` shift amount, and each aligned operand — plus +/// `headroom` bits for the operation's possible carry (one for addition, +/// none for subtraction) — must fit the backend's capacity. The routing is +/// value-indistinguishable: both routes are exact, so this predicate moves +/// cost, never results. On 64-bit targets the capacity clause is +/// unreachable below allocatable memory and the gap clause below any +/// honest exponent, so every rank that exists routes to the backend there; +/// the accumulator route is live exactly where 32-bit targets need it, and +/// under the test ceiling. +fn backend_alignment_fits( + a: &Base, + a_exp: u64, + b: &Base, + b_exp: u64, + e: u64, + headroom: u64, +) -> bool { + let fits = |num: &Base, exp: u64| { + let gap = e - exp; + let aligned = if num.bits() == 0 { + 0 + } else { + num.bits().saturating_add(gap).saturating_add(headroom) + }; + usize::try_from(gap).is_ok() && aligned <= arm_ceiling_bits() + }; + fits(a, a_exp) && fits(b, b_exp) +} + +/// Combine `lhs ± rhs` at the common exponent `e` through the streaming +/// accumulator. +/// +/// The route on which no aligned numerator is ever materialized in the +/// backend, so the only width bounds are digit positions and allocatable +/// memory itself. Digit positions are `usize`-indexed — a panic from +/// gaps at 2³⁷ on a 32-bit target — but sit orders of magnitude above +/// any honest exponent: a decoded exponent is counted from fraction bits +/// actually read, under 2³⁵ from a whole 32-bit address space, and a +/// version-derived exponent is bounded by its tree's stored bit length. +/// +/// The buffer is reserved to the widest aligned operand up front, so the +/// peak transient is the buffer, not a growth-doubling of it; the readout +/// streams back as limbs and lands on the canonical arm. +fn accumulate(lhs: &Rank, rhs: &Rank, e: u64, subtract_rhs: bool) -> Rank { + let mut acc = Accumulator::new(); + let aligned_bits = |rank: &Rank| { + if rank.num.bits() == 0 { + 0 + } else { + rank.num.bits().saturating_add(e - rank.exp) + } + }; + let widest = aligned_bits(lhs).max(aligned_bits(rhs)).saturating_add(1); + if let Ok(digits) = usize::try_from(widest / 32 + 2) { + acc.reserve_digits(digits); + } + lhs.num.fold_into(&mut acc, e - lhs.exp, false); + rhs.num.fold_into(&mut acc, e - rhs.exp, subtract_rhs); + let (sign, limbs) = acc.sign_limbs(); + debug_assert_ne!( + sign, + Ordering::Less, + "rank addition and pre-checked subtraction are nonnegative" + ); + drop(acc); + Rank::from_num(Num::from_limbs(limbs), e) +} + /// Emit the canonical prefix-ascending stream for `num · 2⁻ᵉˣᵖ` (the module doc /// carries the format and the order argument). /// /// `pub(crate)` alongside [`Rank::encode`] so the ranked view's fused emission /// can emit straight from its rank fold's `(numerator, exponent)` output, with /// no walk beyond the fold's own. -pub(crate) fn encode_parts(num: &Base, exp: u64) -> Vec { +pub(crate) fn encode_parts(num: &Num, exp: u64) -> Vec { // The integral part, biased so zero has a (smallest) codeword: // m = ⌊r⌋ + 1, w = bits(m), ρ = bits(w) − 1. The shift is total at any - // exponent — `Base`'s `Shr` clamps past the numerator's width — so + // exponent — both numerator arms clamp a shift past their width — so // a fraction-heavy rank whose `exp` outruns a 32-bit `usize` (from // ~604 MB of decoded input) floors to zero here exactly as any other - // sub-unit value does. - let biased = (num.clone() >> exp) + 1u32; + // sub-unit value does; the bias re-dispatches arms, so an integral + // part carried past the backend's last representable bit is emitted + // from the wide arm rather than handed to the backend. + let biased = num.clone().shr(exp).plus_one(); let w = biased.bits(); let rho = u64::from(63 - w.leading_zeros()); let groups = exp.div_ceil(FRACTION_GROUP_BITS); @@ -617,13 +736,17 @@ pub(crate) fn decode_stream(next_byte: impl FnMut() -> Result) -> Re w = w << 1 | u64::from(src.bit()?); } // The biased integral m: its implied leading bit, then w − 1 stream bits, - // sunk MSB-first and unbiased at materialization. + // sunk MSB-first and unbiased at materialization. The materialization + // lands on the numerator's canonical arm, so a mantissa wider than the + // backend's capacity (reachable on a 32-bit target from ~512 MiB of + // input, well inside its address space) is a value, never a backend + // panic. let mut mantissa = BitSink::new(); mantissa.push(true); for _ in 0..w - 1 { mantissa.push(src.bit()?); } - let integral = mantissa.into_base() - &Base::from(1u8); + let integral = mantissa.into_num().minus_one(); // The fraction's groups, each opened by a set continuation bit; the // stream's one clear closing bit ends the loop. Group bytes stay plain // `u8`s until the single width-metered materialization below. @@ -673,16 +796,19 @@ pub(crate) fn decode_stream(next_byte: impl FnMut() -> Result) -> Re // image's value shifted right by the sub-byte pad. The // `integral << exp` spelling is not available at every scale this // decoder accepts: on a 32-bit target `exp` outruns `usize` from - // ~604 MB of input while the numerator itself still fits the - // backend. Leading zero bytes are stripped before materializing - // because the backend sizes its buffer from the image's byte count, - // and a fraction opening with zero expansion bits would otherwise - // pay capacity for value it does not carry. + // ~604 MB of input. Leading zero bytes are stripped before + // materializing because the backend sizes its buffer from the + // image's byte count, and a fraction opening with zero expansion + // bits would otherwise pay capacity for value it does not carry. + // The materialization lands on the canonical arm: an image wider + // than the backend's capacity (~604 MB of input on a 32-bit + // target) assembles as the wide arm's limbs, bounded only by + // memory. let mut image = integral.to_be_bytes(); image.extend_from_slice(&groups); drop(groups); let lead = image.iter().take_while(|&&byte| byte == 0).count(); - Base::from_be_bytes(&image[lead..]) >> pad + Num::materialize_be(&image[lead..], pad) }; debug_assert!( exp == 0 || num.bit(0), @@ -730,12 +856,19 @@ impl BitSink { self.bytes } - /// The pushed bits as a magnitude, MSB-first: the final byte's zero padding - /// is stripped by one shift, and the materialization rides the - /// width-metered assembly ([`Base::from_be_bytes`]). - fn into_base(self) -> Base { + /// The pushed bits as a magnitude, MSB-first, on the numerator's + /// canonical arm. + /// + /// The final byte's zero padding is stripped by one shift, and the + /// materialization rides the width-metered assembly + /// ([`Num::materialize_be`]). + /// + /// The caller's first pushed bit is set (the mantissa's implied + /// leading one), which is the materialization's no-leading-zero-byte + /// contract. + fn into_num(self) -> Num { let pad = if self.used == 0 { 0 } else { 8 - self.used }; - Base::from_be_bytes(&self.bytes) >> u32::from(pad) + Num::materialize_be(&self.bytes, u32::from(pad)) } } @@ -769,7 +902,7 @@ impl Ord for Rank { let class = |r: &Rank| i128::from(r.num.bits()) - i128::from(r.exp); class(self) .cmp(&class(other)) - .then_with(|| Base::msb_cmp(&self.num, &other.num)) + .then_with(|| Num::msb_cmp(&self.num, &other.num)) } } @@ -804,21 +937,24 @@ impl PartialOrd for Rank { impl Add<&Rank> for &Rank { type Output = Rank; fn add(self, rhs: &Rank) -> Rank { - // The alignment shift materializes `num · 2^gap`, so its width is - // the aligned numerator's own: on a 32-bit target a gap at or past - // `usize` (2^32) names a result past the big-integer backend's - // representable width — the shift's checked conversion and the - // backend's capacity assert bound the same values and both fail - // loudly, never silently — while every gap below that fits the - // conversion. Exponents themselves stay far smaller on honest - // inputs: a decoded rank's exponent is counted from fraction bits - // actually read (under 2^35 from a whole 32-bit address space of - // input), and a version-derived exponent is bounded by its tree's - // stored bit length. + // Two exact routes, split by [`backend_alignment_fits`]: the + // backend's shift-and-add wherever it can materialize both aligned + // numerators and the possible carry bit, the streaming accumulator + // past that. The split is pure routing — both routes compute the + // identical exact sum — so the backend keeps the common case (on + // 64-bit targets, every case below allocatable memory) at its + // historical cost, and a 32-bit target's wide sums (a gap at or + // past `usize`, or an aligned width past the backend's capacity) + // are values priced by memory instead of backend panics. let e = self.exp.max(rhs.exp); - let a = self.num.clone() << (e - self.exp); - let b = rhs.num.clone() << (e - rhs.exp); - Rank::from_raw(a + &b, e) + if let (Num::Base(a), Num::Base(b)) = (&self.num, &rhs.num) { + if backend_alignment_fits(a, self.exp, b, rhs.exp, e, 1) { + let a = a.clone() << (e - self.exp); + let b = b.clone() << (e - rhs.exp); + return Rank::from_raw(a + &b, e); + } + } + accumulate(self, rhs, e, false) } } @@ -889,7 +1025,10 @@ fn sum_ranks, I: Iterator>(iter: I) -> R // 2^35 even if a whole 32-bit address space were one fraction — and a // version-derived exponent is bounded by its tree's stored bit length // (under 2^32, the storage bound), so the documented panic is - // unreachable from this fold. + // unreachable from this fold. Summands enter at the width their arm + // stores — the wide arm through the streaming limb entry — and the + // readout streams back as limbs onto the canonical arm, so no backend + // width bounds the total. let mut acc = Accumulator::new(); let mut exp = 0u64; for rank in iter { @@ -898,15 +1037,16 @@ fn sum_ranks, I: Iterator>(iter: I) -> R acc.shl(rank.exp - exp); exp = rank.exp; } - acc.add_magnitude_shl(&rank.num, exp - rank.exp); + rank.num.fold_into(&mut acc, exp - rank.exp, false); } - let (sign, magnitude) = acc.sign_magnitude(); + let (sign, limbs) = acc.sign_limbs(); debug_assert_ne!( sign, Ordering::Less, "a sum of nonnegative ranks is nonnegative" ); - Rank::from_raw(Base::from(magnitude), exp) + drop(acc); + Rank::from_num(Num::from_limbs(limbs), exp) } /// [`Rank::ZERO`], the additive identity. @@ -921,7 +1061,12 @@ impl Default for Rank { /// /// # Complexity /// -/// Superlinear, subquadratic in the rank's width: decimal conversion. +/// Superlinear, subquadratic in the rank's width: decimal conversion. (A +/// numerator wider than the big-integer backend's capacity — reachable +/// only on 32-bit targets, from hundreds of megabytes of decoded input — +/// renders by schoolbook long division instead, quadratic in the width: +/// exact at any width memory admits, at the honest price of exactness past +/// the backend's reach.) /// #[doc = include_str!(concat!(env!("OUT_DIR"), "/fuelscapes/rank_display.html"))] /// diff --git a/crates/before/src/version/rank/num.rs b/crates/before/src/version/rank/num.rs new file mode 100644 index 00000000..f52a8d5f --- /dev/null +++ b/crates/before/src/version/rank/num.rs @@ -0,0 +1,650 @@ +//! The rank numerator's two-arm storage: the backend magnitude while the +//! backend can represent it, a raw limb vector past that. +//! +//! # Why two arms exist +//! +//! The big-integer backend deliberately caps a magnitude's buffer at +//! `usize::MAX / word-bits` words so its internal bit counts always fit a +//! `usize`. On 64-bit targets that ceiling (2⁶⁴ − 64 bits) sits +//! astronomically past allocatable memory and never binds. On a 32-bit +//! target it is 2³² − 32 bits — about 512 MiB of value in a 4 GiB address +//! space — and the rank wire door can honestly outgrow it: the fraction +//! form reaches a wider numerator from ~604 MB of input, and the integral +//! form from ~512 MiB, both loud backend panics rather than values without +//! this module. [`Num`] closes that gap: the [`Base`] arm keeps the +//! backend as the arithmetic engine of record everywhere it can represent +//! the value, and the [`Wide`] arm stores the sliver it cannot — bounded +//! only by memory — implementing exactly the operation set rank arithmetic +//! needs (byte assembly, bit reads, right shifts, ±1, MSB-window +//! comparison, and limb streaming into the accumulator). +//! +//! # Canonical arm dispatch +//! +//! Every constructor normalizes through the one ceiling +//! ([`arm_ceiling_bits`]): a value is stored [`Wide`] **iff** its bit width +//! exceeds the ceiling. Canonical dispatch is what lets `Rank` keep its +//! derived structural equality and hashing as value equality — equal +//! values are always the same arm — and it makes the arm choice pure +//! routing: both arms denote the same integers exactly, so a misplaced +//! ceiling could misroute cost, never value. The production ceiling is the +//! backend capacity itself, held to the real backend by the wasm32 +//! boundary pins (the below/at-capacity decode pins fill the backend's +//! last word on the [`Base`] arm; the past-capacity pins decode on the +//! [`Wide`] arm); tests may lower it (the test-only `ceiling` module) so +//! every public door drives both arms and the seam between them at +//! host-friendly sizes. +//! +//! # Metering +//! +//! Wide-arm work records into the same limb meter as [`Base`]'s own +//! operations, under the same denomination: 64-bit limbs of operand (and, +//! for materializations, result) value width, independent of which arm +//! stores the value. Base-arm operations delegate to [`Base`]'s already +//! metered methods, so a value below the ceiling meters exactly as it did +//! when [`Base`] was the numerator's only storage. Work routed through the +//! accumulator (`Rank`'s wide-path addition and subtraction) is priced by +//! suanpan's digit-touch meter; the limb meter records those operations' +//! materializations (operands streamed in, the result read out), not the +//! digit engine's internals. + +use core::cmp::Ordering; + +use dashu_int::UBig; +use suanpan::{Accumulator, Limbs}; + +use crate::codec::base::{msb_cmp_windows, MsbWindows}; +use crate::codec::Base; + +/// Record `limbs` 64-bit limbs of wide-arm work into the limb meter. +/// +/// Compiles to nothing without the `limb-meter` feature, so wide-arm +/// operations call it unconditionally — the same shape as the shims in +/// `codec::base`. +#[inline(always)] +fn meter_wide(limbs: u64) { + #[cfg(feature = "limb-meter")] + crate::codec::base::limb_meter::record(limbs); + #[cfg(not(feature = "limb-meter"))] + let _ = limbs; +} + +/// The backend's magnitude capacity in bits: the widest value a [`Base`] +/// can hold on this target. +/// +/// Derived from the backend's own buffer cap of `usize::MAX / word-bits` +/// words (each word `Word::BITS` bits): 2³² − 32 on 32-bit targets, +/// 2⁶⁴ − 64 on 64-bit ones. The constant is a *routing* bound, not a +/// correctness bound — both arms compute exact values, so only its upper +/// side is load-bearing (a ceiling above the true capacity would let the +/// [`Base`] arm hand the backend a value it panics on), and that side is +/// held to the real backend where it is reachable: the wasm32 boundary +/// pins decode at exactly this width on the [`Base`] arm and one fraction +/// group past it on the [`Wide`] arm. +pub(crate) const BACKEND_CAPACITY_BITS: u64 = + (usize::MAX / dashu_int::Word::BITS as usize) as u64 * dashu_int::Word::BITS as u64; + +/// The arm ceiling in force: values at most this many bits wide store as +/// [`Base`], wider ones as [`Wide`]. +/// +/// In production this is [`BACKEND_CAPACITY_BITS`]; under test an +/// override (the test-only `ceiling` module) may lower it so host-scale +/// inputs drive the wide arm through the public doors. +#[inline] +pub(crate) fn arm_ceiling_bits() -> u64 { + #[cfg(test)] + if let Some(bits) = ceiling::override_bits() { + return bits; + } + BACKEND_CAPACITY_BITS +} + +/// The test-only arm-ceiling override: a scoped, thread-local lowering +/// of [`arm_ceiling_bits`]. +/// +/// The lowering makes the wide arm — honestly reachable only past ~2³² +/// bits on a 32-bit target — drivable through the public doors at +/// host-friendly sizes. +/// +/// The override changes routing only, never values: both arms are exact, +/// so every suite that runs under a lowered ceiling checks the same +/// value-level contracts production serves. Values built under one ceiling +/// must not outlive its guard — canonical arm dispatch is relative to the +/// ceiling in force. +#[cfg(test)] +pub(crate) mod ceiling { + use std::cell::Cell; + + thread_local! { + static OVERRIDE: Cell> = const { Cell::new(None) }; + } + + /// The override in force on this thread, if any. + pub(crate) fn override_bits() -> Option { + OVERRIDE.with(Cell::get) + } + + /// Lower the arm ceiling to `bits` until the guard drops. + pub(crate) fn force(bits: u64) -> Guard { + Guard(OVERRIDE.with(|cell| cell.replace(Some(bits)))) + } + + /// Restores the previous ceiling on drop, so forced scopes nest. + pub(crate) struct Guard(Option); + + impl Drop for Guard { + fn drop(&mut self) { + OVERRIDE.with(|cell| cell.set(self.0)); + } + } +} + +/// A rank numerator: the backend magnitude while the backend can hold it, +/// a raw limb vector past that. +/// +/// Canonical by construction (the module doc's dispatch invariant), so the +/// derived equality and hash are value equality and every consumer may +/// match on the arm as a width fact. +#[derive(Clone, Debug)] +pub(crate) enum Num { + /// At most [`arm_ceiling_bits`] bits: the backend arm, whose + /// operations are [`Base`]'s own metered methods. + Base(Base), + /// Strictly more than [`arm_ceiling_bits`] bits: the limb arm. + Wide(Wide), +} + +/// A magnitude wider than the backend arm's ceiling, as little-endian +/// 64-bit limbs. +/// +/// Invariants: the top limb is nonzero (minimal spelling — what makes the +/// derived equality value equality), and the bit width exceeds the arm +/// ceiling in force (canonical dispatch; [`Num`]'s constructors enforce +/// it). Never zero. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct Wide { + limbs: Vec, +} + +impl Num { + pub(crate) const ZERO: Num = Num::Base(Base::ZERO); + + /// The magnitude's bit length: zero for zero, `floor(log2 n) + 1` + /// otherwise. O(1), unmetered — a stored-width read, like + /// [`Base::bits`]. + pub(crate) fn bits(&self) -> u64 { + match self { + Num::Base(base) => base.bits(), + Num::Wide(wide) => wide.bits(), + } + } + + /// Bit `i` of the magnitude (bit 0 the least significant): O(1), + /// unmetered. A position past the width reads zero. + pub(crate) fn bit(&self, i: u64) -> bool { + match self { + Num::Base(base) => base.bit(i), + Num::Wide(wide) => wide.bit(i), + } + } + + /// The number of trailing zero bits, or [`None`] for zero: the + /// normalization read. Width-scale (a bottom-up limb scan), metered + /// on the operand's width in both arms. + pub(crate) fn trailing_zeros(&self) -> Option { + match self { + Num::Base(base) => base.trailing_zeros(), + Num::Wide(wide) => Some(wide.trailing_zeros()), + } + } + + /// The magnitude shifted right by `n`, re-dispatched onto the + /// canonical arm. + /// + /// A wide value whose shift lands at or below the ceiling comes back + /// as [`Base`]. Width-scale, metered on the operand in both arms; + /// total — a shift at or past the width yields zero. + pub(crate) fn shr(self, n: u64) -> Num { + match self { + // The base arm can only shrink, so it stays canonical with no + // re-dispatch — including the shift-by-zero spelling, which + // keeps this arm's cost and metering exactly the historical + // numerator path's. + Num::Base(base) => Num::Base(base >> n), + Num::Wide(wide) => { + if n == 0 { + meter_wide(wide.limb_count()); + return Num::Wide(wide); + } + let shifted = wide.shr_limbs(n); + Num::from_limbs(shifted) + } + } + } + + /// The magnitude plus one (the encoder's `⌊r⌋ + 1` bias), re-dispatched + /// onto the canonical arm: a carry out of the base arm's last + /// representable bit crosses to wide. + /// + /// Base-arm values strictly below the ceiling take the backend's own + /// metered `+ 1`; a base value exactly at the ceiling (where the + /// backend could not hold a carried result) and every wide value go + /// through the limb spelling, metered on the operand's width. + pub(crate) fn plus_one(self) -> Num { + match self { + Num::Base(base) if base.bits() < arm_ceiling_bits() => Num::Base(base + 1u32), + Num::Base(base) => { + // At the ceiling exactly: the backend may not survive the + // carry, so the increment runs in limb space and + // re-dispatches (an all-ones value grows one bit, past the + // ceiling; anything else stays base). + meter_wide(base.bits().div_ceil(64).max(1)); + Num::from_limbs(increment(Limbs::new(&base.0).collect())) + } + Num::Wide(wide) => { + meter_wide(wide.limb_count()); + Num::from_limbs(increment(wide.limbs)) + } + } + } + + /// The magnitude minus one (the decoder's bias removal), re-dispatched + /// onto the canonical arm: a wide power of two can shrink back to the + /// base arm. + /// + /// The caller guarantees the value is at least one (the decoder's + /// biased mantissa always is). + pub(crate) fn minus_one(self) -> Num { + match self { + Num::Base(base) => Num::Base(base - &Base::from(1u8)), + Num::Wide(mut wide) => { + meter_wide(wide.limb_count()); + for limb in wide.limbs.iter_mut() { + let (next, borrowed) = limb.overflowing_sub(1); + *limb = next; + if !borrowed { + break; + } + // A borrow rewrites the limb to all-ones and keeps + // borrowing upward; the invariant value ≥ 1 (indeed, + // wide values exceed the ceiling) means the borrow + // always terminates before running off the top. + } + Num::from_limbs(wide.limbs) + } + } + } + + /// Order two numerators as MSB-aligned bit strings (the class-tie + /// comparison behind `Rank`'s [`Ord`]): the order of `a · 2^x` versus + /// `b · 2^y` whenever the two share a magnitude class. + /// + /// Same-arm base pairs take [`Base::msb_cmp`] — the historical path, + /// metering included; every other pairing streams both arms' windows + /// through the same shared kernel, so the tail rule and the per-window + /// metering are one implementation across arms. + pub(crate) fn msb_cmp(a: &Num, b: &Num) -> Ordering { + match (a, b) { + (Num::Base(x), Num::Base(y)) => Base::msb_cmp(x, y), + (Num::Base(x), Num::Wide(y)) => msb_cmp_windows(x.msb_windows(), y.msb_windows()), + (Num::Wide(x), Num::Base(y)) => msb_cmp_windows(x.msb_windows(), y.msb_windows()), + (Num::Wide(x), Num::Wide(y)) => msb_cmp_windows(x.msb_windows(), y.msb_windows()), + } + } + + /// The magnitude's minimal big-endian bytes: empty for zero, no + /// leading zero byte otherwise. Width-scale, metered on the operand in + /// both arms (the decoder's image-assembly read). + pub(crate) fn to_be_bytes(&self) -> Vec { + match self { + Num::Base(base) => base.to_be_bytes(), + Num::Wide(wide) => { + meter_wide(wide.limb_count()); + let mut bytes: Vec = Vec::with_capacity(wide.limbs.len() * 8); + for limb in wide.limbs.iter().rev() { + bytes.extend_from_slice(&limb.to_be_bytes()); + } + let lead = bytes.iter().take_while(|&&byte| byte == 0).count(); + bytes.drain(..lead); + bytes + } + } + } + + /// Materialize `BE(bytes) >> pad` onto the canonical arm. + /// + /// The decoder's one value-materialization point: `bytes` carry no + /// leading zero byte (the caller strips them — the backend sizes + /// buffers from the image's byte count, and zeros would pay capacity + /// for value they don't carry), and `pad < 8` is the sub-byte + /// alignment shift. Below the ceiling this is exactly the historical + /// spelling ([`Base::from_be_bytes`] then the metered sub-byte shift); + /// above it the limbs are assembled directly — no backend value ever + /// exists — with the materialization metered on the value's width, the + /// wide-decode convention: a meter that missed it would let a decoder + /// build arbitrarily wide values while recording nothing. + pub(crate) fn materialize_be(bytes: &[u8], pad: u32) -> Num { + debug_assert!(pad < 8, "pad is the sub-byte alignment"); + debug_assert!( + bytes.first() != Some(&0), + "the caller strips leading zero bytes" + ); + let bits = (bytes.len() as u64 * 8) + .saturating_sub(u64::from( + bytes.first().map_or(8, |byte| byte.leading_zeros()), + )) + .saturating_sub(u64::from(pad)); + if bits <= arm_ceiling_bits() { + return Num::Base(Base::from_be_bytes(bytes) >> pad); + } + meter_wide(bits.div_ceil(64)); + // LE limbs from the BE image: 8-byte chunks off the tail, the + // partial head chunk last, then the sub-byte shift in place. + let mut limbs: Vec = Vec::with_capacity(bytes.len().div_ceil(8)); + let mut chunks = bytes.rchunks_exact(8); + for chunk in chunks.by_ref() { + limbs.push(u64::from_be_bytes( + chunk.try_into().expect("an exact chunk"), + )); + } + let head = chunks.remainder(); + if !head.is_empty() { + let mut top = [0u8; 8]; + top[8 - head.len()..].copy_from_slice(head); + limbs.push(u64::from_be_bytes(top)); + } + if pad > 0 { + for i in 0..limbs.len() { + let high = limbs.get(i + 1).copied().unwrap_or(0); + limbs[i] = (limbs[i] >> pad) | (high << (64 - pad)); + } + } + Num::from_limbs(limbs) + } + + /// Dispatch little-endian limbs (the accumulator readout's spelling) + /// onto the canonical arm. High zero limbs are stripped; empty (or + /// all-zero) limbs are zero. + /// + /// The base arm materializes through the backend's byte constructor + /// unmetered — the readout that produced the limbs already carries the + /// pass's cost — while a wide materialization records the value's + /// width, the same convention as [`Num::materialize_be`]. + pub(crate) fn from_limbs(mut limbs: Vec) -> Num { + while limbs.last() == Some(&0) { + limbs.pop(); + } + let wide = Wide { limbs }; + if wide.bits() <= arm_ceiling_bits() { + // Exact-capacity byte image, and the limb vector dropped + // before the backend materializes: at the seam's widest + // crossings (a borrow falling back from one bit past a 32-bit + // target's capacity) the value is ~512 MiB, so an amortized + // growth double or one extra live copy is the difference + // between fitting the address space and an honest exhaustion. + let mut bytes: Vec = Vec::with_capacity(wide.limbs.len() * 8); + for limb in &wide.limbs { + bytes.extend_from_slice(&limb.to_le_bytes()); + } + drop(wide); + // The top limb's padding must go: the backend sizes its buffer + // from the image's byte count, so at the seam's widest + // crossings the high zero bytes alone would push the word + // count past its capacity while the value fits exactly. + while bytes.last() == Some(&0) { + bytes.pop(); + } + return Num::Base(Base::from(UBig::from_le_bytes(&bytes))); + } + meter_wide(wide.limb_count()); + Num::Wide(wide) + } + + /// Re-dispatch a backend-held value onto the canonical arm. + /// + /// In production the base arm's ceiling is the backend's own capacity, + /// so the conversion never fires — a backend value above the backend's + /// capacity cannot exist — and every historical `Base` numerator passes + /// through unchanged, unmetered. Under a lowered test ceiling this is + /// where fold outputs and raw constructions cross into the wide arm. + pub(crate) fn from_base(base: Base) -> Num { + if base.bits() <= arm_ceiling_bits() { + return Num::Base(base); + } + meter_wide(base.bits().div_ceil(64)); + Num::Wide(Wide { + limbs: Limbs::new(&base.0).collect(), + }) + } + + /// Fold `±self · 2^shift` into an accumulator, at the width this arm + /// stores: the base arm through the magnitude dispatch, the wide arm + /// through the streaming limb entry. + /// + /// The accumulator's digit-touch meter prices the fold; the limb + /// meter records the operand's width here, so limb-denominated + /// envelopes see the operand materialization whichever arm streams it. + pub(crate) fn fold_into(&self, acc: &mut Accumulator, shift: u64, subtract: bool) { + match self { + Num::Base(base) => { + if subtract { + acc.sub_magnitude_shl(base, shift); + } else { + acc.add_magnitude_shl(base, shift); + } + } + Num::Wide(wide) => { + meter_wide(wide.limb_count()); + if subtract { + acc.sub_limbs_shl(wide.limbs.iter().copied(), shift); + } else { + acc.add_limbs_shl(wide.limbs.iter().copied(), shift); + } + } + } + } + + /// The magnitude's minimal little-endian bytes, for the test oracles' + /// backend-independent reconstruction. + #[cfg(test)] + pub(crate) fn to_bytes_le(&self) -> Vec { + match self { + Num::Base(base) => base.to_bytes_le(), + Num::Wide(wide) => { + let mut bytes: Vec = wide + .limbs + .iter() + .flat_map(|limb| limb.to_le_bytes()) + .collect(); + while bytes.last() == Some(&0) { + bytes.pop(); + } + bytes + } + } + } + + /// Whether this numerator is stored on the wide arm, for the + /// canonicity assertions. + #[cfg(test)] + pub(crate) fn is_wide(&self) -> bool { + matches!(self, Num::Wide(_)) + } +} + +// Manual equality so the base arm keeps [`Base`]'s metered comparison and +// the wide arm records the same two-operand convention; canonical arm +// dispatch makes cross-arm values unequal by construction, recorded at the +// same widths for uniformity. Equality of equal values is structural in +// both arms, so the derived `Hash` on `Wide` and [`Base`]'s own metered +// `Hash` stay consistent with this. +impl PartialEq for Num { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Num::Base(a), Num::Base(b)) => a == b, + (Num::Wide(a), Num::Wide(b)) => { + meter_wide(a.limb_count() + b.limb_count()); + a == b + } + (a, b) => { + meter_wide(a.bits().div_ceil(64).max(1) + b.bits().div_ceil(64).max(1)); + false + } + } + } +} + +impl Eq for Num {} + +/// Renders the exact decimal value, honoring integer padding flags. +/// +/// The base arm is the backend's own (subquadratic) conversion; the wide +/// arm is schoolbook long division by 10¹⁹ — quadratic in the value's +/// width, the honest price of exact decimal past the backend's reach, and +/// metered as one record of the operand's width per emitted 19-digit group +/// (the division pass that produced it). +impl core::fmt::Display for Num { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Num::Base(base) => core::fmt::Display::fmt(base, f), + Num::Wide(wide) => f.pad_integral(true, "", &wide.to_decimal()), + } + } +} + +impl core::hash::Hash for Num { + fn hash(&self, state: &mut H) { + match self { + Num::Base(base) => base.hash(state), + Num::Wide(wide) => { + meter_wide(wide.limb_count()); + wide.hash(state); + } + } + } +} + +impl Wide { + /// The bit width: `64·(limbs − 1) + width(top limb)`. O(1) — the top + /// limb is nonzero by invariant. + fn bits(&self) -> u64 { + match self.limbs.last() { + None => 0, + Some(top) => { + debug_assert_ne!(*top, 0, "the top limb is nonzero"); + self.limbs.len() as u64 * 64 - u64::from(top.leading_zeros()) + } + } + } + + /// The stored limb count, the meter denomination. + fn limb_count(&self) -> u64 { + self.limbs.len() as u64 + } + + /// Bit `i`, zero past the width. + fn bit(&self, i: u64) -> bool { + match usize::try_from(i / 64) { + Ok(index) => self + .limbs + .get(index) + .is_some_and(|limb| limb >> (i % 64) & 1 == 1), + // A bit index past `usize` can only address zeros: the stored + // limbs are `usize`-indexed. + Err(_) => false, + } + } + + /// The trailing zero count: a bottom-up limb scan, metered on the + /// operand's width. Never `None` — a wide value is nonzero. + fn trailing_zeros(&self) -> u64 { + meter_wide(self.limb_count()); + for (index, limb) in self.limbs.iter().enumerate() { + if *limb != 0 { + return index as u64 * 64 + u64::from(limb.trailing_zeros()); + } + } + unreachable!("a wide value is nonzero by invariant"); + } + + /// The limbs of `self >> n`, minimal at the top: the shift's + /// width-scale work, metered on the operand. + fn shr_limbs(&self, n: u64) -> Vec { + meter_wide(self.limb_count()); + let Ok(whole) = usize::try_from(n / 64) else { + // A shift amount past `usize` limbs exceeds the stored width + // (which is `usize`-indexed): the result is zero. + return Vec::new(); + }; + if whole >= self.limbs.len() { + return Vec::new(); + } + let bit = (n % 64) as u32; + let mut limbs: Vec = Vec::with_capacity(self.limbs.len() - whole); + for i in whole..self.limbs.len() { + let low = self.limbs[i] >> bit; + let high = if bit == 0 { + 0 + } else { + self.limbs.get(i + 1).copied().unwrap_or(0) << (64 - bit) + }; + limbs.push(low | high); + } + limbs + } + + /// The MSB-first 64-bit windows of the bit string (the comparison + /// stream). + fn msb_windows(&self) -> MsbWindows + '_> { + MsbWindows::new(self.limbs.iter().rev().copied(), self.bits()) + } + + /// The exact decimal rendering: schoolbook long division by 10¹⁹, + /// quadratic in the width, one operand-width meter record per pass. + fn to_decimal(&self) -> String { + /// The largest power of ten in a limb: each division pass peels + /// 19 decimal digits. + const TEN_POW_19: u128 = 10_000_000_000_000_000_000; + let mut current = self.limbs.clone(); + // 19-digit groups, least significant first. + let mut groups: Vec = Vec::new(); + while !current.is_empty() { + meter_wide(current.len() as u64); + let mut remainder: u128 = 0; + for limb in current.iter_mut().rev() { + let carried = (remainder << 64) | u128::from(*limb); + *limb = (carried / TEN_POW_19) as u64; + remainder = carried % TEN_POW_19; + } + while current.last() == Some(&0) { + current.pop(); + } + groups.push(remainder as u64); + } + let mut rendered = String::new(); + for (index, group) in groups.iter().enumerate().rev() { + if index == groups.len() - 1 { + rendered.push_str(&group.to_string()); + } else { + rendered.push_str(&format!("{group:019}")); + } + } + debug_assert!(!rendered.is_empty(), "a wide value is nonzero"); + rendered + } +} + +/// Little-endian limbs plus one, growing by a limb on a full carry. +fn increment(mut limbs: Vec) -> Vec { + for limb in limbs.iter_mut() { + let (next, carried) = limb.overflowing_add(1); + *limb = next; + if !carried { + return limbs; + } + } + limbs.push(1); + limbs +} + +#[cfg(test)] +mod tests; diff --git a/crates/before/src/version/rank/num/tests.rs b/crates/before/src/version/rank/num/tests.rs new file mode 100644 index 00000000..031953e8 --- /dev/null +++ b/crates/before/src/version/rank/num/tests.rs @@ -0,0 +1,191 @@ +//! Unit suites for the numerator's two-arm storage: every wide-arm +//! operation differentially against the backend as oracle. +//! +//! The host backend's ceiling is astronomically past memory, so under a +//! lowered test ceiling ([`ceiling::force`]) every wide-arm value here is +//! *also* representable as a [`UBig`] — which makes the backend itself the +//! exact oracle for the arm that exists because the backend (on 32-bit +//! targets, at production widths) cannot hold the value. The value-level +//! rank suites (`version/tests.rs`) drive the same arms through the public +//! doors; these tests pin the arm mechanics — dispatch, assembly, shifts, +//! bias steps, windows, and rendering — one operation at a time. + +use dashu_int::ops::BitTest; +use dashu_int::UBig; +use proptest::prelude::*; + +use super::*; + +/// The lowered ceiling every suite here forces: small enough that a few +/// dozen bytes cross it, large enough that both arms and the seam get +/// populated by the generators. +const TEST_CEILING_BITS: u64 = 96; + +/// A `Num` from oracle bytes (little-endian), through the canonical +/// dispatch. +fn num_from_oracle(value: &UBig) -> Num { + let bytes = value.to_be_bytes(); + let lead = bytes.iter().take_while(|&&byte| byte == 0).count(); + Num::materialize_be(&bytes[lead..], 0) +} + +/// The oracle value a `Num` denotes, reconstructed from its test bytes. +fn oracle_of(num: &Num) -> UBig { + UBig::from_le_bytes(&num.to_bytes_le()) +} + +/// Arbitrary oracle values straddling the test ceiling: sub-word, near +/// the ceiling on both sides, and several limbs past it. +fn arb_value() -> impl Strategy { + proptest::collection::vec(any::(), 1..=6).prop_map(|limbs| { + let bytes: Vec = limbs.iter().flat_map(|limb| limb.to_le_bytes()).collect(); + UBig::from_le_bytes(&bytes) + }) +} + +proptest! { + /// Materialization dispatches on the canonical arm — wide exactly + /// when the value outgrows the ceiling — and denotes the input value + /// exactly, at every sub-byte pad. + #[test] + fn materialize_is_exact_and_canonical(value in arb_value(), pad in 0u32..8) { + let _guard = ceiling::force(TEST_CEILING_BITS); + // The pad contract: the low `pad` bits of the image are the + // value's dropped zeros, so build the image as `value << pad`. + let image = value.clone() << (pad as usize); + let bytes = image.to_be_bytes(); + let lead = bytes.iter().take_while(|&&byte| byte == 0).count(); + let num = Num::materialize_be(&bytes[lead..], pad); + prop_assert_eq!(oracle_of(&num), value.clone()); + prop_assert_eq!(num.is_wide(), value.bit_len() as u64 > TEST_CEILING_BITS); + prop_assert_eq!(num.bits(), value.bit_len() as u64); + } + + /// `shr` equals the oracle's shift at every amount up to past the + /// width, and the result re-dispatches onto the canonical arm (a wide + /// value shrinking below the ceiling comes back as the base arm). + #[test] + fn shr_matches_the_oracle_and_redispatches(value in arb_value(), amount in 0u64..512) { + let _guard = ceiling::force(TEST_CEILING_BITS); + let num = num_from_oracle(&value); + let shifted = num.shr(amount); + let expected = value >> usize::try_from(amount).unwrap(); + prop_assert_eq!(oracle_of(&shifted), expected.clone()); + prop_assert_eq!(shifted.is_wide(), expected.bit_len() as u64 > TEST_CEILING_BITS); + } + + /// The bias steps are exact inverses across the arm seam: `plus_one` + /// then `minus_one` is the identity, each matches the oracle, and + /// each lands on the canonical arm. + #[test] + fn bias_steps_match_the_oracle(value in arb_value()) { + let _guard = ceiling::force(TEST_CEILING_BITS); + let num = num_from_oracle(&value); + let up = num.clone().plus_one(); + prop_assert_eq!(oracle_of(&up), value.clone() + 1u8); + prop_assert_eq!(up.is_wide(), (value.clone() + 1u8).bit_len() as u64 > TEST_CEILING_BITS); + let down = up.minus_one(); + prop_assert_eq!(oracle_of(&down), value.clone()); + prop_assert_eq!(&down, &num); + } + + /// Bit reads, widths, and trailing zeros agree with the oracle on + /// both arms. + #[test] + fn bit_reads_match_the_oracle(value in arb_value(), probe in 0u64..400) { + let _guard = ceiling::force(TEST_CEILING_BITS); + let num = num_from_oracle(&value); + prop_assert_eq!(num.bits(), value.bit_len() as u64); + prop_assert_eq!(num.bit(probe), usize::try_from(probe).is_ok_and(|i| value.bit(i))); + prop_assert_eq!(num.trailing_zeros(), value.trailing_zeros().map(|n| n as u64)); + } + + /// The class-tie window comparison agrees with the oracle's numeric + /// order under MSB alignment, across every arm pairing. + /// + /// Alignment: `msb_cmp` compares `a` and `b` as MSB-aligned bit + /// strings, which is the numeric order of `a · 2^(width(b))` versus + /// `b · 2^(width(a))` — the oracle spelled with materialized shifts. + #[test] + fn msb_cmp_matches_the_aligned_oracle(a in arb_value(), b in arb_value()) { + let _guard = ceiling::force(TEST_CEILING_BITS); + // Odd operands: the tail rule's normalization premise (the + // stored numerator invariant). + let (a, b) = (a | UBig::ONE, b | UBig::ONE); + let na = num_from_oracle(&a); + let nb = num_from_oracle(&b); + let aligned_a = a.clone() << b.bit_len(); + let aligned_b = b.clone() << a.bit_len(); + prop_assert_eq!(Num::msb_cmp(&na, &nb), aligned_a.cmp(&aligned_b)); + } + + /// The decimal rendering equals the oracle's on both arms — the wide + /// arm's long division against the backend's own conversion. + #[test] + fn decimal_rendering_matches_the_oracle(value in arb_value()) { + let _guard = ceiling::force(TEST_CEILING_BITS); + let num = num_from_oracle(&value); + prop_assert_eq!(format!("{num}"), format!("{value}")); + } + + /// Structural equality and hashing are value equality across the + /// dispatch: equal values are one arm and equal, unequal values are + /// unequal whatever their arms. + #[test] + fn equality_is_value_equality(a in arb_value(), b in arb_value()) { + let _guard = ceiling::force(TEST_CEILING_BITS); + let na = num_from_oracle(&a); + let nb = num_from_oracle(&b); + prop_assert_eq!(na == nb, a == b); + prop_assert_eq!(&na, &na.clone()); + } +} + +/// The limb increment grows by a limb on a full carry and is exact at +/// the all-ones corners the proptest families essentially never draw. +#[test] +fn increment_carries_across_full_limbs() { + assert_eq!(increment(vec![u64::MAX]), vec![0, 1]); + assert_eq!(increment(vec![u64::MAX, u64::MAX]), vec![0, 0, 1]); + assert_eq!(increment(vec![u64::MAX, 7]), vec![0, 8]); + assert_eq!(increment(vec![5]), vec![6]); +} + +/// The arm seam's exact corners: a base value at the ceiling crosses to +/// wide on `plus_one` only when the carry outgrows the ceiling, and the +/// wide power of two at the ceiling's edge falls back to base on +/// `minus_one`. +#[test] +fn arm_seam_corners_redispatch_exactly() { + let _guard = ceiling::force(TEST_CEILING_BITS); + // All ones at the ceiling: the carry crosses the seam upward. + let all_ones = (UBig::ONE << usize::try_from(TEST_CEILING_BITS).unwrap()) - 1u8; + let num = num_from_oracle(&all_ones); + assert!(!num.is_wide(), "at the ceiling exactly: base arm"); + let carried = num.plus_one(); + assert!(carried.is_wide(), "the carry outgrows the ceiling"); + assert_eq!(oracle_of(&carried), all_ones + 1u8); + // The power of two just past the ceiling: minus one falls back. + let back = carried.minus_one(); + assert!(!back.is_wide(), "the borrow re-dispatches downward"); + // A ceiling-width value whose increment does not carry stays base. + let even = UBig::ONE << usize::try_from(TEST_CEILING_BITS - 1).unwrap(); + let stays = num_from_oracle(&even).plus_one(); + assert!(!stays.is_wide(), "no carry, no crossing"); + assert_eq!(oracle_of(&stays), even + 1u8); +} + +/// `from_limbs` strips high zero limbs, reads empty as zero, and +/// dispatches canonically — the accumulator readout's contract. +#[test] +fn from_limbs_normalizes_and_dispatches() { + let _guard = ceiling::force(TEST_CEILING_BITS); + assert_eq!(Num::from_limbs(vec![]), Num::ZERO); + assert_eq!(Num::from_limbs(vec![0, 0]), Num::ZERO); + let small = Num::from_limbs(vec![7, 0, 0]); + assert!(!small.is_wide()); + assert_eq!(oracle_of(&small), UBig::from(7u8)); + let wide = Num::from_limbs(vec![1, 0, 5, 0]); + assert!(wide.is_wide(), "129 bits exceeds the 96-bit test ceiling"); + assert_eq!(oracle_of(&wide), (UBig::from(5u8) << 128usize) + 1u8); +} diff --git a/crates/before/src/version/tests.rs b/crates/before/src/version/tests.rs index 10229655..40bbdf86 100644 --- a/crates/before/src/version/tests.rs +++ b/crates/before/src/version/tests.rs @@ -1319,9 +1319,9 @@ proptest! { let b = if extend { // A strict extension of `a`'s expansion: the genre where // one stream continues past the other's content. - let (num, exp) = a.raw_parts(); + let (num, exp) = rank_parts(&a); super::Rank::from_raw( - (num.clone() << deepen) + 1u32, + crate::codec::Base::from((num << (deepen as usize)) + 1u8), exp.saturating_add(u64::from(deepen)), ) } else { @@ -1344,6 +1344,326 @@ proptest! { } } +// ────────────────────── the numerator's wide arm ────────────────────── +// +// The wide arm is honestly reachable only past the backend's capacity — +// ~2³² bits, on 32-bit targets, from hundreds of megabytes of input — so +// these suites lower the arm ceiling (`rank::arm_ceiling::force`, a +// test-only routing override that moves no values) and drive the same +// public doors production serves: every rank built under the lowered +// ceiling straddles or crosses the seam at host-friendly sizes, while the +// host backend — whose real capacity is astronomically higher — remains an +// exact oracle for every value. The wasm32 boundary pins hold the same +// doors at the production coordinate itself. + +/// The lowered arm ceiling the wide-regime suites run under: four limbs. +/// +/// Low enough that the seeded generators cross it constantly, high enough +/// that word-scale and multi-limb base-arm values still appear beside the +/// wide ones. +const WIDE_REGIME_CEILING_BITS: u64 = 256; + +/// Assert a rank's structural invariants under the ceiling in force. +/// +/// Canonical arm dispatch (wide iff the numerator outgrows the ceiling) +/// and the normalization the order rests on (an odd numerator whenever a +/// fraction exists; zero pinned to exponent zero). +fn assert_rank_canonical(r: &super::Rank, ceiling: u64) { + let (num, exp) = r.raw_parts(); + assert_eq!( + r.numerator_is_wide(), + num.bits() > ceiling, + "canonical arm dispatch: wide iff past the ceiling ({})", + r + ); + if exp > 0 { + assert!(num.bit(0), "a fractional numerator is odd: {r}"); + } + if num.bits() == 0 { + assert_eq!(exp, 0, "zero is pinned to exponent zero"); + } +} + +/// Without an override, arm dispatch sits at the backend's own capacity: +/// every host-constructible rank is backend-armed, and the constant agrees +/// with the backend's buffer-cap formula. +/// +/// This is the production-routing sanity leg beside the lowered-ceiling +/// suites: on this host the wide arm must never engage (its honest +/// coordinate is 2⁶⁴ − 64 bits here), so the historical numerator path is +/// what every other suite in this file measures and pins. +#[test] +fn rank_arm_dispatch_defaults_to_the_backend_capacity() { + assert_eq!( + super::rank::BACKEND_CAPACITY_BITS, + (usize::MAX / dashu_int::Word::BITS as usize) as u64 * u64::from(dashu_int::Word::BITS), + ); + let mut next = crate::testing::rng::word_stream(RANK_CMP_SWEEP_SEED); + for _ in 0..64 { + assert!( + !stream_rank(&mut next).numerator_is_wide(), + "no host-constructible rank reaches the backend's capacity" + ); + } +} + +/// The class-first streamed order agrees with the alignment oracle across +/// the wide arm and the arm seam, and `checked_sub`'s pre-check stays +/// consistent: 10,000 adversarial pairs under the lowered ceiling. +/// +/// The same generator and oracle as the backend-arm sweep above; under +/// the 256-bit ceiling the pairs mix base–base, base–wide, and wide–wide +/// arms (deep shared prefixes included), so every `msb_cmp` dispatch arm +/// and the cross-arm class comparison run against the exact oracle. +/// Canonicity is asserted on every rank built. +#[test] +fn rank_wide_arm_cmp_agrees_with_the_alignment_oracle_on_10k_pairs() { + let _guard = super::rank::arm_ceiling::force(WIDE_REGIME_CEILING_BITS); + let mut next = crate::testing::rng::word_stream(RANK_CMP_SWEEP_SEED ^ 0xC0FF_EE00_D00D_F00D); + let mut wide_pairs = 0u32; + for case in 0..10_000u32 { + let a = stream_rank(&mut next); + let b = match next() % 4 { + 0 => stream_rank(&mut next), + 1 => a.clone(), + 2 => { + let (num, exp) = rank_parts(&a); + super::Rank::from_raw( + crate::codec::Base::from(num), + exp.saturating_add(next() % 64 + 1), + ) + } + _ => { + let (num, exp) = rank_parts(&a); + use dashu_int::ops::BitTest; + let flipped = num ^ (dashu_int::UBig::from(2u8) << ((next() % 16) as usize)); + let bits_kept = flipped.bit_len() as u64 == a_bits(&a); + let candidate = super::Rank::from_raw(crate::codec::Base::from(flipped), exp); + if bits_kept { + candidate + } else { + a.clone() + } + } + }; + assert_rank_canonical(&a, WIDE_REGIME_CEILING_BITS); + assert_rank_canonical(&b, WIDE_REGIME_CEILING_BITS); + if a.numerator_is_wide() && b.numerator_is_wide() { + wide_pairs += 1; + } + let want = alignment_cmp(&a, &b); + assert_eq!(a.cmp(&b), want, "case {case}: order disagrees: {a} vs {b}"); + assert_eq!( + b.cmp(&a), + want.reverse(), + "case {case}: antisymmetry breaks: {b} vs {a}" + ); + assert_eq!( + a.checked_sub(&b).is_some(), + want != core::cmp::Ordering::Less, + "case {case}: checked_sub pre-check disagrees with the order" + ); + } + assert!( + wide_pairs > 100, + "the wide–wide pairing is live: {wide_pairs} pairs" + ); +} + +proptest! { + /// Every `laws::RANK_TRIPLE` law — the monoid, order, codec + /// round-trip, lexicographic, prefix-freedom, and cross-path + /// normalization laws — holds on ranks straddling the wide arm's + /// seam. + /// + /// The same law group the backend-arm drivers run, under the lowered + /// ceiling: addition, subtraction, and `Sum` route through the + /// accumulator wherever an aligned width crosses the ceiling, and the + /// wire laws hold the wide arm's emission to the same canonical bytes + /// order discipline. + #[test] + fn rank_wide_arm_triple_laws_on_seeded_ranks(seeds in proptest::collection::vec(any::(), 3)) { + let _guard = super::rank::arm_ceiling::force(WIDE_REGIME_CEILING_BITS); + let ranks: Vec = seeds.iter().map(|&seed| seeded_rank(seed)).collect(); + let (a, b, c) = (&ranks[0], &ranks[1], &ranks[2]); + for (name, law) in crate::laws::RANK_TRIPLE { + prop_assert!(law(a, b, c), "law violated: {}", name); + } + } + + /// `Sum` is the pairwise fold on the wide arm too: over mixed-arm + /// multisets, the streaming-limb fold returns exactly the value the + /// pairwise `+` fold produces, and the result is canonical. + #[test] + fn rank_wide_arm_sum_equals_the_pairwise_fold(seeds in proptest::collection::vec(any::(), 0..16)) { + let _guard = super::rank::arm_ceiling::force(WIDE_REGIME_CEILING_BITS); + let ranks: Vec = seeds.iter().map(|&seed| seeded_rank(seed)).collect(); + let reference = ranks + .iter() + .fold(super::Rank::ZERO, |acc, r| acc + r); + let sum = ranks.iter().sum::(); + prop_assert_eq!(&sum, &reference); + assert_rank_canonical(&sum, WIDE_REGIME_CEILING_BITS); + prop_assert_eq!(&ranks.into_iter().sum::(), &reference); + } + + /// Wide-arm addition and subtraction compute the exact rational + /// values: differentially against plain backend arithmetic on the raw + /// parts, which the host backend can hold at these sizes. + /// + /// The oracle aligns both numerators to the common exponent with + /// materialized shifts, adds or subtracts, and strips shared factors + /// of two; the door values must match it exactly, and land canonical. + #[test] + fn rank_wide_arm_arithmetic_matches_the_backend_oracle(sa in any::(), sb in any::()) { + let _guard = super::rank::arm_ceiling::force(WIDE_REGIME_CEILING_BITS); + let a = seeded_rank(sa); + let b = seeded_rank(sb); + let (an, ae) = rank_parts(&a); + let (bn, be) = rank_parts(&b); + let e = ae.max(be); + let aligned_a = an << usize::try_from(e - ae).unwrap(); + let aligned_b = bn << usize::try_from(e - be).unwrap(); + + // The oracle's normalization: strip shared factors of two, pin + // zero to exponent zero. + let normalize = |num: dashu_int::UBig, exp: u64| -> (dashu_int::UBig, u64) { + match num.trailing_zeros() { + None => (num, 0), + Some(tz) => { + let shift = (tz as u64).min(exp); + (num >> usize::try_from(shift).unwrap(), exp - shift) + } + } + }; + + let sum = &a + &b; + assert_rank_canonical(&sum, WIDE_REGIME_CEILING_BITS); + let (sn, se) = rank_parts(&sum); + prop_assert_eq!((sn, se), normalize(aligned_a.clone() + &aligned_b, e), "sum: {} + {}", a, b); + + let (minuend, subtrahend, aligned_hi, aligned_lo) = if a >= b { + (&a, &b, aligned_a.clone(), aligned_b.clone()) + } else { + (&b, &a, aligned_b.clone(), aligned_a.clone()) + }; + let difference = minuend + .checked_sub(subtrahend) + .expect("the minuend dominates"); + assert_rank_canonical(&difference, WIDE_REGIME_CEILING_BITS); + let (dn, de) = rank_parts(&difference); + prop_assert_eq!( + (dn, de), + normalize(aligned_hi - aligned_lo, e), + "difference: {} - {}", minuend, subtrahend + ); + } + + /// Raw-parts normalization lands every wide-regime construction on + /// the canonical form. + /// + /// Odd-or-zero numerator, zero pinned to exponent zero, canonical + /// arm — stated as a family over arbitrary raw numerators and + /// exponents, exercised through the same `from_raw` every fold and + /// reference computation lands through. + #[test] + fn rank_wide_arm_normalization_is_canonical( + limbs in proptest::collection::vec(any::(), 0..12), + twos in 0u64..300, + exp in 0u64..600, + ) { + let _guard = super::rank::arm_ceiling::force(WIDE_REGIME_CEILING_BITS); + let bytes: Vec = limbs.iter().flat_map(|limb| limb.to_le_bytes()).collect(); + let raw = dashu_int::UBig::from_le_bytes(&bytes) << usize::try_from(twos).unwrap(); + let rank = super::Rank::from_raw(crate::codec::Base::from(raw.clone()), exp); + assert_rank_canonical(&rank, WIDE_REGIME_CEILING_BITS); + // The normalized parts denote the same rational: num · 2^-exp is + // invariant under the strip. + let (num, e) = rank_parts(&rank); + prop_assert_eq!(num << usize::try_from(exp - e).unwrap(), raw); + } + + /// The wire round-trip is exact and byte-canonical across the seam. + /// + /// Known-arm constructions — wide integral parts, wide fractions, and + /// base-arm controls — encode, decode to the same value on the same + /// arm, and re-encode to identical bytes through the public doors. + /// + /// This drives the encoder's wide-arm emission (the biased integral + /// re-dispatch included: `width` may sit exactly at the ceiling) and + /// the decoder's wide materialization of both the mantissa and the + /// fraction image, at sizes where the whole stream is a few hundred + /// bytes. + #[test] + fn rank_wide_arm_codec_roundtrips_canonically( + width in 1u64..600, + exp_frac in 0u64..600, + odd_tail in any::(), + ) { + let _guard = super::rank::arm_ceiling::force(WIDE_REGIME_CEILING_BITS); + // A `width`-bit numerator: a top bit, an optional odd tail, and a + // mid bit so both header and mantissa carry structure. + let mut num = dashu_int::UBig::ONE << usize::try_from(width - 1).unwrap(); + if odd_tail { + num |= dashu_int::UBig::ONE; + num |= dashu_int::UBig::ONE << usize::try_from(width / 2).unwrap(); + } + let exp = if odd_tail { exp_frac } else { 0 }; + let rank = super::Rank::from_raw(crate::codec::Base::from(num), exp); + assert_rank_canonical(&rank, WIDE_REGIME_CEILING_BITS); + let bytes = rank.encode(); + let decoded = super::Rank::decode(&bytes[..]).expect("canonical bytes decode"); + prop_assert_eq!(&decoded, &rank); + assert_rank_canonical(&decoded, WIDE_REGIME_CEILING_BITS); + prop_assert_eq!(decoded.encode(), bytes); + } + + /// The wide arm renders the exact decimal: `Display` against the + /// backend's own conversion, integral and fractional forms both. + #[test] + fn rank_wide_arm_display_matches_the_backend(seed in any::()) { + let _guard = super::rank::arm_ceiling::force(WIDE_REGIME_CEILING_BITS); + let rank = seeded_rank(seed); + let (num, exp) = rank_parts(&rank); + let expected = match exp { + 0 => format!("{num}"), + 1 => format!("{num}/2"), + exp => format!("{num}/2^{exp}"), + }; + prop_assert_eq!(rank.to_string(), expected); + } +} + +/// The composite key survives the wide arm end to end. +/// +/// A version whose rank numerator crosses the lowered ceiling encodes +/// through `Ranked::encode` (the fused emission from the fold's raw +/// parts) and decodes through `Ranked::decode` (the streaming rank door +/// plus the fold re-derivation), byte-identically. +/// +/// This is the one door pair whose rank wire form is produced and +/// consumed *around* a version fold, so it pins the fold-output +/// re-dispatch (`from_raw` crossing to the wide arm) against the wire. +#[test] +fn rank_wide_arm_ranked_composite_roundtrips() { + let _guard = super::rank::arm_ceiling::force(WIDE_REGIME_CEILING_BITS); + // A lone leaf of height 2^400 + 1: its rank is the height itself, + // 401 bits — past the 256-bit ceiling. + let height = (dashu_int::UBig::ONE << 400usize) + 1u8; + let version: Version = format!("{height}").parse().expect("a leaf parses"); + let rank = version.rank(); + assert!( + rank.numerator_is_wide(), + "the fold output crosses the ceiling" + ); + assert_rank_canonical(&rank, WIDE_REGIME_CEILING_BITS); + let ranked = crate::Ranked::from(&version); + let key = ranked.encode(); + let decoded = crate::Ranked::decode(&key[..]).expect("the composite decodes"); + assert_eq!(decoded.version(), &version); + assert_eq!(decoded.encode(), key, "byte-identical re-emission"); +} + // ─────────────────────────────── the join fold ─────────────────────────────── // The n-ary fold doors against the sequential pair fold — `join_all`, diff --git a/crates/before/wasm32-pins/guest/src/lib.rs b/crates/before/wasm32-pins/guest/src/lib.rs index 6f7c7b53..bd35cc79 100644 --- a/crates/before/wasm32-pins/guest/src/lib.rs +++ b/crates/before/wasm32-pins/guest/src/lib.rs @@ -588,6 +588,60 @@ pub extern "C" fn pin_version_rank(b: u64, d: u64) -> i64 { 0 } +/// Decode a valid synthesized integral rank `2^k - 1` ([`synth_integral_rank`]) +/// and check exact-order observations: the rank exceeds the rank of the +/// version `1` and equals its own clone. +/// +/// The harness aims `k` at the big-integer backend's capacity through the +/// *integral* wire form — the second path to that coordinate beside the +/// fraction form: the mantissa is read as one `k + 1`-bit stream (the +/// biased value `2^k`), so both the value's own width and the one-wider +/// biased transient cross the backend's capacity here. ~`k/8` bytes of +/// input is the smallest honest trigger: the mantissa's bits are all +/// stream bits. +#[no_mangle] +pub extern "C" fn pin_rank_integral_decode(k: u64) -> i64 { + let bytes = synth_integral_rank(k); + let r = match Rank::decode(&bytes[..]) { + Ok(r) => r, + Err(_) => return -1, + }; + drop(bytes); + let one = match Version::try_from(1) { + Ok(v) => v.rank(), + Err(_) => return -2, + }; + if r <= one { + return -3; + } + if r != r.clone() { + return -4; + } + 0 +} + +/// Decode a valid synthesized rank whose fraction is exactly `exp` +/// expansion bits deep ([`synth_rank`]), re-encode it, and check the +/// canonical bytes reproduce the input exactly. +/// +/// The full-width wire round-trip: byte-identical re-emission is what the +/// lexicographic-order law rides on, so this pins encode-after-decode at +/// the same coordinate the decode pins hold — the harness aims `exp` past +/// the backend capacity, where the emission walks a numerator wider than +/// the backend can hold. +#[no_mangle] +pub extern "C" fn pin_rank_roundtrip(exp: u64) -> i64 { + let bytes = synth_rank(exp); + let r = match Rank::decode(&bytes[..]) { + Ok(r) => r, + Err(_) => return -1, + }; + if r.encode() != bytes { + return -2; + } + 0 +} + /// The small second operand of the rank-arithmetic pins, keyed by its /// exponent: `0` is the integral rank `1`, `1` is the rank `1/2`, and any /// larger value is [`synth_rank`]'s fraction at that depth. diff --git a/crates/before/wasm32-pins/harness/tests/pins.rs b/crates/before/wasm32-pins/harness/tests/pins.rs index b5eea968..9afbec9d 100644 --- a/crates/before/wasm32-pins/harness/tests/pins.rs +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -140,10 +140,10 @@ fn version_decode_deep_in_memory_bounded_range() { /// is unreachable through the doors on this target: a wide value's gamma /// code alone costs a quarter of the address space, and the decode's /// working set exhausts memory first. (The rank wire door reaches the -/// same capacity with no fold transients, where -/// `rank_decode_past_backend_bit_capacity_traps` pins it.) A leaner -/// working set — not a wider denomination — is what would move this -/// terminal outward. +/// same coordinate with no fold transients and crosses it exactly — it +/// is the numerator's arm seam there, pinned by +/// `rank_decode_past_backend_bit_capacity`.) A leaner working set — not +/// a wider denomination — is what would move this terminal outward. #[test] fn version_decode_memory_terminal_traps() { assert_eq!( @@ -157,10 +157,11 @@ fn version_decode_memory_terminal_traps() { /// /// The deepest flush-group exponent whose whole fraction image fits the /// big-integer backend's 32-bit buffer capacity without any stripping: -/// the backend-capacity boundary's lower adjacency witness. ~604 MB of -/// input is the smallest honest trigger, since the fraction's depth is -/// deliberately counted from bits actually read, never from a header's -/// claim. +/// the lower adjacency witness of the numerator's arm seam — the widths +/// where storage crosses from the backend magnitude to the rank's own +/// limb vector. ~604 MB of input is the smallest honest trigger, since +/// the fraction's depth is deliberately counted from bits actually read, +/// never from a header's claim. #[test] fn rank_decode_below_backend_capacity() { assert_eq!( @@ -208,9 +209,10 @@ fn rank_decode_at_usize_exp_boundary() { /// expansion bits deep opening with 64 zero bits. /// The backend caps a magnitude at `usize::MAX / 32` words so bit counts /// fit `usize`; a numerator of exactly that many bits fills the buffer to -/// its last word. This is the backend-capacity boundary's lower adjacency -/// witness on the numerator's own width (the byte-capacity witness above -/// covers the unstripped image's width). +/// its last word. This is the arm seam's at-capacity witness on the +/// numerator's own width (the byte-capacity witness above covers the +/// unstripped image's width): the widest numerator the backend arm +/// stores, holding the arm ceiling to the real backend from below. #[test] fn rank_decode_at_backend_bit_capacity() { assert_eq!( @@ -219,22 +221,83 @@ fn rank_decode_at_backend_bit_capacity() { ); } -/// PINNED AS FOUND: a valid rank whose numerator is 2^32 - 24 value bits — -/// one flush group past the backend's 2^32 - 32-bit capacity — traps on -/// wasm32 instead of decoding or rejecting with a typed error. +/// A valid rank whose numerator is 2^32 - 24 value bits — one flush group +/// past the backend's 2^32 - 32-bit capacity — decodes correctly on +/// wasm32 and orders exactly against reference ranks. /// -/// The ~604 MB input and its ~512 MiB numerator both fit the 4 GiB address -/// space with the decode's whole working set beside them, so the boundary -/// is the backend's structural word cap, not memory: the backend clamps -/// the buffer's capacity to its maximum and the fill's release assert -/// panics on the word past it. Unconditional 32-bit correctness requires -/// this input to decode; the cure flips this pin to the correct-value -/// assertion beside its lower witness `rank_decode_at_backend_bit_capacity`. +/// The arm seam's upper witness, beside +/// `rank_decode_at_backend_bit_capacity`: the ~604 MB input and its +/// ~512 MiB numerator both fit the 4 GiB address space, and past the +/// backend's structural word cap the decoder assembles the numerator +/// into the rank's own limb vector — bounded only by memory — so the +/// wire door is exact on both sides of the backend's capacity. #[test] -fn rank_decode_past_backend_bit_capacity_traps() { +fn rank_decode_past_backend_bit_capacity() { assert_eq!( call1("pin_rank_decode", (1u64 << 32) + 40), - Outcome::Trapped(Trap::UnreachableCodeReached), + Outcome::Value(0), + ); +} + +/// A valid integral rank of 2^32 - 40 value bits — the biased mantissa +/// one byte under the backend's capacity — decodes correctly on wasm32 +/// and orders exactly against reference ranks. +/// +/// The integral wire form's lower adjacency witness at the arm seam: the +/// whole path (mantissa read, bias removal) runs on the backend arm. +/// ~512 MiB of input is the smallest honest trigger — the mantissa's +/// bits are all stream bits. +#[test] +fn rank_integral_decode_below_backend_bit_capacity() { + assert_eq!( + call1("pin_rank_integral_decode", (1u64 << 32) - 40), + Outcome::Value(0), + ); +} + +/// A valid integral rank of exactly 2^32 - 32 value bits — the backend's +/// capacity — decodes correctly on wasm32. +/// +/// The integral form's at-capacity witness, and the biased-transient +/// seam: the mantissa is read as the biased value `2^k` at `k + 1` bits, +/// one past the capacity, so the transient rides the limb arm while the +/// unbiased value re-dispatches back onto the backend arm at exactly its +/// widest representable width. +#[test] +fn rank_integral_decode_at_backend_bit_capacity() { + assert_eq!( + call1("pin_rank_integral_decode", (1u64 << 32) - 32), + Outcome::Value(0), + ); +} + +/// A valid integral rank of 2^32 - 24 value bits — one byte past the +/// backend's capacity — decodes correctly on wasm32. +/// +/// The integral form's upper arm-seam witness, beside the fraction +/// form's `rank_decode_past_backend_bit_capacity`: both wire paths to a +/// past-capacity numerator land on the limb arm, priced by memory alone. +#[test] +fn rank_integral_decode_past_backend_bit_capacity() { + assert_eq!( + call1("pin_rank_integral_decode", (1u64 << 32) - 24), + Outcome::Value(0), + ); +} + +/// A valid rank whose fraction is 2^32 + 40 expansion bits deep — the +/// numerator one flush group past the backend's capacity — decodes and +/// re-encodes to byte-identical canonical form on wasm32. +/// +/// The full-width round-trip witness: byte-identical re-emission is what +/// the lexicographic-order law rides on, and this holds it at the arm +/// seam's far side, where the emission walks a numerator wider than the +/// backend can hold. +#[test] +fn rank_roundtrip_past_backend_bit_capacity() { + assert_eq!( + call1("pin_rank_roundtrip", (1u64 << 32) + 40), + Outcome::Value(0), ); } @@ -578,39 +641,31 @@ fn rank_add_below_gap_boundary() { assert_eq!(call1("pin_rank_add", 128), Outcome::Value(0)); } -/// PINNED AS FOUND: adding the integral rank 1 to a fraction 2^32 -/// expansion bits deep traps on wasm32: the alignment shift's exponent -/// gap is exactly 2^32, one past the widest shift amount a 32-bit target -/// can name. +/// Adding the integral rank 1 to a fraction 2^32 expansion bits deep is +/// exact on wasm32: the sum strictly exceeds both summands. /// -/// Both operands are honest decoded/derived values and the exact sum is -/// representable in memory; the boundary is the alignment shift's `usize` -/// conversion (loud by construction, documented at the shift). The cure -/// flips this pin to the correct-value assertion. +/// The exponent gap is exactly 2^32, one past the widest shift amount a +/// 32-bit target can name — so the addition routes through the streaming +/// accumulator, on which no aligned numerator is ever materialized in the +/// backend, and the 2^32 + 1-bit result lands on the numerator's limb +/// arm. The gap boundary's at-seam witness, beside +/// `rank_add_below_gap_boundary`. #[test] -fn rank_add_at_gap_boundary_traps() { - assert_eq!( - call1("pin_rank_add", 0), - Outcome::Trapped(Trap::UnreachableCodeReached), - ); +fn rank_add_at_gap_boundary() { + assert_eq!(call1("pin_rank_add", 0), Outcome::Value(0)); } -/// PINNED AS FOUND: adding the rank 1/2 to a fraction 2^32 expansion bits -/// deep traps on wasm32. +/// Adding the rank 1/2 to a fraction 2^32 expansion bits deep is exact +/// on wasm32: the sum strictly exceeds both summands. /// -/// The exponent gap, 2^32 - 1, fits the shift amount, but the aligned -/// numerator — 2^32 value bits — exceeds the backend's 2^32 - 32-bit -/// capacity. -/// The gap boundary's other genre: just below the shift-amount seam the -/// terminal moves from the shift's conversion to the backend's capacity -/// assert, so no sub-boundary gap is safe unless the aligned width also -/// fits. The cure flips this pin to the correct-value assertion. +/// The exponent gap, 2^32 - 1, fits a 32-bit shift amount, but the +/// aligned numerator — 2^32 value bits — exceeds the backend's +/// 2^32 - 32-bit capacity: the gap boundary's other genre, routed to the +/// accumulator by the aligned-width clause of the routing predicate +/// rather than the gap clause, with the result on the limb arm. #[test] -fn rank_add_below_gap_wide_result_traps() { - assert_eq!( - call1("pin_rank_add", 1), - Outcome::Trapped(Trap::UnreachableCodeReached), - ); +fn rank_add_below_gap_wide_result() { + assert_eq!(call1("pin_rank_add", 1), Outcome::Value(0)); } /// Rank subtraction is exact just below the 32-bit alignment-gap boundary. @@ -624,21 +679,19 @@ fn rank_checked_sub_below_gap_boundary() { assert_eq!(call1("pin_rank_checked_sub", 128), Outcome::Value(0)); } -/// PINNED AS FOUND: subtracting a fraction 2^32 expansion bits deep from -/// the integral rank 1 traps on wasm32. +/// Subtracting a fraction 2^32 expansion bits deep from the integral +/// rank 1 is exact on wasm32: the difference sits strictly between zero +/// and the minuend. /// -/// The strictly positive difference aligns the minuend by a shift whose -/// exponent gap is exactly 2^32, one past the widest amount a 32-bit -/// target can name. -/// The ordering pre-check settles sign without alignment, so `None` and -/// zero results stay exact at any gap; only the positive arm reaches the -/// shift. The cure flips this pin to the correct-value assertion. +/// The strictly positive difference aligns at an exponent gap of exactly +/// 2^32, one past the widest shift amount a 32-bit target can name — so +/// the subtraction routes through the streaming accumulator, and the +/// ~2^32-bit difference lands on the numerator's limb arm. The ordering +/// pre-check settles sign without alignment, so `None` and zero results +/// never leave the class comparison at any gap. #[test] -fn rank_checked_sub_at_gap_boundary_traps() { - assert_eq!( - call1("pin_rank_checked_sub", 0), - Outcome::Trapped(Trap::UnreachableCodeReached), - ); +fn rank_checked_sub_at_gap_boundary() { + assert_eq!(call1("pin_rank_checked_sub", 0), Outcome::Value(0)); } /// The rank fold is exact on a numerator wider than any 32-bit quantity, @@ -676,10 +729,10 @@ fn version_rank_deep_in_memory_bounded_range() { /// pays five stream bits per level), and that plus the fold's transients /// exhausts memory just below the capacity — the rank wire door, which /// assembles its numerator from bytes with no fold transients, is where -/// the capacity is reachable and pinned -/// (`rank_decode_past_backend_bit_capacity_traps`). A leaner working -/// set — not a wider denomination — is what would move this terminal -/// outward. +/// the capacity is reachable, and it crosses exactly there onto the +/// numerator's limb arm (`rank_decode_past_backend_bit_capacity`). A +/// leaner working set — not a wider denomination — is what would move +/// this terminal outward. #[test] fn version_rank_memory_terminal_traps() { assert_eq!( From 35a09c5bc3a65075dcea7415d3e6d3a54019799b Mon Sep 17 00:00:00 2001 From: finch Date: Wed, 19 Aug 2026 10:55:05 -0400 Subject: [PATCH 20/20] before: sweep the last build-substrate ghosts and a dead assert leg The emit-probe example, the party bench header, the benchmark-results README, and the wire-decode oracle's doc comment all described the external bit-vector baseline as the crate's own storage or builder; each now names it as the comparison baseline it is, with the crate's word-staged PackedBuilder and BitsBuf as the shipping forms. The builder's append assert drops a disjunct for a width its own preceding assert excludes. --- crates/before/benches/party.rs | 2 +- crates/before/examples/emit_probe.rs | 12 +++++++----- crates/before/results/benchmarks/README.md | 2 +- crates/before/src/borsh_impls/tests.rs | 2 +- crates/before/src/codec/build.rs | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/before/benches/party.rs b/crates/before/benches/party.rs index 46c2ec8e..6b911b2f 100644 --- a/crates/before/benches/party.rs +++ b/crates/before/benches/party.rs @@ -1,4 +1,4 @@ -//! `Party` benchmarks: the optimized packed-`BitVec` implementation against the naive +//! `Party` benchmarks: the optimized packed-bit-stream implementation against the naive //! recursive oracle, on the same randomized trees (see `common`). Codec ops have no //! oracle counterpart and are timed for the impl alone. diff --git a/crates/before/examples/emit_probe.rs b/crates/before/examples/emit_probe.rs index 08576931..389984fd 100644 --- a/crates/before/examples/emit_probe.rs +++ b/crates/before/examples/emit_probe.rs @@ -47,8 +47,9 @@ fn bench(name: &str, iters: u32, mut f: impl FnMut() -> usize) { ); } -/// A minimal word-buffered MSB-first bit writer: the shape a -/// `PackedBuilder` replacement would have. +/// A minimal word-buffered MSB-first bit writer: the staging discipline +/// the crate's own `PackedBuilder` ships, reproduced standalone so the +/// comparison needs no crate internals. struct WordWriter { words: Vec, /// Bits already committed to `words` (multiple of 64). @@ -95,7 +96,7 @@ fn main() { let total_bits: usize = codes.iter().map(|(_, l)| 2 + *l as usize).sum(); println!("leaves={LEAVES} total output bits≈{total_bits}"); - // 1. Per-bit bitvec push: the current PackedBuilder discipline + // 1. Per-bit bitvec push: the external baseline at its simplest // (1 flag push + per-bit code pushes per leaf). bench("bitvec push per bit", 2000, || { let mut out: BaselineBits = BitVec::with_capacity(total_bits); @@ -109,8 +110,9 @@ fn main() { out.len() }); - // 2. Current per-leaf heap code + extend_from_bitslice splice: what - // gamma_code + SkylineBuilder::leaf actually do. + // 2. Per-leaf heap code + extend_from_bitslice splice: the external + // baseline shaped like a builder that allocates each code before + // splicing it. bench("bitvec alloc code + splice per leaf", 2000, || { let mut out: BaselineBits = BitVec::with_capacity(total_bits); for &(code, len) in &codes { diff --git a/crates/before/results/benchmarks/README.md b/crates/before/results/benchmarks/README.md index 6e8a632e..91241390 100644 --- a/crates/before/results/benchmarks/README.md +++ b/crates/before/results/benchmarks/README.md @@ -1,7 +1,7 @@ # Benchmark results: optimized impl vs. reference oracle Per-operation timing comparison of this crate's optimized implementation -(`before` — packed `BitVec` storage with a transient working form) against the +(`before` — packed bit-stream storage with a transient working form) against the naive recursive reference (`oracle`, `src/oracle.rs`), on identical randomized inputs. The inputs are built through the public API by the fork-a-universe / preserve-a-subset / join-each-group recipe in diff --git a/crates/before/src/borsh_impls/tests.rs b/crates/before/src/borsh_impls/tests.rs index 341f1fc4..59a1cab9 100644 --- a/crates/before/src/borsh_impls/tests.rs +++ b/crates/before/src/borsh_impls/tests.rs @@ -300,7 +300,7 @@ proptest! { /// The per-bit reference cursor, the wire decode's differential oracle. /// -/// The definitional shape with no fast paths: a growing `BitVec` refilled one +/// The definitional shape with no fast paths: a growing `BitsBuf` refilled one /// byte at a time, per-bit reads only, and the default per-bit `read_int`. struct BitwiseReaderCursor<'a, R> { reader: &'a mut R, diff --git a/crates/before/src/codec/build.rs b/crates/before/src/codec/build.rs index 323c538f..8ae7a370 100644 --- a/crates/before/src/codec/build.rs +++ b/crates/before/src/codec/build.rs @@ -245,7 +245,7 @@ impl PackedBuilder { fn append_bits(&mut self, value: u64, len: u32) { debug_assert!(len <= 63, "appends stage at most 63 bits at once"); debug_assert!( - len == 64 || value >> len == 0, + value >> len == 0, "append value has bits above its stated width" ); let total = self.staged_len + len;