ANTLR4 使用技巧
在上一篇文章中,我简要介绍了一下 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;
}
}