Skip to content

fix(thread_aware_macros): express derive bounds on the relocated field type - #740

Closed
Pato Sandaña (psandana) wants to merge 10 commits into
mainfrom
u/psandana/thread-aware-derive-field-type-bounds
Closed

fix(thread_aware_macros): express derive bounds on the relocated field type#740
Pato Sandaña (psandana) wants to merge 10 commits into
mainfrom
u/psandana/thread-aware-derive-field-type-bounds

Conversation

@psandana

@psandana Pato Sandaña (psandana) commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What & why

#[derive(ThreadAware)] inferred its bounds by collecting every generic parameter a relocated field's type reaches and bounding each by ThreadAware. That predicate is on the parameter, not on the obligation the generated body actually has — the body relocates the whole field, so what it needs is <field type>: ThreadAware.

Bounding the parameter is too strong for a wrapper that implements ThreadAware unconditionally:

struct Wrapper<T>(PhantomData<fn() -> T>);
impl<T> ThreadAware for Wrapper<T> {
    fn relocate(&mut self, _: Option<&Thread>, _: &Thread) {}
}

#[derive(ThreadAware)]
struct Outer<T>(Wrapper<PhantomData<T>>);

The body only relocates the whole Wrapper<PhantomData<T>> field, which the wrapper's own impl already satisfies. The old derive additionally emitted T: ThreadAware, so Outer<Rc<()>> was rejected even though Rc<()> never needs to be ThreadAware.

Change

add_bounds now emits, for each relocated field whose type reaches a generic parameter, a where <field type>: ThreadAware predicate — the exact obligation the generated body discharges — replacing the per-parameter predicates.

  • Slice traversal added: the traversal now also descends into Type::Slice, so a Box<[T]> field emits where Box<[T]>: ThreadAware ([T] has a conditional ThreadAware impl in thread_aware_core). A marker payload behind a function pointer (PhantomData<fn(*const T)>) still owes no bound, so the raw-pointer variance idiom stays bound-free.
  • Recursive generics: a field whose type names the type being derived (or Self) would make where <field type>: ThreadAware self-referential and overflow, so it falls back to bounding the parameters it reaches — recursive types (struct Node<T> { children: Vec<Node<T>> }) still derive.
  • Alias escape hatch: a type alias (type Children<T> = Vec<Node<T>>) can hide that self-reference from the syntactic derive, which can't resolve aliases. #[thread_aware(bound = "...")] replaces the inferred bounds with the author's own for that case.
  • De-duplication: a field that is exactly a parameter the author already bounded by ThreadAware emits nothing, and predicates the author already wrote in a where clause are not repeated, avoiding clippy::trait_duplication_in_bounds.
  • The where Self: Send predicate for skipped fields is unchanged.

Acceptance criteria

  • Compile regression for the unconditional-wrapper case.
  • Compile regressions for recursive generics, a slice-bearing sibling, and the alias-hidden recursion with the bound escape hatch.
  • Existing derive_compiles.rs coverage passes unchanged.
  • Snapshots and the derive's "Generic Bounds" / "Attributes" documentation updated.
  • thread_aware, thread_aware_macros_impl, and the four consuming crates build and lint clean locally.

Refs AB#7783612. Follow-up to #678 (raised in review by Sander Saares).

Copilot AI lite review requested due to automatic review settings September 8, 2026 01:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The updated bound generation aligns with the derive’s actual field-relocation obligations, and the PR adds a targeted compile-regression test plus consistent snapshot/doc updates.

Pull request overview

This PR fixes #[derive(ThreadAware)] in thread_aware_macros_impl to emit generic bounds that match the derive’s actual obligation: the relocated field type must implement ThreadAware, rather than requiring every reached generic parameter to be ThreadAware (which was overly strict for unconditional wrappers).

Changes:

  • Update derive bound inference to emit where <field type>: ::thread_aware::ThreadAware predicates for relocated fields whose types reach generic parameters.
  • Add a compile-regression test covering an unconditional-wrapper field that should derive for non-ThreadAware / non-Send generic arguments.
  • Refresh macro expansion snapshots and update the “Generic Bounds” documentation to reflect the field-type predicate model.
