自己動手開發編譯器(二)正則語言和Regex

來源:互聯網
上載者:User

從今天這一篇起,我們就來正式揭開編譯器的奧秘。首先我們接觸到的模組是詞法分析器,也叫詞法掃描器,代碼裡我常常叫它Scanner。昨天我稍微解釋了一下為什麼需要將詞法分析單獨分離出來,今天來回顧一下這個問題。請看下面這段C#代碼:

 

 string str = "Hello World";

 

即使沒有文法高亮,這段代碼也可以很明顯地分成好幾部分。首先是關鍵字string,之後是變數名str,然後是等號=,接下來是一個字串字面常量”Hello World”。現代語言如C#這樣的,都能明顯地將原始碼分斷成這樣具有明確含義的片段,我們稱之為詞素(lexeme)。與描述整個C#語言的文法相比,我們用比較簡單的規則就能描述不同類型的詞素。比如上面這段代碼中出現的詞素用白話來描述的話就是:

 

類型

規則

例子

關鍵字string

正好是s-t-r-i-n-g這幾個字母按順序組成

string

標識符(變數名)

由字母開頭,後面可以跟零個或多個字母或數字,但不能與關鍵字衝突

str

等號

一個=符號

=

字串字面常量

由雙引號開始,中間可以包含任意個不是雙引號的字元,最後以雙引號結尾

"hello world"

分號

一個;符號

;

 

我們看到,不同詞素可以根據其特徵劃分到幾個類型當中,而接下來的文法分析階段,就可以直接以詞素的類型——我們稱之為單詞(token)——作為輸入。token有時候也翻譯成令牌、記號、象徵什麼的,在本文中統一稱為單詞。如此可見,只要用相對簡潔的規則,就能把原本字串組成的源檔案,分解為一串單詞流,這樣就能大大簡化接下來的文法分析。這就是我們把詞法分析單獨分出來作為一個模組的根本原因。

 

不過,上面表格中所列的規則是用白話來描述的,我們希望能用一種形式化的語言來進行描述,以便電腦自動進行處理。Regex就是一個理想的選擇。

 

大家日常編程中估計多多少少都接觸過Regex,用它來匹配字串等,也可能已經很熟悉其文法了。但我這次想從Regex的最基本概念來重新介紹一次,主要想讓大家更深地理解它。首先我們要重新定義一下“語言”這個概念。“語言”就是指字串的集合,其中的字元來自於一個有限的字元集合。也就是說,語言總要定義在一個有限的字元集上,但是語言本身可以既可以是有窮集合,也可以是無窮集合。比如“C#語言”就是指滿足C#文法的全體字串的集合,它顯然是個無窮集合。當然也可以定義一些簡單的語言,比如這個語言{ a }就只有一個成員,那就是一個字母a。後面我們都用大括弧{}來表示字串的集合。所謂Regex呢,就是描述一類語言的一種特殊運算式,Regex共有2種基本要素:

  1. 運算式ε表示一個語言,僅包含一個長度為零的字串,可以理解為{ String.Empty },我們通常把String.Empty記作ε,讀作epsilon。
  2. 對字元集中任一字元a,運算式a表示僅有一個字元a的語言,即{ a }。

同時Regex定義了3種基本運算規則:

  1. 兩個Regex的,記作X|Y,表示的語言是RegexX所表示的語言與RegexY所表示語言的並集。比如a|b所得的語言就是{a, b}。類似於加法
  2. 兩個Regex的串連,記作XY,表示的語言是將X的語言中每個字串後面串連上Y語言中的每一種字串,再把所有這種串連的結果組成一種新的語言。比如令X = a|b,Y = c|d,那麼XY所表示的語言就是{ac, bc, ad, bd}。因為X表示是{a, b},而Y表示的是{ c, d},串連運算取X語言的每一個字串接上Y語言的每一個字串,最後得到了4種串連結果。這類似於乘法
  3. 一個Regex的克林閉包,記作X*,表示分別將零個,一個,兩個……無窮個X與自己串連,然後再把所有這些求並。也就是說X* = ε | X | XX | XXX | XXX | ……。比如a*這個Regex,就表示的是個無窮語言{ ε, a, aa, aaa, aaaa, …. }。這相當於任意次重複一個語言。

以上三種運算寫在一起時克林閉包的優先順序高於串連運算,而串連運算的優先順序高於並運算。以上就是Regex的全部規則!並不是很難理解對吧?下面我們用Regex來描述一下剛才各個詞素的規則。

 

