From a6acac87b02a23eb5233e12c2deea49fa6994fe5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 13:54:59 +0000 Subject: [PATCH 1/2] Emit proc-macro expansion directly via quote! instead of delegating to __devirt_define! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites expand_trait and expand_impl to generate the dispatch code directly, eliminating the macro_rules pattern-matching bottleneck. This immediately enables support for unsafe trait/fn, method lifetime params, method attributes (#[must_use], doc comments), supertraits, extern "ABI" fn, and method where clauses — all parsed by syn and emitted faithfully via quote!. The declarative macro path (__devirt_define!) is unchanged and keeps its current limited feature set as the default-features=false fallback. New trybuild tests: attr_unsafe_fn, attr_method_lifetimes, attr_supertraits, attr_must_use (pass); attr_unsafe_missing_on_impl (compile-fail). Extended equivalence test with supertraits, method lifetimes, and #[must_use] coverage. https://claude.ai/code/session_01KeSis2aTgupBqgw9RtWXXZ --- crates/core/src/lib.rs | 17 +- crates/core/tests/equivalence.rs | 69 +++ crates/core/tests/ui_attr.rs | 5 + .../tests/ui_attr/attr_method_lifetimes.rs | 33 ++ crates/core/tests/ui_attr/attr_must_use.rs | 32 ++ crates/core/tests/ui_attr/attr_supertraits.rs | 39 ++ crates/core/tests/ui_attr/attr_unsafe_fn.rs | 32 ++ .../ui_attr/attr_unsafe_missing_on_impl.rs | 16 + .../attr_unsafe_missing_on_impl.stderr | 12 + crates/macros/src/lib.rs | 428 ++++++++++++++---- 10 files changed, 594 insertions(+), 89 deletions(-) create mode 100644 crates/core/tests/ui_attr/attr_method_lifetimes.rs create mode 100644 crates/core/tests/ui_attr/attr_must_use.rs create mode 100644 crates/core/tests/ui_attr/attr_supertraits.rs create mode 100644 crates/core/tests/ui_attr/attr_unsafe_fn.rs create mode 100644 crates/core/tests/ui_attr/attr_unsafe_missing_on_impl.rs create mode 100644 crates/core/tests/ui_attr/attr_unsafe_missing_on_impl.stderr diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 13fd5c7..de934b7 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -30,9 +30,12 @@ //! generates: //! - `impl __XImpl for ConcreteType { ... }` with the `__spec_*` bodies //! -//! Both the proc-macro attribute and the declarative macro delegate to -//! `__devirt_define!`, a `#[doc(hidden)]` internal macro that contains -//! all dispatch expansion logic. +//! The proc-macro attribute (`#[devirt]`) emits the dispatch code +//! directly via `quote!`, supporting the full range of method +//! signatures that `syn` can parse (lifetimes, `unsafe fn`, +//! supertraits, attributes, `extern "ABI" fn`, etc.). +//! The declarative macro (`devirt!`) delegates to `__devirt_define!`, +//! which has more limited syntax support. //! //! # Usage //! @@ -121,6 +124,14 @@ extern crate kani; #[doc(hidden)] pub use paste::paste as __paste; +/// Internal dispatch expansion macro. +/// +/// This macro has **limited syntax support** compared to the +/// `#[devirt]` proc-macro attribute. It only handles plain +/// `fn method(&self, arg: Type) -> Ret;` signatures — no `unsafe fn`, +/// method lifetimes, supertraits, method attributes, `where` clauses, +/// or `extern "ABI" fn`. For the full feature set, use the proc-macro +/// attribute (the default with the `macros` feature enabled). #[doc(hidden)] #[macro_export] macro_rules! __devirt_define { diff --git a/crates/core/tests/equivalence.rs b/crates/core/tests/equivalence.rs index f9e0efd..3cc3c3e 100644 --- a/crates/core/tests/equivalence.rs +++ b/crates/core/tests/equivalence.rs @@ -95,6 +95,75 @@ fn decl_dispatch() { assert_eq!(c.val, 100); } +// ── Extended proc-macro tests: supertraits, method lifetimes, #[must_use] ── + +#[cfg(feature = "macros")] +mod attr_extended { + use core::fmt; + + #[derive(Debug)] + pub struct ExtHot { + pub val: u64, + pub label: String, + } + + #[derive(Debug)] + pub struct ExtCold { + pub val: u64, + pub label: String, + } + + #[devirt::devirt(ExtHot)] + pub trait Inspectable: fmt::Debug { + #[must_use] + fn value(&self) -> u64; + // Explicit lifetime to exercise method-lifetime support. + fn name<'a>(&'a self) -> &'a str; + fn set_val(&mut self, v: u64); + } + + #[devirt::devirt] + impl Inspectable for ExtHot { + fn value(&self) -> u64 { self.val } + fn name(&self) -> &str { &self.label } + fn set_val(&mut self, v: u64) { self.val = v; } + } + + #[devirt::devirt] + impl Inspectable for ExtCold { + fn value(&self) -> u64 { self.val + 1 } + fn name(&self) -> &str { &self.label } + fn set_val(&mut self, v: u64) { self.val = v + 1; } + } +} + +#[cfg(feature = "macros")] +#[test] +fn attr_extended_dispatch() { + use attr_extended::{ExtCold, ExtHot, Inspectable}; + + // Supertraits: dyn Inspectable implements Debug + let h = ExtHot { val: 42, label: "hot".into() }; + let c = ExtCold { val: 42, label: "cold".into() }; + drop(format!("{:?}", &h as &dyn Inspectable)); + + // #[must_use] + non-void &self + assert_eq!((&h as &dyn Inspectable).value(), 42); + assert_eq!((&c as &dyn Inspectable).value(), 43); + + // Method lifetimes + assert_eq!((&h as &dyn Inspectable).name(), "hot"); + assert_eq!((&c as &dyn Inspectable).name(), "cold"); + + // &mut self + let mut h = ExtHot { val: 0, label: "hot".into() }; + let mut c = ExtCold { val: 0, label: "cold".into() }; + (&mut h as &mut dyn Inspectable).set_val(10); + (&mut c as &mut dyn Inspectable).set_val(10); + assert_eq!(h.val, 10); + assert_eq!(c.val, 11); +} + #[cfg(feature = "macros")] #[test] fn attr_dispatch() { diff --git a/crates/core/tests/ui_attr.rs b/crates/core/tests/ui_attr.rs index 9de93af..03ed8d2 100644 --- a/crates/core/tests/ui_attr.rs +++ b/crates/core/tests/ui_attr.rs @@ -8,7 +8,12 @@ fn ui_attr() { t.pass("tests/ui_attr/attr_all_arms.rs"); t.pass("tests/ui_attr/attr_unsafe_trait.rs"); t.pass("tests/ui_attr/attr_method_attrs.rs"); + t.pass("tests/ui_attr/attr_unsafe_fn.rs"); + t.pass("tests/ui_attr/attr_method_lifetimes.rs"); + t.pass("tests/ui_attr/attr_supertraits.rs"); + t.pass("tests/ui_attr/attr_must_use.rs"); t.compile_fail("tests/ui_attr/attr_missing_args.rs"); + t.compile_fail("tests/ui_attr/attr_unsafe_missing_on_impl.rs"); t.compile_fail("tests/ui_attr/attr_args_on_impl.rs"); t.compile_fail("tests/ui_attr/attr_on_struct.rs"); } diff --git a/crates/core/tests/ui_attr/attr_method_lifetimes.rs b/crates/core/tests/ui_attr/attr_method_lifetimes.rs new file mode 100644 index 0000000..b2ad456 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_method_lifetimes.rs @@ -0,0 +1,33 @@ +struct Hot { + name: String, +} + +struct Cold { + name: String, +} + +#[devirt::devirt(Hot)] +pub trait Named { + fn name<'a>(&'a self) -> &'a str; +} + +#[devirt::devirt] +impl Named for Hot { + fn name<'a>(&'a self) -> &'a str { &self.name } +} + +#[devirt::devirt] +impl Named for Cold { + fn name<'a>(&'a self) -> &'a str { &self.name } +} + +fn greet(n: &dyn Named) -> String { + format!("Hello, {}!", n.name()) +} + +fn main() { + let h = Hot { name: "world".into() }; + let c = Cold { name: "rust".into() }; + assert_eq!(greet(&h), "Hello, world!"); + assert_eq!(greet(&c), "Hello, rust!"); +} diff --git a/crates/core/tests/ui_attr/attr_must_use.rs b/crates/core/tests/ui_attr/attr_must_use.rs new file mode 100644 index 0000000..7fef530 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_must_use.rs @@ -0,0 +1,32 @@ +struct Hot { + val: f64, +} + +struct Cold { + val: f64, +} + +#[devirt::devirt(Hot)] +pub trait Computed { + #[must_use] + fn compute(&self) -> f64; +} + +#[devirt::devirt] +impl Computed for Hot { + fn compute(&self) -> f64 { self.val * 2.0 } +} + +#[devirt::devirt] +impl Computed for Cold { + fn compute(&self) -> f64 { self.val * 3.0 } +} + +fn main() { + let h = Hot { val: 1.0 }; + let c = Cold { val: 1.0 }; + let dh: &dyn Computed = &h; + let dc: &dyn Computed = &c; + let _ = dh.compute(); // should NOT warn (result is used via let _) + let _ = dc.compute(); +} diff --git a/crates/core/tests/ui_attr/attr_supertraits.rs b/crates/core/tests/ui_attr/attr_supertraits.rs new file mode 100644 index 0000000..e0dbd4f --- /dev/null +++ b/crates/core/tests/ui_attr/attr_supertraits.rs @@ -0,0 +1,39 @@ +use std::fmt::Debug; + +#[derive(Debug)] +struct Hot { + val: u64, +} + +#[derive(Debug)] +struct Cold { + val: u64, +} + +#[devirt::devirt(Hot)] +pub trait Inspectable: Debug { + fn value(&self) -> u64; +} + +#[devirt::devirt] +impl Inspectable for Hot { + fn value(&self) -> u64 { self.val } +} + +#[devirt::devirt] +impl Inspectable for Cold { + fn value(&self) -> u64 { self.val + 1 } +} + +fn inspect(i: &dyn Inspectable) -> String { + format!("{:?} = {}", i, i.value()) +} + +fn main() { + let h = Hot { val: 42 }; + let c = Cold { val: 42 }; + let s = inspect(&h); + assert!(s.contains("42"), "expected '42' in '{s}'"); + let s = inspect(&c); + assert!(s.contains("43"), "expected '43' in '{s}'"); +} diff --git a/crates/core/tests/ui_attr/attr_unsafe_fn.rs b/crates/core/tests/ui_attr/attr_unsafe_fn.rs new file mode 100644 index 0000000..152c8a5 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_unsafe_fn.rs @@ -0,0 +1,32 @@ +struct Hot { + data: *const u8, +} + +struct Cold { + data: *const u8, +} + +#[devirt::devirt(Hot)] +pub trait Dangerous { + unsafe fn deref(&self) -> u8; +} + +#[devirt::devirt] +impl Dangerous for Hot { + unsafe fn deref(&self) -> u8 { unsafe { *self.data } } +} + +#[devirt::devirt] +impl Dangerous for Cold { + unsafe fn deref(&self) -> u8 { unsafe { *self.data } } +} + +fn main() { + let val: u8 = 42; + let h = Hot { data: &val }; + let c = Cold { data: &val }; + let dh: &dyn Dangerous = &h; + let dc: &dyn Dangerous = &c; + assert_eq!(unsafe { dh.deref() }, 42); + assert_eq!(unsafe { dc.deref() }, 42); +} diff --git a/crates/core/tests/ui_attr/attr_unsafe_missing_on_impl.rs b/crates/core/tests/ui_attr/attr_unsafe_missing_on_impl.rs new file mode 100644 index 0000000..3ba41d8 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_unsafe_missing_on_impl.rs @@ -0,0 +1,16 @@ +struct Hot { + val: u64, +} + +#[devirt::devirt(Hot)] +pub unsafe trait Trusted { + fn verify(&self) -> bool; +} + +// Missing `unsafe` on impl — should fail because __TrustedImpl is unsafe. +#[devirt::devirt] +impl Trusted for Hot { + fn verify(&self) -> bool { self.val > 0 } +} + +fn main() {} diff --git a/crates/core/tests/ui_attr/attr_unsafe_missing_on_impl.stderr b/crates/core/tests/ui_attr/attr_unsafe_missing_on_impl.stderr new file mode 100644 index 0000000..06c7635 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_unsafe_missing_on_impl.stderr @@ -0,0 +1,12 @@ +error[E0200]: the trait `__TrustedImpl` requires an `unsafe impl` declaration + --> tests/ui_attr/attr_unsafe_missing_on_impl.rs:11:1 + | +11 | #[devirt::devirt] + | ^^^^^^^^^^^^^^^^^ + | + = note: the trait `__TrustedImpl` enforces invariants that the compiler can't check. Review the trait documentation and make sure this implementation upholds those invariants before adding the `unsafe` keyword + = note: this error originates in the attribute macro `devirt::devirt` (in Nightly builds, run with -Z macro-backtrace for more info) +help: add `unsafe` to this trait implementation + | +11 | unsafe #[devirt::devirt] + | ++++++ diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index edd01e3..7831058 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1,11 +1,12 @@ //! Proc-macro attribute for [`devirt`](https://docs.rs/devirt). //! -//! Provides `#[devirt]` as a proc-macro attribute that delegates to -//! `devirt::__devirt_define!`. This crate is an implementation detail -//! of `devirt` and should not be used directly. +//! Provides `#[devirt]` as a proc-macro attribute that emits the +//! devirtualization dispatch code directly via `quote!`. This crate +//! is an implementation detail of `devirt` and should not be used +//! directly. use proc_macro::TokenStream; -use quote::{ToTokens, quote}; +use quote::{format_ident, quote}; use syn::punctuated::Punctuated; use syn::{Token, parse_macro_input}; @@ -15,7 +16,7 @@ use syn::{Token, parse_macro_input}; /// /// ```ignore /// #[devirt::devirt(Circle, Rect)] -/// pub trait Shape { +/// pub trait Shape: Debug { /// fn area(&self) -> f64; /// fn scale(&mut self, factor: f64); /// } @@ -47,6 +48,8 @@ pub fn devirt(attr: TokenStream, item: TokenStream) -> TokenStream { .into() } +// ── Trait expansion ───────────────────────────────────────────────────────── + fn expand_trait(attr: TokenStream, trait_item: &syn::ItemTrait) -> TokenStream { if attr.is_empty() { return syn::Error::new( @@ -57,102 +60,345 @@ fn expand_trait(attr: TokenStream, trait_item: &syn::ItemTrait) -> TokenStream { .into(); } - // Reject unsupported trait features. + if let Err(e) = validate_trait(trait_item) { + return e.to_compile_error().into(); + } + + let hot_types: Vec = + parse_macro_input!(attr with Punctuated::::parse_terminated) + .into_iter() + .collect(); + + emit_trait_expansion(trait_item, &hot_types) +} + +fn validate_trait(trait_item: &syn::ItemTrait) -> Result<(), syn::Error> { if !trait_item.generics.params.is_empty() { - return syn::Error::new_spanned( + return Err(syn::Error::new_spanned( &trait_item.generics, "#[devirt] does not support generic traits", - ) - .to_compile_error() - .into(); + )); } - if let Some(where_clause) = &trait_item.generics.where_clause { - return syn::Error::new_spanned( - where_clause, + if let Some(wc) = &trait_item.generics.where_clause { + return Err(syn::Error::new_spanned( + wc, "#[devirt] does not support where clauses on traits", - ) - .to_compile_error() - .into(); - } - if !trait_item.supertraits.is_empty() { - return syn::Error::new_spanned( - &trait_item.supertraits, - "#[devirt] does not support supertraits", - ) - .to_compile_error() - .into(); + )); } for item in &trait_item.items { match item { syn::TraitItem::Type(t) => { - return syn::Error::new_spanned( + return Err(syn::Error::new_spanned( t, "#[devirt] does not support associated types", - ) - .to_compile_error() - .into(); + )); } syn::TraitItem::Const(c) => { - return syn::Error::new_spanned( + return Err(syn::Error::new_spanned( c, "#[devirt] does not support associated constants", - ) - .to_compile_error() - .into(); - } - syn::TraitItem::Fn(f) => { - if f.default.is_some() { - return syn::Error::new_spanned( - f, - "#[devirt] does not support default method bodies", - ) - .to_compile_error() - .into(); - } + )); } + syn::TraitItem::Fn(f) => validate_trait_method(f)?, _ => {} } } + Ok(()) +} - let hot_types: Vec = - parse_macro_input!(attr with Punctuated::::parse_terminated) - .into_iter() - .collect(); +fn validate_trait_method(f: &syn::TraitItemFn) -> Result<(), syn::Error> { + if f.default.is_some() { + return Err(syn::Error::new_spanned( + f, + "#[devirt] does not support default method bodies", + )); + } + if f.sig.asyncness.is_some() { + return Err(syn::Error::new_spanned( + &f.sig, + "#[devirt] does not support async methods", + )); + } + let Some(recv) = f.sig.inputs.first().and_then(|a| { + if let syn::FnArg::Receiver(r) = a { + Some(r) + } else { + None + } + }) else { + return Err(syn::Error::new_spanned( + &f.sig, + "#[devirt] methods must have a `&self` or `&mut self` receiver", + )); + }; + if recv.reference.is_none() { + return Err(syn::Error::new_spanned( + recv, + "#[devirt] does not support owned self or custom self types; \ + use `&self` or `&mut self`", + )); + } + // Validate argument patterns are named (ident or wildcard). + for arg in &f.sig.inputs { + if let syn::FnArg::Typed(pat_type) = arg + && !matches!(&*pat_type.pat, syn::Pat::Ident(_) | syn::Pat::Wild(_)) + { + return Err(syn::Error::new_spanned( + &pat_type.pat, + "#[devirt] requires named parameters (use `name: Type` \ + instead of a destructuring pattern)", + )); + } + } + Ok(()) +} +fn emit_trait_expansion( + trait_item: &syn::ItemTrait, + hot_types: &[syn::Type], +) -> TokenStream { let unsafety = &trait_item.unsafety; let vis = &trait_item.vis; let name = &trait_item.ident; + let outer_attrs = &trait_item.attrs; + let supertraits = &trait_item.supertraits; + let inner_name = format_ident!("__{name}Impl"); - // Extract method signatures as token streams, stripping default bodies. - let mut methods_tokens = proc_macro2::TokenStream::new(); - for item in &trait_item.items { - if let syn::TraitItem::Fn(m) = item { - let sig = &m.sig; - for a in &m.attrs { - a.to_tokens(&mut methods_tokens); + // __spec_* method declarations for the inner trait. + let spec_decls: Vec<_> = trait_item + .items + .iter() + .filter_map(|item| { + let syn::TraitItem::Fn(m) = item else { + return None; + }; + let mut spec_sig = m.sig.clone(); + spec_sig.ident = format_ident!("__spec_{}", spec_sig.ident); + let attrs = &m.attrs; + Some(quote! { #(#attrs)* #spec_sig; }) + }) + .collect(); + + // Dispatch methods for the inherent impl on `dyn Trait`. + let dispatch_methods: Vec<_> = trait_item + .items + .iter() + .filter_map(|item| { + let syn::TraitItem::Fn(m) = item else { + return None; + }; + Some(generate_dispatch_method(m, name, &inner_name, hot_types)) + }) + .collect(); + + // Inner trait supertraits: `__FooImpl: Debug + Clone` + let inner_supers = if supertraits.is_empty() { + quote! {} + } else { + quote! { : #supertraits } + }; + + // Public trait supertraits: `Foo: __FooImpl + Debug + Clone` + // The `+ Debug + Clone` is redundant (implied by `__FooImpl`) but + // makes the bounds visible in rustdoc and compiler diagnostics. + let public_supers = if supertraits.is_empty() { + quote! { #inner_name } + } else { + quote! { #inner_name + #supertraits } + }; + + quote! { + // (1) Hidden inner trait — carries __spec_* methods. + #[doc(hidden)] + #vis #unsafety trait #inner_name #inner_supers { + #(#spec_decls)* + } + + // (2) Compile-time fat pointer assertion. + const _: () = assert!( + ::core::mem::size_of::<*const dyn #name>() + == 2 * ::core::mem::size_of::() + ); + + // (3) Vtable helpers on `dyn Trait`. + impl<'__devirt> dyn #name + '__devirt { + /// Split a fat pointer into `[data, vtable]`. + #[doc(hidden)] + #[inline(always)] + pub fn __devirt_raw_parts(this: &Self) -> [usize; 2] { + // SAFETY: `&dyn Trait` is a two-`usize` fat pointer + // (verified by the compile-time assertion above). + unsafe { ::core::mem::transmute::<&Self, [usize; 2]>(this) } + } + + /// Vtable pointer for the `(T, Trait)` pair. + #[doc(hidden)] + #[inline(always)] + pub fn __devirt_vtable_for< + __DevirtT: #inner_name + 'static, + >() -> usize { + let fake: *const __DevirtT = + ::core::ptr::without_provenance( + ::core::mem::align_of::<__DevirtT>(), + ); + let fat: *const Self = fake; + // SAFETY: `*const dyn Trait` is two `usize`s. We read + // only the vtable half; the dangling data half is + // discarded. + let __parts: [usize; 2] = unsafe { + ::core::mem::transmute::<*const Self, [usize; 2]>(fat) + }; + __parts[1] } - sig.to_tokens(&mut methods_tokens); - methods_tokens.extend(quote! { ; }); } + + // (4) Inherent dispatch methods. + impl<'__devirt> dyn #name + '__devirt { + #(#dispatch_methods)* + } + + // (5) Public marker trait. + #(#outer_attrs)* + #vis #unsafety trait #name: #public_supers {} + + // (6) Blanket impl. + #unsafety impl<__DevirtT: #inner_name + ?Sized> #name + for __DevirtT {} } + .into() +} + +// ── Dispatch method generation ────────────────────────────────────────────── - let mut outer_attrs = proc_macro2::TokenStream::new(); - for a in &trait_item.attrs { - a.to_tokens(&mut outer_attrs); +fn generate_dispatch_method( + method: &syn::TraitItemFn, + trait_name: &syn::Ident, + inner_name: &syn::Ident, + hot_types: &[syn::Type], +) -> proc_macro2::TokenStream { + let sig = &method.sig; + let attrs = &method.attrs; + let spec_name = format_ident!("__spec_{}", sig.ident); + + let receiver = sig + .inputs + .first() + .and_then(|a| { + if let syn::FnArg::Receiver(r) = a { + Some(r) + } else { + None + } + }) + .expect("validated: method has receiver"); + + let is_mut = receiver.mutability.is_some(); + let is_unsafe = sig.unsafety.is_some(); + + // Build the dispatch signature, replacing wildcard `_` patterns + // with generated names so we can forward arguments. + let mut dispatch_sig = sig.clone(); + let mut arg_names: Vec = Vec::new(); + for (idx, arg) in dispatch_sig.inputs.iter_mut().enumerate() { + if let syn::FnArg::Typed(pat_type) = arg { + match &*pat_type.pat { + syn::Pat::Ident(pat_ident) => { + arg_names.push(pat_ident.ident.clone()); + } + syn::Pat::Wild(_) => { + let generated = format_ident!("__arg{idx}"); + *pat_type.pat = syn::Pat::Ident(syn::PatIdent { + attrs: vec![], + by_ref: None, + mutability: None, + ident: generated.clone(), + subpat: None, + }); + arg_names.push(generated); + } + _ => { + // Validation already rejects this case, but + // generate a name defensively. + arg_names.push(format_ident!("__arg{idx}")); + } + } + } } + let raw_parts = if is_mut { + quote! { let __raw = ::__devirt_raw_parts(&*self); } + } else { + quote! { let __raw = ::__devirt_raw_parts(self); } + }; + + let hot_checks = gen_hot_checks( + hot_types, trait_name, &spec_name, &arg_names, is_mut, + ); + + let fallback = if is_unsafe { + quote! { unsafe { #inner_name::#spec_name(self, #(#arg_names),*) } } + } else { + quote! { #inner_name::#spec_name(self, #(#arg_names),*) } + }; + quote! { - ::devirt::__devirt_define! { - @trait [#unsafety] - #outer_attrs - #vis #name [#(#hot_types),*] { - #methods_tokens - } + #(#attrs)* + #[doc(hidden)] + #[inline] + pub #dispatch_sig { + #raw_parts + #(#hot_checks)* + #fallback } } - .into() } +fn gen_hot_checks( + hot_types: &[syn::Type], + trait_name: &syn::Ident, + spec_name: &syn::Ident, + arg_names: &[syn::Ident], + is_mut: bool, +) -> Vec { + hot_types + .iter() + .map(|hot| { + if is_mut { + quote! { + if __raw[1] + == ::__devirt_vtable_for::<#hot>() + { + let __p: *mut #hot = __raw[0] as *mut #hot; + // SAFETY: vtable identity implies type identity. + // The `&mut` reborrow is scoped to this method + // call and released before the enclosing `&mut + // dyn Trait` is used again. + return unsafe { + (&mut *__p).#spec_name(#(#arg_names),*) + }; + } + } + } else { + quote! { + if __raw[1] + == ::__devirt_vtable_for::<#hot>() + { + let __p: *const #hot = __raw[0] as *const #hot; + // SAFETY: vtable identity implies type identity. + // The data half is the original `&HotType` the + // caller coerced into the fat pointer. + return unsafe { + (&*__p).#spec_name(#(#arg_names),*) + }; + } + } + } + }) + .collect() +} + +// ── Impl expansion ────────────────────────────────────────────────────────── + fn expand_impl(attr: &TokenStream, impl_item: &syn::ItemImpl) -> TokenStream { if !attr.is_empty() { return syn::Error::new( @@ -172,7 +418,6 @@ fn expand_impl(attr: &TokenStream, impl_item: &syn::ItemImpl) -> TokenStream { .into(); }; - // Reject unsupported impl features. if !impl_item.generics.params.is_empty() { return syn::Error::new_spanned( &impl_item.generics, @@ -181,17 +426,17 @@ fn expand_impl(attr: &TokenStream, impl_item: &syn::ItemImpl) -> TokenStream { .to_compile_error() .into(); } - if let Some(where_clause) = &impl_item.generics.where_clause { + if let Some(wc) = &impl_item.generics.where_clause { return syn::Error::new_spanned( - where_clause, + wc, "#[devirt] does not support where clauses on impl blocks", ) .to_compile_error() .into(); } - // Reject qualified paths (e.g., super::MyTrait, crate::MyTrait) — - // __devirt_define! requires a plain ident, not a path. + // Reject qualified paths — we need a plain ident to construct + // the __TraitNameImpl identifier. if trait_path.leading_colon.is_some() || trait_path.segments.len() > 1 { return syn::Error::new_spanned( trait_path, @@ -203,26 +448,37 @@ fn expand_impl(attr: &TokenStream, impl_item: &syn::ItemImpl) -> TokenStream { } let unsafety = &impl_item.unsafety; - let trait_name = &trait_path.segments.last().expect("trait path is empty").ident; + let trait_name = &trait_path + .segments + .last() + .expect("validated: path non-empty") + .ident; + let inner_name = format_ident!("__{trait_name}Impl"); let ty = &impl_item.self_ty; - let mut method_bodies = proc_macro2::TokenStream::new(); - for item in &impl_item.items { - if let syn::ImplItem::Fn(m) = item { - for a in &m.attrs { - a.to_tokens(&mut method_bodies); - } - m.sig.to_tokens(&mut method_bodies); - m.block.to_tokens(&mut method_bodies); - } - } + let spec_methods: Vec<_> = impl_item + .items + .iter() + .filter_map(|item| { + let syn::ImplItem::Fn(m) = item else { + return None; + }; + let mut spec_sig = m.sig.clone(); + spec_sig.ident = format_ident!("__spec_{}", spec_sig.ident); + let attrs = &m.attrs; + let block = &m.block; + Some(quote! { + #(#attrs)* + #[inline] + #[allow(clippy::unnecessary_literal_bound)] + #spec_sig #block + }) + }) + .collect(); quote! { - ::devirt::__devirt_define! { - @impl [#unsafety] - #trait_name for #ty { - #method_bodies - } + #unsafety impl #inner_name for #ty { + #(#spec_methods)* } } .into() From 692525e00d5a63dfa56a36e5ad199cd15b5e168a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 14:22:47 +0000 Subject: [PATCH 2/2] Update CLAUDE.md for direct-expansion architecture; add #[must_use] compile-fail test - CLAUDE.md: Document that the proc-macro attribute emits directly via quote! while only the declarative macro delegates to __devirt_define!. Update Macro Structure section to describe two independent expansion paths and clarify Dispatch Arms apply to the declarative-macro path only. - Add attr_must_use_unused.rs compile-fail test: verifies #[must_use] propagates through macro expansion by calling compute() without using its result under #![deny(unused_must_use)], proving the attribute reaches the inherent dispatch method on dyn Trait. https://claude.ai/code/session_01KeSis2aTgupBqgw9RtWXXZ --- CLAUDE.md | 18 ++++++------- crates/core/tests/ui_attr.rs | 1 + .../tests/ui_attr/attr_must_use_unused.rs | 25 +++++++++++++++++++ .../tests/ui_attr/attr_must_use_unused.stderr | 15 +++++++++++ 4 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 crates/core/tests/ui_attr/attr_must_use_unused.rs create mode 100644 crates/core/tests/ui_attr/attr_must_use_unused.stderr diff --git a/CLAUDE.md b/CLAUDE.md index 5f748e8..536d4b3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,8 +51,8 @@ verus crates/verify/src/lib.rs --crate-type=lib This is a workspace with four crates: -- **`crates/core`** (`devirt`) — the main macro library. Exports `devirt!` (declarative) or `#[devirt]` (proc-macro attribute) depending on the `macros` feature (on by default). Both delegate to the internal `__devirt_define!` macro. -- **`crates/macros`** (`devirt-macros`) — proc-macro crate providing the `#[devirt]` attribute. Optional dependency of `crates/core`, enabled by the `macros` feature. +- **`crates/core`** (`devirt`) — the main macro library. Exports `devirt!` (declarative) or `#[devirt]` (proc-macro attribute) depending on the `macros` feature (on by default). The declarative macro delegates to the internal `__devirt_define!` macro; the proc-macro attribute emits expansion directly via `quote!`. +- **`crates/macros`** (`devirt-macros`) — proc-macro crate providing the `#[devirt]` attribute. Emits the dispatch expansion directly (not via `__devirt_define!`), giving it full `syn`-level signature fidelity. Optional dependency of `crates/core`, enabled by the `macros` feature. - **`crates/verify`** (`devirt-verify`) — Verus formal proofs of dispatch correctness. - **`fuzz`** — libfuzzer differential fuzzing comparing devirt dispatch vs. plain vtable. @@ -64,15 +64,15 @@ Why inherent methods on `dyn Trait` and not trait default methods: a default met ### Macro Structure -All dispatch expansion logic lives in `__devirt_define!` (`#[doc(hidden)]`, `#[macro_export]`, always available). It has two entry points: +There are two independent expansion paths: -- `__devirt_define! { @trait ... }` — generates the trait, inner trait, dispatch shim, blanket impl -- `__devirt_define! { @impl ... }` — generates `impl __TraitNameImpl for T { __spec_* ... }` +- **`#[devirt::devirt(Hot1, Hot2)]`** (proc-macro attribute, default) — parses the trait/impl with `syn` and emits the full expansion directly via `quote!`. Supports `unsafe trait`/`unsafe fn`, method lifetimes, method attributes, supertraits, `extern "ABI" fn`, and method `where` clauses. +- **`devirt::devirt!`** (declarative macro, `default-features = false`) — thin dispatcher that forwards to `$crate::__devirt_define!`, a `#[doc(hidden)]` `#[macro_export]` internal macro with limited syntax support (plain `fn method(&self, arg: Type) -> Ret;` signatures only). -The public APIs delegate to it: +`__devirt_define!` has two entry points: -- **`#[devirt::devirt(Hot1, Hot2)]`** (proc-macro attribute, default) — parses the trait/impl and emits `::devirt::__devirt_define!` calls -- **`devirt::devirt!`** (declarative macro, `default-features = false`) — thin dispatcher that forwards to `$crate::__devirt_define!` +- `__devirt_define! { @trait ... }` — generates the trait, inner trait, dispatch shim, blanket impl +- `__devirt_define! { @impl ... }` — generates `impl __TraitNameImpl for T { __spec_* ... }` ### `__devirt_define! { @trait ... }` Expansion @@ -88,7 +88,7 @@ For a trait `Foo` with hot types `[A, B]`: For `impl Foo for A { ... }`: - Expands to `impl __FooImpl for A { fn __spec_method(...) { ... } }`. -### Dispatch Arms (inside `__devirt_define!`) +### Dispatch Arms (inside `__devirt_define!`, declarative-macro path only) Four arms handle the combinatorics of `&self`/`&mut self` × void/non-void. Each splits into an outer "set up `__raw`" arm and a recursive `*_chain` arm that walks the hot-type list: - `@dispatch_ref` / `@dispatch_ref_chain` — `&self`, returns value diff --git a/crates/core/tests/ui_attr.rs b/crates/core/tests/ui_attr.rs index 03ed8d2..cc97806 100644 --- a/crates/core/tests/ui_attr.rs +++ b/crates/core/tests/ui_attr.rs @@ -12,6 +12,7 @@ fn ui_attr() { t.pass("tests/ui_attr/attr_method_lifetimes.rs"); t.pass("tests/ui_attr/attr_supertraits.rs"); t.pass("tests/ui_attr/attr_must_use.rs"); + t.compile_fail("tests/ui_attr/attr_must_use_unused.rs"); t.compile_fail("tests/ui_attr/attr_missing_args.rs"); t.compile_fail("tests/ui_attr/attr_unsafe_missing_on_impl.rs"); t.compile_fail("tests/ui_attr/attr_args_on_impl.rs"); diff --git a/crates/core/tests/ui_attr/attr_must_use_unused.rs b/crates/core/tests/ui_attr/attr_must_use_unused.rs new file mode 100644 index 0000000..4fae085 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_must_use_unused.rs @@ -0,0 +1,25 @@ +// Verifies that #[must_use] on a trait method is preserved through +// macro expansion: calling compute() without using the result must +// trigger an error under deny(unused_must_use). +#![deny(unused_must_use)] + +struct Hot { + val: f64, +} + +#[devirt::devirt(Hot)] +pub trait Computed { + #[must_use] + fn compute(&self) -> f64; +} + +#[devirt::devirt] +impl Computed for Hot { + fn compute(&self) -> f64 { self.val * 2.0 } +} + +fn main() { + let h = Hot { val: 1.0 }; + let d: &dyn Computed = &h; + d.compute(); // ERROR: unused return value that must be used +} diff --git a/crates/core/tests/ui_attr/attr_must_use_unused.stderr b/crates/core/tests/ui_attr/attr_must_use_unused.stderr new file mode 100644 index 0000000..0d6815b --- /dev/null +++ b/crates/core/tests/ui_attr/attr_must_use_unused.stderr @@ -0,0 +1,15 @@ +error: unused return value of `<(dyn Computed + '__devirt)>::compute` that must be used + --> tests/ui_attr/attr_must_use_unused.rs:24:5 + | +24 | d.compute(); // ERROR: unused return value that must be used + | ^^^^^^^^^^^ + | +note: the lint level is defined here + --> tests/ui_attr/attr_must_use_unused.rs:4:9 + | + 4 | #![deny(unused_must_use)] + | ^^^^^^^^^^^^^^^ +help: use `let _ = ...` to ignore the resulting value + | +24 | let _ = d.compute(); // ERROR: unused return value that must be used + | +++++++