Skip to content

Commit efedc52

Browse files
committed
KMP Kotlin-to-Java direct actualization
Kotlin#392 Kotlin#391
1 parent ef917ee commit efedc52

File tree

1 file changed

+212
-0
lines changed

1 file changed

+212
-0
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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**: [KEEP-391](https://github.com/Kotlin/KEEP/issues/391)
7+
* **Status**: Implemented as experimental feature in 2.1. Stabilization plans are unclear due to implementation challenges
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", emptyList(), "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 avoids 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+
As if `KotlinActual` was annotated with `@kotlin.Deprecated(level = DeprecationLevel.ERROR)`
76+
- The annotation in Java will function similarly to the `actual` keyword in Kotlin
77+
- It doesn't make sense to mark the annotation with `@ExperimentalMultiplatform` since `OPT_IN_USAGE_ERROR` is not reported in Java
78+
- `ElementType.FIELD` annotation target is not specified by design. It's not possible to actualize Kotlin properties with Java fields
79+
80+
**(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.
81+
In other words, support _direct actualization_ for Kotlin-to-Java actualization.
82+
83+
**(3)** Kotlin compiler should require using `@KotlinActual` on the Java top level class and its respective members.
84+
The rules must be similar to the one with `actual` keyword requirement in Kotlin-to-Kotlin actualization.
85+
86+
**(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.
87+
The rules must be similar to the one with `actual` keyword requirement in Kotlin-to-Kotlin actualization.
88+
Please note that Kotlin can only detect excessive `@KotlinActual` annotations on methods of the classes that actualize some existing Kotlin expect classes.
89+
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.
90+
For the same reason, it's not possible to detect excessive `@KotlinActual` annotation on members of such Java classes.
91+
For these cases, it's proposed to implement Java IDE inspection.
92+
93+
**Worth noting.**
94+
1. Java wasn't capable and still is not capable of actualizing Kotlin top-level functions in any way.
95+
2. Kotlin expect classes are not capable of expressing Java static members [KT-29882](https://youtrack.jetbrains.com/issue/KT-29882)
96+
97+
Example of a valid Kotlin-to-Java direct actualization:
98+
```kotlin
99+
// MODULE: common
100+
expect class Foo() {
101+
fun foo()
102+
}
103+
104+
// MODULE: JVM
105+
@kotlin.annotations.jvm.KotlinActual public class Foo {
106+
@kotlin.annotations.jvm.KotlinActual public Foo() {}
107+
@kotlin.annotations.jvm.KotlinActual public void foo() {}
108+
109+
@Override
110+
public String toString() { return "Foo"; } // No @KotlinActual is required
111+
}
112+
```
113+
114+
## actual keyword is a virtue
115+
116+
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.
117+
118+
The suggestion was rejected because we consider `actual` keyword being beneficial for readers, much like the `override` keyword.
119+
120+
- **Misspelling prevention.**
121+
The explicit `actual` keyword (or `@KotlinActual` annotation) helps against misspelling
122+
(esp. when Kotlin supports expect declarations with bodies [KT-20427](https://youtrack.jetbrains.com/issue/KT-20427))
123+
- **Explicit intent.**
124+
Declarations may be written solely to fullfil the "expect requirement" but the declarations may not be directly used by the platform code.
125+
The `actual` keyword (or `@KotlinActual` annotation) explictly signals the intention to actualize member rather than accidentally defining a new one.
126+
(too bad that members of `actual typealias` break the design in this place)
127+
- **Safeguards during refactoring.**
128+
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.
129+
Without the keyword, there might be already a suitable overload in the actual class that would silently become a new actualization.
130+
131+
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):
132+
[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)
133+
134+
## The proposal doesn't cover Kotlin-free pure Java library use case
135+
136+
There are two cases:
137+
1. The user has a Kotlin-Java mixed project, and they want to KMP-fy it.
138+
2. The user has a pure Java project, and they want to KMP-fy it.
139+
140+
The first case is handled by [the proposal](#the-proposal).
141+
142+
In the second case, the common part of the project is Kotlin sources that depend on `kotlin-stdlib.jar`.
143+
The common part of the project may also define additional regular non-expect Kotlin declarations.
144+
JVM part of the project depends on common.
145+
146+
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.
147+
[The proposal](#the-proposal) doesn't cover that case well since the design would become more complicated.
148+
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.
149+
150+
`KotlinActual` annotation has `SOURCE` retention by design.
151+
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`.
152+
Which doesn't contradict the "pure Java project" case.
153+
154+
## Kotlin-to-Java expect-actual incompatibilities diagnostics reporting
155+
156+
**Invariant 1.** Kotlin compiler cannot report compilation errors in non-kt files.
157+
158+
In Kotlin-to-Kotlin actualization, expect-actual incompatibilities are reported on the actual side.
159+
160+
In Kotlin-to-Java actualization, it's proposed to report incompatibilities on the expect side.
161+
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_.
162+
The reporting may be improved in future versions of Kotlin.
163+
164+
## Direct actualization forces expect and actual to be in the same compilation unit
165+
166+
In the [introduction](#introduction), we mentioned that _direct actualization_ forces expect and actual to be in the same "compilation unit."
167+
It's an implementation limitation that we believe is beneficial, because it reflects the reality.
168+
169+
It's a common pattern for libraries to use a unique package prefix.
170+
We want people to stick to that pattern.
171+
_Direct actualization_ for external declarations encourages wrong behavior.
172+
173+
- Imagine that it is possible to write an expect declaration for the existing JVM library via _direct actualization_ mechanism.
174+
Users may as easily decide to declare non-expect declarations in the same package, which leads to the "split package" problem in JPMS.
175+
- Test case: there is an existing JVM library A.
176+
Library B is a KMP wrapper around library A.
177+
Library B provides expect declarations for the library A.
178+
Later, Library A decides to provide its own KMP API.
179+
If Library B could use _direct actualization_, it would lead to declarations clash between A and B.
180+
181+
## The frontend technical limitation of Kotlin-to-Java direct actualization
182+
183+
Frontend transformers are run only on Kotlin sources.
184+
Java sources are visited lazily only if they are referenced from Kotlin sources.
185+
186+
Given frontend technical restriction, it's proposed to implement Kotlin-to-Java _direct actualization_ matching and checking only on IR backend.
187+
188+
## Alternatives considered
189+
190+
- Implicitly match Kotlin-to-Java with the same ClassId if some predefined annotation is presented on the expect declaration.
191+
- `actual typealias` in Kotlin without target in RHS.
192+
- `actual` declaration that doesn't generate class files.
193+
194+
It could be some special annotation that says that bytecode shouldn't be generated for the class.
195+
The idea is useful by itself, for example, in stdlib, to declare `kotlin.collections.List`.
196+
197+
The disadvantages are clear: unlike the current proposal, there will be no compilation time checks;
198+
compared to the current proposal, it will result in excessive code duplication in the expect-actual case.
199+
200+
See [actual keyword is a virtue](#actual-keyword-is-a-virtue) to understand why alternatives were discarded.
201+
Besides, the proposed solution resembles the already familiar Kotlin-to-Kotlin _direct actualization_, but makes it available for Java.
202+
203+
## Unused declaration inspection in Java
204+
205+
IntelliJ IDEA implements an unused declaration inspection.
206+
The `javac` itself doesn't emit warnings for unused declarations.
207+
208+
The inspection in IDEA should be changed to account for declarations annotated with `@kotlin.annotations.jvm.KotlinActual`.
209+
210+
## Feature interaction with hierarchical multiplatform
211+
212+
There is no feature interaction. It's not possible to have Java files in intermediate fragments.

0 commit comments

Comments
 (0)