使用Visitor模式计算结果

为了给前面的算术表达式语法分析器计算出结果,我们还需要做些其它的事情。

ANTLR v4鼓励我们保持语法的整洁,使用语法分析树Visitor和其它遍历器来实现语言应用。不过在接触这些之前,我们需要对语法做些修改。

首先,我们需要用标签标明规则的选项,标签可以是和规则名没有冲突的任意标志符。如果选项上没有标签,ANTLR只会为每个规则生成一个visit方法。

在本例中,我们希望为每个选项生成一个不同的visit方法,以便每种输入短语都能得到不同的事件。在新的语法中,标签出现在选项的右边缘,且以“#”符号开头:

  1. stat
  2. : expr # printExpr
  3. | ID '=' expr # assign
  4. ;
  5. expr
  6. : expr op=(MUL|DIV) expr # MulDiv
  7. | expr op=(ADD|SUB) expr # AddSub
  8. | INT # int
  9. | ID # id
  10. | '(' expr ')' # parens
  11. ;

接下来,让我们为运算符字面量定义一些记号名字,以便以后可以在visit方法中引用作为Java常量的它们:

  1. MUL : '*' ;
  2. DIV : '/' ;
  3. ADD : '+' ;
  4. SUB : '-' ;

现在,我们有了一个增强型的语法。接下来要做的事情是实现一个EvalVisitor类,它通过遍历表达式语法分析树计算和返回值。

执行下面的命令,让ANTLR生成Visitor接口和它的默认实现,其中-no-listener参数是告诉ANTLR不再生成Listener相关的代码:

  1. antlr -no-listener -visitor Calc.g

所有被标签标明的选项在生成的Visitor接口中都定义了一个visit方法:

  1. public interface CalcVisitor<T> extends ParseTreeVisitor<T> {
  2. T visitProg(CalcParser.ProgContext ctx);
  3. T visitPrintExpr(CalcParser.PrintExprContext ctx);
  4. T visitAssign(CalcParser.AssignContext ctx);
  5. ...
  6. }

接口定义使用的是Java泛型,visit方法的返回值为参数化类型,这允许我们根据表达式计算返回值的类型去设定实现的泛型参数。因为表达式的计算结果是整型,所以我们的EvalVisitor应该继承CalcBaseVisitor<Integer>类。为计算语法分析树的每个节点,我们需要覆写与语句和表达式选项相关的方法。这里是全部的代码:

  1. public class EvalVisitor extends CalcBaseVisitor<Integer> {
  2. /** "memory" for our calculator; variable/value pairs go here */
  3. Map<String, Integer> memory = new HashMap<String, Integer>();
  4. /** ID '=' expr */
  5. @Override
  6. public Integer visitAssign(CalcParser.AssignContext ctx) {
  7. String id = ctx.ID().getText(); // id is left-hand side of '='
  8. int value = visit(ctx.expr()); // compute value of expression on right
  9. memory.put(id, value); // store it in our memory
  10. return value;
  11. }
  12. /** expr */
  13. @Override
  14. public Integer visitPrintExpr(CalcParser.PrintExprContext ctx) {
  15. Integer value = visit(ctx.expr()); // evaluate the expr child
  16. System.out.println(value); // print the result
  17. return 0; // return dummy value
  18. }
  19. /** INT */
  20. @Override
  21. public Integer visitInt(CalcParser.IntContext ctx) {
  22. return Integer.valueOf(ctx.INT().getText());
  23. }
  24. /** ID */
  25. @Override
  26. public Integer visitId(CalcParser.IdContext ctx) {
  27. String id = ctx.ID().getText();
  28. if ( memory.containsKey(id) ) return memory.get(id);
  29. return 0;
  30. }
  31. /** expr op=('*'|'/') expr */
  32. @Override
  33. public Integer visitMulDiv(CalcParser.MulDivContext ctx) {
  34. int left = visit(ctx.expr(0)); // get value of left subexpression
  35. int right = visit(ctx.expr(1)); // get value of right subexpression
  36. if ( ctx.op.getType() == CalcParser.MUL ) return left * right;
  37. return left / right; // must be DIV
  38. }
  39. /** expr op=('+'|'-') expr */
  40. @Override
  41. public Integer visitAddSub(CalcParser.AddSubContext ctx) {
  42. int left = visit(ctx.expr(0)); // get value of left subexpression
  43. int right = visit(ctx.expr(1)); // get value of right subexpression
  44. if ( ctx.op.getType() == CalcParser.ADD ) return left + right;
  45. return left - right; // must be SUB
  46. }
  47. /** '(' expr ')' */
  48. @Override
  49. public Integer visitParens(CalcParser.ParensContext ctx) {
  50. return visit(ctx.expr()); // return child expr's value
  51. }
  52. }

以前开发和测试语法都是使用的TestRig,这次我们试着编写计算器的主程序来启动代码:

  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. EvalVisitor eval = new EvalVisitor();
  10. // 开始遍历语法分析树
  11. eval.visit(tree);
  12. System.out.println(tree.toStringTree(parser));
  13. }
  14. }

创建一个运行主程序的脚本:

  1. #!/bin/sh
  2. java -cp .:./antlr-4.5.1-complete.jar:$CLASSPATH $*

把它保存为run.sh后,执行以下命令:

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

然后你就会看到文本形式的语法分析树以及计算结果:

  1. 193
  2. 17
  3. 9
  4. (prog (stat (expr 193)) (stat a = (expr 5)) (stat b = (expr 6))
  5. (stat (expr (expr a) + (expr (expr b) * (expr 2)))) (stat (expr
  6. (expr ( (expr (expr 1) + (expr 2)) )) * (expr 3))))