解析器的作用
解析器在分词器之上,直接操作 token 流,不用处理单个字符,把代码解析成一个个对象
lambda 解析器
解析标记流的过程中,当遇到 lambda 关键字则会调用parse_lambda
函数
fib = lambda (n) if n < 2 then n else fib(n - 1) + fib(n - 2);
function parse_lambda() {
return {
type: 'lambda',
vars: delimited('(', ')', ',', parse_varname),
body: parse_expression(),
};
}
delimited 函数:获取形参列表
// parser 是一个 function,负责解析 start 和 stop 之间的 token
function delimited(start, stop, separator, parser) {
var a = [],
first = true;
// skip_punc(token):当前 token 是否是给定的符号,若是,将其从输入流中丢弃并继续,否则抛出异常
skip_punc(start);
while (!input.eof()) {
// is_punc(token):若当前 token 是给定的符号,返回 true(不消耗掉当前 token)
if (is_punc(stop)) break;
// first 标识当前 token 是否是第一个
// 因为参数的格式是这样的(arg1, arg2, arg3...)
// 除去第一个参数之外,每次读一个新参数之前都要先把","给读走
if (first) first = false;
else skip_punc(separator);
// 没有和之前的重复
// 加上这个的原因是防止(arg1, arg2, arg3,)的情况,多了一个逗号
// while 开头的 is_punc(stop) 就拦截不下来了,而是继续掠过","读下一个参数,当然这个时候是读到是")",出问题了
if (is_punc(stop)) break;
// 解析出参数名
a.push(parser());
}
skip_punc(stop);
return a;
}
parse_expression 函数:解析表达式
尽可能地向右扩展一个表达式
function parse_expression() {
return maybe_call(function () {
return maybe_binary(parse_atom(), 0);
});
}
有两种可能性:
- 表达式为
f(a);
类型,调用函数 - 表达式为
c = a + b;
类型,就是普通的表达式
maybe_call 函数:如果是后面是调用函数,就拿一个 call
类型的对象把它包裹起来;如果不是就直接返回表达式本身
function maybe_call(expr) {
// expr 是 maybe_binary() 的返回值
expr = expr();
// 如果在那个疑似二元表达式之后,有一个 "(" 那就是说明调用函数型表达式,交给 parse_call(expr) 处理
// 如果不是,说明是普通表达式,直接返回就好了
// 例:f(a);
// expr 传进去的是 function() { f }
// f 后面跟着一个 (,所以是调用函数型表达式
return is_punc("(") ? parse_call(expr) : expr;
}
// 处理调用函数型表达式:用一个 call 类型的对象包起来
function parse_call(func) {
return {
type: "call",
func: func,
args: delimited("(", ")", ",", parse_expression)
};
}
maybe_binary:如果后面跟的是一个二元表达式,那就用一个结点(可能是 binary 类型,也可能是 assign 类型)包裹住它;如果不是就直接返回
谈到二元表达式就避不开操作符优先级的话题,这个用一个 PRECEDENCE 对象解决
// 定义操作符优先级,越大优先级越高
var PRECEDENCE = {
'=': 1,
'||': 2,
'&&': 3,
'<': 7,
'>': 7,
'<=': 7,
'>=': 7,
'==': 7,
'!=': 7,
'+': 10,
'-': 10,
'*': 20,
'/': 20,
'%': 20,
};
实现思路:
1 + 2 * 3;
初始化:
- 读一个原子表达式(1)
- 取当前运行时的优先级({INIT,0}):my_prec 初始化为 0
- 调用 maybe_binary(left, my_prec),左边的是表达式,右边的是运行时当前的优先级
maybe_binary 将会解析紧跟着原子表达式的内容:
紧跟的不是运算符,直接原样返回左参数(1)是运算符,但优先级低于 my_prec,返回左参数(1)- 是运算符且优先级更高({+,10} > {INIT,0}),
- 将左参数(1)包裹到一个新的二元表达式 “binary” 节点中
- 递归调用 maybe_binary,找出右参数(2…)的具体值:
- 读一个原子表达式(2)
- 取当前运行时的优先级({+,10})
- 调用 maybe_binary(left, my_prec),左边的是表达式,右边的是运行时当前的优先级
递归进去:maybe_binary(2, 10) 将会解析紧跟着原子表达式(2)的内容(*)
紧跟的不是运算符,直接原样返回左参数(2)是运算符,但优先级低于 my_prec,返回左参数(2)- 是运算符且优先级更高({*,20} > {+,10}),
- 将左参数包裹到一个新的二元表达式 “binary” 节点中
- 递归调用 maybe_binary,找出右参数(3…)的具体值:
- 读一个原子表达式(3)
- 取当前运行时的优先级({*,20})
- 调用 maybe_binary(left, my_prec),左边的是表达式,右边的是运行时当前的优先级
递归进去:maybe_binary(3, 20) 将会解析紧跟着原子表达式(2)的内容(*)
- 后面跟的是";",不是运算符,直接原样返回左参数(3)
// my_prec 初始化为 0
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 binary 不行吗?
// 原因:以 a * b + c * d 为例:
// 第一层调用:a,返回
// {
// left: a,
// right: maybe_binary(b,*)
// }
// 第二层调用:b,返回 b
// 然后就断了
// 返回 maybe_binary(binary, my_prec) 是为了让这个过程继续进行下去,以现有被分析好的
// {
// left: a,
// right: b
// }
// 为左参数,接着向右拓展
return maybe_binary(binary, my_prec);
}
}
return left;
}
parse_atom:解析原子表达式
parse_atom() 依据当前的 token 进行调度
function parse_atom() {
return maybe_call(function(){
// 如果解析到了一个"(",则其必定是一个括号表达式 — 因此首先会跳过开括号,然后调用 parse_expression(),然后跳过")"
if (is_punc("(")) {
input.next();
var exp = parse_expression();
skip_punc(")");
return exp;
}
// 如果解析到了某个关键字,则会调用对应关键字的解析函数
if (is_punc("{")) return parse_prog();
// is_kw 和 is_punc 是一样的,只是一个是针对字符串一个是针对字符,若当前 token 是给定的符号,返回 true(不消耗掉当前 token)
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();
}
// 如果解析到了一个常量或者标识符,则会原样返回 token
var tok = input.next();
if (tok.type == "var" || tok.type == "num" || tok.type == "str")
return tok;
// 如果所有情况都未满足,则会调用 unexpected() 抛出一个错误。
unexpected();
});
}
parse_prog:解析语句序列
当期望是一个原子表达式但解析到 {
的情况,调用 parse_prog 来解析整个序列的表达式,这里有一个优化,如果只有一个表达式就直接返回那个表达式,不再套一层了
var FALSE = { type: 'bool', value: false };
function parse_prog() {
var prog = delimited('{', '}', ';', parse_expression);
// 如果 prog 节点为空,则直接返回 FALSE
if (prog.length == 0) return FALSE;
// 如果程序只包含一个表达式,则返回表达式的解析结果
if (prog.length == 1) return prog[0];
// 否则返回一个包含表达式的 "prog" 节点
return { type: 'prog', prog: prog };
}
parse_if:解析 if 语句
if a <= b then { # 这里的 then 是可选的
print(a);
if a + 1 <= b {
print(", ");
print-range(a + 1, b);
} else println(""); # newline
};
function parse_if() {
// 类似 skip_punc
skip_kw('if');
// cond 是条件
var cond = parse_expression();
// 如果条件之后不是直接跟着 "{",那肯定是跟着 "then" 了
if (!is_punc('{')) skip_kw('then');
// then 是当条件为 true 是要处理的表达式
var then = parse_expression();
// 用一个 if 类型的对象把 cond 和 then 包起来
var ret = { type: 'if', cond: cond, then: then };
// 如果有 else 的话把 else 也包起来
if (is_kw('else')) {
input.next();
ret.else = parse_expression();
}
return ret;
}
以上这些函数似乎在互相调用:
- parse_atom() 函数基于当前的 token 来调用其它函数,如 parse_if()
- parse_if()调用 parse_expression()
- parse_expression()会再次调用 parse_atom()
之所以没有发生死循环,是因为每步处理中,每个函数都会至少消费掉一个 token。
上述类型的解析器叫做 “递归下降解析器”(recursive descent parser),也可能算是可以手写实现的最简单类型。
整体程序(prog 节点)解析器
通过不停地调用 parse_expression() 函数来读取输入流中的表达式(expression)
function parse_toplevel() {
var prog = [];
while (!input.eof()) {
prog.push(parse_expression());
// 表达式以分号分隔,跳过分号再进行下一个expression的解析
if (!input.eof()) skip_punc(';');
}
return { type: 'prog', prog: prog };
}