Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ verus crates/verify/src/lib.rs --crate-type=lib

This is a workspace with four crates:

- **`crates/core`** (`devirt`) — the main macro library. Exports `devirt!` (declarative) or `#[devirt]` (proc-macro attribute) depending on the `macros` feature (on by default). Both delegate to the internal `__devirt_define!` macro.
- **`crates/macros`** (`devirt-macros`) — proc-macro crate providing the `#[devirt]` attribute. Optional dependency of `crates/core`, enabled by the `macros` feature.
- **`crates/core`** (`devirt`) — the main macro library. Exports `devirt!` (declarative) or `#[devirt]` (proc-macro attribute) depending on the `macros` feature (on by default). The declarative macro delegates to the internal `__devirt_define!` macro; the proc-macro attribute emits expansion directly via `quote!`.
- **`crates/macros`** (`devirt-macros`) — proc-macro crate providing the `#[devirt]` attribute. Emits the dispatch expansion directly (not via `__devirt_define!`), giving it full `syn`-level signature fidelity. Optional dependency of `crates/core`, enabled by the `macros` feature.
- **`crates/verify`** (`devirt-verify`) — Verus formal proofs of dispatch correctness.
- **`fuzz`** — libfuzzer differential fuzzing comparing devirt dispatch vs. plain vtable.

Expand All @@ -64,15 +64,15 @@ Why inherent methods on `dyn Trait` and not trait default methods: a default met

### Macro Structure

All dispatch expansion logic lives in `__devirt_define!` (`#[doc(hidden)]`, `#[macro_export]`, always available). It has two entry points:
There are two independent expansion paths:

- `__devirt_define! { @trait ... }` — generates the trait, inner trait, dispatch shim, blanket impl
- `__devirt_define! { @impl ... }`generates `impl __TraitNameImpl for T { __spec_* ... }`
- **`#[devirt::devirt(Hot1, Hot2)]`** (proc-macro attribute, default) — parses the trait/impl with `syn` and emits the full expansion directly via `quote!`. Supports `unsafe trait`/`unsafe fn`, method lifetimes, method attributes, supertraits, `extern "ABI" fn`, and method `where` clauses.
- **`devirt::devirt!`** (declarative macro, `default-features = false`)thin dispatcher that forwards to `$crate::__devirt_define!`, a `#[doc(hidden)]` `#[macro_export]` internal macro with limited syntax support (plain `fn method(&self, arg: Type) -> Ret;` signatures only).

The public APIs delegate to it:
`__devirt_define!` has two entry points:

- **`#[devirt::devirt(Hot1, Hot2)]`** (proc-macro attribute, default)parses the trait/impl and emits `::devirt::__devirt_define!` calls
- **`devirt::devirt!`** (declarative macro, `default-features = false`) — thin dispatcher that forwards to `$crate::__devirt_define!`
- `__devirt_define! { @trait ... }`generates the trait, inner trait, dispatch shim, blanket impl
- `__devirt_define! { @impl ... }` — generates `impl __TraitNameImpl for T { __spec_* ... }`

### `__devirt_define! { @trait ... }` Expansion

Expand All @@ -88,7 +88,7 @@ For a trait `Foo` with hot types `[A, B]`:
For `impl Foo for A { ... }`:
- Expands to `impl __FooImpl for A { fn __spec_method(...) { ... } }`.

### Dispatch Arms (inside `__devirt_define!`)
### Dispatch Arms (inside `__devirt_define!`, declarative-macro path only)

Four arms handle the combinatorics of `&self`/`&mut self` × void/non-void. Each splits into an outer "set up `__raw`" arm and a recursive `*_chain` arm that walks the hot-type list:
- `@dispatch_ref` / `@dispatch_ref_chain` — `&self`, returns value
Expand Down
17 changes: 14 additions & 3 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@
//! generates:
//! - `impl __XImpl for ConcreteType { ... }` with the `__spec_*` bodies
//!
//! Both the proc-macro attribute and the declarative macro delegate to
//! `__devirt_define!`, a `#[doc(hidden)]` internal macro that contains
//! all dispatch expansion logic.
//! The proc-macro attribute (`#[devirt]`) emits the dispatch code
//! directly via `quote!`, supporting the full range of method
//! signatures that `syn` can parse (lifetimes, `unsafe fn`,
//! supertraits, attributes, `extern "ABI" fn`, etc.).
//! The declarative macro (`devirt!`) delegates to `__devirt_define!`,
//! which has more limited syntax support.
Comment on lines +33 to +38
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Architecture contract drift from the repository macro-expansion rule.

