Skip to content
Published on

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

Authors

중간 코드 생성 - 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)**이나 이진 탐색으로 최적화합니다.


정리

개념핵심 내용
DAGAST에서 공통 부분식을 공유하는 그래프
3주소 코드명령문당 연산자 1개, 주소 최대 3개
쿼드러플(op, arg1, arg2, result) 형식
트리플결과를 명령문 번호로 참조
SSA각 변수가 정확히 한 번만 정의되는 형식
타입 검사연산자와 피연산자의 타입 호환성 검증
단축 평가불리언 식의 부분 평가
백패칭분기 대상을 나중에 채워 넣는 기법

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