Skip to content

오류 처리

드래곤 북에서 인용한 내용

대부분의 프로그래밍 언어 사양서는 컴파일러가 오류에 어떻게 반응해야 하는지 설명하지 않는다. 오류 처리는 컴파일러 설계자에게 맡겨진다. 처음부터 오류 처리를 계획하면, 컴파일러의 구조를 단순화하고 오류 처리 능력을 향상시킬 수 있다.

완전 복구 가능한 파서는 어떤 입력이 와도 유효한 AST를 생성할 수 있다. 라이너 또는 포맷터와 같은 도구의 경우, 프로그램의 일부에 대해 작동할 수 있도록 완전 복구 가능한 파서를 원한다.

패닉 파서는 문법 불일치가 발생하면 종료하고, 부분적으로 복구 가능한 파서는 결정론적 문법에서 복구할 수 있다.

예를 들어, 문법적으로 잘못된 while true {} 형태의 while 문은 괄호가 누락되어 있음을 알 수 있고, 이 문장에서 사용할 수 있는 구두점은 괄호뿐이므로 여전히 유효한 AST를 반환하고 괄호 누락을 나타낼 수 있다.

현행 자바스크립트 파서 대부분은 부분적으로 복구 가능한 것으로, 우리도 마찬가지로 부분적으로 복구 가능한 파서를 구축할 것이다.

INFO

Biome 파서는 완전 복구 가능한 파서이다.

루스트는 오류를 반환하고 전파하기 위해 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

완전성을 위해, 리크스 함수 read_next_token도 리크스 중 예기치 않은 char를 발견했을 때 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 crate의 매크로를 사용할 수 있다:

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