Skip to content

Split View: [컴파일러] 07. 구문 분석 (2) - Bottom-Up 파싱과 LR 파서

|

[컴파일러] 07. 구문 분석 (2) - Bottom-Up 파싱과 LR 파서

구문 분석 (2) - Bottom-Up 파싱과 LR 파서

Bottom-Up 파싱은 입력 문자열에서 출발하여 시작 기호로 축약해 나가는 방식입니다. LL 파서보다 더 넓은 범위의 문법을 처리할 수 있어 실제 컴파일러에서 널리 사용됩니다.


1. Bottom-Up 파싱 개요

기본 아이디어

Bottom-Up 파싱은 **축약(reduction)**을 반복하여 입력 문자열을 시작 기호로 만듭니다.

입력: id * id + id

축약 과정 (우측 유도의 역순):
  id * id + id
  F  * id + id     (F -> id)
  T  * id + id     (T -> F)
  T  * F  + id     (F -> id)
  T     + id       (T -> T * F)
  E     + id       (E -> T)
  E     + F        (F -> id)
  E     + T        (T -> F)
  E                (E -> E + T)

이것은 우측 유도를 역순으로 추적한 것입니다.

핸들(Handle)

핸들은 축약할 수 있는 부분 문자열과 해당 생성 규칙의 조합입니다. 올바른 핸들을 찾아 축약하면 시작 기호에 도달합니다.

문장 형식: T * F + id
핸들: T * F (생성 규칙: T -> T * F)
축약 결과: T + id

잘못된 핸들을 선택하면 시작 기호에 도달할 수 없습니다. 핸들 선택이 Bottom-Up 파싱의 핵심 과제입니다.


2. 이동-축약 파싱(Shift-Reduce Parsing)

이동-축약 파서는 스택입력 버퍼를 사용합니다.

4가지 동작

  1. 이동(Shift): 입력의 다음 토큰을 스택에 push합니다.
  2. 축약(Reduce): 스택 꼭대기의 핸들을 생성 규칙의 좌변 논터미널로 교체합니다.
  3. 수락(Accept): 파싱 성공입니다.
  4. 에러(Error): 구문 오류가 발견되었습니다.

파싱 예제: id * id + id

스택          입력              동작
$            id * id + id $   이동
$ id         * id + id $      축약 (F -> id)
$ F          * id + id $      축약 (T -> F)
$ T          * id + id $      이동
$ T *        id + id $        이동
$ T * id     + id $           축약 (F -> id)
$ T * F      + id $           축약 (T -> T * F)
$ T          + id $           축약 (E -> T)
$ E          + id $           이동
$ E +        id $             이동
$ E + id     $                축약 (F -> id)
$ E + F      $                축약 (T -> F)
$ E + T      $                축약 (E -> E + T)
$ E          $                수락!

이동-축약 충돌

때때로 파서가 이동할지 축약할지 결정할 수 없는 상황이 발생합니다.

  • 이동-축약 충돌(Shift-Reduce Conflict): 이동과 축약 모두 가능한 경우
  • 축약-축약 충돌(Reduce-Reduce Conflict): 여러 생성 규칙으로 축약이 가능한 경우

이러한 충돌을 해결하는 것이 LR 파싱 기법의 핵심입니다.


3. LR 파싱 개요

LR 파서는 가장 강력한 이동-축약 파서입니다.

LR(k)의 의미

  • L: 입력을 왼쪽에서 오른쪽으로 스캔
  • R: 우측 유도를 역순으로 생성
  • (k): k개의 선행 토큰을 보고 결정

LR 파서의 구성 요소

입력: a1 a2 ... an $

        +---+---+---+---+
스택:   | s0| X1| s1| X2| s2 ...
        +---+---+---+---+

파싱 테이블:
  ACTION[s, a] = 이동/축약/수락/에러
  GOTO[s, A]   = 다음 상태
  • 스택: 상태와 문법 기호가 번갈아 쌓입니다.
  • ACTION 테이블: 현재 상태와 입력 토큰에 따른 동작을 결정합니다.
  • GOTO 테이블: 축약 후 전이할 상태를 결정합니다.

LR 파싱 알고리즘

스택에 초기 상태 s0을 push

