Skip to content

필사 모드: [컴파일러] 09. 중간 코드 생성 - 3주소 코드와 타입 검사

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

중간 코드 생성 - 3주소 코드와 타입 검사

중간 코드 생성은 구문 트리를 기계 독립적인 **중간 표현(Intermediate Representation, IR)**으로 변환하는 단계입니다. 이 글에서는 주요 IR 형식과 타입 검사, 제어 흐름 번역을 다룹니다.

1. 구문 트리의 변형

추상 구문 트리(AST)

AST는 파스 트리에서 불필요한 노드를 제거한 간결한 표현입니다.

// a + a * (b - c) + (b - c) * d

AST:

+

/ \

+ *

/ \ / \

a * - d

/ \ / \

a - b c

/ \

b c

방향 비순환 그래프(DAG)

DAG는 AST에서 **공통 부분식(common subexpression)**을 공유합니다.

// a + a * (b - c) + (b - c) * d

DAG:

+

/ \

+ *

/ \ / \

a * | d

/ \|

a - <- (b-c)를 한 번만 표현

/ \

b c

DAG의 장점은 다음과 같습니다.

- **공통 부분식 자동 검출**: 같은 연산을 공유하여 중복 계산을 방지합니다.

- **메모리 절약**: 동일한 노드를 재사용합니다.

DAG 구성 알고리즘

Node createNode(String op, Node left, Node right) {

// 같은 연산과 자식을 가진 노드가 이미 있는지 확인

Node existing = lookup(op, left, right);

if (existing != null) {

return existing; // 기존 노드 재사용

}

Node node = new Node(op, left, right);

insert(op, left, right, node); // 해시 테이블에 등록

return node;

}

2. 3주소 코드(Three-Address Code)

3주소 코드는 가장 널리 사용되는 중간 표현 형식입니다. 각 명령문에는 최대 **하나의 연산자**와 **최대 세 개의 주소**(피연산자와 결과)가 있습니다.

3주소 코드의 형태

기본 형태: x = y op z

주요 명령문 종류:

x = y op z // 이항 연산

x = op y // 단항 연산

x = y // 복사

goto L // 무조건 분기

if x relop y goto L // 조건 분기

param x // 매개변수 전달

call p, n // 함수 호출 (p: 함수, n: 인자 수)

return y // 함수 반환

x = y[i] // 배열 접근 (인덱스)

x[i] = y // 배열 대입

x = &y // 주소

x = *y // 역참조

*x = y // 간접 대입

예제: 수식의 3주소 코드 변환

// 원본 코드

a = b * -c + b * -c

// 3주소 코드 (AST 기반)

t1 = minus c

t2 = b * t1

t3 = minus c

t4 = b * t3

t5 = t2 + t4

a = t5

// 3주소 코드 (DAG 기반 - 최적화)

t1 = minus c

t2 = b * t1

t5 = t2 + t2

a = t5

3. 3주소 코드의 구현 방식

쿼드러플(Quadruples)

4개의 필드로 구성됩니다: `(op, arg1, arg2, result)`

번호 | op | arg1 | arg2 | result

----+-------+------+------+-------

0 | minus | c | | t1

1 | * | b | t1 | t2

2 | minus | c | | t3

3 | * | b | t3 | t4

4 | + | t2 | t4 | t5

5 | = | t5 | | a

트리플(Triples)

result 필드를 제거하고, 명령문 번호로 결과를 참조합니다.

번호 | op | arg1 | arg2

----+-------+------+------

0 | minus | c |

1 | * | b | (0)

2 | minus | c |

3 | * | b | (2)

4 | + | (1) | (3)

5 | = | a | (4)

트리플은 메모리를 절약하지만, 명령문 이동 시 참조를 갱신해야 합니다.

간접 트리플(Indirect Triples)

트리플에 대한 포인터 리스트를 별도로 유지하여, 명령문 순서 변경을 용이하게 합니다.

순서 리스트: [0, 1, 2, 3, 4, 5]

명령문 재배치: [2, 0, 3, 1, 4, 5] // 원본 트리플 수정 불필요

4. 정적 단일 배정(SSA) 형식

**SSA(Static Single Assignment)** 형식에서는 각 변수가 **정확히 한 번만 정의**됩니다.

// 일반 3주소 코드

p = a + b

q = p - c

