前言
現(xiàn)需要實現(xiàn)一個數(shù)據(jù)加工的DSL(domain-specific language 領(lǐng)域特定語言)嫩与,不需要對DSL過多理解攒暇,你可以類比SQL語言即可。現(xiàn)使用JavaCC這個工具來生成一個詞法和語法解析器蚤认,注意是代碼生成器队魏,所以才叫CC(Compiler Compiler編譯器的編譯器)。
技術(shù)理念
DSL
domain-specific language 領(lǐng)域特定語言臼疫,最典型的就是SQL择份,還有比如lucene Query Parser Syntax,promQL等烫堤。DSL主要分為兩類荣赶。
內(nèi)部DSL)內(nèi)嵌到現(xiàn)有的編程語言(java,c鸽斟,python)上拔创。比如lucene,PromQL等富蓄。
外部DSL)擁有自己一套完整的詞法分析剩燥,語法分析,編譯立倍,代碼生成等一整套完整的體系的獨立語言躏吊,比如JSON,正則帐萎,XML比伏,markdown等。
javaCC
想個很簡單的問題疆导,計算機如何識別你表達的語言在按你的語言去執(zhí)行相應(yīng)的邏輯赁项?這就是編譯原理,將你的輸入的內(nèi)容也叫代碼語言通過詞法語法分析、語法翻譯悠菜、中間代碼生成舰攒、存儲管理、代碼優(yōu)化和目標代碼等過程翻譯成計算機能識別的代碼悔醋。那么同理DSL也需要一個這樣的過程摩窃。業(yè)內(nèi)有很多優(yōu)秀的中間件的詞法和語法解析器代碼生成器都是javaCC。比如lucene QueryParser.jj芬骄,Calcite parser.jj猾愿,JsqlParser JSqlParserCC.jjt等,更多可以直接看官網(wǎng)账阻。
編譯器原理
模塊說明
詞法分析:將輸入的文本代碼解析生成Token流蒂秘,也叫掃描器。
語法分析:利用詞法分析生成的Token流轉(zhuǎn)成AST(抽象語法樹)淘太,也叫解析器姻僧。
語義分析:分析語法樹,得到新的語法樹蒲牧。
中間代碼:分析語法樹撇贺,生成中間代碼。
而JavaCC則主要擔當生成詞法分析器和語法分析器冰抢。
javaCC解析器原理
javaCC的工作即為生成詞法分析器(lexical analyzer)和語法分析器(parser generators)的工具松嘶。真正對文本代碼分析的流程入圖。
一些信息
1晒屎,javaCC 使用自頂向下(top-down)遞歸下降(recursive descent)解析喘蟆。
2缓升,默認是LL(1)文法用于預(yù)測性解析鼓鲁,不過在使用 LOOKAHEAD(n) 的時候為LL(k),主要為了消除歧義港谊,而在其他情況下均為LL(1)骇吭,這樣能減少遞歸回溯。至于啥是LL(k)算法歧寺,去看編譯原理吧燥狰,我也看得一知半解。
3斜筐,javaCC詞法分析器使用正則或字符串定義龙致,語法解析器定義使用EBNF(擴展巴科斯范式)語法,使用EBNF可以減少左遞歸的影響且更加易讀顷链,詞法分析器和語法分析器使用同一個文件定義目代。
4,javaCC提供了自帶的JJTree,非常方便直接生成AST樹榛了。
本地環(huán)境安裝
安裝官方參考文檔在讶。官方使用的是linux安裝,我這邊使用的是win本地開發(fā)霜大。
1构哺,下載安裝包(javacc-7.0.10.tar.gz),下載地址战坤。
2曙强,解壓到相應(yīng)的目錄并配置環(huán)境變量
#解壓安裝路徑
F:\soft3\javacc-javacc-7.0.10
#環(huán)境變量
JAVACC_HOME : F:\soft3\javacc-javacc-7.0.10\scripts
#配置path
%JAVACC_HOME%
3,需要特別注意的是湖笨,需要在安裝目錄的根目錄下創(chuàng)建一個target文件夾旗扑,然后將bootstrap文件夾中的javacc.jar拷貝過去,否則會報錯慈省。
4臀防,檢查是否安裝成功
打開cmd,輸入javacc边败,出現(xiàn)如下信息則表示安裝成功袱衷。
寫個Demo
在開始開發(fā)javacc的解釋器前,需要了解一些基本知識笑窜,否則會非常繞不方便理解致燥,我這邊的例子參考官方給的javacc-tutorial(個人感覺是寫的最清楚的英文也比較簡單,網(wǎng)上寫的都讓人云里霧里排截,不過可惜的是一些圖表信息不完整嫌蚤,jjtree說的也不詳細,不過對于我們?nèi)腴T學(xué)習(xí)還是非常足夠了)断傲。
BNF語法
::= //“定義為”, ::== 符號表示為 “可擴展為” 或 “可替換為”, “可定義為”.
<A> //A為必選項
“A” //A是一個術(shù)語脱吱,不用翻譯
'A' //A是一個術(shù)語,不用翻譯
[A] //A是可選項
{A} //A是重復(fù)項认罩,可出現(xiàn)任意次數(shù)箱蝠,包括0次
A* //A是重復(fù)項,可出現(xiàn)任意次數(shù)垦垂,包括0次
A+ //A可出現(xiàn)1次或多次
(A B) //A和B被組合在一起
A|B //A宦搬、B是并列選項,只能選一個
javaCC xx.jj文件模板
options {
JavaCC 的選項
}
PARSER_BEGIN(解析器類名)
package 包名
import 庫名
public class 解析器類名 {
任意的 Java 代碼
}
PARSER_END(解析器類名)
掃描器的描述
解析器的描述
一些說明
1劫拗,options部分间校,用于放置 JavaCC 的選項。常用選項見下文中的 JavaCC 語法描述文件中的 options页慷。
2憔足,PARSER_BEGIN聂渊、PARSER_END部分,用于定義解析器類四瘫。解析器類中需要定義的成員和方法都寫在這里汉嗽。
3,掃描器的描述部分找蜜,用于定義掃描器饼暑。
4,解析器的描述部分洗做,用于定義解析器弓叛。
生成詞法和語法解析器文件說明
XXConstants: 定義一些常量值,比如將TOKEN定義的值轉(zhuǎn)換為一個個的數(shù)字诚纸;
XXParserTokenManager: token管理器, 用于讀取token, 可以自定義處理;
JavaCharStream: CharStream的實現(xiàn)撰筷,會根據(jù)配置選項生成不同的類;
ParseException: 解析錯誤時拋出的類;
Token: 讀取到的單詞描述類;
TokenMgrError: 讀取token錯誤時拋出的錯誤;
xx.jj 一些關(guān)鍵字
TOKEN /* 定義一些確定的普通詞或關(guān)鍵詞,主要用于被引用 */
SPECIAL_TOKEN /* 定義一些確定的特殊用途的普通詞或關(guān)鍵詞畦徘,主要用于被引用或拋棄 */
SKIP /* 定義一些需要跳過或者忽略的單詞或短語毕籽,主要用于分詞或者注釋 */
MORE /* token的輔助定義工具,用于確定連續(xù)的多個token */
EOF /* 文件結(jié)束標識或者語句結(jié)束標識 */
IGNORE_CASE /* 輔助選項井辆,忽略大小寫 */
JAVACODE /* 輔助選項关筒,用于標識本段代碼是java */
LOOKAHEAD /* 語法二義性處理工具,用于預(yù)讀多個token杯缺,以便明確語義 */
PARSER_BEGIN /* 樣板代碼蒸播,固定開頭 */
PARSER_END /* 樣板代碼,固定結(jié)尾 */
TOKEN_MGR_DECLS /* 輔助選項 */
實現(xiàn)javacc-tutorial中的計算器例子
1萍肆,編寫calculator0.jj文件
# javacc全局配置,更多的options可以查看https://javacc.github.io/javacc/documentation/grammar.html#options
options {
STATIC = false ;
}
# 開始java代碼塊袍榆,內(nèi)部的實現(xiàn)主要是實現(xiàn)main函數(shù),完成解析的調(diào)用和執(zhí)行
PARSER_BEGIN(Calculator)
public class Calculator {
static public void main(String[] args) throws ParseException, TokenMgrError, NumberFormatException {
Calculator parser = new Calculator(System.in);
parser.Start(System.out) ;
}
double previousValue = 0.0 ;
}
# java塊結(jié)束
PARSER_END(Calculator)
# 詞語解析器開始,使用正則表達式定義
## SKIP 表示忽略那些字符的掃描
SKIP : { " " }
## 表示掃描的結(jié)束符
TOKEN : { < EOL : "\n" | "\r" | "\r\n" > }
## 加減乘除關(guān)鍵詞定義
TOKEN : { < PLUS : "+" > }
TOKEN : { < MINUS : "-" > }
TOKEN : { < TIMES : "*" > }
TOKEN : { < DIVIDE : "/" > }
## 讀取值
TOKEN : { < PREVIOUS : "$" > }
## 數(shù)值定義,包括整形和浮點數(shù),注意使用DIGITS替換整個(["0"-"9"])+的重復(fù)定義塘揣,增加可讀性包雀,減少重復(fù)部分。
TOKEN : { < NUMBER : <DIGITS> | <DIGITS> "." <DIGITS> | <DIGITS> "." | "."<DIGITS> > }
## 數(shù)值定義勿负,由于被其他Token定義引用馏艾,因此名稱使用#表示
TOKEN : { < #DIGITS : (["0"-"9"])+ > }
## 左右括號
TOKEN : { < OPEN_PAR : "(" > }
TOKEN : { < CLOSE_PAR : ")" > }
# 解析器開始函數(shù)劳曹,內(nèi)部實現(xiàn)為計算表達式結(jié)果后奴愉,控制臺打印結(jié)果。
void Start(PrintStream printStream) throws NumberFormatException :
{}
{
(
previousValue = Expression()
<EOL>
{ printStream.println( previousValue ); }
)*
<EOF>
}
# 表達式解析,可以看到函數(shù)基本上和java語法差不多铁孵,不過函數(shù)內(nèi)部實現(xiàn)為EBNF語法锭硼,這樣看起來比較麻煩,可以可以一層一層看下去蜕劝,Primary()將Token.image返回掃描到表達式中數(shù)值轉(zhuǎn)換成double返回檀头,Term()控制乘法和除法優(yōu)先級轰异, Expression()計算整個表達式的結(jié)果。
# 這樣函數(shù)看起來不夠清晰暑始,我舉一個簡單的例子 <NUMBER>(<PLUS><NUMBER>)*<EOF> 這明顯就是一個EBNF表達式搭独,表示第一個字符串必須為數(shù)值,而()中表示連接廊镜,第二個字符必須為加號牙肝,第三個必須為數(shù)值,然后*表示0到無數(shù)次嗤朴。而抽出多個函數(shù)配椭,主要是為了可讀性和優(yōu)先級以及特殊# # 處理。
double Expression() throws NumberFormatException :
{
double i ;
double value ;
}
{
value = Term()
(
<PLUS>
i = Term()
### 大括號中的表示動作
{ value += i ; }
|
<MINUS>
i = Term()
{ value -= i ; }
)*
{ return value ; }
}
#對乘法和除法優(yōu)先級處理
double Term() throws NumberFormatException :
{
double i ;
double value ;
}
{
value = Primary()
(
<TIMES>
i = Primary()
{ value *= i ; }
|
<DIVIDE>
i = Primary()
{ value /= i ; }
)*
{ return value ; }
}
#如果是數(shù)值雹姊,直接解析為Double數(shù)據(jù)股缸,如果為$則返回計算值,如果使用()括號包括則遞歸調(diào)用計算表達式吱雏,如果第一個字符為-負號敦姻,則取負值。
double Primary() throws NumberFormatException :
{
Token t ;
double d ;
}
{
t=<NUMBER>
{ return Double.parseDouble( t.image ) ; }
|
<PREVIOUS>
{ return previousValue ; }
|
<OPEN_PAR> d=Expression() <CLOSE_PAR>
{ return d ; }
|
<MINUS> d=Primary()
{ return -d ; }
}
2歧杏,生成詞法和語法解析器
#進入項目
cd ..\calculator2
#執(zhí)行
javacc calculator0.jj
看到如此信息則表示創(chuàng)建詞法和語法解析成功替劈。
3,編譯java文件
javac *.java
4得滤,創(chuàng)建表達式文件input.txt陨献,注意要有一個換行的結(jié)束符。
5懂更,執(zhí)行結(jié)果
java Calculator <input.txt
總結(jié)
1眨业,javaCC是java語言中最強大且使用最廣的詞法和語法解析器代碼生成器。
2沮协,當前demo代碼是直接在xx.jj解析器動作中直接計算的龄捡,通常情況下一般不會這樣,而是先試用jjtree轉(zhuǎn)換成AST抽象語法樹后再做后續(xù)的邏輯慷暂。
3聘殖,javaCC主要是生成詞法和語法解析器,將文本代碼轉(zhuǎn)換成計算機可讀可理解的AST抽象語法樹行瑞。
4奸腺,javacc使用正則表達式描述詞法規(guī)則,使用EBNF表示法描述語法規(guī)則血久。