如何使用 JavaScript 实现一门编程语言(4) —— Token stream

tokenizer(标记器, 也称为“词法分析器”), 对 字符输入流 进行操作,并返回具有相同接口的流对象,但由 peek()/next() 返回的值一个个token。token是具有两个属性的对象:type和value。以下是我们所支持的token的一些示例:

{  type:“ punc ”,value:“ (”  }            // 标点符号:parens,逗号,分号等等
{  type:“ num ”,value:5  }               // numbers
{  type:“ str ”,value:“ Hello World !“  }  // 字符串
{  type:” kw “,value:”lambda “  }         // keywords
{  type: ” var “, value: ” a “  }             // 标识符
{  type: ” op “, value: ” != “  }             // 运算符

空白符和注释将被跳过,没有令牌返回。

为了编写tokenizer,我们需要更仔细地认识我们的语言的语法。有个办法是,根据当前字符(由input.peek()返回的)来决定读取哪种类型的token:

  1. 首先,跳过空格。
  2. 如果然后返回。input.eof()null
  3. 如果它是一个井号(#),则跳过注释(在行结束后重试)。
  4. 如果它是一个引号,那么阅读一个字符串。
  5. 如果它是一个数字,那么我们继续阅读一个数字。
  6. 如果它是“字母”,则读取标识符或关键字token。
  7. 如果它是标点符号之一,则返回标点符号token。
  8. 如果它是运算符,则返回运算符token。
  9. 如果以上都不是,那就抛出错误了。input.croak()

“read_next”函数作为tokenizer的核心部分 ,它实现了上面的内容:

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不会被看作是三个token,而是作为一个单一的标识符(一个token)。这条规则的原因是我希望能够定义名为is-pair或者string>=的函数(对不起,这就是我的Lisper)。

此外,read_ident()函数将检查已知关键字列表中的标识符,如果它存在,它将返回一个”kw”令牌.

这里是我们语言的完整tokenizer:

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(),因为它可能在之前被peek过(在这种情况下,read_next()已经被调用并且stream被提前)。因此我们需要一个current变量跟踪当前token。
  • 我们只支持十进制数与通常的符号(没有1E5的东西,没有十六进制,没有八进制)。但是如果我们需要更多,这些更改只能在read_number()中进行,并且很容易实现。
  • 与JavaScript不同,唯一不能在字符串中引用的字符是引号字符本身和反斜杠。你需要反斜杠转义他们。而且,字符串可能包含换行符,制表符等。我们太长不会解析像\n,\t等的转义。

现在,我们有足够强大的工具来方便地编写解析器(parser)了,但我建议您首先看下 AST 的描述。

相关文章

原文链接:http://lisperator.net/pltut/