1. 程式人生 > >前端要以正確的姿勢學習編譯原理(上篇)

前端要以正確的姿勢學習編譯原理(上篇)

webp 法規 怎麽 ref port java 編輯器 等於 都沒有

轉自:https://zhuanlan.zhihu.com/p/36301857

前言

最近在我的 timline 上面出現了很多類似《前端為什麽要學編譯原理》這類文章以及《前端怎麽學AST》這類的問題,但是卻發現並沒有人給大家介紹前端要如何以系統並且正確地學習編譯原理,所以我就結合自己的經驗以及走過的彎路來給大家分享點心得和經驗,希望能讓大家少走點彎路。

最後我並不是前端,只是恰好會寫點 JavaScript 而已。


目錄

上篇:

  • 編譯原理為什麽難
  • 怎麽學好編程語言
  • 代碼到底是什麽
  • 正則與上下文無關文法
  • 編程語言從 AST 才正式開始

下篇:

  • 靜態分析
  • 類型推導
  • AST 的轉換
  • Conitnuation
  • 字節碼虛擬機


編譯原理為什麽難

大家提起編譯原理第一反應都是很難,難到無從下手,但是為什麽難呢?說白了,編譯原理不就是研究把一門語言解析並且轉換成另一門語言的技術嗎?這項技術到底有哪些地方成為了阻礙呢?我認為這個最大的阻礙其實就是“編程語言”本身。

我相信在看這篇文章的朋友至少已經學會了 JavaScript 了吧,但是我想多嘴問一句,大家真的懂 JavaScript 嗎?能描述出 JavaScript 的語法規則嗎?能理解語法所代指的邏輯結構嗎?知道 JavaScript 是如何在被解釋和執行的嗎?所以,大家真的懂 JavaScript 嗎?反正我是至今沒有底氣說自己”精通“ JavaScript ,原因是我還不懂如何實現一個 JIT。

我們多數時候稱自己“精通”某編程語言的時候,僅僅指會熟練使用某編程語言,但是編譯原理這門學科折騰的核心恰恰是編程語言,它要求我們對編程語言有深入的了解,了解它是如何構造和解釋的。我們如果沒有這項基礎其實是很難學好這門學科的。

推薦閱讀:

  • 《七周七語言》——看看不同的編程語言都長什麽樣子
  • 編程語言 —— 維基百科
  • 編程範式 —— 維基百科


怎麽學好編程語言

部分國外高校的計算機專業喜歡用 Lisp 系的 Scheme 入門,一開始我並不明白其中緣由,直到我發現他們的課程作業中最後總會要求實現一個簡易的 Lisp 解釋器時我才恍然大悟。外國學校安排課程的水平真是高明,學校教 Scheme 可不是為了讓學生拿來寫工程代碼,而是讓學生學習編程以及編程語言本身到底是一個什麽東西。

Lisp 是一門具備現代編程語言特性的幾乎最簡的實現,所有編程語言都是 Lisp 方言真的不僅僅是一句玩笑話。簡易的 Lisp 的解釋難度很低,Lisp 語法的解析只有解析 JSON 同等的難度,我們會經常看到很多新手用百來行代碼就能實現一個 Lisp 解釋器。雖然實現一個 Lisp 解釋器不難,但是他對學生來說的意義非常重大,它能讓學生們對編程語言和程序的構造和執行有一個非常非常基礎但又非常全面的認識。而這種對編程語言全面的認識,也正是我們這些拿著 C/C艹 亦或者 JavaScript 入門的大家所缺失的。

所以如何學好編程語言?正途當然是啃我們的經典神書 《SICP》了,不過考慮到 《SICP》嚴重的教科書屬性,講地並不生動有趣,所以還是給大家推薦一個科普性質更強的書,叫做《計算的本質》大家可以用這本書先入門,如果學有余力或者非常有興趣再去啃《SICP》。