首先是關鍵字string,剛才我們描述說它是“正好是s-t-r-i-n-g這幾個字母按順序組成”,用Regex來表示,那就是s-t-r-i-n-g這幾個字母的串連運算,所以寫成正則表達是就是string。大家一定會覺得這個例子很無聊。。那麼我們來看下一個例子:標識符。用白話來描述是“由字母開頭,後面可以跟零個或多個字母或數字”。先用Regex描述“由字母開頭”,那就是指,可以是a-z中任意一個字母來開頭。這是Regex中的運算:a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z。如果每個Regex都這麼寫,那真是要瘋掉了,所以我們引入方括弧文法,寫在方括弧裡就表示這些字元的並運算。比如[abc]就表示a|b|c。而a-z一共26個字母我們也簡寫成a-z,這樣,“由字母開頭”就可以翻譯成Regex[a-z]了。接下來我們翻譯第二句“後面可以跟零個或多個字母或數字”這句話中的“零個或多個”可以翻譯成克林閉包運算,最後相信大家都可以寫出來,就是[a-z0-9]*。最後,前後兩句之間是一個串連運算,因此最後描述標識符“語言”的Regex就是[a-z][a-z0-9]*。其中的*運算也意味著“標識符”是一種無窮語言,有無數種可能的標識符。本來就是這樣,很好理解對吧?

 

從上面例子可以看出,Regex都可以用兩種要素和三種基本運算組合出來。但是如果我們要真的拿來描述詞法單詞的規則,需要一些便於使用的輔助文法,就像上邊的方括弧文法那樣。我們定義一些Regex的擴充運算:

  1. 方括弧表示括弧內的字元並運算。[abc]就等於a|b|c
  2. 方括弧中以^字元開頭,表示字元集中,排除方括弧中的所有字元之後,所剩字元的並運算。[^ab]就表示除了ab以外所有字元求並。
  3. 圓.點表示字元集內所有字元的並。因此 .* 這個運算式就能表示這種字元集所能組成的一切字串。
  4. X?表示 X|ε 。表示X與Null 字元串之間可選。
  5. X+表示XX*。這等於限制了X至少要重複1次。

用過Regex的同學應該都熟悉以上運算了。其實.NET中的Regex還提供更多的擴充文法,但我們這次並不使用.NET的正則庫,所以就不列出其餘的文法了。

 

我們把所有能用Regex表示的語言稱作正則語言。很遺憾,並非所有的語言都是正則語言。比如C#,或者所有程式設計語言、HTML、XML、JSON等等,都不是正則語言。所以不能用Regex定義上述語言的規則。但是,用Regex來定義詞法分析的規則卻是非常合適的。大部分程式設計語言的詞素都可以用一個簡單的Regex來表達。下面就是上述單詞的Regex定義。

 

類型

Regex

例子

關鍵字string

string

string

標識符(變數名)

[a-z][a-z0-9]*

str

等號

=

=

字串字面常量

"[^"]*"

"hello world"

分號

;

;

 

我們大家平時熟悉的Regex是寫成上文這樣的字串形式。但這次我們要自己處理Regex,寫成字串顯然增加了處理的難度(要解析Regex字串)。所以在VBF.Compilers庫的詞法分析庫中,我引入了一種用對象來表示Regex的手法。我定義了一個RegularExpression基類,然後為每一種Regex要素或運算編寫了一個子類:

其中AlternationExpression就是“並”運算,ConcatenationExpression就是“串連”運算,EmptyExpression當然就表示εNull 字元串,KleeneStarExpression表示“克林閉包”運算(你現在可以知道克林閉包也可以叫做克林星——本來就是一星號嘛)和表示單一字元的SymbolExpression。像SymbolExpression裡面其實就儲存了它所表示的一個字元,而AlternationExpression下面儲存了兩個RegularExpression執行個體,用來表示並運算的雙方。所以,任何Regex都能用RegularExpression的對象樹來表示。比如Regex[a|b]*就可以表示為:

 

RegularExpression re = new KleeneStarExpression(             new AlternationExpression(             new SymbolExpression('a'), new SymbolExpression('b')));             

 

有點像Linq to XML有木有?雖然它寫起來比字串長了那麼一點點(觀眾:是長好多吧……),但是我們不需要解析字串就可以獲得它的結構,這對下一步進行處理非常有協助。好吧,我承認全都寫這麼長也受不了,所以我定義了一些輔助的靜態方法和運算子多載。上面的Regex可以寫成:

 

var re = (RE.Symbol('a') | RE.Symbol('b')).Many();

 

其中RE其實是要用using RE=VBF.Compilers.Scanners.RegularExpression;語句來聲明的別名。雖然它還是比字串的Regex長一些,但考慮到無需解析字串帶來的方便,就忍了吧。等到後面文法分析學習完了以後我會帶大家自己開發Regex字串的解析器。

 

接下來的問題是,怎麼用Regex表示的規則來進行詞法分析呢?Regex利於我們理解單詞的規則,但並不能拿來直接解析字串。為此我們要引入有窮自動機的概念來真正處理輸入字串。敬請期待下一篇。

同時大家別忘了關注VBF項目:https://github.com/Ninputer/VBF 和我的微博:http://weibo.com/ninputer 多謝大家支援!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.