Antlr4 語法解析器(下)


Antlr4 的兩種AST遍歷方式:Visitor方式Listener方式

Antlr4規則文法:

  • 注釋:和Java的注釋完全一致,也可參考C的注釋,只是增加了JavaDoc類型的注釋;
  • 標志符:參考Java或者C的標志符命名規范,針對Lexer 部分的 Token 名的定義,采用全大寫字母的形式,對於parser rule命名,推薦首字母小寫的駝峰命名;
  • 不區分字符和字符串,都是用單引號引起來的,同時,雖然Antlr g4支持 Unicode編碼(即支持中文編碼),但是建議大家盡量還有英文;
  • Action,行為,主要有@header 和@members,用來定義一些需要生成到目標代碼中的行為,例如,可以通過@header設置生成的代碼的package信息,@members可以定義額外的一些變量到Antlr4語法文件中;
  • Antlr4語法中,支持的關鍵字有:import, fragment, lexer, parser, grammar, returns, locals, throws, catch, finally, mode, options, tokens

基於IDEA調試Antlr4語法規則(文法可視化)

基於IDEA調試Antlr4語法一般步驟:

1) 創建一個調試工程,並創建一個g4文件

這里,我自己測試用Java開發,所以創建的是一個Maven工程,g4文件放在了src/main/resources 目錄下,取名 Test.g4

2)寫一個簡單的語法結構

這里我們參考寫一個加減乘除操作的表達式,然后在賦值操作對應的Rule上右鍵,可選擇測試:

grammar Test;

@header {
    package com.chaplinthink.antlr;
}

stmt : expr;

expr : expr NUL expr    # Mul
     | expr ADD expr    # Add
     | expr DIV expr    # Div
     | expr MIN expr    # Min
     | INT              # Int
     ;

NUL : '*';
ADD : '+';
DIV : '/';
MIN : '-';

INT : Digit+;
Digit : [0-9];

WS : [ \t\u000C\r\n]+ -> skip;

SHEBANG : '#' '!' ~('\n'|'\r')* -> channel(HIDDEN);

看我們 3/ 4 是可以識別出來的 語法中 channel(HIDDEN) (代表隱藏通道) 中的 Token,不會被語法解析階段處理,但是可以通過Token遍歷獲取到。

Antlr4生成並遍歷AST

1. 通過命令行如上篇文章

java -jar antlr-4.7.2--complete.jar -Dlanguage=Python3 -visitor Test.g4

這樣就可以生成Python3 target的源碼,如果不希望生成Listener,可以添加參數 -no-listener

2. Maven Antlr4插件自動生成(針對Java工程,也可以用於Gradle)

此處使用第一種方式

訪問者模式遍歷Antlr4語法樹

java -jar  /usr/local/lib/antlr-4.7.2-complete.jar  -visitor -no-listener  Test.g4 

生成源碼文件:

通過代碼展示訪問者模式在Antlr4中使用:

public class App {

    public static void main(String[] args) {
        CharStream input = CharStreams.fromString("12*2+12");
        TestLexer lexer = new TestLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        TestParser parser = new TestParser(tokens);
        TestParser.ExprContext tree = parser.expr();
        TestVisitor tv = new TestVisitor();
        tv.visit(tree);
    }

    static class TestVisitor extends TestBaseVisitor<Void> {
        @Override
        public Void visitAdd(TestParser.AddContext ctx) {
            System.out.println("========= test add");
            System.out.println("first arg: " + ctx.expr(0).getText());
            System.out.println("second arg: " + ctx.expr(1).getText());
            return super.visitAdd(ctx);
        }
    }
}

一般來說,面向程序靜態分析時,都是使用訪問者模式的,很少使用監聽器模式(無法主動控制遍歷AST的順序,不方便在不同節點遍歷之間傳遞數據)

Antlr4詞法解析和語法解析

如前面的語法定義,分為Lexer和Parser,實際上表示了兩個不同的階段:

  • 詞法分析階段:對應於Lexer定義的詞法規則,解析結果為一個一個的Token;
  • 解析階段:根據詞法,構造出來一棵解析樹或者語法樹。

如下圖所示:

Spark & Antlr4

Spark SQL /DataFrame 執行過程是這樣子的:

我們看下在 Spark SQL 中是如何使用Antlr4的.

