在宏中使用可选和非可选参数压缩迭代

Zip iterables with Optional and Non Optional parameter in macro

对于我的词法分析器的测试部分,我想出了一个简单的宏,让 met 定义预期的标记类型(枚举)和标记文字(字符串):

macro_rules! token_test {
    ($($ttype:ident: $literal:literal)*) => {
        {
            vec!($($ttype,)*).iter().zip(vec!($($literal,)*).iter())
        }
    }
}

然后我可以这样使用它:

for (ttype, literal) in token_test! {
    Let: "let" Identifier: "five" Assign: "=" Int: "5" Semicolon: ";"
} {
    //...
}

然而,这有点冗长,我们不需要为大部分标记指定文字,因为我有另一个宏将枚举变体转换为字符串(例如:让-> "让").

所以我希望做的是:

for (ttype, literal) in token_test! {
    Let Identifier: "five" Assign Int: "5" Semicolon
} {
    //...
}

如果我理解正确,我可以使用可选参数来匹配 TYPE: LITERALTYPE。也许是这样的:

macro_rules! token_test {
    ($($ttype:ident$(: $literal:literal)?)*) => {
        {
            //...
        }
    }
}

那么我的问题是有没有办法从中构建 Vector

更清楚:

使其与以下宏一起工作(欢迎任何改进):

macro_rules! token_test {
    ($($ttype:ident$(: $literal:literal)?)*) => {
        vec!($($ttype,)*).iter().zip(vec!(
        $(
            {
                let mut literal = $ttype.as_str().unwrap();

                $(literal = $literal;)?

                literal
            }
        ),*).iter())
    }
}

这个 'iterates' 在 literal 宏参数上,并最初设置 as_str 的值,它将枚举变体转换为字符串。然后,如果定义了 $literal,它会将本地文字值替换为该值。最后,它 returns 局部文字变量。

改进

macro_rules! some_or_none {
    () => { None };
    ($entity:literal) => { Some($entity) }
}

macro_rules! token_test {
    ($($ttype:ident$(: $literal:literal)?)*) => {
        vec!($($ttype,)*).iter().zip(vec!($(
            some_or_none!($($literal)?).unwrap_or($ttype.as_str().unwrap())
        ),*))
    }
}

删除了一些不必要的范围,第二个 .iter(),并添加了 some_or_none 宏。通过这种方式,如果提供了文字,我不需要执行 as_str

进一步改进

在上面的例子中,提供了两个宏。一个显然是一个“私有”宏,因为它的存在只对另一个的执行有用。但是,关于宏导出的工作原理有一个小问题。与函数不同,宏无法访问在同一作用域中定义但调用者无法访问的宏。参见 this playground example。如果您不打算导出该宏,这不是问题,这是可能的,因为它的唯一目的是在测试套件中使用。但是,您可能仍希望在 crate 级别公开公开它,而不公开 some_or_none!。执行此操作的常规方法是将 some_or_none! 集成到 token_test! 宏中,方法是在其前面添加 @:

macro_rules! token_test {
    (@some_or_none) => {
        None
    };
    (@some_or_none $entity:literal) => {
        Some($entity)
    };
    ($($ttype:ident $(: $literal:literal)?)*) => {
        vec!($($ttype,)*)
          .iter()
          .zip(vec!($(
            token_test!(@some_or_none $($literal)?)
              .unwrap_or($ttype.as_str().unwrap())
          ),*))
    };
}

使用此版本,您可以安全地将 test_token 导出为 shown in this playground

多一点

  • 来自 Rust 论坛 steffahn 的原创想法

还有另一种类似的方法可以解决这个问题并且不涉及 unwrap_or,而不是在 some_or_none 中包装成 Option,我们实际上可以创建两个分支,它们采用 [= =30=] 或 TYPE,像这样:

macro_rules! token_test {
  (@ttype_or_literal $ttype:ident) => { $ttype.as_str().unwrap() };
  (@ttype_or_literal $ttype:ident: $literal:literal) => { $literal };
  ($($ttype:ident $(: $literal:literal)?)*) => {
    vec!($($ttype,)*)
      .iter()
      .zip(vec![$(token_test!(@ttype_or_literal $ttype$(: $literal)?)),*])
  };
}

再一次

因为我只需要一个可以解构为(type, iterable)的可迭代对象,一对数组就足够了:

macro_rules! token_test {
  (@ttype_or_literal $ttype:ident) => { $ttype.as_str().unwrap() };
  (@ttype_or_literal $ttype:ident: $literal:literal) => { $literal };
  ($($ttype:ident $(: $literal:literal)?)*) => {
    [$(($ttype, token_test!(@ttype_or_literal $ttype$(: $literal)?))),*]
  };
}

所以不再 vec 也不再 zip.

聪明的把戏

A user on the Rust forum 给出了这个潜在的技巧,涉及忽略第二个参数(如果存在)。我通过没有两个宏使解决方案更紧凑:

macro_rules! token_test {
  (@ignore_second $value:expr $(, $_ignored:expr)? $(,)?) => { $value };
  ($($ttype:ident $(: $literal:literal)?)*) => {
    [$(($ttype, token_test!(@ignore_second $($literal,)? $ttype.as_str().unwrap()))),*]
  };
}