Writing a macro_rules! macro used like a match expression


#1

I’ve had this macro in a library for a while:

#[macro_export]
macro_rules! match_ignore_ascii_case {
    ( $value: expr, $( $string: expr => $result: expr ),+ _ => $fallback: expr ) => {
        {
            use std::ascii::AsciiExt;
            match &$value[..] {
                $(
                    s if s.eq_ignore_ascii_case($string) => $result,
                )+
                _ => $fallback
            }
        }
    };
}

It’s used like this:

match_ignore_ascii_case! { string,
    "foo" => Some(Foo),
    "bar" => Some(Bar),
    "baz" => Some(Baz)
    _ => None
}

Note that the second-to-last arm doesn’t end with a comma, while previous ones do. This is because in the macro_rules! pattern:

( $value: expr, $( $string: expr => $result: expr ),+ _ => $fallback: expr )

… the comma is between non-fallback arms, not after them. $( … ),+, not $( … , )+.

I’d actually prefer to have the comma after rather than between. But when I make that change (and change the usage accordingly) I get a build error:

src/lib.rs:134:5: 134:6 error: local ambiguity: multiple parsing options: built-in NTs expr ('string') or 1 other option.
src/lib.rs:134     _ => None
                   ^

So this macro is a bit weird, but it’s been used for about a year now.

The part where I have a problem is that recent nightlies emit a warning:

src/lib.rs:116:59: 116:60 warning: `$result:expr` may be followed by `_`, which is not allowed for `expr` fragments
src/lib.rs:116     ( $value: expr, $( $string: expr => $result: expr ),+ _ => $fallback: expr ) => {
                                                                         ^
src/lib.rs:116:59: 116:60 note: The above warning will be a hard error in the next release.

I think this is related to https://github.com/rust-lang/rfcs/pull/1384.

I haven’t managed to find a way to write this macro that is both “future proof” (does not emit this warning) and not ambiguous. Any suggestion?


#2

@SimonSapin you can go full recursive: https://play.rust-lang.org/?gist=f8b1652f43cc720f89a3&version=nightly (this version puts the trailing comma in, as to follow the spirit of future-proofing we should not put _ after expr).

(Edit: that’s actually not full recursive. With sufficient recursion you could match _ as the separator and evade the future-proofing rules entirely. But that’s a bad idea.)


#3

Nice, I didn’t think of making recursive. I’ve had to raise the recursion limit for this user each CSS color keywords and another one in Servo for CSS property names, and it took me a while what was going on with this error:

<cssparser macros>:12:1: 12:2 error: unexpected token: `@`
<cssparser macros>:12 @ inner $ value , ( $ ( $ rest ) * ) -> (
                      ^

… which turned out to be caused by a user missing a comma (still using the old syntax) but didn’t provide any indication where that user is. I’ll try to reduce this and file a diagnostics issue on the compiler.

But now it works! Thanks a lot.


#4

Diagnostics issue: https://github.com/rust-lang/rust/issues/31022


#5

So, the ambiguity error you are reporting is a bug in the macro parser, but a longstanding one. We’ve been making pretty slow progress towards solving it (read: glacial). I’m not sure what the issue # is off hand though.


#6

@nikomatsakis Is it https://github.com/rust-lang/rust/issues/24827? Do you think it should be fixed before the future-proofing rules that are now warnings in Nightly become errors?


#7

I don’t think that issue is at fault (in fact, I hadn’t really seen it). I was thinking of the error that the parser cannot “reverse course” once it begins parsing a nonterminal like $string:expr. However, I was wrong – it’s not going to work to put the , inside the repetition, or at least it shouldn’t, because that would only parse correctly if _ is not part of expr, which is not something you are allowed to depend on (_ is not in the follow set of expr, in other words).

Another option beyond full recursive is to just introduce an unambiguous terminator like ;:

#[macro_export]
macro_rules! match_ignore_ascii_case {
    ( $value: expr, $( $string: expr => $result: expr ),+; _ => $fallback: expr ) => {
        {
        }
    };
}

fn main() {
    // used like this: note the `;` before the `_`
    match_ignore_ascii_case! { "a", "b" => bar; _ => baz }
}

(Note that the recursive version works ok because the _ is listed first, and hence is given higher-precedence than an expr – even in the case where the definition of expr is expanded in the future to include _.)


#8

I didn’t mean that that issue is at fault. Rather, since people are(?) likely to hit that issue when making tweaking macros to be future-proof, should we wait until it’s fixed before making future-proofing rules hard errors?

The unambiguous terminator trick is goo do know, but in this case it looks weird in the macro usage. Since there’s only one definitions with many users, I prefer to hide complexity in the definition.