2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

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

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

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

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

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

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

基礎編

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

IRノードを生成したい!

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

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

IrBuilder を使うと簡単なDSLを使ってIRノードを生成できます。

// IR上で文字列リテラル "hogehoge" を生成
irBuilder.irString("hogehoge")

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

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

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
irString("hoge") // "hoge"
irTrue()         // true
irFalse()        // false
irInt(0)         // 0
irLong(0L)       // 0L
irChar('a')      // 'a'
irNull()         // null

関数の呼び出し

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

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

レシーバーという概念

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

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

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

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

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

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

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

実引数

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

型引数

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

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

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

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

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

println("Hello, World!")

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

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

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

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

この場合、次のようにirCallを生成することになるでしょう。

irCall(bFunctionSymbol).apply {
    dispatchReceiver = getAInstanceExpression
}

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

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

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

拡張関数の呼び出し

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

superクラスの関数呼び出し(ハマりやすい)

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

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

irCall(
    callee = functionSymbol,
    superQualifierSymbol = superClassSymbol,
)

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

値の返却

irReturn が使えます。

return "hoge"

は、IR上では

irReturn(irString("hoge"))

です。

値の比較演算

演算 IrBuilderのDSL
== irEquals
!= irNotEquals
irEquals(irInt(0), irInt(1))    // 0 == 1
irNotEquals(irInt(0), irInt(1)) // 0 != 1

条件分岐

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

if-else式の生成

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

when式の生成

when {
    value == 1 -> "one"
    value == 2 -> "two"
    else -> "other"
}
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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?