Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions crates/thread_aware/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <field type>: 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<T: ?Sized + Send> ThreadAware for PhantomData<T>`, and a parameter the traversal
/// reaches inside it - the `U` of `PhantomData<U>` - 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<T>` 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<fn(*const T)>` adds no bound at all:
///
/// ```rust
/// # use core::marker::PhantomData;
Expand All @@ -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
Expand Down
142 changes: 142 additions & 0 deletions crates/thread_aware/tests/derive_compiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Rc<()>>` 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<T>(PhantomData<fn() -> T>);

impl<T> thread_aware::ThreadAware for UnconditionalWrapper<T> {
fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {}
}

/// The derive owes `UnconditionalWrapper<PhantomData<T>>: ThreadAware`, which that impl satisfies
/// for every `T` - not `T: ThreadAware`, which `Rc<()>` cannot meet.
#[derive(ThreadAware)]
struct OuterWrapper<T>(UnconditionalWrapper<PhantomData<T>>);

#[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::<OuterWrapper<Rc<()>>>();

let (source, destination) = thread_pair();
let mut value = OuterWrapper::<Rc<()>>(UnconditionalWrapper(PhantomData));
value.relocate(source.as_ref(), &destination);
}

/// A recursive generic type: the `children: Vec<RecursiveNode<T>>` field names the type being
/// derived, so the derive bounds the parameter it reaches (`T`) rather than the whole field type -
/// a `where Vec<RecursiveNode<T>>: 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<T> {
value: T,
children: Vec<RecursiveNode<T>>,
}

/// 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<T> {
Nil,
Cons(T, Box<RecursiveList<T>>),
}

/// 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<T> {
values: Vec<T>,
rest: Box<[T]>,
}

#[test]
fn recursive_and_slice_generics_compile_and_relocate() {
assert_thread_aware::<RecursiveNode<Tracker>>();
assert_thread_aware::<RecursiveList<Tracker>>();
assert_thread_aware::<SliceSibling<Tracker>>();

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, `<T as ItemProvider>::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<T: ItemProvider>(<T as ItemProvider>::Item);

#[test]
fn qualified_self_projection_field_is_bounded_and_relocates() {
let (source, destination) = thread_pair();
let mut value = ProjectedField::<ConcreteProvider>(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<T>`, not that it expands to
/// `Vec<AliasNode<T>>`, so it can't tell the field is a recursive self-reference. Left to infer, it
/// would emit the circular `where AliasChildren<T>: ThreadAware`. The `#[thread_aware(bound = ...)]`
/// escape hatch replaces the inferred bounds with the parameter bound the body actually needs.
type AliasChildren<T> = Vec<AliasNode<T>>;

#[derive(ThreadAware)]
#[thread_aware(bound = "T: thread_aware::ThreadAware")]
struct AliasNode<T> {
value: T,
children: AliasChildren<T>,
}

#[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::<AliasNode<Tracker>>();

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.
///
Expand Down
93 changes: 93 additions & 0 deletions crates/thread_aware_macros_impl/src/field_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<syn::WherePredicate>>,
}

/// Parses the `thread_aware` attributes on the derive input (the struct or enum).
pub(crate) fn parse_container_attrs(attrs: &[Attribute]) -> syn::Result<ContainerAttrCfg> {
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<syn::WherePredicate, syn::Token![,]> =
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<FieldAttrCfg> {
let mut cfg = FieldAttrCfg::default();
Expand Down Expand Up @@ -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<Attribute> = 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<Attribute> = 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<Attribute> = 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<Attribute> = 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<Attribute> = 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<Attribute> = 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"));
}
}
Loading
Loading