Visitor Calculator
我们将以访问者模式做一个计算器。
预期效果
输入
:
193
a=5
b=6
a+b*2
(1+2)*3
输出
:
193
17
9
PS:每次操作都需要换行
,输入’!'可以重置
标识符对应的数值。
语法文件
Visitor模式:通过在语法规则的每条分支后加上 # Identifier
(注意不能和规则名冲突)这样类似标签
的形式。
使得对于每种输入我们都有不同的处理方法,后续会介绍如何定义这些处理方法。
此外分享一下在设计clear也就是清零语法时的一些心得:
- 文中采用的是,输入
!
时,自动释放标识符与数值的所有对应,并且输出clear并且换行。但一开始考虑的是输入字符串clear
,清零后自动换行。放弃这种方法的理由是:clear字符串本身会先被expr规则
中的ID识别出来。如果要在ID的visit处理函数
中识别clear,又会因为返回值
必须是Integar类型而矛盾,最后放弃了输入clear这个方法。 - 要注意CLEAR后必须要跟NEWLINE,因为每一行都需要以回车或者换行结束操作。
//LabeledExpr.g4
grammar LabeledExpr;
prog: stat+ ;
stat: expr NEWLINE # printExpr
| ID '=' expr NEWLINE # assign
| CLEAR NEWLINE # clearMemory
| NEWLINE # blank
;
expr: expr op=('*'|'/') expr # MulDiv
| expr op=('+'|'-') expr # AddSub
| INT # int
| ID # id
| '(' expr ')' # parens
;
CLEAR : '!' ;
MUL : '*' ;
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
ID : [a-zA-Z]+ ; // match identifiers
INT : [0-9]+ ; // match integers
NEWLINE:'\r'? '\n' ; // return newlines to parser (is end-statement signal)
WS : [ \t]+ -> skip ; // toss out whitespace
生成代码
使用IDEA插件生成,可以参照上一节生成。
重写Visitor
观察以下代码我们可以发现,Visitor生成的处理函数命名为 visitor+标签名
。
理论上,我们需要重写每个分支处理函数来应对不同的输入情况。
有一句格外的显眼:visit
(ctx.expr())。
我们不管括号内的expr(),实际上是左循环的token,为了获取子分支的情况,我们需要显式地
调用visit()。
以visitPrintExpr
为例,这句语法的目的是打印出表达式的结果。而表达式本身有多种情况,可以是INT、标识符、嵌套表达式,但是我们
不需要在这个分支中去操心这些,上文说过我们需要重写每个子分支的处理函数,因此在这里只需要通过visit得到expr的值
就可以了。
而在下面的visitInt、visitId等分支处理函数中,最后返回了处理过后的值
。
因此我们以这样一种由上而下的方式,满足了各种输入的情况。
/** "memory" for our calculator; variable/value pairs go here */
Map<String, Integer> memory = new HashMap<String, Integer>();
@Override
public Integer visitProg(LabeledExprParser.ProgContext ctx) {
return super.visitProg(ctx);
}
/** expr NEWLINE */
@Override
public Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) {
// evaluate the expr child
Integer value = visit(ctx.expr());
// print the result
System.out.println(value);
// return dummy value
return 0;
}
/** ID '=' expr NEWLINE */
@Override
public Integer visitAssign(LabeledExprParser.AssignContext ctx) {
// id is left-hand side of '='
String id = ctx.ID().getText();
// compute value of expression on right
int value = visit(ctx.expr());
// store it in our memory
memory.put(id, value);
return value;
}
/** CLEAR MEMORY */
@Override
public Integer visitClearMemory(LabeledExprParser.ClearMemoryContext ctx) {
memory.clear();
System.out.println("clear");
return 0;
}
@Override
public Integer visitBlank(LabeledExprParser.BlankContext ctx) {
return super.visitBlank(ctx);
}
@Override
public Integer visitParens(LabeledExprParser.ParensContext ctx) {
return super.visitParens(ctx);
}
/** expr op=('*'|'/') expr */
@Override
public Integer visitMulDiv(LabeledExprParser.MulDivContext ctx) {
// get value of left subexpression
int left = visit(ctx.expr(0));
// get value of right subexpression
int right = visit(ctx.expr(1));
if ( ctx.op.getType() == LabeledExprParser.MUL) {
return left * right;
}
// must be DIV
return left / right;
}
/** expr op=('+'|'-') expr */
@Override
public Integer visitAddSub(LabeledExprParser.AddSubContext ctx) {
// get value of left subexpression
int left = visit(ctx.expr(0));
// get value of right subexpression
int right = visit(ctx.expr(1));
if ( ctx.op.getType() == LabeledExprParser.ADD ) {
return left + right;
}
// must be SUB
return left - right;
}
/** ID */
@Override
public Integer visitId(LabeledExprParser.IdContext ctx) {
String id = ctx.ID().getText();
if ( memory.containsKey(id) ) {
return memory.get(id);
}
return 0;
}
/** INT */
@Override
public Integer visitInt(LabeledExprParser.IntContext ctx) {
return Integer.valueOf(ctx.INT().getText());
}
运行结果
编写一个主函数去整合语法分析器以及自定义Vistor:
public class LabelMain {
public static void main(String[] args) throws IOException {
String inputFile = null;
if ( args.length > 0 ) {
inputFile = args[0];
}
InputStream is = System.in;
if ( inputFile!=null ) {
is = new FileInputStream(inputFile);
}
ANTLRInputStream input = new ANTLRInputStream(is);
LabeledExprLexer lexer = new LabeledExprLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
LabeledExprParser parser = new LabeledExprParser(tokens);
ParseTree tree = parser.prog(); // parse
EvalVisitor eval = new EvalVisitor();
eval.visit(tree);
}
}
编写一个 t.exp
aa = 5
bb = 6
aa+bb*2
!
aa
bb
运行的时候配置
运行结果如下:
Listener Java
我们将以监听者模式抽取Java类中的方法,并整合成一个接口。
预期效果
输入
:
import java.util.List;
import java.util.Map;
public class Demo {
void f(int x, String y) { }
int[ ] g(/*no args*/) { return null; }
List<Map<String, Integer>>[] h() { return null; }
}
输出
:
语法文件
整个Java.g4文件比较大,这里我们只列举出关于class、method、import的语法识别部分。
Listener模式:与Visitor模式不同的是,Listener无需手动地
在各个分支后打上标签,当我们以监听者模式生成语法分析器时,语法分析器每遍历一个规则都会有两次响应事件
,分别是enter
以及exit
。
例如:classDeclaration,会生成enter
classDeclaration和exit
classDeclaration,我们可以在这两个时刻被监听到时响应
它们。
//Java.g4
classDeclaration
: 'class' Identifier typeParameters? ('extends' type)?
('implements' typeList)?
classBody
;
importDeclaration
: 'import' 'static'? qualifiedName ('.' '*')? ';'
;
methodDeclaration
: type Identifier formalParameters ('[' ']')* methodDeclarationRest
| 'void' Identifier formalParameters methodDeclarationRest
;
访问器模式生成语法分析器
生成代码
除了常见的几个文件以外,我们会生成以下两个文件:
JavaListener.java JavaBaseListener.java
前者是遍历整个语法分析树的全部响应事件定义
,后者是它的实现
。
我们可以重写 JavaBaseListener,选择性地编写那些响应事件。
重写Listener
实际上我们只需要重写ImportDeclaration
的enter、ClassDeclaration
的enter和exit、MethodDeclaration
的enter即可。
因为我们预想的类定义需要开始和结尾的{和},因此需要在exit
时响应。而import和method对结尾并无要求。
以下代码中需要注意的是,有时需要用到parser.getTokenStream
去获取token,有时候直接可以通过ctx.
就可以获取。
比如:qualifiedName
需要通过parser.getTokenStream获取,而Identifier
通过ctx.就可以获取。
观察可以发现前者是语法规则的命名,而后者是词法规则的命名,可见前者这样的语法规则定义的词法符号
往往需要通过语法分析器遍历获取。
//ExtractInterfaceListener.java
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.misc.Interval;
public class ExtractInterfaceListener extends JavaBaseListener {
JavaParser parser;
public ExtractInterfaceListener(JavaParser parser) {this.parser = parser;}
@Override
public void enterImportDeclaration(JavaParser.ImportDeclarationContext ctx) {
TokenStream tokens = parser.getTokenStream();
String temp=tokens.getText(ctx.qualifiedName());
System.out.println("import "+temp+';');
}
/** Listen to matches of classDeclaration */
@Override
public void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx){
System.out.println("interface I"+ctx.Identifier()+" {");
}
@Override
public void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx) {
System.out.println("}");
}
/** Listen to matches of methodDeclaration */
@Override
public void enterMethodDeclaration(
JavaParser.MethodDeclarationContext ctx
)
{
// need parser to get tokens
TokenStream tokens = parser.getTokenStream();
String type = "void";
if ( ctx.type()!=null ) {
type = tokens.getText(ctx.type());
}
String args = tokens.getText(ctx.formalParameters());
System.out.println("\t"+type+" "+ctx.Identifier()+args+";");
}
}
运行结果
编写一个主函数去整合语法分析器以及自定义Listener:
//ExtractInterfaceTool.java
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.*;
import java.io.FileInputStream;
import java.io.InputStream;
public class ExtractInterfaceTool {
public static void main(String[] args) throws Exception {
String inputFile = null;
if ( args.length>0 ) inputFile = args[0];
InputStream is = System.in;
if ( inputFile!=null ) {
is = new FileInputStream(inputFile);
}
ANTLRInputStream input = new ANTLRInputStream(is);
JavaLexer lexer = new JavaLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
JavaParser parser = new JavaParser(tokens);
// parse
ParseTree tree = parser.compilationUnit();
// create standard walker
ParseTreeWalker walker = new ParseTreeWalker();
ExtractInterfaceListener extractor = new ExtractInterfaceListener(parser);
// initiate walk of tree with listener
walker.walk(extractor, tree);
}
}
待用的输入:
//Demo.java
import java.util.List;
import java.util.Map;
public class Demo {
void f(int x, String y) { }
int[ ] g(/*no args*/) { return null; }
List<Map<String, Integer>>[] h() { return null; }
}
编译后,输入java ExtractInterfaceTool Demo.java:
Visitor 与 Listener
我们可以从主函数的角度去对比:
//Visitor
EvalVisitor eval = new EvalVisitor();
eval.visit(tree);
//Listener
// create standard walker
ParseTreeWalker walker = new ParseTreeWalker();
ExtractInterfaceListener extractor = new ExtractInterfaceListener(parser);
// initiate walk of tree with listener
walker.walk(extractor, tree);
在产生Visitor的语法分析器之前,我们需要对子分支打上标签。产生语法分析器后,需要重写Visitor(每个子分支的visit函数)。最后在主函数中,我们用重写的Visitor实例来访问语法分析树。
在产生Listener的语法分析器之前,我们并不需要额外的操作。产生语法分析器后,需要重写Listener(可选的每个子分支的enter及exit函数)。最后在主函数中,我们新建一个语法树唤醒器,再新建一个重写的Listener实例,把语法分析树和Listener实例放到唤醒器中。
对比以下可以发现:
visitor对于子分支的处理是主动的(打标签),而listener是自动的。 visitor会将输入与子分支进行比对再触发响应,而listener会在enter、exit两个节点触发响应。 在对某个分支的处理时,如果需要调用其子分支,visitor需要主动地visit。而listener在遍历时会自动处理,这让我们无需关注子分支的细节