Files
obsidian/Kotlin/Инструментарий/Компилятор K2.md
T

387 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<br/>загрузка classpath"]
B --> C["ANALYZE<br/>фронтенд K2: FIR"]
C --> D["TRANSLATION<br/>FIR → IR"]
D --> E["IR LOWERING<br/>развёртка inline,<br/>suspend, лямбды"]
E --> F["BACKEND<br/>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)