From 97e7aacbdbd008520e212b1e8a4b511cd8543cea Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 11:56:31 +0000 Subject: [PATCH 1/5] Unify devirt APIs: devirt! macro + #[devirt] attribute (0.2.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `devirt::r#trait!` / `devirt::r#impl!` with a single `devirt` name that works as either a proc-macro attribute (default) or a declarative macro (with `default-features = false`). Both APIs delegate to `__devirt_define!`, a #[doc(hidden)] internal macro that holds all dispatch expansion logic. The `[hot]` marker on impls is removed — hot-path specialization is driven entirely by the trait's hot-type list. Breaking changes: - `devirt::r#trait!` and `devirt::r#impl!` removed - `[hot]` marker on impl blocks no longer accepted - Version bumped to 0.2.0 New crate structure: - crates/macros (devirt-macros): proc-macro crate with #[devirt] attr - crates/core: gains optional `macros` feature (on by default) Also updates LTO guidance: LTO is no longer required since all dispatch logic expands via macros into the user's crate. https://claude.ai/code/session_01XRfaF7hqTVzJwR8tBrnkom --- CLAUDE.md | 49 +++- Cargo.lock | 14 +- Cargo.toml | 5 +- README.md | 135 +++++---- crates/core/Cargo.toml | 9 + crates/core/benches/dispatch.rs | 19 +- crates/core/examples/shapes.rs | 24 +- crates/core/src/lib.rs | 258 +++++++++--------- crates/core/tests/equivalence.rs | 70 +++++ crates/core/tests/kani.rs | 56 ++-- crates/core/tests/ui/all_arms.rs | 11 +- crates/core/tests/ui/missing_method.rs | 7 +- crates/core/tests/ui/missing_method.stderr | 24 +- crates/core/tests/ui/multi_arg.rs | 7 +- crates/core/tests/ui/multi_hot.rs | 15 +- crates/core/tests/ui/pub_trait.rs | 7 +- crates/core/tests/ui/single_hot.rs | 7 +- crates/core/tests/ui/wrong_signature.rs | 7 +- crates/core/tests/ui/wrong_signature.stderr | 12 +- crates/core/tests/ui_attr.rs | 12 + crates/core/tests/ui_attr/attr_all_arms.rs | 45 +++ .../core/tests/ui_attr/attr_args_on_impl.rs | 15 + .../tests/ui_attr/attr_args_on_impl.stderr | 67 +++++ .../core/tests/ui_attr/attr_missing_args.rs | 8 + .../tests/ui_attr/attr_missing_args.stderr | 7 + crates/core/tests/ui_attr/attr_multi_hot.rs | 34 +++ crates/core/tests/ui_attr/attr_on_struct.rs | 6 + .../core/tests/ui_attr/attr_on_struct.stderr | 7 + crates/core/tests/ui_attr/attr_single_hot.rs | 18 ++ crates/macros/Cargo.toml | 21 ++ crates/macros/src/lib.rs | 143 ++++++++++ fuzz/fuzz_targets/dispatch.rs | 15 +- 32 files changed, 838 insertions(+), 296 deletions(-) create mode 100644 crates/core/tests/equivalence.rs create mode 100644 crates/core/tests/ui_attr.rs create mode 100644 crates/core/tests/ui_attr/attr_all_arms.rs create mode 100644 crates/core/tests/ui_attr/attr_args_on_impl.rs create mode 100644 crates/core/tests/ui_attr/attr_args_on_impl.stderr create mode 100644 crates/core/tests/ui_attr/attr_missing_args.rs create mode 100644 crates/core/tests/ui_attr/attr_missing_args.stderr create mode 100644 crates/core/tests/ui_attr/attr_multi_hot.rs create mode 100644 crates/core/tests/ui_attr/attr_on_struct.rs create mode 100644 crates/core/tests/ui_attr/attr_on_struct.stderr create mode 100644 crates/core/tests/ui_attr/attr_single_hot.rs create mode 100644 crates/macros/Cargo.toml create mode 100644 crates/macros/src/lib.rs diff --git a/CLAUDE.md b/CLAUDE.md index f4733c0..5f748e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,12 +8,21 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co # Build cargo build +# Build without proc-macro feature +cargo build -p devirt --no-default-features + # Run all tests (includes UI/compile-fail tests via trybuild) cargo test --workspace --exclude devirt-fuzz -# Run only UI/compile-fail tests +# Run declarative-macro-only tests +cargo test -p devirt --no-default-features + +# Run only UI/compile-fail tests (declarative macro) cargo test --test ui -p devirt +# Run only UI/compile-fail tests (proc-macro attribute) +cargo test --test ui_attr -p devirt + # Run Kani bounded model checker (requires cargo-kani) cargo kani --tests -p devirt @@ -22,7 +31,7 @@ cargo kani --tests -p devirt # Stacked Borrows cargo +nightly miri test -p devirt --lib -# Run benchmarks (LTO required for meaningful numbers) +# Run benchmarks cargo bench # Run example @@ -40,34 +49,46 @@ verus crates/verify/src/lib.rs --crate-type=lib ## Architecture -This is a workspace with three crates: +This is a workspace with four crates: -- **`crates/core`** (`devirt`) — the main proc-macro-free macro library. Two public macros: `r#trait!` and `r#impl!`. +- **`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/verify`** (`devirt-verify`) — Verus formal proofs of dispatch correctness. - **`fuzz`** — libfuzzer differential fuzzing comparing devirt dispatch vs. plain vtable. ### The Vtable-Pointer Comparison Pattern -The core idea: the generated dispatch shim lives in an inherent `impl dyn Trait { ... }` block (not as a trait default method). For each call, it extracts the `[data, vtable]` halves of the fat pointer via `transmute::<&dyn Trait, [usize; 2]>` and compares the vtable half against the compile-time-known vtable for each hot type (obtained by coercing a dangling `*const HotType` to `*const dyn Trait`). On match, the data pointer is reinterpreted as `&HotType` and the concrete type's `__spec_*` method is called directly (fully inlined under LTO). On a full miss, the shim falls through to a single vtable call via the inner trait's `__spec_*` method. +The core idea: the generated dispatch shim lives in an inherent `impl dyn Trait { ... }` block (not as a trait default method). For each call, it extracts the `[data, vtable]` halves of the fat pointer via `transmute::<&dyn Trait, [usize; 2]>` and compares the vtable half against the compile-time-known vtable for each hot type (obtained by coercing a dangling `*const HotType` to `*const dyn Trait`). On match, the data pointer is reinterpreted as `&HotType` and the concrete type's `__spec_*` method is called directly (fully inlined). On a full miss, the shim falls through to a single vtable call via the inner trait's `__spec_*` method. Why inherent methods on `dyn Trait` and not trait default methods: a default method body cannot cast `self as *const dyn Trait` because `Self: ?Sized` at that point. Inherent impls on `dyn Trait` have `Self = dyn Trait` directly, so the cast is just a ref-to-pointer conversion. -### `r#trait!` Expansion +### Macro Structure + +All dispatch expansion logic lives in `__devirt_define!` (`#[doc(hidden)]`, `#[macro_export]`, always available). It has two entry points: + +- `__devirt_define! { @trait ... }` — generates the trait, inner trait, dispatch shim, blanket impl +- `__devirt_define! { @impl ... }` — generates `impl __TraitNameImpl for T { __spec_* ... }` + +The public APIs delegate to it: + +- **`#[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 ... }` Expansion For a trait `Foo` with hot types `[A, B]`: -1. Generates hidden inner trait `__FooImpl` with `__spec_*` method declarations — user types provide the bodies via `r#impl!`. +1. Generates hidden inner trait `__FooImpl` with `__spec_*` method declarations. 2. Generates a compile-time assertion that `size_of::<*const dyn Foo>() == 2 * size_of::()`. 3. Generates `impl<'a> dyn Foo + 'a { ... }` with two primitive helpers — `__devirt_raw_parts(&Self) -> [usize; 2]` and `__devirt_vtable_for::() -> usize` — plus inherent methods for each user-declared trait method whose body is the vtable-comparison dispatch shim. 4. Generates public marker trait `Foo: __FooImpl` (no methods of its own). 5. Blanket impl: `impl Foo for T {}`. -### `r#impl!` Expansion +### `__devirt_define! { @impl ... }` Expansion -For `impl [hot] Foo for A { ... }` or `impl Foo for A { ... }`: +For `impl Foo for A { ... }`: - Expands to `impl __FooImpl for A { fn __spec_method(...) { ... } }`. -- The `[hot]` marker is accepted for backward compatibility but is purely documentary: hot-path specialization is driven entirely by the trait's hot-type list in `r#trait!`, not by per-impl overrides. -### Dispatch Arms (inside `src/lib.rs`) +### Dispatch Arms (inside `__devirt_define!`) 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 @@ -80,7 +101,9 @@ Recursive expansion (rather than `$()+` repetition) avoids macro_rules metavar d ### Verification Layers - **Miri** (`cargo +nightly miri test -p devirt --lib`): runs the `#[cfg(test)] mod primitives` harnesses — fat pointer layout, vtable identity, and end-to-end `&self` / `&mut self` / `Box` dispatch — under Stacked/Tree Borrows to catch aliasing violations in the unsafe transmute and `&mut` paths. -- **UI tests** (`tests/ui/`): trybuild compile tests; `.stderr` files capture expected error output for compile-fail cases. +- **UI tests** (`tests/ui/`): trybuild compile tests for `__devirt_define!` direct usage; `.stderr` files capture expected error output for compile-fail cases. +- **UI attr tests** (`tests/ui_attr/`): trybuild compile tests for `#[devirt]` attribute; gated on `macros` feature via `required-features`. +- **Equivalence test** (`tests/equivalence.rs`): verifies both APIs produce identical dispatch behavior. - **Kani** (`tests/kani.rs`): bounded model checker proofs for N=1,2,3 hot types, plus a `mod vt` section that directly verifies vtable-primitive soundness. - **Verus** (`crates/verify/`): full functional correctness proofs (Properties A, B, C) for the abstract `dispatch_spec`, plus a refinement lemma that proves `vtable_dispatch_spec` (a direct recursion over `(vt, hot_vts, values)` modelling the vtable-comparison shim) produces the same result as `dispatch_spec` on a projected `Seq>`, so Properties A/B/C transfer to the vtable-comparison implementation without reproof. @@ -88,5 +111,5 @@ Recursive expansion (rather than `$()+` repetition) avoids macro_rules metavar d - `#![no_std]` throughout `crates/core` - `paste` is a **regular** (not dev) dependency — it's needed at compile time during macro expansion -- Benchmarks require LTO to be meaningful (profile.bench sets `lto = "thin"`) +- `syn`, `quote`, `proc-macro2` are workspace dependencies used by `crates/macros` - Workspace lints are very strict: `deny` on suspicious/complexity/perf/style/cargo/pedantic/nursery clippy groups. `unsafe_code` is `deny` (not `forbid`) so `crates/core` can locally `#![allow(unsafe_code)]` — all other crates still disallow unsafe. diff --git a/Cargo.lock b/Cargo.lock index 9a6969c..6393d88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,9 +208,10 @@ dependencies = [ [[package]] name = "devirt" -version = "0.1.0" +version = "0.2.0" dependencies = [ "criterion", + "devirt-macros", "paste", "trybuild", ] @@ -224,9 +225,18 @@ dependencies = [ "libfuzzer-sys", ] +[[package]] +name = "devirt-macros" +version = "0.2.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "devirt-verify" -version = "0.1.0" +version = "0.2.0" dependencies = [ "vstd", ] diff --git a/Cargo.toml b/Cargo.toml index 8ad4794..c9787d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*", "fuzz"] resolver = "3" [workspace.package] -version = "0.1.0" +version = "0.2.0" edition = "2024" license = "MIT OR Apache-2.0" repository = "https://github.com/Kab1r/devirt" @@ -15,6 +15,9 @@ trybuild = "1" libfuzzer-sys = "0.4" arbitrary = { version = "1", features = ["derive"] } devirt = { path = "crates/core" } +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" vstd = { version = "=0.0.0-2026-04-05-0114", default-features = false } [profile.bench] diff --git a/README.md b/README.md index c613361..cc54cc6 100644 --- a/README.md +++ b/README.md @@ -12,67 +12,96 @@ through `&dyn Trait`, the generated dispatch shim extracts the vtable pointer from the fat pointer and compares it against compile-time-known vtable addresses for each hot type. On a match, the data pointer is reinterpreted as `&HotType` and the method is called directly (fully -inlined under LTO) — no vtable lookup, no indirect call. On a miss, the -shim falls through to a single vtable call via the hidden `__spec_*` -method. Callers use plain `dyn Trait` — no wrappers, no special calls, -zero API change at call sites. +inlined) — no vtable lookup, no indirect call. On a miss, the shim falls +through to a single vtable call via the hidden `__spec_*` method. +Callers use plain `dyn Trait` — no wrappers, no special calls, zero API +change at call sites. -## LTO required +## Usage + +The default API uses a proc-macro attribute: + +```rust +use std::f64::consts::PI; + +struct Circle { radius: f64 } +struct Rect { w: f64, h: f64 } +struct Triangle { a: f64, b: f64, c: f64 } + +// 1. Define trait — list hot types in the attribute +#[devirt::devirt(Circle, Rect)] +pub trait Shape { + fn area(&self) -> f64; + fn perimeter(&self) -> f64; + fn scale(&mut self, factor: f64); +} -This crate relies on cross-function inlining **and** cross-CGU vtable -deduplication. **Without LTO, the vtable-comparison may always miss -(because the trait and the hot type's impl live in different codegen units -and their vtables are not deduplicated), silently degrading to plain -`dyn Trait` dispatch.** +// 2. Hot type — vtable-cmp match, direct inlined call +#[devirt::devirt] +impl Shape for Circle { + fn area(&self) -> f64 { PI * self.radius * self.radius } + fn perimeter(&self) -> f64 { 2.0 * PI * self.radius } + fn scale(&mut self, factor: f64) { self.radius *= factor; } +} -Add this to your `Cargo.toml`: +#[devirt::devirt] +impl Shape for Rect { + fn area(&self) -> f64 { self.w * self.h } + fn perimeter(&self) -> f64 { 2.0 * (self.w + self.h) } + fn scale(&mut self, factor: f64) { self.w *= factor; self.h *= factor; } +} + +// 3. Cold type — falls back to vtable +#[devirt::devirt] +impl Shape for Triangle { + fn area(&self) -> f64 { + let s = (self.a + self.b + self.c) / 2.0; + (s * (s - self.a) * (s - self.b) * (s - self.c)).sqrt() + } + fn perimeter(&self) -> f64 { self.a + self.b + self.c } + fn scale(&mut self, factor: f64) { + self.a *= factor; self.b *= factor; self.c *= factor; + } +} + +// 4. Use — completely normal dyn Trait +fn total_area(shapes: &[Box]) -> f64 { + shapes.iter().map(|s| s.area()).sum() +} +``` + +### Without proc macros + +If you prefer zero proc-macro dependencies, disable the default `macros` +feature: ```toml -[profile.release] -lto = "thin" -codegen-units = 1 +[dependencies] +devirt = { version = "0.2", default-features = false } ``` -## Usage +Then use the declarative macro: ```rust -use devirt; - -// 1. Define trait — list hot types in brackets -devirt::r#trait! { - pub Shape [Circle, Rect] { +devirt::devirt! { + pub trait Shape [Circle, Rect] { fn area(&self) -> f64; fn perimeter(&self) -> f64; fn scale(&mut self, factor: f64); } } -// 2. Hot type — vtable-cmp match, direct inlined call under LTO -devirt::r#impl!(Shape for Circle [hot] { - fn area(&self) -> f64 { - core::f64::consts::PI * self.radius * self.radius - } - fn perimeter(&self) -> f64 { - 2.0 * core::f64::consts::PI * self.radius - } - fn scale(&mut self, factor: f64) { - self.radius *= factor; +devirt::devirt! { + impl Shape for Circle { + fn area(&self) -> f64 { PI * self.radius * self.radius } + fn perimeter(&self) -> f64 { 2.0 * PI * self.radius } + fn scale(&mut self, factor: f64) { self.radius *= factor; } } -}); - -// 3. Cold type — falls back to vtable -devirt::r#impl!(Shape for Triangle { - fn area(&self) -> f64 { /* ... */ } - fn perimeter(&self) -> f64 { /* ... */ } - fn scale(&mut self, factor: f64) { /* ... */ } -}); - -// 4. Use — completely normal dyn Trait -fn total_area(shapes: &[Box]) -> f64 { - shapes.iter().map(|s| s.area()).sum() } ``` +Both APIs produce identical expanded code. + ## When to use Best when a small number of hot types dominate the population (80%+ of trait @@ -91,10 +120,16 @@ objects). Common scenarios: ## Performance characteristics -| Path | Cost | -| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Hot type dispatch | Single `cmp` against a RIP-relative vtable address + direct, inlined method call under LTO. **No indirect call.** | -| Cold type dispatch | Adds ~0.3 ns per hot type over plain vtable dispatch — one `lea + cmp + jne` per hot type before the vtable fallback. Keep the hot list to ≤3 types to keep this overhead below a single cache-miss cycle. | +| Path | Cost | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Hot type dispatch | Single `cmp` against a RIP-relative vtable address + direct, inlined method call. **No indirect call.** | +| Cold type dispatch | Adds ~0.3 ns per hot type over plain vtable dispatch — one `lea + cmp + jne` per hot type before the vtable fallback. Keep the hot list to ≤3 types to keep this overhead below a single cache-miss cycle. | + +LTO is **not required** — all dispatch logic expands via macros into the +user's crate, so there are no cross-crate function calls to inline and +vtable deduplication works within a single crate via COMDAT groups. LTO +may still improve performance for other reasons in your project but is not +necessary for devirt to work correctly. ## Benchmarks @@ -140,10 +175,6 @@ pays the full branch misprediction cost on every call. Notes: -- **LTO is required.** Without it, the trait and the hot type's impl may - end up in different codegen units and their vtables may not be - deduplicated, so the comparison always misses and dispatch silently - degrades to plain vtable. - **Keep the hot list to ≤3 types.** Each hot type adds one `cmp + branch` to the cold path, and more than three becomes a net loss on cold-dominated workloads. @@ -156,11 +187,7 @@ Notes: Run benchmarks yourself: ```bash -# With LTO (default) cargo bench --bench dispatch - -# Without LTO (to stress-test) -RUSTFLAGS="-C lto=off -C codegen-units=256" cargo bench --bench dispatch ``` ## License diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 5714ac7..89e99ca 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -11,11 +11,20 @@ categories = ["no-std", "rust-patterns"] [dependencies] paste.workspace = true +devirt-macros = { path = "../macros", optional = true } + +[features] +default = ["macros"] +macros = ["dep:devirt-macros"] [dev-dependencies] criterion.workspace = true trybuild.workspace = true +[[test]] +name = "ui_attr" +required-features = ["macros"] + [[bench]] name = "dispatch" harness = false diff --git a/crates/core/benches/dispatch.rs b/crates/core/benches/dispatch.rs index e4147cf..b701b42 100644 --- a/crates/core/benches/dispatch.rs +++ b/crates/core/benches/dispatch.rs @@ -28,31 +28,32 @@ struct Hexagon { // ── Devirtualized trait (devirt macros) ─────────────────────────────────────── -devirt::r#trait! { +devirt::__devirt_define! { + @trait pub Shape [Circle, Rect] { fn area(&self) -> f64; fn scale(&mut self, factor: f64); } } -devirt::r#impl!(Shape for Circle [hot] { +devirt::__devirt_define! { @impl Shape for Circle { fn area(&self) -> f64 { core::f64::consts::PI * self.radius * self.radius } fn scale(&mut self, factor: f64) { self.radius *= factor; } -}); +}} -devirt::r#impl!(Shape for Rect [hot] { +devirt::__devirt_define! { @impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } fn scale(&mut self, factor: f64) { self.w *= factor; self.h *= factor; } -}); +}} -devirt::r#impl!(Shape for Triangle { +devirt::__devirt_define! { @impl Shape for Triangle { fn area(&self) -> f64 { let s = (self.a + self.b + self.c) / 2.0; (s * (s - self.a) * (s - self.b) * (s - self.c)).sqrt() @@ -62,12 +63,12 @@ devirt::r#impl!(Shape for Triangle { self.b *= factor; self.c *= factor; } -}); +}} -devirt::r#impl!(Shape for Hexagon { +devirt::__devirt_define! { @impl Shape for Hexagon { fn area(&self) -> f64 { 1.5 * 3.0_f64.sqrt() * self.side * self.side } fn scale(&mut self, factor: f64) { self.side *= factor; } -}); +}} // ── Explicit Branch-Based Dispatch ─────────────────────────────────────────── // This shows what pure branch-based dispatch looks like (comparing TypeTag enum) diff --git a/crates/core/examples/shapes.rs b/crates/core/examples/shapes.rs index 31ab10d..7ffd2e4 100644 --- a/crates/core/examples/shapes.rs +++ b/crates/core/examples/shapes.rs @@ -13,7 +13,8 @@ struct Triangle { a: f64, b: f64, c: f64 } struct Hexagon { side: f64 } // 1. Define trait — list hot types in brackets -devirt::r#trait! { +devirt::__devirt_define! { + @trait /// Shapes with area, perimeter, and uniform scaling. pub Shape [Circle, Rect] { /// Returns the area of this shape. @@ -31,10 +32,9 @@ devirt::r#trait! { } } -// 2. Implement — normal-looking impl blocks; [hot] marks a type as listed -// in the trait's hot list above (documentary — hot-path dispatch is -// driven entirely by the trait declaration, not by per-impl overrides) -devirt::r#impl!(Shape for Circle [hot] { +// 2. Implement — hot-path specialization is driven entirely by the +// trait's hot-type list, not by per-impl overrides +devirt::__devirt_define! { @impl Shape for Circle { fn area(&self) -> f64 { core::f64::consts::PI * self.radius * self.radius } @@ -52,9 +52,9 @@ devirt::r#impl!(Shape for Circle [hot] { println!("circle with radius {:.2}", self.radius); } fn name(&self) -> &str { "circle" } -}); +}} -devirt::r#impl!(Shape for Rect [hot] { +devirt::__devirt_define! { @impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } fn perimeter(&self) -> f64 { 2.0 * (self.w + self.h) } fn scale(&mut self, factor: f64) { @@ -70,9 +70,9 @@ devirt::r#impl!(Shape for Rect [hot] { println!("rectangle {}×{:.2}", self.w, self.h); } fn name(&self) -> &str { "rectangle" } -}); +}} -devirt::r#impl!(Shape for Triangle { +devirt::__devirt_define! { @impl Shape for Triangle { fn area(&self) -> f64 { let s = (self.a + self.b + self.c) / 2.0; (s * (s - self.a) * (s - self.b) * (s - self.c)).sqrt() @@ -94,10 +94,10 @@ devirt::r#impl!(Shape for Triangle { println!("triangle with sides {:.2}, {:.2}, {:.2}", self.a, self.b, self.c); } fn name(&self) -> &str { "triangle" } -}); +}} // Downstream type — not in the hot list, automatically uses vtable -devirt::r#impl!(Shape for Hexagon { +devirt::__devirt_define! { @impl Shape 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; } @@ -109,7 +109,7 @@ devirt::r#impl!(Shape for Hexagon { println!("regular hexagon with side {:.2}", self.side); } fn name(&self) -> &str { "hexagon" } -}); +}} // 3. Use — completely normal dyn Trait. Nothing special. diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 78f2b26..c4b1d24 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -4,7 +4,7 @@ //! extracts the vtable pointer from the `&dyn Trait` fat pointer and compares //! it against the compile-time-known vtable address for each hot type. On //! match, the data pointer is reinterpreted as `&HotType` and the method is -//! called directly (fully inlined under LTO). On miss, the shim falls through +//! called directly (fully inlined). On miss, the shim falls through //! to a single vtable call via the hidden `__spec_*` method. //! //! This eliminates the indirect call entirely on hot paths — there is no @@ -14,64 +14,67 @@ //! //! # Architecture //! -//! `r#trait!` generates: +//! The trait definition (`#[devirt::devirt(Hot1, Hot2)]` or +//! `devirt::devirt! { trait Foo [Hot1, Hot2] { ... } }`) generates: //! - A hidden inner trait `__XImpl` with `__spec_*` method declarations //! - Two `#[doc(hidden)]` inherent helpers on `dyn X`: //! `__devirt_raw_parts` (extracts `[data, vtable]` from a fat pointer) //! and `__devirt_vtable_for::()` (returns the compiler-assigned //! vtable address for `(T, X)`) -//! - A public trait `X` with default methods whose bodies compare the +//! - Inherent dispatch methods on `dyn X` whose bodies compare the //! runtime vtable pointer against each hot type's vtable and dispatch //! directly on match, or fall through to `__spec_*` otherwise //! - A blanket impl: `impl X for T {}` //! -//! `r#impl!` generates: +//! The impl (`#[devirt::devirt]` or `devirt::devirt! { impl Foo for T { ... } }`) +//! generates: //! - `impl __XImpl for ConcreteType { ... }` with the `__spec_*` bodies -//! - The `[hot]` marker is accepted for backward compatibility but is -//! purely documentary now: the hot-path optimization is driven entirely -//! by the trait's hot-type list, not by per-impl overrides +//! +//! Both the proc-macro attribute and the declarative macro delegate to +//! `__devirt_define!`, a `#[doc(hidden)]` internal macro that contains +//! all dispatch expansion logic. //! //! # Usage //! //! ```ignore -//! devirt::r#trait! { +//! // With proc-macro attribute (default): +//! #[devirt::devirt(HotType1, HotType2)] +//! pub trait MyTrait { +//! fn method(&self) -> ReturnType; +//! } +//! +//! #[devirt::devirt] +//! impl MyTrait for HotType1 { +//! fn method(&self) -> ReturnType { ... } +//! } +//! +//! // With declarative macro (default-features = false): +//! devirt::devirt! { //! pub MyTrait [HotType1, HotType2] { -//! /// Doc for the method. //! fn method(&self) -> ReturnType; //! } //! } //! -//! // Hot type — vtable-compare match, directly inlined under LTO -//! devirt::r#impl!(MyTrait for HotType1 [hot] { -//! fn method(&self) -> ReturnType { ... } -//! }); -//! -//! // Cold type — vtable-compare miss, falls back to vtable call -//! devirt::r#impl!(MyTrait for ColdType { -//! fn method(&self) -> ReturnType { ... } -//! }); +//! devirt::devirt! { +//! impl MyTrait for HotType1 { +//! fn method(&self) -> ReturnType { ... } +//! } +//! } //! ``` //! -//! # Required: enable LTO -//! -//! This crate relies on cross-function inlining and cross-CGU vtable -//! deduplication to eliminate dispatch overhead. Without LTO, the helper -//! fns may not inline and the vtable comparison may always miss (because -//! the trait and the hot type's impl live in different codegen units), -//! degrading to plain vtable dispatch. +//! # LTO //! -//! ```toml -//! [profile.release] -//! lto = "thin" -//! codegen-units = 1 -//! ``` +//! LTO is **not required**. All dispatch logic expands via macros into +//! the user's crate, so there are no cross-crate function calls to +//! inline. Vtable deduplication works within a single crate via COMDAT +//! groups, even with multiple codegen units. //! //! # Performance characteristics //! -//! With LTO enabled, hot-type dispatch is a single `cmp + je` against a -//! RIP-relative vtable address followed by an inlined method body — **no -//! indirect call**. Cold types pay a small branch-per-hot-type penalty on -//! the dispatch shim before the vtable fallback. +//! Hot-type dispatch is a single `cmp + je` against a RIP-relative vtable +//! address followed by an inlined method body — **no indirect call**. Cold +//! types pay a small branch-per-hot-type penalty on the dispatch shim +//! before the vtable fallback. //! //! The crate is most effective when hot types dominate the population (80%+ //! of trait objects). It is especially effective on *shuffled* collections @@ -118,34 +121,10 @@ extern crate kani; #[doc(hidden)] pub use paste::paste as __paste; -/// Declares a trait with transparent devirtualization. -/// -/// Hot types listed in brackets get vtable-pointer-comparison dispatch: -/// at each call site the dispatch shim compares the runtime vtable -/// pointer against the compile-time-known vtable for each hot type and, -/// on match, calls the concrete method directly (fully inlined under -/// LTO). Cold types fall through to normal vtable dispatch. Callers use -/// plain `dyn Trait` — no wrappers, no special calls. -/// -/// # Syntax -/// -/// ```ignore -/// devirt::r#trait! { -/// pub MyTrait [HotType1, HotType2] { -/// /// Doc comment forwarded to the generated trait method. -/// fn method(&self) -> ReturnType; -/// fn mut_method(&mut self, arg: ArgType); -/// } -/// } -/// ``` -/// -/// # Notes -/// -/// Hot types must be simple, unqualified type names (e.g., `Circle`, not -/// `crate::Circle`). +#[doc(hidden)] #[macro_export] -macro_rules! r#trait { - ( +macro_rules! __devirt_define { + (@trait $(#[$meta:meta])* $vis:vis $trait_name:ident [$($hot:ty),+ $(,)?] { $($methods:tt)* @@ -154,7 +133,7 @@ macro_rules! r#trait { $crate::__paste! { #[doc(hidden)] $vis trait [<__ $trait_name Impl>] { - $crate::r#trait!{@spec_decl $($methods)*} + $crate::__devirt_define!{@spec_decl $($methods)*} } // Compile-time sanity check: `*const dyn Trait` must be a fat @@ -226,7 +205,7 @@ macro_rules! r#trait { // $trait_name`) avoids the `Self: Sized` requirement that // would otherwise arise in a default method body. impl<'__devirt> dyn $trait_name + '__devirt { - $crate::r#trait!{ + $crate::__devirt_define!{ @inherent_decl [<__ $trait_name Impl>], $trait_name, @@ -264,7 +243,7 @@ macro_rules! r#trait { $crate::__paste! { fn [<__spec_ $method>](&self $(, $arg: $argty)*) $(-> $ret)?; } - $crate::r#trait!{@spec_decl $($rest)*} + $crate::__devirt_define!{@spec_decl $($rest)*} }; (@spec_decl @@ -275,7 +254,7 @@ macro_rules! r#trait { $crate::__paste! { fn [<__spec_ $method>](&mut self $(, $arg: $argty)*) $(-> $ret)?; } - $crate::r#trait!{@spec_decl $($rest)*} + $crate::__devirt_define!{@spec_decl $($rest)*} }; (@spec_decl) => {}; @@ -297,13 +276,13 @@ macro_rules! r#trait { #[inline] #[doc(hidden)] pub fn $method(&self $(, $arg: $argty)*) -> $ret { - $crate::r#trait!( + $crate::__devirt_define!( @dispatch_ref $inner, $trait_name, self, $method, ($($arg),*), [$($hot),+] ) } } - $crate::r#trait!{@inherent_decl $inner, $trait_name, [$($hot),+], $($rest)*} + $crate::__devirt_define!{@inherent_decl $inner, $trait_name, [$($hot),+], $($rest)*} }; // &self, void @@ -316,13 +295,13 @@ macro_rules! r#trait { #[inline] #[doc(hidden)] pub fn $method(&self $(, $arg: $argty)*) { - $crate::r#trait!( + $crate::__devirt_define!( @dispatch_void $inner, $trait_name, self, $method, ($($arg),*), [$($hot),+] ) } } - $crate::r#trait!{@inherent_decl $inner, $trait_name, [$($hot),+], $($rest)*} + $crate::__devirt_define!{@inherent_decl $inner, $trait_name, [$($hot),+], $($rest)*} }; // &mut self, non-void @@ -335,13 +314,13 @@ macro_rules! r#trait { #[inline] #[doc(hidden)] pub fn $method(&mut self $(, $arg: $argty)*) -> $ret { - $crate::r#trait!( + $crate::__devirt_define!( @dispatch_mut $inner, $trait_name, self, $method, ($($arg),*), [$($hot),+] ) } } - $crate::r#trait!{@inherent_decl $inner, $trait_name, [$($hot),+], $($rest)*} + $crate::__devirt_define!{@inherent_decl $inner, $trait_name, [$($hot),+], $($rest)*} }; // &mut self, void @@ -354,13 +333,13 @@ macro_rules! r#trait { #[inline] #[doc(hidden)] pub fn $method(&mut self $(, $arg: $argty)*) { - $crate::r#trait!( + $crate::__devirt_define!( @dispatch_mut_void $inner, $trait_name, self, $method, ($($arg),*), [$($hot),+] ) } } - $crate::r#trait!{@inherent_decl $inner, $trait_name, [$($hot),+], $($rest)*} + $crate::__devirt_define!{@inherent_decl $inner, $trait_name, [$($hot),+], $($rest)*} }; (@inherent_decl $inner:ident, $trait_name:ident, [$($hot:ty),+],) => {}; @@ -384,7 +363,7 @@ macro_rules! r#trait { ) => {{ let __raw: [usize; 2] = ::__devirt_raw_parts($this); - $crate::r#trait!(@dispatch_ref_chain + $crate::__devirt_define!(@dispatch_ref_chain $inner, $trait_name, $this, $method, ($($arg),*), [$($hot),+], __raw) }}; @@ -406,7 +385,7 @@ macro_rules! r#trait { __concrete.[<__spec_ $method>]($($arg),*) }; } - $crate::r#trait!(@dispatch_ref_chain + $crate::__devirt_define!(@dispatch_ref_chain $inner, $trait_name, $this, $method, ($($arg),*), [$($rest),*], $raw) }}; @@ -423,7 +402,7 @@ macro_rules! r#trait { ) => {{ let __raw: [usize; 2] = ::__devirt_raw_parts($this); - $crate::r#trait!(@dispatch_void_chain + $crate::__devirt_define!(@dispatch_void_chain $inner, $trait_name, $this, $method, ($($arg),*), [$($hot),+], __raw) }}; @@ -439,7 +418,7 @@ macro_rules! r#trait { } return; } - $crate::r#trait!(@dispatch_void_chain + $crate::__devirt_define!(@dispatch_void_chain $inner, $trait_name, $this, $method, ($($arg),*), [$($rest),*], $raw) }}; @@ -467,7 +446,7 @@ macro_rules! r#trait { // mutable alias. let __raw: [usize; 2] = ::__devirt_raw_parts(&*$this); - $crate::r#trait!(@dispatch_mut_chain + $crate::__devirt_define!(@dispatch_mut_chain $inner, $trait_name, $this, $method, ($($arg),*), [$($hot),+], __raw) }}; @@ -486,7 +465,7 @@ macro_rules! r#trait { __ref.[<__spec_ $method>]($($arg),*) }; } - $crate::r#trait!(@dispatch_mut_chain + $crate::__devirt_define!(@dispatch_mut_chain $inner, $trait_name, $this, $method, ($($arg),*), [$($rest),*], $raw) }}; @@ -503,7 +482,7 @@ macro_rules! r#trait { ) => {{ let __raw: [usize; 2] = ::__devirt_raw_parts(&*$this); - $crate::r#trait!(@dispatch_mut_void_chain + $crate::__devirt_define!(@dispatch_mut_void_chain $inner, $trait_name, $this, $method, ($($arg),*), [$($hot),+], __raw) }}; @@ -519,7 +498,7 @@ macro_rules! r#trait { } return; } - $crate::r#trait!(@dispatch_mut_void_chain + $crate::__devirt_define!(@dispatch_mut_void_chain $inner, $trait_name, $this, $method, ($($arg),*), [$($rest),*], $raw) }}; @@ -528,62 +507,86 @@ macro_rules! r#trait { ) => { $crate::__paste! { $inner::[<__spec_ $method>] }(&mut *$this $(, $arg)*) }; + + // ── @impl: implement a devirtualized trait for a concrete type ────────── + + (@impl $trait_name:ident for $type:ty { + $(fn $method:ident( $($args:tt)* ) $(-> $ret:ty)? { $($body:tt)* })* + }) => { + $crate::__paste! { + impl [<__ $trait_name Impl>] for $type { + $( + #[inline] + fn [<__spec_ $method>]( $($args)* ) $(-> $ret)? { $($body)* } + )* + } + } + }; } -/// Implements a devirtualized trait for a concrete type. +/// Declares a devirtualized trait or implements one for a concrete type. /// -/// The optional `[hot]` marker is accepted for backward compatibility and -/// as documentation that this type appears in the trait's hot list. It -/// does not change the expansion — hot-path specialization is driven -/// entirely by the trait's hot-type list via vtable-pointer comparison -/// in the generated dispatch shim. +/// Available when the `macros` feature is disabled (i.e., +/// `default-features = false`). When the default `macros` feature is +/// enabled, use the `#[devirt::devirt]` proc-macro attribute instead. /// /// # Syntax /// /// ```ignore -/// // Hot type (listed in the trait's `[...]` hot list): -/// devirt::r#impl!(MyTrait for HotType [hot] { -/// fn method(&self) -> ReturnType { ... } -/// }); +/// // Define a trait with hot types +/// devirt::devirt! { +/// pub MyTrait [HotType1, HotType2] { +/// fn method(&self) -> ReturnType; +/// fn mut_method(&mut self, arg: ArgType); +/// } +/// } /// -/// // Cold type (not in the hot list): -/// devirt::r#impl!(MyTrait for ColdType { -/// fn method(&self) -> ReturnType { ... } -/// }); +/// // Implement for a concrete type +/// devirt::devirt! { +/// impl MyTrait for HotType1 { +/// fn method(&self) -> ReturnType { ... } +/// fn mut_method(&mut self, arg: ArgType) { ... } +/// } +/// } /// ``` -/// -/// # Notes -/// -/// The type name in `[hot]` impls must be the same simple, unqualified name -/// used in the `r#trait!` hot list. +#[cfg(not(feature = "macros"))] #[macro_export] -macro_rules! r#impl { - // Cold impl — or equivalently, any impl without the `[hot]` marker. - ($trait_name:ident for $type:ty { - $(fn $method:ident( $($args:tt)* ) $(-> $ret:ty)? { $($body:tt)* })* - }) => { - $crate::__paste! { - impl [<__ $trait_name Impl>] for $type { - $( - #[inline] - fn [<__spec_ $method>]( $($args)* ) $(-> $ret)? { $($body)* } - )* +macro_rules! devirt { + // Trait definition + ( + $(#[$meta:meta])* + $vis:vis trait $name:ident [$($hot:ty),+ $(,)?] { + $($methods:tt)* + } + ) => { + $crate::__devirt_define! { + @trait + $(#[$meta])* + $vis $name [$($hot),+] { + $($methods)* } } }; - // Hot impl — the `[hot]` marker is purely documentary; the expansion - // is identical to the cold impl above because hot-path specialization - // is driven by the trait's hot list, not by per-impl overrides. - ($trait_name:ident for $type:ty [hot] { - $(fn $method:ident( $($args:tt)* ) $(-> $ret:ty)? { $($body:tt)* })* - }) => { - $crate::r#impl!($trait_name for $type { - $(fn $method( $($args)* ) $(-> $ret)? { $($body)* })* - }); + // Impl block + ( + impl $trait_name:ident for $type:ty { + $($methods:tt)* + } + ) => { + $crate::__devirt_define! { + @impl + $trait_name for $type { + $($methods)* + } + } }; } +// Re-export the proc-macro attribute when the `macros` feature is enabled. +#[cfg(feature = "macros")] +pub use devirt_macros::devirt; + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Unit tests for the unsafe primitives — verify the fat pointer layout // and vtable identity assumptions that underpin dispatch soundness. @@ -605,27 +608,28 @@ mod primitives { val: u64, } - crate::r#trait! { + crate::__devirt_define! { + @trait pub Probe [Hot, Also] { fn get(&self) -> u64; fn set(&mut self, v: u64); } } - crate::r#impl!(Probe for Hot [hot] { + crate::__devirt_define! { @impl Probe for Hot { fn get(&self) -> u64 { self.val } fn set(&mut self, v: u64) { self.val = v; } - }); + }} - crate::r#impl!(Probe for Also [hot] { + crate::__devirt_define! { @impl Probe for Also { fn get(&self) -> u64 { self.val.wrapping_add(1) } fn set(&mut self, v: u64) { self.val = v.wrapping_add(1); } - }); + }} - crate::r#impl!(Probe for Cold { + crate::__devirt_define! { @impl Probe for Cold { fn get(&self) -> u64 { self.val.wrapping_sub(1) } fn set(&mut self, v: u64) { self.val = v.wrapping_sub(1); } - }); + }} /// The fat pointer's first half is the data pointer, second is vtable. /// If this ever fails, every unsafe operation in the dispatch shim diff --git a/crates/core/tests/equivalence.rs b/crates/core/tests/equivalence.rs new file mode 100644 index 0000000..138cf47 --- /dev/null +++ b/crates/core/tests/equivalence.rs @@ -0,0 +1,70 @@ +#![allow(missing_docs, clippy::tests_outside_test_module)] + +struct Hot { + val: u64, +} + +struct Cold { + val: u64, +} + +#[cfg(not(feature = "macros"))] +mod decl { + use super::*; + + devirt::devirt! { + pub trait Probe [Hot] { + fn get(&self) -> u64; + } + } + + devirt::devirt! { + impl Probe for Hot { + fn get(&self) -> u64 { self.val } + } + } + + devirt::devirt! { + impl Probe for Cold { + fn get(&self) -> u64 { self.val + 1 } + } + } +} + +#[cfg(feature = "macros")] +mod attr { + use super::*; + + #[devirt::devirt(Hot)] + pub trait Probe { + fn get(&self) -> u64; + } + + #[devirt::devirt] + impl Probe for Hot { + fn get(&self) -> u64 { self.val } + } + + #[devirt::devirt] + impl Probe for Cold { + fn get(&self) -> u64 { self.val + 1 } + } +} + +#[cfg(not(feature = "macros"))] +#[test] +fn decl_dispatch() { + let h = Hot { val: 42 }; + let c = Cold { val: 42 }; + assert_eq!((&h as &dyn decl::Probe).get(), 42); + assert_eq!((&c as &dyn decl::Probe).get(), 43); +} + +#[cfg(feature = "macros")] +#[test] +fn attr_dispatch() { + let h = Hot { val: 42 }; + let c = Cold { val: 42 }; + assert_eq!((&h as &dyn attr::Probe).get(), 42); + assert_eq!((&c as &dyn attr::Probe).get(), 43); +} diff --git a/crates/core/tests/kani.rs b/crates/core/tests/kani.rs index 1c549bd..6fa88bb 100644 --- a/crates/core/tests/kani.rs +++ b/crates/core/tests/kani.rs @@ -24,7 +24,8 @@ mod n1 { val: u64, } - devirt::r#trait! { + devirt::__devirt_define! { + @trait pub Trait1 [Hot] { fn compute(&self, x: u64) -> u64; fn notify(&self, x: u64); @@ -33,19 +34,19 @@ mod n1 { } } - devirt::r#impl!(Trait1 for Hot [hot] { + devirt::__devirt_define! { @impl Trait1 for Hot { fn compute(&self, x: u64) -> u64 { self.val.wrapping_add(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_add(x); self.val } fn reset(&mut self, x: u64) { self.val = x; } - }); + }} - devirt::r#impl!(Trait1 for Cold { + devirt::__devirt_define! { @impl Trait1 for Cold { fn compute(&self, x: u64) -> u64 { self.val.wrapping_sub(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_sub(x); self.val } fn reset(&mut self, x: u64) { self.val = x.wrapping_add(1); } - }); + }} #[kani::proof] fn t1_hot_compute_equiv() { @@ -119,7 +120,8 @@ mod n2 { val: u64, } - devirt::r#trait! { + devirt::__devirt_define! { + @trait pub Trait2 [HotA, HotB] { fn compute(&self, x: u64) -> u64; fn notify(&self, x: u64); @@ -128,26 +130,26 @@ mod n2 { } } - devirt::r#impl!(Trait2 for HotA [hot] { + devirt::__devirt_define! { @impl Trait2 for HotA { fn compute(&self, x: u64) -> u64 { self.val.wrapping_add(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_add(x); self.val } fn reset(&mut self, x: u64) { self.val = x; } - }); + }} - devirt::r#impl!(Trait2 for HotB [hot] { + devirt::__devirt_define! { @impl Trait2 for HotB { fn compute(&self, x: u64) -> u64 { self.val | x } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val |= x; self.val } fn reset(&mut self, x: u64) { self.val = x.wrapping_add(1); } - }); + }} - devirt::r#impl!(Trait2 for Cold { + devirt::__devirt_define! { @impl Trait2 for Cold { fn compute(&self, x: u64) -> u64 { self.val.wrapping_sub(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_sub(x); self.val } fn reset(&mut self, x: u64) { self.val = x.wrapping_add(2); } - }); + }} #[kani::proof] fn t2_hot_a_compute_equiv() { @@ -253,7 +255,8 @@ mod n3 { val: u64, } - devirt::r#trait! { + devirt::__devirt_define! { + @trait pub Trait3 [HotA, HotB, HotC] { fn compute(&self, x: u64) -> u64; fn notify(&self, x: u64); @@ -262,33 +265,33 @@ mod n3 { } } - devirt::r#impl!(Trait3 for HotA [hot] { + devirt::__devirt_define! { @impl Trait3 for HotA { fn compute(&self, x: u64) -> u64 { self.val.wrapping_add(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_add(x); self.val } fn reset(&mut self, x: u64) { self.val = x; } - }); + }} - devirt::r#impl!(Trait3 for HotB [hot] { + devirt::__devirt_define! { @impl Trait3 for HotB { fn compute(&self, x: u64) -> u64 { self.val | x } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val |= x; self.val } fn reset(&mut self, x: u64) { self.val = x.wrapping_add(1); } - }); + }} - devirt::r#impl!(Trait3 for HotC [hot] { + devirt::__devirt_define! { @impl Trait3 for HotC { fn compute(&self, x: u64) -> u64 { self.val ^ x } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val ^= x; self.val } fn reset(&mut self, x: u64) { self.val = x.wrapping_add(2); } - }); + }} - devirt::r#impl!(Trait3 for Cold { + devirt::__devirt_define! { @impl Trait3 for Cold { fn compute(&self, x: u64) -> u64 { self.val.wrapping_sub(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_sub(x); self.val } fn reset(&mut self, x: u64) { self.val = x.wrapping_add(3); } - }); + }} #[kani::proof] fn t3_hot_a_compute_equiv() { @@ -400,19 +403,20 @@ mod vt { val: u64, } - devirt::r#trait! { + devirt::__devirt_define! { + @trait pub TraitVt [Hot] { fn compute(&self, x: u64) -> u64; } } - devirt::r#impl!(TraitVt for Hot [hot] { + devirt::__devirt_define! { @impl TraitVt for Hot { fn compute(&self, x: u64) -> u64 { self.val.wrapping_add(x) } - }); + }} - devirt::r#impl!(TraitVt for Cold { + devirt::__devirt_define! { @impl TraitVt for Cold { fn compute(&self, x: u64) -> u64 { self.val.wrapping_sub(x) } - }); + }} /// The vtable extracted from a concrete `&dyn TraitVt` must match /// the vtable the macro computes via `__devirt_vtable_for::()`. diff --git a/crates/core/tests/ui/all_arms.rs b/crates/core/tests/ui/all_arms.rs index b44e410..176179d 100644 --- a/crates/core/tests/ui/all_arms.rs +++ b/crates/core/tests/ui/all_arms.rs @@ -6,7 +6,8 @@ struct ColdType { val: f64, } -devirt::r#trait! { +devirt::__devirt_define! { + @trait pub AllArms [Hot] { fn ref_nonvoid(&self, x: f64) -> f64; fn ref_void(&self, x: f64); @@ -15,19 +16,19 @@ devirt::r#trait! { } } -devirt::r#impl!(AllArms for Hot [hot] { +devirt::__devirt_define! { @impl AllArms for Hot { fn ref_nonvoid(&self, x: f64) -> f64 { self.val + x } fn ref_void(&self, _x: f64) { } fn mut_nonvoid(&mut self, x: f64) -> f64 { self.val += x; self.val } fn mut_void(&mut self, x: f64) { self.val = x; } -}); +}} -devirt::r#impl!(AllArms for ColdType { +devirt::__devirt_define! { @impl AllArms for ColdType { fn ref_nonvoid(&self, x: f64) -> f64 { self.val + x } fn ref_void(&self, _x: f64) { } fn mut_nonvoid(&mut self, x: f64) -> f64 { self.val += x; self.val } fn mut_void(&mut self, x: f64) { self.val = x; } -}); +}} fn main() { let mut h: Box = Box::new(Hot { val: 1.0 }); diff --git a/crates/core/tests/ui/missing_method.rs b/crates/core/tests/ui/missing_method.rs index 6eacfc8..1cc1cee 100644 --- a/crates/core/tests/ui/missing_method.rs +++ b/crates/core/tests/ui/missing_method.rs @@ -1,14 +1,15 @@ struct Foo; -devirt::r#trait! { +devirt::__devirt_define! { + @trait pub TwoMethods [Foo] { fn first(&self) -> i32; fn second(&self) -> i32; } } -devirt::r#impl!(TwoMethods for Foo [hot] { +devirt::__devirt_define! { @impl TwoMethods for Foo { fn first(&self) -> i32 { 1 } -}); +}} fn main() {} diff --git a/crates/core/tests/ui/missing_method.stderr b/crates/core/tests/ui/missing_method.stderr index a41957f..e89f99a 100644 --- a/crates/core/tests/ui/missing_method.stderr +++ b/crates/core/tests/ui/missing_method.stderr @@ -1,17 +1,17 @@ error[E0046]: not all trait items implemented, missing: `__spec_second` - --> tests/ui/missing_method.rs:10:1 + --> tests/ui/missing_method.rs:11:1 | - 3 | / devirt::r#trait! { - 4 | | pub TwoMethods [Foo] { - 5 | | fn first(&self) -> i32; - 6 | | fn second(&self) -> i32; - 7 | | } - 8 | | } + 3 | / devirt::__devirt_define! { + 4 | | @trait + 5 | | pub TwoMethods [Foo] { + 6 | | fn first(&self) -> i32; +... | + 9 | | } | |_- `__spec_second` from trait - 9 | -10 | / devirt::r#impl!(TwoMethods for Foo [hot] { -11 | | fn first(&self) -> i32 { 1 } -12 | | }); +10 | +11 | / devirt::__devirt_define! { @impl TwoMethods for Foo { +12 | | fn first(&self) -> i32 { 1 } +13 | | }} | |__^ missing `__spec_second` in implementation | - = note: this error originates in the macro `$crate::impl` which comes from the expansion of the macro `devirt::impl` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `devirt::__devirt_define` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/core/tests/ui/multi_arg.rs b/crates/core/tests/ui/multi_arg.rs index 5e3f737..dda11b7 100644 --- a/crates/core/tests/ui/multi_arg.rs +++ b/crates/core/tests/ui/multi_arg.rs @@ -3,17 +3,18 @@ struct Widget { y: f64, } -devirt::r#trait! { +devirt::__devirt_define! { + @trait pub MultiArg [Widget] { fn add(&self, a: f64, b: f64) -> f64; fn set(&mut self, a: f64, b: f64); } } -devirt::r#impl!(MultiArg for Widget [hot] { +devirt::__devirt_define! { @impl MultiArg for Widget { fn add(&self, a: f64, b: f64) -> f64 { self.x + a + self.y + b } fn set(&mut self, a: f64, b: f64) { self.x = a; self.y = b; } -}); +}} fn main() { let mut w: Box = Box::new(Widget { x: 1.0, y: 2.0 }); diff --git a/crates/core/tests/ui/multi_hot.rs b/crates/core/tests/ui/multi_hot.rs index 0462fcd..54092c2 100644 --- a/crates/core/tests/ui/multi_hot.rs +++ b/crates/core/tests/ui/multi_hot.rs @@ -2,23 +2,24 @@ struct A; struct B; struct C; -devirt::r#trait! { +devirt::__devirt_define! { + @trait pub MultiHot [A, B, C] { fn id(&self) -> u8; } } -devirt::r#impl!(MultiHot for A [hot] { +devirt::__devirt_define! { @impl MultiHot for A { fn id(&self) -> u8 { 1 } -}); +}} -devirt::r#impl!(MultiHot for B [hot] { +devirt::__devirt_define! { @impl MultiHot for B { fn id(&self) -> u8 { 2 } -}); +}} -devirt::r#impl!(MultiHot for C [hot] { +devirt::__devirt_define! { @impl MultiHot for C { fn id(&self) -> u8 { 3 } -}); +}} fn main() { let items: Vec> = vec![ diff --git a/crates/core/tests/ui/pub_trait.rs b/crates/core/tests/ui/pub_trait.rs index e68b1b0..9ab023f 100644 --- a/crates/core/tests/ui/pub_trait.rs +++ b/crates/core/tests/ui/pub_trait.rs @@ -2,7 +2,8 @@ struct Inner { val: i32, } -devirt::r#trait! { +devirt::__devirt_define! { + @trait /// A public trait with documentation. pub DocTrait [Inner] { /// Returns the inner value. @@ -10,9 +11,9 @@ devirt::r#trait! { } } -devirt::r#impl!(DocTrait for Inner [hot] { +devirt::__devirt_define! { @impl DocTrait for Inner { fn get(&self) -> i32 { self.val } -}); +}} fn main() { let d: Box = Box::new(Inner { val: 42 }); diff --git a/crates/core/tests/ui/single_hot.rs b/crates/core/tests/ui/single_hot.rs index 676a0b6..221319f 100644 --- a/crates/core/tests/ui/single_hot.rs +++ b/crates/core/tests/ui/single_hot.rs @@ -2,15 +2,16 @@ struct Foo { val: f64, } -devirt::r#trait! { +devirt::__devirt_define! { + @trait pub SingleHot [Foo] { fn get(&self) -> f64; } } -devirt::r#impl!(SingleHot for Foo [hot] { +devirt::__devirt_define! { @impl SingleHot for Foo { fn get(&self) -> f64 { self.val } -}); +}} fn main() { let f: Box = Box::new(Foo { val: 1.0 }); diff --git a/crates/core/tests/ui/wrong_signature.rs b/crates/core/tests/ui/wrong_signature.rs index 69c08e6..23c53eb 100644 --- a/crates/core/tests/ui/wrong_signature.rs +++ b/crates/core/tests/ui/wrong_signature.rs @@ -1,13 +1,14 @@ struct Bar; -devirt::r#trait! { +devirt::__devirt_define! { + @trait pub WrongSig [Bar] { fn compute(&self, x: f64) -> f64; } } -devirt::r#impl!(WrongSig for Bar [hot] { +devirt::__devirt_define! { @impl WrongSig for Bar { fn compute(&self, x: u32) -> f64 { f64::from(x) } -}); +}} fn main() {} diff --git a/crates/core/tests/ui/wrong_signature.stderr b/crates/core/tests/ui/wrong_signature.stderr index c798c61..86fc52a 100644 --- a/crates/core/tests/ui/wrong_signature.stderr +++ b/crates/core/tests/ui/wrong_signature.stderr @@ -1,18 +1,18 @@ error[E0053]: method `__spec_compute` has an incompatible type for trait - --> tests/ui/wrong_signature.rs:10:26 + --> tests/ui/wrong_signature.rs:11:26 | -10 | fn compute(&self, x: u32) -> f64 { f64::from(x) } +11 | fn compute(&self, x: u32) -> f64 { f64::from(x) } | ^^^ expected `f64`, found `u32` | note: type in trait - --> tests/ui/wrong_signature.rs:5:30 + --> tests/ui/wrong_signature.rs:6:30 | - 5 | fn compute(&self, x: f64) -> f64; + 6 | fn compute(&self, x: f64) -> f64; | ^^^ = note: expected signature `fn(&Bar, f64) -> f64` found signature `fn(&Bar, u32) -> f64` help: change the parameter type to match the trait | -10 - fn compute(&self, x: u32) -> f64 { f64::from(x) } -10 + fn compute(&self, x: f64) -> f64 { f64::from(x) } +11 - fn compute(&self, x: u32) -> f64 { f64::from(x) } +11 + fn compute(&self, x: f64) -> f64 { f64::from(x) } | diff --git a/crates/core/tests/ui_attr.rs b/crates/core/tests/ui_attr.rs new file mode 100644 index 0000000..4d403da --- /dev/null +++ b/crates/core/tests/ui_attr.rs @@ -0,0 +1,12 @@ +#![allow(missing_docs, clippy::tests_outside_test_module)] + +#[test] +fn ui_attr() { + let t = trybuild::TestCases::new(); + t.pass("tests/ui_attr/attr_single_hot.rs"); + t.pass("tests/ui_attr/attr_multi_hot.rs"); + t.pass("tests/ui_attr/attr_all_arms.rs"); + t.compile_fail("tests/ui_attr/attr_missing_args.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_all_arms.rs b/crates/core/tests/ui_attr/attr_all_arms.rs new file mode 100644 index 0000000..708ad6d --- /dev/null +++ b/crates/core/tests/ui_attr/attr_all_arms.rs @@ -0,0 +1,45 @@ +struct Hot { + val: f64, +} + +struct ColdType { + val: f64, +} + +#[devirt::devirt(Hot)] +pub trait AllArms { + fn ref_nonvoid(&self, x: f64) -> f64; + fn ref_void(&self, x: f64); + fn mut_nonvoid(&mut self, x: f64) -> f64; + fn mut_void(&mut self, x: f64); +} + +#[devirt::devirt] +impl AllArms for Hot { + fn ref_nonvoid(&self, x: f64) -> f64 { self.val + x } + fn ref_void(&self, _x: f64) { } + fn mut_nonvoid(&mut self, x: f64) -> f64 { self.val += x; self.val } + fn mut_void(&mut self, x: f64) { self.val = x; } +} + +#[devirt::devirt] +impl AllArms for ColdType { + fn ref_nonvoid(&self, x: f64) -> f64 { self.val + x } + fn ref_void(&self, _x: f64) { } + fn mut_nonvoid(&mut self, x: f64) -> f64 { self.val += x; self.val } + fn mut_void(&mut self, x: f64) { self.val = x; } +} + +fn main() { + let mut h: Box = Box::new(Hot { val: 1.0 }); + assert_eq!(h.ref_nonvoid(2.0), 3.0); + h.ref_void(0.0); + assert_eq!(h.mut_nonvoid(5.0), 6.0); + h.mut_void(10.0); + + let mut c: Box = Box::new(ColdType { val: 1.0 }); + assert_eq!(c.ref_nonvoid(2.0), 3.0); + c.ref_void(0.0); + assert_eq!(c.mut_nonvoid(5.0), 6.0); + c.mut_void(10.0); +} diff --git a/crates/core/tests/ui_attr/attr_args_on_impl.rs b/crates/core/tests/ui_attr/attr_args_on_impl.rs new file mode 100644 index 0000000..4cada5c --- /dev/null +++ b/crates/core/tests/ui_attr/attr_args_on_impl.rs @@ -0,0 +1,15 @@ +struct Foo; + +devirt::__devirt_define! { + @trait + pub ArgsOnImpl [Foo] { + fn get(&self) -> i32; + } +} + +#[devirt::devirt(Foo)] +impl ArgsOnImpl for Foo { + fn get(&self) -> i32 { 1 } +} + +fn main() {} diff --git a/crates/core/tests/ui_attr/attr_args_on_impl.stderr b/crates/core/tests/ui_attr/attr_args_on_impl.stderr new file mode 100644 index 0000000..dcaf634 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_args_on_impl.stderr @@ -0,0 +1,67 @@ +error: hot types are specified on the trait definition, not the impl block + --> tests/ui_attr/attr_args_on_impl.rs:10:1 + | +10 | #[devirt::devirt(Foo)] + | ^^^^^^^^^^^^^^^^^^^^^^ + | + = 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 + | +5 | pub ArgsOnImpl [Foo] { + | ^^^ unsatisfied trait bound + | +help: the trait `__ArgsOnImplImpl` is not implemented for `Foo` + --> tests/ui_attr/attr_args_on_impl.rs:1:1 + | +1 | struct Foo; + | ^^^^^^^^^^ +help: this trait has no implementations, consider adding one + --> tests/ui_attr/attr_args_on_impl.rs:3:1 + | +3 | / devirt::__devirt_define! { +4 | | @trait +5 | | pub ArgsOnImpl [Foo] { +6 | | fn get(&self) -> i32; +7 | | } +8 | | } + | |_^ +note: required by a bound in `<(dyn ArgsOnImpl + '__devirt)>::__devirt_vtable_for` + --> tests/ui_attr/attr_args_on_impl.rs:3:1 + | +3 | / devirt::__devirt_define! { +4 | | @trait +5 | | pub ArgsOnImpl [Foo] { +6 | | fn get(&self) -> i32; +7 | | } +8 | | } + | | ^ + | | | + | |_required by a bound in this associated function + | required by this bound in `::__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 + --> tests/ui_attr/attr_args_on_impl.rs:3:1 + | +3 | / devirt::__devirt_define! { +4 | | @trait +5 | | pub ArgsOnImpl [Foo] { +6 | | fn get(&self) -> i32; +7 | | } +8 | | } + | |_^ method not found in `&Foo` + | + = 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] { +6 | | fn get(&self) -> i32; +7 | | } +8 | | } + | |_^ + = note: this error originates in the macro `$crate::__devirt_define` which comes from the expansion of the macro `devirt::__devirt_define` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/core/tests/ui_attr/attr_missing_args.rs b/crates/core/tests/ui_attr/attr_missing_args.rs new file mode 100644 index 0000000..064cb72 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_missing_args.rs @@ -0,0 +1,8 @@ +struct Foo; + +#[devirt::devirt] +pub trait MissingArgs { + fn get(&self) -> i32; +} + +fn main() {} diff --git a/crates/core/tests/ui_attr/attr_missing_args.stderr b/crates/core/tests/ui_attr/attr_missing_args.stderr new file mode 100644 index 0000000..c10ebef --- /dev/null +++ b/crates/core/tests/ui_attr/attr_missing_args.stderr @@ -0,0 +1,7 @@ +error: expected hot types: #[devirt(Type1, Type2)] + --> tests/ui_attr/attr_missing_args.rs:3:1 + | +3 | #[devirt::devirt] + | ^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `devirt::devirt` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/core/tests/ui_attr/attr_multi_hot.rs b/crates/core/tests/ui_attr/attr_multi_hot.rs new file mode 100644 index 0000000..5d73a2c --- /dev/null +++ b/crates/core/tests/ui_attr/attr_multi_hot.rs @@ -0,0 +1,34 @@ +struct A; +struct B; +struct C; + +#[devirt::devirt(A, B, C)] +pub trait MultiHot { + fn id(&self) -> u8; +} + +#[devirt::devirt] +impl MultiHot for A { + fn id(&self) -> u8 { 1 } +} + +#[devirt::devirt] +impl MultiHot for B { + fn id(&self) -> u8 { 2 } +} + +#[devirt::devirt] +impl MultiHot for C { + fn id(&self) -> u8 { 3 } +} + +fn main() { + let items: Vec> = vec![ + Box::new(A), + Box::new(B), + Box::new(C), + ]; + assert_eq!(items[0].id(), 1); + assert_eq!(items[1].id(), 2); + assert_eq!(items[2].id(), 3); +} diff --git a/crates/core/tests/ui_attr/attr_on_struct.rs b/crates/core/tests/ui_attr/attr_on_struct.rs new file mode 100644 index 0000000..e89fcbd --- /dev/null +++ b/crates/core/tests/ui_attr/attr_on_struct.rs @@ -0,0 +1,6 @@ +#[devirt::devirt(Foo)] +struct Foo { + val: i32, +} + +fn main() {} diff --git a/crates/core/tests/ui_attr/attr_on_struct.stderr b/crates/core/tests/ui_attr/attr_on_struct.stderr new file mode 100644 index 0000000..c0a3d58 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_on_struct.stderr @@ -0,0 +1,7 @@ +error: #[devirt] can only be applied to trait definitions or impl blocks + --> tests/ui_attr/attr_on_struct.rs:1:1 + | +1 | #[devirt::devirt(Foo)] + | ^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `devirt::devirt` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/core/tests/ui_attr/attr_single_hot.rs b/crates/core/tests/ui_attr/attr_single_hot.rs new file mode 100644 index 0000000..942da26 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_single_hot.rs @@ -0,0 +1,18 @@ +struct Foo { + val: f64, +} + +#[devirt::devirt(Foo)] +pub trait SingleHot { + fn get(&self) -> f64; +} + +#[devirt::devirt] +impl SingleHot for Foo { + fn get(&self) -> f64 { self.val } +} + +fn main() { + let f: Box = Box::new(Foo { val: 1.0 }); + assert_eq!(f.get(), 1.0); +} diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml new file mode 100644 index 0000000..bcc190f --- /dev/null +++ b/crates/macros/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "devirt-macros" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +readme = "../../README.md" +description = "Proc-macro attribute for devirt" +keywords = ["devirtualization", "vtable", "no-std", "dispatch", "performance"] +categories = ["no-std", "rust-patterns"] + +[lib] +proc-macro = true + +[dependencies] +syn.workspace = true +quote.workspace = true +proc-macro2.workspace = true + +[lints] +workspace = true diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs new file mode 100644 index 0000000..11cf46c --- /dev/null +++ b/crates/macros/src/lib.rs @@ -0,0 +1,143 @@ +//! 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. + +use proc_macro::TokenStream; +use quote::{ToTokens, quote}; +use syn::punctuated::Punctuated; +use syn::{Token, parse_macro_input}; + +/// Proc-macro attribute for transparent devirtualization. +/// +/// # On a trait definition +/// +/// ```ignore +/// #[devirt::devirt(Circle, Rect)] +/// pub trait Shape { +/// fn area(&self) -> f64; +/// fn scale(&mut self, factor: f64); +/// } +/// ``` +/// +/// # On an impl block +/// +/// ```ignore +/// #[devirt::devirt] +/// impl Shape for Circle { +/// fn area(&self) -> f64 { PI * self.radius * self.radius } +/// fn scale(&mut self, factor: f64) { self.radius *= factor; } +/// } +/// ``` +#[proc_macro_attribute] +pub fn devirt(attr: TokenStream, item: TokenStream) -> TokenStream { + // Try parsing as a trait first, then as an impl. + if let Ok(trait_item) = syn::parse::(item.clone()) { + return expand_trait(attr, &trait_item); + } + if let Ok(impl_item) = syn::parse::(item) { + return expand_impl(&attr, &impl_item); + } + syn::Error::new( + proc_macro2::Span::call_site(), + "#[devirt] can only be applied to trait definitions or impl blocks", + ) + .to_compile_error() + .into() +} + +fn expand_trait(attr: TokenStream, trait_item: &syn::ItemTrait) -> TokenStream { + if attr.is_empty() { + return syn::Error::new( + proc_macro2::Span::call_site(), + "expected hot types: #[devirt(Type1, Type2)]", + ) + .to_compile_error() + .into(); + } + + let hot_types: Vec = + parse_macro_input!(attr with Punctuated::::parse_terminated) + .into_iter() + .collect(); + + let vis = &trait_item.vis; + let name = &trait_item.ident; + + // 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); + } + sig.to_tokens(&mut methods_tokens); + methods_tokens.extend(quote! { ; }); + } + } + + let mut outer_attrs = proc_macro2::TokenStream::new(); + for a in &trait_item.attrs { + a.to_tokens(&mut outer_attrs); + } + + quote! { + ::devirt::__devirt_define! { + @trait + #outer_attrs + #vis #name [#(#hot_types),*] { + #methods_tokens + } + } + } + .into() +} + +fn expand_impl(attr: &TokenStream, impl_item: &syn::ItemImpl) -> TokenStream { + if !attr.is_empty() { + return syn::Error::new( + proc_macro2::Span::call_site(), + "hot types are specified on the trait definition, not the impl block", + ) + .to_compile_error() + .into(); + } + + let Some((_, trait_path, _)) = &impl_item.trait_ else { + return syn::Error::new( + proc_macro2::Span::call_site(), + "#[devirt] requires `impl Trait for Type`, not a bare impl block", + ) + .to_compile_error() + .into(); + }; + + let trait_name = &trait_path.segments.last().expect("trait path is empty").ident; + let ty = &impl_item.self_ty; + + let method_bodies: Vec<_> = impl_item + .items + .iter() + .filter_map(|item| { + if let syn::ImplItem::Fn(m) = item { + let sig = &m.sig; + let block = &m.block; + Some(quote! { #sig #block }) + } else { + None + } + }) + .collect(); + + quote! { + ::devirt::__devirt_define! { + @impl + #trait_name for #ty { + #(#method_bodies)* + } + } + } + .into() +} diff --git a/fuzz/fuzz_targets/dispatch.rs b/fuzz/fuzz_targets/dispatch.rs index 54fc311..8d1b727 100644 --- a/fuzz/fuzz_targets/dispatch.rs +++ b/fuzz/fuzz_targets/dispatch.rs @@ -25,7 +25,8 @@ struct Cold { // ── Devirtualized trait ───────────────────────────────────────────────────── -devirt::r#trait! { +devirt::__devirt_define! { + @trait pub Dispatch [HotA, HotB] { fn compute(&self, x: f64) -> f64; fn notify(&self, x: f64); @@ -37,7 +38,7 @@ devirt::r#trait! { } } -devirt::r#impl!(Dispatch for HotA [hot] { +devirt::__devirt_define! { @impl Dispatch for HotA { fn compute(&self, x: f64) -> f64 { self.val + x } fn notify(&self, x: f64) { self.trace.set(self.val + x); } fn transform(&mut self, x: f64) -> f64 { self.val += x; self.val } @@ -45,9 +46,9 @@ devirt::r#impl!(Dispatch for HotA [hot] { fn combine(&self, x: f64, y: f64) -> f64 { self.val + x + y } fn val(&self) -> f64 { self.val } fn trace_val(&self) -> f64 { self.trace.get() } -}); +}} -devirt::r#impl!(Dispatch for HotB [hot] { +devirt::__devirt_define! { @impl Dispatch for HotB { fn compute(&self, x: f64) -> f64 { self.val * x } fn notify(&self, x: f64) { self.trace.set(self.val * x); } fn transform(&mut self, x: f64) -> f64 { self.val *= x; self.val } @@ -55,9 +56,9 @@ devirt::r#impl!(Dispatch for HotB [hot] { fn combine(&self, x: f64, y: f64) -> f64 { self.val.mul_add(x, y) } fn val(&self) -> f64 { self.val } fn trace_val(&self) -> f64 { self.trace.get() } -}); +}} -devirt::r#impl!(Dispatch for Cold { +devirt::__devirt_define! { @impl Dispatch for Cold { fn compute(&self, x: f64) -> f64 { self.val - x } fn notify(&self, x: f64) { self.trace.set(self.val - x); } fn transform(&mut self, x: f64) -> f64 { self.val -= x; self.val } @@ -65,7 +66,7 @@ devirt::r#impl!(Dispatch for Cold { fn combine(&self, x: f64, y: f64) -> f64 { self.val - x - y } fn val(&self) -> f64 { self.val } fn trace_val(&self) -> f64 { self.trace.get() } -}); +}} // ── Plain trait (baseline — normal vtable dispatch) ───────────────────────── From dd5098734851886465525a43e993b589aa4cd8f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 12:00:07 +0000 Subject: [PATCH 2/5] Fix name collision when trait is named T The __devirt_define! macro used bare `T` as the generic parameter in __devirt_vtable_for and the blanket impl. When the user's trait was also named `T`, the generic parameter shadowed the trait name, causing a compile error. Rename both to `__DevirtT` to avoid collisions. Also updates the equivalence test to use trait name `T` to exercise this edge case. https://claude.ai/code/session_01XRfaF7hqTVzJwR8tBrnkom --- crates/core/src/lib.rs | 20 ++++++++++---------- crates/core/tests/equivalence.rs | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index c4b1d24..7c9d2b5 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -170,18 +170,18 @@ macro_rules! __devirt_define { #[doc(hidden)] #[inline(always)] pub fn __devirt_vtable_for< - T: [<__ $trait_name Impl>] + 'static, + __DevirtT: [<__ $trait_name Impl>] + 'static, >() -> usize { - // A dangling, non-null, aligned `*const T`. We never - // dereference it — the coercion below only reads the - // vtable metadata the compiler attaches. - let fake: *const T = ::core::ptr::without_provenance( - ::core::mem::align_of::(), + // A dangling, non-null, aligned `*const __DevirtT`. + // We never dereference it — the coercion below only + // reads the vtable metadata the compiler attaches. + let fake: *const __DevirtT = ::core::ptr::without_provenance( + ::core::mem::align_of::<__DevirtT>(), ); // Coercion is a metadata-attaching op; the resulting - // fat pointer's vtable half is the `(T, $trait_name)` - // vtable selected by the compiler. Its data half is - // `fake`, which we discard. + // fat pointer's vtable half is the + // `(__DevirtT, $trait_name)` vtable selected by the + // compiler. Its data half is `fake`, which we discard. let fat: *const Self = fake; // SAFETY: `*const Self` (dyn trait fat pointer) is // two `usize`s by the compile-time assertion above. @@ -229,7 +229,7 @@ macro_rules! __devirt_define { $(#[$meta])* $vis trait $trait_name: [<__ $trait_name Impl>] {} - impl] + ?Sized> $trait_name for T {} + impl<__DevirtT: [<__ $trait_name Impl>] + ?Sized> $trait_name for __DevirtT {} } }; diff --git a/crates/core/tests/equivalence.rs b/crates/core/tests/equivalence.rs index 138cf47..64825a3 100644 --- a/crates/core/tests/equivalence.rs +++ b/crates/core/tests/equivalence.rs @@ -13,19 +13,19 @@ mod decl { use super::*; devirt::devirt! { - pub trait Probe [Hot] { + pub trait T [Hot] { fn get(&self) -> u64; } } devirt::devirt! { - impl Probe for Hot { + impl T for Hot { fn get(&self) -> u64 { self.val } } } devirt::devirt! { - impl Probe for Cold { + impl T for Cold { fn get(&self) -> u64 { self.val + 1 } } } @@ -36,17 +36,17 @@ mod attr { use super::*; #[devirt::devirt(Hot)] - pub trait Probe { + pub trait T { fn get(&self) -> u64; } #[devirt::devirt] - impl Probe for Hot { + impl T for Hot { fn get(&self) -> u64 { self.val } } #[devirt::devirt] - impl Probe for Cold { + impl T for Cold { fn get(&self) -> u64 { self.val + 1 } } } @@ -56,8 +56,8 @@ mod attr { fn decl_dispatch() { let h = Hot { val: 42 }; let c = Cold { val: 42 }; - assert_eq!((&h as &dyn decl::Probe).get(), 42); - assert_eq!((&c as &dyn decl::Probe).get(), 43); + assert_eq!((&h as &dyn decl::T).get(), 42); + assert_eq!((&c as &dyn decl::T).get(), 43); } #[cfg(feature = "macros")] @@ -65,6 +65,6 @@ fn decl_dispatch() { fn attr_dispatch() { let h = Hot { val: 42 }; let c = Cold { val: 42 }; - assert_eq!((&h as &dyn attr::Probe).get(), 42); - assert_eq!((&c as &dyn attr::Probe).get(), 43); + assert_eq!((&h as &dyn attr::T).get(), 42); + assert_eq!((&c as &dyn attr::T).get(), 43); } From ecd061564a4de2a7d53af0e20cd13f01eb847f7a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 12:21:21 +0000 Subject: [PATCH 3/5] Address review feedback before merge - Examples, benchmarks, and fuzz target now use the public #[devirt::devirt] attribute API instead of __devirt_define! - Fix missing `trait` keyword in lib.rs doc example for devirt! - Extend equivalence test to exercise all four dispatch arms (&self->val, &self->void, &mut->val, &mut->void) - Add proc-macro validation: reject generic traits/impls, where clauses, supertraits, associated types/constants, and default method bodies with clear error messages - Forward method attributes (#[inline], #[cfg], etc.) through expand_impl instead of silently dropping them https://claude.ai/code/session_01XRfaF7hqTVzJwR8tBrnkom --- crates/core/benches/dispatch.rs | 30 +++++----- crates/core/examples/shapes.rs | 54 +++++++++--------- crates/core/src/lib.rs | 2 +- crates/core/tests/equivalence.rs | 52 +++++++++++++++++ crates/macros/src/lib.rs | 98 +++++++++++++++++++++++++++----- fuzz/fuzz_targets/dispatch.rs | 35 ++++++------ 6 files changed, 200 insertions(+), 71 deletions(-) diff --git a/crates/core/benches/dispatch.rs b/crates/core/benches/dispatch.rs index b701b42..4a6e98d 100644 --- a/crates/core/benches/dispatch.rs +++ b/crates/core/benches/dispatch.rs @@ -28,32 +28,33 @@ struct Hexagon { // ── Devirtualized trait (devirt macros) ─────────────────────────────────────── -devirt::__devirt_define! { - @trait - pub Shape [Circle, Rect] { - fn area(&self) -> f64; - fn scale(&mut self, factor: f64); - } +#[devirt::devirt(Circle, Rect)] +pub trait Shape { + fn area(&self) -> f64; + fn scale(&mut self, factor: f64); } -devirt::__devirt_define! { @impl Shape for Circle { +#[devirt::devirt] +impl Shape for Circle { fn area(&self) -> f64 { core::f64::consts::PI * self.radius * self.radius } fn scale(&mut self, factor: f64) { self.radius *= factor; } -}} +} -devirt::__devirt_define! { @impl Shape for Rect { +#[devirt::devirt] +impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } fn scale(&mut self, factor: f64) { self.w *= factor; self.h *= factor; } -}} +} -devirt::__devirt_define! { @impl Shape for Triangle { +#[devirt::devirt] +impl Shape for Triangle { fn area(&self) -> f64 { let s = (self.a + self.b + self.c) / 2.0; (s * (s - self.a) * (s - self.b) * (s - self.c)).sqrt() @@ -63,12 +64,13 @@ devirt::__devirt_define! { @impl Shape for Triangle { self.b *= factor; self.c *= factor; } -}} +} -devirt::__devirt_define! { @impl Shape for Hexagon { +#[devirt::devirt] +impl Shape for Hexagon { fn area(&self) -> f64 { 1.5 * 3.0_f64.sqrt() * self.side * self.side } fn scale(&mut self, factor: f64) { self.side *= factor; } -}} +} // ── Explicit Branch-Based Dispatch ─────────────────────────────────────────── // This shows what pure branch-based dispatch looks like (comparing TypeTag enum) diff --git a/crates/core/examples/shapes.rs b/crates/core/examples/shapes.rs index 7ffd2e4..62a9fc6 100644 --- a/crates/core/examples/shapes.rs +++ b/crates/core/examples/shapes.rs @@ -12,29 +12,28 @@ struct Rect { w: f64, h: f64 } struct Triangle { a: f64, b: f64, c: f64 } struct Hexagon { side: f64 } -// 1. Define trait — list hot types in brackets -devirt::__devirt_define! { - @trait - /// Shapes with area, perimeter, and uniform scaling. - pub Shape [Circle, Rect] { - /// Returns the area of this shape. - fn area(&self) -> f64; - /// Returns the perimeter of this shape. - fn perimeter(&self) -> f64; - /// Scales this shape uniformly by `factor`. - fn scale(&mut self, factor: f64); - /// Scales and returns whether the shape is still within a 100×100 bounding box. - fn try_scale(&mut self, factor: f64) -> bool; - /// Prints a one-line description to stdout. - fn describe(&self); - /// Returns the human-readable name of this shape. - fn name(&self) -> &str; - } +// 1. Define trait — list hot types in the attribute +#[devirt::devirt(Circle, Rect)] +/// Shapes with area, perimeter, and uniform scaling. +pub trait Shape { + /// Returns the area of this shape. + fn area(&self) -> f64; + /// Returns the perimeter of this shape. + fn perimeter(&self) -> f64; + /// Scales this shape uniformly by `factor`. + fn scale(&mut self, factor: f64); + /// Scales and returns whether the shape is still within a 100×100 bounding box. + fn try_scale(&mut self, factor: f64) -> bool; + /// Prints a one-line description to stdout. + fn describe(&self); + /// Returns the human-readable name of this shape. + fn name(&self) -> &str; } // 2. Implement — hot-path specialization is driven entirely by the // trait's hot-type list, not by per-impl overrides -devirt::__devirt_define! { @impl Shape for Circle { +#[devirt::devirt] +impl Shape for Circle { fn area(&self) -> f64 { core::f64::consts::PI * self.radius * self.radius } @@ -52,9 +51,10 @@ devirt::__devirt_define! { @impl Shape for Circle { println!("circle with radius {:.2}", self.radius); } fn name(&self) -> &str { "circle" } -}} +} -devirt::__devirt_define! { @impl Shape for Rect { +#[devirt::devirt] +impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } fn perimeter(&self) -> f64 { 2.0 * (self.w + self.h) } fn scale(&mut self, factor: f64) { @@ -70,9 +70,10 @@ devirt::__devirt_define! { @impl Shape for Rect { println!("rectangle {}×{:.2}", self.w, self.h); } fn name(&self) -> &str { "rectangle" } -}} +} -devirt::__devirt_define! { @impl Shape for Triangle { +#[devirt::devirt] +impl Shape for Triangle { fn area(&self) -> f64 { let s = (self.a + self.b + self.c) / 2.0; (s * (s - self.a) * (s - self.b) * (s - self.c)).sqrt() @@ -94,10 +95,11 @@ devirt::__devirt_define! { @impl Shape for Triangle { println!("triangle with sides {:.2}, {:.2}, {:.2}", self.a, self.b, self.c); } fn name(&self) -> &str { "triangle" } -}} +} // Downstream type — not in the hot list, automatically uses vtable -devirt::__devirt_define! { @impl Shape for Hexagon { +#[devirt::devirt] +impl Shape 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; } @@ -109,7 +111,7 @@ devirt::__devirt_define! { @impl Shape for Hexagon { println!("regular hexagon with side {:.2}", self.side); } fn name(&self) -> &str { "hexagon" } -}} +} // 3. Use — completely normal dyn Trait. Nothing special. diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 7c9d2b5..d0939d8 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -50,7 +50,7 @@ //! //! // With declarative macro (default-features = false): //! devirt::devirt! { -//! pub MyTrait [HotType1, HotType2] { +//! pub trait MyTrait [HotType1, HotType2] { //! fn method(&self) -> ReturnType; //! } //! } diff --git a/crates/core/tests/equivalence.rs b/crates/core/tests/equivalence.rs index 64825a3..f9e0efd 100644 --- a/crates/core/tests/equivalence.rs +++ b/crates/core/tests/equivalence.rs @@ -15,18 +15,27 @@ mod decl { devirt::devirt! { pub trait T [Hot] { fn get(&self) -> u64; + fn notify(&self, x: u64); + fn transform(&mut self, x: u64) -> u64; + fn reset(&mut self, x: u64); } } devirt::devirt! { impl T for Hot { fn get(&self) -> u64 { self.val } + fn notify(&self, _x: u64) { } + fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_add(x); self.val } + fn reset(&mut self, x: u64) { self.val = x; } } } devirt::devirt! { impl T for Cold { fn get(&self) -> u64 { self.val + 1 } + fn notify(&self, _x: u64) { } + fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_sub(x); self.val } + fn reset(&mut self, x: u64) { self.val = x.wrapping_add(1); } } } } @@ -38,33 +47,76 @@ mod attr { #[devirt::devirt(Hot)] pub trait T { fn get(&self) -> u64; + fn notify(&self, x: u64); + fn transform(&mut self, x: u64) -> u64; + fn reset(&mut self, x: u64); } #[devirt::devirt] impl T for Hot { fn get(&self) -> u64 { self.val } + fn notify(&self, _x: u64) { } + fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_add(x); self.val } + fn reset(&mut self, x: u64) { self.val = x; } } #[devirt::devirt] impl T for Cold { fn get(&self) -> u64 { self.val + 1 } + fn notify(&self, _x: u64) { } + fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_sub(x); self.val } + fn reset(&mut self, x: u64) { self.val = x.wrapping_add(1); } } } #[cfg(not(feature = "macros"))] #[test] fn decl_dispatch() { + // @dispatch_ref: &self, non-void let h = Hot { val: 42 }; let c = Cold { val: 42 }; assert_eq!((&h as &dyn decl::T).get(), 42); assert_eq!((&c as &dyn decl::T).get(), 43); + + // @dispatch_void: &self, void + (&h as &dyn decl::T).notify(1); + (&c as &dyn decl::T).notify(1); + + // @dispatch_mut: &mut self, non-void + let mut h = Hot { val: 10 }; + let mut c = Cold { val: 10 }; + assert_eq!((&mut h as &mut dyn decl::T).transform(5), 15); + assert_eq!((&mut c as &mut dyn decl::T).transform(3), 7); + + // @dispatch_mut_void: &mut self, void + (&mut h as &mut dyn decl::T).reset(99); + (&mut c as &mut dyn decl::T).reset(99); + assert_eq!(h.val, 99); + assert_eq!(c.val, 100); } #[cfg(feature = "macros")] #[test] fn attr_dispatch() { + // @dispatch_ref: &self, non-void let h = Hot { val: 42 }; let c = Cold { val: 42 }; assert_eq!((&h as &dyn attr::T).get(), 42); assert_eq!((&c as &dyn attr::T).get(), 43); + + // @dispatch_void: &self, void + (&h as &dyn attr::T).notify(1); + (&c as &dyn attr::T).notify(1); + + // @dispatch_mut: &mut self, non-void + let mut h = Hot { val: 10 }; + let mut c = Cold { val: 10 }; + assert_eq!((&mut h as &mut dyn attr::T).transform(5), 15); + assert_eq!((&mut c as &mut dyn attr::T).transform(3), 7); + + // @dispatch_mut_void: &mut self, void + (&mut h as &mut dyn attr::T).reset(99); + (&mut c as &mut dyn attr::T).reset(99); + assert_eq!(h.val, 99); + assert_eq!(c.val, 100); } diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 11cf46c..78ea425 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -57,6 +57,63 @@ fn expand_trait(attr: TokenStream, trait_item: &syn::ItemTrait) -> TokenStream { .into(); } + // Reject unsupported trait features. + if !trait_item.generics.params.is_empty() { + return 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, + "#[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( + t, + "#[devirt] does not support associated types", + ) + .to_compile_error() + .into(); + } + syn::TraitItem::Const(c) => { + return 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(); + } + } + _ => {} + } + } + let hot_types: Vec = parse_macro_input!(attr with Punctuated::::parse_terminated) .into_iter() @@ -114,28 +171,43 @@ 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, + "#[devirt] does not support generic impl blocks", + ) + .to_compile_error() + .into(); + } + if let Some(where_clause) = &impl_item.generics.where_clause { + return syn::Error::new_spanned( + where_clause, + "#[devirt] does not support where clauses on impl blocks", + ) + .to_compile_error() + .into(); + } + let trait_name = &trait_path.segments.last().expect("trait path is empty").ident; let ty = &impl_item.self_ty; - let method_bodies: Vec<_> = impl_item - .items - .iter() - .filter_map(|item| { - if let syn::ImplItem::Fn(m) = item { - let sig = &m.sig; - let block = &m.block; - Some(quote! { #sig #block }) - } else { - None + 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); } - }) - .collect(); + m.sig.to_tokens(&mut method_bodies); + m.block.to_tokens(&mut method_bodies); + } + } quote! { ::devirt::__devirt_define! { @impl #trait_name for #ty { - #(#method_bodies)* + #method_bodies } } } diff --git a/fuzz/fuzz_targets/dispatch.rs b/fuzz/fuzz_targets/dispatch.rs index 8d1b727..bc4c694 100644 --- a/fuzz/fuzz_targets/dispatch.rs +++ b/fuzz/fuzz_targets/dispatch.rs @@ -25,20 +25,19 @@ struct Cold { // ── Devirtualized trait ───────────────────────────────────────────────────── -devirt::__devirt_define! { - @trait - pub Dispatch [HotA, HotB] { - fn compute(&self, x: f64) -> f64; - fn notify(&self, x: f64); - fn transform(&mut self, x: f64) -> f64; - fn reset(&mut self, x: f64); - fn combine(&self, x: f64, y: f64) -> f64; - fn val(&self) -> f64; - fn trace_val(&self) -> f64; - } +#[devirt::devirt(HotA, HotB)] +pub trait Dispatch { + fn compute(&self, x: f64) -> f64; + fn notify(&self, x: f64); + fn transform(&mut self, x: f64) -> f64; + fn reset(&mut self, x: f64); + fn combine(&self, x: f64, y: f64) -> f64; + fn val(&self) -> f64; + fn trace_val(&self) -> f64; } -devirt::__devirt_define! { @impl Dispatch for HotA { +#[devirt::devirt] +impl Dispatch for HotA { fn compute(&self, x: f64) -> f64 { self.val + x } fn notify(&self, x: f64) { self.trace.set(self.val + x); } fn transform(&mut self, x: f64) -> f64 { self.val += x; self.val } @@ -46,9 +45,10 @@ devirt::__devirt_define! { @impl Dispatch for HotA { fn combine(&self, x: f64, y: f64) -> f64 { self.val + x + y } fn val(&self) -> f64 { self.val } fn trace_val(&self) -> f64 { self.trace.get() } -}} +} -devirt::__devirt_define! { @impl Dispatch for HotB { +#[devirt::devirt] +impl Dispatch for HotB { fn compute(&self, x: f64) -> f64 { self.val * x } fn notify(&self, x: f64) { self.trace.set(self.val * x); } fn transform(&mut self, x: f64) -> f64 { self.val *= x; self.val } @@ -56,9 +56,10 @@ devirt::__devirt_define! { @impl Dispatch for HotB { fn combine(&self, x: f64, y: f64) -> f64 { self.val.mul_add(x, y) } fn val(&self) -> f64 { self.val } fn trace_val(&self) -> f64 { self.trace.get() } -}} +} -devirt::__devirt_define! { @impl Dispatch for Cold { +#[devirt::devirt] +impl Dispatch for Cold { fn compute(&self, x: f64) -> f64 { self.val - x } fn notify(&self, x: f64) { self.trace.set(self.val - x); } fn transform(&mut self, x: f64) -> f64 { self.val -= x; self.val } @@ -66,7 +67,7 @@ devirt::__devirt_define! { @impl Dispatch for Cold { fn combine(&self, x: f64, y: f64) -> f64 { self.val - x - y } fn val(&self) -> f64 { self.val } fn trace_val(&self) -> f64 { self.trace.get() } -}} +} // ── Plain trait (baseline — normal vtable dispatch) ───────────────────────── From 318fc337a58aabe9fa006fa31b6cd498f79a6107 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 12:56:30 +0000 Subject: [PATCH 4/5] Support `unsafe trait`/`unsafe impl` and forward method attributes Bug 1: `#[devirt(Foo)] unsafe trait Bar` silently dropped `unsafe`, generating a safe trait. Fix: parameterize `__devirt_define!` with `[$($unsafety:tt)*]` and emit it on the inner trait, public trait, blanket impl, and user impl. Bug 2: Method attributes (`#[must_use]`, doc comments, `#[deprecated]`) were parsed but not emitted in `@inherent_decl` and `@impl` arms. Fix: forward `$(#[$attr])*` in all four `@inherent_decl` arms and add `$(#[$method_attr:meta])*` to the `@impl` arm. Also: - Add `unsafe trait`/`unsafe impl` arms to `devirt!` declarative macro - Forward unsafety through proc-macro `expand_trait`/`expand_impl` - Gate example/bench on `required-features = ["macros"]` so `--no-default-features` doesn't try to compile attribute-using code - Fix doc example missing `trait` keyword in `devirt!` macro - Add trybuild tests for both features (ui + ui_attr) - Update all ~38 existing `__devirt_define!` call sites with `[]` https://claude.ai/code/session_01XRfaF7hqTVzJwR8tBrnkom --- crates/core/Cargo.toml | 5 ++ crates/core/src/lib.rs | 66 +++++++++++++++---- crates/core/tests/kani.rs | 30 ++++----- crates/core/tests/ui.rs | 2 + crates/core/tests/ui/all_arms.rs | 6 +- crates/core/tests/ui/method_attrs.rs | 24 +++++++ crates/core/tests/ui/missing_method.rs | 4 +- crates/core/tests/ui/missing_method.stderr | 4 +- crates/core/tests/ui/multi_arg.rs | 4 +- crates/core/tests/ui/multi_hot.rs | 8 +-- crates/core/tests/ui/pub_trait.rs | 4 +- crates/core/tests/ui/single_hot.rs | 4 +- crates/core/tests/ui/unsafe_trait.rs | 33 ++++++++++ crates/core/tests/ui/wrong_signature.rs | 4 +- crates/core/tests/ui_attr.rs | 2 + .../core/tests/ui_attr/attr_args_on_impl.rs | 2 +- .../tests/ui_attr/attr_args_on_impl.stderr | 8 +-- .../core/tests/ui_attr/attr_method_attrs.rs | 23 +++++++ .../core/tests/ui_attr/attr_unsafe_trait.rs | 33 ++++++++++ crates/macros/src/lib.rs | 6 +- 20 files changed, 217 insertions(+), 55 deletions(-) create mode 100644 crates/core/tests/ui/method_attrs.rs create mode 100644 crates/core/tests/ui/unsafe_trait.rs create mode 100644 crates/core/tests/ui_attr/attr_method_attrs.rs create mode 100644 crates/core/tests/ui_attr/attr_unsafe_trait.rs diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 89e99ca..f9a8b71 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -21,6 +21,10 @@ macros = ["dep:devirt-macros"] criterion.workspace = true trybuild.workspace = true +[[example]] +name = "shapes" +required-features = ["macros"] + [[test]] name = "ui_attr" required-features = ["macros"] @@ -28,6 +32,7 @@ required-features = ["macros"] [[bench]] name = "dispatch" harness = false +required-features = ["macros"] [lints] workspace = true diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index d0939d8..13fd5c7 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -124,7 +124,7 @@ pub use paste::paste as __paste; #[doc(hidden)] #[macro_export] macro_rules! __devirt_define { - (@trait + (@trait [$($unsafety:tt)*] $(#[$meta:meta])* $vis:vis $trait_name:ident [$($hot:ty),+ $(,)?] { $($methods:tt)* @@ -132,7 +132,7 @@ macro_rules! __devirt_define { ) => { $crate::__paste! { #[doc(hidden)] - $vis trait [<__ $trait_name Impl>] { + $vis $($unsafety)* trait [<__ $trait_name Impl>] { $crate::__devirt_define!{@spec_decl $($methods)*} } @@ -227,9 +227,9 @@ macro_rules! __devirt_define { // (...)` or call `::__spec_$method // (&t, ...)` via UFCS. $(#[$meta])* - $vis trait $trait_name: [<__ $trait_name Impl>] {} + $vis $($unsafety)* trait $trait_name: [<__ $trait_name Impl>] {} - impl<__DevirtT: [<__ $trait_name Impl>] + ?Sized> $trait_name for __DevirtT {} + $($unsafety)* impl<__DevirtT: [<__ $trait_name Impl>] + ?Sized> $trait_name for __DevirtT {} } }; @@ -273,6 +273,7 @@ macro_rules! __devirt_define { $($rest:tt)* ) => { $crate::__paste! { + $(#[$attr])* #[inline] #[doc(hidden)] pub fn $method(&self $(, $arg: $argty)*) -> $ret { @@ -292,6 +293,7 @@ macro_rules! __devirt_define { $($rest:tt)* ) => { $crate::__paste! { + $(#[$attr])* #[inline] #[doc(hidden)] pub fn $method(&self $(, $arg: $argty)*) { @@ -311,6 +313,7 @@ macro_rules! __devirt_define { $($rest:tt)* ) => { $crate::__paste! { + $(#[$attr])* #[inline] #[doc(hidden)] pub fn $method(&mut self $(, $arg: $argty)*) -> $ret { @@ -330,6 +333,7 @@ macro_rules! __devirt_define { $($rest:tt)* ) => { $crate::__paste! { + $(#[$attr])* #[inline] #[doc(hidden)] pub fn $method(&mut self $(, $arg: $argty)*) { @@ -510,12 +514,16 @@ macro_rules! __devirt_define { // ── @impl: implement a devirtualized trait for a concrete type ────────── - (@impl $trait_name:ident for $type:ty { - $(fn $method:ident( $($args:tt)* ) $(-> $ret:ty)? { $($body:tt)* })* + (@impl [$($unsafety:tt)*] $trait_name:ident for $type:ty { + $( + $(#[$method_attr:meta])* + fn $method:ident( $($args:tt)* ) $(-> $ret:ty)? { $($body:tt)* } + )* }) => { $crate::__paste! { - impl [<__ $trait_name Impl>] for $type { + $($unsafety)* impl [<__ $trait_name Impl>] for $type { $( + $(#[$method_attr])* #[inline] fn [<__spec_ $method>]( $($args)* ) $(-> $ret)? { $($body)* } )* @@ -535,7 +543,7 @@ macro_rules! __devirt_define { /// ```ignore /// // Define a trait with hot types /// devirt::devirt! { -/// pub MyTrait [HotType1, HotType2] { +/// pub trait MyTrait [HotType1, HotType2] { /// fn method(&self) -> ReturnType; /// fn mut_method(&mut self, arg: ArgType); /// } @@ -560,7 +568,23 @@ macro_rules! devirt { } ) => { $crate::__devirt_define! { - @trait + @trait [] + $(#[$meta])* + $vis $name [$($hot),+] { + $($methods)* + } + } + }; + + // Unsafe trait definition + ( + $(#[$meta:meta])* + $vis:vis unsafe trait $name:ident [$($hot:ty),+ $(,)?] { + $($methods:tt)* + } + ) => { + $crate::__devirt_define! { + @trait [unsafe] $(#[$meta])* $vis $name [$($hot),+] { $($methods)* @@ -575,7 +599,21 @@ macro_rules! devirt { } ) => { $crate::__devirt_define! { - @impl + @impl [] + $trait_name for $type { + $($methods)* + } + } + }; + + // Unsafe impl block + ( + unsafe impl $trait_name:ident for $type:ty { + $($methods:tt)* + } + ) => { + $crate::__devirt_define! { + @impl [unsafe] $trait_name for $type { $($methods)* } @@ -609,24 +647,24 @@ mod primitives { } crate::__devirt_define! { - @trait + @trait [] pub Probe [Hot, Also] { fn get(&self) -> u64; fn set(&mut self, v: u64); } } - crate::__devirt_define! { @impl Probe for Hot { + crate::__devirt_define! { @impl [] Probe for Hot { fn get(&self) -> u64 { self.val } fn set(&mut self, v: u64) { self.val = v; } }} - crate::__devirt_define! { @impl Probe for Also { + crate::__devirt_define! { @impl [] Probe for Also { fn get(&self) -> u64 { self.val.wrapping_add(1) } fn set(&mut self, v: u64) { self.val = v.wrapping_add(1); } }} - crate::__devirt_define! { @impl Probe for Cold { + crate::__devirt_define! { @impl [] Probe for Cold { fn get(&self) -> u64 { self.val.wrapping_sub(1) } fn set(&mut self, v: u64) { self.val = v.wrapping_sub(1); } }} diff --git a/crates/core/tests/kani.rs b/crates/core/tests/kani.rs index 6fa88bb..881b742 100644 --- a/crates/core/tests/kani.rs +++ b/crates/core/tests/kani.rs @@ -25,7 +25,7 @@ mod n1 { } devirt::__devirt_define! { - @trait + @trait [] pub Trait1 [Hot] { fn compute(&self, x: u64) -> u64; fn notify(&self, x: u64); @@ -34,14 +34,14 @@ mod n1 { } } - devirt::__devirt_define! { @impl Trait1 for Hot { + devirt::__devirt_define! { @impl [] Trait1 for Hot { fn compute(&self, x: u64) -> u64 { self.val.wrapping_add(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_add(x); self.val } fn reset(&mut self, x: u64) { self.val = x; } }} - devirt::__devirt_define! { @impl Trait1 for Cold { + devirt::__devirt_define! { @impl [] Trait1 for Cold { fn compute(&self, x: u64) -> u64 { self.val.wrapping_sub(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_sub(x); self.val } @@ -121,7 +121,7 @@ mod n2 { } devirt::__devirt_define! { - @trait + @trait [] pub Trait2 [HotA, HotB] { fn compute(&self, x: u64) -> u64; fn notify(&self, x: u64); @@ -130,21 +130,21 @@ mod n2 { } } - devirt::__devirt_define! { @impl Trait2 for HotA { + devirt::__devirt_define! { @impl [] Trait2 for HotA { fn compute(&self, x: u64) -> u64 { self.val.wrapping_add(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_add(x); self.val } fn reset(&mut self, x: u64) { self.val = x; } }} - devirt::__devirt_define! { @impl Trait2 for HotB { + devirt::__devirt_define! { @impl [] Trait2 for HotB { fn compute(&self, x: u64) -> u64 { self.val | x } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val |= x; self.val } fn reset(&mut self, x: u64) { self.val = x.wrapping_add(1); } }} - devirt::__devirt_define! { @impl Trait2 for Cold { + devirt::__devirt_define! { @impl [] Trait2 for Cold { fn compute(&self, x: u64) -> u64 { self.val.wrapping_sub(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_sub(x); self.val } @@ -256,7 +256,7 @@ mod n3 { } devirt::__devirt_define! { - @trait + @trait [] pub Trait3 [HotA, HotB, HotC] { fn compute(&self, x: u64) -> u64; fn notify(&self, x: u64); @@ -265,28 +265,28 @@ mod n3 { } } - devirt::__devirt_define! { @impl Trait3 for HotA { + devirt::__devirt_define! { @impl [] Trait3 for HotA { fn compute(&self, x: u64) -> u64 { self.val.wrapping_add(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_add(x); self.val } fn reset(&mut self, x: u64) { self.val = x; } }} - devirt::__devirt_define! { @impl Trait3 for HotB { + devirt::__devirt_define! { @impl [] Trait3 for HotB { fn compute(&self, x: u64) -> u64 { self.val | x } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val |= x; self.val } fn reset(&mut self, x: u64) { self.val = x.wrapping_add(1); } }} - devirt::__devirt_define! { @impl Trait3 for HotC { + devirt::__devirt_define! { @impl [] Trait3 for HotC { fn compute(&self, x: u64) -> u64 { self.val ^ x } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val ^= x; self.val } fn reset(&mut self, x: u64) { self.val = x.wrapping_add(2); } }} - devirt::__devirt_define! { @impl Trait3 for Cold { + devirt::__devirt_define! { @impl [] Trait3 for Cold { fn compute(&self, x: u64) -> u64 { self.val.wrapping_sub(x) } fn notify(&self, _x: u64) { } fn transform(&mut self, x: u64) -> u64 { self.val = self.val.wrapping_sub(x); self.val } @@ -404,17 +404,17 @@ mod vt { } devirt::__devirt_define! { - @trait + @trait [] pub TraitVt [Hot] { fn compute(&self, x: u64) -> u64; } } - devirt::__devirt_define! { @impl TraitVt for Hot { + devirt::__devirt_define! { @impl [] TraitVt for Hot { fn compute(&self, x: u64) -> u64 { self.val.wrapping_add(x) } }} - devirt::__devirt_define! { @impl TraitVt for Cold { + devirt::__devirt_define! { @impl [] TraitVt for Cold { fn compute(&self, x: u64) -> u64 { self.val.wrapping_sub(x) } }} diff --git a/crates/core/tests/ui.rs b/crates/core/tests/ui.rs index 63a16a4..f315aa7 100644 --- a/crates/core/tests/ui.rs +++ b/crates/core/tests/ui.rs @@ -8,6 +8,8 @@ fn ui() { t.pass("tests/ui/all_arms.rs"); t.pass("tests/ui/multi_arg.rs"); t.pass("tests/ui/pub_trait.rs"); + t.pass("tests/ui/unsafe_trait.rs"); + t.pass("tests/ui/method_attrs.rs"); t.compile_fail("tests/ui/missing_method.rs"); t.compile_fail("tests/ui/wrong_signature.rs"); } diff --git a/crates/core/tests/ui/all_arms.rs b/crates/core/tests/ui/all_arms.rs index 176179d..785fc5f 100644 --- a/crates/core/tests/ui/all_arms.rs +++ b/crates/core/tests/ui/all_arms.rs @@ -7,7 +7,7 @@ struct ColdType { } devirt::__devirt_define! { - @trait + @trait [] pub AllArms [Hot] { fn ref_nonvoid(&self, x: f64) -> f64; fn ref_void(&self, x: f64); @@ -16,14 +16,14 @@ devirt::__devirt_define! { } } -devirt::__devirt_define! { @impl AllArms for Hot { +devirt::__devirt_define! { @impl [] AllArms for Hot { fn ref_nonvoid(&self, x: f64) -> f64 { self.val + x } fn ref_void(&self, _x: f64) { } fn mut_nonvoid(&mut self, x: f64) -> f64 { self.val += x; self.val } fn mut_void(&mut self, x: f64) { self.val = x; } }} -devirt::__devirt_define! { @impl AllArms for ColdType { +devirt::__devirt_define! { @impl [] AllArms for ColdType { fn ref_nonvoid(&self, x: f64) -> f64 { self.val + x } fn ref_void(&self, _x: f64) { } fn mut_nonvoid(&mut self, x: f64) -> f64 { self.val += x; self.val } diff --git a/crates/core/tests/ui/method_attrs.rs b/crates/core/tests/ui/method_attrs.rs new file mode 100644 index 0000000..fb7ca2f --- /dev/null +++ b/crates/core/tests/ui/method_attrs.rs @@ -0,0 +1,24 @@ +struct Hot { + val: f64, +} + +devirt::__devirt_define! { + @trait [] + pub Checked [Hot] { + /// Computes the value. + #[must_use] + fn compute(&self) -> f64; + } +} + +devirt::__devirt_define! { @impl [] Checked for Hot { + /// Computes the value. + #[must_use] + fn compute(&self) -> f64 { self.val } +}} + +fn main() { + let h = Hot { val: 1.0 }; + let r: &dyn Checked = &h; + assert_eq!(r.compute(), 1.0); +} diff --git a/crates/core/tests/ui/missing_method.rs b/crates/core/tests/ui/missing_method.rs index 1cc1cee..0f88465 100644 --- a/crates/core/tests/ui/missing_method.rs +++ b/crates/core/tests/ui/missing_method.rs @@ -1,14 +1,14 @@ struct Foo; devirt::__devirt_define! { - @trait + @trait [] pub TwoMethods [Foo] { fn first(&self) -> i32; fn second(&self) -> i32; } } -devirt::__devirt_define! { @impl TwoMethods for Foo { +devirt::__devirt_define! { @impl [] TwoMethods for Foo { fn first(&self) -> i32 { 1 } }} diff --git a/crates/core/tests/ui/missing_method.stderr b/crates/core/tests/ui/missing_method.stderr index e89f99a..70f14c4 100644 --- a/crates/core/tests/ui/missing_method.stderr +++ b/crates/core/tests/ui/missing_method.stderr @@ -2,14 +2,14 @@ error[E0046]: not all trait items implemented, missing: `__spec_second` --> tests/ui/missing_method.rs:11:1 | 3 | / devirt::__devirt_define! { - 4 | | @trait + 4 | | @trait [] 5 | | pub TwoMethods [Foo] { 6 | | fn first(&self) -> i32; ... | 9 | | } | |_- `__spec_second` from trait 10 | -11 | / devirt::__devirt_define! { @impl TwoMethods for Foo { +11 | / devirt::__devirt_define! { @impl [] TwoMethods for Foo { 12 | | fn first(&self) -> i32 { 1 } 13 | | }} | |__^ missing `__spec_second` in implementation diff --git a/crates/core/tests/ui/multi_arg.rs b/crates/core/tests/ui/multi_arg.rs index dda11b7..491c9f5 100644 --- a/crates/core/tests/ui/multi_arg.rs +++ b/crates/core/tests/ui/multi_arg.rs @@ -4,14 +4,14 @@ struct Widget { } devirt::__devirt_define! { - @trait + @trait [] pub MultiArg [Widget] { fn add(&self, a: f64, b: f64) -> f64; fn set(&mut self, a: f64, b: f64); } } -devirt::__devirt_define! { @impl MultiArg for Widget { +devirt::__devirt_define! { @impl [] MultiArg for Widget { fn add(&self, a: f64, b: f64) -> f64 { self.x + a + self.y + b } fn set(&mut self, a: f64, b: f64) { self.x = a; self.y = b; } }} diff --git a/crates/core/tests/ui/multi_hot.rs b/crates/core/tests/ui/multi_hot.rs index 54092c2..0622654 100644 --- a/crates/core/tests/ui/multi_hot.rs +++ b/crates/core/tests/ui/multi_hot.rs @@ -3,21 +3,21 @@ struct B; struct C; devirt::__devirt_define! { - @trait + @trait [] pub MultiHot [A, B, C] { fn id(&self) -> u8; } } -devirt::__devirt_define! { @impl MultiHot for A { +devirt::__devirt_define! { @impl [] MultiHot for A { fn id(&self) -> u8 { 1 } }} -devirt::__devirt_define! { @impl MultiHot for B { +devirt::__devirt_define! { @impl [] MultiHot for B { fn id(&self) -> u8 { 2 } }} -devirt::__devirt_define! { @impl MultiHot for C { +devirt::__devirt_define! { @impl [] MultiHot for C { fn id(&self) -> u8 { 3 } }} diff --git a/crates/core/tests/ui/pub_trait.rs b/crates/core/tests/ui/pub_trait.rs index 9ab023f..d8b5740 100644 --- a/crates/core/tests/ui/pub_trait.rs +++ b/crates/core/tests/ui/pub_trait.rs @@ -3,7 +3,7 @@ struct Inner { } devirt::__devirt_define! { - @trait + @trait [] /// A public trait with documentation. pub DocTrait [Inner] { /// Returns the inner value. @@ -11,7 +11,7 @@ devirt::__devirt_define! { } } -devirt::__devirt_define! { @impl DocTrait for Inner { +devirt::__devirt_define! { @impl [] DocTrait for Inner { fn get(&self) -> i32 { self.val } }} diff --git a/crates/core/tests/ui/single_hot.rs b/crates/core/tests/ui/single_hot.rs index 221319f..2abafeb 100644 --- a/crates/core/tests/ui/single_hot.rs +++ b/crates/core/tests/ui/single_hot.rs @@ -3,13 +3,13 @@ struct Foo { } devirt::__devirt_define! { - @trait + @trait [] pub SingleHot [Foo] { fn get(&self) -> f64; } } -devirt::__devirt_define! { @impl SingleHot for Foo { +devirt::__devirt_define! { @impl [] SingleHot for Foo { fn get(&self) -> f64 { self.val } }} diff --git a/crates/core/tests/ui/unsafe_trait.rs b/crates/core/tests/ui/unsafe_trait.rs new file mode 100644 index 0000000..b6f4c78 --- /dev/null +++ b/crates/core/tests/ui/unsafe_trait.rs @@ -0,0 +1,33 @@ +struct Hot { + val: u64, +} + +struct Cold { + val: u64, +} + +devirt::__devirt_define! { + @trait [unsafe] + pub Trusted [Hot] { + fn verify(&self) -> bool; + } +} + +devirt::__devirt_define! { @impl [unsafe] Trusted for Hot { + fn verify(&self) -> bool { self.val > 0 } +}} + +devirt::__devirt_define! { @impl [unsafe] Trusted for Cold { + fn verify(&self) -> bool { self.val != 0 } +}} + +fn check(t: &dyn Trusted) -> bool { + t.verify() +} + +fn main() { + let h = Hot { val: 42 }; + let c = Cold { val: 1 }; + assert!(check(&h)); + assert!(check(&c)); +} diff --git a/crates/core/tests/ui/wrong_signature.rs b/crates/core/tests/ui/wrong_signature.rs index 23c53eb..8256f17 100644 --- a/crates/core/tests/ui/wrong_signature.rs +++ b/crates/core/tests/ui/wrong_signature.rs @@ -1,13 +1,13 @@ struct Bar; devirt::__devirt_define! { - @trait + @trait [] pub WrongSig [Bar] { fn compute(&self, x: f64) -> f64; } } -devirt::__devirt_define! { @impl WrongSig for Bar { +devirt::__devirt_define! { @impl [] WrongSig for Bar { fn compute(&self, x: u32) -> f64 { f64::from(x) } }} diff --git a/crates/core/tests/ui_attr.rs b/crates/core/tests/ui_attr.rs index 4d403da..9de93af 100644 --- a/crates/core/tests/ui_attr.rs +++ b/crates/core/tests/ui_attr.rs @@ -6,6 +6,8 @@ fn ui_attr() { t.pass("tests/ui_attr/attr_single_hot.rs"); t.pass("tests/ui_attr/attr_multi_hot.rs"); 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.compile_fail("tests/ui_attr/attr_missing_args.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_args_on_impl.rs b/crates/core/tests/ui_attr/attr_args_on_impl.rs index 4cada5c..995c20b 100644 --- a/crates/core/tests/ui_attr/attr_args_on_impl.rs +++ b/crates/core/tests/ui_attr/attr_args_on_impl.rs @@ -1,7 +1,7 @@ struct Foo; devirt::__devirt_define! { - @trait + @trait [] pub ArgsOnImpl [Foo] { fn get(&self) -> i32; } diff --git a/crates/core/tests/ui_attr/attr_args_on_impl.stderr b/crates/core/tests/ui_attr/attr_args_on_impl.stderr index dcaf634..9b2f882 100644 --- a/crates/core/tests/ui_attr/attr_args_on_impl.stderr +++ b/crates/core/tests/ui_attr/attr_args_on_impl.stderr @@ -21,7 +21,7 @@ help: this trait has no implementations, consider adding one --> tests/ui_attr/attr_args_on_impl.rs:3:1 | 3 | / devirt::__devirt_define! { -4 | | @trait +4 | | @trait [] 5 | | pub ArgsOnImpl [Foo] { 6 | | fn get(&self) -> i32; 7 | | } @@ -31,7 +31,7 @@ note: required by a bound in `<(dyn ArgsOnImpl + '__devirt)>::__devirt_vtable_fo --> tests/ui_attr/attr_args_on_impl.rs:3:1 | 3 | / devirt::__devirt_define! { -4 | | @trait +4 | | @trait [] 5 | | pub ArgsOnImpl [Foo] { 6 | | fn get(&self) -> i32; 7 | | } @@ -46,7 +46,7 @@ error[E0599]: no method named `__spec_get` found for reference `&Foo` in the cur --> tests/ui_attr/attr_args_on_impl.rs:3:1 | 3 | / devirt::__devirt_define! { -4 | | @trait +4 | | @trait [] 5 | | pub ArgsOnImpl [Foo] { 6 | | fn get(&self) -> i32; 7 | | } @@ -58,7 +58,7 @@ note: `__ArgsOnImplImpl` defines an item `__spec_get`, perhaps you need to imple --> tests/ui_attr/attr_args_on_impl.rs:3:1 | 3 | / devirt::__devirt_define! { -4 | | @trait +4 | | @trait [] 5 | | pub ArgsOnImpl [Foo] { 6 | | fn get(&self) -> i32; 7 | | } diff --git a/crates/core/tests/ui_attr/attr_method_attrs.rs b/crates/core/tests/ui_attr/attr_method_attrs.rs new file mode 100644 index 0000000..c0da0a8 --- /dev/null +++ b/crates/core/tests/ui_attr/attr_method_attrs.rs @@ -0,0 +1,23 @@ +struct Hot { + val: f64, +} + +#[devirt::devirt(Hot)] +pub trait Checked { + /// Computes the value. + #[must_use] + fn compute(&self) -> f64; +} + +#[devirt::devirt] +impl Checked for Hot { + /// Computes the value. + #[must_use] + fn compute(&self) -> f64 { self.val } +} + +fn main() { + let h = Hot { val: 1.0 }; + let r: &dyn Checked = &h; + assert_eq!(r.compute(), 1.0); +} diff --git a/crates/core/tests/ui_attr/attr_unsafe_trait.rs b/crates/core/tests/ui_attr/attr_unsafe_trait.rs new file mode 100644 index 0000000..aa09fdf --- /dev/null +++ b/crates/core/tests/ui_attr/attr_unsafe_trait.rs @@ -0,0 +1,33 @@ +struct Hot { + val: u64, +} + +struct Cold { + val: u64, +} + +#[devirt::devirt(Hot)] +pub unsafe trait Trusted { + fn verify(&self) -> bool; +} + +#[devirt::devirt] +unsafe impl Trusted for Hot { + fn verify(&self) -> bool { self.val > 0 } +} + +#[devirt::devirt] +unsafe impl Trusted for Cold { + fn verify(&self) -> bool { self.val != 0 } +} + +fn check(t: &dyn Trusted) -> bool { + t.verify() +} + +fn main() { + let h = Hot { val: 42 }; + let c = Cold { val: 1 }; + assert!(check(&h)); + assert!(check(&c)); +} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 78ea425..b79d994 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -119,6 +119,7 @@ fn expand_trait(attr: TokenStream, trait_item: &syn::ItemTrait) -> TokenStream { .into_iter() .collect(); + let unsafety = &trait_item.unsafety; let vis = &trait_item.vis; let name = &trait_item.ident; @@ -142,7 +143,7 @@ fn expand_trait(attr: TokenStream, trait_item: &syn::ItemTrait) -> TokenStream { quote! { ::devirt::__devirt_define! { - @trait + @trait [#unsafety] #outer_attrs #vis #name [#(#hot_types),*] { #methods_tokens @@ -189,6 +190,7 @@ fn expand_impl(attr: &TokenStream, impl_item: &syn::ItemImpl) -> TokenStream { .into(); } + let unsafety = &impl_item.unsafety; let trait_name = &trait_path.segments.last().expect("trait path is empty").ident; let ty = &impl_item.self_ty; @@ -205,7 +207,7 @@ fn expand_impl(attr: &TokenStream, impl_item: &syn::ItemImpl) -> TokenStream { quote! { ::devirt::__devirt_define! { - @impl + @impl [#unsafety] #trait_name for #ty { #method_bodies } From 48bc89053ca43c509fb94bb4e8612154213ac763 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 13:17:58 +0000 Subject: [PATCH 5/5] Fix publishing, MSRV, and qualified path handling - Add devirt-macros as workspace dependency with version for crates.io publishing (path-only deps can't be published) - Add rust-version = "1.85" MSRV (edition 2024 requires 1.85+, core::ptr::without_provenance stabilized in 1.84) - Reject qualified trait paths in #[devirt] expand_impl (e.g., super::MyTrait) with a clear error instead of silently truncating to the last segment https://claude.ai/code/session_01XRfaF7hqTVzJwR8tBrnkom --- Cargo.toml | 1 + crates/core/Cargo.toml | 3 ++- crates/macros/src/lib.rs | 12 ++++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 39b9423..f9444e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ trybuild = "1" libfuzzer-sys = "0.4" arbitrary = { version = "1", features = ["derive"] } devirt = { path = "crates/core" } +devirt-macros = { path = "crates/macros", version = "0.2.0" } syn = { version = "2", features = ["full"] } quote = "1" proc-macro2 = "1" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index f9a8b71..4f1fdef 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -2,6 +2,7 @@ name = "devirt" version.workspace = true edition.workspace = true +rust-version = "1.85" license.workspace = true repository.workspace = true readme = "../../README.md" @@ -11,7 +12,7 @@ categories = ["no-std", "rust-patterns"] [dependencies] paste.workspace = true -devirt-macros = { path = "../macros", optional = true } +devirt-macros = { workspace = true, optional = true } [features] default = ["macros"] diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index b79d994..edd01e3 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -190,6 +190,18 @@ fn expand_impl(attr: &TokenStream, impl_item: &syn::ItemImpl) -> TokenStream { .into(); } + // Reject qualified paths (e.g., super::MyTrait, crate::MyTrait) — + // __devirt_define! requires a plain ident, not a path. + if trait_path.leading_colon.is_some() || trait_path.segments.len() > 1 { + return syn::Error::new_spanned( + trait_path, + "#[devirt] requires a plain trait name, not a qualified path \ + (e.g., `impl MyTrait for T`, not `impl super::MyTrait for T`)", + ) + .to_compile_error() + .into(); + } + let unsafety = &impl_item.unsafety; let trait_name = &trait_path.segments.last().expect("trait path is empty").ident; let ty = &impl_item.self_ty;