5.6 Macros

Frequently when writing grammars we encounter repetitive constructs that we would like to copy-and-paste. A common example is defining something like a "comma-separated list". Imagine we wanted to parse a comma-separated list of expressions (with an optional trailing comma, of course). If we had to write this out in full, it would look something like:

MST --- 在编写语法时,我们经常会遇到想要复制和粘贴的重复结构。一个常见的示例是定义类似于 “逗号分隔列表” 的内容。想象一下,我们想要解析一个以逗号分隔的表达式列表(当然,带有可选的尾随逗号)。如果我们必须完整地写出来,它看起来会是这样的:

GPT --- 在编写语法时,我们经常遇到重复的构造,通常我们希望能够避免重复的代码。一个常见的例子是定义类似“逗号分隔的列表”的结构。假设我们想解析一个逗号分隔的表达式列表(当然,允许有可选的尾随逗号)。如果我们必须完整地写出这个语法,它可能会是如下所示的样子:

Exprs: Vec<Box<Expr>> = {
    Exprs "," Expr => ...,
    Expr => vec![<>],
}

Of course, this doesn't handle trailing commas, and I've omitted the action code. If we added those, it would get a bit more complicated. So far, this is fine, but then what happens when we later want a comma-separated list of terms? Do we just copy-and-paste everything?

MST --- 当然,这不会处理尾随逗号,并且我省略了操作代码。如果我们添加这些,它会变得有点复杂。到目前为止,这很好,但是当我们稍后需要一个逗号分隔的术语列表时会发生什么呢?我们只是复制和粘贴所有内容吗?

GPT --- 当然,这个语法没有处理尾随逗号,并且我省略了动作代码。如果我们添加这些内容,语法会变得有点复杂。目前这样是可以的,但如果我们之后还想要一个逗号分隔的 Term 列表呢?我们是不是应该复制粘贴所有内容?

