web-glitz-macros 0.2.0

Procedural macros for the web-glitz crate.
Documentation
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::{Attribute, Data, DeriveInput, Field, Ident, Lit, Meta, NestedMeta, Type};

use crate::util::ErrorLog;

pub fn expand_derive_vertex(input: &DeriveInput) -> Result<TokenStream, String> {
    if let Data::Struct(ref data) = input.data {
        let struct_name = &input.ident;
        let mod_path = quote!(web_glitz::pipeline::graphics);
        let mut log = ErrorLog::new();

        let mut position = 0;
        let mut vertex_attributes = Vec::new();

        for field in data.fields.iter() {
            if let VertexField::Attribute(attr) = VertexField::from_ast(field, position, &mut log) {
                vertex_attributes.push(attr);
            }

            position += 1;
        }

        let recurse = vertex_attributes.iter().map(|a| {
            let field_name = a
                .ident
                .clone()
                .map(|i| i.into_token_stream())
                .unwrap_or(a.position.into_token_stream());
            let location = a.location as u32;
            let ty = &a.ty;
            let span = a.span;
            let format_kind = {
                let ident = Ident::new(a.format.as_str(), Span::call_site()).into_token_stream();

                quote_spanned!(span=> {
                    assert_format_compatible::<#ty, #ident>();

                    <#ident as VertexAttributeFormatIdentifier>::FORMAT
                })
            };

            quote! {
                #mod_path::VertexAttributeDescriptor {
                    location: #location,
                    format: #format_kind,
                    offset_in_bytes: web_glitz::offset_of!(#struct_name, #field_name) as u8
                }
            }
        });

        let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

        let impl_block = quote! {

            #[automatically_derived]
            unsafe impl #impl_generics #mod_path::Vertex for #struct_name #ty_generics #where_clause {
                const ATTRIBUTE_DESCRIPTORS: &'static [#mod_path::VertexAttributeDescriptor] =
                    &[
                        #(#recurse),*
                    ];
            }
        };

        let suffix = struct_name.to_string().trim_start_matches("r#").to_owned();
        let dummy_const = Ident::new(&format!("_IMPL_VERTEX_FOR_{}", suffix), Span::call_site());

        let generated = quote! {
            #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
            const #dummy_const: () = {
                #[allow(unknown_lints)]
                #[cfg_attr(feature = "cargo-clippy", allow(useless_attribute))]
                #[allow(rust_2018_idioms)]
                use #mod_path::attribute_format::*;

                const fn assert_format_compatible<T, F>()
                where
                    T: #mod_path::attribute_format::VertexAttributeFormatCompatible<F>,
                    F: #mod_path::attribute_format::VertexAttributeFormatIdentifier
                {}

                #impl_block
            };
        };

        log.compile().map(|_| generated)
    } else {
        Err("`Vertex` can only be derived for a struct.".into())
    }
}

enum VertexField {
    Attribute(AttributeField),
    Excluded,
}

impl VertexField {
    pub fn from_ast(ast: &Field, position: usize, log: &mut ErrorLog) -> Self {
        let vertex_attributes: Vec<&Attribute> = ast
            .attrs
            .iter()
            .filter(|a| is_vertex_attribute(a))
            .collect();
        let field_name = ast
            .ident
            .clone()
            .map(|i| i.to_string())
            .unwrap_or(position.to_string());

        match vertex_attributes.len() {
            0 => VertexField::Excluded,
            1 => {
                let attr = vertex_attributes[0];

                let meta_items: Vec<NestedMeta> = match attr.parse_meta() {
                    Ok(Meta::List(meta)) => meta.nested.iter().cloned().collect(),
                    Ok(Meta::Path(path)) if path.is_ident("vertex_attribute") => Vec::new(),
                    _ => {
                        log.log_error(format!(
                            "Malformed #[vertex_attribute] attribute for field `{}`.",
                            field_name
                        ));

                        Vec::new()
                    }
                };

                let mut location = None;
                let mut format = None;

                for meta_item in meta_items.into_iter() {
                    match meta_item {
                        NestedMeta::Meta(Meta::NameValue(m)) if m.path.is_ident("location") => {
                            if let Lit::Int(i) = &m.lit {
                                if let Ok(value) = i.base10_parse::<u32>() {
                                    location = Some(value);
                                } else {
                                    log.log_error(format!(
                                        "Malformed #[vertex_attribute] attribute for field `{}`: \
                                        expected `location` to be representable as a u32.",
                                        field_name
                                    ));
                                }
                            } else {
                                log.log_error(format!(
                                    "Malformed #[vertex_attribute] attribute for field `{}`: \
                                     expected `location` to be a positive integer.",
                                    field_name
                                ));
                            };
                        }
                        NestedMeta::Meta(Meta::NameValue(m)) if m.path.is_ident("format") => {
                            if let Lit::Str(f) = &m.lit {
                                format = Some(f.value());
                            } else {
                                log.log_error(format!(
                                    "Malformed #[vertex_attribute] attribute for field `{}`: \
                                     expected `format` to be a string.",
                                    field_name
                                ));
                            };
                        }
                        _ => log.log_error(format!(
                            "Malformed #[vertex_attribute] attribute for field `{}`: unrecognized \
                             option `{}`.",
                            field_name,
                            meta_item.into_token_stream()
                        )),
                    }
                }

                if location.is_none() {
                    log.log_error(format!(
                        "Field `{}` is marked a vertex attribute, but does not declare a binding \
                         location.",
                        field_name
                    ));
                }

                if format.is_none() {
                    log.log_error(format!(
                        "Field `{}` is marked a vertex attribute, but does not declare a format.",
                        field_name
                    ));
                }

                if location.is_some() && format.is_some() {
                    let location = location.unwrap();
                    let format = format.unwrap();

                    VertexField::Attribute(AttributeField {
                        ident: ast.ident.clone(),
                        ty: ast.ty.clone(),
                        position,
                        location,
                        format,
                        span: ast.span(),
                    })
                } else {
                    VertexField::Excluded
                }
            }
            _ => {
                log.log_error(format!(
                    "#[vertex_attribute] must not be defined more than once for field `{}`.",
                    field_name
                ));

                VertexField::Excluded
            }
        }
    }
}

struct AttributeField {
    ident: Option<Ident>,
    ty: Type,
    position: usize,
    location: u32,
    format: String,
    span: Span,
}

fn is_vertex_attribute(attribute: &Attribute) -> bool {
    attribute.path.segments[0].ident == "vertex_attribute"
}