基於JavaScript的小型Three-Pass編譯器實現

前言

昨天完成了codewars上的1級題簡單解釋器實現,今天突發奇想上去看看總共有多少1級題,而後發現總共也只有三題。並且,這三題都是編譯器解釋器相關的,因此乾脆都作了了事。
昨天作的是簡單解釋器,還有兩題分別是編譯器以及一個以類型爲重點的不完整的類lisp解釋器。其中編譯器這題和以前作的解釋器很像,因此就從編譯器開始吧:
題目地址:http://www.codewars.com/kata/tiny-three-pass-compiler/train/javascript
github地址:https://github.com/woodensail/SimpleInteractiveInterpreter/blob/master/tiny-three-pass-compiler.js
前文地址:http://segmentfault.com/a/1190000004047915
本文地址:http://segmentfault.com/a/1190000004049048javascript

與前文中解釋器的差異

首先這題的複雜度比以前要低的多,因此幾十分鐘就完成了。以前題目中的語言還算是結構完整,而這題裏的輸入都不能算是一個語言,只能說是帶參數的表達式而已。
沒有參數,沒有全局變量。相比算術表達式只多了參數而已。也所以,語法樹生成過程異常簡單,基本是和波蘭表達式生成沒區別了。java

這題比以前多出的部分則是語義分析和彙編代碼生成。
語義分析部分須要將常量運算優化掉,縮短代碼長度。
彙編代碼生成部分取代了前一題的執行部分。而是生成並返回彙編代碼便可。git

Pass1

這個沒啥好講的了,就是波蘭表達式的生成略改而已,改動部分包括多了值棧和參數列表。
另外就是對參數和當即量作了區分,這一點作的比前一篇要好。前一篇裏面參數與當即量部分不分家帶來了很多麻煩。github

Compiler.prototype.pass1 = function (program) {
    var tokens = this.tokenize(program), index = tokens.indexOf(']'), args = {}, next, dataStack = [];
    operatorStack = [];
    for (var i = 1; i < index; i++) {
        args[tokens[i]] = i - 1;
    }
    tokens = tokens.slice(index + 1);
    tokens.unshift('(');
    tokens.push(')');
    while ((next = tokens.pop()) !== void 0) {
        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]) {
                    operatorStack.push(next);
                    break;
                } else {
                    dataStack.push({op: operatorStack.pop(), a: dataStack.pop(), b: dataStack.pop()});
                }
            }
        } else if (next === '(') {
            while ((next = operatorStack.pop()) !== ')') {
                if (next === void 0) {
                    break
                }
                dataStack.push({op: next, a: dataStack.pop(), b: dataStack.pop()});
            }
        } else if (next === ')') {
            operatorStack.push(next);
        } else {
            if (args[next] !== void 0) {
                dataStack.push({op: 'arg', n: args[next]});
            } else {
                dataStack.push({op: 'imm', n: Number(next)});
            }
        }
    }
    return dataStack[0];
};

Pass2

pass2的目的是把當即量運算優化掉。實現方式是遞歸掃描。
若是當前節點是參數或當即量則直接返回當前節點。
不然依次對當前節點的兩個參數調用pass2。這步事後,a和b應該都是參數或當即量。
若是a和b都是當即量,那麼直接計算當前節點的結果。而後用計算出的結果構建一個新的當即量最後返回。
反之則直接返回當前節點。segmentfault

Compiler.prototype.pass2 = function (ast) {
    if ((ast.op === 'arg') || (ast.op === 'imm')) {
        return ast;
    }
    ast.a = this.pass2(ast.a);
    ast.b = this.pass2(ast.b);
    if ((ast.a.op === 'imm') && (ast.b.op === 'imm')) {
        return {op: 'imm', n: this.execOp(ast.op, ast.a.n, ast.b.n)}
    } else {
        return ast;
    }
};

Pass3

首先全部操做都是以'PU'壓棧結束的。
其中當即量和參數這倆個分別是將數字和參數放入寄存器後壓棧。
其餘的操做則是首先分別執行ab子節點。執行完畢後棧頂的一二元素分別是b,a個操做的結果。經過'PO','SW','PO'取出後執行算術操做,最後壓棧就完成了。優化

須要注意的是這種方式生成的彙編代碼有大量冗餘,主要是無用的['PU', 'PO']以及['PU', 'IM/AR', 'PU', 'PO' "SW", "PO"]。
前者能夠徹底刪去,後者能夠優化爲["SW" , 'IM/AR', "SW"]。this

Compiler.prototype.pass3 = function (ast) {
    switch (ast.op) {
        case 'imm':
            return ["IM " + ast.n, "PU"];
        case 'arg':
            return ["AR " + ast.n, "PU"];
        case '+':
            return this.pass3(ast.a).concat(this.pass3(ast.b)).concat(["PO", "SW", "PO", "AD", "PU"]);
        case '-':
            return this.pass3(ast.a).concat(this.pass3(ast.b)).concat(["PO", "SW", "PO", "SU", "PU"]);
        case '*':
            return this.pass3(ast.a).concat(this.pass3(ast.b)).concat(["PO", "SW", "PO", "MU", "PU"]);
        case '/':
            return this.pass3(ast.a).concat(this.pass3(ast.b)).concat(["PO", "SW", "PO", "DI", "PU"]);
    }
};

總結

這題真是至關簡單,我待會兒去看看最後一題去,那題彷佛和前兩題不太同樣。prototype

相關文章
相關標籤/搜索