Работа с ошибками
Из Dragon Book:
В большинстве спецификаций языков программирования не описано, как компилятор должен реагировать на ошибки; обработку ошибок оставляют на усмотрение разработчика компилятора. Если с самого начала спланировать обработку ошибок, можно и упростить структуру компилятора, и улучшить качество её ответов.
Полностью восстанавливающийся парсер строит AST при любом входе. Инструментам вроде линтера или форматтеру хотелось бы именно его, чтобы можно было работать с частью программы.
Панический парсер прерывает работу при любом расхождении с грамматикой, частично восстанавливающийся — восстанавливается только в детерминированных случаях.
Например, для грамматически неверного while true {} видно: не хватает круглых скобок, и единственно разумным разделителем здесь могут быть они — можно вернуть осмысленный AST и отметить пропуск скобок.
Большинство парсеров JavaScript именно частично восстанавливающиеся; сделаем так же.
INFO
Парсер Biome полностью восстанавливающийся.
В Rust для возврата и распространения ошибок используется тип Result. Вместе с синтаксисом ? функции парсинга остаются простыми.
Часто оборачивают Result, чтобы позже можно было подменить тип ошибки:
pub type Result<T> = std::result::Result<T, ()>;Наши функции парсинга возвращают Result, например:
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, которая возвращает ошибку, если текущий токен не подходит под грамматику:
/// Expect a `Kind` or return error
pub fn expect(&mut self, kind: Kind) -> Result<()> {
if !self.at(kind) {
return Err(())
}
self.advance();
Ok(())
}Использование:
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
Для полноты функция лексера read_next_token тоже должна возвращать Result, если при разборе встретился неожиданный char.
Трейт Error
Чтобы возвращать конкретные ошибки, нужно заполнить часть Err у Result:
pub type Result<T> = std::result::Result<T, SyntaxError>;
^^^^^^^^^^^
#[derive(Debug)]
pub enum SyntaxError {
UnexpectedToken(String),
AutoSemicolonInsertion(String),
UnterminatedMultiLineComment(String),
}Назовём это SyntaxError, потому что все «ранние ошибки» из грамматической части спецификации ECMAScript — это синтаксические ошибки.
Чтобы тип соответствовал Error, нужно реализовать Error. Для лаконичности удобны макросы из крейта thiserror:
#[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, которая возвращает ошибку при несовпадении токена:
/// 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 может использовать expect:
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.toml:
[dependencies]
miette = { version = "5", features = ["fancy"] }Можно обернуть свою ошибку в miette, не меняя тип Result в парсере:
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))
})
}