LALRPOP offers a better option. You can define macros. In fact, LALRPOP comes with four macros builtin: , ?, +, and (...). So you can write something like Expr? to mean "an optional Expr". This will have type Option<Box> (since Expr alone has type Box). Similarly, you can write Expr or Expr+ to get a Vec (with minimum length 0 and 1 respectively). The final macro is parentheses, which is a shorthand for creating a new nonterminal. This lets you write things like ( ",")? to mean an "optionally parse an Expr followed by a comma". Note the angle brackets around Expr: these ensures that the value of the ( ",") is the value of the expression, and not a tuple of the expression and the comma. This means that ( ",")? would have the type Option<Box> (and not Option<(Box, &'input str)>).

MST --- LALRPOP 提供了更好的选择。您可以定义宏。事实上,LALRPOP 内置了四个宏:、?、+ 和 (...)。所以你可以写类似 Expr 的东西吗?表示“可选的 Expr”。这将具有 Option<Box> 类型(因为仅 Expr 具有 Box 类型)。同样,您可以编写 Expr 或 Expr+ 来获取 Vec(最小长度分别为 0 和 1)。最后一个宏是括号,它是创建新的非终止符的简写。这让你写 ( “,”)?来表示 “选择性地解析一个 Expr 后跟一个逗号”。请注意 Expr 周围的尖括号:这些确保 ( “,”) 的值是表达式的值,而不是表达式和逗号的元组。这意味着 ( “,”)? 将具有 Option<Box> 类型(而不是 Option<(Box, &'input str)>)。

GPT --- LALRPOP 提供了一个更好的选项,你可以定义宏。事实上,LALRPOP 内建了四个宏:*?+(...)。所以你可以写类似 Expr? 来表示“一个可选的 Expr”。这将会有类型 Option<Box<Expr>>(因为 Expr 本身是 Box<Expr> 类型)。类似地,你可以写 Expr*Expr+ 来得到 Vec<Expr>(分别表示最小长度为 0 和 1)。最后一个宏是圆括号,它是用来创建新非终结符的简写。这让你可以写类似 <Expr> "," 来表示“可选地解析一个 Expr 后跟一个逗号”。请注意 Expr 周围的尖括号:它确保了 (<Expr> ",") 的值是表达式的值,而不是表达式和逗号的元组。这意味着 (<Expr> ",")? 的类型是 Option<Box<Expr>>(而不是 Option<(Box<Expr>, &'input str)>)。

Using these operations we can define Exprs in terms of a macro Comma that creates a comma-separated list of T, whatever T is (this definition appears in calculator5):

MST --- 使用这些操作,我们可以根据宏逗号定义 Exprs,该宏逗号创建一个以逗号分隔的 T 列表,无论 T 是什么(此定义出现在 calculator5 中):

GPT --- 使用这些操作我们可以定义 Expr 为一个宏 Comma<T>,它创建了一个逗号分隔的 T 列表,无论 T 是什么类型(这个定义出现在 calculator5 中):

pub Exprs = Comma<Expr>; // (0)

Comma<T>: Vec<T> = { // (1)
    <mut v:(<T> ",")*> <e:T?> => match e { // (2)
        None => v,
        Some(e) => {
            v.push(e);
            v
        }
    }
};

The definition of Exprs on line (0) is fairly obvious, I think. It just uses a macro Comma. Let's take a look then at the definition of Comma on line (1). This is sort of dense, so let's unpack it. First, T is some terminal or nonterminal, but note that we can also use it as a type: when the macro is expanded, the T in the type will be replaced with "whatever the type of T is".

MST --- 我认为第 (0) 行 Exprs 的定义相当明显。它只使用一个宏逗号。然后让我们看一下在线 (1) 中 Comma 的定义。这有点密集,所以让我们解读一下。首先,T 是一些终端或非终端,但请注意,我们也可以将其用作类型:当宏扩展时,类型中的 T 将被替换为 “whatever the type of T is” 。

GPT --- 第 (0) 行中 Expr 的定义是相当明显的,应该没什么问题。它只是使用了一个宏 Comma<Expr>。接下来,我们看看第 (1) 行中 Comma<T> 的定义。这个定义有点复杂,让我们逐步分析。首先,T 是某个终结符或非终结符,但需要注意的是,我们也可以将它用作类型:当宏被展开时,类型中的 T 将被替换为“T 的类型”。

Next, on (2), we parse <mut v:( ",")> <e:T?>. That's a lot of symbols, so let's first remove all the angle brackets, which just serve to tell LALRPOP what values you want to propagate and which you want to discard. In that case, we have: (T ",") T?. Hopefully you can see that this matches a comma-separated list with an optional trailing comma. Now let's add those angle-brackets back in. In the parentheses, we get ( ",")* -- this just means that we keep the value of the T but discard the value of the comma when we build our vector. Then we capture that vector and call it v: <mut v:( ",")*>. The mut makes v mutable in the action code. Finally, we capture the optional trailing element e: <e:T?>. This means the Rust code has two variables available to it, v: Vec and e: Option. The action code itself should then be fairly clear -- if e is Some, it appends it to the vector and returns the result.

MST --- 接下来,在 (2) 上,我们解析 <mut v:( “,”)> <e:T?>。这是很多符号,所以让我们首先删除所有尖括号,这些尖括号只是用来告诉 LALRPOP 你想要传播什么值,以及你想要丢弃哪些值。在这种情况下,我们有:(T “,”) T?。希望你能看到这匹配了一个以逗号分隔的列表和一个可选的尾随逗号。现在让我们重新添加这些尖括号。在括号中,我们得到 ( “,”)* -- 这只是意味着我们在构建向量时保留 T 的值,但丢弃逗号的值。然后我们捕获该向量并将其命名为 v: <mut v:( “,”)*>。mut 使 v 在操作代码中可变。最后,我们捕获可选的尾随元素 e: <e:T?>。这意味着 Rust 代码有两个可用的变量,v: Vec 和 e: Option。然后,操作代码本身应该相当清晰 —— 如果 e 是 Some,它会将其附加到向量并返回结果。

GPT --- 接下来,在第 (2) 行中,我们解析 <mut v:(<T> ",")*> <e:T?>。这有很多符号,所以我们先去掉所有的尖括号,这些尖括号只是告诉 LALRPOP 哪些值需要传递,哪些值需要丢弃。去掉尖括号后,我们得到:(T ",")* T?。希望你能看出,这匹配的是一个带有可选尾随逗号的逗号分隔列表。现在让我们再把尖括号加回来。在括号内,我们得到了 <T> ","* —— 这意味着我们保留 T 的值,但丢弃逗号的值来构建我们的向量。然后我们捕获这个向量并命名为 v<mut v:(<T> ",")*>mut 使得 v 在动作代码中是可变的。最后,我们捕获可选的尾随元素 e: <e:T?>。这意味着 Rust 代码中有两个变量可以使用:v: Vec<T>e: Option<T>。动作代码应该很清楚 —— 如果 eSome,它就会被追加到向量中,并返回结果。

As another example of using macros, you may recall the precedence tiers we saw in calculator4 (Expr, Factor, etc), which had a sort of repetitive structure. You could factor that out using a macro. In this case, it's a recursive macro:

MST --- 作为使用宏的另一个示例,您可能还记得我们在 calculator4 中看到的优先层(Expr、Factor 等),它们具有一种重复的结构。您可以使用宏将其分解出来。在本例中,它是一个递归宏:

GPT --- 作为另一个使用宏的例子,您可能还记得我们在 calculator4 中看到的优先级层次(ExprFactor 等),它们具有某种重复结构。您可以使用宏将其提取出来。在这种情况下,它是一个递归宏:

Tier<Op,NextTier>: Box<Expr> = {
    Tier<Op,NextTier> Op NextTier => Box::new(Expr::Op(<>)),
    NextTier
};

Expr = Tier<ExprOp, Factor>;
Factor = Tier<FactorOp, Term>;

ExprOp: Opcode = { // (3)
    "+" => Opcode::Add,
    "-" => Opcode::Sub,
};

FactorOp: Opcode = {
    "*" => Opcode::Mul,
    "/" => Opcode::Div,
};

And, of course, we have to add some tests to main.rs file:

use lalrpop_util::lalrpop_mod;

lalrpop_mod!(pub calculator5);

#[test]
fn calculator5() {
    let expr = calculator5::ExprsParser::new().parse("").unwrap();
    assert_eq!(&format!("{:?}", expr), "[]");

    let expr = calculator5::ExprsParser::new()
        .parse("22 * 44 + 66")
        .unwrap();
    assert_eq!(&format!("{:?}", expr), "[((22 * 44) + 66)]");

    let expr = calculator5::ExprsParser::new()
        .parse("22 * 44 + 66,")
        .unwrap();
    assert_eq!(&format!("{:?}", expr), "[((22 * 44) + 66)]");

    let expr = calculator5::ExprsParser::new()
        .parse("22 * 44 + 66, 13*3")
        .unwrap();
    assert_eq!(&format!("{:?}", expr), "[((22 * 44) + 66), (13 * 3)]");

    let expr = calculator5::ExprsParser::new()
        .parse("22 * 44 + 66, 13*3,")
        .unwrap();
    assert_eq!(&format!("{:?}", expr), "[((22 * 44) + 66), (13 * 3)]");
}

posted on 2025-01-05 12:06  及途又八  阅读(21)  评论(0)    收藏  举报

导航