skip to content

Explain how the Kotlin compiler uses the `dependsOn` graph when compiling a single target, and why a single `expect` in commonMain can resolve to different `actual`s for different targets.

level: middleimportance: should knowfreq 40%

answer

  1. One compilation per target + a metadata compilation
  2. Compiler takes the dependsOn closure of the leaf
  3. Different closures -> different actual per target
  4. Common metadata compilation checks expect signatures only
  5. Missing/duplicate actual caught at target compile time

basics

~10 s

To build one target, the compiler collects that platform's source set plus everything it depends on up to commonMain, then matches each shared declaration to the platform's own implementation.

solid answer

~40 s

Compilation is **per target**. To compile, say, the `iosArm64` target, the compiler gathers the leaf `iosArm64Main` plus every source set it `dependsOn` transitively — `iosMain`, `appleMain`, `nativeMain`, `commonMain` — into one compilation unit. Within that unit it resolves each `expect` to the single reachable `actual`. When it later compiles the `jvm` target, it gathers `jvmMain + commonMain` (a different chain), so the SAME `expect fun foo()` in commonMain resolves to the JVM `actual`. That's why one shared declaration yields different real implementations per platform: each target compiles its own union of the hierarchy. The shared `commonMain` code is type-checked once against the `expect` signatures (a 'common' metadata compilation) and again concretely per target, guaranteeing every leaf provides a valid `actual`.

go deeper

for a junior

Knows shared code gets a platform implementation when each platform is built.

for a middle

Explains per-target compilation gathers the dependsOn closure and resolves expect to the reachable actual.

for a senior

Adds the common metadata compilation, when errors surface, and how intermediate code compiles per target.

for a principal

Reasons about build performance and binary layering from per-target closures and the implications for incremental builds and published klibs.

## Per-target compilation KMP does not compile 'the whole project' into one binary. It runs **one compilation per target** (plus a metadata compilation for shared code). Each target's compilation takes a specific slice of the source-set graph. ## What gets gathered For a leaf source set L (e.g. `iosArm64Main`), the compiler forms the **closure** of L over `dependsOn`: ``` iosArm64Main -> iosMain -> appleMain -> nativeMain -> commonMain ``` All those `.kt` files compile together as ONE unit for the iosArm64 target. Inside it, the compiler: 1. Reads every `expect` declaration (from commonMain / intermediates). 2. Finds the single `actual` reachable in that closure. 3. Links them, then emits the target binary (a `.klib`/framework for native, classes for JVM). ## Why the same expect maps to different actuals Because each target compiles a DIFFERENT closure: ```kotlin // commonMain expect fun threadName(): String // jvmMain (closure: jvmMain+commonMain) actual fun threadName() = Thread.currentThread().name // iosArm64Main resolves via appleMain's actual (closure: leaf+...+commonMain) // appleMain actual fun threadName() = "apple-thread" ``` The `jvm` compilation never sees the Apple `actual`; the iOS compilation never sees the JVM one. So `threadName()` is genuinely polymorphic across builds — chosen at compile time by which closure includes which `actual`. ## The metadata (common) compilation There is also a **common metadata compilation** that type-checks `commonMain` against only the `expect` signatures (no actuals). This is what lets shared code be written and checked independently and packaged for consumers. It guarantees commonMain stays platform-agnostic. ## Consequences - A missing `actual` is caught when the specific target compiles, not in the common compilation. - Intermediate-set code (e.g. `appleMain`) is compiled into every Apple target that includes it, so it's shared source but separately compiled per target. - Two actuals in one closure = ambiguity error; zero = missing-actual error.

  • Why is a missing `actual` not reported during the common metadata compilation?
    The metadata compilation only checks the expect signatures; actuals are resolved per target, so the missing-actual error surfaces when that specific target compiles.
  • Is intermediate code like appleMain compiled once or per target?
    Its source is shared, but it is compiled into each Apple target's binary separately, as part of that target's closure.

Like baking the same recipe (commonMain) in different kitchens (targets): each kitchen adds its own local ingredients (actuals), producing different dishes from one recipe.

saying these in an interview costs you the question

  • Thinking the whole project compiles into one binary
  • Believing all actuals are visible in every compilation
  • Not knowing about the common metadata compilation
  • Claiming the same expect maps to one fixed actual everywhere
  • Expecting missing-actual errors only in commonMain

context