24 KiB
title, description, tags
| title | description | tags | |||
|---|---|---|---|---|---|
| Компилятор K2 | Фазы компиляции, что изменилось по сравнению с K1, умные приведения, экспериментальные флаги |
|
K2 - фронтенд компилятора Kotlin, стабильный с версии 2.0 и единственный в линии 2.x. Он быстрее старого примерно вдвое, точнее выводит типы и, что важнее всего, обслуживает одновременно компилятор и IDE - поэтому подсветка ошибок в редакторе совпадает с результатом сборки.
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
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 переписал анализ потока данных, и это самое заметное для повседневной работы улучшение.
Приведение через переменную с результатом проверки:
fun viaVariable(x: Any?) {
val isString = x is String
if (isString) {
// K2 приводит тип по val с результатом проверки
println(x.length)
}
}
fun main() {
viaVariable("шесть!") // 6
}
Приведение проникает в лямбду:
fun intoLambda(x: Any) {
if (x is String) {
run { println(x.length) } // приведение видно внутри лямбды
}
}
fun main() {
intoLambda("семь!!!") // 7
}
Nullable-функция вызывается после проверки:
fun callable(f: (() -> Unit)?) {
if (f != null) {
f() // без ?.invoke()
}
}
fun main() {
callable { println("лямбда вызвана") } // лямбда вызвана
}
Исчерпывающий when по данным потока:
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. Подробнее: Умные приведения
Чего 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. Решение - локальная копия:
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
-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
^^^^^^^^^^^^^^^^^^
// так проходит 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: для аннотаций на свойствах |
// 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
Три версии, которые путают
// 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
Progressive mode
// 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-функций |
// 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
Инкрементальная компиляция
# 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-checkerIDE имеет свой анализатор 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 не часть платформы, и возможность писать общий код для сервера и браузера.
[!warning] Подводные камни Умное приведение не работает для
var-свойства класса: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.4apiVersion = 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