Search code examples
rustmacrosmatchguard

Is there a way to match the parameters to a Rust macro?


Consider the following code:

trait Trait {
    fn x(&self) -> u32;
}

struct A {}
impl Trait for A {
    fn x(&self) -> u32 {
        10
    }
}

struct B {}
impl Trait for B {
    fn x(&self) -> u32 {
        20
    }
}

struct C {
    created_time: u64,
}

impl Trait for C {
    fn x(&self) -> u32 {
        30
    }
}

impl C {
    pub fn new() -> C {
        C { created_time: 1000 } // for simplicity
    }
}

macro_rules! create {
    ($type:ident) => {
        match stringify!($type) {
            "C" => Box::new(C::new()) as Box<dyn Trait>,
            _ => Box::new($type {}) as Box<dyn Trait>,
        }
    };
}

fn main() {
    let a: Box<dyn Trait> = create!(A);
    let b: Box<dyn Trait> = create!(B);
    let c: Box<dyn Trait> = create!(C);

    assert_eq!(a.x(), 10);
    assert_eq!(b.x(), 20);
    assert_eq!(c.x(), 30);
}

If you ask the compiler to expand the macro, this resolves to:

let a: Box<dyn T> =
    match "A" {
        "C" => Box::new(C::new()) as Box<dyn T>,
        _ => Box::new(A{}) as Box<dyn T>,
    };
let b: Box<dyn T> =
    match "B" {
        "C" => Box::new(C::new()) as Box<dyn T>,
        _ => Box::new(B{}) as Box<dyn T>,
    };
let c: Box<dyn T> =
    match "C" {
        "C" => Box::new(C::new()) as Box<dyn T>,
        _ => Box::new(C{}) as Box<dyn T>,
    };

and this explains nicely why the compiler gives the following error when trying to compile it:

error[E0063]: missing field `created_time` in initializer of `C`
  --> mwe.rs:29:27
   |
29 |             _ => Box::new($type { }) as Box<dyn T>,
   |                           ^^^^^ missing `created_time`
...
37 |     let c: Box<dyn T> = create!(C);
   |                         ---------- in this macro invocation

error: aborting due to previous error

However, I had expected the compiler to notice the match "C" { "C" => ..., _ => ... } case and drop the second clause because it can't ever be run anyhow. Sadly it didn't and instead complained about the second (impossible) clause being impossible to compile.

I also tried replacing the match with if in the macro as follows, but to no avail:

macro_rules! create {
    ($type:ident) => {
        if stringify!($type) == "C" {
            Box::new(C::new()) as Box<dyn T>
        } else {
            Box::new($type { }) as Box<dyn T>
        }
    }
}

leads to

let c: Box<dyn T> =
    if "C" == "C" { Box::new(C::new()) as Box<dyn T> }
    else { Box::new(C{}) as Box<dyn T> };

with the same error as the match attempt.

Hopeful that Haskell's guard pipe syntax would somehow work in Rust, I finally also tried the following:

macro_rules! create {
    ($type:ident) | (stringify!($type) == "C") => {
        Box::new(C::new()) as Box<dyn T>
    },
    ($type:ident) | (stringify!($type) != "C") => {
        Box::new($type { }) as Box<dyn T>
    },
}

but that gave an error: no rules expected the token '|'


Which ultimately leads me back to the question in the title:

Is there a way to add "guards" to the macro rules to tell the compiler "Only run A if this parameter is passed, or run B on something else" ?


Solution

  • While it does seem that your is an X/Y problem and would be more elegantly solved using a trait such as Default, it is possible to match macro parameters to some extent.

    Your macro could be re-written as

    macro_rules! create {
        (C) => {
            Box::new(C::new()) as Box<dyn Trait>
        };
    
        ($type:ident) => {
            Box::new($type {}) as Box<dyn Trait>
        };
    }
    

    The compiler stops at the first successful match.

    Note that this has some limitation: As you might expect, the compiler makes a literal comparison of tokens, and for example things like the following will fail:

    type D = C;
    
    let really_just_another_c: Box<dyn Trait> = create!(D);
    

    with

    error[E0063]: missing field `created_time` in initializer of `C`
      --> src/main.rs:41:18
       |
    41 |         Box::new($type {}) as Box<dyn Trait>
       |                  ^^^^^ missing `created_time`
    ...
    51 |     let c: Box<dyn Trait> = create!(D);
       |                             ---------- in this macro invocation