しょぼいマシンで自作プログラミング言語を作ってみる (13)
最小限のLLVM IRが生成できた!
コメントアウトのAST化ができた
まず、コメントアウトのAST化ができました。
この記事で触れていた仕様通りにコメントアウトをAST化できました。
コマンドラインオプションで以下の指定があった場合のみ、ASTのノード
に追加されます。
"--with-commentout-all"が指定された場合はすべてのコメントアウトがASTのノードに追加。
"--with-commentout-for-doc"が指定された場合はドキュメント生成用のコメントアウトがASTのノードに追加。
ドキュメント生成用のコメントから、ドキュメントHTMLを生成するのはセルフホスティングができてから着手します。
パッケージ名、モジュール名のAST情報への設定
この記事で触れていた通り、パッケージ名、モジュール名を指定できるようにしました。
pkgdef pkg1.pkg2;
moddef module1;
上記の通り、pkgdef文で指定された場合にパッケージ名、moddef文で指定された場合にモジュール名が設定できるようになりました。
改めて仕様を整理すると、以下の通りになります。
インタプリタの場合
パッケージ名、モジュール名は宣言できない。
デフォルトのモジュール名は"moduleForInterpreter"。標準入力ストリームからの単一コンパイルの場合
パッケージ名は宣言できない。モジュール名は宣言できる。
デフォルトのモジュール名は"moduleFromStdin"。ファイルからのコンパイルの場合
パッケージ名、モジュール名を宣言できる。
デフォルトのパッケージ名は"main"。
デフォルトのモジュール名は入力ファイル名からディレクトリ名、拡張子を取り除いたもの。
例)hoge/hoge.trotならば”hoge”がデフォルトのモジュール名に。
宣言した位置によるコンパイルエラーの発生はC++での実装時はしません。
セルフホスティングの際に実装します。
ASTの表示
ASTの表示において、ツリー構造を一度、平坦化してから、表示するようにしました。
深さ優先探索(行きがけ順)でノードを動的配列に追加していきます。その時には深さも情報として保存します。
あとは深さ毎にインデントをしながら、ASTの情報を表示させます。
実際に表示させるとこんな感じですかね?
/#<
comment out (コメントアウト ドキュメント用)\
aa
aa
/#>
//<
comment out (コメントアウト)
comm\
ent
//>
pkgdef pkg1.pkg2;
moddef module1;
このソースをASTにすると?
-- ../example/bootstrap/pkgmoddef0.trot to AST --
ast::Root(File)
=> Module(File)
|-> filename: ../example/bootstrap/pkgmoddef0.trot
|-> package name: pkg1.pkg2
|-> module name: module1
=> CommentOut For Doc
|-> beginLocation: [2,1]
|-> endLocation: [6,4]
|-> body: comment out (コメントアウト ドキュメント用)aa
aa
=> CommentOut
|-> beginLocation: [8,1]
|-> endLocation: [12,4]
|-> body: comment out (コメントアウト)
comment
LLVM IRの生成
最低限のASTができたので、LLVM IRに変換していきます。
LLVM IRの変換もASTの表示と同様に深さ優先探索(行きがけ順)で平坦化してから、生成する方法で行けるとみています。
前述のASTの表示もそうですが、なんで、そんなことする? って話ですが、コールスタックをガシガシ積むのが嫌だった! って話です。
それはそれとして、最低限のLLVM IRを作るには以下のヘッダが必要になります。
#include <llvm/IR/ConstantFolder.h>
#include <llvm/IR/IRBuilder.h>
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/Module.h>
このヘッダを使って実装するというわけですが、以下のようにLLVM IR生成のための情報を保持するクラスを用意しました。
class IrGenContext {
public:
IrGenContext();
~IrGenContext() {};
IrGenContext(const IrGenContext &rhs);
IrGenContext &operator=(const IrGenContext &rhs);
void pushModule(llvm::Module *const module);
llvm::LLVMContext *getContext();
llvm::IRBuilder<> *getBuilder();
size_t getModuleCnt() const;
llvm::Module *getModule(const size_t pos);
private:
llvm::LLVMContext *context;
llvm::IRBuilder<> *builder;
size_t moduleCnt;
std::vector<llvm::Module *> modules;
};
LLVMContextクラスがコンパイル時の大きな塊の情報を持つクラスですね。
Moduleクラスがソースファイル単位の情報を持つクラスですね。
IRBuilderクラスがLLVM IRを生成するためのクラスですね。
で、コンストラクタは以下の通りになります。
IrGenContext::IrGenContext()
: context(new llvm::LLVMContext()),
builder(new llvm::IRBuilder<>(*context, llvm::ConstantFolder(),
llvm::IRBuilderDefaultInserter())),
moduleCnt(0), modules(std::vector<llvm::Module *>()) {};
GithubにあるLLVMプロジェクトのサンプルソースは古いっぽいですね。
何が古いかっていうと、IRBuilderのコンストラクタです。最新のLLVM-19では第2引数、第3引数が必要っぽいです。
ということで、ファイル読み込みによるコンパイル時においてはそれ専用のASTノードに情報設定してから、llvm::Moduleへの変換を行うというわけです。
void AstNodeModuleFile::generateLlvmIr(IrGenContext *const context_) {
llvm::LLVMContext *lContext = context_->getContext();
llvm::Module *module = new llvm::Module(getFullModuleName(), *lContext);
context_->pushModule(module);
module->setSourceFileName(filename);
};
こんな感じでLLVMコンテキストを取得してから、モジュール名とLLVMコンテキストを使ってLLVMモジュールを定義します。
更に元となったソースファイル名も追加してしまいましょうか。
ここまでできたら、LLVM IRを表示してみましょう。
module->print(llvm::outs(), nullptr);
上記の行を実行すれば、LLVM IRはコンソールに表示されます。
まあ、実際には以下の関数内でLLVM IRを表示するようにしました。
void CodeGen::printLlvmIr() {
size_t moduleCnt = context->getModuleCnt();
for (size_t moduleIdx = 0; moduleIdx < moduleCnt; moduleIdx++) {
llvm::Module *module = context->getModule(moduleIdx);
std::string moduleFilename = module->getSourceFileName();
llvm::outs() << "\n";
if (moduleFilename != "") {
llvm::outs() << "-- " << moduleFilename << " to LLVM IR --\n";
} else {
llvm::outs() << "-- to LLVM IR --\n";
}
module->print(llvm::outs(), nullptr);
}
};
で、実際にLLVM IRを表示すると?
-- ../example/bootstrap/pkgmoddef0.trot to LLVM IR --
; ModuleID = 'pkg1.pkg2.module1'
source_filename = "../example/bootstrap/pkgmoddef0.trot"
おお! 出てきた!
いやあ、第一歩です! これが大いなる第一歩です!
次はfmainブロックをASTノードに追加したり、LLVM IRへ出力するようにしましょう。