Line 33 through Line 38 document direct proc-macro dispatch emission instead of routing through __devirt_define!. That introduces two expansion engines and diverges from the repo’s stated single-source expansion contract; please either restore delegation or update the project rule/docs in the same change set.

As per coding guidelines "crates/core/src/**/*.rs: Place all dispatch expansion logic in __devirt_define! macro with #[doc(hidden)], #[macro_export] attributes, and implement two entry points: @trait ... and @impl ...".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/core/src/lib.rs` around lines 33 - 38, The doc comment claims the
proc-macro attribute `#[devirt]` emits dispatch code directly, which violates
the repo rule that all dispatch expansion must be centralized in the hidden,
exported declarative macro `__devirt_define!`; fix by restoring delegation so
`#[devirt]` and the `devirt!` declarative macro both call `__devirt_define!`
(which must have `#[doc(hidden)]` and `#[macro_export]` and support the `@trait`
and `@impl` entry points), or alternatively update this comment to explicitly
state the new architecture and add/update `__devirt_define!` to match the
documented contract — ensure references to `#[devirt]`, `devirt!`, and
`__devirt_define!` in the code and docs are consistent.

//!
//! # Usage
//!
Expand Down Expand Up @@ -121,6 +124,14 @@ extern crate kani;
#[doc(hidden)]
pub use paste::paste as __paste;

/// Internal dispatch expansion macro.
///
/// This macro has **limited syntax support** compared to the
/// `#[devirt]` proc-macro attribute. It only handles plain
/// `fn method(&self, arg: Type) -> Ret;` signatures — no `unsafe fn`,
/// method lifetimes, supertraits, method attributes, `where` clauses,
/// or `extern "ABI" fn`. For the full feature set, use the proc-macro
/// attribute (the default with the `macros` feature enabled).
#[doc(hidden)]
#[macro_export]
macro_rules! __devirt_define {
Expand Down
69 changes: 69 additions & 0 deletions crates/core/tests/equivalence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,75 @@ fn decl_dispatch() {
assert_eq!(c.val, 100);
}

// ── Extended proc-macro tests: supertraits, method lifetimes, #[must_use] ──

#[cfg(feature = "macros")]
mod attr_extended {
use core::fmt;

#[derive(Debug)]
pub struct ExtHot {
pub val: u64,
pub label: String,
}

#[derive(Debug)]
pub struct ExtCold {
pub val: u64,
pub label: String,
}

#[devirt::devirt(ExtHot)]
pub trait Inspectable: fmt::Debug {
#[must_use]
fn value(&self) -> u64;
// Explicit lifetime to exercise method-lifetime support.
fn name<'a>(&'a self) -> &'a str;
fn set_val(&mut self, v: u64);
}

#[devirt::devirt]
impl Inspectable for ExtHot {
fn value(&self) -> u64 { self.val }
fn name(&self) -> &str { &self.label }
fn set_val(&mut self, v: u64) { self.val = v; }
}

#[devirt::devirt]
impl Inspectable for ExtCold {
fn value(&self) -> u64 { self.val + 1 }
fn name(&self) -> &str { &self.label }
fn set_val(&mut self, v: u64) { self.val = v + 1; }
}
}

