--- title: Компилятор K2 description: Фазы компиляции, что изменилось по сравнению с K1, умные приведения, экспериментальные флаги tags: - kotlin - tooling - compiler --- K2 - фронтенд компилятора Kotlin, стабильный с версии 2.0 и единственный в линии 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и, что важнее всего, обслуживает одновременно компилятор и IDE - поэтому подсветка ошибок в редакторе совпадает с результатом сборки. ```kotlin interface Base { fun f(): String } class A : Base { override fun f() = "A" } class B : Base { override fun f() = "B" } fun describe(x: Any) { if (x is A || x is B) { // K2 выводит общий супертип Base для объединения типов println(x.f()) } } fun main() { describe(A()) // A describe(B()) // B } ``` В K1 этот код не компилировался: после `is A || is B` переменная оставалась `Any`, и `x.f()` требовал явного приведения. ## Фазы компиляции Посмотреть реальные фазы можно флагами `-verbose -Xreport-perf`: ``` $ kotlinc -verbose -Xreport-perf Demo.kt -d out PERF: INIT 271 ms PERF: ANALYZE 530 ms 11.311 loc/s PERF: TRANSLATION to IR 144 ms 41.453 loc/s PERF: KLIB WRITING 0 ms PERF: IR LOWERING 122 ms 48.799 loc/s PERF: BACKEND 75 ms 79.456 loc/s ``` ```mermaid graph LR A[".kt"] --> B["INIT
загрузка classpath"] B --> C["ANALYZE
фронтенд K2: FIR"] C --> D["TRANSLATION
FIR → IR"] D --> E["IR LOWERING
развёртка inline,
suspend, лямбды"] E --> F["BACKEND
IR → байт-код"] F --> G[".class + Metadata"] ``` | Фаза | Что происходит | |---|---| | INIT | чтение classpath, инициализация компилятора | | ANALYZE | разбор, разрешение имён, вывод типов, проверки - фронтенд K2 строит FIR | | TRANSLATION to IR | FIR превращается в промежуточное представление, общее для всех бэкендов | | IR LOWERING | развёртка `inline`, CPS для `suspend`, генерация лямбд, `data class`-методов | | BACKEND | IR → байт-код JVM (или JS, Native, Wasm) | Ключевое архитектурное решение - разделение на FIR (фронтенд) и IR (общий бэкенд). Фронтенд один для всех платформ, бэкенды разные. Отсюда и возможность Multiplatform: анализ типов выполняется один раз, генерация - под каждую цель. ## Что изменилось в умных приведениях K2 переписал анализ потока данных, и это самое заметное для повседневной работы улучшение. **Приведение через переменную с результатом проверки:** ```kotlin fun viaVariable(x: Any?) { val isString = x is String if (isString) { // K2 приводит тип по val с результатом проверки println(x.length) } } fun main() { viaVariable("шесть!") // 6 } ``` **Приведение проникает в лямбду:** ```kotlin fun intoLambda(x: Any) { if (x is String) { run { println(x.length) } // приведение видно внутри лямбды } } fun main() { intoLambda("семь!!!") // 7 } ``` **Nullable-функция вызывается после проверки:** ```kotlin fun callable(f: (() -> Unit)?) { if (f != null) { f() // без ?.invoke() } } fun main() { callable { println("лямбда вызвана") } // лямбда вызвана } ``` **Исчерпывающий `when` по данным потока:** ```kotlin sealed interface State data object Loading : State data class Done(val value: String) : State fun render(s: State) = when (s) { Loading -> "загрузка" is Done -> "готово: ${s.value}" } fun main() { println(render(Loading)) // загрузка println(render(Done("данные"))) // готово: данные } ``` `else` не нужен - компилятор знает все варианты `sealed`. Подробнее: [Умные приведения](../Синтаксис%20и%20типы/Null-безопасность/Умные%20приведения.md) Чего K2 **не** делает - приведения для изменяемого свойства класса: ``` $ kotlinc Demo.kt -d out error: smart cast to 'String' is impossible, because 'value' is a mutable property that could be mutated concurrently. ``` Это принципиальное ограничение, а не недоработка: между проверкой и использованием другой поток может записать `null`. Решение - локальная копия: ```kotlin class Holder { var value: String? = "есть" } fun main() { val h = Holder() val snapshot = h.value if (snapshot != null) { println(snapshot.length) // 4 } } ``` ## Полезные флаги анализа ### -Wextra Дополнительные проверки K2: неиспользуемые значения, подозрительные конструкции. ``` kotlinc -Wextra Demo.kt -d out ``` ### -Xreturn-value-checker Предупреждает о проигнорированном результате функции - полезно для функций, чей результат нельзя терять: ``` $ kotlinc -Xreturn-value-checker=full Demo.kt -d out warning: unused return value of 'compute'. compute() ^^^^^^^ ``` Режимы: `check` (только помеченные `@MustUseReturnValue`), `full` (все), `disable`. Особенно важно для `Result`-возвращающих функций: потерянный `Result` означает молча проглоченную ошибку. Подробнее: [Result и runCatching](../Исключения/Result%20и%20runCatching.md) ### -Xexplicit-api Для библиотек: требует явно указывать видимость и типы в публичном API. ``` $ kotlinc -Xexplicit-api=strict Lib.kt -d out error: visibility must be specified in explicit API mode. class Public { ^^^^^^^^^^^^ error: visibility must be specified in explicit API mode. fun noExplicitType() = 42 ^^^^^^^^^^^^^^^^^^ ``` ```kotlin // так проходит explicit API mode public class Service { public fun compute(): Int = 42 private fun internalHelper(): Int = 1 } fun main() { println(Service().compute()) // 42 } ``` В Gradle - `kotlin { explicitApi() }`. Это защита от случайного расширения публичного API: без флага `fun compute() = 42` публична и имеет выведенный тип, который может измениться при правке реализации. ### Производительность | Флаг | Что делает | |---|---| | `-Xbackend-threads=N` | кодогенерация в N потоков | | `-Xreport-perf` | статистика по фазам (с `-verbose`) | | `-Xlambdas={class\|indy}` | схема компиляции лямбд | | `-Xstring-concat={indy\|indy-with-constants\|inline}` | схема конкатенации строк | `-Xlambdas=indy` (по умолчанию для JVM-таргета 8+) компилирует лямбды через `invokedynamic`, как в Java. `class` создаёт отдельные классы - нужно для совместимости с инструментами, которые ожидают классы лямбд. ## Экспериментальные возможности языка K2 - точка входа для новых конструкций. Полный список - `kotlinc -X`. | Флаг | Возможность | |---|---| | `-Xcontext-parameters` | context parameters (в 2.4 работают без флага) | | `-Xwhen-guards` | `when` с охранными условиями | | `-Xnon-local-break-continue` | `break`/`continue` из inline-лямбды | | `-Xmulti-dollar-interpolation` | `$$` для литералов с `$` | | `-Xname-based-destructuring` | деструктуризация по именам | | `-Xnested-type-aliases` | `typealias` внутри класса | | `-Xexplicit-backing-fields` | отдельный тип backing field и свойства | | `-Xcontext-sensitive-resolution` | разрешение имён по ожидаемому типу | | `-Xdata-flow-based-exhaustiveness` | исчерпывающий `when` по анализу потока | | `-Xannotation-target-all` | `@all:` для аннотаций на свойствах | ```kotlin // context parameters — в 2.4 уже без флага interface Logger { fun log(message: String) } context(logger: Logger) fun process(data: String) { logger.log("обработка: $data") } fun main() { val logger = object : Logger { override fun log(message: String) = println(message) } with(logger) { process("запись") // обработка: запись } } ``` Подробнее: [Receivers и context parameters](../Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md) ## Три версии, которые путают ```kotlin // build.gradle.kts import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion kotlin { jvmToolchain(21) compilerOptions { jvmTarget.set(JvmTarget.JVM_21) // версия байт-кода languageVersion.set(KotlinVersion.KOTLIN_2_4) // возможности языка apiVersion.set(KotlinVersion.KOTLIN_2_2) // доступный API stdlib } } ``` | Опция | Отвечает за | Типичная ошибка | |---|---|---| | `jvmTarget` | формат `.class` | оставить дефолтный 1.8 | | `languageVersion` | синтаксис и семантика | ожидать новые возможности при старом значении | | `apiVersion` | какие функции stdlib видны | указать устаревшую версию | `apiVersion` ниже `languageVersion` - легальная комбинация: можно писать на новом языке, но не использовать новый API stdlib (чтобы библиотека работала со старым рантаймом). Важно: устаревшая `apiVersion` сама даёт предупреждение. В Kotlin 2.4 значение 2.1 выдаёт «API version 2.1 is deprecated», и с `allWarningsAsErrors` это ломает сборку. Подробнее: [Gradle и Kotlin DSL](Gradle%20и%20Kotlin%20DSL.md) ## Progressive mode ```kotlin // build.gradle.kts kotlin { compilerOptions { progressiveMode.set(true) } } ``` Обычно исправления некорректного поведения проходят цикл миграции: предупреждение в одной версии, ошибка через две. Progressive mode применяет их сразу. Код, написанный в этом режиме, совместим с обычным, но не наоборот. Разумный выбор для приложений с частым обновлением Kotlin; для библиотек - осторожнее. ## Плагины компилятора K2 поддерживает плагины, работающие на уровне FIR/IR - они видят типы и могут генерировать код: | Плагин | Что делает | |---|---| | `kotlinx-serialization` | генерирует сериализаторы при компиляции | | `all-open` | делает классы `open` (для Spring, Mockito) | | `no-arg` | добавляет конструктор без аргументов (JPA) | | `kotlin-spring` | предустановленный `all-open` для Spring-аннотаций | | `kotlin-jpa` | предустановленный `no-arg` для JPA | | `kotlin-lombok` | понимание Lombok-геттеров в смешанном коде | | `power-assert` | подробные сообщения в `assert` | | `compose` | оптимизация composable-функций | ```kotlin // build.gradle.kts plugins { kotlin("jvm") version "2.4.0" kotlin("plugin.serialization") version "2.4.0" kotlin("plugin.spring") version "2.4.0" kotlin("plugin.jpa") version "2.4.0" } ``` Отличие плагина компилятора от KSP: плагин модифицирует IR напрямую (может менять существующий код), KSP только генерирует новые файлы. Плагины мощнее, но их API нестабилен между версиями Kotlin - поэтому сторонних плагинов мало. Подробнее: [KSP](KSP.md) ## Инкрементальная компиляция ``` # gradle.properties kotlin.incremental=true kotlin.incremental.useClasspathSnapshot=true kotlin.daemon.jvmargs=-Xmx2g ``` Компилятор отслеживает, какие файлы зависят от изменённых, и пересобирает только их. Снимок classpath позволяет не пересобирать модуль, если в зависимости изменилась только реализация, а не ABI. Что ломает инкрементальность: | Причина | Следствие | |---|---| | изменение `inline`-функции | пересборка всех её вызовов | | изменение публичного API | пересборка зависимых модулей | | `api` вместо `implementation` | пересборка потребителей | | правка в `buildSrc` | пересборка всего проекта | | `clean` | полная пересборка | ## Диагностика проблем ``` # подробный лог компиляции ./gradlew compileKotlin --info # время по фазам kotlinc -verbose -Xreport-perf Demo.kt -d out # что попало в байт-код kotlinc Demo.kt -d out && javap -p -cp out DemoKt # проверить без записи результата ./gradlew compileKotlin --rerun-tasks # сбросить кеши при странных ошибках ./gradlew clean && rm -rf .gradle .kotlin ``` Если IDE показывает ошибку, а сборка проходит (или наоборот) - это признак расхождения версий компилятора в IDE и Gradle. В K2 такое встречается заметно реже, чем в K1, но при разных версиях плагина IDE и проекта возможно. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `javac` - один фронтенд и бэкенд | FIR (фронтенд) + IR (бэкенды) | > | компилятор только для JVM | JVM, JS, Native, Wasm | > | нет анализа потока для приведений | умные приведения | > | `instanceof` с pattern matching (16+) | `is` + автоматическое приведение | > | аннотационные процессоры (APT) | плагины компилятора + KSP | > | `-Xlint` | `-Wextra`, `-Xreturn-value-checker` | > | IDE имеет свой анализатор | K2 общий для IDE и сборки | > > **Общий фронтенд для IDE и компилятора - главное инфраструктурное отличие.** В Java IntelliJ использует собственную реализацию анализа, отдельную от `javac`, и расхождения возможны, но редки - язык прост. Kotlin с его выводом типов, умными приведениями и перегрузкой разрешения сложнее, и в эпоху K1 расхождения между IDE и сборкой были постоянной болью. K2 устранил причину: это один и тот же код. > > **Умные приведения заменяют то, что в Java решается pattern matching.** Java 16 добавил `if (o instanceof String s)` - явное связывание переменной. Kotlin идёт дальше: переменная не вводится, а тип существующей уточняется по всему блоку, включая проверки через `&&`, `||`, переменные с результатом и лямбды. Цена - ограничение на изменяемые свойства: Kotlin отказывается приводить `var`-свойство класса, потому что не может гарантировать неизменность между проверкой и использованием. Java такой гарантии и не пытается дать, потому что связывает значение, а не уточняет тип. > > **Плагины компилятора - то, чего в Java осознанно нет.** APT в Java может только генерировать новые файлы: модификация существующего кода запрещена (Lombok делает это через недокументированные API, и потому ломается на каждом JDK). Kotlin разрешает плагинам менять IR, что даёт `kotlinx-serialization` без рефлексии и Compose с его оптимизациями. Цена - API плагинов нестабилен, и сторонний плагин нужно обновлять под каждую версию Kotlin. > > **Многоплатформенность определила архитектуру.** `javac` компилирует в байт-код JVM - это его единственная задача. Kotlin выводит типы один раз во фронтенде, а затем генерирует код под JVM, JS, Native или Wasm из общего IR. Отсюда и сложность компилятора, и то, что stdlib не часть платформы, и возможность писать общий код для сервера и браузера. > > Сравнить: [javac](../../Программирование%20на%20Java/JDK/javac.md), [JIT](../../Программирование%20на%20Java/JVM/JIT.md) > [!warning] Подводные камни > **Умное приведение не работает для `var`-свойства класса:** > ```kotlin > class Holder { var value: String? = "есть" } > > fun main() { > val h = Holder() > // if (h.value != null) println(h.value.length) // ошибка компиляции > val snapshot = h.value > println(if (snapshot != null) snapshot.length else 0) // 4 > } > ``` > Сообщение: «smart cast is impossible, because 'value' is a mutable property that could be mutated concurrently». Это не обойти флагом - нужна локальная копия. > > **`-Xexplicit-api` обнаруживает проблему поздно.** Включение на существующей библиотеке даёт сотни ошибок сразу. Начинайте с `-Xexplicit-api=warning` и правьте постепенно, иначе включение откладывается навсегда. > > **Экспериментальные флаги `-X` не гарантируют совместимость.** Возможность может измениться или исчезнуть в следующей версии. Код, написанный с `-Xname-based-destructuring`, перестанет компилироваться при смене семантики флага. Для production-кода используйте только стабильные возможности. > > **`progressiveMode` - односторонний переход.** Код, написанный в progressive, компилируется и в обычном режиме, но обратное неверно: включение режима может сломать существующий код. Для библиотек, собираемых пользователями, это риск. > > **Устаревшая `apiVersion` вместе с `-Werror` роняет сборку.** В Kotlin 2.4 `apiVersion = 2.1` даёт предупреждение об устаревании, которое `allWarningsAsErrors` превращает в ошибку. Поддерживается около трёх предыдущих версий. > > **Изменение `inline`-функции обнуляет инкрементальность её вызовов.** Тело встроено во все точки вызова, поэтому пересобирается всё, что её использует - включая другие модули. `inline` в публичном API библиотеки фиксирует реализацию в ABI. > > **Нехватка памяти демона маскируется под медленную сборку.** Без `kotlin.daemon.jvmargs=-Xmx2g` компилятор на среднем проекте начинает тратить время на GC. Ошибки нет - просто сборка идёт в разы дольше. > > **`-Xbackend-threads` не всегда ускоряет.** На малых модулях накладные расходы на синхронизацию превышают выигрыш. Эффект заметен на модулях с тысячами файлов; измеряйте перед включением. > > **Расхождение IDE и сборки означает разные версии компилятора.** Плагин Kotlin в IDE и версия в `build.gradle.kts` должны соответствовать. Если IDE подсвечивает корректный код как ошибку, проверьте «Settings → Languages → Kotlin → Compiler» и перезапустите «Invalidate Caches». > > **`-Xreport-perf` без `-verbose` ничего не печатает.** Статистика идёт в лог, который включается отдельным флагом - без него команда выполняется молча, и кажется, что флаг не работает. > > **Плагин компилятора фиксирует версию Kotlin.** Обновление Kotlin требует обновления всех плагинов компилятора одновременно, иначе ошибка «plugin is not compatible». Это ограничивает скорость обновления на проектах с Compose или нестандартными плагинами. Далее: [KSP](KSP.md)