Anrlr4 生成C++版本的語法解析器

  • 2019 年 10 月 17 日
  • 筆記

一、 寫在前面

  我最早是在2005年,首次在實際開發中實現語法解析器,當時調研了Yacc&Lex,覺得風格不是太好,關鍵當時yacc對多線程也支持的不太好,接着就又學習了Bison&Flex,那時Bison的版本還是v1.x.y,對C++的支持比較差,最終選擇了Biso++ & Flex++,兩者支持C++版本並且跨平台支持Linux和windows。業務需求是實現全文檢索Contains表達式的解析,包括調研、學習、實現和測試,大致用了2月,很多時間花費在解決語法衝突、內存管理等方面。

  後來換了工作單位,在2012年再次需要實現Contains表達式語法,這時的Bison和Flex都穩定支持C++版本了,所以就直接採用Bison&Flex,大約用了不到2周,實現了Contains表達式解析和單元測試。

  最近一次是在2018年,需要用Java版本的Contains表達式解析,這次採用的是Antlr4,開發和測試僅用了一個周六日在家的業餘時間。感覺Antlr4明顯比Bison&Flex簡單,對語法規則的支持很直觀易懂,用在語法衝突比較少的業務環境中非常合適,更關鍵的是:相對於Bison,Antlr4產生的Parser可以順手生成對應規則的嵌套類,如果象我一樣喜歡Visitor風格的話,完全可以做到語法文件與代碼文件分離,從而大大縮短語法解析器的開發周期,並大大降低維護難度。

  但是Antlr4最大的問題是對低版本C++的支持不太好,它需要高版本的GCC,在Centos7中的GCC為4.8.5無法編譯通過,好在Centos8剛剛發佈,它的GCC為8.2.1,正好來試驗Antlr4的C++版本來實現Contains表達式語法。

  網上的Antlr4生成C++版本語法解析器的資料較少,本文側重整理與之相關的內容,並以Contains語法表達式為例,而具體的Antlr4的學習材料請見文尾的參考材料。

二、 搭建開發環境

1)、首先是安裝Centos8的虛擬機環境,如上文所述,其gcc版本為8.2.1。

2)、Antlr4需要使用jdk,在Centos8中包含了jdk1.8和jdk11,我們選擇安裝jdk1.8

su  yum install java-1.8.0-openjdk

安裝完成後查看版本

java -version  openjdk version "1.8.0_201"  OpenJDK Runtime Environment (build 1.8.0_201-b09)  OpenJDK 64-Bit Server VM (build 25.201-b09, mixed mode)

3)、下載Antlr4的Java包,用於根據語法文件生成C++版的解析器。

  在antlr的下載頁https://www.antlr.org/download.html中找到Complete ANTLR 4.7.2 Java binaries jar. Complete ANTLR 4.7.2 tool, Java runtime and ST 4.0.8, which lets you run the tool and the generated code.,註:隨着時間的變化antlr4版本和下載鏈接都會變化呀。

mkdir libs  curl https://www.antlr.org/download/antlr-4.7.2-complete.jar -o ./libs/antlr-4.7.2-complete.jar

  驗證

java -jar ./libs/antlr-4.7.2-complete.jar  ANTLR Parser Generator  Version 4.7.2   -o ___              specify output directory where all output is generated   -lib ___            specify location of grammars, tokens files   -atn                generate rule augmented transition network diagrams   -encoding ___       specify grammar file encoding; e.g., euc-jp   -message-format ___ specify output style for messages in antlr, gnu, vs2005   -long-messages      show exception details when available for errors and warnings   -listener           generate parse tree listener (default)   -no-listener        don't generate parse tree listener   -visitor            generate parse tree visitor   -no-visitor         don't generate parse tree visitor (default)   -package ___        specify a package/namespace for the generated code   -depend             generate file dependencies   -D<option>=value    set/override a grammar-level option   -Werror             treat warnings as errors   -XdbgST             launch StringTemplate visualizer on generated code   -XdbgSTWait         wait for STViz to close before continuing   -Xforce-atn         use the ATN simulator for all predictions   -Xlog               dump lots of logging info to antlr-timestamp.log   -Xexact-output-dir  all output goes into -o dir regardless of paths/package

