Implemented preview of the prototype of the Kotlin Coverage plugin
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/build.gradle.kts b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/build.gradle.kts
new file mode 100644
index 0000000..eedb904
--- /dev/null
+++ b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+    id("gradle-plugin-common-configuration")
+}
+
+dependencies {
+    commonApi(platform(project(":kotlin-gradle-plugins-bom")))
+}
+
+gradlePlugin {
+    plugins {
+        create("kotlinCoverage") {
+            id = "org.jetbrains.kotlin.plugin.coverage"
+            displayName = "Kotlin compiler plugin for collecting test coverage"
+            description = displayName
+            implementationClass = "org.jetbrains.kotlin.coverage.CoverageCompilerSubplugin"
+        }
+    }
+}
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/kotlin/org/jetbrains/kotlin/coverage/CoverageCompilerSubplugin.kt b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/kotlin/org/jetbrains/kotlin/coverage/CoverageCompilerSubplugin.kt
new file mode 100644
index 0000000..ed337b0
--- /dev/null
+++ b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/kotlin/org/jetbrains/kotlin/coverage/CoverageCompilerSubplugin.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.coverage
+
+import org.gradle.api.Project
+import org.gradle.api.provider.Provider
+import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
+import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin
+import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact
+import org.jetbrains.kotlin.gradle.plugin.SubpluginOption
+
+class CoverageCompilerSubplugin : KotlinCompilerPluginSupportPlugin {
+    override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true
+
+    override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider<List<SubpluginOption>> {
+        return kotlinCompilation.target.project.provider { emptyList() }
+    }
+
+    override fun apply(target: Project) {
+        super.apply(target)
+    }
+
+    override fun getCompilerPluginId(): String = "org.jetbrains.kotlin.coverage"
+
+    override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact(GROUP_NAME, ARTIFACT_NAME)
+
+    companion object {
+        const val GROUP_NAME = "org.jetbrains.kotlin"
+        const val ARTIFACT_NAME = "coverage-compiler-plugin-embeddable"
+    }
+}
\ No newline at end of file
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/resources/META-INF/services/org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/resources/META-INF/services/org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin
new file mode 100644
index 0000000..d9e4d09
--- /dev/null
+++ b/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin/src/common/resources/META-INF/services/org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin
@@ -0,0 +1 @@
+org.jetbrains.kotlin.coverage.CoverageCompilerSubplugin
\ No newline at end of file
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/build.gradle.kts b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/build.gradle.kts
new file mode 100644
index 0000000..d0eba2d
--- /dev/null
+++ b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+    kotlin("jvm")
+}
+
+kotlin {
+    explicitApi()
+}
+
+configureKotlinCompileTasksGradleCompatibility()
+
+publish()
+
+standardPublicJars()
+
+dependencies {
+    // remove stdlib dependency from api artifact in order not to affect the dependencies of the user project
+    val coreDepsVersion = libs.versions.kotlin.`for`.gradle.plugins.compilation.get()
+    compileOnly(kotlin("stdlib", coreDepsVersion))
+}
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/FileRoutines.kt b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/FileRoutines.kt
new file mode 100644
index 0000000..319d794
--- /dev/null
+++ b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/FileRoutines.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.coverage.runtime
+
+import java.io.File
+import java.io.OutputStream
+import kotlin.experimental.or
+
+// expect-actual
+public object FileRoutines {
+    public fun writeToFile(filePath: String, block: Writer.() -> Unit) {
+        val file = File(filePath)
+        file.parentFile.mkdirs()
+        file.outputStream().buffered().use {
+            WriterActual(it).block()
+        }
+    }
+
+
+}
+
+// expect
+public interface Writer {
+    public fun writeInt(value: Int)
+    public fun writeBooleanArray(array: BooleanArray)
+}
+
+// actual
+private class WriterActual(val output: OutputStream) : Writer {
+    override fun writeInt(value: Int) {
+        output.write(value and 0xFF000000.toInt() shr 24)
+        output.write(value and 0xFF0000 shr 16)
+        output.write(value and 0xFF00 shr 8)
+        output.write(value and 0xFF)
+    }
+
+    override fun writeBooleanArray(array: BooleanArray) {
+        val bytesSize = array.size / 8 + if (array.size % 8 > 0) 1 else 0
+        val result = ByteArray(bytesSize) { 0 }
+        array.forEachIndexed { index, value ->
+            val byteIndex = index / 8
+            val bitIndex = index % 8
+            if (value) {
+                result[byteIndex] = result[byteIndex] or (1 shl bitIndex).toByte()
+            }
+        }
+        output.write(result)
+    }
+}
+
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitStorage.kt b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitStorage.kt
new file mode 100644
index 0000000..ca7f20e
--- /dev/null
+++ b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitStorage.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.coverage.runtime
+
+import kotlin.random.Random
+
+// expect-actual
+public object BooleanHitStorage {
+    private val segments: MutableMap<Long, BooleanArray> = mutableMapOf()
+
+    public fun reset() {
+        synchronized(this) {
+            segments.clear()
+        }
+
+    }
+
+    public fun getOrCreateSegment(moduleId: Int, segmentNumber: Int, size: Int): BooleanArray {
+        val id = (moduleId.toLong() shl 32) or segmentNumber.toLong()
+
+        synchronized(this) {
+            if (segments.isEmpty()) {
+                initShutdownHook()
+            }
+
+            segments[id]?.let { return it }
+
+            val array = BooleanArray(size)
+            segments[id] = array
+            return array
+        }
+    }
+
+    public fun saveHits() {
+        val dir = (System.getProperties()["kotlin.coverage.executions.path"] as? String) ?: "kover"
+        val id = Random.nextLong()
+        HitWriter.writeBoolean("$dir/coverage-$id.kex", segments)
+    }
+
+    private fun initShutdownHook() {
+        Runtime.getRuntime().addShutdownHook(Thread {
+            saveHits()
+        })
+    }
+}
+
+
diff --git a/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitWriter.kt b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitWriter.kt
new file mode 100644
index 0000000..259b90e
--- /dev/null
+++ b/libraries/tools/kotlin-coverage/kotlin-coverage-runtime/src/main/kotlin/org/jetbrains/kotlin/coverage/runtime/HitWriter.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.coverage.runtime
+
+public object HitWriter {
+    public fun writeBoolean(filePath: String, segments: Map<Long, BooleanArray>) {
+        FileRoutines.writeToFile(filePath) {
+            writeInt(100500)
+            writeInt(segments.size)
+            for ((id, array) in segments) {
+                val moduleId = (id shr 32).toInt()
+                val segmentNumber = id.toInt()
+                writeInt(moduleId)
+                writeInt(segmentNumber)
+                writeInt(array.size)
+                writeBooleanArray(array)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/kotlinTopLevelExtensionConfig.kt b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/kotlinTopLevelExtensionConfig.kt
index f262c32..f7a5e64 100644
--- a/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/kotlinTopLevelExtensionConfig.kt
+++ b/libraries/tools/kotlin-gradle-plugin-api/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/kotlinTopLevelExtensionConfig.kt
@@ -5,6 +5,8 @@
 
 package org.jetbrains.kotlin.gradle.dsl
 
+import org.gradle.api.provider.Property
+
 /**
  * A plugin DSL extension for configuring common options for the entire project.
  *
@@ -54,6 +56,17 @@
      * Sets [explicitApi] option to report issues as warnings.
      */
     fun explicitApiWarning()
+
+    fun coverage(block: CoverageConfig.() -> Unit) {
+        this.coverage.apply(block)
+    }
+
+    val coverage: CoverageConfig
+        get() = error("Not implemented yet")
+}
+
+interface CoverageConfig {
+    val enabled: Property<Boolean>
 }
 
 /**
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt
index 199260a..896474c 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinMultiplatformExtension.kt
@@ -275,6 +275,8 @@
             .also {
                 syncCommonMultiplatformOptions(it)
             }
+
+    override val coverage: CoverageConfig = project.objects.newInstance(CoverageConfig::class.java).also { it.enabled.convention(false) }
 }
 
 private const val targetsExtensionDeprecationMessage =
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt
index 84b73e3..a1b11e4 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/dsl/KotlinProjectExtension.kt
@@ -206,6 +206,8 @@
     }
 
     override val publishing: KotlinPublishing = KotlinJvmPublishingDsl(project)
+
+    override val coverage: CoverageConfig = project.objects.newInstance(CoverageConfig::class.java).also { it.enabled.convention(false) }
 }
 
 private class KotlinJvmPublishingDsl(private val project: Project) : KotlinPublishing {
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/coverage/KotlinCoverageAction.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/coverage/KotlinCoverageAction.kt
new file mode 100644
index 0000000..a09c548
--- /dev/null
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/coverage/KotlinCoverageAction.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.gradle.plugin.coverage
+
+import org.gradle.api.tasks.testing.Test
+import org.jetbrains.kotlin.gradle.dsl.kotlinJvmExtensionOrNull
+import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle
+import org.jetbrains.kotlin.gradle.plugin.KotlinProjectSetupCoroutine
+import org.jetbrains.kotlin.gradle.plugin.await
+import org.jetbrains.kotlin.gradle.plugin.getKotlinPluginVersion
+import org.jetbrains.kotlin.gradle.plugin.mpp.internal
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import org.jetbrains.kotlin.gradle.utils.withType
+
+internal val KotlinCoverageAction = KotlinProjectSetupCoroutine {
+    KotlinPluginLifecycle.Stage.AfterEvaluateBuildscript.await()
+
+    val jvmExtension = kotlinJvmExtensionOrNull
+    val multiplatformExtension = multiplatformExtensionOrNull
+    if (jvmExtension == null && multiplatformExtension == null) return@KotlinProjectSetupCoroutine
+
+    val kgpVersion = getKotlinPluginVersion()
+
+    jvmExtension?.target?.compilations?.all { compilation ->
+        val dependencyProvider = provider {
+            if (jvmExtension.coverage.enabled.get()) {
+                listOf(dependencies.create("org.jetbrains.kotlin:coverage-compiler-plugin-embeddable:$kgpVersion"))
+            } else {
+                emptyList()
+            }
+        }
+        compilation.internal.configurations.pluginConfiguration.dependencies.addAllLater(dependencyProvider)
+    }
+
+    multiplatformExtension?.targets
+        ?.filter { it.platformType == KotlinPlatformType.jvm || it.platformType == KotlinPlatformType.androidJvm }
+        ?.forEach { target ->
+            target.compilations.all { compilation ->
+                if (compilation.name.contains("test", ignoreCase = true)) return@all
+
+                val dependencyProvider = provider {
+                    if (multiplatformExtension.coverage.enabled.get()) {
+                        listOf(dependencies.create("org.jetbrains.kotlin:coverage-compiler-plugin-embeddable:$kgpVersion"))
+                    } else {
+                        emptyList()
+                    }
+                }
+                compilation.internal.configurations.pluginConfiguration.dependencies.addAllLater(dependencyProvider)
+            }
+        }
+
+    tasks.withType<KotlinCompile>().configureEach { compileTask ->
+        compileTask.pluginClasspath.from()
+    }
+
+    KotlinPluginLifecycle.Stage.AfterEvaluateBuildscript.await()
+
+
+    if (jvmExtension != null) {
+        dependencies.add("implementation", "org.jetbrains.kotlin:coverage-runtime:$kgpVersion")
+    } else if (multiplatformExtension != null) {
+        dependencies.add("jvmMainImplementation", "org.jetbrains.kotlin:coverage-runtime:$kgpVersion")
+    } else {
+        return@KotlinProjectSetupCoroutine
+    }
+
+    tasks.withType<Test>().configureEach { testTask ->
+        val reportPath = layout.buildDirectory.dir("kover/exec/${testTask.name}").get().asFile.absolutePath
+        testTask.systemProperty("kotlin.coverage.executions.path", reportPath)
+        testTask.doFirst {
+            file(reportPath).deleteRecursively()
+        }
+    }
+    tasks.withType<KotlinCompile>().configureEach { compileTask ->
+        compileTask.pluginClasspath.from()
+
+        val moduleName = compileTask.compilerOptions.moduleName.get()
+        val metadataFilePath = layout.buildDirectory.file("kover/metadata/${moduleName}.kim").get().asFile.absolutePath
+        compileTask.compilerOptions.freeCompilerArgs.addAll(
+            "-P", "plugin:org.jetbrains.kotlin.coverage:modulePath=${projectDir.absolutePath}",
+            "-P", "plugin:org.jetbrains.kotlin.coverage:metadataFilePath=$metadataFilePath"
+        )
+    }
+}
\ No newline at end of file
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/registerKotlinPluginExtensions.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/registerKotlinPluginExtensions.kt
index f1634e3..dd98c3e 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/registerKotlinPluginExtensions.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/registerKotlinPluginExtensions.kt
@@ -11,6 +11,7 @@
 import org.jetbrains.kotlin.gradle.internal.CustomizeKotlinDependenciesSetupAction
 import org.jetbrains.kotlin.gradle.plugin.PropertiesProvider.Companion.kotlinPropertiesProvider
 import org.jetbrains.kotlin.gradle.plugin.abi.AbiValidationSetupAction
+import org.jetbrains.kotlin.gradle.plugin.coverage.KotlinCoverageAction
 import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinGradleProjectChecker
 import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnosticsSetupAction
 import org.jetbrains.kotlin.gradle.plugin.diagnostics.checkers.*
@@ -71,6 +72,7 @@
         if (isAbiValidationEnabled) {
             register(project, AbiValidationSetupAction)
         }
+        register(project, KotlinCoverageAction)
 
         if (isJvm || isMultiplatform) {
             register(project, ScriptingGradleSubpluginSetupAction)
diff --git a/plugins/coverage/coverage-compiler-embeddable/build.gradle.kts b/plugins/coverage/coverage-compiler-embeddable/build.gradle.kts
new file mode 100644
index 0000000..9e473d1
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-embeddable/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+    kotlin("jvm")
+}
+
+dependencies {
+    embedded(project(":coverage-compiler-plugin")) { isTransitive = false }
+}
+
+publish()
+
+runtimeJar(rewriteDefaultJarDepsToShadedCompiler())
+sourcesJarWithSourcesFromEmbedded(
+    project(":coverage-compiler-plugin").tasks.named<Jar>("sourcesJar")
+)
+javadocJarWithJavadocFromEmbedded(
+    project(":coverage-compiler-plugin").tasks.named<Jar>("javadocJar")
+)
diff --git a/plugins/coverage/coverage-compiler-plugin/build.gradle.kts b/plugins/coverage/coverage-compiler-plugin/build.gradle.kts
new file mode 100644
index 0000000..260e9f9
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/build.gradle.kts
@@ -0,0 +1,29 @@
+description = "Kotlin Serialization Compiler Plugin (CLI)"
+
+plugins {
+    kotlin("jvm")
+    id("jps-compatible")
+}
+
+dependencies {
+    compileOnly(project(":compiler:util"))
+    compileOnly(project(":compiler:cli"))
+    compileOnly(project(":compiler:ir.backend.common"))
+    compileOnly(project(":compiler:plugin-api"))
+    compileOnly(project(":compiler:ir.tree"))
+    compileOnly(project(":compiler:fir:entrypoint"))
+    compileOnly(project(":kotlin-util-klib-metadata"))
+
+    compileOnly(intellijCore())
+}
+
+optInToExperimentalCompilerApi()
+
+sourceSets {
+    "main" { projectDefault() }
+    "test" { none() }
+}
+
+runtimeJar()
+sourcesJar()
+javadocJar()
diff --git a/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor b/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
new file mode 100644
index 0000000..1e529d1
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
@@ -0,0 +1 @@
+org.jetbrains.kotlin.coverage.compiler.extensions.KotlinCoveragePluginOptions
\ No newline at end of file
diff --git a/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar b/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
new file mode 100644
index 0000000..6459782
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
@@ -0,0 +1,17 @@
+#
+# Copyright 2010-2017 JetBrains s.r.o.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.jetbrains.kotlin.coverage.compiler.extensions.KotlinCoveragePluginRegistrar
\ No newline at end of file
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/common/Context.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/common/Context.kt
new file mode 100644
index 0000000..5dd10f8
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/common/Context.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+@file:OptIn(InternalSymbolFinderAPI::class, UnsafeDuringIrConstructionAPI::class)
+
+package org.jetbrains.kotlin.coverage.compiler.common
+
+import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
+import org.jetbrains.kotlin.coverage.compiler.common.Constants.BOOLEAN_STORAGE_NAME
+import org.jetbrains.kotlin.coverage.compiler.common.Constants.CREATE_BOOLEAN_SEGMENT_NAME
+import org.jetbrains.kotlin.coverage.compiler.common.Constants.KOTLIN_COVERAGE_DECLARATION_ORIGIN
+import org.jetbrains.kotlin.coverage.compiler.common.Constants.KOTLIN_COVERAGE_STATEMENT_ORIGIN
+import org.jetbrains.kotlin.coverage.compiler.common.Constants.LET_NAME
+import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
+import org.jetbrains.kotlin.descriptors.Modality
+import org.jetbrains.kotlin.ir.InternalSymbolFinderAPI
+import org.jetbrains.kotlin.ir.IrStatement
+import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
+import org.jetbrains.kotlin.ir.declarations.*
+import org.jetbrains.kotlin.ir.declarations.impl.IrVariableImpl
+import org.jetbrains.kotlin.ir.expressions.*
+import org.jetbrains.kotlin.ir.expressions.impl.*
+import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
+import org.jetbrains.kotlin.ir.symbols.IrReturnTargetSymbol
+import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
+import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
+import org.jetbrains.kotlin.ir.symbols.impl.IrSimpleFunctionSymbolImpl
+import org.jetbrains.kotlin.ir.symbols.impl.IrVariableSymbolImpl
+import org.jetbrains.kotlin.ir.types.IrType
+import org.jetbrains.kotlin.ir.types.defaultType
+import org.jetbrains.kotlin.ir.util.defaultType
+import org.jetbrains.kotlin.ir.util.functions
+import org.jetbrains.kotlin.ir.util.substitute
+import org.jetbrains.kotlin.name.CallableId
+import org.jetbrains.kotlin.name.ClassId
+import org.jetbrains.kotlin.name.FqName
+import org.jetbrains.kotlin.name.Name
+
+internal class KotlinCoverageInstrumentationContext(pluginContext: IrPluginContext) {
+    val irFactory = pluginContext.irFactory
+
+    val declarationOrigin = KOTLIN_COVERAGE_DECLARATION_ORIGIN
+    val statementOrigin = KOTLIN_COVERAGE_STATEMENT_ORIGIN
+
+    val builtIns = KotlinBuiltIns(pluginContext)
+    val runtime = CoverageRuntime(pluginContext)
+    val factory = Factory(builtIns, irFactory)
+}
+
+class KotlinBuiltIns(private val pluginContext: IrPluginContext) {
+    val unitType = pluginContext.irBuiltIns.unitType
+    val unitClass = pluginContext.irBuiltIns.unitClass
+    val nothingType = pluginContext.irBuiltIns.nothingType
+    val stringType = pluginContext.irBuiltIns.stringType
+    val intType = pluginContext.irBuiltIns.intType
+    val booleanType = pluginContext.irBuiltIns.booleanType
+    val arrayOfPrimitiveBooleansClass = pluginContext.irBuiltIns.primitiveArrayForType.getValue(pluginContext.irBuiltIns.booleanType)
+    val primitiveArrayOfBooleanType = arrayOfPrimitiveBooleansClass.defaultType
+
+    val function0 = pluginContext.irBuiltIns.functionN(0)
+    val function0InvokeFun = function0.functions.firstOrNull { it.name.asString() == "invoke" }?.symbol
+        ?: throw IllegalStateException("Can't find function 'invoke' in the class 'kotlin.Function0'")
+
+    val letFun = pluginContext.referenceFunctions(LET_NAME).firstOrNull()
+        ?: throw IllegalStateException("Can't find built-in function '${LET_NAME.asSingleFqName().asString()}'")
+
+    val primitiveArrayOfBooleansSetter = arrayOfPrimitiveBooleansClass.functions.firstOrNull { it.owner.name.asString() == "set" }
+        ?: throw IllegalStateException("Can't find function set for primitive byte array")
+
+    fun functionNType(returnType: IrType, types: List<IrType>): IrType {
+        val irClass = pluginContext.irBuiltIns.functionN(types.size)
+
+        val typeParams = irClass.typeParameters
+        return irClass.defaultType.substitute(typeParams, listOf(returnType) + types)
+    }
+
+
+}
+
+class CoverageRuntime(pluginContext: IrPluginContext) {
+    val booleanStorageClass = pluginContext.referenceClass(BOOLEAN_STORAGE_NAME)
+        ?: throw IllegalStateException("Can't find class '${BOOLEAN_STORAGE_NAME.asString()}'")
+    val getOrCreateBooleanSegmentFun = pluginContext.irBuiltIns.symbolFinder.findFunctions(CREATE_BOOLEAN_SEGMENT_NAME).firstOrNull()
+        ?: throw IllegalStateException("Can't find function '${CREATE_BOOLEAN_SEGMENT_NAME.asSingleFqName().asString()}'")
+}
+
+class Factory(private val builtIns: KotlinBuiltIns, private val irFactory: IrFactory) {
+    fun call(function: IrSimpleFunctionSymbol, returnType: IrType, block: IrCall.() -> Unit = {}): IrCall {
+        return IrCallImpl(
+            UNDEFINED_OFFSET,
+            UNDEFINED_OFFSET,
+            returnType,
+            function,
+            origin = KOTLIN_COVERAGE_STATEMENT_ORIGIN
+        ).also { call ->
+            call.block()
+        }
+    }
+
+    fun `val`(name: Name, type: IrType, parent: IrDeclarationParent, initializer: IrExpression? = null): IrVariable {
+        return IrVariableImpl(
+            UNDEFINED_OFFSET,
+            UNDEFINED_OFFSET,
+            KOTLIN_COVERAGE_DECLARATION_ORIGIN,
+            IrVariableSymbolImpl(),
+            name,
+            type,
+            isVar = false,
+            isConst = false,
+            isLateinit = false,
+        ).also { variable ->
+            variable.initializer = initializer
+            variable.parent = parent
+        }
+    }
+
+    fun intConst(value: Int): IrConst {
+        return IrConstImpl(
+            UNDEFINED_OFFSET,
+            UNDEFINED_OFFSET,
+            builtIns.intType,
+            IrConstKind.Int,
+            value
+        )
+    }
+
+    fun booleanConst(value: Boolean): IrConst {
+        return IrConstImpl(
+            UNDEFINED_OFFSET,
+            UNDEFINED_OFFSET,
+            builtIns.booleanType,
+            IrConstKind.Boolean,
+            value
+        )
+    }
+
+    fun getObjectValue(classSymbol: IrClassSymbol): IrGetObjectValue {
+        return IrGetObjectValueImpl(
+            UNDEFINED_OFFSET,
+            UNDEFINED_OFFSET,
+            classSymbol.defaultType,
+            classSymbol
+        )
+    }
+
+    fun lambda(
+        returnType: IrType,
+        argumentTypes: List<IrType>,
+        parent: IrDeclarationParent,
+        builder: MutableList<IrStatement>.() -> Unit,
+    ): IrFunctionExpression {
+        val function =
+            irFactory.createSimpleFunction(
+                UNDEFINED_OFFSET,
+                UNDEFINED_OFFSET,
+                KOTLIN_COVERAGE_DECLARATION_ORIGIN,
+                name = Name.special("<anonymous>"),
+                isExternal = false,
+                visibility = DescriptorVisibilities.LOCAL,
+                containerSource = null,
+                isSuspend = false,
+                isInline = false,
+                isExpect = false,
+                modality = Modality.FINAL,
+                isFakeOverride = false,
+                symbol = IrSimpleFunctionSymbolImpl(),
+                isTailrec = false,
+                isOperator = false,
+                isInfix = false,
+                returnType = returnType,
+            )
+        function.body = block(builder)
+        function.parent = parent
+
+
+        return IrFunctionExpressionImpl(
+            UNDEFINED_OFFSET,
+            UNDEFINED_OFFSET,
+            builtIns.functionNType(returnType, argumentTypes),
+            function,
+            KOTLIN_COVERAGE_STATEMENT_ORIGIN
+        )
+    }
+
+    fun getValue(variable: IrVariable, irType: IrType = variable.type): IrGetValue {
+        return IrGetValueImpl(
+            UNDEFINED_OFFSET,
+            UNDEFINED_OFFSET,
+            irType,
+            variable.symbol,
+            KOTLIN_COVERAGE_STATEMENT_ORIGIN
+        )
+    }
+
+    fun returnValue(returnTarget: IrReturnTargetSymbol, value: IrExpression): IrReturn {
+        return IrReturnImpl(
+            UNDEFINED_OFFSET,
+            UNDEFINED_OFFSET,
+            builtIns.nothingType,
+            returnTarget,
+            value
+        )
+    }
+
+    fun block(builder: MutableList<IrStatement>.() -> Unit): IrBlockBody {
+        return irFactory.createBlockBody(UNDEFINED_OFFSET, UNDEFINED_OFFSET) {
+            statements.builder()
+        }
+    }
+}
+
+private object Constants {
+    val KOTLIN_COVERAGE_STATEMENT_ORIGIN = IrStatementOriginImpl("KOTLIN_COVERAGE_ORIGIN")
+    val KOTLIN_COVERAGE_DECLARATION_ORIGIN = IrDeclarationOriginImpl("KOTLIN_COVERAGE_ORIGIN", true)
+
+    val PRINTLN_NAME = CallableId(FqName("kotlin.io"), Name.identifier("println"))
+    val LET_NAME = CallableId(FqName("kotlin"), Name.identifier("let"))
+    val COVERAGE_RUNTIME_PACKAGE = FqName("org.jetbrains.kotlin.coverage.runtime")
+    val BOOLEAN_STORAGE_NAME = ClassId(COVERAGE_RUNTIME_PACKAGE, Name.identifier("BooleanHitStorage"))
+    val CREATE_BOOLEAN_SEGMENT_NAME = CallableId(BOOLEAN_STORAGE_NAME, Name.identifier("getOrCreateSegment"))
+}
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/CoverageLoweringExtension.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/CoverageLoweringExtension.kt
new file mode 100644
index 0000000..e9d1aad
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/CoverageLoweringExtension.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+@file:OptIn(UnsafeDuringIrConstructionAPI::class)
+
+package org.jetbrains.kotlin.coverage.compiler.extensions
+
+import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
+import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
+import org.jetbrains.kotlin.coverage.compiler.instrumentation.IrFileVisitor
+import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext
+import org.jetbrains.kotlin.coverage.compiler.hit.HitRegistrarFactory
+import org.jetbrains.kotlin.coverage.compiler.instrumentation.LineBranchInstrumenter
+import org.jetbrains.kotlin.coverage.compiler.metadata.ModuleIM
+import org.jetbrains.kotlin.coverage.compiler.metadata.writeToFile
+import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.ir.declarations.path
+import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
+import org.jetbrains.kotlin.platform.jvm.isJvm
+import java.io.File
+import kotlin.random.Random
+
+class CoverageLoweringExtension(val modulePath: String, val metadataFilePath: String) : IrGenerationExtension {
+    override fun generate(
+        moduleFragment: IrModuleFragment,
+        pluginContext: IrPluginContext,
+    ) {
+        if (!pluginContext.platform.isJvm()) {
+            // only JVM platform supported for now
+            return
+        }
+
+        val context = KotlinCoverageInstrumentationContext(pluginContext)
+
+        val hitRegistrarFactory = HitRegistrarFactory()
+        val instrumenter = LineBranchInstrumenter()
+
+        val moduleDir = File(modulePath)
+
+        val moduleId = moduleFragment.name.asString().hashCode() and Random.nextInt()
+
+        val moduleIM = ModuleIM()
+
+        moduleFragment.files.forEach { file ->
+            val relativePath = File(file.path).toRelativeString(moduleDir)
+            val fileIM = moduleIM.addFile(relativePath, file.packageFqName.asString())
+
+            val segmentGenerator = hitRegistrarFactory.create(moduleId, fileIM.number, context)
+            IrFileVisitor(file, fileIM, instrumenter, segmentGenerator, context).process()
+        }
+
+        moduleIM.writeToFile(File(metadataFilePath))
+    }
+}
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/KotlinCoveragePluginRegistrar.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/KotlinCoveragePluginRegistrar.kt
new file mode 100644
index 0000000..e292354
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/extensions/KotlinCoveragePluginRegistrar.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.coverage.compiler.extensions
+
+import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
+import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption
+import org.jetbrains.kotlin.compiler.plugin.CliOption
+import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException
+import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
+import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
+import org.jetbrains.kotlin.config.CompilerConfiguration
+import org.jetbrains.kotlin.config.CompilerConfigurationKey
+
+private const val PLUGIN_ID = "org.jetbrains.kotlin.coverage"
+
+private val MODULE_PATH: CompilerConfigurationKey<String> =
+    CompilerConfigurationKey.create("Path to the root of the current module.")
+
+private val METADATA_PATH: CompilerConfigurationKey<String> =
+    CompilerConfigurationKey.create("Path to the kotlin coverage metadata file.")
+
+class KotlinCoveragePluginRegistrar : CompilerPluginRegistrar() {
+    override val pluginId: String = PLUGIN_ID
+
+    override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
+        val modulePath = configuration.get(MODULE_PATH)
+            ?: throw CliOptionProcessingException("${KotlinCoveragePluginOptions.MODULE_PATH_OPTION.optionName} option not specified")
+        val metadataFilePath = configuration.get(METADATA_PATH)
+            ?: throw CliOptionProcessingException("${KotlinCoveragePluginOptions.METADATA_PATH_OPTION.optionName} option not specified")
+
+        IrGenerationExtension.registerExtension(CoverageLoweringExtension(modulePath, metadataFilePath))
+    }
+
+    override val supportsK2: Boolean = true
+}
+
+class KotlinCoveragePluginOptions : CommandLineProcessor {
+    companion object {
+        val MODULE_PATH_OPTION = CliOption(
+            "modulePath", "Module directory path",
+            "Path to the root of the current module",
+            required = true, allowMultipleOccurrences = false
+        )
+        val METADATA_PATH_OPTION = CliOption(
+            "metadataFilePath", "Coverage metadata file path",
+            "Path to the file with coverage metadata",
+            required = true, allowMultipleOccurrences = false
+        )
+    }
+
+    override val pluginId get() = PLUGIN_ID
+    override val pluginOptions = listOf(MODULE_PATH_OPTION, METADATA_PATH_OPTION)
+
+    override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) = when (option) {
+        MODULE_PATH_OPTION -> configuration.put(MODULE_PATH, value)
+        METADATA_PATH_OPTION -> configuration.put(METADATA_PATH, value)
+        else -> throw CliOptionProcessingException("Unknown option: ${option.optionName}")
+    }
+}
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/BooleanHitRegistrar.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/BooleanHitRegistrar.kt
new file mode 100644
index 0000000..da06dc9
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/BooleanHitRegistrar.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.coverage.compiler.hit
+
+import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext
+import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
+import org.jetbrains.kotlin.descriptors.Modality
+import org.jetbrains.kotlin.ir.IrStatement
+import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
+import org.jetbrains.kotlin.ir.declarations.*
+import org.jetbrains.kotlin.ir.symbols.impl.IrPropertySymbolImpl
+import org.jetbrains.kotlin.ir.symbols.impl.IrSimpleFunctionSymbolImpl
+import org.jetbrains.kotlin.name.Name
+
+internal class BooleanHitRegistrar(val moduleId: Int, val segmentNumber: Int, val context: KotlinCoverageInstrumentationContext) :
+    HitRegistrar {
+    private val segmentPropertyName = Name.identifier($$"$kover_segment_")
+    private val segmentGetterName = Name.identifier($$"$kover_get_segment_")
+
+    private val segmentProperty: IrProperty = context.irFactory.createProperty(
+        UNDEFINED_OFFSET,
+        UNDEFINED_OFFSET,
+        context.declarationOrigin,
+        segmentPropertyName,
+        DescriptorVisibilities.PRIVATE,
+        Modality.FINAL,
+        IrPropertySymbolImpl(),
+        isVar = true,
+        isConst = false,
+        isLateinit = false,
+        isDelegated = false,
+    )
+
+    private val segmentGetter: IrSimpleFunction = context.irFactory.createSimpleFunction(
+        UNDEFINED_OFFSET,
+        UNDEFINED_OFFSET,
+        context.declarationOrigin,
+        segmentGetterName,
+        DescriptorVisibilities.PRIVATE,
+        isInline = false,
+        isExpect = false,
+        context.builtIns.primitiveArrayOfBooleanType,
+        Modality.FINAL,
+        IrSimpleFunctionSymbolImpl(),
+        isTailrec = false,
+        isSuspend = false,
+        isOperator = false,
+        isInfix = false
+    ).also { function -> function.returnType = context.builtIns.primitiveArrayOfBooleanType}
+
+    val pointsCounter = Counter()
+
+    override val extraDeclarations: List<IrDeclaration> = listOf(segmentProperty, segmentGetter)
+
+    override fun body(irFunction: IrFunction): BlockWithExecutionPoints {
+        return BooleanBlockWithExecutionPoints(segmentGetter, irFunction, pointsCounter, context)
+    }
+
+    override fun finalize() {
+        val myCall = context.factory.call(context.runtime.getOrCreateBooleanSegmentFun, context.builtIns.primitiveArrayOfBooleanType) {
+            arguments[0] = context.factory.getObjectValue(context.runtime.booleanStorageClass)
+            arguments[1] = context.factory.intConst(moduleId)
+            arguments[2] = context.factory.intConst(segmentNumber)
+            arguments[3] = context.factory.intConst(pointsCounter.count)
+        }
+
+        val returnCall = context.factory.returnValue(segmentGetter.symbol, myCall)
+
+        segmentGetter.body = context.factory.block {
+            add(returnCall)
+        }
+    }
+}
+
+private class BooleanBlockWithExecutionPoints(
+    private val segmentGetter: IrSimpleFunction,
+    private val thisFunction: IrFunction,
+    private val counter: Counter,
+    val context: KotlinCoverageInstrumentationContext,
+) : BlockWithExecutionPoints {
+    override val pointsCount: Int
+        get() = counter.count
+
+    private val variableName = Name.identifier($$"$coverage_segment")
+
+    private val variable: IrVariable = context.factory.`val`(
+        variableName,
+        context.builtIns.primitiveArrayOfBooleanType,
+        thisFunction,
+        context.factory.call(segmentGetter.symbol, context.builtIns.primitiveArrayOfBooleanType)
+    )
+
+    override val firstStatement: IrStatement = variable
+
+    override fun registerPoint(): ExecutionPoint {
+        val id = counter.count++
+
+        val statement = context.factory.call(context.builtIns.primitiveArrayOfBooleansSetter, context.builtIns.unitType) {
+            arguments[0] = context.factory.getValue(variable)
+            arguments[1] = context.factory.intConst(id)
+            arguments[2] = context.factory.booleanConst(true)
+        }
+
+        return ExecutionPoint(id, statement)
+    }
+}
+
+internal class Counter(var count: Int = 0)
\ No newline at end of file
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/HitRegistrar.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/HitRegistrar.kt
new file mode 100644
index 0000000..e6c2640
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/hit/HitRegistrar.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+@file:OptIn(UnsafeDuringIrConstructionAPI::class)
+
+package org.jetbrains.kotlin.coverage.compiler.hit
+
+import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext
+import org.jetbrains.kotlin.ir.IrStatement
+import org.jetbrains.kotlin.ir.declarations.IrDeclaration
+import org.jetbrains.kotlin.ir.declarations.IrFunction
+import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
+
+internal class HitRegistrarFactory {
+    fun create(moduleId: Int, segmentNumber: Int, context: KotlinCoverageInstrumentationContext): HitRegistrar {
+        // now we support only boolean hits
+        return BooleanHitRegistrar(moduleId, segmentNumber, context)
+    }
+}
+
+internal interface HitRegistrar {
+    val extraDeclarations: List<IrDeclaration>
+    fun body(irFunction: IrFunction): BlockWithExecutionPoints
+    fun finalize()
+}
+
+internal interface BlockWithExecutionPoints {
+    val firstStatement: IrStatement
+    val pointsCount: Int
+    fun registerPoint(): ExecutionPoint
+}
+
+internal class ExecutionPoint(
+    val id: Int,
+    val hitStatement: IrStatement,
+)
+
+
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/Instrumenter.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/Instrumenter.kt
new file mode 100644
index 0000000..e71c82e
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/Instrumenter.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.coverage.compiler.instrumentation
+
+import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext
+import org.jetbrains.kotlin.coverage.compiler.hit.HitRegistrar
+import org.jetbrains.kotlin.coverage.compiler.metadata.FunctionIM
+import org.jetbrains.kotlin.ir.declarations.IrFile
+import org.jetbrains.kotlin.ir.declarations.IrFunction
+
+internal sealed interface Instrumenter {
+    fun instrument(
+        irFunction: IrFunction,
+        functionIM: FunctionIM,
+        irFile: IrFile,
+        hitRegistrar: HitRegistrar,
+        context: KotlinCoverageInstrumentationContext,
+    )
+}
+
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/IrFileVisitor.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/IrFileVisitor.kt
new file mode 100644
index 0000000..61ad196
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/IrFileVisitor.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+@file:OptIn(UnsafeDuringIrConstructionAPI::class)
+
+package org.jetbrains.kotlin.coverage.compiler.instrumentation
+
+import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext
+import org.jetbrains.kotlin.coverage.compiler.hit.HitRegistrar
+import org.jetbrains.kotlin.coverage.compiler.metadata.DeclarationContainerIM
+import org.jetbrains.kotlin.coverage.compiler.metadata.FileIM
+import org.jetbrains.kotlin.coverage.compiler.metadata.positionRange
+import org.jetbrains.kotlin.ir.declarations.*
+import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
+
+/**
+ * Class to walk over an IR declarations tree, add required declarations and instrument functions.
+ */
+internal class IrFileVisitor(
+    val irFile: IrFile,
+    val fileIM: FileIM,
+    val instrumenter: Instrumenter,
+    val hitRegistrar: HitRegistrar,
+    val context: KotlinCoverageInstrumentationContext,
+) {
+    internal fun process() {
+        // TODO can we use file to collect coverage or we should place only in classes/objects/companions/top level files
+
+        // register declarations
+        hitRegistrar.extraDeclarations.forEach { irDeclaration ->
+            irDeclaration.parent = irFile
+            irFile.declarations.add(irDeclaration)
+        }
+
+        fileIM.processTopLevelContainer(irFile)
+
+        hitRegistrar.finalize()
+    }
+
+    private fun DeclarationContainerIM.processTopLevelContainer(container: IrDeclarationContainer) {
+        container.declarations.forEach { irDeclaration ->
+            if (irDeclaration.origin != IrDeclarationOrigin.DEFINED) return@forEach
+
+            when (irDeclaration) {
+                is IrProperty -> processProperty(irDeclaration)
+                is IrClass -> processClass(irDeclaration)
+                is IrFunction -> processFunction(irDeclaration)
+            }
+        }
+    }
+
+    private fun DeclarationContainerIM.processClass(irClass: IrClass) {
+        // TODO is init{} block always injected to <init>?
+        val classIM = addClass(irClass.name.asString(), irClass.isCompanion, irFile.positionRange(irClass))
+        classIM.processTopLevelContainer(irClass)
+    }
+
+    private fun DeclarationContainerIM.processFunction(irFunction: IrFunction) {
+        // TODO (irClass.parent as IrFunction).body?.statements?.filterIsInstance<IrClass>()
+
+        val range = irFile.positionRange(irFunction)
+        val functionIM = addFunction(irFunction.name.asString(), listOf(), "", range)
+
+        instrumenter.instrument(irFunction, functionIM, irFile, hitRegistrar, context)
+    }
+
+    private fun DeclarationContainerIM.processProperty(irProperty: IrProperty) {
+        if (irProperty.getter == null && irProperty.setter == null) return
+
+        irProperty.backingField?.initializer
+
+        val range = irFile.positionRange(irProperty)
+        val propertyIM = addProperty(irProperty.name.asString(), irProperty.isVar, irProperty.isConst, range)
+
+        // TODO irProperty.backingField?.initializer
+    }
+
+}
\ No newline at end of file
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/LineBranchInstrumenter.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/LineBranchInstrumenter.kt
new file mode 100644
index 0000000..922728d
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/instrumentation/LineBranchInstrumenter.kt
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+@file:OptIn(UnsafeDuringIrConstructionAPI::class)
+
+package org.jetbrains.kotlin.coverage.compiler.instrumentation
+
+import org.jetbrains.kotlin.coverage.compiler.common.KotlinCoverageInstrumentationContext
+import org.jetbrains.kotlin.coverage.compiler.hit.BlockWithExecutionPoints
+import org.jetbrains.kotlin.coverage.compiler.hit.HitRegistrar
+import org.jetbrains.kotlin.coverage.compiler.metadata.FunctionIM
+import org.jetbrains.kotlin.coverage.compiler.metadata.LineBranchBodyIM
+import org.jetbrains.kotlin.coverage.compiler.metadata.LineBranchBodyIM.LineInfo
+import org.jetbrains.kotlin.coverage.compiler.metadata.Position
+import org.jetbrains.kotlin.coverage.compiler.metadata.position
+import org.jetbrains.kotlin.ir.IrStatement
+import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
+import org.jetbrains.kotlin.ir.declarations.IrDeclarationParent
+import org.jetbrains.kotlin.ir.declarations.IrFile
+import org.jetbrains.kotlin.ir.declarations.IrFunction
+import org.jetbrains.kotlin.ir.declarations.IrParameterKind
+import org.jetbrains.kotlin.ir.declarations.IrVariable
+import org.jetbrains.kotlin.ir.expressions.IrBlockBody
+import org.jetbrains.kotlin.ir.expressions.IrCall
+import org.jetbrains.kotlin.ir.expressions.IrConst
+import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
+import org.jetbrains.kotlin.ir.expressions.IrExpression
+import org.jetbrains.kotlin.ir.expressions.IrExpressionBody
+import org.jetbrains.kotlin.ir.expressions.IrGetValue
+import org.jetbrains.kotlin.ir.expressions.IrReturn
+import org.jetbrains.kotlin.ir.expressions.IrSetValue
+import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
+import org.jetbrains.kotlin.ir.expressions.IrTypeOperatorCall
+import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
+import org.jetbrains.kotlin.name.Name
+
+internal class LineBranchInstrumenter() : Instrumenter {
+    override fun instrument(
+        irFunction: IrFunction,
+        functionIM: FunctionIM,
+        irFile: IrFile,
+        hitRegistrar: HitRegistrar,
+        context: KotlinCoverageInstrumentationContext,
+    ) {
+        val body = irFunction.body ?: return
+
+        val bodyPointsRegistry = hitRegistrar.body(irFunction)
+
+        when (body) {
+            is IrBlockBody -> instrumentBlock(body, irFunction, functionIM, bodyPointsRegistry, irFile, context)
+            is IrExpressionBody -> {}
+            else -> {}// do nothing
+        }
+    }
+
+
+    private fun instrumentBlock(
+        block: IrBlockBody,
+        irFunction: IrFunction,
+        functionIM: FunctionIM,
+        pointsRegistry: BlockWithExecutionPoints,
+        irFile: IrFile,
+        context: KotlinCoverageInstrumentationContext,
+    ) {
+        val bodyInstrumentation = BodyInstrumentation(pointsRegistry, irFile, irFunction, context)
+        val instrumented = bodyInstrumentation.instrument(block.statements)
+
+        if (bodyInstrumentation.finishedLines.isNotEmpty()) {
+            block.statements.clear()
+            block.statements.addAll(instrumented)
+            functionIM.body = LineBranchBodyIM(bodyInstrumentation.finishedLines)
+        }
+    }
+}
+
+
+private class BodyInstrumentation(
+    val pointsRegistry: BlockWithExecutionPoints,
+    val irFile: IrFile,
+    val parentFunction: IrDeclarationParent,
+    val context: KotlinCoverageInstrumentationContext,
+) {
+    var currentLine: LineInfo? = null
+    var tmpVariableCount = 0
+
+    val finishedLines: MutableList<LineInfo> = mutableListOf()
+    val currentBranches: MutableList<LineBranchBodyIM.BranchInfo> = mutableListOf()
+
+    fun instrument(statements: List<IrStatement>): List<IrStatement> {
+        val instrumentedStatements = mutableListOf<IrStatement>()
+
+        instrumentedStatements.add(pointsRegistry.firstStatement)
+        statements.forEach { statement ->
+            instrumentedStatements.addAll(instrumentStatement(statement).allStatements)
+        }
+
+        if (currentLine != null) {
+            finishedLines.add(currentLine!!)
+        }
+
+        return instrumentedStatements
+    }
+
+
+    private fun instrumentStatement(statement: IrStatement): InstrumentationResult {
+        return when (statement) {
+            is IrReturn -> instrument(statement)
+            is IrConst -> instrument(statement)
+            is IrVariable -> instrument(statement)
+            is IrSetValue -> instrument(statement)
+//            is IrGetValue -> instrument(statement)
+            is IrCall -> instrument(statement)
+            is IrConstructorCall -> instrument(statement)
+            is IrTypeOperatorCall -> instrument(statement)
+            else -> {
+                InstrumentationResult.fromStatement(statement)
+            }
+        }
+    }
+
+    private fun instrument(returnStatement: IrReturn): InstrumentationResult {
+        val hitPointStatement = registerLinePoint(returnStatement)
+        returnStatement.value = instrumentStatement(returnStatement.value).wrap()
+        return InstrumentationResult.fromPointed(hitPointStatement, returnStatement)
+    }
+
+    private fun instrument(constStatement: IrConst): InstrumentationResult {
+        val hitPointStatement = registerLinePoint(constStatement)
+        return InstrumentationResult.fromPointed(hitPointStatement, constStatement)
+    }
+
+    private fun instrument(variable: IrVariable): InstrumentationResult {
+        if (variable.origin != IrDeclarationOrigin.DEFINED) {
+            // process only user-defined variables
+            return InstrumentationResult.fromStatement(variable)
+        }
+
+        val hitPointStatement = registerLinePoint(variable)
+        variable.initializer = variable.initializer?.let { instrumentStatement(it).wrap() }
+        return InstrumentationResult.fromPointed(hitPointStatement, variable)
+    }
+
+    private fun instrument(setValue: IrSetValue): InstrumentationResult {
+        if (setValue.origin != IrStatementOrigin.EQ) {
+            // process only user-defined variables
+            return InstrumentationResult.fromStatement(setValue)
+        }
+
+        val hitPointStatement = registerLinePoint(setValue)
+        setValue.value = instrumentStatement(setValue.value).wrap()
+        return InstrumentationResult.fromPointed(hitPointStatement, setValue)
+    }
+
+    private fun instrument(irCall: IrCall): InstrumentationResult {
+        val leadingStatements = mutableListOf<IrStatement>()
+        // process receivers
+        irCall.symbol.owner.parameters.forEachIndexed { index, parameter ->
+            if (parameter.kind != IrParameterKind.DispatchReceiver && parameter.kind != IrParameterKind.ExtensionReceiver) {
+                return@forEachIndexed
+            }
+            // skip null arguments (default values in most cases)
+            val arg = irCall.arguments[index] ?: return@forEachIndexed
+
+            val instrumentedReceiver = instrumentReceiver(arg)
+
+            irCall.arguments[index] = instrumentedReceiver.asExpression
+            // add initialization of receivers in separate variables
+            leadingStatements.addAll(instrumentedReceiver.leadingStatements)
+        }
+
+        val hitPointStatement = registerLinePoint(irCall)
+
+        // process arguments
+        irCall.symbol.owner.parameters.forEachIndexed { index, parameter ->
+            if (parameter.kind != IrParameterKind.Regular) {
+                return@forEachIndexed
+            }
+            // skip null arguments - in [actualArgs] it's already null
+            val arg = irCall.arguments[index] ?: return@forEachIndexed
+            irCall.arguments[index] = instrumentStatement(arg).wrap()
+        }
+
+        return InstrumentationResult.fromPointed(leadingStatements, hitPointStatement, irCall)
+    }
+
+    private fun instrument(irConstructorCall: IrConstructorCall): InstrumentationResult {
+        val hitPointStatement = registerLinePoint(irConstructorCall)
+
+        // process arguments
+        irConstructorCall.symbol.owner.parameters.forEachIndexed { index, parameter ->
+            if (parameter.kind != IrParameterKind.Regular) {
+                return@forEachIndexed
+            }
+            // skip null arguments - in [actualArgs] it's already null
+            val arg = irConstructorCall.arguments[index] ?: return@forEachIndexed
+            irConstructorCall.arguments[index] = instrumentStatement(arg).wrap()
+        }
+        return InstrumentationResult.fromPointed(hitPointStatement, irConstructorCall)
+    }
+
+    private fun instrument(irTypeOperatorCall: IrTypeOperatorCall): InstrumentationResult {
+        val instrumented = instrumentStatement(irTypeOperatorCall.argument).wrap()
+        irTypeOperatorCall.argument = instrumented
+        // don't change the operator itself
+        return InstrumentationResult.fromStatement(irTypeOperatorCall)
+    }
+
+    private fun instrumentReceiver(irExpression: IrExpression): InstrumentationResult {
+        val name = Name.identifier($$"$tmp" + tmpVariableCount++)
+        val instrumented = instrumentStatement(irExpression)
+        return when {
+            instrumented.hasHitPoint && instrumented.leadingStatements.isEmpty() ->
+                InstrumentationResult.fromPointed(listOf(instrumented.hitPoint), null, instrumented.statement)
+
+            instrumented.hasHitPoint && instrumented.leadingStatements.isNotEmpty() -> {
+                val variable = context.factory.`val`(name, irExpression.type, parentFunction, instrumented.asExpression)
+                val get = context.factory.getValue(variable)
+                InstrumentationResult.fromPointed(instrumented.leadingStatements + variable + instrumented.hitPoint, null, get)
+            }
+
+            else -> return instrumented
+        }
+    }
+
+    /**
+     * Add execution point before [irStatement] if needed.
+     */
+    private fun registerLinePoint(irStatement: IrStatement): IrStatement? {
+        val position = irFile.position(irStatement.startOffset)
+        return if (isNextLine(position)) {
+            val point = pointsRegistry.registerPoint()
+            startNewLine(position, point.id)
+            point.hitStatement
+        } else {
+            null
+        }
+    }
+
+    fun isNextLine(position: Position): Boolean {
+        val currentLineStart = currentLine?.lineNumber
+        return if (currentLineStart == null) {
+            true
+        } else {
+            currentLineStart != position.line
+        }
+    }
+
+    fun startNewLine(position: Position, pointId: Int) {
+        val current = currentLine
+        if (current != null) {
+            finishedLines += current
+        }
+
+        currentLine = LineInfo(position.line, position.column, pointId, mutableListOf())
+        currentBranches.clear()
+    }
+
+    private fun InstrumentationResult.wrap(): IrExpression {
+        val expression =
+            asExpression ?: throw IllegalStateException("Instrumentation result can't be wrapped to expression; $leadingStatements")
+        if (hitPoint == null && leadingStatements.isEmpty()) {
+            return expression
+        }
+        return context.factory.call(context.builtIns.function0InvokeFun, expression.type) {
+            arguments[0] = context.factory.lambda(expression.type, emptyList(), parentFunction) {
+                addAll(allStatements)
+            }
+        }
+    }
+
+    private class InstrumentationResult private constructor(
+        val leadingStatements: List<IrStatement>,
+        val hitPoint: IrStatement?,
+        val statement: IrStatement,
+    ) {
+        val asExpression: IrExpression
+            get() {
+                return statement as? IrExpression ?: error("Instrumented statement is not an expression, actual $statement")
+            }
+        val allStatements: List<IrStatement>
+            get() {
+                return if (hitPoint == null) {
+                    leadingStatements + statement
+                } else {
+                    leadingStatements + hitPoint + statement
+                }
+            }
+        val hasHitPoint: Boolean = hitPoint != null
+
+        companion object {
+            fun fromPointed(hitPointStatement: IrStatement?, instrumentedStatement: IrStatement): InstrumentationResult {
+                return InstrumentationResult(emptyList(), hitPointStatement, instrumentedStatement)
+            }
+
+            fun fromPointed(
+                leadingStatements: List<IrStatement?>,
+                hitPointStatement: IrStatement?,
+                statement: IrStatement,
+            ): InstrumentationResult {
+                return InstrumentationResult(leadingStatements.filterNotNull(), hitPointStatement, statement)
+            }
+
+            fun fromStatement(instrumentedStatement: IrStatement): InstrumentationResult {
+                return InstrumentationResult(emptyList(), null, instrumentedStatement)
+            }
+        }
+    }
+}
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadata.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadata.kt
new file mode 100644
index 0000000..70040cf
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadata.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.coverage.compiler.metadata
+
+import org.jetbrains.kotlin.ir.IrElement
+import org.jetbrains.kotlin.ir.declarations.IrFile
+
+
+internal class ModuleIM {
+    private val _files = mutableListOf<FileIM>()
+    val files: List<FileIM> = _files
+
+    fun addFile(path: String, packageName: String): FileIM {
+        val file = FileIM(_files.size, path, packageName)
+        _files.add(file)
+        return file
+    }
+}
+
+internal class FileIM(
+    val number: Int,
+    val path: String,
+    val packageName: String,
+) : DeclarationContainerIM() {
+    override val parent: DeclarationContainerIM? = null
+}
+
+internal sealed class DeclarationContainerIM {
+    abstract val parent: DeclarationContainerIM?
+
+    private val _declarations = mutableListOf<DeclarationIM>()
+    val declarations: List<DeclarationIM> = _declarations
+
+    open fun addFunction(
+        name: String,
+        params: List<String>,
+        returnType: String,
+        range: PositionRange,
+    ): FunctionIM {
+        return FunctionIM(name, params, returnType, range, this).also { functionIM -> _declarations.add(functionIM) }
+    }
+
+    open fun addClass(
+        name: String,
+        isCompanion: Boolean,
+        range: PositionRange,
+    ): ClassIM {
+        return ClassIM(name, isCompanion, range, this).also { _declarations.add(it) }
+    }
+
+    open fun addProperty(
+        name: String,
+        isVar: Boolean,
+        isConst: Boolean,
+        range: PositionRange,
+    ): PropertyIM {
+        return PropertyIM(name, isVar, isConst, range, this).also { _declarations.add(it) }
+    }
+}
+
+internal sealed class DeclarationIM : DeclarationContainerIM() {
+    abstract val range: PositionRange
+}
+
+internal class ClassIM(
+    val name: String,
+    val isCompanion: Boolean,
+    override val range: PositionRange,
+    override val parent: DeclarationContainerIM,
+) : DeclarationIM() {
+    val isLocal: Boolean get() = parent is FunctionIM
+}
+
+internal class PropertyIM(
+    val name: String,
+    val isVar: Boolean,
+    val isConst: Boolean,
+    override val range: PositionRange,
+    override val parent: DeclarationContainerIM,
+) : DeclarationIM() {
+
+    // TODO add initializer, getter and setter
+
+    override fun addProperty(
+        name: String,
+        isVar: Boolean,
+        isConst: Boolean,
+        range: PositionRange,
+    ): PropertyIM {
+        throw UnsupportedOperationException("Property cannot have classes")
+    }
+
+    override fun addClass(
+        name: String,
+        isCompanion: Boolean,
+        range: PositionRange,
+    ): ClassIM {
+        throw UnsupportedOperationException("Property cannot have properties")
+    }
+}
+
+internal class FunctionIM(
+    val name: String,
+    val params: List<String>,
+    val returnType: String,
+    override val range: PositionRange,
+    override val parent: DeclarationContainerIM,
+) : DeclarationIM() {
+    val isLocal: Boolean get() = parent is FunctionIM
+    var body: BodyIM? = null
+}
+
+internal sealed interface BodyIM
+
+
+internal class Position(val line: Int, val column: Int) {
+    override fun toString(): String {
+        return "$line:$column"
+    }
+}
+
+internal class PositionRange(val start: Position, val end: Position) {
+    override fun toString(): String {
+        return "${start.line}:${start.column}-${end.line}:${end.column}"
+    }
+}
+
+internal fun IrFile.positionRange(element: IrElement): PositionRange {
+    return PositionRange(position(element.startOffset), position(element.endOffset))
+}
+
+internal fun IrFile.position(offset: Int): Position {
+    return Position(
+        fileEntry.getLineNumber(offset),
+        fileEntry.getColumnNumber(offset)
+    )
+}
\ No newline at end of file
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadataWriter.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadataWriter.kt
new file mode 100644
index 0000000..33b4fcb
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/InstrumentationMetadataWriter.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+@file:OptIn(UnsafeDuringIrConstructionAPI::class)
+
+package org.jetbrains.kotlin.coverage.compiler.metadata
+
+import org.jetbrains.kotlin.ir.symbols.UnsafeDuringIrConstructionAPI
+import java.io.File
+
+internal fun ModuleIM.writeToFile(file: File) {
+    file.parentFile.mkdirs()
+    file.bufferedWriter().use { out ->
+        out.writeModule(this)
+    }
+}
+
+private fun Appendable.writeModule(moduleIM: ModuleIM) {
+    moduleIM.files.forEach { fileIM ->
+        appendLine(fileIM.path)
+        appendLine(fileIM.packageName)
+        writeDeclarations(SINGLE_INDENT, fileIM)
+    }
+}
+
+private fun Appendable.writeDeclarations(indent: String, containerIM: DeclarationContainerIM) {
+    containerIM.declarations.forEach { declarationIM ->
+        when (declarationIM) {
+            is ClassIM -> writeClass(indent, declarationIM)
+            is FunctionIM -> writeFunction(indent, declarationIM)
+            is PropertyIM -> writeProperty(indent, declarationIM)
+            else -> error("Unknown declaration type: ${declarationIM::class.simpleName}")
+        }
+    }
+}
+
+private fun Appendable.writeProperty(indent: String, propertyIM: PropertyIM) {
+    append(indent)
+    if (propertyIM.isConst) append("const ")
+    if (propertyIM.isVar) append("var ") else append("val ")
+    appendLine(propertyIM.name)
+    appendLine("$indent[${propertyIM.range}]")
+    writeDeclarations("$indent    ", propertyIM)
+}
+
+private fun Appendable.writeClass(indent: String, classIM: ClassIM) {
+    append(indent)
+    if (classIM.isCompanion) append("companion object ") else append("class ")
+    appendLine(classIM.name)
+    appendLine("$indent[${classIM.range}]")
+    writeDeclarations("$indent    ", classIM)
+}
+
+private fun Appendable.writeFunction(indent: String, functionIM: FunctionIM) {
+    appendLine("${indent}fun ${functionIM.name}(${functionIM.params}): ${functionIM.returnType}")
+    val nextIndent = indent + SINGLE_INDENT
+    appendLine("${nextIndent}range: ${functionIM.range}")
+    writeBody(nextIndent, functionIM.body)
+    writeDeclarations(nextIndent, functionIM)
+}
+
+
+private fun Appendable.writeBody(indent: String, body: BodyIM?) {
+    append("${indent}body: ")
+    if (body != null && body is LineBranchBodyIM) {
+        body.lines.forEach { line ->
+            append("${line.pointId}=${line.lineNumber}:${line.columnStart}[")
+
+            append("];")
+        }
+    } else {
+        append("null")
+    }
+
+    appendLine()
+}
+
+
+const val SINGLE_INDENT = "    "
diff --git a/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/LineBranchInstrumentationMetadata.kt b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/LineBranchInstrumentationMetadata.kt
new file mode 100644
index 0000000..a93e434
--- /dev/null
+++ b/plugins/coverage/coverage-compiler-plugin/src/org/jetbrains/kotlin/coverage/compiler/metadata/LineBranchInstrumentationMetadata.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.coverage.compiler.metadata
+
+internal class LineBranchBodyIM(val lines: MutableList<LineInfo>) : BodyIM {
+    internal class LineInfo(val lineNumber: Int, val columnStart: Int, val pointId: Int, val branches: MutableList<BranchInfo>)
+
+    internal class BranchInfo(val id: Int, val columnStart: Int, val columnEnd: Int, val pointId: Int)
+}
diff --git a/settings.gradle b/settings.gradle
index f2c53dc..1a1e785 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -482,6 +482,11 @@
         ":kotlin-serialization",
         ":kotlin-serialization-unshaded"
 
+include ":coverage-compiler-plugin-embeddable",
+        ":coverage-compiler-plugin",
+        ":coverage-compiler-gradle",
+        ":coverage-runtime"
+
 include ":kotlin-dataframe-compiler-plugin",
         ":kotlin-dataframe-compiler-plugin.embeddable",
         ":kotlin-dataframe-compiler-plugin.common",
@@ -999,6 +1004,11 @@
 project(':kotlin-serialization').projectDir = file("$rootDir/libraries/tools/kotlin-serialization")
 project(':kotlin-serialization-unshaded').projectDir = file("$rootDir/libraries/tools/kotlin-serialization-unshaded")
 
+project(':coverage-compiler-plugin-embeddable').projectDir = "$rootDir/plugins/coverage/coverage-compiler-embeddable" as File
+project(':coverage-compiler-plugin').projectDir = "$rootDir/plugins/coverage/coverage-compiler-plugin" as File
+project(':coverage-compiler-gradle').projectDir = "$rootDir/libraries/tools/kotlin-coverage/kotlin-coverage-compiler-subplugin" as File
+project(':coverage-runtime').projectDir = "$rootDir/libraries/tools/kotlin-coverage/kotlin-coverage-runtime" as File
+
 project(':kotlin-atomicfu-compiler-plugin').projectDir = file("$rootDir/plugins/atomicfu/atomicfu-compiler")
 project(':kotlin-atomicfu-compiler-plugin-embeddable').projectDir = file("$rootDir/plugins/atomicfu/atomicfu-compiler-embeddable")
 project(':kotlinx-atomicfu-runtime').projectDir = file("$rootDir/plugins/atomicfu/atomicfu-runtime")