p = q * d // p가 두 번 정의됨

e = p + q

// SSA 형식

p1 = a + b

q1 = p1 - c

p2 = q1 * d // 새 이름 p2

e1 = p2 + q1

Phi 함수

제어 흐름이 합류하는 지점에서 **phi 함수**를 사용합니다.

// 원본 코드

if (flag) x = 1; else x = 2;

y = x;

// SSA 형식

if (flag) x1 = 1; else x2 = 2;

x3 = phi(x1, x2); // 어느 경로로 왔는지에 따라 선택

y1 = x3;

SSA의 장점

- **데이터 흐름 분석이 단순**해집니다: 각 변수의 정의가 유일합니다.

- **최적화가 용이**합니다: 상수 전파, 죽은 코드 제거 등이 직관적입니다.

- **현대 컴파일러(LLVM, GCC)**에서 핵심 IR로 사용됩니다.

5. 타입 표현식(Type Expression)

타입 시스템의 타입을 표현하는 방법입니다.

기본 타입

boolean, char, integer, float, void

타입 생성자

1. 배열: array(크기, 원소타입)

int a[10] -> array(10, integer)

2. 레코드/구조체: record(필드목록)

struct { int x; float y; }

-> record((x, integer), (y, float))

3. 포인터: pointer(타입)

int *p -> pointer(integer)

4. 함수: 인자타입 -> 반환타입

int f(float, char) -> float x char -> integer

타입 동치(Type Equivalence)

두 가지 동치 기준이 있습니다.

구조적 동치 (Structural Equivalence):

- 두 타입의 구조가 같으면 동치

- C 언어의 기본 접근법

이름 동치 (Name Equivalence):

- 같은 이름일 때만 동치

- Pascal, Ada의 접근법

예:

type A = array(10, integer)

type B = array(10, integer)

구조적 동치: A == B (구조가 같으므로)

이름 동치: A != B (이름이 다르므로)

6. 타입 검사(Type Checking)

타입 검사 규칙의 SDD

E -> num { E.type = integer }

E -> num . num { E.type = float }

E -> id { E.type = lookup(id.entry) }

E -> E1 + E2 { E.type = max(E1.type, E2.type)

if compatible(E1.type, E2.type)

else type_error }

E -> E1 [ E2 ] { E.type = if E1.type == array(s, t)

and E2.type == integer

then t

else type_error }

타입 변환(Type Coercion)

타입이 다른 피연산자를 자동으로 변환합니다.

타입 변환 계층 (자동 확대 변환):

char -> int -> long -> float -> double

예: int + float -> float + float -> float

변환 규칙 (widening):

E -> E1 + E2

if E1.type == integer and E2.type == integer:

E.type = integer

else if E1.type == float and E2.type == integer:

t = new Temp()

emit(t "=" "intToFloat" E2.addr)

E.type = float

else if E1.type == integer and E2.type == float:

t = new Temp()

emit(t "=" "intToFloat" E1.addr)

E.type = float

7. 제어 흐름의 번역

If-Else 문의 번역

// 소스 코드

if (x < 100) {

x = x + 1;

} else {

x = x - 1;

}

// 3주소 코드

if x < 100 goto L1

goto L2

L1: t1 = x + 1

x = t1

goto L3

L2: t2 = x - 1

x = t2

L3: ...

While 문의 번역

// 소스 코드

while (x < 100) {

x = x * 2;

}

// 3주소 코드

L1: if x < 100 goto L2

goto L3

L2: t1 = x * 2

x = t1

goto L1

L3: ...

8. 불리언 표현식의 번역

단축 평가(Short-Circuit Evaluation)

대부분의 언어에서 불리언 표현식은 **단축 평가**를 사용합니다.

// a < b || c < d && e < f

번역:

if a < b goto Ltrue

if c < d goto L1

goto Lfalse

L1: if e < f goto Ltrue

goto Lfalse

`a < b`가 참이면 나머지를 평가하지 않습니다.

불리언 표현식의 SDD

B -> B1 || B2

B1.true = B.true

B1.false = newLabel()

B2.true = B.true

B2.false = B.false

B.code = B1.code || label(B1.false) || B2.code

B -> B1 && B2

B1.true = newLabel()

B1.false = B.false

B2.true = B.true

B2.false = B.false

B.code = B1.code || label(B1.true) || B2.code

