Skip to main content

subetha_pointers/
umbra_pointer.rs

1//! `UmbraPointer<T>` - generic content-prefixed pointer.
2//!
3//! 16-byte slot. Actual `#[repr(C, align(16))]` layout is:
4//! `target: *const T` at offset 0..8, `prefix: u32` at offset 8..12,
5//! `_pad: u32` at offset 12..16. The prefix is 4 bytes derived from
6//! the target's content (either the first 4 bytes of an underlying
7//! byte-representation, or a 4-byte hash).
8//!
9//! The architectural win: equality / lookup operations check the
10//! 4-byte prefix in-register BEFORE dereferencing `target`. For
11//! workloads where most comparisons fail (HashMap bucket-chain
12//! walks, dedup scans, RDF subject lookups), the prefix short-
13//! circuits the dereference, eliminating the cache miss on the
14//! pointed-to object.
15//!
16//! This is the generic primitive that callers specialise per content
17//! type: a string-content overlay (prefix = first 4 bytes of the
18//! UTF-8 bytes) and a bit-sliced N-pointer tile overlay both fit
19//! inside the same 16-byte slot by reinterpreting `prefix` as
20//! content-specific bits.
21//!
22//! Two prefix construction modes:
23//!
24//! - [`UmbraPointer::with_content_prefix`] copies 4 bytes from the
25//!   target's byte-representation (caller supplies bytes).
26//! - [`UmbraPointer::with_hash_prefix`] takes a 4-byte hash of the
27//!   target's identity. Near-perfect rejection rate (~2^-32
28//!   collision) at the cost of computing the hash on construction.
29
30use std::marker::PhantomData;
31use std::sync::Arc;
32
33/// 16-byte content-prefixed pointer. Layout is fixed so SIMD scans
34/// over an array of `UmbraPointer<T>` see consistent prefix-byte
35/// positions.
36#[repr(C, align(16))]
37pub struct UmbraPointer<T> {
38    /// Pointer to the heap-allocated target. Placed first so its
39    /// natural 8-byte alignment does not push the layout off the
40    /// 16-byte boundary. Requires `T: Sized` so the pointer stays
41    /// thin (8 bytes); for unsized targets, wrap in `Box<[u8]>` or
42    /// equivalent at the application layer.
43    target: *const T,
44    /// 4-byte content prefix, at offset 8.
45    prefix: u32,
46    /// Padding to fill out the 16-byte slot.
47    _pad: u32,
48    _phantom: PhantomData<T>,
49}
50
51unsafe impl<T: Send> Send for UmbraPointer<T> {}
52unsafe impl<T: Sync> Sync for UmbraPointer<T> {}
53
54impl<T> UmbraPointer<T> {
55    /// Direction signature of `UmbraPointer<T>`. Engages the
56    /// `K_content_prefix` axis (4-byte prefix stored at slot for
57    /// short-circuit equality before deref).
58    pub const SIGNATURE: subetha_core::AxisMask = subetha_core::AxisMask::from_axes(
59        &[subetha_core::Axis::ContentPrefix],
60    );
61
62    /// Construct from an existing raw pointer with an explicit prefix.
63    /// The caller is responsible for keeping the pointee alive.
64    ///
65    /// # Safety
66    ///
67    /// `target` must remain valid for the lifetime of this
68    /// `UmbraPointer`. `prefix` should be a deterministic function of
69    /// the pointee's content; otherwise prefix comparisons are
70    /// meaningless.
71    #[inline]
72    pub const unsafe fn from_raw(prefix: u32, target: *const T) -> Self {
73        Self { target, prefix, _pad: 0, _phantom: PhantomData }
74    }
75
76    #[inline]
77    pub const fn prefix(&self) -> u32 { self.prefix }
78
79    #[inline]
80    pub const fn as_raw(&self) -> *const T { self.target }
81
82    /// Compare prefixes only. Single in-register equality check; no
83    /// dereference of `target`. Returns true when prefixes are equal,
84    /// false when they differ. Use this as the first step in a staged
85    /// equality check (Umbra paper's prefix short-circuit).
86    #[inline]
87    pub const fn prefix_eq(&self, other: &Self) -> bool {
88        self.prefix == other.prefix
89    }
90
91    /// Compare against a literal query prefix.
92    #[inline]
93    pub const fn matches_prefix(&self, query: u32) -> bool {
94        self.prefix == query
95    }
96}
97
98impl<T> UmbraPointer<T> {
99    /// Build by moving `value` onto the heap and copying its first
100    /// 4 bytes (in declaration order) as the prefix.
101    ///
102    /// Useful for types whose first 4 bytes are a meaningful key
103    /// field (database row IDs, packet headers, etc.). For more
104    /// general use, prefer [`UmbraPointer::with_hash_prefix`].
105    pub fn with_content_prefix(value: T) -> Box<UmbraOwner<T>> {
106        // SAFETY: we read 4 bytes from `&value` without moving it,
107        // then move value into a Box. Reading the raw bytes does not
108        // require T: Copy; we only need the bytes for prefix
109        // derivation. Endianness is platform-native.
110        let bytes = unsafe {
111            let p = &value as *const T as *const u8;
112            let n = std::mem::size_of::<T>().min(4);
113            let mut buf = [0u8; 4];
114            std::ptr::copy_nonoverlapping(p, buf.as_mut_ptr(), n);
115            buf
116        };
117        let prefix = u32::from_le_bytes(bytes);
118        let boxed = Box::new(value);
119        let target = Box::into_raw(boxed) as *const T;
120        let ptr = unsafe { Self::from_raw(prefix, target) };
121        Box::new(UmbraOwner { ptr })
122    }
123
124    /// Build by moving `value` onto the heap and hashing its byte
125    /// representation as the prefix. Near-perfect rejection rate
126    /// because hash distribution is approximately random over u32.
127    pub fn with_hash_prefix(value: T) -> Box<UmbraOwner<T>>
128    where T: std::hash::Hash,
129    {
130        use std::collections::hash_map::DefaultHasher;
131        use std::hash::Hasher;
132        let mut h = DefaultHasher::new();
133        value.hash(&mut h);
134        let full = h.finish();
135        // Take the low 32 bits as the prefix.
136        let prefix = full as u32;
137        let boxed = Box::new(value);
138        let target = Box::into_raw(boxed) as *const T;
139        let ptr = unsafe { Self::from_raw(prefix, target) };
140        Box::new(UmbraOwner { ptr })
141    }
142
143    /// Wrap an `Arc<T>` without taking ownership of the heap
144    /// allocation. Uses hash-based prefix.
145    pub fn from_arc(value: Arc<T>, prefix: u32) -> ArcUmbra<T> {
146        let target = Arc::as_ptr(&value);
147        let ptr = unsafe { Self::from_raw(prefix, target) };
148        ArcUmbra { ptr, _arc: value }
149    }
150
151    /// # Safety
152    ///
153    /// Caller must guarantee the target is still live.
154    #[inline]
155    pub unsafe fn deref_unchecked(&self) -> &T {
156        unsafe { &*self.target }
157    }
158}
159
160/// RAII wrapper for an `UmbraPointer<T>` whose target was heap-allocated
161/// via [`UmbraPointer::with_content_prefix`] or
162/// [`UmbraPointer::with_hash_prefix`]. Drops the boxed target when the
163/// owner is dropped.
164pub struct UmbraOwner<T> {
165    ptr: UmbraPointer<T>,
166}
167
168impl<T> UmbraOwner<T> {
169    #[inline]
170    pub fn ptr(&self) -> &UmbraPointer<T> { &self.ptr }
171    #[inline]
172    pub fn prefix(&self) -> u32 { self.ptr.prefix }
173    #[inline]
174    pub fn value(&self) -> &T {
175        // SAFETY: we own the heap allocation, so deref is always valid.
176        unsafe { &*self.ptr.target }
177    }
178}
179
180impl<T> Drop for UmbraOwner<T> {
181    fn drop(&mut self) {
182        let raw = self.ptr.target as *mut T;
183        if !raw.is_null() {
184            // SAFETY: target was created via Box::into_raw in
185            // with_*_prefix.
186            unsafe { drop(Box::from_raw(raw)); }
187        }
188    }
189}
190
191/// `UmbraPointer<T>` wrapping an `Arc<T>`. The Arc reference count
192/// keeps the target alive; the UmbraPointer is a copy of the Arc's
193/// data pointer plus the prefix.
194pub struct ArcUmbra<T> {
195    ptr: UmbraPointer<T>,
196    _arc: Arc<T>,
197}
198
199impl<T> ArcUmbra<T> {
200    #[inline]
201    pub fn ptr(&self) -> &UmbraPointer<T> { &self.ptr }
202    #[inline]
203    pub fn prefix(&self) -> u32 { self.ptr.prefix }
204    #[inline]
205    pub fn value(&self) -> &T {
206        // SAFETY: the Arc keeps the target alive.
207        unsafe { &*self.ptr.target }
208    }
209    #[inline]
210    pub fn into_arc(self) -> Arc<T> { self._arc.clone() }
211}
212
213impl<T> Clone for ArcUmbra<T> {
214    fn clone(&self) -> Self {
215        Self {
216            ptr: unsafe { UmbraPointer::from_raw(self.ptr.prefix, self.ptr.target) },
217            _arc: self._arc.clone(),
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn layout_is_exactly_16_bytes() {
228        assert_eq!(std::mem::size_of::<UmbraPointer<u64>>(), 16);
229        assert_eq!(std::mem::align_of::<UmbraPointer<u64>>(), 16);
230    }
231
232    #[test]
233    fn prefix_eq_does_not_deref() {
234        // Construct two UmbraPointers with the same prefix but
235        // bogus (null-adjacent) targets. The compare must use the
236        // prefix only and not crash.
237        let p1: UmbraPointer<u64> = unsafe {
238            UmbraPointer::from_raw(0xDEADBEEF, std::ptr::dangling::<u64>())
239        };
240        let p2: UmbraPointer<u64> = unsafe {
241            UmbraPointer::from_raw(0xDEADBEEF, std::ptr::dangling::<u64>())
242        };
243        let p3: UmbraPointer<u64> = unsafe {
244            UmbraPointer::from_raw(0xCAFEBABE, std::ptr::dangling::<u64>())
245        };
246        assert!(p1.prefix_eq(&p2), "same prefix matches without deref");
247        assert!(!p1.prefix_eq(&p3), "different prefix does not match");
248        assert!(p1.matches_prefix(0xDEADBEEF));
249        assert!(!p1.matches_prefix(0));
250    }
251
252    #[test]
253    fn with_content_prefix_copies_first_4_bytes() {
254        // For u64 0x0000_0000_0000_BEEF on little-endian, the first
255        // 4 bytes are 0xEF, 0xBE, 0x00, 0x00 -> u32 = 0x0000_BEEF.
256        let owner = UmbraPointer::with_content_prefix(0x0000_0000_0000_BEEF_u64);
257        assert_eq!(owner.prefix(), 0x0000_BEEF);
258        assert_eq!(*owner.value(), 0x0000_0000_0000_BEEF_u64);
259    }
260
261    #[test]
262    fn with_hash_prefix_is_deterministic_for_same_value() {
263        let a = UmbraPointer::with_hash_prefix(42u64);
264        let b = UmbraPointer::with_hash_prefix(42u64);
265        // Same value -> same hash -> same prefix.
266        assert_eq!(a.prefix(), b.prefix());
267        assert_eq!(*a.value(), 42);
268        assert_eq!(*b.value(), 42);
269    }
270
271    #[test]
272    fn with_hash_prefix_distinguishes_different_values() {
273        let a = UmbraPointer::with_hash_prefix(42u64);
274        let b = UmbraPointer::with_hash_prefix(43u64);
275        // Different values -> different hashes -> different prefixes
276        // (with overwhelming probability).
277        assert_ne!(a.prefix(), b.prefix());
278    }
279
280    #[test]
281    fn arc_umbra_keeps_target_alive() {
282        let arc: Arc<u64> = Arc::new(1234);
283        let u = UmbraPointer::from_arc(arc.clone(), 0xABCD);
284        // Drop the original arc; UmbraPointer's internal arc keeps
285        // the target alive.
286        drop(arc);
287        assert_eq!(u.prefix(), 0xABCD);
288        assert_eq!(*u.value(), 1234);
289    }
290
291    #[test]
292    fn owner_drops_target() {
293        // Use a Drop-counting struct to verify the boxed target is freed.
294        use std::sync::atomic::{AtomicUsize, Ordering};
295        static DROPS: AtomicUsize = AtomicUsize::new(0);
296
297        struct DropCounter(u64);
298        impl Drop for DropCounter {
299            fn drop(&mut self) { DROPS.fetch_add(1, Ordering::Relaxed); }
300        }
301
302        let before = DROPS.load(Ordering::Relaxed);
303        let owner = UmbraPointer::with_content_prefix(DropCounter(99));
304        assert_eq!(owner.value().0, 99);
305        drop(owner);
306        let after = DROPS.load(Ordering::Relaxed);
307        assert!(after > before, "boxed target should be dropped");
308    }
309
310    #[test]
311    fn dedup_scan_via_prefix() {
312        // Realistic workload: scan an array of UmbraOwners, count
313        // distinct prefixes. No dereference required.
314        let owners: Vec<_> = (0..100u32)
315            .map(UmbraPointer::with_content_prefix)
316            .collect();
317        let mut prefixes: Vec<u32> = owners.iter().map(|o| o.prefix()).collect();
318        prefixes.sort_unstable();
319        prefixes.dedup();
320        // 100 distinct u32 values have 100 distinct content-prefix bytes
321        // because the LE byte 0 captures the low byte uniquely for 0..100.
322        assert_eq!(prefixes.len(), 100);
323    }
324
325    #[test]
326    fn skip_on_mismatch_zero_dereferences() {
327        // Build 10 ArcUmbras with distinct prefixes, then scan for a
328        // prefix that doesn't match any. The scan must run without
329        // touching any of the underlying Arc<T> data.
330        let umbras: Vec<ArcUmbra<u64>> = (0..10u64)
331            .map(|i| UmbraPointer::from_arc(Arc::new(i * 1000), (i + 1) as u32))
332            .collect();
333        let query_prefix = 99u32;
334        let mut matches = 0;
335        for u in &umbras {
336            if u.ptr().matches_prefix(query_prefix) {
337                matches += 1;
338                // would deref here only when matched; never reached
339                let _val = u.value();
340            }
341        }
342        assert_eq!(matches, 0,
343                   "no prefix in 1..=10 should equal 99");
344    }
345}