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}