IC: Implement expect/actual tracking

fixes NewMultiplatformIT.testIncrementalCompilation
also #KT-61590 fixed
This commit is contained in:
Ilya Chernikov
2023-09-14 17:26:32 +02:00
committed by Space Team
parent 9ed23a7f07
commit e5ee364419
15 changed files with 117 additions and 46 deletions
@@ -6,21 +6,23 @@
package org.jetbrains.kotlin.backend.common.actualizer
import org.jetbrains.kotlin.KtDiagnosticReporterWithImplicitIrBasedContext
import org.jetbrains.kotlin.incremental.components.ExpectActualTracker
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.PsiIrFileEntry
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.symbols.*
import org.jetbrains.kotlin.ir.types.IrTypeSystemContext
import org.jetbrains.kotlin.ir.types.classifierOrFail
import org.jetbrains.kotlin.ir.util.callableId
import org.jetbrains.kotlin.ir.util.classIdOrFail
import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
import org.jetbrains.kotlin.ir.visitors.IrElementVisitor
import org.jetbrains.kotlin.mpp.DeclarationSymbolMarker
import org.jetbrains.kotlin.mpp.RegularClassSymbolMarker
import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.calls.mpp.AbstractExpectActualCompatibilityChecker
import org.jetbrains.kotlin.resolve.multiplatform.ExpectActualCompatibility
import java.io.File
/**
* This class is used to collect mapping between all expect and actuals declarations, which are declared in passed module fragments
@@ -41,6 +43,7 @@ internal class ExpectActualCollector(
private val dependentFragments: List<IrModuleFragment>,
private val typeSystemContext: IrTypeSystemContext,
private val diagnosticsReporter: KtDiagnosticReporterWithImplicitIrBasedContext,
private val expectActualTracker: ExpectActualTracker?,
) {
data class Result(val expectToActualMap: MutableMap<IrSymbol, IrSymbol>, val classActualizationInfo: ClassActualizationInfo)
@@ -61,12 +64,16 @@ internal class ExpectActualCollector(
destination: MutableMap<IrSymbol, IrSymbol>,
classActualizationInfo: ClassActualizationInfo,
) {
val linkCollector = ExpectActualLinkCollector(destination, classActualizationInfo, typeSystemContext, diagnosticsReporter)
dependentFragments.forEach { linkCollector.visitModuleFragment(it) }
val linkCollector = ExpectActualLinkCollector()
val linkCollectorContext =
ExpectActualLinkCollector.MatchingContext(
typeSystemContext, destination, diagnosticsReporter, expectActualTracker, classActualizationInfo, null
)
dependentFragments.forEach { linkCollector.visitModuleFragment(it, linkCollectorContext) }
// It doesn't make sense to link expects from the last module because actuals always should be located in another module
// Thus relevant actuals are always missing for the last module
// But the collector should be run anyway to detect and report "hanging" expect declarations
linkCollector.visitModuleFragment(mainFragment)
linkCollector.visitModuleFragment(mainFragment, linkCollectorContext)
}
}
@@ -75,7 +82,8 @@ internal data class ClassActualizationInfo(
val actualClasses: Map<ClassId, IrClassSymbol>,
// mapping from classId to actual typealias
val actualTypeAliases: Map<ClassId, IrTypeAliasSymbol>,
val actualTopLevels: Map<CallableId, List<IrDeclarationWithName>>,
val actualTopLevels: Map<CallableId, List<IrSymbol>>,
val actualSymbolsToFile: Map<IrSymbol, IrFile?>,
) {
fun getActualWithoutExpansion(classId: ClassId): IrSymbol? {
return actualTypeAliases[classId] ?: actualClasses[classId]
@@ -94,16 +102,19 @@ private class ActualDeclarationsCollector {
return ClassActualizationInfo(
collector.actualClasses,
collector.actualTypeAliasesWithoutExpansion,
collector.actualTopLevels
collector.actualTopLevels,
collector.actualSymbolsToFile
)
}
}
private val actualClasses: MutableMap<ClassId, IrClassSymbol> = mutableMapOf()
private val actualTypeAliasesWithoutExpansion: MutableMap<ClassId, IrTypeAliasSymbol> = mutableMapOf()
private val actualTopLevels: MutableMap<CallableId, MutableList<IrDeclarationWithName>> = mutableMapOf()
private val actualTopLevels: MutableMap<CallableId, MutableList<IrSymbol>> = mutableMapOf()
private val actualSymbolsToFile: MutableMap<IrSymbol, IrFile?> = mutableMapOf()
private val visitedActualClasses = mutableSetOf<IrClass>()
private var currentFile: IrFile? = null
private fun collect(element: IrElement) {
when (element) {
@@ -112,6 +123,12 @@ private class ActualDeclarationsCollector {
collect(file)
}
}
is IrFile -> {
currentFile = element
for (declaration in element.declarations) {
collect(declaration)
}
}
is IrTypeAlias -> {
if (!element.isActual) return
@@ -120,6 +137,8 @@ private class ActualDeclarationsCollector {
actualClasses[classId] = expandedTypeSymbol
actualTypeAliasesWithoutExpansion[classId] = element.symbol
actualSymbolsToFile[expandedTypeSymbol] = currentFile
actualSymbolsToFile[element.symbol] = currentFile
collect(expandedTypeSymbol.owner)
}
@@ -127,6 +146,7 @@ private class ActualDeclarationsCollector {
if (element.isExpect || !visitedActualClasses.add(element)) return
actualClasses[element.classIdOrFail] = element.symbol
actualSymbolsToFile[element.symbol] = currentFile
for (declaration in element.declarations) {
collect(declaration)
}
@@ -154,65 +174,86 @@ private class ActualDeclarationsCollector {
if (callableId.classId == null) {
actualTopLevels
.getOrPut(callableId) { mutableListOf() }
.add(callableDeclaration)
.add(callableDeclaration.symbol)
actualSymbolsToFile[callableDeclaration.symbol] = currentFile
}
}
}
private class ExpectActualLinkCollector(
private val destination: MutableMap<IrSymbol, IrSymbol>,
private val classActualizationInfo: ClassActualizationInfo,
typeSystemContext: IrTypeSystemContext,
private val diagnosticsReporter: KtDiagnosticReporterWithImplicitIrBasedContext,
) : IrElementVisitorVoid {
private val context = MatchingContext(typeSystemContext)
private class ExpectActualLinkCollector : IrElementVisitor<Unit, ExpectActualLinkCollector.MatchingContext> {
override fun visitFunction(declaration: IrFunction) {
override fun visitFile(declaration: IrFile, data: MatchingContext) {
super.visitFile(declaration, data.withNewCurrentFile(declaration))
}
override fun visitFunction(declaration: IrFunction, data: MatchingContext) {
if (declaration.isExpect) {
matchExpectCallable(declaration, declaration.callableId)
matchExpectCallable(declaration, declaration.callableId, data)
}
}
override fun visitProperty(declaration: IrProperty) {
override fun visitProperty(declaration: IrProperty, data: MatchingContext) {
if (declaration.isExpect) {
matchExpectCallable(declaration, declaration.callableId)
matchExpectCallable(declaration, declaration.callableId, data)
}
}
private fun matchExpectCallable(declaration: IrDeclarationWithName, callableId: CallableId) {
val actualSymbols = classActualizationInfo.actualTopLevels[callableId]?.map { it.symbol }.orEmpty()
matchExpectDeclaration(declaration.symbol, actualSymbols)
}
override fun visitClass(declaration: IrClass) {
if (!declaration.isExpect) return
val classId = declaration.classIdOrFail
val expectClassSymbol = declaration.symbol
val actualClassLikeSymbol = classActualizationInfo.getActualWithoutExpansion(classId)
matchExpectDeclaration(expectClassSymbol, listOfNotNull(actualClassLikeSymbol))
}
private fun matchExpectDeclaration(expectSymbol: IrSymbol, actualSymbols: List<IrSymbol>) {
AbstractExpectActualCompatibilityChecker.matchSingleExpectTopLevelDeclarationAgainstPotentialActuals(
expectSymbol,
actualSymbols,
private fun matchExpectCallable(declaration: IrDeclarationWithName, callableId: CallableId, context: MatchingContext) {
matchExpectDeclaration(
declaration.symbol,
context.classActualizationInfo.actualTopLevels[callableId].orEmpty(),
context
)
}
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
override fun visitClass(declaration: IrClass, data: MatchingContext) {
if (!declaration.isExpect) return
val classId = declaration.classIdOrFail
val expectClassSymbol = declaration.symbol
val actualClassLikeSymbol = data.classActualizationInfo.getActualWithoutExpansion(classId)
matchExpectDeclaration(expectClassSymbol, listOfNotNull(actualClassLikeSymbol), data)
}
private inner class MatchingContext(
private fun matchExpectDeclaration(
expectSymbol: IrSymbol,
actualSymbols: List<IrSymbol>,
context: MatchingContext
) {
AbstractExpectActualCompatibilityChecker.matchSingleExpectTopLevelDeclarationAgainstPotentialActuals(
expectSymbol,
actualSymbols,
context,
)
}
override fun visitElement(element: IrElement, data: MatchingContext) {
element.acceptChildren(this, data)
}
class MatchingContext(
typeSystemContext: IrTypeSystemContext,
private val destination: MutableMap<IrSymbol, IrSymbol>,
private val diagnosticsReporter: KtDiagnosticReporterWithImplicitIrBasedContext,
private val expectActualTracker: ExpectActualTracker?,
internal val classActualizationInfo: ClassActualizationInfo,
private val currentExpectFile: IrFile?,
) : IrExpectActualMatchingContext(typeSystemContext, classActualizationInfo.actualClasses) {
private val currentExpectIoFile by lazy(LazyThreadSafetyMode.PUBLICATION) { currentExpectFile?.toIoFile() }
fun withNewCurrentFile(newCurrentFile: IrFile) =
MatchingContext(
typeContext, destination, diagnosticsReporter, expectActualTracker, classActualizationInfo, newCurrentFile
)
override fun onMatchedClasses(expectClassSymbol: IrClassSymbol, actualClassSymbol: IrClassSymbol) {
destination[expectClassSymbol] = actualClassSymbol
expectActualTracker?.reportWithCurrentFile(actualClassSymbol)
recordActualForExpectDeclaration(expectClassSymbol, actualClassSymbol, destination)
}
override fun onMatchedCallables(expectSymbol: IrSymbol, actualSymbol: IrSymbol) {
expectActualTracker?.reportWithCurrentFile(actualSymbol)
recordActualForExpectDeclaration(expectSymbol, actualSymbol, destination)
}
@@ -233,5 +274,19 @@ private class ExpectActualLinkCollector(
}
}
}
private fun ExpectActualTracker.reportWithCurrentFile(actualSymbol: IrSymbol) {
if (currentExpectFile != null) {
val actualIoFile = classActualizationInfo.actualSymbolsToFile[actualSymbol]?.toIoFile()
if (actualIoFile != null) {
report(currentExpectIoFile!!, actualIoFile)
}
}
}
}
}
private fun IrFile.toIoFile(): File? = when (val fe = fileEntry) {
is PsiIrFileEntry -> fe.psiFile.virtualFile.let { it.fileSystem.getNioPath(it)?.toFile() }
else -> File(fileEntry.name).takeIf { it.exists() }
}
@@ -9,6 +9,7 @@ import org.jetbrains.kotlin.KtDiagnosticReporterWithImplicitIrBasedContext
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.config.LanguageVersionSettings
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.incremental.components.ExpectActualTracker
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.symbols.IrSymbol
import org.jetbrains.kotlin.ir.types.IrTypeSystemContext
@@ -27,7 +28,8 @@ object IrActualizer {
mangler: KotlinMangler.IrMangler,
// TODO: drop this argument in favor of using [IrModuleDescriptor::shouldSeeInternalsOf] in FakeOverrideBuilder KT-61384
friendModules: Map<String, List<String>>,
useIrFakeOverrideBuilder: Boolean
useIrFakeOverrideBuilder: Boolean,
expectActualTracker: ExpectActualTracker?
): IrActualizedResult {
val ktDiagnosticReporter = KtDiagnosticReporterWithImplicitIrBasedContext(diagnosticReporter, languageVersionSettings)
@@ -37,7 +39,8 @@ object IrActualizer {
mainFragment,
dependentFragments,
typeSystemContext,
ktDiagnosticReporter
ktDiagnosticReporter,
expectActualTracker
).collect()
if (languageVersionSettings.supportsFeature(LanguageFeature.MultiplatformRestrictions)) {
IrExpectActualAnnotationMatchingChecker(expectActualMap, actualDeclarations, typeSystemContext, ktDiagnosticReporter).check()