如何用JavaScript實現一門編程語言

解析器可以直接操作標記流而不用處理單個字符。爲了降低複雜度,它同樣定義了很多工具函數。這裏首先討論一下構成解析器的主要函數。我們從一個上層的lambda解析器開始講起:

function parse_lambda() {
    return {
        type: "lambda",
        vars: delimited("(", ")", ",", parse_varname),
        body: parse_expression()
    };
}

解析標記流的過程中,當遇到lambda關鍵字則會調用上面的函數,所以它關心的就是解析參數名字:被圓括號包裹並由逗號分隔。相比於直接將代碼實現在parse_lambda中,我更傾向於寫一個delimited函數,delimited接收start,stop,separator及parser四個參數,parser是一個函數,會解析start和stop之間的標記。上面parse_lambda中將parse_varname函數傳遞給parser,parse_varname函數在解析到非變量時會拋出錯誤。parse_lambda函數體是一個表達式,所以可通過parse_expression解析得到。

delimited是一個相對底層的函數:

function delimited(start, stop, separator, parser) {
    var a = [], first = true;
    skip_punc(start);
    while (!input.eof()) {
        if (is_punc(stop)) break;
        if (first) first = false; else skip_punc(separator);
        if (is_punc(stop)) break; // the last separator can be missing
        a.push(parser());
    }
    skip_punc(stop);
    return a;
}

正如你所看到的,它使用了更多的工具函數:is_punc和skip_punc。當前token如果是給定的符號,is_punc會返回true(不會消耗掉當前token),skip_punc會確認token是給定的符號(否則會拋錯)並將其從輸入流中丟棄。

解析整體程序(prog節點)的函數可能是最簡單的:

function parse_toplevel() {
    var prog = [];
    while (!input.eof()) {
        prog.push(parse_expression());
        if (!input.eof()) skip_punc(";");
    }
    return { type: "prog", prog: prog };
}

由於不支持語句,所以我們就簡單通過不停地調用parse_expression()函數來讀取輸入流中的表達式。使用skip_punc(";")因爲表達式要求由分號分隔。

另外一個簡單的示例:parse_if():

function parse_if() {
    skip_kw("if");
    var cond = parse_expression();
    if (!is_punc("{")) skip_kw("then");
    var then = parse_expression();
    var ret = { type: "if", cond: cond, then: then };
    if (is_kw("else")) {
        input.next();
        ret.else = parse_expression();
    }
    return ret;
}

