一直很瞭解人們對於parser的誤解,但是一直都提不起興趣來闡述對它的觀點。然而我以爲是有必要解釋一下這個問題的時候了。我感受獲得大部分人對於parser的誤解之深,再不澄清一下,恐怕這些謬誤就要寫進歪曲的歷史教科書,到時候就沒有人知道真相了。node
首先來科普一下。所謂parser,通常是指把某種格式的文本(字符串)轉換成某種數據結構的過程。最多見的parser,是把程序文本轉換成編譯器內部的一種叫作「抽象語法樹」(AST)的數據結構。也有簡單一些的parser,用於處理CSV,JSON,XML之類的格式。程序員
舉個例子,一個處理算數表達式的parser,能夠把「1+2」這樣的,含有1
,+
,2
三個字符的字符串,轉換成一個對象(object)。這個對象就像new BinaryExpression(ADD, new Number(1), new Number(2))
這樣的Java構造函數調用生成出來的那樣。編程
之因此須要作這種從字符串到數據結構的轉換,是由於編譯器是沒法直接操做「1+2」這樣的字符串的。實際上,代碼的本質根本就不是字符串,它原本就是一個具備複雜拓撲的數據結構,就像電路同樣。「1+2」這個字符串只是對這種數據結構的一種「編碼」,就像ZIP或者JPEG只是對它們壓縮的數據的編碼同樣。數據結構
這種編碼能夠方便你把代碼存到磁盤上,方便你用文本編輯器來修改它們,然而你必須知道,文本並非代碼自己。因此從磁盤讀取了文本以後,你必須先「解碼」,才能方便地操做代碼的數據結構。好比,若是上面的Java代碼生成的AST節點叫node
,你就能夠用node.operator
來訪問ADD
,用node.left
來訪問1
,node.right
來訪問2
。這是很方便的。app
對於程序語言,這種解碼的動做就叫作parsing,用於解碼的那段代碼就叫作parser。編輯器
那麼貌似這樣說來,parser是編譯器裏面很關鍵的一個部分了?顯然,parser是必不可少的,然而它並不像不少人想象的那麼重要。Parser的重要性和技術難度,被不少人嚴重的誇大了。一些人提到「編譯器」,就跟你提LEX,YACC,ANTLR等用於構造parser的工具,彷彿編譯器跟parser是等價的似的。還有些人,只要據說別人寫了個parser,就以爲這人編程水平很高,開始膜拜了。這些其實都顯示出人的膚淺。函數
我喜歡把parser稱爲「萬里長征的第0步」,由於等你parse完畢獲得了AST,真正精華的編譯技術纔算開始。一個先進的編譯器包含許多的步驟:語義分析,類型檢查/推導,代碼優化,機器代碼生成,…… 這每一個步驟都是在對某種中間數據結構(好比AST)進行分析或者轉化,它們徹底不須要知道代碼的字符串形式。也就是說,一旦代碼經過了parser,在後面的編譯過程裏,你就能夠徹底忘記parser的存在。因此parser對於編譯器的地位,其實就像ZIP之於JVM,就像JPEG之於PhotoShop。Parser雖然必不可少,然而它比起編譯器裏面最重要的過程,是處於一種輔助性,工具性,次要的地位。工具
鑑於這個緣由,好一點的大學裏的程序語言(PL)課程,都徹底沒有關於parser的內容。學生們每每直接用Scheme這樣代碼數據同形的語言,或者直接使用AST數據結構來構造程序。在Kent Dybvig這樣編譯器大師的課程上,學生直接跳過parser的構造,開始學習最精華的語義轉換和優化技術。實際上,Kent Dybvig根本不認爲parser算是編譯器的一部分。由於AST數據結構其實才是程序自己,而程序的文本只是這種數據結構的一種編碼形式。性能
既然parser在編譯器中處於次要的地位,但是爲何還有人花那麼大功夫研究各類炫酷的parser技術呢。LL,LR,GLR,LEX, YACC,Bison,parser combinator,ANTLR,PEG,…… 製造parser的工具彷佛層出不窮,每出現一個新的工具都號稱能夠處理更加複雜的語法。學習
不少人盲目地設計複雜的語法,而後用愈來愈複雜的parser技術去parse它們,這就是parser技術仍然在發展的緣由。其實,嚮往複雜的語法,是程序語言領域流傳很是廣,危害很是大的錯誤傾向。在人類歷史的長河中,留下了許多難以磨滅的歷史性糟粕,它們固化了人類對於語言設計的理念。不少人設計語言彷佛不是爲了拿來好用的,而是爲了讓用它的人迷惑或者懼怕。
有些人假定了數學是美好的語言,因此他們盲目的但願程序語言看起來更加像數學。因而他們模仿數學,製造了各類奇怪的操做符,制定它們的優先級,這樣你就能夠寫出2 << 7 - 2 * 3
這樣的代碼,而不須要給子表達式加上括號。還有不少人喜歡讓語法變得「簡練」,就爲了少打幾個括號,分號,花括號,…… 但是由此帶來的結果是複雜,不一致,有多義性,難擴展的語法,以及障眼難讀,模棱兩可的代碼。
更有甚者,對數學的愚蠢作法執迷不悟的人,設計了像Haskell和Coq那樣的語言。在Haskell裏面,你能夠在代碼裏定義新的操做符,指定它的「結合律」(associativity)和「優先級」(precedence)。這樣的語法設計,要求parser必須可以在parse過程當中途讀入而且加入新的parse規則。Coq試圖更加「強大」一些,它讓你能夠定義「mixfix操做符」,也就是說你的操做符能夠鏈接超過兩個表達式。這樣你就能夠定義像if...then...else...
這樣的「操做符」。
製造這樣複雜難懂的語法,其實沒有什麼真正的好處。不但給程序員的學習形成了沒必要要的困難,讓代碼難以理解,並且也給parser的做者帶來了嚴重的挑戰。但是有些人就是喜歡製造問題,就像一句玩笑話說的:有困難要上,沒有困難,製造困難也要上!
若是你的語言語法很簡單(像Scheme那樣),你是不須要任何高深的parser理論的。說白了,你只須要知道如何parse匹配的括號。最多一個小時,幾百行Java代碼,我就能寫出一個Scheme的parser。
但是不少人老是嫌問題不夠有難度,因而他們不停地製造更加複雜的語法,甚至會故意讓本身的語言看起來跟其它的不同,以示「創新」。固然了,這樣的語言就得用更加複雜的parser技術,這正好讓那些喜歡折騰複雜parser技術的人洋洋得意。
程序員們對於parser的誤解,很大程度上來自於大學編譯原理課程照本宣科的教育。不少老師本身都不理解編譯器的精髓,因此就只有循序漸進的講一些「死知識」,灌輸「業界作法」。通常大學裏上編譯原理課,都是捧着一本大部頭的「龍書」或者「虎書」,花掉一個學期1/3甚至2/3的時間來學寫parser。因爲parser佔據了大量時間,以致於不少真正精華的內容都被一筆帶過:語義分析,代碼優化,類型推導,靜態檢查,機器代碼生成,…… 以致於不少人上完了編譯原理課程,記憶中只留下寫parser的痛苦回憶。
「龍書」之類的教材在不少人心目中地位是如此之高,被譽爲「經典」,然而其實除了開頭很大篇幅來說 parser 理論,這本書其它部分的水準其實通常般。大部分學生的反應實際上是「看不懂」,然而因爲一直以來沒有更好的選擇,它經典的地位真是難以動搖。「龍書」後來的新版我瀏覽過一下,新加入了類型檢查/推導的部分,但是我看得出來,其實做者們本身對於類型理論都是隻知其一;不知其二,因此也就無法寫清楚。
若是你想真的深刻理解編譯理論,最好是從PL課程的讀物,好比 EOPL 開始。我能夠說 PL 這個領域,真的和編譯器的領域很不同。請不要期望編譯器的做者可以輕易設計出好的語言,由於他們可能根本不理解不少語言設計的東西,他們只是會實現某些別人設計的語言。但是反過來,理解了 PL 的理論,編譯器的東西只不過是把一種語言轉換成另一種語言(機器語言)而已。工程的細枝末節很麻煩,但是當你掌握了精髓的原理,那些都容易摸索出來。
雖然我已經告訴你,給過分複雜的語言寫parser實際上是很苦逼,沒有意思的工做,然而有些歷史性的錯誤已經形成了深遠的影響,因此不少時候雖然心知肚明,你也不得不妥協一下。因爲像C++,Java,JavaScript,Python之類語言的流行,有時候你是被迫要給它們寫parser。在這一節,我告訴你一些祕訣,也許能夠幫助你更加容易的寫出這些語言的parser。
不少人都以爲寫parser很難,一方面是因爲語言設計的錯誤思想致使了複雜的語法,另一方面是因爲人們對於parser構造過程的思惟誤區。不少人不理解parser的本質和真正的用途,因此他們老是試圖讓parser幹一些它們原本不該該乾的事情,或者對parser有一些不切實際的標準。固然,他們就會以爲parser很是難寫,很是容易出錯。
儘可能拿別人寫的parser來用。維護一個parser是至關繁瑣耗時,回報很低的事情。一旦語言有所改動,你的parser就得跟着改。因此若是你能找到免費的parser,那就最好不要本身寫。如今的趨勢是愈來愈多的語言在標準庫裏提供能夠parse它本身的parser,好比Python和Ruby。這樣你就能夠用那語言寫一小段代碼調用標準的parser,而後把它轉換成一種經常使用的數據交換格式,好比JSON。而後你就能夠用通用的JSON parser解析出你想要的數據結構了。
若是你直接使用別人的parser,最好不要使用它原來的數據結構。由於一旦parser的做者在新版本改變了他的數據結構,你全部的代碼都會須要修改。個人祕訣是作一個「AST轉換器」,先把別人的AST結構轉換成本身的AST結構,而後在本身的AST結構之上寫其它的代碼,這樣若是別人的parser修改了,你能夠只改動AST轉換器,其它的代碼基本不須要修改。
用別人的parser也會有一些小麻煩。好比Python之類語言自帶的parser,丟掉了不少我須要的信息,好比函數名的位置,等等。我須要進行一些hack,找回我須要的數據。相對來講,這樣小的修補仍是比從頭寫一個parser要划得來。可是若是你實在找不到一個好的parser,那就只好本身寫一個。
不少人寫parser,很在意所謂的「one-pass parser」。他們試圖掃描一遍代碼文本就構造出最終的AST結構。但是其實若是你放鬆這個條件,容許用多pass的parser,就會容易不少。你能夠在第一遍用很容易的辦法構造一個粗略的樹結構,而後再寫一個遞歸樹遍歷過程,把某些在第一遍的時候無法肯定的結構進行小規模的轉換,最後獲得正確的AST。
想要一遍就parse出最終的AST,能夠說是一種過早優化(premature optimization)。有些人盲目地認爲只掃描一遍代碼,會比掃描兩遍要快一些。然而因爲你必須在這一遍掃描裏進行多度複雜的操做,最終的性能也許還不如很快的掃完第一遍,而後再很快的遍歷轉換由今生成的樹結構。
另一些人試圖在parse的過程當中作一些原本不屬於它作的事情,好比進行一些基本的語義檢查。有些人會讓parser檢查「使用未定義的變量」等語義錯誤,一旦發現就在當時報錯,終止。這種作法其實混淆了parser的做用,形成了沒必要要的複雜性。
就像我說的,parser其實只是一個解碼器。parser要作的事情,應該是從無結構的字符串裏面,解碼產生有結構的數據結構。而像「使用未定義的變量」這樣的語義檢查,應該是在生成了AST以後,使用單獨的樹遍從來進行的。人們經常混淆「解碼」,「語法」和「語義」三者的不一樣,致使他們寫出過分複雜,效率低下,難以維護的parser。
另外一種常見的誤區是盲目的相信YACC,ANTLR之類所謂「parser generator」。實際上parser generator的概念看起來雖然美好,但是實際用起來幾乎全都是噩夢。事實上最好的parser,好比EDG C++ parser,幾乎全都是直接用普通的程序語言手寫而成的,而不是自動生成的。
這是由於parser generator都要求你使用某種特殊的描述語言來表示出語法,而後自動把它們轉換成parser的程序代碼。在這個轉換過程當中,這種特殊的描述語言和生成的parser代碼之間,並無很強的語義鏈接關係。若是生成的parser有bug,你很難從生成的parser代碼回溯到語法描述,找到錯誤的位置和緣由。你無法對語法描述進行debug,由於它只是一個文本文件,根本不能運行。
因此若是你真的要寫parser,我建議你直接用某種程序語言手寫代碼,使用普通的遞歸降低(recursive descent)寫法,或者parser combinator的寫法。只有手寫的parser才能夠方便的debug,並且能夠輸出清晰,人類可理解的出錯信息。
有些人喜歡死扣BNF範式,盲目的相信「LL」,「LR」等語法的區別,因此他們常常落入誤區,說「哎呀,這個語法不是LL的」,因而採用一些像YACC那樣的LR parser generator,結果落入很是大的麻煩。其實,雖然有些語法看起來不是LL的,它們的parser卻仍然能夠用普通的recursive descent的方式來寫。
這裏的祕訣在於,語言規範裏給出的BNF範式,其實並非惟一的能夠寫出parser的作法。BNF只是一個基本的參照物,它讓你能夠對語法有個清晰的概念,但是實際的parser卻不必定非得按照BNF的格式來寫。有時候你能夠把語法的格式稍微改一改,變通一下,卻照樣能夠正確地parse原來的語言。其實因爲不少語言的語法都相似於C,因此不少時候你寫parser只須要看一些樣例程序,而後根據本身的經驗來寫,而不須要依據BNF。
Recursive descent和parser combinator寫出來的parser其實能夠很是強大,甚至能夠超越所謂「上下文無關文法」,由於在遞歸函數裏面你能夠作幾乎任意的事情,因此你甚至能夠把上下文傳遞到遞歸函數裏,而後根據上下文來決定對當前的節點作什麼事情。並且因爲代碼能夠獲得不少的上下文信息,若是輸入的代碼有語法錯誤,你能夠根據這些信息生成很是人性化的出錯信息。
因此你看到了,parser並非編譯器,它甚至不屬於編譯裏很重要的東西。程序語言和編譯器裏面有比parser重要不少,有趣不少的東西。Parser的研究,實際上是在解決一些根本不存在,或者人爲製造的問題。複雜的語法致使了複雜的parser技術,它們仍然在給計算機世界帶來沒必要要的困擾和麻煩。對parser寫法的不少誤解,過分工程和過早優化,形成了不少人錯誤的高估寫parser的難度。
能寫parser並非什麼了不得的事情,其實它是很是苦逼,真正的程序語言和編譯器專家根本不屑於作的事情。因此若是你會寫parser,請不要覺得是什麼了不得的事情,若是你看到有人寫了某種語言的parser,也不要表現出讓人啼笑皆非的膜拜之情。