Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 4 additions & 3 deletions crates/core/examples/shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//!
//! Note: this example uses `std`; the `devirt` crate itself is `#![no_std]`.
#![expect(clippy::print_stdout, reason = "example intentionally prints output to demonstrate API usage")]
#![expect(clippy::unnecessary_literal_bound, reason = "trait declares &str, not &'static str")]

struct Circle { radius: f64 }
struct Rect { w: f64, h: f64 }
Expand Down Expand Up @@ -97,9 +98,9 @@ impl Shape for Triangle {
fn name(&self) -> &str { "triangle" }
}

// Downstream type — not in the hot list, automatically uses vtable
#[devirt::devirt]
impl Shape for Hexagon {
// Cold type — implements ShapeBase directly, no #[devirt] needed.
// Downstream crates can do this without depending on devirt at all.
impl ShapeBase for Hexagon {
fn area(&self) -> f64 { 1.5 * 3.0_f64.sqrt() * self.side * self.side }
fn perimeter(&self) -> f64 { 6.0 * self.side }
fn scale(&mut self, factor: f64) { self.side *= factor; }
Expand Down
235 changes: 132 additions & 103 deletions crates/core/src/lib.rs

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions crates/core/tests/kani.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ mod n1 {

devirt::__devirt_define! {
@trait []
pub Trait1 [Hot] {
pub Trait1 Trait1Base [Hot] {
fn compute(&self, x: u64) -> u64;
fn notify(&self, x: u64);
fn transform(&mut self, x: u64) -> u64;
Expand Down Expand Up @@ -122,7 +122,7 @@ mod n2 {

devirt::__devirt_define! {
@trait []
pub Trait2 [HotA, HotB] {
pub Trait2 Trait2Base [HotA, HotB] {
fn compute(&self, x: u64) -> u64;
fn notify(&self, x: u64);
fn transform(&mut self, x: u64) -> u64;
Expand Down Expand Up @@ -257,7 +257,7 @@ mod n3 {

devirt::__devirt_define! {
@trait []
pub Trait3 [HotA, HotB, HotC] {
pub Trait3 Trait3Base [HotA, HotB, HotC] {
fn compute(&self, x: u64) -> u64;
fn notify(&self, x: u64);
fn transform(&mut self, x: u64) -> u64;
Expand Down Expand Up @@ -405,7 +405,7 @@ mod vt {

devirt::__devirt_define! {
@trait []
pub TraitVt [Hot] {
pub TraitVt TraitVtBase [Hot] {
fn compute(&self, x: u64) -> u64;
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/ui/all_arms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct ColdType {

devirt::__devirt_define! {
@trait []
pub AllArms [Hot] {
pub AllArms AllArmsBase [Hot] {
fn ref_nonvoid(&self, x: f64) -> f64;
fn ref_void(&self, x: f64);
fn mut_nonvoid(&mut self, x: f64) -> f64;
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/ui/method_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ struct Hot {

devirt::__devirt_define! {
@trait []
pub Checked [Hot] {
pub Checked CheckedBase [Hot] {
/// Computes the value.
#[must_use]
fn compute(&self) -> f64;
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/ui/missing_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ struct Foo;

devirt::__devirt_define! {
@trait []
pub TwoMethods [Foo] {
pub TwoMethods TwoMethodsBase [Foo] {
fn first(&self) -> i32;
fn second(&self) -> i32;
}
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/ui/missing_method.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ error[E0046]: not all trait items implemented, missing: `__spec_second`
|
3 | / devirt::__devirt_define! {
4 | | @trait []
5 | | pub TwoMethods [Foo] {
5 | | pub TwoMethods TwoMethodsBase [Foo] {
6 | | fn first(&self) -> i32;
... |
9 | | }
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/ui/multi_arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ struct Widget {

devirt::__devirt_define! {
@trait []
pub MultiArg [Widget] {
pub MultiArg MultiArgBase [Widget] {
fn add(&self, a: f64, b: f64) -> f64;
fn set(&mut self, a: f64, b: f64);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/ui/multi_hot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ struct C;

devirt::__devirt_define! {
@trait []
pub MultiHot [A, B, C] {
pub MultiHot MultiHotBase [A, B, C] {
fn id(&self) -> u8;
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/ui/pub_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ struct Inner {
devirt::__devirt_define! {
@trait []
/// A public trait with documentation.
pub DocTrait [Inner] {
pub DocTrait DocTraitBase [Inner] {
/// Returns the inner value.
fn get(&self) -> i32;
}
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/ui/single_hot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ struct Foo {

devirt::__devirt_define! {
@trait []
pub SingleHot [Foo] {
pub SingleHot SingleHotBase [Foo] {
fn get(&self) -> f64;
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/ui/unsafe_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ struct Cold {

devirt::__devirt_define! {
@trait [unsafe]
pub Trusted [Hot] {
pub Trusted TrustedBase [Hot] {
fn verify(&self) -> bool;
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/ui/wrong_signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ struct Bar;

devirt::__devirt_define! {
@trait []
pub WrongSig [Bar] {
pub WrongSig WrongSigBase [Bar] {
fn compute(&self, x: f64) -> f64;
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/ui_attr/attr_args_on_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ struct Foo;

devirt::__devirt_define! {
@trait []
pub ArgsOnImpl [Foo] {
pub ArgsOnImpl ArgsOnImplBase [Foo] {
fn get(&self) -> i32;
}
}
Expand Down
57 changes: 47 additions & 10 deletions crates/core/tests/ui_attr/attr_args_on_impl.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ error: hot types are specified on the trait definition, not the impl block
= note: this error originates in the attribute macro `devirt::devirt` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0277]: the trait bound `Foo: __ArgsOnImplImpl` is not satisfied
--> tests/ui_attr/attr_args_on_impl.rs:5:21
--> tests/ui_attr/attr_args_on_impl.rs:5:36
|
5 | pub ArgsOnImpl [Foo] {
| ^^^ unsatisfied trait bound
5 | pub ArgsOnImpl ArgsOnImplBase [Foo] {
| ^^^ unsatisfied trait bound
|
help: the trait `__ArgsOnImplImpl` is not implemented for `Foo`
help: the trait `ArgsOnImplBase` is not implemented for `Foo`
--> tests/ui_attr/attr_args_on_impl.rs:1:1
|
1 | struct Foo;
Expand All @@ -22,7 +22,18 @@ help: this trait has no implementations, consider adding one
|
3 | / devirt::__devirt_define! {
4 | | @trait []
5 | | pub ArgsOnImpl [Foo] {
5 | | pub ArgsOnImpl ArgsOnImplBase [Foo] {
6 | | fn get(&self) -> i32;
7 | | }
8 | | }
| |_^
note: required for `Foo` to implement `__ArgsOnImplImpl`
--> tests/ui_attr/attr_args_on_impl.rs:3:1
|
3 | / devirt::__devirt_define! {
4 | | @trait []
5 | | pub ArgsOnImpl ArgsOnImplBase [Foo] {
| | -------------- unsatisfied trait bound introduced here
6 | | fn get(&self) -> i32;
7 | | }
8 | | }
Expand All @@ -32,7 +43,7 @@ note: required by a bound in `<(dyn ArgsOnImpl + '__devirt)>::__devirt_vtable_fo
|
3 | / devirt::__devirt_define! {
4 | | @trait []
5 | | pub ArgsOnImpl [Foo] {
5 | | pub ArgsOnImpl ArgsOnImplBase [Foo] {
6 | | fn get(&self) -> i32;
7 | | }
8 | | }
Expand All @@ -42,24 +53,50 @@ note: required by a bound in `<(dyn ArgsOnImpl + '__devirt)>::__devirt_vtable_fo
| required by this bound in `<dyn ArgsOnImpl>::__devirt_vtable_for`
= note: this error originates in the macro `devirt::__devirt_define` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0599]: no method named `__spec_get` found for reference `&Foo` in the current scope
error[E0599]: the method `__spec_get` exists for reference `&Foo`, but its trait bounds were not satisfied
--> tests/ui_attr/attr_args_on_impl.rs:3:1
|
1 | struct Foo;
| ---------- doesn't satisfy `Foo: ArgsOnImplBase` or `Foo: __ArgsOnImplImpl`
2 |
3 | / devirt::__devirt_define! {
4 | | @trait []
5 | | pub ArgsOnImpl ArgsOnImplBase [Foo] {
6 | | fn get(&self) -> i32;
7 | | }
8 | | }
| |_^ method cannot be called on `&Foo` due to unsatisfied trait bounds
|
note: the following trait bounds were not satisfied:
`&Foo: ArgsOnImplBase`
`Foo: ArgsOnImplBase`
--> tests/ui_attr/attr_args_on_impl.rs:5:20
|
3 | / devirt::__devirt_define! {
4 | | @trait []
5 | | pub ArgsOnImpl [Foo] {
5 | | pub ArgsOnImpl ArgsOnImplBase [Foo] {
| | ^^^^^^^^^^^^^^ unsatisfied trait bound introduced here
6 | | fn get(&self) -> i32;
7 | | }
8 | | }
| |_^ method not found in `&Foo`
| |_-
note: the trait `ArgsOnImplBase` must be implemented
--> tests/ui_attr/attr_args_on_impl.rs:3:1
|
3 | / devirt::__devirt_define! {
4 | | @trait []
5 | | pub ArgsOnImpl ArgsOnImplBase [Foo] {
6 | | fn get(&self) -> i32;
7 | | }
8 | | }
| |_^
= help: items from traits can only be used if the trait is implemented and in scope
note: `__ArgsOnImplImpl` defines an item `__spec_get`, perhaps you need to implement it
--> tests/ui_attr/attr_args_on_impl.rs:3:1
|
3 | / devirt::__devirt_define! {
4 | | @trait []
5 | | pub ArgsOnImpl [Foo] {
5 | | pub ArgsOnImpl ArgsOnImplBase [Foo] {
6 | | fn get(&self) -> i32;
7 | | }
8 | | }
Expand Down
121 changes: 121 additions & 0 deletions crates/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,118 @@ fn build_delegating_methods(
.collect()
}

fn build_base_trait_items(
trait_item: &syn::ItemTrait,
) -> Vec<proc_macro2::TokenStream> {
trait_item
.items
.iter()
.filter_map(|item| {
if let syn::TraitItem::Fn(m) = item {
let attrs = &m.attrs;
let sig = &m.sig;
Some(quote! { #(#attrs)* #sig; })
} else {
None
}
})
.collect()
}

fn build_base_bridge_items(
trait_item: &syn::ItemTrait,
base_name: &syn::Ident,
) -> Vec<proc_macro2::TokenStream> {
trait_item
.items
.iter()
.filter_map(|item| match item {
syn::TraitItem::Type(t) => {
let type_name = &t.ident;
Some(quote! {
type #type_name = <Self as #base_name>::#type_name;
})
}
syn::TraitItem::Fn(m) => {
let method_name = &m.sig.ident;
let spec_name = format_ident!("__spec_{method_name}");
let mut bridge_sig = m.sig.clone();
bridge_sig.ident = spec_name;
let arg_names: Vec<_> = m
.sig
.inputs
.iter()
.filter_map(|arg| {
if let syn::FnArg::Typed(pat) = arg
&& let syn::Pat::Ident(pi) = &*pat.pat
{
return Some(&pi.ident);
}
None
})
.collect();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Some(quote! {
#[inline(always)]
#bridge_sig {
#base_name::#method_name(self, #(#arg_names),*)
}
Comment on lines +510 to +513

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve cfg-gated methods in base bridge impl

build_base_bridge_items drops the original method attributes when generating __spec_* methods, so a trait method with #[cfg(...)] is conditionally removed from __{Trait}Impl but still emitted unconditionally in the blanket impl __{Trait}Impl for T. In cfg-disabled builds this produces a hard compile error (method ... is not a member of trait). The bridge method needs to carry through method attributes (at least #[cfg]) to stay structurally consistent with the generated inner trait.

Useful? React with 👍 / 👎.

})
Comment on lines +510 to +514

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The bridge methods generated for the blanket implementation do not preserve attributes from the original trait methods. While #[inline(always)] is added, other functional attributes (like #[must_use]) or documentation are lost on the __FooImpl side. Since __FooImpl is an internal implementation detail used for dispatch, this is likely fine, but ensuring the public FooBase trait (handled in build_base_trait_items) retains them is critical.

}
_ => None,
})
.collect()
}
Comment on lines +485 to +519

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current implementation of build_base_bridge_items has several issues that will lead to compilation failures:

  1. Unstable Syntax: It uses "let chains" (&& let), which is currently an unstable Rust feature and will fail on stable compilers.
  2. Wildcard Handling: It skips arguments with wildcard patterns (_). This results in a mismatch between the function signature and the call site in the generated bridge code.
  3. Missing Generics: It does not include trait generics in the UFCS calls or associated type aliases. If the trait is generic (e.g., Trait<T>), the generated code will fail to compile because the base trait name will be used without its required type arguments.

Using the existing rewrite_sig_with_named_args helper and passing trait_ty_generics addresses all these points.

fn build_base_bridge_items(
    trait_item: &syn::ItemTrait,
    base_name: &syn::Ident,
    trait_ty_generics: &syn::TypeGenerics<'_>,
) -> Vec<proc_macro2::TokenStream> {
    trait_item
        .items
        .iter()
        .filter_map(|item| match item {
            syn::TraitItem::Type(t) => {
                let type_name = &t.ident;
                Some(quote! {
                    type #type_name = <Self as #base_name #trait_ty_generics>::#type_name;
                })
            }
            syn::TraitItem::Fn(m) => {
                let method_name = &m.sig.ident;
                let spec_name = format_ident!("__spec_{method_name}");
                let (mut bridge_sig, arg_names) = rewrite_sig_with_named_args(&m.sig);
                bridge_sig.ident = spec_name;
                Some(quote! {
                    #[inline(always)]
                    #bridge_sig {
                        <Self as #base_name #trait_ty_generics>::#method_name(self, #(#arg_names),*)
                    }
                })
            }
            _ => None,
        })
        .collect()
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Rust edition 2024 has been stable for years

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Items 2 and 3 were fixed in 16205cbbuild_base_bridge_items now uses rewrite_sig_with_named_args for proper wildcard handling and takes trait_ty_generics for fully-qualified UFCS calls.


Generated by Claude Code


#[expect(clippy::too_many_arguments)]
fn build_base_trait_expansion(
trait_item: &syn::ItemTrait,
unsafety: Option<&syn::token::Unsafe>,
has_trait_generics: bool,
base_name: &syn::Ident,
inner_name: &syn::Ident,
trait_generic_params: &Punctuated<syn::GenericParam, Token![,]>,
trait_ty_generics: &syn::TypeGenerics<'_>,
trait_where_clause: Option<&syn::WhereClause>,
inner_supers: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let vis = &trait_item.vis;
let base_trait_items = build_base_trait_items(trait_item);
let base_bridge_items = build_base_bridge_items(trait_item, base_name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The call to build_base_bridge_items must be updated to pass trait_ty_generics to ensure that the generated bridge code correctly handles generic traits.

Suggested change
let base_bridge_items = build_base_bridge_items(trait_item, base_name);
let base_bridge_items = build_base_bridge_items(trait_item, base_name, trait_ty_generics);

let assoc_type_decls = collect_assoc_types(trait_item).decls;
let trait_def_generics = if has_trait_generics {
quote! { <#trait_generic_params> }
} else {
quote! {}
};
let base_blanket = if has_trait_generics {
quote! {
#unsafety impl<
__DevirtT: #base_name #trait_ty_generics + ?Sized,
#trait_generic_params
> #inner_name #trait_ty_generics for __DevirtT #trait_where_clause {
#(#base_bridge_items)*
}
}
} else {
quote! {
#unsafety impl<__DevirtT: #base_name + ?Sized>
#inner_name for __DevirtT #trait_where_clause
{
#(#base_bridge_items)*
}
}
};
quote! {
/// Base implementation trait: implement this for cold types
/// without depending on `devirt`.
#vis #unsafety trait #base_name #trait_def_generics
#inner_supers #trait_where_clause
{ #(#assoc_type_decls)* #(#base_trait_items)* }

#base_blanket
}
}

fn emit_trait_expansion(
trait_item: &syn::ItemTrait,
hot_types: &[syn::Type],
Expand Down Expand Up @@ -535,12 +647,21 @@ fn emit_trait_expansion(
);
let assoc_type_decls = &assoc_info.decls;

let base_name = format_ident!("{name}Base");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid forcing a public {Trait}Base symbol name

The proc-macro expansion unconditionally creates a new public trait named {Trait}Base via format_ident!("{name}Base"). That introduces a source-breaking collision for existing modules that already define an item with that name, causing duplicate-definition compile errors after upgrade even when previous #[devirt] usage was valid. This breaks backward compatibility and should be made configurable or collision-safe.

Useful? React with 👍 / 👎.

let base_expansion = build_base_trait_expansion(
trait_item, unsafety.as_ref(), has_trait_generics, &base_name,
&inner_name, trait_generic_params, &trait_ty_generics,
trait_where_clause.as_ref(), &inner_supers,
);

quote! {
#[doc(hidden)]
#vis #unsafety trait #inner_name #trait_def_generics
#inner_supers #trait_where_clause
{ #(#assoc_type_decls)* #(#spec_decls)* }

#base_expansion

#fat_ptr_assertion

#vtable_helpers
Expand Down