parse_if中通過skip_kw(判斷當前token如果不是給定的關鍵字會拋出錯誤)跳過if關鍵字 ,通過parse_expression()來讀取條件。接下來,如果條件成立分支(consequent branch)不是以左花括號“{”開始,則要求有then關鍵字(如果不要求then關鍵字,這樣的語法可能會太罕見)。分支就是表達式,所以我們通過parse_expression()解析它們。else 分支是可選的,所以需要先檢測關鍵字是否存在再來決定是否解析它。

有很多小工具函數的好處就是能很大程度上保持代碼簡潔。我們幾乎像使用了專門用做解析的高級語言一樣來實現解析器。所有這些小工具函數是“互斥的”,例如,parse_atom()函數是主要的調度者 — 基於當前的token來調用其它函數。其中一個就是parse_if()(當前token爲 if 時會被調用) ,並且它會進一步調用parse_expression()。但parse_expression()會再次調用parse_atom()。之所以沒有發生死循環,是因爲每步處理中,每個函數都會至少消費掉一個token。

上述類型的解析器叫做“遞歸下降解析器”(recursive descent parser),也可能算是可以手寫實現的最簡單類型。

更低層次:parse_atom()和parse_expression()
parse_atom()依據當前的token完成了主要的調度工作:

function parse_atom() {
    return maybe_call(function(){
        if (is_punc("(")) {
            input.next();
            var exp = parse_expression();
            skip_punc(")");
            return exp;
        }

        // This is the proper place to implement unary operators.
        // Following is the code for boolean negation, which is present
        // in the final version of lambda.js, but I'm leaving it out
        // here since support for it is not implemented in the interpreter
        // nor compiler, in this tutorial:
        //
        // if (is_op("!")) {
        //     input.next();
        //     return {
        //         type: "not",
        //         body: parse_atom()
        //     };
        // }

        if (is_punc("{")) return parse_prog();
        if (is_kw("if")) return parse_if();
        if (is_kw("true") || is_kw("false")) return parse_bool();
        if (is_kw("lambda") || is_kw("λ")) {
            input.next();
            return parse_lambda();
        }
        var tok = input.next();
        if (tok.type == "var" || tok.type == "num" || tok.type == "str")
            return tok;
        unexpected();
    });
}

如果解析到了一個開括號,則其必定是一個括號表達式 — 因此首先會跳過開括號,然後調用parse_expression()並預期以閉括號結尾。如果解析到了某個關鍵字,則會調用對應關鍵字的解析函數。如果解析到了一個常量或者標識符,則會原樣返回token。如果所有情況都未滿足,則會調用unexpected()拋出一個錯誤。

當期望是一個原子表達式(atomic expression)但是解析到了“{”,解析器會調用parse_prog來解析整個序列的表達式。正如下面所定義的。它做了一些小的優化 — 如果prog節點爲空,則直接返回 FALSE。如果程序只包含一個表達式,則返回表達式的解析結果。否則返回一個包含表達式的"prog"節點。

// we're going to use the FALSE node in various places,
// so I'm making it a global.
var FALSE = { type: "bool", value: false };

function parse_prog() {
    var prog = delimited("{", "}", ";", parse_expression);
    if (prog.length == 0) return FALSE;
    if (prog.length == 1) return prog[0];
    return { type: "prog", prog: prog };
}

下面是parse_expression()函數。與parse_atom()相反,這個函數將會使用maybe_binary()來儘可能地向右擴展一個表達式,將會在下面解釋。

function parse_expression() {
    return maybe_call(function(){
        return maybe_binary(parse_atom(), 0);
    });
}

maybe_*函數
這些函數會根據表達式後面跟隨的具體內容來決定是用另外一個節點來包裹表達式,還是直接按原樣返回表達式。

maybe_call()非常簡單。接收一個解析當前表達式的函數。如果在那個表達式後解析到一個“(”符號,則其必定是一個調用“call”節點,將會由parse_call()來處理(包含在下面代碼中)。可以再次注意到delimited()如何被用來讀取參數列表。

function maybe_call(expr) {
    expr = expr();
    return is_punc("(") ? parse_call(expr) : expr;
}

function parse_call(func) {
    return {
        type: "call",
        func: func,
        args: delimited("(", ")", ",", parse_expression)
    };
}

操作符優先級
maybe_binary(left, my_prec) 用來組合類似 1 + 2 * 3 的二元表達式。能正確解析它們的技巧就是要準確地定義操作符的優先級,如下:

var PRECEDENCE = {
    "=": 1,
    "||": 2,
    "&&": 3,
    "<": 7, ">": 7, "<=": 7, ">=": 7, "==": 7, "!=": 7,
    "+": 10, "-": 10,
    "*": 20, "/": 20, "%": 20,
};

上述定義意味着 要比 + 有更強的約束,所以對一個表達式,比如 1 + 2 3,必須被解析爲 (1 + (2 3)) 而不是 ((1 + 2) 3),後者是按通常從左到右(left-to-right)順序解析的結果。

這裏的技巧就是讀一個原子表達式(只有1)並將它(作爲左參數)和當前的優先級(my_prec)一起傳給maybe_binary()。maybe_binary將會解析緊跟着的內容。如果沒有解析到運算符,或者運算符的優先級更低,則直接原樣返回左參數。

如果解析到一個更高優先級的運算符,則會將左參數包裹到一個新的二元表達式"binary"節點中,並在新的優先級(*)上對右參數重複上面的技巧:

function maybe_binary(left, my_prec) {
    var tok = is_op();
    if (tok) {
        var his_prec = PRECEDENCE[tok.value];
        if (his_prec > my_prec) {
            input.next();
            var right = maybe_binary(parse_atom(), his_prec) // (*);
            var binary = {
                type     : tok.value == "=" ? "assign" : "binary",
                operator : tok.value,
                left     : left,
                right    : right
            };
            return maybe_binary(binary, my_prec);
        }
    }
    return left;
}

需要注意的是在返回二元表達式之前,爲了能將表達式包裹到另一個緊跟着的具有更高優先級的表達式中,必須在舊的優先級(my_prec)上也調用maybe_binary。如果這些難以理解,請一遍遍反覆閱讀代碼(也許可以在腦中嘗試某些輸入表達式的執行)直到搞明白了。

最終,由於my_prec初始化爲0,任何運算符都會構建出一個二元表達式"binary"節點(或當運算符爲=時構建一個賦值"assign"節點)。解析器中還有另外一些其它的函數,所以我將整體解析函數放到了下面(大約150行)。

var FALSE = { type: "bool", value: false };
function parse(input) {
    var PRECEDENCE = {
        "=": 1,
        "||": 2,
        "&&": 3,
        "<": 7, ">": 7, "<=": 7, ">=": 7, "==": 7, "!=": 7,
        "+": 10, "-": 10,
        "*": 20, "/": 20, "%": 20,
    };
    return parse_toplevel();
    function is_punc(ch) {
        var tok = input.peek();
        return tok && tok.type == "punc" && (!ch || tok.value == ch) && tok;
    }
    function is_kw(kw) {
        var tok = input.peek();
        return tok && tok.type == "kw" && (!kw || tok.value == kw) && tok;
    }
    function is_op(op) {
        var tok = input.peek();
        return tok && tok.type == "op" && (!op || tok.value == op) && tok;
    }
    function skip_punc(ch) {
        if (is_punc(ch)) input.next();
        else input.croak("Expecting punctuation: \"" + ch + "\"");
    }
    function skip_kw(kw) {
        if (is_kw(kw)) input.next();
        else input.croak("Expecting keyword: \"" + kw + "\"");
    }
    function skip_op(op) {
        if (is_op(op)) input.next();
        else input.croak("Expecting operator: \"" + op + "\"");
    }
    function unexpected() {
        input.croak("Unexpected token: " + JSON.stringify(input.peek()));
    }
    function maybe_binary(left, my_prec) {
        var tok = is_op();
        if (tok) {
            var his_prec = PRECEDENCE[tok.value];
            if (his_prec > my_prec) {
                input.next();
                return maybe_binary({
                    type     : tok.value == "=" ? "assign" : "binary",
                    operator : tok.value,
                    left     : left,
                    right    : maybe_binary(parse_atom(), his_prec)
                }, my_prec);
            }
        }
        return left;
    }
    function delimited(start, stop, separator, parser) {
        var a = [], first = true;
        skip_punc(start);
        while (!input.eof()) {
            if (is_punc(stop)) break;
            if (first) first = false; else skip_punc(separator);
            if (is_punc(stop)) break;
            a.push(parser());
        }
        skip_punc(stop);
        return a;
    }
    function parse_call(func) {
        return {
            type: "call",
            func: func,
            args: delimited("(", ")", ",", parse_expression),
        };
    }
    function parse_varname() {
        var name = input.next();
        if (name.type != "var") input.croak("Expecting variable name");
        return name.value;
    }
    function parse_if() {
        skip_kw("if");
        var cond = parse_expression();
        if (!is_punc("{")) skip_kw("then");
        var then = parse_expression();
        var ret = {
            type: "if",
            cond: cond,
            then: then,
        };
        if (is_kw("else")) {
            input.next();
            ret.else = parse_expression();
        }
        return ret;
    }
    function parse_lambda() {
        return {
            type: "lambda",
            vars: delimited("(", ")", ",", parse_varname),
            body: parse_expression()
        };
    }
    function parse_bool() {
        return {
            type  : "bool",
            value : input.next().value == "true"
        };
    }
    function maybe_call(expr) {
        expr = expr();
        return is_punc("(") ? parse_call(expr) : expr;
    }
    function parse_atom() {
        return maybe_call(function(){
            if (is_punc("(")) {
                input.next();
                var exp = parse_expression();
                skip_punc(")");
                return exp;
            }
            if (is_punc("{")) return parse_prog();
            if (is_kw("if")) return parse_if();
            if (is_kw("true") || is_kw("false")) return parse_bool();
            if (is_kw("lambda") || is_kw("λ")) {
                input.next();
                return parse_lambda();
            }
            var tok = input.next();
            if (tok.type == "var" || tok.type == "num" || tok.type == "str")
                return tok;
            unexpected();
        });
    }
    function parse_toplevel() {
        var prog = [];
        while (!input.eof()) {
            prog.push(parse_expression());
            if (!input.eof()) skip_punc(";");
        }
        return { type: "prog", prog: prog };
    }
    function parse_prog() {
        var prog = delimited("{", "}", ";", parse_expression);
        if (prog.length == 0) return FALSE;
        if (prog.length == 1) return prog[0];
        return { type: "prog", prog: prog };
    }
    function parse_expression() {
        return maybe_call(function(){
            return maybe_binary(parse_atom(), 0);
        });
    }
}

致謝
我是在學習Marijn Haverbeke的parse-js庫(Common Lisp)時明白瞭如何去寫一個有意義的解析器。上面的解析器仿照他的代碼,儘管是爲了解析一個更簡單的語言。

以上就是本次分享的所有內容,想要了解更多歡迎前往公衆號:web前端開發社區,每日干貨分享

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章