【感謝@李欲純 的熱心翻譯。如果其他朋友也有不錯的原創或譯文,可以嘗試推薦給伯樂線上。】
Little Lisp是一個直譯器,支援函式呼叫、lambda表示式、 變數繫結(let)、數字、字串、幾個庫函式和列表(list)。我寫這個是為了在Hacker School(一所位於紐約的程式設計師培訓學校)的一個閃電秀中展示寫一個直譯器不是很難。一共只有116行的JavaScript程式碼,下文我會解釋它是如何執行的。
首先,讓我們學習一些Lisp。
Lisp基礎
這是一個原子,最簡單的Lisp形式:
1 |
1 |
這是另一個原子,一個字串:
1 |
"a" |
這是一個空列表:
()
這是一個包含了一個原子的列表:
1 |
(1) |
這是一個包含了兩個原子的列表:
1 |
(1 2) |
這是一個包含了一個原子和另一個列表的列表:
1 |
(1 (2)) |
這是一個函式呼叫。函式呼叫由一個列表組成,列表的第一個元素是要呼叫的函式,其餘的元素是函式的引數。函式first
接受一個引數(1 2)
,返回1
。
1 2 3 |
(first (1 2)) => 1 |
這是一個lambda表示式,即一個函式定義。這個函式接受一個引數x
,然後原樣返回它。
1 2 |
(lambda (x) x) |
這是一個lambda呼叫。lambda呼叫由一個列表組成,列表的第一個元素是一個lambda表示式,其餘的元素是由lambda表示式所定義的函式的引數。這個lambda表示式接受一個引數"lisp"
並返回它。
1 2 3 4 5 |
((lambda (x) x) "Lisp") => "Lisp" |
Little Lisp是如何執行的
寫一個Lisp直譯器真的很容易。
Little Lisp的程式碼包括兩部分:分析器和直譯器
分析器
分析分兩個階段:分詞(tokenizing)和加括號(parenthesizing)。
tokenize()
接受一個Lisp字串,在每個括號周圍加上空格,然後用空格作為分隔符拆分整個字串。舉個例子,它接受((lambda (x) x) "Lisp")
,將它變換為( ( lambda ( x ) x ) "Lisp" )
,然後進一步變換為['(', '(', 'lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')']
。
1 2 3 4 5 6 |
var tokenize = function(input) { return replace(/\(/g, ' ( ') .replace(/\)/g, ' ) ') .trim() .split(/\s+/); }; |
parenthesize()
接受由tokenize()
產生的詞元列表,生成一個巢狀的陣列來模擬出Lisp程式碼的結構。在這個巢狀的陣列中的每個原子會被標記為識別符號或文字表示式。例如,['(', '(', 'lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')']
被變換為:
1 2 3 |
[[{ type: 'identifier', value: 'lambda' }, [{ type: 'identifier', value: 'x' }], { type: 'identifier', value: 'x' }], { type: 'literal', value: 'Lisp' }] |
parenthesize()
一個挨一個地遍歷詞元。如果當前詞元是左括號,就開始構建一個新的陣列。如果當前詞元是原子,就標記其型別並將其新增到當前陣列中。如果當前詞元是右括號,就停止當前陣列的構建,繼續構建外層的陣列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var parenthesize = function(input, list) { if (list === undefined) { return parenthesize(input, []); } else { var token = input.shift(); if (token === undefined) { return list.pop(); } else if (token === "(") { list.push(parenthesize(input, [])); return parenthesize(input, list); } else if (token === ")") { return list; } else { return parenthesize(input, list.concat(categorize(token))); } } }; |
當parenthesize()
第一次被呼叫時,input
引數包含由tokenize()
返回的詞元列表陣列。例如:
1 |
['(', '(', 'lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')'] |
第一次呼叫parenthesize()
時,引數list
是undefined
,第2-3行執行,遞迴呼叫parenthesize()
,list
被設定為空陣列。
在遞迴中,第5行執行,input
的第一個左括號被移除。第9行中,傳一個新的空陣列給遞迴呼叫,開始一個新的空列表。
在新的遞迴中,第5行執行,從input
中移除了另一個左括號。與前面類似,第9行中,傳另一個新的空陣列給遞迴呼叫,開始另一個新的空列表。
繼續進入遞迴,現在input
是['lambda', '(', 'x', ')', 'x', ')', '"Lisp"', ')']
。第14行執行,token
被設定為lambda
,呼叫categorize()
函式並傳遞lambda
作為引數。categorize()
的第7行執行,返回一個物件,其type
屬性被設定為identifier
,value
屬性被設定為lambda
。
1 2 3 4 5 6 7 8 9 |
var categorize = function(input) { if (!isNaN(parseFloat(input))) { return { type:'literal', value: parseFloat(input) }; } else if (input[0] === '"' && input.slice(-1) === '"') { return { type:'literal', value: input.slice(1, -1) }; } else { return { type:'identifier', value: input }; } }; |
parenthesize()
的第14行向list
中加入由categorize()
返回的物件,然後用input
的剩餘元素和list
進一步遞迴。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var parenthesize = function(input, list) { if (list === undefined) { return parenthesize(input, []); } else { var token = input.shift(); if (token === undefined) { return list.pop(); } else if (token === "(") { list.push(parenthesize(input, [])); return parenthesize(input, list); } else if (token === ")") { return list; } else { return parenthesize(input, list.concat(categorize(token))); } } }; |
在遞迴中,下一個詞元是括號。parenthesize()
的第9行用一個新的空陣列遞迴建立一個新的空列表,進入新的遞迴,這時input
是['x', ')', 'x', ')', '"Lisp"', ')']
。第14行執行,token
被設定成x
,這樣建立了一個新的物件,其值為x
,型別為identifier
,然後將這個物件加入到list
中,然後接著遞迴。
在遞迴中,下一個詞元是右括號,第12行執行,返回完成了的list
:[{ type: 'identifier', value: 'x' }]
。
parenthesize()
繼續遞迴直到它處理完全部的輸入詞元,最後返回由包含了型別資訊的原子所組成的巢狀陣列。
parse()
是tokenize()
和parenthesize()
的組合呼叫:
1 2 3 |
var parse = function(input) { return parenthesize(tokenize(input)); }; |
如果原始的輸入給的是((lambda (x) x) "Lisp")
,則分析器給出的最後輸出是:
1 2 3 |
[[{ type: 'identifier', value: 'lambda' }, [{ type: 'identifier', value: 'x' }], { type: 'identifier', value: 'x' }], { type: 'literal', value: 'Lisp' }] |
直譯器
在分析結束後,解釋就開始了。
interpret()
接收parse()
的輸出並執行它。提供上例中的輸出,interpret()
會構造一個lambda表示式,然後用"Lisp"
作為引數呼叫它。lambda呼叫會返回"Lisp"
,這就是整個程式的輸出。
除了要執行的輸入外,interpret()
還接收一個執行上下文。執行上下文是變數和變數對應的值所儲存的地方。當一段Lisp程式碼被interpret()
執行時,執行上下文包含著這段程式碼可訪問的變數。
這些變數是分層儲存的。當前作用域的的變數處在最底層,在包含域中的變數處在上一層,包含域的上一層包含域中的變數處於更上層,依次類推。例如,在下面的程式碼中:
1 2 3 4 5 |
((lambda (a) ((lambda (b) (b a)) "b")) "a") |
第3行,執行上下文有兩個活動的作用域。內層的lambda形成了當前作用域。外層的lambda形成了包含作用域。當前作用域中b
被繫結到"b"
,包含作用域中a
被繫結到"a"
。當第3行執行時,直譯器嘗試在作用域中去查詢b
,它檢查當前作用域,發現了b
並返回它的值。還是在第3行上,直譯器嘗試去查詢a
,它檢查當前作用域,結果沒找到a
,所以它嘗試去包含域找,在那裡它找到了a
並返回它的值。
在Little Lisp中,執行上下文用一個物件來表示,這個物件通過呼叫Context
建構函式來生成。這個函式接受scope
引數,即一個由在當前作用域中的變數和值組成的物件;還接受parent
引數,如果parent
是undefined
,作用域即位於頂層,或者說是全域性的。
1 2 3 4 5 6 7 8 9 10 11 12 |
var Context = function(scope, parent) { this.scope = scope; this.parent = parent; this.get = function(identifier) { if (identifier in this.scope) { return this.scope[identifier]; } else if (this.parent !== undefined) { return this.parent.get(identifier); } }; }; |
我們已看到((lambda (x) x) "Lisp")
是如何被分析的,現在讓我們看看分析過後的程式碼是如何被執行的。
1 2 3 4 5 6 7 8 9 10 11 |
var interpret = function(input, context) { if (context === undefined) { return interpret(input, new Context(library)); } else if (input instanceof Array) { return interpretList(input, context); } else if (input.type === "identifier") { return context.get(input.value); } else { return input.value; } }; |
interpret()
第一次被呼叫時,context
是undefined
,第2-3行執行,建立一個執行上下文。
當初始上下文被例項化時,建構函式接受了一個叫library
的物件。這個物件包含了內建在語言中的函式:first
, rest
和print
。這些函式是用JavaScript寫的。
interpret()
用原始的輸入和新的上下文進行遞迴。
input
包含了上節中例子產生的輸出:
1 2 3 |
[[{ type: 'identifier', value: 'lambda' }, [{ type: 'identifier', value: 'x' }], { type: 'identifier', value: 'x' }], { type: 'literal', value: 'Lisp' }] |
因為input
是陣列而且context
已定義,第4-5行執行,interpretList()
被呼叫。
1 2 3 4 5 6 7 8 9 10 11 12 |
var interpretList = function(input, context) { if (input[0].value in special) { return special[input[0].value](input, context); } else { var list = input.map(function(x) { return interpret(x, context); }); if (list[0] instanceof Function) { return list[0].apply(undefined, list.slice(1)); } else { return list; } } }; |
在interpretList()
中,第5行遍歷input
陣列,對每個元素呼叫interpret()
。當interpret()
在lambda定義上呼叫時,interpretList()
再一次被呼叫。這次,interpretList()
的input
引數為:
1 2 |
[{ type: 'identifier', value: 'lambda' }, [{ type: 'identifier', value: 'x' }], { type: 'identifier', value: 'x' }] |
interpretList()
的第3行被呼叫,因為陣列的第一個元素lambda
是特殊形式。lambda()
被呼叫來建立lambda函式。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var special = { lambda: function(input, context) { return function() { var lambdaArguments = arguments; var lambdaScope = input[1].reduce(function(acc, x, i) { acc[x.value] = lambdaArguments[i]; return acc; }, {}); return interpret(input[2], new Context(lambdaScope, context)); }; } }; |
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"
。
1 2 3 4 5 6 7 8 9 10 11 |
var interpret = function(input, context) { if (context === undefined) { return interpret(input, new Context(library)); } else if (input instanceof Array) { return interpretList(input, context); } else if (input.type === "identifier") { return context.get(input.value); } else { return input.value; } }; |
interpret()
的第9行執行,這行做的事情僅僅是返回字面量物件的value
屬性'Lisp'
。interpretList()
的第5行的map操作至此完成。list
成為:
1 2 |
[function(args) { /* code to invoke lambda */ }, 'Lisp'] |
interpretList()
的第6行執行,發現List
的第一個元素是一個Javascript函式,這意味著list
是一個函式呼叫。第7行執行,呼叫lambda函式,並將list
的剩餘部分作為引數傳遞。
1 2 3 4 5 6 7 8 9 10 11 12 |
var interpretList = function(input, context) { if (input[0].value in special) { return special[input[0].value](input, context); } else { var list = input.map(function(x) { return interpret(x, context); }); if (list[0] instanceof Function) { return list[0].apply(undefined, list.slice(1)); } else { return list; } } }; |
在lambda呼叫函式中,第8行對lambda主體呼叫interpret()
,{ type: 'identifier', value: 'x' }
。
1 2 3 4 5 6 7 8 9 |
function() { var lambdaArguments = arguments; var lambdaScope = input[1].reduce(function(acc, x, i) { acc[x.value] = lambdaArguments[i]; return acc; }, {}); return interpret(input[2], new Context(lambdaScope, context)); }; |
interpret()
的第6行發現input
是一個識別符號型別的原子,第7行去上下文裡查詢識別符號x
,返回'Lisp'
。
1 2 3 4 5 6 7 8 9 10 11 |
var interpret = function(input, context) { if (context === undefined) { return interpret(input, new Context(library)); } else if (input instanceof Array) { return interpretList(input, context); } else if (input.type === "identifier") { return context.get(input.value); } else { return input.value; } }; |
'Lisp'
被lambda呼叫函式返回,接著被interpretList()
返回,接著被interpret()
返回,就是這樣。
全部的程式碼見GitHub repository。還可以看看lis.py,一個優秀而簡單的Scheme直譯器,由Peter Norvig用Python編寫。