// This file is part of ICU4X. For terms of use, please see the file // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). //! Custom derives for `Yokeable` from the `yoke` crate. use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; use syn::spanned::Spanned; use syn::{parse_macro_input, parse_quote, DeriveInput, Ident, Lifetime, Type, WherePredicate}; use synstructure::Structure; mod visitor; /// Custom derive for `yoke::Yokeable`, /// /// If your struct contains `zerovec::ZeroMap`, then the compiler will not /// be able to guarantee the lifetime covariance due to the generic types on /// the `ZeroMap` itself. You must add the following attribute in order for /// the custom derive to work with `ZeroMap`. /// /// ```rust,ignore /// #[derive(Yokeable)] /// #[yoke(prove_covariance_manually)] /// ``` /// /// Beyond this case, if the derive fails to compile due to lifetime issues, it /// means that the lifetime is not covariant and `Yokeable` is not safe to implement. #[proc_macro_derive(Yokeable, attributes(yoke))] pub fn yokeable_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); TokenStream::from(yokeable_derive_impl(&input)) } fn yokeable_derive_impl(input: &DeriveInput) -> TokenStream2 { let tybounds = input .generics .type_params() .map(|ty| { // Strip out param defaults, we don't need them in the impl let mut ty = ty.clone(); ty.eq_token = None; ty.default = None; ty }) .collect::>(); let typarams = tybounds .iter() .map(|ty| ty.ident.clone()) .collect::>(); // We require all type parameters be 'static, otherwise // the Yokeable impl becomes really unweildy to generate safely let static_bounds: Vec = typarams .iter() .map(|ty| parse_quote!(#ty: 'static)) .collect(); let lts = input.generics.lifetimes().count(); if lts == 0 { let name = &input.ident; quote! { // This is safe because there are no lifetime parameters. unsafe impl<'a, #(#tybounds),*> yoke::Yokeable<'a> for #name<#(#typarams),*> where #(#static_bounds),* { type Output = Self; #[inline] fn transform(&self) -> &Self::Output { self } #[inline] fn transform_owned(self) -> Self::Output { self } #[inline] unsafe fn make(this: Self::Output) -> Self { this } #[inline] fn transform_mut(&'a mut self, f: F) where F: 'static + for<'b> FnOnce(&'b mut Self::Output) { f(self) } } } } else { if lts != 1 { return syn::Error::new( input.generics.span(), "derive(Yokeable) cannot have multiple lifetime parameters", ) .to_compile_error(); } let name = &input.ident; let manual_covariance = input.attrs.iter().any(|a| { if let Ok(i) = a.parse_args::() { if i == "prove_covariance_manually" { return true; } } false }); if manual_covariance { let mut structure = Structure::new(input); let generics_env = typarams.iter().cloned().collect(); let static_bounds: Vec = typarams .iter() .map(|ty| parse_quote!(#ty: 'static)) .collect(); let mut yoke_bounds: Vec = vec![]; structure.bind_with(|_| synstructure::BindStyle::Move); let owned_body = structure.each_variant(|vi| { vi.construct(|f, i| { let binding = format!("__binding_{i}"); let field = Ident::new(&binding, Span::call_site()); let fty_static = replace_lifetime(&f.ty, static_lt()); let (has_ty, has_lt) = visitor::check_type_for_parameters(&f.ty, &generics_env); if has_ty { // For types without type parameters, the compiler can figure out that the field implements // Yokeable on its own. However, if there are type parameters, there may be complex preconditions // to `FieldTy: Yokeable` that need to be satisfied. We get them to be satisfied by requiring // `FieldTy<'static>: Yokeable>` if has_lt { let fty_a = replace_lifetime(&f.ty, custom_lt("'a")); yoke_bounds.push( parse_quote!(#fty_static: yoke::Yokeable<'a, Output = #fty_a>), ); } else { yoke_bounds.push( parse_quote!(#fty_static: yoke::Yokeable<'a, Output = #fty_static>), ); } } if has_ty || has_lt { // By calling transform_owned on all fields, we manually prove // that the lifetimes are covariant, since this requirement // must already be true for the type that implements transform_owned(). quote! { <#fty_static as yoke::Yokeable<'a>>::transform_owned(#field) } } else { // No nested lifetimes, so nothing to be done quote! { #field } } }) }); let borrowed_body = structure.each(|binding| { let f = binding.ast(); let field = &binding.binding; let (has_ty, has_lt) = visitor::check_type_for_parameters(&f.ty, &generics_env); if has_ty || has_lt { let fty_static = replace_lifetime(&f.ty, static_lt()); let fty_a = replace_lifetime(&f.ty, custom_lt("'a")); // We also must assert that each individual field can `transform()` correctly // // Even though transform_owned() does such an assertion already, CoerceUnsized // can cause type transformations that allow it to succeed where this would fail. // We need to check both. // // https://github.com/unicode-org/icu4x/issues/2928 quote! { let _: &#fty_a = &<#fty_static as yoke::Yokeable<'a>>::transform(#field); } } else { // No nested lifetimes, so nothing to be done quote! {} } }); return quote! { unsafe impl<'a, #(#tybounds),*> yoke::Yokeable<'a> for #name<'static, #(#typarams),*> where #(#static_bounds,)* #(#yoke_bounds,)* { type Output = #name<'a, #(#typarams),*>; #[inline] fn transform(&'a self) -> &'a Self::Output { // These are just type asserts, we don't need them for anything if false { match self { #borrowed_body } } unsafe { // safety: we have asserted covariance in // transform_owned ::core::mem::transmute(self) } } #[inline] fn transform_owned(self) -> Self::Output { match self { #owned_body } } #[inline] unsafe fn make(this: Self::Output) -> Self { use core::{mem, ptr}; // unfortunately Rust doesn't think `mem::transmute` is possible since it's not sure the sizes // are the same debug_assert!(mem::size_of::() == mem::size_of::()); let ptr: *const Self = (&this as *const Self::Output).cast(); #[allow(clippy::forget_copy)] // This is a noop if the struct is copy, which Clippy doesn't like mem::forget(this); ptr::read(ptr) } #[inline] fn transform_mut(&'a mut self, f: F) where F: 'static + for<'b> FnOnce(&'b mut Self::Output) { unsafe { f(core::mem::transmute::<&'a mut Self, &'a mut Self::Output>(self)) } } } }; } quote! { // This is safe because as long as `transform()` compiles, // we can be sure that `'a` is a covariant lifetime on `Self` // // This will not work for structs involving ZeroMap since // the compiler does not know that ZeroMap is covariant. // // This custom derive can be improved to handle this case when // necessary unsafe impl<'a, #(#tybounds),*> yoke::Yokeable<'a> for #name<'static, #(#typarams),*> where #(#static_bounds),* { type Output = #name<'a, #(#typarams),*>; #[inline] fn transform(&'a self) -> &'a Self::Output { self } #[inline] fn transform_owned(self) -> Self::Output { self } #[inline] unsafe fn make(this: Self::Output) -> Self { use core::{mem, ptr}; // unfortunately Rust doesn't think `mem::transmute` is possible since it's not sure the sizes // are the same debug_assert!(mem::size_of::() == mem::size_of::()); let ptr: *const Self = (&this as *const Self::Output).cast(); #[allow(clippy::forget_copy)] // This is a noop if the struct is copy, which Clippy doesn't like mem::forget(this); ptr::read(ptr) } #[inline] fn transform_mut(&'a mut self, f: F) where F: 'static + for<'b> FnOnce(&'b mut Self::Output) { unsafe { f(core::mem::transmute::<&'a mut Self, &'a mut Self::Output>(self)) } } } } } } fn static_lt() -> Lifetime { Lifetime::new("'static", Span::call_site()) } fn custom_lt(s: &str) -> Lifetime { Lifetime::new(s, Span::call_site()) } fn replace_lifetime(x: &Type, lt: Lifetime) -> Type { use syn::fold::Fold; struct ReplaceLifetime(Lifetime); impl Fold for ReplaceLifetime { fn fold_lifetime(&mut self, _: Lifetime) -> Lifetime { self.0.clone() } } ReplaceLifetime(lt).fold_type(x.clone()) }