rustpattern-matchingrust-macros

Rust macro to generate custom arrays using byte slice


I want to make a macro in rust to generate a custom syntax array in order to use in a match:

The match at it's baseline:

    match &self.text[self.offset..] {
        [b'f', b'o', b'r', b' ', ..] => {
            Some(CToken::new(CTokenKind::For, self.line, self.column))
        }
        _ => None,
    }

I would like the macro to generate the slice for the match branch, as follows:

    match &self.text[self.offset..] {
        mm!(b"for") => {
            Some(CToken::new(CTokenKind::For, self.line, self.column))
        }
        _ => None,
    }

I would like this macro because the keywords can get very long and it reduces a lot the readability of the code.

I have tried to implement a macro but can't get it right.

I succeeded to generate the array however my macro takes u8 elements and not a whole string:

   macro_rules! mm {
      ($($ch:literal), *) => {
          [$($ch,)* b' ', ..]
      }
   }

Using this macro I can use my macro as follows:

    mm!(b'f', b'o', b'r') => ...

However it does not changes anything, so I would like the macro to take a whole b"my string here"


Solution

  • You could create a procedural macro to do this, but match expressions of this form don't generate good assembly anyway. It's better to use starts_with.

    match slice {
        s if s.starts_with(b"for ") => true,
        _ => false,
    }
    

    Unfortunately, a single macro can't expand into both a pattern and an if-guard, so something that looks like mm!("for") => true can't do this. However, you can put the entire match inside a macro, which is nicer when this kind of pattern is all you need in the match.

    macro_rules! slice_match {
        ($slice:ident {
            $($pat:literal => $e:expr,)*
            _ => $else:expr $(,)?
        }) => {
            match $slice {
                $(s if s.starts_with($pat.as_bytes()) => $e,)*
                _ => $else,
            }
        };
    
        ($slice:ident {
            $($pat:literal => $e:expr,)*
            $i:ident => $else:expr $(,)?
        }) => {
            match $slice {
                $(s if s.starts_with($pat.as_bytes()) => $e,)*
                $i => $else,
            }
        };
    }
    
    // With _ as catch-all
    slice_match!(slice {
        "for " => true,
        _ => false,
    })
    
    // With identifier as catch-all
    slice_match!(slice {
        "for " => true,
        _ident => false,
    })
    

    The two variations of the macro only differ in whether the catch-all pattern is an identifier or the _ pattern. It also requires you to include the space in each string.

    This macro requires every branch to end with a comma, even if it's in brackets, which is different from real match expressions. You could also change this to allow other kinds of branches, but you'll need to introduce some kind of signal (such as a keyword or symbol) to indicate which branches will transform into starts_with and which are left unchanged.