當你調用spark.sql的時候, 會調用下面的方法:

  def sql(sqlText: String): DataFrame = {
    Dataset.ofRows(self, sessionState.sqlParser.parsePlan(sqlText))
  }

parse sql階段主要是parsePlan(sqlText)這一部分。而這里又會輾轉去org.apache.spark.sql.catalyst.parser.AbstractSqlParser調用parse方法:

protected def parse[T](command: String)(toResult: SqlBaseParser => T): T = {
    logDebug(s"Parsing command: $command")

    val lexer = new SqlBaseLexer(new UpperCaseCharStream(CharStreams.fromString(command)))
    lexer.removeErrorListeners()
    lexer.addErrorListener(ParseErrorListener)

    val tokenStream = new CommonTokenStream(lexer)
    val parser = new SqlBaseParser(tokenStream)
    parser.addParseListener(PostProcessor)
    parser.removeErrorListeners()
    parser.addErrorListener(ParseErrorListener)

    try {
      try {
        // first, try parsing with potentially faster SLL mode
        parser.getInterpreter.setPredictionMode(PredictionMode.SLL)
        toResult(parser)
      }
      catch {
        case e: ParseCancellationException =>
          // if we fail, parse with LL mode
          tokenStream.seek(0) // rewind input stream
          parser.reset()

          // Try Again.
          parser.getInterpreter.setPredictionMode(PredictionMode.LL)
          toResult(parser)
      }
    }
    catch {
      case e: ParseException if e.command.isDefined =>
        throw e
      case e: ParseException =>
        throw e.withCommand(command)
      case e: AnalysisException =>
        val position = Origin(e.line, e.startPosition)
        throw new ParseException(Option(command), e.message, position, position)
    }
  }

這里SqlBaseLexer 、SqlBaseParser都是Antlr4的東西,包括最后的toResult(parser)也是調用訪問者模式的類去遍歷語法樹來生成Logical Plan

spark提供了一個.g4文件,編譯的時候會使用Antlr根據這個.g4生成對應的詞法分析類和語法分析類,同時還使用了訪問者模式,用以構建Logical Plan(語法樹)。

訪問者模式簡單說就是會去遍歷生成的語法樹(針對語法樹中每個節點生成一個visit方法),以及返回相應的值。我們接下來看看一條簡單的select語句生成的樹是什么樣子:

這個sqlBase.g4文件我們也可以直接復制出來,用antlr相關工具就可以生成一個生成一個解析SQL的圖

將SELECT A.B FROM A,轉換成一棵語法樹。我們可以看到這顆語法樹非常復雜,這是因為SQL解析中,要適配這種SELECT語句之外,還有很多其他類型的語句,比如INSERT,ALERT等等。Spark SQL這個模塊的最終目標,就是將這樣的一棵語法樹轉換成一個可執行的Dataframe(RDD)

Spark使用Antlr4的訪問者模式,生成Logical Plan. 我們繼承SqlBaseBaseVisitor,里面提供了默認的訪問各個節點的觸發方法。我們可以通過繼承這個類,重寫對應節點的visit方法,實現自己的訪問邏輯,Spark SQL中這個繼承的類就是org.apache.spark.sql.catalyst.parser.AstBuilder

通過觀察這棵樹,我們可以發現針對我們的SELECT語句,比較重要的一個節點,是querySpecification節點,實際上,在AstBuilder類中,visitQuerySpecification也是比較重要的一個方法(訪問對應節點時觸發),正是在這個方法中生成主要的Logical Plan的。

以下是querySpecification在Spark SQL 中實現的 代碼:

  /**
   * Create a logical plan using a query specification.
   */
  override def visitQuerySpecification(
      ctx: QuerySpecificationContext): LogicalPlan = withOrigin(ctx) {
    val from = OneRowRelation().optional(ctx.fromClause) {
      visitFromClause(ctx.fromClause)
    }
    withQuerySpecification(ctx, from)
  }

先判斷是否有FROM子語句,有的話會去生成對應的Logical Plan,再調用withQuerySpecification()方法,

withQuerySpecification是邏輯計划核心方法, 根據不同的子語句生成不同的Logical Plan.

參考:

[1] Spark SQL: Relational Data Processing in Spark: https://amplab.cs.berkeley.edu/wp-content/uploads/2015/03/SparkSQLSigmod2015.pdf

[2] Antlr4簡明使用教程: https://bbs.huaweicloud.com/blogs/226877


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM