diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index 1c2661d4b..f5f17e596 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 @@ -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:463:38: replace < with <= in recode", # --- before::version::skyline::fill / prescan --- diff --git a/Cargo.toml b/Cargo.toml index 970c26823..d47cfd61f 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/Cargo.lock b/crates/before-fuelscape/Cargo.lock index 200afdcca..fb035dc87 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-fuelscape/src/count/tests.rs b/crates/before-fuelscape/src/count/tests.rs index 1583cba2d..81e3b9940 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 d26fe781e..3d5149b7a 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 d0434c147..fe5a70461 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" @@ -60,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/party.rs b/crates/before/benches/party.rs index 46c2ec8e6..6b911b2fc 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/benches/presize.rs b/crates/before/benches/presize.rs index c836154dc..83c0895a9 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 392add6ba..e14f7fc10 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/emit_probe.rs b/crates/before/examples/emit_probe.rs index 3ba72fa7b..389984fd7 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; @@ -46,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). @@ -94,10 +96,10 @@ 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: 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() { @@ -108,12 +110,13 @@ 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: 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 +140,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 +153,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/examples/space_consumption.rs b/crates/before/examples/space_consumption.rs index e6b12dc35..c7120a582 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/fuzz/Cargo.lock b/crates/before/fuzz/Cargo.lock index cdd2f16f6..221345d44 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 405b3864d..d8c9ef039 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/fuzzfit/harness/Cargo.toml b/crates/before/fuzzfit/harness/Cargo.toml index c75e4f0ab..2b614d3da 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 ef1a476eb..48f1cd72c 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/results/benchmarks/README.md b/crates/before/results/benchmarks/README.md index 6e8a632e7..912413908 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.rs b/crates/before/src/borsh_impls.rs index 4b1b2a1ad..250764902 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,16 @@ 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). + fn finish(mut self) -> Result, Decode> { if !self.read_bit()? { return Err(Decode::TrailingBits); } @@ -74,9 +81,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,17 +92,18 @@ 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 { + fn position(&self) -> u64 { self.position } @@ -111,7 +117,7 @@ impl BitCursor for ReaderCursor<'_, R> { // 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) + codec::decode_int_window(codec::BitsView::whole(&self.bytes), self.position) { self.position = next; return Ok(codec::Int::Small(n)); @@ -120,15 +126,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 +160,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 +175,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()))) } } @@ -266,20 +277,21 @@ 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 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/borsh_impls/tests.rs b/crates/before/src/borsh_impls/tests.rs index ea72530bb..59a1cab9a 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}; @@ -300,12 +300,12 @@ 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, - bits: BitsMut, - position: usize, + bits: BitsBuf, + position: u64, } impl BitCursor for BitwiseReaderCursor<'_, R> { @@ -315,14 +315,14 @@ 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)); + self.bits.push_bits(u64::from(byte[0]), 8); } - let bit = self.bits[self.position]; + let bit = self.bits.get(self.position); self.position += 1; Ok(bit) } - fn position(&self) -> usize { + fn position(&self) -> u64 { self.position } } @@ -352,7 +352,7 @@ 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)?; @@ -372,7 +372,7 @@ 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)?; @@ -520,10 +520,10 @@ 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(), &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 9e890e396..b28a33e20 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 ef63ad29c..dc8932d01 100644 --- a/crates/before/src/clock.rs +++ b/crates/before/src/clock.rs @@ -755,21 +755,24 @@ 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. + // 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(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 // 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)?; + let id_bytes = + usize::try_from(id_bytes).expect("the id prefix ends within the read buffer"); + codec::require_marker_padding(&buf[..id_bytes], id_end)?; + let tail = &buf[id_bytes..]; + let v_end = crate::version::skyline::validate_prefix(codec::BitsView::whole(tail))?; codec::require_marker_padding(tail, v_end)?; id_bytes }; @@ -789,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)`. @@ -798,10 +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 { - 8 * (self.party().encoded_bits() + 1).div_ceil(8) + self.version().encoded_bits() + #[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 @@ -891,7 +904,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/clock/tests.rs b/crates/before/src/clock/tests.rs index ee3be13b3..60e744dec 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. @@ -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.rs b/crates/before/src/codec.rs index 4aad88092..ee214c138 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::{ - byte_view, bytes_as_bits, canonical_eq, canonical_hash, padding_is_canonical, - require_marker_padding, slice_ptr_eq, -}; +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, BitsSlice}; +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}; @@ -71,4 +73,8 @@ pub(crate) use tree::{parse_id, validate_id}; // (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(all(test, feature = "borsh"))] pub(crate) use tree::parse_id_from; diff --git a/crates/before/src/codec/base.rs b/crates/before/src/codec/base.rs index f14e361bd..a70667ccd 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)] @@ -131,6 +116,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 @@ -147,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 @@ -164,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 { @@ -430,17 +464,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 +493,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/codec/base/limb_meter.rs b/crates/before/src/codec/base/limb_meter.rs index fc8965fbd..200ed0039 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/codec/bits.rs b/crates/before/src/codec/bits.rs index e95210649..076783769 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 //! @@ -58,27 +61,12 @@ #![allow(rustdoc::private_intra_doc_links)] use core::hash::Hasher; -use core::ops::Deref; -use bitvec::domain::Domain; -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; - -/// 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 +83,11 @@ 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 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 @@ -120,15 +108,19 @@ 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. + /// + /// 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); Bits { - bytes: Bytes::from(buf.into_vec()), + bytes: Bytes::from(buf.into_bytes()), } } @@ -139,6 +131,11 @@ 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. + /// + /// 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 { let bits = Bits { bytes }; debug_assert!( @@ -148,22 +145,41 @@ 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 /// invariant this rests on — a nonempty buffer's final byte is nonzero — is /// exactly what the freeze and decode doors establish. - pub fn len(&self) -> usize { + /// + /// The arithmetic runs at `u64` width: `bytes.len() * 8` wraps a 32-bit + /// `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) => { debug_assert!(last != 0, "stored stream missing its padding marker"); - self.bytes.len() * 8 - 1 - last.trailing_zeros() as usize + self.bytes.len() as u64 * 8 - 1 - u64::from(last.trailing_zeros()) } } } + /// 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 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.len(), + } + } + /// Whether the stream holds no bits at all. /// /// This is emptiness of the *storage* (the anonymous id), not of the value @@ -203,13 +219,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`]. -impl Deref for Bits { - type Target = BitsSlice; - fn deref(&self) -> &BitsSlice { - &self.bytes.view_bits::()[..self.len()] +/// A borrowed view of one packed bit stream's live bits: the stream's bytes +/// beside a `u64` live bit length. +/// +/// 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 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 +/// `(view, start, end)`), never as re-sliced views, so byte alignment of +/// every view is an invariant of the type, not a runtime question. +/// +/// 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 buffer-vocabulary assertions. + #[cfg(test)] + 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 + } + + /// 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 } } @@ -224,46 +401,6 @@ impl PartialEq for Bits { impl Eq for Bits {} -/// Borrow bytes as an MSB-first bit stream without first copying them into a -/// [`BitsMut`]. -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. -/// -/// 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. /// @@ -315,41 +452,17 @@ 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. +/// 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. /// -/// `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, - } -} - -/// Require that the bits from `pos` onward are exactly the canonical padding: -/// one `1` marker bit, then zeros to the byte boundary. -/// -/// 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`. 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 +475,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: &[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/buf.rs b/crates/before/src/codec/buf.rs new file mode 100644 index 000000000..072b37f47 --- /dev/null +++ b/crates/before/src/codec/buf.rs @@ -0,0 +1,391 @@ +//! 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 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. + +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 dcf0d1b1b..8ae7a3709 100644 --- a/crates/before/src/codec/build.rs +++ b/crates/before/src/codec/build.rs @@ -33,12 +33,18 @@ //! empty, a two-shift merge per byte otherwise. use super::code::SMALL_CODE_BITS; -use super::{BitsMut, BitsSlice, 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: Vec, @@ -53,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, } @@ -64,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. @@ -80,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)); } @@ -104,14 +114,17 @@ 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::buf::built_view(bits); + self.splice(src, 0, src.len()); + } } } /// 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; @@ -128,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; @@ -153,24 +166,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); } } @@ -180,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", @@ -188,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` @@ -220,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; @@ -258,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; @@ -268,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; @@ -280,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 @@ -288,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 e66195ca6..cb4f4184d 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::{BitsBuf, BitsView}; /// One complete payload code, value-packed when it fits a word. pub(crate) enum Code { @@ -19,31 +18,43 @@ 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(), } } - /// 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 { 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 = BitsBuf::with_capacity(len); + super::buf::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 11285e9ba..8082d4823 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. /// @@ -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; } @@ -78,14 +85,18 @@ 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 } } } @@ -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 @@ -105,12 +116,12 @@ impl BitCursor for SliceCursor<'_> { Ok(bit) } - fn position(&self) -> usize { + fn position(&self) -> u64 { self.position } 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 +129,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 40cc88963..9c936a5b1 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::{BitsBuf, BitsView}; /// While rendering an open id node, which child the walk is inside. /// @@ -22,14 +22,14 @@ 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 { 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 efa8b1f55..84b2e56e7 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, @@ -52,23 +52,27 @@ 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`: 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, } impl<'a> DsiCursor<'a> { - /// Open a cursor at bit 0 of a stored stream. - /// - /// # Panics + /// Open a cursor at bit 0 of a stream's view. /// - /// 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 @@ -76,16 +80,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") - }; - 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) @@ -106,11 +106,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 +130,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 +151,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(()) } @@ -180,13 +182,13 @@ impl BitCursor for DsiCursor<'_> { Ok(bit) } - fn position(&self) -> 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(k + 1); + super::scan::record_bits_u64(k + 1); self.position += k + 1; Ok(k) } @@ -215,10 +217,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 +235,34 @@ 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. 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; @@ -266,7 +283,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))) } @@ -275,8 +292,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 ecb74c1a5..a5c7d9b9a 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,11 +34,11 @@ 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(&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"); @@ -48,7 +48,7 @@ fn gamma_reader_matches_decoder_across_the_word_seam() { 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"); @@ -82,16 +82,16 @@ 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(&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 width = reader.position(); + let mut skipper = DsiCursor::new(crate::codec::built_view(&bits)); crate::meter::reset_scan_bits(); skipper .skip_int() @@ -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 = &bits[..cut]; + 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}" @@ -150,14 +150,14 @@ 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] { - let mut bits = BitsMut::new(); + 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); } 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); let mut cursor = DsiCursor::new(unterminated); assert!( matches!(cursor.read_unary(), Err(Truncated)), @@ -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(&bits, pos); - let mut walked = DsiCursor::new(&bits); - let mut consumed = 0usize; + 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 = 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 { @@ -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()); 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 5f0ab28a8..99dd5469f 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, BitsSlice, 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, BitsSlice, 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) } @@ -114,14 +113,14 @@ 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())) } /// 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. @@ -133,8 +132,6 @@ const WINDOW_BITS: usize = u64::BITS as usize; /// 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,11 +146,23 @@ const WINDOW_BITS: usize = u64::BITS as usize; /// 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)> { +/// +/// 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 +/// `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 +173,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. @@ -204,19 +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`. - 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; @@ -235,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/literal.rs b/crates/before/src/codec/literal.rs index 2eca2343e..409e7dfa1 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, BitsBuf, 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 { @@ -30,8 +30,8 @@ pub(crate) fn id_is_empty(bits: &BitsSlice) -> 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: &BitsSlice) -> 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: &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: &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(&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/scan.rs b/crates/before/src/codec/scan.rs index ac1a51422..2748f42df 100644 --- a/crates/before/src/codec/scan.rs +++ b/crates/before/src/codec/scan.rs @@ -57,3 +57,16 @@ 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/stack.rs b/crates/before/src/codec/stack.rs index 42456160a..d2c7e3b1c 100644 --- a/crates/before/src/codec/stack.rs +++ b/crates/before/src/codec/stack.rs @@ -35,8 +35,13 @@ impl BitStack { } /// The stack's height in bits. - 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. @@ -99,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 340bfe2bb..0ff59d208 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 afec618df..c7e511615 100644 --- a/crates/before/src/codec/tests.rs +++ b/crates/before/src/codec/tests.rs @@ -7,13 +7,12 @@ use std::sync::Arc; -use bitvec::prelude::*; use proptest::prelude::*; use proptest::test_runner::TestCaseError; use super::{ - bytes_as_bits, decode_int, decode_int_from, encode_int, Base, BitCursor, BitsMut, BitsSlice, + bits_buf, decode_int, decode_int_from, encode_int, Base, BitCursor, BitsBuf, BitsView, DsiCursor, SliceCursor, }; use crate::oracle; @@ -36,9 +35,9 @@ 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(&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()); } @@ -54,9 +53,9 @@ 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(&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()); } @@ -72,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() }; @@ -88,9 +87,9 @@ 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(&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()); @@ -100,31 +99,41 @@ fn gamma_roundtrip_just_above_u64_max() { /// end (empty input, or all-zeros with no terminating `1`). #[test] fn gamma_truncated() { - let empty = BitsMut::new(); - assert!(matches!(decode_int(&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))); + let empty = BitsBuf::new(); + assert!(matches!( + decode_int(crate::codec::built_view(&empty), 0), + Err(Decode::Truncated) + )); + let zeros: BitsBuf = bits_buf![0, 0, 0, 0, 0]; + assert!(matches!( + decode_int(crate::codec::built_view(&zeros), 0), + Err(Decode::Truncated) + )); } // ───────────────────────── frozen storage (Bits) ───────────────────────── /// `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, &buf[..]); + assert_eq!(frozen.live().to_buf(), buf); } /// The marker padding makes stored bytes injective on streams: a stream @@ -132,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)); @@ -144,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)); @@ -163,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)); @@ -173,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)); } @@ -186,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 @@ -196,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 @@ -213,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 { @@ -226,14 +409,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,7 +431,7 @@ 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); @@ -271,7 +454,7 @@ 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), @@ -303,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![ @@ -318,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); @@ -345,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); @@ -356,7 +539,8 @@ 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()); } @@ -375,23 +559,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 +587,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()))?; } @@ -427,58 +607,69 @@ 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!(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. 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(&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(); + let mut bits = BitsBuf::new(); encode_int(&mut bits, &Base::from(5u64)); let code_len = bits.len(); 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)) + ); } -/// 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; - let mut bits = BitsMut::new(); + let mut bits = BitsBuf::new(); 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); + let zeros = BitsBuf::repeat(false, 70); + 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 @@ -488,9 +679,9 @@ 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(&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()); } @@ -501,19 +692,19 @@ 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 = &bits[..cut]; + 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", ); } // 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()); } @@ -1250,7 +1441,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. @@ -1345,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", ); @@ -1392,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", ); @@ -1499,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() { @@ -1537,17 +1728,17 @@ 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); } - super::validate_id(&bits)?; + super::validate_id(crate::codec::built_view(&bits))?; Ok(bits) } @@ -1647,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()); } } @@ -1706,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 1fe503a27..d745d8922 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,14 +72,14 @@ 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 } - validate_id(&bits)?; + validate_id(super::built_view(&bits))?; Ok(bits) } @@ -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/codec/tree.rs b/crates/before/src/codec/tree.rs index 71af3cd73..4ea5db866 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,13 +32,29 @@ 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) +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()) } -/// Parse and validate one id tree from a sequential bit cursor. -pub(crate) fn parse_id_from(cursor: &mut C) -> Result +/// 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, +{ + parse_id_core(cursor)?; + Ok(cursor.position()) +} + +/// 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 [`BitCursor::position`] +/// read. +pub(crate) fn parse_id_core(cursor: &mut C) -> Result<(), Decode> where Decode: From, { @@ -62,7 +80,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, @@ -93,7 +111,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 af93b2b76..48a7372fc 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/laws.rs b/crates/before/src/laws.rs index d88c95275..d20a453ad 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.rs b/crates/before/src/meter.rs index 8e0d8f05c..bdb835b51 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,18 +99,18 @@ 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, } } /// The generator's live bits, borrowed. - 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), @@ -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/ceilings.rs b/crates/before/src/meter/board/ceilings.rs index 5c32aa99b..087551cc4 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 8cdc59010..c43f04a82 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,17 +92,16 @@ 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::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 - 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); - out.into_vec() + out.into_bytes() } /// `p`'s stream with its preorder-last terminal split into a collapsible @@ -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::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); for _ in 0..2 { @@ -128,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 0369d69bc..aa38c0055 100644 --- a/crates/before/src/meter/board/family.rs +++ b/crates/before/src/meter/board/family.rs @@ -1109,12 +1109,13 @@ 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::BitsBuf::with_capacity(view.len() + 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() + bits.into_bytes() }; let (a, b) = (mount(true), mount(false)); assert!( @@ -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::BitsBuf::with_capacity(bits.len() + 2 * path.len() as u64 + 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,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() + 2); + let mut b = codec::BitsBuf::with_capacity(bits.len() + 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()); + 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" @@ -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) }); } @@ -1218,13 +1219,13 @@ fn rightmost_terminal_path(bits: &codec::BitsSlice) -> 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/board/operand.rs b/crates/before/src/meter/board/operand.rs index 48f890b8a..d75808526 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` @@ -295,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 7977feb5e..bcc1268c4 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/board/tests.rs b/crates/before/src/meter/board/tests.rs index 23825e695..e3d4c38a7 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/board/worst.rs b/crates/before/src/meter/board/worst.rs index 60212fed5..fa71722d7 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"]), diff --git a/crates/before/src/meter/tests.rs b/crates/before/src/meter/tests.rs index 21125db08..a5597fecc 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(), @@ -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 { @@ -1217,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.rs b/crates/before/src/meter/tier2.rs index 46ff9c1d3..ee2c70454 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 1782d8710..7ccd2b4b3 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 { @@ -89,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 @@ -98,10 +106,12 @@ 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); + 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 + @@ -115,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()); } } @@ -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 { @@ -164,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 @@ -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, + out.len(), + 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, + out.len(), + 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 861abf1e0..28fc5b97f 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}; @@ -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) @@ -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)) @@ -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,10 @@ 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 { - self.as_bits().len() + #[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() } /// Decodes a [`Party`] from a reader of canonical bytes, strictly rejecting @@ -578,10 +586,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 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 bits = codec::bytes_as_bits(&buf); - let end = codec::parse_id(bits, 0)?; - codec::require_marker_padding(bits, 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 @@ -608,7 +618,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 @@ -658,8 +668,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 @@ -668,9 +678,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)) } @@ -702,7 +712,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, ", ") } } @@ -744,8 +754,8 @@ 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 { - if codec::id_is_empty(&bits) { +fn finish_id(bits: codec::BitsBuf) -> Result { + if codec::id_is_empty(codec::built_view(&bits)) { Err(Parse::Anonymous) } else { Ok(Party::from_bits(bits)) @@ -769,11 +779,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)), @@ -783,13 +793,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 e3f803e47..52b343569 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, 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), } @@ -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 @@ -148,7 +149,7 @@ impl IdBuilder { self.terminal() } - pub(super) fn finish(self) -> BitsMut { + pub(super) fn finish(self) -> BitsBuf { self.out.finish() } } @@ -190,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(), @@ -206,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" @@ -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: u64, 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,12 +263,12 @@ 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); } /// 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" @@ -334,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). @@ -350,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; } @@ -361,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 627b01f3b..2b274dc2a 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 59cdfe791..17e9e6fa1 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::{BitsBuf, BitsView}; use crate::idbits::IdReader; use crate::version::skyline::overlay::{self, PlateauCursor}; @@ -70,7 +70,7 @@ 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. @@ -89,7 +89,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 +210,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,21 +243,24 @@ 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, + 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, + /// + /// `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). @@ -273,10 +276,10 @@ 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(), + path: BitsBuf::new(), + pending_right: BitsBuf::new(), open_lefts: 0, item: Item::Plateau { owned: false }, }; @@ -307,7 +310,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 +350,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 { @@ -385,7 +388,10 @@ impl PlateauCursor for IdLeafCursor<'_> { type Crossing = bool; /// The current item'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 stored id. + fn depth(&self) -> u64 { self.path.len() } @@ -405,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 @@ -417,7 +423,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/index.rs b/crates/before/src/party/ops/index.rs index f5824243b..1f541b942 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 1f2a88884..0b2631e5f 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, 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,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) -> (BitsBuf, BitsBuf) { 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; match kind { SpineEnd::Branch => { @@ -65,22 +66,20 @@ 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 = 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 - 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 = 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 - b.extend_from_bitslice(i2); + extend_from_view(&mut b, bits, right_child, branch_end); (a, b) } @@ -92,15 +91,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 = 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); - b.extend_from_bitslice(prefix); + 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); b.push(false); // ... over a terminal @@ -113,7 +112,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 e6a408469..f6388bccf 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,7 +25,7 @@ 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. @@ -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 877e28be1..9ce12f250 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, 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 { @@ -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), + Merged(BitsBuf), } impl UnionChild<'_> { - fn bits(&self) -> &BitsSlice { + /// The child's bit length, for the halves' exact capacity hints. + fn len(&self) -> u64 { match self { - UnionChild::Verbatim(bits) => bits, - UnionChild::Merged(bits) => bits, + 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 BitsBuf) { + match self { + UnionChild::Verbatim(bits, start, end) => extend_from_view(out, *bits, *start, *end), + UnionChild::Merged(bits) => out.extend_from_buf(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,20 +204,20 @@ 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()); - 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); - 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 { - 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 1a129430e..ea6b795ef 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)), @@ -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)"); @@ -581,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 @@ -593,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()) } @@ -629,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 } @@ -660,16 +663,17 @@ 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(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); } } @@ -751,14 +755,15 @@ 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(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, @@ -796,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()); @@ -819,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]; @@ -831,8 +836,9 @@ 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) { - let d = IdReader::root(a).diff(IdReader::root(b)); + 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!( &d, expected, "diff diverged from the constructed expectation (k={k})" @@ -883,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); } } @@ -923,9 +929,10 @@ 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(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] { @@ -938,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 \ @@ -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_buf() == b.as_bits().to_buf(); prop_assert_eq!(a == b, bit_eq); prop_assert_eq!(b == a, bit_eq); if a == b { @@ -1048,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}"); @@ -1071,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(), @@ -1086,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.rs b/crates/before/src/span.rs index 0ee8f0ad8..955612bdc 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 c925d7bcd..2c50bd19a 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; @@ -127,24 +128,30 @@ 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. + // 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 bits = codec::bytes_as_bits(&buf); - let lo_end = skyline::validate_prefix(bits)?; + 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 // 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); + let lo_bytes = + usize::try_from(lo_bytes).expect("the meet's prefix ends within the read buffer"); + 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::new(codec::BitsView::whole(tail)); + let admission = skyline::validate_dominating_from(lo, &mut cursor)?; + 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/testing/bridge.rs b/crates/before/src/testing/bridge.rs index 68dc746ad..e8425f626 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,9 +81,11 @@ 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(&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 783a57d0d..ad47baca4 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; @@ -80,8 +80,8 @@ 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 current_bits = packed.len() as u64; + let tier2 = tier2_size(crate::codec::built_view(&packed)); + 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 { @@ -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/generators.rs b/crates/before/src/testing/generators.rs index df173ac9d..23ed320fe 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 4a0a38754..8cedaae56 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, BitsBuf, 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"`. @@ -23,12 +25,12 @@ 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)", n, - bits_to_string(&bits), + bits_to_string(crate::codec::built_view(&bits)), bits.len() ) } @@ -66,11 +68,15 @@ 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!( - 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)" ); } @@ -166,11 +172,11 @@ 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()); + 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(&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 d42241578..7d06cacd6 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,10 +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 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 bits = codec::bytes_as_bits(&buf); - let end = skyline::validate_prefix(bits)?; - codec::require_marker_padding(bits, 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 @@ -1080,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)`. @@ -1091,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() } @@ -1128,8 +1137,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 @@ -1139,7 +1148,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)) } @@ -1363,7 +1372,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())) } } @@ -1445,7 +1454,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(), + )?)) } } @@ -1667,7 +1680,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 { @@ -1677,7 +1690,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 { @@ -1687,7 +1700,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 a74e01c85..5992e713f 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/rank.rs b/crates/before/src/version/rank.rs index a16894d81..5499d16b4 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,10 +304,27 @@ impl Rank { Ordering::Less => None, Ordering::Equal => Some(Rank::ZERO), Ordering::Greater => { + // 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) } } } @@ -436,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 @@ -458,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) } @@ -473,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, } } @@ -489,16 +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. - let biased = (num.clone() >> exp) + 1u32; + // m = ⌊r⌋ + 1, w = bits(m), ρ = bits(w) − 1. The shift is total at any + // 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; 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); @@ -610,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. @@ -659,7 +789,26 @@ 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. 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(); + Num::materialize_be(&image[lead..], pad) }; debug_assert!( exp == 0 || num.bit(0), @@ -707,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)) } } @@ -746,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)) } } @@ -781,10 +937,24 @@ impl PartialOrd for Rank { impl Add<&Rank> for &Rank { type Output = Rank; fn add(self, rhs: &Rank) -> Rank { + // 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) } } @@ -847,6 +1017,18 @@ 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. 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 { @@ -855,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. @@ -878,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 000000000..f52a8d5f3 --- /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 000000000..031953e86 --- /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/ranked.rs b/crates/before/src/version/ranked.rs index cf75d86d7..6f80d0064 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 76bd3692f..c29a68746 100644 --- a/crates/before/src/version/skyline.rs +++ b/crates/before/src/version/skyline.rs @@ -156,8 +156,8 @@ 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::BitsSlice; +pub use crate::codec::BitsBuf; +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. @@ -216,16 +216,25 @@ pub(crate) use validate::validate_prefix; #[cfg(feature = "borsh")] pub(crate) use validate::validate_from; +/// A build buffer's contents as a [`BitsView`]: the instrument surface's +/// 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"))] +pub fn view(bits: &BitsBuf) -> 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); +pub fn encode(version: &Version) -> BitsBuf { + let view = version.as_bits(); + 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 } @@ -243,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) } @@ -257,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) } @@ -266,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 e9e718f84..7c91d8739 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, BitsSlice, Int}; +use crate::codec::{BitCursor, BitsBuf, BitsView, Int}; use crate::error::Decode; use super::overlay::{fold, LeafCursor, PlateauCursor, Side, Step}; @@ -75,14 +75,17 @@ 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. - 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). @@ -98,8 +101,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,7 +124,11 @@ where } /// The current leaf's depth: its plateau 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 { self.path.len() } @@ -158,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). @@ -187,7 +194,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); @@ -274,7 +281,7 @@ pub(crate) enum Admission { /// shares [`causal_cmp`](super::sweep::causal_cmp)'s contract. The parsed /// stream needs no such trust; that is the point. pub(crate) fn validate_dominating_from( - lo: &BitsSlice, + lo: BitsView<'_>, cursor: &mut C, ) -> Result where diff --git a/crates/before/src/version/skyline/build.rs b/crates/before/src/version/skyline/build.rs index ff577a8dd..717357960 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, 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, BitsSlice, 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, @@ -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) => { @@ -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(), @@ -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 } @@ -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,13 +230,16 @@ 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, - root_depth: usize, - first_rel_depth: usize, - last_rel_depth: usize, - last_code_len: usize, + src: BitsView<'_>, + start: u64, + end: u64, + 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, @@ -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, "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 - 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 @@ -288,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() @@ -327,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(); @@ -338,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 a43f27902..0374d55a8 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<(u64, 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 { @@ -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)], -) -> (BitsMut, usize, usize, usize) { - let mut range = BitsMut::new(); + 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); @@ -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"); @@ -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 122fe16c7..21aab50c2 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::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 331025250..b1f65ea4d 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::{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: &BitsSlice, b: &BitsSlice) -> BitsMut { +pub fn join(a: BitsView<'_>, b: BitsView<'_>) -> BitsBuf { 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<'_>) -> 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 @@ -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. @@ -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, +) -> BitsBuf { let OpenedPair { a: mut cursor_a, b: mut cursor_b, @@ -314,9 +318,9 @@ fn emit(a_bits: &BitsSlice, b_bits: &BitsSlice, pick: impl Fn(Ordering, Side) -> 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 e55f2a1fa..c92db7174 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::{BitsBuf, 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), @@ -83,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), @@ -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); @@ -144,7 +145,7 @@ fn assert_pointwise(a: &BitsSlice, b: &BitsSlice, out: &BitsSlice, meet: bool) { // 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(); @@ -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), @@ -265,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); @@ -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), @@ -307,17 +311,41 @@ 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(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" + ); } } } @@ -325,18 +353,42 @@ 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 { 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 @@ -389,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(); @@ -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 bded61525..010898a61 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, BitsBuf, 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<'_>) -> 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 // 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 1371c8ac5..54dea4aa4 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, 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: &BitsSlice, 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,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) -> 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 { - return event.to_bitvec(); + let mut out = BitsBuf::with_capacity(event.len()); + 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") @@ -274,7 +276,7 @@ pub fn ticks(event: &BitsSlice, 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), @@ -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, @@ -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 @@ -430,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 @@ -633,7 +623,7 @@ impl FillWalk<'_> { } /// The cursor's bit position: the next node's flag. - fn pos(&self) -> usize { + fn pos(&self) -> u64 { self.cursor.position() } @@ -685,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" @@ -752,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 − @@ -859,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(); @@ -899,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 @@ -963,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" @@ -1014,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 @@ -1068,7 +1058,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 +1087,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 +1148,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 +1158,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 } } @@ -1220,7 +1212,7 @@ impl Frames { } /// Open branch levels (the walk's current depth). - fn len(&self) -> usize { + fn len(&self) -> u64 { self.site.len() } @@ -1247,7 +1239,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 +1247,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 +1269,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 +1278,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 +1301,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 0c5cdcd30..163df1de3 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::{BitCursor, BitStack, BitsBuf, 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 }; @@ -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 { .. } => { @@ -170,17 +170,22 @@ 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, - root_depth: usize, - first_rel_depth: usize, - last_rel_depth: usize, - last_code_len: usize, + src: BitsView<'_>, + start: u64, + end: u64, + root_depth: u64, + first_rel_depth: u64, + last_rel_depth: u64, + last_code_len: u64, ) { match self { Out::Built(builder) => builder.continue_verbatim( - rest, + src, + start, + end, root_depth, first_rel_depth, last_rel_depth, @@ -203,7 +208,7 @@ 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, @@ -224,7 +229,7 @@ impl Out { .expect("a matched prefix is a proper prefix of the tiling"); let start = cursor.position(); 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())); } debug_assert_eq!( cursor.position(), @@ -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 } @@ -261,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 @@ -272,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, @@ -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 bdcd91a7c..37de924d1 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 77baa14a5..a5a959b19 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 { @@ -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 4f816ec78..feccc2799 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), @@ -407,22 +410,22 @@ fn materialize_is_a_noop_once_built() { let matched_end = event.len(); 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,7 +1276,7 @@ proptest! { let v = from_oracle_version(&ov); if !p.as_bits().is_empty() { let ev = encode(&v); - let out = tick(&ev, &p); + let out = tick(crate::codec::built_view(&ev), &p); let bound = 2 * ev.len() + 4 * p.as_bits().len() + 32; prop_assert!( out.len() <= bound, @@ -1302,17 +1311,17 @@ 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 + 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(), @@ -1336,27 +1345,30 @@ 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, + e.len() <= b1 + 4 * logk as u64 + 8, "fixed-id orbit: {} bits at tick {k} (first-tick size {b1})", e.len(), ); } 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, + e.len() <= b2 + 4 * logk as u64 + 8, "alternating orbit: {} bits at tick {k} (two-tick size {b2})", e.len(), ); @@ -1428,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", ); @@ -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]); @@ -1734,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); @@ -1761,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"), } } @@ -1777,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 } @@ -1799,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 ... @@ -1848,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 @@ -1866,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) @@ -1891,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 eefc2880e..13575a445 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, BitsBuf, BitsView, Code}; use super::build::SkylineBuilder; use super::signed::{gamma_code, gamma_code_signed, unzigzag_base, zigzag_signed, Sign}; @@ -183,32 +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`). - pub(super) fn record(&mut self, key: usize, left: bool) { + /// + /// 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, 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.get(key) } /// The raw direction bits, for the route differential. #[cfg(test)] - pub(super) fn dirs(&self) -> &BitsSlice { + pub(super) fn dirs(&self) -> &BitsBuf { &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 { + fn pos(&self) -> u64 { self.cursor.position() } /// 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,7 +256,7 @@ 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 @@ -275,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"); @@ -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,16 +318,16 @@ 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, + first_rel_depth: u64, /// 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, + last_rel_depth: u64, } /// Locate the subtree at `start`: its end, and the first/last leaf coordinates @@ -336,11 +339,11 @@ 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, u64)> = None; let mut last_code = 0..0; let mut last_rel_depth = 0; let mut walk = LeafWalk::new(); @@ -369,28 +372,31 @@ fn scan_subtree(bits: &BitsSlice, start: usize) -> 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 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, ); } event.seek(subtree.end); @@ -419,9 +425,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,11 +491,11 @@ 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 { +) -> BitsBuf { debug_assert!( !id_bits.is_empty(), "grow requires an id owning at least one region" @@ -501,14 +507,16 @@ 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); // 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 depth = 0usize; + let mut pending = BitsBuf::new(); + // `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, @@ -535,7 +543,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"), } } @@ -565,7 +573,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; @@ -606,8 +614,10 @@ 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 `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 = &event_bits[original_range]; + let original = original_range; debug_assert_eq!( path_depth, pending.len(), @@ -617,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[level] { + if !chain_dirs.get(level) { let code = if emitted_in_chain { gamma_code(&Base::ZERO) } else { @@ -625,7 +635,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,13 +651,13 @@ 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. let mut first_after_grown = true; for level in (0..chain).rev() { - if chain_dirs[level] { + 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/grow/tests.rs b/crates/before/src/version/skyline/grow/tests.rs index 7f4c443f1..7a7eef606 100644 --- a/crates/before/src/version/skyline/grow/tests.rs +++ b/crates/before/src/version/skyline/grow/tests.rs @@ -21,8 +21,8 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use proptest::prelude::*; use rayon::prelude::*; -use crate::codec::BitsMut; -use crate::codec::BitsSlice; +use crate::codec::BitsBuf; +use crate::codec::BitsView; use crate::meter::registry::Shape; use crate::meter::Packed; use crate::recurse::descend; @@ -79,21 +79,21 @@ 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(&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) { +fn reference_probe(ev_bits: BitsView<'_>, id_bits: BitsView<'_>) -> (Route, Cost) { let mut route = Route::new(id_bits.len()); 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 70cb7f35b..9b44e3455 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, BitsSlice, 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: &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); @@ -50,13 +50,14 @@ pub(crate) fn node(base: u64, left: &BitsSlice, right: &BitsSlice) -> Result = None; for flag in &mut flags { @@ -78,11 +79,13 @@ pub(crate) fn node(base: u64, left: &BitsSlice, right: &BitsSlice) -> Result (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; + // `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 8daeebf0f..7738cafc6 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, @@ -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/masked/tests.rs b/crates/before/src/version/skyline/masked/tests.rs index c7fe6bb94..a60b8e6d7 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,25 +25,48 @@ 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)); 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(); + let mut good = BitsBuf::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 79092578c..7720e4514 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; @@ -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. @@ -316,7 +320,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 +334,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(), @@ -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,7 +412,7 @@ 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() } @@ -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, } @@ -492,7 +499,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(), @@ -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) => { @@ -690,7 +697,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 1e5809fa2..e79dcd7cd 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); @@ -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 @@ -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, @@ -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 1754c0a44..288bb1707 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() { @@ -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(); @@ -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() { @@ -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/place/tests.rs b/crates/before/src/version/skyline/place/tests.rs index cebe59a2c..c6326fbb4 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 2f832db12..8e6bf0389 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, BitsBuf, BitsView, Int}; use crate::Rank; use self::integral::{int_digits, Integrator, FREEZE_ALLOWANCE_DIGITS}; @@ -198,11 +198,11 @@ 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. - 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: &BitsSlice) -> 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: &BitsSlice) -> 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) } @@ -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' @@ -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) } @@ -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) -> BitsBuf { let id_bits = id.as_bits(); let (mut event_cursor, first) = LeafCursor::open(event_bits); let mut id_cursor = IdLeafCursor::open(id_bits); @@ -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: &BitsSlice) -> 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/integral.rs b/crates/before/src/version/skyline/query/integral.rs index 312eef6d6..75d3a44e6 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/tests.rs b/crates/before/src/version/skyline/query/tests.rs index 2a9566488..29bc1a406 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}" ); @@ -500,27 +530,37 @@ 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(); 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 ); @@ -720,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(), @@ -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,9 +1419,9 @@ 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 scale = max_depth; let (mut cursor, first) = LeafCursor::open(bits); let mut total = Accumulator::new(); let mut live_height = Accumulator::new(); @@ -1390,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); } @@ -1418,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) @@ -1431,14 +1471,14 @@ 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, "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 @@ -1643,13 +1683,13 @@ 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(); 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; @@ -1658,12 +1698,12 @@ 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 /// 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, @@ -1684,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; @@ -1711,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 @@ -1721,14 +1761,14 @@ 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, "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 @@ -1741,14 +1781,15 @@ 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, "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 @@ -2006,13 +2047,13 @@ 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(); 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; @@ -2021,12 +2062,12 @@ 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 /// 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, @@ -2047,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; @@ -2074,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 @@ -2084,14 +2125,14 @@ 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, "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 @@ -2104,14 +2145,14 @@ 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, "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 @@ -2313,13 +2354,13 @@ 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(); 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; @@ -2328,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 @@ -2342,14 +2383,14 @@ 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, "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 @@ -2546,13 +2587,13 @@ 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(); 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; @@ -2561,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 @@ -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!( @@ -2580,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/query/web.rs b/crates/before/src/version/skyline/query/web.rs index 8806485c3..68444967e 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 { @@ -228,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/signed/tests.rs b/crates/before/src/version/skyline/signed/tests.rs index 427d36d3b..bfa3208e2 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.rs b/crates/before/src/version/skyline/sweep.rs index f6aac247c..02ab0c40d 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 36ec245d5..ba8945e9c 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}; @@ -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}" ); @@ -180,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); @@ -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}" ); @@ -242,26 +253,26 @@ 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(); 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 b527952cb..025d734f0 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,9 +52,12 @@ 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_eq!(decode_bits(&bits).expect("canonical"), v); + 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 + ); } /// One fork `(1, 0, 2)` codes as hand-derived: 3 topology bits, `gamma(1)` for @@ -75,14 +78,17 @@ 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!(decode_bits(&bits).expect("canonical"), v); + assert_eq!(bits.iter().collect::>(), expected); + assert_eq!( + decode_bits(crate::codec::built_view(&bits)).expect("canonical"), + v + ); } // ─── 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)); } @@ -92,11 +98,14 @@ 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 - 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 — @@ -109,31 +118,35 @@ 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 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 /// 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(bits, pos).expect("a stored stream is canonical"); + let (_, next) = codec::decode_int(crate::codec::built_view(bits), pos) + .expect("a stored stream is canonical"); out.push((pos - 1, next)); pos = next; } @@ -160,13 +173,14 @@ 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..]); - prop_assert!(matches!(validate_bits(&planted), Err(Decode::NotCanonical))); + crate::codec::extend_from_view(&mut planted, view, end, bits.len()); + prop_assert!(matches!(validate_bits(crate::codec::built_view(&planted)), Err(Decode::NotCanonical))); } } @@ -186,14 +200,17 @@ 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 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); } @@ -203,11 +220,14 @@ 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 - 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: @@ -216,13 +236,16 @@ 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 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)), + 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); + two_trees.extend_from_buf(&clean); assert!(matches!( - validate_bits(&two_trees), + validate_bits(crate::codec::built_view(&two_trees)), Err(Decode::TrailingBits) )); } @@ -316,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 @@ -335,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 } @@ -343,21 +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(bits, pos).expect("a payload code per leaf"); - out.extend_from_bitslice(&bits[pos..next]); + let (_, next) = codec::decode_int(crate::codec::built_view(bits), pos) + .expect("a payload code per leaf"); + 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"); @@ -421,11 +451,11 @@ 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(&mutated) { + match decode_bits(crate::codec::built_view(&mutated)) { Err(_) => {} Ok(w) => { assert_ne!( @@ -468,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); } } @@ -484,18 +514,18 @@ 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, + bits.len(), size.total_bits, "stored skyline length disagrees with the tier2 sizer: one of the \ 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 2af79f53e..868e04055 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, BitsBuf, 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 @@ -236,8 +236,8 @@ pub fn render(bits: &BitsSlice) -> 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: &BitsSlice) -> 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: &BitsSlice) -> 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: &BitsSlice) -> 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,16 @@ pub fn render(bits: &BitsSlice) -> String { /// entry's length. const ARENA_SEP: char = ';'; +/// 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 rendered text's length") +} + /// Render one finalized printed base into the digit arena, keyed by its node's /// preorder index — unless it is zero. /// @@ -481,7 +491,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 +500,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 +509,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. diff --git a/crates/before/src/version/skyline/text/tests.rs b/crates/before/src/version/skyline/text/tests.rs index eb7d585fb..e0ab6571f 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; @@ -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!( @@ -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; @@ -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 { @@ -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) } @@ -498,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; @@ -508,10 +509,10 @@ 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(&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 0c6bebd2c..73efd9db3 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, BitsSlice, DsiCursor}; +use crate::codec::{BitCursor, BitsBuf, BitsView, DsiCursor}; use crate::error::Decode; use super::signed::{fold_signed_int, unzigzag, Sign}; @@ -38,7 +38,7 @@ use super::signed::{fold_signed_int, unzigzag, Sign}; /// 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() { @@ -47,13 +47,14 @@ 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 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 { +/// 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()) @@ -72,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/skyline/walk.rs b/crates/before/src/version/skyline/walk.rs index 36bd3cd3a..a653b3b89 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 ca4b1438b..b69ea21f5 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 3c7d061ff..248dee195 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/src/version/tests.rs b/crates/before/src/version/tests.rs index 648a65739..40bbdf860 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`, @@ -1895,7 +2215,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_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 2c10dd2d1..2ef81986b 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 0b1524980..af09a5cbf 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 ──────────────────────────────────────────────── @@ -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)) } @@ -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"); } @@ -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() } @@ -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, @@ -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 }; @@ -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"); } @@ -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() / 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, @@ -2170,7 +2173,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 +2194,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 +2211,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 +2229,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 +2371,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 +2446,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 +2518,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 +2657,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 +2701,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 +2832,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 +4712,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 +5042,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 +5569,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 +6987,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 +7006,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 +7026,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 +7047,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 +7069,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 +7086,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 +7109,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 +7147,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 +7187,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/Cargo.lock b/crates/before/wasm32-pins/Cargo.lock new file mode 100644 index 000000000..3693b5f3d --- /dev/null +++ b/crates/before/wasm32-pins/Cargo.lock @@ -0,0 +1,1153 @@ +# 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 = [ + "borsh", + "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 = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +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" +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 = "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" +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 = "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 = "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 = "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", + "borsh", +] + +[[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 = "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 000000000..aabc9f517 --- /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 000000000..fa5ef7e92 --- /dev/null +++ b/crates/before/wasm32-pins/guest/Cargo.toml @@ -0,0 +1,21 @@ +[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] +# 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. 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/crates/before/wasm32-pins/guest/src/lib.rs b/crates/before/wasm32-pins/guest/src/lib.rs new file mode 100644 index 000000000..bd35cc79c --- /dev/null +++ b/crates/before/wasm32-pins/guest/src/lib.rs @@ -0,0 +1,725 @@ +//! 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: +//! +//! - 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: 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 +//! checks turn exactly those wraps into observable traps instead of wrong +//! values downstream code would have to detect after the fact. + +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 +/// `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, 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); + 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 coordinates: the sizes straddling +/// 2^29 bits (where a `usize >> 3` bit-count encoding would bind on a +/// 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) { + 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 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 +/// 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 +} + +/// 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: 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`: +/// `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 stored view — so this +/// 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) { + 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 +/// 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 size memory admits. +#[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 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) { + 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 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 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) { + 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)`, returning 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 — the build buffer and its freeze hand-off, +/// 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)[..]) { + 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() != 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 +} + +/// 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. +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/Cargo.toml b/crates/before/wasm32-pins/harness/Cargo.toml new file mode 100644 index 000000000..75f9c4c20 --- /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 000000000..e32e2a31b --- /dev/null +++ b/crates/before/wasm32-pins/harness/src/lib.rs @@ -0,0 +1,114 @@ +//! 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; + +// `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. +/// +/// 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 + /// 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, &[]) +} + +/// Call a one-argument pin export in a fresh instance. +pub fn call1(export: &str, arg: u64) -> Outcome { + call(export, &[arg]) +} + +/// 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 *args { + [] => { + let func = instance + .get_typed_func::<(), i64>(&mut store, export) + .expect("the export exists with the pinned signature"); + func.call(&mut store, ()) + } + [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), + 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 000000000..9afbec9db --- /dev/null +++ b/crates/before/wasm32-pins/harness/tests/pins.rs @@ -0,0 +1,742 @@ +//! The 32-bit boundary pins: each test drives one guest export at one +//! boundary size and pins its exact behavior on wasm32. +//! +//! 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, call2, Outcome, Trap}; + +/// The largest buffer byte count whose whole-buffer bit count stays below +/// 2^29 bits — `usize::MAX >> 3`, the cap a 32-bit bit-vector length +/// encoding imposes — so 67108863 bytes. +/// +/// 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 +/// 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 count stays below the 2^29-bit +/// straddle coordinate ([`BUILD_CAP_BYTES`]) decodes with its exact bit +/// length. +/// +/// 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] +fn version_decode_below_build_cap() { + assert_eq!( + call1("pin_version_decode", 67_108_863), + Outcome::Value(8 * 67_108_863 - 8), + ); +} + +/// 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 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!( + call1("pin_version_decode", 67_108_864), + Outcome::Value(8 * 67_108_864 - 8), + ); +} + +/// A valid 67108865-byte version encoding decodes correctly on wasm32, +/// returning its exact live bit length. +/// +/// 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 would produce (a silently empty view, +/// then an element-count guard panic). +#[test] +fn version_decode_past_build_cap() { + assert_eq!( + call1("pin_version_decode", 67_108_865), + Outcome::Value(8 * 67_108_865 - 8), + ); +} + +/// 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 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_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 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!( + 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. +/// +/// The deepest flush-group exponent whose whole fraction image fits the +/// big-integer backend's 32-bit buffer capacity without any stripping: +/// 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!( + call1("pin_rank_decode", (1u64 << 32) - 32), + Outcome::Value(0), + ); +} + +/// 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 +/// 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!( + call1("pin_rank_decode", (1u64 << 32) - 8), + Outcome::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. +/// +/// 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)); +} + +/// 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 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!( + call1("pin_rank_decode", (1u64 << 32) + 32), + Outcome::Value(0), + ); +} + +/// 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 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() { + assert_eq!( + call1("pin_rank_decode", (1u64 << 32) + 40), + 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), + ); +} + +/// 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 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 +/// witness of the boundary straddle the two pins above it complete. +#[test] +fn ranked_decode_below_build_cap() { + assert_eq!( + call1("pin_ranked_decode", BUILD_CAP_BYTES), + Outcome::Value(0) + ); +} + +/// A valid composite key whose version component is 67108864 bytes — +/// exactly 2^29 bits — decodes through the byte door `Ranked::decode` on +/// wasm32. +/// +/// 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", BUILD_CAP_BYTES + 1), + Outcome::Value(0), + ); +} + +/// 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_build_cap() { + assert_eq!( + 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),); +} + +/// 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. +/// +/// The streaming door runs the same rank re-derivation as the byte door: +/// the boundary straddle's lower witness. +#[test] +fn ranked_borsh_below_build_cap() { + assert_eq!( + call1("pin_ranked_borsh", BUILD_CAP_BYTES), + Outcome::Value(0) + ); +} + +/// 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 and the rank +/// re-derivation walks the version through the crate-owned view: the +/// boundary straddle's middle witness. +#[test] +fn ranked_borsh_at_build_cap() { + assert_eq!( + call1("pin_ranked_borsh", BUILD_CAP_BYTES + 1), + Outcome::Value(0), + ); +} + +/// 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_build_cap() { + assert_eq!( + 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 below the straddle coordinate. +/// +/// 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, 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)); +} + +/// 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. +/// +/// 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_build_cap() { + assert_eq!( + call1("pin_span_borsh", BUILD_CAP_BYTES + 1), + Outcome::Value(0), + ); +} + +/// 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_build_cap() { + assert_eq!( + 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 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 +/// front. +#[test] +fn version_cmp_below_build_cap() { + assert_eq!(call1("pin_version_cmp", BUILD_CAP_BYTES), Outcome::Value(0)); +} + +/// 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 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_build_cap() { + assert_eq!( + call1("pin_version_cmp", BUILD_CAP_BYTES + 1), + Outcome::Value(0), + ); +} + +/// Causal comparison decides a valid stored 67108865-byte pair exactly on +/// wasm32: the boundary straddle's upper witness. +#[test] +fn version_cmp_past_build_cap() { + assert_eq!( + call1("pin_version_cmp", BUILD_CAP_BYTES + 2), + Outcome::Value(0), + ); +} + +/// Causal comparison decides a valid stored pair exactly at 512 MiB both +/// ways around. +/// +/// 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_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. +/// +/// 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 the coordinate. +#[test] +fn version_join_below_build_cap() { + assert_eq!( + call1("pin_version_join_covering", BUILD_CAP_BYTES), + Outcome::Value(0), + ); +} + +/// 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 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() { + assert_eq!( + call1("pin_version_join_covering", BUILD_CAP_BYTES + 1), + Outcome::Value(0), + ); +} + +/// 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() { + assert_eq!( + call1("pin_version_join_covering", BUILD_CAP_BYTES + 2), + Outcome::Value(0), + ); +} + +/// 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. +/// +/// 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!( + call2("pin_version_join_emit", 100_000_000, 168_435_449), + Outcome::Value(536_870_903), + ); +} + +/// 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. +/// +/// 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::Value(536_870_905), + ); +} + +/// 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 +/// 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)); +} + +/// Adding the integral rank 1 to a fraction 2^32 expansion bits deep is +/// exact on wasm32: the sum strictly exceeds both summands. +/// +/// 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() { + assert_eq!(call1("pin_rank_add", 0), Outcome::Value(0)); +} + +/// 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 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() { + assert_eq!(call1("pin_rank_add", 1), Outcome::Value(0)); +} + +/// 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)); +} + +/// 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 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() { + 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, +/// 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 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!( + call2("pin_version_rank", (1u64 << 32) - 96, 64), + Outcome::Trapped(Trap::UnreachableCodeReached), + ); +} diff --git a/crates/suanpan/README.md b/crates/suanpan/README.md index 232214c88..01252b649 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 4948f4982..55487037e 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 7cf3e1069..6911c74b9 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 e3282234d..97a0588d9 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 405f9a787..6a30c60a6 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 ceba6e115..f4ccb84f6 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 67a65c0f5..fa2969dd0 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 01409a61f..aed8bc1d7 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/justfile b/justfile index 8d11636d5..7ab401de9 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,43 @@ 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 surface — the byte and borsh decode doors, the semantic walks +# and emitters, and rank arithmetic — at the exact sizes where 32-bit +# 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. + +# 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 diff --git a/tools/mutantcheck-expected.json b/tools/mutantcheck-expected.json index 5755dfb72..9d53d8d7f 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 }, @@ -25,7 +25,7 @@ "listed": 1, "suppressed": 1 }, - "grow\\.rs:457:38: replace < with <= in recode": { + "grow\\.rs:463:38: replace < with <= in recode": { "listed": 1, "suppressed": 1 },