Skip to main content

linearize_derive/
lib.rs

1use {
2    proc_macro2::{Ident, Literal, Span, TokenStream, TokenTree},
3    quote::{quote, quote_spanned},
4    syn::{
5        parse::{Parse, ParseStream},
6        parse_macro_input, parse_quote,
7        spanned::Spanned,
8        Attribute, Error, Generics, Item, ItemEnum, ItemStruct, LitInt, Path, Token, Type,
9    },
10};
11
12/// A proc macro to derive the `Linearize` trait.
13///
14/// This macro can be used to derive the `Linearize` trait for structs and enums.
15///
16/// The structure of these types can be arbitrary except that all contained fields must
17/// also implement the `Linearize` trait.
18///
19/// # Using different crate names
20///
21/// If you use the linearize crate under a name other than `linearize`, you can use the
22/// `crate` attribute to have the proc macro reference the correct crate. For example,
23/// if you import the linearize crate like this:
24///
25/// ```toml
26/// linearize-0_1 = { package = "linearize", version = "0.1" }
27/// ```
28///
29/// Then you can use this attribute as follows:
30///
31/// ```rust,ignore
32/// #[derive(Linearize)]
33/// #[linearize(crate = linearize_0_1)]
34/// struct S;
35/// ```
36///
37/// <div class="warning">
38///
39/// If you import the linearize crate under a name other than `linearize` or use the crate
40/// attribute, you must ensure that these two names are in sync. Otherwise the macro
41/// might not uphold the invariants of the `Linearize` trait.
42///
43/// </div>
44///
45/// # Implementing const functions
46///
47/// If you want to use the forms of the `static_map` and `static_copy_map` macros that
48/// work in constants and statics, you must enable the `const` attribute:
49///
50/// ```rust,ignore
51/// #[derive(Linearize)]
52/// #[linearize(const)]
53/// struct S;
54/// ```
55///
56/// In this case, your type must only contain fields that also enabled this attribute. In
57/// particular, you cannot use any of the standard types `u8`, `bool`, etc.
58///
59/// # Performance
60///
61/// If the type is a C-style enum with default discriminants, the derived functions will
62/// be compiled to a jump table in debug mode and will be completely optimized away in
63/// release mode.
64///
65/// If the type contains fields, the generated code will still be reasonably efficient.
66///
67/// # Variant Order
68///
69/// If the type is a C-style enum, the implementation of `linearize()` produced by this
70/// macro will order the variants in the same order as they are defined, independent of
71/// the discriminants, either default or specified.
72///
73/// # Limitations
74///
75/// While this macro fully supports types with generics, the generated output will not
76/// compile. This is due to limitations of the rust type system. If a future version of
77/// the rust compiler lifts these limitations, this macro will automatically start working
78/// for generic types.
79#[proc_macro_derive(Linearize, attributes(linearize))]
80pub fn derive_linearize(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
81    let mut input: Input = parse_macro_input!(input as Input);
82    let crate_name = &input.attributes.crate_name;
83    let FullyLinearized {
84        linearize,
85        delinearize,
86        const_linearize,
87        const_delinearize,
88        const_names,
89        consts,
90        max_len,
91    } = input.build_linearize();
92    let where_clause = input.generics.make_where_clause();
93    for ty in &input.critical_types {
94        where_clause
95            .predicates
96            .push(parse_quote!(#ty: #crate_name::Linearize));
97    }
98    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
99    let ident = input.ident;
100    let mut const_impl = quote! {};
101    if input.attributes.enable_const {
102        const_impl = quote! {
103            #[doc(hidden)]
104            impl #impl_generics #ident #type_generics #where_clause {
105                #[inline]
106                pub const fn __linearize_d66aa8fa_6974_4651_b2b7_75291a9e7105(&self) -> usize {
107                    #const_linearize
108                }
109
110                #[inline]
111                pub const unsafe fn __from_linear_unchecked_fb2f0b31_5b5a_48b4_9264_39d0bdf94f1d(linear: usize) -> Self {
112                    #const_delinearize
113                }
114            }
115        };
116    }
117    let res = quote_spanned! { input.span =>
118        #[allow(clippy::modulo_one, clippy::manual_range_contains)]
119        const _: () = {
120            trait __C {
121                #(const #const_names: usize;)*
122            }
123
124            impl #impl_generics __C for #ident #type_generics #where_clause {
125                #(#consts)*
126            }
127
128            // SAFETY:
129            //
130            // Storage and CopyStorage obviously are the required type.
131            //
132            // The bodies if `linearize` and `from_linear_unchecked` are generated as follows:
133            //
134            // First, consider a struct s = { a1: T1, ..., an: Tn }. The calculated LENGTH
135            // is the product of the lengths of the Ti. We write |T| for the LENGTH of T.
136            // Write Bi = |T{i+1}| * ... * |Tn|, the product of the LENGTHs of the later types.
137            // Write linear(v) for the linearization of v. Then we define
138            // linear(s) = \sum_{i} linear(ai) * Bi.
139            // It is easy to see that linear(s) / Bi % Ti = linear(ai).
140            // Therefore we have created a bijection between the struct and [0, B0).
141            //
142            // Now consider an enum e = { V1, ..., Vn } where each variant can have fields.
143            // Each Vi can be treated like a struct and we can define a bijection between
144            // the enum and [0, |V1| + ... + |Vn|) by mapping V1 to [0, |V1|), V2 to
145            // [|V1|, |V1| + |V2|), and so on.
146            #[automatically_derived]
147            unsafe impl #impl_generics
148            #crate_name::Linearize for #ident #type_generics
149            #where_clause
150            {
151                type Storage<__T> = [__T; <Self as #crate_name::Linearize>::LENGTH];
152
153                type CopyStorage<__T> = [__T; <Self as #crate_name::Linearize>::LENGTH] where __T: Copy;
154
155                const LENGTH: usize = <Self as __C>::#max_len;
156
157                #[inline]
158                fn linearize(&self) -> usize {
159                    #linearize
160                }
161
162                #[inline]
163                unsafe fn from_linear_unchecked(linear: usize) -> Self {
164                    #delinearize
165                }
166            }
167
168            #const_impl
169        };
170    };
171    res.into()
172}
173
174struct Input {
175    span: Span,
176    ident: Ident,
177    generics: Generics,
178    critical_types: Vec<Type>,
179    kind: Kind,
180    attributes: InputAttributes,
181}
182
183struct InputAttributes {
184    crate_name: Path,
185    enable_const: bool,
186}
187
188#[derive(Default)]
189struct InputAttributesOpt {
190    crate_name: Option<Path>,
191    enable_const: bool,
192}
193
194enum Kind {
195    Struct(StructInput),
196    Enum(EnumInput),
197}
198
199struct StructInput {
200    fields: Vec<StructField>,
201}
202
203struct EnumInput {
204    variants: Vec<EnumVariant>,
205}
206
207struct EnumVariant {
208    ident: Ident,
209    fields: Vec<StructField>,
210}
211
212struct PartialLinearized {
213    linearize: TokenStream,
214    delinearize: TokenStream,
215    const_linearize: TokenStream,
216    const_delinearize: TokenStream,
217    max_len: Option<TokenStream>,
218}
219
220struct FullyLinearized {
221    linearize: TokenStream,
222    delinearize: TokenStream,
223    const_linearize: TokenStream,
224    const_delinearize: TokenStream,
225    const_names: Vec<Ident>,
226    consts: Vec<TokenStream>,
227    max_len: Ident,
228}
229
230struct StructField {
231    original_name: Option<Ident>,
232    generated_name: Option<Ident>,
233    ty: Type,
234}
235
236fn build_linearize_struct(
237    input: &Input,
238    fields: &[StructField],
239    base: &Ident,
240) -> PartialLinearized {
241    let crate_name = &input.attributes.crate_name;
242    let mut linearize_parts = vec![];
243    let mut delinearize_parts = vec![];
244    let mut const_linearize_parts = vec![];
245    let mut const_delinearize_parts = vec![];
246    let mut max_len = quote!(1usize);
247    for (idx, field) in fields.iter().enumerate().rev() {
248        let idx = LitInt::new(&idx.to_string(), Span::call_site());
249        let ref_name = match &field.generated_name {
250            Some(i) => quote! {#i},
251            None => match &field.original_name {
252                Some(i) => quote! { &self.#i },
253                None => quote! { &self.#idx },
254            },
255        };
256        let mut_name = match &field.original_name {
257            Some(i) => quote! { #i },
258            None => quote! { #idx },
259        };
260        let ty = &field.ty;
261        linearize_parts.push(quote! {
262            res = res.wrapping_add(<#ty as #crate_name::Linearize>::linearize(#ref_name).wrapping_mul(const { #max_len }));
263        });
264        delinearize_parts.push(quote! {
265            #mut_name: {
266                let idx = (linear / const { #max_len }) % <#ty as #crate_name::Linearize>::LENGTH;
267                <#ty as #crate_name::Linearize>::from_linear_unchecked(idx)
268            },
269        });
270        if input.attributes.enable_const {
271            const_linearize_parts.push(quote! {
272                res = res.wrapping_add(<#ty>::__linearize_d66aa8fa_6974_4651_b2b7_75291a9e7105(#ref_name).wrapping_mul(const { #max_len }));
273            });
274            const_delinearize_parts.push(quote! {
275                #mut_name: {
276                    let idx = (linear / const { #max_len }) % <#ty as #crate_name::Linearize>::LENGTH;
277                    <#ty>::__from_linear_unchecked_fb2f0b31_5b5a_48b4_9264_39d0bdf94f1d(idx)
278                },
279            });
280        }
281        max_len = quote! {
282            #max_len * <#ty as #crate_name::Linearize>::LENGTH
283        };
284    }
285    delinearize_parts.reverse();
286    const_delinearize_parts.reverse();
287    let make_linearize = |parts: &[TokenStream]| {
288        if fields.is_empty() {
289            quote! { <Self as __C>::#base }
290        } else {
291            quote! {
292                let mut res = <Self as __C>::#base;
293                #(#parts)*
294                res
295            }
296        }
297    };
298    let make_delinearize = |parts: &[TokenStream]| {
299        quote! {
300            { #(#parts)* }
301        }
302    };
303    let mut max_len = Some(max_len);
304    if fields.is_empty() {
305        max_len = None;
306    }
307    PartialLinearized {
308        linearize: make_linearize(&linearize_parts),
309        delinearize: make_delinearize(&delinearize_parts),
310        const_linearize: make_linearize(&const_linearize_parts),
311        const_delinearize: make_delinearize(&const_delinearize_parts),
312        max_len,
313    }
314}
315
316impl StructInput {
317    fn build_linearize(&self, input: &Input) -> FullyLinearized {
318        let b0 = Ident::new("B0", Span::mixed_site());
319        let b1 = Ident::new("B1", Span::mixed_site());
320        let PartialLinearized {
321            linearize,
322            delinearize,
323            const_linearize,
324            const_delinearize,
325            max_len,
326        } = build_linearize_struct(input, &self.fields, &b0);
327        let max_len = max_len.unwrap_or_else(|| quote!(1usize));
328        let mut consts = vec![];
329        consts.push(quote! { const B0: usize = 0; });
330        consts.push(quote! { const B1: usize = #max_len; });
331        FullyLinearized {
332            linearize,
333            delinearize: quote! { Self #delinearize },
334            const_linearize,
335            const_delinearize: quote! { Self #const_delinearize },
336            max_len: b1.clone(),
337            consts,
338            const_names: vec![b0, b1],
339        }
340    }
341}
342
343impl EnumInput {
344    fn build_linearize(&self, input: &Input) -> FullyLinearized {
345        let mut linearize_cases = vec![];
346        let mut delinearize_cases = vec![];
347        let mut const_linearize_cases = vec![];
348        let mut const_delinearize_cases = vec![];
349        let mut consts = vec![];
350        consts.push(quote! { const B0: usize = 0; });
351        let mut prev_const_name = Ident::new("B0", Span::mixed_site());
352        let mut const_base = prev_const_name.clone();
353        let mut const_base_offset = 0;
354        let mut const_names = vec![prev_const_name.clone()];
355        for (variant_idx, variant) in self.variants.iter().enumerate() {
356            let mut exposition = vec![];
357            for (idx, field) in variant.fields.iter().enumerate() {
358                let idx = LitInt::new(&idx.to_string(), Span::call_site());
359                let generated_name = field.generated_name.as_ref().unwrap();
360                match &field.original_name {
361                    None => exposition.push(quote! { #idx: #generated_name }),
362                    Some(i) => exposition.push(quote! { #i: #generated_name }),
363                }
364            }
365            let exposition = quote! {
366                { #(#exposition),* }
367            };
368            let PartialLinearized {
369                linearize,
370                delinearize,
371                const_linearize,
372                const_delinearize,
373                max_len,
374            } = build_linearize_struct(input, &variant.fields, &prev_const_name);
375            let next_base = match &max_len {
376                Some(len) => quote! { <Self as __C>::#prev_const_name + #len },
377                None => {
378                    const_base_offset += 1;
379                    let offset = Literal::usize_unsuffixed(const_base_offset);
380                    quote! { <Self as __C>::#const_base + #offset }
381                }
382            };
383            let ident = &variant.ident;
384            linearize_cases.push(quote! {
385                Self::#ident #exposition => {
386                    #linearize
387                }
388            });
389            if input.attributes.enable_const {
390                const_linearize_cases.push(quote! {
391                    Self::#ident #exposition => {
392                        #const_linearize
393                    }
394                });
395            }
396            let const_name = Ident::new(&format!("B{}", variant_idx + 1), Span::mixed_site());
397            consts.push(quote! { const #const_name: usize = #next_base; });
398            if variant.fields.is_empty() {
399                let guard = if input.generics.params.is_empty() {
400                    quote! {
401                        <Self as __C>::#prev_const_name
402                    }
403                } else {
404                    quote! {
405                        n if n == <Self as __C>::#prev_const_name
406                    }
407                };
408                delinearize_cases.push(quote! {
409                    #guard => Self::#ident { },
410                });
411                if input.attributes.enable_const {
412                    const_delinearize_cases.push(quote! {
413                        #guard => Self::#ident { },
414                    });
415                }
416            } else {
417                let make_case = |delinearize: &TokenStream| {
418                    quote! {
419                        #[allow(clippy::impossible_comparisons)]
420                        n if n >= <Self as __C>::#prev_const_name && n < <Self as __C>::#const_name => {
421                            let linear = linear.wrapping_sub(<Self as __C>::#prev_const_name);
422                            Self::#ident #delinearize
423                        },
424                    }
425                };
426                delinearize_cases.push(make_case(&delinearize));
427                if input.attributes.enable_const {
428                    const_delinearize_cases.push(make_case(&const_delinearize));
429                }
430            }
431            prev_const_name = const_name;
432            const_names.push(prev_const_name.clone());
433            if max_len.is_some() {
434                const_base = prev_const_name.clone();
435                const_base_offset = 0;
436            }
437        }
438        let make_linearize = |cases: &[TokenStream]| {
439            if self.variants.is_empty() {
440                quote! {
441                    #[cold]
442                    const fn unreachable() -> ! {
443                        unsafe { ::core::hint::unreachable_unchecked() }
444                    }
445                    unreachable()
446                }
447            } else {
448                quote! {
449                    match self {
450                        #(#cases)*
451                    }
452                }
453            }
454        };
455        let make_delinearize = |cases: &[TokenStream]| {
456            quote! {
457                match linear {
458                    #(#cases)*
459                    _ => {
460                        #[cold]
461                        const fn unreachable() -> ! {
462                            unsafe { ::core::hint::unreachable_unchecked() }
463                        }
464                        unreachable()
465                    },
466                }
467            }
468        };
469        FullyLinearized {
470            linearize: make_linearize(&linearize_cases),
471            const_linearize: make_linearize(&const_linearize_cases),
472            delinearize: make_delinearize(&delinearize_cases),
473            const_delinearize: make_delinearize(&const_delinearize_cases),
474            max_len: prev_const_name,
475            const_names,
476            consts,
477        }
478    }
479}
480
481impl Input {
482    fn parse_enum(input: ItemEnum) -> syn::Result<Self> {
483        let span = input.span();
484        let mut critical_types = Vec::new();
485        let mut variants = vec![];
486        let mut i = 0;
487        for variant in input.variants {
488            let mut fields = vec![];
489            for field in variant.fields {
490                critical_types.push(field.ty.clone());
491                let name = Ident::new(&format!("f{i}"), Span::mixed_site());
492                i += 1;
493                fields.push(StructField {
494                    original_name: field.ident,
495                    generated_name: Some(name),
496                    ty: field.ty,
497                })
498            }
499            variants.push(EnumVariant {
500                ident: variant.ident,
501                fields,
502            });
503        }
504        Ok(Self {
505            span,
506            ident: input.ident,
507            generics: input.generics,
508            critical_types,
509            kind: Kind::Enum(EnumInput { variants }),
510            attributes: parse_attributes(&input.attrs)?,
511        })
512    }
513
514    fn parse_struct(input: ItemStruct) -> syn::Result<Self> {
515        let span = input.span();
516        let mut critical_types = Vec::new();
517        let mut fields = vec![];
518        for field in input.fields {
519            critical_types.push(field.ty.clone());
520            fields.push(StructField {
521                original_name: field.ident,
522                generated_name: None,
523                ty: field.ty,
524            });
525        }
526        Ok(Self {
527            span,
528            ident: input.ident,
529            generics: input.generics,
530            critical_types,
531            kind: Kind::Struct(StructInput { fields }),
532            attributes: parse_attributes(&input.attrs)?,
533        })
534    }
535
536    fn build_linearize(&self) -> FullyLinearized {
537        match &self.kind {
538            Kind::Struct(s) => s.build_linearize(self),
539            Kind::Enum(e) => e.build_linearize(self),
540        }
541    }
542}
543
544fn parse_attributes(attrs: &[Attribute]) -> syn::Result<InputAttributes> {
545    let mut res = InputAttributesOpt::default();
546    for attr in attrs {
547        if !attr.meta.path().is_ident("linearize") {
548            continue;
549        }
550        let new: InputAttributesOpt = attr.meta.require_list()?.parse_args()?;
551        res.enable_const |= new.enable_const;
552        macro_rules! opt {
553            ($name:ident) => {
554                if new.$name.is_some() {
555                    res.$name = new.$name;
556                }
557            };
558        }
559        opt!(crate_name);
560    }
561    Ok(InputAttributes {
562        crate_name: res.crate_name.unwrap_or_else(|| parse_quote!(::linearize)),
563        enable_const: res.enable_const,
564    })
565}
566
567impl Parse for InputAttributesOpt {
568    fn parse(input: ParseStream) -> syn::Result<Self> {
569        let mut res = Self::default();
570        while !input.is_empty() {
571            let key: TokenTree = input.parse()?;
572            match key.to_string().as_str() {
573                "crate" => {
574                    let _: Token![=] = input.parse()?;
575                    let path: Path = input.parse()?;
576                    res.crate_name = Some(path);
577                }
578                "const" => {
579                    res.enable_const = true;
580                }
581                _ => {
582                    return Err(Error::new(
583                        key.span(),
584                        format!("Unknown attribute: {}", key),
585                    ))
586                }
587            }
588            if !input.is_empty() {
589                let _: Token![,] = input.parse()?;
590            }
591        }
592        Ok(res)
593    }
594}
595
596impl Parse for Input {
597    fn parse(input: ParseStream) -> syn::Result<Self> {
598        let item: Item = input.parse()?;
599        match item {
600            Item::Enum(e) => Self::parse_enum(e),
601            Item::Struct(s) => Self::parse_struct(s),
602            _ => Err(Error::new(item.span(), "expected enum or struct")),
603        }
604    }
605}