yoke_derive/lib.rs
1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5// https://github.com/unicode-org/icu4x/blob/main/documents/process/boilerplate.md#library-annotations
6// #![cfg_attr(not(any(test, doc)), no_std)]
7#![cfg_attr(
8 not(test),
9 deny(
10 clippy::indexing_slicing,
11 clippy::unwrap_used,
12 clippy::expect_used,
13 clippy::panic,
14 )
15)]
16#![warn(missing_docs)]
17
18//! Custom derives for `Yokeable` from the `yoke` crate.
19
20mod lifetimes;
21mod visitor;
22
23use proc_macro::TokenStream;
24use proc_macro2::TokenStream as TokenStream2;
25use quote::quote;
26use syn::ext::IdentExt as _;
27use syn::spanned::Spanned;
28use syn::{parse_macro_input, parse_quote, DeriveInput, GenericParam, Ident, WherePredicate};
29use synstructure::Structure;
30
31use self::lifetimes::{custom_lt, ignored_lifetime_ident, replace_lifetime, static_lt};
32use self::visitor::{
33 check_parameter_for_bound_lts, check_type_for_parameters, check_where_clause_for_bound_lts,
34 CheckResult,
35};
36
37/// Custom derive for `yoke::Yokeable`.
38///
39/// If your struct contains `zerovec::ZeroMap`, then the compiler will not
40/// be able to guarantee the lifetime covariance due to the generic types on
41/// the `ZeroMap` itself. You must add the following attribute in order for
42/// the custom derive to work with `ZeroMap`.
43///
44/// ```rust,ignore
45/// #[derive(Yokeable)]
46/// #[yoke(prove_covariance_manually)]
47/// ```
48///
49/// Beyond this case, if the derive fails to compile due to lifetime issues, it likely
50/// means that the lifetime is not covariant and `Yokeable` is not safe to implement.
51#[proc_macro_derive(Yokeable, attributes(yoke))]
52pub fn yokeable_derive(input: TokenStream) -> TokenStream {
53 let input = parse_macro_input!(input as DeriveInput);
54 TokenStream::from(yokeable_derive_impl(&input))
55}
56
57/// A small amount of metadata about a field of the yokeable type
58struct FieldParamUsage {
59 uses_lt: bool,
60 uses_ty: bool,
61}
62
63impl From<CheckResult> for FieldParamUsage {
64 fn from(value: CheckResult) -> Self {
65 Self {
66 uses_lt: value.uses_lifetime_param,
67 uses_ty: value.uses_type_params,
68 }
69 }
70}
71
72fn yokeable_derive_impl(input: &DeriveInput) -> TokenStream2 {
73 let name = &input.ident;
74 let tybounds = input
75 .generics
76 .params
77 .iter()
78 .filter_map(|param| {
79 match param {
80 GenericParam::Lifetime(_) => None,
81 GenericParam::Type(ty) => {
82 // Strip out param defaults, we don't need them in the impl
83 let mut ty = ty.clone();
84 ty.eq_token = None;
85 ty.default = None;
86 Some(GenericParam::Type(ty))
87 }
88 // TODO: support const-generics in a future PR
89 // GenericParam::Const(const_param) => {
90 // // Strip out param defaults, we don't need them in the impl
91 // let mut const_param = const_param.clone();
92 // const_param.eq_token = None;
93 // const_param.default = None;
94 // Some(GenericParam::Const(const_param))
95 // }
96 GenericParam::Const(_) => None,
97 }
98 })
99 .collect::<Vec<_>>();
100 let typarams = tybounds
101 .iter()
102 .map(|param| match param {
103 // We filtered out lifetime parameters
104 GenericParam::Lifetime(_) => unreachable!(),
105 GenericParam::Type(ty) => ty.ident.clone(),
106 // TODO: support const-generics in a future PR
107 // GenericParam::Const(const_param) => const_param.ident.clone(),
108 GenericParam::Const(_) => unreachable!(),
109 })
110 .collect::<Vec<_>>();
111 let wherebounds = input
112 .generics
113 .where_clause
114 .iter()
115 .flat_map(|wc| wc.predicates.iter())
116 // If some future version of Rust adds more than just lifetime and type where-bound
117 // predicates, we may want to match more predicates.
118 .filter(|p| matches!(p, WherePredicate::Type(_)))
119 .collect::<Vec<_>>();
120 // We require all type parameters be 'static, otherwise
121 // the Yokeable impl becomes really unwieldy to generate safely.
122 let static_bounds: Vec<WherePredicate> = tybounds
123 .iter()
124 .filter_map(|param| {
125 if let GenericParam::Type(ty) = param {
126 let ty = &ty.ident;
127 Some(parse_quote!(#ty: 'static))
128 } else {
129 None
130 }
131 })
132 .collect();
133 // Above idents are *not* `unraw`d, because they may be emitted by the derive
134 // (so they might actually need to be raw).
135
136 // Either the `unraw`d first lifetime parameter of the yokeable, or some ignored ident.
137 // This parameter affects `uses_lifetime_param` values of `CheckResult`s and `uses_lt`
138 // values of `FieldParamUsage`, but those values only impact the generated code if there
139 // is at least one lifetime parameter; therefore, the random ident doesn't matter.
140 let lt_param = input
141 .generics
142 .lifetimes()
143 .next()
144 .map_or_else(ignored_lifetime_ident, |lt| lt.lifetime.ident.unraw());
145 let typarams_env = tybounds
146 .iter()
147 .filter_map(|param| {
148 if let GenericParam::Type(ty) = param {
149 Some(ty.ident.unraw())
150 } else {
151 None
152 }
153 })
154 .collect();
155 let mut underscores_for_lt = 0;
156
157 // We need to do this analysis even before the case where there are zero lifetime parameters
158 // in order to choose a `'__[underscores]__yoke` lifetime.
159 // We need to check:
160 // - trait bounds on generic type parameters
161 // - default types for generic type parameters
162 // - type bounds on const generic parameters
163 // - where-bounds
164 // - field types
165 // Checking lifetime parameters and default values for const generic parameters isn't
166 // particularly useful, but does no harm, so the simplest approach to knock out the first three
167 // is to just check every parameter.
168 for param in &input.generics.params {
169 underscores_for_lt = underscores_for_lt.max(check_parameter_for_bound_lts(param));
170 }
171 if let Some(where_clause) = &input.generics.where_clause {
172 underscores_for_lt = underscores_for_lt.max(check_where_clause_for_bound_lts(where_clause));
173 }
174
175 let structure = {
176 let mut structure = Structure::new(input);
177 structure.bind_with(|_| synstructure::BindStyle::Move);
178 structure
179 };
180
181 // Information from `synstructure::Structure`, whose ordering of fields is deterministic.
182 // Note that it's crucial that we don't filter out any variants or fields from the `Structure`.
183 let mut field_info: Vec<FieldParamUsage> = Vec::new();
184 // This code creating `field_info` should not be carelessly modified, else it could cause
185 // a panic in a below `expect`.
186 for variant_info in structure.variants() {
187 for field_binding_info in variant_info.bindings() {
188 let field = field_binding_info.ast();
189 // Note: `lt_param` and everything in `typarams_env` were `unraw`d
190 let check_result = check_type_for_parameters(<_param, &typarams_env, &field.ty);
191
192 underscores_for_lt = underscores_for_lt.max(check_result.min_underscores_for_yoke_lt);
193 field_info.push(check_result.into());
194 }
195 }
196 let field_info = field_info;
197
198 // All usages of the `check_*` functions are above this point,
199 // in order to ensure that `yoke_lt` is correct.
200 let (yoke_lt, bound_lt) = {
201 let underscores = vec![b'_'; underscores_for_lt];
202 #[expect(clippy::expect_used, reason = "invariant is ensured immediately above")]
203 let underscores = core::str::from_utf8(&underscores).expect("_ is ASCII and thus UTF-8");
204 (
205 format!("'{underscores}yoke"),
206 format!("'_{underscores}yoke"),
207 )
208 };
209 // This is used where the `Yokeable<'a>` trait uses `'a` by default
210 let yoke_lt = custom_lt(&yoke_lt);
211 // This is used where the `Yokeable<'a>` trait uses `'b` by default
212 let bound_lt = custom_lt(&bound_lt);
213
214 let mut lts = input.generics.lifetimes();
215
216 if lts.next().is_none() {
217 // There are 0 lifetime parameters.
218
219 return quote! {
220 // This is safe because there are no lifetime parameters, and `type Output = Self`.
221 unsafe impl<#yoke_lt, #(#tybounds),*> yoke::Yokeable<#yoke_lt>
222 for #name<#(#typarams),*>
223 where
224 #(#static_bounds,)*
225 #(#wherebounds,)*
226 Self: Sized
227 {
228 type Output = Self;
229 #[inline]
230 fn transform(&self) -> &Self::Output {
231 self
232 }
233 #[inline]
234 fn transform_owned(self) -> Self::Output {
235 self
236 }
237 #[inline]
238 unsafe fn make(this: Self::Output) -> Self {
239 this
240 }
241 #[inline]
242 fn transform_mut<F>(&#yoke_lt mut self, f: F)
243 where
244 F: 'static + for<#bound_lt> FnOnce(&#bound_lt mut Self::Output) {
245 f(self)
246 }
247 }
248 };
249 };
250
251 if lts.next().is_some() {
252 // We already extracted one lifetime into `source_lt`, so this means there are
253 // multiple lifetimes.
254 return syn::Error::new(
255 input.generics.span(),
256 "derive(Yokeable) cannot have multiple lifetime parameters",
257 )
258 .to_compile_error();
259 }
260
261 let manual_covariance = input.attrs.iter().any(|a| {
262 if a.path().is_ident("yoke") {
263 if let Ok(i) = a.parse_args::<Ident>() {
264 if i == "prove_covariance_manually" {
265 return true;
266 }
267 }
268 }
269 false
270 });
271
272 if !manual_covariance {
273 // This is safe because as long as `transform()` compiles,
274 // we can be sure that `'a` is a covariant lifetime on `Self`.
275 // (Using `'a` as shorthand for `#yoke_lt`.)
276 //
277 // In particular, the operand of `&raw const` is not a location where implicit
278 // type coercion can occur, so the type of `&raw const self` is `*const &'a Self`.
279 // The RHS of a `let` with an explicit type annotation allows type coercion, so
280 // `transform` checks that `*const &'a Self` can coerce to `*const &'a Self::Output`.
281 // Most of the possible type coercions
282 // (listed at https://doc.rust-lang.org/reference/type-coercions.html)
283 // do not apply, other than subtyping coercions and transitive coercions (which do
284 // not add anything beyond subtyping coercions). In particular, there's nothing
285 // like a `DerefRaw` on `*const T`, and `&T` does not implement `Unsize`, so
286 // there cannot be an unsizing coercion from `*const &'a Self` to
287 // `*const &'a Self::Output`. Therefore, `transform` compiles if and only if
288 // a subtyping coercion is possible; this requires that `Self` must be a subtype
289 // of `Self::Output`, just as `&'static T` is a subtype of `&'a T` (for `T: 'static`).
290 // This ensures covariance.
291 //
292 // This will not work for structs involving ZeroMap since
293 // the compiler does not know that ZeroMap is covariant.
294 //
295 // This custom derive can be improved to handle this case when necessary,
296 // with `prove_covariance_manually`.
297 return quote! {
298 unsafe impl<#yoke_lt, #(#tybounds),*> yoke::Yokeable<#yoke_lt>
299 for #name<'static, #(#typarams),*>
300 where
301 #(#static_bounds,)*
302 #(#wherebounds,)*
303 // Adding `Self: Sized` here doesn't work.
304 // `for<#bound_lt> #name<#bound_lt, #(#typarams),*>: Sized`
305 // might work, though. Since these trait bounds are very finicky, it's best to just
306 // not try unless necessary.
307 {
308 type Output = #name<#yoke_lt, #(#typarams),*>;
309 #[inline]
310 fn transform(&#yoke_lt self) -> &#yoke_lt Self::Output {
311 if false {
312 let _: *const &#yoke_lt Self::Output = &raw const self;
313 }
314 self
315 }
316 #[inline]
317 fn transform_owned(self) -> Self::Output {
318 self
319 }
320 #[inline]
321 unsafe fn make(from: Self::Output) -> Self {
322 ::core::mem::transmute::<Self::Output, Self>(from)
323 }
324 #[inline]
325 fn transform_mut<F>(&#yoke_lt mut self, f: F)
326 where
327 F: 'static + for<#bound_lt> FnOnce(&#bound_lt mut Self::Output) {
328 let y = unsafe { &mut *(self as *mut Self as *mut Self::Output) };
329 f(y)
330 }
331 }
332 };
333 }
334
335 // `prove_covariance_manually` requires additional bounds
336 let mut manual_proof_bounds: Vec<WherePredicate> = Vec::new();
337 let mut yokeable_checks = TokenStream2::new();
338 let mut output_checks = TokenStream2::new();
339 let mut field_info = field_info.into_iter();
340
341 // See `synstructure::Structure::each` and `synstructure::VariantInfo::each`
342 // for the setup of the two `*_checks` token streams. We can elide some brackets compared
343 // to `synstructure`, since we know that each check defines no local variables, items, etc.
344
345 // We iterate over the fields of `structure` in the same way that `field_info` was created.
346 for variant_info in structure.variants() {
347 let mut yokeable_check_body = TokenStream2::new();
348 let mut output_check_body = TokenStream2::new();
349
350 for field_binding_info in variant_info.bindings() {
351 let field = field_binding_info.ast();
352 let field_binding = &field_binding_info.binding;
353
354 // This invariant is somewhat complicated, but immutable variables, iteration order,
355 // and creating/using one `FieldParamUsage per iteration ensure that `field_info`
356 // has an entry for this field (and it refers to the expected field).
357 #[expect(
358 clippy::expect_used,
359 reason = "See above comment; this should never panic"
360 )]
361 let FieldParamUsage { uses_lt, uses_ty } = field_info
362 .next()
363 .expect("fields of an unmutated synstructure::Structure should remain the same");
364
365 // Note that this type could be a weird non-pure macro type. However, even though
366 // we evaluate it once or twice in where-bounds, we evaluate it exactly once
367 // in the soundness-critical checks, so it can't cause UB by unexpectedly evaluating
368 // to a different type. That can only cause a compile error at worst.
369 let fty_static = replace_lifetime(<_param, &field.ty, static_lt());
370
371 // For field types that don't use type or lifetime parameters, we don't add `Yokeable`
372 // or `'static` where-bounds, and the field is required to unconditionally meet a
373 // `'static` requirement (in its output form).
374 //
375 // For field types that use the lifetime parameter but no type parameters, we also don't
376 // add any where-bounds, and the field is required to unconditionally meet a
377 // `Yokeable` requirement (in its static yokeable form).
378 // (The compiler should be able to figure out whether that requirement is satisfied.
379 // A where-bound is intentionally avoided, to avoid letting `derive(Yokeable)`
380 // compile on a struct when it's statically known that the where-bound is never
381 // satisfied.)
382 //
383 // For field types that use a type parameter but not the lifetime parameter, the field
384 // is assumed not to borrow from the cart and is therefore required to be `'static`
385 // (in its output form), and a where-bound is added for this field being `'static`.
386 //
387 // For field types that use both the lifetime parameter and type parameters, the
388 // field is required to be `Yokeable` (in its static form). Since there may be complex
389 // preconditions to `FieldTy: Yokeable` that need to be satisfied, a where-bound
390 // requires that `FieldTy<'static>: Yokeable<#yoke_lt, Output = FieldTy<#yoke_lt>>`.
391 // This requirement is also tested on the field's static yokeable form.
392
393 // Note: if `field.ty` involves a non-pure macro type, each time it's evaluated, it
394 // could be a different type. The where-bounds are relied on to make the impl compile
395 // in sane cases, *not* for soundness. Our `transform()` impl does not blindly assume
396 // that the fields' types implement `Yokeable` or `'static`, regardless of these bounds.
397 if uses_ty {
398 if uses_lt {
399 let fty_output = replace_lifetime(<_param, &field.ty, yoke_lt.clone());
400
401 manual_proof_bounds.push(
402 parse_quote!(#fty_static: yoke::Yokeable<#yoke_lt, Output = #fty_output>),
403 );
404 } else {
405 manual_proof_bounds.push(parse_quote!(#fty_static: 'static));
406 }
407 }
408 if uses_lt {
409 // This confirms that this `FieldTy` is a subtype of something which implements
410 // `Yokeable<'a>`, and since only `'static` types can be subtypes of a `'static`
411 // type (and all `Yokeable` implementors are `'static`), we have that either:
412 // - `FieldTy` is some `'static` type which does NOT implement `Yokeable`, but via
413 // function pointer subtyping or something similar, is a subtype of something
414 // implementing `Yokeable`, or
415 // - `FieldTy` is some type which does itself implement `Yokeable`.
416 // In either of those cases, it is sound to treat `FieldTy` as covariant in the `'a`
417 // parameter. (Using `'a` as shorthand for `#yoke_lt`.)
418 //
419 // Now, to justify that `FieldTy` (the field's actual type,
420 // not just `field.ty`, which may have a non-pure macro type)
421 // is a subtype of something which implements `Yokeable<'a>`:
422 //
423 // `#field_binding` has type `&'a FieldTy` (since it's a field of `&'a Self` matched
424 // as `self`). The operand of `&raw const` is not a location where implicit type
425 // coercion can occur. Therefore, `&raw const #field_binding` is guaranteed to be
426 // type `*const &'a FieldTy`. The argument to `__yoke_derive_require_yokeable`
427 // does allow type coercion.
428 // Looking at <https://doc.rust-lang.org/reference/type-coercions.html>,
429 // there are only three types of coercions that could plausibly apply:
430 // - subtyping coercions,
431 // - transitive coercions, and
432 // - unsizing coercions.
433 // (If some sort of `DerefRaw` trait gets added for `*const`, there could plausibly
434 // be problems with that. But there's no reason to think that such a trait will be
435 // added, since it'd mess with `unsafe` code, and Rust devs should recognize that.)
436 //
437 // Since `&'a _` does not implement `Unsize`, we have that `*const &'a _` does not
438 // allow an unsizing coercion to occur. Therefore, there are only subtyping
439 // coercions, since transitive coercions add nothing on top of subtyping coercions.
440 // Therefore, if this compiles, `*const &'a FieldTy` must be a subtype of
441 // `*const &'a T` where `T = #fty_static` is the generic parameter of
442 // `__yoke_derive_require_yokeable`.
443 // Looking at the signature of that function generated below, we have that
444 // `T: Yokeable<'a>` (if it compiles). Note that if `#fty_static` is incorrect,
445 // even if there is some other `T` which would work, this will just fail to compile.
446 // Since `*const _` and `&'a _` are covariant over their type parameters, we have
447 // that `FieldTy` must be a subtype of `T` in order for a subtyping coercion from
448 // `*const &'a FieldTy` to `*const &'a T` to occur.
449 //
450 // Therefore, `FieldTy` must be a subtype of something which implements
451 // `Yokeable<'a>` in order for this to compile. (Though that is not a _sufficient_
452 // condition to compile, as some weird macro type could break stuff.)
453 yokeable_check_body.extend(quote! {
454 __yoke_derive_require_yokeable::<#yoke_lt, #fty_static>(&raw const #field_binding);
455 });
456 } else {
457 // No visible nested lifetimes, so there should be nothing to be done in sane cases.
458 // However, in case a macro type does something strange and accesses the available
459 // `#yoke_lt` lifetime, we still need to check that the field's actual type is
460 // `'static` regardless of `#yoke_lt` (which we can check by ensuring that it
461 // be a subtype of a `'static` type).
462 // See reasoning in the `if` branch for why this works. The difference is that
463 // `FieldTy` is guaranteed to be a subtype of `T = #fty_static` where `T: 'static`
464 // (if this compiles). Since the field's type is a subtype of something which is
465 // `'static`, it must itself be `'static`, and therefore did not manage to use
466 // `#yoke_lt` via a macro.
467 // Note that creating and using `#fty_output` is not necessary here, since
468 // `field.ty == fty_static == fty_output` (no lifetime was visibly present which
469 // could be replaced).
470 output_check_body.extend(quote! {
471 __yoke_derive_require_static::<#yoke_lt, #fty_static>(&raw const #field_binding);
472 });
473 }
474 }
475
476 let pat = variant_info.pat();
477 yokeable_checks.extend(quote! { #pat => { #yokeable_check_body }});
478 output_checks.extend(quote! { #pat => { #output_check_body }});
479 }
480
481 quote! {
482 // SAFETY: we assert covariance in `borrowed_checks`
483 unsafe impl<#yoke_lt, #(#tybounds),*> yoke::Yokeable<#yoke_lt>
484 for #name<'static, #(#typarams),*>
485 where
486 #(#static_bounds,)*
487 #(#wherebounds,)*
488 #(#manual_proof_bounds,)*
489 // Adding `Self: Sized` here doesn't work.
490 // `for<#bound_lt> #name<#bound_lt, #(#typarams),*>: Sized`
491 // might work, though. Since these trait bounds are very finicky, it's best to just
492 // not try unless necessary.
493 {
494 type Output = #name<#yoke_lt, #(#typarams),*>;
495 #[inline]
496 fn transform(&#yoke_lt self) -> &#yoke_lt Self::Output {
497 // These are just type asserts, we don't need to run them
498 if false {
499 // This could, hypothetically, conflict with the name of one of the `FieldTy`s
500 // we read (and cause a compilation error). However, such a conflict cannot
501 // cause unsoundness, since this function is in scope no matter what.
502 // (The problem is that attempting to refer to a type named
503 // `__yoke_derive_require_yokeable` would instead refer to this function item
504 // and therefore fail.)
505 #[allow(dead_code)]
506 fn __yoke_derive_require_yokeable<
507 #yoke_lt: #yoke_lt,
508 T: yoke::Yokeable<#yoke_lt>,
509 >(_t: *const &#yoke_lt T) {}
510
511 match self {
512 #yokeable_checks
513 }
514 }
515 let output = unsafe { ::core::mem::transmute::<&#yoke_lt Self, &#yoke_lt Self::Output>(self) };
516 if false {
517 // Same deal as above.
518 #[allow(dead_code)]
519 fn __yoke_derive_require_static<
520 #yoke_lt: #yoke_lt,
521 T: 'static,
522 >(_t: *const &#yoke_lt T) {}
523
524 match output {
525 #output_checks
526 }
527 }
528 output
529 }
530 #[inline]
531 fn transform_owned(self) -> Self::Output {
532 unsafe { ::core::mem::transmute::<Self, Self::Output>(self) }
533 }
534 #[inline]
535 unsafe fn make(from: Self::Output) -> Self {
536 unsafe { ::core::mem::transmute::<Self::Output, Self>(from) }
537 }
538 #[inline]
539 fn transform_mut<F>(&#yoke_lt mut self, f: F)
540 where
541 F: 'static + for<#bound_lt> FnOnce(&#bound_lt mut Self::Output) {
542 let y = unsafe { &mut *(self as *mut Self as *mut Self::Output) };
543 f(y)
544 }
545 }
546 }
547}