Skip to main content

intid_derive/
lib.rs

1//! Implements the [`IntegerId`] derive macro.
2//!
3//! Generally, you want to use the re-export from the `intid` or `idmap` crates.
4//! In the `intid` crate this requires explicitly enabling the `derive` feature.
5//! In the `idmap` crate, the derive feature is on by default.
6#![allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]
7
8use crate::analyze::{analyze, AnalyzedType, EnumIdBounds, TargetTrait, UninhabitedEnumError};
9use proc_macro2::{Ident, Span, TokenStream};
10use quote::{quote, quote_spanned};
11use syn::spanned::Spanned;
12use syn::DeriveInput;
13
14mod analyze;
15
16#[allow(clippy::needless_pass_by_value)]
17fn maybe_expand(input: TokenStream, name: &str) -> TokenStream {
18    let _ = name;
19    #[cfg(not(feature = "expander"))]
20    {
21        #[cfg(intid_derive_use_expander)]
22        {
23            compile_error!(
24                "Enabled `cfg(intid_derive_use_expander)`, but missing 'expander' feature"
25            )
26        }
27        input
28    }
29    #[cfg(feature = "expander")]
30    {
31        let random: u64 = {
32            use core::hash::{BuildHasher, Hasher};
33            use std::hash::RandomState;
34            RandomState::new().build_hasher().finish()
35        };
36        let input = &input;
37        let output = quote! {
38            #[allow(clippy::undocumented_unsafe_blocks, unused_parens)]
39            const _: () = {
40                #input
41            };
42        };
43        // fixes a bug with unit tests conflicting with integration tests
44        let expanded = expander::Expander::new(format!("{name}-{random:X}"))
45            .fmt(expander::Edition::_2021)
46            .verbose(true)
47            // It would be nice to use an environment variable here,
48            // but that would require `proc_macro::tracked_env`
49            .dry(cfg!(not(intid_derive_use_expander)))
50            .write_to_out_dir(output)
51            .unwrap_or_else(|e| {
52                eprintln!("Failed to write to file: {e:?}");
53                input.clone()
54            });
55        expanded
56    }
57}
58
59/// See `intid` crate for docs.
60#[proc_macro_derive(IntegerIdContiguous, attributes(intid))]
61pub fn integer_id_contiguous(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
62    let ast = syn::parse(input).unwrap();
63    maybe_expand(
64        impl_contiguous(&ast).unwrap_or_else(syn::Error::into_compile_error),
65        "IntegerIdContiguous",
66    )
67    .into()
68}
69
70fn impl_contiguous(ast: &DeriveInput) -> syn::Result<TokenStream> {
71    const TARGET_TRAIT: TargetTrait = TargetTrait::IntegerIdContiguous;
72    let analyzed = analyze(ast, TARGET_TRAIT)?;
73    impl_contiguous_for(&analyzed)
74}
75
76fn impl_contiguous_for(analyzed: &AnalyzedType) -> syn::Result<TokenStream> {
77    // No need to parse options (we don't care)
78    let newtype = analyzed.ensure_only_newtype()?;
79    let name = newtype.ident();
80    let wrapped_type = newtype.wrapped_field_type;
81    let require_contig = quote_spanned!(newtype.wrapped_field_type.span() => {
82        fn require_contig<T: intid::IntegerIdContiguous>() {}
83        let _ = require_contig::<#wrapped_type>;
84    });
85    Ok(quote! {
86        const _: () = {
87            #require_contig
88        };
89        #[automatically_derived]
90        impl intid::IntegerIdContiguous for #name {}
91    })
92}
93
94/// See `intid` crate for docs.
95#[proc_macro_derive(IntegerIdCounter, attributes(intid))]
96pub fn integer_id_counter(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
97    let ast = syn::parse(input).unwrap();
98    maybe_expand(
99        impl_id_counter(&ast).unwrap_or_else(syn::Error::into_compile_error),
100        "IntegerIdCounter",
101    )
102    .into()
103}
104
105fn impl_id_counter(ast: &DeriveInput) -> syn::Result<TokenStream> {
106    const TARGET_TRAIT: TargetTrait = TargetTrait::IntegerIdCounter;
107    let options = parse_options(ast)?;
108    // No need to parse options (we don't care)
109    let name = &ast.ident;
110    let analyzed = analyze(ast, TARGET_TRAIT)?;
111    let newtype = analyzed.ensure_only_newtype()?;
112    let field_type_as_counter = newtype.wrapped_as(quote!(intid::IntegerIdCounter));
113    let contig_impl = match options.counter {
114        Some(ref x) if x.skip_contiguous.is_some() => quote!(),
115        None | Some(_) => impl_contiguous_for(&analyzed)?,
116    };
117    let start_int = quote!(#field_type_as_counter::START_INT);
118    let start = newtype.construct(&start_int);
119    Ok(quote! {
120        #contig_impl
121        #[automatically_derived]
122        impl intid::IntegerIdCounter for #name {
123            const START: Self = #start;
124            const START_INT: Self::Int = #start_int;
125        }
126    })
127}
128
129/// See the documentation in the `intid` crate for details.
130#[proc_macro_derive(IntegerId, attributes(intid))]
131pub fn integer_id(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
132    let ast = syn::parse(input).unwrap();
133    maybe_expand(
134        impl_integer_id(&ast).unwrap_or_else(syn::Error::into_compile_error),
135        "IntegerId",
136    )
137    .into()
138}
139
140// The compiler doesn't seem to know when variables are used in the macro
141fn impl_integer_id(ast: &DeriveInput) -> syn::Result<TokenStream> {
142    let options = parse_options(ast)?;
143    let name = &ast.ident;
144    // TODO: Replace From<&'_ #name> with From<#wrapped_type>?
145    let from_impl = if options.from.is_none() {
146        quote!()
147    } else {
148        quote! {
149            impl From<&'_ #name> for #name {
150                #[inline]
151                fn from(this: &'_ #name) -> #name {
152                    *this
153                }
154            }
155        }
156    };
157    const TARGET_TRAIT: TargetTrait = TargetTrait::IntegerId;
158    let analyzed = analyze::analyze(ast, TARGET_TRAIT)?;
159    match analyzed {
160        AnalyzedType::NewType(ref tp) => {
161            let field_type = tp.wrapped_field_type;
162            let field_name = &tp.wrapped_field_name;
163            let field_type_as_id = tp.wrapped_as(quote!(intid::IntegerId));
164            let int_type = quote_spanned! {
165                field_type.span() => #field_type_as_id::Int
166            };
167            let int_constructor = |method_name: &str, needs_try: bool| {
168                let maybe_try = if needs_try { quote!(?) } else { quote!() };
169                let method_name = Ident::new(method_name, tp.wrapped_field_type.span());
170                tp.construct(quote!(#field_type_as_id::#method_name(int)#maybe_try))
171            };
172            let impl_from_int = int_constructor("from_int", false);
173            let impl_from_int_checked = int_constructor("from_int_checked", true);
174            let impl_from_int_unchecked = int_constructor("from_int_unchecked", false);
175            let impl_to_int =
176                quote_spanned! { field_type.span() => #field_type_as_id::to_int(self.#field_name) };
177            let impl_decl = quote_spanned! { name.span() => impl intid::IntegerId for #name };
178            let verify_counter_impl = match options.counter {
179                Some(CounterOptions { name_span, .. }) => {
180                    // If the counter option is used, we should be a counter
181                    quote_spanned! { name_span =>
182                        {
183                            #[inline(always)]
184                            fn verify_counter<T: intid::IntegerIdCounter>() {}
185                            verify_counter::<#name>();
186                        }
187                    }
188                }
189                None => quote!(),
190            };
191            let field_name = &tp.wrapped_field_name;
192            Ok(quote! {
193                #[automatically_derived]
194                #[allow(clippy::init_numbered_fields)]
195                #impl_decl {
196                    type Int = #int_type;
197                    const MIN_ID: Option<Self> = match #field_type_as_id::MIN_ID {
198                        Some(min) => Some(#name { #field_name: min }),
199                        None => None,
200                    };
201                    const MAX_ID: Option<Self> = match #field_type_as_id::MAX_ID {
202                        Some(max) => Some(#name { #field_name: max }),
203                        None => None,
204                    };
205                    const MIN_ID_INT: Option<Self::Int> = #field_type_as_id::MIN_ID_INT;
206                    const MAX_ID_INT: Option<Self::Int> = #field_type_as_id::MIN_ID_INT;
207                    const TRUSTED_RANGE: Option<intid::trusted::TrustedRangeToken<Self>> = {
208                        // SAFETY: We simply delegate, so are valid if #field_type is
209                        unsafe { intid::trusted::TrustedRangeToken::assume_valid_if::<#field_type>() }
210                    };
211
212                    #[inline]
213                    fn from_int(int: #int_type) -> Self {
214                        #verify_counter_impl
215                        #impl_from_int
216                    }
217                    #[inline]
218                    fn from_int_checked(int: #int_type) -> Option<Self> {
219                        Some(#impl_from_int_checked)
220                    }
221                    #[inline]
222                    #[allow(unsafe_code)]
223                    unsafe fn from_int_unchecked(int: #int_type) -> Self {
224                        // SAFETY: Simply delegating responsibility
225                        unsafe { #impl_from_int_unchecked }
226                    }
227                    #[inline]
228                    fn to_int(self) -> #int_type {
229                        #impl_to_int
230                    }
231                }
232                #from_impl
233            })
234        }
235        AnalyzedType::Enum(ref tp) => {
236            let variant_matches = tp
237                .variants
238                .iter()
239                .map(|variant| {
240                    let idx = variant.discriminant;
241                    let variant_name = variant.name();
242                    quote!(#idx => #name::#variant_name)
243                })
244                .collect::<Vec<_>>();
245            let int_type = tp.discriminant_type;
246            let EnumIdBounds { min_id, max_id } = match tp.determine_id_bounds() {
247                Ok(bounds) => bounds.map(|src| quote!(Some(#src))),
248                Err(UninhabitedEnumError) => EnumIdBounds {
249                    min_id: quote!(None),
250                    max_id: quote!(None),
251                },
252            };
253            Ok(quote! {
254                impl intid::IntegerId for #name {
255                    type Int = #int_type;
256                    const MAX_ID: Option<Self> = #max_id;
257                    const MIN_ID: Option<Self> = #min_id;
258                    const MAX_ID_INT: Option<#int_type> = match Self::MAX_ID {
259                        Some(max) => Some(max as #int_type),
260                        None => None,
261                    };
262                    const MIN_ID_INT: Option<#int_type> = match Self::MIN_ID {
263                        Some(min) => Some(min as #int_type),
264                        None => None,
265                    };
266                    const TRUSTED_RANGE: Option<intid::trusted::TrustedRangeToken<Self>> = {
267                        // SAFETY: We accurately report the range of enum discriminants
268                        Some(unsafe { intid::trusted::TrustedRangeToken::assume_valid() })
269                    };
270
271                    #[inline]
272                    #[allow(unreachable_code)]
273                    fn from_int_checked(x: #int_type) -> Option<Self> {
274                        // NOTE: Works assuming that x fits in u64
275                        // Needed since the literals in variant_matches have to have a concrete type
276                        const _: () = {
277                            assert!(#int_type::BITS <= u64::BITS, "too many bits for derive");
278                        };
279                        Some(match u64::from(x) {
280                            #(#variant_matches,)*
281                            _ => return None,
282                        })
283                    }
284
285                    #[inline]
286                    #[allow(unreachable_code)]
287                    unsafe fn from_int_unchecked(x: #int_type) -> Self {
288                        match u64::from(x) {
289                            #(#variant_matches,)*
290                            _ => {
291                                // SAFETY: Validity guaranteed by caller
292                                unsafe { core::hint::unreachable_unchecked() }
293                            }
294                        }
295                    }
296
297                    #[inline]
298                    fn to_int(self) -> #int_type {
299                        self as #int_type
300                    }
301                }
302                #from_impl
303            })
304        }
305    }
306}
307
308/// See the documentation in the `intid` crate for details.
309#[proc_macro_derive(EnumId, attributes(intid))]
310pub fn enum_id(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
311    let ast = syn::parse(input).unwrap();
312    maybe_expand(
313        impl_enum_id(&ast).unwrap_or_else(syn::Error::into_compile_error),
314        "EnumId",
315    )
316    .into()
317}
318
319fn impl_enum_id(ast: &DeriveInput) -> syn::Result<TokenStream> {
320    let _options = parse_options(ast);
321    let name = &ast.ident;
322    const TARGET_TRAIT: TargetTrait = TargetTrait::EnumId;
323    let analyzed = analyze::analyze(ast, TARGET_TRAIT)?;
324    let analyzed = analyzed.ensure_only_enum()?;
325    let EnumIdBounds { min_id: _, max_id } = match analyzed.determine_id_bounds() {
326        Ok(res) => res.map(Some),
327        Err(UninhabitedEnumError) => EnumIdBounds {
328            min_id: None,
329            max_id: None,
330        },
331    };
332    let upper_bound = max_id.map_or_else(|| quote!(0), |max_id| quote!(#max_id as usize + 1));
333    const BITSET_LIMB_SIZE: u32 = u64::BITS;
334    let verify_bitset_limbs = {
335        assert_eq!(BITSET_LIMB_SIZE, u64::BITS);
336        quote! {
337            const _: () = {
338                const fn assert_type_bitset_limbs(_x: intid::array::BitsetLimb) {}
339                assert_type_bitset_limbs(0u64)
340            };
341        }
342    };
343    fn divide_round_up(num: &TokenStream, denom: &TokenStream) -> TokenStream {
344        let denom = denom.clone();
345        quote! {(((#num) + ((#denom) - 1)) / (#denom))}
346    }
347    let bitset_upper_bound = divide_round_up(&upper_bound, &quote!(#BITSET_LIMB_SIZE as usize));
348    let count = analyzed.variants.len();
349    Ok(quote! {
350        impl intid::EnumId for #name {
351            const COUNT: u32 = #count as u32;
352            type Array<T> = [T; #upper_bound];
353            type BitSet = [u64; #bitset_upper_bound];
354        }
355        #verify_bitset_limbs
356    })
357}
358
359fn parse_options(ast: &DeriveInput) -> syn::Result<MainOptions> {
360    ast.attrs
361        .iter()
362        .find(|attr| attr.meta.path().is_ident("intid"))
363        .map_or_else(|| Ok(MainOptions::default()), MainOptions::parse_attr)
364}
365
366#[derive(Default, Debug)]
367struct MainOptions {
368    /// Automatically generate a `From<&Self>` implementation.
369    ///
370    /// TODO: This should instead generate a `From<Inner>` implementation.
371    from: Option<Span>,
372    /// Options specific to a counter.
373    counter: Option<CounterOptions>,
374}
375impl MainOptions {
376    fn parse_attr(attr: &syn::Attribute) -> syn::Result<Self> {
377        let mut res = MainOptions::default();
378        attr.parse_nested_meta(|meta| {
379            if meta.path.is_ident("from") {
380                res.from = Some(meta.path.span());
381                Ok(())
382            } else if meta.path.is_ident("counter") {
383                if res.counter.is_some() {
384                    return Err(syn::Error::new_spanned(
385                        &meta.path,
386                        "Specified counter twice",
387                    ));
388                }
389                let mut counter_opts = CounterOptions {
390                    name_span: meta.path.span(),
391                    skip_contiguous: None,
392                };
393                if meta.input.peek(syn::token::Paren) {
394                    meta.parse_nested_meta(|meta| {
395                        if meta.path.is_ident("skip_contiguous") {
396                            counter_opts.skip_contiguous = Some(meta.path.span());
397                            Ok(())
398                        } else {
399                            Err(meta.error("Invalid `counter` attribute"))
400                        }
401                    })?;
402                }
403                res.counter = Some(counter_opts);
404                Ok(())
405            } else {
406                Err(meta.error("Invalid attribute"))
407            }
408        })?;
409        Ok(res)
410    }
411}
412
413#[derive(Debug)]
414struct CounterOptions {
415    /// The span for the `counter` ident.
416    name_span: Span,
417    skip_contiguous: Option<Span>,
418}