Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ahicks92 committed Oct 22, 2023
1 parent 744e4ff commit 7a0afa5
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ derive_more = "0.99.17"
enum_dispatch = "0.3.12"
env_logger = "0.10.0"
eye_dropper = { path = "crates/eye_dropper" }
ident_case = "1.0.1"
im = "15.1.0"
itertools = "0.10.5"
lazy_static = "1.4.0"
Expand Down
2 changes: 2 additions & 0 deletions crates/synthizer_macros_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ proc-macro = true

[dependencies]
darling.workspace = true
ident_case.workspace = true
proc-macro2.workspace = true
proc-macro-error.workspace = true
quote.workspace = true
syn.workspace = true
14 changes: 14 additions & 0 deletions crates/synthizer_macros_internal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,17 @@
//! lot of boilerplate abstraction, for example understanding the concept of a property and building node descriptors.
//! Rather than type this tens of times and refactor them all tens of times per change, we have this crate to help us
//! out.
//!
//! This crate contains dummy forwarders at the root to let us split it into files. See the individual modules for docs
//! on the macro. Procmacro limitations currently require that they be at the root.
mod property_slots_impl;
mod utils;

#[proc_macro_attribute]
#[proc_macro_error::proc_macro_error]
pub fn property_slots(
attrs: proc_macro::TokenStream,
body: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
property_slots_impl::property_slots_impl(attrs.into(), body.into()).into()
}
138 changes: 138 additions & 0 deletions crates/synthizer_macros_internal/src/property_slots_impl.rs
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!()
}
61 changes: 61 additions & 0 deletions crates/synthizer_macros_internal/src/utils.rs
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),
})
}

0 comments on commit 7a0afa5

Please sign in to comment.