🎩

Kotlinコンパイラの全体像を理解する

2021/08/31に公開約8,200字

はじめに

この記事はKotlinコンパイラの全体像を理解するためにまとめました。AbelさんのKotlinコンパイラのダイアグラムが素晴らしいので、合わせて参照ください。なお、ダイアグラムのリンクはKotlin Slackの#compilerチャンネルを指します。

Kotlinコンパイラのフロー

KotlinのソースコードからJVMのバイトコード[1]へ変換するまでに以下のフロー[2]を通ります。Kotlinはコンパイラ型言語のため、ソースコードをコンパイルして動作環境に合わせたバイナリ形式に出力され、ソースコードから中間表現を出力するまでをFrontend、中間表現からバイナリを出力するまでをBackendと呼びます。

以下のソースコードは、引数のuser:Stringに応じて"Hello, $user"を返す関数です。それでは以下のソースコードを例にFrontendからBackendまで解説します。

Hello.kt
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)の制御フローは以下の通りです。

  1. Enter function hello:入口関数 hello()
  2. Enter block: 入口ブロック
  3. Const: String Hello
  4. Access variable $user
  5. String concatenation call: Hello $user
  6. Jump hello(Hello, $user)
  7. 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

参考資料

脚注
  1. 作図したフローはJVMバイトコードに変換されていますが、Kotlin/JSも同様のフローのようです。ただし、JVMバイトコード変換ではなく、JSコード変換になります。 ↩︎

  2. Kotlin1.4、1.5の資料など基にKotlinコンパイラのフローを作図しているため、Kotlinのバージョンによって動作やフローが異なる可能性があります。 ↩︎

Discussion

ログインするとコメントできます