B -> ! B1

B1.true = B.false

B1.false = B.true

B.code = B1.code

B -> E1 relop E2

B.code = gen("if" E1.addr relop E2.addr "goto" B.true)

|| gen("goto" B.false)

B -> true

B.code = gen("goto" B.true)

B -> false

B.code = gen("goto" B.false)

9. 백패칭(Backpatching)

**백패칭**은 분기 대상 레이블이 아직 결정되지 않았을 때, 나중에 채워 넣는 기법입니다.

백패칭의 핵심 아이디어

1. 분기 명령을 생성할 때 대상 주소를 비워 둠

2. 대상이 결정되면 비워둔 주소를 채워 넣음 (backpatch)

예:

100: if x < y goto ___ <- 대상 미정

101: goto ___ <- 대상 미정

...

105: x = x + 1 <- 여기가 대상임을 알게 됨

백패칭 후:

100: if x < y goto 105 <- 채워 넣음

101: goto ___ <- 아직 미정

백패칭에 사용되는 함수

makelist(i): 명령문 번호 i만을 포함하는 리스트 생성

merge(p1, p2): 두 리스트 p1, p2를 합침

backpatch(p, i): 리스트 p의 모든 명령문의 대상 주소를 i로 설정

백패칭을 사용한 불리언 표현식 번역

B -> B1 || M B2

backpatch(B1.falselist, M.quad)

B.truelist = merge(B1.truelist, B2.truelist)

B.falselist = B2.falselist

B -> B1 && M B2

backpatch(B1.truelist, M.quad)

B.truelist = B2.truelist

B.falselist = merge(B1.falselist, B2.falselist)

B -> ! B1

B.truelist = B1.falselist

B.falselist = B1.truelist

M -> epsilon

M.quad = nextquad (현재 명령문 번호 기록)

백패칭 예제

// a < b || c < d && e < f

생성 과정:

100: if a < b goto ___ (B1.truelist = {100})

101: goto ___ (B1.falselist = {101})

102: if c < d goto ___ (B2.truelist = {102})

103: goto ___ (B2.falselist = {103})

104: if e < f goto ___ (B3.truelist = {104})

105: goto ___ (B3.falselist = {105})

백패칭 결과:

100: if a < b goto Ltrue (truelist)

101: goto 102 (B1.false -> B2 시작)

102: if c < d goto 104 (B2.true -> B3 시작)

103: goto Lfalse (falselist)

104: if e < f goto Ltrue (truelist)

105: goto Lfalse (falselist)

최종 truelist = {100, 104}

최종 falselist = {103, 105}

10. Switch 문의 번역

// 소스 코드

switch (x) {

case 1: stmt1; break;

case 5: stmt2; break;

default: stmt3;

}

// 3주소 코드

t = x

if t == 1 goto L1

if t == 5 goto L2

goto L3 // default

L1: (stmt1 코드)

goto Lnext

L2: (stmt2 코드)

goto Lnext

L3: (stmt3 코드)

goto Lnext

Lnext: ...

case가 많으면 **점프 테이블(jump table)**이나 **이진 탐색**으로 최적화합니다.

정리

| 개념 | 핵심 내용 |

| ---------- | -------------------------------------- |

| DAG | AST에서 공통 부분식을 공유하는 그래프 |

| 3주소 코드 | 명령문당 연산자 1개, 주소 최대 3개 |

| 쿼드러플 | (op, arg1, arg2, result) 형식 |

| 트리플 | 결과를 명령문 번호로 참조 |

| SSA | 각 변수가 정확히 한 번만 정의되는 형식 |

| 타입 검사 | 연산자와 피연산자의 타입 호환성 검증 |

| 단축 평가 | 불리언 식의 부분 평가 |

| 백패칭 | 분기 대상을 나중에 채워 넣는 기법 |

중간 코드 생성은 소스 언어의 의미를 기계 독립적인 형태로 표현하는 핵심 단계입니다. 다음 글에서는 프로그램이 실행되는 환경인 실행 시간 환경을 다루겠습니다.

현재 단락 (1/293)

중간 코드 생성은 구문 트리를 기계 독립적인 **중간 표현(Intermediate Representation, IR)**으로 변환하는 단계입니다. 이 글에서는 주요 IR 형식과 타...

작성 글자: 0원문 글자: 5,834작성 단락: 0/293