File summaries
File Description
crates/thread_aware/tests/derive_compiles.rs Adds a regression test ensuring unconditional wrapper fields don’t force T: ThreadAware.
crates/thread_aware/src/lib.rs Updates derive documentation to describe field-type where predicates.
crates/thread_aware_macros_impl/src/lib.rs Reworks add_bounds to collect relocated field types and emit where <field type>: ThreadAware predicates with de-duplication.
crates/thread_aware_macros_impl/tests/derive.rs Updates test commentary to match the new field-type bound model.
crates/thread_aware_macros_impl/tests/snapshots/*.snap Updates expected expansions to reflect where-clause field-type bounds (multiple snapshot adjustments).
Review details
  • Files reviewed: 20/20 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.0%. Comparing base (9794095) to head (04554f5).

Additional details and impacted files
@@           Coverage Diff           @@
##            main     #740    +/-   ##
=======================================
  Coverage   99.9%   100.0%            
=======================================
  Files        634      634            
  Lines      84746    84892   +146     
=======================================
+ Hits       84744    84892   +148     
+ Misses         2        0     -2     
Flag Coverage Δ
linux 93.4% <100.0%> (?)
linux-arm 93.4% <100.0%> (?)
windows 93.5% <100.0%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI review requested due to automatic review settings September 8, 2026 12:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The macro currently appends generated where predicates without checking for identical user-provided where predicates, which can introduce redundant bounds in the expanded impl.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

crates/thread_aware_macros_impl/src/lib.rs:166

  • add_bounds appends generated where predicates without checking whether an identical predicate is already present in the user's where clause. If the user already wrote (for example) where T: ThreadAware and a relocated field type is T, this will add a second redundant T: ThreadAware predicate.
  • Files reviewed: 21/21 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Pato Sandaña (psandana) added a commit that referenced this pull request Sep 9, 2026
… where clause

Addresses review feedback on PR #740: `add_bounds` appended a generated
`where <field type>: ThreadAware` predicate even when the author had already
written an identical predicate in their own `where` clause (e.g. a field `Vec<T>`
with `where Vec<T>: ThreadAware`), producing a redundant bound that trips
`clippy::trait_duplication_in_bounds`.

Seed the emitted-predicate set from the author's `where` clause so a duplicate is
suppressed, and add a snapshot pinning it. The inline-bound path is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 9, 2026 00:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The bound-generation change matches the documented/implemented relocation obligation, adds a focused compile regression test, and updates snapshots/docs consistently without introducing observable correctness or API issues in the reviewed diffs.

Review details
  • Files reviewed: 22/22 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@psandana

Copy link
Copy Markdown
Contributor Author

Addressed the suggestion in fcf1ce2: add_bounds now seeds its emitted-predicate set from the author's own where clause, so a generated where <field type>: ThreadAware predicate that duplicates one the author already wrote (e.g. a Vec<T> field with where Vec<T>: ThreadAware) is suppressed rather than repeated. Added a snapshot pinning it (user_where_clause_thread_aware_bound_is_not_duplicated).

continue;
}

predicates.push(parse_quote!(#bound_ty: #thread_aware_path));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Recursive generic types no longer derive: the emitted predicate is self-referential

When a relocated field''s type mentions the type being derived, where <field type>: ThreadAware is the impl being defined, so trait selection cycles.

#[derive(ThreadAware)]
pub struct Node<T> {
    value: T,
    children: Vec<Node<T>>,
}

Expansion at this commit:

impl<T> ::thread_aware::ThreadAware for Node<T>
where
    T: ::thread_aware::ThreadAware,
    Vec<Node<T>>: ::thread_aware::ThreadAware,   // requires Node<T>: ThreadAware
{ /* ... */ }

assert_ta::<Node<u32>>() then fails with error[E0275]: overflow evaluating the requirement Vec<Node<u32>>: ThreadAware. Enums are hit the same way: enum List<T> { Nil, Cons(T, Box<List<T>>) } gives E0275 ... Box<List<u32>>: ThreadAware.

Both compile at the merge base (efd8178), where the header was impl<T: ThreadAware>: a bound on the parameter bottoms out at the concrete argument, so the body''s Vec<Node<T>>: ThreadAware obligation is discharged inductively from the impl under construction. Verified by building the same source against both revisions.

Worth noting that the same type spelled Vec<Self> still derives and instantiates, because Self is not a generic ident so no predicate is emitted - identical types, opposite outcomes, decided purely by spelling.

Suggested direction: skip the field-type predicate when the field type names the deriving type (or Self) and fall back to bounding the parameters that field reaches. A derive_compiles.rs case that actually requires Node<u32>: ThreadAware would pin this down - nothing in the current suite exercises a recursive generic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch — fixed in 535e5e7. A field whose type names the deriving type (or Self) now bounds the parameters it reaches rather than the field type, so Node<T> / List<T> derive again without the E0275 overflow — the parameter bound bottoms out at the concrete argument. Added a derive_compiles.rs regression covering a recursive struct and enum, plus a snapshot. Vec<Self> still emits nothing (it reaches no parameter), which stays correct: the impl is then unconditional.

/// through: a path's type arguments, a reference, a tuple, an array, or a `Group`/`Paren` wrapper.
///
/// When it does, the field's `ThreadAware`-ness depends on that parameter and the impl owes
/// `<field type>: ThreadAware`. The shapes deliberately left out - `Slice`, `Ptr`, `BareFn`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 A slice-bearing sibling field loses the bound it previously got for free

