Search code examples
rustrust-macros

Rust macro that calls itself differently depending on the number of arguments


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

let c = "Content of c";
map![
    a: "Content of a",
    c, // auto expand, same as `c: "Content of c"`
    d: 0xDC097397
]

and returns a Map(&[(String, String)]) struct.

I wrapped it into a struct to implement some properties like Display or Debug, and I'm 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've 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 `,`.

Solution

  • 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