Skip to content

파서

우리가 구축할 파서는 재귀 내림 파서라고 불립니다. 이는 문법을 따라 내려가면서 추상구문트리(АСТ)를 구성하는 수동적인 과정입니다.

파서는 간단하게 시작합니다. 소스 코드, 렉서, 그리고 렉서로부터 소비된 현재 토큰을 보유합니다.

rust
pub struct Parser<'a> {
    /// 소스 코드
    source: &'a str,

    lexer: Lexer<'a>,

    /// 렉서로부터 소비된 현재 토큰
    cur_token: Token,

    /// 이전 토큰의 종료 범위
    prev_token_end: usize,
}

impl<'a> Parser<'a> {
    pub fn new(source: &'a str) -> Self {
        Self {
            source,
            lexer: Lexer::new(source),
            cur_token: Token::default(),
        }
    }

    pub fn parse(&mut self) -> Program<'a> {
        Ok(Program {
            node: Node {
                start: 0,
                end: self.source.len(),
            }
            body: vec![]
        })
    }
}

도우미 함수

현재 토큰 cur_token: Token은 렉서로부터 반환된 현재 토큰을 저장합니다. 이 토큰을 탐색하고 검사하기 위한 몇 가지 도우미 함수를 추가함으로써 파서 코드를 더욱 깔끔하게 만들 수 있습니다.

rust
impl<'a> Parser<'a> {
    fn start_node(&self) -> Node {
        let token = self.cur_token();
        Node::new(token.start, 0)
    }

    fn finish_node(&self, node: Node) -> Node {
        Node::new(node.start, self.prev_token_end)
    }

    fn cur_token(&self) -> &Token {
        &self.cur_token
    }

    fn cur_kind(&self) -> Kind {
        self.cur_token.kind
    }

    /// 현재 인덱스에 지정된 `Kind` 토큰이 있는지 확인합니다
    fn at(&self, kind: Kind) -> bool {
        self.cur_kind() == kind
    }

    /// 현재 토큰이 `Kind`라면 진행합니다
    fn bump(&mut self, kind: Kind) {
        if self.at(kind) {
            self.advance();
        }
    }

    /// 어떤 토큰이든 진행합니다
    fn bump_any(&mut self) {
        self.advance();
    }

    /// 현재 토큰이 `Kind`라면 진행하고 `true`를 반환, 그렇지 않으면 `false`를 반환합니다
    fn eat(&mut self, kind: Kind) -> bool {
        if self.at(kind) {
            self.advance();
            return true;
        }
        false
    }

    /// 다음 토큰으로 이동합니다
    fn advance(&mut self) {
        let token = self.lexer.next_token();
        self.prev_token_end = self.cur_token.end;
        self.cur_token = token;
    }
}

파싱 함수

DebuggerStatement은 파싱하기 가장 간단한 문장이므로, 이를 시도해보고 유효한 프로그램을 반환해보겠습니다.

rust
impl<'a> Parser<'a> {
    pub fn parse(&mut self) -> Program {
        let stmt = self.parse_debugger_statement();
        let body = vec![stmt];
        Program {
            node: Node {
                start: 0,
                end: self.source.len(),
            }
            body,
        }
    }

    fn parse_debugger_statement(&mut self) -> Statement {
        let node = self.start_node();
        // NOTE: 렉서에서 반환된 토큰은 `Kind::Debugger`입니다. 나중에 수정하겠습니다.
        self.bump_any();
        Statement::DebuggerStatement {
            node: self.finish_node(node),
        }
    }
}

다른 모든 파싱 함수는 이러한 기본적인 도우미 함수들을 기반으로 구축됩니다. 예를 들어, swc에서 while 문장을 파싱하는 방식은 다음과 같습니다:

rust
// https://github.com/swc-project/swc/blob/554b459e26b24202f66c3c58a110b3f26bbd13cd/crates/swc_ecma_parser/src/parser/stmt.rs#L952-L970

fn parse_while_stmt(&mut self) -> PResult<Stmt> {
    let start = cur_pos!(self);

    assert_and_bump!(self, "while");

    expect!(self, '(');
    let test = self.include_in_expr(true).parse_expr()?;
    expect!(self, ')');

    let ctx = Context {
        is_break_allowed: true,
        is_continue_allowed: true,
        ..self.ctx()
    };
    let body = self.with_ctx(ctx).parse_stmt(false).map(Box::new)?;

    let span = span!(self, start);
    Ok(Stmt::While(WhileStmt { span, test, body }))
}

표현식 파싱

표현식의 문법은 깊이 중첩되고 재귀적이며, 긴 표현식에서는 스택 오버플로우가 발생할 수 있습니다 (예: 이 타입스크립트 테스트).

재귀를 피하기 위해 '프래트 파싱(Pratt Parsing)'이라는 기술을 사용할 수 있습니다. 더 깊이 있는 안내는 여기에서 찾을 수 있으며, 이 글은 루스트-애널라이저의 작성자에 의해 작성되었습니다. 그리고 루스트 버전은 로미에서 확인할 수 있습니다.

