Skip to content

Commit 3020cbd

Browse files
committed
Enable skipping of derived traversals, with reason
1 parent 15326d2 commit 3020cbd

File tree

3 files changed

+474
-76
lines changed

3 files changed

+474
-76
lines changed

compiler/rustc_macros/src/lib.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ decl_derive!([TyEncodable] => serialize::type_encodable_derive);
7474
decl_derive!([MetadataDecodable] => serialize::meta_decodable_derive);
7575
decl_derive!([MetadataEncodable] => serialize::meta_encodable_derive);
7676
decl_derive!(
77-
[TypeFoldable] =>
77+
[TypeFoldable, attributes(skip_traversal)] =>
7878
/// Derives `TypeFoldable` for the annotated `struct` or `enum` (`union` is not supported).
7979
///
8080
/// Folds will produce a value of the same struct or enum variant as the input, with trivial
@@ -83,7 +83,18 @@ decl_derive!(
8383
/// both does not reference any generic type parameters and either
8484
/// - does not reference any `'tcx` lifetime parameter; or
8585
/// - does not contain anything that could be of interest to folders.
86-
///
86+
///
87+
/// Non-trivial fields (e.g. of type `T`) can instead be left unchanged by applying
88+
/// `#[skip_traversal(because_trivial)]` to the field definition (or even to a variant
89+
/// definition if it should apply to all fields therein), but the derived implementation will
90+
/// only be applicable if `T` does not contain anything that may be of interest to folders
91+
/// (thus preventing fields from being so-skipped erroneously).
92+
///
93+
/// In some rare situations, it may be desirable to skip folding of an item or field (or
94+
/// variant) that might otherwise be of interest to folders: **this is dangerous and could lead
95+
/// to miscompilation if user expectations are not met!** Nevertheless, such can be achieved
96+
/// via a `#[skip_traversal(despite_potential_miscompilation_because = "<reason>"]` attribute.
97+
///
8798
/// If the annotated type has a `'tcx` lifetime parameter, then that will be used as the
8899
/// lifetime for the type context/interner; otherwise the lifetime of the type context/interner
89100
/// will be unrelated to the annotated type. It therefore matters how any lifetime parameters of
@@ -98,7 +109,7 @@ decl_derive!(
98109
traversable::traversable_derive::<traversable::Foldable>
99110
);
100111
decl_derive!(
101-
[TypeVisitable] =>
112+
[TypeVisitable, attributes(skip_traversal)] =>
102113
/// Derives `TypeVisitable` for the annotated `struct` or `enum` (`union` is not supported).
103114
///
104115
/// Each non-trivial field of the struct or enum variant will be visited (in definition order)
@@ -108,6 +119,17 @@ decl_derive!(
108119
/// - does not reference any `'tcx` lifetime parameter; or
109120
/// - does not contain anything that could be of interest to visitors.
110121
///
122+
/// Non-trivial fields (e.g. of type `T`) can instead be ignored by applying
123+
/// `#[skip_traversal(because_trivial)]` to the field definition (or even to a variant
124+
/// definition if it should apply to all fields therein), but the derived implementation will
125+
/// only be applicable if `T` does not contain anything that may be of interest to visitors
126+
/// (thus preventing fields from being so-skipped erroneously).
127+
///
128+
/// In some rare situations, it may be desirable to skip visiting of an item or field (or
129+
/// variant) that might otherwise be of interest to visitors: **this is dangerous and could lead
130+
/// to miscompilation if user expectations are not met!** Nevertheless, such can be achieved
131+
/// via a `#[skip_traversal(despite_potential_miscompilation_because = "<reason>"]` attribute.
132+
///
111133
/// If the annotated type has a `'tcx` lifetime parameter, then that will be used as the
112134
/// lifetime for the type context/interner; otherwise the lifetime of the type context/interner
113135
/// will be unrelated to the annotated type. It therefore matters how any lifetime parameters of

compiler/rustc_macros/src/traversable.rs

Lines changed: 200 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use proc_macro2::{Ident, Span, TokenStream};
22
use quote::{quote, ToTokens};
3-
use std::collections::HashSet;
4-
use syn::{parse_quote, visit, Field, Generics, Lifetime};
3+
use std::collections::{hash_map::Entry, HashMap};
4+
use syn::{
5+
meta::ParseNestedMeta, parse::Error, parse_quote, visit, Attribute, Field, Generics, Lifetime,
6+
LitStr, Token,
7+
};
58

69
#[cfg(test)]
710
mod tests;
@@ -31,6 +34,114 @@ enum Type {
3134
}
3235
use Type::*;
3336

37+
#[derive(Clone, Copy)]
38+
enum WhenToSkip {
39+
/// No skip_traversal annotation requires the annotated item to be skipped
40+
Never,
41+
42+
/// A skip_traversal annotation requires the annotated item to be skipped, with its type
43+
/// constrained to TriviallyTraversable
44+
Always(Span),
45+
46+
/// A `despite_potential_miscompilation_because` annotation is present, thus requiring the
47+
/// annotated item to be forcibly skipped without its type being constrained to
48+
/// TriviallyTraversable
49+
Forced,
50+
}
51+
52+
impl Default for WhenToSkip {
53+
fn default() -> Self {
54+
Self::Never
55+
}
56+
}
57+
58+
impl WhenToSkip {
59+
fn is_skipped(&self) -> bool {
60+
!matches!(self, WhenToSkip::Never)
61+
}
62+
}
63+
64+
impl std::ops::BitOrAssign for WhenToSkip {
65+
fn bitor_assign(&mut self, rhs: Self) {
66+
match self {
67+
Self::Forced => (),
68+
Self::Always(_) => {
69+
if matches!(rhs, Self::Forced) {
70+
*self = Self::Forced;
71+
}
72+
}
73+
Self::Never => *self = rhs,
74+
}
75+
}
76+
}
77+
78+
impl WhenToSkip {
79+
fn find<const IS_TYPE: bool>(&mut self, attrs: &[Attribute], ty: Type) -> Result<(), Error> {
80+
fn parse_reason(meta: &ParseNestedMeta<'_>) -> Result<(), Error> {
81+
if !meta.value()?.parse::<LitStr>()?.value().trim().is_empty() {
82+
Ok(())
83+
} else {
84+
Err(meta.error("skip reason must be a non-empty string"))
85+
}
86+
}
87+
88+
let mut found = None;
89+
for attr in attrs {
90+
if attr.path().is_ident("skip_traversal") {
91+
found = Some(attr);
92+
attr.parse_nested_meta(|meta| {
93+
if meta.path.is_ident("despite_potential_miscompilation_because") {
94+
parse_reason(&meta)?;
95+
*self |= Self::Forced;
96+
return Ok(());
97+
}
98+
99+
if !IS_TYPE && ty == Generic && meta.path.is_ident("because_trivial") {
100+
*self |= Self::Always(meta.error("").span());
101+
return Ok(());
102+
}
103+
104+
Err(meta.error("unsupported skip reason"))
105+
})?;
106+
}
107+
}
108+
109+
if let (Self::Never, Some(attr)) = (self, found) {
110+
Err(Error::new_spanned(
111+
attr,
112+
if IS_TYPE {
113+
match ty {
114+
Trivial => {
115+
"trivially traversable types are always skipped, so this attribute is superfluous"
116+
}
117+
_ => {
118+
"\
119+
Justification must be provided for skipping this potentially interesting type, by specifying\n\
120+
`despite_potential_miscompilation_because = \"<reason>\"`\
121+
"
122+
}
123+
}
124+
} else {
125+
match ty {
126+
Trivial => {
127+
"trivially traversable fields are always skipped, so this attribute is superfluous"
128+
}
129+
_ => {
130+
"\
131+
Justification must be provided for skipping potentially interesting fields, by specifying EITHER:\n\
132+
`because_trivial` if concrete instances do not actually contain anything of interest (enforced by the compiler); OR\n\
133+
`despite_potential_miscompilation_because = \"<reason>\"` in the rare case that a field should always be skipped regardless\
134+
"
135+
}
136+
}
137+
},
138+
))
139+
} else {
140+
Ok(())
141+
}
142+
}
143+
}
144+
34145
pub struct Interner<'a>(Option<&'a Lifetime>);
35146

36147
impl<'a> Interner<'a> {
@@ -73,7 +184,7 @@ pub trait Traversable {
73184
/// A `match` arm for `variant`, where `f` generates the tokens for each binding.
74185
fn arm(
75186
variant: &synstructure::VariantInfo<'_>,
76-
f: impl FnMut(&synstructure::BindingInfo<'_>) -> TokenStream,
187+
f: impl FnMut(&synstructure::BindingInfo<'_>) -> Result<TokenStream, Error>,
77188
) -> TokenStream;
78189

79190
/// The body of an implementation given the `interner`, `traverser` and match expression `body`.
@@ -100,10 +211,10 @@ impl Traversable for Foldable {
100211
}
101212
fn arm(
102213
variant: &synstructure::VariantInfo<'_>,
103-
mut f: impl FnMut(&synstructure::BindingInfo<'_>) -> TokenStream,
214+
mut f: impl FnMut(&synstructure::BindingInfo<'_>) -> Result<TokenStream, Error>,
104215
) -> TokenStream {
105216
let bindings = variant.bindings();
106-
variant.construct(|_, index| f(&bindings[index]))
217+
variant.construct(|_, index| f(&bindings[index]).unwrap_or_else(Error::into_compile_error))
107218
}
108219
fn impl_body(
109220
interner: Interner<'_>,
@@ -137,9 +248,14 @@ impl Traversable for Visitable {
137248
}
138249
fn arm(
139250
variant: &synstructure::VariantInfo<'_>,
140-
f: impl FnMut(&synstructure::BindingInfo<'_>) -> TokenStream,
251+
f: impl FnMut(&synstructure::BindingInfo<'_>) -> Result<TokenStream, Error>,
141252
) -> TokenStream {
142-
variant.bindings().iter().map(f).collect()
253+
variant
254+
.bindings()
255+
.iter()
256+
.map(f)
257+
.collect::<Result<_, _>>()
258+
.unwrap_or_else(Error::into_compile_error)
143259
}
144260
fn impl_body(
145261
interner: Interner<'_>,
@@ -199,49 +315,101 @@ impl Interner<'_> {
199315

200316
pub fn traversable_derive<T: Traversable>(
201317
mut structure: synstructure::Structure<'_>,
202-
) -> TokenStream {
318+
) -> Result<TokenStream, Error> {
319+
use WhenToSkip::*;
320+
203321
let ast = structure.ast();
204322

205323
let interner = Interner::resolve(&ast.generics);
206324
let traverser = gen_param("T", &ast.generics);
207325
let traversable = T::traversable(&interner);
208326

327+
let skip_traversal =
328+
|t: &dyn ToTokens| parse_quote! { #interner: ::rustc_middle::ty::TriviallyTraverses<#t> };
329+
209330
structure.underscore_const(true);
210331
structure.add_bounds(synstructure::AddBounds::None);
211332
structure.bind_with(|_| synstructure::BindStyle::Move);
212333

213-
if interner.0.is_none() {
334+
let not_generic = if interner.0.is_none() {
214335
structure.add_impl_generic(parse_quote! { 'tcx });
215-
}
336+
Trivial
337+
} else {
338+
NotGeneric
339+
};
216340

217341
// If our derived implementation will be generic over the traversable type, then we must
218342
// constrain it to only those generic combinations that satisfy the traversable trait's
219343
// supertraits.
220-
if ast.generics.type_params().next().is_some() {
344+
let ty = if ast.generics.type_params().next().is_some() {
221345
let supertraits = T::supertraits(&interner);
222346
structure.add_where_predicate(parse_quote! { Self: #supertraits });
223-
}
347+
Generic
348+
} else {
349+
not_generic
350+
};
224351

225-
// We add predicates to each generic field type, rather than to our generic type parameters.
226-
// This results in a "perfect derive", but it can result in trait solver cycles if any type
227-
// parameters are involved in recursive type definitions; fortunately that is not the case (yet).
228-
let mut predicates = HashSet::new();
229-
let arms = structure.each_variant(|variant| {
230-
T::arm(variant, |bind| {
231-
let ast = bind.ast();
232-
let field_ty = interner.type_of(&bind.referenced_ty_params(), [ast]);
233-
if field_ty == Generic {
234-
predicates.insert(ast.ty.clone());
352+
let mut when_to_skip = WhenToSkip::default();
353+
when_to_skip.find::<true>(&ast.attrs, ty)?;
354+
let body = if when_to_skip.is_skipped() {
355+
if let Always(_) = when_to_skip {
356+
structure.add_where_predicate(skip_traversal(&<Token![Self]>::default()));
357+
}
358+
T::traverse(quote! { self }, true, &interner)
359+
} else {
360+
// We add predicates to each generic field type, rather than to our generic type parameters.
361+
// This results in a "perfect derive" that avoids having to propagate `#[skip_traversal]` annotations
362+
// into wrapping types, but it can result in trait solver cycles if any type parameters are involved
363+
// in recursive type definitions; fortunately that is not the case (yet).
364+
let mut predicates = HashMap::<_, (_, _)>::new();
365+
366+
let arms = structure.each_variant(|variant| {
367+
let variant_ty = interner.type_of(&variant.referenced_ty_params(), variant.ast().fields);
368+
let mut skipped_variant = WhenToSkip::default();
369+
if let Err(err) = skipped_variant.find::<false>(variant.ast().attrs, variant_ty) {
370+
return err.into_compile_error();
235371
}
236-
T::traverse(bind.into_token_stream(), field_ty == Trivial, &interner)
237-
})
238-
});
239-
// the order in which `where` predicates appear in rust source is irrelevant
240-
#[allow(rustc::potential_query_instability)]
241-
for ty in predicates {
242-
structure.add_where_predicate(parse_quote! { #ty: #traversable });
243-
}
244-
let body = quote! { match self { #arms } };
372+
T::arm(variant, |bind| {
373+
let ast = bind.ast();
374+
let is_skipped = variant_ty == Trivial || {
375+
let field_ty = interner.type_of(&bind.referenced_ty_params(), [ast]);
376+
field_ty == Trivial || {
377+
let mut skipped_field = skipped_variant;
378+
skipped_field.find::<false>(&ast.attrs, field_ty)?;
379+
380+
match predicates.entry(ast.ty.clone()) {
381+
Entry::Occupied(existing) => match (&mut existing.into_mut().0, skipped_field) {
382+
(Never, Never) | (Never, Forced) | (Forced, Forced) | (Always(_), Always(_)) => (),
383+
(existing @ Forced, Never) => *existing = Never,
384+
(&mut Always(span), _) | (_, Always(span)) => return Err(Error::new(span, format!("\
385+
This annotation only makes sense if all fields of type `{0}` are annotated identically.\n\
386+
In particular, the derived impl will only be applicable when `{0}: TriviallyTraversable` and therefore all traversals of `{0}` will be no-ops;\n\
387+
accordingly it makes no sense for other fields of type `{0}` to omit `#[skip_traversal]` or to include `despite_potential_miscompilation_because`.\
388+
", ast.ty.to_token_stream()))),
389+
},
390+
Entry::Vacant(entry) => { entry.insert((skipped_field, field_ty)); }
391+
}
392+
393+
skipped_field.is_skipped()
394+
}
395+
};
396+
397+
Ok(T::traverse(bind.into_token_stream(), is_skipped, &interner))
398+
})
399+
});
400+
401+
// the order in which `where` predicates appear in rust source is irrelevant
402+
#[allow(rustc::potential_query_instability)]
403+
for (ty, (when_to_skip, field_ty)) in predicates {
404+
structure.add_where_predicate(match when_to_skip {
405+
Always(_) => skip_traversal(&ty),
406+
// we only need to add traversable predicate for generic types
407+
Never if field_ty == Generic => parse_quote! { #ty: #traversable },
408+
_ => continue,
409+
});
410+
}
411+
quote! { match self { #arms } }
412+
};
245413

246-
structure.bound_impl(traversable, T::impl_body(interner, traverser, body))
414+
Ok(structure.bound_impl(traversable, T::impl_body(interner, traverser, body)))
247415
}

0 commit comments

Comments
 (0)