type_reaches_param does not traverse Type::Slice, so Box<[T]> reaches no parameter and owes no predicate. Under the old per-parameter model that was harmless: a sibling Vec<T> field emitted T: ThreadAware on the impl, which also discharged the slice field''s obligation. A field-type predicate does not do that - Vec<T>: ThreadAware does not imply T: ThreadAware inside the body.

#[derive(ThreadAware)]
pub struct Mixed<T> {
    values: Vec<T>,
    rest: Box<[T]>,
}

Expansion at this commit:

impl<T> ::thread_aware::ThreadAware for Mixed<T>
where
    Vec<T>: ::thread_aware::ThreadAware,   // nothing emitted for `rest`
{ /* ... */ }

This fails with error[E0277]: the trait bound T: ThreadAware is not satisfied (required for [T] to implement ThreadAware, then Box<[T]>), plus the matching T: Send error. It compiles at the merge base efd8178. Verified against both revisions.

A lone Box<[T]> field fails on both sides, so the traversal gap itself is pre-existing - but this shape is a regression, because the sibling bound that used to cover it is gone.

Relatedly, the comment just above states that the omitted shapes are ones "the rest have no impl at all". That is not true of Slice: thread_aware_core''s impls.rs has impl<T: ThreadAware> ThreadAware for [T], a conditional impl - precisely the case the maintenance rule removed in this diff said required a matching arm here. Adding a Type::Slice arm makes Mixed emit where Box<[T]>: ThreadAware and compile again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 535e5e7type_reaches_param now traverses Type::Slice, so Box<[T]> emits where Box<[T]>: ThreadAware and the sibling case compiles again. Also corrected the stale comment that claimed the omitted shapes have no impl ([T] has a conditional one). Added a SliceSibling case to derive_compiles.rs and a snapshot.

Pato Sandaña (psandana) added a commit that referenced this pull request Sep 9, 2026
…eriving

Addresses two regressions found by @wukchung in review of PR #740:

1. Recursive generics overflowed. A field whose type names the type being
   derived (e.g. `children: Vec<Node<T>>`) produced `where Vec<Node<T>>:
   ThreadAware`, a bound on the impl under construction, so trait selection
   cycled (`E0275`). Now such a field is bounded by the parameters it reaches
   (`T`) instead, which bottom out at the concrete argument and prove the
   recursive impl inductively. `add_bounds` chooses per field: the field type
   normally, the reached parameters when the field type is self-referential.

2. A slice-bearing sibling lost its bound. `type_reaches_param` did not
   traverse `Type::Slice`, so `Box<[T]>` owed no predicate; under the old
   per-parameter model a sibling `Vec<T>` field's `T: ThreadAware` covered it,
   but a field-type predicate does not. `[T]` has a conditional impl in
   `thread_aware_core`, so `Slice` is now traversed and `Box<[T]>` emits
   `where Box<[T]>: ThreadAware`. Corrected the stale comment that claimed the
   omitted shapes have no impl.

Adds snapshots for both shapes and a `derive_compiles.rs` regression covering a
recursive struct, a recursive enum, and a slice sibling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 9, 2026 14:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The bound-generation change is well-scoped, aligns the emitted predicates with the derived body’s actual obligations, and is backed by targeted compile-regression tests plus updated snapshots/documentation.

Review details
  • Files reviewed: 24/24 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Pato Sandaña (psandana) added a commit that referenced this pull request Sep 9, 2026
…-reference checks

Fixes the mutation-testing gate on PR #740. The recursive-generics fix added two
new structural-traversal helpers (`type_names_deriving_type`, `collect_params_reached`)
whose match arms were not exercised by tests, so `cargo mutants` reported them as
missed.

Generalize the existing (already mutation-tested) traversal to `type_reaches_ident`,
which takes a target ident set, and use it three ways: with the generic parameters
(does the field owe a bound?), with the derived type name plus `Self` (is the bound
self-referential?), and per parameter (which parameters does a self-referential field
reach?). The two duplicate helpers are deleted.

Adds snapshots pinning that a recursive field bounds only the parameters it reaches
(not every parameter) and that `Self` is detected like the type's own name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 9, 2026 17:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The derive-bound change is aligned with the generated body’s actual obligations and is backed by updated documentation, targeted regression tests, and refreshed macro expansion snapshots.

Review details
  • Files reviewed: 26/26 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Comment thread crates/thread_aware/src/lib.rs Outdated
/// 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:
/// The predicate lands on the field type as written, not on the parameters inside it. A

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this reads like AI mumbo-jumbo. Make it more concise and understandable for humans?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — rewrote it (1908886). Trimmed the mechanism-heavy prose down to the three things a user actually needs: one where <field type>: ThreadAware bound per relocated field, where Self: Send for skipped fields, and the PhantomData<fn(*const T)> marker escape hatch. Also updated the branch onto current main. Thanks for the approve!

Pato Sandaña (psandana) added a commit that referenced this pull request Sep 10, 2026
… where clause

