Skip to content

오류 다루기

Dragon Book 인용:

대부분의 프로그래밍 언어 명세는 컴파일러가 오류에 어떻게 반응해야 하는지를 설명하지 않습니다. 오류 처리는 컴파일러 설계자에게 맡겨져 있습니다. 처음부터 오류 처리를 계획하면 컴파일러 구조를 단순히 하면서 오류 처리 품질도 높일 수 있습니다.

무엇을 던져도 AST를 만들 수 있는 완전 회복 가능 파서가 있으면, 린터나 포매터처럼 프로그램 일부라도 건드리고 싶을 때 이상적입니다.

패닉 파서는 문법 불일치 하나면 중단하고, 부분 회복 파서는 정해진 문법에서는 복구합니다.

예를 들어 문법적으로 틀린 while true {}는 괄호가 빠진 것으로 알 수 있고 가능한 구두점도 괄호뿐이니 유효한 AST와 “괄호 없음” 표시를 같이 줄 수 있습니다.

대부분의 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() => {
          // ... code omitted
        }
        _ => Err(()), 
    }
}

현재 토큰이 문법과 다르면 오류를 반환하는 expect 헬퍼를 둘 수 있습니다:

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

완전성을 위해 렉서의 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 명세 문법 절의 "early error"는 모두 구문 오류이므로 SyntaxError라 부릅니다.

제대로 된 Error가 되려면 Error 트레이트를 구현해야 합니다. 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 헬퍼를 바꿀 수 있습니다:

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_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 뒤의 ?"물음표 연산자"로, expectErr면 함수가 즉시 반환됩니다.

보기 좋은 오류 출력

miette는 색 들어간 보기 좋은 오류 출력을 제공합니다.

miette

Cargo.toml에 추가:

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

파서 안의 Result 정의를 바꾸지 않고 miette로 오류를 감쌀 수 있습니다:

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))
    })
}