목록

목록을 구분하는 구두점으로 나누어 파싱해야 하는 경우가 많습니다. 예를 들어 [a, b, c] 또는 {a, b, c}와 같은 형태 말입니다.

목록을 파싱하는 코드는 모두 비슷하며, 템플릿 메서드 패턴을 사용하여 특성(trait)을 통해 중복을 피할 수 있습니다.

rust
// https://github.com/rome/tools/blob/85ddb4b2c622cac9638d5230dcefb6cf571677f8/crates/rome_js_parser/src/parser/parse_lists.rs#L131-L157

fn parse_list(&mut self, p: &mut Parser) -> CompletedMarker {
    let elements = self.start_list(p);
    let mut progress = ParserProgress::default();
    let mut first = true;
    while !p.at(JsSyntaxKind::EOF) && !self.is_at_list_end(p) {
        if first {
            first = false;
        } else {
            self.expect_separator(p);

            if self.allow_trailing_separating_element() && self.is_at_list_end(p) {
                break;
            }
        }

        progress.assert_progressing(p);

        let parsed_element = self.parse_element(p);

        if parsed_element.is_absent() && p.at(self.separating_element_kind()) {
            // 누락된 요소
            continue;
        } else if self.recover(p, parsed_element).is_err() {
            break;
        }
    }
    self.finish_list(p, elements)
}

이 패턴은 무한 루프를 방지할 수도 있으며, 특히 progress.assert_progressing(p);에 의해서 가능합니다.

구현 세부 사항은 다양한 목록에 대해 제공될 수 있습니다. 예를 들어:

rust
// https://github.com/rome/tools/blob/85ddb4b2c622cac9638d5230dcefb6cf571677f8/crates/rome_js_parser/src/syntax/expr.rs#L1543-L1580

struct ArrayElementsList;

impl ParseSeparatedList for ArrayElementsList {
    fn parse_element(&mut self, p: &mut Parser) -> ParsedSyntax {
        match p.cur() {
            T![...] => parse_spread_element(p, ExpressionContext::default()),
            T![,] => Present(p.start().complete(p, JS_ARRAY_HOLE)),
            _ => parse_assignment_expression_or_higher(p, ExpressionContext::default()),
        }
    }

    fn is_at_list_end(&self, p: &mut Parser) -> bool {
        p.at(T![']'])
    }

    fn recover(&mut self, p: &mut Parser, parsed_element: ParsedSyntax) -> RecoveryResult {
        parsed_element.or_recover(
            p,
            &ParseRecovery::new(
                JS_UNKNOWN_EXPRESSION,
                EXPR_RECOVERY_SET.union(token_set!(T![']'])),
            ),
            js_parse_error::expected_array_element,
        )
    }

    fn list_kind() -> JsSyntaxKind {
        JS_ARRAY_ELEMENT_LIST
    }

    fn separating_element_kind(&mut self) -> JsSyntaxKind {
        T![,]
    }

    fn allow_trailing_separating_element(&self) -> bool {
        true
    }
}

커버 문법

커버 문법에서 자세히 설명되어 있듯이, 때때로 표현식(Експрессия)바인딩 식별자(Вязующий идентификатор)로 변환해야 할 때가 있습니다. 동적 언어인 자바스크립트는 단순히 노드 유형을 다시 작성할 수 있지만:

javascript
https://github.com/acornjs/acorn/blob/11735729c4ebe590e406f952059813f250a4cbd1/acorn/src/lval.js#L11-L26

루스트에서는 구조체에서 구조체로의 변환을 수행해야 합니다. 이를 깔끔하게 수행하는 좋은 방법은 특성(트레이트)을 사용하는 것입니다.

rust
pub trait CoverGrammar<'a, T>: Sized {
    fn cover(value: T, p: &mut Parser<'a>) -> Result<Self>;
}

이 특성은 입력 타입으로 T를 받으며, Self와 출력 타입을 받으므로 다음과 같이 정의할 수 있습니다:

rust
impl<'a> CoverGrammar<'a, Expression<'a>> for BindingPattern<'a> {
    fn cover(expr: Expression<'a>, p: &mut Parser<'a>) -> Result<Self> {
        match expr {
            Expression::Identifier(ident) => Self::cover(ident.unbox(), p),
            Expression::ObjectExpression(expr) => Self::cover(expr.unbox(), p),
            Expression::ArrayExpression(expr) => Self::cover(expr.unbox(), p),
            _ => Err(()),
        }
    }
}

impl<'a> CoverGrammar<'a, ObjectExpression<'a>> for BindingPattern<'a> {
    fn cover(obj_expr: ObjectExpression<'a>, p: &mut Parser<'a>) -> Result<Self> {
        ...
        BindingIdentifier::ObjectPattern(ObjectPattern { .. })
    }
}