Addresses review feedback on PR #740: `add_bounds` appended a generated
`where <field type>: ThreadAware` predicate even when the author had already
written an identical predicate in their own `where` clause (e.g. a field `Vec<T>`
with `where Vec<T>: ThreadAware`), producing a redundant bound that trips
`clippy::trait_duplication_in_bounds`.

Seed the emitted-predicate set from the author's `where` clause so a duplicate is
suppressed, and add a snapshot pinning it. The inline-bound path is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pato Sandaña (psandana) added a commit that referenced this pull request Sep 10, 2026
…eriving

Addresses two regressions found by @wukchung in review of PR #740:

1. Recursive generics overflowed. A field whose type names the type being
   derived (e.g. `children: Vec<Node<T>>`) produced `where Vec<Node<T>>:
   ThreadAware`, a bound on the impl under construction, so trait selection
   cycled (`E0275`). Now such a field is bounded by the parameters it reaches
   (`T`) instead, which bottom out at the concrete argument and prove the
   recursive impl inductively. `add_bounds` chooses per field: the field type
   normally, the reached parameters when the field type is self-referential.

2. A slice-bearing sibling lost its bound. `type_reaches_param` did not
   traverse `Type::Slice`, so `Box<[T]>` owed no predicate; under the old
   per-parameter model a sibling `Vec<T>` field's `T: ThreadAware` covered it,
   but a field-type predicate does not. `[T]` has a conditional impl in
   `thread_aware_core`, so `Slice` is now traversed and `Box<[T]>` emits
   `where Box<[T]>: ThreadAware`. Corrected the stale comment that claimed the
   omitted shapes have no impl.

Adds snapshots for both shapes and a `derive_compiles.rs` regression covering a
recursive struct, a recursive enum, and a slice sibling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pato Sandaña (psandana) added a commit that referenced this pull request Sep 10, 2026
…-reference checks

Fixes the mutation-testing gate on PR #740. The recursive-generics fix added two
new structural-traversal helpers (`type_names_deriving_type`, `collect_params_reached`)
whose match arms were not exercised by tests, so `cargo mutants` reported them as
missed.

Generalize the existing (already mutation-tested) traversal to `type_reaches_ident`,
which takes a target ident set, and use it three ways: with the generic parameters
(does the field owe a bound?), with the derived type name plus `Self` (is the bound
self-referential?), and per parameter (which parameters does a self-referential field
reach?). The two duplicate helpers are deleted.

Adds snapshots pinning that a recursive field bounds only the parameters it reaches
(not every parameter) and that `Self` is detected like the type's own name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 10, 2026 16:22
@psandana
Pato Sandaña (psandana) force-pushed the u/psandana/thread-aware-derive-field-type-bounds branch from aad5481 to 1908886 Compare September 10, 2026 16:22
@psandana

Copy link
Copy Markdown
Contributor Author

Sure. The motivating case is a wrapper that implements ThreadAware unconditionally (ignoring its type parameter) — the merge-base derive rejects deriving through it for arguments the wrapper itself never constrains.

use std::rc::Rc;
use core::marker::PhantomData;
use thread_aware::ThreadAware;

// ThreadAware for every T — it holds no T, so relocation is a no-op.
struct Wrapper<T>(PhantomData<fn() -> T>);
impl<T> ThreadAware for Wrapper<T> {
    fn relocate(&mut self, _: Option<&thread_aware::Thread>, _: &thread_aware::Thread) {}
}

#[derive(ThreadAware)]
struct Outer<T>(Wrapper<PhantomData<T>>);

fn main() {
    // Rc<()> is neither Send nor ThreadAware.
    fn assert_ta<X: ThreadAware>() {}
    assert_ta::<Outer<Rc<()>>>();
}

Merge base (per-parameter bounds): the derive walks into Wrapper<PhantomData<T>>, reaches T, and emits impl<T: ThreadAware> ThreadAware for Outer<T>. Outer<Rc<()>> is then rejected with Rc<()>: !ThreadAware — even though the only field, Wrapper<PhantomData<T>>, is ThreadAware for every T and asks nothing of it.

This PR (field-type bound): it emits where Wrapper<PhantomData<T>>: ThreadAware, which the wrapper's own blanket impl satisfies, so Outer<Rc<()>> compiles. That's the exact obligation the generated body has (it only relocates the whole field).

This isn't hypothetical: it's the shape a lot of typed-handle / phantom-typed wrappers take (an id or handle that is generic for type-safety but carries no T). derive_compiles.rs::field_type_bound_lets_an_unconditional_wrapper_derive_for_any_argument pins it. The story that motivated it is AB#7783612 (raised by Sander in review of #678).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Moderate issues remain in skipped-field Self: Send handling and recursive or qualified-type fallback bounds.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (3)

