plugmem_arena/arena.rs
1//! Sharded sorted arena over one flat byte pool.
2//!
3//! Full v2 mechanics of: each shard owns a **chain of
4//! range-partitioned pages** — every page holds a sorted run of keys, pages
5//! in a chain follow each other in ascending key ranges (a B+-tree leaf
6//! level without interior nodes; chains stay short because shard hashing
7//! spreads the load). A full page **splits** in half instead of failing, and
8//! a page emptied by removals is unlinked into a **free-list** for reuse, so
9//! capacity is bounded only by [`ArenaCfg::max_bytes`].
10//!
11//! The structure is tuned for **wasm environments first**: `no_std + alloc`,
12//! a single linear byte pool (snapshot = memcpy), no threads, and the one
13//! measured `unsafe` (uninitialized page allocation) whose entire benefit is
14//! on the wasm allocation path — see [`Arena::insert`] internals and the
15//! crate-level documentation for the numbers.
16//!
17//! # Overlay opens
18//!
19//! [`Arena::load_overlay`] opens an arena whose existing pages are **borrowed**
20//! from a longer-lived buffer — typically a memory-mapped file — while the
21//! arena stays fully mutable. Because an arena mutates pages *in place* (slot
22//! shifts, page splits), it cannot use the append-only tail of
23//! [`BlobHeap`](crate::BlobHeap): instead the first write to a borrowed page
24//! copies just *that* page into an owned overlay, and pages grown after open
25//! live in an owned tail (per-page copy-on-write). The borrowed base is never
26//! mutated and never cloned as a whole, so a multi-gigabyte arena can be
27//! *written to* while resident only in the pages it actually touches. Staying
28//! true to the crate's flat philosophy, the overlay is two flat `Vec`s (a
29//! dense redirect and one contiguous copy pool) — no `Box`, no map. See
30//! `examples/overlay.rs`.
31
32use alloc::vec::Vec;
33use core::fmt;
34use core::marker::PhantomData;
35
36#[cfg(feature = "counters")]
37use core::cell::Cell;
38
39use crate::error::Error;
40use crate::paged::Paged;
41use crate::slot::Slot;
42
43/// Size of one arena page in bytes.
44///
45/// Fixed, not per-slot-count: whatever the slot size, a page is one
46/// L1-friendly unit of work, and every operation touches O(1) pages.
47pub const PAGE_BYTES: usize = 4096;
48
49/// Sentinel for "no page": an empty chain head, the end of a chain, or an
50/// empty free-list.
51const NONE: u32 = u32::MAX;
52
53/// Fibonacci hashing multiplier (2^64 / phi), used by [`ShardMode::Uniform`].
54const FIB: u64 = 0x9E37_79B9_7F4A_7C15;
55
56/// Fixed prefix of the serialized metadata section (see
57/// [`Arena::dump_meta`]).
58const IMAGE_HEADER: usize = 24;
59
60/// Serialized width of one page index entry (`heads`, `next` — little-endian
61/// `u32`), used to size and offset those metadata arrays.
62const PAGE_IDX_BYTES: usize = core::mem::size_of::<u32>();
63
64/// Serialized width of one page's slot count (`counts` — little-endian `u16`).
65const COUNT_BYTES: usize = core::mem::size_of::<u16>();
66
67/// Serialized bytes of per-page metadata: one `next` index plus one `counts`
68/// entry.
69const PAGE_META_BYTES: usize = PAGE_IDX_BYTES + COUNT_BYTES;
70
71/// `true` when the `counters` feature is enabled; lets hot loops keep their
72/// counting code in one expression that constant-folds away otherwise.
73const COUNT: bool = cfg!(feature = "counters");
74
75/// Adds `$n` to counter field `$field` when the `counters` feature is on;
76/// expands to nothing otherwise (zero cost, and the counter expression is
77/// not even evaluated).
78macro_rules! bump {
79 ($self:ident, $field:ident, $n:expr) => {
80 #[cfg(feature = "counters")]
81 {
82 let mut c = $self.counters.get();
83 c.$field += $n as u64;
84 $self.counters.set(c);
85 }
86 };
87}
88
89/// How keys are mapped to shards.
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub enum ShardMode {
93 /// Shard = Fibonacci hash of the key's leading bytes. Spreads any key
94 /// distribution (including sequential ids) evenly across shards. Global
95 /// iteration order is *not* the key order. Use for lookup tables.
96 Uniform,
97 /// Shard = top bits of the key's leading bytes. Shard index order equals
98 /// key order, so [`Arena::iter`] yields globally ascending keys and
99 /// [`Arena::range`] scans work. Keys arriving in a narrow value range
100 /// will concentrate in few shards — their chains simply grow longer;
101 /// that is the trade-off for ordering.
102 Ordered,
103}
104
105/// Arena configuration.
106///
107/// Stored verbatim in snapshots later, so everything that
108/// affects byte interpretation lives here.
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
111pub struct ArenaCfg {
112 /// Number of shards; must be a non-zero power of two.
113 pub shards: usize,
114 /// Hard ceiling for the byte pool; exceeding it fails with
115 /// [`Error::CapacityExceeded`] instead of growing.
116 pub max_bytes: usize,
117 /// Key-to-shard mapping mode.
118 pub mode: ShardMode,
119}
120
121impl ArenaCfg {
122 /// Creates a config with the given shard count and mode, and no byte
123 /// limit (`max_bytes = usize::MAX`).
124 pub const fn new(shards: usize, mode: ShardMode) -> Self {
125 Self {
126 shards,
127 max_bytes: usize::MAX,
128 mode,
129 }
130 }
131
132 /// Returns the config with `max_bytes` replaced.
133 pub const fn with_max_bytes(mut self, max_bytes: usize) -> Self {
134 self.max_bytes = max_bytes;
135 self
136 }
137}
138
139impl Default for ArenaCfg {
140 /// 1024 shards, [`ShardMode::Uniform`], unlimited bytes.
141 fn default() -> Self {
142 Self::new(1024, ShardMode::Uniform)
143 }
144}
145
146/// Deterministic work counters (feature `counters`).
147///
148/// These are the basis of the project's CI performance gates: unlike
149/// wall-clock time they are identical on every machine, so a complexity
150/// regression fails the same way everywhere.
151#[cfg(feature = "counters")]
152#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
153pub struct Counters {
154 /// Key comparisons performed by binary searches.
155 pub cmp_ops: u64,
156 /// Bytes moved by insert/remove shifts and page splits.
157 pub bytes_shifted: u64,
158 /// Pages taken from the pool or the free-list.
159 pub pages_allocated: u64,
160 /// Steps taken walking page chains (each step peeks one page's first
161 /// key — one cache line).
162 pub chain_steps: u64,
163 /// Page splits performed by inserts into full pages.
164 pub splits: u64,
165}
166
167/// A sharded collection of fixed-size records in one contiguous byte pool,
168/// sorted by key prefix within each shard.
169///
170/// See the [crate-level documentation](crate) for the philosophy and the
171/// module documentation for the chain/split mechanics. Highlights:
172///
173/// - all state is `pool` + small metadata arrays — persisting the arena is a
174/// `memcpy` of defined bytes;
175/// - every operation touches O(1) pages: a short chain walk (one first-key
176/// peek per step), then binary search and shifts inside one 4 KiB page;
177/// - capacity is bounded only by [`ArenaCfg::max_bytes`] — full pages split,
178/// emptied pages are recycled through a free-list;
179/// - `Arena` deliberately implements neither `Clone` nor `PartialEq`: pages
180/// are allocated **uninitialized** (the measured wasm optimization), and a
181/// byte-wise clone/compare would read the uninitialized tails.
182pub struct Arena<'a, T: Slot> {
183 /// Page pool; length is always a multiple of [`PAGE_BYTES`]. Bytes of a
184 /// page beyond `counts[page] * T::SIZE` are uninitialized and must
185 /// never be read.
186 ///
187 /// A [`Paged`] backing so the arena can either own the pool (`new`/`load` —
188 /// the writable default, and the only shape on wasm32) or borrow a base
189 /// from a longer-lived buffer such as a memory-mapped snapshot
190 /// (`load_borrowed`/`load_overlay` —). The overlay open borrows
191 /// the base read-only and copies just the touched pages up on first write
192 /// (per-page copy-on-write), so a mapped database is mutated without
193 /// cloning it. The backing is `alloc`-only (no `std`/`Arc`), so the crate
194 /// stays `no_std`, and the lifetime `'a` lets the compiler prove the arena
195 /// never outlives its base. The uninitialized-tail invariant is untouched:
196 /// base pages are a fully-initialized dumped image, overlay copies are
197 /// taken from them, and freshly grown pages keep the same
198 /// read-only-up-to-`count` contract as before.
199 pool: Paged<'a, PAGE_BYTES>,
200 /// Shard -> first page of its chain (`NONE` = empty shard).
201 heads: Vec<u32>,
202 /// Page -> successor: the next page in a shard chain, or the next free
203 /// page when the page sits in the free-list.
204 next: Vec<u32>,
205 /// Page -> predecessor in a shard chain. Rebuilt from `next` on load and
206 /// kept in memory so ordered arenas can scan a range newest-first without
207 /// changing the on-disk metadata format.
208 prev: Vec<u32>,
209 /// Page -> number of occupied slots.
210 counts: Vec<u16>,
211 /// Shard -> last page of its chain (`NONE` when empty). Runtime-only
212 /// metadata paired with `heads` for reverse ordered scans.
213 tails: Vec<u32>,
214 /// Head of the free-list of recycled pages (linked through `next`).
215 free_head: u32,
216 /// Total records across all shards.
217 total: usize,
218 /// Reusable serialization buffer for [`Arena::insert`] (`T::SIZE` bytes,
219 /// allocated once) — inserts do not allocate after the first call except
220 /// when growing the pool itself.
221 scratch: Vec<u8>,
222 /// Reusable buffer for the cross-page slot moves in [`Arena::split`]. A
223 /// split reads a run of slots out of one page and writes it into another;
224 /// under the overlay backing those may be distinct owned pages, so the
225 /// bytes route through this buffer instead of a single in-pool
226 /// `copy_within`. Grown once; splits are amortized and off the recall path.
227 split_buf: Vec<u8>,
228 cfg: ArenaCfg,
229 #[cfg(feature = "counters")]
230 counters: Cell<Counters>,
231 /// `fn() -> T` keeps the arena `Send`/`Sync`-neutral and avoids bounding
232 /// auto-traits on `T` itself.
233 _marker: PhantomData<fn() -> T>,
234}
235
236/// Where a key's record lives (or would live).
237struct Target {
238 /// Page preceding `page` in its chain, `NONE` when `page` is the head.
239 prev: u32,
240 /// The chain page whose key range covers the key.
241 page: u32,
242}
243
244impl<'a, T: Slot> Arena<'a, T> {
245 /// Creates an empty arena.
246 ///
247 /// # Errors
248 ///
249 /// - [`Error::BadSlot`] unless `1 <= T::KEY_LEN <= T::SIZE <= PAGE_BYTES`;
250 /// - [`Error::BadShardCount`] unless `cfg.shards` is a non-zero power of
251 /// two.
252 pub fn new(cfg: ArenaCfg) -> Result<Self, Error> {
253 if T::SIZE == 0 || T::SIZE > PAGE_BYTES || T::KEY_LEN == 0 || T::KEY_LEN > T::SIZE {
254 return Err(Error::BadSlot {
255 size: T::SIZE,
256 key_len: T::KEY_LEN,
257 });
258 }
259 if cfg.shards == 0 || !cfg.shards.is_power_of_two() {
260 return Err(Error::BadShardCount { got: cfg.shards });
261 }
262 Ok(Self {
263 pool: Paged::owned_empty(),
264 heads: alloc::vec![NONE; cfg.shards],
265 next: Vec::new(),
266 prev: Vec::new(),
267 counts: Vec::new(),
268 tails: alloc::vec![NONE; cfg.shards],
269 free_head: NONE,
270 total: 0,
271 scratch: Vec::new(),
272 split_buf: Vec::new(),
273 cfg,
274 #[cfg(feature = "counters")]
275 counters: Cell::new(Counters::default()),
276 _marker: PhantomData,
277 })
278 }
279
280 /// Number of slots one page holds for this slot size (at least 1).
281 pub const fn slots_per_page() -> usize {
282 PAGE_BYTES / T::SIZE
283 }
284
285 /// Total number of records.
286 pub fn len(&self) -> usize {
287 self.total
288 }
289
290 /// `true` when the arena holds no records.
291 pub fn is_empty(&self) -> bool {
292 self.total == 0
293 }
294
295 /// Bytes currently allocated in the page pool (including recycled
296 /// free-list pages — the pool never shrinks; emptied pages are reused).
297 pub fn pool_bytes(&self) -> usize {
298 self.pool.len()
299 }
300
301 /// The configuration this arena was created with.
302 pub fn cfg(&self) -> &ArenaCfg {
303 &self.cfg
304 }
305
306 /// Inserts a record, keeping its shard's chain sorted by key prefix.
307 ///
308 /// Returns `Ok(false)` if a record with the same key prefix already
309 /// exists (the arena is a map keyed by prefix; existing payload is left
310 /// untouched — use [`Arena::payload_mut`] to update in place). A full
311 /// target page splits in half — inserts never fail on page capacity.
312 ///
313 /// # Errors
314 ///
315 /// [`Error::CapacityExceeded`] if a needed page allocation would cross
316 /// [`ArenaCfg::max_bytes`].
317 pub fn insert(&mut self, value: &T) -> Result<bool, Error> {
318 // Take the scratch buffer out of `self` to sidestep aliasing between
319 // `&self.scratch` and `&mut self.pool` below.
320 let mut buf = core::mem::take(&mut self.scratch);
321 buf.resize(T::SIZE, 0);
322 value.write(&mut buf);
323 let result = self.insert_bytes(&buf);
324 self.scratch = buf;
325 result
326 }
327
328 /// Insert path operating on the serialized slot.
329 fn insert_bytes(&mut self, slot: &[u8]) -> Result<bool, Error> {
330 let key = &slot[..T::KEY_LEN];
331 let shard = self.shard_of(key);
332
333 let target = match self.find_page(shard, key) {
334 Some(t) => t,
335 None => {
336 // Empty shard: allocate its first chain page.
337 let page = self.alloc_page()?;
338 self.heads[shard] = page;
339 self.tails[shard] = page;
340 Target { prev: NONE, page }
341 }
342 };
343
344 let mut page = target.page;
345 let mut count = self.counts[page as usize] as usize;
346 let mut pos = {
347 let mut cmps = 0u64;
348 let found = self.search_in(page, count, key, &mut cmps);
349 bump!(self, cmp_ops, cmps);
350 match found {
351 Ok(_) => return Ok(false),
352 Err(pos) => pos,
353 }
354 };
355
356 if count == Self::slots_per_page() {
357 // Split the full page; afterwards (page, pos, count) address the
358 // half that must receive the new record.
359 (page, pos, count) = self.split(page, pos)?;
360 }
361
362 let slot_start = pos * T::SIZE;
363 let used_end = count * T::SIZE;
364 let shifted = used_end - slot_start;
365 let bytes = self.pool.page_mut(page);
366 if shifted > 0 {
367 // Shift the sorted tail right by one slot; stays within the page.
368 bytes.copy_within(slot_start..used_end, slot_start + T::SIZE);
369 }
370 bytes[slot_start..slot_start + T::SIZE].copy_from_slice(slot);
371 bump!(self, bytes_shifted, shifted);
372
373 self.counts[page as usize] += 1;
374 self.total += 1;
375 Ok(true)
376 }
377
378 /// Splits full `page`, given the insert position `pos` inside it.
379 /// Returns `(target_page, target_pos, target_count)` for the record that
380 /// triggered the split.
381 fn split(&mut self, page: u32, pos: usize) -> Result<(u32, usize, usize), Error> {
382 let spp = Self::slots_per_page();
383 let fresh = self.alloc_page()?;
384 bump!(self, splits, 1);
385
386 // Link the fresh page right after the split one.
387 let successor = self.next[page as usize];
388 self.next[fresh as usize] = successor;
389 self.prev[fresh as usize] = page;
390 self.next[page as usize] = fresh;
391 if successor == NONE {
392 let shard = self.shard_of(self.first_key(page));
393 self.tails[shard] = fresh;
394 } else {
395 self.prev[successor as usize] = fresh;
396 }
397
398 if spp == 1 {
399 // Degenerate single-slot pages (T::SIZE > PAGE_BYTES / 2): the
400 // "upper half" is the whole record when the new key precedes it,
401 // otherwise the fresh page simply receives the new record.
402 return Ok(if pos == 0 {
403 self.move_slots(page, 0, fresh, T::SIZE);
404 bump!(self, bytes_shifted, T::SIZE);
405 self.counts[page as usize] = 0;
406 self.counts[fresh as usize] = 1;
407 (page, 0, 0)
408 } else {
409 (fresh, 0, 0)
410 });
411 }
412
413 // Move the upper half [half..spp) into the fresh page.
414 let half = spp / 2;
415 let moved = spp - half;
416 self.move_slots(page, half * T::SIZE, fresh, moved * T::SIZE);
417 bump!(self, bytes_shifted, moved * T::SIZE);
418 self.counts[page as usize] = half as u16;
419 self.counts[fresh as usize] = moved as u16;
420
421 // `pos` was computed against the full page; place the new record in
422 // whichever half now owns that position (pos == half belongs at the
423 // end of the lower page: the key sorts before the fresh page's first).
424 Ok(if pos <= half {
425 (page, pos, half)
426 } else {
427 (fresh, pos - half, moved)
428 })
429 }
430
431 /// Copies `len` bytes from `src_page` (starting at in-page offset
432 /// `src_off`) to the start of `dst_page`. The two pages may be distinct
433 /// owned pages under the overlay backing, so the bytes route through the
434 /// reusable [`Arena::split_buf`] rather than a single in-pool
435 /// `copy_within` — one immutable read of the source, then one write of the
436 /// destination (which copies-up if it is still a borrowed base page).
437 fn move_slots(&mut self, src_page: u32, src_off: usize, dst_page: u32, len: usize) {
438 let mut buf = core::mem::take(&mut self.split_buf);
439 buf.clear();
440 buf.extend_from_slice(&self.pool.page(src_page)[src_off..src_off + len]);
441 self.pool.page_mut(dst_page)[..len].copy_from_slice(&buf);
442 self.split_buf = buf;
443 }
444
445 /// Returns the record with the given key prefix, if present.
446 ///
447 /// # Panics
448 ///
449 /// Panics if `key.len() != T::KEY_LEN` (a caller bug, not a data
450 /// condition — mirrors slice indexing).
451 pub fn get(&self, key: &[u8]) -> Option<T> {
452 self.locate(key).map(|off| {
453 let (page, rel) = (off / PAGE_BYTES, off % PAGE_BYTES);
454 T::read(&self.pool.page(page as u32)[rel..rel + T::SIZE])
455 })
456 }
457
458 /// `true` if a record with the given key prefix exists.
459 ///
460 /// # Panics
461 ///
462 /// Panics if `key.len() != T::KEY_LEN`.
463 pub fn contains(&self, key: &[u8]) -> bool {
464 self.locate(key).is_some()
465 }
466
467 /// Returns the raw bytes of the record with the given key prefix.
468 ///
469 /// # Panics
470 ///
471 /// Panics if `key.len() != T::KEY_LEN`.
472 pub fn get_slot(&self, key: &[u8]) -> Option<&[u8]> {
473 self.locate(key).map(|off| {
474 let (page, rel) = (off / PAGE_BYTES, off % PAGE_BYTES);
475 &self.pool.page(page as u32)[rel..rel + T::SIZE]
476 })
477 }
478
479 /// Returns a mutable view of the record's **payload** (the bytes after
480 /// the key prefix), for in-place updates.
481 ///
482 /// The key prefix itself is not reachable through this method — sorted
483 /// order cannot be corrupted by construction, which is why this is safe
484 /// to expose at all.
485 ///
486 /// # Panics
487 ///
488 /// Panics if `key.len() != T::KEY_LEN`.
489 pub fn payload_mut(&mut self, key: &[u8]) -> Option<&mut [u8]> {
490 let off = self.locate(key)?;
491 let (page, rel) = (off / PAGE_BYTES, off % PAGE_BYTES);
492 Some(&mut self.pool.page_mut(page as u32)[rel + T::KEY_LEN..rel + T::SIZE])
493 }
494
495 /// Removes the record with the given key prefix. Returns `true` if it
496 /// existed. A page emptied by the removal is unlinked from its chain and
497 /// recycled through the free-list.
498 ///
499 /// # Panics
500 ///
501 /// Panics if `key.len() != T::KEY_LEN`.
502 pub fn remove(&mut self, key: &[u8]) -> bool {
503 assert_eq!(key.len(), T::KEY_LEN, "key length must equal Slot::KEY_LEN");
504 let shard = self.shard_of(key);
505 let Some(Target { prev, page }) = self.find_page(shard, key) else {
506 return false;
507 };
508 let count = self.counts[page as usize] as usize;
509 let pos = {
510 let mut cmps = 0u64;
511 let found = self.search_in(page, count, key, &mut cmps);
512 bump!(self, cmp_ops, cmps);
513 match found {
514 Ok(pos) => pos,
515 Err(_) => return false,
516 }
517 };
518
519 let slot_start = pos * T::SIZE;
520 let used_end = count * T::SIZE;
521 let tail = used_end - (slot_start + T::SIZE);
522 if tail > 0 {
523 // Shift the tail left over the removed slot; stays within the page.
524 self.pool
525 .page_mut(page)
526 .copy_within(slot_start + T::SIZE..used_end, slot_start);
527 }
528 bump!(self, bytes_shifted, tail);
529 self.counts[page as usize] -= 1;
530 self.total -= 1;
531
532 if self.counts[page as usize] == 0 {
533 // Unlink the emptied page and recycle it.
534 let successor = self.next[page as usize];
535 if prev == NONE {
536 self.heads[shard] = successor;
537 } else {
538 self.next[prev as usize] = successor;
539 }
540 if successor == NONE {
541 self.tails[shard] = prev;
542 } else {
543 self.prev[successor as usize] = prev;
544 }
545 self.next[page as usize] = self.free_head;
546 self.prev[page as usize] = NONE;
547 self.free_head = page;
548 }
549 true
550 }
551
552 /// Iterates all records shard by shard, following each shard's chain.
553 ///
554 /// In [`ShardMode::Ordered`] the shard index order equals key order and
555 /// chains are range-partitioned, so this yields **globally ascending
556 /// keys**. In [`ShardMode::Uniform`] the global order is unspecified
557 /// (each shard is still internally sorted).
558 pub fn iter(&self) -> Iter<'_, T> {
559 Iter {
560 arena: self,
561 shard: 0,
562 page: NONE,
563 idx: 0,
564 remaining: self.total,
565 }
566 }
567
568 /// Iterates records whose key prefix lies in `[from, to)` — `from`
569 /// inclusive, `to` exclusive — in ascending key order.
570 ///
571 /// Only meaningful when shard order equals key order, hence restricted
572 /// to [`ShardMode::Ordered`].
573 ///
574 /// # Panics
575 ///
576 /// Panics if the arena is in [`ShardMode::Uniform`], or if either bound's
577 /// length differs from `T::KEY_LEN`.
578 pub fn range<'s>(&'s self, from: &[u8], to: &'s [u8]) -> Range<'s, T> {
579 assert_eq!(
580 self.cfg.mode,
581 ShardMode::Ordered,
582 "range scans require ShardMode::Ordered"
583 );
584 assert_eq!(
585 from.len(),
586 T::KEY_LEN,
587 "key length must equal Slot::KEY_LEN"
588 );
589 assert_eq!(to.len(), T::KEY_LEN, "key length must equal Slot::KEY_LEN");
590
591 // Position on the first record with key >= `from`.
592 let shard = self.shard_of(from);
593 let mut page = NONE;
594 let mut idx = 0usize;
595 if let Some(t) = self.find_page(shard, from) {
596 let count = self.counts[t.page as usize] as usize;
597 let mut cmps = 0u64;
598 let pos = match self.search_in(t.page, count, from, &mut cmps) {
599 Ok(p) | Err(p) => p,
600 };
601 bump!(self, cmp_ops, cmps);
602 if pos < count {
603 page = t.page;
604 idx = pos;
605 } else {
606 // Past the last record of the covering page: continue with
607 // the next page in this chain, or fall through to the next
608 // shard.
609 page = self.next[t.page as usize];
610 }
611 }
612 Range {
613 arena: self,
614 // The shard the iterator moves to once the current chain (if
615 // any) is exhausted.
616 shard: shard + 1,
617 page,
618 idx,
619 to,
620 }
621 }
622
623 /// Iterates records whose key prefix lies in `[from, to)` — `from`
624 /// inclusive, `to` exclusive — in descending key order.
625 ///
626 /// This is the bounded newest-first counterpart to [`Arena::range`]. It
627 /// is restricted to ordered arenas and keeps the predecessor links in
628 /// runtime metadata, so the on-disk arena format remains unchanged.
629 pub fn range_rev<'s>(&'s self, from: &'s [u8], to: &'s [u8]) -> RangeRev<'s, T> {
630 assert_eq!(
631 self.cfg.mode,
632 ShardMode::Ordered,
633 "reverse range scans require ShardMode::Ordered"
634 );
635 assert_eq!(
636 from.len(),
637 T::KEY_LEN,
638 "key length must equal Slot::KEY_LEN"
639 );
640 assert_eq!(to.len(), T::KEY_LEN, "key length must equal Slot::KEY_LEN");
641
642 if from >= to {
643 return RangeRev {
644 arena: self,
645 shard: 0,
646 page: NONE,
647 idx: 0,
648 from,
649 };
650 }
651
652 // Position immediately before `to`: an exact key is excluded, while
653 // an insertion point after the page's last key starts at that last
654 // key. If the covering page has no key below `to`, walk to its
655 // predecessor.
656 let shard = self.shard_of(to);
657 let mut page = NONE;
658 let mut idx = 0usize;
659 if let Some(t) = self.find_page(shard, to) {
660 let count = self.counts[t.page as usize] as usize;
661 let mut cmps = 0u64;
662 let pos = match self.search_in(t.page, count, to, &mut cmps) {
663 Ok(p) | Err(p) => p,
664 };
665 bump!(self, cmp_ops, cmps);
666 if pos > 0 {
667 page = t.page;
668 idx = pos;
669 } else {
670 page = self.prev[t.page as usize];
671 if page != NONE {
672 idx = self.counts[page as usize] as usize;
673 }
674 }
675 }
676
677 RangeRev {
678 arena: self,
679 // The shard the iterator moves to once the current chain (if
680 // any) is exhausted.
681 shard,
682 page,
683 idx,
684 from,
685 }
686 }
687
688 /// A snapshot of the work counters (feature `counters`).
689 #[cfg(feature = "counters")]
690 pub fn counters(&self) -> Counters {
691 self.counters.get()
692 }
693
694 /// Resets all work counters to zero (feature `counters`).
695 #[cfg(feature = "counters")]
696 pub fn reset_counters(&self) {
697 self.counters.set(Counters::default());
698 }
699
700 /// Appends the arena's metadata section to `out`.
701 ///
702 /// Layout (all little-endian): `[shards u32][pages u32][free_head u32]
703 /// [total u64][mode u8][reserved 3]`, then `heads` (`shards × u32`),
704 /// `next` (`pages × u32`), `counts` (`pages × u16`). Together with
705 /// [`Arena::dump_pool`] this is the complete state; both dumps are
706 /// canonical — dump → [`Arena::load`] → dump reproduces identical
707 /// bytes.
708 pub fn dump_meta(&self, out: &mut Vec<u8>) {
709 let pages = self.pool.len() / PAGE_BYTES;
710 debug_assert!(self.cfg.shards <= u32::MAX as usize && pages < u32::MAX as usize);
711 out.reserve(IMAGE_HEADER + self.heads.len() * PAGE_IDX_BYTES + pages * PAGE_META_BYTES);
712 out.extend_from_slice(&(self.cfg.shards as u32).to_le_bytes());
713 out.extend_from_slice(&(pages as u32).to_le_bytes());
714 out.extend_from_slice(&self.free_head.to_le_bytes());
715 out.extend_from_slice(&(self.total as u64).to_le_bytes());
716 out.push(match self.cfg.mode {
717 ShardMode::Uniform => 0,
718 ShardMode::Ordered => 1,
719 });
720 out.extend_from_slice(&[0u8; 3]);
721 for &head in &self.heads {
722 out.extend_from_slice(&head.to_le_bytes());
723 }
724 for &next in &self.next {
725 out.extend_from_slice(&next.to_le_bytes());
726 }
727 for &count in &self.counts {
728 out.extend_from_slice(&count.to_le_bytes());
729 }
730 }
731
732 /// Appends the arena's pool section to `out`.
733 ///
734 /// Each page contributes its initialized prefix (`counts[page] ×
735 /// SIZE` bytes) followed by zero padding to [`PAGE_BYTES`]: the
736 /// uninitialized page tails (see [`Arena::insert`] internals) are
737 /// never read, and zero-filling them makes the image canonical and
738 /// leak-free.
739 pub fn dump_pool(&self, out: &mut Vec<u8>) {
740 out.reserve(self.pool.len());
741 for (page, &count) in self.counts.iter().enumerate() {
742 let used = count as usize * T::SIZE;
743 out.extend_from_slice(&self.pool.page(page as u32)[..used]);
744 out.resize(out.len() + (PAGE_BYTES - used), 0);
745 }
746 }
747
748 /// Rebuilds an arena from its two dumped sections.
749 ///
750 /// The input is **untrusted**: every metadata invariant is checked
751 /// before adoption and any inconsistency returns
752 /// [`Error::Corrupt`] — this method never panics on arbitrary bytes.
753 /// Cost is O(pages) on top of the pool copy:
754 ///
755 /// - exact section lengths; `cfg` agreement (shards, mode);
756 /// - per-page slot counts within `PAGE_BYTES / SIZE`;
757 /// - every page reachable exactly once — shard chains and the
758 /// free-list are walked with a visited bitmap (no cycles, no shared
759 /// or orphan pages); chain pages are non-empty, free pages empty;
760 /// - chain pages ascend by key range and sit in the shard their first
761 /// key maps to; the record total matches the page counts.
762 ///
763 /// Slot *content* is not validated beyond the per-page first/last key
764 /// reads — record payloads are semantically validated lazily by the
765 /// owning engine.
766 ///
767 /// # Errors
768 ///
769 /// [`Error::BadSlot`] / [`Error::BadShardCount`] for an invalid `cfg`
770 /// (same gates as [`Arena::new`]); [`Error::Corrupt`] for any image
771 /// inconsistency.
772 pub fn load(cfg: ArenaCfg, meta: &[u8], pool: &[u8]) -> Result<Self, Error> {
773 Self::load_impl(cfg, meta, pool, Paged::owned_from(pool.to_vec()))
774 }
775
776 /// Rebuilds an arena that **borrows** its page pool from a longer-lived
777 /// buffer (a memory-mapped snapshot) instead of copying it.
778 /// Validation is identical to [`Arena::load`] — it reads each page's
779 /// first and last keys, so it touches one OS page per arena page (the
780 /// key-order check needs them), but no pool byte is copied.
781 ///
782 /// # Errors
783 ///
784 /// Same as [`Arena::load`].
785 pub fn load_borrowed(cfg: ArenaCfg, meta: &[u8], pool: &'a [u8]) -> Result<Self, Error> {
786 Self::load_impl(cfg, meta, pool, Paged::borrowed(pool))
787 }
788
789 /// Opens an arena over a borrowed base for the **overlay** write path: the
790 /// base pages are mapped read-only, and the first write to any base page
791 /// copies just that page into owned storage (per-page copy-on-write, see
792 /// [`Paged`](crate::paged)), while pages grown after open live in an owned
793 /// tail. Unlike [`Arena::load_borrowed`] the returned arena is fully
794 /// mutable — inserts, removals and splits work — yet the borrowed base is
795 /// never cloned as a whole and never mutated, so a memory-mapped database
796 /// can be written to while resident only in the pages it actually touches.
797 /// Validation is identical to [`Arena::load`].
798 ///
799 /// # Errors
800 ///
801 /// Same as [`Arena::load`].
802 pub fn load_overlay(cfg: ArenaCfg, meta: &[u8], pool: &'a [u8]) -> Result<Self, Error> {
803 Self::load_impl(cfg, meta, pool, Paged::borrowed(pool))
804 }
805
806 /// Shared body of [`Arena::load`] / [`Arena::load_borrowed`] /
807 /// [`Arena::load_overlay`]: validates the untrusted `meta`/`pool` image and
808 /// adopts `backing` as the pool.
809 fn load_impl(
810 cfg: ArenaCfg,
811 meta: &[u8],
812 pool: &[u8],
813 backing: Paged<'a, PAGE_BYTES>,
814 ) -> Result<Self, Error> {
815 let mut arena = Self::new(cfg)?;
816 if meta.len() < IMAGE_HEADER {
817 return Err(Error::Corrupt("arena meta shorter than its header"));
818 }
819 let shards = u32::from_le_bytes(meta[0..4].try_into().unwrap()) as usize;
820 let pages = u32::from_le_bytes(meta[4..8].try_into().unwrap()) as usize;
821 let free_head = u32::from_le_bytes(meta[8..12].try_into().unwrap());
822 let total = u64::from_le_bytes(meta[12..20].try_into().unwrap());
823 let mode = meta[20];
824 if meta[21..24] != [0u8; 3] {
825 return Err(Error::Corrupt("arena meta reserved bytes must be zero"));
826 }
827 if shards != cfg.shards {
828 return Err(Error::Corrupt("arena meta shard count disagrees with cfg"));
829 }
830 let want_mode = match cfg.mode {
831 ShardMode::Uniform => 0u8,
832 ShardMode::Ordered => 1,
833 };
834 if mode != want_mode {
835 return Err(Error::Corrupt("arena meta shard mode disagrees with cfg"));
836 }
837 if pages as u64 >= u64::from(NONE) {
838 return Err(Error::Corrupt("arena page count overflows the index space"));
839 }
840 let want_meta = IMAGE_HEADER as u64
841 + shards as u64 * PAGE_IDX_BYTES as u64
842 + pages as u64 * PAGE_META_BYTES as u64;
843 if meta.len() as u64 != want_meta {
844 return Err(Error::Corrupt("arena meta length mismatch"));
845 }
846 if pool.len() as u64 != pages as u64 * PAGE_BYTES as u64 {
847 return Err(Error::Corrupt("arena pool length mismatch"));
848 }
849
850 let mut heads = Vec::with_capacity(shards);
851 for i in 0..shards {
852 let at = IMAGE_HEADER + i * PAGE_IDX_BYTES;
853 heads.push(u32::from_le_bytes(
854 meta[at..at + PAGE_IDX_BYTES].try_into().unwrap(),
855 ));
856 }
857 let next_base = IMAGE_HEADER + shards * PAGE_IDX_BYTES;
858 let mut next = Vec::with_capacity(pages);
859 for i in 0..pages {
860 let at = next_base + i * PAGE_IDX_BYTES;
861 next.push(u32::from_le_bytes(
862 meta[at..at + PAGE_IDX_BYTES].try_into().unwrap(),
863 ));
864 }
865 let counts_base = next_base + pages * PAGE_IDX_BYTES;
866 let mut counts = Vec::with_capacity(pages);
867 for i in 0..pages {
868 let at = counts_base + i * COUNT_BYTES;
869 counts.push(u16::from_le_bytes(
870 meta[at..at + COUNT_BYTES].try_into().unwrap(),
871 ));
872 }
873 let mut prev_links = alloc::vec![NONE; pages];
874 let mut tails = alloc::vec![NONE; shards];
875
876 let spp = Self::slots_per_page();
877 if counts.iter().any(|&c| c as usize > spp) {
878 return Err(Error::Corrupt("arena page count exceeds slots per page"));
879 }
880
881 // Reachability walk: every page must appear exactly once across all
882 // shard chains and the free-list; the bitmap catches cycles, shared
883 // pages and (after the walks) orphans.
884 let mut seen = alloc::vec![false; pages];
885 let mut live = 0u64;
886 for (shard, &head) in heads.iter().enumerate() {
887 let mut page = head;
888 let mut predecessor = NONE;
889 while page != NONE {
890 let p = page as usize;
891 if p >= pages {
892 return Err(Error::Corrupt("arena chain page out of bounds"));
893 }
894 if core::mem::replace(&mut seen[p], true) {
895 return Err(Error::Corrupt("arena page linked more than once"));
896 }
897 let count = counts[p] as usize;
898 if count == 0 {
899 return Err(Error::Corrupt("arena chain contains an empty page"));
900 }
901 live += count as u64;
902 let first = &pool[p * PAGE_BYTES..p * PAGE_BYTES + T::KEY_LEN];
903 if arena.shard_of(first) != shard {
904 return Err(Error::Corrupt("arena page sits in the wrong shard"));
905 }
906 if predecessor != NONE {
907 let pr = predecessor as usize;
908 let last_at = pr * PAGE_BYTES + (counts[pr] as usize - 1) * T::SIZE;
909 if pool[last_at..last_at + T::KEY_LEN] >= *first {
910 return Err(Error::Corrupt("arena chain pages out of key order"));
911 }
912 }
913 prev_links[p] = predecessor;
914 if next[p] == NONE {
915 tails[shard] = page;
916 }
917 predecessor = page;
918 page = next[p];
919 }
920 }
921 let mut page = free_head;
922 while page != NONE {
923 let p = page as usize;
924 if p >= pages {
925 return Err(Error::Corrupt("arena free page out of bounds"));
926 }
927 if core::mem::replace(&mut seen[p], true) {
928 return Err(Error::Corrupt("arena page linked more than once"));
929 }
930 if counts[p] != 0 {
931 return Err(Error::Corrupt("arena free page has a nonzero count"));
932 }
933 page = next[p];
934 }
935 if seen.iter().any(|&s| !s) {
936 return Err(Error::Corrupt("arena has an orphan page"));
937 }
938 if live != total {
939 return Err(Error::Corrupt(
940 "arena record total disagrees with page counts",
941 ));
942 }
943
944 arena.pool = backing;
945 arena.heads = heads;
946 arena.next = next;
947 arena.prev = prev_links;
948 arena.counts = counts;
949 arena.tails = tails;
950 arena.free_head = free_head;
951 arena.total = total as usize;
952 Ok(arena)
953 }
954
955 /// Walks the shard's chain to the page whose key range covers `key`.
956 /// `None` when the shard is empty.
957 fn find_page(&self, shard: usize, key: &[u8]) -> Option<Target> {
958 let head = self.heads[shard];
959 if head == NONE {
960 return None;
961 }
962 let mut prev = NONE;
963 let mut page = head;
964 let mut steps = 0u64;
965 loop {
966 let nxt = self.next[page as usize];
967 // Advance while the next page's range can still contain the key
968 // (its first key is <= key). Peeking a first key touches one
969 // cache line.
970 if nxt == NONE || self.first_key(nxt) > key {
971 break;
972 }
973 steps += COUNT as u64;
974 prev = page;
975 page = nxt;
976 }
977 bump!(self, chain_steps, steps);
978 let _ = steps; // read only by the counters feature
979 Some(Target { prev, page })
980 }
981
982 /// First key of a page. Caller guarantees the page is non-empty (every
983 /// page in a chain holds at least one record — emptied pages are
984 /// unlinked immediately).
985 fn first_key(&self, page: u32) -> &[u8] {
986 &self.pool.page(page)[..T::KEY_LEN]
987 }
988
989 /// Binary search inside a page; wraps the free-function search with the
990 /// page slice resolution.
991 fn search_in(
992 &self,
993 page: u32,
994 count: usize,
995 key: &[u8],
996 cmps: &mut u64,
997 ) -> Result<usize, usize> {
998 search::<T>(self.pool.page(page), count, key, cmps)
999 }
1000
1001 /// Byte offset of the slot with the given key, if present.
1002 fn locate(&self, key: &[u8]) -> Option<usize> {
1003 assert_eq!(key.len(), T::KEY_LEN, "key length must equal Slot::KEY_LEN");
1004 let shard = self.shard_of(key);
1005 let Target { page, .. } = self.find_page(shard, key)?;
1006 let count = self.counts[page as usize] as usize;
1007 let mut cmps = 0u64;
1008 let found = self.search_in(page, count, key, &mut cmps).ok();
1009 bump!(self, cmp_ops, cmps);
1010 found.map(|pos| page as usize * PAGE_BYTES + pos * T::SIZE)
1011 }
1012
1013 /// Maps a key to its shard. Only the first 8 key bytes participate;
1014 /// longer keys sharing an 8-byte prefix land in the same shard (harmless
1015 /// for `Ordered`: order across shards is still by prefix).
1016 fn shard_of(&self, key: &[u8]) -> usize {
1017 let mut pad = [0u8; 8];
1018 let n = key.len().min(8);
1019 pad[..n].copy_from_slice(&key[..n]);
1020 let v = u64::from_be_bytes(pad);
1021 let bits = self.cfg.shards.trailing_zeros();
1022 if bits == 0 {
1023 return 0; // single shard; also avoids the undefined `v >> 64`
1024 }
1025 let h = match self.cfg.mode {
1026 ShardMode::Ordered => v,
1027 ShardMode::Uniform => v.wrapping_mul(FIB),
1028 };
1029 (h >> (64 - bits)) as usize
1030 }
1031
1032 /// Takes a page from the free-list, or grows the pool by one page.
1033 fn alloc_page(&mut self) -> Result<u32, Error> {
1034 if self.free_head != NONE {
1035 let page = self.free_head;
1036 self.free_head = self.next[page as usize];
1037 self.next[page as usize] = NONE;
1038 self.prev[page as usize] = NONE;
1039 self.counts[page as usize] = 0;
1040 bump!(self, pages_allocated, 1);
1041 return Ok(page);
1042 }
1043
1044 let old_len = self.pool.len();
1045 let new_len = old_len + PAGE_BYTES;
1046 if new_len > self.cfg.max_bytes {
1047 return Err(Error::CapacityExceeded {
1048 max_bytes: self.cfg.max_bytes,
1049 });
1050 }
1051 let page = (old_len / PAGE_BYTES) as u32;
1052 // Grow the owned tail by one page: the whole vector when owned, the
1053 // overlay's grown tail when borrowing (the borrowed base is never
1054 // resized). `grown_tail_mut` hands back that owned `Vec` directly — no
1055 // clone of the base — and the new page's global index is `old_len /
1056 // PAGE_BYTES` regardless of which tail holds it.
1057 let tail = self.pool.grown_tail_mut();
1058 let tail_len = tail.len() + PAGE_BYTES;
1059 tail.reserve(PAGE_BYTES);
1060 // SAFETY: the new page is left uninitialized on purpose — this is the
1061 // one measured unsafe of the crate. Zeroing fresh pages was benched
1062 // at 12x slower on the wasm allocation path (wasmtime, 32k pages:
1063 // 3889 us zeroed vs 316 us uninit; native: noise) — and wasm is this
1064 // structure's primary environment. The invariant making it sound:
1065 // *no byte of a page beyond `counts[page] * T::SIZE` is ever read*.
1066 // Every read (search, get, iter, range, first_key, shifts) is
1067 // bounded by the page count, and a slot's bytes are fully written
1068 // before the count is incremented. Consequently `Arena` exposes no
1069 // whole-pool reads (no `Clone`/`PartialEq`/`as_bytes`); the snapshot
1070 // writer emits only the initialized prefixes of pages.
1071 // Routing the same reserve+set_len at the grown tail (not the base)
1072 // keeps this the crate's sole `unsafe` — the overlay adds none.
1073 #[allow(clippy::uninit_vec)]
1074 unsafe {
1075 tail.set_len(tail_len);
1076 }
1077 self.next.push(NONE);
1078 self.prev.push(NONE);
1079 self.counts.push(0);
1080 bump!(self, pages_allocated, 1);
1081 Ok(page)
1082 }
1083}
1084
1085/// Binary search over a page's occupied slots, comparing key prefixes.
1086///
1087/// Free function (not a method) so the borrow of the page slice stays
1088/// independent from `&mut self` at call sites. Counting compiles away when
1089/// the `counters` feature is off (`COUNT` is a const `false`).
1090fn search<T: Slot>(page: &[u8], count: usize, key: &[u8], cmps: &mut u64) -> Result<usize, usize> {
1091 let mut lo = 0usize;
1092 let mut hi = count;
1093 while lo < hi {
1094 let mid = lo + (hi - lo) / 2;
1095 let off = mid * T::SIZE;
1096 // Branchless: adds 0 when the `counters` feature is off, which the
1097 // compiler folds away entirely.
1098 *cmps += COUNT as u64;
1099 match page[off..off + T::KEY_LEN].cmp(key) {
1100 core::cmp::Ordering::Less => lo = mid + 1,
1101 core::cmp::Ordering::Greater => hi = mid,
1102 core::cmp::Ordering::Equal => return Ok(mid),
1103 }
1104 }
1105 Err(lo)
1106}
1107
1108impl<T: Slot> fmt::Debug for Arena<'_, T> {
1109 /// Summary only — dumping the pool would both flood output and read
1110 /// uninitialized page tails.
1111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1112 f.debug_struct("Arena")
1113 .field("len", &self.total)
1114 .field("shards", &self.cfg.shards)
1115 .field("mode", &self.cfg.mode)
1116 .field("pages", &(self.pool.len() / PAGE_BYTES))
1117 .field("slot_size", &T::SIZE)
1118 .finish()
1119 }
1120}
1121
1122/// Iterator over all records of an [`Arena`]; see [`Arena::iter`] for
1123/// ordering guarantees.
1124pub struct Iter<'a, T: Slot> {
1125 arena: &'a Arena<'a, T>,
1126 shard: usize,
1127 /// Current chain page; `NONE` means "advance to the next shard's head".
1128 page: u32,
1129 idx: usize,
1130 remaining: usize,
1131}
1132
1133impl<T: Slot> Iterator for Iter<'_, T> {
1134 type Item = T;
1135
1136 fn next(&mut self) -> Option<T> {
1137 loop {
1138 if self.page == NONE {
1139 if self.shard >= self.arena.cfg.shards {
1140 return None;
1141 }
1142 self.page = self.arena.heads[self.shard];
1143 self.idx = 0;
1144 self.shard += 1;
1145 continue;
1146 }
1147 if self.idx < self.arena.counts[self.page as usize] as usize {
1148 let rel = self.idx * T::SIZE;
1149 self.idx += 1;
1150 self.remaining -= 1;
1151 return Some(T::read(
1152 &self.arena.pool.page(self.page)[rel..rel + T::SIZE],
1153 ));
1154 }
1155 self.page = self.arena.next[self.page as usize];
1156 self.idx = 0;
1157 }
1158 }
1159
1160 fn size_hint(&self) -> (usize, Option<usize>) {
1161 (self.remaining, Some(self.remaining))
1162 }
1163}
1164
1165impl<T: Slot> ExactSizeIterator for Iter<'_, T> {}
1166
1167impl<'a, T: Slot> IntoIterator for &'a Arena<'a, T> {
1168 type Item = T;
1169 type IntoIter = Iter<'a, T>;
1170
1171 fn into_iter(self) -> Self::IntoIter {
1172 self.iter()
1173 }
1174}
1175
1176/// Iterator over a key range of an [`Arena`]; see [`Arena::range`].
1177pub struct Range<'a, T: Slot> {
1178 arena: &'a Arena<'a, T>,
1179 shard: usize,
1180 /// Current chain page; `NONE` means "advance to the next shard's head".
1181 page: u32,
1182 idx: usize,
1183 /// Exclusive upper bound on key prefixes.
1184 to: &'a [u8],
1185}
1186
1187impl<T: Slot> Iterator for Range<'_, T> {
1188 type Item = T;
1189
1190 fn next(&mut self) -> Option<T> {
1191 loop {
1192 if self.page == NONE {
1193 if self.shard >= self.arena.cfg.shards {
1194 return None;
1195 }
1196 self.page = self.arena.heads[self.shard];
1197 self.idx = 0;
1198 self.shard += 1;
1199 continue;
1200 }
1201 if self.idx < self.arena.counts[self.page as usize] as usize {
1202 let rel = self.idx * T::SIZE;
1203 let page = self.arena.pool.page(self.page);
1204 if page[rel..rel + T::KEY_LEN] >= *self.to {
1205 // Keys only grow from here on — the scan is complete.
1206 return None;
1207 }
1208 self.idx += 1;
1209 return Some(T::read(&page[rel..rel + T::SIZE]));
1210 }
1211 self.page = self.arena.next[self.page as usize];
1212 self.idx = 0;
1213 }
1214 }
1215}
1216
1217/// Iterator over a key range of an [`Arena`] in descending order; see
1218/// [`Arena::range_rev`].
1219pub struct RangeRev<'a, T: Slot> {
1220 arena: &'a Arena<'a, T>,
1221 /// The current shard. Lower shards are visited after the current chain.
1222 shard: usize,
1223 /// Current chain page; `NONE` means "advance to the previous shard's
1224 /// tail".
1225 page: u32,
1226 /// Exclusive upper slot index in the current page.
1227 idx: usize,
1228 /// Inclusive lower key bound.
1229 from: &'a [u8],
1230}
1231
1232impl<T: Slot> Iterator for RangeRev<'_, T> {
1233 type Item = T;
1234
1235 fn next(&mut self) -> Option<T> {
1236 loop {
1237 if self.page == NONE {
1238 if self.shard == 0 {
1239 return None;
1240 }
1241 self.shard -= 1;
1242 self.page = self.arena.tails[self.shard];
1243 self.idx = if self.page == NONE {
1244 0
1245 } else {
1246 self.arena.counts[self.page as usize] as usize
1247 };
1248 continue;
1249 }
1250
1251 if self.idx == 0 {
1252 self.page = self.arena.prev[self.page as usize];
1253 self.idx = if self.page == NONE {
1254 0
1255 } else {
1256 self.arena.counts[self.page as usize] as usize
1257 };
1258 continue;
1259 }
1260
1261 self.idx -= 1;
1262 let rel = self.idx * T::SIZE;
1263 let page = self.arena.pool.page(self.page);
1264 if page[rel..rel + T::KEY_LEN] < *self.from {
1265 return None;
1266 }
1267 return Some(T::read(&page[rel..rel + T::SIZE]));
1268 }
1269 }
1270}