-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
218 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
crates/synthizer_macros_internal/src/property_slots_impl.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
//! A macro to handle PropertyCommandReceiver, which we apply to XXXSlots structs. This does two things: | ||
//! | ||
//! - Implements PropertyCommandReceiver. | ||
//! - Creates a XXXProperties struct which is for the public API of a node. | ||
//! | ||
//! Input structs *must* be named with the Slots suffix, and will be sealed magically with this macro. See the structs | ||
//! in this file for input options. | ||
//! | ||
//! This is an attribute macro and not a derive because it must seal the struct, and so must have the ability to move | ||
//! the original definition. All struct-level options will go in the `property_slots` attribute (also the macro | ||
//! invocation) and (later, when we implement options for such), field options will be `slot`. | ||
//! | ||
//! To document properties, document them on the input struct. The docs are then copied over to the output and (in | ||
//! future) augmented with additional metadata such as ranges. | ||
//! | ||
//! It is strictly assumed that all fields of a slots struct are `PropertySlot<Marker>`. | ||
use darling::{FromDeriveInput, FromField}; | ||
use syn::parse::Parse; | ||
use syn::spanned::Spanned; | ||
|
||
// Note that we can get a DeriveInput from an attribute macro simply by ignoring the attribute token stream. | ||
|
||
#[derive(Debug, FromDeriveInput)] | ||
#[darling(supports(struct_named))] | ||
struct PropSlotsInput { | ||
ident: syn::Ident, | ||
data: darling::ast::Data<(), syn::Field>, | ||
} | ||
|
||
pub(crate) fn property_slots_impl( | ||
_attrs: proc_macro2::TokenStream, | ||
body: proc_macro2::TokenStream, | ||
) -> proc_macro2::TokenStream { | ||
let syn_input: syn::DeriveInput = match syn::parse2(body.clone()) { | ||
Ok(x) => x, | ||
Err(e) => proc_macro_error::abort_call_site!(e), | ||
}; | ||
|
||
let input = match PropSlotsInput::from_derive_input(&syn_input) { | ||
Ok(x) => x, | ||
Err(e) => { | ||
return e.write_errors().into(); | ||
} | ||
}; | ||
|
||
if !input.ident.to_string().ends_with("Slots") { | ||
proc_macro_error::abort!(input.ident, "Slot struct names must end with 'Slots'"); | ||
} | ||
|
||
// The input to this macro is already a complete slots struct; sep 1 is to seal it. | ||
let sealed_slots = crate::utils::seal(&input.ident, &body); | ||
|
||
// Step 2 is to generate a struct for the public API, of the form XxxProperties. We do this in three steps: | ||
// | ||
// 1. Figure out the identifier; | ||
// 2. Figure out the declaration; | ||
// 3. Figure out the PropertyCommandReceiver implementation. | ||
|
||
let mut public_name = input | ||
.ident | ||
.to_string() | ||
.strip_suffix("Slots") | ||
.expect("We validated that it ends with slots earlier") | ||
.to_string(); | ||
public_name.push_str("Props"); | ||
|
||
struct PropDef { | ||
name: syn::Ident, | ||
marker: syn::Type, | ||
} | ||
|
||
let mut fields: Vec<PropDef> = vec![]; | ||
|
||
for f in input | ||
.data | ||
.clone() | ||
.take_struct() | ||
.expect("Darling should validate this is a struct") | ||
.into_iter() | ||
{ | ||
let name = f.ident.unwrap(); | ||
let ty_path = match f.ty { | ||
syn::Type::Path(ref p) => p.clone(), | ||
_ => proc_macro_error::abort_call_site!( | ||
"Found a non-struct field in this struct. Fields should always be 'Slot<Marker>'" | ||
), | ||
}; | ||
|
||
let last_seg = match ty_path.path.segments.last() { | ||
Some(x) => x, | ||
None => { | ||
proc_macro_error::abort!( | ||
f.ty.span(), | ||
"This path has no segments; it should be of the form 'Slot<MarkerHere>'" | ||
); | ||
} | ||
}; | ||
|
||
// That last segment should have exactly one generic; that generic should be the path to a concrete marker type. | ||
let args = match last_seg.arguments { | ||
syn::PathArguments::AngleBracketed(ref a) => a.clone(), | ||
_ => proc_macro_error::abort!( | ||
last_seg.span(), | ||
"This should have some generics on it, but does not" | ||
), | ||
}; | ||
|
||
let args = args.args; | ||
|
||
if args.len() != 1 { | ||
proc_macro_error::abort!(args.span(), "This should have exactly one generic argument"); | ||
} | ||
|
||
let marker = match args[0] { | ||
syn::GenericArgument::Type(ref t) => t.clone(), | ||
_ => proc_macro_error::abort!(args[0].span(), "Thios should be a type"), | ||
}; | ||
|
||
fields.push(PropDef { name, marker }); | ||
} | ||
|
||
// Our generic lifetime is `'a`. | ||
let public_field_decls = fields | ||
.iter() | ||
.map(|f| { | ||
let name = &f.name; | ||
let marker = &f.marker; | ||
quote::quote!(pub(crate) #name: crate::properties::Property<'a, #marker>) | ||
}) | ||
.collect::<Vec<proc_macro2::TokenStream>>(); | ||
|
||
let pub_struct_decl = quote::quote!(pub struct #public_name<'a> { | ||
#(#public_field_decls),* | ||
}); | ||
|
||
|
||
unreachable!() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/// Given an identifier an some code, wrap the code in a "sealed" module: | ||
/// | ||
/// ```IGNORE | ||
/// mod xxx_sealed { | ||
/// use super::*; | ||
/// tokens here... | ||
/// } | ||
/// | ||
/// pub(crate) use xxx_sealed::*; | ||
/// ``` | ||
/// | ||
/// The sealed module name is derived from the identifier by converting it to snake case and adding _sealed. | ||
pub(crate) fn seal<T: quote::ToTokens>(ident: &syn::Ident, tokens: &T) -> proc_macro2::TokenStream { | ||
let cased = ident_case::RenameRule::SnakeCase.apply_to_field(ident.to_string()); | ||
let mod_token: syn::Ident = syn::parse_quote!(#cased); | ||
quote::quote!( | ||
mod #mod_token { | ||
use super::*; | ||
|
||
#tokens | ||
} | ||
|
||
pub(crate) use sealed::*; | ||
) | ||
} | ||
|
||
/// Builds a match statement like this: | ||
/// | ||
/// ```IGNORE | ||
/// match index { | ||
/// 0 => expr0, | ||
/// 1 => expr1, | ||
/// ... | ||
/// _ => panic!("Index out of bounds"), | ||
/// } | ||
/// ``` | ||
/// | ||
/// This is used to allow turning user-supplied indices into references to fields on heterogeneous types, primarily | ||
/// method calls, where we can't use arrays without overhead. This shows up in e.g. property slots, where propertis of | ||
/// different types have different concrete representations. | ||
fn build_indexing_match<I: Iterator<Item = impl quote::ToTokens>>( | ||
index: syn::Expr, | ||
items: I, | ||
) -> syn::ExprMatch { | ||
let arms = items | ||
.enumerate() | ||
.map(|(index, expr)| quote::quote!(#index => #expr)) | ||
.collect::<Vec<_>>(); | ||
let arm_count = arms.len(); | ||
let index_error = if arm_count > 0 { | ||
format!("Index {{}} must not be over {}", arm_count - 1) | ||
} else { | ||
"Got index {}, but no indices are accepted here; this is expected to be dead code" | ||
.to_string() | ||
}; | ||
|
||
syn::parse_quote!(match #index { | ||
#(#arms),* | ||
_ => panic!(#index_error, #index), | ||
}) | ||
} |