
파싱
최근 수정 시각:
[ 펼치기 · 접기 ]
|

- 어휘 분석(lexing) 과정에서는 코드 문자열은 개별 토큰으로 분리되며, 각 토큰은 코드의 기본적인 의미 단위로써 자료형, 식별자, 리터럴, 연산자 및 기타 구문 기호를 나타낸다. 이러한 과정을 토큰화(tokenize)라고 한다.
어휘 분석기에서 생성된 토큰들이 문법 규칙에 맞게 배열 되어있는지를 확인하고 그 구조를 파스 트리(parse tree)로 구성하는 과정이다. 이때 언어 규칙을 정의할 때 배커스-나우르 표기법(BNF)으로 기술하며, 이런 형식으로 표현되는 문법을 문맥 자유 문법(Context Free Grammar)라고 한다. 앞으로 서술할 내용은 BNF를 안다는 전제로 작성된다.
분석 과정을 간략히 적자면, 파서는
이 축약 과정을 계속 반복해서, 입력한 문자열이 시작 심볼 하나로 딱 줄어들면, 그 문자열은 '문법적으로 올바르다'고 결정된다. 이 상태를 '문자열 s가 문법 G가 정의하는 언어 L(G)에 속한다. ()'라고 한다.
분석 과정을 간략히 적자면, 파서는
<숫자> + <숫자> 같은 토큰 뭉치를 보고, 언어 규칙 목록에서 <숫자> + <숫자> 꼴이 있는지를 확인한다. 예를 들어 <수식> ::= <숫자> + <숫자>라는 규칙을 발견하면, 이 토큰 뭉치를 <수식>이라는 더 큰 개념으로 바꿔버린다. 이렇게 규칙의 오른쪽(RHS)을 왼쪽(LHS)으로 바꾸는 걸 '축약(Reduction)'이라고 한다.이 축약 과정을 계속 반복해서, 입력한 문자열이 시작 심볼 하나로 딱 줄어들면, 그 문자열은 '문법적으로 올바르다'고 결정된다. 이 상태를 '문자열 s가 문법 G가 정의하는 언어 L(G)에 속한다. ()'라고 한다.
아래 문법에 대해 문자열 이 있다고 하자. 표기에 대한 자세한 설명은 배커스-나우르 표기법 참조.
문자열 에 대해 해당 문법을 기반으로 파싱하면 다음과 같이 될 것이다. 굵은 글씨는 non-terminal, 일반 텍스트는 terminal 노드다. 간략화를 위해 수를
<expr> ::= <term> | <expr> '+' <term> // 시작 non-terminal <term> ::= <factor> | <term> '*' <factor> <factor> ::= <number> <number> ::= <digit> | <digit> <number> <digit> ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'문자열 에 대해 해당 문법을 기반으로 파싱하면 다음과 같이 될 것이다. 굵은 글씨는 non-terminal, 일반 텍스트는 terminal 노드다. 간략화를 위해 수를
<digit>에서 <number>로 변환하는 부분은 생략한다. 아래 문법에 대해
문자열 을 유도한다고 하자. 위의 BNF를 기반으로 좌측 유도(Leftmost derivation)와 우측 유도(Rightmost derivation)을 수행하면 아래와 같이 다른 형태의 파스 트리가 구성되는 것을 확인할 수 있다.
<expr> ::= <expr> '+' <expr> | <expr> '-' <expr> | <number> // 시작 non-terminal <number> ::= <digit> | <digit> <number> // <number>는 예시 부분과 동일문자열 을 유도한다고 하자. 위의 BNF를 기반으로 좌측 유도(Leftmost derivation)와 우측 유도(Rightmost derivation)을 수행하면 아래와 같이 다른 형태의 파스 트리가 구성되는 것을 확인할 수 있다.
좌측 유도 | 파스 트리 |
![]() |
우측 유도 | 파스 트리 |
![]() |
위와 같이 어떤 문법 에 대해 특정 문장이 2개 이상의 서로 다른 파스 트리를 가지면 문법 는 모호하다(ambiguous)고 한다.
연산자의 우선순위와 결합 법칙을 기반으로 파스 트리가 모호하지 않게 수정할 수 있다.
- 문법 수정을 위해서 각 연산자마다 새로운 non-terminal을 만들고 시작 non-terminal부터 가장 낮은 우선순위를 가진 연산자를 적용한다.
- 결합 법칙 상으로는 좌측 결합이면 left recursion, 우측 결합이 되면 right recursion을 적용한다. 예를 들어 모호성 문단의 BNF는
+,-연산자가 동일한 우선순위를 가지며 좌측 결합이고,**를 Python의 제곱 연산자라고 하면,+,-보다 높은 우선순위를 가지며 우측결합이다. 이를 기반으로 BNF를 다음과 같이 수정하면, 모호성이 해결된다.<expr> ::= <expr> '+' <term> | <expr> '-' <term> | <term> // +, -은 좌측 결합이므로 left recursion 적용 <term> ::= <number> '**' <term> | <number> // 제곱 연산은 우측 결합이므로 right recursion 적용 <number> ::= <digit> | <digit> <number> // <number>는 예시 부분과 동일
위 수정된 문법에 대해 문자열 을 파스 트리로 구성하면, 으로 시작하는 경우만 해당 문자열로 유도되며 으로 시작하는 경우는 유도되지 않는다. 식 도 이전 방법은 , 두 가지로 파스 트리가 구성될 수 있었지만 위와 같이 결합 법칙을 기반으로 수정하면 모호성이 해결된다.
C언어의 포인터와 증가 연산자(++)에 대한 문법을 생각해보자. 이 문법에 대해 *p++라는 표현식은 (*p)++인지 *(p++)인지에 따라 결과가 완전히 달라진다.[2] 아래의 우선순위를 가지며 문법이 모호하지 않도록 문법을 재작성해보자.
|
[풀이]
- 연산자 우선 순위는 (후위 증가 연산자 > 전위 증가 연산자 = 역참조 연산자 > 이항 연산자)다.
- 좌측 결합하는 연산자는 후위 증가 연산자, 이항 연산자, 나머지는 우측 결합한다.
이 연산자 우선 순위와 결합 방향을 정리하면 다음과 같이 나온다.
<expr> ::= <expr> '+' <unary_expr> | <unary_expr> <unary_expr> ::= '++' <unary_expr> | '*' <unary_expr> | <postfix_expr> <postfix_expr>::= <postfix_expr> '++' | id이 문법은 문자열 if E1 then if E2 then S1 else S2 에 대해
두 가지로 해석될 수 있어서 모호하다.[3] 이 문법을 수정해서 else가 가장 가까운 then과만 짝이 되어 유도되도록 해보자. 즉 if E1 then (if E2 then S1 else S2)으로만 유도되도록 해야 한다. |
[풀이]
우선 if-then-else로 짝을 이루는 문장을 'matched statement', if-then으로 끝나는 문장을 else랑 매칭이 안 된 'unmatched statement'라고 하자. 이 두 statement에 대해 논터미널을 정의해서 문법을 다음과 같이 만들면 된다.
<stmt> ::= <matched_stmt> | <unmatched_stmt> <matched_stmt> ::= 'if' <expr> 'then' <matched_stmt> 'else' <matched_stmt> <unmatched_stmt>::= 'if' <expr> 'then' <stmt> | 'if' <expr> 'then' <matched_stmt> 'else' <unmatched_stmt>if 다음에 then이 오고, 그 뒤에는 반드시
<matched_stmt>만 올 수 있도록 강제하였다. if E1 then if E2 then S1 else S2 같은 문장은 if E1 then (if E2 then S1 else S2) 로만 해석될 수 있다. 안쪽의 if E2 then S1 else S2는 이미 else 절을 포함하여 완벽한 matched statement이기 때문이다. 이렇게 되면, if...then...else... 형태의 문장이 되려면, then 절에 들어가는 내용물 역시 else에 대한 모호함 없이 완전하게 된다. 만약 if E1 then ... 에서 then 뒤에 if E2 then S1 같은 짝 없는 문장이 온다면, 그건 if E1 then의 else를 잡아먹을 수 있는 가능성이 생길 것이다.
Top-down |
![]() |
Bottom-up |
![]() |
파서의 구조는 크게 하향식(Top-down) 방식과 상향식(Bottom-up) 방식으로 나눌 수 있다. 상향식은 루트 노드에서부터 leaf 노드를 생성해나가는 방식이다. 좌측 유도 순서의 생성 규칙을 적용한다.
하향식은 leaf 노드들로부터 루트 노드 쪽으로 위로 생성해나가는 방식이다. 우측 유도의 역순의 생성규칙을 적용한다. 유도 자체는 우측부터 하지만 입력 문자열은 왼쪽부터 진행해 나가기 때문이다.
하향식은 leaf 노드들로부터 루트 노드 쪽으로 위로 생성해나가는 방식이다. 우측 유도의 역순의 생성규칙을 적용한다. 유도 자체는 우측부터 하지만 입력 문자열은 왼쪽부터 진행해 나가기 때문이다.
- Recursive Descent Parser
- Predictive Parser
- LL(1)
- LL(k)
- LL(*): ANTLR4에서 사용하는 파싱 방식이다.
- Shift-Reduce Parser
- LR(0)
- SLR(1), Simple LR(1)
- CLR(1), Canonical LR(1)
- LALR(1), LookAhead LR(1)
![]()
이 저작물은 CC BY-NC-SA 2.0 KR에 따라 이용할 수 있습니다. (단, 라이선스가 명시된 일부 문서 및 삽화 제외)
기여하신 문서의 저작권은 각 기여자에게 있으며, 각 기여자는 기여하신 부분의 저작권을 갖습니다.
나무위키는 백과사전이 아니며 검증되지 않았거나, 편향적이거나, 잘못된 서술이 있을 수 있습니다.
나무위키는 위키위키입니다. 여러분이 직접 문서를 고칠 수 있으며, 다른 사람의 의견을 원할 경우 직접 토론을 발제할 수 있습니다.




