はじめに
少し遅刻しましたが、これはKotlin Advent Calendar 201914日目の記事です。 少し前からkotlinc-dotnetというKotlinを.Netで動くexe形式にコンパイルするコンパイラを作っていて、kotlinのコンパイラ周りの知見が溜まってきたので、この記事では自作Kotlinコンパイラの作り方を解説します
自作Kotlinコンパイラとは?
自作Kotlinコンパイラとはその名の通り、自作したkotlinコンパイラの事です。 現在Kotlin公式のコンパイラではJVM・JS・Native以外の環境でKotlinを動かすことはできませんが、独自でコンパイラを作れば任意の環境でKotlinを動かすことができます。 この記事では簡単なKotlinのプログラムを.NETで動くexe形式に変換するコンパイラのざっくり解説をします。
自作Kotlinコンパイラの作り方
それでは自作Kotlinコンパイラの作り方をざっくり解説します。 いきなり完璧なコンパイラ作るのは無理なのでコンパイラの仕様をめちゃくちゃ簡単にします。 今回作るコンパイラの仕様です。
- コンパイルできるのはmain関数だけ
- main関数では
println
関数しか呼び出せない - Kotlinのプログラムを.NETで動くexe形式に変換する
つまり以下のようなプログラムを.NETで動くexe形式に変換するコンパイラを作ります。
fun main() {
println("hello world.")
}
今回作るコンパイラのソースコードはGitHubにあります。
検証環境
- mono 6.4.0.198
- kotlin 1.3.61
- gradle 5.6
step1 プロジェクトを作成
step1での変更点はこちら
まずはプロジェクトを作成します。プロジェクトを作成する際にgradleの依存関係にorg.jetbrains.kotlin kotlin-compiler
を追加します。
org.jetbrains.kotlin kotlin-compiler
はKotlin/JVM, Kotlin/JS, Kotlin/Nativeのコンパイラが共通で使っているパッケージでAstパーサーやKotlinの中間表現(IR)などが含まれています。
KotlinはC言語に比べると言語仕様が複雑なので、パーサーを一から書こうとすると骨が折れます。なので今回はorg.jetbrains.kotlin kotlin-compiler
を使う事にします。
//build.gradle.kts
plugins {
kotlin("jvm") version "1.3.61"
id("com.github.johnrengelman.shadow") version "5.0.0"
application
}
group = "com.github.youta1119"
version = "1.0-SNAPSHOT"
val mainClass = "com.github.youta1119.kotlin.MainKt"
repositories {
jcenter()
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("compiler"))//追加
}
step2 コンパイラのCLI部分を作成
step2での変更点はこちら
次にコンパイラのCLI部分を作成します。
まず、コンパイラのコマンドライン引数を定義をします。
CommonCompilerArguments
を継承したK2DotNetCompilerArguments
というクラスを作成します。
CommonCompilerArguments
を継承-version
などの引数を使えるようにするためです。
//K2DotNetCompilerArguments.kt
package com.github.youta1119.kotlin.cli
import org.jetbrains.kotlin.cli.common.arguments.Argument
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
//コマンドライン引数の定義
class K2DotNetCompilerArguments: CommonCompilerArguments() {
// @Argumentアノテーションをフィールドにつけるとコマンドライン引数をパースした結果がフィールドに入る
@Argument(value = "-output", shortName = "-o", valueDescription = "<name>", description = "Output name")
var outputName: String? = null
}
次にCLI部を作成します。CLICompiler
というクラスを継承したK2DotNetCompiler
というクラスを作ります。
CLICompiler
はコマンドライン引数のパースをいい感じにやってくれるクラスです。
//K2DotNetCompiler.kt
package com.github.youta1119.kotlin.cli
import com.github.youta1119.kotlin.compiler.DotNetConfigurationKeys
import com.intellij.openapi.Disposable
import org.jetbrains.kotlin.cli.common.CLICompiler
import org.jetbrains.kotlin.cli.common.CommonCompilerPerformanceManager
import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.config.addKotlinSourceRoot
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.Services
import org.jetbrains.kotlin.metadata.deserialization.BinaryVersion
import org.jetbrains.kotlin.utils.KotlinPaths
class K2DotNetCompiler : CLICompiler<K2DotNetCompilerArguments>() {
override val performanceManager: CommonCompilerPerformanceManager by lazy {
K2DotNetCompilerPerformanceManager()
}
//コンパイラの処理を書くところ
override fun doExecute(
arguments: K2DotNetCompilerArguments,
configuration: CompilerConfiguration,
rootDisposable: Disposable,
paths: KotlinPaths?
): ExitCode {
return ExitCode.OK
}
//Platform特有の設定はここでやる
override fun setupPlatformSpecificArgumentsAndServices(
configuration: CompilerConfiguration,
arguments: K2DotNetCompilerArguments,
services: Services
) {
val commonSources = arguments.commonSources?.toSet().orEmpty()
arguments.freeArgs.forEach {
configuration.addKotlinSourceRoot(it, it in commonSources)
}
with(DotNetConfigurationKeys) {
with(configuration) {
arguments.outputName?.let { put(OUTPUT_NAME, it) }
}
}
}
override fun createArguments(): K2DotNetCompilerArguments =
K2DotNetCompilerArguments()
override fun createMetadataVersion(versionArray: IntArray): BinaryVersion =
K2DotNetMetadataVersion()
override fun executableScriptFileName(): String = "kotlinc-dotnet"
//よくわからないけど必要
class K2DotNetCompilerPerformanceManager : CommonCompilerPerformanceManager("Kotlin to .Net Compiler")
//よくわからないけど必要
class K2DotNetMetadataVersion(vararg numbers: Int) : BinaryVersion(*numbers) {
override fun isCompatible(): Boolean = false
}
companion object {
@JvmStatic
fun main(args: Array<String>) {
doMain(K2DotNetCompiler(), args)
}
}
}
これでコンパイラのCLI部分は完成です。今の段階で-help
オプションをつけて自作コンパイラを動かしてみるとこんな感じになります。
ちゃんとヘルプが表示されますね。
step3. コンパイラのベース部分作成
step3での変更点はこちら
CLIができたので、次にコンパイラのベース部分を作っていきます。
まずCommonBackendContext
を継承したDotNetBackendContext
を作成します。DotNetBackendContext
はコンパイル中の状態を保持するために使われるものです。
// DotNetBackendContext.kt
package com.github.youta1119.kotlin.compiler
import com.github.youta1119.kotlin.config.DotNetConfigurationKeys
import org.jetbrains.kotlin.backend.common.CommonBackendContext
import org.jetbrains.kotlin.backend.common.ir.DeclarationFactory
import org.jetbrains.kotlin.backend.common.ir.Ir
import org.jetbrains.kotlin.backend.common.ir.SharedVariablesManager
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.descriptors.IrBuiltIns
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.BindingContext
class DotNetBackendContext(val environment: KotlinCoreEnvironment, override val configuration: CompilerConfiguration) :
CommonBackendContext {
lateinit var moduleDescriptor: ModuleDescriptor
lateinit var bindingContext: BindingContext
val phaseConfig = configuration.get(CLIConfigurationKeys.PHASE_CONFIG)!!
val outputName = configuration.get(DotNetConfigurationKeys.OUTPUT_NAME) ?: DEFAULT_OUTPUT_NAME
override val builtIns: DotNetBuiltIns by lazy {
moduleDescriptor.builtIns as DotNetBuiltIns
}
override val irBuiltIns: IrBuiltIns
get() = TODO("not implemented")
override val sharedVariablesManager: SharedVariablesManager
get() = TODO("not implemented")
override val declarationFactory: DeclarationFactory
get() = TODO("not implemented")
override var inVerbosePhase: Boolean = false
override val internalPackageFqn: FqName
get() = TODO("not implemented")
override val ir: Ir<CommonBackendContext>
get() = TODO("not implemented")
override fun log(message: () -> String) {
if (inVerbosePhase) {
println(message())
}
}
override fun report(element: IrElement?, irFile: IrFile?, message: String, isError: Boolean) {
this.messageCollector.report(
if (isError) CompilerMessageSeverity.ERROR else CompilerMessageSeverity.WARNING,
message, null
)
}
private val messageCollector: MessageCollector
get() = configuration.getNotNull(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
companion object {
private const val DEFAULT_OUTPUT_NAME = "program"
}
}
次にコンパイラのフェーズを作っていきます。公式のKotlinコンパイラはフェーズという単位で処理を分割する設計になっているのでそれを真似します。とりあえず何もしないNoOpをいうフェーズを作ってみます。
//CompilerPhases.kt
package com.github.youta1119.kotlin.compiler
import org.jetbrains.kotlin.backend.common.phaser.*
import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport
import org.jetbrains.kotlin.config.languageVersionSettings
internal fun createUnitPhase(
name: String,
description: String,
prerequisite: Set<AnyNamedPhase> = emptySet(),
op: DotNetBackendContext.() -> Unit
) = namedOpUnitPhase(name, description, prerequisite, op)
// 何もしないフェーズ
internal val noOpPhase = createUnitPhase(
op = {
println("no op")
},
name = "NoOp",
description = "no-op"
)
//コンパイルに使うの全てのフェーズをまとめたフェーズ
val toplevelPhase: CompilerPhase<DotNetBackendContext, Unit, Unit> = namedUnitPhase(
name = "Compiler",
description = "The whole compilation process",
lower = noOpPhase
)
フェーズを作成したらtoplevelPhase
を呼び出すcompileToDotNetByteCode
関数を作成します。
またK2DotNetCompiler
のdoExecute
メソッドを修正してcompileToDotNetByteCode
を呼び出すようにします。
//compiler.kt
package com.github.youta1119.kotlin.compiler
import org.jetbrains.kotlin.backend.common.phaser.invokeToplevel
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.config.CompilerConfiguration
fun compileToDotNetByteCode(environment: KotlinCoreEnvironment, configuration: CompilerConfiguration) {
val context = DotNetBackendContext(environment, configuration)
toplevelPhase.invokeToplevel(context.phaseConfig, context, Unit)
}
// K2DotNetCompiler.kt
package com.github.youta1119.kotlin.cli
import com.github.youta1119.kotlin.compiler.compileToDotNetByteCode
import com.github.youta1119.kotlin.compiler.toplevelPhase
import com.github.youta1119.kotlin.config.DotNetConfigurationKeys
import com.intellij.openapi.Disposable
import org.jetbrains.kotlin.cli.common.*
import org.jetbrains.kotlin.cli.common.config.addKotlinSourceRoot
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
import org.jetbrains.kotlin.cli.common.messages.MessageUtil
import org.jetbrains.kotlin.cli.common.messages.OutputMessageUtil
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.codegen.CompilationException
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.Services
import org.jetbrains.kotlin.metadata.deserialization.BinaryVersion
import org.jetbrains.kotlin.utils.KotlinPaths
class K2DotNetCompiler : CLICompiler<K2DotNetCompilerArguments>() {
/*中略*/
//修正
override fun doExecute(
arguments: K2DotNetCompilerArguments,
configuration: CompilerConfiguration,
rootDisposable: Disposable,
paths: KotlinPaths?
): ExitCode {
// KotlinCoreEnvironment生成. コンパイラに渡されたソースファイルのパス情報とかを保持してるっぽい
val environment = KotlinCoreEnvironment.createForProduction(
rootDisposable,
configuration,
EnvironmentConfigFiles.NATIVE_CONFIG_FILES
)
val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY) ?: MessageCollector.NONE
// コンパイラのフェーズの設定
configuration.put(
CLIConfigurationKeys.PHASE_CONFIG,
createPhaseConfig(toplevelPhase, arguments, messageCollector)
)
if (environment.getSourceFiles().isEmpty()) {
if (arguments.version) return ExitCode.OK
messageCollector.report(CompilerMessageSeverity.ERROR, "No source files")
return ExitCode.COMPILATION_ERROR
}
return try {
//コンパイルの実行
compileToDotNetByteCode(environment, configuration)
ExitCode.OK
} catch (e: CompilationException) {
messageCollector.report(
CompilerMessageSeverity.EXCEPTION,
OutputMessageUtil.renderException(e),
MessageUtil.psiElementToMessageLocation(e.element)
)
ExitCode.INTERNAL_ERROR
}
}
/*中略*/
}
これでコンパイラのベース部分は完成です。 今の段階で-Xlist-phases
オプションをつけて自作コンパイラを動かしてみるとこんな感じになります。-Xlist-phases
はコンパイラの全フェーズを表示する開発者向けのオプションです。
ちゃんと今回作成したNoOp Phaseが表示されますね。
step4. Astを作成する
step4での変更点はこちら
コンパイラのベース部分ができたので、ソースコードからAstに生成する部分を作っていきます。
Astとはコードをパースした抽象構文木のことでKotlinの場合はPsiという形式で表現されています。
まずソースコードをパースしてAstに生成するTopDownAnalyzerFacadeForDotNet
を作ります。
// TopDownAnalyzerFacadeForDotNet.kt
package com.github.youta1119.kotlin.compiler
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.analyzer.AnalysisResult
import org.jetbrains.kotlin.cli.common.config.KotlinSourceRoot
import org.jetbrains.kotlin.cli.jvm.compiler.createSourceFilesFromSourceRoots
import org.jetbrains.kotlin.config.*
import org.jetbrains.kotlin.context.ContextForNewModule
import org.jetbrains.kotlin.context.ModuleContext
import org.jetbrains.kotlin.context.ProjectContext
import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.*
import org.jetbrains.kotlin.resolve.lazy.declarations.FileBasedDeclarationProviderFactory
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.streams.toList
object TopDownAnalyzerFacadeForDotNet {
// ソースコードからAstを生成する
@JvmStatic
fun analyzeFiles(
project: Project,
configuration: CompilerConfiguration,
files: Collection<KtFile>
): AnalysisResult {
val projectContext = ProjectContext(project, "TopDownAnalyzer for DotNet")
val builtIns = DotNetBuiltIns(projectContext.storageManager)
// <main>という名前のモジュールを作成
// モジュールという単位でソースコードをパースしていく
val context = ContextForNewModule(
projectContext,
Name.special("<main>"),
builtIns, null
)
val module = context.module
builtIns.builtInsModule = module
context.setDependencies(module)
return analyzeFilesWithGivenTrace(files, BindingTraceContext(), context,configuration)
}
// ソースコードからAstを変換する
private fun analyzeFilesWithGivenTrace(
files: Collection<KtFile>,
trace: BindingTrace,
moduleContext: ModuleContext,
configuration: CompilerConfiguration
): AnalysisResult {
// we print out each file we compile for now
files.forEach { println(it) }
// パーサインスタンス生成
val analyzerForKonan = createTopDownAnalyzerForDotNet(
moduleContext, trace,
FileBasedDeclarationProviderFactory(moduleContext.storageManager, files),
configuration.get(CommonConfigurationKeys.LANGUAGE_VERSION_SETTINGS)!!
)
// ソースコード->Astへの変換する
analyzerForKonan.analyzeDeclarations(TopDownAnalysisMode.TopLevelDeclarations, files)
return AnalysisResult.success(trace.bindingContext, moduleContext.module)
}
}
// DotNetPlatform.kt
//よくわからないけど必要っぽい
package com.github.youta1119.kotlin.compiler
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.platform.konan.KonanPlatform
import org.jetbrains.kotlin.platform.konan.KonanPlatforms
import org.jetbrains.kotlin.storage.StorageManager
class DotNetBuiltIns(storageManager: StorageManager) : KotlinBuiltIns(storageManager)
@Suppress("DEPRECATION_ERROR")
object DotNetPlatform {
private object DefaultSimpleDotNetPlatform : KonanPlatform()
@Deprecated(
message = "Should be accessed only by compatibility layer, other clients should use 'defaultDotNetPlatform'",
level = DeprecationLevel.ERROR
)
object CompatKonanPlatform : TargetPlatform(setOf(DefaultSimpleDotNetPlatform)),
org.jetbrains.kotlin.resolve.TargetPlatform {
override val platformName: String
get() = "DotNet"
}
val defaultDotNetPlatform: TargetPlatform
get() = KonanPlatforms.CompatKonanPlatform
}
// DotNetPlatformAnalyzerServices.kt
//これもよくわからないけど必要っぽい
package com.github.youta1119.kotlin.compiler
import org.jetbrains.kotlin.container.StorageComponentContainer
import org.jetbrains.kotlin.resolve.*
import org.jetbrains.kotlin.resolve.checkers.ExpectedActualDeclarationChecker
import org.jetbrains.kotlin.resolve.jvm.checkers.SuperCallWithDefaultArgumentsChecker
import org.jetbrains.kotlin.storage.StorageManager
object DotNetPlatformConfigurator : PlatformConfiguratorBase(
additionalDeclarationCheckers = listOf(
ExpectedActualDeclarationChecker(
ModuleStructureOracle.SingleModule,
emptyList()
)
),
additionalCallCheckers = listOf(SuperCallWithDefaultArgumentsChecker())
) {
override fun configureModuleComponents(container: StorageComponentContainer) {
}
}
object DotNetPlatformAnalyzerServices : PlatformDependentAnalyzerServices() {
override val platformConfigurator: PlatformConfigurator =
DotNetPlatformConfigurator
override fun computePlatformSpecificDefaultImports(
storageManager: StorageManager,
result: MutableList<ImportPath>
) {
//no-op
}
}
//injection.kt
package com.github.youta1119.kotlin.compiler
import org.jetbrains.kotlin.config.LanguageVersionSettings
import org.jetbrains.kotlin.container.*
import org.jetbrains.kotlin.context.ModuleContext
import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl
import org.jetbrains.kotlin.frontend.di.configureModule
import org.jetbrains.kotlin.frontend.di.configureStandardResolveComponents
import org.jetbrains.kotlin.incremental.components.LookupTracker
import org.jetbrains.kotlin.resolve.*
import org.jetbrains.kotlin.resolve.lazy.FileScopeProviderImpl
import org.jetbrains.kotlin.resolve.lazy.KotlinCodeAnalyzer
import org.jetbrains.kotlin.resolve.lazy.ResolveSession
import org.jetbrains.kotlin.resolve.lazy.declarations.DeclarationProviderFactory
// ソースコードのパーサインスタンス生成のためにDI設定
// Kotlin/Nativeのコンパイラの実装を参考にしながら設定したらなんか動いた
fun createTopDownAnalyzerForDotNet(
moduleContext: ModuleContext,
bindingTrace: BindingTrace,
declarationProviderFactory: DeclarationProviderFactory,
languageVersionSettings: LanguageVersionSettings
): LazyTopDownAnalyzer {
val storageComponentContainer = createContainer("TopDownAnalyzerForDotNet", DotNetPlatformAnalyzerServices) {
configureModule(
moduleContext,
DotNetPlatform.defaultDotNetPlatform,
DotNetPlatformAnalyzerServices,
bindingTrace,
languageVersionSettings
)
configureStandardResolveComponents()
useInstance(declarationProviderFactory)
useImpl<FileScopeProviderImpl>()
CompilerEnvironment.configure(this)
useInstance(LookupTracker.DO_NOTHING)
//useInstance(languageVersionSettings)
}.apply {
get<ModuleDescriptorImpl>().initialize(get<KotlinCodeAnalyzer>().packageFragmentProvider)
}
return storageComponentContainer.get()
}
次にソースコードからAstを生成するFrontendフェーズを作成します。
internal val frontendPhase = createUnitPhase(
op = {
val environment = environment
val analyzerWithCompilerReport = AnalyzerWithCompilerReport(
messageCollector,
environment.configuration.languageVersionSettings
)
// Astの生成
analyzerWithCompilerReport.analyzeAndReport(environment.getSourceFiles()) {
TopDownAnalyzerFacadeForDotNet.analyzeFiles(
environment.project,
configuration,
environment.getSourceFiles()
)
}
// ソースコードに構文エラーやimportのエラーがあった場合はここでエラーになる
if (analyzerWithCompilerReport.hasErrors()) {
throw DotNetCompilationException()
}
//Astの情報が入ってる
moduleDescriptor = analyzerWithCompilerReport.analysisResult.moduleDescriptor
bindingContext = analyzerWithCompilerReport.analysisResult.bindingContext
},
name = "Frontend",
description = "Frontend builds Ast"
)
これでいったんstep4は完了です。
step5. 標準ライブラリの定義
step5での変更点はこちら
step4の状態で以下のようなソースコードをコンパイルしようとするとコンパイルエラーになってしまいます。
fun main() {
println("hello")
}
コンパイルエラーになるのは標準ライブラリが存在しないからです。
なので、標準ライブラリを実装してやる必要があります。
ただ、コンパイルエラーを消すだけなら標準ライブラリをすべて実装する必要はなく、クラス定義だけあればとりあえず動きます。toString
みたいな実装しないといけないメソッドにはexternal
修飾子をつけておけば怒られなくなるのでとりあえずつけておきます。
//Array.kt
package kotlin
// ArrayというクラスさえあればOK
public final class Array<T> {
}
// Any.kt
package kotlin
public open class Any {
// external修飾子をつけると実装してなくても怒られない
external public open operator fun equals(other: Any?): Boolean
//external public open fun hashCode(): Int
external open fun toString(): String
}
標準ライブラリの定義が終わったらTopDownAnalyzerFacadeForDotNet
でAstを生成する際に標準ライブラリも一緒に含めるように修正します。
object TopDownAnalyzerFacadeForDotNet {
// ソースコードからAstを生成する
@JvmStatic
fun analyzeFiles(
project: Project,
configuration: CompilerConfiguration,
files: Collection<KtFile>
): AnalysisResult {
val projectContext = ProjectContext(project, "TopDownAnalyzer for DotNet")
val builtIns = DotNetBuiltIns(projectContext.storageManager)
// <main>という名前のモジュールを作成
// モジュールという単位でソースコードをパースしていく
val context = ContextForNewModule(
projectContext,
Name.special("<main>"),
builtIns, null
)
val module = context.module
builtIns.builtInsModule = module
//修正
context.setDependencies(listOf(module) + loadStdlibModules(projectContext, configuration))
return analyzeFilesWithGivenTrace(files, BindingTraceContext(), context,configuration)
}
/*中略*/
//追加
//標準ライブラリの読み込み
private fun loadStdlibModules(
projectContext: ProjectContext,
configuration: CompilerConfiguration
): ModuleDescriptorImpl {
val stdlibFiles = Files.walk(Paths.get(Distribution.stdlibDir))
.filter { it.toFile().extension == "kt" }
.map { KotlinSourceRoot(it.toString(), false) }.toList()
val ktFiles = createSourceFilesFromSourceRoots(configuration, projectContext.project, stdlibFiles)
val builtIns = DotNetBuiltIns(projectContext.storageManager)
// <stdlib>モジュールとして標準ライブラリを読み込み
val context = ContextForNewModule(
projectContext,
STDLIB_MODULE_NAME,
builtIns, null
)
val module = context.module
builtIns.builtInsModule = module
context.setDependencies(module)
val analyzerForKonan = createTopDownAnalyzerForDotNet(
context, BindingTraceContext(),
FileBasedDeclarationProviderFactory(context.storageManager, ktFiles),
configuration.get(CommonConfigurationKeys.LANGUAGE_VERSION_SETTINGS)!!
)
analyzerForKonan.analyzeDeclarations(TopDownAnalysisMode.TopLevelDeclarations, ktFiles)
return context.module
}
private val STDLIB_MODULE_NAME = Name.special("<stdlib>")
}
step6. AstとKotlin IRに変換する
step6での変更点はこちら
ソースコードをパースしてAst(Psi)に変換することができたので、次はAstをIR(中間表現)に変換するPsiToIrフェーズを作っていきます。ついでに変換したIRを見るためにDumpIrフェーズを作ります。
Astには=,-,+,/
などの余分な構文情報を含んでいるため、余分な構文情報を取り除いて抽象度を上げるため、AstをIR(中間表現)に変換します。IR(中間表現)はASTから余分な構文情報を取り除いて抽象度を上げたものです。
//CompilerPhases.kt
// PsiをIRに変換するフェーズ
internal val psiToIrPhase = createUnitPhase(
op = {
// Psi2IrTranslatorを使うとPsiとIRに変換できるっぽい
// 動いているが、Kotlinコンパイラのソースをコピペしたのでよくわからない
val translator = Psi2IrTranslator(environment.configuration.languageVersionSettings, Psi2IrConfiguration(false))
val symbolTable = SymbolTable()
val generatorContext = translator.createGeneratorContext(
moduleDescriptor,
bindingContext,
symbolTable,
GeneratorExtensions()
)
// PsiをIRに変換
val module = translator.generateModuleFragment(generatorContext, environment.getSourceFiles())
irModule = module
},
name = "Psi2Ir",
description = "Psi to IR conversion"
)
// IRをdumpするフェーズ
internal val dumpIrPhase = createUnitPhase(
op = {
println(irModule.dump())
},
name = "DumpIr",
description = "dump ir"
)
これでAstとKotlin IRに変換する部分は完成です。この状態で自作コンパイラを実行すると以下のような感じで変換されてIRが表示されます。
step7. Kotlin IRを.Net ByteCodeに変換する
step7での変更点はこちら
IRを生成することができたのでIRの情報を使って.Netアセンブリを生成するIrToDotNetAsmフェーズと.Netアセンブリとexeに変換するGenerateByteCodeフェーズを作っていきます。
internal val irToCILPhase = createUnitPhase(
op = {
// IRの情報を使って.Netアセンブリ生成
val pw = File(tempFileName).printWriter()
pw.println(".assembly extern mscorlib {}")
pw.println(".assembly main {}")
irModule.acceptChildrenVoid(object : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
override fun visitFunction(declaration: IrFunction) {
assert(declaration.name.asString() == "main") {
"this is toy compiler. only main function can be compiled."
}
pw.println(".method static void Main() cil managed {")
pw.println(".entrypoint")
val body = declaration.body!! as IrBlockBody
body.statements.forEach { statement ->
val callExpr = statement as IrCall
val calleeFunctionExpr = callExpr.symbol.owner.target
assert(calleeFunctionExpr.fqNameForIrSerialization.asString() == "kotlin.io.println") {
"this is toy compiler. only kotlin.io.println function can be called."
}
val args = (callExpr as IrMemberAccessExpression).getArguments()
args.forEach { (_, expr) ->
if (expr is IrConst<*>) {
pw.println("ldstr \"${expr.value}\"")
}
}
//println関数の呼び出しを.NetのSystem.Console::WriteLine関数呼び出しに変換
pw.println("call void [mscorlib]System.Console::WriteLine(string)")
}
pw.println("ret")
pw.println("}")
}
})
pw.close()
},
name = "IrToDotNetAsm",
description = "IR to .Net assembly conversion"
)
internal val generateBytecodePhase = createUnitPhase(
op = {
// ilasmコマンドを実行してアセンブリをexeに変換
val command = listOf("ilasm") + listOf("/output:${outputFileName}", tempFileName)
val builder = ProcessBuilder(command)
builder.redirectOutput(ProcessBuilder.Redirect.INHERIT)
builder.redirectInput(ProcessBuilder.Redirect.INHERIT)
builder.redirectError(ProcessBuilder.Redirect.INHERIT)
val process = builder.start()
val exitCode = process.waitFor()
if (exitCode != 0) {
throw DotNetCompilationException("ilasm command returned non-zero exit code: $exitCode.")
}
},
name = "GenerateByteCode",
description = "Generate .Net Bytecode from .Net assembly"
)
これでKotlinのプログラムを.Netのexeに変換するコンパイラの完成です。
.Netアセンブリの解説はまた今度追記します。
終わりに
Kotlinではソースコードのパーサーはライブラリ形式で配布されているため比較的少ないコード量でコンパイラを作ることができます。ちなみにこのコンパイラのコード量は570行でした しかし、Kotlinコンパイラを自作したい物好きな人はあまり居ないのかKotlinコンパイラに関するドキュメントは存在せず、色々ハマったのでこの記事を書きました。
理論上は今回解説したやり方で、コンパイラを書けばどの環境でもKotlinを動かすことができます。 この記事を読んで自作Kotlinコンパイラに興味をもってくださった方がいれば幸いです。