ANTLR4实践笔记

最近科研项目需要加入一些新东西,需要我构建一个程序信息库,然后就想设计一套类似sql的查询语言去查询库里包含的内容,因此就需要用到语法分析器生成工具了。之前编译原理课上用过flex+bison那一套,做毕设的时候也用过JavaCC,这次开始动手之前在github上搜了一下“sqlParser”的类似Java项目,发现很多项目都是使用ANTLR4来生成语法分析器的(如下图),于是我这次就尝试使用它来进行实现。


github.png

ANTLR4是什么?

ANTLR 是 ANother Tool for Language Recognition 的缩写,官网:http://www.antlr.org/

它是一款强大的语法分析器生成工具,可用于读取、处理、执行和翻译结构化的文本或二进制文件。

而且开发过程也比较简单,一般开发流程如下:

  1. 定义 .g4 语法文件;
  2. 使用 ANTLR 4 生成词法分析器(Lexer)和语法分析器(Parser)目标编程语言代码,支持的编程语言:Java、JavaScript、Python、C 和 C++ 等;
  3. 遍历 AST(Abstract Syntax Tree 抽象语法树),ANTLR 4 支持两种模式:访问者模式(Visitor)和监听器模式(Listener)。

ANTLR4的IDEA开发

ANTLR4可以在多种环境中运行,下面主要介绍在IDEA里如何使用ANTLR4:

  • 环境配置

    • 首先,安装ANTLR4插件
    setting.png
  • 之后,创建一个maven项目,在pom.xml中导入依赖,其中版本最好选择最新版本

    
     org.antlr
     antlr4-runtime
     4.7.2
    
    
  • 接着,在项目中创建g4语法文件

mulu.png
  • 最后,点击Configure ANTLR添加生成配置

    config.png
  • 设置生成文件的目录和package名

config1.png
  • 点击Generate ANTLR Recongnizer后,即可生成语法分析器
