summaryrefslogtreecommitdiff
path: root/macros/src/lib.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-12-16 14:43:02 +0100
committerLaurenz <laurmaedje@gmail.com>2021-12-16 14:43:02 +0100
commit958f74f77707340f34ee36d09492bdb74523aa2a (patch)
tree4ab59a7a532c8023a5e8bb4c9a6090886cb4e538 /macros/src/lib.rs
parent2a3d0f4b390457174ed09347dd29e97ff9a783e4 (diff)
Set Rules Episode VIII: The First Macro
Diffstat (limited to 'macros/src/lib.rs')
-rw-r--r--macros/src/lib.rs171
1 files changed, 171 insertions, 0 deletions
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
new file mode 100644
index 00000000..3b1d0548
--- /dev/null
+++ b/macros/src/lib.rs
@@ -0,0 +1,171 @@
+extern crate proc_macro;
+
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::parse_quote;
+use syn::spanned::Spanned;
+use syn::{Error, Result};
+
+/// Generate node properties.
+#[proc_macro_attribute]
+pub fn properties(_: TokenStream, item: TokenStream) -> TokenStream {
+ let impl_block = syn::parse_macro_input!(item as syn::ItemImpl);
+ expand(impl_block).unwrap_or_else(|err| err.to_compile_error()).into()
+}
+
+/// Expand a property impl block for a node.
+fn expand(mut impl_block: syn::ItemImpl) -> Result<TokenStream2> {
+ // Split the node type into name and generic type arguments.
+ let (self_name, self_args) = parse_self(&*impl_block.self_ty)?;
+
+ // Rewrite the const items from values to keys.
+ let mut style_ids = vec![];
+ let mut modules = vec![];
+ for item in &mut impl_block.items {
+ if let syn::ImplItem::Const(item) = item {
+ let (style_id, module) = process_const(item, &self_name, &self_args)?;
+ style_ids.push(style_id);
+ modules.push(module);
+ }
+ }
+
+ // Here, we use the collected `style_ids` to provide a function that checks
+ // whether a property belongs to the node.
+ impl_block.items.insert(0, parse_quote! {
+ /// Check whether the property with the given type id belongs to `Self`.
+ pub fn has_property(id: StyleId) -> bool {
+ [#(#style_ids),*].contains(&id)
+ }
+ });
+
+ // Put everything into a module with a hopefully unique type to isolate
+ // it from the outside.
+ let module = quote::format_ident!("{}_types", self_name);
+ Ok(quote! {
+ #[allow(non_snake_case)]
+ mod #module {
+ use std::marker::PhantomData;
+ use once_cell::sync::Lazy;
+ use crate::eval::{Property, StyleId};
+ use super::*;
+
+ #impl_block
+ #(#modules)*
+ }
+ })
+}
+
+/// Parse the name and generic type arguments of the node type.
+fn parse_self(self_ty: &syn::Type) -> Result<(String, Vec<&syn::Type>)> {
+ // Extract the node type for which we want to generate properties.
+ let path = match self_ty {
+ syn::Type::Path(path) => path,
+ ty => return Err(Error::new(ty.span(), "must be a path type")),
+ };
+
+ // Split up the type into its name and its generic type arguments.
+ let last = path.path.segments.last().unwrap();
+ let self_name = last.ident.to_string();
+ let self_args = match &last.arguments {
+ syn::PathArguments::AngleBracketed(args) => args
+ .args
+ .iter()
+ .filter_map(|arg| match arg {
+ syn::GenericArgument::Type(ty) => Some(ty),
+ _ => None,
+ })
+ .collect(),
+ _ => vec![],
+ };
+
+ Ok((self_name, self_args))
+}
+
+/// Process a single const item.
+fn process_const(
+ item: &mut syn::ImplItemConst,
+ self_name: &str,
+ self_args: &[&syn::Type],
+) -> Result<(syn::Expr, syn::ItemMod)> {
+ // The type of the property's value is what the user of our macro wrote
+ // as type of the const ...
+ let value_ty = &item.ty;
+
+ // ... but the real type of the const becomes Key<#key_param>.
+ let key_param = if self_args.is_empty() {
+ quote! { #value_ty }
+ } else {
+ quote! { (#value_ty, #(#self_args),*) }
+ };
+
+ // The display name, e.g. `TextNode::STRONG`.
+ let name = format!("{}::{}", self_name, &item.ident);
+
+ // The default value of the property is what the user wrote as
+ // initialization value of the const.
+ let default = &item.expr;
+
+ // Look for a folding function like `#[fold(u64::add)]`.
+ let mut combinator = None;
+ for attr in &item.attrs {
+ if attr.path.is_ident("fold") {
+ let fold: syn::Expr = attr.parse_args()?;
+ combinator = Some(quote! {
+ fn combine(inner: Self::Value, outer: Self::Value) -> Self::Value {
+ let f: fn(Self::Value, Self::Value) -> Self::Value = #fold;
+ f(inner, outer)
+ }
+ });
+ }
+ }
+
+ // The implementation of the `Property` trait.
+ let property_impl = quote! {
+ impl<T: 'static> Property for Key<T> {
+ type Value = #value_ty;
+
+ const NAME: &'static str = #name;
+
+ fn default() -> Self::Value {
+ #default
+ }
+
+ fn default_ref() -> &'static Self::Value {
+ static LAZY: Lazy<#value_ty> = Lazy::new(|| #default);
+ &*LAZY
+ }
+
+ #combinator
+ }
+ };
+
+ // The module that will contain the `Key` type.
+ let module_name = &item.ident;
+
+ // Generate the style id and module code.
+ let style_id = parse_quote! { StyleId::of::<#module_name::Key<#key_param>>() };
+ let module = parse_quote! {
+ #[allow(non_snake_case)]
+ mod #module_name {
+ use super::*;
+
+ pub struct Key<T>(pub PhantomData<T>);
+ impl<T> Copy for Key<T> {}
+ impl<T> Clone for Key<T> {
+ fn clone(&self) -> Self {
+ *self
+ }
+ }
+
+ #property_impl
+ }
+ };
+
+ // Replace type and initializer expression with the `Key`.
+ item.attrs.retain(|attr| !attr.path.is_ident("fold"));
+ item.ty = parse_quote! { #module_name::Key<#key_param> };
+ item.expr = parse_quote! { #module_name::Key(PhantomData) };
+
+ Ok((style_id, module))
+}