當前位置: 華文問答 > 數碼

程式語言都是怎麽發明的?

2019-12-01數碼

對程式語言最大的誤解之一,可能就是【實作一門高級語言,必須要靠更低階的語言】了。其實我們完全可以用 JavaScript 實作一個 C 編譯器。畢竟編譯器的工作,不就是把 if else 之類的源碼 字串 ,轉換成 JMP 之類的組譯碼 字串 或字節碼 陣列 嗎?這個 輸入字串,輸出字串或陣列 的過程,也就是演算法比較復雜,有哪門主流語言幹不了嗎?

可是,編譯器明明生成的是機器指令,怎麽可能只是倒騰字串這麽簡單呢?其實像我們日常用的 gcc 就只是個殼,裏面會替你依次呼叫預編譯器 cc1、組譯器 as 和連結器 ld。經典的詞法、語法、語意分析和目標碼最佳化等部份,都是 cc1 裏做的,這時輸出就是 .S 格式的組譯碼字串。至於二進制機器碼的產生,靠的則是【經典編譯過程結束後】組譯和連結部份的臟活。

知道了編譯器的輸入和輸出都是字串以後,你應該就能明白, 任何圖靈完備的程式語言,理論上都是能用來寫編譯器的 。例如我們剛剛說的那種用 JavaScript 編譯 C 的逆向操作,就還真有人這麽幹(話說還有什麽是 JavaScript 不能寫的嗎):

https:// github.com/Captainarash /CaptCC

但是,相信題主真正想問的應該是這個問題: 在沒有其它高級語言的時候,那些高級語言的編譯器又是怎麽寫出來的呢 ?譬如 C++ 的編譯器是 C++ 寫的,那第一個 C++ 編譯器要怎麽編譯自己呢?這就是個先有雞還是先有蛋的經典問題了。

在電腦科學裏,這個問題對應於所謂的 自舉 (Bootstrapping) 概念。一般來說,某種新語言 X 的第一個編譯器 C1,需要用另一種語言來編寫。接下來你就可以用 C1 編譯出一個部份用 X 來寫的新編譯器 C2,而 C2 又可以編譯出一個支持更多 X 特性的 C3,然後 C3 編譯出 X 支持更好的 C4……該過程重復 114514 次即可前進演化到完全自己 X 自己(誤)。

更通俗的理解,是將編譯器想像成一個加工零件的機床。這個機床既可以加工出普通零件,也可以加工出高級零件,最後組裝出一台新的機床。第一台機床的零件都需要手工打造,造出的零件比較粗糙,但用第一台機床加工出的第二台機床,可能還有一些零件要靠手工,但機械化比例肯定比第一台機床高一些,加工效果也會更好一些。多重復幾次這種【用粗糙機床制造更高精度機床】的過程後, 即便制造第一台機床的手工技術失傳,我們仍然可以標準化地用機床來制造機床

歷史上,最早的 C 編譯器就是組譯實作的。而最早的 C++ 編譯器,則是用 C with class 這種介於 C 和 C++ 之間的語言來實作的。當時已經有了能編譯 C with class 的編譯器,用這種編譯器編譯出來的第一個 C++ 編譯器還比較簡陋,只是把 C++ 轉成 C,然後再用 C 編譯器去編譯。但有了這個起點後,人們就能用 C++ 來開發越來越強的 C++ 編譯器了。所謂道生一,一生二,二生三,三生萬物,大抵如此。

為了防誤解,附上兩個備註:

  1. 自舉在技術上其實也不是必須的。理論上你也可以一直用 C 甚至組譯來實作 C++ 編譯器,可是這樣你幹嘛還要發明 C++ 呢?但對於直譯器執行時效能已經被 C++ 最佳化到極致的手稿語言,就沒什麽必要玩自舉這一套。例如 JavaScript 就是典型的不需要自舉的語言,簡稱 不舉 (誤)。
  2. 只要你夠牛逼,你還可以直接用自己設計的語言寫出這門語言的編譯器源碼,然後 用你的腦子 把這份源碼編譯成該語言的第一個編譯器。遠古天尊 Donald Knuth 就這麽幹過。
思考題:雖然 JavaScript 不舉,但 TypeScript 和 CoffeeScript 都是自舉的,為什麽呢?

直到今天,我們還能看到自舉的影子。例如 gcc 編譯器就把自舉當作了對自身的測試用例。你可以用大版本為 N-1 的舊版 gcc 構建出大版本為 N 的新版 gcc,但這個過程一共會編譯三遍:

  1. 用舊版 gcc 編譯出新版 gcc
  2. 用新版 gcc 編譯自己
  3. 重復一次步驟 2,對比結果是否完全一致

理論依據:NewCompiler1 和 NewCompiler2 都是用相同的 NewCompiler 源碼編譯出的程式。它們都給定了 NewCompiler 這份源碼作為相同的輸入,因此不管它們自己是用什麽編譯器編譯的(如 NewCompiler1 用的 OldCompiler 和 NewCompiler2 用的 NewCompiler1),它們所生成的輸出都應該一致。所以步驟 2 和 3 所輸出的 NewCompiler2 和 NewCompiler3 就也應該一致。

繼續開個腦洞, 假設外星人刪掉了世界上所有的二進制程式和全部的程式碼送出歷史,只留下了寫在紙上的源碼,那麽人類的程式碼還怎麽執行呢 ?表面看來,你必須從最早的 C 編譯器開始寫,把歷史上這麽多編譯器陸續都實作出來自舉一遍,才能最後拯救電腦文明。但是,Fabric Bellard 大神寫的 TCC (Tiny CC) 就相當於一條捷徑,或者說文明的火種。這個編譯器的程式碼很少,並且不光能編譯出自己,還能編譯出 Linux 內核。這樣,只要徒手用組譯弄出一個能編譯 TCC 的更簡單的 TTCC (TinyTiny CC),我們就能先用 TTCC 來編譯 TCC,然後用自舉後的 TCC 直接編譯出 Linux 了——感覺是個程式設計師拯救世界的硬核科幻題材啊。

理解了編譯器的自舉之後,再去看看哲學上的因果困境、經濟學上的工業化,甚至生物學上的物種演化,是不是都能感到某種相通之處呢?私以為這就是電腦科學的樂趣所在吧~

參考

  • 程式設計師的自我修養:連結、裝載與庫
  • Bootstrapping (compilers)
  • How can a compiler compile itself?
  • How are GCC and g++ bootstrapped?
  • How could the first C++ compiler be written in C++?