用JavaScript實現一門編程語言 3-3 (解析器之詞法分析器)

詞法分析器運行在字符輸入流之上,經過相同的接口返回一個流對象,可是經過peek() /
next()返回的值是tokens。一個token是一個對象,包含兩個屬性:typevalue。下面是一些支持tokens的例子:git

{ type: "punc", value: "(" }           // 標點符號: 括號、逗號、分號等等。
{ type: "num", value: 5 }              // 數字
{ type: "str", value: "Hello World!" } // 字符串
{ type: "kw", value: "lambda" }        // 關鍵字
{ type: "var", value: "a" }            // 標識符
{ type: "op", value: "!=" }            // 運算符複製代碼

空格和評論會被跳過,不會返回tokensbash

爲了寫詞法分析器,咱們須要深刻研究咱們語言的語法。要注意當前字符(經過input.peek()返回),咱們須要怎樣處理:ide

  • 首先,跳過空格。函數

  • If input.eof() then return null.工具

  • 若是是井號 (#),跳過評論。ui

  • 若是是引號,識別成字符串。spa

  • 若是是數字,咱們就處理成數字。.net

  • 若是是字母,就處理成標識符或者關鍵詞。code

  • 若是是一個標點符號,返回一個標點符號token對象

  • 若是是運算符,返回運算符token

  • 若是以上都沒有就經過input.croak()拋出異常。

下面是「read_next」函數 —— 詞法分析器的核心 :

function read_next() {
    read_while(is_whitespace);
    if (input.eof()) return null;
    var ch = input.peek();
    if (ch == "#") {
        skip_comment();
        return read_next();
    }
    if (ch == '"') return read_string();
    if (is_digit(ch)) return read_number();
    if (is_id_start(ch)) return read_ident();
    if (is_punc(ch)) return {
        type  : "punc",
        value : input.next()
    };
    if (is_op_char(ch)) return {
        type  : "op",
        value : read_while(is_op_char)
    };
    input.croak("Can't handle character: " + ch);
}複製代碼

這是一個「調度」函數,next()方法用來接收下一個token。注意它使用不少工具來聚焦特殊的token類型,好比read_string(), read_number() 等等。雖然不少函數都沒有調用過,可是並非想把「調度」複雜化。

另一個須要注意的是咱們沒有一次將輸入的流處理完。每次編譯器只調用下一個token,咱們讀取一個token。若是解析出錯咱們甚至到不了流的末尾。

只要它們容許做爲一個標識符(is_id)的一部分,read_ident()將一直讀取字符。標識符必須以字符,λ或者_開頭,後面能夠跟隨數字,或者?!-=。所以 foo-bar不會被識別成三個tokens,只會識別成一個"var"token。

同時,read_ident()函數將比對已知關鍵詞列表檢查標識符,以及若是在列表中將返回"kw"token,而不是"var"token。

我認爲代碼能夠很好的說清楚本身是什麼,因此下面是咱們語言已完成的詞法分析器代碼。末尾有幾個小提示:

function TokenStream(input) {
    var current = null;
    var keywords = " if then else lambda λ true false ";
    return {
        next  : next,
        peek  : peek,
        eof   : eof,
        croak : input.croak
    };
    function is_keyword(x) {
        return keywords.indexOf(" " + x + " ") >= 0;
    }
    function is_digit(ch) {
        return /[0-9]/i.test(ch);
    }
    function is_id_start(ch) {
        return /[a-zλ_]/i.test(ch);
    }
    function is_id(ch) {
        return is_id_start(ch) || "?!-<>=0123456789".indexOf(ch) >= 0;
    }
    function is_op_char(ch) {
        return "+-*/%=&|<>!".indexOf(ch) >= 0;
    }
    function is_punc(ch) {
        return ",;(){}[]".indexOf(ch) >= 0;
    }
    function is_whitespace(ch) {
        return " \t\n".indexOf(ch) >= 0;
    }
    function read_while(predicate) {
        var str = "";
        while (!input.eof() && predicate(input.peek()))
            str += input.next();
        return str;
    }
    function read_number() {
        var has_dot = false;
        var number = read_while(function(ch){
            if (ch == ".") {
                if (has_dot) return false;
                has_dot = true;
                return true;
            }
            return is_digit(ch);
        });
        return { type: "num", value: parseFloat(number) };
    }
    function read_ident() {
        var id = read_while(is_id);
        return {
            type  : is_keyword(id) ? "kw" : "var",
            value : id
        };
    }
    function read_escaped(end) {
        var escaped = false, str = "";
        input.next();
        while (!input.eof()) {
            var ch = input.next();
            if (escaped) {
                str += ch;
                escaped = false;
            } else if (ch == "\\") {
                escaped = true;
            } else if (ch == end) {
                break;
            } else {
                str += ch;
            }
        }
        return str;
    }
    function read_string() {
        return { type: "str", value: read_escaped('"') };
    }
    function skip_comment() {
        read_while(function(ch){ return ch != "\n" });
        input.next();
    }
    function read_next() {
        read_while(is_whitespace);
        if (input.eof()) return null;
        var ch = input.peek();
        if (ch == "#") {
            skip_comment();
            return read_next();
        }
        if (ch == '"') return read_string();
        if (is_digit(ch)) return read_number();
        if (is_id_start(ch)) return read_ident();
        if (is_punc(ch)) return {
            type  : "punc",
            value : input.next()
        };
        if (is_op_char(ch)) return {
            type  : "op",
            value : read_while(is_op_char)
        };
        input.croak("Can't handle character: " + ch);
    }
    function peek() {
        return current || (current = read_next());
    }
    function next() {
        var tok = current;
        current = null;
        return tok || read_next();
    }
    function eof() {
        return peek() == null;
    }
}複製代碼
  • next()函數不會總去調用read_next(),咱們須要一個current變量來一直跟蹤當前的token。

  • 咱們只使用經常使用符號來支持小數(沒有1E5這種寫法,沒有十六進制,沒有八進制)。可是若是咱們須要更多,咱們只能去read_number()中修改,改起來很容易。

  • 不像JavaScript,在字符串中,引號字符和反斜槓字符是惟一須要加引號的字符。

咱們如今已經有足夠的工具來輕鬆實現解析器了,在此以前我建議你最好先去熟悉下咱們的抽象語法樹。

原文:lisperator.net/pltut/

相關文章
相關標籤/搜索