推薦閱讀:

  • 《計算的本質:深入剖析程序和計算機》—— 科普版 SICP
  • 《計算機程序的構造和解釋》—— 經典神書,學有余力或者感興趣再啃
  • 《微信小程序也要強行熱更代碼,鵝廠不服你來肛我呀》 —— 我自己寫的文章,用 JavaScript 實現一個 JavaScript 解釋器


代碼到底是什麽

上一節我們提過,Lisp 的解析難度和 JSON 是一樣的,那我們能不能幹脆用 JSON 代替代碼呢?當然可以,JavaScript 的解析後的語法樹就是用 JSON 表示的。所以就表達能力來說,JavaScript 的代碼和 JSON 是沒有差別的。那麽問題來了,代碼到底是什麽?

其實代碼跟 JSON 一樣,是一種結構化的文本數據格式。在這裏我們要僅僅抓著兩個特點——“文本”和“結構化”。

代碼的第一個特點是文本,那意味著我們所有對字符串的拼接、截取或者替換等所有操作,都可以應用在代碼上面。很多程序員雖然都能對各類文本的讀寫了如指掌,但大家好像都沒有意識到代碼文件,也可以是那個可以讀寫、修改的文件之一。

對代碼文件的讀寫和操作是進入編譯世界的第一個重要門檻,有的時候並不需要太復雜的算法就能夠對代碼做一些有意義的轉換,比如我們可以直接通過正則分析 import / export / require 來實現一個簡易的 webpack,比如在我之前一篇文章也是通過簡單的正則優化尾遞歸代碼。真正有意識地把代碼文件當成文本文件以後,我們就能把代碼從此拉下“神壇”,可以讓大家能夠像思考文本一樣思考代碼。

代碼的第二個特點則是結構化。不知道大家能不能理解,代碼裏面除了字面量意外,其他部分都只是標識結構而並不具有實際意義,賦予這些結構意義是解釋器如何和執行這段代碼。這個特點就是要求我們在看待代碼的時候,要在腦中形成一種結構,而不再是一行一行的字符串。

var a = 123 // 除了字面量 123 外其他所有字符都是標識結構

比如上面這串簡單的 JavaScript 代碼,var 這是一個抽象符號,他是 var 也好是 val 也好,就算是 #%$ 都沒有任何問題,唯一的目的就是標識了這個結構(語句)是一個聲明賦值。變量名 a 標識的是一種聯系,這個 a 具體是什麽也是無關緊要的,只要它所標識的聯系不變,a 也是可以替換成任何字符。這裏面唯一有實際意義的就是那個 123,我不能把它換成 456。

知乎之前有一個問題問為什麽一些大佬能夠在兩個星期內學會一門編程語言,我的回答是兩個星期都夠我們造一門編程語言了,就像 JavaScript 也就是 布蘭登·艾克 大佬花了一個星期設計的。我雖然肯定不及這些大佬們,但是讓我兩個星期內拿 C艹 造一個 JavaScript 1.0 還是沒什麽太大問題的。所以只要把文章到這裏之前推薦的書好好看了,基礎補上了,那麽其實大家每個人都能輕松在兩個星期內學好一門編程語言。

最後還是要提一下,能夠用兩個星期學好一門編程語言並不代表能用兩個星期學好一個領域。就像你不能說你學會了 JavaScript 就等於學會了前端,也不能說學會了 Python 就等於學會了人工智能(雖然現在很多坑爹培訓班打著人工智能旗號教 Python 基礎),編程語言僅僅是編程語言,僅僅是一個工具。

推薦閱讀:

  • 《尾遞歸為啥能優化?》 —— 我寫的文章,使用簡單的字符處理來優化尾遞歸函數成循環

推薦工具:

  • AST Explorer —— 看看自己常寫的 JavaScript 長什麽樣子


正則與上下文無關文法

這篇文章到這裏已經是第四個小節了,但直到這裏才算能夠正式抱起我們的經典教材——龍書、虎書或者鯨魚書進行學習了。這一節簡單介紹一下編譯器前端技術 —— Parser。

