diff options
author | Martin Fischer <martin@push-f.com> | 2021-11-15 10:29:52 +0100 |
---|---|---|
committer | Martin Fischer <martin@push-f.com> | 2021-11-18 23:36:01 +0100 |
commit | 2a8a0601afcb82d90d0766db5a954b70b10f856d (patch) | |
tree | 0271062335d450e151598d4ad9aa327ffa0dfaea /src/lib.rs |
publishv0.1.0
Diffstat (limited to 'src/lib.rs')
-rw-r--r-- | src/lib.rs | 425 |
1 files changed, 425 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1f0432c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,425 @@ +#![doc = include_str!("../README.md")] +use std::collections::HashMap; + +use proc_macro::TokenStream; +use proc_macro2::Group; +use proc_macro2::Ident; +use quote::format_ident; +use quote::quote; +use quote::quote_spanned; +use quote::ToTokens; +use syn::parse_macro_input; +use syn::punctuated::Punctuated; +use syn::token::Brace; +use syn::token::Gt; +use syn::token::Lt; +use syn::token::Trait; +use syn::AngleBracketedGenericArguments; +use syn::Block; +use syn::Expr; +use syn::GenericArgument; +use syn::GenericParam; +use syn::ImplItemMethod; +use syn::ItemTrait; +use syn::Path; +use syn::PathArguments; +use syn::PathSegment; +use syn::Signature; +use syn::Stmt; +use syn::TraitBound; +use syn::TraitItem; +use syn::TraitItemMethod; +use syn::Type; +use syn::TypeParam; +use syn::TypeParamBound; +use syn::TypePath; +use syn::Visibility; +use syn_utils::TypeMatcher; + +use crate::parse_assoc_type::parse_assoc_type; +use crate::parse_assoc_type::AssocTypeParseError; +use crate::parse_trait_sig::parse_trait_signature; +use crate::parse_trait_sig::MethodParseError; +use crate::parse_trait_sig::SignatureChanges; +use crate::parse_trait_sig::TypeTransform; +mod parse_assoc_type; +mod parse_trait_sig; +mod syn_utils; + +macro_rules! abort { + ($span:expr, $message:literal) => { + quote_spanned! {$span => compile_error!($message);}.into() + }; +} + +#[proc_macro_attribute] +pub fn dynamize(_attr: TokenStream, input: TokenStream) -> TokenStream { + let mut original_trait = parse_macro_input!(input as ItemTrait); + assert!(original_trait.auto_token.is_none()); + + let original_trait_name = original_trait.ident.clone(); + + let mut objectifiable_methods: Vec<(Signature, SignatureChanges)> = Vec::new(); + + let mut assoc_type_conversions: HashMap<&Ident, &Type> = HashMap::new(); + + for item in &original_trait.items { + if let TraitItem::Type(assoc_type) = item { + match parse_assoc_type(assoc_type) { + Err((_, AssocTypeParseError::NoIntoBound)) => continue, + Err((span, AssocTypeParseError::AssocTypeInBound)) => { + return abort!(span, "dynamize does not support associated types here") + } + Err((span, AssocTypeParseError::GenericAssociatedType)) => { + return abort!( + span, + "dynamize does not (yet?) support generic associated types" + ) + } + Ok((ident, type_)) => { + assoc_type_conversions.insert(ident, type_); + } + } + } + } + + for item in &original_trait.items { + if let TraitItem::Method(method) = item { + let mut signature = method.sig.clone(); + match parse_trait_signature(&mut signature, &assoc_type_conversions) { + Err((_, MethodParseError::NonDispatchableMethod)) => continue, + Err((span, MethodParseError::UnconvertibleAssocType)) => { + return abort!( + span, + "associated type is either undefined or doesn't have an Into bound" + ) + } + Err((span, MethodParseError::AssocTypeInInputs)) => { + return abort!( + span, + "dynamize does not support associated types in parameter types" + ) + } + Err(( + span, + MethodParseError::AssocTypeInUnsupportedReturnType + | MethodParseError::UnconvertibleAssocTypeInFnInput, + )) => return abort!(span, "dynamize does not know how to convert this type"), + Err((span, MethodParseError::UnconvertibleAssocTypeInTraitBound)) => { + return abort!(span, "dynamize does not support associated types here") + } + Err((span, MethodParseError::ImplTraitInInputs)) => { + return abort!( + span, + "dynamize does not support impl here, change it to a method generic" + ) + } + Ok(parsed_method) => objectifiable_methods.push((signature, parsed_method)), + }; + } + } + + let mut method_impls: Vec<ImplItemMethod> = Vec::new(); + + let mut blanket_impl_attrs = Vec::new(); + let mut dyn_trait_attrs = Vec::new(); + + // FUTURE: use Vec::drain_filter once it's stable + let mut i = 0; + while i < original_trait.attrs.len() { + if original_trait.attrs[i].path.is_ident("blanket_impl_attr") { + let attr = original_trait.attrs.remove(i); + let group: Group = match syn::parse2(attr.tokens) { + Ok(g) => g, + Err(err) => { + return abort!( + err.span(), + "expected parenthesis: #[blanket_impl_attr(...)]" + ) + } + }; + let tokens = group.stream(); + blanket_impl_attrs.push(quote! {#[#tokens]}); + } else if original_trait.attrs[i].path.is_ident("dyn_trait_attr") { + let attr = original_trait.attrs.remove(i); + let group: Group = match syn::parse2(attr.tokens) { + Ok(g) => g, + Err(err) => { + return abort!(err.span(), "expected parenthesis: #[dyn_trait_attr(...)]") + } + }; + let tokens = group.stream(); + dyn_trait_attrs.push(quote! {#[#tokens]}); + } else { + i += 1; + } + } + + let mut dyn_trait = ItemTrait { + ident: format_ident!("Dyn{}", original_trait.ident), + + attrs: Vec::new(), + vis: original_trait.vis.clone(), + unsafety: original_trait.unsafety, + auto_token: None, + trait_token: Trait::default(), + generics: original_trait.generics.clone(), + colon_token: None, + supertraits: Punctuated::new(), + brace_token: Brace::default(), + items: Vec::new(), + }; + + for (signature, parsed_method) in objectifiable_methods { + let mut new_method = TraitItemMethod { + attrs: Vec::new(), + sig: signature, + default: None, + semi_token: None, + }; + + let fun_name = &new_method.sig.ident; + + let args = new_method + .sig + .inputs + .iter() + .enumerate() + .map(|(idx, arg)| match arg { + syn::FnArg::Receiver(_) => quote! {self}, + syn::FnArg::Typed(pat_type) => match pat_type.pat.as_ref() { + syn::Pat::Ident(ident) => match &parsed_method.inputs[idx] { + None => ident.ident.to_token_stream(), + Some(transforms) => { + let args = (0..transforms.len()).map(|i| format_ident!("a{}", i)); + let mut calls: Vec<_> = + args.clone().map(|i| i.into_token_stream()).collect(); + for i in 0..calls.len() { + transforms[i].append_conversion(&mut calls[i]); + } + let move_opt = new_method.sig.asyncness.map(|_| quote! {move}); + quote!(#move_opt |#(#args),*| #ident(#(#calls),*)) + } + }, + _other => { + panic!("unexpected"); + } + }, + }); + + // in order for a trait to be object-safe its methods may not have + // generics so we convert method generics into trait generics + if new_method + .sig + .generics + .params + .iter() + .any(|p| matches!(p, GenericParam::Type(_))) + { + // syn::punctuated::Punctuated doesn't have a remove(index) + // method so we firstly move all elements to a vector + let mut params = Vec::new(); + while let Some(generic_param) = new_method.sig.generics.params.pop() { + params.push(generic_param.into_value()); + } + + // FUTURE: use Vec::drain_filter once it's stable + let mut i = 0; + while i < params.len() { + if matches!(params[i], GenericParam::Type(_)) { + dyn_trait.generics.params.push(params.remove(i)); + } else { + i += 1; + } + } + + new_method.sig.generics.params.extend(params); + + if dyn_trait.generics.lt_token.is_none() { + dyn_trait.generics.lt_token = Some(Lt::default()); + dyn_trait.generics.gt_token = Some(Gt::default()); + } + } + + let mut expr = quote!(#original_trait_name::#fun_name(#(#args),*)); + if new_method.sig.asyncness.is_some() { + expr.extend(quote! {.await}) + } + parsed_method.return_type.append_conversion(&mut expr); + + method_impls.push(ImplItemMethod { + attrs: Vec::new(), + vis: Visibility::Inherited, + defaultness: None, + sig: new_method.sig.clone(), + block: Block { + brace_token: Brace::default(), + stmts: vec![Stmt::Expr(Expr::Verbatim(expr))], + }, + }); + dyn_trait.items.push(new_method.into()); + } + + let blanket_impl = generate_blanket_impl(&dyn_trait, &original_trait, &method_impls); + + let expanded = quote! { + #original_trait + + #(#dyn_trait_attrs)* + #dyn_trait + + #(#blanket_impl_attrs)* + #blanket_impl + }; + TokenStream::from(expanded) +} + +fn generate_blanket_impl( + dyn_trait: &ItemTrait, + original_trait: &ItemTrait, + method_impls: &[ImplItemMethod], +) -> proc_macro2::TokenStream { + let mut blanket_generics = dyn_trait.generics.clone(); + let some_ident = format_ident!("__to_be_dynamized"); + blanket_generics.params.push(GenericParam::Type(TypeParam { + attrs: Vec::new(), + ident: some_ident.clone(), + colon_token: None, + bounds: std::iter::once(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: syn::TraitBoundModifier::None, + lifetimes: None, + path: Path { + leading_colon: None, + segments: std::iter::once(path_segment_for_trait(original_trait)).collect(), + }, + })) + .collect(), + eq_token: None, + default: None, + })); + let (_, type_gen, _where) = dyn_trait.generics.split_for_impl(); + let dyn_trait_name = &dyn_trait.ident; + quote! { + impl #blanket_generics #dyn_trait_name #type_gen for #some_ident { + #(#method_impls)* + } + } +} + +struct AssocTypeMatcher; +impl TypeMatcher<Path> for AssocTypeMatcher { + fn match_path<'a>(&self, path: &'a Path) -> Option<&'a Path> { + if path.segments.first().unwrap().ident == "Self" { + return Some(path); + } + None + } +} + +impl<TypeMatch, PathMatch, T> TypeMatcher<T> for (TypeMatch, PathMatch) +where + TypeMatch: Fn(&Type) -> Option<&T>, + PathMatch: Fn(&Path) -> Option<&T>, +{ + fn match_type<'a>(&self, t: &'a Type) -> Option<&'a T> { + self.0(t) + } + + fn match_path<'a>(&self, path: &'a Path) -> Option<&'a T> { + self.1(path) + } +} + +impl TypeTransform { + fn append_conversion(&self, stream: &mut proc_macro2::TokenStream) { + match self { + TypeTransform::Into => stream.extend(quote! {.into()}), + TypeTransform::Map(opt) => { + let mut inner = quote!(x); + opt.append_conversion(&mut inner); + stream.extend(quote! {.map(|x| #inner)}) + } + TypeTransform::Result(ok, err) => { + if !matches!(ok.as_ref(), TypeTransform::NoOp) { + let mut inner = quote!(x); + ok.append_conversion(&mut inner); + stream.extend(quote! {.map(|x| #inner)}) + } + if !matches!(err.as_ref(), TypeTransform::NoOp) { + let mut inner = quote!(x); + err.append_conversion(&mut inner); + stream.extend(quote! {.map_err(|x| #inner)}) + } + } + _other => {} + } + } +} + +/// Just a convenience trait for us to avoid match/if-let blocks everywhere. +trait As<T> { + fn get_as(&self) -> Option<&T>; + fn get_as_mut(&mut self) -> Option<&mut T>; +} + +impl As<AngleBracketedGenericArguments> for PathArguments { + fn get_as(&self) -> Option<&AngleBracketedGenericArguments> { + match self { + PathArguments::AngleBracketed(args) => Some(args), + _other => None, + } + } + fn get_as_mut(&mut self) -> Option<&mut AngleBracketedGenericArguments> { + match self { + PathArguments::AngleBracketed(args) => Some(args), + _other => None, + } + } +} + +impl As<Type> for GenericArgument { + fn get_as(&self) -> Option<&Type> { + match self { + GenericArgument::Type(typearg) => Some(typearg), + _other => None, + } + } + fn get_as_mut(&mut self) -> Option<&mut Type> { + match self { + GenericArgument::Type(typearg) => Some(typearg), + _other => None, + } + } +} + +fn path_segment_for_trait(sometrait: &ItemTrait) -> PathSegment { + PathSegment { + ident: sometrait.ident.clone(), + arguments: match sometrait.generics.params.is_empty() { + true => PathArguments::None, + false => PathArguments::AngleBracketed(AngleBracketedGenericArguments { + colon2_token: None, + lt_token: Lt::default(), + args: sometrait + .generics + .params + .iter() + .map(|param| match param { + GenericParam::Type(type_param) => { + GenericArgument::Type(Type::Path(TypePath { + path: type_param.ident.clone().into(), + qself: None, + })) + } + GenericParam::Lifetime(lifetime_def) => { + GenericArgument::Lifetime(lifetime_def.lifetime.clone()) + } + GenericParam::Const(_) => todo!("const generic param not supported"), + }) + .collect(), + gt_token: Gt::default(), + }), + }, + } +} |