词法分析器运行在字符输入流
之上,经过相同的接口返回一个流对象,可是经过peek()
/next()
返回的值是tokens
。一个token
是一个对象,包含两个属性:type
和value
。下面是一些支持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: "!=" } // 运算符复制代码
空格和评论会被跳过,不会返回tokens
。bash
为了写词法分析器,咱们须要深刻研究咱们语言的语法。要注意当前字符(经过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,在字符串中,引号字符和反斜杠字符是惟一须要加引号的字符。
咱们如今已经有足够的工具来轻松实现解析器了,在此以前我建议你最好先去熟悉下咱们的抽象语法树。