Skip to content

解析器

我们将构建的解析器被称为 递归下降解析器,它是一种手动根据语法规则逐层向下推进并构建抽象语法树(AST)的过程。

该解析器起始时较为简单,它包含源代码、词法分析器以及从词法分析器中获取的当前标记。

rust
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 存储了从词法分析器返回的当前标记。我们通过添加一些辅助函数来让解析器代码更清晰,这些函数用于导航和检查该标记。

rust
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 是最简单的语句之一,因此我们尝试解析它并返回一个有效的程序。

rust
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 语句:

rust
// 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)避免重复。

rust
// 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); 这一行。

不同列表的具体实现细节可以通过以下方式提供,例如:

rust
// 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 可以直接重写节点类型:

javascript
https://github.com/acornjs/acorn/blob/11735729c4ebe590e406f952059813f250a4cbd1/acorn/src/lval.js#L11-L26

但在 Rust 中,我们需要进行结构体到结构体的转换。一种优雅且清晰的方法是使用特质(trait)。

rust
pub trait CoverGrammar<'a, T>: Sized {
    fn cover(value: T, p: &mut Parser<'a>) -> Result<Self>;
}

该特质接受 T 作为输入类型,Self 作为输出类型,因此我们可以定义如下内容:

rust
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

对于以下代码:

javascript
let foo = <string> bar;

如果是 tsx 文件,这会是一个语法错误(未终止的 JSX),
但在 ts 文件中,它是正确的 VariableDeclaration 并带有 TSTypeAssertion

前瞻(Lookahead)

在某些情况下,解析器需要前瞻多个标记以确定正确的语法规则。

TSIndexSignature

例如,解析 TSIndexSignature 时,考虑以下两种情况:

typescript
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 语法,情况过于复杂,例如:

typescript
(<x>a, b as c, d!);
(a?: b = {} as c!) => {};

建议研究 TypeScript 源码中的这一特定情况。相关代码如下:

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 解析器结合使用前瞻(快速路径)和回溯来解析箭头函数。