小言_互联网的博客

打破国外垄断,开发中国人自己的编程语言(2):使用监听器实现计算器

399人阅读  评论(0)

上一篇:编写解析表达式的计算器

目录

1. Visitor与Listener

2. Listener对应的接口和基类

3. 用Listener方式实现可计算器


 

1. Visitor与Listener

在上一篇文章中使用Antlr和Visitor实现了一个可以计算表达式的程序MarvelCalc。这个程序非常简单,相当于Antlr的HelloWorld。不过Antlr除了Visitor方式外,还支持Listener方式,也就是监听器方式。不管是哪种方式,其目的都是遍历AST(抽象语法树),只是Visitor方式需要显式访问子节点(通过visit方法访问),例如,下面的代码访问了MulDiv的两个子节点,也就是MulDiv的左右操作数(ctx.expr(0)和ctx.expr(1))。


  
  1. // expr op=('*'|'/') expr # MulDiv
  2. public Integer visitMulDiv(CalcParser.MulDivContext ctx) {
  3. int left = visit(ctx.expr( 0)); // 访问MulDiv的左操作数
  4. int right = visit(ctx.expr( 1)); // 访问MulDiv的右操作数
  5. if ( ctx.op.getType() == CalcParser.MUL ) return left * right;
  6. return left / right;
  7. }
  8. }

而Listener方式是由系统自动访问当前节点的子节点的,并不需要显式访问子节点。而且Listener可以拦截当前节点的开始处理和结束处理动作。开始处理动作的事件方法以enter开头,结束处理动作的事件方法以exit开头。例如,处理MulDiv动作时,会生成两个事件方法:enterMulDiv和exitMulDiv,分别表示开始处理MulDiv和结束处理MulDiv,这两个方法的代码如下:


  
  1. @Override
  2. public void enterMulDiv(CalcParser.MulDivContext ctx) {
  3. }
  4. @Override
  5. public void exitMulDiv(CalcParser.MulDivContext ctx) {
  6. }

那么开始处理动作和结束处理动作有什么区别呢?如果是原子表达式(内部不包含其他表达式的表达式),如id、数值等,这两个事件方法没什么不同的(用哪一个处理表达式都可以)。但如果是非原子表达式,就要考虑下使用enter还是exit了。例如,下面的表达式:

3 * (20 / x * 43)

这个表达式明显是非原子的。编译器会从左向右扫描整个表达式,当扫描到第一个乘号(*)时,会将右侧的所有内容(20 / x * 43)当做一个整体处理,这就会第一次调用enterMulDiv方法和exitMulDiv方法。只不过在调用enterMulDiv方法后,还会做很多其他的工作,最后才会调用exitMulDiv方法。那么中间要做什么工作呢?当然是处理表达式(20 / x * 43)了。由于这个表达式中有一个变量x,所以在扫描到x时,需要搜索该变量是否存在,如果存在,需要提取该变量的值。也就是说,在第一次调用enterMulDiv方法时还没有处理这个变量x,如果在enterMulDiv方法中要计算整个表达式的值显然是不可能的(因为x的值还没有确定),所以正确的做法应该是在exitMulDiv方法中计算整个表达式的值,因为在该方法被调用时,整个表达式的每一个子表达式的值都已经计算完了。

enterXxx和exitXxx方法也经常被用于处理作用域,例如,在扫描到下面的函数时, 在该函数对应的enterXxx方法中会将当前作用域切换到myfun函数(通常用Stack处理),而在exitXxx方法中,会恢复myfun函数的parent作用域。类、条件语句、循环语句也同样涉及到作用域的问题。关于作用域的问题,在后面的文章中会详细介绍作用域的实现方法。


  
  1. void myfun() {
  2. }

从前面的介绍可知,Listener比Visitor更灵活,Listener也是我推荐的遍历AST的方式,后面的文章也基本上使用Listener的方式实现编译器。

2. Listener对应的接口和基类

现在回到本文的主题上来,本文的目的是使用Listener的方式取代Visitor的方式实现计算器。在编译Calc.g4时,除了生成CalcVisitor.java和CalcBaseVisitor.java,还生成了另外两个文件:CalcListener.java和CalcBaseListener.java。其中CalcListener.java文件是Listener的接口文件,接口中的方法会根据Calc.g4文件中的产生式生成,该文件的代码如下:


  
  1. import org.antlr.v4.runtime.tree.ParseTreeListener;
  2. public interface CalcListener extends ParseTreeListener {
  3. void enterProg(CalcParser.ProgContext ctx);
  4. void exitProg(CalcParser.ProgContext ctx);
  5. void enterPrintExpr(CalcParser.PrintExprContext ctx);
  6. void exitPrintExpr(CalcParser.PrintExprContext ctx);
  7. void enterAssign(CalcParser.AssignContext ctx);
  8. void exitAssign(CalcParser.AssignContext ctx);
  9. void enterBlank(CalcParser.BlankContext ctx);
  10. void exitBlank(CalcParser.BlankContext ctx);
  11. void enterParens(CalcParser.ParensContext ctx);
  12. void exitParens(CalcParser.ParensContext ctx);
  13. void enterMulDiv(CalcParser.MulDivContext ctx);
  14. void exitMulDiv(CalcParser.MulDivContext ctx);
  15. void enterAddSub(CalcParser.AddSubContext ctx);
  16. void exitAddSub(CalcParser.AddSubContext ctx);
  17. void enterId(CalcParser.IdContext ctx);
  18. void exitId(CalcParser.IdContext ctx);
  19. void enterInt(CalcParser.IntContext ctx);
  20. void exitInt(CalcParser.IntContext ctx);
  21. }

