소스 프로그램에서 토큰(Token) 을 찾아내는 컴파일러의 첫 번째 단계.
핵심: Scanner는 토큰을 한꺼번에 다 찾는 게 아니라, Parser가 요청할 때마다 하나씩 찾아 전달한다.
소스 프로그램 ──→ [어휘 분석기] ──토큰──→ [구문 분석기] ──→ ...
↑ ↕ 다음 토큰 요구
[기호표]
- Parser가
getToken()호출 → Scanner가 토큰 1개 반환 - 반환값:
TokenType getToken(void);- Token의 type (ex:
ID,NUM,+...) - Token의 value (ex:
"a",42...)
- Token의 type (ex:
(ex: a[index] = 4 + 2 파싱 중 첫 호출 → return ID("a"))
| 기능 | 설명 |
|---|---|
| 토큰 전달 | Parser 요청 시 토큰을 찾아 반환 |
| 공백/주석 제거 | white space, comment 무시 |
| 기호표 구성 | symbol table에 식별자 등록 |
| 용어 | 영어 | 설명 | 예시 |
|---|---|---|---|
| 토큰 | Token | 문법적으로 의미있는 최소 단위 (terminal 기호로 구성) | if, 123, +, myVar |
| 패턴 | Pattern | 토큰의 특징을 표현하는 규칙 → 정규 표현(RE) 으로 표현 | letter(letter|digit)* |
| 렉심 | Lexeme | 패턴과 매칭되는 실제 문자열 | myVar, 123 |
비유: 패턴은 "식별자란 알파벳으로 시작하는 문자열"이라는 규칙이고, 렉심은 그 규칙을 만족하는 실제 소스코드 속 문자열 (
count,total등)이다.
정의: 현재 토큰 문자열이 어디까지인지 알려주는 경계 기호.
- Delimiter는 현재 토큰에 포함되지 않음
- (ex:
xtemp=ytemp→=이 구분기호,xtemp가 토큰) - (ex:
while x→ 공백(blank)이 구분기호) - (ex:
do/* */if→ 주석(comment)이 구분기호)
whitespace = ( newline | blank | tab | comment )+
문제 상황: Delimiter가 다음 토큰의 일부일 수 있다.
| 예 | delimiter | 다음 토큰 |
|---|---|---|
xtemp=ytemp |
= |
= 자체가 다음 토큰 |
x+=2; |
+ |
+=가 다음 토큰 |
해결: 읽은 문자를 버리지 말고 먼저 살펴보고(lookahead):
- 현재 토큰의 일부 → 지금 처리
- 아니면 (delimiter) → 입력 버퍼에 돌려놓고, 다음 토큰 찾을 때 사용
비유: 신호등에서 "노란불"을 보고 지금 건너야 하는지, 멈춰야 하는지 판단하는 것과 같다. 읽긴 했지만 아직 처리할지 결정 안 한 상태.
Regular Expression ←→ Finite Automata ←→ Regular Grammar
이 세 가지는 표현하는 언어가 동일하다. 서로 변환 가능.
- 어휘 분석기 설계 흐름:
토큰 패턴 → 정규 표현(RE) → NFA → DFA → 최소화된 DFA ↕ 정규 문법
무제약 문법 (type 0)
└─ 문맥인식 문법 (type 1) : CSG → CSL
└─ 문맥자유 문법 (type 2) : CFG → CFL
└─ 정규 문법 (type 3) : RG → RL
| type | 문법 | 언어 | 인식기 |
|---|---|---|---|
| 3 | 정규 문법 (RG) | 정규 언어 (RL) | 유한 오토마타 (FA) |
| 2 | 문맥자유 문법 (CFG) | 문맥자유 언어 (CFL) | 푸시다운 오토마타 |
| 1 | 문맥인식 문법 (CSG) | 문맥인식 언어 (CSL) | 선형 유계 오토마타 |
| 0 | 무제약 문법 | 재귀열거 가능 언어 | 튜링 기계 |
어휘분석은 type 3 (정규 문법/FA) 으로 처리. 구문분석은 type 2 (CFG/PDA).
| 언어 | 형태 | 분류 |
|---|---|---|
| 단순 매칭 | CFL (type 2) | |
| 중복 매칭 | CSL (type 1) | |
| 좌우 대칭 | CFL (type 2) | |
| 회문 | CFL (type 2) | |
| 괄호 | balanced parenthesis | CFL (type 2) |
$a^n b^n$ 이 CFL인 이유: a를 스택에 쌓고 b를 만날 때마다 pop하면 인식 가능 → 푸시다운 오토마타 필요 → FA로는 불가.
입력: FA
출력: 정규 문법
변환 규칙:
-
$V_N = Q$ (상태 → Nonterminal) -
$V_T = \Sigma$ (입력 알파벳 → Terminal) -
$S = q_0$ (시작 상태 → 시작 기호) - 전이 함수 → 생성 규칙:
-
$\delta(q, a) = r$ 이면 →$q \rightarrow ar$ -
$q \in F$ (종결 상태) 이면 →$q \rightarrow \varepsilon$
-
이렇게 만들어지는 문법은 Right-linear Grammar (우선형 문법):
$A \rightarrow aB$ 형태
FA 구조 (슬라이드 예):
start → A ---(a|b)--→ A
A ---a------→ B
B ---b------→ C
C ---b------→ D (D는 종결 상태)
정규 문법 변환:
A → aA | bA | aB
B → bC
C → bD
D → ε
정규 표현 변환:
D = ε
C = bD = b
B = bC = bb
A = aA + bA + aB
= (a+b)A + abb
→ A = (a+b)* abb [∵ X = αX + β → X = α*β]
∴ L(M) = (a+b)*abb
$\alpha$ 가$\varepsilon$ 을 포함하지 않을 때:$$X = \alpha X + \beta \Rightarrow X = \alpha^* \beta$$
증명: $$X = \alpha(\alpha^\beta) + \beta = \alpha^+\beta + \beta = (\alpha^+ + \varepsilon)\beta = \alpha^\beta$$
이 공식이 핵심이다. 정규 문법 → 정규 표현 변환의 거의 모든 문제가 이 꼴로 귀결된다.
| 법칙 | 식 |
|---|---|
| 결합법칙 | |
| 교환법칙 | |
| 분배법칙 | |
| 항등 |
|
| 클리니 | |
| 이중 클리니 | $(\alpha^)^ = \alpha^*$ |
기호 정리:
+= OR(선택),·= 연결(concatenate),*= 0회 이상 반복
S → aA | bS
A → aS | bB
B → aB | bB | ε
풀이:
B = (a+b)B + ε → B = (a+b)*
A = aS + b(a+b)*
S = aA + bS = a(aS + b(a+b)*) + bS
= aaS + ab(a+b)* + bS
= (aa+b)S + ab(a+b)*
→ S = (aa+b)* ab(a+b)*
∴ L(G) = (aa+b)*ab(a+b)*
S → aA | bS
A → aA | bA | b
풀이:
A = (a+b)A + b → A = (a+b)*b
S = a(a+b)*b + bS = bS + a(a+b)*b
→ S = b*a(a+b)*b
∴ L(G) = b*a(a+b)*b
G: A → aA | bA | aB, B → bC, C → bD, D → ε
풀이:
D = ε, C = b, B = bb
A = (a+b)A + abb
→ A = (a+b)*abb
∴ L(G) = (a+b)*abb
A → aB | bC | ε
B → bA (aD 전이는 도달 불가 → 제거)
C → aA (bD 전이는 도달 불가 → 제거)
풀이:
B = bA, C = aA
A = aB + bC + ε
= a(bA) + b(aA) + ε
= abA + baA + ε
= (ab+ba)A + ε
→ A = (ab+ba)*
∴ L(G) = (ab+ba)*
DFA:
start → S --d-→ [C] --d-→ (자기 루프)
정규 문법:
S → dC
C → dC | ε
정규 표현:
C = dC + ε → C = d*
S = dC = dd* = d+
∴ L(G) = d+ (한 자리 이상의 정수)
정규 문법 정의:
<ident> ::= (letter | _) { letter | digit | _ }
<letter> ::= a | b | ... | z | A | ... | Z
<digit> ::= 0 | 1 | ... | 9
(편의상 letter=l, digit=d로 축약)
DFA:
start → [1] --(l|_)-→ [2] --(l|d|_)-→ (자기 루프, 종결상태)
정규 문법:
S → lA | _A
A → lA | dA | _A | ε (종결 상태이므로 ε 추가)
정규 표현:
A = (l+d+_)A + ε → A = (l+d+_)*
S = (l+_)A = (l+_)(l+d+_)*
∴ L(G) = (l+_)(l+d+_)*
즉, 식별자는 알파벳 또는 언더스코어로 시작하고, 이후 알파벳/숫자/언더스코어가 0회 이상 반복. (
_count,myVar2등)
소스코드
↓
[어휘분석기 (Scanner)]
- 정규 표현(RE)으로 토큰 패턴 정의
- FA(유한 오토마타)로 인식
- getToken() 호출 시 토큰 1개 반환
↓
토큰 스트림 → [구문분석기 (Parser)]
설계 순서:
토큰 패턴 → 정규 문법 → 정규 표현 → NFA → DFA → 최소화 DFA → 코드
핵심 공식: