類Lisp解譯器JavaScript實現

來源:互聯網
上載者:User

標籤:翻譯   undefined   http   解釋   代碼   eof   rom   語言   .exe   

離職之前得把這坑填了……可能會有些倉促,如果有錯誤之處之後還請大家自行勘誤啦。

類 Lisp 語言文法
(define fib (lambda (n)    (if (< n 2) 1    (+ (fib (- n 1)) (fib (- n 2))))))

觀察上述斐波那契數產生函數。

  1. 使用 define 關鍵字綁定變數與實體
  2. 全部使用首碼運算式
  3. 沒有 return 關鍵字
  4. 迴圈多採用遞迴實現
  5. 函數被稱為“lambda運算式”,使用 lambda 關鍵字定義
  6. 函數必須有傳回值
編譯原理基本知識

使用某種語言來編寫的一段程式本質上是字串,編譯器/解譯器的任務是將這段字串翻譯為某個“執行器”可以理解的編碼。

我認為編譯器和解譯器最大的不同在於,前者存在明顯輸出,且輸出為某執行器可以理解的字串(編碼);後者沒有明顯輸出,它直接執行了該段字串(來源程式)所表達的東西(此處應當被提出質疑)。

本文只介紹解譯器的基本組成。

解譯器從接受來源程式到最終執行的基本過程有:

  1. 詞法分析。這個步驟會將原始碼分解為該語言的最小組成元素(一個一個的短字串)。如 var a = 1; 會被分解為 [‘var‘, ‘a‘, ‘=‘, ‘1‘]。形如這樣的短字串,我們稱其為“Token”。
  2. 文法分析。這個步驟會根據該語言的自身文法規則分析 Token 列表,檢查原始碼是否有語法錯誤,如果沒有,則解析出一個抽象文法樹(AST),語義及代碼塊之間的關係都由樹狀資料結構進行描述。
  3. 執行。當 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實現

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.