Skip to content

Emit dispatch shims for dyn Trait + Send/Sync/Send+Sync#17

Merged
Kab1r merged 3 commits intomainfrom
claude/dyn-trait-send-sync-3WaWg
Apr 12, 2026
Merged

Emit dispatch shims for dyn Trait + Send/Sync/Send+Sync#17
Kab1r merged 3 commits intomainfrom
claude/dyn-trait-send-sync-3WaWg

Conversation

@Kab1r
Copy link
Copy Markdown
Owner

@Kab1r Kab1r commented Apr 12, 2026

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

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
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 12, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Extended devirtualization to trait objects with auto traits (Send, Sync, Send+Sync) so call sites need no extra annotations.
  • Tests

    • Added UI and cfg-gated tests covering auto-trait dispatch across reference, mutable, and boxed trait-object forms; added benchmarks exercising Send-bounded trait-object dispatch.
  • Documentation

    • README updated with examples, clarifications on auto-trait behavior, and a note contrasting proc-macro vs. the declarative macro.

Walkthrough

Emits delegating inherent impls for dyn Trait + Send, dyn Trait + Sync, and dyn Trait + Send + Sync from the proc-macro; adds benchmarks, UI and integration tests, and README documentation demonstrating auto-trait devirtualization and examples.

Changes