loop:
  s = 스택 꼭대기 상태
  a = 현재 입력 기호

  case ACTION[s, a]:
    이동 sn:
      a와 상태 sn을 스택에 push
      다음 입력 기호로 전진

    축약 (A -> beta):
      스택에서 2*|beta|개 기호를 pop
      s' = 새로운 스택 꼭대기 상태
      A와 GOTO[s', A]를 스택에 push

    수락:
      파싱 성공, 종료

    에러:
      에러 처리 루틴 호출

4. LR(0) 항목과 오토마타

LR(0) 항목(Item)

LR(0) 항목은 생성 규칙에 **점(dot)**을 추가한 것으로, 파싱 진행 상태를 나타냅니다.

생성 규칙: A -> X Y Z

가능한 LR(0) 항목:
  A -> . X Y Z    (아직 아무것도 보지 않음)
  A -> X . Y Z    (X를 본 상태)
  A -> X Y . Z    (X Y를 본 상태)
  A -> X Y Z .    (축약 가능!)

클로저(Closure) 연산

항목 집합의 클로저를 계산합니다.

closure(I):
  I의 모든 항목을 결과에 추가
  결과에 A -> alpha . B beta 형태의 항목이 있으면:
    B -> gamma인 모든 생성 규칙에 대해
    B -> . gamma를 결과에 추가 (아직 없으면)
  변화가 없을 때까지 반복

GOTO 연산

goto(I, X):
  I에서 A -> alpha . X beta 형태의 항목에 대해
  A -> alpha X . beta를 모은 집합의 closure

LR(0) 오토마타 구성 예제

문법:
  E' -> E       (증강 문법의 시작)
  E  -> E + T
  E  -> T
  T  -> T * F
  T  -> F
  F  -> ( E )
  F  -> id

I0 = closure({E' -> . E}):
  E' -> . E
  E  -> . E + T
  E  -> . T
  T  -> . T * F
  T  -> . F
  F  -> . ( E )
  F  -> . id

goto(I0, E) = I1:
  E' -> E .
  E  -> E . + T

goto(I0, T) = I2:
  E  -> T .
  T  -> T . * F

goto(I0, F) = I3:
  T  -> F .

goto(I0, '(') = I4:
  F  -> ( . E )
  E  -> . E + T
  E  -> . T
  ...

goto(I0, id) = I5:
  F  -> id .

5. SLR 파싱 테이블 구성

SLR(Simple LR) 파싱은 LR(0) 항목과 FOLLOW 집합을 사용합니다.

SLR 파싱 테이블 구성 알고리즘

1. 증강 문법의 LR(0) 항목 집합 C를 구성

2. 항목 집합 Ii에 대해:
   a) A -> alpha . a beta가 Ii에 있고, goto(Ii, a) = Ij이면:
      ACTION[i, a] = shift j

   b) A -> alpha .가 Ii에 있으면 (A != S'):
      FOLLOW(A)의 모든 a에 대해:
      ACTION[i, a] = reduce (A -> alpha)

   c) S' -> S .가 Ii에 있으면:
      ACTION[i, $] = accept

3. goto(Ii, A) = Ij이면: GOTO[i, A] = j

SLR 파싱 테이블 예제

       ACTION                          GOTO
상태 | id  | +   | *   | (   | )   | $ | E | T | F
-----+-----+-----+-----+-----+-----+---+---+---+---
  0  | s5  |     |     | s4  |     |   | 1 | 2 | 3
  1  |     | s6  |     |     |     | acc|   |   |
  2  |     | r2  | s7  |     | r2  | r2|   |   |
  3  |     | r4  | r4  |     | r4  | r4|   |   |
  4  | s5  |     |     | s4  |     |   | 8 | 2 | 3
  5  |     | r6  | r6  |     | r6  | r6|   |   |
  6  | s5  |     |     | s4  |     |   |   | 9 | 3
  7  | s5  |     |     | s4  |     |   |   |   | 10
  8  |     | s6  |     |     | s11 |   |   |   |
  9  |     | r1  | s7  |     | r1  | r1|   |   |
  10 |     | r3  | r3  |     | r3  | r3|   |   |
  11 |     | r5  | r5  |     | r5  | r5|   |   |

여기서 s5는 shift 5, r2는 reduce by production 2, acc는 accept입니다.


6. 정규 LR(1) 항목

SLR 파서로 처리할 수 없는 문법이 있습니다. 정규 LR(1) 항목은 **선행 기호(lookahead)**를 추가하여 더 강력한 파싱이 가능합니다.

LR(1) 항목의 형태

[A -> alpha . beta, a]

- A -> alpha . beta: LR(0) 항목과 동일
- a: 선행 기호 (이 항목으로 축약할 때 참조)

LR(1) 클로저

closure(I):
  [A -> alpha . B beta, a]가 I에 있으면:
    B -> gamma인 모든 생성 규칙에 대해
    FIRST(beta a)의 모든 b에 대해
    [B -> . gamma, b]를 추가

정규 LR(1) 파싱의 장단점

장점: SLR보다 더 많은 문법을 처리 가능
단점: 상태 수가 매우 많아질 수 있음
      (LR(0)의 수십~수백 배)

7. LALR 파싱

**LALR(Look-Ahead LR)**은 정규 LR(1)의 강력함과 SLR의 효율성을 절충한 방법입니다.

핵심 아이디어

LR(1) 항목에서 **코어(core)**가 같은 항목 집합을 합칩니다.

LR(1) 상태:
  I4: {[C -> .c C, c], [C -> .c C, d], [C -> .d, c], [C -> .d, d]}
  I7: {[C -> .c C, $], [C -> .d, $]}

코어가 같으므로 합침:
  I47: {[C -> .c C, c/d/$], [C -> .d, c/d/$]}

LALR 파서의 특징

  • 정규 LR(1)과 같은 수의 상태를 가지는 SLR과 같은 크기의 테이블
  • SLR보다 강력하고, 정규 LR(1)보다 약간 약함
  • 대부분의 프로그래밍 언어를 처리할 수 있음
  • YaccBison이 LALR 파서를 생성

SLR, LR(1), LALR의 관계

LR(0) 문법 ⊂ SLR 문법 ⊂ LALR 문법 ⊂ LR(1) 문법

실용적 선택:
- 간단한 문법: SLR
- 대부분의 프로그래밍 언어: LALR (Yacc/Bison)
- 복잡한 문법: 정규 LR(1)

8. Yacc 파서 생성기

**Yacc (Yet Another Compiler Compiler)**는 LALR(1) 파서를 자동 생성하는 도구입니다. GNU 버전인 Bison이 널리 사용됩니다.

Yacc 프로그램 구조

선언부 (declarations)
%%
문법 규칙과 동작 (grammar rules and actions)
%%
보조 C 함수 (supporting C functions)

Yacc 프로그램 예제: 계산기

%{
#include <stdio.h>
#include <ctype.h>
int yylex();
void yyerror(const char *s);
%}

%token NUM

%%

line   : expr '\n'          { printf("Result: %d\n", $1); }
       ;

expr   : expr '+' term      { $$ = $1 + $3; }
       | expr '-' term      { $$ = $1 - $3; }
       | term               { $$ = $1; }
       ;

term   : term '*' factor    { $$ = $1 * $3; }
       | term '/' factor    { $$ = $1 / $3; }
       | factor             { $$ = $1; }
       ;

factor : '(' expr ')'       { $$ = $2; }
       | NUM                 { $$ = $1; }
       ;

%%

int yylex() {
    int c = getchar();
    if (isdigit(c)) {
        yylval = c - '0';
        while (isdigit(c = getchar()))
            yylval = yylval * 10 + c - '0';
        ungetc(c, stdin);
        return NUM;
    }
    return c;
}

void yyerror(const char *s) {
    fprintf(stderr, "Error: %s\n", s);
}

int main() {
    return yyparse();
}

Yacc의 충돌 해결 규칙

Yacc는 LALR 파싱 테이블에 충돌이 있을 때 다음 규칙을 적용합니다.

  1. 이동-축약 충돌: 기본적으로 이동을 선택합니다 (dangling else 문제 해결).
  2. 축약-축약 충돌: 먼저 나온 규칙으로 축약합니다.

Yacc의 우선순위 지시어

%left '+' '-'       // 왼쪽 결합, 낮은 우선순위
%left '*' '/'       // 왼쪽 결합, 높은 우선순위
%right UMINUS       // 오른쪽 결합, 단항 마이너스

이 지시어를 사용하면 모호한 문법도 충돌 없이 처리할 수 있습니다.


9. LR 파서의 에러 복구

패닉 모드 복구

1. 스택에서 A를 처리할 수 있는 상태를 찾을 때까지 pop
2. 입력에서 FOLLOW(A)에 속하는 토큰을 찾을 때까지 건너뜀
3. GOTO[s, A]를 push하고 파싱 계속

Yacc의 에러 복구: error 토큰

stmt : expr ';'
     | error ';'   { yyerrok; printf("Skipping bad statement\n"); }
     ;

error는 Yacc의 특수 토큰으로, 에러 상황에서 자동으로 매치됩니다. yyerrok은 에러 복구가 완료되었음을 선언합니다.


정리

개념핵심 내용
Bottom-Up 파싱입력에서 시작하여 시작 기호로 축약
핸들축약할 부분 문자열과 생성 규칙의 조합
이동-축약스택 기반의 4가지 동작 (이동, 축약, 수락, 에러)
LR(0) 항목점(dot)으로 파싱 진행 상태를 표현
SLRLR(0) 항목 + FOLLOW 집합 사용
정규 LR(1)선행 기호를 포함한 항목, 가장 강력
LALR코어가 같은 LR(1) 상태를 합침, 실용적
Yacc/BisonLALR(1) 파서 자동 생성 도구

LR 파싱은 대부분의 프로그래밍 언어를 효율적으로 처리할 수 있는 강력한 기법입니다. 다음 글에서는 파싱 결과를 활용하여 의미 있는 번역을 수행하는 구문 지시 번역을 다루겠습니다.

[Compiler] 07. Syntax Analysis (2) - Bottom-Up Parsing and LR Parsers

Syntax Analysis (2) - Bottom-Up Parsing and LR Parsers

Bottom-Up parsing starts from the input string and reduces it toward the start symbol. It can handle a wider range of grammars than LL parsers and is widely used in actual compilers.


1. Bottom-Up Parsing Overview

Basic Idea

Bottom-Up parsing repeatedly performs reductions to turn the input string into the start symbol.

Input: id * id + id

Reduction process (reverse of rightmost derivation):
  id * id + id
  F  * id + id     (F -> id)
  T  * id + id     (T -> F)
  T  * F  + id     (F -> id)
  T     + id       (T -> T * F)
  E     + id       (E -> T)
  E     + F        (F -> id)
  E     + T        (T -> F)
  E                (E -> E + T)

This traces the rightmost derivation in reverse.

Handle

A handle is the combination of a reducible substring and the corresponding production rule. Finding and reducing the correct handle leads to the start symbol.

Sentential form: T * F + id
Handle: T * F (production: T -> T * F)
After reduction: T + id

Choosing the wrong handle makes it impossible to reach the start symbol. Handle selection is the core challenge of Bottom-Up parsing.


2. Shift-Reduce Parsing

A shift-reduce parser uses a stack and an input buffer.

Four Operations

  1. Shift: Pushes the next input token onto the stack.
  2. Reduce: Replaces the handle at the top of the stack with the nonterminal on the left side of a production.
  3. Accept: Parsing succeeds.
  4. Error: A syntax error is found.

Parsing Example: id * id + id

Stack          Input              Action
$            id * id + id $   Shift
$ id         * id + id $      Reduce (F -> id)
$ F          * id + id $      Reduce (T -> F)
$ T          * id + id $      Shift
$ T *        id + id $        Shift
$ T * id     + id $           Reduce (F -> id)
$ T * F      + id $           Reduce (T -> T * F)
$ T          + id $           Reduce (E -> T)
$ E          + id $           Shift
$ E +        id $             Shift
$ E + id     $                Reduce (F -> id)
$ E + F      $                Reduce (T -> F)
$ E + T      $                Reduce (E -> E + T)
$ E          $                Accept!

Shift-Reduce Conflicts

Sometimes the parser cannot decide whether to shift or reduce.

  • Shift-Reduce Conflict: Both shift and reduce are possible
  • Reduce-Reduce Conflict: Multiple production rules can be used for reduction

Resolving these conflicts is the core of LR parsing techniques.


3. LR Parsing Overview

LR parsers are the most powerful shift-reduce parsers.

Meaning of LR(k)

  • L: Scans input from left to right
  • R: Produces a rightmost derivation in reverse
  • (k): Makes decisions based on k lookahead tokens

Components of an LR Parser

Input: a1 a2 ... an $

        +---+---+---+---+
Stack:  | s0| X1| s1| X2| s2 ...
        +---+---+---+---+

Parsing table:
  ACTION[s, a] = shift/reduce/accept/error
  GOTO[s, A]   = next state
  • Stack: States and grammar symbols are stacked alternately.
  • ACTION table: Determines the action based on current state and input token.
  • GOTO table: Determines the state to transition to after a reduction.

LR Parsing Algorithm

Push initial state s0 onto the stack

loop:
  s = top state on stack
  a = current input symbol

  case ACTION[s, a]:
    shift sn:
      Push a and state sn onto the stack
      Advance to next input symbol

    reduce (A -> beta):
      Pop 2*|beta| symbols from the stack
      s' = new top state on stack
      Push A and GOTO[s', A] onto the stack

    accept:
      Parsing success, terminate

    error:
      Call error handling routine

4. LR(0) Items and Automaton

LR(0) Items

An LR(0) item is a production rule with a dot added, representing the parsing progress.

Production: A -> X Y Z

Possible LR(0) items:
  A -> . X Y Z    (nothing seen yet)
  A -> X . Y Z    (X has been seen)
  A -> X Y . Z    (X Y have been seen)
  A -> X Y Z .    (ready to reduce!)

Closure Operation

Computes the closure of an item set.

closure(I):
  Add all items in I to the result
  If A -> alpha . B beta is in the result:
    For all productions B -> gamma:
    Add B -> . gamma to the result (if not already present)
  Repeat until no changes

GOTO Operation

goto(I, X):
  For items A -> alpha . X beta in I,
  collect A -> alpha X . beta, then take their closure

LR(0) Automaton Construction Example

Grammar:
  E' -> E       (augmented grammar start)
  E  -> E + T
  E  -> T
  T  -> T * F
  T  -> F
  F  -> ( E )
  F  -> id

I0 = closure({E' -> . E}):
  E' -> . E
  E  -> . E + T
  E  -> . T
  T  -> . T * F
  T  -> . F
  F  -> . ( E )
  F  -> . id

goto(I0, E) = I1:
  E' -> E .
  E  -> E . + T

goto(I0, T) = I2:
  E  -> T .
  T  -> T . * F

goto(I0, F) = I3:
  T  -> F .

goto(I0, '(') = I4:
  F  -> ( . E )
  E  -> . E + T
  E  -> . T
  ...

goto(I0, id) = I5:
  F  -> id .

5. SLR Parsing Table Construction

SLR (Simple LR) parsing uses LR(0) items and FOLLOW sets.

SLR Parsing Table Construction Algorithm

1. Construct the LR(0) item set collection C for the augmented grammar

2. For item set Ii:
   a) If A -> alpha . a beta is in Ii and goto(Ii, a) = Ij:
      ACTION[i, a] = shift j

   b) If A -> alpha . is in Ii (A != S'):
      For all a in FOLLOW(A):
      ACTION[i, a] = reduce (A -> alpha)

   c) If S' -> S . is in Ii:
      ACTION[i, $] = accept

3. If goto(Ii, A) = Ij: GOTO[i, A] = j

SLR Parsing Table Example

       ACTION                          GOTO
State | id  | +   | *   | (   | )   | $ | E | T | F
-----+-----+-----+-----+-----+-----+---+---+---+---
  0  | s5  |     |     | s4  |     |   | 1 | 2 | 3
  1  |     | s6  |     |     |     | acc|   |   |
  2  |     | r2  | s7  |     | r2  | r2|   |   |
  3  |     | r4  | r4  |     | r4  | r4|   |   |
  4  | s5  |     |     | s4  |     |   | 8 | 2 | 3
  5  |     | r6  | r6  |     | r6  | r6|   |   |
  6  | s5  |     |     | s4  |     |   |   | 9 | 3
  7  | s5  |     |     | s4  |     |   |   |   | 10
  8  |     | s6  |     |     | s11 |   |   |   |
  9  |     | r1  | s7  |     | r1  | r1|   |   |
  10 |     | r3  | r3  |     | r3  | r3|   |   |
  11 |     | r5  | r5  |     | r5  | r5|   |   |

Here s5 means shift 5, r2 means reduce by production 2, and acc means accept.


6. Canonical LR(1) Items

Some grammars cannot be handled by SLR parsers. Canonical LR(1) items add a lookahead symbol for more powerful parsing.

Form of LR(1) Items

[A -> alpha . beta, a]

- A -> alpha . beta: Same as LR(0) item
- a: Lookahead symbol (consulted when reducing with this item)

LR(1) Closure

closure(I):
  If [A -> alpha . B beta, a] is in I:
    For all productions B -> gamma:
    For all b in FIRST(beta a):
    Add [B -> . gamma, b]

Pros and Cons of Canonical LR(1) Parsing

Pros: Can handle more grammars than SLR
Cons: Number of states can become very large
      (tens to hundreds of times more than LR(0))

7. LALR Parsing

LALR (Look-Ahead LR) is a compromise between the power of canonical LR(1) and the efficiency of SLR.

Core Idea

Merge item sets in LR(1) that have the same core.

LR(1) states:
  I4: {[C -> .c C, c], [C -> .c C, d], [C -> .d, c], [C -> .d, d]}
  I7: {[C -> .c C, $], [C -> .d, $]}

Same core, so merge:
  I47: {[C -> .c C, c/d/$], [C -> .d, c/d/$]}

Characteristics of LALR Parsers

  • Same table size as SLR with the same number of states as canonical LR(1)
  • More powerful than SLR, slightly weaker than canonical LR(1)
  • Can handle most programming languages
  • Yacc and Bison generate LALR parsers

Relationship Between SLR, LR(1), and LALR

LR(0) grammars < SLR grammars < LALR grammars < LR(1) grammars

Practical choices:
- Simple grammars: SLR
- Most programming languages: LALR (Yacc/Bison)
- Complex grammars: Canonical LR(1)

8. Yacc Parser Generator

Yacc (Yet Another Compiler Compiler) is a tool that automatically generates LALR(1) parsers. The GNU version, Bison, is widely used.

Yacc Program Structure

Declarations
%%
Grammar Rules and Actions
%%
Supporting C Functions

Yacc Program Example: Calculator

%{
#include <stdio.h>
#include <ctype.h>
int yylex();
void yyerror(const char *s);
%}

%token NUM

%%

line   : expr '\n'          { printf("Result: %d\n", $1); }
       ;

expr   : expr '+' term      { $$ = $1 + $3; }
       | expr '-' term      { $$ = $1 - $3; }
       | term               { $$ = $1; }
       ;

term   : term '*' factor    { $$ = $1 * $3; }
       | term '/' factor    { $$ = $1 / $3; }
       | factor             { $$ = $1; }
       ;

factor : '(' expr ')'       { $$ = $2; }
       | NUM                 { $$ = $1; }
       ;

%%

int yylex() {
    int c = getchar();
    if (isdigit(c)) {
        yylval = c - '0';
        while (isdigit(c = getchar()))
            yylval = yylval * 10 + c - '0';
        ungetc(c, stdin);
        return NUM;
    }
    return c;
}

void yyerror(const char *s) {
    fprintf(stderr, "Error: %s\n", s);
}

int main() {
    return yyparse();
}

Yacc Conflict Resolution Rules

When conflicts exist in the LALR parsing table, Yacc applies the following rules:

  1. Shift-Reduce Conflict: Defaults to shift (resolves the dangling else problem).
  2. Reduce-Reduce Conflict: Reduces by the first listed rule.

Yacc Precedence Directives

%left '+' '-'       // Left-associative, lower precedence
%left '*' '/'       // Left-associative, higher precedence
%right UMINUS       // Right-associative, unary minus

Using these directives, ambiguous grammars can be handled without conflicts.


9. Error Recovery in LR Parsers

Panic Mode Recovery

1. Pop the stack until a state that can handle A is found
2. Skip input until a token in FOLLOW(A) is found
3. Push GOTO[s, A] and continue parsing

Yacc Error Recovery: error Token

stmt : expr ';'
     | error ';'   { yyerrok; printf("Skipping bad statement\n"); }
     ;

error is a special Yacc token that automatically matches in error situations. yyerrok declares that error recovery is complete.


Summary

ConceptKey Content
Bottom-Up ParsingStarts from input and reduces to start symbol
HandleReducible substring and production rule combination
Shift-ReduceStack-based 4 operations (shift, reduce, accept, error)
LR(0) ItemsDot represents parsing progress
SLRLR(0) items + FOLLOW sets
Canonical LR(1)Items with lookahead, most powerful
LALRMerges LR(1) states with same core, practical
Yacc/BisonAutomatic LALR(1) parser generation tool

LR parsing is a powerful technique that can efficiently handle most programming languages. In the next article, we will cover syntax-directed translation, which performs meaningful translation using parsing results.