impl<'a> CoverGrammar<'a, ArrayExpression<'a>> for BindingPattern<'a> {
    fn cover(expr: ArrayExpression<'a>, p: &mut Parser<'a>) -> Result<Self> {
        ...
        BindingIdentifier::ArrayPattern(ArrayPattern { .. })
    }
}

그러면 표현식바인딩 패턴으로 변환해야 하는 어느 곳이라도 BindingPattern::cover(expression)를 호출하면 됩니다.


타입스크립트

자바스크립트를 마무리하고 타입스크립트를 파싱하는 것을 도전하고자 하시나요?
악담은 규격이 없다는 것이지만, 다행히도 타입스크립트 파서는 하나의 파일 안에 존재합니다 🙃.

JSX vs TSX

다음 코드를 살펴보세요.

javascript
let foo = <string> bar;

이 코드가 tsx라면 문법 오류입니다 (종료되지 않은 JSX), 하지만 VariableDeclarationTSTypeAssertion로 올바르게 해석됩니다.

앞선 보기

일부 위치에서는 파서가 여러 토큰을 미리 보아야 하며, 올바른 문법을 결정하기 위해 한 번 이상의 토큰을 조사해야 합니다.

타입스크립트 인덱스 서명

예를 들어 TSIndexSignature를 파싱할 때, 다음 두 경우를 고려해보세요:

typescript
type A = { readonly [a: number]: string }
           ^__________________________^ TSIndexSignature

type B = { [a]: string }
           ^_________^ TSPropertySignature

type A에서 첫 번째 {에서, readonly, [, a, :number까지 총 5개의 토큰을 미리 읽어봐야, 이것이 TSIndexSignature인지 아닌지를 확실히 판단할 수 있습니다.

이렇게 가능하고 효율적으로 하기 위해 렉서는 여러 토큰을 저장하기 위한 버퍼가 필요합니다.

화살표 표현식

커버 문법에서 논의된 바와 같이, SequenceExpression 뒤에 => 토큰을 발견하면 표현식(Експрессия)에서 바인딩 패턴(Вязующий паттерн)으로 변환해야 합니다.

그러나 이 접근법은 타입스크립트에는 적용되지 않습니다. 괄호 안의 각 항목은 타입스크립트 문법을 가질 수 있기 때문에 다루어야 할 경우가 너무 많습니다. 예를 들어:

typescript
(<x>a, b as c, d!);
(a?: b = {} as c!) => {};

이 특정 경우에 대해 타입스크립트 소스 코드를 공부하는 것이 권장됩니다. 관련 코드는 다음과 같습니다:

typescript
function tryParseParenthesizedArrowFunctionExpression(
  allowReturnTypeInArrowFunction: boolean,
): Expression | undefined {
  const triState = isParenthesizedArrowFunctionExpression();
  if (triState === Tristate.False) {
    // 분명히 괄호화된 화살표 함수 표현식이 아님.
    return undefined;
  }

  // 분명히 화살표 함수라면, 후속 `=>` 또는 `{` 토큰이 필요 없이 그냥 파싱할 수 있다. 그렇지 않다면, 화살표 함수일 가능성만 있을 뿐이다.
  // 파싱해보지만 모호함을 허용하지 않으며, 만약 이것이 표현식일 수 있다면 `undefined`를 반환한다.
  return triState === Tristate.True
    ? parseParenthesizedArrowFunctionExpression(
        /*allowAmbiguity*/ true,
        /*allowReturnTypeInArrowFunction*/ true,
      )
    : tryParse(() =>
        parsePossibleParenthesizedArrowFunctionExpression(allowReturnTypeInArrowFunction),
      );
}

//  True        -> 여기서 분명히 괄호화된 화살표 함수를 기대합니다.
//  False       -> 여기서 괄호화된 화살표 함수가 *절대* 존재할 수 없습니다.
//  Unknown     -> 여기서 괄호화된 화살표 함수가 *있을 수도* 있습니다.
//                 확률적으로 앞선 보기로 확인하고, 아니라면 롤백합니다.
function isParenthesizedArrowFunctionExpression(): Tristate {
  if (
    token() === SyntaxKind.OpenParenToken ||
    token() === SyntaxKind.LessThanToken ||
    token() === SyntaxKind.AsyncKeyword
  ) {
    return lookAhead(isParenthesizedArrowFunctionExpressionWorker);
  }

  if (token() === SyntaxKind.EqualsGreaterThanToken) {
    // 오류 복구 튜닝:
    // 독립된 `=>`를 보았다면, 사용자가 의도한 바가 아마도 화살표 함수 표현식일 것이라고 판단하여 파싱합니다.
    return Tristate.True;
  }
  // 분명히 괄호화된 화살표 함수가 아님.
  return Tristate.False;
}

요약하면, 타입스크립트 파서는 앞선 보기(빠른 경로)와 백트래킹을 조합하여 화살표 함수를 파싱합니다.