generate.png
  • 生成后文件目录

    calc.png
  • 编写语法规则g4文件

    • 按照Antlr4规范编写特定语言的语法规则文件(绝大部分语言的都已提供,详见语法库);

    • 这里是官方提供的一个简单计算器的例子:

      grammar Calc;
      
      // parser
      prog: stmt;
      
      stmt:   expr                            # printExpr
          |   ID '=' expr                     # assign
          |   NEWLINE                         # blank
          ;
      
      expr:    expr op='^' expr  # pow
          |   expr op=('*'|'/') expr          # mulDiv
          |   expr op=('+'|'-') expr          # addSub
          |   NUM                             # int
          |   ID                              # id
          |   '(' expr ')'                    # parens
          ;
      
      // lexer
      MUL : '*';
      DIV : '/';
      ADD : '+';
      SUB : '-';
      ID  : Letter LetterOrDigit*;
      NUM : Number;
      fragment Letter: [a-zA-Z_];
      fragment Digit : ('0' .. '9') +;
      fragment Number: ('0' .. '9') + ('.' ('0' .. '9') +)?;
      fragment LetterOrDigit: Letter | Digit;
      NEWLINE: '\r'? '\n';
      WS  : [ \t]+ -> skip;
      
    • 语法规则和编译原理上描述的类似,一些主要的说明如下:

      • grammar 名称和文件名要一致
      • Parser 规则(即 non-terminal)以小写字母开始
      • Lexer 规则(即 terminal)以大写字母开始
      • 所有的 Lexer 规则无论写在哪里都会被重排到 Parser 规则之后
      • 所有规则中若有冲突,先出现的规则优先匹配
      • 'string' 单引号引出字符串
      • | 用于分隔两个产生式,(a|b) 括号用于指定子产生式,?+*用法同正则表达式
      • 在产生式后面 # label 可以给某条产生式命名,在生成的代码中即可根据标签分辨不同产生式
      • 不需要指定开始符号
      • 规则以分号终结
      • /* block comment */ 以及 // line comment
      • 默认的左结合,可以用 指定右结合
      • 可以处理直接的左递归,不能处理间接的左递归
      • 如果用 MUL: '*'; 指定了某个字符串的名字,在程序里面就能用这个名字了
      • fragment 可以给 Lexer 规则中的公共部分命名
  • 调用语法分析器

    • 创建一个main方法调用分析器

      public class Main {
      
          public static void run(String expr) throws Exception {
      
              //对每一个输入的字符串,构造一个 CodePointCharStream
              CodePointCharStream cpcs = CharStreams.fromString(expr);
      
              //用 cpcs 构造词法分析器 lexer,词法分析的作用是产生记号
              AntlrTestLexer lexer = new AntlrTestLexer(cpcs);
      
              //用词法分析器 lexer 构造一个记号流 tokens
              CommonTokenStream tokens = new CommonTokenStream(lexer);
      
              //再使用 tokens 构造语法分析器 parser,至此已经完成词法分析和语法分析的准备工作
              AntlrTestParser parser = new AntlrTestParser(tokens);
      
              //最终调用语法分析器的规则 prog,完成对表达式的验证
              ParseTree tree = parser.prog();
          }
      
          public static void main(String[] args) throws Exception {
              run("a+b-3");
          }
      }
      
    • 其中parser.prog()可以检验输入的语句是否符合我们定义的语法,如果不符合会相应报错

  • 遍历语法树

    • 在确定编写的语法文件没有问题之后,我们就可以遍历语法分析器构建的抽象语法书,对输入的语句进行语义的分析或计算

    • ANTLR4中提供了两种遍历模式:Listener和Visitor,他们的区别主要在:

      • Listener模式会由ANTLR提供的walker对象自动调用,而Visitor模式则必须通过显式的访问调用遍历其子级,如果 忘记在节点的子节点上调用visit(),意味着这些子树不会被访问;
      • Listener模式不能返回值,而Visitor模式可以返回任何自定义类型。 因此,Listener模式就只能用一些变量来存储中间值,而Visitor可以直接返回计算值;
      • Listener模式使用分配在堆上的显式堆栈,而Visitor模式使用调用堆栈来管理树遍历,在深度嵌套的AST上使用访客时,这可能会导致StackOverFlow异常。
    • 我目前只尝试了Listener的方法,实现如下:

      • 先在上面的run()方法最后加上遍历代码

        ParseTreeWalker walker = new ParseTreeWalker();
        QueryListenerImp evalByListener = new QueryListenerImp();
        walker.walk(evalByListener, tree);
        
      • 然后实现一个QueryListenerImp继承自QueryBaseListener

        public class QueryListenerImp extends QueryBaseListener {
        
      • 然后充血其中的方法,例如下面代码,在访问Expr节点结束后,输出节点内容

        @Override
        public void exitExpr(QueryParser.ExprContext ctx) {
            System.out.println(ctx.getText());
        }
        
    • 之后,就可以根据需求遍历语法树,实现自己想要的需求。

  • 最后附上我设计的查询语言的g4文件和遍历Listener文件

    • Query.g4:

      grammar Query;
      
      // parser
      query : expr;
      
      expr : SELECT result FROM repo (WHERE orCondition SEMI)?;
      
      result : resultType (COMMA resultType)*;
      
      resultType : 'class'
          | 'method'
          | 'package'
          | 'dependenceGraph'
          | 'controlFlowFraph'
          | 'callGraph';
      
      repo : app (COMMA app)*;
      
      app : '[' ID ']';
      
      
      // TODO 改为与或表达式
      orCondition : andCondition (operator=OR andCondition)*
          ;
      
      andCondition : conditionExpr (operator=AND conditionExpr)*
          ;
      
      conditionExpr : conditionLeft op=('=' | '!=') conditionRight;
      
      conditionLeft : app DOT type DOT attr;
      
      conditionRight : STRING;
      
      type: 'package'
          | 'class'
          | 'method';
      
      attr : 'name'
          | 'type'
          | 'visibility'
          | 'isAbstract';
      
      // keyword
      SELECT : 'select';
      FROM : 'from';
      WHERE : 'where';
      AND : 'and';
      OR : 'or';
      NOT : 'not';
      COMMA : ',';
      SEMI : ';';
      DOT : '.';
      
      // lexer
      ID  : Letter LetterOrDigit*;
      STRING
          : '\'' ( ~('\''|'\\') | ('\\' .) )* '\''
          | '"' ( ~('"'|'\\') | ('\\' .) )* '"'
          ;
      fragment Letter: [a-zA-Z_];
      fragment Digit : ('0' .. '9') +;
      fragment LetterOrDigit: Letter | Digit;
      
      WS : [ \t\n\r]+ -> skip ;
      
    • QueryListenerImp.java:

      package modelQuery.parser;
      
      import org.antlr.v4.runtime.tree.ParseTreeProperty;
      
      import java.util.HashSet;
      
      public class QueryListenerImp extends QueryBaseListener {
          HashSet resultSet = new HashSet<>();
          HashSet appSet = new HashSet<>();
          StringBuilder condition = new StringBuilder();
          boolean Sign = false;
          public ParseTreeProperty values = new ParseTreeProperty<>();
      
          @Override
          public void exitExpr(QueryParser.ExprContext ctx) {
              if(Sign)
                  return;
              else {
                  System.out.println("resultSet: " + resultSet);
                  System.out.println("appSet: " + appSet);
                  System.out.println("condition: " + values.get(ctx.orCondition()));
      
                  // 后面调用XQuery查询xml信息库中内容
              }
          }
      
          @Override
          public void exitResultType(QueryParser.ResultTypeContext ctx) {
              resultSet.add(ctx.getText());
              System.out.println("exitResultType: " + ctx.getText());
          }
      
          @Override
          public void exitRepo(QueryParser.RepoContext ctx) {
              for(QueryParser.AppContext appCtx : ctx.app()) {
                  appSet.add(appCtx.getText());
              }
          }
      
          @Override
          public void exitOrCondition(QueryParser.OrConditionContext ctx) {
              if(ctx.operator != null){
                  values.put(ctx, values.get(ctx.andCondition(0)) + " or " + values.get(ctx.andCondition(1)));
              } else {
      
              }
              values.put(ctx, values.get(ctx.andCondition(0)));
          }
      
          @Override
          public void exitAndCondition(QueryParser.AndConditionContext ctx) {
              if(ctx.operator != null) {
                  values.put(ctx, values.get(ctx.conditionExpr(0)) + " and " + values.get(ctx.conditionExpr(1)));
              } else {
                  values.put(ctx, values.get(ctx.conditionExpr(0)));
              }
          }
      
          @Override
          public void exitConditionExpr(QueryParser.ConditionExprContext ctx) {
              String left = values.get(ctx.conditionLeft());
              values.put(ctx, left + ctx.op.getText() + ctx.conditionRight().getText());
          }
      
          @Override
          public void exitConditionLeft(QueryParser.ConditionLeftContext ctx) {
              String app = ctx.app().getText();
              if(!appSet.contains(app)) {
                  System.err.println(ctx.start.getLine() + ":" + ctx.start.getStartIndex() + ", variable " + ctx.app().getText() + " not defined");
                  Sign = true;
                  return;
              } else {
                  String type = ctx.type().getText();
                  String attr = ctx.attr().getText();
                  values.put(ctx, "$x/" + type + "/@" + attr);
              }
          }
      }
      
    • 最后可以测试如下语句:

      select class,method, package from [aa],[bb] 
      where [app].method.name = "yyf" 
      and [app].class.name = "www";
      
    • 利用IDEA插件可以看到解析的语法树:


      test.png

你可能感兴趣的:(ANTLR4实践笔记)