【ストレンジコード】Filskaコンパイラ開発計画(3) LLVM IRを出力する【MLIR】

シリーズ一覧:

- [【ストレンジコード】Filskaコンパイラ開発計画(1) 言語仕様を整理する【MLIR】](https://qiita.com/Syuparn/items/e8f4046c4a5b551db310)
- [【ストレンジコード】Filskaコンパイラ開発計画(2) ソースコードをパースする【MLIR】](https://qiita.com/Syuparn/items/ca502ec32aded84ebac1)
- 【ストレンジコード】Filskaコンパイラ開発計画(3) LLVM IRを出力する【MLIR】(本記事)
- [【ストレンジコード】Filskaコンパイラ開発計画(4) 実行ファイル出力【MLIR】](https://qiita.com/Syuparn/items/24ac10c76b73485ff85d)
- [【ストレンジコード】Filskaコンパイラ開発計画(5) サブプログラムを複数作成可能にする【MLIR】](https://qiita.com/Syuparn/items/35ee40e8b77bb8657cee)
- [【ストレンジコード】Filskaコンパイラ開発計画(6) llvmir以外のdialectを経由したloweringを可能にする【MLIR】](https://qiita.com/drafts/0c327b5af5bfed62e4d0)

# TL; DR

- [Filska](https://esolangs.org/wiki/Filska)をMLIR製コンパイラへ移植
- 最低限の命令のみ実装し、処理系の骨組みを作成
    - Lexer: 字句解析
    - Parser: 構文解析
    - Sema: 意味解析
    - CodeGen: MLIR生成
    - Lowering: MLIRをLLVM IRへ変換

https://github.com/Syuparn/filskalang

# はじめに
本シリーズでは、『ストレンジコード』に登場する難解プログラミング言語「[Filska](https://esolangs.org/wiki/Filska)」をMLIRへ移植します。本家がPython製インタプリタなので、コンパイラにすることで高速化できるのでは？という淡い期待も寄せています。

（ストレンジコードにはFilska以外の難解プログラミング言語もたくさん登場します！言語好きは必見です ~~（ダイマ）~~）

https://www.shuwasystem.co.jp/book/9784798069746.html

今回はASTからMLIRを生成し、LLVM IRへ変換するための導線を準備します。

思い立った背景や開発の概要については初回の記事をご覧ください。

https://qiita.com/Syuparn/items/e8f4046c4a5b551db310

# 記事投稿者のスペック
MLIRもC++も初心者のため、誤った記述があればコメントで指摘いただけますと幸いです :bow:

- C++: 未経験
- LLVM: 未経験
- MLIR: 未経験
- 言語処理系: インタプリタのみ自作経験あり

# 処理系の骨組みをつくる
まずは処理系の設計指針を立てるため、ソースコードを読み込んでからLLVM IRを出力するまでに必要な処理を一通り実装します。
（ただし、命令は動作確認できる最低限のみ対応）

Semaまでについては[前回の記事](https://qiita.com/Syuparn/items/ca502ec32aded84ebac1)をご覧ください。

- Lexer: 字句解析
    - ソースコード→トークン列
- Parser: 構文解析
    - トークン列→AST
- Sema: 意味解析
    - ASTがFilskaの制約を満たしているかチェック
        - 例：同名のサブプログラムが存在した場合エラー
- CodeGen: MLIR生成
    - AST→MLIR (filskalang dialect)
- Lowering: MLIRをLLVM IRへ変換
    - MLIR→LLVM IR

## 設計
以下の2つの言語処理系を参考にしています。

- [Toy](https://github.com/llvm/llvm-project/tree/main/mlir/examples/toy/Ch7)
    - MLIRの公式チュートリアル
    - toyソースコードをMLIRを経由しLLVM IRへ変換、その後JITコンパイラで実行

https://mlir.llvm.org/docs/Tutorials/Toy/Ch-1/

- [tinylang](https://github.com/PacktPublishing/Learn-LLVM-17/tree/main/Chapter05/tinylang)
    - 書籍『Learn LLVM 17』の題材として取り上げられる言語
    - Modula-2のサブセット
    - tinylangソースコードをLLVM IRへ変換したのちバイナリ生成

https://www.packtpub.com/product/learn-llvm-17-second-edition/9781837631346

## 開発環境

- 使用バージョン
    - LLVM: 18.1.4
    - MLIR: 18.1.4
    - clang: 17.0.1
- 開発環境
    - WSL2 Ubuntu 20.04

環境構築のあれこれに詰まると辛いので ~~(2敗)~~ 、LLVMとMLIRはaptでインストールしています。
また、こうすることで将来的にFilskaコンパイラをDockerイメージで配布できるようになります。

aptでの環境構築とコンパイラのイメージ化について詳細は以下の記事をご覧ください。

https://qiita.com/Syuparn/items/1de554d862b5de4bc76f

## 実装する命令
動作確認をするための最小構成として、今回は以下の要素のみ実装します。

- プログラム
- サブプログラム: Filskaの命令をまとめた単位（他言語の「関数」に相当）
- `set` 命令: サブプログラムのレジスタ `m` に指定した数値リテラルを格納
- `prt` 命令: サブプログラムのレジスタ `m` の内容を数値として標準出力へ表示
- `hlt` 命令: プログラムを終了する（サブプログラムはデフォルトで無限ループするため）

下記のソースコードが動作するのがゴールです。

```filska
{ main
    set,10
    prt
    hlt
}
```

```
$ filska example/simple.filska
10
```

## CodeGen
MLIRチュートリアルのtoyを参考にしています。

ASTからMLIRを生成します。MLIRの命令はdialect（直訳すると「方言」？）の単位で管理することができるため、FilskaのASTをそのまま表したfilskalang dialectを作成します。

dialectの定義は

- TableGen
- C++

の2通りの方法で記述可能です[^tablegen]。今回は記述が簡潔なTableGenを採用しました。
TableGenの記法についてはtoyチュートリアルと公式リファレンスを参考にしました。

https://mlir.llvm.org/docs/DefiningDialects/Operations/

https://mlir.llvm.org/docs/Tutorials/Toy/Ch-2/

基本的にはFilskaの命令をdialectの命令と1対1対応させています。

例： `set` 命令

```tablegen:include/filskalang/CodeGen/Ops.td
def SetOp : Filskalang_Op<"set"> {
  let summary = "set operation";
  let description = [{
    The "filskalang.set" builtin operation represents the "set" instruction
  }];

  # NOTE: レジスタを参照するためサブプログラム名を追加で渡している（理由は後述）
  let arguments = (ins F64Attr:$value, SymbolNameAttr:$subprogramName);
}
```

サブプログラムは複数命令を内部に持つ必要があるため、`FunctionOpInterface` を継承して作成しています。

<details><summary>SubprogramOp(長いので折りたたみ)</summary>

```tablegen:include/filskalang/CodeGen/Ops.td
def SubprogramOp : Filskalang_Op<"subprogram", [
    FunctionOpInterface,
  ]> {
  let summary = "subprogram operation";
  let description = [{
    The "filskalang.subprogram" operation represents a subprogram.
    This must have no parameters and return values.
  }];

  // NOTE: dummy parameters and return values are required for FunctionOpInterface
  let arguments = (ins
    SymbolNameAttr:$sym_name,
    TypeAttrOf<FunctionType>:$function_type,
    OptionalAttr<DictArrayAttr>:$arg_attrs,
    OptionalAttr<DictArrayAttr>:$res_attrs
  );

  let regions = (region AnyRegion:$body);

  let builders = [
    OpBuilder<(ins
      "StringRef":$name,
      "FunctionType":$type,
      CArg<"ArrayRef<NamedAttribute>", "{}">:$attrs
    )>
  ];

  let extraClassDeclaration = [{
    //===------------------------------------------------------------------===//
    // SubprogramOpInterface Methods
    //===------------------------------------------------------------------===//

    /// Returns the argument types of this function.
    ArrayRef<Type> getArgumentTypes() { return getFunctionType().getInputs(); }

    /// Returns the result types of this function.
    ArrayRef<Type> getResultTypes() { return getFunctionType().getResults(); }

    Region *getCallableRegion() { return &getBody(); }
  }];

  # デフォルトのビルドメソッド生成をスキップ（自分で別途実装する必要がある）
  let skipDefaultBuilders = 1;
}
```
</details>

ソースコードをfilskalang dialectに変換するとこのようになります。`"filskalang.subprogram"` のボディに命令が格納されているのが確認できます。

```filska:sample.filska
{ main
    set,10
    prt
    hlt
}
```

```mlir:生成されたMLIR
"builtin.module"() ({
  "filskalang.subprogram"() <{function_type = () -> (), sym_name = "main"}> ({
    "filskalang.set"() <{subprogramName = "main", value = 1.000000e+01 : f64}> : () -> ()
    "filskalang.prt"() <{subprogramName = "main"}> : () -> ()
    "filskalang.hlt"() : () -> ()
  }) : () -> ()
}) : () -> ()
```

## lowering
filskalang dialectのままでは実行できないため、LLVM IRへ変換します。MLIRでは複数の中間表現(dialect)を経由して段階的に別の中間表現へ変換(lowering)することができます。

loweringについてはこちらの記事で詳しく解説されています。

https://zenn.dev/lemolatoon3/articles/self-made-lang-run-on-gpu#affine-dialect-%E3%81%B8%E3%81%AE-lowering

今回実装する

- サブプログラム
- `set` 命令
- `prt` 命令
- `hlt` 命令

はいずれも既存のdialectで上手く表現できるものがなかったため、直接LLVM IRへ変換しています[^llvmir]。

### サブプログラム
MLIRには関数を表す [`func` dialect](https://mlir.llvm.org/docs/Dialects/Func/)が存在しますが、今回は使用しませんでした。

理由はサブプログラムには **returnの概念が存在しない** ためです。
サブプログラムは関数と異なり、ジャンプ先からジャンプ元へ戻ることはありません。そのため、サブプログラムのジャンプを関数呼び出しとして実装するとコールスタックが溜まり続けてしまいます。

サブプログラムをmain関数内にブロックとして展開し、ジャンプをbr命令によって表すことでこの問題を回避することにしました。
（現状ではmain関数内のインライン展開のみ実装済み）

実装に以下の課題がありますが、今回は無視してひとまずLLVM IRの生成を目指します。

- サブプログラムの無限ループが実装できていない
  - サブプログラムは本来最後の命令を実行し終えると先頭の命令から再び実行する
- 複数のサブプログラムが存在する場合に正しく処理できない
  - LLVM IRに `main` 関数が複数作成されてしまう

<details><summary>サブプログラムの変換</summary>

```cpp:lib/CodeGen/LowerToLLVM.cpp
  mlir::LogicalResult
  matchAndRewrite(mlir::Operation *Op, mlir::ArrayRef<mlir::Value> Operands,
                  mlir::ConversionPatternRewriter &Rewriter) const override {
    auto Loc = Op->getLoc();
    auto *Context = Op->getContext();

    mlir::filskalang::SubprogramOp Subprogram =
        mlir::cast<mlir::filskalang::SubprogramOp>(Op);
    Rewriter.setInsertionPointToEnd(Subprogram->getBlock());

    // `main` 関数 (LLVM IRのエントリポイント)にサブプログラムの命令一覧をインライン展開
    auto DummyType = mlir::LLVM::LLVMFunctionType::get(
        mlir::LLVM::LLVMVoidType::get(Context), mlir::ArrayRef<mlir::Type>{});
    auto Func = Rewriter.create<mlir::LLVM::LLVMFuncOp>(Loc, "main", DummyType);
    Rewriter.inlineRegionBefore(Subprogram.getBody(), Func.getBody(),
                                Func.end());
    mlir::Block *EntryBlock = &Func.getBody().front();

    // ...

    // LLVM IRのブロックはterminatorで終了しないといけないため、ダミーのreturnを挿入
    // https://llvm.org/docs/LangRef.html#terminator-instructions
    // TODO: 無限ループへ置き換え
    Rewriter.setInsertionPointToEnd(EntryBlock);
    Rewriter.create<mlir::LLVM::ReturnOp>(Loc, mlir::ArrayRef<mlir::Value>());

    Rewriter.setInsertionPointToEnd(Subprogram->getBlock());

    // 不要になった変換前の命令(filskalang.subprogram)を削除
    Rewriter.eraseOp(Op);

    return mlir::success();
  }
};
```
</details>

### set命令

`set` はサブプログラムのレジスタ `m` への代入を行います。そのため以下全てを満たす必要があります。

- `m` に対し再代入が行える
- どのサブプログラムの `m` かを識別できる

#### `m` に対し再代入が行える
LLVM IRはSSA(Static Single Assignment, 静的単一代入)形式のため、同じ変数へ再代入することは **できません**。
一方、メモリオブジェクトについてこの制約は無いため、再代入可能な変数を `alloca` と `store` 命令で表すことが可能です。
詳細はLLVMのチュートリアル「Kaleidoscope」言語の解説で詳しく紹介されています。

https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl07.html

```llvm:再代入を実現するLLVM IR(上記サイトより引用)
define i32 @example() {
entry:
  %X = alloca i32           ; %X用のメモリ割り当て
  ...
  %tmp = load i32, i32* %X  ; スタックから %X の値を読み込み
  %tmp2 = add i32 %tmp, 1
  store i32 %tmp2, i32* %X  ; %Xへ書き込み
```

ご覧の通り、メモリを使用するためパフォーマンスが下がってしまいます。
幸いLLVMの最適化パス `mem2reg` によってレジスタ割り当てに置換されるため、実際には効率を損なわない処理に変換されます。

実際、filskalangが生成したLLVM IRも最適化によってalloca, store命令が削除されています[^mem2reg]。

```llvm:変換前
define void @main() !dbg !6 {
  %1 = alloca double, i64 0, align 8, !dbg !8
  store double 1.000000e+01, ptr %1, align 8, !dbg !9
  %2 = load double, ptr %1, align 8, !dbg !10
  %3 = call double (ptr, ...) @printf(ptr @frmt_spec, double %2), !dbg !10
  ret void, !dbg !8
}
```

```llvm:変換後
define void @main() local_unnamed_addr !dbg !6 {
  %1 = tail call double (ptr, ...) @printf(ptr nonnull @frmt_spec, double 1.000000e+01), !dbg !8
  ret void, !dbg !9
}
```

#### どのサブプログラムの `m` かを識別できる
サブプログラムごとに別々の `m` を用意し適切なものを使用する必要があります。しかし、 `prt` 命令から直接対応するサブプログラムを取得することはできません。

そのため、サブプログラム名と対応する `alloca` 命令をmapで管理し、対応するものに`store`するようにしています。
（このためにfilskalang dialectの `set` 命令の引数にサブプログラム名を持たせていました）

<details><summary>set命令の実装</summary>

```cpp:lib/CodeGen/LowerToLLVM.cpp
std::unordered_map<std::string, mlir::TypedValue<::mlir::LLVM::LLVMPointerType>>
    SubprogramMemory;

class SetOpLowering : public mlir::ConversionPattern {
public:
  mlir::LogicalResult
  matchAndRewrite(mlir::Operation *Op, mlir::ArrayRef<mlir::Value> Operands,
                  mlir::ConversionPatternRewriter &Rewriter) const override {
    auto Loc = Op->getLoc();
    auto SetOp = mlir::cast<mlir::filskalang::SetOp>(Op);

    mlir::Value ConstantOp = Rewriter.create<mlir::LLVM::ConstantOp>(
        Loc, Rewriter.getF64Type(),
        Rewriter.getF64FloatAttr(SetOp.getValue().convertToDouble()));

    // サブプログラム名から対応するalloca命令を取得
    auto MemoryPointer = SubprogramMemory.at(SetOp.getSubprogramName().str());
    auto Value = llvm::APFloat(SetOp.getValue());
    // 指定した命令へstore
    Rewriter.create<mlir::LLVM::StoreOp>(Loc, ConstantOp, MemoryPointer);

    // Notify the rewriter that this operation has been removed.
    Rewriter.eraseOp(Op);
    return mlir::success();
  }
};


class SubprogramOpLowering : public mlir::ConversionPattern {
public:
  mlir::LogicalResult
  matchAndRewrite(mlir::Operation *Op, mlir::ArrayRef<mlir::Value> Operands,
                  mlir::ConversionPatternRewriter &Rewriter) const override {
    // ...

    // レジスタ `m` を定義するためにalloca命令を挿入
    Rewriter.setInsertionPointToStart(EntryBlock);
    mlir::Value Cst0 = Rewriter.create<mlir::LLVM::ConstantOp>(
        Loc, Rewriter.getI64Type(), Rewriter.getIndexAttr(0));
    auto Alloca = Rewriter.create<mlir::LLVM::AllocaOp>(
        Loc, /*resultType*/ mlir::LLVM::LLVMPointerType::get(Context),
        /*elementType*/ Rewriter.getF64Type(), Cst0);
    // mapへ格納
    SubprogramMemory[Subprogram.getName().str()] = Alloca.getRes();

    // ...
  }
};
```
</details>

MLIR (LLVM dialect)上でも、`alloca` 命令のポインタへ `store` できていることが確認できます。

```mlir
%1 = llvm.alloca %0 x f64 : (i64) -> !llvm.ptr
%2 = llvm.mlir.constant(1.000000e+01 : f64) : f64
llvm.store %2, %1 : f64, !llvm.ptr
```

### prt命令

`printf` を呼び出しています。数値をそのまま出力するため書式 `%f` を使用しています。
出力対象はレジスタ `m` なので、上記と同様 `load` 命令でレジスタの中身を取得して `printf` の引数にしています。

実際には `m` が整数の場合のみ `%d` で出力する必要がありますが、これは今後の課題とします。

（処理は長いので割愛）

https://github.com/Syuparn/filskalang/blob/1ecafd655858cc48f4f8f56db588d7ae0c9a851e/lib/CodeGen/LowerToLLVM.cpp#L45

### hlt命令

サブプログラムの無限ループを止めるために必要ですが、無限ループ機構自体未実装のため現状何もしていません。

<details><summary>hltの変換</summary>

```cpp:lib/CodeGen/LowerToLLVM.cpp
  mlir::LogicalResult
  matchAndRewrite(mlir::Operation *Op, mlir::ArrayRef<mlir::Value> Operands,
                  mlir::ConversionPatternRewriter &Rewriter) const override {

    // TODO: 処理を追加

    // 不要になった変換前の命令を削除
    Rewriter.eraseOp(Op);
    return mlir::success();
  }
```
</details>

# 動作確認

LLVM IRまで生成できるようになったため、実際に動かしてみます。

```filska:ソースコード
{ main
    set,10
    prt
    hlt
}
```

```bash
$ ./bin/filskalang example/simple.filska -emit llvm
```

```llvm:生成されたLLVM IR
; ModuleID = 'LLVMDialectModule'
source_filename = "LLVMDialectModule"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

@frmt_spec = internal constant [3 x i8] c"%f\00"

declare !dbg !3 double @printf(ptr, ...)

define void @main() !dbg !6 {
  %1 = alloca double, i64 0, align 8, !dbg !8
  store double 1.000000e+01, ptr %1, align 8, !dbg !9
  %2 = load double, ptr %1, align 8, !dbg !10
  %3 = call double (ptr, ...) @printf(ptr @frmt_spec, double %2), !dbg !10
  ret void, !dbg !8
}

!llvm.module.flags = !{!0}
!llvm.dbg.cu = !{!1}

!0 = !{i32 2, !"Debug Info Version", i32 3}
!1 = distinct !DICompileUnit(language: DW_LANG_C, file: !2, producer: "MLIR", isOptimized: true, runtimeVersion: 0, emissionKind: LineTablesOnly)
!2 = !DIFile(filename: "<unknown>", directory: "")
!3 = !DISubprogram(name: "printf", linkageName: "printf", scope: !2, file: !2, line: 1, type: !4, scopeLine: 1, spFlags: DISPFlagOptimized)
!4 = !DISubroutineType(cc: DW_CC_normal, types: !5)
!5 = !{}
!6 = distinct !DISubprogram(name: "main", linkageName: "main", scope: !7, file: !7, line: 1, type: !4, scopeLine: 7, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !1)
!7 = !DIFile(filename: "simple.filska", directory: "example")
!8 = !DILocation(line: 1, column: 7, scope: !6)
!9 = !DILocation(line: 2, column: 8, scope: !6)
!10 = !DILocation(line: 3, column: 8, scope: !6)
```

LLVM IRのインタプリタ `lli` に渡して実行したところ、無事想定通りの結果が表示されました。

```bash
$ ./bin/filskalang example/simple.filska -emit llvm | lli
10.000000
```

本家では `10` と表示されるため、書式については今後修正が必要そうです。

# はまったところ
ここまでさらっと書きましたが、実際にはここに至るまで七転八倒していました。
以下はまった点と解消方法についてです。
（備忘録なので適当に読み飛ばしてください）

## CodeGen
### `llvm::SMLoc` を `mlir::Location` に変換できない
Lexer, Parserではエラーメッセージに `llvm::SourceMgr#PrintMessage` を使用しているため、エラー発生個所のソースコード上の位置情報は `llvm::SMLoc` から取得しています。

一方、filskalang dialectのコード生成時には位置情報として `mlir::Location` が要求されます。両者はどうやら相互変換ができないよう[^smloc]なので、両方の形式に対応した位置情報クラスを定義しています。

<details><summary>位置情報クラスの実装</summary>

```cpp:include/filskalang/Basic/Location.h
class Location {
  mlir::SMLoc Loc;
  int Line;
  int Column;
  const char *InputFileName;

public:
  Location(mlir::SMLoc Loc, int Line, int Column, const char *InputFileName)
      : Loc(Loc), Line(Line), Column(Column), InputFileName(InputFileName) {}

  mlir::SMLoc getLoc() { return Loc; }

  mlir::Location getLocation(mlir::OpBuilder Builder) {
    return mlir::FileLineColLoc::get(Builder.getStringAttr(InputFileName), Line,
                                     Column);
  }
};
```
</details>


2つの処理系の実装をそのまま引っ張ってきたのが原因とはいえ少し煩雑なので、将来別の言語をMLIRで作る際には `mlir::Location` にまとめられないか検討したいです。

### TableGenからC++ソースコードが生成できない
tbファイルと同じディレクトリの `CMakeLists.txt` に設定を入れてもソースコードが生成されませんでした（原因不明）。
本来はtoyのこの設定のように記述できるはずですが...

```cmake:mlir/examples/toy/Ch7/include/toy/CMakeLists.txt
# toyより引用
set(LLVM_TARGET_DEFINITIONS ShapeInferenceInterface.td)
mlir_tablegen(ShapeInferenceOpInterfaces.h.inc -gen-op-interface-decls)
mlir_tablegen(ShapeInferenceOpInterfaces.cpp.inc -gen-op-interface-defs)
add_public_tablegen_target(ToyCh7ShapeInferenceInterfaceIncGen)
```

解決方法が分からないので、汚いですがプロジェクト直下の `CMakeLists.txt` に設定を書きました。

```cmake:
add_mlir_dialect(FilskalangOps filskalang)
set(LLVM_TARGET_DEFINITIONS ${CMAKE_CURRENT_SOURCE_DIR}/include/filskalang/CodeGen/Ops.td)
mlir_tablegen(include/filskalang/CodeGen/Ops.h.inc -gen-op-decls)
mlir_tablegen(include/filskalang/CodeGen/Ops.cpp.inc -gen-op-defs)
mlir_tablegen(include/filskalang/CodeGen/Dialect.h.inc -gen-dialect-decls)
mlir_tablegen(include/filskalang/CodeGen/Dialect.cpp.inc -gen-dialect-defs)
add_public_tablegen_target(FilskalangOpsIncGen)
```

### dialectのbuildメソッドでコンパイルエラーが発生する

```
undefined reference to `mlir::filskalang::SubprogramOp::build(mlir::OpBuilder&, mlir::OperationState&, llvm::StringRef, mlir::FunctionType, llvm::ArrayRef<mlir::NamedAttribute>)'
```

`Builder.create<作成したい命令の型>` の引数のシグネチャが、`作成したい命令の型::build` に合致していない場合に発生します。オーバーロードごとにエラーが発生するためエラーログが大量発生しますが、その中のどれかに当てはまるように修正すればコンパイルが通ります。

```cpp:lib/CodeGen/MLIRGen.cpp
    // 正しいシグネチャに修正する
    mlir::filskalang::SubprogramOp Sub =
        Builder.create<mlir::filskalang::SubprogramOp>(
            Loc, Subprogram.getName(), funcType);
```

## Lowering
### lowering失敗時に何が起きているか分からない
`--mlir-print-ir-after-all` オプションを付けることで、変換パスごとにMLIRの途中経過を確認できました。

```bash
$ ./bin/filskalang --mlir-print-ir-after-all example/simple.filska -emit llvm
```

### dialectの実装が見つからない
ビルド時に以下のエラーが発生しました。

```
undefined reference to `filskalang::PrtOp::print(mlir::OpAsmPrinter&)'
undefined reference to `filskalang::PrtOp::verifyInvariantsImpl()'
```

TableGenから生成された実装 (`.cpp.inc`)がincludeできていないのが原因だったため、以下を追加する必要がありました[^include]。

```diff:lib/CodeGen/Dialect.cpp
#include "filskalang/CodeGen/Dialect.h"

using namespace mlir::filskalang;

#include "filskalang/CodeGen/Dialect.cpp.inc"

+#define GET_OP_CLASSES
+#include "filskalang/CodeGen/Ops.cpp.inc"

void FilskalangDialect::initialize() {
  addOperations<
#define GET_OP_LIST
#include "filskalang/CodeGen/Ops.cpp.inc"
      >();
}
```

### 関数のボディに命令を追加できない

サブプログラムを `funcOp` にloweringした際、`alloca` 命令を追加する必要がありましたが、命令が関数の外側に挿入されてしまいました。

命令の挿入ポイントに `Func.getBody().front()` を使用することで、想定通りボディ内に入れることができました。

```cpp:lib/CodeGen/LowerToLLVM.cpp
auto Func = Rewriter.create<mlir::LLVM::LLVMFuncOp>(Loc, "main", DummyType);
mlir::Block *EntryBlock = &Func.getBody().front();
Rewriter.setInsertionPointToStart(EntryBlock);

// 以降はmain関数の先頭に挿入される
mlir::Value Cst0 = Rewriter.create<mlir::LLVM::ConstantOp>(
        Loc, Rewriter.getI64Type(), Rewriter.getIndexAttr(0));
```

こちらの実装を参考にしました。

https://github.com/llvm/llvm-project/blob/c32a4f83b59c4293f0b91503ed217371ae1ab543/mlir/lib/Conversion/FuncToLLVM/FuncToLLVM.cpp#L284C3-L284C49

### load命令でコンパイルエラー
`alloca` 命令を `load` 命令の引数に指定したところ、以下のエラーが発生しました。

```
filskalang: /usr/lib/llvm-18/include/llvm/Support/Casting.h:566: decltype(auto) llvm::cast(const From &) [To = mlir::detail::TypedValue<mlir::LLVM::LLVMPointerType>, From = mlir::OpResult]: Assertion `isa<To>(Val) && "cast<Ty>() argument of incompatible type!"' failed.
Aborted
```

正しくは以下のように指定する必要がありました。

```diff:lib/CodeGen/LowerToLLVM.cpp
auto Alloca = Rewriter.create<mlir::LLVM::AllocaOp>(
        Loc, /*resultType*/ mlir::LLVM::LLVMPointerType::get(Context),
        /*elementType*/ Rewriter.getF64Type(), Cst0);
 auto Load = Rewriter.create<mlir::LLVM::LoadOp>(
-        Loc, mlir::Float64Type::get(Context), Alloca);
+        Loc, mlir::Float64Type::get(Context), Alloca.getRes());
```

### loadした値が0のまま
第二引数にはポインタ型ではなく格納している値の型を指定する必要がありました。

```cpp:lib/CodeGen/LowerToLLVM.cpp
// Float64の指定が必要
auto Load = Rewriter.create<mlir::LLVM::LoadOp>(
    Loc, mlir::Float64Type::get(Context), MemoryPointer);
```

## C++初心者シリーズ
### 値が0になってしまう
あやまって参照を持たせていたため解放されてしまいました...

```diff
private:
  UnaryOperator Operator;
+  NumberLiteral *Operand;
-  NumberLiteral *&Operand;
```

### mapが更新されない
キーの型を `const char*` ではなく `string` にする必要がありました。


# おわりに
以上、Filskaコンパイラの設計の骨組みの紹介でした。次回はLLVM IRをバイナリへコンパイルする処理を取り上げる予定です。

[^learn-llvm17]: まだコンパイラフロントエンドの章しか読んでいませんが...
[^number]: 自作言語だったら仕様を減らして実装をサボっていたところです。~~題材に既存言語の移植を選んだのは正解でした。~~
[^llvmir]: まだ使っていないだけで、今後数学関連の命令は `math`, `arith` dialectへloweringする予定です。
[^tablegen]: ビルド時にTableGenファイルからC++ソースコードが生成されます。
[^mem2reg]: この例だと、さらに最適化されて引数に直接定数が入る形になっています。
[^include]: `filskalang/CodeGen/Ops.cpp.inc` を2回includeしていますが、defineで定義されている定数によって内容が変わるためいずれも必要です。（この仕組みはどこかで記事にしたい）
[^smloc]: コード生成時に `llvm::SMLoc` から行番号、列番号を抽出できないか試しましたがSegmentation Faultが発生してしまいました。lexer稼働時にリアルタイムで抽出する必要があるようです。