[컴파일러] Chapter 3. Syntax Analysis(Part 2)
경희대학교 허선영 교수님의 컴파일러 수업을 기반으로 정리한 글입니다.
1. Shift-Reduce Parsing - LR(k) Parsing
- predictive parser: 어떤 production이 사용될지 예측
 - 오른쪽 항 전체에 해당하는 입력 토큰을 볼 때까지 결정을 미룲
 - LR(k)
- Left-to-right parse
 - Rightmost derivation
 - k-token lookahead
 
 - predictive parsing(LL aprsing)보다 더 많은 grammars에 대해서 parsing 가능
 
- 동작 방식
- parser는 stack & input을 가짐
- stack contents & next input token에 의해서, 2가지 동작 중 하나가 결정됨
- Shift: top of stack에 next input을 push
 - Reduce: choose a production X -> ABC
- Pop off C, B, A
 - Push X
 
 
 
 - stack contents & next input token에 의해서, 2가지 동작 중 하나가 결정됨
 

- Initialization
- Stack is Empty
 - Parset is at the beginnig of input stream
 
 - if $ is shifted -> input stream has been successfully parsed
 
- Example





-> 어떤 타이밍에 shift/reduce를 진행할지 결정해야 함
- to make shift/reduce decisions -> parser uses deterministic finite automata(DFA)
- each state -> corresponds to the contents of stack at some time
 - each edge -> is labeled with a symbol(terminal or non-terminal)
 
 - DFA를 사용하여 transition table을 build
- make decisions based on transition table
 
 

- sn: shift in to state n
 - gn: goto state n
 - rk: reduce by rule k
 - a: accept
 
- Example
- shift + state num
 - reduce + production num
 

- 4 Actions
- Shift(n)
- 입력을 한 칸 앞으로 진행
 - 현재 input token과 new state n을 stack에 push
 
 - Reduce(k)
- rule k의 우변 길이(symbols 개수) 만큼 stack을 pop
 - goto n 수행
 - 전이된 new state n을 stack에 push
 
 

- Accept
- Stop parsing, report success
 
 - Error
- Stop parsing, report error
 
 
2. LR(k) Parsing
- LR(k) parsing -> content of its stack & next k tokens 사용
- 실제로는 k>1은 사용되지 않음 -> parsing algorithm이 복잡해지고, memory도 많이 사용됨
 - 대부분의 programming languages는 LR(1) grammars로 표현됨
 
 - LR(0) parsing
- lookahead를 아예 보지 않고, shift/reduce decisions 진행
 
 
2.1. LR(0) Parsing
- stack is empty
 - input contains 'S' string followed by a '$'
 - parser position: parser가 보고있는 위치
 

- Basic Operations
- Closure(I): parser position 오른쪽에 위치한 non-terminal로 시작하는 모든 productions를 LR(0) items를 나타내는 I에 추가 -> I가 변하지 않을 때까지
 


-> S로 시작되는 모든 productions 추가
- Goto(I, X): 모든 items에서 parser position을 symbol X 이후로 이동하는 함수
- 현재 state I에서 symbol X를 읽었을 때, 다음 state는 무엇인지 알려줌
 - item 집합 I 내부에서, parser position 바로 우측에 X가 있는 items만 골라서 해당 items의 parser position을 한 칸 오른쪽으로 이동 -> 해당 아이템들로 만든 집합에 Clousre 연산 적용
 
 


- Example(DFA 생성)


-> DFA 완성
- Building Transition Table


-> DFA 이용하여 작성
-> conflict 발생 시에, LR(0)로 parsing 불가능한 grammar

- 최종 Example

2.2. Simple LR Parsing
- LR(0) parser를 기반으로 하지만, reduce할 때 FOLLOW 집합을 사용해 reduce의 범위를 제한
 

-> state3에서 기존에는 모든 terminals에 대해서 reduce 진행
-> SLR에서는 T의 follow인 +, $에 대해서만 reduce를 진행
-> r3에서도 마찬가지로 E의 follow인 $에 대해서만 reduce 진행
-> LR(0)로는 parsing 불가능, but SLR로는 parsing 가능한 gammar!!
2.3. LR(1) Parsing
LR(0): A의 follower를 모두 고려
SLR: 모든 terminal 고려
LR(1): lookahead symbol만 고려
- LR(1) item은 2개의 components를 가짐
- Production: A -> α.β
- parser position은 현재 분석이 어디까지 진행되었는지 나타냄
 
 - Lookahead symbol: x 
- 아이템이 적용될 수 있는 다음 input symbol
 
 
 - Production: A -> α.β
 - ex) E -> T.+E, $ : T까지 분석을 완료했고, +E를 남겨두었으며, 해당 production은 input symbol이 $일 때 사용 가능하다는 뜻
 
- Basic Functions
- Closure
 


-> lookahead symbol인 z까지 고려,
- β is empty: FIRST(βz) = FIRST(z)
- β is nullable: FIRST(βz) = FIRST(β) ∪ FIRST(z)
- β is not nullable: FIRST(β)만 확인
- Goto
 


-> LR(0)에서는 단순히 parser position 이동 후 closure 적용
-> LR(1)에서는 parser position 이동 후 closure를 적용하는데, lookahead가 유지돼야 함
- Example
1) First State 빌드 - Closure 연산

- V -> x, $과 V -> x, =는 다른 것으로 취급!!
2) Goto

-> $ sign이 나오는 경우에만 reduce 연산 수행
-> 기존 LR(0), SLR이 수행하던 범위보다 축소
-> but, state 개수가 많아지고 구조가 복잡해짐