通常来讲,并不需要实现CalcListener接口中的所有方法,所以antlr还为我们生成了一个默认实现类CalcBaseListener,该类位于CalcBaseListener.java文件中。CalcListener接口的每一个方法都在CalcBaseListener类中提供了一个空实现,所以使用Listener方式遍历AST,只需要从CalcBaseListener类继承,并且覆盖必要的方法即可。

3. 用Listener方式实现可计算器

现在创建一个MyCalcParser.java文件,并在该文件中编写一个名为MyCalcParser的空类,代码如下:


  
  1. public class MyCalcParser extends CalcBaseListener{
  2. ... ...
  3. }

现在的问题是,在MyCalcParser类中到底要覆盖CalcBaseListener中的哪一个方法,而且如何实现这些方法呢?

要回答这个问题,就要先分析一下上一篇文章中编写的EvalVisitor类的代码了。其实在EvalVisitor中覆盖了哪一个动作对应的方法,在MyCalcParser类中也同样需要覆盖该动作对应的方法,区别只是使用enterXxx,还是使用exitXxx,或是都使用。

现在将EvalVisitor类的关键点提出来:

(1) 在EvalVisitor类中有一个名为memory的Map对象,用来保存变量的值,这在Listener中同样需要;

(2)在EvalVisitor类中有一个error变量,用来标识分析的过程中是否有错误,在Listener中同样需要;

(3)每一个visitXxx方法都有返回值,其实这个返回值是向上一层节点传递的值。而Listener中的方法并没有返回值,但仍然需要将值向上一层节点传递,所以需要想其他的方式实现向上传值;

那么为什么要向上传值呢?先来举一个例子,看下面的表达式:

4 * 5

这是一个乘法表达式,编译器对这个表达式扫描时,会先识别两个整数(4和5),这两个整数是两个原子表达式。如果使用Listener的方式,需要在这两个整数对应的enterInt方法(exitInt方法也可以)中将'4'和'5'转换为整数,这是因为不管值是什么类型,编译器读上来的都是字符串,所以需要进行类型转换。

包含4和5的表达式是MulDiv,对应的动作方法是exitMulDiv(不能用enterMulDiv,因为这时4和5还没有扫描到)。在exitMulDiv方法中要获取乘号(*)左右两个操作数的值(ctx.expr(0)和ctx.expr(1))。而这两个操作数的值在enterInt方法中已经获取了,我们要做的只是将获取的值传递给上一层表达式,也就是MulDiv表达式。向上一层传值的方法很多,这里采用一个我非常推荐的方式,通过用一个Map对象保存所有需要传递的值,key就是上一层节点的ParseTree对象(每一个enterXxx和exitXxx方法的ctx参数的类型都实现了ParseTree接口),而value则是待传递的值,可以使用下面的方式定义这个Map对象。

 private Map<ParseTree,Integer> values = new HashMap<>();

同时还需要两个方法来设置和获取值,分别是setValue和getValue,代码如下:


  
  1. public void setValue(ParseTree node, int value) {
  2. values.put(node,value);
  3. }
  4. public int getValue(ParseTree node) {
  5. try {
  6. return values.get(node);
  7. } catch (Exception e) {
  8. return 0;
  9. }
  10. }

