|
| 1 | +# KMP Kotlin-to-Java direct actualization |
| 2 | + |
| 3 | +* **Type**: Design proposal |
| 4 | +* **Author**: Nikita Bobko |
| 5 | +* **Contributors**: Dmitriy Novozhilov, Kevin Bierhoff, Mikhail Zarechenskiy, Pavel Kunyavskiy |
| 6 | +* **Discussion**: todo |
| 7 | +* **Status**: Submitted |
| 8 | +* **Related YouTrack issue**: [KT-67202](https://youtrack.jetbrains.com/issue/KT-67202) |
| 9 | + |
| 10 | +**Definition.** ClassId is a class identifier that consists of three independent components: the package where class is declared in, names of all outer classes (if presented), and the name of the class itself. |
| 11 | +In Kotlin, it could be represented as the data class: |
| 12 | +```kotlin |
| 13 | +data class ClassId(val package: String, val outerClasses: List<String>, val className: String) |
| 14 | +``` |
| 15 | + |
| 16 | +Two ClassIds are equal if their respective components are equal. |
| 17 | +Example: `ClassId("foo.bar", emptyListOf(), "baz")` and `ClassId("foo", listOf("bar"), "baz")` are different ClassIds |
| 18 | + |
| 19 | +## Introduction |
| 20 | + |
| 21 | +In Kotlin, there are **two ways** to write an actual declaration for the existing expect declaration. |
| 22 | +You can either write an actual declaration with the same ClassId as its appropriate expect and mark the appropriate declarations with `expect` and `actual` keywords (From now on, we will call such actualizations _direct actualizations_), |
| 23 | +or you can use `actual typealias`. |
| 24 | + |
| 25 | +**The first way.** |
| 26 | +_direct actualization_ has a nice property that two declarations share the same ClassId. |
| 27 | +It's good because when users move code between common and platform fragments, their imports stay unchanged. |
| 28 | +But _direct actualization_ has a "downside" that it doesn't allow declaring actuals in external binaries (jars or klibs). |
| 29 | +In other words, expect declaration and its appropriate actual must be located in the same "compilation unit." |
| 30 | +[Below](#direct-actualization-forces-expect-and-actual-to-be-in-the-same-compilation-unit) we say why, in fact, it's not a "downside" but a "by design" restriction that reflects the reality. |
| 31 | + |
| 32 | +**The second way.** |
| 33 | +Contrary, `actual typealias` forces users to change the ClassId of the actual declaration. |
| 34 | +(An attempt to specify the very same ClassId in the `typealias` target leads to `RECURSIVE_TYPEALIAS_EXPANSION` diagnostic) |
| 35 | +But we gain the possibility to declare expect and actual declarations in different "compilation units." |
| 36 | + |
| 37 | +> [!NOTE] |
| 38 | +> Though it's a philosophical question what is "the real actual declaration" in this case. |
| 39 | +> Is it the `actual typealias` itself (which is still declared in the same "compilation unit"), or is it the target of the `actual typealias` (which, in fact, can be declared in external jar or klib)? |
| 40 | +
|
| 41 | +| | _Direct actualization_ | `actual typealias` | |
| 42 | +|---------------------------------------------------------------------|------------------------|--------------------| |
| 43 | +| Do expect and actual share the same ClassId? | Yes | No | |
| 44 | +| Can expect and actual be declared in different "compilation units"? | No | Yes | |
| 45 | + |
| 46 | +While `actual typealias` already allows actualizing Kotlin expect declarations with Java declarations (Informally: Kotlin-to-Java actualization), _direct actualization_ only allows Kotlin-to-Kotlin actualizations. |
| 47 | +The idea of this proposal is to support _direct actualization_ for Kotlin-to-Java actualizations. |
| 48 | + |
| 49 | +## Motivation |
| 50 | + |
| 51 | +As stated in the [introduction](#introduction), unlike `actual typealias`, _direct actualization_ allows to keep the same ClassIds for common and platform declarations in case of Kotlin-to-Java actualization. |
| 52 | + |
| 53 | +One popular use case for Kotlin-to-Java actualization is KMP-fying existing Java libraries. |
| 54 | +For library authors, the possibility to keep the same ClassIds between common and platform declarations is highly valuable: |
| 55 | + |
| 56 | +- Since it avoids the creation of two ClassIds that refer to the same object, it avoids the confusion on which ClassId should be used |
| 57 | +- It simplifies the migration of client code from the Java library to a KMP version of the same library (no need to replace imports) |
| 58 | +- It avoid duplication of potentially entire API surface, which can otherwise become cumbersome |
| 59 | +- Later replacing the Java actualization with a Kotlin `actual` class is possible without keeping the previous `actual typealias` in place indefinitely |
| 60 | + |
| 61 | +## The proposal |
| 62 | + |
| 63 | +**(1)** Introduce `kotlin.annotations.jvm.KotlinActual` annotation in `kotlin-annotations-jvm.jar` |
| 64 | +```java |
| 65 | +package kotlin.annotations.jvm; |
| 66 | + |
| 67 | +@Retention(RetentionPolicy.SOURCE) |
| 68 | +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR}) |
| 69 | +public @interface KotlinActual { |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +- The annotation is intended to be used on Java declarations |
| 74 | +- **Any usage** of the annotation in Kotlin will be prohibited (even as a type, even as a reference) |
| 75 | +- The annotation in Java will function similarly to the `actual` keyword in Kotlin |
| 76 | +- It doesn't make sense to mark the annotation with `@ExperimentalMultiplatform` since `OPT_IN_USAGE_ERROR` is not reported in Java |
| 77 | +- `ElementType.FIELD` annotation target is not specified by design. It's not possible to actualize Kotlin properties with Java fields |
| 78 | + |
| 79 | +**(2)** If Kotlin expect and Java class have the same ClassId, Kotlin compiler should consider Kotlin expect class being actualized with the appropriate Java class. |
| 80 | +In other words, support _direct actualization_ for Kotlin-to-Java actualization. |
| 81 | + |
| 82 | +**(3)** Kotlin compiler should require using `@KotlinActual` on the Java top level class and its respective members. |
| 83 | +The rules must be similar to the one with `actual` keyword requirement in Kotlin-to-Kotlin actualization. |
| 84 | + |
| 85 | +**(4)** If `@KotlinActual` is used on Java class members that don't have respective Kotlin expect members, it should be reported by the Kotlin compiler. |
| 86 | +The rules must be similar to the one with `actual` keyword requirement in Kotlin-to-Kotlin actualization. |
| 87 | +Please note that Kotlin can only detect excessive `@KotlinActual` annotations on methods of the classes that actualize some existing Kotlin expect classes. |
| 88 | +Since Kotlin doesn't traverse all Java files, it's not possible to detect excessive `@KotlinActual` annotation on the top-level Java classes for which a respective Kotlin expect class doesn't exist. |
| 89 | +For the same reason, it's not possible to detect excessive `@KotlinActual` annotation on members of such Java classes. |
| 90 | +For these cases, it's proposed to implement Java IDE inspection. |
| 91 | + |
| 92 | +**Worth noting.** |
| 93 | +1. Java wasn't capable and still is not capable of actualizing Kotlin top-level functions in any way. |
| 94 | +2. Kotlin expect classes are not capable of expressing Java static members [KT-29882](https://youtrack.jetbrains.com/issue/KT-29882) |
| 95 | + |
| 96 | +Example of a valid Kotlin-to-Java direct actualization: |
| 97 | +```kotlin |
| 98 | +// MODULE: common |
| 99 | +expect class Foo() { |
| 100 | + fun foo() |
| 101 | +} |
| 102 | + |
| 103 | +// MODULE: JVM |
| 104 | +@kotlin.annotations.jvm.KotlinActual public class Foo { |
| 105 | + @kotlin.annotations.jvm.KotlinActual public Foo() {} |
| 106 | + @kotlin.annotations.jvm.KotlinActual public void foo() {} |
| 107 | + |
| 108 | + @Override |
| 109 | + public String toString() { return "Foo"; } // No @KotlinActual is required |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +## actual keyword is a virtue |
| 114 | + |
| 115 | +An alternative suggestion is to match only by ClassIds, and to drop `actual` keyword in Kotlin-to-Kotlin actualizations, and to drop `@KotlinActual` annotation in Kotlin-to-Java actualization. |
| 116 | + |
| 117 | +The suggestion was rejected because we consider `actual` keyword being beneficial for readers, much like the `override` keyword. |
| 118 | + |
| 119 | +- **Misspelling prevention.** |
| 120 | + The explicit `actual` keyword (or `@KotlinActual` annotation) helps against misspelling |
| 121 | + (esp. when Kotlin supports expect declarations with bodies [KT-20427](https://youtrack.jetbrains.com/issue/KT-20427)) |
| 122 | +- **Explicit intent.** |
| 123 | + Declarations may be written solely to fullfil the "expect requirement" but the declarations may not be directly used by the platform code. |
| 124 | + The `actual` keyword (or `@KotlinActual` annotation) explictly signals the intention to actualize member rather than accidentally defining a new one. |
| 125 | + (too bad that members of `actual typealias` break the design in this place) |
| 126 | +- **Safeguards during refactoring.** |
| 127 | + If the member in expect class changes (e.g. a new parameter added), `actual` keyword (or `@KotlinActual` annotation) helps to quickly identify members in the actual class that needs an update. |
| 128 | + Without the keyword, there might be already a suitable overload in the actual class that would silently become a new actualization. |
| 129 | + |
| 130 | +To support our arguments, we link Swift community disscussions about the explicit keyword for protocol conformance (as of swift 5.9, no keyword is required): |
| 131 | +[1](https://forums.swift.org/t/pre-pitch-explicit-protocol-fulfilment-with-the-conformance-keyword/60246), [2](https://forums.swift.org/t/keyword-for-protocol-conformance/3837) |
| 132 | + |
| 133 | +## The proposal doesn't cover Kotlin-free pure Java library use case |
| 134 | + |
| 135 | +There are two cases: |
| 136 | +1. The user has a Kotlin-Java mixed project, and they want to KMP-fy it. |
| 137 | +2. The user has a pure Java project, and they want to KMP-fy it. |
| 138 | + |
| 139 | +The first case is handled by [the proposal](#the-proposal). |
| 140 | + |
| 141 | +In the second case, the common part of the project is Kotlin sources that depend on `kotlin-stdlib.jar`. |
| 142 | +The common part of the project may also define additional regular non-expect Kotlin declarations. |
| 143 | +JVM part of the project depends on common. |
| 144 | + |
| 145 | +If users want to keep their JVM part free of Kotlin, they have to be accurate and avoid accidental usages of `kotlin-stdlib.jar`, and avoid declaring additional non-expect declarations in the common. |
| 146 | +[The proposal](#the-proposal) doesn't cover that case well since the design would become more complicated. |
| 147 | +The current proposal is a small incremental addition to the existing model, and it doesn't block us from covering the second case later if needed. |
| 148 | + |
| 149 | +`KotlinActual` annotation has `SOURCE` retention by design. |
| 150 | +This way, the annotation is least invasive for the Java sources, and it should be enough to have compile-only dependency on `kotlin-annotations-jvm.jar`. |
| 151 | +Which doesn't contradict the "pure Java project" case. |
| 152 | + |
| 153 | +## Kotlin-to-Java expect-actual incompatibilities diagnostics reporting |
| 154 | + |
| 155 | +**Invariant 1.** Kotlin compiler cannot report compilation errors in non-kt files. |
| 156 | + |
| 157 | +In Kotlin-to-Kotlin actualization, expect-actual incompatibilities are reported on the actual side. |
| 158 | + |
| 159 | +In Kotlin-to-Java actualization, it's proposed to report incompatibilities on the expect side. |
| 160 | +It's inconsistent with Kotlin-to-Kotlin actualizations, but we don't believe that Kotlin-to-Java actualization is significant enough to break the _invariant 1_. |
| 161 | +The reporting may be improved in future versions of Kotlin. |
| 162 | + |
| 163 | +## Direct actualization forces expect and actual to be in the same compilation unit |
| 164 | + |
| 165 | +In the [introduction](#introduction), we mentioned that _direct actualization_ forces expect and actual to be in the same "compilation unit." |
| 166 | +It's an implementation limitation that we believe is beneficial, because it reflects the reality. |
| 167 | + |
| 168 | +It's a common pattern for libraries to use a unique package prefix. |
| 169 | +We want people to stick to that pattern. |
| 170 | +_Direct actualization_ for external declarations encourages wrong behavior. |
| 171 | + |
| 172 | +- Imagine that it is possible to write an expect declaration for the existing JVM library via _direct actualization_ mechanism. |
| 173 | + Users may as easily decide to declare non-expect declarations in the same package, which leads to the "split package" problem in JPMS. |
| 174 | +- Test case: there is an existing JVM library A. |
| 175 | + Library B is a KMP wrapper around library A. |
| 176 | + Library B provides expect declarations for the library A. |
| 177 | + Later, Library A decides to provide its own KMP API. |
| 178 | + If Library B could use _direct actualization_, it would lead to declarations clash between A and B. |
| 179 | + |
| 180 | +## The frontend technical limitation of Kotlin-to-Java direct actualization |
| 181 | + |
| 182 | +Frontend transformers are run only on Kotlin sources. |
| 183 | +Java sources are visited lazily only if they are referenced from Kotlin sources. |
| 184 | + |
| 185 | +Given frontend technical restriction, it's proposed to implement Kotlin-to-Java _direct actualization_ matching and checking only on IR backend. |
| 186 | + |
| 187 | +## Alternatives considered |
| 188 | + |
| 189 | +- Implicitly match Kotlin-to-Java with the same ClassId if some predefined annotation is presented on the expect declaration. |
| 190 | +- `actual typealias` in Kotlin without target in RHS. |
| 191 | +- `actual` declaration that doesn't generate class files. |
| 192 | + |
| 193 | + It could be some special annotation that says that bytecode shouldn't be generated for the class. |
| 194 | + The idea is useful by itself, for example, in stdlib, to declare `kotlin.collections.List`. |
| 195 | + |
| 196 | + The disadvantages are clear: unlike the current proposal, there will be no compilation time checks; |
| 197 | + compared to the current proposal, it will result in excessive code duplication in the expect-actual case. |
| 198 | + |
| 199 | +See [actual keyword is a virtue](#actual-keyword-is-a-virtue) to understand why alternatives were discarded. |
| 200 | +Besides, the proposed solution resembles the already familiar Kotlin-to-Kotlin _direct actualization_, but makes it available for Java. |
| 201 | + |
| 202 | +## Unused declaration inspection in Java |
| 203 | + |
| 204 | +IntelliJ IDEA implements an unused declaration inspection. |
| 205 | +The `javac` itself doesn't emit warnings for unused declarations. |
| 206 | + |
| 207 | +The inspection in IDEA should be changed to account for declarations annotated with `@kotlin.annotations.jvm.KotlinActual`. |
| 208 | + |
| 209 | +## Feature interaction with hierarchical multiplatform |
| 210 | + |
| 211 | +There is no feature interaction. It's not possible to have Java files in intermediate fragments. |
0 commit comments