原文地址:https://lisperator.net/pltut/
分词器的作用
通过解析字符流,把里面的 token 提取出来,提取后的格式为:
{ type: "punc", value: "(" } // punctuation: parens, comma, semicolon etc.
{ type: "num", value: 5 } // numbers
{ type: "str", value: "Hello World!" } // strings
{ type: "kw", value: "lambda" } // keywords
{ type: "var", value: "a" } // identifiers
{ type: "op", value: "!=" } // operators
每一个 token 都是一个 AST 的节点
分词器的实现原理
- 跳过所有的空格
- 若输入流已到结尾(input.eof() === true),返回 null。
- 若遇到
#
,则跳过注释 - 若遇到
"
,则读取字符串。 - 若遇到
数字
,则连续读取得到一个数字。 - 若遇到
字母
,则读取标识符(identifier)或者关键字(keyword)。 - 若遇到
标点符号
,则返回标点符号 token。 - 若遇到
操作符
,则返回操作符 token。 - 若上面的情况都不符合,则抛出一个异常 input.croak()。
下面的read_next函数作为分词器的核心,实现了上面各描述情况:
function read_next() {
read_while(is_whitespace); // 1
if (input.eof()) return null; // 2
var ch = input.peek();
if (ch == '#') {
skip_comment();
return read_next();
} // 3
if (ch == '"') return read_string(); // 4
if (is_digit(ch)) return read_number(); // 5
if (is_id_start(ch)) return read_ident(); // 6
if (is_punc(ch))
return {
type: 'punc',
value: input.next(),
}; // 7
if (is_op_char(ch))
return {
type: 'op',
value: read_while(is_op_char),
}; // 8
input.croak("Can't handle character: " + ch); // 9
}
注意:没有一次性把输入流全部读掉。每次当解析器需要下一个 token 时,才会读取一个 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) };
}
// 将标识符和已知的关键字列表进行对比,如果标识符是一个关键字则会返回一个 "kw" token,否则返回一个 "var" token。
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;
}
// 4
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;
}
}