跟vczh看實例學編譯原理——二:實現Tinymoe的詞法分析

文章中引用的代碼均來自https://github.com/vczh/tinymoehtml

 

實現Tinymoe的第一步天然是一個詞法分析器。詞法分析其所做的事情很簡單,就是把一份代碼分割成若干個token,記錄下他們所在文件的位置,以及丟掉沒必要要的信息。可是Tinymoe是一個按行分割的語言,天然token列表也就是二維的,第一維是行,第二維是每一行的token。在繼續講詞法分析器以前,先看看Tinymoe包含多少token: git

  • 符號:(、)、,、:、&、+、-、*、/、\、%、<、>、<=、>=、=、<>
  • 關鍵字:module、using、phrase、sentence、block、symbol、type、cps、category、expression、argument、assignable、list、end、and、or、not
  • 數字:12三、456.789
  • 字符串:"abc\r\ndef"
  • 標識符:tinymoe
  • 註釋:-- this is a comment

 

Tinymoe對於token有一個特殊的規定,就是字符串和註釋都是單行的。所以若是一個字符串在沒有結束以前就遇到了換行,那麼這種寫法定義爲你遇到了一個沒有右雙引號的字符串,須要報個錯,而後下一行就不是這個字符串的內容了。 程序員

 

一個詞法分析器所須要作的事情,就是把一個字符串分解成描述此法的數據結構。既然上面已經說到Tinymoe的token列表是二維的,所以數據結構確定會體現這個觀點。Tinymoe的詞法分析器代碼能夠在這裏找到:https://github.com/vczh/tinymoe/blob/master/Development/Source/Compiler/TinymoeLexicalAnalyzer.hgithub

 

首先是token: 正則表達式

CodeTokenType是一個枚舉類型,標記一個token的類型。這個類型比較細化,每個關鍵字有本身的類型,每個符號也有本身的類型,剩下的按種類來分。咱們能夠看到token須要記錄的最關鍵的東西只有三個:類型、內容和代碼位置。在token記錄代碼位置是十分重要的,正確地記錄代碼位置可讓你可以報告帶位置的錯誤、從語法樹的節點還原成代碼位置、甚至在調試的時候能夠把指令也換成位置。 express

 

這裏須要提到的是,string_t是一個typedef,具體的聲明能夠在這裏看到:https://github.com/vczh/tinymoe/blob/master/Development/Source/TinymoeSTL.h。Tinymoe是徹底由標準的C++11和STL寫成的,可是爲了適應不一樣狀況的須要,Tinymoe分爲依賴code page的版本和Unicode版本。若是編譯Tinymoe代碼的時候聲明瞭全局的宏UNICODE_TINYMOE的話,那Tinymoe全部的字符處理將使用wchar_t,不然使用char。char的類型和Tinymoe編譯器在運行的時候操做系統當前的code page是綁定的。因此這裏會有相似string_t啊、ifstream_t啊、char_t等類型,會在不一樣的編譯選項的影響下指向不一樣的STL類型或者原生的C++類型。github上的VC++2013工程使用的是wchar_t的版本,因此string_t就是std::wstring。 api

 

Tinymoe的詞法分析器除了token的類型之外,確定還須要定義整個文件結構在詞法分析後的結果: 數據結構

這個數據結構體現了"Tinymoe的token列表是二維的"的這個觀點。一個文件會被詞法分析器處理成一個shared_ptr<CodeFIle>對象,CodeFile::lines記錄了全部非空的行,CodeLine::tokens記錄了該行的全部token。 函數

 

