淺談 編譯器 & 天然語言處理

==============================================java

copyright: KIRA-lznios

==============================================c++

轉載請註明出處,這篇是我原創,翻版必究!!!!!!!!!!!!!!!!!!!程序員

==============================================正則表達式

若是以爲寫個好,請留個言,點個贊。redis

 

最喜歡吳軍博士的一句話,和我本人的學習理念比較接近,因此對他的書也很是着迷:技術分爲術 和 道,術 是具體作事的算法,道是其背後隱藏的根本機理算法

就像吳軍博士說的那樣,sql

1.高大上的天然語言處理背後模型機理盡然如此簡單(固然細節不簡單)數據庫

2.怎麼像你奶奶解釋搜索引擎?其實搜索引擎的背後機理其實簡單的不能再簡單了,就是布爾運算!!!三句話就能講明白,一是下載儘量多的網頁,二是創建索引,三是根據相關性給網頁排序!沒了,這就是搜索引擎,任何智能的搜素引擎都逃不出布爾運算的框架。express

3…..

以我我的愚見,首先得深入理解道,而後再去發揚術會比較好。由於只有深入理解道,而後才能舉一反十!!!而後在你接觸新東西的時候,能對之前學的知識加以聯繫,發現其中的隱含機理的類似性。並能把一個領域的經典研究方法帶到另外一個研究領域。


先交代一下:

1.這是我第一篇,忽然想寫點有質量的文章,來和你們分享知識,寫的很差的地方歡迎拍磚。

2.本人寫過編譯器,編譯器根本不是什麼高大上的東西,本質就是一種數據(信息/語言)處理的方法而已,和處理其餘數據同樣,並和處理天然語言進行對比

3.下一篇是關於學完編譯器以後,應該掌握的技能,即進階信息安全的基礎:

關於一段c/c++代碼,編譯以後,生成怎麼樣的x86,calling convention,prolog/epilog,caller-saved/callee-saved register,堆棧平衡,全部變量的內存分佈,函數符號修飾成什麼樣,靜態連接,動態連接,地址修正,連接指示對編譯過程的影響,如dllimport,dllexport,#pragma,函數聲明順便提一下連接器,以及windows下病毒的運行機理,我不會重點寫什麼是動態連接,而是解釋爲何動態連接,及其背後隱藏的緣由

4.下一篇關於OO object model,本人對OO有必定了解,封裝,繼承(單一,多重,怎麼解決菱形多重繼承數據二義性問題,微軟怎麼解決?gcc怎麼辦?分析咱們用的 prefix算法 實現對象模型的繼承 ,並給出拓展),多態,這篇以c++爲基本,講述c++ object model,並給出c++爲何轉換指針會變化(Base* b = new Derived();編譯器怎麼理解對象模型的,怎麼就能多態??對象模型長成什麼樣,怎麼樣會形成覆蓋,遮蔽?和多態在對象模型上有什麼區別?遮蔽,覆蓋爲毛就不能多態了?),並分析一下c++對象模型優缺點,容易受到什麼攻擊(堆溢出,堆噴射),雖然hook 函數指針本質不是c++語言自己形成的。。可是c++對象模型若是對於你們都是好人的狀況下,是很優秀的對象模型,but。。。

5.下一篇準備寫關於高級(多核)操做系統內核的理解,固然是基於MIT的 xv6 和 Yale的pios ,關於 Vitrual Memory:邏輯地址->線性地址->物理地址, fork/join/ret ,copy-on-write…..

6.再下一篇多是關於 內存數據庫 新存儲方式的新實現(本人拍腦殼想的),並和 sqlite3,nosql,redis 等內存數據庫進行 性能,實現方式 的比較

 

本文參考了數學之美,編譯器(虎書 和 龍書),和在USTC老師教的,加上我本身寫編譯器過程的理解,

最後加上我本身設計的final project:code generation(minijava->x86,AT&T,IA32)

 

本人花了一個學期的時間,認真的寫了一個編譯器,差很少由如下部分組成:

miniJava compiler ->

implement: lexer,parser,AST,elabrator,garbage collector(Gimple algorithm),

                 code generation(minijava->C), code generation(minijava->java bytecode),

                 code generation(minijava->x86,AT&T,IA32),

                 object model(encapsulation,single inherit,polymorphism)

theory: exception,closure,SSA(static single assignment),

           register allocation(graph coloring)

optimiztion:CFG(liveness analysis,Reaching Definition analysis),DFG,SSA, Lattice, register allocation(graph coloring)

 

寫本文的目的:

