Anrlr4 生成C++版本的語法解析器
一、 寫在前面
我最早是在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 | ~["\r\n\\] )* 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'? [\n\f] ; 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
&n