JavaScript 編寫的迷你 Lisp 直譯器

liyuan462發表於2013-07-29

【感謝@李欲純 的熱心翻譯。如果其他朋友也有不錯的原創或譯文,可以嘗試推薦給伯樂線上。】

Little Lisp是一個直譯器,支援函式呼叫、lambda表示式、 變數繫結(let)、數字、字串、幾個庫函式和列表(list)。我寫這個是為了在Hacker School(一所位於紐約的程式設計師培訓學校)的一個閃電秀中展示寫一個直譯器不是很難。一共只有116行的JavaScript程式碼,下文我會解釋它是如何執行的。

 

首先,讓我們學習一些Lisp。

Lisp基礎

這是一個原子,最簡單的Lisp形式:

這是另一個原子,一個字串:

這是一個空列表:

()

這是一個包含了一個原子的列表:

這是一個包含了兩個原子的列表:

這是一個包含了一個原子和另一個列表的列表:

這是一個函式呼叫。函式呼叫由一個列表組成,列表的第一個元素是要呼叫的函式,其餘的元素是函式的引數。函式first接受一個引數(1 2),返回1

這是一個lambda表示式,即一個函式定義。這個函式接受一個引數x,然後原樣返回它。

這是一個lambda呼叫。lambda呼叫由一個列表組成,列表的第一個元素是一個lambda表示式,其餘的元素是由lambda表示式所定義的函式的引數。這個lambda表示式接受一個引數"lisp"並返回它。

 

Little Lisp是如何執行的

寫一個Lisp直譯器真的很容易。

Little Lisp的程式碼包括兩部分:分析器和直譯器

分析器

分析分兩個階段:分詞(tokenizing)和加括號(parenthesizing)。

tokenize()接受一個Lisp字串,在每個括號周圍加上空格,然後用空格作為分隔符拆分整個字串。舉個例子,它接受((lambda (x) x) "Lisp"),將它變換為( ( lambda ( x ) x ) "Lisp" ),然後進一步變換為['(', '(', 'lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')']

parenthesize()接受由tokenize()產生的詞元列表,生成一個巢狀的陣列來模擬出Lisp程式碼的結構。在這個巢狀的陣列中的每個原子會被標記為識別符號或文字表示式。例如,['(', '(', 'lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')']被變換為:

parenthesize()一個挨一個地遍歷詞元。如果當前詞元是左括號,就開始構建一個新的陣列。如果當前詞元是原子,就標記其型別並將其新增到當前陣列中。如果當前詞元是右括號,就停止當前陣列的構建,繼續構建外層的陣列。

parenthesize()第一次被呼叫時,input引數包含由tokenize()返回的詞元列表陣列。例如:

第一次呼叫parenthesize()時,引數listundefined,第2-3行執行,遞迴呼叫parenthesize()list被設定為空陣列。

在遞迴中,第5行執行,input的第一個左括號被移除。第9行中,傳一個新的空陣列給遞迴呼叫,開始一個新的空列表。

在新的遞迴中,第5行執行,從input中移除了另一個左括號。與前面類似,第9行中,傳另一個新的空陣列給遞迴呼叫,開始另一個新的空列表。

繼續進入遞迴,現在input['lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')']。第14行執行,token被設定為lambda,呼叫categorize()函式並傳遞lambda作為引數。categorize()的第7行執行,返回一個物件,其type屬性被設定為identifiervalue屬性被設定為lambda

parenthesize()的第14行向list中加入由categorize()返回的物件,然後用input的剩餘元素和list進一步遞迴。

在遞迴中,下一個詞元是括號。parenthesize()的第9行用一個新的空陣列遞迴建立一個新的空列表,進入新的遞迴,這時input['x', ')', 'x', ')', '"Lisp"', ')']。第14行執行,token被設定成x,這樣建立了一個新的物件,其值為x,型別為identifier,然後將這個物件加入到list中,然後接著遞迴。

在遞迴中,下一個詞元是右括號,第12行執行,返回完成了的list[{ type: 'identifier', value: 'x' }]

parenthesize()繼續遞迴直到它處理完全部的輸入詞元,最後返回由包含了型別資訊的原子所組成的巢狀陣列。

parse()tokenize()parenthesize()的組合呼叫:

