cd /news/developer-tools/a-more-reliable-compilation-scheme-f… · home › topics › developer-tools › article
[ARTICLE · art-140940] src=blog.jetbrains.com ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

A More Reliable Compilation Scheme for Kotlin Multiplatform Modules

JetBrains introduced an optional "separate compilation" scheme for Kotlin Multiplatform in Kotlin 2.5.0-Beta1, enabled by setting kotlin.kmp.separateCompilation=true in gradle.properties. The experimental, off-by-default approach makes compiler results consistent with IDE analysis and more consistently flags problematic calls to library code from common source sets, and it enables incremental compilation for common source sets.

by read7 min views1 publishedSep 28, 2026
A More Reliable Compilation Scheme for Kotlin Multiplatform Modules
Image: Blog (auto-discovered)

Kotlin #

A concise multiplatform language developed by JetBrains

The current compilation approach to Kotlin Multiplatform projects works, but sometimes can lead to unexpected or hard-to-predict behavior. For example:

  • The IDE analysis disagrees with the compiler on an overload, type inference, or whether the code should compile at all – with the IDE being stricter than the compiler.
  • Your commonTest code can unexpectedly call something from a platform source set, breaking your assumptions.

With Kotlin 2.5.0-Beta1, we introduced an optional “separate compilation” approach to KMP that solves both problems: make the compilation results consistent with IDE analysis, and point more consistently to problematic calls of library code from common source sets. As a bonus, this approach enables us to implement incremental compilation for common source sets.

The separate compilation feature is Experimental and disabled by default. To opt in, add the following compiler option to your gradle.properties file (note the known issues):

kotlin.kmp.separateCompilation=true

Let’s look closer at the problem and the solution.

How multiplatform declarations are resolved within a module #

Within a module, multiplatform code performs predictably:

// jvmMain
fun foo() {}

// commonMain
fun test() {
    foo() // Unresolved reference in IDE and during compilation
}

The compiler and the IDE agree: The code in commonMain is also compiled to other platforms, for example, to Kotlin/JS, and there might not be a fun foo() declaration in jsMain. Expect/actual declarations exist precisely to solve this problem: explicitly tie platform-specific declarations to common ones.

You would expect that the reference is also flagged as unresolved when the foo() function is declared in a dependency (a module in the same project or a binary artifact like Kotlin standard library or kotlinx-coroutines). Unfortunately, with the current compilation scheme that’s where the IDE and the compiler disagree.

How multiplatform declarations are resolved between modules #

To understand where the problems stem from, let’s look closer at the current compilation setup.

Kotlin Multiplatform compilation setup

In a KMP project, a module with common code usually consists of several source sets: in the example above it’s a shared source set (commonMain) and a platform source set for a declared target ( jvmMain for Kotlin/JVM).

With that setup, the compiler can produce the following artifacts:

  • A platform artifact for each platform source set like jvmMain –*.jar on JVM,*.klib on other platforms.
  • A metadata KLIB for each common or intermediate source set (like commonMain ornativeMain ). A metadata KLIB contains all declarations from the compiled source set without bodies.

So where do the problems start?

Common code is compiled against platform artifacts; IDE disagrees

During compilation of a platform source set, both the code inside it (jvmMain) and the code in all relevant shared source sets ( commonMain and other intermediate source sets) is compiled against the platform artifacts of its dependencies. Here code from commonMain has a chance to implicitly resolve to a jvmMain declaration.

If you expect this to happen, you’re probably fine. It would be more transparent and predictable, however, if commonMain could only call declarations from the dependency’s commonMain (as listed in the dependency’s metadata KLIB).

This is exactly what the code analysis in IntelliJ IDEA already assumes. As jvmMain declarations are not included in the KLIB metadata, IntelliJ IDEA reports the unresolved reference error when commonMain refers to something not declared in common code explicitly:

// lib/jvmMain
class Foo

// app/commonMain
fun main() {
    Foo() // Unresolved reference in the IDE, no error during compilation
}

The same mechanism is also in play with commonTest source sets, because tests also “depend” on main code: commonTest compiled against jvmMain can successfully call a declaration from platform code, instead of being restricted to declarations in commonMain. IDE reports the same unresolved reference problem:

// app/jvmMain
class Foo

// app/commonTest
fun main() {
    Foo() // "Unresolved reference" in the IDE, no error during compilation
}

Let’s see how separate compilation solves these problems.

How separate compilation solves the problem #

KMP separate compilation makes the compiler behave stricter (aligning with the IDE expectation) when compiling common source sets of KMP projects. This makes the overall experience more predictable, although you may need to adjust your old code for stricter compile-time checks.

Let’s look at specific code examples and what happens to them with the switch between compilation schemes.

Red code in IDE, no error during compilation

When you turn on separate compilation, this code stops compiling, because compiler now resolves common code calls strictly using metadata KLIB declarations:

// lib/jvmMain
class Foo

// app/commonMain
fun main() {
    Foo() // Unresolved reference in IDE, now also a compilation error
}

The fix is to show an explicit connection: declare an expect class in lib/commonMain and make the platform Foo class an actual class:

// lib/commonMain
expect class Foo()

// lib/jvmMain
actual class Foo

// app/commonMain
fun main() {
    Foo() // ok
}

Same for commonTest: with separate compilation, the test code won’t resolve to a platform call in jvmMain. If that’s what you want to test, declare the corresponding expect in commonMain:

// app/jvmMain
actual class Foo

// app/commonMain
expect class Foo

// app/commonTest
fun main() {
    Foo() // Ok, calls Foo from jvmMain
}

IDE and compiler don’t agree on selected overload

When several overloads are declared in lib/commonMain and lib/jvmMain, the IDE currently interprets the call from app/commonMain differently from the compiler:

// lib/commonMain
fun foo(x: Any) = "common"

// lib/jvmMain
fun foo(x: String) = "platform"

// app/commonMain
fun main() {
    // "Go to declaration" on foo() jumps to lib/commonMain,
    // but at runtime "platform" is printed
    println(foo(""))
}

The IDE resolves the foo("") call to the declaration fun foo(x: Any) in lib/commonMain, as it assumes that common code can only depend on common declarations. But during the actual compilation app/commonMain currently sees both declarations and chooses the more specific overload fun foo(x: String). At runtime, “platform” is printed.

With the new compilation scheme, the call is resolved to fun foo(x: Any) in lib/commonMain and at runtime, and “common” is printed.

Unexpected type inference

A trickier problem can occur when the error is triggered not at the app/commonMain call site, but in platform code. In the following example the IDE actually sees no problem, but the JVM compilation fails.

actual classes are allowed to have different supertypes than their expect counterparts, so you can reasonably arrive at the following code structure:

// eventsLib/commonMain
interface Event { val name: String }

expect class ClickEvent(target: String) : Event { override val name: String }
expect class ScrollEvent(offset: Int) : Event { override val name: String }

// eventsLib/jvmMain — serialized events get queued or stored in a session
actual class ClickEvent actual constructor(target: String) : Event, Serializable { /* ... */ }
actual class ScrollEvent actual constructor(offset: Int) : Event, Serializable { /* ... */ }

// app/commonMain
fun currentEvent() = if (clicked()) ClickEvent("buy") else ScrollEvent(120)

fun report() = currentEvent().name // e: Unresolved reference 'name' only on the JVM

The IDE only matches commonMain declarations, which allows it to (correctly) infer the return type of currentEvent() as Event and resolve the .name reference.

On JVM, however, the library declares an additional supertype for each actual class. When compiling app for the JVM, the compiler currently resolves common code against the eventsLib JAR which has the eventsLib/jvmMain version of the classes. With the two supertypes, the return type inference for the currentEvent() call arrives at Any, which doesn’t have a .name, and the JVM compilation fails.

With the separate compilation enabled, app/commonMain code is resolved only against eventsLib/commonMain declarations. So when compiling the JVM target of app, the compiler already has the resolved common code and has inferred the Event return type for the currentEvent() function. The JVM compilation runs separately, doesn’t have to infer anything, and finishes successfully.

How to enable separate compilation #

The separate compilation feature is Experimental and disabled by default. To opt in, add the following compiler option to your gradle.properties file (note the known issues below):

kotlin.kmp.separateCompilation=true

Known issues #

For separate compilation to work correctly in KMP projects, it is important that authors of multiplatform libraries publish the metadata KLIBs. This is the default behavior for publishing tasks of the KMP Gradle plugin, but the new compilation scheme makes metadata KLIBs essential.

There are some known issues with KMP separate compilation that are still being fixed:

  • Some problems with commonization of cinterops (like KT-88178 orKT-41509 ). These are unlikely to affect an average KMP project, but enable the separate compilation mode with caution if you useC interoperability .
  • KMP modules with common source sets and a single declared target are not affected by separate compilation for now.
  • Various smaller issues such as KT-88148 .

Subscribe to Kotlin Blog updates

── more in #developer-tools 4 stories · sorted by recency
── more on @jetbrains 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/a-more-reliable-comp…] indexed:0 read:7min 2026-09-28 · —