編譯器前段就在幹一件事,把代碼這個結構化的文本文件解析成我們計算機可以理解的數據結構 —— 抽象語法樹(AST)。解析代碼是一個比較無聊、復雜而又繁瑣的過程。這種復雜和繁瑣是來由於編程語言本身語法設計的繁瑣和復雜導致的。比如我們前文討論過的 Lisp 由於語法設計的非常簡單、一致而又無歧義,所以解析起來非常輕松,但是作為代價的就是 Lisp 那個被吐槽很多的括號括號括號。

解析代碼一般分成兩個步驟,第一個步驟是詞法分析,將文本的代碼轉化成一個個 Token。看到這裏的大家應該都有一些正則表達式的基礎吧,在解析代碼的過程中,我們需要用正則來分詞做詞法分析。在編譯原理面我們學習正則的時候就不僅僅是學習正則表達式了,也會學習正則的內核 DFA,不過這部分難度不大就是了。

解析代碼的第二個步驟是語法分析,語法分析是將我們上面詞法分析出的 Token 轉化成 AST。語法分析我們要學習上下文無關文法(CFG),並且可以用 BNF 這個表示。CFG 比正則表達能力更強,強在 CFG 能表達遞歸結構,常見的遞歸結構有表達式和代碼塊。在語法分析這個部分,會基本的 LL(1) 算法,能夠對自頂向下的分析有足夠的了解,就已經足夠了。

無論是正則還是 CFG,他們都是在用一種形式語言(我們的編程語言也是一種形式語言),來描述一種抽象結構,所以在學習的過程中,腦子裏面一定要這種從抽象結構的概念,能夠事半功倍。

Parser 在編譯原理裏面是難點但卻不是重點,所以在這一部分大家覺得復雜的算法完全可以跳過,不建議浪費太多時間。Parser 都是可以根據正則和 CFG 自動生成的,並不需要自己手寫。所以這部分主要目的是學好的是正則和 CFG,那些復雜的算法學起來意義很小。

最後還有一個非常有趣的現象,正則表達式是上下文無關文法,而 BNF 卻又是正則文法,大家可以想想為什麽?

推薦閱讀:

  • 對 Parser 的誤解 —— 我自己的真正入門的文章
  • 龍書、虎書或者鯨魚書任選 —— 經典編譯原理經典教材
  • 當然我在扯淡 —— 垠神的博客,看的時候請自動屏蔽垠神的主觀自嗨

推薦工具:

  • Acorn —— JavaScript Parser
  • REGEXPER —— 將正則表達式以圖形的形式展示
  • Ohm —— 可以可視化的 BNF 編輯器
  • Jison —— 用正則和 BNF 構建通用的 Parser


編程語言從 AST 才正式開始

其實在大多數眼裏的編譯原理,都停留在 Parser 這個階段,因為大部分人都在學習的時候卡在了個這個階段。但是事實上 Parser 不過是這個領域最表面的一層技術而已。編程語言從 AST 才算正是開始,只有到了 AST 的階段,我們的計算機才可以對我們的編程語言進行包括分析、解釋或者翻譯,而我們前面我們所辛辛苦苦寫的代碼只不過是給我們這些愚蠢的人類看的罷了。

對編程語言 AST 的分析、轉換、解釋以及翻譯理應是編譯原理中最重要的一個部分,但由於我們經典編譯原理書出版時間都比較早(1985年),並且也只著眼於當時流行的以 C 為主的編譯型語言,所以它的重點都放在了解析代碼和生成匯編兩個部分。但是以現在的編程語言角度來看的話,前端有 Parser Generator,後端有 LLVM 那麽我們更多的重點其實應該跟多地放在中端上來。

不過到這裏為止,我們介紹的內容其實已經足夠大部分小夥伴給自己寫個 DSL,給自己寫一個編譯到 JavaScript 的小語言玩了。 但是這足夠了嗎?我們到底可以對 AST 做些什麽呢?讓我們下篇再見吧。

參考項目:

  • bramblex/blx-fsm —— 自己造的用於描述 FSM 的玩具 DSL
  • bramblex/Smooth —— 自己造的玩具小語言

Hello World!

前端要以正確的姿勢學習編譯原理(上篇)