From db3fef8b0acfb68f92f1e86efac1249cf78b877a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 14:46:14 +0000 Subject: [PATCH 1/3] Emit dispatch shims for dyn Trait + Send/Sync/Send+Sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inherent dispatch methods were only emitted on `impl dyn Trait`, so callers holding `&(dyn Trait + Send)` silently fell back to plain vtable dispatch with zero devirt benefit. Emit three additional inherent impl blocks that delegate to the base dispatch via coercion. Auto traits do not change the vtable layout, so the delegation is zero-cost — LLVM eliminates it entirely with `#[inline(always)]`. - Extract `rewrite_sig_with_named_args` shared helper - Add `generate_delegating_method` for auto-trait delegation - Emit impl blocks for Send, Sync, and Send+Sync in `expand_trait` - Add trybuild pass test (attr_dyn_send.rs) - Add equivalence test for auto-trait dispatch - Add shuffled_send benchmark group - Update README with dyn Trait + Send documentation https://claude.ai/code/session_01UvCHSU2321W1zxHppYuyyG --- README.md | 20 +++ crates/core/benches/dispatch.rs | 70 ++++++++- crates/core/tests/equivalence.rs | 49 +++++++ crates/core/tests/ui_attr.rs | 1 + crates/core/tests/ui_attr/attr_dyn_send.rs | 53 +++++++ crates/macros/src/lib.rs | 160 +++++++++++++++++---- 6 files changed, 323 insertions(+), 30 deletions(-) create mode 100644 crates/core/tests/ui_attr/attr_dyn_send.rs diff --git a/README.md b/README.md index cc54cc6..c891d41 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,26 @@ devirt::devirt! { Both APIs produce identical expanded code. +### `dyn Trait + Send` / `Sync` + +The proc-macro attribute automatically emits dispatch shims for +`dyn Trait + Send`, `dyn Trait + Sync`, and `dyn Trait + Send + Sync`. +This means `Box` and `&(dyn Shape + Send + Sync)` +benefit from devirtualization without any extra annotation: + +```rust +fn total_area_send(shapes: &[Box]) -> f64 { + shapes.iter().map(|s| s.area()).sum() +} +``` + +Auto traits do not change the vtable layout, so the delegation is +zero-cost — LLVM eliminates it entirely with `#[inline(always)]`. + +> **Note:** The declarative macro (`default-features = false`) does not +> emit auto-trait inherent impls. Use the proc-macro attribute for +> `dyn Trait + Send` support. + ## When to use Best when a small number of hot types dominate the population (80%+ of trait diff --git a/crates/core/benches/dispatch.rs b/crates/core/benches/dispatch.rs index 4a6e98d..fdcf0a7 100644 --- a/crates/core/benches/dispatch.rs +++ b/crates/core/benches/dispatch.rs @@ -586,11 +586,79 @@ fn bench_shuffled_mixed(c: &mut Criterion) { group.finish(); } +// ───────────────────────────────────────────────────────────────────── +// dyn Trait + Send benchmark. Exercises the same shuffled 80/20 +// workload as `bench_shuffled_mixed` but through `Box` to verify that the delegating inherent impls are inlined +// and devirt still wins over plain vtable dispatch. +// ───────────────────────────────────────────────────────────────────── + +fn make_shuffled_devirt_send(n: usize) -> Vec> { + let mut v: Vec> = Vec::with_capacity(n); + for i in 0..n { + let bucket = (i * 7 + 3) % 10; + v.push(match bucket { + 0..=3 => Box::new(Circle { radius: 5.0 }), + 4..=7 => Box::new(Rect { w: 3.0, h: 4.0 }), + 8 => Box::new(Triangle { a: 3.0, b: 4.0, c: 5.0 }), + _ => Box::new(Hexagon { side: 1.5 }), + }); + } + v +} + +fn make_shuffled_plain_send(n: usize) -> Vec> { + let mut v: Vec> = Vec::with_capacity(n); + for i in 0..n { + let bucket = (i * 7 + 3) % 10; + v.push(match bucket { + 0..=3 => Box::new(Circle { radius: 5.0 }), + 4..=7 => Box::new(Rect { w: 3.0, h: 4.0 }), + 8 => Box::new(Triangle { a: 3.0, b: 4.0, c: 5.0 }), + _ => Box::new(Hexagon { side: 1.5 }), + }); + } + v +} + +fn bench_shuffled_send(c: &mut Criterion) { + let mut group = c.benchmark_group("shuffled_send"); + + for &n in &[10_usize, 100, 1000] { + let label_devirt = format!("devirt_n{n}"); + group.bench_function(&label_devirt, |b| { + let shapes = make_shuffled_devirt_send(n); + b.iter(|| { + let mut total = 0.0_f64; + for s in &shapes { + total += black_box(s.as_ref()).area(); + } + total + }); + }); + + let label_plain = format!("plain_n{n}"); + group.bench_function(&label_plain, |b| { + let shapes = make_shuffled_plain_send(n); + b.iter(|| { + let mut total = 0.0_f64; + for s in &shapes { + total += black_box(s.as_ref()).area(); + } + total + }); + }); + } + + group.finish(); +} + criterion_group!( benches, bench_area, bench_scale, bench_mixed_vec, - bench_shuffled_mixed + bench_shuffled_mixed, + bench_shuffled_send ); criterion_main!(benches); diff --git a/crates/core/tests/equivalence.rs b/crates/core/tests/equivalence.rs index 3cc3c3e..d274437 100644 --- a/crates/core/tests/equivalence.rs +++ b/crates/core/tests/equivalence.rs @@ -95,6 +95,55 @@ fn decl_dispatch() { assert_eq!(c.val, 100); } +// ── Auto-trait dispatch: dyn Trait + Send / Sync / Send + Sync ────────────── + +#[cfg(feature = "macros")] +#[test] +fn attr_auto_trait_dispatch() { + // Verify that dispatch through &(dyn T + Send), &(dyn T + Sync), + // and &(dyn T + Send + Sync) produces the same results as &dyn T + // for both hot and cold types. + + // --- &self, non-void (hot) --- + let h = Hot { val: 42 }; + let base = (&h as &dyn attr::T).get(); + assert_eq!((&h as &(dyn attr::T + Send)).get(), base); + assert_eq!((&h as &(dyn attr::T + Sync)).get(), base); + assert_eq!((&h as &(dyn attr::T + Send + Sync)).get(), base); + + // --- &self, non-void (cold) --- + let c = Cold { val: 42 }; + let base = (&c as &dyn attr::T).get(); + assert_eq!((&c as &(dyn attr::T + Send)).get(), base); + assert_eq!((&c as &(dyn attr::T + Sync)).get(), base); + assert_eq!((&c as &(dyn attr::T + Send + Sync)).get(), base); + + // --- &self, void --- + (&h as &(dyn attr::T + Send)).notify(1); + (&h as &(dyn attr::T + Sync)).notify(1); + (&h as &(dyn attr::T + Send + Sync)).notify(1); + + // --- &mut self, non-void (hot) --- + let mut h1 = Hot { val: 10 }; + let mut h2 = Hot { val: 10 }; + let expected = (&mut h1 as &mut dyn attr::T).transform(5); + assert_eq!( + (&mut h2 as &mut (dyn attr::T + Send)).transform(5), + expected, + ); + + // --- &mut self, void (hot) --- + let mut h3 = Hot { val: 10 }; + let mut h4 = Hot { val: 10 }; + (&mut h3 as &mut dyn attr::T).reset(99); + (&mut h4 as &mut (dyn attr::T + Send)).reset(99); + assert_eq!(h3.val, h4.val); + + // --- Box --- + let boxed: Box = Box::new(Hot { val: 7 }); + assert_eq!(boxed.get(), 7); +} + // ── Extended proc-macro tests: supertraits, method lifetimes, #[must_use] ── #[cfg(feature = "macros")] diff --git a/crates/core/tests/ui_attr.rs b/crates/core/tests/ui_attr.rs index cc97806..5301ac6 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.pass("tests/ui_attr/attr_dyn_send.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"); diff --git a/crates/core/tests/ui_attr/attr_dyn_send.rs b/crates/core/tests/ui_attr/attr_dyn_send.rs new file mode 100644 index 0000000..7c551f4 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_dyn_send.rs @@ -0,0 +1,53 @@ +struct Hot { val: u64 } +struct Cold { val: u64 } + +#[devirt::devirt(Hot)] +pub trait Shape { + fn area(&self) -> f64; + fn scale(&mut self, factor: f64); +} + +#[devirt::devirt] +impl Shape for Hot { + fn area(&self) -> f64 { self.val as f64 } + fn scale(&mut self, factor: f64) { self.val = (self.val as f64 * factor) as u64; } +} + +#[devirt::devirt] +impl Shape for Cold { + fn area(&self) -> f64 { self.val as f64 + 1.0 } + fn scale(&mut self, factor: f64) { self.val = (self.val as f64 * factor) as u64 + 1; } +} + +fn use_ref(s: &dyn Shape) -> f64 { s.area() } +fn use_send_ref(s: &(dyn Shape + Send)) -> f64 { s.area() } +fn use_sync_ref(s: &(dyn Shape + Sync)) -> f64 { s.area() } +fn use_send_sync_ref(s: &(dyn Shape + Send + Sync)) -> f64 { s.area() } + +fn use_send_mut(s: &mut (dyn Shape + Send), f: f64) { s.scale(f); } + +fn use_boxed_send(s: Box) -> f64 { s.area() } + +fn main() { + let h = Hot { val: 42 }; + + assert_eq!(use_ref(&h), 42.0); + assert_eq!(use_send_ref(&h), 42.0); + assert_eq!(use_sync_ref(&h), 42.0); + assert_eq!(use_send_sync_ref(&h), 42.0); + + // Cold type — falls through to vtable + let c = Cold { val: 42 }; + assert_eq!(use_send_ref(&c), 43.0); + assert_eq!(use_sync_ref(&c), 43.0); + assert_eq!(use_send_sync_ref(&c), 43.0); + + // &mut self through dyn Trait + Send + let mut h2 = Hot { val: 10 }; + use_send_mut(&mut h2, 2.0); + assert_eq!(h2.val, 20); + + // Box + let boxed: Box = Box::new(Hot { val: 5 }); + assert_eq!(use_boxed_send(boxed), 5.0); +} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 7831058..d16ed0b 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -191,6 +191,20 @@ fn emit_trait_expansion( }) .collect(); + // Delegating methods for auto-trait inherent impls (Send, Sync, + // Send + Sync). Each method coerces `self` to the base `dyn Trait` + // and calls the dispatch method. + let delegating_methods: Vec<_> = trait_item + .items + .iter() + .filter_map(|item| { + let syn::TraitItem::Fn(m) = item else { + return None; + }; + Some(generate_delegating_method(m, name)) + }) + .collect(); + // Inner trait supertraits: `__FooImpl: Debug + Clone` let inner_supers = if supertraits.is_empty() { quote! {} @@ -257,6 +271,21 @@ fn emit_trait_expansion( #(#dispatch_methods)* } + // (4a) dyn Trait + Send — delegate to base dispatch. + impl<'__devirt> dyn #name + ::core::marker::Send + '__devirt { + #(#delegating_methods)* + } + + // (4b) dyn Trait + Sync — delegate to base dispatch. + impl<'__devirt> dyn #name + ::core::marker::Sync + '__devirt { + #(#delegating_methods)* + } + + // (4c) dyn Trait + Send + Sync — delegate to base dispatch. + impl<'__devirt> dyn #name + ::core::marker::Send + ::core::marker::Sync + '__devirt { + #(#delegating_methods)* + } + // (5) Public marker trait. #(#outer_attrs)* #vis #unsafety trait #name: #public_supers {} @@ -268,6 +297,45 @@ fn emit_trait_expansion( .into() } +// ── Shared helpers ────────────────────────────────────────────────────────── + +/// Clone a method signature, replacing wildcard `_` patterns with +/// generated names so arguments can be forwarded. Returns the +/// rewritten signature and a list of argument identifiers (excluding +/// `self`). +fn rewrite_sig_with_named_args( + sig: &syn::Signature, +) -> (syn::Signature, Vec) { + let mut sig = sig.clone(); + let mut arg_names = Vec::new(); + for (idx, arg) in 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}")); + } + } + } + } + (sig, arg_names) +} + // ── Dispatch method generation ────────────────────────────────────────────── fn generate_dispatch_method( @@ -295,35 +363,7 @@ fn generate_dispatch_method( 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 (dispatch_sig, arg_names) = rewrite_sig_with_named_args(sig); let raw_parts = if is_mut { quote! { let __raw = ::__devirt_raw_parts(&*self); } @@ -397,6 +437,68 @@ fn gen_hot_checks( .collect() } +// ── Delegating method generation (auto-trait impls) ───────────────────────── + +/// Generate a delegating shim that coerces `&(dyn Trait + Send)` (or +/// `Sync`, `Send + Sync`) to `&dyn Trait` and calls the base dispatch +/// method. Auto traits do not change the vtable layout, so the +/// coercion is zero-cost and LLVM eliminates the delegation entirely +/// with `#[inline(always)]`. +fn generate_delegating_method( + method: &syn::TraitItemFn, + trait_name: &syn::Ident, +) -> proc_macro2::TokenStream { + let sig = &method.sig; + let method_name = &sig.ident; + let attrs = &method.attrs; + + 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(); + let (dispatch_sig, arg_names) = rewrite_sig_with_named_args(sig); + + // Coerce to the base `dyn Trait` and call the dispatch method. + let coerce_and_call = if is_mut { + quote! { + let __devirt_base: &mut (dyn #trait_name + '__devirt) = self; + __devirt_base.#method_name(#(#arg_names),*) + } + } else { + quote! { + let __devirt_base: &(dyn #trait_name + '__devirt) = self; + __devirt_base.#method_name(#(#arg_names),*) + } + }; + + // When the method is `unsafe fn`, the base dispatch method is also + // `unsafe fn`, so the call must be inside an `unsafe` block. + let delegation = if is_unsafe { + quote! { unsafe { #coerce_and_call } } + } else { + coerce_and_call + }; + + quote! { + #(#attrs)* + #[doc(hidden)] + #[inline(always)] + pub #dispatch_sig { + #delegation + } + } +} + // ── Impl expansion ────────────────────────────────────────────────────────── fn expand_impl(attr: &TokenStream, impl_item: &syn::ItemImpl) -> TokenStream { From 545a496f57909011ce35f8fd58f00c37cb2ccf14 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 16:41:02 +0000 Subject: [PATCH 2/3] Use __devirt_ prefix for generated argument names Rename generated wildcard-replacement idents from `__arg{idx}` to `__devirt_arg{idx}` to match the crate's naming convention and eliminate any possibility of colliding with user parameter names. Removes the `unique_arg_ident` helper since the namespaced prefix makes collision-detection machinery unnecessary. https://claude.ai/code/session_01UvCHSU2321W1zxHppYuyyG --- crates/macros/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index d16ed0b..148f481 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -315,7 +315,7 @@ fn rewrite_sig_with_named_args( arg_names.push(pat_ident.ident.clone()); } syn::Pat::Wild(_) => { - let generated = format_ident!("__arg{idx}"); + let generated = format_ident!("__devirt_arg{idx}"); *pat_type.pat = syn::Pat::Ident(syn::PatIdent { attrs: vec![], by_ref: None, @@ -328,7 +328,7 @@ fn rewrite_sig_with_named_args( _ => { // Validation already rejects this case, but // generate a name defensively. - arg_names.push(format_ident!("__arg{idx}")); + arg_names.push(format_ident!("__devirt_arg{idx}")); } } } From d1cb7d135b61f4c4de7f07e9f73503bc5645bc2f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 16:48:44 +0000 Subject: [PATCH 3/3] Rewrite pat_type.pat in defensive fallback branch The `_ =>` branch in `rewrite_sig_with_named_args` pushed a generated ident into arg_names but did not rewrite the pattern in the signature, leaving the signature and call site out of sync. Now rewrites pat_type.pat to match, consistent with the Wild branch. https://claude.ai/code/session_01UvCHSU2321W1zxHppYuyyG --- crates/macros/src/lib.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 148f481..92c5e19 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -328,7 +328,15 @@ fn rewrite_sig_with_named_args( _ => { // Validation already rejects this case, but // generate a name defensively. - arg_names.push(format_ident!("__devirt_arg{idx}")); + let generated = format_ident!("__devirt_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); } } }