乐者为王

Do one thing, and do it well.

ANTLR 4权威参考读书笔记(23)

默认情况下,ANTLR为每个规则生成一个单一的事件类型,而不管语法分析器匹配了哪个选项。这很不方便,因为监听器和访问者方法必须确定哪个选项被语法分析器匹配。在本节中,我们将看到如何得到更细粒度的事件。

为规则选项贴标签以得到精确的事件方法

为阐明事件粒度问题,让我们为以下的表达式语法构建一个带有监听器的简单计算器:

1
2
3
4
5
6
grammar Expr;
s : e ;
e : e op=MULT e    // MULT is '*'
  | e op=ADD e     // ADD is '+'
  | INT
  ;

按照上面的语法,规则e会产生一个相当无用的监听器,因为规则e的所有选项导致树遍历器触发相同的enterE()和exitE()方法。

1
2
3
public interface ExprListener extends ParseTreeListener {
    void enterE(ExprParser.EContext ctx);
    void exitE(ExprParser.EContext ctx);

监听器方法必须使用op记号标签和ctx的方法进行测试以查看语法分析器为每个e子树匹配了哪个选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void exitE(ExprParser.EContext ctx) {
    if (ctx.getChildCount() == 3) {    // operations have 3 children
        int left = values.get(ctx.e(0));
        int right = values.get(ctx.e(1));
        if (ctx.op.getType() == ExprParser.MULT) {
            values.put(ctx, left * right);
        } else {
            values.put(ctx, left + right);
        }
    } else {
        values.put(ctx, values.get(ctx.getChild(0)));    // an INT
    }
}

在exitE()中引用的MULT字段是在ExprParser中由ANTLR自动生成的:

1
2
public class ExprParser extends Parser {
    public static final int MULT=1, ADD=2, INT=3, WS=4;

如果我们查看在类ExprParser中的类EContext,我们可以看到ANTLR把来自3个选项的所有元素都塞进了相同的上下文对象。

1
2
3
4
5
public static class EContext extends ParserRuleContext {
    public Token op;                     // derived from label op
    public List<EContext> e() { ... }    // get all e subtrees
    public EContext e(int i) { ... }     // get ith e subtree
    public TerminalNode INT() { ... }    // get INT node if alt 3 of e

为得到更精确的监听器事件,ANTLR让我们使用#运算符给任何规则最外层的选项打标签。让我们从Expr派生语法LExpr,并给e的选项打上标签,以下是修改后的e规则:

1
2
3
4
e : e MULT e  # Mult
  | e ADD e   # Add
  | INT       # Int
  ;

现在,ANTLR为e的每个选项生成了单独的监听器方法,因此,我们不再需要op记号标签。对于选项标签X,ANTLR生成方法enterX()和exitX()。

1
2
3
4
5
6
7
8
9
public interface LExprListener extends ParseTreeListener {
    void enterMult(LExprParser.MultContext ctx);
    void exitMult(LExprParser.MultContext ctx);
    void enterAdd(LExprParser.AddContext ctx);
    void exitAdd(LExprParser.AddContext ctx);
    void enterInt(LExprParser.IntContext ctx);
    void exitInt(LExprParser.IntContext ctx);
    ...
}

注意,ANTLR也为选项生成特定的以标签命名的上下文对象(EContext的子类)。专门的上下文对象的getter方法只限于应用在那些相关的选项。例如,IntContext只有一个INT()方法,我们可以在enterInt()中调用ctx.INT(),但在enterAdd()中就不能。

监听器和访问者是极好的。我们只需要通过充实事件方法就可以得到可复用和可重定目标的语法以及封装的语言应用。ANTLR甚至会为我们自动生成骨架代码。

Comments