Rust로 구현하는 LLVM IR 코드 생성기: Kata-Kaleidoscope 프로젝트 Chapter 2 분석

Rust와 LLVM IR 코드 생성을 통한 컴파일러 개발 입문

컴파일러 개발의 핵심 중 하나는 원시 코드를 중간 표현(Intermediate Representation)으로 변환하는 과정이다.本章에서는 Rust 언어와 LLVM 라이브러리를 활용하여 AST(추상 구문 트)에서 LLVM IR을 생성하는 과정을详细히 다룬다. 예제 프로젝트인 Kata-Kaleidoscope를 통해 실제 코드 생성기의 구조와 구현 방식을 학습한다.

LLVM IR의 이해: 컴파일러의 핵심 다리

LLVM IR은 LLVM 컴파일러 프레임워크의 중심이 되는 중간 표현이다. 다음 세 가지 형태롤 존재한다:

  • 메모리 내 존재하는 컴파일러 IR
  • 바이트코드 형태의 디스크 저장소
  • 인간이 읽을 수 있는 어셈블리 형태

이러한统一된 중간 표현은 코드 최적화와 플랫폼 간 컴파일을 가능하게 한다. IR 생성 모듈은 Kata-Kaleidoscope 프로젝트의 chapters/2/src/builder.rs에 위치하며, AST에서 LLVM IR로의 변환을 담당한다.

개발 환경 구성: Rust와 LLVM 연동

프로젝트 설정은 Cargo.toml에서 다음과 같이 정의한다:

[dependencies]
llvm-sys = "*"

[dependencies.iron_llvm]
git = "https://github.com/jauhien/iron-llvm.git"

코드 생성 컨텍스트는 chapters/2/src/lib.rs에 정의되어 있으며, 네 가지 핵심 구성 요소를 포함한다:

  • Context: LLVM 전역 컨텍스트
  • Builder: IR 명령어 빌더
  • symbol_table: 함수 매개변수 매핑 테이블
  • ty: 기본 데이터 타입(본 장에서는 f64 사용)

아키텍처 설계: 모듈과 함수 생성

모듈 설계 패턴

Module은 LLVM IR의 컴파일 단위로, 함수와 전역 변수를 포함한다. 프로젝트에서는 ModuleProvider trait을 통해 모듈 작업을 추상화하여 JIT 컴파일러 통합을 단순화한다:

pub trait ModuleProvider {
    fn dump(&self);
    fn get_module(&mut self) -> &mut core::Module;
    fn get_function(&mut self, name: &str) -> Option<(FunctionRef, bool)>;
}

SimpleModuleProvider가 기본 모듈 관리 기능을 제공하며, 구현 코드는 chapters/2/src/builder.rs에 있다.

함수 코드 생성流程

함수 생성은 IR 구성의 핵심으로, 세 단계로 나뉜다:

  1. 프로토타입 생성: 함수 시그니처와 매개변수 정의
  2. 기본 블록 생성: 함수 진입 기본 블록 구성
  3. 명령어 생성: AST 노드를 LLVM 명령어로 변환

함수 생성 핵심 구현:

impl IRBuilder for parser::Function {
    fn codegen(&self, ctx: &mut Context, mp: &mut ModuleProvider) -> IRResult {
        ctx.symbol_table.clear();
        
        let (func, _) = try!(self.prototype.codegen(ctx, mp));
        let mut function = unsafe { FunctionRef::from_ref(func) };
        
        let entry_bb = function.append_basic_block_in_context(&mut ctx.context, "entry");
        ctx.builder.position_at_end(&mut entry_bb);
        
        // 매개변수 설정
        for (param, arg) in function.params_iter().zip(&self.prototype.args) {
            ctx.symbol_table.insert(arg.clone(), param.to_ref());
        }
        
        // 함수 본문 생성
        let body = match self.body.codegen(ctx, mp) {
            Ok((val, _)) => val,
            Err(msg) => {
                unsafe { LLVMDeleteFunction(function.to_ref()) };
                return Err(msg);
            }
        };
        
        ctx.builder.build_ret(&body);
        function.verify(LLVMAbortProcessAction);
        ctx.symbol_table.clear();
        
        Ok((function.to_ref(), self.prototype.name.as_str() == ""))
    }
}

