int $0x80

3. Kaleidoscope: Code generation to LLVM IR 한글 번역 본문

컴퓨터공부/엘엘비엠

3. Kaleidoscope: Code generation to LLVM IR 한글 번역

cd80 cd80 2017.07.24 16:47

3.1. Chapter 3 Introduction

3장에서는 2장에서 만든 AST를 LLVM IR로 변환하는 법과, LLVM 프레임워크가 얼마나 사용하기 편한지 설명합니다. 이번장에서 하는게 Lexer나 Parser를 구현하는 것보다 할게 많이 없습니다


주의: 이번장부터는 LLVM API를 사용합니다. 반드시 튜토리얼과 같은 LLVM버젼을 사용해주세요. 저는 LLVM 4.0.0버젼을 사용하고 있습니다



3.2. Code Generation Setup

먼저 Parser.h를 열어 모든 AST클래스에 codegen() 메소드를 추가해줍니다


codegen() 함수는 각 AST노드에 해당하는 LLVM IR을 생성해줍니다

LLVM IR이 익숙치 않으신 분들은 LLVM IR자체에 대해서 한번 공부해오시면 이해하는데 훨씬 수월합니다

그리고 PrototypeAST 클래스에 getName함수를 추가했습니다.


사실 이렇게 함수들을 다 선언해서 해주는것말고 Visitor를 작성해 처리하는것도 좋겠지만, 이 튜토리얼은 좋은 알고리즘 구조를 갖추는것에 초점을 맞추는것이 아니고, 우리 언어에 있어서는 단순히 codegen함수를 모든 클래스에 추가해주는게 가장 간단합니다


두번쨰로, Parser에서도 만들었던것처럼 Code generator에서 사용할 함수를 정의합니다

CodeGen.cpp 를 엽니다


헤더파일은 이 파일 내에서 사용할 모든 헤더파일을 인클루드 했습니다.

우선 LogErrorV를 그냥 LogError의 래퍼로 구현했고

TheContext, Builder, TheModule, NamedValues 변수들은 코드제네레이션에서 계속 사용될 변수들입니다


TheContext: LLVM프레임워크에서 사용할 여러 타입이나 데이터들을 저장하고 있습니다. 정확하게 이해할 필요는 없고 그냥 LLVMContext를 인자로 받는 함수들에 전달해주면 됩니다


Builder: LLVM IR을 생성할때 사용할 인스턴스입니다. 일종의 엔진이라고 생각하면 됩니다


TheModule: 함수, 전역변수등을 저장하는 구조입니다. 코드제네레이션을 할 때 이 모듈에 LLVM IR을 저장합니다. 모든 IR에 대한 실질적 소유권을 갖고 있고, 이것때문에 codegen함수를 선언할 때 unique_ptr<Value *>가 아닌 Value *로 선언한것입니다


NamedValues: 컴파일러가 사용할 일종의 심볼테이블입니다. Kaleidoscope에서는 함수 코드를 생성할 때 그 함수의 인자들을 여기에 저장해둡니다




그리고 이건 튜토리얼 초반에 좀 누락돼있는 내용인데, 몇가지 추가 설정을 더하겠습니다

우선 Parser.h에 std::unique_ptr<ExprAST> LogError(const char *Str); 함수 선언을 추가해줍니다

CodeGen에서 include한 llvm 헤더들과, using namespace llvm; 도 추가해주세요


중요: 직접 해본 결과 시스템에 있는 기본 헤더들중에 우리가 필요한게 누락된게 많습니다

LLVM을 직접 4.0.0 버젼으로 다운받아 빌드하시고, 소스코드에 있는 include디렉토리와, 빌드디렉토리에 있는 인클루드 디렉토리를 컴파일플래그에 직접 명시하셔야합니다

예를들어 LogErrorV까지 작성 했고 이걸 컴파일하려면

이렇게 일단 llvm안에 있는 include, 그다음에 build디렉토리 안에 있는 include를 둘다 설정해 줘야합니다.



3.3. Expression Code Generation

표현식 노드들의 code generation은 아주 직관적입니다. 

우선 숫자노드부터 처리합니다

Kaleidoscope에서의 숫자는 LLVM IR에서 ConstantFP로 처리되고, ConstantFP는 내부적으로 값을 APFloat형태로 저장합니다(Arbitrary Precision Float)


VariableExprAST::codegen에서는 함수 인자를 리턴해줍니다

참고로 아직은 함수안에서 변수를 선언해줄 수 없고, 지금까지의 변수는 함수의 인자만을 뜻합니다

그래서 NamedValues는 함수의 codegen()에서 초기화됩니다


VariableExprAST::codegen이 하는 일은, AST상에서 변수참조 노드가 있을 때 호출돼서 그 변수참조가 유효한 변수참조인지 확인하는 일입니다

C언어로 예를들면

void func(int x, int y){ return x+y+z; }

일 때 x, y는 valid하지만 z는 Unknown variable name이 뜨죠


사용자 정의 변수는 6장에서 추가합니다


binary expression codegen입니다

여기서는 LHS->codegen(); 과 RHS->codegen(); 을 호출해 재귀적으로 llvm ir을 생성합니다

그리고 < 을 처리할때는 cmp문의 결과로 0이나 1이 나올텐데, 그걸 0.0, 1.0으로 바꿔주는 작업을 한번 거치고 리턴합니다

그 외에 나머지는 이해하기 쉽습니다


