上一篇文章中,我简要介绍了一下 ANTLR4 。当时只是把官方的 Reference 看了一遍,还没有什么实战的经验,写的都是比较不具体的东西。在这篇文章里面,我把在写编译器的时候用到 ANTLR4 的实战经验拿出来记录一下。

关于结合性

结合性的修饰在低版本的 ANTLR4 中是针对运算符的,而在高版本中则是针对产生式本身。

关于顺序

比如在识别关键字和标识符的时候会用如下词法规则:

Identifier
    :   [a-zA-Z_] [a-zA-Z0-9_]*
    ;

NullLiteral
    :   'null'
    ;

看起来没什么问题。但是当你随意试了一组数据之后发现根本识别不出 NullLiteral,反倒是变成了 Identifier。原因就是在 ANTLR4 中,先写的名称优先级高。在这里 Identifier 也能识别 null,所以 null 就变成了 Identifier 了。解决的办法也很简单,把像 Identifier 这样通用型的规则放后面就行了。

关于标签

ANTLR4 可以在产生式的后面加上以井号开头的标签 # label,要求:

  • 对于某个 nonterminal
    • 要么所有的产生式都有标签
    • 要么所有的产生式都没有标签
    • 两个产生式可以使用相同的标签
  • 对于不同的 nonterminal ,标签不能相同

特别是两个产生式可以使用相同的标签这一点,在编写表达式相关的语法时特别好用。例如一个类C++语言就可以这么写:

expression
    :   expression op=('++' | '--')                  # PostfixIncDec    // Precedence 1
    |   expression '(' parameterList? ')'            # FunctionCall
    |   expression '[' expression ']'                # Subscript
    |   expression '.' Identifier                    # MemberAccess

    |   <assoc=right> op=('++'|'--') expression      # UnaryExpr        // Precedence 2
    |   <assoc=right> op=('+' | '-') expression      # UnaryExpr
    |   <assoc=right> op=('!' | '~') expression      # UnaryExpr
    |   <assoc=right> 'new' creator                  # New

    |   expression op=('*' | '/' | '%') expression   # BinaryExpr       // Precedence 3
    |   expression op=('+' | '-') expression         # BinaryExpr       // Precedence 4
    |   expression op=('<<'|'>>') expression         # BinaryExpr       // Precedence 5
    |   expression op=('<' | '>') expression         # BinaryExpr       // Precedence 6
    |   expression op=('<='|'>=') expression         # BinaryExpr
    |   expression op=('=='|'!=') expression         # BinaryExpr       // Precedence 7
    |   expression op='&' expression                 # BinaryExpr       // Precedence 8
    |   expression op='^' expression                 # BinaryExpr       // Precedence 9
    |   expression op='|' expression                 # BinaryExpr       // Precedence 10
    |   expression op='&&' expression                # BinaryExpr       // Precedence 11
    |   expression op='||' expression                # BinaryExpr       // Precedence 12

    |   <assoc=right> expression op='=' expression   # BinaryExpr       // Precedence 14

    |   Identifier                                   # Identifier
    |   constant                                     # Literal
    |   '(' expression ')'                           # SubExpression

对应的 Listener 就能够合并判断了:

@Override
public void exitBinaryExpr(MillParser.BinaryExprContext ctx) {
    BinaryExpr.BinaryOp op;
    switch (ctx.op.getType()) {
        case MillParser.Star: op = BinaryExpr.BinaryOp.MUL; break;
        case MillParser.Div: op = BinaryExpr.BinaryOp.DIV; break;
        case MillParser.Mod: op = BinaryExpr.BinaryOp.MOD; break;
        case MillParser.Plus: op = BinaryExpr.BinaryOp.ADD; break;
        case MillParser.Minus: op = BinaryExpr.BinaryOp.SUB; break;
        case MillParser.LeftShift: op = BinaryExpr.BinaryOp.SHL; break;
        case MillParser.RightShift: op = BinaryExpr.BinaryOp.SHR; break;
        case MillParser.Less: op = BinaryExpr.BinaryOp.LT; break;
        case MillParser.Greater: op = BinaryExpr.BinaryOp.GT; break;
        case MillParser.LessEqual: op = BinaryExpr.BinaryOp.LE; break;
        case MillParser.GreaterEqual: op = BinaryExpr.BinaryOp.GE; break;
        case MillParser.Equal: op = BinaryExpr.BinaryOp.EQ; break;
        case MillParser.NotEqual: op = BinaryExpr.BinaryOp.NE; break;
        case MillParser.And: op = BinaryExpr.BinaryOp.BITWISE_AND; break;
        case MillParser.Caret: op = BinaryExpr.BinaryOp.XOR; break;
        case MillParser.Or: op = BinaryExpr.BinaryOp.BITWISE_OR; break;
        case MillParser.AndAnd: op = BinaryExpr.BinaryOp.LOGICAL_AND; break;
        case MillParser.OrOr: op = BinaryExpr.BinaryOp.LOGICAL_OR; break;
        case MillParser.Assign: op = BinaryExpr.BinaryOp.ASSIGN; break;
        default: throw new RuntimeException("Unknown binary operator.");
    }
    map.put(ctx, new BinaryExpr(
            op,
            (Expr)map.get(ctx.expression(0)),
            (Expr)map.get(ctx.expression(1)),
            new SourcePosition(ctx.op),
            new SourcePosition(ctx.expression(0)),
            new SourcePosition(ctx.expression(1))
    ));
}

获得行号和列号

一般来说 AST 里面还会存下一个 Token 在源程序中的行号和列号,用于错误提示,或者作为在编写编译器的时候查错的参考。把 ANTLR4 的代码稍微再封装一下之后,就很好用了,无论传入Token, ParserRuleContext 还是 TerminalNode 都可以得到行号(例子见上面那个程序):

public class SourcePosition {
    public final int line;
    public final int column;

    public SourcePosition(int line, int column) {
        this.line = line;
        this.column = column;
    }

    public SourcePosition(Token token) {
        this.line = token.getLine();
        this.column = token.getCharPositionInLine();
    }

    public SourcePosition(ParserRuleContext ctx) {
        this(ctx.start);
    }

    public SourcePosition(TerminalNode terminal) {
        this(terminal.getSymbol());
    }

    @Override
    public String toString() {
        return "Line " + line + " Column " + column;
    }
}