下面给出MyCalcParser类的完整代码:


  
  1. import org.antlr.v4.runtime.tree.ParseTree;
  2. import java.util.HashMap;
  3. import java.util.Map;
  4. public class MyCalcParser extends CalcBaseListener{
  5. private Map<ParseTree,Integer> values = new HashMap<>(); // 用于保存向上一层节点传递的值
  6. Map<String, Integer> memory = new HashMap<String, Integer>(); // 用于保存变量的值
  7. boolean error = false; // 用于标识分析的过程是否出错
  8. // 设置值
  9. public void setValue(ParseTree node, int value) {
  10. values.put(node,value);
  11. }
  12. // 获取值
  13. public int getValue(ParseTree node) {
  14. try {
  15. return values.get(node);
  16. } catch (Exception e) {
  17. return 0;
  18. }
  19. }
  20. @Override public void enterPrintExpr(CalcParser.PrintExprContext ctx) {
  21. // 当开始处理表达式时,默认没有错误
  22. error = false;
  23. }
  24. @Override public void exitPrintExpr(CalcParser.PrintExprContext ctx) {
  25. if(!error) {
  26. // 只有在没有错误的情况下,才会输出表达式的值
  27. System.out.println(getValue(ctx.expr()));
  28. }
  29. }
  30. // 必须要放在exitAssign里
  31. @Override public void exitAssign(CalcParser.AssignContext ctx) {
  32. String id = ctx.ID().getText(); // 获取变量名
  33. int value = getValue(ctx.expr()); // 获取右侧表达式的值
  34. memory.put(id, value); // 保存变量
  35. }
  36. // 必须在exitParens中完成
  37. @Override public void exitParens(CalcParser.ParensContext ctx) {
  38. setValue(ctx,getValue(ctx.expr()));
  39. }
  40. // 计算乘法和除法(必须在exitMulDiv中完成)
  41. @Override public void exitMulDiv(CalcParser.MulDivContext ctx) {
  42. int left = getValue(ctx.expr( 0)); // 获取左操作数的值
  43. int right = getValue(ctx.expr( 1)); // 获取右操作数的值
  44. if ( ctx.op.getType() == CalcParser.MUL )
  45. setValue(ctx,left * right); // 向上传递值
  46. else
  47. setValue(ctx,left / right); // 向上传递值
  48. }
  49. // 计算加法和减法(必须在exitAddSub中完成)
  50. @Override public void exitAddSub(CalcParser.AddSubContext ctx) {
  51. int left = getValue(ctx.expr( 0)); // 获取左操作数的值
  52. int right = getValue(ctx.expr( 1)); // 获取右操作数的值
  53. if ( ctx.op.getType() == CalcParser.ADD )
  54. setValue(ctx,left + right);
  55. else
  56. setValue(ctx,left - right);
  57. }
  58. // 在enterId方法中也可以
  59. @Override public void exitId(CalcParser.IdContext ctx) {
  60. String id = ctx.ID().getText();
  61. if ( memory.containsKey(id) ) {
  62. setValue(ctx,memory.get(id)); // 将变量的值向上传递
  63. } else {
  64. // 变量不存在,输出错误信息(包括行和列),
  65. System.err.println(String.format( "行:%d, 列:%d, 变量<%s> 不存在!",ctx.getStart().getLine(),ctx.getStart().getCharPositionInLine() + 1, id));
  66. error = true;
  67. }
  68. }
  69. // 处理int类型的值
  70. @Override public void enterInt(CalcParser.IntContext ctx) {
  71. int value = Integer.valueOf(ctx.getText());
  72. setValue(ctx, value); // 将整数值向上传递
  73. }
  74. }

现在编写用于遍历AST和计算结果的MarvelListenerCalc类,代码如下:


  
  1. import org.antlr.v4.runtime.CharStream;
  2. import org.antlr.v4.runtime.CharStreams;
  3. import org.antlr.v4.runtime.CommonTokenStream;
  4. import org.antlr.v4.runtime.tree.ParseTree;
  5. import org.antlr.v4.runtime.tree.ParseTreeWalker;
  6. import java.io.FileInputStream;
  7. import java.io.InputStream;
  8. public class MarvelListenerCalc {
  9. public static void main(String[] args) throws Exception {
  10. String inputFile = null;
  11. if ( args.length> 0 ) {
  12. inputFile = args[ 0];
  13. } else {
  14. System.out.println( "语法格式:MarvelCalc inputfile");
  15. return;
  16. }
  17. InputStream is = System.in;
  18. if ( inputFile!= null ) is = new FileInputStream(inputFile);
  19. CharStream input = CharStreams.fromStream(is);
  20. // 创建词法分析器
  21. CalcLexer lexer = new CalcLexer(input);
  22. CommonTokenStream tokens = new CommonTokenStream(lexer);
  23. //
  24. CalcParser parser = new CalcParser(tokens);
  25. ParseTree tree = parser.prog();
  26. MyCalcParser calc = new MyCalcParser();
  27. ParseTreeWalker walker = new ParseTreeWalker();
  28. // 开始遍历AST
  29. walker.walk(calc, tree);
  30. }
  31. }

我们仍然使用上一篇文章使用的测试用例:


  
  1. 1+ 3 * 4 - 12 / 6;
  2. x = 40;
  3. y = 13;
  4. x * y + 20 - 42/ 6;
  5. z = 12;
  6. 4;
  7. x + 41 * z - y;

运行MarvelListenerCalc的执行结果如下图所示:

本文实现的程序还支持错误捕捉,例如,将最后一个表达式的变量x改成xx,再执行程序,就会抛出异常,出错的表达式没有输出任何值,异常会指示出错的位置(行和列),如下图所示:

 

 

 


转载:https://blog.csdn.net/nokiaguy/article/details/108130621
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场