寫完編譯器,發現編譯器更多的是一種數據處理的方法,而不是什麼高大上的東西,我寫這篇文章的目的,是想任何讀完我文章的人,知道編譯器到底在幹嘛,編譯器到底能幹些什麼?學了編譯器以後有神馬好處?學完編譯器應該掌握什麼技能???

 

我會不斷提出問題,引起讀者的思考,我喜歡有邏輯的思考問題,但願這樣能讓文章更有邏輯性。

並且我寫東西,不喜歡記流水帳,好比這個應該怎麼怎麼樣,而是寫爲何要這樣,我喜歡搞清楚其背後的緣由。

本文可能會很長,我會從背後隱含的原理去寫,而不去探討高大上的技術。

好了,廢話很少說,正文開始。

 

1. 先說說天然語言處理吧(本人不是很懂),一些基本概念,懂行的人直接跳過,謝謝。

a.首先古老的文明爲何會出現文字?

由於文字僅僅是信息的一種載體,意圖仍是想把信息記錄下來,本質仍是信息,古代沒有文字的時候,人們好比到了冬天冷,會發出一些 ,"嗖嗖"的聲音,肚子餓了,會發出一些什麼什麼聲音,而後因爲聲音太多,信息太多,人們沒法記住,也沒法統一,如此纔出現了文字,沒有爲何,就是由於沒有人能記住全部的聲音,

這樣就須要一種文字,去記錄那些信息。

b.有了文字,就必定會有語句,N個文字用不一樣的語法規則去拼湊生成的語句,不一樣的語法規則,生成不一樣的語言,這個很好理解。

c.隨着文明的發展,信息愈來愈多,可是文字的數量不能成倍的增加,不然也不便於記憶,這樣就出現了聚類,把一些相同概念的意思,概括用一個字(詞)去表示,好比一次多義,"日"表示太陽,表示太陽早上從東邊升起,從西邊落下,因此又能夠表示一天,等等。

第c條就是所謂的一詞多義,絕對是困擾古今中外語言學者,包括計算機科學家的一個大問題,也就是理解這個詞的意思,須要參照上下文(context)

d.常識

The pen is in the box.

The box is in the pen.

第一句正常人都懂,第二句有點坑了,不過外國人很容易理解,因爲外國人的常識,經驗,因此外國人立馬就明白,第二句的pen的意思是圍欄。

天然語言處理,想分析語句的語義就又多了一坑。

 

其實我就是想說 c 和 d 是基於 編譯器技術的 lexer+parser分析 天然語言的語義 上的一個大坑, 這個就是困擾計算機科學家,語言學家多年,以及阻礙處理天然語言的緣由之一。

 

 e.爲何要分詞?

像英語這種基本不須要,由於空格就是活生生的分隔符(可是對於手寫識別英文,空格不明顯,仍是須要分詞的),可是對於 中,日,韓,泰 等語言,好比 今天我學會了開汽車,中間沒有分隔符,因此須要分詞。

分詞其實也是一坑,好比:

此地\安能\居住,其人\好不\悲傷

此地安\能居住,其人好\不悲傷

 

2.爲何要扯天然語言處理,這個和編譯器到底有什麼關係?

聽我慢慢道來。

天然語言處理,其實就是處理好比,今天\我\學會了\開\汽車。 you \ are \ so \ cool.

而基於編譯器技術的 lexer + parser ,則也是同樣, 今天\我\學會了\開\汽車,不過一般是處理計算機語言,相似,static void main(string[] args)等等。

 

so:

a.天然語言處理,處理的是天然語言,好比上面舉得例子,The box is in the pen. 定義的上下文相關文法,即其中詞語的意思不能肯定(一次多義),須要結合相應的語境才能知道pen的意思,和你們作的英文完形填空是差很少的。

b.編譯器的如java語法,static void main()定義的是上下文無關文法,注意,上下文無關文法的好處就是,只要你定義的好,不會發生歧義,由於不存在一次多義,稍稍舉個小例子。

exp -> NUM

      -> ID

      -> exp + exp

      -> exp * exp

碰見exp就能夠無條件分解爲後面這四種狀況,而後再不斷的遞歸降低(recursive decendent parser/ top-down parsing/predicative parsing )迭代,解析語句。

爲何說只要定義的好呢?由於咱們lab用的是ll(k),也就是說,只支持從左到右parser,若是出現左遞歸就會出現永遠循環下去,由於是無條件分解。

定義左遞歸上下文無關文法坑:

a.左遞歸->右遞歸

b.歧義->提取公因式

一些其餘編譯器應該支持lr(k)

到這裏看不懂不要緊,這裏只是隨便提提。

 

我只是想說,像編譯器編譯的 c/c++/java...,包括sql語句,都是上下文無關文法,均可以用基於編譯器的技術,lexer + parser 去解決問題

 

