Skip to content

Commit d938704

Browse files
committed
KMP Kotlin-to-Java direct actualization
1 parent eeaddfc commit d938704

File tree

1 file changed

+211
-0
lines changed

1 file changed

+211
-0
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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

Comments
 (0)