1 stable release

new 1.0.0 Mar 16, 2025

#713 in Rust patterns

Download history 104/week @ 2025-03-12

104 downloads per month

Custom license

140KB
3.5K SLoC

Enumtrait

A crate for deriving enum based polymorphism.

Pattern

Often we want to implement traits separately for each variant of a enum. This is traditionally implemented in two ways:

  1. By manually writing the enum of Typename(TypeName) variants, and implementing the trait on that enum with match statements.
  2. Using dyn with either references, or heap allocating (classic Box<dyn trait>)

The former is tedious. The later can require heap allocation, and the cost of a virtual call.

This crate removes the tediousness of the former.

use enumtrait;

struct Bar { bar_field: usize }
struct Bing { bing_field: usize, other_field: String }

#[enumtrait::quick_enum]
#[enumtrait::store(foo_macro_store)]
enum Foo {
    Bar,
    Bing,
}

#[enumtrait::store(foo_trait_store)]
trait FooTrait {
    const baz: usize;
    fn foo(&self) -> usize;
}

impl FooTrait for Bar {
    const baz: usize = 2;  
    fn foo(&self) -> usize { self.bar_field }
}

impl FooTrait for Bing {  
    const baz: usize = 2;
    fn foo(&self) -> usize { self.bing_field }
}

#[enumtrait::impl_trait(foo_trait_store for foo_macro_store)]
impl FooTrait for Foo {
    const baz: usize = 42;
}

fn check(f: Foo) -> usize {
    f.foo()
}

Performance

When comparing the cost of a virtual call, versus call through enums.

cargo bench
call_cost               fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ call_with_blackhole                │               │               │               │         │
   ├─ Concrete                        │               │               │               │         │
   │  ├─ 1              0.162 ns      │ 0.184 ns      │ 0.163 ns      │ 0.163 ns      │ 100     │ 819200
   │  ├─ 16             14.71 ns      │ 15.43 ns      │ 15.35 ns      │ 15.17 ns      │ 100     │ 12800
   │  ╰─ 268435456      75.95 ms      │ 91.18 ms      │ 80.98 ms      │ 81.65 ms      │ 100     │ 100
   ├─ Double                          │               │               │               │         │
   │  ├─ 1              0.094 ns      │ 1.111 ns      │ 0.095 ns      │ 0.105 ns      │ 100     │ 819200
   │  ├─ 16             1.89 ns       │ 23.5 ns       │ 2.088 ns      │ 2.239 ns      │ 100     │ 102400
   │  ╰─ 268435456      78.77 ms      │ 129.3 ms      │ 99.86 ms      │ 99.96 ms      │ 100     │ 100
   ├─ ImplDyn                         │               │               │               │         │
   │  ├─ 1              0.788 ns      │ 1.345 ns      │ 0.793 ns      │ 0.805 ns      │ 100     │ 204800
   │  ├─ 16             16.25 ns      │ 16.37 ns      │ 16.29 ns      │ 16.3 ns       │ 100     │ 12800
   │  ╰─ 268435456      249.2 ms      │ 288.8 ms      │ 258.9 ms      │ 261 ms        │ 100     │ 100
   ├─ Single                          │               │               │               │         │
   │  ├─ 1              0.15 ns       │ 1.917 ns      │ 0.18 ns       │ 0.194 ns      │ 100     │ 819200
   │  ├─ 16             14.95 ns      │ 15.58 ns      │ 15.11 ns      │ 15.13 ns      │ 100     │ 12800
   │  ╰─ 268435456      78.88 ms      │ 91.74 ms      │ 83.3 ms       │ 83.98 ms      │ 100     │ 100
   ╰─ Sixteen                         │               │               │               │         │
      ├─ 1              0.417 ns      │ 0.544 ns      │ 0.419 ns      │ 0.425 ns      │ 100     │ 409600
      ├─ 16             22.77 ns      │ 40.55 ns      │ 23.29 ns      │ 23.42 ns      │ 100     │ 6400
      ╰─ 268435456      73.63 ms      │ 87.76 ms      │ 77.36 ms      │ 78.1 ms       │ 100     │ 100

Inter-Macro Communication

Rust macro invocations are independent, and affected by incremental compilation.

  • Change in token input means macro needs to be re-expanded
  • Macros can be expanded in any order
  • Macros are not eagrely expanded (with an exception for some built in macros)

This is highly restrictive, solutions to avoid this include:

  • Communication through shared data structures or files (cannot share tokens)
  • Avoiding communication (verbose)
  • not all macros are necessarily invokes, due to incremental compilation

There are proposals for sharing macro state, defining macro order, through a new interface. All have the core drawback of requiring language/compiler changes.

I hope such a feature (e.g. crate local macro persistent state, message passing between macros, etc ) is implemented, but in the meantime, we have this.

Rust macro invocations are independent. However, macro definitions are ordered!. We can use changing macro definitions to force an ordered invocation of other macros.

See Little Book of Rust macros > Callbacks

We do this by building immutable token stores from macro_rules! definitions that reapply macros that read.

// we can define a basic macro we want to pass information to as
macro_rules! my_macro {
    ($($t:tt)*) => {
        stringify!($($t)*)
    }
}

// We then use the macro_store pattern (trademark pending😂) to store tokens 
// in macros. This can be made into a proc_macro that produces a macro_rules, 
// as is done for `enumtrait::store`
macro_rules! my_name {
    ($p:ident => $($t:tt)*) => {
        $p!( $($t)* Oliver Killane ) // storing a name
    }
}
macro_rules! my_passtime {
    ($p:ident => $($t:tt)*) => {
        $p!( $($t)* makes unecessarily complex macros ) 
    }
}


// reading from name into my_macro, with some extra tokens passable
let name = my_name!(my_macro => some extra data and ); 

// reading from two means applying (tokens get collected over `=>`)
let message = my_name!(my_passtime => my_macro =>); 

assert_eq!(name, "some extra data and Oliver Killane");
assert_eq!(message, "Oliver Killane makes unecessarily complex macros");

With that we can now pass tokens between macros in a DAG.

Additional modification of the store is required to support accessing and exporting from modules, as well as the differing item and expression macro contexts (trailing ; on macro invocation).

enumtrait passes information between macros using this method.

enum_dispatch

Attempts to solve the same problem as enumtrait, but communicates between macro expansions using a shared hashmap.

This technique is discussed here.

The abuse of macro_rules! used by enumtrait is more verbose than enum_dispatch's, however allows for identifiers with spans to be communicated between macros, allowing better error messages.

eagre

Simulates eagre execution of macros generated by its own eagre_macro_rules! macro.

Dependencies

~270–740KB
~17K SLoC