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