ミニキャン言語でグローバル変数を実装した話

はじめに

ミニキャン言語でグローバル変数を実装したのでそれについて書きます。
ミニキャン言語は何かっていう方は以下を参照してください。

github.com

私が実際に作成してるプログラムは以下になります。

github.com

この記事は、こう実装したというメモ的な形になります。 あまり解説らしい解説は書けませんが、こういった実装があるんだな程度に参考にしてもらえればと思います。

実装の前にIRの標準出力について

今までのミニキャン言語の実装ではファイル出力ではない方、標準出力の方には関数のIRしか出力されません。
なので、今回グローバル変数を出力するにあたってそこを少し修正します。
codegen.hのHandleDefinition関数では以下のようにしてIRが標準出力されていると思います。

if (auto *FnIR = FnAST->codegen()) {
    FnIR->print(stream);
}

これはFunctionクラスのprintメソッドを使っているのですが、実際に確認するときにグローバル変数が出力されず結果が確認できません。
".ll" ファイルも出力されていれば別ですが、ミニキャン言語では".o"ファイルでoutputファイルが生成されているので、別に確認する手段が必要となります。
なので上記の部分を

auto *FnIR = FnAST->codegen();

と変更し、mc.cppにModuleクラスのprintメソッド処理を追加してあげましょう。

MainLoop();
myModule->print(llvm::outs(), nullptr);
write_output();

場所的にはこの辺りで良いと思います。
自分が正しいと思う場所へ追加しましょう。
myModuleは今回使うModuleクラスのインスタンスです。
Moduleクラスのprintメソッドを使うことでグローバル変数も一緒に出力されます。
結果も確認できるようになったので、実装に移ります。

グローバル宣言の実装

mc.cpp

今回はGlobalVariableクラスを使ってグローバル変数を実装していくのでGlobalVariable.hをインクルードしておきます。

#include "llvm/IR/GlobalVariable.h"

parser.h

はじめに

グローバル変数の実装や処理の流れについては通常の変数の実装について理解しておく必要がある場合があります。
その場合は以下の公式チュートリアルを参照してください。

llvm.org

実装

まずコードを載せます。

static std::pair<std::string, std::unique_ptr<ExprAST>> ParseGVarExpr() {
    getNextToken();

    std::string Name = lexer.getIdentifier();
    getNextToken();

    std::unique_ptr<ExprAST> Init = nullptr;

    if (CurTok == '=') {
        getNextToken();

        Init = ParseExpression();
    }


    if (CurTok != tok_in)
        fprintf(stderr, "expected 'in' keyword after var");
    getNextToken();

    return std::make_pair(Name, std::move(Init));
}

この関数では変数名と初期値を解析します。

通常の変数ではASTを作成したのですが、今回は処理の都合であえて作らず変数名と初期値のpairを呼び出し元に返すだけにしました。

処理内容は見たままだと思います。

getNextToken();
std::string Name = lexer.getIdentifier();
getNextToken();

最初のgetNextToken()でvar x inのvarからxにトークンを進めています。
進めた後で変数名を保持しておいて、トークンを進めます。

std::unique_ptr<ExprAST> Init = nullptr;
if (CurTok == '=') {
    getNextToken();
    Init = ParseExpression();
}

ここの処理ではvar x = 4 inのように初期値も宣言されている場合の処理を書いています。
宣言されていない場合はInitはnullptrになります。

return std::make_pair(Name, std::move(Init));

最後に変数名と初期値のpairを作成してreturnします。
この関数はcodegenで呼び出します。

codegen.h

今回仕様としては

var x in

def func() {
    x = 4
}

上のような関数外で変数が宣言された場合グローバル変数とみなす形式にしました。
他にはglobalというトークンがあったらグローバル変数にするといった実装もあるかもしれません。

今回は上のような形式なので、関数定義と同じレベルにvarトークンがあればグローバル変数の生成処理を行うという方法を行いました。

MainLoop関数にtok_varのケースを追加します。

static void MainLoop() {
  ~ 省略 ~
    case tok_def:
        HandleDefinition();
        break;
     case tok_var:
        GVarDeclaration();
        break;
  ~ 省略 ~
}

GvarDeclaration関数はグローバル変数の宣言処理を行う関数です。
中身を載せます。

static void GVarDeclaration() {
    std::pair<std::string, std::unique_ptr<ExprAST>> body = ParseGVarExpr();
    auto name = body.first.c_str();

    Constant *Init;
    if (body.second != nullptr) {
        auto Val = body.second->codegen();
        Init = (Constant *) Val;
    } else {
        Init = ConstantInt::get(Context, APInt(64, 0, true));
    }

    IntegerType *IntegerTy = IntegerType::get(Context, 64);
    auto gvar = new GlobalVariable(
        *myModule,
        IntegerTy,
        false,
        GlobalValue::InternalLinkage,
        Init,
        name
    );
    GlobalNamedValues[name] = gvar;
}

