• 用antlr4来实现《按编译原理的思路设计的一个计算器》中的计算器


    上次在公司内部讲《词法分析——使用正则文法》是一次失败的尝试——上午有十几个人在场,下午就只来了四个听众。

    本来我还在构思如何来讲“语法分析”的知识呢,但现在看来已不太可能。

    这个课程没有预想中的受欢迎,其原因可能是:

    1.课程内容相对复杂,听众知识背景与基础差异比较大。

    2.授课技巧不够,不能把复杂的知识简单化的呈现给基础稍差一点的人。

    针对这两个可能的原因,我要尝试做出以下调整:

    1.使用antlr来实现词法和语法的部分。

    2.暂时把“编译”过程改为“解释”来实现。

    使用antlr的原因是:

    1.采用文法生成器可直接略过词法和语法的部分直接进入语义分析,这样利于速成,同时避免学员被词法分析和语法分析的复杂性吓到,而失去了继续学习的勇气。

    2.antlr的文法是LL(k)型,非常易于编写——虽然k型方法的性能肯定不如1型文法,但与初学者谈性能问题并不是一个好主意,不如直接避开性能不谈,能运行即可。

    3.antlr默认生成的是java代码,这与公司内大多数员工的现有知识是相吻合的。

     下面进入正文。

    一、什么是antlr?如何安装?

    这不是一篇凑字数的文章,所以请直接参考官方网站(http://www.antlr.org/)。

    我使用的是目前的最新版本(V4.2.2).

    我上传了参考资料(包括jar包、电子书和官方示例)到百度云上,可从这个地址下载(http://pan.baidu.com/s/1hq65XWC)。

    二、本计算器的文法示例及文法的解释。

    整个计算器的词法的语法就由以下几行的antlr4代码来实现,先贴在下面:

    grammar Calc;                            // 文法的名字为Calc
    
    // 以下以小写字母开头的文法表示为语法元素
    // 由大写字母开头的文法表示为词法元素
    // 词法元素的表示类似于正则表示式
    // 语法元素的表示类似于BNF
    
    exprs : setExpr                            // set表达式
        | calcExpr                            // 或calc表达式
        ;
    
    setExpr : 'set' agmts ;                    // 以set命令开头,后面是多个赋值语句
    agmts   : agmt (';' agmts)? ';'? ;        // 多个赋值语句是由一个赋值语句后根着多个赋值语句,中间由分号分隔,结尾有一个可选的分号
    agmt    : id=ID '=' num=NUMBER ;        // 一个赋值语句是由一个ID,后跟着一个等号,再后面跟送一个数字组成
    calcExpr: 'calc' expr ;                    // 以calc命令开头,后面是一个计算表达式
    
    // expr可能由多个产生式
    // 在前面的产生式优先于在后面的产生式
    // 这样来解决优先级的问题
    
    expr: expr op=(MUL | DIV) expr            // 乘法或除法
        | expr op=(ADD | SUB) expr            // 加法或减法
        | factor                            // 一个计算因子——可做为+-*/的操作数据的东西
        ;
    
    factor: (sign=(ADD | SUB))? num=NUMBER    // 计算因子可以是一个正数或负数
        | '(' expr ')'                        // 计算因子可以是括号括起来的表示式
        | id=ID                                // 计算因子可以是一个变量
        | funCall                            // 计算因子可以是一个函数调用
        ;
    
    funCall: name=ID '(' params ')' ;        // 函数名后面加参数列表
    params : expr (',' params)? ;            // 参数列表是由一个表达式后面跟关一个可选的参数列表组成
    
    WS : [ \t\n\r]+ -> skip ;                // 空白, 后面的->skip表示antlr4在分析语言的文本时,符合这个规则的词法将被无视
    ID : [a-z]+ ;                            // 标识符,由0到多个小写字母组成
    NUMBER : [0-9]+('.'([0-9]+)?)? ;        // 数字
    ADD : '+' ;
    SUB : '-' ;
    MUL : '*' ;
    DIV : '/' ;

    我们把这段文法保存到一个文件Calc.g4中,并运行命令“antlr4 -visitor Calc.g4”即生成6个java文件和两个tokens文件。

    这几个文件包括了这个计算器的“词法分析程序”、“语法分析程序”和一个visitor(CalcBaseVisitor.java),不过此时这个visitor内部实现都是空的,我们需要自己实现它。

    在实现这个visitor之前,我们先实现一个上下文,上下文的做用有两个:

    1.保存变量——用于在计算表达式中引用变量。

    2.保存堆栈——用于函数的参数传递。

    这个上下文的内容很少,代码也很短,直接贴在下面:

     1 public class Context {
     2     private static Context ourInstance = new Context();
     3 
     4     public static Context getInstance() {
     5         return ourInstance;
     6     }
     7 
     8     private Context() {
     9     }
    10 
    11     private Map<String, Double> map = new HashMap<>();
    12     private Deque<Double> stack = new ArrayDeque<>();
    13 
    14     public Double getValue(String key) {
    15         Double d = map.get(key);
    16         return d == null ? Double.NaN : d;
    17     }
    18 
    19     public void setContext(String key, Double value) {
    20         map.put(key, value);
    21     }
    22 
    23     public void setContext(String key, String value) {
    24         setContext(key, Double.valueOf(value));
    25     }
    26 
    27     public void pushStack(Double d) {
    28         stack.push(d);
    29     }
    30 
    31     public Double popStack() {
    32         return stack.pop();
    33     }
    34 }

    下面我们开始实现这个计算器的visitor,

     1 public class MyCalcVisitor extends CalcBaseVisitor<Double> {
     2 
     3     @Override
     4     public Double visitExprs(CalcParser.ExprsContext ctx) {
     5         return visit(ctx.getChild(0));
     6     }
     7 
     8     @Override
     9     public Double visitAgmt(CalcParser.AgmtContext ctx) {
    10         Context.getInstance().setContext(ctx.id.getText(), ctx.num.getText());
    11         return null;
    12     }
    13 
    14     @Override
    15     public Double visitAgmts(CalcParser.AgmtsContext ctx) {
    16         visit(ctx.agmt());
    17         if (ctx.agmts() != null)
    18             visit(ctx.agmts());
    19         return null;
    20     }
    21 
    22     @Override
    23     public Double visitCalcExpr(CalcParser.CalcExprContext ctx) {
    24         return visit(ctx.expr());
    25     }
    26 
    27     @Override
    28     public Double visitExpr(CalcParser.ExprContext ctx) {
    29         int cc = ctx.getChildCount();
    30         if (cc == 3) {
    31             switch (ctx.op.getType()) {
    32             case CalcParser.ADD:
    33                 return visit(ctx.expr(0)) + visit(ctx.expr(1));
    34             case CalcParser.SUB:
    35                 return visit(ctx.expr(0)) - visit(ctx.expr(1));
    36             case CalcParser.MUL:
    37                 return visit(ctx.expr(0)) * visit(ctx.expr(1));
    38             case CalcParser.DIV:
    39                 return visit(ctx.expr(0)) / visit(ctx.expr(1));
    40             }
    41         } else if (cc == 1) {
    42             return visit(ctx.getChild(0));
    43         }
    44         throw new RuntimeException();
    45     }
    46 
    47     @Override
    48     public Double visitFactor(CalcParser.FactorContext ctx) {
    49         int cc = ctx.getChildCount();
    50         if (cc == 3) {
    51             return visit(ctx.getChild(1));
    52         } else if (cc == 2) {
    53             if (ctx.sign.getType() == CalcParser.ADD)
    54                 return Double.valueOf(ctx.getChild(1).getText());
    55             if (ctx.sign.getType() == CalcParser.SUB)
    56                 return -1 * Double.valueOf(ctx.getChild(1).getText());
    57         } else if (cc == 1) {
    58             if (ctx.num != null)
    59                 return Double.valueOf(ctx.getChild(0).getText());
    60             if (ctx.id != null)
    61                 return Context.getInstance().getValue(ctx.id.getText());
    62             return visit(ctx.funCall());
    63         }
    64         throw new RuntimeException();
    65     }
    66 
    67     @Override
    68     public Double visitParams(CalcParser.ParamsContext ctx) {
    69         if (ctx.params() != null)
    70             visit(ctx.params());
    71         Context.getInstance().pushStack(visit(ctx.expr()));
    72         return null;
    73     }
    74 
    75     @Override
    76     public Double visitFunCall(CalcParser.FunCallContext ctx) {
    77         visit(ctx.params());
    78         String funName = ctx.name.getText();
    79         switch (funName) {
    80         case "pow":
    81             return Math.pow(Context.getInstance().popStack(), Context.getInstance().popStack());
    82         case "sqrt":
    83             return Math.sqrt(Context.getInstance().popStack());
    84         }
    85         throw new RuntimeException();
    86     }
    87 
    88     @Override
    89     public Double visitSetExpr(CalcParser.SetExprContext ctx) {
    90         return visit(ctx.agmts());
    91     }
    92 
    93 }

     最后再实现一个入口,调用这个Visitor即完成了我们的计算器。

    入口代码如下:

     1 import java.util.Scanner;
     2 
     3 import org.antlr.v4.runtime.ANTLRInputStream;
     4 import org.antlr.v4.runtime.CommonTokenStream;
     5 import org.antlr.v4.runtime.tree.ParseTree;
     6 
     7 public class Portal {
     8 
     9     private static final String lineStart = "CALC> ";
    10 
    11     public static void main(String[] args) {
    12         try (Scanner scanner = new Scanner(System.in)) {
    13             System.out.print(lineStart);
    14             while (scanner.hasNext()) {
    15                 String line = scanner.nextLine();
    16                 if (line != null) {
    17                     line = line.trim();
    18                     if (line.length() != 0) {
    19                         if ("exit".equals(line) || "bye".equals(line))
    20                             break;
    21                         ANTLRInputStream input = new ANTLRInputStream(line);
    22                         CalcLexer lexer = new CalcLexer(input);
    23                         CommonTokenStream tokens = new CommonTokenStream(lexer);
    24                         CalcParser parser = new CalcParser(tokens);
    25                         ParseTree tree = parser.exprs();
    26                         MyCalcVisitor mv = new MyCalcVisitor();
    27                         Double res = mv.visit(tree);
    28                         if (res != null)
    29                             System.out.println(res);
    30                     }
    31                 }
    32 
    33                 System.out.print(lineStart);
    34             }
    35         }
    36     }
    37 
    38 }

    整个计算器只写了一个文法和三个类,所有代码都贴在上面了,相对于完全自己手写的计算器来说,的确是简单很多了。

    -------------------------------------------------------------------

    周游[http://www.cnblogs.com/naturemickey]版权所有,未经许可请勿转载

  • 相关阅读:
    DSP builder安装指南(以9.1为例)
    浅谈FPGA电脑
    Altera 在线资源使用
    Altium Designer 发现的机密——摘自CRAZY BINGO
    dom4j 练习
    java 工程和类等路径汇总
    读取xml文件基于xpath
    简化 XML 读写
    Java GUI 开发专题
    java中路径示例
  • 原文地址:https://www.cnblogs.com/naturemickey/p/3764108.html
Copyright © 2020-2023  润新知