Skip to main content

plugmem_arena/
lib.rs

1//! The crate README is included below as module documentation, which makes
2//! every Rust example in it a doctest: a README that drifts from the API stops
3//! compiling instead of quietly lying.
4#![doc = include_str!("../README.md")]
5//! Flat byte-pool storage structures for plugmem.
6//!
7//! This crate is the storage foundation of the plugmem engine, but it is
8//! deliberately generic: nothing here knows about facts, vectors or LLMs.
9//! If you need a compact, allocation-frugal, `no_std` sorted container whose
10//! in-memory representation *is* its serialized form, you can lift it into
11//! your own project as-is — see `examples/` for self-contained walkthroughs.
12//!
13//! # Philosophy
14//!
15//! 1. **State is flat bytes.** A container is one contiguous byte pool plus a
16//!    few small metadata arrays. No per-element allocations, no pointers, no
17//!    `Box`/`Rc` graphs. Persisting a container is `memcpy`; loading it back
18//!    is bounds-checking the metadata and adopting the bytes.
19//! 2. **Costs are local and visible.** Every operation touches one 4 KiB
20//!    page (one cache-friendly unit). Worst cases are small, fixed and
21//!    measured — the optional [`counters`](#feature-flags) feature exposes
22//!    deterministic work counters used as CI performance gates.
23//! 3. **Keys are big-endian.** Byte-wise comparison of an encoded key must
24//!    equal the numeric comparison of its source value, so binary search and
25//!    ordered iteration work directly on raw bytes. Helpers live in [`key`].
26//!
27//! # The four structures
28//!
29//! | Structure | Shape | Typical use |
30//! |---|---|---|
31//! | [`Arena`] | sorted fixed-size records, sharded 4 KiB pages | primary record store, ordered indexes |
32//! | [`BlobHeap`] | append-only variable-length blobs, dense ids | texts, names, raw vectors |
33//! | [`ChunkPool`] | many small growable lists over 64-byte chunks | posting lists, adjacency lists |
34//! | [`Interner`] | string -> dense `u32` (heap + flat hash table) | terms, tags, entity names |
35//!
36//! # Quick start
37//!
38//! ```
39//! use plugmem_arena::{Arena, ArenaCfg, ShardMode, Slot, key};
40//!
41//! /// A tiny fixed-size record: 4-byte big-endian key + 1-byte payload.
42//! #[derive(Debug, PartialEq)]
43//! struct Rec {
44//!     id: u32,
45//!     level: u8,
46//! }
47//!
48//! impl Slot for Rec {
49//!     const SIZE: usize = 5;
50//!     const KEY_LEN: usize = 4;
51//!     fn write(&self, out: &mut [u8]) {
52//!         key::write_u32(out, self.id);
53//!         out[4] = self.level;
54//!     }
55//!     fn read(bytes: &[u8]) -> Self {
56//!         Rec { id: key::read_u32(bytes), level: bytes[4] }
57//!     }
58//! }
59//!
60//! let mut arena = Arena::<Rec>::new(ArenaCfg::new(64, ShardMode::Ordered)).unwrap();
61//! arena.insert(&Rec { id: 7, level: 3 }).unwrap();
62//! arena.insert(&Rec { id: 1, level: 9 }).unwrap();
63//!
64//! let mut key_buf = [0u8; 4];
65//! key::write_u32(&mut key_buf, 7);
66//! assert_eq!(arena.get(&key_buf), Some(Rec { id: 7, level: 3 }));
67//!
68//! // Ordered mode: iteration yields ascending keys across all shards.
69//! let ids: Vec<u32> = arena.iter().map(|r| r.id).collect();
70//! assert_eq!(ids, [1, 7]);
71//! ```
72//!
73//! # The one `unsafe`
74//!
75//! The single default `unsafe` in this crate is page allocation without
76//! zeroing (`Vec::reserve` + `set_len`). It is kept because it was
77//! *measured*, not assumed: on the wasm target (our primary portability
78//! target) zeroing freshly grown pages made the allocation path **12x
79//! slower** (wasmtime, 32k pages: 3889 us zeroed vs 316 us uninit), while on
80//! native x86-64 the difference is noise. The safety invariant is simple and
81//! local: *bytes of a page beyond `count * Slot::SIZE` are never read* —
82//! every read is bounded by the per-shard element count, and a slot is fully
83//! written before `count` is incremented. See `Arena::ensure_page` for the
84//! full safety comment. A consequence worth knowing: `Arena` intentionally
85//! implements neither `Clone` nor `PartialEq`, because a byte-wise clone or
86//! comparison would read those uninitialized tails.
87//!
88//! Bounds-check elimination (`get_unchecked`) was measured on the same
89//! harness and rejected: <= 1% on native, *slower* under wasm (the runtime
90//! bounds-checks linear memory anyway). Safe indexing everywhere else.
91//!
92//! # Feature flags
93//!
94//! - `std` *(default)* — nothing yet beyond linking `std` for consumers'
95//!   convenience; the crate is fully functional as `no_std + alloc`.
96//! - `counters` — deterministic work counters (`Counters`) on every
97//!   container: key comparisons, bytes shifted, pages allocated. Zero cost
98//!   when disabled (the increments compile away).
99#![no_std]
100
101extern crate alloc;
102
103pub mod key;
104
105mod arena;
106mod blob;
107mod chunk;
108mod error;
109mod interner;
110mod paged;
111mod slot;
112
113pub use arena::{Arena, ArenaCfg, Iter, PAGE_BYTES, ShardMode};
114pub use blob::{BlobHeap, BlobHeapBuilder, BlobHeapCfg, BlobId};
115pub use chunk::{CHUNK_BYTES, CHUNK_PAYLOAD, ChunkIter, ChunkPool, ChunkPoolCfg, ListHandle};
116pub use error::Error;
117pub use interner::{Interner, TermId};
118pub use slot::Slot;
119
120#[cfg(feature = "counters")]
121pub use arena::Counters;