dunge-macro 0.3.10

Procmacro for the dunge library
Documentation
use {
    crate::member,
    proc_macro2::{Span, TokenStream},
    syn::{Data, DataStruct, DeriveInput, Fields, GenericParam, Ident, Lifetime, spanned::Spanned},
};

pub(crate) fn derive(input: DeriveInput) -> TokenStream {
    use std::iter;

    let Data::Struct(DataStruct { fields, .. }) = input.data else {
        return quote::quote_spanned! { input.ident.span() =>
            ::std::compile_error!("the group type must be a struct");
        };
    };

    let named = match &fields {
        Fields::Named(_) => true,
        Fields::Unnamed(_) => false,
        Fields::Unit => {
            return quote::quote_spanned! { input.ident.span() =>
                ::std::compile_error!("the group type cannot be a unit struct");
            };
        }
    };

    let mut lts = Vec::with_capacity(input.generics.params.len());
    for param in input.generics.params {
        let GenericParam::Lifetime(param) = param else {
            return quote::quote_spanned! { param.span() =>
                ::std::compile_error!("the group struct cannot have non-lifetime generic parameters");
            };
        };

        if !param.attrs.is_empty() {
            return quote::quote_spanned! { param.span() =>
                ::std::compile_error!("the lifetime cannot have any attributes");
            };
        }

        if !param.bounds.is_empty() {
            return quote::quote_spanned! { param.span() =>
                ::std::compile_error!("the lifetime cannot have any bounds");
            };
        }

        lts.push(param.lifetime);
    }

    if fields.is_empty() {
        return quote::quote_spanned! { fields.span() =>
            ::std::compile_error!("the group struct must have some fields");
        };
    }

    let static_lt = Lifetime {
        apostrophe: Span::call_site(),
        ident: Ident::new("static", Span::call_site()),
    };

    let static_lts = lts.iter().map(|_| &static_lt);
    let anon_lt = Lifetime {
        apostrophe: Span::call_site(),
        ident: Ident::new("_", Span::call_site()),
    };

    let anon_lts = lts
        .iter()
        .map(|lt| if lt.ident == "static" { lt } else { &anon_lt });

    let name = input.ident;
    let projection_name = quote::format_ident!("{name}Proj");
    let group_types = fields.iter().map(|field| {
        let ty = &field.ty;
        quote::quote! { <#ty as dunge::group::MemberProjection>::MEMBER }
    });

    let n_members = fields.len();
    let group_visit_members = iter::zip(0.., &fields).map(|(index, field)| {
        let ident = member::make(index, field.ident.clone());
        quote::quote! { dunge::set::Visit::visit(&self.#ident, visitor) }
    });

    let group_fields = iter::zip(0.., &fields).map(|(index, field)| {
        let vis = &field.vis;
        let ident = member::make(index, field.ident.clone());
        let ty = &field.ty;
        if named {
            quote::quote! { #vis #ident: <#ty as dunge::group::MemberProjection>::Field }
        } else {
            quote::quote! { #vis <#ty as dunge::group::MemberProjection>::Field }
        }
    });

    let group_member_projections = iter::zip(0.., &fields).map(|(index, field)| {
        let ident = member::make(index, field.ident.clone());
        let ty = &field.ty;
        quote::quote! { #ident: <#ty as dunge::group::MemberProjection>::member_projection(id, #index, out.clone()) }
    });

    let projection = if named {
        quote::quote! {
            pub struct #projection_name<#(#lts),*> {
                #(#group_fields),*,
            }
        }
    } else {
        quote::quote! {
            pub struct #projection_name<#(#lts),*>(
                #(#group_fields),*,
            );
        }
    };

    quote::quote! {
        impl<#(#lts),*> dunge::Group for #name<#(#lts),*> {
            type Projection = #projection_name<#(#static_lts),*>;
            const DEF: dunge::sl::Define<dunge::types::MemberData> = dunge::sl::Define::new(&[
                #(#group_types),*,
            ]);
        }

        impl dunge::set::Visit for #name<#(#anon_lts),*> {
            const N_MEMBERS: ::core::primitive::usize = #n_members;
            fn visit<'visit>(&'visit self, visitor: &mut dunge::set::Visitor<'visit>) {
                #(#group_visit_members);*;
            }
        }

        #projection

        impl<#(#lts),*> dunge::group::Projection for #projection_name<#(#lts),*> {
            fn projection(id: ::core::primitive::u32, out: dunge::sl::GlobalOut) -> Self {
                Self {
                    #(#group_member_projections),*,
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn derive_group() {
        let input = quote::quote! {
            struct Map<'tx> {
                pub tex: BoundTexture,
                pub sam: &'tx Sampler,
            }
        };

        let input = syn::parse2(input).expect("parse input");
        let actual = derive(input);
        let expected = quote::quote! {
            impl<'tx> dunge::Group for Map<'tx> {
                type Projection = MapProj<'static>;
                const DEF: dunge::sl::Define<dunge::types::MemberData> = dunge::sl::Define::new(&[
                    <BoundTexture as dunge::group::MemberProjection>::MEMBER,
                    <&'tx Sampler as dunge::group::MemberProjection>::MEMBER,
                ]);
            }

            impl dunge::set::Visit for Map<'_> {
                const N_MEMBERS: ::core::primitive::usize = 2usize;
                fn visit<'visit>(&'visit self, visitor: &mut dunge::set::Visitor<'visit>) {
                    dunge::set::Visit::visit(&self.tex, visitor);
                    dunge::set::Visit::visit(&self.sam, visitor);
                }
            }

            pub struct MapProj<'tx> {
                pub tex: <BoundTexture as dunge::group::MemberProjection>::Field,
                pub sam: <&'tx Sampler as dunge::group::MemberProjection>::Field,
            }

            impl<'tx> dunge::group::Projection for MapProj<'tx> {
                fn projection(id: ::core::primitive::u32, out: dunge::sl::GlobalOut) -> Self {
                    Self {
                        tex: <BoundTexture as dunge::group::MemberProjection>::member_projection(id, 0u32, out.clone()),
                        sam: <&'tx Sampler as dunge::group::MemberProjection>::member_projection(id, 1u32, out.clone()),
                    }
                }
            }
        };

        helpers::eq_lines(&actual.to_string(), &expected.to_string());
    }

    #[test]
    fn derive_tuple_group() {
        let input = quote::quote! {
            struct Map<'tx>(BoundTexture, &'tx Sampler);
        };

        let input = syn::parse2(input).expect("parse input");
        let actual = derive(input);
        let expected = quote::quote! {
            impl<'tx> dunge::Group for Map<'tx> {
                type Projection = MapProj<'static>;
                const DEF: dunge::sl::Define<dunge::types::MemberData> = dunge::sl::Define::new(&[
                    <BoundTexture as dunge::group::MemberProjection>::MEMBER,
                    <&'tx Sampler as dunge::group::MemberProjection>::MEMBER,
                ]);
            }

            impl dunge::set::Visit for Map<'_> {
                const N_MEMBERS: ::core::primitive::usize = 2usize;
                fn visit<'visit>(&'visit self, visitor: &mut dunge::set::Visitor<'visit>) {
                    dunge::set::Visit::visit(&self.0, visitor);
                    dunge::set::Visit::visit(&self.1, visitor);
                }
            }

            pub struct MapProj<'tx>(
                <BoundTexture as dunge::group::MemberProjection>::Field,
                <&'tx Sampler as dunge::group::MemberProjection>::Field,
            );

            impl<'tx> dunge::group::Projection for MapProj<'tx> {
                fn projection(id: ::core::primitive::u32, out: dunge::sl::GlobalOut) -> Self {
                    Self {
                        0: <BoundTexture as dunge::group::MemberProjection>::member_projection(id, 0u32, out.clone()),
                        1: <&'tx Sampler as dunge::group::MemberProjection>::member_projection(id, 1u32, out.clone()),
                    }
                }
            }
        };

        helpers::eq_lines(&actual.to_string(), &expected.to_string());
    }
}