Skip to main content

compact_option/
lib.rs

1#![feature(const_trait_impl)]
2#![feature(generic_const_exprs)]
3#![feature(const_cmp)]
4#![feature(transmutability)]
5#![allow(incomplete_features)]
6
7//! Niche-packing optional: [`CompactOption<R, T>`][CompactOption] uses exactly as much memory as
8//! raw `R` to store either [`CompactOption::NONE`] or a `Some(T)` payload, where `T: Copy` via the
9//! unsafe [`CompactRepr`] contract.
10//!
11//! Intended for raw representations `R` with spare bit patterns. Primary use case:
12//! `#[repr(u8)]` enums with fewer than 256 variants.
13//!
14//! - [`CompactOption`] is the safe-ish wrapper API (transmute-based; see docs and Miri).
15//! - Implement [`CompactRepr`] manually, or enable the **`macros`** feature for
16//!   `#[compact_option(repr(R = …, sentinel = …))]` (see the `compact-option-proc-macro` crate).
17//!
18//! **Toolchain:** this crate pins a nightly toolchain via `rust-toolchain.toml` and relies on
19//! unstable features.
20
21use core::marker::PhantomData;
22use core::mem::{Assume, TransmuteFrom};
23
24const TRANSMUTATION_ASSUMPTION: Assume = Assume {
25    alignment: false,
26    lifetimes: false,
27    safety: true,
28    validity: true,
29};
30
31mod __layout {
32    use core::marker::PhantomData;
33    use core::mem::{align_of, size_of};
34
35    use crate::CompactRepr;
36
37    pub(crate) struct LayoutInvariant<R, T: ?Sized>(PhantomData<(R, T)>);
38
39    impl<R, T> LayoutInvariant<R, T>
40    where
41        T: CompactRepr<R>,
42    {
43        pub(crate) const CHECK: () = {
44            assert!(size_of::<T>() == size_of::<R>());
45            assert!(align_of::<T>() == align_of::<R>());
46        };
47    }
48}
49
50/// # Safety
51/// Implementors must guarantee:
52/// 1. For every `T` value stored via [`CompactOption::some`], the transmuted
53///    `R` bit pattern must not equal [`CompactRepr::UNUSED_SENTINEL`].
54/// 2. Non-sentinel `R` values used as `Some` payloads must be sound to transmute
55///    back to `T` under the same `Assume` bundle used by [`CompactOption`] for
56///    `TransmuteFrom` between `R` and `T`.
57/// 3. If you care about logical round-tripping, transmuting that raw value back
58///    yields an equivalent `T`.
59///
60/// # Choosing `UNUSED_SENTINEL`
61///
62/// Pick an `R` value that is **not** the transmuted bit pattern of any `T` you
63/// will ever store as `Some`. If the sentinel aliases a valid `Some` encoding,
64/// `NONE` and `Some` collide and the type becomes logically unusable.
65///
66/// # Validation
67///
68/// After changing an `unsafe impl CompactRepr`, run `cargo miri test` (or your
69/// project’s Miri CI) to exercise transmute-based paths under the stacked
70/// borrows / provenance model.
71///
72/// ## Procedural macro
73///
74/// Enable the **`macros`** crate feature for a re-exported `#[compact_option(...)]`
75/// attribute, or depend on the **`compact-option-proc-macro`** crate directly.
76///
77/// The `#[compact_option(repr(R = …, sentinel = …))]` macro only emits `unsafe impl CompactRepr`;
78/// it does **not** validate `#[repr]`, discriminants, or sentinel collisions. Structs additionally
79/// get `size_of` / `align_of` checks against `R`. See the proc-macro crate’s rustdoc and Miri for
80/// safety review.
81pub const unsafe trait CompactRepr<R>: Copy + Sized {
82    /// Raw value reserved for [`CompactOption::NONE`].
83    ///
84    /// # Safety (encoding)
85    ///
86    /// This bit pattern must **never** equal the transmuted `R` encoding of any `T` you store via
87    /// [`CompactOption::some`]. If it does, `NONE` and `Some` collide: [`CompactOption::is_none`]
88    /// may return `true` for a value you constructed with `some`, and [`CompactOption::try_unwrap`]
89    /// returns `None`.
90    const UNUSED_SENTINEL: R;
91}
92
93/// When built with the `macros` feature, re-exports the `#[compact_option(...)]` attribute.
94#[cfg(feature = "macros")]
95pub use compact_option_proc_macro::compact_option;
96
97/// Niche-packing optional: stores either [`Self::NONE`] or a `Some(T)` payload in exactly as much
98/// memory as raw `R`. `T` must be [`Copy`] (via the [`CompactRepr`] contract); the wrapper itself
99/// is `Copy` whenever `R` and `T` are.
100///
101/// ## Layout checks
102///
103/// `R` and `T` must have identical size and alignment. The same layout assertions run when
104/// evaluating [`Self::NONE`] in a `const` context and when calling [`Self::some`] (so `some`
105/// cannot silently skip layout validation). A plain `let _ = Self::NONE` in non-const code may
106/// not const-evaluate [`Self::NONE`]; prefer `const { CompactOption::<R, T>::NONE }` or similar
107/// if you need the check guaranteed at compile time.
108///
109/// ```compile_fail
110/// use compact_option::{CompactOption, CompactRepr};
111///
112/// #[derive(Clone, Copy)]
113/// #[repr(C)]
114/// struct Pair(u8, u8);
115///
116/// unsafe impl CompactRepr<u8> for Pair {
117///     const UNUSED_SENTINEL: u8 = 0xFF;
118/// }
119///
120/// const _FORCE_LAYOUT: CompactOption<u8, Pair> = CompactOption::NONE;
121///
122/// fn main() {}
123/// ```
124///
125/// [`CompactRepr`] requires a `Copy` payload:
126///
127/// ```compile_fail
128/// use compact_option::{CompactOption, CompactRepr};
129///
130/// #[derive(Clone)]
131/// struct Opaque(u8);
132///
133/// unsafe impl CompactRepr<u8> for Opaque {
134///     const UNUSED_SENTINEL: u8 = 0xFF;
135/// }
136///
137/// fn main() {
138///     let _ = CompactOption::<u8, Opaque>::NONE;
139/// }
140/// ```
141#[repr(transparent)]
142#[derive(Clone, Eq, PartialEq, Hash, Debug)]
143pub struct CompactOption<R, T: CompactRepr<R>> {
144    raw_value: R,
145    _marker: PhantomData<T>,
146}
147
148impl<R: Copy, T: CompactRepr<R> + Copy> Copy for CompactOption<R, T> {}
149
150impl<R, T> CompactOption<R, T>
151where
152    R: Copy + PartialEq,
153    T: CompactRepr<R>,
154{
155    /// Sentinel-backed empty value: the stored `R` equals [`CompactRepr::UNUSED_SENTINEL`].
156    ///
157    /// Layout of `T` and `R` is checked here (see struct-level **Layout checks**). Using `NONE` in
158    /// a `const` context ensures that check runs; a plain `let _ = Self::NONE` in non-const `main`
159    /// may not const-evaluate it.
160    pub const NONE: Self = {
161        let () = __layout::LayoutInvariant::<R, T>::CHECK;
162        Self {
163            raw_value: T::UNUSED_SENTINEL,
164            _marker: PhantomData,
165        }
166    };
167
168    /// Construct a `Some` by transmuting `T` → `R` using the same `Assume` bundle as
169    /// [`try_unwrap`](Self::try_unwrap) / [`unwrap_unchecked`](Self::unwrap_unchecked).
170    ///
171    /// Layout of `T` and `R` is asserted here (same as [`Self::NONE`]).
172    ///
173    /// # Sentinel collisions
174    ///
175    /// If `value`’s transmuted bit pattern equals [`CompactRepr::UNUSED_SENTINEL`], this value is
176    /// indistinguishable from [`Self::NONE`]: [`is_none`](Self::is_none) may be `true` and
177    /// [`try_unwrap`](Self::try_unwrap) returns `None`. A correct [`CompactRepr`] must rule that
178    /// out for all stored `T`.
179    ///
180    /// Not `const` because `TransmuteFrom::transmute` is not a `const fn` on this toolchain.
181    pub fn some(value: T) -> Self
182    where
183        T: CompactRepr<R>,
184        R: TransmuteFrom<T, { TRANSMUTATION_ASSUMPTION }>,
185    {
186        let () = __layout::LayoutInvariant::<R, T>::CHECK;
187        Self {
188            raw_value: unsafe {
189                <R as TransmuteFrom<T, { TRANSMUTATION_ASSUMPTION }>>::transmute(value)
190            },
191            _marker: PhantomData,
192        }
193    }
194
195    /// Returns `true` when this value encodes [`Self::NONE`] (raw equals [`CompactRepr::UNUSED_SENTINEL`]).
196    pub const fn is_none(self) -> bool
197    where
198        R: [const] PartialEq,
199    {
200        self.raw_value == T::UNUSED_SENTINEL
201    }
202
203    /// Returns `true` when this value encodes `Some` (raw differs from [`CompactRepr::UNUSED_SENTINEL`]).
204    pub const fn is_some(self) -> bool
205    where
206        R: [const] PartialEq,
207    {
208        !self.is_none()
209    }
210
211    /// If this is `Some`, transmute the raw `R` back to `T`. If raw equals [`CompactRepr::UNUSED_SENTINEL`],
212    /// returns `None` (including sentinel-collision cases described on [`Self::some`]).
213    pub fn try_unwrap(self) -> Option<T>
214    where
215        T: TransmuteFrom<R, { TRANSMUTATION_ASSUMPTION }>,
216    {
217        if self.raw_value == T::UNUSED_SENTINEL {
218            None
219        } else {
220            debug_assert!(
221                self.raw_value != T::UNUSED_SENTINEL,
222                "CompactOption::try_unwrap: raw must differ from UNUSED_SENTINEL"
223            );
224            // SAFETY: `CompactRepr` requires non-sentinel `R` values used as
225            // `Some` to transmute to a bit-valid `T`.
226            Some(unsafe { self.unwrap_unchecked() })
227        }
228    }
229
230    /// Like [`Option::unwrap`]: returns the payload or panics if this is [`Self::NONE`].
231    pub fn unwrap(self) -> T
232    where
233        T: TransmuteFrom<R, { TRANSMUTATION_ASSUMPTION }>,
234    {
235        match self.try_unwrap() {
236            Some(t) => t,
237            None => panic!("called `CompactOption::unwrap` on a `NONE` value"),
238        }
239    }
240
241    /// Like [`Option::expect`]: returns the payload or panics with `msg` if this is [`Self::NONE`].
242    pub fn expect(self, msg: &str) -> T
243    where
244        T: TransmuteFrom<R, { TRANSMUTATION_ASSUMPTION }>,
245    {
246        match self.try_unwrap() {
247            Some(t) => t,
248            None => panic!("{msg}"),
249        }
250    }
251
252    /// If `Some`, applies `f` to the payload; if [`Self::NONE`], returns `None` without calling `f`.
253    pub fn map<U, F>(self, f: F) -> Option<U>
254    where
255        F: FnOnce(T) -> U,
256        T: TransmuteFrom<R, { TRANSMUTATION_ASSUMPTION }>,
257    {
258        self.try_unwrap().map(f)
259    }
260
261    /// If `Some`, runs `f` on the payload; if [`Self::NONE`], returns `None` without calling `f`.
262    pub fn and_then<U, F>(self, f: F) -> Option<U>
263    where
264        F: FnOnce(T) -> Option<U>,
265        T: TransmuteFrom<R, { TRANSMUTATION_ASSUMPTION }>,
266    {
267        self.try_unwrap().and_then(f)
268    }
269
270    /// # Safety
271    /// `self` must not be `NONE`, and `self.raw_value` must satisfy the
272    /// `CompactRepr` encoding invariant for `T`.
273    pub unsafe fn unwrap_unchecked(self) -> T
274    where
275        T: TransmuteFrom<R, { TRANSMUTATION_ASSUMPTION }>,
276    {
277        debug_assert!(
278            self.raw_value != T::UNUSED_SENTINEL,
279            "CompactOption::unwrap_unchecked: self must not be NONE (raw != UNUSED_SENTINEL)"
280        );
281        unsafe { <T as TransmuteFrom<R, { TRANSMUTATION_ASSUMPTION }>>::transmute(self.raw_value) }
282    }
283}
284
285#[cfg(test)]
286mod fixtures {
287    use crate::{CompactOption, CompactRepr};
288
289    /// `repr(u8)` payload backed by `u8` storage; sentinel `0xFF`.
290    #[repr(u8)]
291    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
292    pub(crate) enum SmallEnum {
293        Var1 = 0,
294        Var2 = 1,
295    }
296
297    unsafe impl const CompactRepr<u8> for SmallEnum {
298        const UNUSED_SENTINEL: u8 = 0xFF;
299    }
300
301    pub(crate) type OptSmall = CompactOption<u8, SmallEnum>;
302
303    /// `repr(transparent)` single-byte struct (same pattern as a newtype over `u8`).
304    #[repr(transparent)]
305    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
306    pub(crate) struct ByteSlot(pub u8);
307
308    unsafe impl const CompactRepr<u8> for ByteSlot {
309        const UNUSED_SENTINEL: u8 = 0xFE;
310    }
311
312    pub(crate) type OptByte = CompactOption<u8, ByteSlot>;
313
314    /// Non-scalar raw `R`: transparent `u32` handle, payload is another `u32` newtype.
315    #[repr(transparent)]
316    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
317    pub(crate) struct Handle(pub u32);
318
319    #[repr(transparent)]
320    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
321    pub(crate) struct Id(pub u32);
322
323    unsafe impl const CompactRepr<Handle> for Id {
324        const UNUSED_SENTINEL: Handle = Handle(u32::MAX);
325    }
326
327    pub(crate) type OptId = CompactOption<Handle, Id>;
328
329    /// Sentinel equals a valid discriminant: `NONE` collides with `some(A)`.
330    #[repr(u8)]
331    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
332    pub(crate) enum BadSentinel {
333        A = 0,
334    }
335
336    unsafe impl const CompactRepr<u8> for BadSentinel {
337        const UNUSED_SENTINEL: u8 = 0;
338    }
339
340    pub(crate) type OptBad = CompactOption<u8, BadSentinel>;
341
342    pub(crate) const NONE_IS_NONE: bool = OptSmall::NONE.is_none();
343    pub(crate) const NONE_NOT_SOME: bool = !OptSmall::NONE.is_some();
344}
345
346#[cfg(test)]
347mod proptests;
348
349#[cfg(test)]
350use core::hash::{Hash, Hasher};
351
352#[cfg(test)]
353use fixtures::{BadSentinel, ByteSlot, Id, OptBad, OptByte, OptId, OptSmall, SmallEnum};
354
355#[cfg(test)]
356#[test]
357fn const_predicates_on_none() {
358    const { assert!(fixtures::NONE_IS_NONE) };
359    const { assert!(fixtures::NONE_NOT_SOME) };
360    assert!(OptSmall::some(SmallEnum::Var1).is_some());
361    assert!(!OptSmall::some(SmallEnum::Var1).is_none());
362    assert!(OptSmall::some(SmallEnum::Var2).is_some());
363    assert!(!OptSmall::some(SmallEnum::Var2).is_none());
364}
365
366#[cfg(test)]
367#[test]
368fn repr_u8_enum_roundtrip_and_combinators() {
369    let foo = OptSmall::some(SmallEnum::Var1);
370    assert_eq!(foo.raw_value, SmallEnum::Var1 as u8);
371    assert_eq!(foo.try_unwrap(), Some(SmallEnum::Var1));
372    assert_eq!(OptSmall::some(SmallEnum::Var1).unwrap(), SmallEnum::Var1);
373
374    let bar = OptSmall::some(SmallEnum::Var2);
375    assert_eq!(bar.map(|x| x as u8), Some(1u8));
376    assert_eq!(bar.and_then(Some), Some(SmallEnum::Var2));
377    assert_eq!(bar.and_then(|_| None::<()>), None);
378
379    assert_eq!(OptSmall::NONE.try_unwrap(), None);
380    assert_eq!(
381        OptSmall::some(SmallEnum::Var1).expect("some"),
382        SmallEnum::Var1
383    );
384
385    unsafe {
386        assert_eq!(
387            OptSmall::some(SmallEnum::Var2).unwrap_unchecked(),
388            SmallEnum::Var2
389        );
390    }
391}
392
393#[cfg(test)]
394#[test]
395fn map_and_then_skip_closure_on_none() {
396    assert_eq!(OptSmall::NONE.map::<(), _>(|_| panic!("map on NONE")), None);
397    assert_eq!(
398        OptSmall::NONE.and_then::<(), _>(|_| panic!("and_then on NONE")),
399        None
400    );
401}
402
403#[cfg(test)]
404#[test]
405fn transparent_struct_payload_roundtrip() {
406    let b = ByteSlot(7);
407    let o = OptByte::some(b);
408    assert_eq!(o.try_unwrap(), Some(ByteSlot(7)));
409    assert_eq!(o.unwrap(), ByteSlot(7));
410}
411
412#[cfg(test)]
413#[test]
414fn non_integer_handle_roundtrip() {
415    let id = Id(42);
416    let o = OptId::some(id);
417    assert_eq!(o.try_unwrap(), Some(Id(42)));
418    assert_eq!(o.unwrap().0, 42);
419}
420
421#[cfg(test)]
422#[test]
423fn sentinel_collision_some_equals_none() {
424    let none = OptBad::NONE;
425    let some_a = OptBad::some(BadSentinel::A);
426    assert_eq!(none.raw_value, some_a.raw_value);
427    assert_eq!(none, some_a);
428    assert!(none.is_none());
429    assert!(!some_a.is_some());
430    assert_eq!(some_a.try_unwrap(), None);
431}
432
433#[cfg(test)]
434#[test]
435fn derives_clone_partial_eq_hash_debug() {
436    assert_eq!(OptSmall::NONE, OptSmall::NONE);
437    assert_eq!(
438        OptSmall::some(SmallEnum::Var1),
439        OptSmall::some(SmallEnum::Var1)
440    );
441    assert_ne!(
442        OptSmall::some(SmallEnum::Var1),
443        OptSmall::some(SmallEnum::Var2)
444    );
445
446    let a = OptSmall::some(SmallEnum::Var1);
447    let b = a;
448    assert_eq!(a, b);
449    assert_eq!(a.clone(), b);
450    assert_eq!(OptSmall::NONE.clone(), OptSmall::NONE);
451    assert_ne!(a, OptSmall::NONE);
452    let mut h1 = std::collections::hash_map::DefaultHasher::new();
453    let mut h2 = std::collections::hash_map::DefaultHasher::new();
454    a.hash(&mut h1);
455    b.hash(&mut h2);
456    assert_eq!(h1.finish(), h2.finish());
457    let s = format!("{a:?}");
458    assert!(s.contains("CompactOption"));
459}
460
461#[cfg(test)]
462#[test]
463#[should_panic(expected = "called `CompactOption::unwrap` on a `NONE` value")]
464fn none_unwrap_panics() {
465    let _ = OptSmall::NONE.unwrap();
466}
467
468#[cfg(test)]
469#[test]
470#[should_panic(expected = "empty")]
471fn none_expect_panics() {
472    let _ = OptSmall::NONE.expect("empty");
473}
474
475/// `unwrap_unchecked` on `NONE` is UB for `SmallEnum` + `0xFF` sentinel; run
476/// `cargo miri test -- --ignored` to let Miri flag it.
477#[cfg(test)]
478#[test]
479#[ignore = "undefined behavior; run under Miri with --ignored"]
480fn miri_ub_unwrap_unchecked_on_none() {
481    unsafe {
482        let _ = OptSmall::NONE.unwrap_unchecked();
483    }
484}