自制Monkey語言編譯器:實現函式閉包功能和為語言增加複雜資料結構

starkbl發表於2021-09-09

Monkey語言有點類似於JS,它的函式可以當做引數進行傳遞,而且語法支援函式閉包功能,例如下面程式碼:

let newAdder = fn(x) { return fn(y) { return x + y;};};let addTwo = newAdder(3);
addTwo(2);

在上面程式碼中,我們把newAdder定義為一個函式變數,該函式里面又返回一個函式,在第二次定義變數addTwo時,它對應的是上面函式返回另一個函式,而且上面函式已經把x變數定義為3,於是addTwo(2)在執行時,它的返回值是5.為了實現這種函式閉包功能,我們必須為每個函式變數配置一個繫結環境,因此對上節程式碼做相應修改如下:

case "FunctionLiteral":            var props = {}
            props.token = node.token
            props.identifiers = node.parameters
            props.blockStatement = node.body            var funObj = new FunctionCall(props)
            funObj.enviroment  = this.newEnclosedEnvironment(this.enviroment)

上面程式碼為函式構建符號物件時,會專門配置一個繫結環境物件,於是上面程式碼addTwo(3)執行時,它遇到變數x,就能在函式對應的繫結環境中查詢到。我們在函式的解析執行部分做如下修改:

case "CallExpression":
....            // change 12 執行函式前保留當前繫結環境
            var oldEnviroment = this.enviroment            //設定新的變數繫結環境
            this.enviroment = functionCall.enviroment            //將輸入引數名稱與傳入值在新環境中繫結
            for (i = 0; i 

上面程式碼執行時,在執行呼叫函式前會將解析器的變數繫結環境設定為要執行函式的變數環境,這樣一來在函式體內定義的變數,即使在函式體外查詢不到,但是當函式執行時,還是能透過它自帶的變數繫結環境找到對應變數的值,完成上面程式碼後,我們就可以解釋執行開頭的Monkey程式碼,執行結果如下:

圖片描述

這裡寫圖片描述

示例中的newAdder稱之為高階函式,所謂高階函式就是能返回函式物件或是接收函式物件作為引數的函式。由於它返回的函式包含著自己的變數繫結環境,因此我們也稱newAdder為一個函式閉包。

接下來我們要為Monkey語言增加複雜資料結構的支援,目前我們的語言智慧識別整數,Boolean,這兩種很基礎的資料型別,為了語言的表達力能更強,我們要新增相應的複雜資料型別,例如字串,雜湊表,陣列等,接下來我們先新增的資料型別是字串。

所謂字串就是雙引號中包含一連串字元,例如"Hello World",我們現在lexer裡面增加相應token標誌,在MonkeyLexer.js中新增:

initTokenType() {
    ....    //change 1
    this.STRING = 25}

nextToken () {
    ....    // change 2
        case '"':        var str = this.readString()
        tok = new Token(this.STRING, str, lineCount)        break
        ....
}// change 3
    readString() {        // 越過開始的雙引號
        this.readChar()        var str =""
        while (this.ch != '"' && this.ch != this.EOF) {
            str += this.ch            this.readChar()
        }        if (this.ch != '"') {            return undefined
        }        return str 
    }

詞法解析器讀取第一個雙引號時,構造一個型別為STRING的token,然後依次讀取後面字元作為token物件內容,直到讀取第二個雙引號為止。完成上面程式碼後,詞法解析器就成功構造了型別為字串的Token。接下來我們在語法解析器中構造對應的語法節點。在MonkeyCompilerParser.js中新增如下程式碼:

class StringLiteral extends Node {  constructor(props) {    super(props)    this.token = props.token 
    this.tokenLiteral = props.token.getLiteral()    this.type = "String"
  }
}
....

class MonkeyCompilerParser {    constructor(lexer) {
    ...
    this.prefixParseFns[this.lexer.STRING] = 
    this.parseStringLiteral
    ...
    }
   ...
}

parseStringLiteral(caller) {      var props = {}
      props.token = caller.curToken      return new StringLiteral(props)
    }

上面程式碼定義了一個語法樹節點StringLiteral,然後在語法解析器的建構函式將字串的解析函式parseStringLiteral註冊到字首表示式解析函式呼叫表中,一旦型別為STRING的token物件傳遞給語法解析器時,它會呼叫parseStringLiteral構造一個StringLiteral語法節點。接下來我們要做解析器中,增加對字串節點物件的解析執行。在evaluator.js中新增如下程式碼:

class BaseObject {    constructor (props) {
    ...            //change 7
        this.STRING_OBJ = "String"
    }   
}//change 8class String extends BaseObject {    constructor(props) {        super(props)        this.value = props.value
    }

    inspect() {        return "content of string is: " + this.value
    }

    type() {        return this.STRING_OBJ
    }
}class MonkeyEvaluator {
....
    eval (node) {        var props = {}        switch (node.type) {            case "program":              return this.evalProgram(node)            // change 9
            case "String":
              props.value = node.tokenLiteral              return new String(props)
              ....
        }
        ....
    }
    evalInfixExpression(operator, left, right) {
    ....    //change 9 增加字串加法操作
        if (left.type() === left.STRING_OBJ && 
            right.type() === right.STRING_OBJ) {            return this.evalStringInfixExpression(operator,
                left, right)
        }
    }    //change 10 實現字串加法操作
    evalStringInfixExpression(operator, left, right) {        if (operator != "+") {            return this.newError("unknown operator for string operation")
        }        var leftVal = left.value 
        var rightVal = right.value 
        var props = {}
        props.value = leftVal + rightVal        console.log("reuslt of string add is: ", props.value)        return new String(props)
    }
....
}

程式碼在直譯器中先增加了一個String型別的符號物件,一旦從語法解析器接收到String型別的語法物件時,解析器就會構造對應的符號物件。接著我們增加了對“+”運算子的處理,當做加法時,如果解析器發現加號兩邊對應的都是字串物件,那麼就把兩個字串前後串聯起來,當上面程式碼完成後,我們在編輯框中輸入如下程式碼:

let s1 = "hello ";let s2 = "world!";let s3 = s1 + s2;

點選底下的parsing按鈕得到的結果為:

圖片描述

這裡寫圖片描述

從執行結果上看,我們的編譯器正確實現了兩個字串變數的加法操作。



作者:望月從良
連結:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3705/viewspace-2810107/,如需轉載,請註明出處,否則將追究法律責任。

相關文章