Antlr学习笔记

Antlr学习笔记

一、引言

​ 什么是Antlr?引用官方的说明就是:

What is ANTLR?

ANTLR (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files. It’s widely used to build languages, tools, and frameworks. From a grammar, ANTLR generates a parser that can build and walk parse trees.

​ 从编译器的角度来看,Antlr可以用来帮助我们完成编译器前端所需要完成的一些工作:词法分析(Lexer Analysis)、语法分析(Syntax Analysis)、生成抽象语法树(Abstract Syntax Tree, AST)等等。语义分析(Semantic Analysis),例如类型检查,就需要我们自己来完成。

​ 要使用Antlr生成语言的词法分析器(Lexer)和语法分析器(Parser),我们需要告诉Antlr语言的文法(Grammar)。Antlr采用的是上下文无关文法(Context Free Grammar),使用类似BNF的符号集来描述。使用上下文无关文法的语言比较常用的Parser有两种,LL Parser和LR Parser,而Antlr帮我们生成的是前者。

二、Antlr安装

1.命令行安装

  • 安装好Java,在这里就不再赘述了

  • 下载Antlr的jar包——Antlr Download

  • 将antlr-4.7-complete.jar添加到系统环境变量的CLASSPATH中:

    D:\antlr-v4.7\antlr-4.7-complete.jar;

  • 为Antlr Tool和TestRig建立batch命令:

    • 为了方便管理,我就在antlr-4.7-complete.jar所在文件夹下建立一个bat文件夹

    • 在里面创建两个bat文件,一个叫antlr4.bat,内容如下:

    java org.antlr.v4.Tool %*

    另一个叫grun.bat,内容如下:

    java org.antlr.v4.runtime.misc.TestRig %*

    • 将这两个bat文件添加到系统环境变量的PATH中:

    Antlr学习笔记_第1张图片

  • 测试是否安装成功:

    • 打开CMD

    • 分别输入antlr4命令和grun命令,出现下图:
      Antlr学习笔记_第2张图片

2.IDEA插件安装

打开IDEA,File -> Settings -> Plugins -> 导航栏搜索Antlr -> Search in repositories -> Install

这样就安装完成了,方便快捷。

三、Antlr入门

  • 首先我们先给出一个能识别Hello World那样的简单语法:

    grammar Hello;    // 定义文法的名字
    s : 'hello' ID;   // 匹配关键字hello,后面跟着一个标志符
    ID : [a-z]+;  // 匹配小写字母标志符
    WS : [ \t\r\n]+ -> skip;  // 跳过空格、制表符、回车符和换行符
  • 将这个语法保存为Hello.g4,然后执行以下命令来生成识别器:

    antlr4 Hello.g4
  • Anltr会为我们在当前目录下生成6个文件:

    Hello.tokens  HelloLexer.java     HelloParser.java
    HelloLexer.tokens HelloBaseListener.java  HelloListener.java
  • 执行javac Hello*.java 将所有java文件编译。这样,我们已经有了一个可以被HelloParser和HelloLexer利用的可执行的识别器,只缺一个主程序去触发语言识别了。

  • ANTLR运行库有提供称之为TestRig的测试工具,可以让你不创建主程序就能测试语法。TestRig使用Java反射调用编译后的识别器,它能显示关于识别器如何匹配输入的大量信息。还记得我们之前创建的grun.bat文件吗?那就是用来调用TestRig的。

    现在我们来执行一下:

    grun Hello s -tokens

    接着输入以下内容:

    hello world   #输入后回车
    EOF   #Windows下按Ctrl+Z再按回车

    TestRig会打印出记号列表,每一行输出表示一个记号以及它的有关信息:

    [@0,0:4='hello',<1>,1:0]
    [@1,6:10='world',<2>,1:6]
    [@2,13:12='',<-1>,2:0]

    这里详细讲解下[@1,6:10=’world’,\<2>,1:6]的意义。@1表示记号索引(从0开始);6:10表示
    记号开始与结束的位置(从0开始);\<2>表示记号类型,具体数值和类型存储在后缀名为
    tokens的文件中;最后的1:6表示记号在第一行(从1开始),从第6个字符开始(从0开始,
    制表符作为单个字符计算)。

    以下是TestRig可用的所有参数:

    • -tokens 打印出记号流。
    • -tree 以LISP风格的文本形式打印出语法分析树。
    • -gui 在对话框中可视化地显示语法分析树。
    • -ps file.ps在PostScript中生成一个可视化的语法分析树表示,并把它存储在file.ps文件
    • -encoding encodingname 指定输入文件的编码。
    • -trace 在进入/退出规则前打印规则名字和当前的记号。
    • -diagnostics 分析时打开诊断消息。此生成消息仅用于异常情况,如二义性输入短语。
    • -SLL 使用更快但稍弱的分析策略。

四、实战

现在让我们来用Antlr来实现一个简单的四则运算计算器吧!

1.准备工作

  • 使用IDEA安装好Antlr插件,并创建一个Maven项目,在pom.xml下加入如下内容:

    
    <dependency>
    <groupId>org.antlrgroupId>
    <artifactId>antlr4-runtimeartifactId>
    <version>4.7version>
    dependency>
  • 创建Cal.g4文件,内容如下

    grammar Cal;
    
    prog: stat+;  //一个程序由至少一条语句组成
    
    /*为了以后的运算的方便性,我们需要给每一步规则打上标签,标签以”#”开头,出现在每一条规则的右边。打上标签后,antlr会为每一个规则都生成一个事件*/
    
    stat: ID '=' expr ';' #Assign //变量赋值语句
    | 'print' '(' expr ')' ';' #printExpr   //输出语句
    ;
    
    expr: expr op=('*'|'/') expr #MulDiv  //表达式可以是表达式之间乘除
      | expr op=('+'|'-') expr #AddSub  //表达式可以是表达式之间加减
      | NUM #NUM    //表达式可以是一个数字
      | ID #ID  //表达式可以是一个变脸
      | '(' expr ')' #parens    //表达式可以被括号括起来
      ;
    
    MUL:'*';
    DIV:'/';
    ADD:'+';
    SUB:'-';
    
    ID: [a-zA-Z][a-zA-Z0-9]*; //变量可以是数字和字母,但必须以字母开头
    
    //负数必须要用"()"括起来
    NUM: [0-9]+   //正整数
      | '(' '-' [0-9]+ ')'  //负整数
    | [0-9]+'.'[0-9]+   //正浮点数
    | '(' '-' [0-9]+'.'[0-9]+ ')'   //负浮点数
    ;
    
    WS: [ \t\r\n] -> skip;    //跳过空格、制表符、回车、换行
  • 在IDEA中配置文件,右击文件 -> Configure ANTLR…,如下图所示:

    Antlr学习笔记_第3张图片

    ​ (注意要勾选generate parse tree visitor,后来的程序要用到)

    配置完成后右键文件 -> Generate ANNTLR Recognizer

2.定义自己的Visitor

  • 我们先看一下刚才生成的两个Visitor:

    import org.antlr.v4.runtime.tree.ParseTreeVisitor;
    
    public interface CalVisitor<T> extends ParseTreeVisitor<T> {
    T visitProg(CalParser.ProgContext ctx);
    T visitAssign(CalParser.AssignContext ctx);
    T visitPrintExpr(CalParser.PrintExprContext ctx);
    T visitParens(CalParser.ParensContext ctx);
    T visitMulDiv(CalParser.MulDivContext ctx);
    T visitAddSub(CalParser.AddSubContext ctx);
    T visitNUM(CalParser.NUMContext ctx);
    T visitID(CalParser.IDContext ctx);
    }
    • 接口定义使用的是Java泛型,visit方法的返回值为参数化类型,这允许我们根据表达式计算返回值的类型去设定实现的泛型参数。
    • 看看我们这几个方法的名字,PrintExpr, Assign,Blank……有没有感觉很熟悉。没错这就是我们刚才在g4文件的时候为每一条规则的后面打上了tag。
    • 而另一个visitor,LabeledExprBaseVisitor.java则是对本接口的一个简单的实现,我们后续的操作需要继承LabeledExprBaseVisitor.java来实现。
  • Visitor的作用顾名思义就是对整个语法树进行遍历,然后进行相关操作。我们需要重写每一个方法来实现我们自己的遍历,在这之前,我们先声明一个map。用于存放变量与其对应的值,即符号表

    Map memory = new HashMap();

    在这里,我们选择统一使用DOUBLE来表示数字,在输出结果的时候再做进一步的转换

  • visitAssign():

    @Override
    public Double visitAssign(CalParser.AssignContext ctx) {
        //获取ID的名字
    String id=ctx.ID().getText();
        //调用expr()遍历子树,获取结果
    Double value=visit(ctx.expr());
        //赋值给ID,存放入符号表中
    table.put(id,value);
        //返回dummy value
    return null;
    }
  • visitPrint():

    @Override
    public Double visitPrintExpr(CalParser.PrintExprContext ctx) {
      Double value=visit(ctx.expr());
      String str=value.toString();
      int index=str.indexOf('.');
      //检查是否为整数,截取小数点后的字符串,与"0"比较
      if(str.substring(index+1).equals("0"))
            //是整数,只输出小数点之前的字符串
          System.out.println(str.substring(0,index));
      else
            //不是整数,直接输出
          System.out.println(str);
      return null;
    }
  • visitMulDiv():

    @Override
    public Double visitMulDiv(CalParser.MulDivContext ctx) {
      Double left=visit(ctx.expr(0));   //左值
      Double right=visit(ctx.expr(1));  //右值
      //如果逻辑错误,获取出错地方的行号和列数
      int line,column;
      if(ctx.op.getType()==CalParser.DIV){  //如果是除法
          if(right==0.0){
              line=ctx.expr(1).start.getLine();
              column=ctx.expr(1).start.getStartIndex();
                //除数为0,抛出异常
              try{
                  throw new CalException(line,column,"Divided by zero");
              }catch (CalException e){
                  System.out.println(e.toString());
              }
              return null;
          }else
              return left/right;
      }else
          return left*right;
    }

    CalException的定义如下:

    public class CalException extends Exception {
      private int line;
      private int column;
      private String msg;
    
      public CalException(int line,int column,String msg){
          this.line=line;
          this.column=column;
          this.msg=msg;
      }
    
      @Override
      public String toString() {
          return msg+",line:"+line+",column:"+column;
      }
    }
  • visitAddSub():

    @Override
    public Double visitAddSub(CalParser.AddSubContext ctx) {
      Double left=visit(ctx.expr(0));
      Double right=visit(ctx.expr(1));
      if(ctx.op.getType()==CalParser.ADD)
          return left+right;
      else
          return left-right;
    }

    以上这两个就是我们加减乘除的核心函数了

  • visitNum():

    @Override
    public Double visitNUM(CalParser.NUMContext ctx) {
      return Double.valueOf(ctx.getText()); //直接将字符串转为数字并返回
    }
  • visitID():

    @Override
    public Double visitID(CalParser.IDContext ctx) {
      String id=ctx.getText();
      int line,column;
        //符号表中存在这个变量并且已经赋值,直接返回
      if(table.containsKey(id))
          return table.get(id);
      else{
            //变量未初始化,抛出异常
          line=ctx.start.getLine();
          column=ctx.start.getStartIndex();
          try{
              throw new CalException(line,column,"Undefined variable:"+id);
          }catch (CalException e){
              System.out.println(e.toString());
          }
          return null;
      }
    }
  • visitParens():

    @Override
    public Double visitParens(CalParser.ParensContext ctx) {
      return visit(ctx.expr());
    }
  • 全部代码一览:

    import java.util.HashMap;
    import java.util.Map;
    
    public class EvalVisitor extends CalBaseVisitor<Double> {
      private Map table;
    
      public EvalVisitor(){
          table=new HashMap<>();
      }
    
      @Override
      public Double visitPrintExpr(CalParser.PrintExprContext ctx) {
          Double value=visit(ctx.expr());
          String str=value.toString();
          int index=str.indexOf('.');
          //check if it is an integer
          if(str.substring(index+1).equals("0"))
              System.out.println(str.substring(0,index));
          else
              System.out.println(str);
          return null;
      }
    
      @Override
      public Double visitAssign(CalParser.AssignContext ctx) {
          String id=ctx.ID().getText();
          Double value=visit(ctx.expr());
          table.put(id,value);
          return null;
      }
    
      @Override
      public Double visitMulDiv(CalParser.MulDivContext ctx) {
          Double left=visit(ctx.expr(0));
          Double right=visit(ctx.expr(1));
          //if divided by zero,get the position of zero
          int line,column;
          if(ctx.op.getType()==CalParser.DIV){
              if(right==0.0){
                  line=ctx.expr(1).start.getLine();
                  column=ctx.expr(1).start.getStartIndex();
                  try{
                      throw new CalException(line,column,"Divided by zero");
                  }catch (CalException e){
                      System.out.println(e.toString());
                  }
                  return null;
              }else
                  return left/right;
          }else
              return left*right;
      }
    
      @Override
      public Double visitAddSub(CalParser.AddSubContext ctx) {
          Double left=visit(ctx.expr(0));
          Double right=visit(ctx.expr(1));
          if(ctx.op.getType()==CalParser.ADD)
              return left+right;
          else
              return left-right;
      }
    
      @Override
      public Double visitNUM(CalParser.NUMContext ctx) {
          return Double.valueOf(ctx.getText());
      }
    
      @Override
      public Double visitID(CalParser.IDContext ctx) {
          String id=ctx.getText();
          int line,column;
          if(table.containsKey(id))
              return table.get(id);
          else{
              line=ctx.start.getLine();
              column=ctx.start.getStartIndex();
              try{
                  throw new CalException(line,column,"Undefined variable:"+id);
              }catch (CalException e){
                  System.out.println(e.toString());
              }
              return null;
          }
      }
    
      @Override
      public Double visitParens(CalParser.ParensContext ctx) {
          return visit(ctx.expr());
      }
    }

3.编写主程序:

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.ParseTree;

import java.io.FileInputStream;
import java.io.IOException;

public class MyCalculator {
    public static void main(String[] args) throws IOException {
        String file="test_case\\test.in";
        //创建输入文件流
        FileInputStream inputStream=new FileInputStream(file);
        //转化为字符流
        CharStream input= CharStreams.fromStream(inputStream);
        //创建词法分析器
        CalLexer lexer=new CalLexer(input);
        //获取Token集
        CommonTokenStream tokenStream=new CommonTokenStream(lexer);
        //创建语法分析器
        CalParser parser=new CalParser(tokenStream);
        //分析语法
        ParseTree tree=parser.prog();
        //遍历语法树,输出结果
        EvalVisitor visitor=new EvalVisitor();
        //当遍历树时如果出错,会返回空指针,在这里捕获
        try{
            visitor.visit(tree);
        }catch (NullPointerException e){
            System.out.println("oops, we have some problem");
        }
    }
}

样例输入:

a=(10.44*356+1.28)/2-1024*1.6;
b=a*2-a/2;
c123=a+b*b/5-(a-a*2)/b;
print(a);
print(b);
print(c123);
print(1+2-3*4/5);

输出结果:

220.55999999999995
330.8399999999999
22112.24778666666
0.6000000000000001

参考链接:antlr入门实例——计算器

你可能感兴趣的:(编译原理)