Cohort / File(s) Summary
Documentation
README.md
Added section documenting expanded devirt support for auto-trait object forms (+Send, +Sync, +Send + Sync), examples, and notes about vtable layout and LLVM inlining.
Benchmarks
crates/core/benches/dispatch.rs
Added bench_shuffled_send group and helpers to benchmark accumulation over shuffled Vec<Box<dyn Shape + Send>> for n ∈ {10,100,1000}.
Tests — integration
crates/core/tests/equivalence.rs
Added #[cfg(feature = "macros")] test attr_auto_trait_dispatch validating devirtualized dispatch across &dyn T, &(dyn T + Send), &(dyn T + Sync), &(dyn T + Send + Sync), and Box<dyn T + Send> for hot/cold types and multiple receiver signatures.
Tests — UI
crates/core/tests/ui_attr.rs, crates/core/tests/ui_attr/attr_dyn_send.rs
Registered new UI test and added attr_dyn_send.rs which defines Shape, concrete types, and helper callers exercising dyn Shape with auto-trait and mutability permutations and asserting expected dispatch behavior.
Proc-macro implementation
crates/macros/src/lib.rs
Added rewrite_sig_with_named_args to convert wildcard (_) args to idents; refactored generate_dispatch_method to use it; added generate_delegating_method; extended emit_trait_expansion to emit inherent impls for dyn Trait + Send, + Sync, and + Send + Sync with delegating shims (wrapping calls in unsafe when original methods are unsafe).

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐇 I hopped through tokens bright,
Shims for Send and Sync in sight,
I coerce and forward, neat and small,
Calls find home—no bounce at all,
Tiny paws, big dispatch delight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely describes the main change: emitting dispatch shim methods for trait objects with auto traits (Send/Sync bounds).
Description check ✅ Passed The description is directly related to the changeset, explaining the problem, solution, and all code/test/documentation changes included in the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/dyn-trait-send-sync-3WaWg

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/macros/src/lib.rs`:
- Around line 306-337: The function rewrite_sig_with_named_args should avoid
generating idents that collide with existing parameter names: when handling
syn::Pat::Wild inside rewrite_sig_with_named_args, detect all existing idents
from sig.inputs (and the arg_names being built) and, instead of directly using
format_ident!("__arg{idx}"), generate a unique ident (e.g., "__arg{idx}",
"__arg{idx}_1", etc.) that does not appear in the existing set, assign that
unique ident into pat_type.pat and push it to arg_names; also apply the same
uniqueness logic for the defensive fallback that uses
format_ident!("__arg{idx}") so no generated name can duplicate an explicit
parameter ident (ensure you reference sig, sig.inputs, arg_names, Pat::Wild, and
the generated ident variable when locating the change).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5c4fa3dc-24fa-4526-a313-7472f0569bf2

📥 Commits

Reviewing files that changed from the base of the PR and between bfb3eba and db3fef8.

📒 Files selected for processing (6)
  • README.md
  • crates/core/benches/dispatch.rs
  • crates/core/tests/equivalence.rs
  • crates/core/tests/ui_attr.rs
  • crates/core/tests/ui_attr/attr_dyn_send.rs
  • crates/macros/src/lib.rs

Comment thread crates/macros/src/lib.rs
Comment on lines +306 to +337
fn rewrite_sig_with_named_args(
sig: &syn::Signature,
) -> (syn::Signature, Vec<syn::Ident>) {
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)
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 12, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd crates/macros && wc -l src/lib.rs

Repository: Kab1r/devirt

Length of output: 70


🏁 Script executed:

sed -n '306,337p' crates/macros/src/lib.rs

Repository: Kab1r/devirt

Length of output: 1236


🏁 Script executed:

# Look for test files or usages of this function
fd -e rs test | xargs grep -l "rewrite_sig_with_named_args" 2>/dev/null || echo "No test files found"
rg "rewrite_sig_with_named_args" crates/macros --type rs -A 3 -B 3

Repository: Kab1r/devirt

Length of output: 119


🏁 Script executed:

# Check the full context of the function and how it's used
sed -n '300,400p' crates/macros/src/lib.rs

Repository: Kab1r/devirt

Length of output: 3187


🏁 Script executed:

# Look for how arg_names is used in the generation
sed -n '340,450p' crates/macros/src/lib.rs

Repository: Kab1r/devirt

Length of output: 3615


🏁 Script executed:

# Search for all usages of arg_names in the context of generate_dispatch_method
rg "arg_names" crates/macros/src/lib.rs -B 5 -A 5

Repository: Kab1r/devirt

Length of output: 4172


🏁 Script executed:

# Check if there are any tests or examples that might demonstrate this issue
find . -name "*.rs" -type f | xargs grep -l "devirt\|Devirt" | head -5

Repository: Kab1r/devirt

Length of output: 210


🏁 Script executed:

# Check the equivalence tests
cat crates/core/tests/equivalence.rs | head -100

Repository: Kab1r/devirt

Length of output: 2853


🏁 Script executed:

# Look for examples or tests with __arg patterns
rg "__arg" . --type rust

Repository: Kab1r/devirt

Length of output: 239


🏁 Script executed:

# Check if there's any validation that prevents this case
rg "validation\|validate" crates/macros/src/lib.rs -B 2 -A 5

Repository: Kab1r/devirt

Length of output: 38


🏁 Script executed:

# Look for validation in the entire macros crate
rg "validate\|validation" crates/macros/src/lib.rs -B 3 -A 10

Repository: Kab1r/devirt

Length of output: 38


🏁 Script executed:

# Look for where the function is being validated
grep -n "validate" crates/macros/src/lib.rs | head -20

Repository: Kab1r/devirt

Length of output: 491


🏁 Script executed:

# Search for the main entry points and validation
sed -n '1,100p' crates/macros/src/lib.rs

Repository: Kab1r/devirt

Length of output: 3226


🏁 Script executed:

# Look at the validate_trait_method function
sed -n '109,160p' crates/macros/src/lib.rs

Repository: Kab1r/devirt

Length of output: 1680


🏁 Script executed:

# Let's create a test case to see if the collision issue actually occurs
# First, let me understand how the generated code looks
rg "dispatch_sig\|arg_names" crates/macros/src/lib.rs -B 2 -A 2

Repository: Kab1r/devirt

Length of output: 38


🏁 Script executed:

# Let's trace through an example manually - look at the full signature generation
sed -n '305,400p' crates/macros/src/lib.rs

Repository: Kab1r/devirt

Length of output: 2905


Synthesized argument names can collide with explicit parameters.

rewrite_sig_with_named_args generates __arg{idx} for wildcard parameters without checking if names already exist in the signature. A valid signature like fn f(&self, _: u32, __arg1: u32) creates two __arg1 parameters in the generated method, causing compilation failure.

♻️ Suggested fix
 fn rewrite_sig_with_named_args(
     sig: &syn::Signature,
 ) -> (syn::Signature, Vec<syn::Ident>) {
     let mut sig = sig.clone();
     let mut arg_names = Vec::new();
+    let mut used: std::collections::BTreeSet<String> = sig
+        .inputs
+        .iter()
+        .filter_map(|arg| match arg {
+            syn::FnArg::Typed(pat_type) => match &*pat_type.pat {
+                syn::Pat::Ident(pat_ident) => Some(pat_ident.ident.to_string()),
+                _ => None,
+            },
+            syn::FnArg::Receiver(_) => None,
+        })
+        .collect();
     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}");
+                    let generated = {
+                        let mut n = idx;
+                        loop {
+                            let candidate = format_ident!("__devirt_arg{n}");
+                            if used.insert(candidate.to_string()) {
+                                break candidate;
+                            }
+                            n += 1;
+                        }
+                    };
                     *pat_type.pat = syn::Pat::Ident(syn::PatIdent {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/macros/src/lib.rs` around lines 306 - 337, The function
rewrite_sig_with_named_args should avoid generating idents that collide with
existing parameter names: when handling syn::Pat::Wild inside
rewrite_sig_with_named_args, detect all existing idents from sig.inputs (and the
arg_names being built) and, instead of directly using
format_ident!("__arg{idx}"), generate a unique ident (e.g., "__arg{idx}",
"__arg{idx}_1", etc.) that does not appear in the existing set, assign that
unique ident into pat_type.pat and push it to arg_names; also apply the same
uniqueness logic for the defensive fallback that uses
format_ident!("__arg{idx}") so no generated name can duplicate an explicit
parameter ident (ensure you reference sig, sig.inputs, arg_names, Pat::Wild, and
the generated ident variable when locating the change).

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.

