Skip to content

Commit

Permalink
new & improved Control macro is back
Browse files Browse the repository at this point in the history
  • Loading branch information
sowbug committed May 7, 2023
1 parent d31cf01 commit 9556d8d
Show file tree
Hide file tree
Showing 2 changed files with 280 additions and 1 deletion.
237 changes: 237 additions & 0 deletions proc-macros/src/control.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
use convert_case::{Case, Casing};
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use std::collections::HashSet;
use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Fields, Ident};

use crate::core_crate_name;

// TODO: see
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=03943d1dfbf41bd63878bfccb1c64670
// for an intriguing bit of code. Came from
// https://users.rust-lang.org/t/is-implementing-a-derive-macro-for-converting-nested-structs-to-flat-structs-possible/65839/3

pub(crate) fn impl_control_derive(input: TokenStream, primitives: &HashSet<Ident>) -> TokenStream {
TokenStream::from({
let input = parse_macro_input!(input as DeriveInput);
let generics = &input.generics;
let data = &input.data;
let struct_name = &input.ident;
let (_impl_generics, ty_generics, _where_clause) = generics.split_for_impl();
let core_crate = format_ident!("{}", core_crate_name());

// Code adapted from https://blog.turbo.fish/proc-macro-error-handling/
// Thank you!
let fields = match data {
Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) => &fields.named,
_ => panic!("this derive macro works only on structs with named fields"),
};
let attr_fields = fields.into_iter().fold(Vec::default(), |mut v, f| {
let attrs: Vec<_> = f
.attrs
.iter()
.filter(|attr| attr.path.is_ident("control"))
.collect();
if !attrs.is_empty() {
match &f.ty {
syn::Type::Path(t) => {
if let Some(ident) = t.path.get_ident() {
v.push((f.ident.as_ref().unwrap().clone(), ident.clone()));
}
}
_ => todo!(),
}
}
v
});

// FOO_SIZE = 3; self.foo contains 3 controlled fields
fn size_const_id(ident: &Ident) -> Ident {
format_ident!("{}_SIZE", ident.to_string().to_case(Case::UpperSnake))
}
// FOO_NAME = "foo"; self.foo is addressable by "foo"
fn name_const_id(ident: &Ident) -> Ident {
let ident_upper = ident.to_string().to_case(Case::UpperSnake);
format_ident!("{}_NAME", ident_upper)
}
// FOO_INDEX = 5; address the foo element with index 5 (and maybe higher if node)
fn index_const_id(ident: &Ident) -> Ident {
let ident_upper = ident.to_string().to_case(Case::UpperSnake);
format_ident!("{}_INDEX", ident_upper)
}
// FOO_RANGE_END = 7; address foo's flattened elements with indexes 5, 6, and 7.
fn index_range_end_const_id(ident: &Ident) -> Ident {
let ident_upper = ident.to_string().to_case(Case::UpperSnake);
format_ident!("{}_RANGE_END", ident_upper)
}

let mut size_const_ids = Vec::default();
let mut size_const_values = Vec::default();
attr_fields.iter().for_each(|(ident, ident_type)| {
let size_const_name = size_const_id(ident);
size_const_ids.push(size_const_name.clone());

if primitives.contains(ident_type) {
size_const_values.push(quote! { 1 });
} else {
size_const_values.push(quote! { #ident_type::STRUCT_SIZE });
}
});
let size_const_body = quote! {
#( const #size_const_ids: usize = #size_const_values; )*
};

let mut index_const_ids = Vec::default();
let mut index_const_range_end_ids = Vec::default();
let mut index_const_values = Vec::default();

// This loop calculates each field's index.
//
// Since proc macros don't have access to any other information than the
// struct TokenStream, we can't incorporate any sub-structure
// information (such as how big the field is) except by referring to
// consts. In other words, if Struct contains EmbeddedStruct, we can't
// ask how big EmbeddedStruct is, but we can refer to
// EmbeddedStruct::STRUCT_SIZE and let the compiler figure out that
// value during the build.
//
// Thus, a field's index will always be either (1) zero if it's the
// first, or (2) the index of the prior field + the size of the prior
// field. So we need to keep track of the prior field name, which
// enables us to build up the current value from the prior one.
let mut prior_ident: Option<&Ident> = None;
attr_fields.iter().for_each(|(ident, _)| {
index_const_ids.push(index_const_id(ident));
index_const_range_end_ids.push(index_range_end_const_id(ident));
if let Some(prior) = prior_ident {
let prior_index_const_name = index_const_id(prior);
let prior_size_const_name = size_const_id(prior);
index_const_values
.push(quote! { Self::#prior_index_const_name + Self::#prior_size_const_name });
} else {
index_const_values.push(quote! { 0 });
}
prior_ident = Some(ident);
});
let mut name_const_ids = Vec::default();
let mut name_const_values = Vec::default();
attr_fields.iter().for_each(|(ident, _)| {
let name_const = name_const_id(ident);
name_const_ids.push(name_const.clone());
name_const_values.push(ident.to_string().to_case(Case::Kebab));
});

let main_const_body = quote! {
#( pub const #index_const_ids: usize = #index_const_values; )*
#( pub const #name_const_ids: &str = #name_const_values; )*
};
let range_const_body = quote! {
#( pub const #index_const_range_end_ids: usize = #index_const_values + #size_const_values - 1; )*
};
let struct_size_const_body = quote! {
pub const STRUCT_SIZE: usize = 0 + #( Self::#size_const_ids )+* ;
};

let mut id_bodies = Vec::default();
let mut setter_bodies = Vec::default();
attr_fields.iter().for_each(|(ident, ident_type)| {
let id = ident.to_string().to_case(Case::Kebab);
if primitives.contains(ident_type) {
let name_const = format_ident!("set_{}", ident);
id_bodies.push(quote! {Some(#id.to_string())});
setter_bodies.push(quote! {self.#name_const(value.into());});
} else {
let field_index_name = index_const_id(ident);
let name_const = name_const_id(ident);
id_bodies.push(quote! { Some(format!("{}-{}", Self::#name_const, self.#ident.control_name_for_index(index - Self::#field_index_name).unwrap()))});
setter_bodies
.push(quote! {self.#ident.control_set_param_by_index(index - Self::#field_index_name, value);});
}
});
let control_name_for_index_body = quote! {
fn control_name_for_index(&self, index: usize) -> Option<String> {
match index {
#( Self::#index_const_ids..=Self::#index_const_range_end_ids => {#id_bodies} ),*
_ => {None},
}
}
};
let control_set_param_by_index_bodies = quote! {
fn control_set_param_by_index(&mut self, index: usize, value: #core_crate::control::F32ControlValue) {
match index {
#( Self::#index_const_ids..=Self::#index_const_range_end_ids => {#setter_bodies} ),*
_ => {},
}
}
};

// These need to be separate vecs because we divide the fields into
// groups of maybe different sizes, which is a repetitions no-no.
let mut leaf_names = Vec::default();
let mut leaf_indexes = Vec::default();
let mut node_names = Vec::default();
let mut node_indexes = Vec::default();
let mut node_fields = Vec::default();
let mut node_field_lens = Vec::default();
attr_fields.iter().for_each(|(ident, ident_type)| {
let const_name = name_const_id(ident);
let field_index_name = index_const_id(ident);
if primitives.contains(ident_type) {
leaf_names.push(quote! { Self::#const_name });
leaf_indexes.push(quote! { Self::#field_index_name });
} else {
node_names.push(quote! { Self::#const_name });
node_indexes.push(quote! { Self::#field_index_name });
node_fields.push(ident);
// Includes the dash at the end that separates the field parts in the ID
let node_field_len = ident.to_string().len() + 1;
node_field_lens.push(quote! {#node_field_len});
}
});
let control_index_for_name_body = quote! {
fn control_index_for_name(&self, name: &str) -> Option<usize> {
match name {
#( #leaf_names => Some(#leaf_indexes), )*
_ => {
#(
if name.starts_with(#node_names) {
if let Some(r) = self.#node_fields.control_index_for_name(&name[#node_field_lens..]) {
return Some(r + #node_indexes)
}
}
)*
None
},
}
}
};

let quote = quote! {
#[automatically_derived]
impl #generics #struct_name #ty_generics {
#size_const_body
#main_const_body
#range_const_body
#struct_size_const_body
}
#[automatically_derived]
impl #generics #core_crate::traits::Controllable for #struct_name #ty_generics {
fn control_index_count(&self) -> usize { Self::STRUCT_SIZE }
fn control_set_param_by_name(&mut self, name: &str, value: #core_crate::control::F32ControlValue) {
if let Some(index) = self.control_index_for_name(name) {
self.control_set_param_by_index(index, value);
} else {
eprintln!("Warning: couldn't set param named '{}'", name);
}
}
#control_name_for_index_body
#control_index_for_name_body
#control_set_param_by_index_bodies
}
};
quote
})
}
44 changes: 43 additions & 1 deletion proc-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

//! This crate provides macros that make Entity development easier.

use control::impl_control_derive;
use everything::parse_and_generate_everything;
use nano::impl_nano_derive;
use proc_macro::TokenStream;
use proc_macro_crate::crate_name;
use quote::{format_ident, quote};
use syn::{parse_macro_input, DeriveInput};
use std::collections::HashSet;
use syn::{parse_macro_input, DeriveInput, Ident};
use uid::impl_uid_derive;
use views::parse_and_generate_views;

mod control;
mod everything;
mod nano;
mod uid;
Expand Down Expand Up @@ -49,6 +52,45 @@ pub fn derive_views(input: TokenStream) -> TokenStream {
))
}

/// field types that don't recurse further for #[derive(Control)] purposes
fn make_primitives() -> HashSet<Ident> {
vec![
"BipolarNormal",
"FrequencyHz",
"Normal",
"ParameterType",
"Ratio",
"String",
"Waveform",
"bool",
"char",
"f32",
"f64",
"i128",
"i16",
"i32",
"i64",
"i8",
"u128",
"u16",
"u32",
"u64",
"u8",
"usize",
]
.into_iter()
.fold(HashSet::default(), |mut hs, e| {
hs.insert(format_ident!("{}", e));
hs
})
}

/// The [Control] macro derives the code that allows automation (one entity's output driving another entity's control).
#[proc_macro_derive(Control, attributes(control))]
pub fn derive_control(input: TokenStream) -> TokenStream {
impl_control_derive(input, &make_primitives())
}

// Some of the code generated in these macros uses the groove-core crate, but
// groove-core also uses this proc-macro lib. So we need to correct the
// reference to groove-core to sometimes be just `crate`.
Expand Down

0 comments on commit 9556d8d

Please sign in to comment.