如果原始的輸入給的是((lambda (x) x) "Lisp"),則分析器給出的最後輸出是:

 

直譯器

在分析結束後,解釋就開始了。

interpret()接收parse()的輸出並執行它。提供上例中的輸出,interpret()會構造一個lambda表示式,然後用"Lisp"作為引數呼叫它。lambda呼叫會返回"Lisp",這就是整個程式的輸出。

除了要執行的輸入外,interpret()還接收一個執行上下文。執行上下文是變數和變數對應的值所儲存的地方。當一段Lisp程式碼被interpret()執行時,執行上下文包含著這段程式碼可訪問的變數。

這些變數是分層儲存的。當前作用域的的變數處在最底層,在包含域中的變數處在上一層,包含域的上一層包含域中的變數處於更上層,依次類推。例如,在下面的程式碼中:

第3行,執行上下文有兩個活動的作用域。內層的lambda形成了當前作用域。外層的lambda形成了包含作用域。當前作用域中b被繫結到"b",包含作用域中a被繫結到"a"。當第3行執行時,直譯器嘗試在作用域中去查詢b,它檢查當前作用域,發現了b並返回它的值。還是在第3行上,直譯器嘗試去查詢a,它檢查當前作用域,結果沒找到a,所以它嘗試去包含域找,在那裡它找到了a並返回它的值。

在Little Lisp中,執行上下文用一個物件來表示,這個物件通過呼叫Context建構函式來生成。這個函式接受scope引數,即一個由在當前作用域中的變數和值組成的物件;還接受parent引數,如果parentundefined,作用域即位於頂層,或者說是全域性的。

我們已看到((lambda (x) x) "Lisp")是如何被分析的,現在讓我們看看分析過後的程式碼是如何被執行的。

interpret()第一次被呼叫時,contextundefined,第2-3行執行,建立一個執行上下文。

當初始上下文被例項化時,建構函式接受了一個叫library的物件。這個物件包含了內建在語言中的函式:first, restprint。這些函式是用JavaScript寫的。

interpret()用原始的輸入和新的上下文進行遞迴。

input包含了上節中例子產生的輸出:

因為input是陣列而且context已定義,第4-5行執行,interpretList()被呼叫。

interpretList()中,第5行遍歷input陣列,對每個元素呼叫interpret()。當interpret()在lambda定義上呼叫時,interpretList()再一次被呼叫。這次,interpretList()input引數為:

interpretList()的第3行被呼叫,因為陣列的第一個元素lambda是特殊形式。lambda()被呼叫來建立lambda函式。

special.lambda()接受input中定義lambda的部分,返回一個函式,當這個函式被呼叫時,會對一些引數呼叫這個lambda函式。

第3行開始lambda呼叫函式的定義。第4行儲存了傳遞給lambda呼叫的引數。第5行開始為lambda呼叫建立一個新的作用域,收集input中定義lambda的引數的部分: [{ type: 'identifier', value: 'x' }],針對input中的每一個lambda形參和傳遞給lambda的對應實參,往lambda作用域中新增一個鍵值對。第10行對lambda的主體呼叫interpret(){ type: 'identifier', value: 'x' }。它傳遞給的lambda上下文包含lambda的作用域和父上下文。

lambda現在就變成了被special.lambda()返回的函式。

interpretList() 繼續遍歷input陣列,對列表的第二個元素呼叫interpret():字串"Lisp"

interpret()的第9行執行,這行做的事情僅僅是返回字面量物件的value屬性'Lisp'interpretList()的第5行的map操作至此完成。list成為:

interpretList()的第6行執行,發現List的第一個元素是一個Javascript函式,這意味著list是一個函式呼叫。第7行執行,呼叫lambda函式,並將list的剩餘部分作為引數傳遞。

在lambda呼叫函式中,第8行對lambda主體呼叫interpret(){ type: 'identifier', value: 'x' }

interpret()的第6行發現input是一個識別符號型別的原子,第7行去上下文裡查詢識別符號x,返回'Lisp'

'Lisp'被lambda呼叫函式返回,接著被interpretList()返回,接著被interpret()返回,就是這樣。

全部的程式碼見GitHub repository。還可以看看lis.py,一個優秀而簡單的Scheme直譯器,由Peter Norvig用Python編寫。

相關文章