【随時更新・決定版】Kotlin Compiler Plugin IR改変レシピ

こんにちは、こんばんは、kitakkunです。

Kotlin Fest 2024 で Kotlin Compiler Plugin について発表をさせていただきましたが、依然として開発のハードルが高いのではないかと感じています。

特に、複雑なことをやろうとするとIR改変は避けて通ることができません。それぞれのIRノードが実コードでは何に該当するのかといった深い理解も要求されますし、何より経験が要求されます。

この記事では、やりたいことがすぐにわかる、できるを主眼にレシピ本的な感じで簡潔にまとめていきたいと思います。

絶賛編集中です。随時更新して行きます。

:::note warn
スキマ時間に脳内コンパイルで記事書いているので、なんかおかしいところあったら教えてください :pray:
一応、後で確認しておくつもりです。
:::

# 基礎編

IR操作全ての基礎となる知識をここでは解説します。

## IRノードを生成したい！

IRノードの生成には IrBuilder を使いましょう。IrBuilderは、`IrPluginContext.irBuiltIns.createIrBuilder(symbol)` を使って生成することができます。

```kotlin
val symbol: IrSymbol
val pluginContext: IrPluginContext
val irBuilder = pluginContext.irBuiltIns.createIrBuilder(symbol)
```

`IrBuilder` を使うと簡単なDSLを使ってIRノードを生成できます。
```kotlin
// IR上で文字列リテラル "hogehoge" を生成
irBuilder.irString("hogehoge")
```

:::note warn
`IrXXXXImpl` のコンストラクタを直接呼び出してIRノードを作る方法もありますが、めんどくさいだけなのでお勧めしません。まあ筆者が初めて触った時はIrBuilderの存在を知らず全部Impl使ってたんですけどね・・。
:::

実践編のセクションでは、特に断りがなければ irBuilder のスコープでIR生成していると考えてください。

```kotlin
val irBuilder = pluginContext.irBuiltIns.createIrBuilder(symbol)
with (irBuilder) {
    // ここで実行している想定
}
```


## IRノードを解析したい！変換したい！

解析 → `IrElementVisitorVoid`
改変 → `IrElementTransformerVoid`

を実装するのがシンプルなためお勧めです。`IrElementTransformerVoidWithContext`とか、`IrElementTransformer` とかもありますが `Void` で終わるやつ使うのが無難です。

# 実践編

ここからは、実際に使えるIR改変の例をご紹介します！

## 値・リテラルなどの生成

生成したい値 | IrBuilderのDSL
:--: | :--:
String | `irString`
Boolean | `irTrue`, `irFalse`
Int | `irInt`
Long | `irLong`
Char | `irChar`
null | `irNull`

```kotlin
irString("hoge") // "hoge"
irTrue()         // true
irFalse()        // false
irInt(0)         // 0
irLong(0L)       // 0L
irChar('a')      // 'a'
irNull()         // null
```

## 関数の呼び出し

`irCall` を使って関数コールを作ることができます。
関数コールを構成する要素は、大きく以下の3つです。

- レシーバー
- 実引数
- 型引数

### レシーバーという概念

例えば、以下のようなクラスと拡張関数があったとします。

```kotlin
class A {
    fun b() { /* do nothing */ }
}

fun A.c() { /* do nothing */ }
```

ここで、次のようなコードを考えます。

```kotlin
val a = A()
a.b()
a.c()
```

このとき、`a.b()` における a は dispatchReceiver、`a.c()` における a は extensionReceiverとなります。

簡単に言うと、レシーバーはその関数の受け手、であり、呼び出し対象となるインスタンスのことを指します。

### 実引数

実引数は、コンパイラプラグイン上では valueArguments として表現されます。逆に、仮引数は valueParameters として厳密に区別されます。

### 型引数

ジェネリック関数を呼び出すときに指定する型引数です。typeArguments と表現されます。こちらも宣言部の方では異なる名前で呼ばれており、typeParameters と厳密に区別されます。