ok,有的人就要問了,那爲何基於編譯器的技術,lexer + parser 把天然語言,先分解爲一系列的token,以後生成語法樹,而後用llk or lrk 去遍歷這棵樹,而後進行 語義分析, 爲何不能很好的處理天然語言?

誤區:本來科學家覺得,隨着語言學家對天然語言語法的歸納愈來愈完備,計算機計算能力又在逐漸提升,基於編譯器的技術應該可以很好的解決天然語言處理。

but:一條很簡單的上下文相關的語句,卻能分析出很龐大複雜的 AST(parser 返回結果是 語法樹), 若是再複雜一點,基於語法樹的分析根本行不通。

考慮一句很長的文言文,此處省略100字。

結論:因此說,基於編譯器技術的lexer + parser 只適合解決上下文無關文法 定義出的語言。

 

那上下文無關文法 就不能定義 天然語言了??要不試試看?

反正我不試。。緣由以下:

a.想要經過上下文無關文法定義漢語的50%語句,語言學家不只會累死,並且因爲一詞多義,須要結合語境,因此還要在文法裏定義各類語境,能夠想象那個工做量  嗎

b.定義的上下文無關文法越多,越容易出現歧義(提取公因式),並且會出現左遞歸(改爲右遞歸),如此,如此,會瘋掉的。因此 沒法涵蓋全部語言語法不說,還有歧義,這個是要作成實際應用的,這樣能忍嗎?

 

如此說來,20世紀50年代到70年代,用  基於編譯器技術 lexer + parser 分析天然語言的語義,絕對是科學家們走的彎路。

直到20世紀70年代,纔有先驅從新認識這個問題,基於數學模型和統計,天然語言處理進入第二個階段。

 

再總結一下結論: 基於編譯器技術 lexer + parser 分析語言的語義, 只適合 上下文無關文法, 而上下文無關文法 沒法(不容易)定義 天然語言,so,不能用lexer + parser 去分析天然語言的語義。

 

3. 那到底怎麼處理天然語言呢?(本文不會詳細寫怎麼處理,只寫基本原理),懂行的請自覺跳過,謝謝。

規則統計,用數學的方法去描述語言規律。

注意,統計語言模型的產生初衷是爲了解決語音識別問題,也就是說 一句話,讓你分析,這句話究竟是不是具備正確意義的天然語言。

用統計的思想思考:一個句子,由特定的單詞串組成,s = w1,w2,...,wn ,一個句子有意義的機率是 P(s) ,

由條件機率很容易獲得 P(s) = P(w1) * P(w2 | w1) * ..... * P(wn | w1,w2,...,wn-1)

只要算出這個語句有意義的機率,不就能判斷到底這句話有木有意義了呢

可是越到後面這個條件機率越難算了,怎麼破?

不要緊,馬爾可夫爲咱們想了一個偷懶並且頗爲有效的方法就是,假設 一個詞 wi 出現的 機率 只和它前面的那個詞 wi-1 有關係,

因此公式就簡化爲 P(S) = P(w1) * P(w2 | w1) * P(w3 | w2) *  .....  * P(wn | wn-1)

固然,這個模型,不少人第一次見到,確定會問,就這東東,能分析這麼難文法的天然語言。。。。嗎?

答案是確定的,Google 的 羅塞塔 系統,僅僅開發2年,就是基於相似這種數學統計模型,就一舉成名的得到了 NIST 評測的第一。

 

固然,對於高階一點的語言模型,其餘模型,模型的訓練,零機率問題,我在本文不想深刻討論,討論的重點,主要仍是想放在編譯器上面。

 

一點點思考:

說到這裏,說一點題外話。本人還寫過內存數據庫,因此,須要支持sql語句,爲sql語句也寫過 lexer 和 parser,用的也是上下文無關文法。

考慮若是sql語言,若是發展足夠強大,就像天然語言同樣,語法愈來愈多,會不會出現 聚類(一詞多義) ?若是出現聚類,那根據個人結論,

lexer + paser這種方法不work了,那是否是得用到 天然語言處理的 某些方法,或者其餘方法???

因爲目前的語言c/c++/java/sql 仍是處於上下文無關文法就能夠定義的語言,有個度(界限)的問題,若是跨越到天然語言,則之前的方法根本不能用了,是否是得考慮新的技術。

嘖嘖,隨便說說。

 

4.關於天然語言處理 和 編譯器相關技術處理 的淺薄關係 在上面已經說過了,接下來就是我要講清楚,編譯器到底在幹什麼?

我以前說過,編譯器也是對一種語言的處理過程,因此上文和天然語言處理進行了對比,而後引起了一點點小思考。

 

