Skip to content

エラーの処理

ドラゴンブック からの引用

多くのプログラミング言語仕様書では、コンパイラがエラーにどう対応すべきかを記述していない。エラー処理はコンパイラ設計者に任されている。 最初から正しいエラー処理を計画することは、コンパイラの構造を単純化し、エラーの扱いを改善することができる。

完全に回復可能なパーサーは、何を投げられてもアスト(抽象構文木)を構築できる。
リナーやフォーマッターなどのツールにおいては、プログラムの一部に対して処理を行えるようにするために、完全に回復可能なパーサーが望ましい。

パニックを起こすパーサーは、文法上の不一致があると中断する。一方、部分的に回復可能なパーサーは決定論的な文法から回復できる。

たとえば、文法的に誤った while true {} のような while ステートメントの場合、丸括弧が欠けていることは明らかであり、これに許される唯一の句読点は丸括弧であるため、依然として有効なアストを返すことができ、括弧の欠如を示すことができる。

現在存在する多くの JavaScript パーサーは部分的に回復可能であるため、同様に部分的に回復可能なパーサーを構築する。

INFO

Biome パーサーは完全に回復可能なパーサーです。

Rust には、エラーを返して伝播するための Result 型があります。
? 構文と組み合わせることで、パース関数はシンプルかつ綺麗なまま保たれます。

通常、Result 型をラップして、後にエラーを置き換えるようにすることがあります:

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

私たちのパース関数は Result を返すようになります。例えば:

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() => {
          // ... コード省略
        }
        _ => Err(()), 
    }
}

現在のトークンが文法に合致しない場合にエラーを返すために、expect 関数を追加できます:

rust
/// 指定された `Kind` が期待される場合、またはエラーを返す
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

完全性のために、字句解析を行う際、予期しない char が検出されたときに read_next_token 関数も Result を返す必要があります。

Error トレイト

特定のエラーを返すために、ResultErr 部分を埋める必要があります:

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

ECMAScript 規格の文法セクションで定義されているすべての「早期エラー」が構文エラーであるため、これを SyntaxError と呼んでいます。

これを適切な Error にするには、Error トレイト を実装する必要があります。コードをより簡潔にするために、thiserror パッケージのマクロを使用できます:

rust
#[derive(Debug, Error)]
pub enum SyntaxError {
    #[error("予期しないトークン")]
    UnexpectedToken,

    #[error("ステートメントの後にはセミコロンまたは暗黙のセミコロンが必要ですが、見つかりませんでした")]
    AutoSemicolonInsertion,

    #[error("複数行コメントが終了していません")]
    UnterminatedMultiLineComment,
}

その後、トークンが一致しない場合にエラーをスローするための expect ヘルパー関数を追加できます:

rust
/// 指定された `Kind` が期待される場合、またはエラーを返す
pub fn expect(&mut self, kind: Kind) -> Result<()> {
    if self.at(kind) {
        return Err(SyntaxError::UnexpectedToken);
    }
    self.advance(kind);
    Ok(())
}

これにより、parse_debugger_statementexpect 関数を用いて適切なエラー管理が行えるようになります:

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 の後に ? が付いていることに注目してください。これは、「質問マーク演算子」と呼ばれる構文的糖衣であり、expect 関数が Err を返した場合、関数が早期に返るようにするものです。

細かいエラー報告

miette は、最も美しいエラー報告ライブラリの一つで、色付きの美しい出力を提供します。

miette

Cargo.tomlmiette を追加します。

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

Errormiette でラップし、パーサーで定義された 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))
    })
}