2.4. LALR(1)
- LR(1)의 table 크기가 너무 커지는 문제
 - production이 같은데, lookahead symbol이 다른 경우를 merge
 - LALR(1): Look-Ahead LR(1)
 


2.5. Parsing Power

3. Real-world Parser Generators
- Parser Generator: 문법만 정의해주면, 해당 문법에 맞는 parser를 자동으로 생성해주는 프로그램 
- yacc, bison: LALR parser generators for C
 
 - Parser Generator Specification
- Input: a set of context-free grammars specifying a parser 
- context-free grammar: 문맥에 상관 없이 어떤 Non-terminal이 들어와도, 그에 대한 production-rule이 존재하는 문법
 
 - Outputs
- a parser in target language -> 파서 코드 자동 생성
 - a description of state machine -> 어떤 state에서 어떤 symbol을 만나면 어떻게 전이되는지
 
 - Rules: pattern & action
- pattern: context free grammar
 - action: 문법이 만족되었을 때 실행할 code fragment
 - example) exp: exp PLUS exp { $$ = $1 + $2; }
- exp PLUS exp라는 pattern -> { $$ = $1 + $2 ; }라는 코드 실행
 
 
 
 - Input: a set of context-free grammars specifying a parser 
 
- Parser Generator Example: GNU Bison

-> yyparse() 호출로 파서 실행
- User Declaration
- rules section에서 사용가능한 various values 정의
 - terminal, non-terminal symbols, and thier types 선언
- %token NUM
 - %token <type> NUM
 - %token <type> exp
 
 - precedences(우선순위)선언 -> shift-reduce conflicts 해결
 - (optionally) specify start symbol
- %start prog
 
 
 - Rules
- pattern & action -> pattern에 따라 어떤 action을 취할지
 
 - Location
- @$: location of the whole group
 - @1: location of the first subexpression
 
 

4. Ambigous Grammar
- Ambiguous Grammar: 같은 expression에 대해서 2개 이상의 parse tree가 존재하는 grammar
 - example) 4 + 5 * 6
 


4.1. Handling Ambiguous Grammar
- YACC -> shift-reduce conflicts로 알림
- 4 + 5 * 6
- stack에 expr + expr이 쌓인 상태에서, current token으로 *가 들어옴
 - parser: expr -> expr + expr rule을 통해서 reduce or shift
 - Prefer shift
 
 - 4 + 5 + 6
- stack에 expr + expr이 쌓인 상태에서, current token으로 +가 들어옴
 - parser: expr -> expr + expr rule을 통해서 reduce or shift
 - Prefer reduce
 
 
 - 4 + 5 * 6
 

-> shift 이후, 위에부터 reduce 진행하니까, 6 * 5를 먼저 reduce
4.2. Directives
- Three Solutions
- Let YACC complain, but check if the choice (shift) is correct
 - Rewrite grammar to eliminate ambiguity
 - Keep grammar, but add precedence directives -> conflicts가 해결되는 방향으로
- %left, %right, %nonassoc
 - for this grammar -> 라인의 순서가 낮으면 우선순위가 높음
- $left PLUS MINUS -> PLUS MINUS 동등
 - $left MULT DIV -> MULT DIV 동등
 
 
 
 - 주어진 directives에 따라, YACC은 each terminal & rule에 precedence 부여
- rightmost terminal과 shift할 terminal의 precedence 비교
 
 

- 주어진 shift-reduce conflict에 따라, YACC는 action 수행
- reduce 수행할 rule과 shift 수행할 terminal의 precedence find
- prec(terminal) > prec(rule): shift
 - prec(terminal) < prec(rule): reduce
 - prec(terminal) = prec(rule)
- assoc(terminal) = left : reduce
 - assoc(terminal) = right: shift
 - assoc(terminal) = nonassoc : report error -> 결합 불가(오류 처리)
 - associativity(결합법칙): 연산자끼리 우선순위가 같을 때, 어느 방향으로 묶을 것인지 결정하는 규칙
 
 
 
 - reduce 수행할 rule과 shift 수행할 terminal의 precedence find
 

4.3. Dangling Else Problem
- Valid Program: if a then if b then S1 else S2
 

-> if a then if b then s1 else s2 동작을 원하지만, S1이 stack의 top에 위치할 때 shift-reduce conflict 발생
- solution: eliminate ambiguity by modifying grammar(matched/unmatched)
 


4.4. Default Behavior
- Directives not specified
- shift-reduce: report error, shift by default
 - reduce-reduce: report error, reduce by rule that occurs first
 
 - What to do
- shift-reduce: acceptable in well defined class, 그러나 웬만하면 없애는 것이 좋음
 - reduce-reduce: unacceptable, rewrite grammar
 
 
4.5. %prec Directive
- commonly used for the unary minus problem
- %left PLUS MINUS
 - %left MULT DIV
 
 - consider - 4 * 6
 - unary minus가 우선순위가 높을 것을 권장, but MINUS의 precedence가 MULT보다 낮음
- -( 4 * 6 ) not ( -4 ) * 6
 
 - solution
- %token NUM PLUS MINUS MULT DIV UMINUS
 - %left PLUS MINUS
 - %left MULT DIV
 - %left UMINUS
 - expr: MINUX expr %prec UMINUS {}
| expr PLUS expr {} 
 
참고자료
[프로그래밍언어] - Context-Free grammar
📜 Parser * 📢 Parser의 역할* Parser는 scanner로부터 tokenized 된 word stream을 받아 syntax를 확인하고, 올바른 syntax라면 중간 표현(Intermediate Representation, IR)을 도출해내는 컴파
velog.io