Files
kotlin-fork/docs/fir/k2_kmp.md
T

561 lines
23 KiB
Markdown

# K2 KMP (Kotlin multiplatform) implementation documentation
This document describes the implementation of the KMP support in the K2 Compiler along components.
**Note**: The current compilation model is temporary, and should be changed to final in [KT-57327](https://youtrack.jetbrains.com/issue/KT-57327)
## Document structure
- [Glossary](#glossary)
- [Metadata Compilation](#metadata-compilation)
- [Platform Compilation](#platform-compilation)
- [FIR2IR](#fir2ir)
- [Fake overrides](#fake-overrides)
- [Lazy classes](#lazy-classes)
- [IRActualizer](#iractualizer)
- [Frontend support for expect/actual](#frontend-support-for-expectactual)
- [Type refinement](#type-refinement)
- [Default propagation](#default-propagation)
- [Actual -> expect binding construction](#fir-actual---expect-binding-construction)
- [Frontend checkers](#frontend-checkers)
## Glossary
### Source-set
A bunch of source files along with its depends-on relations with other source-sets.
See: https://kotlinlang.org/docs/multiplatform-discover-project.html#source-sets
**Note:** Binary dependencies are provided on the [compilation](#compilation) level and not considered a part of the source-set definition
by the compiler.
### Module
A compiler module, entity with source-code and dependencies.
- One-to-one mapped to the corresponding FirSession.
- One-to-one mapped to the FirModuleData.
- Compiler representation of a [source-set](#source-set)
In a [platform compilation](#platform-compilation),
[multiple modules are created from the input](#source-set-and-module-relation) [source-sets](#source-set).
### Compilation
A single invocation of the compiler entry-point
### Metadata dependencies/KLibs
- KLib that only contains frontend metadata, without IR. It can only be used to analyze dependent code.
### HMPP
Hierarchical multi-platform projects
- See https://kotlinlang.org/docs/multiplatform-hierarchy.html
### Actualization
The process of replacing references to expect declarations with corresponding actual declarations.
See [IRActualizer](#iractualizer)
### actual->expect binding
A relation between two declarations that form an expect-actual pair.
- For classes: defined as identical ClassId.
- For callables: defined as complex matching rule, which is similar to overloading.
In general case, the relation is many-to-many.
However, in correct code it is either one-to-one, or one-to-many in case of type-aliases.
See [FIR: actual -> expect binding construction](#fir-actual---expect-binding-construction)
### Common source-set
A [source-set] that may contain expect declarations.
As opposed to a platform [source-sets](#source-set) that must only contain actual or regular declarations.
Common source-sets usually contain code that is shared across different targets, and by that is included
in multiple [platform compilations](#platform-compilation).
However, one may define [HMPP](#hmpp) hierarchy with only one target while still having common source-sets.
## Metadata Compilation
- Aimed to check code platform-independent compilation.
- Each [common source set](#common-source-set) is compiled with its metadata binary dependencies.
- Artifacts aren't used for subsequent platform compilations but for the IDE and other metadata compilations.
- In this compilation, we analyze code as if it is fully isolated from the corresponding actualizations.
- Doing so will allow us to check that code can be compiled for multiple targets.
- Only the [common source-set](#common-source-set) and its [metadata binary dependencies](#metadata-dependenciesklibs) are taken into account.
- No access to the target-specific dependencies or source-sets.
- Source-sets from depends-on are provided as [metadata binary dependencies](#metadata-dependenciesklibs) in the form of unpacked klib
- See `-Xrefines-path`
**Entry-point:** [`org.jetbrains.kotlin.cli.metadata.K2MetadataCompiler`](../../compiler/cli/src/org/jetbrains/kotlin/cli/metadata/K2MetadataCompiler.kt)
Also, see: [`org.jetbrains.kotlin.cli.metadata.FirMetadataSerializer`](../../compiler/cli/src/org/jetbrains/kotlin/cli/metadata/FirMetadataSerializer.kt)
**Inputs:**
- [Metadata KLibs for dependencies](#metadata-dependenciesklibs)
- [Metadata KLibs for dependsOn source-sets](#metadata-dependenciesklibs)
- Source files of a single [common source-set](#common-source-set), such as "commonMain"
**Outputs:**
- [Metadata KLibs](#metadata-dependenciesklibs)
In this mode, we analyze only common sources.
This compilation mode is very similar to a usual compilation pipeline with the exception that no IR is serialized to the output.
Actual-expect [matching](#frontend-support-for-expectactual) and [checking](#frontend-checkers) are performed,
since in [HMPP](#hmpp) we can have actuals in metadata compilation inputs.
**Note:** The full set of actual declarations can't be determined at that point.
The only actual -> expect binding could be obtained and checked.
**Note:** In this compilation mode [actual->expect binding](#actual-expect-binding) may point to a deserialized element,
since expect declarations from depends-on source-sets will be present as [metadata binary dependencies](#metadata-dependenciesklibs).
## Platform Compilation
- Invoked per each target.
- Input includes [source-sets](#source-set) from entire target depends-on graph.
- Outputs a platform artifact (jar file or klib).
- [Source-sets](#source-set) form a directed acyclic graph.
- The graph must be terminated by one leaf [source-set](#source-set)
- Compilation requires a platform classpath. See: [binary dependencies](#platform-compilation-binary-dependencies)
Please consult with https://kotlinlang.org/docs/multiplatform-hierarchy.html for more info on source-set structure.
This is the main part where the pipeline diverges from simple compilation.
Ex:
```kotlin
// MODULE: common
expect fun a1(): String
expect fun a2(): String
// MODULE: intermediate()()(common)
actual fun a1(): String = ""
expect fun a4(): String
expect fun a5(): String
// MODULE: unrelated
expect fun a3(): String
expect fun a5(): String
// MODULE: platform()()(intermediate, unrelated)
actual fun a2(): String = ""
actual fun a3(): String = ""
actual fun a4(): String = ""
actual fun a5(): String = "" // ambiguous
```
In the given example, modules aka source-sets form the following dependency graph:
```
common
^
|
intermediate unrelated
^ ^
| /
| /
platform
```
### Source-set and module relation
During the platform compilation, the entire [source-set](#source-set) graph is passed to the compiler CLI.
See: `-Xfragments`, `-Xfragment-sources`, `-Xfragment-refines`.
When combined with [binary dependencies](#platform-compilation-binary-dependencies) passed, it allows constructing graph of
compiler [modules](#module).
Compiler [module](#module) is created for each passed [source-set](#source-set).
### Platform compilation binary dependencies
The K2 uses shared **platform binary dependencies** for analysis of **all** source-sets in the platform compilation.
The key reason for that is that we are unable to provide full KLibs for [common source-sets](#common-source-set) yet.
Observed behavior is the following:
```kotlin
// MODULE: common_dep
// library: dep
fun foo(a: Any) {} // (dep.1)
// MODULE: platform_dep
// library: dep
fun foo(a: String) {} // (dep.2)
// MODULE: common
// dependencies { implementation("dep") }
fun bar(a: Any) {} // (a.1)
fun test() {
bar("") // (a.1)
foo("") // (dep.2) !!! While platform compilation for this module
}
// MODULE: platform
// depends-on: common
fun bar(a: String) {} // (a.2)
```
### Platform compilation pipeline
Given that, source analysis order is defined as the following:
Analyze from the most common module to the platform modules.
So that all `depends-on` modules of the module are analyzed before the module itself.
In order to achieve it, [modules](#module) are sorted topologically over depends-on relation graph.
Platform compilation pipeline consists of the following steps:
1. For each [module](#module) in source analysis order
- [Use a shared set of binary dependencies](#platform-compilation-binary-dependencies)
- [Run the frontend, store FIR representation.](#frontend-support-for-expectactual)
- Use FIR representations of depends on modules that were obtained before as dependencies.
- *[Resolution requires knowledge of actual->expect binding.](#fir-actual---expect-binding-construction)
- [Run the frontend checkers for module FIR.](#frontend-checkers)
- [Run the FIR2IR, store the resulting IR module fragment.](#fir2ir)
- *[Re-use the FIR2IR state](#fir2ir-shared-state)
2. [Combine all resulting IR module fragments](#iractualizer)
- Perform [actualization](#actualization)
- Check that every expect declaration has corresponding actual and was actualized.
- *Reconstruct fake-overrides.
- Remove expect declarations.
- Merge IR module fragments into last fragment
- Now, we have complete IR for module that doesn't reference any expects and could be passed to backend.
3. Pass the resulting IR to the backend or serialize it into KLib.
- Backend compiler plugins are invoked before running the backend
### FIR2IR
FIR2IR is applied to the modules in the topological order.
**Inputs:**
- FIR of one of the [modules](#module)
- [Compilation shared state](#fir2ir-shared-state)
**Outputs:**
- IR for the module
- Mutation of [shared state](#fir2ir-shared-state)
#### FIR2IR shared state
The FIR2IR state, represented by `Fir2IrCommonMemberStorage` is shared across entire [platform compilation](#platform-compilation).
See: [`org.jetbrains.kotlin.fir.backend.Fir2IrCommonMemberStorage`](../../compiler/fir/fir2ir/src/org/jetbrains/kotlin/fir/backend/Fir2IrCommonMemberStorage.kt)
Since FIR2IR is invoked over each [module](#module) over depends-on graph,
we need to avoid creating IR for declarations in a [common source-sets](#common-source-set) multiple times.
The same applies to the declarations from [binary dependencies](#platform-compilation-binary-dependencies), which are shared among the entire compilation.
Therefore, FIR2IR uses shared storage for get-or-create operations for declarations.
### Fake overrides
FIR doesn't use fake-overrides in the frontend, and that's why we construct them during FIR2IR in order to match backend expectations
Also, the following example shows that it is impossible to build valid fake overrides during common
module compilation.
Ex:
```kotlin
// MODULE: common
expect class A
expect class B
interface I {
fun foo(q: A)
}
interface J {
fun foo(q: B)
}
interface K : I, J {
// FIR2IR F/O fun foo(q: A) (1)
// FIR2IR F/O fun foo(q: B) (2)
}
// MODULE: platform()()(common)
actual typealias A = Int
actual typealias B = Int
class Impl : K {
override fun foo(q: Int) {}
}
```
***Note: Approach described below is a target. It's not yet enabled by default. See KT-61514 for details.***
In the given example during the actualization, common compilation would produce (1) and (2), which need to be combined
into one fake override.
There is a new scheme, which is enabled by -Xuse-ir-fake-override-builder flag, and should become default at some point.
In it, [Fir2Ir](#fir2ir) creates a special symbol for fake overrides calls, and no declarations for fake overrides
(except one in [Lazy classes](#lazy-classes)). A special symbol, represented by [org.jetbrains.kotlin.ir.symbols.impl.IrFakeOverrideSymbolBase](../../compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/symbols/impl/IrFakeOverrideSymbol.kt) and its inheritors,
is effectively a pair of real declaration symbol and dispatch receiver class. This symbol can't be bound,
so backend can't work with it. To fix it, there is a phase, which replaces them with normal ones
after [actualization](#actualization). The symbol is mapped to single one overriding corresponding
real symbol within corresponding dispatch receiver class. It can be both a fake override and real declared symbol.
In theory, doing the same with other synthetic declarations (delegated members, data class generated members, etc)
can lead to more consistent behaviour, but we don't do it know, as we are not aware of any problems
### Lazy classes
Lazy classes represent classes used from compiled sources, but not present in them. This can be classes
from dependencies, java sources, or sources already compiled on previous round of incremental compilation.
Lazy classes are a special case for fake override building. They are now handled with frontend builder.
It can be done correctly, as they can only exist in platform session.
There is a technical issue with them. It's possible to have a lazy class with normal class super-type,
so it can refer to a fake override symbol in overridden symbols of some methods.
But we can't fix them all in the same place, as normal ones, as it would trigger lazy computations, which should be avoided.
To fix this, the rebuild is delayed while possible, using [org.jetbrains.kotlin.fir.backend.Fir2IrSymbolsMappingForLazyClasses](../../compiler/fir/fir2ir/src/org/jetbrains/kotlin/fir/backend/Fir2IrSymbolsMappingForLazyClasses.kt),
which stores delayed operations to happen, when some read lazy property.
### IRActualizer
The IR actualizer is a component performing [actualization](#actualization) over IR
See: [`org.jetbrains.kotlin.backend.common.actualizer.IrActualizer`](../../compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/actualizer/IrActualizer.kt)
In the current model, IR actualizer is used during the [platform compilation](#platform-compilation), to produce complete IR that is correct
from the backend standpoint.
**Inputs:**
- IR for each of the modules.
- IR might contain expect declarations and references to it.
- IR can contain unbound fake override symbols
**Outputs:**
- Single IR module.
- No expect declarations in the IR.
- No unbound symbols in the IR
- IR is in the usual state, as for non-multiplatform projects
**Constraints:**
- No access to FIR or frontend state allowed
- Must operate over IR
- Diagnostic reporting is complicated and won't work for the IDE without special support
The following actions are preformed:
- Expect -> actual binding for classifiers is constructed and applied
- Fake overrides are constructed
- Expect -> actual binding for classifiers is callables and applied
- We have all expects and that's why we can construct it as opposed to the [actual -> expect binding](#actual-expect-binding)
- We can report diagnostics at that moment
- IR for default values is copied from expects to actuals. See [default propagation](#default-propagation) for the frontend part of work.
- All module fragments are merged into one
- Fake override symbols are replaced with normal ones
- Constants are evaluated
- Top-level expect declarations are detached from IrModuleFragment
- Member expect declarations can only be contained within top-level expects, and is detached together with its container
- Modulo [`@OptionalExpectation`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-optional-expectation/)
- Errors can be emitted if some expect-actual matches are bad.
There are several restrictions, because of which this order of actions is required:
- Fake overrides can't be built before classes are actualized
- Because we need to know what is real supertype.
- Callables can't be actulaized before fake overrides are built
- Some of them can match with fake override
- Constants can't be evaluated before callables are actualized
- As some expect function can become possible to evaluate
- Checkers can't run before constants are evaluated
- They need to check of some value are equal
## Frontend support for expect/actual
During the resolution of each module, a special set of measures is implemented to allow proper resolution.
### Type refinement
Type refinement is a process of obtaining use-site view to the declaration.
Since we re-use FIR of each module during the analysis of its dependants, we need to perform
type refinement.
```kotlin
// MODULE: common
expect class Foo
val foo: Foo = TODO()
// declaration-site type: expect class Foo
// MODULE: platform()()(common)
actual class Foo {
fun bar() { }
}
fun test() {
foo.bar() // use-site type: actual class Foo
}
```
**It is possible due to the following principles:**
- It isn't possible to obtain the scope of a type without knowing the use-site
- It isn't possible to obtain the type declaring class without knowing the use-site
- Every type is subject to the refinement before use
- ClassId can only be used to identify class within one module
Refinement of a type happens in two stages:
1. `ConeKotlinType.fullyExpandedType(useSiteSession: FirSession): ConeKotlinType`
- At that stage, we unwrap any type-aliases in the type using useSiteSession as a point of view.
2. `ConeClassifierLookupTag.toSymbol(useSiteSession: FirSession): FirClassifierSymbol<*>`
- All classifier types contain lookupTag inside them.
- To obtain reference to the type declaring class, one should call toSymbol on it providing the useSiteSession as a point of view.
- Then, the lookup of the class in the use-site module will be performed based on the class id stored within the lookup tag.
**WARNING:** Type refinement is also needed for general dependency substitution algorithm, such as classpath order substitution.
**It works for expect/actual classes only because [actual->expect binding](#actual-expect-binding) is defined as a matching of ClassId**
### Default propagation
We need to know a set of arguments that should be provided for a call to resolve it due to the Kotlin resolution and overloading rules.
```kotlin
// MODULE: common
expect fun foo(a: String = "")
expect class Bar {
fun buz(a: String = "")
}
// MODULE: intermediate()()(common)
actual fun foo(a: String) {}
actual class Bar {
fun buz(a: String) {}
}
fun test() {
foo() // use-site
Bar().buz() // use-site
}
```
In the given example, we need to know [actual->expect binding](#actual-expect-binding) in order to resolve use-site calls.
As during the resolution on the use-site, we will resolve to the actual declarations, we need to obtain its default argument positions from
actuals.
### FIR: actual -> expect binding construction
We compute that binding by analyzing module with `FirExpectActualMatcherTransformer`.
See: [`org.jetbrains.kotlin.fir.resolve.transformers.mpp.FirExpectActualMatcherTransformer`](../../compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/transformers/mpp/FirExpectActualMatcherTransformer.kt)
**The responsibility** of this phase is to perform [expect/actual matching](#matching).
This phase happens before body/implicit type resolution
as the binding is required to perform [call resolution in case of defaults](#default-propagation)
Binding is stored in the `FirDeclaration.expectForActual` attribute and allow to determine expects that corresponds to the
actual declaration during the analysis of `intermediate` module.
**Hard-constraint:**
`FirExpectActualMatcherTransformer` cannot use return types of declarations to bind actual with expect.
Since the return type of actual function might be not yet resolved before the implicit type body resolve phase.
```kotlin
// MODULE: common
expect fun foo(a: String = "")
// MODULE: platform()()(common)
actual fun foo(a: String) = run {
foo() // In order to resolve this call, we need to have actual -> expect binding
}
```
**Explanation:** While it is possible to compute the binding on-demand and remove this constraint,
we chose to have it as a separate phase to avoid further complication of body resolve.
There are no overloads by return type in Kotlin, and it makes it possible to avoid return type matching as a part of
[actual->expect binding construction](#fir-actual---expect-binding-construction).
However, we still [check](#frontend-checkers) that return type matches afterward.
#### Matching
Expect-actual matching is performed in the `AbstractExpectActualMatcher`.
See: [`org.jetbrains.kotlin.resolve.calls.mpp.AbstractExpectActualMatcher`](../../compiler/resolution.common/src/org/jetbrains/kotlin/resolve/calls/mpp/AbstractExpectActualMatcher.kt)
Matching is a process of finding expect-actual pairs.
For classes, it is matching of class-ids.
For callables, it is a complex rule, similar to overloading.
If declarations don't match, no pair is formed and **matching continues** over other declarations.
All possible mismatches that can be reported by `AbstractExpectActualMatcher` are declared in
[`ExpectActualMatchingCompatibility`](../../core/compiler.common/src/org/jetbrains/kotlin/resolve/multiplatform/ExpectActualCompatibility.kt)
#### Checking
Expect-actual checking is performed in the `AbstractExpectActualChecker`.
See: [`org.jetbrains.kotlin.resolve.calls.mpp.AbstractExpectActualChecker`](../../compiler/resolution.common/src/org/jetbrains/kotlin/resolve/calls/mpp/AbstractExpectActualChecker.kt)
Checking is a process of checking that a pair of [already matched declarations](#matching) is correct w.r.t all compatibility requirements.
If checking failed for a pair, error is reported for the pair.
It will fail compilation.
All possible "checking" incompatibilities that can be reported by `AbstractExpectActualChecker` are declared in
[`ExpectActualCheckingCompatibility`](../../core/compiler.common/src/org/jetbrains/kotlin/resolve/multiplatform/ExpectActualCompatibility.kt)
## Frontend checkers
Frontend checkers are executed in the context of each module (use-site), after its resolution.
**Inputs:**
- FIR of one of the [modules](#module)
- It can contain both expects and actuals
**Constraints:**
- The full set of actuals aren't known yet
- [Actual -> expect binding](#actual-expect-binding) is available
- [Limitations](#limitations)
### Limitations
The key limitation is the fact that frontend checkers are run in the context of declaration site.
Thus, it is impossible to observe member scopes of classes with respect to actualization since the corresponding actuals
are contained in further modules.
Type checks can also be performed with knowledge available on expect declaration site.
In order to reduce the need to perform additional checks on the backend and in the [IRActualizer](#iractualizer) it is advised to
follow the [LSP](https://en.wikipedia.org/wiki/Liskov_substitution_principle) in the design.
By that, meaning that actual declaration must be as compatible with the corresponding expect declaration, as that
it could replace the expect declaration, without the appearance of errors.
**Note:** There are compiler checks that violate LSP to the extent that it is reported on certain leaf types.
It is expected that such checks won't trigger in case of actualization.
E.g:
```kotlin
// MODULE: common
expect class E()
fun foo() {
E() // It's OK, since it isn't deprecated in common
}
// MODULE: platform()()(common)
@Deprecated("Not OK")
actual class E actual constructor() {}
```