基於JavaScript的簡單解釋器實現(一)——表達式的語法樹生成

前言

這個系列是關於CodeWars上的一條1Kyu題:Simple Interactive Interpreter。也就是實現一個簡單的交互式解釋器。
題目地址:http://www.codewars.com/kata/52ffcfa4aff455b3c2000750/train/javascript
github地址:https://github.com/woodensail/SimpleInteractiveInterpreter
本文地址:http://segmentfault.com/a/1190000004044789javascript

補充

11月26日更新:
增長了對左結合運算符的支持。
增長了變量統計功能,用於支持下一步的函數parser
當表達式不符合語法要求時,拋出異常java

實現要求

具體的細節能夠參見上面的原題網站,大概的要求以下:
1:支持變量賦值語句git

x = 7

2:支持四則運算,表達式中可使用變量github

x = (1 + 2) * y

3:函數聲明:express

fn add x y => x + y

4:函數調用segmentfault

z = add a b

5:其餘
也就是命名衝突檢測,做用域鏈等,你們本身看吧。數組

語法樹

這一章主要是完成語法樹的生成。其中因爲函數聲明部分過於簡單,不必生成語法樹,打算留到下一章一塊兒處理。因此只作了表達式的語法樹生成。函數

首先,題目所給的語言結構基本上是前綴表達式和中綴表達式的混雜。因此只須要將語句裏面中綴的部分轉化爲前綴便可獲得波蘭式。
固然,我爲了方便下一步處理仍是選擇將其進一步轉化爲語法樹的結構。可是實現思路依舊能夠參考波蘭式生成網站

準備工做

var SPACE = {}, params = [], operatorStack = [], dataStack = [SPACE], expressionFlag = true, lValue, rValue, operator, vars = {};

聲明變量:
params 用於存儲函數調用的參數,其實這裏不須要初始化,但我懶得改了。
operatorStack 運算符棧,用於存儲各類操做符
dataStack 存儲數據。包括數值,變量以及語法樹的節點
expressionFlag 因爲改語言中沒有逗號,因此沒有顯式的標誌來分割相鄰的兩個表達式。所以須要自行判斷前一個表達式是否結束。
lValue,rValue 相似params, 只不過是給運算符用的,其實能夠去掉,但我懶得改。
operator 一個用於存儲當前運算符的臨時變量this


tokens = tokens.slice();
tokens.push(')');
tokens.unshift('(');
while (tokens.length) {
  ……
}
var varList = [];
for (var k in vars) {
    varList.push(k);
}
if (dataStack[0] === SPACE) {
    dataStack.shift();
} else {
    throw 'expression error'
}
if (dataStack.length > 1) {
    throw 'expression error'
}
return [dataStack[0], varList];

複製token數組,防止修改原始數據。向開頭和結尾加上括號,以簡化後面的操做。最後就是開始主循環。
主循環結束後數據棧中的第一位元素則爲語法樹。若數據棧中元素數量多於一個或棧低佔位符被取出,說明語句有錯。

主循環

var next = tokens.pop();

首先取出一個token。須要注意的是這裏用的是pop,也就是從後向前掃描。而後根據token類型作不一樣處理。


1:token爲運算符

if (operators[next]) {
    while (true) {
        if (!operatorStack.length) {
            operatorStack.push(next);
            break;
        } else if (operatorStack[operatorStack.length - 1] === ')') {
            operatorStack.push(next);
            break;
        }  else if ((operators[operatorStack[operatorStack.length - 1]] === operators[next] ) && (next !== '=')) {
            operatorStack.push(next);
            break;
        } else if (operators[operatorStack[operatorStack.length - 1]] > operators[next]) {
            operatorStack.push(next);
            break;
        } else {
            operator = operatorStack.pop();
            lValue = dataStack.pop();
            rValue = dataStack.pop();
            dataStack.push([operator, lValue, rValue]);
        }
    }
    expressionFlag = true;
    continue;
}

a:若此時運算符棧爲空,則將該運算符入棧。
b:若棧頂運算符爲右括號,則將該運算符入棧。
c:若棧頂運算符優先級等於當前運算符且當前運算符不是左結合運算符,則將該運算符入棧。
d:若棧頂運算符優先級小於當前運算符,則將該運算符入棧。
e:若非以上四種狀況。則運算符棧出棧存入operator,數據棧出棧兩次分別存入lValue,rValue,而後將[operator, lValue, rValue]壓入數據棧。並繼續循環直到出現前四種狀況爲止。
前面的循環結束後將expressionFlag設爲真,以標誌當前表達式未結束。最後調用continue跳事後面的部分。


2:token爲左括號

else if (next === '(') {
    next = operatorStack.pop();
    while (next !== ')') {
        if (next === void 0) {
            break
        }
        lValue = dataStack.pop();
        rValue = dataStack.pop();
        dataStack.push([next, lValue, rValue]);
        next = operatorStack.pop();
    }
    continue;
}

持續出棧直到棧頂元素爲右括號爲止。對於每一個出棧的操做符將其存入operator並從數據棧中出棧兩次獲得lValue和rValue,並將[operator, lValue, rValue]壓入數據棧。最後continue跳事後續。


3:expressionFlag的判斷