#[cfg(feature = "macros")]
#[test]
fn attr_extended_dispatch() {
use attr_extended::{ExtCold, ExtHot, Inspectable};

// Supertraits: dyn Inspectable implements Debug
let h = ExtHot { val: 42, label: "hot".into() };
let c = ExtCold { val: 42, label: "cold".into() };
drop(format!("{:?}", &h as &dyn Inspectable));

// #[must_use] + non-void &self
assert_eq!((&h as &dyn Inspectable).value(), 42);
assert_eq!((&c as &dyn Inspectable).value(), 43);

// Method lifetimes
assert_eq!((&h as &dyn Inspectable).name(), "hot");
assert_eq!((&c as &dyn Inspectable).name(), "cold");

// &mut self
let mut h = ExtHot { val: 0, label: "hot".into() };
let mut c = ExtCold { val: 0, label: "cold".into() };
(&mut h as &mut dyn Inspectable).set_val(10);
(&mut c as &mut dyn Inspectable).set_val(10);
assert_eq!(h.val, 10);
assert_eq!(c.val, 11);
}

#[cfg(feature = "macros")]
#[test]
fn attr_dispatch() {
Expand Down
6 changes: 6 additions & 0 deletions crates/core/tests/ui_attr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ fn ui_attr() {
t.pass("tests/ui_attr/attr_all_arms.rs");
t.pass("tests/ui_attr/attr_unsafe_trait.rs");
t.pass("tests/ui_attr/attr_method_attrs.rs");
t.pass("tests/ui_attr/attr_unsafe_fn.rs");
t.pass("tests/ui_attr/attr_method_lifetimes.rs");
t.pass("tests/ui_attr/attr_supertraits.rs");
t.pass("tests/ui_attr/attr_must_use.rs");
t.compile_fail("tests/ui_attr/attr_must_use_unused.rs");
t.compile_fail("tests/ui_attr/attr_missing_args.rs");
t.compile_fail("tests/ui_attr/attr_unsafe_missing_on_impl.rs");
t.compile_fail("tests/ui_attr/attr_args_on_impl.rs");
t.compile_fail("tests/ui_attr/attr_on_struct.rs");
}
33 changes: 33 additions & 0 deletions crates/core/tests/ui_attr/attr_method_lifetimes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
struct Hot {
name: String,
}

struct Cold {
name: String,
}

#[devirt::devirt(Hot)]
pub trait Named {
fn name<'a>(&'a self) -> &'a str;
}

#[devirt::devirt]
impl Named for Hot {
fn name<'a>(&'a self) -> &'a str { &self.name }
}

#[devirt::devirt]
impl Named for Cold {
fn name<'a>(&'a self) -> &'a str { &self.name }
}

fn greet(n: &dyn Named) -> String {
format!("Hello, {}!", n.name())
}

fn main() {
let h = Hot { name: "world".into() };
let c = Cold { name: "rust".into() };
assert_eq!(greet(&h), "Hello, world!");
assert_eq!(greet(&c), "Hello, rust!");
}
32 changes: 32 additions & 0 deletions crates/core/tests/ui_attr/attr_must_use.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
struct Hot {
val: f64,
}

struct Cold {
val: f64,
}

