[K2] Document KMP implementation in the K2 Compiler
Add comprehensive documentation explaining the implementation and functions of the Kotlin multiplatform (KMP) support in the K2 Compiler.
This commit is contained in:
committed by
Space Team
parent
04c0cc5a5c
commit
024e94a479
@@ -37,6 +37,14 @@ import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmProtoBufUtil
|
||||
import org.jetbrains.kotlin.utils.KotlinPaths
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* This class is the entry-point for compiling Kotlin code into a metadata KLib.
|
||||
*
|
||||
* **Note: `2` in the name stands for Kotlin `TO` metadata compiler.
|
||||
* This entry-point used by both K1 and K2.**
|
||||
*
|
||||
* Please see `/docs/fir/k2_kmp.md` for more info on the K2/FIR implementation.
|
||||
*/
|
||||
class K2MetadataCompiler : CLICompiler<K2MetadataCompilerArguments>() {
|
||||
|
||||
override val defaultPerformanceManager: CommonCompilerPerformanceManager = K2MetadataCompilerPerformanceManager()
|
||||
|
||||
@@ -9,6 +9,16 @@ import org.jetbrains.kotlin.name.ClassId
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
import org.jetbrains.kotlin.types.model.TypeConstructorMarker
|
||||
|
||||
/**
|
||||
* The main purpose of the lookup tag is to provide a reference to concrete classifier that
|
||||
* allows obtaining FirClassifierSymbol within a given use-site session.
|
||||
*
|
||||
* During the deserialization we use lookup tags to avoid loading the entire class-type hierarchy.
|
||||
*
|
||||
* Use-site lookup of classifiers that are referenced by lookup tags allows type refinement that is needed for the expect/actual support.
|
||||
*
|
||||
* See `/docs/fir/k2_kmp.md`
|
||||
*/
|
||||
abstract class ConeClassifierLookupTag : TypeConstructorMarker {
|
||||
abstract val name: Name
|
||||
|
||||
@@ -17,6 +27,9 @@ abstract class ConeClassifierLookupTag : TypeConstructorMarker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ConeClassifierLookupTag
|
||||
*/
|
||||
abstract class ConeClassLikeLookupTag : ConeClassifierLookupTag() {
|
||||
abstract val classId: ClassId
|
||||
|
||||
|
||||
@@ -14,6 +14,13 @@ import org.jetbrains.kotlin.ir.util.IdSignatureComposer
|
||||
import org.jetbrains.kotlin.ir.util.SymbolTable
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* This storage contains the shared state of FIR2IR.
|
||||
*
|
||||
* State is shared between the conversions of different modules in the same platform compilation.
|
||||
*
|
||||
* See `/docs/fir/k2_kmp.md`
|
||||
*/
|
||||
class Fir2IrCommonMemberStorage(
|
||||
signatureComposer: IdSignatureComposer,
|
||||
firMangler: FirMangler
|
||||
|
||||
@@ -93,6 +93,27 @@ inline val FirBasedSymbol<*>.isJavaOrEnhancement: Boolean
|
||||
|
||||
private fun FirFunction.containsDefaultValue(index: Int): Boolean = valueParameters[index].defaultValue != null
|
||||
|
||||
/**
|
||||
* Checks, if the value parameter has a default value w.r.t expect/actuals.
|
||||
*
|
||||
* Requires [FirResolvePhase.EXPECT_ACTUAL_MATCHING]
|
||||
*
|
||||
* In expect/actual declarations, the presence of default values can be determined by the expect declaration.
|
||||
*
|
||||
* ```kotlin
|
||||
* // MODULE: common
|
||||
* expect fun foo(bar: Int = 42)
|
||||
*
|
||||
* // MODULE: platform()(common)
|
||||
* actual fun foo(bar: Int) { ... }
|
||||
* ```
|
||||
*
|
||||
* See `/docs/fir/k2_kmp.md`
|
||||
*
|
||||
* @param index Index of the value parameter to check
|
||||
* @return `true` if a parameter has defined default value, or if there is a default value defined on the expect declaration
|
||||
* for this actual.
|
||||
*/
|
||||
fun FirFunction.itOrExpectHasDefaultParameterValue(index: Int): Boolean =
|
||||
containsDefaultValue(index) || symbol.getSingleExpectForActualOrNull()?.fir?.containsDefaultValue(index) == true
|
||||
|
||||
|
||||
@@ -29,6 +29,15 @@ import org.jetbrains.kotlin.types.ConstantValueKind
|
||||
import org.jetbrains.kotlin.util.WeakPair
|
||||
import org.jetbrains.kotlin.utils.exceptions.errorWithAttachment
|
||||
|
||||
/**
|
||||
* Main operation on the [ConeClassifierLookupTag]
|
||||
*
|
||||
* Lookups the tag into its target within the given [useSiteSession]
|
||||
*
|
||||
* The second step of type refinement, see `/docs/fir/k2_kmp.md`
|
||||
*
|
||||
* @see ConeClassifierLookupTag
|
||||
*/
|
||||
fun ConeClassifierLookupTag.toSymbol(useSiteSession: FirSession): FirClassifierSymbol<*>? =
|
||||
when (this) {
|
||||
is ConeClassLikeLookupTag -> toSymbol(useSiteSession)
|
||||
@@ -37,6 +46,9 @@ fun ConeClassifierLookupTag.toSymbol(useSiteSession: FirSession): FirClassifierS
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* @see toSymbol
|
||||
*/
|
||||
@OptIn(LookupTagInternals::class)
|
||||
fun ConeClassLikeLookupTag.toSymbol(useSiteSession: FirSession): FirClassLikeSymbol<*>? {
|
||||
if (this is ConeClassLookupTagWithFixedSymbol) {
|
||||
@@ -49,6 +61,9 @@ fun ConeClassLikeLookupTag.toSymbol(useSiteSession: FirSession): FirClassLikeSym
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see toSymbol
|
||||
*/
|
||||
fun ConeClassLikeLookupTag.toFirRegularClassSymbol(session: FirSession): FirRegularClassSymbol? =
|
||||
toSymbol(session) as? FirRegularClassSymbol
|
||||
|
||||
|
||||
@@ -19,6 +19,17 @@ import org.jetbrains.kotlin.util.WeakPair
|
||||
import org.jetbrains.kotlin.util.component1
|
||||
import org.jetbrains.kotlin.util.component2
|
||||
|
||||
/**
|
||||
* Compute the recursive type-alias expansion in the given type.
|
||||
*
|
||||
* A type of an expect class, that is actualized by a typealias will be expanded to the expansion of the typealias,
|
||||
* when supplied with the session of actual.
|
||||
*
|
||||
* See `/docs/fir/k2_kmp.md`
|
||||
*
|
||||
* @param useSiteSession Session to be used for classifier lookups, see [toSymbol]
|
||||
* @return Type, that is expanded to the concrete class type w.r.t to the [useSiteSession]
|
||||
*/
|
||||
fun ConeClassLikeType.fullyExpandedType(
|
||||
useSiteSession: FirSession,
|
||||
expandedConeType: (FirTypeAlias) -> ConeClassLikeType? = { alias ->
|
||||
@@ -40,6 +51,9 @@ fun ConeClassLikeType.fullyExpandedType(
|
||||
return fullyExpandedTypeNoCache(useSiteSession, expandedConeType)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see fullyExpandedType
|
||||
*/
|
||||
fun ConeKotlinType.fullyExpandedType(
|
||||
useSiteSession: FirSession
|
||||
): ConeKotlinType = when (this) {
|
||||
@@ -50,6 +64,9 @@ fun ConeKotlinType.fullyExpandedType(
|
||||
else -> this
|
||||
}
|
||||
|
||||
/**
|
||||
* @see fullyExpandedType
|
||||
*/
|
||||
fun ConeSimpleKotlinType.fullyExpandedType(
|
||||
useSiteSession: FirSession
|
||||
): ConeSimpleKotlinType = when (this) {
|
||||
@@ -167,6 +184,9 @@ private fun mapTypeAliasArguments(
|
||||
return substitutor.substituteOrSelf(resultingType)
|
||||
}
|
||||
|
||||
/**
|
||||
* @see fullyExpandedType
|
||||
*/
|
||||
fun FirTypeAlias.fullyExpandedConeType(useSiteSession: FirSession): ConeClassLikeType? {
|
||||
return expandedConeType?.fullyExpandedType(useSiteSession)
|
||||
}
|
||||
|
||||
+8
@@ -31,6 +31,14 @@ class FirExpectActualMatcherProcessor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This transformer populates [expectForActual] mapping for actual declarations.
|
||||
* Also, populates it [memberExpectForActual] mapping
|
||||
*
|
||||
* Should run before any kind of body resolution, since [expectForActual] is used there.
|
||||
*
|
||||
* See `/docs/fir/k2_kmp.md`
|
||||
*/
|
||||
open class FirExpectActualMatcherTransformer(
|
||||
final override val session: FirSession,
|
||||
private val scopeSession: ScopeSession,
|
||||
|
||||
@@ -17,12 +17,27 @@ private object ExpectForActualAttributeKey : FirDeclarationDataKey()
|
||||
|
||||
typealias ExpectForActualData = Map<ExpectActualCompatibility<FirBasedSymbol<*>>, List<FirBasedSymbol<*>>>
|
||||
|
||||
/**
|
||||
* Actual declaration -> (many) expect declaration mapping. For top-level declarations.
|
||||
*
|
||||
* Populated by [FirResolvePhase.EXPECT_ACTUAL_MATCHING] phase for every top-level actual declaration.
|
||||
*
|
||||
* **Note: Mapping is computed from the declaration-site session of the actual declaration and not refined on the use-sites**
|
||||
*
|
||||
* See `/docs/fir/k2_kmp.md`
|
||||
*/
|
||||
@SymbolInternals
|
||||
var FirDeclaration.expectForActual: ExpectForActualData? by FirDeclarationDataRegistry.data(ExpectForActualAttributeKey)
|
||||
|
||||
/**
|
||||
* @see expectForActual
|
||||
*/
|
||||
fun FirFunctionSymbol<*>.getSingleExpectForActualOrNull(): FirFunctionSymbol<*>? =
|
||||
(this as FirBasedSymbol<*>).getSingleExpectForActualOrNull() as? FirFunctionSymbol<*>
|
||||
|
||||
/**
|
||||
* @see expectForActual
|
||||
*/
|
||||
fun FirBasedSymbol<*>.getSingleExpectForActualOrNull(): FirBasedSymbol<*>? {
|
||||
val expectForActual = expectForActual ?: return null
|
||||
val compatibleOrWeakCompatible: List<FirBasedSymbol<*>> =
|
||||
@@ -30,6 +45,9 @@ fun FirBasedSymbol<*>.getSingleExpectForActualOrNull(): FirBasedSymbol<*>? {
|
||||
return compatibleOrWeakCompatible.singleOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* @see expectForActual
|
||||
*/
|
||||
val FirBasedSymbol<*>.expectForActual: ExpectForActualData?
|
||||
get() {
|
||||
lazyResolveToPhase(FirResolvePhase.EXPECT_ACTUAL_MATCHING)
|
||||
@@ -45,4 +63,40 @@ typealias MemberExpectForActualData =
|
||||
Map<Pair</* actual member */ FirBasedSymbol<*>, /* expect class */ FirRegularClassSymbol>,
|
||||
Map</* expect member */ FirBasedSymbol<*>, ExpectActualCompatibility<*>>>
|
||||
|
||||
/**
|
||||
* Actual class + expect class + actual member declaration -> (many) expect member declaration mapping.
|
||||
*
|
||||
* This attribute allows finding complex actualizations through type-aliases.
|
||||
*
|
||||
* ```kotlin
|
||||
* // MODULE: common
|
||||
* expect class A {
|
||||
* fun foo() // expect.1
|
||||
* }
|
||||
* expect class B {
|
||||
* fun foo() // expect.2
|
||||
* }
|
||||
*
|
||||
* // MODULE: platform()(common)
|
||||
*
|
||||
* actual typealias A = Impl
|
||||
* actual typealias B = Impl
|
||||
*
|
||||
* // attribute: memberExpectForActual
|
||||
* // value: {
|
||||
* // (symbol: expect class A, symbol: impl.1) => { symbol: expect.1 => Compatible },
|
||||
* // (symbol: expect class B, symbol: impl.1) => { symbol: expect.2 => Compatible },
|
||||
* // }
|
||||
* class Impl {
|
||||
* fun foo() {} // impl.1
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Populated by [FirResolvePhase.EXPECT_ACTUAL_MATCHING] phase for every top-level class declaration that is either actual class or
|
||||
* expansion of actual type-alias.
|
||||
*
|
||||
* **Note: Mapping is computed from the declaration-site session of the actual declaration and not refined on the use-sites**
|
||||
*
|
||||
* See `/docs/fir/k2_kmp.md`
|
||||
*/
|
||||
var FirRegularClass.memberExpectForActual: MemberExpectForActualData? by FirDeclarationDataRegistry.data(MemberExpectForActualAttributeKey)
|
||||
|
||||
+3
@@ -7,6 +7,9 @@ package org.jetbrains.kotlin.fir.symbols
|
||||
|
||||
import org.jetbrains.kotlin.fir.symbols.impl.FirClassifierSymbol
|
||||
|
||||
/**
|
||||
* @see ConeClassifierLookupTag
|
||||
*/
|
||||
abstract class ConeClassifierLookupTagWithFixedSymbol : ConeClassifierLookupTag() {
|
||||
abstract val symbol: FirClassifierSymbol<*>
|
||||
}
|
||||
+7
@@ -19,6 +19,13 @@ import org.jetbrains.kotlin.ir.util.*
|
||||
|
||||
data class IrActualizedResult(val actualizedExpectDeclarations: List<IrDeclaration>)
|
||||
|
||||
/**
|
||||
* IrActualizer is responsible for performing actualization.
|
||||
*
|
||||
* The main action is the replacement of references to expect declarations with corresponding actuals on the IR level.
|
||||
*
|
||||
* See `/docs/fir/k2_kmp.md`
|
||||
*/
|
||||
object IrActualizer {
|
||||
fun actualize(
|
||||
mainFragment: IrModuleFragment,
|
||||
|
||||
+6
@@ -23,6 +23,12 @@ import org.jetbrains.kotlin.utils.keysToMap
|
||||
import org.jetbrains.kotlin.utils.zipIfSizesAreEqual
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This object is responsible for matching and checking of
|
||||
* expect-actual pairs
|
||||
*
|
||||
* See `/docs/fir/k2_kmp.md` for details
|
||||
*/
|
||||
object AbstractExpectActualCompatibilityChecker {
|
||||
fun <T : DeclarationSymbolMarker> getClassifiersCompatibility(
|
||||
expectClassSymbol: RegularClassSymbolMarker,
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
# 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)
|
||||
- [IRActualizer](#iractualizer)
|
||||
- [Fake overrides](#fake-overrides)
|
||||
- [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.
|
||||
|
||||
### 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.
|
||||
|
||||
**Outputs:**
|
||||
- Single IR module.
|
||||
- No expect declarations 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 is constructed
|
||||
- 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
|
||||
- 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/)
|
||||
- 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 actualization is performed](#fake-overrides)
|
||||
|
||||
### 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
|
||||
By that, we require special handling of fake-overrides during the [platform compilation](#platform-compilation).
|
||||
|
||||
during the analysis and couldn't construct fake-overrides properly.
|
||||
|
||||
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) {}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
In the given example during the actualization, we need to combine (1) and (2) into one fake-override.
|
||||
|
||||
## 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-vs-checking).
|
||||
|
||||
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 vs checking
|
||||
|
||||
Currently, both matching and a part of checking are performed in the `AbstractExpectActualCompatibilityChecker`.
|
||||
|
||||
See: [`org.jetbrains.kotlin.resolve.calls.mpp.AbstractExpectActualCompatibilityChecker`](../../compiler/resolution.common/src/org/jetbrains/kotlin/resolve/calls/mpp/AbstractExpectActualCompatibilityChecker.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.
|
||||
|
||||
Checking is a process of checking that a pair is correct w.r.t all compatibility requirements.
|
||||
If checking failed for a pair, error is reported for the pair.
|
||||
It will fail compilation.
|
||||
|
||||
## 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() {}
|
||||
```
|
||||
Reference in New Issue
Block a user