JavaCC:詞法和語法分析器生成器

前言

現(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 SyntaxpromQL等烫堤。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)账阻。

編譯器原理

dsl-編譯器流程.drawio.png

模塊說明
詞法分析:將輸入的文本代碼解析生成Token流蒂秘,也叫掃描器。
語法分析:利用詞法分析生成的Token流轉(zhuǎn)成AST(抽象語法樹)淘太,也叫解析器姻僧。
語義分析:分析語法樹,得到新的語法樹蒲牧。
中間代碼:分析語法樹撇贺,生成中間代碼。
而JavaCC則主要擔當生成詞法分析器和語法分析器冰抢。

javaCC解析器原理

javaCC的工作即為生成詞法分析器(lexical analyzer)和語法分析器(parser generators)的工具松嘶。真正對文本代碼分析的流程入圖。


dsl-javaCC原理.drawio.png

一些信息
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拷貝過去,否則會報錯慈省。


image.png

4臀防,檢查是否安裝成功
打開cmd,輸入javacc边败,出現(xiàn)如下信息則表示安裝成功袱衷。


image.png

寫個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
image.png

image.png

看到如此信息則表示創(chuàng)建詞法和語法解析成功替劈。

3,編譯java文件

javac *.java
image.png

image.png

4得滤,創(chuàng)建表達式文件input.txt陨献,注意要有一個換行的結(jié)束符。


image.png

5懂更,執(zhí)行結(jié)果

java Calculator <input.txt
image.png

總結(jié)

1眨业,javaCC是java語言中最強大且使用最廣的詞法和語法解析器代碼生成器。
2沮协,當前demo代碼是直接在xx.jj解析器動作中直接計算的龄捡,通常情況下一般不會這樣,而是先試用jjtree轉(zhuǎn)換成AST抽象語法樹后再做后續(xù)的邏輯慷暂。
3聘殖,javaCC主要是生成詞法和語法解析器,將文本代碼轉(zhuǎn)換成計算機可讀可理解的AST抽象語法樹行瑞。
4奸腺,javacc使用正則表達式描述詞法規(guī)則,使用EBNF表示法描述語法規(guī)則血久。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末突照,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子氧吐,更是在濱河造成了極大的恐慌讹蘑,老刑警劉巖末盔,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異座慰,居然都是意外死亡陨舱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門版仔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來隅忿,“玉大人,你說我怎么就攤上這事邦尊”惩” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵蝉揍,是天一觀的道長链峭。 經(jīng)常有香客問我,道長又沾,這世上最難降的妖魔是什么弊仪? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮杖刷,結(jié)果婚禮上励饵,老公的妹妹穿的比我還像新娘。我一直安慰自己滑燃,他們只是感情好役听,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著表窘,像睡著了一般典予。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上乐严,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天瘤袖,我揣著相機與錄音,去河邊找鬼昂验。 笑死捂敌,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的既琴。 我是一名探鬼主播占婉,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼呛梆!你這毒婦竟也來了锐涯?” 一聲冷哼從身側(cè)響起磕诊,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤填物,失蹤者是張志新(化名)和其女友劉穎纹腌,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滞磺,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡升薯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了击困。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涎劈。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖阅茶,靈堂內(nèi)的尸體忽然破棺而出蛛枚,到底是詐尸還是另有隱情,我是刑警寧澤脸哀,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布蹦浦,位于F島的核電站,受9級特大地震影響撞蜂,放射性物質(zhì)發(fā)生泄漏盲镶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一蝌诡、第九天 我趴在偏房一處隱蔽的房頂上張望溉贿。 院中可真熱鬧,春花似錦浦旱、人聲如沸宇色。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽代兵。三九已至,卻和暖如春爷狈,著一層夾襖步出監(jiān)牢的瞬間植影,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工涎永, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留思币,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓羡微,卻偏偏與公主長得像谷饿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子妈倔,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容