Skip to main content

bounded_integer/
macro.rs

1/// Generate a bounded integer type.
2///
3/// It takes in single struct or enum, with the content being a bounded range expression, whose
4/// upper bound can be inclusive (`x, y`) or exclusive (`x, y`). The attributes and visibility
5/// (e.g. `pub`) of the type are forwarded directly to the output type.
6///
7/// If the type is a struct and the bounded integer's range does not include zero,
8/// the struct will have a niche at zero,
9/// allowing for `Option<BoundedInteger>` to be the same size as `BoundedInteger` itself.
10///
11/// See the [`examples`](crate::examples) module for examples of what this macro generates.
12///
13/// # Examples
14///
15/// With a struct:
16/// ```
17#[cfg_attr(feature = "step_trait", doc = "# #![feature(step_trait)]")]
18/// # mod force_item_scope {
19/// # use bounded_integer::bounded_integer;
20/// bounded_integer! {
21///     pub struct S(2, 5);
22/// }
23/// # }
24/// ```
25/// The generated item should look like this (u8 is chosen as it is the smallest repr):
26/// ```
27/// #[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
28/// #[repr(transparent)]
29/// pub struct S(u8);
30/// ```
31/// And the methods will ensure that `2 ≤ S.0 ≤ 5`.
32///
33/// With an enum:
34/// ```
35#[cfg_attr(feature = "step_trait", doc = "# #![feature(step_trait)]")]
36/// # mod force_item_scope {
37/// # use bounded_integer::bounded_integer;
38/// bounded_integer! {
39///     pub enum S(-1, 1);
40/// }
41/// # }
42/// ```
43/// The generated item should look like this (i8 is chosen as it is the smallest repr):
44/// ```
45/// #[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
46/// #[repr(i8)]
47/// pub enum S {
48///     N1 = -1, Z, P1
49/// }
50/// ```
51///
52/// You can also ascribe dedicated names to the enum variants:
53/// ```
54#[cfg_attr(feature = "step_trait", doc = "# #![feature(step_trait)]")]
55/// # use bounded_integer::bounded_integer;
56/// bounded_integer! {
57///     pub enum Sign {
58///         Negative = -1,
59///         Zero,
60///         Positive,
61///     }
62/// }
63/// assert_eq!(Sign::Negative.get(), -1);
64/// assert_eq!(Sign::new(1).unwrap(), Sign::Positive);
65/// ```
66///
67/// # Custom repr
68///
69/// The item can have a `repr` attribute to specify how it will be represented in memory, which can
70/// be a `u*` or `i*` type. In this example we override the `repr` to be a `u16`, when it would
71/// have normally been a `u8`.
72///
73/// ```
74#[cfg_attr(feature = "step_trait", doc = "# #![feature(step_trait)]")]
75/// # mod force_item_scope {
76/// # use bounded_integer::bounded_integer;
77/// bounded_integer! {
78///     #[repr(u16)]
79///     pub struct S(2, 4);
80/// }
81/// # }
82/// ```
83/// The generated item should look like this:
84/// ```
85/// #[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
86/// #[repr(transparent)]
87/// pub struct S(u16);
88/// ```
89#[macro_export]
90macro_rules! bounded_integer {
91    (
92        $(#![$($outer_attr:tt)*])*
93        $(#[$($attr:tt)*])*
94        $(pub $(($($vis:tt)*))?)? struct $name:ident($min:expr, $max:expr);
95    ) => {
96        $crate::__helper! { validate_attrs
97            [$([$($outer_attr)*])*]
98            [$([$($attr)*])*]
99            [$(pub $(($($vis)*))?)?]
100            [-] [struct] [$name] [$min] [$max]
101            [$crate]
102        }
103    };
104    (
105        $(#![$($outer_attr:tt)*])*
106        $(#[$($attr:tt)*])*
107        $(pub $(($($vis:tt)*))?)? enum $name:ident($min:expr, $max:expr);
108    ) => {
109        $crate::__helper! { validate_attrs
110            [$([$($outer_attr)*])*]
111            [$([$($attr)*])*]
112            [$(pub $(($($vis)*))?)?]
113            [-] [enum] [$name] [$min] [$max]
114            [$crate]
115        }
116    };
117    (
118        $(#![$($outer_attr:tt)*])*
119        $(#[$($attr:tt)*])*
120        $(pub $(($($vis:tt)*))?)? enum $name:ident {
121            $($(#[$($var_attr:tt)*])* $variant:ident $(= $val:literal)?),* $(,)?
122        }
123    ) => {
124        $crate::__helper! { validate_attrs
125            [$([$($outer_attr)*])*]
126            [$([$($attr)*])*]
127            [$(pub $(($($vis)*))?)?]
128            [+] [enum] [$name] [$([[$(#[$($var_attr)*])*] $variant [$($val)?]])*] []
129            [$crate]
130        }
131    };
132    // Migration
133    ($(#[$($attr:tt)*])* $vis:vis struct $name:ident { $($_:tt)* }) => {
134        compile_error!(concat!(
135            "syntax has changed; use `struct ",
136            stringify!($name),
137            "(MIN, MAX);` instead.",
138        ));
139    };
140    ($(#[$($attr:tt)*])* $vis:vis enum $name:ident { $($_:tt)* }) => {
141        compile_error!(concat!(
142            "syntax has changed; use `enum ",
143            stringify!($name),
144            "(MIN, MAX);` instead.",
145        ));
146    };
147}
148
149#[macro_export]
150#[cfg(feature = "zerocopy")]
151#[doc(hidden)]
152macro_rules! __dispatch_zerocopy {
153    ($outer_attr:tt [$($attr:tt)*] $($t:tt)*) => {
154        $crate::__helper! { vis
155            [+]
156            $outer_attr
157            [
158                $($attr)*
159                [derive($crate::__private::zerocopy::IntoBytes)]
160                [derive($crate::__private::zerocopy::Immutable)]
161            ]
162            $($t)*
163        }
164    };
165}
166#[macro_export]
167#[cfg(not(feature = "zerocopy"))]
168#[doc(hidden)]
169macro_rules! __dispatch_zerocopy {
170    ($($t:tt)*) => {
171        $crate::__helper! { vis [-] $($t)* }
172    };
173}
174
175#[macro_export]
176#[doc(hidden)]
177macro_rules! __helper {
178    (validate_attrs [$($outer_attr:tt)*] [$($attr:tt)*] $($t:tt)*) => {
179        $crate::__dispatch_zerocopy! { [$(#$outer_attr)*] [$($attr)*] $($t)* }
180        $($crate::__helper! { validate_attr $outer_attr })*
181        $($crate::__helper! { validate_attr $attr })*
182    };
183    (validate_attr [doc = $($_:tt)*]) => {};
184    (validate_attr [repr($($_:tt)*)]) => {};
185    (validate_attr [allow($($_:tt)*)]) => {};
186    (validate_attr [expect($($_:tt)*)]) => {};
187    (validate_attr [warn($($_:tt)*)]) => {};
188    (validate_attr [deny($($_:tt)*)]) => {};
189    (validate_attr [forbid($($_:tt)*)]) => {};
190    (validate_attr [deprecated$(($($_:tt)*))?]) => {};
191    (validate_attr [must_use]) => {};
192    (validate_attr [cfg_attr($cfg:meta, $($attr:tt)*)]) => { $crate::__helper! { validate_attr [$($attr)*] } };
193    (validate_attr [$($attr:tt)*]) => {
194        ::core::compile_error!("for soundness reasons, custom attributes are not allowed");
195    };
196    (vis $zerocopy:tt $outer_attr:tt $attr:tt [$(pub($(in)? self))?] $($t:tt)*) => {
197        $crate::__private::proc_macro! { $zerocopy $outer_attr $attr [] [pub(super)] $($t)* }
198    };
199    (vis $zerocopy:tt $outer_attr:tt $attr:tt [pub] $($t:tt)*) => {
200        $crate::__private::proc_macro! { $zerocopy $outer_attr $attr [pub] [pub] $($t)* }
201    };
202    (vis $zerocopy:tt $outer_attr:tt $attr:tt [pub($(in)? crate$(::$($path:ident)::+)?)] $($t:tt)*) => {
203        $crate::__private::proc_macro! { $zerocopy $outer_attr $attr [pub(in crate$(::$($path)::+)?)] [pub(in crate$(::$($path)::+)?)] $($t)* }
204    };
205    (vis $zerocopy:tt $outer_attr:tt $attr:tt [pub(super)] $($t:tt)*) => {
206        $crate::__private::proc_macro! { $zerocopy $outer_attr $attr [pub(super)] [pub(in super::super)] $($t)* }
207    };
208    (vis $zerocopy:tt $outer_attr:tt $attr:tt [pub(in $($path:ident)::+)] $($t:tt)*) => {
209        $crate::__private::proc_macro! { $zerocopy $outer_attr $attr [pub(in $($path)::+)] [pub(in super::$($path)::+)] $($t)* }
210    };
211}
212
213#[cfg(test)]
214mod tests {
215    #[test]
216    fn all_below_zero() {
217        bounded_integer!(#[expect(unused)] struct Struct(-400, -203););
218        bounded_integer!(#[expect(unused)] enum Enum(-500, -483););
219    }
220
221    #[test]
222    fn outer_attrs() {
223        bounded_integer!(#![expect(double_negations)] #[expect(unused)] struct S(--1, 4_i8););
224    }
225
226    #[test]
227    fn publicity() {
228        mod a {
229            #![expect(unused)]
230            bounded_integer!(struct A(0, 0););
231            bounded_integer!(pub(self) struct B(0, 0););
232            bounded_integer!(pub(in self) struct C(0, 0););
233            mod c {
234                bounded_integer!(pub(in super) struct D(0, 0););
235            }
236            pub(super) use c::*;
237        }
238        #[expect(unused)]
239        use a::*;
240        mod b {
241            bounded_integer!(pub(super) struct A(0, 0););
242            bounded_integer!(pub(crate) struct B(0, 0););
243            bounded_integer!(pub(in crate::r#macro) struct C(0, 0););
244            mod c {
245                bounded_integer!(pub(in super::super) struct D(0, 0););
246            }
247            pub(super) use c::*;
248        }
249        use b::*;
250        // would cause an ambiguity error if the number of reachable items above is >1
251        A::default();
252        B::default();
253        C::default();
254        D::default();
255    }
256
257    #[test]
258    fn inferred_reprs() {
259        bounded_integer!(struct ByteStruct(0, 255););
260        const _: u8 = ByteStruct::MIN_VALUE;
261        bounded_integer!(enum ByteEnum(0, 255););
262        const _: u8 = ByteEnum::MIN_VALUE;
263
264        bounded_integer!(struct U16Struct(0, 256););
265        const _: u16 = U16Struct::MIN_VALUE;
266        bounded_integer!(enum U16Enum(0, 256););
267        const _: u16 = U16Enum::MIN_VALUE;
268
269        bounded_integer!(struct I16Struct(-1, 255););
270        const _: i16 = I16Struct::MIN_VALUE;
271        bounded_integer!(enum I16Enum(-1, 255););
272        const _: i16 = I16Enum::MIN_VALUE;
273
274        bounded_integer!(struct SignedByteStruct(-128, 127););
275        const _: i8 = SignedByteStruct::MIN_VALUE;
276        bounded_integer!(struct SignedByteEnum(-128, 127););
277        const _: i8 = SignedByteEnum::MIN_VALUE;
278    }
279
280    #[test]
281    fn simple_enum() {
282        bounded_integer!(enum M(-3, 2););
283        assert_eq!(M::MIN_VALUE, -3);
284        assert_eq!(M::MAX_VALUE, 2);
285        assert_eq!(M::N3.get(), -3);
286        assert_eq!(M::N2.get(), -2);
287        assert_eq!(M::N1.get(), -1);
288        assert_eq!(M::Z.get(), 0);
289        assert_eq!(M::P1.get(), 1);
290        assert_eq!(M::P2.get(), 2);
291        assert_eq!(M::N3 as i8, -3);
292        assert_eq!(M::N2 as i8, -2);
293        assert_eq!(M::N1 as i8, -1);
294        assert_eq!(M::Z as i8, 0);
295        assert_eq!(M::P1 as i8, 1);
296        assert_eq!(M::P2 as i8, 2);
297
298        bounded_integer!(
299            enum X {
300                A = -1,
301                B,
302                C = 1,
303                D,
304            }
305        );
306        assert_eq!(X::A.get(), -1);
307        assert_eq!(X::B.get(), 0);
308        assert_eq!(X::C.get(), 1);
309        assert_eq!(X::D.get(), 2_i8);
310
311        bounded_integer!(
312            enum Y {
313                A = 4_294_967_295,
314            }
315        );
316        assert_eq!(Y::A.get(), 4_294_967_295_u32);
317
318        bounded_integer!(
319            enum Z {
320                A = 4_294_967_295,
321                B,
322            }
323        );
324        assert_eq!(Z::A.get(), 4_294_967_295_u64);
325        assert_eq!(Z::B.get(), 4_294_967_296_u64);
326    }
327
328    #[test]
329    fn zeroable() {
330        #[cfg(all(feature = "bytemuck1", feature = "zerocopy", feature = "num-traits02"))]
331        fn assert_zeroable<T>()
332        where
333            T: Default + bytemuck1::Zeroable + zerocopy::FromZeros + num_traits02::Zero,
334        {
335        }
336        #[cfg(not(all(feature = "bytemuck1", feature = "zerocopy", feature = "num-traits02")))]
337        fn assert_zeroable<T: Default>() {}
338        #[allow(unused)]
339        trait NotZeroable<const N: usize> {}
340        impl<T: Default> NotZeroable<0> for T {}
341        #[cfg(feature = "bytemuck1")]
342        impl<T: bytemuck1::Zeroable> NotZeroable<1> for T {}
343        #[cfg(feature = "zerocopy")]
344        impl<T: zerocopy::FromZeros> NotZeroable<2> for T {}
345        #[cfg(feature = "num-traits02")]
346        impl<T: num_traits02::Zero> NotZeroable<3> for T {}
347        macro_rules! not_zeroable {
348            ($t:ty) => {
349                impl NotZeroable<0> for $t {}
350                #[cfg(feature = "bytemuck1")]
351                impl NotZeroable<1> for $t {}
352                #[cfg(feature = "zerocopy")]
353                impl NotZeroable<2> for $t {}
354                #[cfg(feature = "num-traits02")]
355                impl NotZeroable<3> for $t {}
356            };
357        }
358
359        bounded_integer!(struct A(0, 0););
360        assert_zeroable::<A>();
361
362        bounded_integer!(struct B(-459, 3););
363        assert_zeroable::<B>();
364
365        bounded_integer!(struct C(1, 5););
366        not_zeroable!(C);
367        assert_eq!(size_of::<Option<C>>(), size_of::<C>());
368
369        bounded_integer!(struct D(-5, -1););
370        not_zeroable!(D);
371        assert_eq!(size_of::<Option<D>>(), size_of::<D>());
372
373        bounded_integer!(struct E(-(5), 0_i8););
374        not_zeroable!(E);
375        assert_ne!(size_of::<Option<E>>(), size_of::<E>());
376    }
377
378    #[test]
379    #[cfg(feature = "num-traits02")]
380    fn one() {
381        fn assert_one<T: num_traits02::One>() {}
382        bounded_integer!(struct A(1, 1););
383        assert_one::<A>();
384    }
385
386    #[test]
387    #[cfg(feature = "schemars1")]
388    fn schemars() {
389        use schemars1::{generate::SchemaSettings, schema_for};
390        use serde_json1::json;
391
392        let meta = SchemaSettings::default().meta_schema.unwrap();
393
394        bounded_integer!(struct A(3, 12););
395        assert_eq!(
396            schema_for!(A),
397            json!({
398                "$schema": meta,
399                "type": "integer",
400                "title": "uint8",
401                "format": "uint8",
402                "minimum": 3,
403                "maximum": 12,
404            })
405        );
406
407        // Signed ranges keep their negative bounds.
408        bounded_integer!(struct B(-10_000_000_005, -10_000_000_001););
409        assert_eq!(
410            schema_for!(B),
411            json!({
412                "$schema": meta,
413                "type": "integer",
414                "title": "int64",
415                "format": "int64",
416                "minimum": -10_000_000_005i64,
417                "maximum": -10_000_000_001i64,
418            })
419        );
420
421        // Enums work just like structs.
422        bounded_integer!(enum C(-1, 1););
423        assert_eq!(
424            schema_for!(C),
425            json!({
426                "$schema": meta,
427                "type": "integer",
428                "title": "int8",
429                "format": "int8",
430                "minimum": -1,
431                "maximum": 1,
432            })
433        );
434
435        // Including enums with named variants.
436        bounded_integer! {
437            enum Sign {
438                #[allow(unused)]
439                Negative = -1,
440                #[allow(unused)]
441                Zero,
442                #[allow(unused)]
443                Positive,
444            }
445        }
446        assert_eq!(
447            schema_for!(Sign),
448            json!({
449                "$schema": meta,
450                "type": "integer",
451                "title": "int8",
452                "format": "int8",
453                "minimum": -1,
454                "maximum": 1,
455            })
456        );
457
458        // A custom `repr` is reflected in the schema’s `format`.
459        bounded_integer!(#[repr(u16)] struct D(2, 4););
460        assert_eq!(
461            schema_for!(D),
462            json!({
463                "$schema": meta,
464                "type": "integer",
465                "title": "uint16",
466                "format": "uint16",
467                "minimum": 2,
468                "maximum": 4,
469            })
470        );
471
472        // Assuming serde_json/arbitrary_precision is disabled:
473        // Bounds that fall outside the i64/u64 range are omitted but the type is still supported.
474        bounded_integer!(struct E(0x1_0000_0000_0000_0000, 0x2_0000_0000_0000_0000););
475        assert_eq!(
476            schema_for!(E),
477            json!({
478                "$schema": meta,
479                "type": "integer",
480                "title": "uint128",
481                "format": "uint128",
482            })
483        );
484    }
485
486    #[test]
487    #[cfg(feature = "zerocopy")]
488    fn unaligned() {
489        fn assert_unaligned<T: zerocopy::Unaligned>() {}
490        bounded_integer!(struct A(0, 255););
491        assert_unaligned::<A>();
492        bounded_integer!(struct B(-128, 127););
493        assert_unaligned::<B>();
494
495        #[allow(unused)]
496        trait NotUnaligned {}
497        impl<T: zerocopy::Unaligned> NotUnaligned for T {}
498
499        bounded_integer!(struct C(255, 256););
500        bounded_integer!(struct D(-129, -128););
501        impl NotUnaligned for C {}
502        impl NotUnaligned for D {}
503    }
504
505    #[test]
506    fn lit_radix() {
507        #![expect(clippy::mixed_case_hex_literals)]
508
509        bounded_integer!(struct Hex(0x5, 0xf_F););
510        assert_eq!(Hex::MIN_VALUE, 5);
511        assert_eq!(Hex::MAX_VALUE, 255);
512
513        bounded_integer!(struct Oct(0o35, 0o40););
514        assert_eq!(Oct::MIN_VALUE, 29);
515        assert_eq!(Oct::MAX_VALUE, 32);
516
517        bounded_integer!(struct Bin(0b1101, 0b11101););
518        assert_eq!(Bin::MIN_VALUE, 0b1101);
519        assert_eq!(Bin::MAX_VALUE, 0b11101);
520    }
521
522    #[test]
523    fn repr_without_repr() {
524        bounded_integer!(#[expect(unused)] struct Owo(0_u8, 2 + 2););
525        bounded_integer!(#[expect(unused)] struct Uwu(-57 * 37, 8_i64););
526    }
527
528    #[test]
529    fn allowed_attrs() {
530        #![expect(deprecated)]
531        bounded_integer! {
532            #[cfg_attr(all(), doc = "…")]
533            #[deprecated]
534            #[must_use]
535            struct S(0, 255);
536        }
537
538        #[expect(deprecated, unused_must_use)]
539        S::new(0).unwrap();
540    }
541
542    #[test]
543    fn complex_expr() {
544        bounded_integer!(#[repr(u8)] struct S(0, 10 + 10););
545        assert_eq!(S::MIN_VALUE, 0);
546        assert_eq!(S::MAX_VALUE, 20);
547    }
548}