diff --git a/crates/thread_aware/src/lib.rs b/crates/thread_aware/src/lib.rs index 682a33183..b22fba470 100644 --- a/crates/thread_aware/src/lib.rs +++ b/crates/thread_aware/src/lib.rs @@ -158,21 +158,25 @@ mod thread; /// Unions are not supported and will produce a compile error. /// /// # Attributes -/// * `#[thread_aware(skip)]`: Prevents a field from being recursively transferred. +/// * `#[thread_aware(skip)]` (on a field): moves the field without calling `relocate` on it. +/// * `#[thread_aware(bound = "...")]` (on the type): replaces the derive's inferred `where` +/// predicates with your own. An escape hatch for a field whose bound the derive can't infer - +/// most often a type alias that hides a recursive self-reference, where the inferred field-type +/// bound would be circular. Write `#[thread_aware(bound = "T: ThreadAware")]` to bound the +/// parameter instead. /// /// # Generic Bounds -/// Every field except a `#[thread_aware(skip)]` one is relocated, and the bounds follow from -/// that: -/// * a generic type parameter the traversal reaches through a relocated field receives a -/// `::thread_aware::ThreadAware` bound; -/// * if any field carries `#[thread_aware(skip)]`, a `where Self: Send` predicate is added, -/// since the `ThreadAware: Send` supertrait still has to hold. +/// The derive bounds each relocated field by its own type - `where : ThreadAware` - +/// not by the type parameters inside it. A field that refers back to the type being derived is the +/// exception: it bounds the parameters it reaches instead, so recursive types still compile (a +/// recursion hidden behind a type alias is invisible to the derive - use `#[thread_aware(bound = +/// "...")]` there). A `#[thread_aware(skip)]` field isn't relocated, so it contributes +/// `where Self: Send` instead, which is all the `ThreadAware: Send` supertrait needs. /// -/// A `PhantomData<..>` field is no exception: it relocates through the no-op -/// `impl ThreadAware for PhantomData`, and a parameter the traversal -/// reaches inside it - the `U` of `PhantomData` - is bound like any other. The traversal -/// does not enter a function pointer, so writing the payload as one carries the parameter for -/// variance, stays `Send` for every argument, and emits no bound at all: +/// Bounding the field type means a `Wrapper` field defers to that wrapper's own impl, so a +/// wrapper that is `ThreadAware` for every `T` keeps deriving even where `T` isn't. The derive +/// doesn't look inside function pointers, so a variance marker written as +/// `PhantomData` adds no bound at all: /// /// ```rust /// # use core::marker::PhantomData; @@ -184,8 +188,8 @@ mod thread; /// } /// ``` /// -/// A trait referred to by the bare name `ThreadAware` in your own bounds is assumed to be this -/// crate's and suppresses the generated bound; qualify the path to disambiguate. +/// If you already wrote a bare `ThreadAware` bound yourself, the derive assumes it's this trait and +/// skips the duplicate; qualify the path to disambiguate. /// /// # Example /// ```rust diff --git a/crates/thread_aware/tests/derive_compiles.rs b/crates/thread_aware/tests/derive_compiles.rs index 168f309a1..fe306eadc 100644 --- a/crates/thread_aware/tests/derive_compiles.rs +++ b/crates/thread_aware/tests/derive_compiles.rs @@ -638,6 +638,148 @@ fn payloads_invisible_to_a_syntactic_scan_need_no_escape_hatch() { assert_eq!(hidden.tracked.relocations, 1); } +// A field whose type implements `ThreadAware` unconditionally must keep deriving for parameters +// that type ignores. Bounding a parameter reached inside the field - rather than the field type +// itself - was a compile regression against the merge base of PR #678, which rejected +// `OuterWrapper>` even though the wrapper's own impl asks nothing of the parameter. See +// AB#7783612. + +/// A wrapper that is `ThreadAware` for every `T`, delegating to nothing. +struct UnconditionalWrapper(PhantomData T>); + +impl thread_aware::ThreadAware for UnconditionalWrapper { + fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {} +} + +/// The derive owes `UnconditionalWrapper>: ThreadAware`, which that impl satisfies +/// for every `T` - not `T: ThreadAware`, which `Rc<()>` cannot meet. +#[derive(ThreadAware)] +struct OuterWrapper(UnconditionalWrapper>); + +#[test] +fn field_type_bound_lets_an_unconditional_wrapper_derive_for_any_argument() { + // `Rc<()>` is neither `Send` nor `ThreadAware`; the field-type bound still holds. + assert_thread_aware::>>(); + + let (source, destination) = thread_pair(); + let mut value = OuterWrapper::>(UnconditionalWrapper(PhantomData)); + value.relocate(source.as_ref(), &destination); +} + +/// A recursive generic type: the `children: Vec>` field names the type being +/// derived, so the derive bounds the parameter it reaches (`T`) rather than the whole field type - +/// a `where Vec>: ThreadAware` predicate would be self-referential and overflow. +#[expect( + clippy::use_self, + reason = "the explicit self-referential spelling is what this regression exercises" +)] +#[derive(ThreadAware)] +struct RecursiveNode { + value: T, + children: Vec>, +} + +/// A recursive generic enum, reached through `Box`. +#[expect( + clippy::use_self, + reason = "the explicit self-referential spelling is what this regression exercises" +)] +#[derive(ThreadAware)] +enum RecursiveList { + Nil, + Cons(T, Box>), +} + +/// A slice-bearing field whose sibling used to cover its bound under the per-parameter model. +/// `Box<[T]>` reaches `T` through the conditional `[T]` impl, so it owes `Box<[T]>: ThreadAware`. +#[derive(ThreadAware)] +struct SliceSibling { + values: Vec, + rest: Box<[T]>, +} + +#[test] +fn recursive_and_slice_generics_compile_and_relocate() { + assert_thread_aware::>(); + assert_thread_aware::>(); + assert_thread_aware::>(); + + let (source, destination) = thread_pair(); + let mut node = RecursiveNode { + value: Tracker::default(), + children: vec![RecursiveNode { + value: Tracker::default(), + children: Vec::new(), + }], + }; + node.relocate(source.as_ref(), &destination); + assert_eq!(node.value.relocations, 1); + assert_eq!(node.children[0].value.relocations, 1, "relocation reaches recursive children"); +} + +/// A field written as a qualified-self projection, `::Item`. +/// +/// The field reaches `T` only through the `qself` type, not the path segments +/// (`ItemProvider::Item`), so the derive has to look at the `qself` to know the field owes a bound +/// - otherwise the body relocates a field the impl never bounded and fails to compile. +trait ItemProvider { + type Item; +} + +struct ConcreteProvider; + +impl ItemProvider for ConcreteProvider { + type Item = Tracker; +} + +#[derive(ThreadAware)] +struct ProjectedField(::Item); + +#[test] +fn qualified_self_projection_field_is_bounded_and_relocates() { + let (source, destination) = thread_pair(); + let mut value = ProjectedField::(Tracker::default()); + value.relocate(source.as_ref(), &destination); + assert_eq!(value.0.relocations, 1); +} + +/// A recursive self-reference hidden behind a type alias. +/// +/// The derive only sees the syntactic field type `AliasChildren`, not that it expands to +/// `Vec>`, so it can't tell the field is a recursive self-reference. Left to infer, it +/// would emit the circular `where AliasChildren: ThreadAware`. The `#[thread_aware(bound = ...)]` +/// escape hatch replaces the inferred bounds with the parameter bound the body actually needs. +type AliasChildren = Vec>; + +#[derive(ThreadAware)] +#[thread_aware(bound = "T: thread_aware::ThreadAware")] +struct AliasNode { + value: T, + children: AliasChildren, +} + +#[test] +fn alias_hidden_recursion_compiles_with_bound_override() { + // `Rc<()>` is not `ThreadAware`, but the field is a `Vec` of the type itself, so the override + // bound `T: ThreadAware` is what the body needs - and it must relocate, not skip. + assert_thread_aware::>(); + + let (source, destination) = thread_pair(); + let mut node = AliasNode { + value: Tracker::default(), + children: vec![AliasNode { + value: Tracker::default(), + children: Vec::new(), + }], + }; + node.relocate(source.as_ref(), &destination); + assert_eq!(node.value.relocations, 1); + assert_eq!( + node.children[0].value.relocations, 1, + "relocation reaches aliased recursive children" + ); +} + /// A trait of the user's own that happens to be called `ThreadAware`, named by a qualified /// path. /// diff --git a/crates/thread_aware_macros_impl/src/field_attrs.rs b/crates/thread_aware_macros_impl/src/field_attrs.rs index 1708e89b0..809b0a8d2 100644 --- a/crates/thread_aware_macros_impl/src/field_attrs.rs +++ b/crates/thread_aware_macros_impl/src/field_attrs.rs @@ -10,6 +10,42 @@ pub(crate) struct FieldAttrCfg { pub(crate) skip: bool, } +/// Configuration parsed from `#[thread_aware(...)]` on the derive input itself (the struct or enum). +#[derive(Default, Debug)] +pub(crate) struct ContainerAttrCfg { + /// An explicit replacement for the derive's inferred `where` predicates, from + /// `#[thread_aware(bound = "...")]`. When set it is non-empty, and the derive emits these + /// predicates verbatim instead of inferring bounds from the fields - the escape hatch for a + /// field type whose obligation the derive can't infer correctly (for example a type alias that + /// hides a recursive self-reference). + pub(crate) bound: Option>, +} + +/// Parses the `thread_aware` attributes on the derive input (the struct or enum). +pub(crate) fn parse_container_attrs(attrs: &[Attribute]) -> syn::Result { + let mut cfg = ContainerAttrCfg::default(); + for attr in attrs.iter().filter(|a| a.path().is_ident("thread_aware")) { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("bound") { + if cfg.bound.is_some() { + return Err(meta.error("duplicate 'bound'")); + } + let lit: syn::LitStr = meta.value()?.parse()?; + let parsed: syn::punctuated::Punctuated = + lit.parse_with(syn::punctuated::Punctuated::parse_terminated)?; + if parsed.is_empty() { + return Err(meta.error("empty 'bound' - list at least one predicate, or omit the attribute to infer bounds")); + } + cfg.bound = Some(parsed.into_iter().collect()); + Ok(()) + } else { + Err(meta.error("unknown thread_aware attribute (only 'bound' is supported on the type)")) + } + })?; + } + Ok(cfg) +} + /// Parses the `thread_aware` attributes on a field. pub(crate) fn parse_field_attrs(attrs: &[Attribute]) -> syn::Result { let mut cfg = FieldAttrCfg::default(); @@ -135,4 +171,61 @@ mod tests { let err_msg = result.unwrap_err().to_string(); assert!(err_msg.contains("duplicate")); } + + #[test] + fn test_parse_container_attrs_none() { + // No container attribute leaves the override unset. + let attrs: Vec = vec![]; + let cfg = parse_container_attrs(&attrs).unwrap(); + assert!(cfg.bound.is_none()); + } + + #[test] + fn test_parse_container_attrs_bound() { + // A `bound = "..."` parses into the listed where-predicates. + let attrs: Vec = vec![parse_quote! { #[thread_aware(bound = "T: ThreadAware, U: Send")] }]; + let cfg = parse_container_attrs(&attrs).unwrap(); + let bounds = cfg.bound.expect("bound should be set"); + assert_eq!(bounds.len(), 2); + } + + #[test] + fn test_parse_container_attrs_duplicate_bound() { + // Two `bound` values are rejected. + let attrs: Vec = vec![parse_quote! { #[thread_aware(bound = "T: ThreadAware", bound = "U: Send")] }]; + let result = parse_container_attrs(&attrs); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("duplicate 'bound'")); + } + + #[test] + fn test_parse_container_attrs_unknown() { + // An unknown container key is rejected. + let attrs: Vec = vec![parse_quote! { #[thread_aware(nonsense = "x")] }]; + let result = parse_container_attrs(&attrs); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("only 'bound' is supported")); + } + + #[test] + fn test_parse_container_attrs_invalid_predicate() { + // A `bound` string that isn't a valid where-predicate list surfaces a parse error. + let attrs: Vec = vec![parse_quote! { #[thread_aware(bound = "!!!")] }]; + parse_container_attrs(&attrs).unwrap_err(); + } + + #[test] + fn test_parse_container_attrs_empty_bound() { + // An empty `bound = ""` is rejected rather than silently emitting no predicates. + let attrs: Vec = vec![parse_quote! { #[thread_aware(bound = "")] }]; + let err = parse_container_attrs(&attrs).unwrap_err(); + assert!(err.to_string().contains("empty 'bound'")); + } + + #[test] + fn test_container_attr_cfg_debug() { + // Exercise the derived `Debug` (the error-path tests need the bound but never format it). + let cfg = ContainerAttrCfg::default(); + assert!(format!("{cfg:?}").contains("ContainerAttrCfg")); + } } diff --git a/crates/thread_aware_macros_impl/src/lib.rs b/crates/thread_aware_macros_impl/src/lib.rs index 892cb72fc..0b5002e58 100644 --- a/crates/thread_aware_macros_impl/src/lib.rs +++ b/crates/thread_aware_macros_impl/src/lib.rs @@ -20,7 +20,7 @@ use std::collections::HashSet; use proc_macro2::TokenStream as TokenStream2; -use quote::quote; +use quote::{ToTokens, quote}; use syn::{Data, DeriveInput, Fields, GenericParam, Path, PathArguments, Type, TypePath, parse_quote}; mod enum_gen; @@ -34,7 +34,7 @@ mod field_attrs; mod struct_gen; use enum_gen::build_enum_body; -use field_attrs::parse_field_attrs; +use field_attrs::{parse_container_attrs, parse_field_attrs}; use struct_gen::build_struct_body; /// Core implementation used by both `thread_aware_macros` and `oxidizer_macros`. @@ -104,33 +104,121 @@ pub(crate) fn param_idents() -> (syn::Ident, syn::Ident) { fn add_bounds(input: &DeriveInput, root_path: &Path) -> syn::Result { let mut generics = input.generics.clone(); - let mut usage = GenericUsage::default(); - match &input.data { - Data::Struct(s) => collect_generics_in_fields(&s.fields, &generics, &mut usage)?, - Data::Enum(e) => { - for v in &e.variants { - collect_generics_in_fields(&v.fields, &generics, &mut usage)?; - } + + // Escape hatch: an explicit `#[thread_aware(bound = "...")]` on the type replaces the derive's + // inferred `where` predicates wholesale. Use it when the derive can't infer the right bound for + // a field - for example a type alias like `type Children = Vec>` hides that the field + // is a recursive self-reference, so the inferred `Children: ThreadAware` becomes circular. + // Writing `#[thread_aware(bound = "T: ThreadAware")]` restores the parameter bound instead. + if let Some(bounds) = parse_container_attrs(&input.attrs)?.bound { + // `parse_container_attrs` rejects an empty list, so there is at least one predicate here. + let where_clause = generics.make_where_clause(); + for predicate in bounds { + where_clause.predicates.push(predicate); } - Data::Union(_) => {} + return Ok(generics); } + // Type parameters in declaration order (for deterministic output) and as a set (for fast + // membership tests). + let generic_param_idents: Vec = generics + .params + .iter() + .filter_map(|gp| match gp { + GenericParam::Type(t) => Some(t.ident.clone()), + _ => None, + }) + .collect(); + let generic_idents: HashSet = generic_param_idents.iter().cloned().collect(); + + // The idents that make a field type self-referential: the type being derived, and `Self`. + let self_idents: HashSet = [input.ident.clone(), syn::Ident::new("Self", proc_macro2::Span::call_site())] + .into_iter() + .collect(); + let mut thread_aware_path = root_path.clone(); thread_aware_path.segments.push(parse_quote!(ThreadAware)); - for param in &mut generics.params { - let GenericParam::Type(ty_param) = param else { + // Gather the relocated (non-skipped) field types in declaration order, and note whether any + // field is skipped. + let mut relocated_fields: Vec = Vec::new(); + let mut has_skipped_field = false; + collect_relocated_fields(&input.data, &mut relocated_fields, &mut has_skipped_field)?; + + // The generated body relocates each field by calling `::relocate`, so the impl + // owes `: ThreadAware` for every field whose relocation depends on a generic + // parameter. Bounding the field type - rather than the parameters inside it - lets a type + // with an unconditional impl, such as a wrapper that ignores its parameter, satisfy the + // predicate for arguments no per-parameter bound could admit. A field type that reaches no + // parameter is `ThreadAware` (or not) at the definition site and needs no predicate. + let mut emitted_keys: Vec = Vec::new(); + + // Seed with the field-type predicates the author already wrote in a `where` clause, so a + // generated one that duplicates it is suppressed - a redundant predicate trips + // `clippy::trait_duplication_in_bounds` at the author's own declaration. + if let Some(where_clause) = &input.generics.where_clause { + for predicate in &where_clause.predicates { + if let syn::WherePredicate::Type(pt) = predicate + && pt + .bounds + .iter() + .any(|b| matches!(b, syn::TypeParamBound::Trait(t) if is_same_trait(&t.path, &thread_aware_path))) + { + emitted_keys.push(strip_group_paren(&pt.bounded_ty).to_token_stream().to_string()); + } + } + } + + let mut predicates: Vec = Vec::new(); + for field_ty in &relocated_fields { + if !type_reaches_ident(field_ty, &generic_idents) { continue; - }; + } - if usage.relocated.contains(&ty_param.ident) { - let already = ty_param - .bounds + // Choose what to bound. A field whose type names the type being derived (or `Self`) cannot + // be bounded by its own type: `where : ThreadAware` would be a bound on the impl + // under construction and the trait solver overflows on it. Bound the parameters it reaches + // instead - those bottom out at the concrete argument, proving the recursive impl + // inductively. Every other field is bounded by its own type. + let targets: Vec = if type_reaches_ident(field_ty, &self_idents) { + generic_param_idents .iter() - .any(|b| matches!(b, syn::TypeParamBound::Trait(t) if is_same_trait(&t.path, &thread_aware_path))); - if !already { - ty_param.bounds.push(parse_quote!(#thread_aware_path)); + .filter(|¶m| { + let single: HashSet = std::iter::once(param.clone()).collect(); + type_reaches_ident(field_ty, &single) + }) + .map(|param| parse_quote!(#param)) + .collect() + } else { + vec![strip_group_paren(field_ty).clone()] + }; + + for target in targets { + // Two fields owing the same predicate contribute it once; repeating it trips + // `clippy::trait_duplication_in_bounds`. + let key = target.to_token_stream().to_string(); + if emitted_keys.iter().any(|seen| seen == &key) { + continue; } + emitted_keys.push(key); + + // A target that is exactly a parameter the author already bounded by `ThreadAware` needs + // no generated predicate: emitting one would duplicate the author's own bound and trip + // `clippy::trait_duplication_in_bounds` at their declaration. + if let Some(param) = as_bare_param(&target, &generic_idents) + && param_has_thread_aware_bound(&generics, param, &thread_aware_path) + { + continue; + } + + predicates.push(parse_quote!(#target: #thread_aware_path)); + } + } + + if !predicates.is_empty() { + let where_clause = generics.make_where_clause(); + for predicate in predicates { + where_clause.predicates.push(predicate); } } @@ -142,7 +230,7 @@ fn add_bounds(input: &DeriveInput, root_path: &Path) -> syn::Result bool { candidate_idents == emitted_idents || candidate_idents == ["ThreadAware"] } -/// How the fields of a type contribute to the bounds of the generated impl. -#[derive(Default)] -struct GenericUsage { - /// Type parameters the traversal reaches through a relocated field; each is bound by - /// `ThreadAware`. - relocated: HashSet, - - /// Whether any field carries `#[thread_aware(skip)]`, which is what makes the - /// `Self: Send` predicate necessary. - has_skipped_field: bool, +/// Collects the relocated (non-skipped) field types of a struct or enum, in declaration order, +/// and records whether any field carries `#[thread_aware(skip)]`. +/// +/// Mirrors exactly what the body generators relocate: a skipped field is absent from the +/// generated body, so it owes no `ThreadAware` predicate - only the `Self: Send` one. Keeping +/// this in step with `struct_gen`/`enum_gen` is what stops the header and the body disagreeing +/// about which fields are relocated. +fn collect_relocated_fields(data: &Data, out: &mut Vec, has_skipped_field: &mut bool) -> syn::Result<()> { + match data { + Data::Struct(s) => collect_relocated_from_fields(&s.fields, out, has_skipped_field)?, + Data::Enum(e) => { + for v in &e.variants { + collect_relocated_from_fields(&v.fields, out, has_skipped_field)?; + } + } + Data::Union(_) => {} + } + Ok(()) } -#[cfg_attr(coverage_nightly, coverage(off))] // can't figure out how to get to 100% coverage of this function -fn collect_generics_in_fields(fields: &Fields, generics: &syn::Generics, usage: &mut GenericUsage) -> syn::Result<()> { - let generic_idents: HashSet<_> = generics - .params - .iter() - .filter_map(|gp| match gp { - syn::GenericParam::Type(t) => Some(t.ident.clone()), - _ => None, - }) - .collect(); +fn collect_relocated_from_fields(fields: &Fields, out: &mut Vec, has_skipped_field: &mut bool) -> syn::Result<()> { for field in fields { - // Mirror exactly what the body generators skip. A skipped field is absent from the - // generated body, so it needs no `ThreadAware` bound; the `Self: Send` predicate - // covers it instead. Keeping this test identical to the one in `struct_gen`/`enum_gen` - // is what stops the header and the body disagreeing about which fields are relocated. if parse_field_attrs(&field.attrs)?.skip { - usage.has_skipped_field = true; + *has_skipped_field = true; continue; } - collect_generics_in_type(&field.ty, &generic_idents, usage)?; + out.push(field.ty.clone()); } Ok(()) } +/// Strips any outer `Type::Group` and `Type::Paren` layers. +/// +/// A `Type::Group` is the invisible wrapper macro expansion leaves around a captured type; a +/// `Type::Paren` is an explicit `(T)`. Neither changes the type, so the emitted predicate reads +/// more naturally written on what they wrap. +fn strip_group_paren(ty: &Type) -> &Type { + let mut current = ty; + loop { + match current { + Type::Group(g) => current = &g.elem, + Type::Paren(p) => current = &p.elem, + other => return other, + } + } +} + +/// Returns the parameter a field type names directly, when the field type is exactly one of the +/// generic parameters. The caller has already stripped any `Group`/`Paren` layers. +fn as_bare_param<'a>(ty: &'a Type, generic_idents: &HashSet) -> Option<&'a syn::Ident> { + if let Type::Path(TypePath { qself: None, path, .. }) = ty + && let Some(ident) = path.get_ident() + && generic_idents.contains(ident) + { + return Some(ident); + } + None +} + +/// Reports whether the parameter's own declaration already carries an inline `ThreadAware` bound. +/// +/// Only the inline bounds on the parameter are inspected. An equivalent predicate the author wrote +/// in a `where` clause is suppressed separately, by seeding the emitted-predicate set from that +/// `where` clause in `add_bounds`. +fn param_has_thread_aware_bound(generics: &syn::Generics, ident: &syn::Ident, thread_aware_path: &Path) -> bool { + generics.params.iter().any(|param| { + matches!(param, GenericParam::Type(ty_param) + if &ty_param.ident == ident + && ty_param + .bounds + .iter() + .any(|b| matches!(b, syn::TypeParamBound::Trait(t) if is_same_trait(&t.path, thread_aware_path)))) + }) +} + +/// Reports whether `ty` reaches one of `targets` through a shape the generated body relocates +/// through: a path's type arguments, a reference, a tuple, an array, a slice, or a `Group`/`Paren` +/// wrapper. +/// +/// Used two ways: with the generic parameters, to decide whether a field owes a bound at all; and +/// with the type being derived (plus `Self`), to decide whether that bound would be self-referential +/// and must fall back to the reached parameters. `Slice` is traversed because `thread_aware_core` +/// implements `ThreadAware` for `[T]` conditionally on `T`, so a `[T]` (or `Box<[T]>`) field's +/// obligation does reduce to one on the parameter. The shapes left out - `Ptr`, `BareFn`, +/// `TraitObject`, `ImplTrait` - cannot: a safe `fn` pointer implements `ThreadAware` unconditionally, +/// so a parameter carried only for variance inside one (`PhantomData`) owes no bound, +/// and the rest have no impl at all. This keeps the marker-payload idiom bound-free. #[cfg_attr(coverage_nightly, coverage(off))] // can't figure out how to get to 100% coverage of this function -fn collect_generics_in_type(ty: &Type, generic_idents: &HashSet, acc: &mut GenericUsage) -> syn::Result<()> { +fn type_reaches_ident(ty: &Type, targets: &HashSet) -> bool { match ty { - Type::Path(TypePath { path, .. }) => { + Type::Path(TypePath { qself, path, .. }) => { + // A qualified-self type like `::Item` reaches whatever its self type + // (`T`) reaches; the path after `as` names the trait/assoc-item, not the parameter. + if let Some(qself) = qself + && type_reaches_ident(&qself.ty, targets) + { + return true; + } for segment in &path.segments { - if generic_idents.contains(&segment.ident) { - acc.relocated.insert(segment.ident.clone()); + if targets.contains(&segment.ident) { + return true; } if let PathArguments::AngleBracketed(ab) = &segment.arguments { for arg in &ab.args { - if let syn::GenericArgument::Type(t) = arg { - collect_generics_in_type(t, generic_idents, acc)?; + if let syn::GenericArgument::Type(t) = arg + && type_reaches_ident(t, targets) + { + return true; } } } } + false } - Type::Reference(r) => collect_generics_in_type(&r.elem, generic_idents, acc)?, - Type::Tuple(t) => { - for elem in &t.elems { - collect_generics_in_type(elem, generic_idents, acc)?; - } - } - Type::Array(a) => collect_generics_in_type(&a.elem, generic_idents, acc)?, - Type::Group(g) => collect_generics_in_type(&g.elem, generic_idents, acc)?, - Type::Paren(p) => collect_generics_in_type(&p.elem, generic_idents, acc)?, - // Not traversed: `Type::Slice`, `Type::Ptr`, `Type::BareFn`, `Type::TraitObject` and - // `Type::ImplTrait`. A bare `fn` pointer has `ThreadAware` impls in `impls.rs`, but - // they are unconditional - no bound on the argument or return types - so descending - // would emit a bound nothing requires. The rest have no impl, so an enclosing field - // cannot be relocated through one and no bound is owed. - // - // This list is not a mirror of `impls.rs` and should not be read as one: `Array` and - // `Reference` are traversed here although `impls.rs` implements neither, so those emit - // a bound for a field that cannot be relocated at all. The maintenance rule runs one - // way only - adding a *conditional* impl in `impls.rs` for any shape listed above - // means adding the matching arm here, or the header will under-constrain the body. - _ => {} + Type::Reference(r) => type_reaches_ident(&r.elem, targets), + Type::Tuple(t) => t.elems.iter().any(|elem| type_reaches_ident(elem, targets)), + Type::Array(a) => type_reaches_ident(&a.elem, targets), + Type::Slice(s) => type_reaches_ident(&s.elem, targets), + Type::Group(g) => type_reaches_ident(&g.elem, targets), + Type::Paren(p) => type_reaches_ident(&p.elem, targets), + _ => false, } - Ok(()) } diff --git a/crates/thread_aware_macros_impl/tests/derive.rs b/crates/thread_aware_macros_impl/tests/derive.rs index 9f89d1cd7..95afe63b3 100644 --- a/crates/thread_aware_macros_impl/tests/derive.rs +++ b/crates/thread_aware_macros_impl/tests/derive.rs @@ -54,8 +54,9 @@ fn tuple_struct_and_enum() { #[test] #[cfg_attr(miri, ignore)] fn generics_add_bounds() { - // Both parameters gain a ThreadAware bound: the traversal reaches U through the marker's - // type argument exactly as it reaches T directly. + // Each relocated field owes a bound on its own type: the `T` field a bound on `T`, and the + // `PhantomData` field a bound on `PhantomData` (which reduces to `U: Send`), rather than + // a bound on `U` itself. let input = quote! { #[derive(ThreadAware)] struct Gen(T, core::marker::PhantomData); @@ -63,6 +64,108 @@ fn generics_add_bounds() { assert_snapshot!(expand(input)); } +#[test] +#[cfg_attr(miri, ignore)] +fn repeated_field_type_is_bounded_once() { + // Two fields of the same type owe a single predicate; the derive must not repeat it, which + // would trip `clippy::trait_duplication_in_bounds`. + let input = quote! { + #[derive(ThreadAware)] + struct TwoVecs(Vec, Vec); + }; + assert_snapshot!(expand(input)); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn user_where_clause_thread_aware_bound_is_not_duplicated() { + // A field-type predicate the author already wrote in a `where` clause must not be emitted a + // second time, which would trip `clippy::trait_duplication_in_bounds`. + let input = quote! { + #[derive(ThreadAware)] + struct Foo(Vec) where Vec: ThreadAware; + }; + assert_snapshot!(expand(input)); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn recursive_generic_bounds_the_reached_parameter() { + // A field naming the type being derived would make `where : ThreadAware` + // self-referential and overflow; the derive bounds the parameter the field reaches instead. + let input = quote! { + #[derive(ThreadAware)] + struct Node { + value: T, + children: Vec>, + } + }; + assert_snapshot!(expand(input)); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn slice_field_reaches_its_parameter() { + // `[T]` has a conditional `ThreadAware` impl, so a `Box<[T]>` field owes a field-type bound + // rather than silently relying on a sibling to carry it. + let input = quote! { + #[derive(ThreadAware)] + struct SliceHolder(Vec, Box<[T]>); + }; + assert_snapshot!(expand(input)); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn recursive_field_bounds_only_the_parameters_it_reaches() { + // The recursive field reaches `T` but not `U`, so only `T` is bounded (`U` comes from the + // marker field). Bounding every parameter rather than only the reached ones would be wrong. + let input = quote! { + #[derive(ThreadAware)] + struct Tree(core::marker::PhantomData, Vec>); + }; + assert_snapshot!(expand(input)); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn self_reference_through_the_self_keyword_falls_back_to_parameters() { + // `Self` inside a parameter-reaching field is self-referential just like the type's own name, + // so the field must fall back to bounding the parameter it reaches. + let input = quote! { + #[derive(ThreadAware)] + struct SelfRef((T, Vec)); + }; + assert_snapshot!(expand(input)); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn container_bound_override_replaces_inferred_bounds() { + // `#[thread_aware(bound = "...")]` replaces the inferred field-type predicates - the escape + // hatch for a field whose bound the derive can't infer, such as a type alias hiding recursion. + let input = quote! { + #[derive(ThreadAware)] + #[thread_aware(bound = "T: ThreadAware")] + struct Node { + value: T, + children: Children, + } + }; + assert_snapshot!(expand(input)); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn qualified_self_field_reaches_its_parameter() { + // `::Item` reaches `T` through the qself type, so the field owes a bound. + let input = quote! { + #[derive(ThreadAware)] + struct Projected(::Item); + }; + assert_snapshot!(expand(input)); +} + #[test] #[cfg_attr(miri, ignore)] fn generics_prebound_bare_no_dup() { @@ -128,8 +231,8 @@ fn error_unknown_attr() { #[test] #[cfg_attr(miri, ignore)] fn phantom_data_named_fields() { - // PhantomData in named fields is relocated through its own no-op impl, and the parameter - // inside it takes the ordinary bound. + // The `PhantomData` field is relocated through its own no-op impl and owes + // `PhantomData: ThreadAware` (which reduces to `T: Send`). let input = quote! { #[derive(ThreadAware)] struct WithPhantom { @@ -143,8 +246,8 @@ fn phantom_data_named_fields() { #[test] #[cfg_attr(miri, ignore)] fn phantom_data_unnamed_fields() { - // PhantomData in tuple fields is relocated through its own no-op impl, and the parameter - // inside it takes the ordinary bound. + // The `PhantomData` field is relocated through its own no-op impl and owes + // `PhantomData: ThreadAware` (which reduces to `T: Send`). let input = quote! { #[derive(ThreadAware)] struct TupleWithPhantom(Vec, core::marker::PhantomData); @@ -262,8 +365,9 @@ fn generics_paren_adds_bound() { #[test] #[cfg_attr(miri, ignore)] fn phantom_only_generic_gets_thread_aware_bound() { - // A parameter named only inside `PhantomData` takes the ordinary bound, like one reached - // anywhere else. Without it the impl cannot satisfy the `ThreadAware: Send` supertrait. + // The `PhantomData` field owes `PhantomData: ThreadAware`, which reduces to `U: Send` - + // the exact obligation the generated body needs, and enough for the `ThreadAware: Send` + // supertrait. let input = quote! { #[derive(ThreadAware)] struct DirectPhantom(T, core::marker::PhantomData); @@ -302,8 +406,9 @@ fn skipped_generic_field_gets_self_send_predicate() { #[test] #[cfg_attr(miri, ignore)] fn relocated_and_phantom_param_shares_one_bound() { - // A parameter reached both directly and through a marker's type argument takes a single - // `ThreadAware` bound - the two traversal paths converge on the same parameter. + // The direct `T` field owes `T: ThreadAware`; the `PhantomData` marker reaches the + // parameter only through a function pointer, which is `ThreadAware` unconditionally, so it owes + // no bound. The parameter is bound exactly once. let input = quote! { #[derive(ThreadAware)] struct RelocatedAndPhantom<'a, T: 'a>(T, core::marker::PhantomData); @@ -338,7 +443,7 @@ fn prebound_bare_thread_aware_assumed_real() { #[test] #[cfg_attr(miri, ignore)] fn no_skipped_field_means_no_self_send_predicate() { - // Every field is relocated, so `Self: Send` follows from the per-parameter bounds. + // Every field is relocated, so `Self: Send` follows from the field-type bounds. let input = quote! { #[derive(ThreadAware)] struct AllRelocated(T, U); @@ -367,9 +472,10 @@ fn user_where_clause_is_preserved() { #[test] #[cfg_attr(miri, ignore)] fn generics_lifetime_and_const_params_untouched() { - // Only type parameters can carry bounds; lifetimes and const generics are skipped. - // The field shape is one the crate can actually relocate, so the pinned expansion is one - // that compiles - a snapshot of an uncompilable expansion proves nothing. + // The generated predicate lands on the field type, leaving the lifetime and const parameters + // untouched in the impl header. The field shape is one the crate can actually relocate, so the + // pinned expansion is one that compiles - a snapshot of an uncompilable expansion proves + // nothing. let input = quote! { #[derive(ThreadAware)] struct Mixed<'a, const N: usize, T: Sync>(Tracker, core::marker::PhantomData<(&'a T, [u8; N])>); diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__container_bound_override_replaces_inferred_bounds.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__container_bound_override_replaces_inferred_bounds.snap new file mode 100644 index 000000000..ecd8e1522 --- /dev/null +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__container_bound_override_replaces_inferred_bounds.snap @@ -0,0 +1,25 @@ +--- +source: crates/thread_aware_macros_impl/tests/derive.rs +expression: expand(input) +--- +impl ::thread_aware::ThreadAware for Node +where + T: ThreadAware, +{ + fn relocate( + &mut self, + __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, + __thread_aware_destination: &::thread_aware::Thread, + ) { + ::thread_aware::ThreadAware::relocate( + &mut self.value, + __thread_aware_source, + __thread_aware_destination, + ); + ::thread_aware::ThreadAware::relocate( + &mut self.children, + __thread_aware_source, + __thread_aware_destination, + ); + } +} diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__enum_named_phantom_data.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__enum_named_phantom_data.snap index 5e0d5bc06..fe9fb07dd 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__enum_named_phantom_data.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__enum_named_phantom_data.snap @@ -2,10 +2,11 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl< - T: ::thread_aware::ThreadAware, - U: ::thread_aware::ThreadAware, -> ::thread_aware::ThreadAware for EnumNamedPhantom { +impl ::thread_aware::ThreadAware for EnumNamedPhantom +where + core::marker::PhantomData: ::thread_aware::ThreadAware, + core::marker::PhantomData: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__enum_unnamed_phantom_data.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__enum_unnamed_phantom_data.snap index 637f1f91b..faaddf65c 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__enum_unnamed_phantom_data.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__enum_unnamed_phantom_data.snap @@ -2,10 +2,11 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl< - T: ::thread_aware::ThreadAware, - U: ::thread_aware::ThreadAware, -> ::thread_aware::ThreadAware for EnumUnnamedPhantom { +impl ::thread_aware::ThreadAware for EnumUnnamedPhantom +where + core::marker::PhantomData: ::thread_aware::ThreadAware, + core::marker::PhantomData: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_add_bounds.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_add_bounds.snap index b95bc540f..a7e20f97a 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_add_bounds.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_add_bounds.snap @@ -2,10 +2,11 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl< +impl ::thread_aware::ThreadAware for Gen +where T: ::thread_aware::ThreadAware, - U: ::thread_aware::ThreadAware, -> ::thread_aware::ThreadAware for Gen { + core::marker::PhantomData: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_array_usage_adds_bound.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_array_usage_adds_bound.snap index 8a53c4510..9a8e601de 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_array_usage_adds_bound.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_array_usage_adds_bound.snap @@ -2,7 +2,10 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl ::thread_aware::ThreadAware for ArrUse { +impl ::thread_aware::ThreadAware for ArrUse +where + [T; 2]: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_group_usage_adds_bound.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_group_usage_adds_bound.snap index 1328ba68c..02abd7dea 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_group_usage_adds_bound.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_group_usage_adds_bound.snap @@ -2,7 +2,10 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: rendered --- -impl ::thread_aware::ThreadAware for GroupUse { +impl ::thread_aware::ThreadAware for GroupUse +where + T: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_lifetime_and_const_params_untouched.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_lifetime_and_const_params_untouched.snap index 423698b51..8def843be 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_lifetime_and_const_params_untouched.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_lifetime_and_const_params_untouched.snap @@ -2,11 +2,10 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl< - 'a, - const N: usize, - T: Sync + ::thread_aware::ThreadAware, -> ::thread_aware::ThreadAware for Mixed<'a, N, T> { +impl<'a, const N: usize, T: Sync> ::thread_aware::ThreadAware for Mixed<'a, N, T> +where + core::marker::PhantomData<(&'a T, [u8; N])>: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_paren_adds_bound.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_paren_adds_bound.snap index 8d1d63774..77a999de6 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_paren_adds_bound.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_paren_adds_bound.snap @@ -2,8 +2,10 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl ::thread_aware::ThreadAware -for ParenthesizedType { +impl ::thread_aware::ThreadAware for ParenthesizedType +where + T: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_ref_usage_adds_bound.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_ref_usage_adds_bound.snap index 34db83024..91c198f98 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_ref_usage_adds_bound.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_ref_usage_adds_bound.snap @@ -2,7 +2,10 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl ::thread_aware::ThreadAware for RefUse { +impl ::thread_aware::ThreadAware for RefUse +where + &'static T: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_tuple_usage_adds_bound.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_tuple_usage_adds_bound.snap index c99daae24..2761e39ba 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_tuple_usage_adds_bound.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__generics_tuple_usage_adds_bound.snap @@ -2,7 +2,10 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl ::thread_aware::ThreadAware for TupUse { +impl ::thread_aware::ThreadAware for TupUse +where + (T,): ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__no_skipped_field_means_no_self_send_predicate.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__no_skipped_field_means_no_self_send_predicate.snap index dc13760ec..a42cdbc64 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__no_skipped_field_means_no_self_send_predicate.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__no_skipped_field_means_no_self_send_predicate.snap @@ -2,10 +2,11 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl< +impl ::thread_aware::ThreadAware for AllRelocated +where T: ::thread_aware::ThreadAware, U: ::thread_aware::ThreadAware, -> ::thread_aware::ThreadAware for AllRelocated { +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_data_named_fields.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_data_named_fields.snap index 8677db0ad..15200af79 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_data_named_fields.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_data_named_fields.snap @@ -2,7 +2,10 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl ::thread_aware::ThreadAware for WithPhantom { +impl ::thread_aware::ThreadAware for WithPhantom +where + core::marker::PhantomData: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_data_unnamed_fields.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_data_unnamed_fields.snap index febf2678e..51b32e44e 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_data_unnamed_fields.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_data_unnamed_fields.snap @@ -2,8 +2,10 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl ::thread_aware::ThreadAware -for TupleWithPhantom { +impl ::thread_aware::ThreadAware for TupleWithPhantom +where + core::marker::PhantomData: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_only_generic_gets_thread_aware_bound.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_only_generic_gets_thread_aware_bound.snap index 35d54aa2b..cc1791e05 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_only_generic_gets_thread_aware_bound.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__phantom_only_generic_gets_thread_aware_bound.snap @@ -2,10 +2,11 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl< +impl ::thread_aware::ThreadAware for DirectPhantom +where T: ::thread_aware::ThreadAware, - U: ::thread_aware::ThreadAware, -> ::thread_aware::ThreadAware for DirectPhantom { + core::marker::PhantomData: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__qualified_self_field_reaches_its_parameter.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__qualified_self_field_reaches_its_parameter.snap new file mode 100644 index 000000000..60fef15a3 --- /dev/null +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__qualified_self_field_reaches_its_parameter.snap @@ -0,0 +1,20 @@ +--- +source: crates/thread_aware_macros_impl/tests/derive.rs +expression: expand(input) +--- +impl ::thread_aware::ThreadAware for Projected +where + ::Item: ::thread_aware::ThreadAware, +{ + fn relocate( + &mut self, + __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, + __thread_aware_destination: &::thread_aware::Thread, + ) { + ::thread_aware::ThreadAware::relocate( + &mut self.0, + __thread_aware_source, + __thread_aware_destination, + ); + } +} diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__recursive_field_bounds_only_the_parameters_it_reaches.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__recursive_field_bounds_only_the_parameters_it_reaches.snap new file mode 100644 index 000000000..3b5d0a2dc --- /dev/null +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__recursive_field_bounds_only_the_parameters_it_reaches.snap @@ -0,0 +1,26 @@ +--- +source: crates/thread_aware_macros_impl/tests/derive.rs +expression: expand(input) +--- +impl ::thread_aware::ThreadAware for Tree +where + core::marker::PhantomData: ::thread_aware::ThreadAware, + T: ::thread_aware::ThreadAware, +{ + fn relocate( + &mut self, + __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, + __thread_aware_destination: &::thread_aware::Thread, + ) { + ::thread_aware::ThreadAware::relocate( + &mut self.0, + __thread_aware_source, + __thread_aware_destination, + ); + ::thread_aware::ThreadAware::relocate( + &mut self.1, + __thread_aware_source, + __thread_aware_destination, + ); + } +} diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__recursive_generic_bounds_the_reached_parameter.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__recursive_generic_bounds_the_reached_parameter.snap new file mode 100644 index 000000000..1b4fad80b --- /dev/null +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__recursive_generic_bounds_the_reached_parameter.snap @@ -0,0 +1,25 @@ +--- +source: crates/thread_aware_macros_impl/tests/derive.rs +expression: expand(input) +--- +impl ::thread_aware::ThreadAware for Node +where + T: ::thread_aware::ThreadAware, +{ + fn relocate( + &mut self, + __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, + __thread_aware_destination: &::thread_aware::Thread, + ) { + ::thread_aware::ThreadAware::relocate( + &mut self.value, + __thread_aware_source, + __thread_aware_destination, + ); + ::thread_aware::ThreadAware::relocate( + &mut self.children, + __thread_aware_source, + __thread_aware_destination, + ); + } +} diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__relocated_and_phantom_param_shares_one_bound.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__relocated_and_phantom_param_shares_one_bound.snap index e7241d230..83505e33a 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__relocated_and_phantom_param_shares_one_bound.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__relocated_and_phantom_param_shares_one_bound.snap @@ -2,8 +2,10 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl<'a, T: 'a + ::thread_aware::ThreadAware> ::thread_aware::ThreadAware -for RelocatedAndPhantom<'a, T> { +impl<'a, T: 'a> ::thread_aware::ThreadAware for RelocatedAndPhantom<'a, T> +where + T: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__repeated_field_type_is_bounded_once.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__repeated_field_type_is_bounded_once.snap new file mode 100644 index 000000000..dbf9464f3 --- /dev/null +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__repeated_field_type_is_bounded_once.snap @@ -0,0 +1,25 @@ +--- +source: crates/thread_aware_macros_impl/tests/derive.rs +expression: expand(input) +--- +impl ::thread_aware::ThreadAware for TwoVecs +where + Vec: ::thread_aware::ThreadAware, +{ + fn relocate( + &mut self, + __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, + __thread_aware_destination: &::thread_aware::Thread, + ) { + ::thread_aware::ThreadAware::relocate( + &mut self.0, + __thread_aware_source, + __thread_aware_destination, + ); + ::thread_aware::ThreadAware::relocate( + &mut self.1, + __thread_aware_source, + __thread_aware_destination, + ); + } +} diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__self_reference_through_the_self_keyword_falls_back_to_parameters.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__self_reference_through_the_self_keyword_falls_back_to_parameters.snap new file mode 100644 index 000000000..6db4aac29 --- /dev/null +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__self_reference_through_the_self_keyword_falls_back_to_parameters.snap @@ -0,0 +1,20 @@ +--- +source: crates/thread_aware_macros_impl/tests/derive.rs +expression: expand(input) +--- +impl ::thread_aware::ThreadAware for SelfRef +where + T: ::thread_aware::ThreadAware, +{ + fn relocate( + &mut self, + __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, + __thread_aware_destination: &::thread_aware::Thread, + ) { + ::thread_aware::ThreadAware::relocate( + &mut self.0, + __thread_aware_source, + __thread_aware_destination, + ); + } +} diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__slice_field_reaches_its_parameter.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__slice_field_reaches_its_parameter.snap new file mode 100644 index 000000000..25b1c3a31 --- /dev/null +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__slice_field_reaches_its_parameter.snap @@ -0,0 +1,26 @@ +--- +source: crates/thread_aware_macros_impl/tests/derive.rs +expression: expand(input) +--- +impl ::thread_aware::ThreadAware for SliceHolder +where + Vec: ::thread_aware::ThreadAware, + Box<[T]>: ::thread_aware::ThreadAware, +{ + fn relocate( + &mut self, + __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, + __thread_aware_destination: &::thread_aware::Thread, + ) { + ::thread_aware::ThreadAware::relocate( + &mut self.0, + __thread_aware_source, + __thread_aware_destination, + ); + ::thread_aware::ThreadAware::relocate( + &mut self.1, + __thread_aware_source, + __thread_aware_destination, + ); + } +} diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__unrelated_trait_named_thread_aware_does_not_suppress_the_bound.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__unrelated_trait_named_thread_aware_does_not_suppress_the_bound.snap index 1eb84a6f3..63735b3f5 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__unrelated_trait_named_thread_aware_does_not_suppress_the_bound.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__unrelated_trait_named_thread_aware_does_not_suppress_the_bound.snap @@ -2,8 +2,10 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl ::thread_aware::ThreadAware -for CustomTa { +impl ::thread_aware::ThreadAware for CustomTa +where + T: ::thread_aware::ThreadAware, +{ fn relocate( &mut self, __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__user_where_clause_is_preserved.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__user_where_clause_is_preserved.snap index d2d152cf3..62902de27 100644 --- a/crates/thread_aware_macros_impl/tests/snapshots/derive__user_where_clause_is_preserved.snap +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__user_where_clause_is_preserved.snap @@ -2,12 +2,11 @@ source: crates/thread_aware_macros_impl/tests/derive.rs expression: expand(input) --- -impl< - T: ::thread_aware::ThreadAware, - U: ::thread_aware::ThreadAware, -> ::thread_aware::ThreadAware for WithWhere +impl ::thread_aware::ThreadAware for WithWhere where T: Clone, + T: ::thread_aware::ThreadAware, + core::marker::PhantomData: ::thread_aware::ThreadAware, { fn relocate( &mut self, diff --git a/crates/thread_aware_macros_impl/tests/snapshots/derive__user_where_clause_thread_aware_bound_is_not_duplicated.snap b/crates/thread_aware_macros_impl/tests/snapshots/derive__user_where_clause_thread_aware_bound_is_not_duplicated.snap new file mode 100644 index 000000000..a2db350a7 --- /dev/null +++ b/crates/thread_aware_macros_impl/tests/snapshots/derive__user_where_clause_thread_aware_bound_is_not_duplicated.snap @@ -0,0 +1,20 @@ +--- +source: crates/thread_aware_macros_impl/tests/derive.rs +expression: expand(input) +--- +impl ::thread_aware::ThreadAware for Foo +where + Vec: ThreadAware, +{ + fn relocate( + &mut self, + __thread_aware_source: ::core::option::Option<&::thread_aware::Thread>, + __thread_aware_destination: &::thread_aware::Thread, + ) { + ::thread_aware::ThreadAware::relocate( + &mut self.0, + __thread_aware_source, + __thread_aware_destination, + ); + } +}