解析器
我们将构建的解析器被称为 递归下降解析器,它是一种手动根据语法规则逐层向下推进并构建抽象语法树(AST)的过程。
该解析器起始时较为简单,它包含源代码、词法分析器以及从词法分析器中获取的当前标记。
pub struct Parser<'a> {
/// 源代码
source: &'a str,
lexer: Lexer<'a>,
/// 从词法分析器获取的当前标记
cur_token: Token,
/// 上一个标记的结束位置
prev_token_end: usize,
}
impl<'a> Parser<'a> {
pub fn new(source: &'a str) -> Self {
Self {
source,
lexer: Lexer::new(source),
cur_token: Token::default(),
}
}
pub fn parse(&mut self) -> Program<'a> {
Ok(Program {
node: Node {
start: 0,
end: self.source.len(),
}
body: vec![]
})
}
}辅助函数
当前标记 cur_token: Token 存储了从词法分析器返回的当前标记。我们通过添加一些辅助函数来让解析器代码更清晰,这些函数用于导航和检查该标记。
impl<'a> Parser<'a> {
fn start_node(&self) -> Node {
let token = self.cur_token();
Node::new(token.start, 0)
}
fn finish_node(&self, node: Node) -> Node {
Node::new(node.start, self.prev_token_end)
}
fn cur_token(&self) -> &Token {
&self.cur_token
}
fn cur_kind(&self) -> Kind {
self.cur_token.kind
}
/// 检查当前索引处是否为指定标记类型
fn at(&self, kind: Kind) -> bool {
self.cur_kind() == kind
}
/// 如果当前位置是 `Kind`,则向前推进
fn bump(&mut self, kind: Kind) {
if self.at(kind) {
self.advance();
}
}
/// 推进任意标记
fn bump_any(&mut self) {
self.advance();
}
/// 如果当前位置是 `Kind`,则推进并返回 true;否则返回 false
fn eat(&mut self, kind: Kind) -> bool {
if self.at(kind) {
self.advance();
return true;
}
false
}
/// 移动到下一个标记
fn advance(&mut self) {
let token = self.lexer.next_token();
self.prev_token_end = self.cur_token.end;
self.cur_token = token;
}
}解析函数
DebuggerStatement 是最简单的语句之一,因此我们尝试解析它并返回一个有效的程序。
impl<'a> Parser<'a> {
pub fn parse(&mut self) -> Program {
let stmt = self.parse_debugger_statement();
let body = vec![stmt];
Program {
node: Node {
start: 0,
end: self.source.len(),
}
body,
}
}
fn parse_debugger_statement(&mut self) -> Statement {
let node = self.start_node();
// NOTE: 词法分析器返回的标记是 `Kind::Debugger`,我们稍后会修复这个问题。
self.bump_any();
Statement::DebuggerStatement {
node: self.finish_node(node),
}
}
}所有其他解析函数都基于这些基础辅助函数构建。例如,在 swc 中解析 while 语句:
// https://github.com/swc-project/swc/blob/554b459e26b24202f66c3c58a110b3f26bbd13cd/crates/swc_ecma_parser/src/parser/stmt.rs#L952-L970
fn parse_while_stmt(&mut self) -> PResult<Stmt> {
let start = cur_pos!(self);
assert_and_bump!(self, "while");
expect!(self, '(');
let test = self.include_in_expr(true).parse_expr()?;
expect!(self, ')');
let ctx = Context {
is_break_allowed: true,
is_continue_allowed: true,
..self.ctx()
};
let body = self.with_ctx(ctx).parse_stmt(false).map(Box::new)?;
let span = span!(self, start);
Ok(Stmt::While(WhileStmt { span, test, body }))
}解析表达式
表达式的语法规则是深度嵌套且递归的,这可能导致长表达式时栈溢出(例如在 此 TypeScript 测试 中)。
为了避免递归,我们可以使用一种称为“普拉特解析”(Pratt Parsing)的技术。更深入的教程可参见 这里,由 Rust-Analyzer 的作者撰写。Rust 版本可在 Rome 中找到。
列表
在许多地方需要解析以标点分隔的列表,例如 [a, b, c] 或 {a, b, c}。
解析列表的代码结构非常相似,可以使用 模板方法模式 通过特性(traits)避免重复。
// https://github.com/rome/tools/blob/85ddb4b2c622cac9638d5230dcefb6cf571677f8/crates/rome_js_parser/src/parser/parse_lists.rs#L131-L157
fn parse_list(&mut self, p: &mut Parser) -> CompletedMarker {
let elements = self.start_list(p);
let mut progress = ParserProgress::default();
let mut first = true;
while !p.at(JsSyntaxKind::EOF) && !self.is_at_list_end(p) {
if first {
first = false;
} else {
self.expect_separator(p);
if self.allow_trailing_separating_element() && self.is_at_list_end(p) {
break;
}
}
progress.assert_progressing(p);
let parsed_element = self.parse_element(p);
if parsed_element.is_absent() && p.at(self.separating_element_kind()) {
// 缺失的元素
continue;
} else if self.recover(p, parsed_element).is_err() {
break;
}
}
self.finish_list(p, elements)
}这种模式还可以防止无限循环,特别是 progress.assert_progressing(p); 这一行。
不同列表的具体实现细节可以通过以下方式提供,例如:
// https://github.com/rome/tools/blob/85ddb4b2c622cac9638d5230dcefb6cf571677f8/crates/rome_js_parser/src/syntax/expr.rs#L1543-L1580
struct ArrayElementsList;
impl ParseSeparatedList for ArrayElementsList {
fn parse_element(&mut self, p: &mut Parser) -> ParsedSyntax {
match p.cur() {
T![...] => parse_spread_element(p, ExpressionContext::default()),
T![,] => Present(p.start().complete(p, JS_ARRAY_HOLE)),
_ => parse_assignment_expression_or_higher(p, ExpressionContext::default()),
}
}
fn is_at_list_end(&self, p: &mut Parser) -> bool {
p.at(T![']'])
}
fn recover(&mut self, p: &mut Parser, parsed_element: ParsedSyntax) -> RecoveryResult {
parsed_element.or_recover(
p,
&ParseRecovery::new(
JS_UNKNOWN_EXPRESSION,
EXPR_RECOVERY_SET.union(token_set!(T![']'])),
),
js_parse_error::expected_array_element,
)
}
fn list_kind() -> JsSyntaxKind {
JS_ARRAY_ELEMENT_LIST
}
fn separating_element_kind(&mut self) -> JsSyntaxKind {
T![,]
}
fn allow_trailing_separating_element(&self) -> bool {
true
}
}封装语法(Cover Grammar)
详见 封装语法,有时我们需要将 Expression 转换为 BindingIdentifier。动态语言如 JavaScript 可以直接重写节点类型:
https://github.com/acornjs/acorn/blob/11735729c4ebe590e406f952059813f250a4cbd1/acorn/src/lval.js#L11-L26但在 Rust 中,我们需要进行结构体到结构体的转换。一种优雅且清晰的方法是使用特质(trait)。
pub trait CoverGrammar<'a, T>: Sized {
fn cover(value: T, p: &mut Parser<'a>) -> Result<Self>;
}该特质接受 T 作为输入类型,Self 作为输出类型,因此我们可以定义如下内容:
impl<'a> CoverGrammar<'a, Expression<'a>> for BindingPattern<'a> {
fn cover(expr: Expression<'a>, p: &mut Parser<'a>) -> Result<Self> {
match expr {
Expression::Identifier(ident) => Self::cover(ident.unbox(), p),
Expression::ObjectExpression(expr) => Self::cover(expr.unbox(), p),
Expression::ArrayExpression(expr) => Self::cover(expr.unbox(), p),
_ => Err(()),
}
}
}
impl<'a> CoverGrammar<'a, ObjectExpression<'a>> for BindingPattern<'a> {
fn cover(obj_expr: ObjectExpression<'a>, p: &mut Parser<'a>) -> Result<Self> {
...
BindingIdentifier::ObjectPattern(ObjectPattern { .. })
}
}
impl<'a> CoverGrammar<'a, ArrayExpression<'a>> for BindingPattern<'a> {
fn cover(expr: ArrayExpression<'a>, p: &mut Parser<'a>) -> Result<Self> {
...
BindingIdentifier::ArrayPattern(ArrayPattern { .. })
}
}之后,只要需要将 Expression 转换为 BindingPattern,只需调用 BindingPattern::cover(expression) 即可。
TypeScript
所以你已经完成了 JavaScript 的解析,现在想挑战一下 TypeScript?
坏消息是目前没有正式规范,但好消息是 TypeScript 的解析器位于 单个文件 内 🙃。
JSX 与 TSX
对于以下代码:
let foo = <string> bar;如果是 tsx 文件,这会是一个语法错误(未终止的 JSX),
但在 ts 文件中,它是正确的 VariableDeclaration 并带有 TSTypeAssertion。
前瞻(Lookahead)
在某些情况下,解析器需要前瞻多个标记以确定正确的语法规则。
TSIndexSignature
例如,解析 TSIndexSignature 时,考虑以下两种情况:
type A = { readonly [a: number]: string }
^__________________________^ TSIndexSignature
type B = { [a]: string }
^_________^ TSPropertySignature对于 type A 中的开头 {,我们需要前瞻 5 个标记(readonly, [, a, :, number),以确保这是一个 TSIndexSignature 而非 TSPropertySignature。
为了实现这种高效前瞻,词法分析器需要一个缓冲区来存储多个标记。
箭头表达式
在 封装语法 中讨论过,当在 SequenceExpression 后遇到 => 标记时,我们需要将 Expression 转换为 BindingPattern。
但这种方法对 TypeScript 不适用,因为括号内的每一项都可能包含 TypeScript 语法,情况过于复杂,例如:
(<x>a, b as c, d!);
(a?: b = {} as c!) => {};建议研究 TypeScript 源码中的这一特定情况。相关代码如下:
function tryParseParenthesizedArrowFunctionExpression(
allowReturnTypeInArrowFunction: boolean,
): Expression | undefined {
const triState = isParenthesizedArrowFunctionExpression();
if (triState === Tristate.False) {
// 明确不是带括号的箭头函数表达式。
return undefined;
}
// 如果我们确定是箭头函数,则可以直接解析,无需依赖后续的 `=>` 或 `{` 标记。否则,我们可能只是暂时遇到一个箭头函数。尝试解析它,但不允许歧义,如果存在表达式可能,则返回 `undefined`。
return triState === Tristate.True
? parseParenthesizedArrowFunctionExpression(
/*allowAmbiguity*/ true,
/*allowReturnTypeInArrowFunction*/ true,
)
: tryParse(() =>
parsePossibleParenthesizedArrowFunctionExpression(allowReturnTypeInArrowFunction),
);
}
// True -> 我们在此处肯定期望一个带括号的箭头函数。
// False -> 此处绝对不可能是带括号的箭头函数。
// Unknown -> 此处可能是一个带括号的箭头函数。推测性地前瞻以确认,并在不匹配时回滚。
function isParenthesizedArrowFunctionExpression(): Tristate {
if (
token() === SyntaxKind.OpenParenToken ||
token() === SyntaxKind.LessThanToken ||
token() === SyntaxKind.AsyncKeyword
) {
return lookAhead(isParenthesizedArrowFunctionExpressionWorker);
}
if (token() === SyntaxKind.EqualsGreaterThanToken) {
// 错误恢复优化:
// 如果看到单独的 `=>`,尝试将其解析为箭头函数表达式,这很可能是用户想要写的。
return Tristate.True;
}
// 绝对不是带括号的箭头函数。
return Tristate.False;
}总之,TypeScript 解析器结合使用前瞻(快速路径)和回溯来解析箭头函数。