ok,書上說編譯器就是把高級語言翻譯成低級語言,忘了,書上好像是這麼寫的。

 

不過我理解的編譯器應該是這樣,

a. 編譯器會通過 lexer + parser + elabraor + code generation : IR(N種)  for optimization + 可能還連接一個garbage collector 

    ->而後生成object file(目標文件),注意目標文件仍是不能運行,可是就差那麼一點點,這一點點是什麼(對於外部符號,編譯器不知道,只能進入符號表,等待連接器來修正)?

    好比你  cl /c main.c  這樣只編譯不連接,若是出現編譯器不認識的符號,不要緊,反正生成目標文件,那些符號就進入了符號表,等連接器下一步工做。

    可是你 cl main.c ,這樣既編譯又連接,若是有不認識的符號,直接報錯(假設你其餘目標文件也木有這個符號)

總結:等連接器,把其餘的目標文件link到一塊兒(主要是地址修正),而後生成可執行文件(靜態連接/動態連接/動態連接靜態加載/動態連接動態加載,不同), 這樣就生成了可執行文件 .exe / a.out ... 芯片上跑去吧

詳細細節留給下一篇吧,要寫就停不下來。。。

    

b. 編譯器確實是把高級語言翻譯成低級語言,可是其中會通過不少種IR(中間代碼),大部分緣由是由於優化,像gcc就通過N種優化,而後生成一個最簡的x86機器碼,而後跑在intel的芯片上,固然ARM,MIPS均可以。。。固然你翻譯成java的bytecode ,在虛擬機上跑,都是能夠的。

IR嘛,舉個例子,好比

第一步我就要對AST進行優化,優化一般有 常量摺疊,代數簡化,標量代替聚量, 常量傳播,拷貝傳播,死代碼刪除,公共子表達式刪除等等

ne.g.: a = 0+b    ==>  a = b
ne.g.: a = 1*b    ==>  a = b
ne.g.: 2*a          ==>  a + a
ne.g.: 2*a          ==>  a<<1         (strength reduction)
ne.g.: *(&a)       ==>  a
看吧,常量摺疊很好理解吧,就是直接把AST給改了,
這裏暫時不討論其餘優化。
note:
像a++這種,咱們稱之爲 "語法糖" 的東西,可能不是在優化器裏把改成 a = a + 1; 可能在 parser 裏面看到 就直接改了,呼呼。
固然 a += 1,也是赤裸裸的  "語法糖"
第二步,我可能要通過CFG(control flow graph)(SSA在後面討論)
那就要把IR翻譯成三地址碼,而後以跳轉爲分界線,把不跳轉的部分組織成塊,最後組織成圖形結構
第三步,多是DFG優化,利用數據流方程進行優化
第四步,多是活性分析,寄存器分配(圖着色)
第五步,多是基於離散數學,格(Lattice)的優化
第N步, 等等 。。。。。。
 
note:小插曲:
編譯器優化程序員永不失業理論 : 由於沒有一種優化可以老是起好的做用(視具體狀況而定),因此任何一種算法都不能把全部程序化簡到最簡。。。因此。。
但有一種優化總能起好的做用,嘻嘻,那就是寄存器分配(前提是你得有幾個寄存器。。。),必定會讓你的程序變快。
 
要再也不專門寫一篇,討論編譯器優化,我有不少話想寫。
 
算了,詳細不作討論,在這裏只想說,不少編譯器爲了進行優化,生成好多好多種IR,在每一層IR上進行不一樣的優化,就是爲了用數學的方法,去不斷簡化程序員寫的代碼,由於不一樣的IR對不一樣的優化有不一樣的功效
 
->目的很明顯,就是爲了生成最優的機器碼
 
 
note:爲何編譯器要分 lexer + parser + elabraor + code generation 這麼多層? 合併幾層不行嗎?
         答案是固然能夠,不過爲何這樣分,有它的道理,緣由就是模塊化,好比,有專門的人研究lexer,好比有Flex等工具,專門的人研究parser。。。但一層一層向下,向上暴露的接口固然是一致的,這樣的好處就是,能夠專門研究某一層算法,而後直接替換某一層。
 
初窺了什麼是編譯器,接下來,我想一步一步分析,到底lexer + parser + elabraor + code generation 這麼多層,每層編譯器在搞什麼?
 
a. lexer
 
b. parser
 
c. elabrator
 
d. code generation
 
e. 討論 exception,closure,SSA(static single assignment) 是怎麼樣實現的
 
f.  討論一下garbage collector
 
g. optimization
 
h. 關於咱們學校課程最後的 final project 思考,關於 java 反射機制,gc的世代收集,翻譯成 go / js ,jvm ,還有本人寫的 minijava 直接 -> x86,我幾回推翻重寫,不過最後完成了,仍是很 happy (minijava沒有很高深的java語法,僅僅是封裝,繼承,多態,我用x86模擬了而已)
 