표현식 생성: AST에서 LLVM 명령어로

표현식은 IR 생성의最小 단위이며, Kata-Kaleidoscope는 네 가지 기본 표현식 타입을 지원한다. 구현은 chapters/2/src/parser.rs에 있다.

리터럴 표현식

상수 값을 직접 생성한다:

&parser::LiteralExpr(ref value) => {
    Ok((RealConstRef::get(&ctx.ty, *value).to_ref(), false))
}

변수 표현식

심볼 테이블에서 변수 값을 조회한다:

&parser::VariableExpr(ref name) => {
    match ctx.symbol_table.get(name) {
        Some(val) => Ok((*val, false)),
        None => error("undeclared variable")
    }
}

이항 연산 표현식

산술 연산을 위한 명령어를 생성한다. 加法 연산 예시:

"+" => Ok((ctx.builder.build_fadd(lhs_val, rhs_val, "addtmp"), false))

함수 호출 표현식

함수 매개변수를 처리하고 호출 명령어를 생성한다:

&parser::CallExpr(ref name, ref args) => {
    let (callee, _) = match mp.get_function(name) {
        Some(func) => func,
        None => return error("undefined function")
    };
    
    let mut arg_values = Vec::new();
    for arg in args.iter() {
        let (arg_val, _) = try!(arg.codegen(ctx, mp));
        arg_values.push(arg_val);
    }
    
    Ok((ctx.builder.build_call(callee.to_ref(), arg_values.as_mut_slice(), "calltmp"), false))
}

구현 실무: 일반적인 문제와 해결책

타입 안전성 보장

Rust의 타입 시스템은 LLVM IR 생성에 추가적인 안전성을 제공한다. iron-llvm 라이브러리는 LLVM C API를 감싸서不安全한 연산을 최소한으로 제한하면서도 Rust 풍의 API를 제공한다.

오류 처리 전략

함수 생성 중에는 "失败即清理" 방식을 채택한다. 코드 생성 실패 시 不完整한 함수 정의를 즉시 삭제한다:

Err(msg) => {
    unsafe { LLVMDeleteFunction(function.to_ref()) };
    return Err(msg);
}

검증과 디버깅

IR 생성 후 function.verify(LLVMAbortProcessAction)를 통해 검증하여 생성된 코드가 LLVM 사양을 준수하는지 확인한다. dump() 메서드로 모듈 내용을 출력하여 디버깅할 수 있다:

fn dump(&self) {
    self.module.dump();
}

핵심 정리

Chapter 2는 완전한 IR 생성기를 구축하면서 컴파일러前端에서 중간 표현으로의 변환 과정을 보여준다. 주요 학습 내용은 다음과 같다:

  • LLVM IR의 설계 철학과 활용 시나리오
  • Rust와 LLVM 결합의 최적화된 방법
  • 모듈화된 코드 생성 아키텍처 설계
  • 표현식에서 LLVM 명령어로의 변환 기법

프로젝트 코드 구조는 명확하며, 각 컴포넌트는 명확한 역할을 갖는다. 이는 이후 장에서 최적화와 JIT 컴파일러를 구현하는 데 좋은 기반이 된다. chapters/2/src/ 디렉토리下的 코드를 통해 개발자들은 IR 생성의 각 세부를 심층적으로 이해하고 컴파일러 개발의 핵심 기술을 습득할 수 있다.

실습을 시작하려면 다음 명령어를 실행한다:

git clone https://gitcode.com/gh_mirrors/ir/iron-kaleidoscope
cd iron-kaleidoscope/chapters/2
cargo run

Kata-Kaleidoscope 프로젝트는 점진적인 방식으로 복잡한 LLVM 기술을 쉽게 이해하고 구현할 수 있도록 도와주며, 컴파일러 개발 학습에理想的 인 선택이다.

태그: llvm Rust compiler ir code-generation

6월 4일 23:06에 게시됨