fix(thread_aware_macros): express derive bounds on the relocated field type - #740
fix(thread_aware_macros): express derive bounds on the relocated field type#740Pato Sandaña (psandana) wants to merge 10 commits into
Conversation
There was a problem hiding this comment.
🟢 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::ThreadAwarepredicates 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-Sendgeneric 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 Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🔵 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_boundsappends generatedwherepredicates without checking whether an identical predicate is already present in the user'swhereclause. If the user already wrote (for example)where T: ThreadAwareand a relocated field type isT, this will add a second redundantT: ThreadAwarepredicate.
- Files reviewed: 21/21 changed files
- Comments generated: 0 new
- Review effort level: Lite
… 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>
There was a problem hiding this comment.
🟢 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
|
Addressed the suggestion in fcf1ce2: |
| continue; | ||
| } | ||
|
|
||
| predicates.push(parse_quote!(#bound_ty: #thread_aware_path)); |
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
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`, |
There was a problem hiding this comment.
🤖 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.
There was a problem hiding this comment.
Fixed in 535e5e7 — type_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.
…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>
There was a problem hiding this comment.
🟢 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
…-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>
There was a problem hiding this comment.
🟢 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
| /// 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 |
There was a problem hiding this comment.
this reads like AI mumbo-jumbo. Make it more concise and understandable for humans?
There was a problem hiding this comment.
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!
… 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>
aad5481 to
1908886
Compare
|
Sure. The motivating case is a wrapper that implements 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 This PR (field-type bound): it emits 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 |
There was a problem hiding this comment.
🟡 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 instantiateAliasNode<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 testsat 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 noT: ThreadAware, but this branch emits it;Node<T::Assoc>is worse becauseT: ThreadAwaredoes not establish theT::Assoc: ThreadAwareobligation 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(|¶m| {
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); |
| 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"); |
There was a problem hiding this comment.
🟡 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 skipscollect_relocated_fieldsand the laterhas_skipped_fieldhandling. A type that combines the override with a#[thread_aware(skip)]field therefore omits the requiredSelf: Sendpredicate, even thoughThreadAwarehasSendas 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 derivedNode<T>. The fallback then emitsT: ThreadAwareinstead of the actualother::Node<T>: ThreadAwareobligation; an unrelatedother::Node<T>with an unconditionalThreadAwareimpl 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(|¶m| {
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
Evgenii (Vaiz)
left a comment
There was a problem hiding this comment.
Sure. The motivating case is a wrapper that implements
ThreadAwareunconditionally (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>>, reachesT, and emitsimpl<T: ThreadAware> ThreadAware for Outer<T>.Outer<Rc<()>>is then rejected withRc<()>: !ThreadAware— even though the only field,Wrapper<PhantomData<T>>, isThreadAwarefor everyTand asks nothing of it.This PR (field-type bound): it emits
where Wrapper<PhantomData<T>>: ThreadAware, which the wrapper's own blanket impl satisfies, soOuter<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_argumentpins 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>
…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>
…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>
3a18647 to
04554f5
Compare
There was a problem hiding this comment.
🔵 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 anexpectmessage 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::Typeand returns false for pointer/trait-object type nodes. That misses parameters carried inside otherwise valid user-defined conditional implementations; for example, a localConditionalWrapper<*const T>can implementThreadAwareonly whenT: ThreadAware, but this scan emits noTbound 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, sohas_skipped_fieldis never observed when aboundoverride is present. A type that combines#[thread_aware(bound = "T: ThreadAware")]with a skippedUfield therefore emits noSelf: Sendpredicate, even thoughThreadAwarehasSendas 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 theSelf: Sendpredicate 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>containingother::Node<T>would take the parameter-fallback path and requireT: ThreadAware, even ifother::Node<T>has an unconditionalThreadAwareimpl; 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 unambiguousSelfspelling).
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
There was a problem hiding this comment.
🔵 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_fieldscall and thehas_skipped_fieldhandling below, so a type-level bound override combined with a#[thread_aware(skip)]field never receives the requiredSelf: Sendpredicate. An override such asbound = "T: ThreadAware"on a type with an otherwise-unconstrained skippedUleaves the generated impl'sThreadAware: Sendobligation 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>::Itemin a derivedNodeis therefore classified as recursive becauseother::Nodecontains that segment, and the fallback emitsT: ThreadAwareinstead of the actual field obligation<T as other::Node>::Item: ThreadAware; a provider can be non-ThreadAwarewhile its associated item isThreadAware. 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
|
Evgenii (@Vaiz) thanks — that's a fair point, and you're right that Where I'd push back: relying on that asks the author to know an arcane rule — "spell your phantom as I do take the point that the field-type model costs more machinery — the self-reference guard, the 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 |
|
Closing as won't fix, per discussion with Evgenii (@Vaiz) (Evgenii). The case this PR addressed — a wrapper that is 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 Thanks Evgenii (@Vaiz) and martin-kolinek for the review. Abandoning the branch; AB#7783612 is closed as won't fix. |
Pull request was closed
What & why
#[derive(ThreadAware)]inferred its bounds by collecting every generic parameter a relocated field's type reaches and bounding each byThreadAware. 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
ThreadAwareunconditionally:The body only relocates the whole
Wrapper<PhantomData<T>>field, which the wrapper's own impl already satisfies. The old derive additionally emittedT: ThreadAware, soOuter<Rc<()>>was rejected even thoughRc<()>never needs to beThreadAware.Change
add_boundsnow emits, for each relocated field whose type reaches a generic parameter, awhere <field type>: ThreadAwarepredicate — the exact obligation the generated body discharges — replacing the per-parameter predicates.Type::Slice, so aBox<[T]>field emitswhere Box<[T]>: ThreadAware([T]has a conditionalThreadAwareimpl inthread_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.Self) would makewhere <field type>: ThreadAwareself-referential and overflow, so it falls back to bounding the parameters it reaches — recursive types (struct Node<T> { children: Vec<Node<T>> }) still derive.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.ThreadAwareemits nothing, and predicates the author already wrote in awhereclause are not repeated, avoidingclippy::trait_duplication_in_bounds.where Self: Sendpredicate for skipped fields is unchanged.Acceptance criteria
boundescape hatch.derive_compiles.rscoverage passes unchanged.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).