文章中引用的代碼均來自https://github.com/vczh/tinymoe。 html
看了前面的三篇文章,你們應該基本對Tinymoe的代碼有一個初步的感受了。在正確分析"print sum from 1 to 100"以前,咱們首先得分析"phrase sum from (lower bound) to (upper bound)"這樣的聲明。Tinymoe的函數聲明又不少關於block和sentence的配置,不過這裏並不打算將全部細節,我會將重點放在如何寫一個針對無歧義語法的遞歸降低語法分析器上。因此咱們這裏不會涉及sentence和block的什麼category和cps的配置。 git
雖然"print sum from 1 to 100"沒法用無歧義的語法分析的方法作出來,可是咱們能夠借用對"phrase sum from (lower bound) to (upper bound)"的語法分析的結果,動態構造可以分析"print sum from 1 to 100"的語法分析器。這種說法看起來好像很高大上,可是其實並無什麼特別難的技巧。關於"構造"的問題我將在下一篇文章《跟vczh看實例學編譯原理——三:Tinymoe與有歧義語法分析》詳細介紹。 github
在我以前的博客裏我曾經寫過《如何手寫語法分析器》,這篇文章講了一些簡單的寫遞歸降低語法分析器的規則,儘管不少人來信說這篇文章幫他們解決了不少問題,但實際上細節還不夠豐富,用來對編程語言作語法分析的話,仍是會以爲複雜性過高。這篇文章也同時做爲《如何手寫語法分析器》的補充。好了,咱們開始進入無歧義語法分析的主題吧。 正則表達式
咱們須要的第一個函數是用來讀token並判斷其內容是否是咱們但願看到的東西。這個函數比較特別,因此單獨拿出來說。在詞法分析裏面咱們已經把文件分行,每一行一個CodeToken的列表。可是因爲一個函數聲明獨佔一行,所以在這裏咱們只須要對每一行進行分析。咱們判斷這一行是否以cps、category、symbol、type、phrase、sentence或block開頭,若是是那Tinymoe就認爲這必定是一個聲明,不然就是普通的代碼。因此這裏假設咱們找到了一行代碼以上面的這些token做爲開頭,因而咱們就要進入語法分析的環節。做爲整個分析器的基礎,咱們須要一個ConsumeToken的函數: express
做爲一個純粹的C++11的項目,咱們應該使用STL的迭代器。其實在寫語法分析器的時候,基於迭代器的代碼也比基於"token在數組裏的下表"的代碼要簡單得多。這個函數所作的內容是這樣的,它查看it指向的那個token,若是token的類型跟tokenType描述的同樣,他就it++而後返回true;不然就是用content和ownerToken來產生一個錯誤信息加入errors列表裏,而後返回false。固然,若是傳進去的參數it自己就等於end,那天然要產生一個錯誤。天然,函數體也十分簡單: 編程
那對於標識符和數字怎麼辦呢?明眼人確定一眼就看出來,這是給檢查符號用的,譬如左括號、右括號、冒號和關鍵字等。在聲明裏面咱們是不須要太複雜的東西的,所以咱們還須要兩外一個函數來輸入標識符。Tinymoe事實上有兩個針對標識符的語法分析函數,第一個是讀入標識符,第二個不只要讀入標識符還要判斷是否到了行末不然報錯: 數組
在這裏我須要強調一個重點,在寫語法分析器的時候,函數的各式必定要整齊劃一。Tinymoe的語法分析函數有兩個格式,分別是針對parse一行的一個部分,和parse一個文件的一些行的。ParseToEnd和ParseToFarest就屬於parse一行的一個部分的函數。這種函數的格式以下: 編程語言
除了函數格式之外,咱們還須要全部的函數都遵循某些前置條件和後置條件。在語法分析裏,若是你試圖分析一個結構可是不幸出現了錯誤,這個時候,你有可能能夠返回一個語法樹的節點,你也有可能什麼都返回不了。因而這裏就有兩種狀況: ide
當你根據這樣的格式寫了不少語法分析函數以後,你會發現你能夠很容易用簡單結構的語法分析函數,拼湊出一個複雜的語法分析函數。可是因爲Tinymoe的聲明並無一個編程語言那麼複雜,因此這種嵌套結構出現的次數並很少,因而咱們這裏先跳過關於嵌套的討論,等到後面具體分析"函數指針類型的參數"的時候天然會涉及到。 函數
說了這麼多,我以爲也應該上ParseToEnd和ParseToFarest的代碼了。首先是ParseToEnd:
咱們很快就能夠發現,其實語法分析器裏面絕大多數篇幅的代碼都是關於錯誤處理的,真正處理正確代碼的部分其實不多。ParseToEnd作的事情很少,他就是從it開始一直讀到end的位置,把全部不是標識符的token都扔掉,而後把全部遇到的標識符token都連起來做爲一個完整的標識符。也就是說,ParseToEnd遇到相似"the real 100 Tinymoe programmer"的時候,他會返回"the real Tinymoe programmer",而後在"100"的地方報一個錯誤。
ParseToFarest的邏輯差很少:
只是當這個函數遇到"the real 100 Tinymoe programmer"的時候,會返回"the real",而後把光標移動到"100",可是沒有報錯。
看了這幾個基本的函數以後,咱們能夠進入正題了。作語法分析器,固然仍是從文法開始。跟上一篇文章同樣,咱們來嘗試直接構造一下文法。可是語法分析器跟詞法分析器的文法的區別是,詞法分析其的文法能夠 "定義函數"和"調用函數"。
首先,咱們來看symbol的文法:
SymbolName ::= <identifier> { <identifier> }
Symbol ::= "symbol" SymbolName
其次,就是type的聲明。type是多行的,不過咱們這裏只關心開頭的同樣:
Type ::= "type" SymbolName [ ":" SymbolName ]
在這裏,中括號表明無關緊要,大括號表明重複0次或以上。如今讓咱們來看函數的聲明。函數的生命略爲複雜:
Function ::= ("phrase" | "sentence" | "block") { SymbolName | "(" Argument ")" } [ ":" SymbolName ]
Argument ::= ["list" | "expression" | "argument" | "assignable"] SymbolName
Argument ::= SymbolName
Argument ::= Function
Declaration ::= Symbol | Type | Function
在這裏咱們看到Function遞歸了本身,這是由於函數的參數能夠是另外一個函數。爲了讓這個參數調用起來更加漂亮一點,你能夠把參數寫成函數的形式,譬如說:
pharse (the number) is odd : odd numbers
return the number % 2 == 1
end
print all (phrase (the number) is wanted) in (numbers)
repeat with the number in all numbers
if the number is wanted
print the number
end
end
end
print main
print all odd numbers in array of (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
end
咱們給"(the number) is odd"這個判斷一個數字是否是奇數的函數,起了一個別名叫"odd numbers",這個別名不能被調用,可是他等價於一個只讀的變量保存着奇數函數的函數指針。因而咱們就能夠把它傳遞給"print all (…) in (…)"這個函數的第一個參數了。第一個參數聲明成函數,因此咱們能夠在print函數內直接調用這個參數指向的odd numbers函數。
事實上Tinymoe的SymbolName是能夠包含關鍵字的,可是我爲了避免讓它寫的太長,因而我就簡單的寫成了上面的那條式子。那Argument是否能夠包含關鍵字呢?答案固然是能夠的,只是當它以list、expression、argument、assignable、phrase、sentence、block開始的時候,咱們強行認爲他有額外的意義。
如今一個Tinymoe的聲明的第一行都由Declaration來定義。當咱們識別出一個正確的Declaration以後,咱們就能夠根據分析的結果來對後面的行進行分析。譬如說symbol後面沒有東西,因而就這麼完了。type後面都是成員函數,因此咱們一直找到"end"爲止。函數的函數體就更復雜了,因此咱們會直接跳到下一個看起來像Declaration的東西——也就是以symbol、type、phrase、sentence、block、cps、category開始的行。這些步驟都很簡單,因此問題的重點就是,如何根據Declaration的文法來處理輸入的字符串。
爲了讓文法能夠真正的運行,咱們須要把它作成狀態機。根據以前的描述,這個狀態及仍然須要有"定義函數"和"執行函數"的能力。咱們能夠先僞裝他們是正則表達式,而後把整個狀態機畫出來。這個時候,"函數"自己咱們把它當作是一個跟標識符無關的輸入,而後就能夠獲得下面的狀態機:
這樣咱們的狀態機就暫時完成了。可是如今還不能直接把它轉換成代碼,由於當咱們遇到一個輸入,而咱們能夠選擇調用函數,並且能夠用的函數還不止一個的時候,那應該怎麼辦呢?答案就是要檢查咱們的文法是否是有歧義。
文法的歧義是一個頗有意思的問題。在咱們真的實踐一個編譯器的時候,咱們會遇到三種歧義:
看一眼咱們剛纔寫出來的文法,明顯就是LookAhead=0的狀況,並且連左遞歸都沒有,寫起來確定很容易。那接下來咱們要作的就是給"函數"算first set。一個函數的first set,顧名思義就是,他的第一個token均可以是什麼。SymbolName、Symbol、Type、Function都不用看了,由於他們的文法第一個輸入都是token,那就是他們的first set。最後就剩下Argument。Argument的第一個token除了list、expression、argument和assignable之外,還有Function。所以Argument的first set就是這些token加上Function的first set。若是文法有左遞歸的話,也能夠用相似的方法作,只要咱們在函數A->B->C->…->A的時候,知道A正在計算因而返回空集就能夠了。固然,只有左遞歸纔會遇到這種狀況。
而後咱們檢查一下每個狀態,能夠發現,任何一個狀態出去的全部邊,他接受的token或者函數的first set都是沒有交集的。譬如Argument的0狀態,第一條邊接受的token、第二條邊接受的SymbolName的first set,和第三條邊接受的Function的first set,是沒有交集的,因此咱們就能夠判定,這個文法必定沒有歧義。按照上次狀態機到代碼的寫法,咱們能夠機械的寫出代碼了。寫代碼的時候,咱們把每個文法的函數,都寫成一個C++的函數。每到一個狀態的時候,咱們看一下當前的token是什麼,而後再決定走哪條邊。若是選中的邊是token邊,那咱們就跳過一個token。若是選中的邊是函數邊,那咱們不跳過token,轉而調用那個函數,讓函數本身去跳token。《如何手寫語法分析器》用的也是同樣的方法,若是對這個過程不清楚的,能夠再看一遍這個文章。
因而咱們到了定義語法樹的時候了。幸運的是,咱們能夠直接從文法上看到語法樹的形狀,而後稍微作一點微調就能夠了。咱們把每個函數都當作一個類,而後使用下面的規則:
對於每個函數,要不要用shared_ptr來裝則見仁見智。因而咱們能夠直接經過上面的文法獲得咱們所須要的語法樹:
首先是SymbolName:
其次是Symbol:
而後是Type:
接下來是Argument:
最後是Function:
你們能夠看到,在Argument那裏,同時出去的三條邊就組成了三個子類,都繼承自FunctionFragment。圖中紅色的部分就是Tinymoe源代碼裏在上述的文法裏出現的那部分。至於爲何還有多出來的部分,實際上是由於這裏的文法是爲了敘述方便簡化過的。至於Tinymoe關於函數聲明的全部語法能夠分別看下面的四個github的wiki page:
https://github.com/vczh/tinymoe/wiki/Phrases,-Sentences-and-Blocks
https://github.com/vczh/tinymoe/wiki/Manipulating-Functions
https://github.com/vczh/tinymoe/wiki/Category
https://github.com/vczh/tinymoe/wiki/State-and-Continuation
在本章的末尾,我將向你們展現Tinymoe關於函數聲明的那一個Parse函數。文章已經把全部關鍵的知識點都講了,具體怎麼作你們能夠上https://github.com/vczh/tinymoe 閱讀源代碼來學習。
首先是咱們的函數頭:
回想一下咱們以前講到的關於語法分析函數的格式:
咱們能夠清楚地看到這個函數知足上文提出來的三個要求。剩下來的參數有兩個,第一個是decl,若是不爲空那表明調用函數的人已經幫你吧語法樹給new出來了,你應該直接使用它。領一個參數ownerToken則是爲了產生語法錯誤使用的。而後咱們看代碼:
第一步,咱們判斷輸入是否爲空,而後根據須要報錯:
第二步,根據第一個token來肯定函數的形式是phrase、sentence仍是block,並記錄在成員變量type裏:
第三步是一個循環,咱們根據當前的token(還記不記得以前說過,要先看一下token是什麼,而後再決定走哪條邊?)來決定咱們接下來要分析的,是ArgumentFragment的兩個子類(分別是VariableArgumentFragment和FunctionArgumentFragment),仍是普通的函數名的一部分,仍是說函數已經結束了,遇到了一個冒號,所以開始分析別名:
最後就不貼了,是檢查格式是否知足語義的一些代碼,譬如說block的函數名必須由參數開始啦,或者phrase的參數不能是argument和assignable等。
這篇文章就到此結束了,但願你們在看了這片文章以後,配合wiki關於語法的全面描述,已經知道如何對Tinymoe的聲明部分進行語法分析。緊接着就是下一篇文章——Tinymoe與帶歧義語法分析了,會讓你們明白,想對諸如"print sum from 1 to 100"這樣的代碼作語法分析,也不須要多複雜的手段就能夠作出來。