Skip to main content

thin_status/
thin_arc_or_int.rs

1// Copyright 2026 <https://github.com/ppetr/>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14use std::cmp::Ordering;
15use std::ffi::c_void;
16use std::fmt;
17use std::hash::{Hash, Hasher};
18use std::marker::PhantomData;
19use std::ptr::NonNull;
20use triomphe::ThinArc;
21
22/// Stores an `isize` as a tagged value inside a pointer. This means that one bit of `isize` isn't
23/// available, and therefore only numbers within `IsizeInPtr::MIN` and `IsizeIntPtr::MAX` are
24/// convertible.
25///
26/// To wrap `isize` (or any other numerical `i..` type) into `IsizeInPtr` (if it fits), use
27/// `try_from` from `impl TryFrom<isize> for IsizeInPtr`.
28#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Debug)]
29pub struct IsizeInPtr {
30    /// As all numerical values are tagged by `TAG_MASK`, this is always non-zero.
31    ptr: NonNull<c_void>,
32}
33
34impl IsizeInPtr {
35    /// Maximal value that can be stored.
36    pub const MAX: isize = isize::MAX >> 1;
37    pub const MIN: isize = isize::MIN >> 1;
38    const TAG_MASK: isize = 1;
39
40    fn from_ptr(ptr: *const c_void) -> Option<Self> {
41        if (ptr as isize & Self::TAG_MASK) == 0 {
42            None
43        } else {
44            Some(unsafe { Self::new_unchecked(ptr) })
45        }
46    }
47
48    fn from_isize_unchecked(value: isize) -> Self {
49        let tagged = (value << 1) | Self::TAG_MASK;
50        unsafe { Self::new_unchecked(tagged as *const c_void) }
51    }
52
53    unsafe fn new_unchecked(ptr: *const c_void) -> Self {
54        IsizeInPtr {
55            ptr: unsafe { NonNull::from_ref(&*ptr) },
56        }
57    }
58
59    /// Returns the wrapped value.
60    pub fn get(&self) -> isize {
61        (self.ptr.as_ptr() as isize) >> 1
62    }
63}
64
65impl Default for IsizeInPtr {
66    fn default() -> Self {
67        Self::from_isize_unchecked(0)
68    }
69}
70
71impl From<IsizeInPtr> for isize {
72    /// Convert `value` back to `isize`.
73    fn from(value: IsizeInPtr) -> isize {
74        value.get()
75    }
76}
77
78impl TryFrom<isize> for IsizeInPtr {
79    type Error = TryFromIsizeError<isize>;
80
81    /// Wrap a `value` if it fits inside `IsizeInPtr`.
82    fn try_from(value: isize) -> Result<IsizeInPtr, Self::Error> {
83        if (value <= Self::MAX) && (value >= Self::MIN) {
84            Ok(Self::from_isize_unchecked(value))
85        } else {
86            Err(TryFromIsizeError { original: value })
87        }
88    }
89}
90
91macro_rules! impl_try_from_for_integral {
92    ($($t:ty),*) => {
93        $(
94            impl TryFrom<$t> for IsizeInPtr {
95                type Error = TryFromIsizeError<$t>;
96
97                fn try_from(value: $t) -> Result<Self, Self::Error> {
98                    isize::try_from(value).ok()
99                        .and_then(|n: isize| IsizeInPtr::try_from(n).ok())
100                        .ok_or(TryFromIsizeError{ original: value })
101                }
102            }
103        )*
104    };
105}
106impl_try_from_for_integral!(i8, i16, i32, i64, i128);
107
108/// Returned when a value is out of the range of `IsizeError`.
109#[derive(Debug, Clone, Copy)]
110pub struct TryFromIsizeError<N> {
111    pub original: N,
112}
113
114impl<N: std::fmt::Display> std::fmt::Display for TryFromIsizeError<N> {
115    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
116        write!(
117            f,
118            "Value {} doesn't fit inside taggeed isize bounds [{}, {}]",
119            self.original,
120            IsizeInPtr::MIN,
121            IsizeInPtr::MAX
122        )
123    }
124}
125
126impl<N: std::fmt::Display + std::fmt::Debug> std::error::Error for TryFromIsizeError<N> {}
127
128/// A type representing either a signed pointer-sized integer (`isize`) or
129/// a reference-counted pointer (`ThinArc<H, T>`).
130///
131/// Optimized using `NonNull` so that `Option<ThinArcOrInt<H, T>>` takes up exactly
132/// the size of a single architecture pointer.
133///
134/// This is a **general purpose class**, independent from the rest of this crate and can be used for
135/// purposes unrelated to error handling.
136pub struct ThinArcOrInt<H, T> {
137    /// As all numerical values are tagged by `TAG_MASK`, this is always non-zero.
138    raw: NonNull<c_void>,
139    _marker: PhantomData<ThinArc<H, T>>,
140}
141
142unsafe impl<H, T> Send for ThinArcOrInt<H, T> where ThinArc<H, T>: Send {}
143unsafe impl<H, T> Sync for ThinArcOrInt<H, T> where ThinArc<H, T>: Sync {}
144
145impl<H, T> ThinArcOrInt<H, T> {
146    /// Constructs an instance from a signed integer that will be stored as a tagged value inside
147    /// this pointer.
148    pub fn from_isize(val: IsizeInPtr) -> Self {
149        Self {
150            raw: val.ptr,
151            _marker: PhantomData,
152        }
153    }
154
155    /// Constructs an instance from a ThinArc pointer.
156    /// (Assumes the pointer is aligned and its LSB is 0.)
157    pub fn from_arc(arc: ThinArc<H, T>) -> Self {
158        let ptr = ThinArc::into_raw(arc);
159        debug_assert!(
160            IsizeInPtr::from_ptr(ptr).is_none(),
161            "Pointer must be 2-aligned!"
162        );
163        // Safety: ThinArc allocations on the heap are never null.
164        Self {
165            raw: unsafe { NonNull::from_ref(&*ptr) },
166            _marker: PhantomData,
167        }
168    }
169
170    /// If `slice` is empty, tries to convert a `value` using `try_into()` to `IsizeInPtr`. If it
171    /// succeeds, stores it as a tagged integer inside this pointer. Otherwise constructs a
172    /// `ThinArc` to hold everything.
173    pub fn from_convertible<U, E>(value: U, slice: &[T]) -> Self
174    where
175        U: TryInto<IsizeInPtr, Error = E> + Into<H>,
176        E: Into<H>,
177        T: Copy,
178    {
179        if slice.is_empty() {
180            match value.try_into() {
181                Ok(i) => Self::from_isize(i),
182                Err(e) => {
183                    Self::from_arc(ThinArc::from_header_and_iter(e.into(), std::iter::empty()))
184                }
185            }
186        } else {
187            Self::from_arc(ThinArc::from_header_and_slice(value.into(), slice))
188        }
189    }
190
191    /// Returns `true` iff this instance holds a number as a tagged value inside this pointer, that
192    /// is, without any memory allocation.
193    pub fn has_number(&self) -> bool {
194        self.as_isize().is_some()
195    }
196
197    /// Returns `true` iff this instance holds real pointer to a `ThinArc<H, T>` value.
198    pub fn has_ref(&self) -> bool {
199        !self.has_number()
200    }
201
202    /// Returns the tagged integer value inside this pointer if present, or `None` otherwise.
203    pub fn as_isize(&self) -> Option<isize> {
204        IsizeInPtr::from_ptr(self.raw.as_ptr()).map(|i| i.into())
205    }
206
207    /// Returns a shared reference to a `ThinArc<H, T>` if present, or `None` otherwise.
208    pub fn as_arc(&self) -> Option<&ThinArc<H, T>> {
209        if self.has_ref() {
210            unsafe { Some(self.as_arc_internal()) }
211        } else {
212            None
213        }
214    }
215
216    unsafe fn as_arc_internal(&self) -> &ThinArc<H, T> {
217        &*(&self.raw as *const NonNull<c_void> as *const ThinArc<H, T>)
218    }
219}
220
221impl<H, T> Default for ThinArcOrInt<H, T> {
222    fn default() -> Self {
223        Self::from_isize(Default::default())
224    }
225}
226
227impl<H, T> Drop for ThinArcOrInt<H, T> {
228    fn drop(&mut self) {
229        if self.has_ref() {
230            let _arc = unsafe { ThinArc::<H, T>::from_raw(self.raw.as_ptr()) };
231        }
232    }
233}
234
235impl<H, T> Clone for ThinArcOrInt<H, T> {
236    fn clone(&self) -> Self {
237        if self.has_number() {
238            Self {
239                raw: self.raw,
240                _marker: PhantomData,
241            }
242        } else {
243            let arc = unsafe { self.as_arc_internal() };
244            let cloned_arc = arc.clone();
245            Self::from_arc(cloned_arc)
246        }
247    }
248}
249
250impl<H, T> PartialEq for ThinArcOrInt<H, T>
251where
252    H: PartialEq,
253    T: PartialEq,
254{
255    fn eq(&self, other: &Self) -> bool {
256        match (self.as_isize(), other.as_isize()) {
257            (Some(s), Some(o)) => s == o,
258            (None, None) => unsafe { self.as_arc_internal() == other.as_arc_internal() },
259            _ => false,
260        }
261    }
262}
263
264impl<H, T> Eq for ThinArcOrInt<H, T>
265where
266    H: Eq,
267    T: Eq,
268{
269}
270
271impl<H, T> PartialOrd for ThinArcOrInt<H, T>
272where
273    H: PartialOrd,
274    T: PartialOrd,
275{
276    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
277        match (self.as_isize(), other.as_isize()) {
278            (Some(s), Some(o)) => s.partial_cmp(&o),
279            (None, None) => unsafe { self.as_arc_internal().partial_cmp(other.as_arc_internal()) },
280            (Some(_), None) => Some(Ordering::Less),
281            (None, Some(_)) => Some(Ordering::Greater),
282        }
283    }
284}
285
286impl<H, T> Ord for ThinArcOrInt<H, T>
287where
288    H: Ord,
289    T: Ord,
290{
291    fn cmp(&self, other: &Self) -> Ordering {
292        self.partial_cmp(other)
293            .expect("ThinArc::partial_cmp returned `None`")
294    }
295}
296
297impl<H, T> Hash for ThinArcOrInt<H, T>
298where
299    H: Hash,
300    T: Hash,
301{
302    fn hash<S: Hasher>(&self, state: &mut S) {
303        if let Some(num) = self.as_isize() {
304            0.hash(state);
305            num.hash(state);
306        } else {
307            1.hash(state);
308            unsafe { self.as_arc_internal().hash(state) };
309        }
310    }
311}
312
313impl<H: fmt::Debug, T: fmt::Debug> fmt::Debug for ThinArcOrInt<H, T> {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        if let Some(num) = self.as_isize() {
316            f.debug_tuple("ThinArcOrInt::Number").field(&num).finish()
317        } else {
318            f.debug_tuple("ThinArcOrInt::ThinArc")
319                .field(self.as_arc().unwrap())
320                .finish()
321        }
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    fn from_isize(value: isize) -> ThinArcOrInt<(), ()> {
330        ThinArcOrInt::<(), ()>::from_isize(
331            IsizeInPtr::try_from(value).expect("Out of allowed bounds"),
332        )
333    }
334
335    #[test]
336    fn test_size() {
337        assert_eq!(
338            std::mem::size_of::<ThinArcOrInt<(), String>>(),
339            std::mem::size_of::<usize>()
340        );
341    }
342
343    #[test]
344    fn test_clone_and_drop() {
345        let arc = ThinArc::from_header_and_slice((), "Shared data".as_bytes());
346        let val1 = ThinArcOrInt::from_arc(arc);
347
348        let val2 = val1.clone();
349
350        assert_eq!(&val1.as_arc().unwrap().slice, "Shared data".as_bytes());
351        assert!(std::ptr::eq(
352            &val1.as_arc().unwrap().slice,
353            &val2.as_arc().unwrap().slice
354        ));
355    }
356
357    #[test]
358    fn test_option_size_optimization() {
359        assert_eq!(
360            std::mem::size_of::<ThinArcOrInt<(), String>>(),
361            std::mem::size_of::<usize>()
362        );
363        assert_eq!(
364            std::mem::size_of::<Option<ThinArcOrInt<(), String>>>(),
365            std::mem::size_of::<usize>()
366        );
367        assert_ne!(None, Some(from_isize(0)));
368    }
369
370    #[test]
371    fn test_negative_numbers() {
372        let negative = from_isize(-42);
373        assert!(negative.has_number());
374        assert_eq!(negative.as_isize(), Some(-42));
375    }
376
377    #[test]
378    fn test_max_number() {
379        let negative = from_isize(IsizeInPtr::MAX);
380        assert!(negative.has_number());
381        assert_eq!(negative.as_isize(), Some(IsizeInPtr::MAX));
382    }
383
384    #[test]
385    fn test_min_number() {
386        let negative = from_isize(IsizeInPtr::MIN);
387        assert!(negative.has_number());
388        assert_eq!(negative.as_isize(), Some(IsizeInPtr::MIN));
389    }
390
391    #[test]
392    fn test_thin_arc() {
393        let arc = ThinArc::from_header_and_slice((), b"Hello Rust");
394        let val = ThinArcOrInt::from_arc(arc);
395
396        assert!(val.has_ref());
397        assert!(!val.has_number());
398        assert_eq!(
399            &val.as_arc().expect("Must be Arc, not isize").slice,
400            b"Hello Rust"
401        );
402    }
403}