This is quite a contrived case

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@Kab1r, fair point — requiring a user to deliberately name a parameter __devirt_arg{n} (a clearly internal/generated naming convention) to trigger the collision is indeed very contrived. I'll drop this concern.

😿


🧠 Learnings used
Learnt from: CR
Repo: Kab1r/devirt PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-12T14:28:01.121Z
Learning: Limit declarative macro syntax support to plain fn method(&self, arg: Type) -> Ret; signatures only

Learnt from: CR
Repo: Kab1r/devirt PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-12T14:28:01.121Z
Learning: Support unsafe trait/unsafe fn, method lifetimes, method attributes, supertraits, extern ABI fn, and method where clauses in the proc-macro attribute expansion

Learnt from: CR
Repo: Kab1r/devirt PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-12T14:28:01.121Z
Learning: Use recursive macro expansion for dispatch arms instead of $()+  repetition to avoid macro_rules metavar depth conflicts

Learnt from: CR
Repo: Kab1r/devirt PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-12T14:28:01.121Z
Learning: Generate dispatch shim as an inherent impl dyn Trait block rather than trait default method to allow casting self as *const dyn Trait

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
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/macros/src/lib.rs`:
- Around line 328-332: The defensive fallback currently pushes a generated name
into arg_names but fails to rewrite the pattern stored in pat_type.pat, leaving
the signature and call out of sync; update the `_ => { ... }` branch so after
generating the identifier with format_ident!("__devirt_arg{idx}") you also
replace pat_type.pat with a Pat that binds that identifier (so the generated
name is actually used in the signature), referencing the existing symbols
arg_names, pat_type.pat, and the generated "__devirt_arg{idx}" identifier.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c231d5f5-cd16-4f8c-b1e7-0a6ad9ffb8cc

📥 Commits

Reviewing files that changed from the base of the PR and between db3fef8 and 545a496.

📒 Files selected for processing (1)
  • crates/macros/src/lib.rs

Comment thread crates/macros/src/lib.rs
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
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/macros/src/lib.rs`:
- Around line 317-340: The Pat::Wild(_) arm and the fallback _ arm contain
identical logic; replace them with a single match arm that handles both cases
(e.g., match on syn::Pat::Wild(_) | _ ) and perform the shared generation/update
steps (create generated via format_ident!("__devirt_arg{idx}"), assign to
*pat_type.pat using syn::Pat::Ident(syn::PatIdent { ... }), and push generated
into arg_names) in that one arm to remove duplication while preserving behavior
for syn::Pat::Wild, the defensive fallback, and the same references to
pat_type.pat and arg_names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1850278b-bbe3-43ac-b007-c6194a0038f0

📥 Commits

Reviewing files that changed from the base of the PR and between 545a496 and d1cb7d1.

📒 Files selected for processing (1)
  • crates/macros/src/lib.rs

Comment thread crates/macros/src/lib.rs
Comment on lines +317 to +340
syn::Pat::Wild(_) => {
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);
}
_ => {
// Validation already rejects this case, but
// generate a name defensively.
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider deduplicating Wild and fallback branches.

The Pat::Wild branch (lines 317-326) and the fallback _ branch (lines 328-340) contain identical logic. While the separation documents intent (expected vs. defensive), combining them would reduce maintenance burden.

♻️ Optional: Combine identical branches
                 syn::Pat::Ident(pat_ident) => {
                     arg_names.push(pat_ident.ident.clone());
                 }
-                syn::Pat::Wild(_) => {
-                    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);
-                }
-                _ => {
-                    // Validation already rejects this case, but
-                    // generate a name defensively.
+                // Pat::Wild is expected; other patterns are rejected by
+                // validation but handled defensively.
+                _ => {
                     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);
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
syn::Pat::Wild(_) => {
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);
}
_ => {
// Validation already rejects this case, but
// generate a name defensively.
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);
}
syn::Pat::Ident(pat_ident) => {
arg_names.push(pat_ident.ident.clone());
}
// Pat::Wild is expected; other patterns are rejected by
// validation but handled defensively.
_ => {
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);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/macros/src/lib.rs` around lines 317 - 340, The Pat::Wild(_) arm and
the fallback _ arm contain identical logic; replace them with a single match arm
that handles both cases (e.g., match on syn::Pat::Wild(_) | _ ) and perform the
shared generation/update steps (create generated via
format_ident!("__devirt_arg{idx}"), assign to *pat_type.pat using
syn::Pat::Ident(syn::PatIdent { ... }), and push generated into arg_names) in
that one arm to remove duplication while preserving behavior for syn::Pat::Wild,
the defensive fallback, and the same references to pat_type.pat and arg_names.

@Kab1r Kab1r merged commit 0b72d09 into main Apr 12, 2026
15 checks passed
@Kab1r Kab1r deleted the claude/dyn-trait-send-sync-3WaWg branch April 12, 2026 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants