標籤:翻譯 undefined http 解釋 代碼 eof rom 語言 .exe
離職之前得把這坑填了……可能會有些倉促,如果有錯誤之處之後還請大家自行勘誤啦。
類 Lisp 語言文法
(define fib (lambda (n) (if (< n 2) 1 (+ (fib (- n 1)) (fib (- n 2))))))
觀察上述斐波那契數產生函數。
- 使用
define 關鍵字綁定變數與實體
- 全部使用首碼運算式
- 沒有
return 關鍵字
- 迴圈多採用遞迴實現
- 函數被稱為“lambda運算式”,使用
lambda 關鍵字定義
- 函數必須有傳回值
編譯原理基本知識
使用某種語言來編寫的一段程式本質上是字串,編譯器/解譯器的任務是將這段字串翻譯為某個“執行器”可以理解的編碼。
我認為編譯器和解譯器最大的不同在於,前者存在明顯輸出,且輸出為某執行器可以理解的字串(編碼);後者沒有明顯輸出,它直接執行了該段字串(來源程式)所表達的東西(此處應當被提出質疑)。
本文只介紹解譯器的基本組成。
解譯器從接受來源程式到最終執行的基本過程有:
- 詞法分析。這個步驟會將原始碼分解為該語言的最小組成元素(一個一個的短字串)。如
var a = 1; 會被分解為 [‘var‘, ‘a‘, ‘=‘, ‘1‘]。形如這樣的短字串,我們稱其為“Token”。
- 文法分析。這個步驟會根據該語言的自身文法規則分析 Token 列表,檢查原始碼是否有語法錯誤,如果沒有,則解析出一個抽象文法樹(AST),語義及代碼塊之間的關係都由樹狀資料結構進行描述。
- 執行。當 AST 被產生之後,解譯器會繼續對 AST 進行分析,遵照 AST 表達出的語義來執行,其執行結果就是原始碼所想要的執行結果。一些語言特性,比如閉包,就是在執行函數中實現的。
實現
接下來我們將實現一個支援整數與浮點數運算、數群組類型、lambda 運算式,有著基本邏輯語句的類 Lisp 語言解譯器。
詞法分析
由於類 Lisp 語言本身文法比較簡潔,所以詞法分析的實現也會比較簡單:
function tokenize(program) { return program.replace(/\(/g, ‘ ( ‘).replace(/\)/g, ‘ ) ‘).split(‘ ‘)}
文法分析
在產生 AST 的過程中,需要將每一個 Token 的“身份”做確認,比如 var 和 a 是關鍵字,1 是整數,給 Token 做身份標記。這裡使用數組實現 AST。
// 產生抽象文法樹function read_from_tokens(tokens) { if (tokens.length === 0) { throw new Error(‘unexpected EOF while reading‘) } let token = tokens.shift() while (token === ‘‘) { token = tokens.shift() } if (‘(‘ === token) { let L = [] while (tokens[0] === ‘‘) { tokens.shift() } while (tokens[0] !== ‘)‘) { L.push(read_from_tokens(tokens)) while (tokens[0] === ‘‘) { tokens.shift() } } tokens.shift() return L } else if (‘)‘ === token) { throw new Error(‘unexpected )‘) } else { return atom(token) }}// 元類型 Meta,所有資料類型的基類,隨著解譯器的完善會變得有用,亦在語義的角度可體現出其合理性,但在本文中不做討論class Meta { constructor(value) { this.value = value }}// 符號類型 如 if define 自訂變數等語言關鍵字都屬於此類型class Sym extends Meta { constructor(value) { super(value) }}// 將 token 的類型具體化function atom(token) { let temp = parseInt(token) if (isNaN(temp)) { return new Sym(token) } else if (token - temp === 0) { return temp } else { return parseFloat(token) }}
執行
執行的過程是解譯器的核心,在這裡實現為一個 eval 函數。
eval 函數
eval 函數的大致結構是一個狀態機器,按照該語言的文法規則對 AST 進行解析。
function eval(x, env=global_env) { if (x instanceof Sym) { // 如果該 Token 是關鍵字 return env.find(x.value)[x.value] // 在當前範圍中尋找與該 Token 綁定的實體 } else if (! (x instanceof Array)) { // 不是數組 return x // 直接返回(因為此時會認為它是整數或浮點數) } else if (x[0].value == ‘if‘) { // 如果是 if let [sym, test, conseq, alt] = x // 按照該語言文法中約定的 if 語句的格式提取資訊 let exp = (eval(test, env) ? conseq : alt) return eval(exp, env) } else if (x[0].value == ‘define‘) { // 如果是 define let [vari, exp] = x.slice(1) env.add(vari.value, eval(exp, env)) } else if (x[0].value == ‘lambda‘) { // 如果是 lambda let [parms, body] = x.slice(1) return new Procedure(parms, body, env) // 建立一個 Procedure 執行個體 } else if (x[0].value == ‘quote‘) { // 如果是 quote let [sym, exp] = x return exp } else { // 否則(這裡可能的情況是:x 是一個數組或一個過程(函數,在這裡的實現為 Procedure 的執行個體,下面會講到)) let proc = eval(x[0], env) let args = [] x.slice(1).forEach(function(arg) { args.push(eval(arg, env)) }) if (proc instanceof Procedure) { return proc.execute.call(proc, args) } return proc.apply(this, args) }}
函數與環境
在該語言中,我們規定,範圍的界線是函數,且該範圍是詞法範圍(與 JavaScript 相同)。
所以每當碰到函數調用的時候,我們都需要建立一個新的求值環境,並使該函數被調用時所在的環境成為這個新的求值環境的父環境(如果不明白為什麼這樣規定,請自行搜尋詞法範圍)。故當我們在任何地方尋找變數時,都應順著環境鏈(範圍鏈)向上尋找,直到找到。如果找不到,則拋異常。
由此,我們可以將範圍理解為一個類似單向鏈表的資料結構。
// Procedure 類,該語言中函數的實現class Procedure { constructor(parms, body, env) { this.parms = parms this.body = body this.env = env } execute(args) { return eval(this.body, new Env(this.parms, args, this.env)) }}// Env 類,該語言中求值環境的實現class Env { constructor(parms=[], args=[], outer=null) { this.e = new Object() this.init(parms, args) this.outer = outer // 父環境 } // 在該環境中尋找某個變數 find(vari) { if ((! (vari in this.e)) && (! this.outer)) { throw new ReferenceError(‘variable ‘ + vari + ‘ is undefined.‘) } return vari in this.e ? this.e : this.outer.find(vari) } init(keys, values) { keys.forEach((key, index) => { this.e[key.value] = values[index] }) } assign(subEnv) { Object.assign(this.e, subEnv) } add(key, value) { this.e[key] = value }}// 初始化一個全域環境let global_env = new Env()global_env.assign(baseEnv)
尾聲
至此,這個簡單的解譯器就已經完成了,涉及到更多細節,如異常定義、全域環境定義,可以點此查看完整的代碼。
我建議大家如果有興趣可以自己動手實現一下,對解譯器原理以及閉包會有更深刻的理解。
本文的完成比較倉促,如果大家有什麼疑問可以點此閱讀該解譯器的 Python 實現,作者講解得非常詳細,也有很多測試案例,我正是閱讀了這篇文章才寫了 JavaScript 版。
如果有任何錯誤之處,請自行勘誤,或者通過郵箱([email protected])和 GitHub (Sevenskey)聯絡我。
感謝閱讀qwq
類Lisp解譯器JavaScript實現