ANTLR4 笔记
ANTLR4 是一个非常厉害的程序/库,可以用来生成 Lexer 和 Parser,而且生成的接口非常易用。
安装
$ cd /usr/local/lib
$ curl -O http://www.antlr.org/download/antlr-4.5-complete.jar$ vim ~/.zshrc # or vim ~/.bashrc
export CLASSPATH=".:/usr/local/lib/antlr-4.5-complete.jar:$CLASSPATH"
alias antlr4='java -Xmx500M -cp "/usr/local/lib/antlr-4.5-complete.jar:$CLASSPATH" org.antlr.v4.Tool'
alias grun='java org.antlr.v4.runtime.misc.TestRig'
$ . ~/.zshrc # or restart the terminal
语法
官方已经提供了非常多的常用的语言的语法文件了,拿来看看可以学到很多,甚至可以删删改改直接拿来用: https://github.com/antlr/grammars-v4
grammar
名称和文件名要一致- Parser 规则(即 non-terminal)以小写字母开始
- Lexer 规则(即 terminal)以大写字母开始
- 所有的 Lexer 规则无论写在哪里都会被重排到 Parser 规则之后
- 所有规则中若有冲突,先出现的规则优先匹配
- 用
'string'
单引号引出字符串 |
用于分隔两个产生式,(a|b)
括号用于指定子产生式,?+*
用法同正则表达式- 在产生式后面
# label
可以给某条产生式命名,在生成的代码中即可根据标签分辨不同产生式 - 不需要指定开始符号
- 规则以分号终结
/* block comment */
以及// line comment
- 默认的左结合,可以用
<assoc=right>
指定右结合 - 可以处理直接的左递归,不能处理间接的左递归
- 如果用
MUL: '*';
指定了某个字符串的名字,在程序里面就能用这个名字了 - 用
fragment
可以给 Lexer 规则中的公共部分命名
例子:
stmt: expr NEWLINE # printExpr| ID '=' expr NEWLINE # assign| NEWLINE # blank;expr: <assoc=right> expr op='^' expr # pow| expr op=('*'|'/') expr # mulDiv| expr op=('+'|'-') expr # addSub| INT # int| ID # id| '(' expr ')' # parensMUL : '*';
DIV : '/';
ADD : '+';
SUB : '-';
ID : Letter LetterOrDigit*
fragment Letter: [a-zA-Z_]
fragment Digit: [0-9]
fragment LetterOrDigit: Letter | Digit
NEWLINE: '\r'? '\n'
WS : [ \t]+ -> skip
常见 Lexer 规则
//------ Puncuation
call : ID '(' exprList ')' ;
// or define token labels
call : ID LP exprList RP ;
LP : '(';
RP : ')';//------ Keywords
returnStmt : 'return' expr ';' ;//------ Identifiers
ID : ID_LETTER (ID_LETTER | DIGIT)* ;
fragment ID_LETTER : 'a'..'z' | 'A'..'Z' | '_' ;
fragment DIGIT : '0'..'9';//------ Numbers
INT : DIGIT+ ;
FLOAT : DIGIT+ '.' DIGIT*| '.' DIGIT+;//------ Strings
STRING : '"' (ESC | .)*? '"' ;
fragment ESC : '\\' [btnr"\\] ; // \b, \t, \n, ...//------ Comments
LINE_COMMENT : '//' .*? '\n' -> skip;
BLOCK_COMMENT : '/*' .*? '*/' -> skip;//------ Whitespace
WS : [ \t\n\r]+ -> skip
整合到自己的程序中
ANTLR 4 提供了 Visitor 和 Listener 两种模式,通过这两种模式可以很轻松地把 Parser 的结果做各种处理。ANTLR 4 默认会生成 Listener 模式,如果不需要要加上 -no-listener
,如果要生成 Visitor 模式要加上 -visitor
。
$ antlr4 -visitor Calc.g4
$ ls
Calc.g4 CalcBaseVisitor.java CalcListener.java
Calc.tokens CalcLexer.java CalcParser.java
CalcBaseListener.java CalcLexer.tokens CalcVisitor.java
运行 ANTLR 4 会生成以下文件:
<Grammar>Lexer.java
: Lexer<Grammar>Parser.java
: Parser<Grammar>Listener.java
: Listener 接口<Grammar>BaseListener.java
: Listener 默认实现<Grammar>Visitor.java
: Visitor 接口<Grammar>BaseVisitor.java
: Visitor 默认实现<Grammar>[Lexer].tokens
: 当语法被拆分成多个多个文件时用于同步编号
使用方法就是把 *.java
复制到项目中合适的位置,然后编写调用代码、Visitor及(或)Listener。
调用代码
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;
import java.io.*;public class Calc {public static void main(String[] args) throws IOException {InputStream is = new FileInputStream("example/1.txt"); // or System.in;ANTLRInputStream input = new ANTLRInputStream(is);CalcLexer lexer = new CalcLexer(input);CommonTokenStream tokens = new CommonTokenStream(lexer);CalcParser parser = new CalcParser(tokens);ParseTree tree = parser.calc(); // calc is the starting ruleSystem.out.println("LISP:");System.out.println(tree.toStringTree(parser));System.out.println();System.out.println("Visitor:");EvalVisitor evalByVisitor = new EvalVisitor();evalByVisitor.visit(tree);System.out.println();System.out.println("Listener:");ParseTreeWalker walker = new ParseTreeWalker();Evaluator evalByListener = new Evaluator();walker.walk(evalByListener, tree);}
}
可以看到使用方法就是把输入流包装一下喂给 Lexer,之后将 Token 流喂给 Parser,最后调用 ParseTree::<starting>
生成解析树。
解析树可以直接用 .toStringTree
按照 LISP 风格打印出来。
使用 Visitor 模式的话,就是新建 Visitor 对象,之后 visit(tree)
。
使用 Listener 模式的话,需要一个 ParseTreeWalker
和一个 Listener 对象,然后用这个 walker 在树上用这个 Listener 行走。
不论是 Visitor 模式还是 Listener 模式,解决的痛点都是把结构和行为分开,真的十分佩服这些设计模式的创造者。下面简单讲下这两个模式。
Visitor 模式
假设有一个复杂的结构,其中有个基类 B
,以及很多的派生类 Derived1
, Derived2
, …。然后我们现在有一些动作 Action1
, Action2
, …。
用 Visitor 模式的话,首先要在每个基类中指定一个 accept
函数来接受访客,接下来每个派生类重载这个函数,让传进来的访客访问自己。
另外一方面,规定 IVisitor
接口,里面对每个不同类型的派生类 Derived
都有分别的 void visit(Derived obj);
函数。每一个 Visitor 都要实现这个接口。
在对某个派生类对象obj
执行某个动作visitor
时,用 obj.accept(visitor);
。
具体可以看下面这个例子。由基类 Shape
派生出了 Rectangle
和 Circle
。我们分别想要求每种图形的周长和面积,于是编写了 PerimeterVisitor
和 AreaVisitor
两个 Visitor。注意调用的方式,是让派生类接受访问者,再让访问者访问自己。
import java.util.*;//------ Interfaces
interface IShapeVisitor {void visit(Rectangle r);void visit(Circle c);
}abstract class Shape {public abstract void accept(IShapeVisitor visitor);
}//------ Shapes
class Rectangle extends Shape {public double height;public double width;Rectangle(double height, double width) { this.height = height; this.width = width; }@Overridepublic void accept(IShapeVisitor visitor) { visitor.visit(this); }
}class Circle extends Shape {public double radius;Circle(double radius) { this.radius = radius; }@Overridepublic void accept(IShapeVisitor visitor) { visitor.visit(this); }
}//------ Visitors
class PerimeterVisitor implements IShapeVisitor {@Overridepublic void visit(Rectangle r) {System.out.println((r.height + r.width) * 2);}@Overridepublic void visit(Circle c) {System.out.println(2 * Math.PI * c.radius);}
}class AreaVisitor implements IShapeVisitor {@Overridepublic void visit(Rectangle r) {System.out.println(r.height * r.width);}@Overridepublic void visit(Circle c) {System.out.println(Math.PI * Math.pow(c.radius, 2.));}
}//------ Test
public class VisitorTest {public static void main(String[] args) {List<Shape> shapes = new ArrayList<>();shapes.add(new Rectangle(3, 4));shapes.add(new Circle(1));PerimeterVisitor perimeterVisitor = new PerimeterVisitor();shapes.forEach(x -> x.accept(perimeterVisitor));AreaVisitor areaVisitor = new AreaVisitor();shapes.forEach(x -> x.accept(areaVisitor));}
}
在上面的例子里面,我们也可以在基类里面加上一个 .getArea()
而不使用 Visitor 模式。那么用 Visitor 的好处是什么呢?就是前面说到的把结构和行为分离。
假设我现在要多增加一个行为 Action
,我不需要改动我的结构,也就是不用在每个派生类里面多重载一个 .getAction()
。不改动结构有什么好处呢?第一, Java 中每个 public
类都需要独立成一个文件,如果要在每个类里面都加上这么个行为,那么就需要分别打开一个个文件,与此同时这个行为的代码也被拆散到了一个个文件中,这无疑是非常不利于维护的。第二,有些情况下,我们对结构代码没有控制权,这个时候我们就不能往里面加代码了。
要增加一个行为,我需要做的只是增加一个 Visitor,在这个 Visitor 里面实现所有类的对应的行为即可。程序的其余部分完全不需要管。
Listener 模式
Listener 模式对于 Javascript 用户来说应该是非常熟悉的。简单地说,某段程序定义了一系列的事件,我们可以编写当某些事件发生时做什么的回调函数,也就是 Listener,并且绑定到这些事件上。那么这段程序触发了这些事件的时候,就会调用我们的回调函数。
一个很简单的例子就是在做树的遍历的时候,遍历程序提供 enterNode
和 exitNode
事件,我们就可以编写当进入节点和退出节点时要处理的事情。
使用 ANTLR 4 中的 Visitor 模式
下面以一个计算器为例子,语法如下:
grammar Calc;calc: stmt*;stmt: expr NEWLINE # printExpr| ID '=' expr NEWLINE # assign| NEWLINE # blank;expr: expr op=('*'|'/') expr # mulDiv| expr op=('+'|'-') expr # addSub| NUMBER # literal| ID # id| '(' expr ')' # paren;MUL : '*';
DIV : '/';
ADD : '+';
SUB : '-';ID : [a-zA-Z_]+ ;
NUMBER : DIGIT+| DIGIT+ '.' DIGIT*| '.' DIGIT+;
fragment DIGIT : [0-9];
NEWLINE : '\r'? '\n';
WS : [ \t]+ -> skip;
我们实现一个求值的 Visitor。
import java.util.HashMap;
import java.util.Map;public class EvalVisitor extends CalcBaseVisitor<Double> {public Map<String, Double> vars = new HashMap<>();// stmt : ID '=' expr NEWLINE ;@Overridepublic Double visitAssign(CalcParser.AssignContext ctx) {String id = ctx.ID().getText();Double val = visit(ctx.expr());vars.put(id, val);return val;}// stmt : expr NEWLINE ;@Overridepublic Double visitPrintExpr(CalcParser.PrintExprContext ctx) {Double value = visit(ctx.expr());System.out.println(value);return .0;}// expr : INT ;@Overridepublic Double visitLiteral(CalcParser.LiteralContext ctx) {return Double.valueOf(ctx.NUMBER().getText());}// expr : ID ;@Overridepublic Double visitId(CalcParser.IdContext ctx) {String id = ctx.ID().getText();if (vars.containsKey(id)) return vars.get(id);return .0;}// expr : expr op=('*'|'/') expr ;@Overridepublic Double visitMulDiv(CalcParser.MulDivContext ctx) {double lhs = visit(ctx.expr(0));double rhs = visit(ctx.expr(1));if (ctx.op.getType() == CalcParser.MUL) return lhs * rhs;return lhs / rhs;}// expr : expr op=('+'|'-') expr ;@Overridepublic Double visitAddSub(CalcParser.AddSubContext ctx) {double lhs = visit(ctx.expr(0));double rhs = visit(ctx.expr(1));if (ctx.op.getType() == CalcParser.ADD) return lhs + rhs;return lhs - rhs;}// expr : '(' expr ')' ;@Overridepublic Double visitParen(CalcParser.ParenContext ctx) {return visit(ctx.expr());}
}
通过上面的例子,可以看到, ANTLR 4 为每个产生式生成了对应的 visit 函数,并且有各自不同的 Context 对象 ctx
。要访问子树需要使用 visit(ctx.<sublabel>());
ctx.<nonterminal>()
可以访问语法规则中的<nonterminal>
部分的 Contextctx.getText()
可以获得在原文中的串
想知道 Context 对象里面有什么?当然,你可以看 <Grammar>Parser.java
里面写的。但是,如果你有一个带智能提示的 IDE 的话,那就非常舒服了!
使用 ANTLR 4 中的 Listener 模式
ANTLR 4 会为产生式生成
public void enter<Label>(CalcParser.<Label>Context ctx);
public void exit<Label>(CalcParser.<Label>Context ctx);
这样的事件,类似 Visitor 模式按需填空即可。
传递参数与返回值
细心的读者应该注意到了,ANTLR 4 生成的 Visitor 模式中返回类型是统一的,而 Listener 模式直接就是 void
,并且两个模式都没有提供传入参数的地方。那么如果想要手动操纵返回值和参数怎么办呢?
ANTLR 4 Runtime 提供了一个 ParseTreeProperty<T>
,其实大致就是个 IdentityHashMap
。你可以把 Context 当作 key 把相关的东西丢进去。
Listener 例子
还是前面的计算器,演示下 Listener 模式以及 ParseTreeProperty
的用法。
import org.antlr.v4.runtime.tree.ParseTreeProperty;import java.util.HashMap;
import java.util.Map;/*** Created by abcdabcd987 on 2016-03-23.*/
public class Evaluator extends CalcBaseListener {public Map<String, Double> vars = new HashMap<>();public ParseTreeProperty<Double> values = new ParseTreeProperty<>();// stmt : ID '=' expr NEWLINE ;@Overridepublic void exitAssign(CalcParser.AssignContext ctx) {String id = ctx.ID().getText();Double val = values.get(ctx.expr());vars.put(id, val);}// stmt : expr NEWLINE ;@Overridepublic void exitPrintExpr(CalcParser.PrintExprContext ctx) {System.out.println(values.get(ctx.expr()));}// expr : NUMBER ;@Overridepublic void exitLiteral(CalcParser.LiteralContext ctx) {values.put(ctx, Double.valueOf(ctx.NUMBER().getText()));}// expr : ID ;@Overridepublic void exitId(CalcParser.IdContext ctx) {values.put(ctx, vars.containsKey(ctx.ID().getText()) ? vars.get(ctx.ID().getText()) : .0);}// expr : expr op=('*'|'/') expr ;@Overridepublic void exitMulDiv(CalcParser.MulDivContext ctx) {double lhs = values.get(ctx.expr(0));double rhs = values.get(ctx.expr(1));values.put(ctx, ctx.op.getType() == CalcParser.MUL ? lhs * rhs : lhs / rhs);}// expr : expr op=('+'|'-') expr ;@Overridepublic void exitAddSub(CalcParser.AddSubContext ctx) {double lhs = values.get(ctx.expr(0));double rhs = values.get(ctx.expr(1));values.put(ctx, ctx.op.getType() == CalcParser.ADD ? lhs + rhs : lhs - rhs);}// expr : '(' expr ')' ;@Overridepublic void exitParen(CalcParser.ParenContext ctx) {values.put(ctx, values.get(ctx.expr()));}
}
Listener 模式与 Visitor 模式的比较
在 Visitor 模式中,树的遍历是需要我们自己手动控制的。这个有好处也有坏处。当你要实现一个树上的解释器的时候,用 Visitor 就很方便,比如你可以只执行 if-else
块中的一个,比如你可以重复执行循环语句的主体部分。当然坏处就是万一意外忘记遍历或者重复遍历就麻烦了。
在 Listener 模式中, walker 自顾自地走着,按顺序恰好遍历每个节点一次,进入或者退出一个节点的时候调用你的 Listener。因此,如果要实现一个树上解释器的话, Listener 模式就非常蛋疼了。但是,如果想要构建一个 AST ,这种自动帮你一遍的事情就很舒服了。再比如要支持函数的后向调用,可以在第一次遍历中先把所有的函数名称找出来,然后再在第二遍遍历中做类型检查等等。
添加 ANTLR 4 JAR 到 Intellij Idea 中
http://stackoverflow.com/questions/21051991/importing-jar-file-into-intellij-idea
参考:https://abcdabcd987.com/notes-on-antlr4/