Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user