#[devirt::devirt(Hot)]
pub trait Computed {
#[must_use]
fn compute(&self) -> f64;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#[devirt::devirt]
impl Computed for Hot {
fn compute(&self) -> f64 { self.val * 2.0 }
}

#[devirt::devirt]
impl Computed for Cold {
fn compute(&self) -> f64 { self.val * 3.0 }
}

fn main() {
let h = Hot { val: 1.0 };
let c = Cold { val: 1.0 };
let dh: &dyn Computed = &h;
let dc: &dyn Computed = &c;
let _ = dh.compute(); // should NOT warn (result is used via let _)
let _ = dc.compute();
}
25 changes: 25 additions & 0 deletions crates/core/tests/ui_attr/attr_must_use_unused.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Verifies that #[must_use] on a trait method is preserved through
// macro expansion: calling compute() without using the result must
// trigger an error under deny(unused_must_use).
#![deny(unused_must_use)]

struct Hot {
val: f64,
}

#[devirt::devirt(Hot)]
pub trait Computed {
#[must_use]
fn compute(&self) -> f64;
}

#[devirt::devirt]
impl Computed for Hot {
fn compute(&self) -> f64 { self.val * 2.0 }
}

fn main() {
let h = Hot { val: 1.0 };
let d: &dyn Computed = &h;
d.compute(); // ERROR: unused return value that must be used
}
15 changes: 15 additions & 0 deletions crates/core/tests/ui_attr/attr_must_use_unused.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
error: unused return value of `<(dyn Computed + '__devirt)>::compute` that must be used
--> tests/ui_attr/attr_must_use_unused.rs:24:5
|
24 | d.compute(); // ERROR: unused return value that must be used
| ^^^^^^^^^^^
|
note: the lint level is defined here
--> tests/ui_attr/attr_must_use_unused.rs:4:9
|
4 | #![deny(unused_must_use)]
| ^^^^^^^^^^^^^^^
help: use `let _ = ...` to ignore the resulting value
|
24 | let _ = d.compute(); // ERROR: unused return value that must be used
| +++++++
39 changes: 39 additions & 0 deletions crates/core/tests/ui_attr/attr_supertraits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::fmt::Debug;

#[derive(Debug)]
struct Hot {
val: u64,
}

#[derive(Debug)]
struct Cold {
val: u64,
}

#[devirt::devirt(Hot)]
pub trait Inspectable: Debug {
fn value(&self) -> u64;
}

#[devirt::devirt]
impl Inspectable for Hot {
fn value(&self) -> u64 { self.val }
}

#[devirt::devirt]
impl Inspectable for Cold {
fn value(&self) -> u64 { self.val + 1 }
}

fn inspect(i: &dyn Inspectable) -> String {
format!("{:?} = {}", i, i.value())
}

fn main() {
let h = Hot { val: 42 };
let c = Cold { val: 42 };
let s = inspect(&h);
assert!(s.contains("42"), "expected '42' in '{s}'");
let s = inspect(&c);
assert!(s.contains("43"), "expected '43' in '{s}'");
}
32 changes: 32 additions & 0 deletions crates/core/tests/ui_attr/attr_unsafe_fn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
struct Hot {
data: *const u8,
}

struct Cold {
data: *const u8,
}

#[devirt::devirt(Hot)]
pub trait Dangerous {
unsafe fn deref(&self) -> u8;
}

#[devirt::devirt]
impl Dangerous for Hot {
unsafe fn deref(&self) -> u8 { unsafe { *self.data } }
}

#[devirt::devirt]
impl Dangerous for Cold {
unsafe fn deref(&self) -> u8 { unsafe { *self.data } }
}

fn main() {
let val: u8 = 42;
let h = Hot { data: &val };
let c = Cold { data: &val };
let dh: &dyn Dangerous = &h;
let dc: &dyn Dangerous = &c;
assert_eq!(unsafe { dh.deref() }, 42);
assert_eq!(unsafe { dc.deref() }, 42);
}
16 changes: 16 additions & 0 deletions crates/core/tests/ui_attr/attr_unsafe_missing_on_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
struct Hot {
val: u64,
}

#[devirt::devirt(Hot)]
pub unsafe trait Trusted {
fn verify(&self) -> bool;
}

// Missing `unsafe` on impl — should fail because __TrustedImpl is unsafe.
#[devirt::devirt]
impl Trusted for Hot {
fn verify(&self) -> bool { self.val > 0 }
}

fn main() {}
12 changes: 12 additions & 0 deletions crates/core/tests/ui_attr/attr_unsafe_missing_on_impl.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
error[E0200]: the trait `__TrustedImpl` requires an `unsafe impl` declaration
--> tests/ui_attr/attr_unsafe_missing_on_impl.rs:11:1
|
11 | #[devirt::devirt]
| ^^^^^^^^^^^^^^^^^
|
= note: the trait `__TrustedImpl` enforces invariants that the compiler can't check. Review the trait documentation and make sure this implementation upholds those invariants before adding the `unsafe` keyword
= note: this error originates in the attribute macro `devirt::devirt` (in Nightly builds, run with -Z macro-backtrace for more info)
help: add `unsafe` to this trait implementation
|
11 | unsafe #[devirt::devirt]
| ++++++
Loading