a. lexer -> translates the source program into a stream of lexical tokens
 
輸入: source program
輸出: a stream of lexical tokens
 
 
先舉個通俗易懂的例子,好比我要對如下java程序進行 lexer,怎麼作 ?

class TreeVisitor{

    public static void main(String[] a){

         System.out.println(new TV().Start());

    }

}

lexer的輸出,很明顯是,a stream of lexical tokens :class | TreeVisitor | { | publicstatic | void | main | ( | String | [ | ] | a | ) | {System | . | out | . | println | (new | TV | ( | ) | . | Start | ( | ) | ) | ; | } | }

 

 

看一下 Token結構體長成神馬樣子?

class Token{

public Kind kind; // kind of the token

public String lexeme; // extra lexeme for this token, if any

public Integer lineNum; // on which line of the source file this token appears 目前能夠忽略,只是爲了輸出

 ......}

 

看下輸出,你們就明白了:

TOKEN_CLASS: class : at line 5

TOKEN_ID: TreeVisitor : at line 5

TOKEN_LBRACE: <NONE> : at line 5

TOKEN_PUBLIC: public : at line 6

TOKEN_STATIC: static : at line 6

TOKEN_VOID: void : at line 6

TOKEN_MAIN: main : at line 6

TOKEN_LPAREN: <NONE> : at line 6

TOKEN_STRING: String : at line 6

TOKEN_LBRACK: <NONE> : at line 6

TOKEN_RBRACK: <NONE> : at line 6

TOKEN_ID: a : at line 6

TOKEN_RPAREN: <NONE> : at line 6

TOKEN_LBRACE: <NONE> : at line 6

..................

 

ok,分解爲了 a stream of lexical tokens ,很明顯用一個 隊列 去存儲它們。

note:

很是建議用隊列去存儲,爲何?

1.咱們lab用的是直接在parser裏面一個一個直接讀取lexer分解出來的Token,即不能回滾,即上一個Token還得用一個value記錄下來,固然你能夠

定義回滾幾個,而後記錄 rollbackToken1,rollbackToken2,rollbackToken3....等

2.用隊列雖然浪費了存儲空間,可是能夠任意回滾任意個數的Token

so,建議看具體須要。

神馬狀況下會遇到回滾Token?

好比,

MyVisitor v ;

root = new Tree();

因爲是遞歸降低分析(在paser中詳細討論,看完paser再回來理解),只能像微軟的編譯器同樣,寫c語言的時候,定義放在語句前面,若是你在中間某個地方寫了,int a = fun(1,2);則微軟編譯器會報錯,可是一個這樣的小錯誤,微軟的優化器會爆出各類錯。。。讓你根本就不知道哪錯了

回到正題:因爲和c語言同樣,本編譯器算法是,前面是定義,後面是語句。

so,檢測到root 的時候 Token是個ID,沒問題,可是後面發現Token 是 = 號,也就是你進入 定義和語句的 臨界區域了,so,你的代碼還在分析定義的代碼裏,怎麼破?你得回滾,而後跳出整個 分析 定義的代碼,進入分析語句的代碼,而後 current Token 得回滾到 root (原來在=)。

 

note:

可是gcc支持語句中有定義,不是由於 ANSI c 支持,而是gcc進行的拓展。

gcc怎麼實現的?其實很容易,和c++/java 同樣,加減符號表運算便可

gcc的c還支持bool呢,呼呼。

note:

吐槽微軟編譯器:

void fun(){}

這樣的空函數,微軟還不優化,

fun:

    push ebp

    mov ebp,esp 

    push ebx

    push esi

    push edi

這三個是 callee-saved 寄存器,微軟還要入棧保存,是否是有點懶了,別說寄存器分配了,若是 寄存器分配(好比圖着色) 只用到一個寄存器,這樣入棧保存一個不就好了嗎?

note:算了,仍是表揚下微軟的編譯器吧,好比你看到,

   push ebp 

   mov ebp,esp

   push ecx    // 而不是  sub esp,4

爲何不用sub esp,4 ? 。。。。。。。這個緣由很深入,由於,一樣是往下開闢4個byte, push ecx 用的(指x86,ARM不知道)機器碼更少哦

    

 

其實我是想解釋,爲何 lexer 要 translates the source program into a stream of lexical tokens ?而不分解爲其餘結構 ?

