Parsing in Practice — 실전 파싱
Douglas Thain, Introduction to Compilers and Language Design (2nd ed., 2023) · pp.69–84
📌단원 개요
앞에서 배운 LL(1)·LR(1) grammar 이론을 실제로 적용해, 간단한 expression language용 동작하는 parser를 만든다. 이 장이 다음 장의 B-Minor parser 작성의 토대가 된다.
- LL(1) parser → 보통 손으로(by hand) 작성한다.
- LR(1) parser → 너무 복잡(cumbersome)해서 손으로 못 만든다 → parser generator로 자동 생성한다.
- 이 장에서는 C 계열용 parser generator인 Bison으로 LALR grammar(algebraic expression)를 정의하고 활용한다.
Bison으로 만드는 3가지 프로그램 유형
| 유형 | 하는 일 | 핵심 |
|---|---|---|
| Validator | 입력이 grammar에 맞는 valid sentence인지 여부만 알려줌 | 표준 준수(standards compliant) 여부 확인용 |
| Interpreter | 프로그램을 실제로 실행해 결과 산출 | ① 파싱하며 즉시 계산 / ② AST로 만든 뒤 실행 |
| Translator | 프로그램을 AST로 파싱 후, 순회하며 다른 형식의 동등한 프로그램 생성 | 형식 변환 |
5.1The Bison Parser Generator
LALR parser는 손으로 구현하기 비현실적이므로, grammar 명세로부터 table과 code를 자동 생성하는 도구를 쓴다.
🛠 도구의 계보
- YACC (Yet Another Compiler Compiler) — Unix의 대표 parser generator
- GNU Bison — YACC를 대체, 대체로 호환(compatible)
- Bison은 필요시 Flex(scanner)를 자동 호출 → 둘을 쉽게 결합
📄 Bison 파일 구조 (Flex와 유사)
%{
(C preamble code)
%}
(declarations)
%%
(grammar rules)
%%
(C postamble code)
- 1st (preamble): 임의의 C code — 주로
#include, 전역 선언 - 2nd (declarations): Bison 전용 선언 —
%token키워드로 모든 terminal 선언 - 3rd (rules):
non-terminal : ...rule... ;형태의 규칙들
문법 규칙(rule)의 형태
expr : expr TOKEN_ADD expr
| TOKEN_INT
;
non-terminal expr는 expr TOKEN_ADD expr 또는 단일 terminal TOKEN_INT를 생성할 수 있다.
- C에서 대문자는 관습적으로 상수(constant)에 쓰임 → 그래서 terminal = 대문자 (예:
TOKEN_INT) - non-terminal = 소문자 (예:
expr,term,factor) - White space(공백)는 의미 없음 → 가독성 위해 자유롭게 배치 가능
생성되는 함수: yyparse() · yylex()
| 요소 | 설명 |
|---|---|
yyparse() | parser의 핵심 함수. 정수 반환 |
| 반환값 0 | parse 성공 (successful) |
| 반환값 1 | parse error |
| 반환값 2 | 내부 문제 (예: memory exhaustion) |
yylex() | yyparse가 호출하는 함수. 정수 token type 반환. 손으로 쓰거나 Flex로 자동 생성 |
yyin | file pointer. 입력 소스를 바꿀 때 수정 |
예시 grammar (Figure 5.1 — Validator용)
parser.bison
%token TOKEN_INT TOKEN_PLUS TOKEN_MINUS TOKEN_MUL TOKEN_DIV
%token TOKEN_LPAREN TOKEN_RPAREN TOKEN_SEMI TOKEN_ERROR
%%
program : expr TOKEN_SEMI ;
expr : expr TOKEN_PLUS term
| expr TOKEN_MINUS term
| term ;
term : term TOKEN_MUL factor
| term TOKEN_DIV factor
| factor ;
factor: TOKEN_MINUS factor
| TOKEN_LPAREN expr TOKEN_RPAREN
| TOKEN_INT ;
%%
- operator precedence를 grammar 구조로 표현:
+ -<* /<parentheses · unary minus · INT - Bison은 LR(1) grammar를 받으므로 규칙 안의 left recursion이 허용된다. (예:
expr : expr TOKEN_PLUS term)
빌드 절차 (Figure 5.3)
bison --defines=token.h --output=parser.c parser.bison
--defines=token.h:%token선언으로부터 token.h 자동 생성 → parser와 scanner가 같은 정보 공유--output=parser.c: 알아보기 힘든 기본 파일명yy.tab.c대신parser.c로 출력
bison -v→ LALR automaton을parser.output파일로 출력. 각 state의 item을 dot(.)으로 parser 위치 표시- grammar가 ambiguous해지면(예:
expr : expr TOKEN_PLUS expr) → shift-reduce conflict 발생 - conflict 시 Bison은 일부 action을 억제(suppress)하고
[대괄호]로 표시 → 그래도 동작하는 코드를 그냥 출력한다! - 단순 입력엔 맞는 듯 보여도 전체 프로그램에선 예기치 못한 결과 → 진행 전 항상 conflict 검사
state 9
2 expr: expr . TOKEN_PLUS expr
2 | expr TOKEN_PLUS expr .
TOKEN_PLUS shift, and go to state 7
TOKEN_PLUS [reduce using rule 2 (expr)] ← 억제됨
$default reduce using rule 2 (expr)
5.2Expression Validator
Figure 5.1의 명세는 입력이 원하는 grammar와 일치하는지만 평가한다 → 이런 프로그램이 validator.
main.c (Figure 5.2)
extern int yyparse();
int main() {
if(yyparse()==0) printf("Parse successful!\n");
else printf("Parse failed.\n");
}
yyparse()가 0이면 성공, 그 외는 실패만 판정 (실행·계산은 안 함)- 실제 활용 예: HTML(validator.w3.org), CSS(css-validator.org), JSON(jsonlint.com)
- 장점: 구현과 분리된 엄격한 language definition → standards compliance = portability 판단이 쉬움
5.3Expression Interpreter
단순 검증을 넘어서려면 grammar 안에 semantic action을 심어야 한다.
- production rule 우변 뒤 중괄호
{ }안에 임의의 C code 삽입 = semantic action - $기호 = semantic value (이미 계산된 non-terminal의 값)
$1, $2, $3 ...= 우변에서 해당 위치 심볼의 값$$= 현재 rule(좌변)의 semantic value
| 규칙 | 의미 |
|---|---|
expr : expr TOKEN_PLUS term { $$ = $1 + $3; } | 좌측값($1) + 우측값($3) → 덧셈 |
factor : TOKEN_INT { $$ = atoi(yytext); } | 정수 token의 값 = token 텍스트를 정수로 변환 |
term : factor { $$ = $1; } | non-terminal이 단일 non-terminal로 확장 → 값 그대로 전달 |
yytext 배열(scanner의 token 텍스트)을 쓰는 것은, 규칙 우변에 terminal이 단 하나일 때만 가능하다.
Bison은 bottom-up parser이므로 parse tree의 leaf node 값을 먼저 결정하고, 그 값을 위(interior node)로 전달하며, 최종적으로 start symbol에 결과가 도달한다.
Figure 5.4 = 완전한 interpreter. main이 yyparse()를 호출하고, 성공 시 결과가 전역변수 parser_result에 저장된다.
Figure 5.4 — Interpreter
prog : expr TOKEN_SEMI { parser_result = $1; } ;
expr : expr TOKEN_PLUS term { $$ = $1 + $3; }
| expr TOKEN_MINUS term { $$ = $1 - $3; }
| term { $$ = $1; } ;
term : term TOKEN_MUL factor { $$ = $1 * $3; }
| term TOKEN_DIV factor { $$ = $1 / $3; }
| factor { $$ = $1; } ;
factor : TOKEN_MINUS factor { $$ = -$2; }
| TOKEN_LPAREN expr TOKEN_RPAREN { $$ = $2; }
| TOKEN_INT { $$ = atoi(yytext); } ;
5.4Expression Trees (AST)
파싱 도중 즉시 계산하는 방식의 한계를 해결하기 위해, 값을 바로 계산하지 않고 abstract syntax tree(AST) 자료구조를 만든다.
- 즉시 계산은 많은 연산을 한 뒤에야 뒤늦게 parse error를 발견할 수 있음
- 일반적으로 실행 전에 모든 parse error를 먼저 찾는 것이 바람직
- AST를 만들어 두면 → 트리를 순회하며 검사(check)·실행(execute)·번역(translate)을 필요에 따라 수행 가능
AST 자료구조 (Figure 5.5)
expr.h
typedef enum {
EXPR_ADD,
EXPR_SUBTRACT,
EXPR_DIVIDE,
EXPR_MULTIPLY,
EXPR_VALUE
} expr_t;
struct expr {
expr_t kind;
struct expr *left;
struct expr *right;
int value; // leaf용
};
expr.c — 생성 함수
// 모든 종류의 노드 생성
struct expr * expr_create(
expr_t kind,
struct expr *left,
struct expr *right);
// 특별히 EXPR_VALUE leaf 노드 생성
struct expr * expr_create_value(int value);
노드 종류를 type이 아니라 kind라고 부른다. type은 나중에(semantic analysis 단계) 아주 구체적인 의미로 따로 쓰이기 때문이다.
AST 손으로 만들기 — 예: (10+20)*30
struct expr *e =
expr_create(EXPR_MULTIPLY,
expr_create(EXPR_ADD,
expr_create_value(10),
expr_create_value(20)),
expr_create_value(30));
Bison으로 AST 자동 생성 (Figure 5.6)
각 element가 인식될 때마다 새 노드를 만들고, 위로 전달(pass up)해 알맞은 자리에 연결한다. bottom-up이므로 leaf 먼저 생성 후 parent에 연결.
%{
#include "expr.h"
#define YYSTYPE struct expr * // ★ semantic type 지정
struct expr * parser_result = 0;
%}
prog : expr TOKEN_SEMI { parser_result = $1; } ;
expr : expr TOKEN_PLUS term { $$ = expr_create(EXPR_ADD,$1,$3); }
| expr TOKEN_MINUS term { $$ = expr_create(EXPR_SUBTRACT,$1,$3); }
| term { $$ = $1; } ;
term : term TOKEN_MUL factor { $$ = expr_create(EXPR_MULTIPLY,$1,$3); }
| term TOKEN_DIV factor { $$ = expr_create(EXPR_DIVIDE,$1,$3); }
| factor { $$ = $1; } ;
factor : TOKEN_MINUS factor
{ $$ = expr_create(EXPR_SUBTRACT, expr_create_value(0), $2); }
| TOKEN_LPAREN expr TOKEN_RPAREN { $$ = $2; }
| TOKEN_INT { $$ = expr_create_value(atoi(yytext)); } ;
- YYSTYPE 매크로를
struct expr *로 정의해야 함. 그래야$$,$1등 모든 semantic value의 내부 type이struct expr *가 됨. (최종 parser_result도 같은 type) - AST ≠ parse tree (항상 직접 대응하지 않음).
expr → factor처럼 단순 통과는 포인터만 전달($$=$1). - Unary minus는
0 - expr형태의 subtree로 구현 → 왼쪽VALUE:0, 오른쪽이 실제 expr인 subtraction.
-20 의 AST
괄호는 AST에 직접 표현되지 않는다. 대신 노드의 순서를 정해 원하는 평가 순서(evaluation order)를 만든다.
(5+6)/7
5+(6/7)
5.4+AST 순회: 평가 & 출력
AST를 만든 뒤에는 이를 계산·출력 등 여러 연산의 기반으로 쓴다.
expr_evaluate() — Figure 5.7
Post-order traversal: 왼쪽·오른쪽 자식을 먼저 재귀 평가 후 현재 노드 계산.
int expr_evaluate(struct expr *e){
if(!e) return 0; // ★ null 체크
int l = expr_evaluate(e->left);
int r = expr_evaluate(e->right);
switch(e->kind){
case EXPR_VALUE: return e->value;
case EXPR_ADD: return l+r;
case EXPR_SUBTRACT: return l-r;
case EXPR_MULTIPLY: return l*r;
case EXPR_DIVIDE:
if(r==0){ printf("error: divide by zero\n");
exit(1); } // ★ divide-by-zero 명시 체크
return l/r;
}
return 0;
}
expr_print() — Figure 5.8
In-order traversal: 왼쪽 출력 → 현재 노드 → 오른쪽 출력. 텍스트로 역변환.
void expr_print(struct expr *e){
if(!e) return; // ★ null 체크
printf("(");
expr_print(e->left);
switch(e->kind){
case EXPR_VALUE: printf("%d",e->value); break;
case EXPR_ADD: printf("+"); break;
case EXPR_SUBTRACT: printf("-"); break;
case EXPR_MULTIPLY: printf("*"); break;
case EXPR_DIVIDE: printf("/"); break;
}
expr_print(e->right);
printf(")");
}
| 함수 | 순회 방식 | 순서 |
|---|---|---|
expr_evaluate | post-order | left → right → node |
expr_print | in-order | left → node → right |
- 두 함수 모두 시작 시 null 포인터 검사 필수.
evaluate는 divide-by-zero를 명시적으로 검사(안 하면 r=0일 때 crash).print는 보수적으로 모든 값에 괄호를 둘러 출력 → 괄호가 너무 많아짐. 더 나은 방법: subtree가 더 낮은 우선순위 연산자를 포함할 때만 괄호 출력.
5.5Exercises (연습문제)
- Bison manual을 참고해 grammar로부터 LALR automaton 그래프를 자동 생성하고, 손으로 그린 것과 비교하라.
expr_evaluate()(외 필요한 부분)를 정수 대신 floating point 값을 다루도록 수정하라.expr_print()가 정확성에 필요한 최소한의 괄호만 표시하도록 수정하라.- parser와 interpreter를 확장해
sin(x),sqrt(x)등 내장 수학 함수 호출을 허용하라. (함수 이름을 keyword로? 아니면 identifier로 두고 evaluate에서 검사?) - parser/interpreter를 확장해 변수 할당·사용을 허용하라. 예:
g=9.8; t=5; g*t*t - 7*t + 10;
5.6Further Reading
YACC는 최초의 컴파일러 도구는 아니지만 지금도 널리 쓰이며, 다양한 언어·문법 클래스를 다루는 유사 도구들을 파생시켰다.
| 도구 / 논문 | 특징 |
|---|---|
| YACC (S.C. Johnson, 1975) | Yet Another Compiler-Compiler |
| LL(1) generator (Grune & Jacobs) | programmer-friendly LL(1) parser generator |
| ANTLR (Parr & Quong, 1995) | predicated LL(k) parser generator |
| Elkhound (McPeak & Necula, 2004) | Fast, Practical GLR parser generator |
⭐핵심 정리 / 시험 포인트
- LL(1)은 손으로, LR(1)/LALR은 generator(Bison)로 만든다.
- Bison은 LR(1) grammar를 받음 → left recursion 허용.
- Bison은 bottom-up parser → leaf 값 먼저 → 위로 전달 → start symbol에 결과 도달.
- terminal = 대문자, non-terminal = 소문자 (C 관습 때문에 거꾸로).
yyparse(): 0=성공, 1=parse error, 2=내부 문제(memory).- Validator / Interpreter / Translator 3가지 프로그램 유형 구분.
- AST는 parse tree와 다르다. parentheses는 AST에 표현되지 않고 순서로만 반영. unary minus =
0 - xsubtree. YYSTYPE매크로로 semantic value의 type 지정.- 순회:
evaluate=post-order,print=in-order. - Shift-reduce / reduce-reduce conflict는 반드시 확인(Bison은 conflict를 억제하고 동작하는 code를 그냥 출력!).
$$ | 현재 rule(좌변)의 값 |
$1, $3 … | 우변 n번째 심볼의 값 |
yytext | scanner의 token 텍스트(단일 terminal일 때만) |
YYSTYPE | semantic value의 type 정의 |
expr→+ , -(가장 낮음)term→* , /factor→ unary-, parentheses( ),INT(가장 높음)
- Q. 즉시 계산 방식의 단점과 AST 도입 이유는? → 실행 후에야 늦게 parse error 발견 / 실행 전 모든 error 검출, 검사·실행·번역 재사용.
- Q. 왜 LR parser는 손으로 안 만드나? → 너무 cumbersome(복잡) → table을 generator가 자동 생성.
- Q.
-20의 AST를 그려라 →SUB(VALUE:0, VALUE:20). - Q. Bison에서 token.h는 어떻게 만들어지나? →
bison --defines=token.h로%token선언에서 자동 생성(scanner와 공유).
📋한눈에 보기 — Cheat Sheet
| 키워드 / 명령 | 의미 |
|---|---|
| Bison / YACC | LR/LALR parser generator (C 계열) |
| Flex | scanner generator. Bison이 자동 호출 |
| %token | 모든 terminal 선언 (declarations 섹션) |
| %{ … %} | C preamble (include, 전역 선언) |
| %% … %% | grammar rules 구역 구분자 |
| yyparse() | parser 본체. 0=성공 / 1=error / 2=내부문제 |
| yylex() | token type 반환. Flex가 생성 |
| yyin / yytext | 입력 file pointer / 현재 token 텍스트 |
| $$ / $1,$3 | 좌변 값 / 우변 n번째 심볼 값 |
| YYSTYPE | semantic value의 type 매크로 |
| expr / term / factor | +− / ×÷ / unary − · parentheses · INT (precedence 계층) |
| AST | abstract syntax tree. 검사·실행·번역 기반 |
| expr_create / _value | 일반 노드 / VALUE leaf 노드 생성 |
| kind (≠ type) | 노드 종류 enum (EXPR_ADD 등) |
| expr_evaluate | post-order 평가, divide-by-zero 체크 |
| expr_print | in-order 출력(모든 값에 괄호) |
| shift-reduce conflict | ambiguous grammar 신호. 반드시 확인 |
| bison -v | parser.output에 LALR automaton·state 출력(dot 표기) |
| --defines=token.h | %token에서 token.h 자동 생성 |