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中导入依赖,其中版本最好选择最新版本

    <dependency>
     <groupId>org.antlr</groupId>
     <artifactId>antlr4-runtime</artifactId>
     <version>4.7.2</version>
    </dependency>
    
  • 接着,在项目中创建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:   <assoc=right> 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
      • 默认的左结合,可以用 <assoc=right> 指定右结合
      • 可以处理直接的左递归,不能处理间接的左递归
      • 如果用 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<String> resultSet = new HashSet<>();
          HashSet<String> appSet = new HashSet<>();
          StringBuilder condition = new StringBuilder();
          boolean Sign = false;
          public ParseTreeProperty<String> 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345