위에 두개(NumberExprAST, VariableExprAST)에서는 값만을 처리했지만 이번에는 Builder를 이용해 LLVM 인스트럭션을 삽입했습니다. Builder가 명령을 삽입할 포인트는 명령 삽입전에 setInsertPoint 함수를 이용해 정할 수 있습니다. 뒤에서 나옵니다


그리고 Builder.CreateFAdd(L, R, "addtmp") 여기서 "addtmp"처럼 명령어의 이름을 지정할 수 있는데, 이 이름이 겹칠 경우 엔진에서 자동으로 addtmp1, addtmp2 이렇게 suffix를 붙여 충돌을 없애줍니다

이 이름은 안정해도 상관은 없지만 개발할때는 정해두는게 나중에 IR을 덤프해서 분석할 때 편합니다



이 메소드가 호출되기 전에 TheModule에서 함수들을 모두 처리해둘것입니다

그 때 CallExprAST 인스턴스에 저장된 Args의 갯수와 LLVM엔진에서 처리한 CalleeF의 인자 갯수가 같은지 확인하고

인자들을 각각 codegen()해 인자 벡터를 생성하고 Builder로 call inst를 생성합니다


좀더 LLVM을 더 잘 알고 싶으시다면 LLVM 문서를 읽어보시면 LLVM으로 어떤것들이 또 가능한지 보실 수 있습니다



3.4. Function Code Generation


Function은 LLVM IR에서 basic block, instruction들에 대한 wrapper역할을 하기 때문에 위에서 했던것들보다는 조금 더 많은 처리가 필요합니다

먼저 Prototype부터 처리하겠습니다.

Parser에서 했던것처럼 Prototype 처리 루틴을 Function에서도 호출해서 사용합니다


Prototype과 Function은 codegen 리턴 타입이 Function * 입니다. 다른 codegen들은 Value였죠

사실 대부분 Value를 상속받기때문에 크게 상관은없지만 Function을 리턴하도록 하는게 더 말이 맞아서 그렇게 했습니다


코드는 일단 처음이 함수 인자들의 타입을 만들어준거고,

두번째줄에서 그 인자타입과, 함수리턴타입을 이용해 함수 자체의 타입을 생성해줍니다

그리고 그 함수의 타입으로 LLVM Function을 생성합니다


Kaleidoscope에서는 함수 리턴타입, 인자 타입 모두가 double이기 때문에 그냥 모두 더블로 초기화했습니다


그다음에 LLVM 함수 인자들에, 코드에서 파싱한 인자들의 이름을 넣어줍니다.

NamedValues는 FunctionAST::codegen에서 사용합니다

여기까지만해도 함수는 생성됩니다. 그리고 이렇게 구현체가 없는 함수를 함수 선언이라고 하고, 보통 라이브러리 함수들을 사용할 때 딱 여기까지만 처리가 된 상태로 사용됩니다



일단, getFunction했을 때 존재하지 않으면 Proto codegen먼저 하고

그래도 존재하지 않는다면 codegen이 실패했으니 nullptr을 리턴하고

세번쨰로, 이미 코드젠이 된 함수일 경우 cannot be redefined 에러를 띄우고 종료합니다



그다음에 엔트리 BB를 만듭니다. 아직 분기문이 없기 때문에 최종적으로 BB가 하나 있는 함수가 만들어질것입니다

그리고 Builder의 InsertPoint를 지정해서 이 이후로 호출되는 CreateInst[CreateCall, CreateFMul등]가 현재 함수의 BB부터 삽입되도록 합니다


그다음 함수 안에서 사용될 NamedValues를 처리합니다


그다음 Body->codegen을 하고 마지막에 리턴을 삽입해 준 뒤 리턴합니다

만약 codegen에서 nullptr이 리턴될 경우 함수를 지우고 nullptr을 리턴합니다


verifyFunction은 LLVM에 내장된 함수로, 함수를 동적으로 만든뒤에 제대로 만들어졌는지 검증해주는 함수입니다


이렇게 CodeGen이 완료됐습니다. 한가지 더 남은것은 아직 예외처리가 미흡하다는 것인데, 예제코드에서는 그냥 Body->codegen 성공여부만 예외처리를 해놨고, 이외에도 여러 예외상황이 발생할 수 있습니다

여러 예외상황을 테스트해보시고 고쳐보세요

예외상황 예시를 하나 드리겠습니다




3.5. Driver Changes and Closing Thoughts

사실 코드 작성만으로는 내가 지금 뭘하고 있는건가.. 하실겁니다

하지만 이제 코드작성이 끝났으니 드라이버를 살짝 바꿔서 우리가 지금까지 뭘하고 있던건지 보도록 합시다

yeah!!!! 멋있다~~

코드가 좀 길고 여러개 많이 수정해서 zip해서 올리겠습니다

clang++ -o main *.cpp -std=c++17 `/Users/cd80/AppSolidiOS/DevAppsolidiOS/build/Ninja-ReleaseAssert/llvm-macosx-x86_64/bin/llvm-config --cxxflags --ldflags --system-libs --libs core`


컴파일은 이렇게 직접 컴파일한 LLVM의 llvm-config를 이용해야 합니다

이거때문에 살짝 헤맸었습니다


다음장에서는 top level expression을 처리한것을 이용해 LLVM JIT를 써서 python에서의 쉘처럼 인터프리터를 만들어보겠습니다


Chapter3.zip


0 Comments
댓글쓰기 폼