Revision control
Copy as Markdown
Other Tools
// 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
//! 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::<Vec<_>>();
let typarams = tybounds
.iter()
.map(|ty| ty.ident.clone())
.collect::<Vec<_>>();
// We require all type parameters be 'static, otherwise
// the Yokeable impl becomes really unweildy to generate safely
let static_bounds: Vec<WherePredicate> = 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,)* Self: Sized {
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<F>(&'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::<Ident>() {
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<WherePredicate> = typarams
.iter()
.map(|ty| parse_quote!(#ty: 'static))
.collect();
let mut yoke_bounds: Vec<WherePredicate> = 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<FieldTy<'a>>`
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.
//
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::<Self::Output>() == mem::size_of::<Self>());
let ptr: *const Self = (&this as *const Self::Output).cast();
#[allow(forgetting_copy_types, clippy::forget_copy, clippy::forget_non_drop)] // This is a noop if the struct is copy, which Clippy doesn't like
mem::forget(this);
ptr::read(ptr)
}
#[inline]
fn transform_mut<F>(&'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::<Self::Output>() == mem::size_of::<Self>());
let ptr: *const Self = (&this as *const Self::Output).cast();
#[allow(forgetting_copy_types, clippy::forget_copy, clippy::forget_non_drop)] // This is a noop if the struct is copy, which Clippy doesn't like
mem::forget(this);
ptr::read(ptr)
}
#[inline]
fn transform_mut<F>(&'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())
}