Kotlinコンパイラの全体像を理解する
はじめに
この記事はKotlinコンパイラの全体像を理解するためにまとめました。AbelさんのKotlinコンパイラのダイアグラムが素晴らしいので、合わせて参照ください。なお、ダイアグラムのリンクはKotlin Slackの#compilerチャンネルを指します。
Kotlinコンパイラのフロー
KotlinのソースコードからJVMのバイトコード[1]へ変換するまでに以下のフロー[2]を通ります。Kotlinはコンパイラ型言語のため、ソースコードをコンパイルして動作環境に合わせたバイナリ形式に出力され、ソースコードから中間表現を出力するまでをFrontend、中間表現からバイナリを出力するまでをBackendと呼びます。
以下のソースコードは、引数のuser:String
に応じて"Hello, $user"
を返す関数です。それでは以下のソースコードを例にFrontendからBackendまで解説します。
fun hello(user: String) = "Hello, $user"
Frontend
Kotlinのソースコードから中間表現を出力するまでのFrontendについて紹介します。
Lexer & Parser
Kotlinのソースコードから字句解析、構文解析を行った上で、ソースコードはテキストからASTそしてPSIに変換されます。字句解析はこちらのコードになり、JFlexという字句解析ジェネレーターを使用されます。さらにKotlin.flex定義ファイルを用いて生成され、構文解析を経てIRを生成します。
PSI
PSI(Program Structure Interface)はJetBrainsの木構造解析APIです。hello(user: String)
の場合、以下の構造となります。
また、PSIを参照するには、PSIViewerを利用すると簡単にデータ構造を確認できます。
Code analysis BindingContext
BindingContextとは、PSIと対になっており、プログラムの情報をマップ形式で保持されます。BindingContextを用いて解析し、hello(user: String)
の場合は以下の通りです。
出典:Kotlin Compiler In past, 1.4 and beyond
fun hello(user: String) = "Hello, $user"
の場合、キーはFUNCTION(PSI)、バリューはpublic fun hello(user: kotlin.String): kotlin.String
です。user: String
の場合、キーはVALUE_PARAMETER(PSI)、バリューはvalue-parameter user: kotlin.String
となります。
FIR
FIR(Frontend Intermediate Representation)は、Kotlinコンパイラの新しいFrontendであり、PSI、BindingContextなど、既存のFrontendに置き変わります。
FIRには2つの目標があります。
- コンパイラのパフォーマンスを向上する
- Frontendの新しいアーキテクチャーを作成する
IDEプラグインを新しいFIRに切り替えると、コード補完と構文のハイライト表示が高速化されることが期待されます。
hello(user: String)
の構造は以下の通りです。
FIRの構造を知るには、Kotlin-FirViewerという便利なIntelliJプラグインを利用します。このツールは、FIRの構造を知るためのツールで、FIR以外にもPSI構造や制御フローグラフが提供されています。
Kotlin-FirViewerでは、CFG Viewer(Control Flow Graph)が提供されており、graphvizをインストールすることで利用可能となります。CFGは、プログラムを実行したときに通る可能性のある全ての経路を示したグラフです。基本ブロックを表し、グラフ全体の入口ブロックと出口ブロックがあります。
hello(user: String)
の制御フローは以下の通りです。
- Enter function hello:入口関数 hello()
- Enter block: 入口ブロック
- Const: String Hello
- Access variable $user
- String concatenation call: Hello $user
- Jump hello(Hello, $user)
- Exit function hello:出口関数 hello()
IR
IR(Intermediate Representation)は、Backendで使用されることを目的された中間表現を示し、Kotlinのソースコードを解析するときにKotlinコンパイラによって使用されます。デッドコードの削除やtailrec、suspend function、forなど最適化されます。
hello(user: String)
は以下のような形式に変換されます。
出典:Kotlin Compiler Internals In 1.4 and beyond
Backend
中間表現からバイナリを出力するまでのBackendについて紹介します。
Intermediate bytecode generator
バイトコードの生成は古いBackend(上部)と新しいBackend(下部)に分かれます。PSI + BindingContext経由であれば、こちらのコードジェネレータ。新しいFrontendであるFIR経由であれば、こちらのコードジェネレータを使用されます。
Code transformations
新しいJVM Backendを表し、対象となるコードはこちらです。IRから新しいBackendを経由して新しいIRに変換しています。ちなみにKotlin/JSの場合はこちらのコードになります。
ASM
ASMとは、Javaのバイトコードを解析、生成するフレームワークです。KotlinのコンパイラはASMを使ってバイトコードを生成しています。今回はJVMについて紹介しましたが、Kotlin/JSなどでもフローは変わらず、JSコードジェネレータを利用されています。
hello(user: String)
のバイトコードは以下の通りです。
public final static hello(Ljava/lang/String;)Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;() // invisible
// annotable parameter count: 1 (visible)
// annotable parameter count: 1 (invisible)
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 0
LDC "user"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 1 L1
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "Hello, "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ARETURN
L2
LOCALVARIABLE user Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
おわりに
ここまでKotlinコンパイラの全体像について紹介しました。新しいFrontend、新しいBackendのステータスはCurrent stability of Kotlin componentsから参照できるようになっているので、気になる方は見てみてください。
出典:Current stability of Kotlin components
参考資料
- Kotlin 1.5.0 – 2021 年最初の大規模リリース
- Kotlin Compiler In past, 1.4 and beyond
- Kotlin Compiler Internals In 1.4 and beyond
- Prototype IDE plugin with the new compiler frontend
- Release the new compiler frontend in Alpha for JVM target
- Control- and data-flow analysis
-
作図したフローはJVMバイトコードに変換されていますが、Kotlin/JSも同様のフローのようです。ただし、JVMバイトコード変換ではなく、JSコード変換になります。 ↩︎
-
Kotlin1.4、1.5の資料など基にKotlinコンパイラのフローを作図しているため、Kotlinのバージョンによって動作やフローが異なる可能性があります。 ↩︎
Discussion