### トップレベル関数の呼び出し

レシーバーのないprintln関数を呼び出すには、例えば次のようにします。

```kotlin
irCall(printlnFunctionSymbol).apply {
    putValueArgument(0, irString("Hello, World!"))
}
```

こちらのirCallは次のKotlinのコードと等価です。

```kotlin
println("Hello, World!")
```

### クラスメソッドの呼び出し

クラスのメソッドを呼び出すときは、dispatchReceiverにクラスのインスタンスを返す式を配置する必要があります。

再掲しますが、次のクラス A のメソッド b を呼び出したい場合を考えます。

```kotlin
class A {
    fun b() { /* do nothing */ }
}
```

この場合、次のようにirCallを生成することになるでしょう。
```kotlin
irCall(bFunctionSymbol).apply {
    dispatchReceiver = getAInstanceExpression
}
```

getAInstanceExpressionには、クラスAのインスタンスを取得するIrExpressionが来ます。

クラスAのインスタンスは、クラスAの各メソッドにはdispatchReceiverParameterとして渡ってきますので、このdispatchReceiverParameterをirGetするIrExpressionを生成して渡せばよいです。つまり、

```kotlin
// visitFunctionで適当なAのメソッドを訪問中であることを想定
irCall(bFunctionSymbol).apply {
    dispatchReceiver = irGet(declaration.dispatchReceiverParameter!!)
}
```

### 拡張関数の呼び出し

拡張関数を呼び出す場合は、クラスのインスタンスを返す式を置くべきプロパティが extensionReceiver に変わるだけで、通常のクラスメソッドと大きく変わることはありません。

### superクラスの関数呼び出し（ハマりやすい）

superクラスの関数を呼び出したいことが割と出てくると思いますが、こちら少々厄介です。私は結構ハマりました。

具体的にはirCallに対して、呼び出したいメソッドに加えて、superQualifierSymbol、すなわちスーパークラスのシンボルを一緒に渡してあげる必要があります。

```kotlin
irCall(
    callee = functionSymbol,
    superQualifierSymbol = superClassSymbol,
)
```

スーパークラスのシンボルを取得するには、IrClassに対して superClass?.symbol を呼び出して取得できます。


## 値の返却

`irReturn` が使えます。

```kotlin
return "hoge"
```
は、IR上では
```kotlin
irReturn(irString("hoge"))
```
です。

## 値の比較演算

演算 | IrBuilderのDSL
:--: | :--:
== | `irEquals`
!= | `irNotEquals`

```kotlin
irEquals(irInt(0), irInt(1))    // 0 == 1
irNotEquals(irInt(0), irInt(1)) // 0 != 1
```

## 条件分岐

条件分岐 | IrBuilderのDSL
:--: | :--:
if | `irIfThen`, `irIfThenElse`, ... ※実体は`IrWhen`です。
when | `irWhen`

### if-else式の生成
```kotlin
if (true) "then" else "else"
```

```kotlin
irIfThenElse(
    type = pluginContext.irBuiltIns.unitType,
    condition = irTrue(),
    thenBranch = irString("then"),
    elseBranch = irString("else"),
)
```

### when式の生成

```kotlin
when {
    value == 1 -> "one"
    value == 2 -> "two"
    else -> "other"
}
```

```kotlin
irWhen(
    type = pluginContext.irBuiltIns.stringType,
    branches = listOf(
        irBranch(
            condition = irEquals(irGet(value), irInt(1)),
            result = irString("one"),
        ),
        irBranch(
            condition = irEquals(irGet(value), irInt(2)),
            result = irString("two"),
        ),
        irElseBranch(irString("other"))
    ),
)
```

## キャスト周り

キャスト演算子 | IrBuilderのDSL
:--: | :--: 
as | `irAs`

まだまだ更新中！！
WIP