Macro that calls itself differently depending on the number of arguments

50 Views Asked by At

I am trying to create a simple map macro that should be called like this:

let c = "asdads";
map![
    a: "b",
    c, // auto expand, same as `c: "asdads"`
    d: 345
]

and return a crate::Map(&[(String, String)]).

I have wrapped it in a struct to implement some properties like Display or Debug, and I am using a reference to a slice because a slice is not Sized.

I can write a macro that parses only the full form (a: "b") or the short form (just a) but I can't think of one that will be able to match both.

The one matching the short form:

($($key:ident),*) => {
    crate::Map(&[
        $((stringify!($key).to_string(), $key.to_string())),*
    ][..])
};

The one matching the full form:

($($key:ident : $value:expr),*) => {
    crate::Map(&[
        $((stringify!($key).to_string(), $value.to_string())),*
    ][..])
};

What I have tried: making two separate macros that match only one line and then passing a tt to it.

macro_rules! map_one {
    ($key:ident) => {
        (stringify!($key).to_string(), $key.to_string())
    };
    ($key:ident : $value:expr) => {
        (stringify!($key).to_string(), $value.to_string())
    };
}

macro_rules! map {
    ($($tt:tt),*) => {
        crate::Map(&[
            $(map_one!($tt)),*
        ][..])
    };
}

I get a compile error:

no rules expected the token `:`
while trying to match `,`.
1

There are 1 best solutions below

0
PitaJ On BEST ANSWER

The tt matcher matches a token tree, which is either a single token (ident, operator, :, etc) or a set of tokens grouped within a pair of parenthesis, square brackets, or curly braces.

So key: "value" is actually three tts. The way to work around this is to match optionally on the : $value:expr part with a $()? group, them pass the whole thing along to your map_one macro:

macro_rules! map {
    ($($key:ident $(: $value:expr)?),*) => {
        crate::Map(&[
            $(map_one!($key $(: $value)?)),*
        ][..])
    };
}

Playground

I would also recommend merging the two macros using the @prefix pattern:

macro_rules! map {
    (@one $key:ident) => {
        (stringify!($key).to_string(), $key.to_string())
    };
    (@one $key:ident : $value:expr) => {
        (stringify!($key).to_string(), $value.to_string())
    };
    ($($key:ident $(: $value:expr)?),*) => {
        crate::Map(&[
            $(map!(@one $key $(: $value)?)),*
        ][..])
    };
}

Playground