1#![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 let expanded = expander::Expander::new(format!("{name}-{random:X}"))
45 .fmt(expander::Edition::_2021)
46 .verbose(true)
47 .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#[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 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#[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 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#[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
140fn impl_integer_id(ast: &DeriveInput) -> syn::Result<TokenStream> {
142 let options = parse_options(ast)?;
143 let name = &ast.ident;
144 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 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 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 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 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 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 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#[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, "e!(#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 from: Option<Span>,
372 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 name_span: Span,
417 skip_contiguous: Option<Span>,
418}