crates/thread_aware/tests/derive_compiles.rs:738

  • This comment says the test uses Rc<()>, but the assertion and constructed value instantiate AliasNode<Tracker>. Update the comment to describe the recursive field and the explicit override that this test actually exercises.
    // `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.

crates/thread_aware_macros_impl/src/field_attrs.rs:172

  • This new test code is inside the existing #[cfg(test)] mod tests at line 77, but that module has no #[cfg_attr(coverage_nightly, coverage(off))]. The repository applies that attribute to test modules (for example, crates/thread_aware_core/src/impls.rs:270-272); add it to keep the test-only additions out of the 100% coverage gate.
    #[test]

crates/thread_aware_macros_impl/src/lib.rs:188

  • This blanket fallback changes every self-containing field into bounds on the syntactically reached parameters, even when the field's own impl is the exact obligation. A recursive field such as UnconditionalWrapper<Box<Node<T>>> (using the unconditional wrapper defined in the new regression) needs no T: ThreadAware, but this branch emits it; Node<T::Assoc> is worse because T: ThreadAware does not establish the T::Assoc: ThreadAware obligation of the recursive instantiation. Restrict the fallback to recursive shapes whose parameter mapping is preserved, or require an explicit bound for transformed arguments, so unconditional field impls remain usable.
        let targets: Vec<Type> = if type_reaches_ident(field_ty, &self_idents) {
            generic_param_idents
                .iter()
                .filter(|&param| {
                    let single: HashSet<syn::Ident> = std::iter::once(param.clone()).collect();
  • Files reviewed: 28/28 changed files
  • Comments generated: 3
  • Review effort level: Lite

}
}
Data::Union(_) => {}
return Ok(generics);
Comment on lines +184 to +185
let targets: Vec<Type> = if type_reaches_ident(field_ty, &self_idents) {
generic_param_idents
// 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");
Copilot AI review requested due to automatic review settings September 11, 2026 16:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved findings affect skipped-field Send predicates, qualified self-reference detection, and qualified-self generic traversal.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

crates/thread_aware_macros_impl/src/lib.rs:119

  • When a #[thread_aware(bound = "...")] override is present, this early return skips collect_relocated_fields and the later has_skipped_field handling. A type that combines the override with a #[thread_aware(skip)] field therefore omits the required Self: Send predicate, even though ThreadAware has Send as a supertrait and the skipped field is not covered by a relocated-field bound. Keep the override for inferred field predicates, but still preserve the skipped-field supertrait predicate.
    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);
        }
        return Ok(generics);