如今讓咱們來看詞法分析的具體過程。關於如何從正則表達式構造詞法分析器能夠在這裏(http://www.cppblog.com/vczh/archive/2008/05/22/50763.html)看到,不過咱們今天要講一講如何人肉構造詞法分析器。方法實際上是同樣的,首先人肉構造狀態機,而後把用狀態機分析輸入的字符串的代碼抄過來就能夠了。可是不多有人會解耦得那麼開,由於這樣寫很容易看不懂,其次有可能會遇到一些極端狀況是你沒法用純粹的正則表達式來分詞的,譬如說C++的raw string literal:R"tinymoe(這裏的字符串沒有轉義)tinymoe"。一個用【R"<一些字符>(】開始的字符串只能由【)<一樣的字符>"】來結束,要順利分析這種狀況,只能經過在狀態機裏面作hack才能解決。這就是爲何咱們人肉構造詞法分析器的時候,會把狀態和動做都混在一塊兒寫,由於這樣便於處理特殊狀況。 單元測試

 

不過幸虧的是,Tinymoe並無這種狀況發生。因此咱們能夠直接從狀態機入手。爲了簡單起見,我在下面的狀態機中去掉全部不是+和-的符號。首先,咱們須要一個起始狀態和一個結束狀態:

 

首先咱們添加整數和標識符進去:

 

其次是加減和浮點:

 

最後把字符串和註釋補全:

 

這樣狀態機就已經完成了。讀過編譯原理的可能會問,爲何終結狀態都是由虛線而不是帶有輸入的實現指向的?由於虛線在這裏有特殊的意義,也就是說它不能移動輸入的字符串的指針,並且他還要負責添加一個token。當狀態跳到End以後,那他就會變成Start,因此實際上Start和End是同一個狀態。這個狀態機也不是輸入什麼字符都能跳轉到下一個狀態的。因此當你發現輸入的字符讓你無路可走的時候,你就是遇到了一個詞法錯誤

 

這樣咱們的設計就算完成了,接下來就是如何用C++來實現它了。爲了讓代碼更容易閱讀,咱們應該給Start和1-9這麼多狀態起名字,作法以下:

在這裏相似狀態3這樣的狀態被我省略掉了,由於這個狀態惟一的出路就是虛線,因此跳到這個狀態意味着你要馬上執行虛線,也就是說你不須要作"跳到這個狀態"這個動做。所以它不須要有一個名字。

 

而後你只要按照下面的作法翻譯這個狀態機就能夠了:

 

只要寫到這裏,那麼咱們就初步完成了詞法分析器了。其實任何系統的主要功能都是相對容易實現的,每每是次要的功能才須要花費大量的精力來完成,並且還很容易出錯。在這裏"次要的功能"就是——記錄token的行列號,還有維護CodeFile::lines避免空行的出現!

 

儘管我已經作過了那麼屢次詞法分析器,可是我仍然沒法一鼓作氣寫對,仍然會出一些bug。面對編譯器這種純計算程序,debug的最好方法就是寫單元測試。不過對於不熟悉單元測試的人來說可能很難一會兒想到要作什麼測試,在這裏我能夠把我給Tinymoe謝的一部分單元測試在這裏貼一下。

 

第一個,固然是傳說中的"Hello, world!"測試了:

 

TEST_CASE和TEST_ASSERT(這裏暫時沒有直接用到TEST_ASSERT)是我爲了開發Tinymoe隨手擼的一個宏,能夠在Tinymoe的代碼裏看到。爲了檢查咱們有沒有粗枝大葉,咱們有必要檢查詞法分析器的任何一個輸出的數據,譬如每一行有多少token,譬如每個token的行號列好內容和類型。我爲了讓這些枯燥的測試用例容易看懂,在這個文件(https://github.com/vczh/tinymoe/blob/master/Development/TinymoeUnitTest/TestLexicalAnalyzer.cpp)的開頭能夠看到FIRST_LINE、FIRST_TOKEN、TOKEN、LAST_TOKEN、NEXT_LINE、LAST_LINE是怎麼定義的。其實吧這些宏展開,就是一個普通的遍歷CodeFile::lines和CodeLine::tokens的程序,而後TEST_ASSERT一下CodeToken的每個成員是否咱們所須要的值。我相信看到這裏不少人確定把重點放在宏和炫技上,而不是如何設計測試用例上,這是有前途的程序員和沒前途的程序員面對一份資料的反應的重要區別之一。沒前途的程序員老是會把注意力放在一些莫名其妙的地方,其中一個例子就是"過早優化"。

 

第二個測試用例針對的是整數和浮點的輸出和報錯上,重點在於檢查每個token的列號是否是正確的計算了出來:

 

第三個測試用例的重點主要是-符號和—註釋的分析:

 

第四個測試用例則是測試字符串的escaping和在面對換行的時候是否正確的處理(以前提到字符串不能換行,遇到一個忽然的換行將會被當作缺乏雙引號):

 

鑑於詞法分析原本內容也很少,因此這篇文章也不會太長。相信有前途的程序員也會在這裏獲得一些編譯原理之外的知識。下一篇文章將會描述Tinymoe的函數頭的語法分析部分,將會描述一個編譯器的不帶歧義的語法分析是如何人肉出來的。敬請期待。

相關文章
相關標籤/搜索