ParseGVarExpr()で解析した変数名と初期値のペアを受け取ります。
初期値が存在しない場合は今回はInt型の0を初期値としました。
初期値が存在した場合はcodegenを行って初期値とします。

後半の部分がグローバル変数の宣言になります。
GlobalVariableクラスのインスタンスを生成するとそれが自動的にグローバル変数としてIRを生成してくれます。
グローバル変数のIRは@変数名という形で出力されます。
例:
C言語ソース

int H = 0;
int main() {
    H = 5;
    return 0;
}

上記のIR

@H = global i32 0, align 4
define i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  store i32 5, i32* @H, align 4
  ret i32 0
}

上の例だと@Hがグローバル変数です。
ではそれぞれのコードについて書きます。

IntegerType *IntegerTy = IntegerType::get(Context, 64);

このソースはInteger64tyを取得する1行です。
これはインスタンスを作成するときに用い入ります。
なので今回実装するグローバル変数はすべてこの型になります。

auto gvar = new GlobalVariable(
    *myModule,
    IntegerTy,
    false,
    GlobalValue::InternalLinkage,
    Init,
    name
);

GlobalVariableの引数はドキュメントを参照してください。

llvm.org

何だこれ?と自分が思ったところを書きます。
ですが、自分自身正確には把握しきれていないので間違いがありましたら指摘いただけるとありがたいです。
まずisConstantの部分ですが、定数かどうかを聞かれているのだと思います。
今回は変数なのでfalseにしました。
LinkageTypesについてはこれも公式ドキュメントを参考にしていただいて、目的にあった物を選べば良いと思います。
私はそれぞれの違いについてよく理解できなかったのでInternalにしました。
InternalかPrivateにすれば良いと思います。

GlobalNamedValues[name] = gvar;

最後のこの部分は変数の参照と値の変更の際に使います。
これはこの後に説明します。

グローバル変数の参照と変更の実装

ここからは変数の宣言だけでなく、参照と変更(代入)の実装について書きます。

変数の参照

codegen.h

グローバル変数を参照するために値を保持しておくmapをNamedValuesとは別に作成します。
NamedValuesはミニキャン言語の実装通りいくと関数のコード生成とともに新しくされてしまうので別のmap配列が必要になります。

static std::map<std::string, GlobalVariable *> GlobalNamedValues;

mapの組み合わせは変数名とGlovalVariableのインスタンスにします。
このmap配列にはインスタンス作成時に格納します。
グローバル宣言処理の最後の一行になります。

GlobalNamedValues[name] = gvar;

次にグローバル変数の参照ですが以下のような関数を実装しました。

tatic Value *getNamedValues(std::string name) {
    if (NamedValues.count(name)) {
        return NamedValues[name];
    }
    if (GlobalNamedValues.count(name)) {
        return GlobalNamedValues[name];
    }
    return nullptr;
}

これをVariableExprASTで呼び出します。
最初の実装では

Value *V = NamedValues[variableName];

となっていますが、そこを

Value *V = getNamedValues(variableName); 

に変更します。
ローカル変数や引数を優先的にreturnし、次の優先度でグローバル変数をreturnします。
ローカル変数を実装しているとNamedValuesはstringとAllocaInstのmap配列になっています。
AllocaInst
と GlobalVariable では値が違ってダメではないかと最初思いましたが、どちらもValue の継承先なのでValue *を受け手にすることでその差異を吸収してくれます。
継承って便利ですね。

以上の実装により変数の参照ができるようになりました。

変数の変更

変数の変更ですが、どうすれば良いのかClangが出力するIRを参考にしました。

@H = global i32 0, align 4
define i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  store i32 5, i32* @H, align 4
  ret i32 0
}

これを見るとグローバル変数への代入はstoreを使って行われていることがわかりました。
なので、通常の変数と同じ要領で変更できることがわかりました。
以下の実装をBinaryASTのcodegenの一番最初に追加します。
詳しい処理説明は公式ドキュメントの変数の実装を参考にしてください。

    if (Op == '=') {
        VariableExprAST *LHSE = dynamic_cast<VariableExprAST *> (LHS.get());
        if (!LHSE)
            return LogErrorV("destinatin of '=' must be a variable");

        Value *R = RHS->codegen();
        if (!R)
            return nullptr;

        //Value *Variable = NamedValues[LHSE->getName()];
        Value *Variable = getNamedValues(LHSE->getName());
        if (!Variable)
            return LogErrorV("Unknown variable name");

        Builder.CreateStore(R, Variable);

        return R;
    }

今回でいうstoreの処理は

Builder.CreateStore(R, Variable);

この部分に該当します。
この処理で変数をRHSで上書きします。
以上で変数の変更もできるようになりました。

まとめ

公式ドキュメントを参照する部分が多くて申し訳ないです。

llvm.org

再度にリンクを張らせていただきますが、こちらを参考にローカル変数を実装した後で読んでいただければ理解しやすいかと思います。

ご指摘等ありましたら、お願いします。