if (expressionFlag) {
    expressionFlag = false;
} else {
    while (operatorStack.length) {
        operator = operatorStack.pop();
        if (operator === ')') {
            operatorStack.push(operator);
            break;
        } else {
            lValue = dataStack.pop();
            rValue = dataStack.pop();
            dataStack.push([operator, lValue, rValue]);
        }
    }
}

若token不是前兩種狀況,則須要判斷expressionFlag。若expressionFlag爲真則將其置爲假,標準該token處理完後,當前表達式能夠結束。
若其爲假則說明當前表達式已經結束,須要開始下一個表達式。此時須要將運算符棧重複出棧並與數據棧頂的兩位合併後壓入數據棧,直到棧頂運算符爲右括號爲止。


4:token爲右括號或其餘在函數列表中不存在的內容

if (next === ')') {
    expressionFlag = true;
    operatorStack.push(next);
} else if (!this.functions[next]) {
    if (!this.regexNum.test(next)) {
        vars[next] = 1;
    } else {
        next = Number(next);
    }
    dataStack.push(next);
}

將token入棧,其中若token爲右括號,則expressionFlag置真表示表達式未結束。若不爲右括號,當next爲純數字時將其轉化爲Number型,不然在變量表中標記。


5:token在函數表中存在

else {
    params = [next];
    for (var i in this.functions[next].params) {
        params.push(dataStack.pop());
    }
    dataStack.push(params);
}

初始化params而且第一位爲當前token。根據函數表中形參的數量,從數據棧中取出一樣數量的數據,壓入params。
將params壓入數據棧

運行分析:

這裏用'a*(test q (e+q))-(a+b)/e'作例子來跟蹤並展現程序是怎樣運行的。
首先tokenize,結果是:

["(","a","*","(","test","q","(","e","+","q",")",")","-","(","a","+","b",")","/","e",")"]

而後開始循環,我會在每一個操做的下發依次註明操做完成後的數據棧,運算符棧以及expressionFlag
1:')', 右括號,壓入運算符棧。
[] [')'] true
2:'e', 非運算符或括號或函數,壓入數據棧。
['e'] [')'] false
3:'/', 運算符,棧頂爲右括號,壓入運算符棧。
['e'] [')', '/'] true
4:')', 右括號,壓入運算符棧。
['e'] [')', '/', ')'] true
5:'b', 非運算符或括號或函數,壓入數據棧。
['e', 'b'] [')', '/', ')'] false
6:'+', 運算符,棧頂爲右括號,壓入運算符棧。
['e', 'b'] [')', '/', ')', '+'] true
7:'a', 非運算符或括號或函數,壓入數據棧。
['e', 'b', 'a'] [')', '/', ')', '+'] false
8:'a', 左括號,執行運算符出棧操做,直到右括號爲止。
['e', ['+','a','b']] [')', '/'] false
9:'-', 運算符,優先級小於棧頂元素,執行運算符出棧操做,而後壓入運算符棧。
[['/',['+','a','b'],'e']] [')', '-'] true
10:')', 右括號,壓入運算符棧。
[['/',['+','a','b'],'e']] [')', '-', ')'] true
11:')', 右括號,壓入運算符棧。
[['/',['+','a','b'],'e']] [')', '-', ')', ')'] true
12:'q', 非運算符或括號或函數,壓入數據棧。
[['/',['+','a','b'],'e'], 'q'] [')', '-', ')', ')'] false
13:'+', 運算符,棧頂爲右括號,壓入運算符棧。
[['/',['+','a','b'],'e'], 'q'] [')', '-', ')', ')', '+'] true
14:'e', 非運算符或括號或函數,壓入數據棧。
[['/',['+','a','b'],'e'], 'q', 'e'] [')', '-', ')', ')', '+'] false
15:'(', 左括號,執行運算符出棧操做,直到右括號爲止。
[['/',['+','a','b'],'e'], ['+','e','q']] [')', '-', ')'] false
16:'q', 非運算符或括號或函數,壓入數據棧。因爲expressionFlag爲false,須要提早出棧到右括號爲止。
[['/',['+','a','b'],'e'], ['+','e','q'], 'q'] [')', '-', ')',] false
17:'test', 函數,執行函數出棧程序。因爲expressionFlag爲false,須要提早出棧到右括號爲止。
[['/',['+','a','b'],'e'], ['test','q',['+','e','q']]] [')', '-', ')'] false
18:'(', 左括號,執行運算符出棧操做,直到右括號爲止。
[['/',['+','a','b'],'e'], ['test','q',['+','e','q']]] [')', '-'] false
18:'*', 運算符,優先級大於等於棧頂運算符,壓入運算符棧。
[['/',['+','a','b'],'e'], ['test','q',['+','e','q']]] [')', '-', '*'] true
18:'a', 非運算符或括號或函數,壓入數據棧。
[['/',['+','a','b'],'e'], ['test','q',['+','e','q']], 'a'] [')', '-', '*'] false
18:'(', 左括號,執行運算符出棧操做,直到右括號爲止。
[['-',['*','a',['test','q',['+','e','q']]],['/',['+','a','b'],'e']]] [] false


這是最後生成的語法樹:
圖片描述

總結

總之,語法樹就算是生成完畢了。上面的代碼還有缺陷,主要是沒有作異常檢查之類的。可是至少對於符合語法的表達式是沒什麼問題了。下一章會作函數聲明的解析和保存。

相關文章
相關標籤/搜索