reactjs開發自制編程語言編譯器:實現變量綁定和函數調用

在編程時,我們會初始化一個變量,給變量賦初值,例如下面語句:

let x = 5*5;

上面代碼被編譯器解讀後,變量x就會和數值25綁定在一起。下次使用到變量x時,編譯器會讀取它綁定的值,然後用於相關代碼的執行,例如下面代碼:

let y = x + 5;

編譯器執行上面語句後,變量y就會跟數值30綁定起來,本節我們就先增加變量綁定的功能。

變量綁定功能不難實現,我們只要創建一個哈希表,把變量名和它對應的數值關聯起來即可,於是我們在MonkeyEvaluator.js中增加如下代碼:

class Enviroment {
constructor(props) {
this.map = {}
}
get(name) {
return this.map[name]
}
set(name, obj) {
this.map[name] = obj
}
}

在類Enviroment中,代碼創建了一個哈希表map,它提供兩個接口,get接收變量名,然後把其對應的數值返回,set用來把變量名跟一個數值關聯起來。在eval函數中,我們增加對let語句的解釋執行,然後把let後面的變量跟等號後面的數值關聯起來:

eval (node) {
var props = {}
switch (node.type) {
....
class MonkeyEvaluator {
// change 3
constructor (props) {
this.enviroment = new Enviroment()
}
eval (node) {
var props = {}
switch (node.type) {
case "program":
return this.evalProgram(node)
// change 1
case "LetStatement":
var val = this.eval(node.value)
if (this.isError(val)) {
return val
}
// change 4
this.enviroment.set(node.name.tokenLiteral, val)
return val
...
}
...
}

當解析器解析到LetStatement節點時,它執行等號右邊表達式,獲取要賦值給變量的數值,例如對前面的let語句let x = 25 * 25;代碼中的node.value對應的就是等號右邊的”25*25”,解析器執行右邊表達式後得到數值25,然後調用set接口,把變量名”x”與數值25關聯到哈希表中。

一旦變量和具體數值關聯後,編譯器在讀取變量名時就可以查詢其對應的數值,為了實現該功能,我們還得在eval函數中增添相應代碼:

eval (node) {
var props = {}
switch (node.type) {

....
case "Identifier":
return this.evalIdentifier(node, this.enviroment)
....
}
....
//change 6
evalIdentifier(node, env) {
var val = env.get(node.tokenLiteral)
if (val === undefined) {
return this.newError("identifier no found:"+node.name)
}
return val
}

當編譯器讀取到一個變量名時,它會調用evalIdentifier函數查找變量綁定的數值,該函數直接調用Eviroment類的get接口,傳入變量名把其綁定的數值拿出來。有了上面代碼後,我們就可以執行下面的語句:

let x = 10;
if (x) {
11;
}

把上面代碼輸入編輯框,點擊Parsing後得到如下執行結果:

reactjs開發自制編程語言編譯器:實現變量綁定和函數調用

根據結果來看,編譯器能夠解讀變量x,把它當做數值10,於是if條件成立,編譯器執行大括號裡面的代碼,也是就解讀了常量值11.

實現函數調用

當我們完成函數調用功能後,我們的編譯器就能執行如下代碼:

let addThree = fn(x){return x+3;}
addThree(3)

上面代碼被編譯器執行後,add函數調用會返回結果6.而且編譯器還能執行更復雜的函數間套調用,例如:

let callTwoTimes = fn(x ,func) {
func(func(x));
};
callTwoTimes(3)

上面代碼執行後,編譯器將會返回9。

為了實現上面功能,我們需要做兩件事,一是增加函數對應的符號對象,而是在解析函數eval中增加相應功能。首先我們看看如何構建函數的符號對象。在Monkey語言中,函數跟常量一樣,可以直接賦值給變量,於是它就能跟變量綁定起來,於是函數就可以像變量一樣作為參數進行傳遞,或作為一個函數調用的返回值,首先我們先增加函數的符號對象:

//change 8
class FunctionLiteral extends BaseObject {
constructor(props) {
this.token = props.token //對應關鍵字fn
this.parameters = props.identifiers
this.blockStatement = props.blockStatement
}
type() {
return this.FUNCTION_LITERAL
}
inspect() {
s = "fn("
var identifiers = []
for (var i = 0; i < this.paremeters.length; i++) {
identifiers[i] = this.parameters[i].tokenLiteral
}
s += identifiers.join(',')
s += "){\\n"
s += this.blockStatement.tokenLiteral
s += "\\n}"
}
}
//change 8
class FunctionCall extends BaseObject {
constructor(props) {
this.identifier = props.identifier
this.blockStatement = props.blockStatement
this.eviroment = new Enviroment()
}
}

我們定義函數調用對象FunctionCall時,專門配置一個環境對象,這樣函數中的變量綁定能跟函數外的執行環境分離開來。然後我們在解析函數eval中增加如下代碼:

eval (node) {
var props = {}
switch (node.type) {
...
//change 9
case "FunctionLiteral":
var props = {}
props.token = node.token
props.identifiers = node.parameters
props.blockStatement = node.body

return new FunctionCall(props)
case "CallExpression":
console.log("execute a function with content:",
node.function.tokenLiteral)
var functionCall = this.eval(node.function)
if (this.isError(functionCall)) {
return functionCall
}
console.log("evalute function call params:")
var args = this.evalExpressions(node.arguments)
if (args.length === 1 && this.isError(args[0])) {
return args[0]
}
for (var i = 0; i < args.length; i++) {
console.log(args[i].inspect())
}
return functionCall
....
}
....
}
//change 10
evalExpressions(exps) {
var result = []
for(var i = 0; i < exps.length; i++) {
var evaluated = this.eval(exps[i])
if (this.isError(evaluated)) {
return evaluated
}
result[i] = evaluated
}
return result
}

添加上面代碼後,在編輯框裡輸入如下代碼:

let add = fn(x,y){x+y;};
add(2+2,5+5);

然後點擊底下”Parsing”按鈕,於是我們剛才添加的代碼就會運行起來。當語法解析器讀取語句”let add = fn(x,y){x+y;};”時會構造一個LetStatement語法節點,在讀取等號右邊的”fn(x,y){x+y;}”時會構造一個FunctionLiteral語法節點,於是構建的LetStatement語法節點中,其name域為”add”,value域對應的就是FunctionLiteral語法節點。當該語法節點傳入eval函數進行解釋執行時,讀取到FuntioncLiteral語法節點,執行就會進入前面添加的“FunctionLiteral”分支,在該分支中執行器構建一個FunctionCall符號對象,然後代碼返回到LetStatementfen分支後,將變量名add和FunctionCall符號對象在哈希表中關聯起來。

接著語法解析器在解讀代碼”add(2+2,5+5)”時,它會構造一個CallExpression語法節點,然後該節點會傳入解釋執行函數eval,從而進入該函數的”CallExpression”分支,在該分支的代碼中,通過函數變量名add找到上一步創建的FunctionCall符號對象,從中拿到函數調用時的參數表達式語法節點,接著調用evalExpressions函數解釋執行參數表達式,從而獲得最後要傳入函數的結果,也就是evalExpressions會將”2+2”,”5+5”解釋執行,得到結果4和10,這兩個值將會作為調用參數,在執行函數add時傳入。

完成上面代碼並執行後,得到結果如下:

reactjs開發自制編程語言編譯器:實現變量綁定和函數調用

從輸出看,我們的編譯器能夠識別”add(2+2,5+5)”是函數調用,同時它把參數表達式“2+2”和”5+5“解釋執行後得到4和10,並把這兩個值作為函數的真正調用參數。

執行輸入參數表達式,確定輸入參數後,如何真正“調用”函數呢,顯然我們需要把函數內的代碼一行行的執行。有一個問題需要確定的是,函數被執行時,它的變量綁定環境對象必須和調用函數代碼所對應的變量綁定對象不同,要不然函數執行時就會產生錯誤,例如下面代碼:

let i = 5;
k = 6
fn() {
let i = 10;
print(i);
print(k)
}();
print(i)

上面代碼有兩個同名變量,第一個變量i跟數值5綁定,第二個變量i在函數體內,跟數值10綁定,函數體內的print(i)輸出結果是10,最後一句print(i)輸出結果是5,因此兩個同名變量i必須跟不同的數值綁定,於是兩個同名變量i得在不同的Enviroment對象中實現變量綁定。由此我們要實現變量綁定環境的切換,在函數fn外部有一個變量綁定環境,在那裡變量i和5綁定,變量k和6綁定,在fn內部又有一個變量綁定環境,在那裡,一個新的變量i與10綁定,如下圖:

reactjs開發自制編程語言編譯器:實現變量綁定和函數調用

當程序沒有調用fn前,程序的綁定環境是第一個方塊,當程序調用fn後,綁定環境變為第二個方塊,當fn執行時訪問到變量k,這時在第二個方塊代表的綁定環境中找不到對應關係,於是編譯器在執行代碼時跑到上一個綁定環境去查找。為了實現該功能,我們添加如下代碼:

class Enviroment {
constructor(props) {
this.map = {}
//change 10
this.outer = undefined

}
get(name) {
var obj = this.map[name]
if (obj != undefined) {
return obj
}
//change 12 在當前綁定環境找不到變量時,通過回溯
//查找外層綁定環境是否有給定變量
if (this.outer != undefined) {
obj = this.outer.get(name)
}
return obj
}
set(name, obj) {
this.map[name] = obj
}
}

Enviroment類就是用來將變量與數值綁定的“環境”,get接口根據輸入的變量名在哈希表中查詢其對應的數值,set用於將變量名與給定數值綁定起來,其中的outer用於將不同的綁定環境連接起來,例如上面講過的函數調用例子,在函數調用前代碼執行對應一個Enviroment對象,當函數調用後,在執行函數體內的語句時對應一個新的Enviroment對象,後者用outer指針跟前者關聯起來,outer就如上圖兩個方塊間連接起來的箭頭。當在函數體內查找一個變量與數值的對應關係時,如果在當前的綁定環境中找不到,就通過outer指針到上一個綁定環境去找,例如在上面的示例代碼例子裡,函數執行時要訪問變量k的值,這個變量在函數執行時的綁定環境裡是找不到的,但是上面實現的get函數會通過outer進入上一個綁定環境然後再查詢k與數值的綁定,這時候編譯器就能找到變量k綁定的數值。

接著我們在MonkeyEvaluator裡面先增加對Enviroment變量的創建:

class MonkeyEvaluator {
// change 3
constructor (props) {
this.enviroment = new Enviroment()
}
// change 11
newEnclosedEnvironment(outerEnv) {
var env = new Enviroment()
env.outer = outerEnv
return env
}
....
}

然後再解析LetStatement的分支處理中,增加變量與數值綁定的操作:

eval (node) {
var props = {}
switch (node.type) {
case "LetStatement":
var val = this.eval(node.value)
if (this.isError(val)) {
return val
}
// change 4
this.enviroment.set(node.name.tokenLiteral, val)
return val
//change 5
case "Identifier":
console.log("variable name is:" + node.tokenLiteral)
var value = this.evalIdentifier(node, this.enviroment)
console.log("it is binding value is " + value.inspect())
return value
....
}

當編譯器執行let賦值語句時,它會調用Enviroment類的set函數將變量名與數值在哈希表中關聯起來,當編譯器讀取到一個變量時,編譯器在解釋執行時進入”Identifier”分支,然後編譯器從Enviroment的哈希表中把變量對應的數值讀取出來。完成上面代碼後,我們在編輯框中輸入如下代碼:

let x = 10;
x;

點擊parsing按鈕後,得到結果如下:

reactjs開發自制編程語言編譯器:實現變量綁定和函數調用

由此可見,我們的編譯器在執行代碼時,遇到變量x後,它從綁定環境中讀取到變量x對應的數值是10.接下來我們看看如何執行函數調用。在”CallExpression”分支中,我們添加如下代碼:

case "CallExpression":
....
// change 12 執行函數前保留當前綁定環境

var oldEnviroment = this.enviroment
//為函數調用創建新的綁定環境
functionCall.enviroment = this.newEnclosedEnvironment(oldEnviroment)
//設置新的變量綁定環境
this.enviroment = functionCall.enviroment
//將輸入參數名稱與傳入值在新環境中綁定
for (i = 0; i < functionCall.identifiers.length; i++) {
var name = functionCall.identifiers[i].tokenLiteral
var val = args[i]
this.enviroment.set(name, val)
}
//執行函數體內代碼
var result = this.eval(functionCall.blockStatement)
//執行完函數後,裡面恢復原有綁定環境
this.enviroment = oldEnviroment
if (result.type() === result.RETURN_VALUE_OBJECT) {
console.log("function call return with :",
result.valueObject.inspect())
return result.valueObject
}
return result

在執行被調函數的代碼前,我們先把當前綁定環境緩存在oldEnviroment,然後newEnclosedEnvironment創建新的執行環境,該函數在創建新的Enviroment變量時,會把其outer指針指向oldEnviroment綁定對象,這就像前面示例圖中,後一個方塊伸出一個箭頭指向前面那個方塊。

然後編譯器將綁定環境對象設置成新生成的Enviroment對象,然後將函數參數變量名和參數值在新綁定環境對象中關聯起來,然後執行“this.eval(functionCall.blockStatement)”,這條語句的執行相當於編譯器解釋執行函數體內的代碼,注意這時候解釋器的綁定環境變量已經變了。如果函數體內有return語句產生返回值的話,返回值對象會存儲在代碼裡的result變量裡,然後解釋器將返回結果打印出來。有了上面代碼後,我們在編輯框裡輸入如下代碼:

let x = 5;
let k = 6;
let add = fn(x,y){
let i = 10;
return x + y + i + k;
};
add(1,2);

然後點擊parsing按鈕,編譯器解釋執行上面代碼後,情況如下:

reactjs開發自制編程語言編譯器:實現變量綁定和函數調用

從運行結果看,add輸入參數是1,2,執行後返回結果是19,這意味著函數體內的變量i對應的值是10而不是外層變量i對應的5,由此我們編譯器對代碼執行的結果是正確的,它能將變量與正確的數值對應起來,在函數體內的綁定環境裡並沒有定義變量k,編譯器在執行時,會通過當前綁定環境Enviroment的outer指針找到上一個綁定環境,從而找到變量k對應的數值。

至此我們的編譯器就具備了變量綁定功能和函數的調用執行功能。


分享到:


相關文章: