- 用Go語言自制解釋器
- (德)索斯藤·鮑爾
- 3435字
- 2022-06-17 10:50:30
前言
前言的第一句話原先是“解釋器非常神奇”,但一位不愿透露姓名的審稿人說,這樣寫太蠢了。好吧,Christian,我不同意這種說法。我仍然認為解釋器很神奇。讓我來解釋一下。
從表面上看,解釋器很簡單:輸入文本,然后得到一些輸出。也就是說,解釋器是將其他程序作為輸入來生成一些內容的程序。這看上去很簡單,但你越深入思考,就越會發現它讓人著迷。給解釋器提供字母、數字和特殊字符等看似隨機的內容,輸出后這些內容突然就變得有意義了。這是因為解釋器解讀了這些內容,它讓無意義的內容變得有意義。計算機是只能明白0和1的機器,現在卻能理解我們提供的奇怪語言并進行反饋。這歸功于解釋器,它讀取語言并同步翻譯給計算機。
我一直在問自己:解釋器是如何工作的?當腦海中第一次冒出這個問題時,我就知道,只有自己寫一個解釋器,才能從中得到令自己滿意的答案。于是我就這么做了。
現在有許多書、文章、博客和教程介紹解釋器。這些資料大體可分為兩類:一類是極其重視理論的大部頭,這類資料更適合那些對這方面已經有了深刻理解的人;另一類則篇幅很短,蜻蜓點水般介紹解釋器,重點介紹使用像黑盒一樣的外部工具,來實現一些只能作為示例的解釋器。
在學習中,挫折感主要來自第二類資料,因為其中介紹的解釋器只能解釋語法極其簡單的語言。而我并不想走捷徑,我真的很想明白解釋器的工作原理,包括理解其中的語法分析器和詞法分析器。特別是,我想知道類C語言以及其中的大括號和分號是怎么解析的。學術類的教科書中有我想知道的答案,但其中冗長的理論解釋和晦澀的數學公式對我來說還是困難了一點。
一邊是教科書,用900頁的篇幅介紹編譯器;一邊是博客,用50行Ruby代碼編寫了一個Lisp解釋器,而我想要的是介于兩者之間的東西。
所以我為大家編寫了這本我理想中的書。本書適用于那些希望對底層知識有所了解,即想了解事物運作機制的人。
本書將從零開始,為一門新語言編寫一個解釋器,其中不會用到任何第三方的工具和庫。最后實現的解釋器無法用于生產環境,其性能并不等同于完全成熟的解釋器,所實現的語言也會缺少某些特性,但我們能從其中學到許多知識。
由于解釋器種類繁多且各不相同,因此很難為解釋器給出明確的定義。但至少有一個功能是所有解釋器都有的,那就是將源代碼作為輸入并求值,中途不會生成任何可見且之后還能再次運行的中間結果。編譯器就不同,它會將源代碼轉換成底層系統可以理解的另一種語言。
有些解釋器很精簡,甚至不包括解析步驟,只是單純地解釋輸入。
除此之外,也有精心設計的解釋器。有些解釋器進行了高層次優化,使用了高級解析和求值技術。有些則不會直接求值,而是將輸入編譯成稱為字節碼的內部形式,之后再求值。還有更高級的JIT解釋器,它會將需要執行的輸入即時轉換成機器代碼。
有些解釋器則介于上述兩類之間,這類解釋器會解析源代碼,構建抽象語法樹(AST),然后對這棵樹求值。這類解釋器稱為樹遍歷(tree-walking)解釋器,因為其工作方式是在AST上遍歷并解釋。
本書將構建這樣一個樹遍歷解釋器。
我們將構建自己的詞法分析器、語法分析器、樹表示形式和求值器(evaluator),其中涉及詞法單元(token)、抽象語法樹、如何構建抽象語法樹、如何求值,以及如何使用新的數據結構和內置函數擴展所用的編程語言。
Monkey編程語言和解釋器
每個解釋器都用來解釋一種特定的編程語言,這就是實現一門編程語言的方式。沒有對應的編譯器或解釋器,一門編程語言只不過是一個想法或一種規范。
我們將要解析和求值的語言叫Monkey。這是專為本書設計的語言,其唯一實現就是本書中構建的解釋器。
Monkey具有以下特性:
- 類C語法
- 變量綁定
- 整型和布爾型
- 算術表達式
- 內置函數
- 頭等函數和高階函數
- 閉包
- 字符串數據結構
- 數組數據結構
- 哈希數據結構
本書剩余部分會詳細研究并實現這些特性。現在先來看看Monkey長什么樣。
在Monkey中綁定值和名稱的方法如下:
let age = 1;
let name = "Monkey";
let result = 10 * (20 / 2);
除了整型、布爾型和字符串,Monkey解釋器還支持數組和哈希表。下面展示如何將一個整型數組綁定到一個名稱上:
let myArray = [1, 2, 3, 4, 5];
下面是一個哈希表,其中的值和鍵進行了關聯:
let thorsten = {"name": "Thorsten", "age": 28};
下面使用索引表達式訪問數組和哈希表中的元素:
myArray[0] // => 1
thorsten["name"] // => "Thorsten"
let
語句還可以用來綁定函數和名稱。下面是將兩個數字相加的一個函數:
let add = fn(a, b) { return a + b; };
Monkey不僅支持return
語句,還支持隱式返回值,這意味著可以省略return
:
let add = fn(a, b) { a + b; };
調用函數很簡單:
add(1, 2);
下面是一個復雜一點的函數fibonacci
,它會返回斐波那契數列的第n項:
let fibonacci = fn(x) {
if (x == 0) {
0
} else {
if (x == 1) {
1
} else {
fibonacci(x - 1) + fibonacci(x - 2);
}
}
};
注意,fibonacci
函數在遞歸中調用了自身。
Monkey還支持一類特殊的函數,即高階函數。這類函數以其他函數為參數,如下所示:
let twice = fn(f, x) {
return f(f(x));
};
let addTwo = fn(x) {
return x + 2;
};
twice(addTwo, 2); // => 6
這里的twice
接受了兩個參數:函數addTwo
和整數2
。這段代碼調用了addTwo
兩次,第一次以2
為參數,第二次以第一次的返回值為參數。最后一行代碼返回結果6
。
是的,我們可以在函數調用中將函數作為參數。Monkey中的函數只是值,與整數或字符串一樣。具有這個特性的函數稱為“頭等函數”(first-class function)。
本書構建的解釋器將實現所有這些特性。解釋器會在REPL中對Monkey源代碼進行詞法分析和語法分析,將代碼構建成稱為抽象語法樹的中間表示,然后對該樹求值。解釋器包含以下幾個主要部分:
- 詞法分析器
- 語法分析器
- 抽象語法樹(AST)
- 內部對象系統
- 求值器
我們將完全按照此順序自上而下構建這幾部分,即從源代碼到輸出的順序。這種方法的缺點是在第1章還無法生成簡單的Hello World
,但好處是更容易理解各部分如何協同工作以及數據如何在程序內部流動。
至于為什么名字叫Monkey?好吧,因為猴子是美妙、優雅、迷人且有趣的動物,就像我們的解釋器一樣。
另外,書名是什么意思呢?
為什么用Go語言
讀者應該已經注意到本書的書名和其中的“用Go語言”了。是的,我們將用Go語言自制解釋器。為什么用Go?
我喜歡用Go編寫代碼。我喜歡使用這門語言、其標準庫及其提供的工具。除此之外,我還認為Go具有的一些屬性使其非常適合本書。
Go很容易閱讀和理解。即使對于Go初學者,本書中的Go代碼也淺顯易懂。我相信哪怕讀者從未寫過一行Go代碼,也可以跟著本書學習。
另一個原因是Go提供了出色的工具。本書的重點是編寫解釋器,包括理解其背后的思想和概念并完成其實現。借助gofmt
帶來的Go通用格式樣式以及內置的測試框架,我們可以專注于解釋器本身,而不必分心于第三方庫、工具和依賴。除了Go語言提供的工具以外,本書不會使用任何其他工具。
更重要的一個原因是,本書中展現出了Go的代碼風格與其他更底層的語言(C、C++和Rust)非常相似。這或許是Go本身的原因,Go側重于簡單和樸素的美感,與上述語言之間沒有什么難以轉換的編程語言結構;抑或是本書中我編寫Go所使用的風格與這些語言相似??傊瑫械腉o代碼不會取巧地使用任何元編程技巧,這些復雜的技巧會讓代碼在一段時間后就難以理解。書中也沒有用冗長的篇幅來解釋面向對象的設計和模式,最后還來一句“看,這很簡單”。
所有這些因素使得本書的代碼在概念和技術上都易于理解,并且可以復用。讀者在閱讀完本書后,可以很輕松地用另一種語言編寫自己的解釋器。本書旨在給讀者理解和構造解釋器提供一個起點,相信書中的代碼也體現出了這個初衷。
如何使用本書
本書既不是參考書,也不是描述解釋器實現概念并在附錄中附加代碼的理論讀物。本書需要按順序閱讀,我建議讀者從頭到尾,一邊閱讀,一邊敲出書中的代碼并調試。
每章的代碼和內容都以其之前的章節為基礎。每一章都會構建解釋器的一部分,一點一點積累,直至完成。為了使后續操作更容易,本書附帶了一個名為code的文件夾,其中包含代碼。可以在此處下載:
https://interpreterbook.com/waiig_code_1.7.zip
code文件夾有多個子文件夾,每個子文件夾對應一章的內容,且含有對應章節的最終結果。
有時我只會在書中提及完整代碼中的某些內容,不會完整列出相關代碼,因為完整的代碼會占用太多篇幅,其中有些只是測試文件或不重要的細節。讀者可以在對應章節的文件夾中找到完整的代碼。
跟隨書中示例進行操作需要哪些工具?并不多:一個文本編輯器和Go語言。Go版本高于1.0應該都可以,但事先聲明:本書最初編寫時使用的是Go 1.7,而本書的最新版使用的是Go 1.14。
如果讀者使用Go >= 1.13,則code文件夾中的代碼應該是“開箱即用”的。
如果使用的是不支持Go模塊的舊版本Go,那么建議使用direnv工具,它可以根據.envrc文件修改shell環境。本書隨附的code文件夾中的每個子文件夾都有一個.envrc文件,該文件可為該子文件夾正確設置GOPATH
。這樣就可以輕松地使用不同章節的代碼了。
有了這些準備之后,讓我們開始吧!
更多信息
掃描下方二維碼,即可獲取電子書相關信息及讀者群通道入口。

- Spring 5.0 Microservices(Second Edition)
- Docker進階與實戰
- 無代碼編程:用云表搭建企業數字化管理平臺
- ASP.NET Core Essentials
- 認識編程:以Python語言講透編程的本質
- 用Flutter極速構建原生應用
- Learning Laravel's Eloquent
- Creating Mobile Apps with jQuery Mobile(Second Edition)
- Microsoft Dynamics AX 2012 R3 Financial Management
- Building Microservices with .NET Core
- HTML5秘籍(第2版)
- 區塊鏈技術進階與實戰(第2版)
- Node.js開發指南
- Web性能實戰
- Python 3 數據分析與機器學習實戰