4)、下載Antltr4的C++運行庫,採用編譯安裝的方式。

  在antlr的下載頁https://www.antlr.org/download.html中找到Linux and others use source distribution: antlr4-cpp-runtime-4.7.2-source.zip (.h, .cpp),註:隨着時間的變化antlr4版本和下載鏈接都會變化呀。

  

mkdir work  url https://www.antlr.org/download/antlr4-cpp-runtime-4.7.2-source.zip -o ./work/antlr4-cpp-runtime-4.7.2-source.zip  mkdir cpp
cd cpp unzip ../antlr4-cpp-runtime-4.7.2-source.zip mkdir build && mkdir run && cd build cmake .. -DANTLR_JAR_LOCATION=/home/ansible/libs/antlr-4.7.2-complete.jar -DWITH_DEMO=True make su make install ll /usr/local/include/antlr4-runtime/antlr4-runtime.h ll /usr/local/lib/libantlr4*

三、編寫Contains表達式解析器

Contains函數完整語法如下:

Contains(column_name,query_expression[,score_flag])

其中query_expression是一個字符串表達式,它可以由SQL解析完成。

3.1、  基本功能

功能列表如下:

1)、顯式的與(AND)操作符‘&’,例如 hello & world

2)、隱式的與(AND)操作符‘空格’,例如’hello  world’

3)、或(OR)操作符‘|’, 例如 hello | world

4)、非(NOT)操作符‘-’, 例如 hello – world

5)、首字詞操作符‘^’, 例如 ^hello

6)、尾字詞操作符‘$’, 例如 mouse$

7)、詞組查詢操作符 “”,例如”南大”

8)、分組操作符(),例如( hello world ) & (cat | dog)

9)、閥值匹配符 ‘/’, 例如 “the great wall is a wonderful place”/3

10)、  NEAR搜索函數 near((term1, term2), num,order), 例如 near((great, place), 2, 1), num表示詞距,order 為 0 代表無詞序, 為 1代表有詞序

11)、  擴展選項,搜索表達式通過”:”分作基本表達式和擴展選項兩個部分,總長度的限制為255字符,其中擴展選項可以為空,目前擴展選項僅支持rank=tf,表示相關度算法採用詞頻而不是缺省的bm25算法。例如”南大: rank=tf” 表示搜索南大,相關度為詞頻。

3.2、  語法規則

query_expression具體由Contains表達式解析器完成,其語法規則用Antlr4語法描述如下:

contains_param:  	contains_expr  	|contains_expr ':' fti_optstring  	;  fti_optstring :    fti_opt  	| fti_opt '&' fti_optstring  	;  fti_opt:    ID  '=' string_value  	;  contains_expr:         contains_string       | CONST_STRING '/' NUMBER       | func_near_expr       | '(' contains_expr ')'       | contains_expr  contains_expr       | contains_expr OPT_AND contains_expr       | contains_expr OPT_OR contains_expr       | contains_expr OPT_NOT contains_expr  	 ;  contains_string:         string_value       | SENTENCE_HEAD string_value       | SENTENCE_HEAD string_value SENTENCE_TAIL       | string_value SENTENCE_TAIL  	   ;  string_value:        ID       | STRING       | CONST_STRING       | NUMBER       ;  func_near_expr:       NEAR '(' '(' near_term_list ')' ',' NUMBER near_order ')'       ;  near_term_list:  	   near_term  	   | near_term ',' near_term_list  	 ;  near_term:       func_near_expr  	 | contains_string  	 ;  near_order:  	 | ',' NUMBER  	 ;	

3.3、  詞法規則

