Skip to content

处理错误

《龙书》 有云:

大多数语言规范并不会规定编译器在遇到错误时具体该如何反应;如何报错留给了实现者。 从一开始就规划好错误处理策略,既能简化编译器结构,也能提升整体体验。

理论上完全可恢复的 Parser 无论源码多糟都能给出某种 AST。 面向 Linter/Formatter 的场景,我们往往希望尽量「能跑多少跑多少」,因此也倾向部分可恢复的策略。

若 Parser 在任意句法不符时直接 panic,就属于不可恢复部分可恢复则是在句法可判定的场景下继续建树。

例如 while true {} 缺了圆括号,但仍能唯一确定应插入 ( / ), 于是可以返回带诊断的 AST,同时指出缺失位置。

多数 JavaScript Parser 都是部分可恢复,我们也采用同样策略。

INFO

Biome 的 Parser 属于完全可恢复实现。

Rust 的 Result? 能让解析函数保持可读。

常见做法是把 Result 包一层,后续再替换具体错误类型:

rust
pub type Result<T> = std::result::Result<T, ()>;

示例:

rust
pub fn parse_binding_pattern(&mut self, ctx: Context) -> Result<BindingPattern<'a>> {
    match self.cur_kind() {
        Kind::LCurly => self.parse_object_binding_pattern(ctx),
        Kind::LBrack => self.parse_array_binding_pattern(ctx),
        kind if kind.is_binding_identifier() => {
          // ... code omitted
        }
        _ => Err(()), 
    }
}

可以写 expect(kind):若当前 Token 不符合语法就返回错误:

rust
/// Expect a `Kind` or return error
pub fn expect(&mut self, kind: Kind) -> Result<()> {
    if !self.at(kind) {
        return Err(())
    }
    self.advance();
    Ok(())
}

使用方式:

rust
pub fn parse_paren_expression(&mut self, ctx: Context) -> Result<Expression> {
    self.expect(Kind::LParen)?;
    let expression = self.parse_expression(ctx)?;
    self.expect(Kind::RParen)?;
    Ok(expression)
}

INFO

为保持对称性,Lexer 里的 read_next_token 在遇到非法字符时也应返回 Result

Error trait

若需要具体错误类型,需要把 Err 部分替换成真实结构:

rust
pub type Result<T> = std::result::Result<T, SyntaxError>;
                                            ^^^^^^^^^^^
#[derive(Debug)]
pub enum SyntaxError {
    UnexpectedToken(String),
    AutoSemicolonInsertion(String),
    UnterminatedMultiLineComment(String),
}

称为 SyntaxError,是因为 ECMAScript 规范语法章节里所有 “early error” 本质上都属于语法层面。

实现 Error trait 后即可与生态互操作,推荐配合 thiserror 宏简化书写:

rust
#[derive(Debug, Error)]
pub enum SyntaxError {
    #[error("Unexpected Token")]
    UnexpectedToken,

    #[error("Expected a semicolon or an implicit semicolon after a statement, but found none")]
    AutoSemicolonInsertion,

    #[error("Unterminated multi-line comment")]
    UnterminatedMultiLineComment,
}

再提供 expect,在 token 不符时返回语义化错误:

rust
/// Expect a `Kind` or return error
pub fn expect(&mut self, kind: Kind) -> Result<()> {
    if !self.at(kind) {
        return Err(SyntaxError::UnexpectedToken);
    }
    self.advance(kind);
    Ok(())
}

parse_debugger_statement 里就能写:

rust
fn parse_debugger_statement(&mut self) -> Result<Statement> {
    let node = self.start_node();
    self.expect(Kind::Debugger)?;
    Ok(Statement::DebuggerStatement {
        node: self.finish_node(node),
    })
}

expect 后的 ?问号运算符:遇到 Err 会提前返回。

花哨一点的诊断输出

miette 能输出高质量的彩色诊断:

miette

Cargo.toml 中添加:

toml
[dependencies]
miette = { version = "5", features = ["fancy"] }

可把解析错误转换成 miette::Error,而无需重写 Result 别名:

rust
pub fn main() -> Result<()> {
    let source_code = "".to_string();
    let file_path = "test.js".to_string();
    let mut parser = Parser::new(&source_code);
    parser.parse().map_err(|error| {
        miette::Error::new(error).with_source_code(miette::NamedSource::new(file_path, source_code))
    })
}