Search code examples
rustrust-macros

Can't make this macro rule compile: type annotation needed


In the following code, I really don't find a way to make the macro rule args! compile, without changing its syntax like used in the main function:

use std::rc::Rc;

trait Checker: Fn(i32) -> bool {}
impl<F> Checker for F where F: Fn(i32) -> bool {}

fn is_odd(n: i32) -> bool {
    n % 2 == 1
}

fn is_positive(n: i32) -> bool {
    n >= 0
}

fn and(a: impl Checker, b: impl Checker) -> impl Checker {
    move |n: i32| a(n) && b(n)
}

macro_rules! args {
    ( $($name:ident : $checker:expr),* $(,)? ) => {{
        let args: Vec<(String, Option<Rc<dyn Checker>>)> = vec![
            $( args!(__arg stringify!($name).to_string(), $checker) ),*
        ];
        args
    }};
    // LINE 26: the following rule is never matched:
    (__arg $name:expr, None) => {
        ($name, None)
    };
    (__arg $name:expr, $checker:expr) => {
        (
            $name,
            // LINE 33: can't give a type for argument `c` (this is a fn type):
            $checker.map(|c| {
                let c: Rc<dyn Checker> = Rc::new(c);
                c
            })
        )
    };
}

fn main() {
    let _args = args![
        a: Some(is_odd),
        b: Some(and(is_positive, is_odd)),
        c: None, // <-- This compiles well if this line is commented
    ];
}

This gives this error:

error[E0282]: type annotations needed
  --> src/main.rs:33:27
   |
33 |               $checker.map(|c| {
   |                             ^ consider giving this closure parameter a type
...
42 |       let _args = args![
   |  _________________-
43 | |         a: Some(is_odd),
44 | |         b: Some(and(is_positive, is_odd)),
45 | |         c: None, // <-- This compiles well if this line is commented
46 | |     ];
   | |_____- in this macro invocation

The problem is that None.map(|c| ...) (line 33) needs a type for c; but I can't give one because it is unnameable and it changes everytime.

So I tried to add a rule in the macro, specialized for None (line 26); but the rule never matches : I guess it is because in the first, main rule of the macro, $checker matches as an expr, but the pattern None is certainly considered as a token tree.

The only workaround I found is to make the final syntax more cumbersome by adding square brackets around each arg definition and change the main rule of the macro like this (but I would like to avoid it):

( $( [ $name:ident : $($checker:tt)* ] ),* $(,)? ) => ...

How can I correct this macro rule in order to make it work?


Solution

  • Unfortunately, in order to get the (__arg $name:expr, None) rule to match, you must match your $checkers as tts, idents or lifetimes (tts being the only one that will work in this case). This can be cumbersome at times, as you found out yourself.

    How about a simpler (for the user, at least) macro syntax?

    macro_rules! args {
        ( $($name:ident $(: $checker:expr)?),* $(,)? ) => {{
            let args: Vec<(String, Option<Rc<dyn Checker>>)> = vec![
                $( args!(__arg stringify!($name).to_string() $(, $checker)?) ),*
            ];
            args
        }};
        (__arg $name:expr) => {
            ($name, None)
        };
        (__arg $name:expr, $checker:expr) => {
            (
                $name,
                {
                    let c: Rc<dyn Checker> = Rc::new($checker);
                    Some(c)
                }
            )
        };
    }
    

    Which can be called in your example as

    let _args = args![
        a: is_odd,
        b: and(is_positive, is_odd),
        c,
    ];
    

    I realize this does not directly fix your macro, but if the only reason you are using an Option in the macro arguments is to specify the presence or absence of a Checker, this may be a good (and arguably cleaner) alternative.

    Playground