想一想中文爲何要分詞? eg,今天我學會了開汽車,你用指針去掃源代碼的時候,掃到 unicode  "今" ,你能把它做爲一個Token嗎?明顯不行,由於"今天"纔是一個Token。。。那怎麼樣斷句呢?即,怎麼分詞呢?最簡單的方法就是查字典,這種方法最先是由北航的梁南元教授提出的。即,字典裏有的詞就表示出來,遇到複合詞就最長匹配。

可是最長匹配也有問題。

好比, 上海大學城書店,你怎麼分?

最長匹配是: 上海大學/城/書店?

顯然不對,應該是 上海/大學城/書店

這裏不進一步討論。

 

好了,以前說過,像英文這樣 I am so cool. 語句之間有標點符號,語句之中有空格,因此,不須要分詞,Token很容易找到!!!!!!

代碼也是這樣,大部分是有分隔符(以空格分開)的,可是也有例外,好比,

/

//

/*

遇到一個/,你能武斷說這個Token是 / 嗎?嗯,得看看後面跟的是啥。

 

回到正題,爲何要分解爲a stream of lexical tokens?

由於好比天然語言是由一個一個單詞組成的,單詞組成的順序,則是語法。 

只有先把一個一個單詞分解出來,而後去分析每一個單詞之間爲何這樣排列(這就是分析這句話是神馬語法 -> 找出它的語法規則 ),而後生成一棵語法樹,存儲起來。

分詞就是lexer乾的事情,它的輸出就是給 parser 的輸入,parser 則負責生成 AST(抽象語法樹),並傳給 elabrator。

 

note:

說道分詞,編譯器技術已經完美解決了這個問題(僅僅針對上下文無關文法),即用 正則表達式。 NFA -> DFA

我不想延伸,由於內容太多,之後有機會再寫。

 

固然lexer有不少,好比 flex, sml-lex, Ocaml-lex, JLex, C#lex ...... 

 

說道這裏,lexer我是否已經講清楚了呢??我以爲差很少了,之後有機會補充。

 

 b. parser -> 根據 遞歸降低 分析算法,生成語法樹 

note:
recursive decendent parser/top-down parsing/predicative parsing  這幾個單詞是一個算法,都是遞歸降低分析
 
想了想怎麼來講這個parser,我想我仍是舉個實例比較容易理解! 我不喜歡把一個很簡單的東西,用不少數學公式去弄的很複雜,我以爲作學術,反而應該把複雜的東西,簡單化,這樣讓更多人能看懂其背後的機理其實很easy。
 
先隨手寫段程序好了, 不要在乎語義上的細節,只是爲了說明parser工做機制。

class TreeVisitor{

    public static void main(String[] a){

          System.out.println(new Visitor().Start());

    }

}

class Visitor {

    Tree l ;

    Tree r ;

    public int Strat(Tree n){

    int nti ;

    int a;

    while(n < 10)

        a = 1;

    if (n.GetHas_Right())

        a = 3;

    else

          a = 12 ;

    return a;      

    }

}

遞歸降低,能夠用一個詞來來歸納,其實就是 while循環

若是說要返回一個AST,這樣固然須要先定義全部抽象語句的類,而後生成其對象,而後reference相互連起來,造成一棵樹。

parser 輸出返回一棵AST -> theAst = parser.parse();

 

ast.program.T prog = parseProgram();

.......

ast.mainClass.MainClass mainclass = parseMainClass();

java.util.LinkedList<ast.classs.T> classes = parseClassDecls();

...... 

java.util.LinkedList<ast.classs.T> classes = new java.util.LinkedList<ast.classs.T>();

ast.classs.T oneclass = null;

while (current.kind == Kind.TOKEN_CLASS) {

      oneclass = parseClassDecl();

      classes.add(oneclass);

    }

注意,我爲何說,遞歸降低就是while循環,上面漂綠的字體很明顯了,當你分析某一種語法的時候,不斷用while探測,若是進入下一個語法,則跳出while循環。

再說細一點:

int nti ;

int a;

while(n < 10)

    a = 1;

if (n.GetHas_Right())

    a = 3;

else

    a = 12 ;

函數開始的時候,先分析 "定義" ,分析到 int nti; 沒問題,是 "定義" ,而後到 int a; 也沒問題,是 "定義"。

可是到了 while 語句,則 編譯器代碼跳出 分析 「定義」 的代碼,進入 分析 "語句" 的代碼。

注意一點便可,我上面舉得例子。

MyVisitor v ;

root = new Tree();

 

 

OK,返回了AST,好辦了,能夠直接 pretty print 出來了,由於你已經有了AST,即一棵樹,全部這段程序的語義都存儲起來了,你想怎麼打印,

不就怎麼打印了?

好比:

  @Override

  public void visit(ast.stm.If s)

  {

    this.sayln("");//if語句前換個行先

    this.printSpaces();

    this.say("if (");

    s.condition.accept(this);

    this.sayln(")");

    this.indent();

    s.thenn.accept(this);

    this.unIndent();

    this.printSpaces();

    this.sayln("else");

    this.indent();

    s.elsee.accept(this);

    this.unIndent();

    return;

  }

 
note:
這裏用的是訪問者模式,這裏不作討論。
 
note:
so,對於paser,能夠pretty print,因而可知,相似vim / emac 等編輯器,是怎麼樣智能的處理了? 本人本身想的其中一種方法。
好比文件裏有一段code,而後lexer + parser 以後生成了AST,而後修改AST,再把AST打印出來不就好了!!!!固然這個只是其中一種實現方式,
具體vim / emac 怎麼實現的,我沒看過源碼,不知道,我只是給出了一種我本身的想法。
 
parser,就是分析語法(你本身定義的語言的上下文無關文法),而後返回一顆語法樹,存儲起來,而後傳給elabrator。
 
關於parser,我應該說清楚了吧???
 
 
c. elaborator -> 其實就是語義分析,這裏被稱爲  「精細化」  ,其實本質是 type checking,我日常就稱之爲類型檢查。
 
工做職責:好比,看看類型,好比會不會出現相似 string = int + char ?  好比function call會不會調用參數個數是否是多一個,類型和聲明的一不同,不然報錯,高級一點的elaborator,還會看看哪些變量,聲明瞭,卻沒有用到,而後報出一個警告,等等。。。。。。。。。。。。。。
 
傳統的語義分析方法:
Traditionally, semantics takes the form of natural language specification
 
可是最新的論文,證實能夠用數學的方法來完美解決這個技術:
But recent research has revealed that semantics can also be addressed via math, it's rigorous and clean
 
這篇論文的題目是 -> Quick Introduction to Type Systems   ->  type judging
 
舉個例子
 
 
上面是假設條件, hypothesis
類型系統是能夠計算的, 好比 int + int -> 若是返回 int ,則 juding 正確

理論聯繫下實際:

 假設定義語義 : int + int -> int

  @Override

  public void visit(ast.exp.Add e)

  {                       

    e.left.accept(this);

    if (!this.type.toString().equals("@int"))

       error("operator '+' left expression must be int type",e.addleftexplineNum);

    ast.type.T leftty = this.type;

    e.right.accept(this);

    if (!this.type.toString().equals("@int"))

        error("operator '+' right expression must be int type" ,e.addrightexplineNum);

    this.type = new ast.type.Int(); //表示當前操做 add,完成以後,「返回」一個操做數類型爲 int

    return;

  }

 

 

note:

這裏不討論關於繼承(多態),function call等再難一點的語義分析,不是本文重點。

 

OK, elabrator 的工做,總結下,就是先掃一遍AST,而後生成相應的符號表(多態涉及prefix算法計算繼承後的對象模型中虛函數表的函數指針排列順序,這裏不討論),而後進行類型系統的判斷,報出一些語句出錯的信息,或者警告信息。

 

elabrator我是否已經講清楚了呢?

 

d. code generation -> 生成 IR

本人作了 minijava -> java bytecode / c / x86

minijava 直接 -> x86,我幾回推翻重寫,不過最後完成了,仍是很 happy (minijava沒有很高深的java語法,僅僅是封裝,繼承,多態,我用x86模擬了而已)

仍是有必定難度的,用匯編這種低級語言去模擬封裝,繼承,多態,仍是有必定難度的,放在之後討論吧,寫不完了。

 

e. 討論 exception,closure,SSA(static single assignment) 是怎麼樣實現的

 

exception:其實編譯器一般有2種方法,

1.基於異常棧

2.基於異常表:pay as you go

細節,不想在本文討論了。

 

closure: 我會討論在java非要支持nested function以後,一步一步逃逸變量是怎麼樣不可以存儲,而後引出closure的解決方法的,還會給出closure 和 object model 有什麼區別?

 

SSA(static single assignment) 真心是一種牛逼的IR,讓不少優化變得很是簡單。可是內容太多,寫不完了。自從有的這個SSA,gcc版本從某一個版本,忘了,開始所有把基於 CFG , DFG 的優化,變成SSA了

 

f.  討論一下garbage collector
 
gc實際上要有很大的篇幅去討論,基本上有這幾種,我來數數:
1.基於引用計數的gc(浪費一個int大小要去存引用計數)
2.基於微軟的 mark & swap ,mark有個很trick的技巧
3.copy收集 ,咱們lab作的,基於tiger book,13.3節
4.世代收集,關於代數,每代大小的閾值討論,給予一個可靠的理論分析,本人喜歡用 平攤分析中的,基於動態表(其實就是c++的new)的方法
5.併發的gc
 
仍是須要花大篇幅去討論,這裏不說了。
 
g. optimization
 
優化太多了,真是說不完啊,暫時不想寫了。對於優化容易出錯,或者直接違反語義的,極端激進的死代碼刪除等等,老師說,有句名言叫作
1.請不要優化
2.實在想優化,請參考第1條
哈。
 
h. 關於咱們學校課程最後的 final project 思考,關於 java 反射機制,gc的世代收集,翻譯成 go / js ,jvm ,還有本人寫的 minijava 直接 -> x86,我幾回推翻重寫,不過最後完成了,仍是很 happy (minijava沒有很高深的java語法,僅僅是封裝,繼承,多態,我用x86模擬了而已)
 
這個final project 實在是寫不下,可能要花很長時間,才能把我是怎麼想的寫出來,先這樣吧。。。
 

5. 用編譯器知識理解語言小細節

1.好比到底應該寫成 char* p; 還應該寫成 char *p; 這種問題其實很好理解,爲何,編譯器怎麼處理指針? 即,碰到類型後面碰到*,就把後面的變量當作指針,好理解了嗎,這就是爲神馬 char* p1,p2;  p2不是指針的緣由

我我的喜愛,就把 char* 當作一個類型,只須要注意 char* p1,p2;  p2 這種狀況便可,不少人不是喜歡這樣寫typedef char* pchar嗎,這不就是赤裸裸的認爲char*就是一個類型嗎?沒錯,我就喜歡把它當作一個類型。

 

2.好比 const 修飾的 變量,老是分不清 ,

 const char* p;

 char const* p;

 char* const p;

 const char* const p;

我說一句話,你就能永遠分清,信不信?固然這個是我從effective c++裏面學的,

const 出如今*左邊修飾的是指針指向的value,而出如今右邊則是修飾的是指針,

沒錯,你已經會了。

前面2個一個意思,都是修飾value是const,第三個是修飾指針是const,第四個是2個都修飾。

 

3.好比神馬 前加加,後加加 ,搞不清楚  ++a;a++;....

int a = 1;

printf("%d",a++); 爲毛答案仍是1 ?

 

int a = 1;

printf("%d",++a); 爲毛答案就是2了 ?

 

你若是學過編譯器,你就懂了,你能夠這樣理解:

printf("%d",a++); 其實會被編譯成2句話

printf("%d",a);

a++;(a = a + 1;)

這樣,答案是神馬,不用我說了吧。這個就叫作後加,懂了吧

printf("%d",++a);實際上也是會被編譯成2句話

a++;(a = a + 1;)

printf("%d",a);

爲何是2?一目瞭然,之後還分不清前加後加嗎?嘻嘻。

 

4. 其實 循環語句,其實對於x86來講就1種->跳轉,固然跳轉有2種,

一是無條件跳轉
L:
   jmp L;
二是有條件跳轉
L:
  cmp eax,3
  jb L;
隨便寫的僞代碼。
 
固然有的人問了,do while,while ,for 3種循環,你不是打本身臉嗎?
 
我能說 其實翻譯成 x86就是do while 嗎,而 do while 不就是條件跳轉嗎?
 
while 就是 do while,爲何? 由於多一個入口檢測而已
for 就是 do while + 入口檢測 + update而已。具體神馬的,本身去想吧。。。
 

 

 

 

結語:

 

note:

 

    其實編譯器技術,還有不少不少,我只是討論了其中的九牛一毛,並且因爲篇幅限制,我寫不下太多。做爲第一篇文章,暫時先這樣吧,之後再更新。

 

 

    本人對信息安全也略懂,因此對底層的一些東西有一些本身的理解,其實這些都是基礎,作安全最最重要的基礎,是在課本上根本學不到的東西,最最精華的東西,在之後的文章中我會陸續提到:

學完編譯器,對語言的理解又更深了一步,好比你看到以下東西,

int c =  4;

int d;

void fun(int a,int b)

{   int n = 4;

     int i;

     for(i=0;i<n;i++)

         printf("a+b=%d\n",a+b);

}

int main()

{

    fun(1,2);

    return 0;

}

要思考,編譯以後,生成怎麼樣的x86,calling convention,prolog/epilog,caller-saved/callee-saved register,堆棧平衡,全部變量的內存分佈,函數符號修飾成什麼樣,靜態連接,動態連接,地址修正,連接指示對編譯過程的影響,如dllimport,dllexport,#pragma,函數聲明等等

之後的文章我會陸續解釋。

其實學完編譯器的真正效果,就是你看到上面的c,能想到,其實它就是神馬。。。

相關文章
相關標籤/搜索