오류 다루기
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를 반환하는 편이 좋습니다.
Error 트레이트
구체적인 오류를 내리려면 Result의 Err 타입을 채웁니다:
pub type Result<T> = std::result::Result<T, SyntaxError>;
^^^^^^^^^^^
#[derive(Debug)]
pub enum SyntaxError {
UnexpectedToken(String),
AutoSemicolonInsertion(String),
UnterminatedMultiLineComment(String),
}ECMAScript 명세 문법 절의 "early error"는 모두 구문 오류이므로 SyntaxError라 부릅니다.
제대로 된 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는 색 들어간 보기 좋은 오류 출력을 제공합니다.

Cargo.toml에 추가:
[dependencies]
miette = { version = "5", features = ["fancy"] }파서 안의 Result 정의를 바꾸지 않고 miette로 오류를 감쌀 수 있습니다:
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))
})
}