使用Listener模式计算结果

在上一节中的计算器是以解释的方式执行的,现在我们想要把它转换成以编译的方式执行。编译执行和解释执行相比,需要依赖于特定的目标机器。在这里我们假设有一台这样的机器,它用堆栈进行运算,支持如下表所示的几种指令:

指令 说明 运算元数目 用途
LDV Load Variable 1 变量入栈
LDC Load Constant 1 常量入栈
STR Store Value 1 栈顶一个元素存入指定变量
ADD Add 0 栈顶两个元素出栈,求和后入栈
SUB Subtract 0 栈顶两个元素出栈,求差后入栈
MUL Multiply 0 栈顶两个元素出栈,求积后入栈
DIV Divide 0 栈顶两个元素出栈,求商后入栈
RET Return 0 栈顶一个元素出栈,计算结束

做这个最简单的方法是使用ANTLR的语法分析树Listener机制实现DirectiveListener类,然后它通过监听来自树遍历器触发的事件,输出对应的机器指令。

Listener机制的优势是我们不必要自己去做任何树遍历,甚至我们不必要知道遍历语法分析树的运行时如何调用我们的方法,我们只要知道我们的DirectiveListener类得到通知,在与语法规则匹配的短语开始和结束时。这种方法减少了我们学习ANTLR必须要花费的时间,让我们回到我们所熟悉的编程语言领域。

这里不需要创建新的语法规则,还是继续沿用前文Calc.g所包含的语法,标签也要保留:

  1. grammar Calc;
  2. prog
  3. : stat+
  4. ;
  5. stat
  6. : expr # printExpr
  7. | ID '=' expr # assign
  8. ;
  9. expr
  10. : expr op=(MUL|DIV) expr # MulDiv
  11. | expr op=(ADD|SUB) expr # AddSub
  12. | INT # int
  13. | ID # id
  14. | '(' expr ')' # parens
  15. ;
  16. MUL : '*' ;
  17. DIV : '/' ;
  18. ADD : '+' ;
  19. SUB : '-' ;
  20. ID : [a-zA-Z]+ ;
  21. INT : [0-9]+ ;
  22. WS : [ \t\r\n]+ -> skip ; // toss out whitespace

然后,我们可以运行ANTLR工具:

  1. antlr Calc.g

它会生成后缀名为tokens和java的六个文件:

  1. Calc.tokens CaclLexer.java CalcParser.java
  2. CalcLexer.tokens CalcBaseListener.java CalcListener.java

正如这里我们看到的,ANTLR会为我们自动生成Listener基础设施。其中CalcListener是语法和Listener对象之间的关键接口,描述我们可以实现的回调方法:

  1. public interface CalcListener extends ParseTreeListener {
  2. void enterProg(CalcParser.ProgContext ctx);
  3. void exitProg(CalcParser.ProgContext ctx);
  4. void enterPrintExpr(CalcParser.PrintExprContext ctx);
  5. ...
  6. }

CalcBaseListener则是ANTLR生成的一组空的默认实现。ANTLR内建的树遍历器会去触发在Listener中像enterProg()和exitProg()这样的一串回调方法,如同它对语法分析树执行了一次深度优先遍历。为响应树遍历器触发的事件,我们的DirectiveListener需要继承CalcBaseListener并实现一些方法。我们不需要实现全部的接口方法,我们也不需要去覆写每个enter和exit方法,我们只需要去覆写那些我们感兴趣的回调方法。

在本例中,我们需要通过覆写6个方法对6个事件——当树遍历器exit那些有标签的选项时触发——作出响应。我们的基本策略是当这些事件发生时打印出已转换的指令。以下是完整的实现代码:

  1. public class DirectiveListener extends CalcBaseListener {
  2. @Override
  3. public void exitPrintExpr(CalcParser.PrintExprContext ctx) {
  4. System.out.println("RET\n");
  5. }
  6. @Override
  7. public void exitAssign(CalcParser.AssignContext ctx) {
  8. String id = ctx.ID().getText();
  9. System.out.println("STR " + id);
  10. }
  11. @Override
  12. public void exitMulDiv(CalcParser.MulDivContext ctx) {
  13. if (ctx.op.getType() == CalcParser.MUL) {
  14. System.out.println("MUL");
  15. } else {
  16. System.out.println("DIV");
  17. }
  18. }
  19. @Override
  20. public void exitAddSub(CalcParser.AddSubContext ctx) {
  21. if (ctx.op.getType() == CalcParser.ADD) {
  22. System.out.println("ADD");
  23. } else {
  24. System.out.println("SUB");
  25. }
  26. }
  27. @Override
  28. public void exitId(CalcParser.IdContext ctx) {
  29. System.out.println("LDV " + ctx.ID().getText());
  30. }
  31. @Override
  32. public void exitInt(CalcParser.IntContext ctx) {
  33. System.out.println("LDC " + ctx.INT().getText());
  34. }
  35. }

为了让它运行起来,余下我们唯一需要做的事是创建一个主程序去调用它:

  1. public class Calc {
  2. public static void main(String[] args) throws Exception {
  3. InputStream is = args.length > 0 ? new FileInputStream(args[0]) : System.in;
  4. ANTLRInputStream input = new ANTLRInputStream(is);
  5. CalcLexer lexer = new CalcLexer(input);
  6. CommonTokenStream tokens = new CommonTokenStream(lexer);
  7. CalcParser parser = new CalcParser(tokens);
  8. ParseTree tree = parser.prog();
  9. ParseTreeWalker walker = new ParseTreeWalker();
  10. walker.walk(new DirectiveListener(), tree);
  11. // print LISP-style tree
  12. System.out.println(tree.toStringTree(parser));
  13. }
  14. }

这个程序和前文Calc.java中的代码极度相似,区别只在12-13行。这两行代码负责创建树遍历器,然后让树遍历器去遍历那颗从语法分析器返回的语法分析树,当树遍历器遍历时,它就会触发调用到我们的DirectiveListener中实现的方法。此外,通过传入一个不同的Listener实现我们能简单地生成完全不同的输出。Listener机制有效地隔离了语法和语言应用,使语法可以被其它应用再次使用。

现在一切完备,让我们尝试着去编译和运行它吧!下面是完整的命令序列:

  1. compile *.java
  2. run Calc calc.txt

编译的输出结果如下所示:

  1. LDC 19
  2. RET
  3. LDC 5
  4. STR a
  5. LDC 6
  6. STR b
  7. LDV a
  8. LDV b
  9. LDC 2
  10. MUL
  11. ADD
  12. RET
  13. LDC 1
  14. LDC 2
  15. ADD
  16. LDC 3
  17. MUL
  18. RET