CONST_STRING : DQuote ( EscSeq | ~["rn\] )* DQuote	;  NEAR   : N E A R	;  SENTENCE_HEAD : '^' ;  SENTENCE_TAIL : '$' ;  OPT_AND       : '&' ;  OPT_OR        : '|' ;  OPT_NOT       : '-' ;  NUMBER	:  	'0'  	|	[1-9] DecDigit*  	;    ID: [a-zA-Z] ([a-zA-Z] | DecDigit | '_')*  ;// Identifier  STRING  : NameChar + ;  WS	: ( Hws | Vws )+ -> skip;    fragment DQuote : '"'	;  fragment Esc    : '\'	;    fragment A : [aA];  fragment B : [bB];  fragment C : [cC];  fragment D : [dD];  fragment E : [eE];  fragment F : [fF];  fragment G : [gG];  fragment H : [hH];  fragment I : [iI];  fragment J : [jJ];  fragment K : [kK];  fragment L : [lL];  fragment M : [mM];  fragment N : [nN];  fragment O : [oO];  fragment P : [pP];  fragment Q : [qQ];  fragment R : [rR];  fragment S : [sS];  fragment T : [tT];  fragment U : [uU];  fragment V : [vV];  fragment W : [wW];  fragment X : [xX];  fragment Y : [yY];  fragment Z : [zZ];    fragment DecDigits	: DecDigit+	;  fragment DecDigit	: [0-9]		;    fragment HexDigits	: HexDigit+	;  fragment HexDigit	: [0-9a-fA-F]	;    fragment Hws		: [ t]		;  fragment Vws		: 'r'? [nf]	;    fragment NameChar     : NameStartChar     | '0'..'9'     | '_'     | 'u00B7'     | 'u0300'..'u036F'     | 'u203F'..'u2040'     ;  fragment NameStartChar     : 'A'..'Z' | 'a'..'z'     | 'u00C0'..'u00D6'     | 'u00D8'..'u00F6'     | 'u00F8'..'u02FF'     | 'u0370'..'u037D'     | 'u037F'..'u1FFF'     | 'u200C'..'u200D'     | 'u2070'..'u218F'     | 'u2C00'..'u2FEF'     | 'u3001'..'uD7FF'     | 'uF900'..'uFDCF'     | 'uFDF0'..'uFFFD'     ;    fragment UnicodeEsc  	:	'u' (HexDigit (HexDigit (HexDigit HexDigit?)?)?)?  	;    // Any kind of escaped character that we can embed within ANTLR literal strings.  fragment EscSeq  	:	Esc  		( [btnfr"'\]	// The standard escaped character set such as tab, newline, etc.  		| UnicodeEsc	// A Unicode escape sequence  		| .		// Invalid escape character  		| EOF		// Incomplete at EOF  		)  	;

四、代碼實現

  Antlr4支持Visitor模式和Listener模式,一個是在語法分析完成後執行遍歷語法樹,一個是在語法分析過程中實時處理,相當於XML分析的DOM模式和SAX模式。在本次實驗中因為表達式是相對簡單的小對象,所以僅考慮Visitor模式。

  由語法規則文件生成C++代碼:

java -jar /home/ansible/libs/antlr-4.7.2-complete.jar -Dlanguage=Cpp FtiExpr.g4 -visitor -no-listener -o ./antlr4

  在antlr4下生成cpp代碼文件列表如下:

詞法分析器  FtiExprLexer.h  FtiExprLexer.cpp  語法分析器  FtiExprParser.h  FtiExprParser.cpp  Visitor模式訪問語法樹的抽象類  FtiExprVisitor.h  FtiExprVisitor.cpp  Visitor模式訪問語法樹的最簡示例類  FtiExprBaseVisitor.h  FtiExprBaseVisitor.cpp

    對於Visitor模式,我們自然要從FtiExprVisitor派生出遍歷語法樹的類,同時一般還會從BaseErrorListener派生出合適的錯誤處理類,來收集錯誤信息。

    驅動框架的代碼如下:

///brief 分析Contains表達式  ///param strExpr 表達式字符串  ///param strOutput 如果符合語法,返回格式化後的表達式字符串,反之則返回分析過程中的錯誤信息  ///return 是否符合語法 true 符合;false 不符合  bool CTestFtiExprVisitorFixture::ParseString(const std::string &strExpr, std::string &strOutput)  {      bool bParse = false;      ANTLRInputStream input(strExpr);      FtiExprLexer lexer(&input);      CommonTokenStream tokens(&lexer);      FtiExprParser parser(&tokens);      parser.removeErrorListeners();      CFtiExprErrorListener listenerError;      parser.addErrorListener(&listenerError);      FtiExprParser::Contains_paramContext *pParamContext = parser.contains_param();      if(listenerError.m_strErrMsg.empty())      {          CTestFtiExprVisitor visitor;          antlrcpp::Any strExpr = visitor.visit(pParamContext);          strOutput = strExpr.as<std::string>();          bParse = true;      }      else      {          char cLine[32],cCol[32];          snprintf(cLine, 31, "%d", listenerError.m_nLine);          snprintf(cCol, 31, "%d", listenerError.m_nPositionInLine);          strOutput = "Line: " + std::string(cLine) + " Col: " + std::string(cCol) + " Msg:" + listenerError.m_strErrMsg;      }      return bParse;  }

  主要就是字符串=》詞法分析器 =》Token串 =》語法規則

  FtiExprParser::Contains_paramContext *pParamContext = parser.contains_param();

  語法解析

  antlrcpp::Any strExpr = visitor.visit(pParamContext);

  進行語法樹遍歷。

    其他測試的主要代碼如下:

  

void CTestFtiExprVisitorFixture::TestParseOk(std::string strExpr, std::string strExpected)  {      std::string strOutput;      ParseString(strExpr, strOutput);      CPPUNIT_ASSERT_EQUAL(strExpected, strOutput);  }    void CTestFtiExprVisitorFixture::TestParseFail(std::string strExpr, std::string strExpected)  {      std::string strOutput;      ParseString(strExpr, strOutput);      CPPUNIT_ASSERT_EQUAL(strExpected, strOutput);  }    void CTestFtiExprVisitorFixture::TestParsePass(void)  {      TestParseOk("tianjin", "tianjin");      TestParseOk("中國", "中國");      TestParseOk("tianjin", "tianjin");      TestParseOk("12345", "12345");      TestParseOk(""tianjin"", ""tianjin"");        TestParseOk("^tianjin", "^ tianjin");      TestParseOk("^tianjin$", "^ tianjin $");      TestParseOk("tianjin$", "tianjin $");        TestParseOk(""tianjin beijing"/ 12", ""tianjin beijing" / 12");        TestParseOk("tianjin   beijing", "( tianjin ) & ( beijing )");      TestParseOk("tianjin & beijing", "( tianjin ) & ( beijing )");      TestParseOk("tianjin | beijing", "( tianjin ) | ( beijing )");      TestParseOk("tianjin - beijing", "( tianjin ) - ( beijing )");        TestParseOk("tianjin  beijing | shangxi hebei", "( ( tianjin ) & ( beijing ) ) | ( ( shangxi ) & ( hebei ) )");      TestParseOk("(tianjin  beijing) | (shangxi hebei)", "( ( ( tianjin ) & ( beijing ) ) ) | ( ( ( shangxi ) & ( hebei ) ) )");        TestParseOk("NEAR((tianjin , beijing),10)", "NEAR((tianjin,beijing),10)");      TestParseOk("NEAR((tianjin,beijing),10,1)", "NEAR((tianjin,beijing),10,1)");  }    void CTestFtiExprVisitorFixture::TestParseNoPass(void)  {      TestParseFail("", "Line: 1 Col: 0 Msg:mismatched input '<EOF>' expecting {'(', CONST_STRING, NEAR, '^', NUMBER, ID, STRING}");  }    void CTestFtiExprVisitorFixture::TestFtiOpt(void)  {      TestParseOk("NEAR((tianjin,beijing),10,1) : rank = wordcount", "NEAR((tianjin,beijing),10,1) : rank = wordcount");      TestParseOk("NEAR((tianjin,beijing),10,1) : rank = wordcount&mode=fast", "NEAR((tianjin,beijing),10,1) : rank = wordcount & mode = fast");      TestParseOk("NEAR((12tianjin,beijing),10,1) : rank = wordcount", "NEAR((12tianjin,beijing),10,1) : rank = wordcount");      TestParseFail("NEAR((tianjin,beijing),10,1) : 1rank = wordcount", "Line: 1 Col: 31 Msg:mismatched input '1rank' expecting ID");  }

  完整代碼示例在:https://github.com/ZhenYongFan/Blog/tree/master/TestFtiExpr

五、參考資料

官方資料,生成目標語言為C++的Antlr4

https://github.com/antlr/antlr4/blob/master/doc/cpp-target.md

ANTLR 4簡明教程

https://www.cntofu.com/book/115/index.html

Antlr4 —詞法規則

https://blog.csdn.net/yangguosb/article/details/85624640

antlr v4 使用指南連載4——詞法規則入門之黃金定律

https://www.cnblogs.com/laud/p/antlr4_4.html

antlr v4 使用指南連載5——如何編寫詞法定義

https://www.cnblogs.com/laud/p/anltrv4_5.html