crates/thread_aware_macros_impl/src/lib.rs:194

  • The self-reference check matches any path segment by identifier, so a field such as other::Node<T> is classified as referring to the derived Node<T>. The fallback then emits T: ThreadAware instead of the actual other::Node<T>: ThreadAware obligation; an unrelated other::Node<T> with an unconditional ThreadAware impl is consequently over-constrained, which is the wrapper case this change is intended to allow. Restrict the self check to a syntactically self-resolving path rather than any qualified path segment with the same name.
        let targets: Vec<Type> = if type_reaches_ident(field_ty, &self_idents) {
            generic_param_idents
                .iter()
                .filter(|&param| {
                    let single: HashSet<syn::Ident> = 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()]
        };
  • Files reviewed: 28/28 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread crates/thread_aware_macros_impl/src/lib.rs Outdated

@Vaiz Evgenii (Vaiz) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. The motivating case is a wrapper that implements ThreadAware unconditionally (ignoring its type parameter) — the merge-base derive rejects deriving through it for arguments the wrapper itself never constrains.

use std::rc::Rc;
use core::marker::PhantomData;
use thread_aware::ThreadAware;

// ThreadAware for every T — it holds no T, so relocation is a no-op.
struct Wrapper<T>(PhantomData<fn() -> T>);
impl<T> ThreadAware for Wrapper<T> {
    fn relocate(&mut self, _: Option<&thread_aware::Thread>, _: &thread_aware::Thread) {}
}

#[derive(ThreadAware)]
struct Outer<T>(Wrapper<PhantomData<T>>);

fn main() {
    // Rc<()> is neither Send nor ThreadAware.
    fn assert_ta<X: ThreadAware>() {}
    assert_ta::<Outer<Rc<()>>>();
}

Merge base (per-parameter bounds): the derive walks into Wrapper<PhantomData<T>>, reaches T, and emits impl<T: ThreadAware> ThreadAware for Outer<T>. Outer<Rc<()>> is then rejected with Rc<()>: !ThreadAware — even though the only field, Wrapper<PhantomData<T>>, is ThreadAware for every T and asks nothing of it.

This PR (field-type bound): it emits where Wrapper<PhantomData<T>>: ThreadAware, which the wrapper's own blanket impl satisfies, so Outer<Rc<()>> compiles. That's the exact obligation the generated body has (it only relocates the whole field).

This isn't hypothetical: it's the shape a lot of typed-handle / phantom-typed wrappers take (an id or handle that is generic for type-safety but carries no T). derive_compiles.rs::field_type_bound_lets_an_unconditional_wrapper_derive_for_any_argument pins it. The story that motivated it is AB#7783612 (raised by Sander in review of #678).

that was done on purpose. if you want to make your type ThreadAware, please use correct form of PhantomData that doesn't depends on T

for example PhantomData<fn() -> T>

Pato Sandaña (psandana) added a commit that referenced this pull request Sep 11, 2026
…erence

Addresses a Copilot review comment on PR #740: `type_reaches_ident` walked only a
path's segments and their angle-bracketed arguments, never `TypePath::qself`. A
relocated field written as `<T as Provider>::Item` therefore looked like it
reached no parameter, so no `<T as Provider>::Item: ThreadAware` predicate was
emitted even though the generated body relocates it - leaving an impl that fails
to compile.

Traverse the qself type for reachability. The self-reference check benefits too:
a `<Self as Trait>::Item` field is now correctly seen as self-referential.

Adds a snapshot and a `derive_compiles.rs` regression that relocates a
`<T as ItemProvider>::Item` field.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 11, 2026 18:21
…d type

`#[derive(ThreadAware)]` inferred its bounds by collecting every generic
parameter a relocated field's type reaches and bounding each by `ThreadAware`.
That predicate is on the parameter, not on the obligation the generated body
has: the body relocates the whole field, so what it needs is
`<field type>: ThreadAware`.

Bounding the parameter is too strong for a wrapper that implements `ThreadAware`
unconditionally. `Outer<T>(Wrapper<PhantomData<T>>)` with an unconditional
`impl<T> ThreadAware for Wrapper<T>` was rejected for `Outer<Rc<()>>`, because
the derive emitted `T: ThreadAware` (which `Rc<()>` cannot meet) rather than
`Wrapper<PhantomData<T>>: ThreadAware` (which the wrapper's own impl satisfies
for every `T`).

`add_bounds` now emits, for each relocated field whose type reaches a generic
parameter, a `where <field type>: ThreadAware` predicate - the exact obligation
the body discharges - replacing the per-parameter predicates. The traversal
shape-set still decides which fields reach a parameter, so a marker payload
behind a function pointer (`PhantomData<fn(*const T)>`) owes no bound and the
raw-pointer variance idiom stays bound-free. The `is_same_trait` de-duplication
survives, so a field that is exactly a parameter the author already bounded by
`ThreadAware` emits nothing.

- Adds a compile regression for the unconditional-wrapper case.
- Updates the derive's "Generic Bounds" documentation to the field-type model.
- Rewrites the 16 affected macro snapshots; derive_compiles.rs passes unchanged.

Refs AB#7783612.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a snapshot for a struct with two fields of the same parameter-reaching type
(`TwoVecs<T>(Vec<T>, Vec<T>)`), exercising the `where`-predicate de-duplication
so the derive emits `where Vec<T>: ThreadAware` exactly once. Restores 100% patch
coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… where clause

Addresses review feedback on PR #740: `add_bounds` appended a generated
`where <field type>: ThreadAware` predicate even when the author had already
written an identical predicate in their own `where` clause (e.g. a field `Vec<T>`
with `where Vec<T>: ThreadAware`), producing a redundant bound that trips
`clippy::trait_duplication_in_bounds`.

Seed the emitted-predicate set from the author's `where` clause so a duplicate is
suppressed, and add a snapshot pinning it. The inline-bound path is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eriving

Addresses two regressions found by @wukchung in review of PR #740:

1. Recursive generics overflowed. A field whose type names the type being
   derived (e.g. `children: Vec<Node<T>>`) produced `where Vec<Node<T>>:
   ThreadAware`, a bound on the impl under construction, so trait selection
   cycled (`E0275`). Now such a field is bounded by the parameters it reaches
   (`T`) instead, which bottom out at the concrete argument and prove the
   recursive impl inductively. `add_bounds` chooses per field: the field type
   normally, the reached parameters when the field type is self-referential.

2. A slice-bearing sibling lost its bound. `type_reaches_param` did not
   traverse `Type::Slice`, so `Box<[T]>` owed no predicate; under the old
   per-parameter model a sibling `Vec<T>` field's `T: ThreadAware` covered it,
   but a field-type predicate does not. `[T]` has a conditional impl in
   `thread_aware_core`, so `Slice` is now traversed and `Box<[T]>` emits
   `where Box<[T]>: ThreadAware`. Corrected the stale comment that claimed the
   omitted shapes have no impl.

Adds snapshots for both shapes and a `derive_compiles.rs` regression covering a
recursive struct, a recursive enum, and a slice sibling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-reference checks

Fixes the mutation-testing gate on PR #740. The recursive-generics fix added two
new structural-traversal helpers (`type_names_deriving_type`, `collect_params_reached`)
whose match arms were not exercised by tests, so `cargo mutants` reported them as
missed.

Generalize the existing (already mutation-tested) traversal to `type_reaches_ident`,
which takes a target ident set, and use it three ways: with the generic parameters
(does the field owe a bound?), with the derived type name plus `Self` (is the bound
self-referential?), and per parameter (which parameters does a self-referential field
reach?). The two duplicate helpers are deleted.

Adds snapshots pinning that a recursive field bounds only the parameters it reaches
(not every parameter) and that `Self` is detected like the type's own name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses martintmk's review comment on PR #740: make the "Generic Bounds"
docs concise and readable. Cuts the mechanism-heavy prose (the no-op
PhantomData impl walk-through, "the obligation the generated body actually
has", the per-parameter reduction) down to the three facts a user needs: one
bound per relocated field on the field type, `where Self: Send` for skipped
fields, and the function-pointer marker escape hatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Generic Bounds

Addresses the follow-up Copilot review on PR #740:
- "one bound per relocated field" was misleading, since the derive de-duplicates
  identical field-type predicates and falls back to bounding reached parameters
  for a field that names the type being derived. Reworded to state the field-type
  rule and note the recursive-type exception in plain language.
- Remove the duplicated `Marked<T>` example and the duplicated "bare ThreadAware
  bound" sentence that the previous edit left behind - one of each now.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hatch

Addresses review feedback on PR #740 (Vaiz / Clawpilot): a type alias can hide a
recursive self-reference from the derive. With `type Children<T> = Vec<Node<T>>`
and `struct Node<T> { value: T, children: Children<T> }`, the derive only sees
the syntactic field type `Children<T>`, not that it expands to `Vec<Node<T>>`, so
its self-reference guard doesn't fire and it emits the circular
`where Children<T>: ThreadAware`, which overflows. A proc-macro can't resolve
aliases, so it can't detect this case on its own.

Add a container attribute `#[thread_aware(bound = "...")]` (the serde-style
escape hatch) that replaces the inferred `where` predicates with the author's
own - `#[thread_aware(bound = "T: ThreadAware")]` restores the parameter bound
the body needs, without skipping relocation.

- New `parse_container_attrs` with unit tests for the bound/duplicate/unknown/
  invalid-predicate cases.
- Snapshot pinning the override expansion, and a `derive_compiles.rs` regression
  that compiles and relocates the alias-recursion case.
- Documents the attribute in the derive's `# Attributes` and `# Generic Bounds`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… hatch

The `bound` escape hatch added one uncovered region (the `if !bounds.is_empty()`
skip path, never exercised) and a derived `Debug` that no passing test invokes -
together the single miss that dropped project coverage to 99.9%.

- `parse_container_attrs` now rejects an empty `bound = ""` instead of silently
  emitting no predicates, so `add_bounds` can drop the `is_empty` guard (bounds
  are always non-empty when present). Covered by a new unit test.
- Add a test that formats `ContainerAttrCfg` via its derived `Debug`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…erence

Addresses a Copilot review comment on PR #740: `type_reaches_ident` walked only a
path's segments and their angle-bracketed arguments, never `TypePath::qself`. A
relocated field written as `<T as Provider>::Item` therefore looked like it
reached no parameter, so no `<T as Provider>::Item: ThreadAware` predicate was
emitted even though the generated body relocates it - leaving an impl that fails
to compile.

Traverse the qself type for reachability. The self-reference check benefits too:
a `<Self as Trait>::Item` field is now correctly seen as self-referential.

Adds a snapshot and a `derive_compiles.rs` regression that relocates a
`<T as ItemProvider>::Item` field.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@psandana
Pato Sandaña (psandana) force-pushed the u/psandana/thread-aware-derive-field-type-bounds branch from 3a18647 to 04554f5 Compare September 11, 2026 18:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Four unresolved moderate findings affect bound reachability, skipped-field Self: Send handling, recursion detection, and a test assertion.

Review details

Suppressed comments (4)

crates/thread_aware_macros_impl/src/field_attrs.rs:188

  • This is a unit-test assertion; use unwrap() here, matching the surrounding tests in this module, rather than adding an expect message that provides no extra value in test backtraces.
        let bounds = cfg.bound.expect("bound should be set");

crates/thread_aware_macros_impl/src/lib.rs:369

  • The reachability scan only descends through GenericArgument::Type and returns false for pointer/trait-object type nodes. That misses parameters carried inside otherwise valid user-defined conditional implementations; for example, a local ConditionalWrapper<*const T> can implement ThreadAware only when T: ThreadAware, but this scan emits no T bound even though the generated body relocates that field. Since this logic now decides whether the field obligation needs a generic bound, please traverse the remaining type-bearing forms (while retaining the function-pointer marker exception) or explicitly reject/document this supported shape.
                        if let syn::GenericArgument::Type(t) = arg
                            && type_reaches_ident(t, targets)
                        {

crates/thread_aware_macros_impl/src/lib.rs:119

  • This early return bypasses collect_relocated_fields, so has_skipped_field is never observed when a bound override is present. A type that combines #[thread_aware(bound = "T: ThreadAware")] with a skipped U field therefore emits no Self: Send predicate, even though ThreadAware has Send as a supertrait and the public docs say skipped fields contribute that predicate (crates/thread_aware/src/lib.rs:161-174). Apply the override without bypassing the skipped-field handling, or append the Self: Send predicate before returning.
        return Ok(generics);

crates/thread_aware_macros_impl/src/lib.rs:363

  • This segment-by-segment check misclassifies an unrelated qualified type as recursive when it shares the derived type's final identifier. For example, a local Node<T> containing other::Node<T> would take the parameter-fallback path and require T: ThreadAware, even if other::Node<T> has an unconditional ThreadAware impl; the exact field-type obligation should not impose that extra bound. Restrict the recursion test to a path known to denote the current type (or require an unambiguous Self spelling).
            for segment in &path.segments {
                if targets.contains(&segment.ident) {
                    return true;
  • Files reviewed: 29/29 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 11, 2026 18:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Bound overrides can omit Self: Send, and qualified paths can trigger incorrect recursive fallback.

Review details

Suppressed comments (2)

crates/thread_aware_macros_impl/src/lib.rs:119

  • This early return bypasses the collect_relocated_fields call and the has_skipped_field handling below, so a type-level bound override combined with a #[thread_aware(skip)] field never receives the required Self: Send predicate. An override such as bound = "T: ThreadAware" on a type with an otherwise-unconstrained skipped U leaves the generated impl's ThreadAware: Send obligation uncovered. Apply the explicit bounds without returning before the skipped-field check.
        return Ok(generics);

crates/thread_aware_macros_impl/src/lib.rs:363

  • The self-reference check treats every path segment as the derived type's name. A field such as <T as other::Node>::Item in a derived Node is therefore classified as recursive because other::Node contains that segment, and the fallback emits T: ThreadAware instead of the actual field obligation <T as other::Node>::Item: ThreadAware; a provider can be non-ThreadAware while its associated item is ThreadAware. Check only the path's terminal type segment and continue traversing its generic arguments, so module/trait qualifiers do not trigger the recursive fallback.
                if targets.contains(&segment.ident) {
                    return true;
  • Files reviewed: 29/29 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@psandana

Copy link
Copy Markdown
Contributor Author

Evgenii (@Vaiz) thanks — that's a fair point, and you're right that PhantomData<fn() -> T> is a workaround: written that way the merge-base traversal doesn't enter the fn pointer, so no T: ThreadAware bound is emitted and Outer<Rc<()>> compiles there too.

Where I'd push back: relying on that asks the author to know an arcane rule — "spell your phantom as fn() -> T so the derive's traversal doesn't reach T" — to avoid a bound the generated body never actually needs. The body relocates the whole field by calling <field type>::relocate, so its real obligation is <field type>: ThreadAware, which is what this PR emits. For a wrapper that is ThreadAware for every T (a typed handle, a phantom-typed id, …), the per-parameter bound is strictly stronger than the body requires, and there's no #[thread_aware(skip)]-free way to say "trust the field's own impl" under the merge-base model.

I do take the point that the field-type model costs more machinery — the self-reference guard, the bound escape hatch for alias-hidden recursion, and the qself/slice traversal — that the per-parameter model didn't need. That's a genuine tradeoff.

martin-kolinek approved the field-type direction and it's the shape Sander asked for when he raised this in the #678 review (AB#7783612). But you've raised a real design question, and I'd rather settle the direction explicitly than merge over a change request. Happy to discuss here or offline — and if the team would rather keep the per-parameter model and document the fn() -> T idiom instead, I'm fine closing this in favour of that.

@psandana

Copy link
Copy Markdown
Contributor Author

Closing as won't fix, per discussion with Evgenii (@Vaiz) (Evgenii).

The case this PR addressed — a wrapper that is ThreadAware for every T being rejected when reached through a PhantomData<T> — has an easy author-side workaround: spell the marker as PhantomData<fn() -> T> so the derive's traversal doesn't reach T, and it derives fine under the current (per-parameter) model.

Moving the derive to field-type bounds to remove that workaround turned out to cost more than it's worth: it needs a self-reference guard, a #[thread_aware(bound = ...)] escape hatch for alias-hidden recursion, and qself/slice traversal — a lot of machinery for a case users can already handle. The per-parameter model stays.

Thanks Evgenii (@Vaiz) and martin-kolinek for the review. Abandoning the branch; AB#7783612 is closed as won't fix.

auto-merge was automatically disabled September 11, 2026 19:24

Pull request was closed

@psandana
Pato Sandaña (psandana) deleted the u/psandana/thread-aware-derive-field-type-bounds branch September 11, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants