본문으로 건너뛰기
김신건의 로그

[PLT] Parsing & Grammars

· 수정 · 📖 약 3분 · 1,193자/단어 #plt #compiler #parser #grammar #cfg #bnf
Parsing, Parser, BNF, EBNF, CFG, context-free grammar, recursive descent, LL parser, LR parser, LALR, PEG, parser combinator, 구문 분석, 파싱

정의

Parsing (구문 분석) 은 lexer 의 토큰 스트림 을 언어의 문법 규칙 에 따라 구조화된 트리 (AST, Abstract Syntax Tree) 로 조립하는 과정입니다.

  • 토큰들이 어떤 구문 구조 를 이루는가
  • 우선순위 (precedence): 2 + 3 * 4 = 2 + (3 * 4)
  • 결합성 (associativity): 1 - 2 - 3 = (1 - 2) - 3 (좌결합)

문법 (Grammar)

프로그래밍 언어의 문법 규칙 을 형식적으로 기술 하는 표기법.

BNF (Backus-Naur Form)

가장 기초적인 표기:

<expression> ::= <term> | <expression> "+" <term>
<term>       ::= <factor> | <term> "*" <factor>
<factor>     ::= <number> | "(" <expression> ")"
<number>     ::= [0-9]+
  • ::=: “정의”
  • |: “또는”
  • <...>: 비단말 (non-terminal, 다른 규칙 참조)
  • 문자열: 단말 (terminal, 실제 토큰)

EBNF (Extended BNF)

? (선택), * (0+), + (1+) 확장:

expression = term ( "+" term )*
term       = factor ( "*" factor )*
factor     = number | "(" expression ")"
number     = digit+
digit      = "0" | "1" | ... | "9"

정규식 같이 간결.

CFG (Context-Free Grammar)

BNF/EBNF 의 이론 이름. 문맥 자유 = 규칙 적용이 주변 문맥과 무관.

프로그래밍 언어 대부분이 CFG (완전히는 아니고 근사).

시각화: 파싱

입력: 2 + 3 * 4

토큰: [NUM 2] [PLUS] [NUM 3] [STAR] [NUM 4]

파스 트리 (문법 규칙 따라):

                expression
               /    |     \
            term   "+"   term
             |            |  \  \
           factor       factor "*" factor
             |            |         |
           NUM 2        NUM 3     NUM 4

AST (더 간결):

        +
       / \
      2   *
         / \
        3   4

AST 는 파스 트리에서 불필요한 노드 (expression, term, factor 같은 중간) 제거.

Ambiguity (모호성)

같은 입력이 여러 파스 트리를 가지면 문법이 모호.

: 2 + 3 * 4 를 아래 문법으로:

expr ::= expr op expr | number
op   ::= "+" | "*"

두 파스 트리 가능:

  • (2 + 3) * 4 = 20
  • 2 + (3 * 4) = 14

해결: 우선순위/결합성 반영한 문법 재작성 (위의 expression → term → factor 계층).

하향식 vs 상향식

Top-Down (하향식)

Start 규칙 부터 시작해 규칙 적용:

  • LL(k): Left-to-right, Leftmost derivation, k 토큰 lookahead
  • Recursive Descent: 재귀 함수로 각 규칙 표현
  • Predictive Parser: 다음 토큰 보고 어떤 규칙 적용할지 결정

Bottom-Up (상향식)

토큰 부터 시작해 규칙으로 결합:

  • LR(k): Left-to-right, Rightmost derivation reversed
  • LALR(1): LR 축약, yacc/bison 이 채택
  • SLR: LR 의 단순 변종

Recursive Descent (가장 흔한 실전 방식)

각 non-terminal 을 함수로:

function parseExpression():
    left = parseTerm()
    while peek() is "+":
        consume("+")
        right = parseTerm()
        left = BinaryOp("+", left, right)
    return left

function parseTerm():
    left = parseFactor()
    while peek() is "*":
        consume("*")
        right = parseFactor()
        left = BinaryOp("*", left, right)
    return left

function parseFactor():
    if peek() is number:
        return NumberLiteral(consumeNumber())
    if peek() is "(":
        consume("(")
        expr = parseExpression()
        consume(")")
        return expr
    error("expected number or '('")

장점:

  • 손으로 쉽게 작성
  • 에러 메시지 세밀 제어
  • 디버깅 쉬움
  • 재귀 자연스러움

단점:

  • 좌측 재귀 (expr → expr + term) 처리 어려움 → while loop 로 변환
  • 문법 규모 크면 코드도 큼

대부분 실전 언어 (Rust, TypeScript, Go, Swift) 는 recursive descent.

연산자 우선순위 (Pratt Parser / Precedence Climbing)

Recursive descent 의 확장. 각 연산자에 우선순위 부여, 하나의 함수로 처리.

function parseExpr(minPrec):
    left = parsePrimary()
    while peek() is operator and precedence(peek()) >= minPrec:
        op = consume()
        rightPrec = precedence(op) + (rightAssociative(op) ? 0 : 1)
        right = parseExpr(rightPrec)
        left = BinaryOp(op, left, right)
    return left
  • 여러 연산자 (+, -, *, /, **) 를 한 함수로
  • 우선순위 표 관리만 하면 됨

Vaughan Pratt 의 원래 알고리즘. Rust, Kotlin, Swift 등에서 사용.

LR Parser (자동 생성)

Yacc, Bison, ANTLR 이 사용.

LR(1) 파서: 상향식, 1 토큰 lookahead. 대부분 실용 문법 수용.

Shift-Reduce:

  • Shift: 다음 토큰을 스택에.
  • Reduce: 스택 상단이 규칙 우변과 매치되면 좌변으로 축약.

LALR(1): LR(1) 상태를 병합해 크기 축소. Yacc/Bison 기본.

장점: 어떤 CFG 든 처리, 자동 생성. 단점: 에러 메시지 어색, 디버깅 어려움, 상태 폭발 가능.

2020년대 트렌드: 파서 생성기 → 수동 recursive descent. 이유는 UX.

PEG (Parsing Expression Grammar)

CFG 대안. 결정적 파싱 (모호성 없음). Ordered choice: a | ba 먼저 시도.

Expr    <- Term ('+' Term)*
Term    <- Factor ('*' Factor)*
Factor  <- Number / '(' Expr ')'
  • /: ordered choice (좌 우선)
  • &: positive lookahead
  • !: negative lookahead

Packrat parsing: 메모이제이션으로 선형 시간. Peg 스타일 대표.

도구: Pest (Rust), TatSu (Python), PEG.js.

Parser Combinator

파서를 값처럼 다루는 접근. Haskell 스타일:

expr = do
  left <- term
  rest <- many (do op <- symbol "+"; t <- term; return (op, t))
  return (foldl (BinaryOp) left rest)

장점: 파싱 로직 조합, 언어 임베딩. 단점: 에러 메시지, 성능.

도구: parsec (Haskell), Chumsky (Rust), nom (Rust).

AST 생성

파서의 출력물 은 AST. 노드 예시:

type ASTNode =
  | { kind: 'Number'; value: number }
  | { kind: 'Identifier'; name: string }
  | { kind: 'BinaryOp'; op: '+' | '-' | '*' | '/'; left: ASTNode; right: ASTNode }
  | { kind: 'Assignment'; target: ASTNode; value: ASTNode }
  | { kind: 'If'; cond: ASTNode; then: ASTNode; else?: ASTNode }
  | { kind: 'While'; cond: ASTNode; body: ASTNode[] }
  | { kind: 'Function'; name: string; params: string[]; body: ASTNode[] }
  | { kind: 'Call'; callee: ASTNode; args: ASTNode[] }

자세한 것은 AST 참조.

에러 복구

파서가 오류 만나면:

  • Panic mode: 다음 세미콜론까지 건너뜀
  • Phrase-level recovery: 특정 토큰 삽입/삭제
  • Error productions: 문법에 흔한 오류 규칙 추가

좋은 컴파일러는 한 파일에 여러 오류를 함께 보고. 하나 발견 후 즉시 중단 X.

실전 파서 예시

TypeScript

Recursive descent. parser.ts 안에 각 parseXXX 함수.

Rust

rustc_parse crate. Pratt 스타일 우선순위 처리.

Python

PEG parser (Python 3.9+). 이전에는 LL(1). PEP 617 이 이관.

JavaScript (V8, SpiderMonkey)

수동 recursive descent, 성능 극도로 튜닝.

함정

WARNING

좌측 재귀 는 recursive descent 에서 무한 재귀. expr → expr + term 은 while loop 로 재작성.

CAUTION

우선순위 오류. 2 + 3 * 4 가 20 이 나오면 문법 재점검.

WARNING

에러 메시지 개선을 후순위로. 실전에서는 좋은 에러가 언어 채택 좌우. 초기 설계부터.

IMPORTANT

Comment / trailing comma 등 언어 UX. 문법 규칙에 명시 필요.

CAUTION

파서 생성기의 유혹. 초기엔 빠르지만 유지보수 어려움. 수동 recursive descent 검토.

관련 위키

이 글의 용어 (5개)
[PLT] Abstract Syntax Tree (AST)plt
정의 Abstract Syntax Tree (AST, 추상 구문 트리) 는 프로그램의 문법 구조를 트리로 표현 한 자료구조입니다. 파서의 출력물이자, 이후 컴파일러/인터프리터의 …
[PLT] Lexical Analysis (어휘 분석)plt
정의 어휘 분석 (Lexical Analysis) 은 소스 코드 문자 시퀀스 를 의미 있는 최소 단위인 토큰 (Token) 으로 분해하는 과정입니다. 담당 컴포넌트를 Lexer …
[PLT] Semantic Analysis (의미 분석)plt
정의 Semantic Analysis (의미 분석) 는 파서가 생성한 AST 를 검사하여 문법적으로 올바르지만 의미상 오류인 경우 를 발견하고, 각 이름 (identifier) …
이산수학 (Discrete Mathematics)discrete-math
정의 이산수학 (Discrete Mathematics) 은 이산 (discrete, 셀 수 있는) 대상을 다루는 수학의 분야입니다. 연속 (continuous) 대상 을 다루는 …
프로그래밍 언어론 (Programming Language Theory)plt
정의 프로그래밍 언어론 (Programming Language Theory, PLT) 은 프로그래밍 언어를 어떻게 설계하고, 정의하고, 처리하는가 를 다루는 컴퓨터 과학 분야입니…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기