[JVM] Lower .entries calls on Kotlin enums

Leveraging the same mechanism with $EntriesMapping as Java enums.
Old (compiled with LV/AV < 1.8) enums are detected by looking for
static special <get-entries> method that cannot be introduced on
Kotlin enums otherwise

#KT-53236
This commit is contained in:
Vsevolod Tolstopyatov
2022-07-22 14:33:28 +02:00
committed by Space
parent a12a31ce68
commit e3bff290bd
20 changed files with 268 additions and 18 deletions
@@ -16293,6 +16293,24 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT
runTest("compiler/testData/codegen/box/enum/enumEntriesFromJava.kt");
}
@Test
@TestMetadata("enumEntriesMultimodule.kt")
public void testEnumEntriesMultimodule() throws Exception {
runTest("compiler/testData/codegen/box/enum/enumEntriesMultimodule.kt");
}
@Test
@TestMetadata("enumEntriesMultimoduleMultipleMappings.kt")
public void testEnumEntriesMultimoduleMultipleMappings() throws Exception {
runTest("compiler/testData/codegen/box/enum/enumEntriesMultimoduleMultipleMappings.kt");
}
@Test
@TestMetadata("enumEntriesMultimoduleMultipleMappingsForMultipleClassfiles.kt")
public void testEnumEntriesMultimoduleMultipleMappingsForMultipleClassfiles() throws Exception {
runTest("compiler/testData/codegen/box/enum/enumEntriesMultimoduleMultipleMappingsForMultipleClassfiles.kt");
}
@Test
@TestMetadata("enumEntryMembers.kt")
public void testEnumEntryMembers() throws Exception {
@@ -2200,6 +2200,18 @@ public class FirBytecodeTextTestGenerated extends AbstractFirBytecodeTextTest {
runTest("compiler/testData/codegen/bytecodeText/enum/enumEntries.kt");
}
@Test
@TestMetadata("enumEntriesMapping.kt")
public void testEnumEntriesMapping() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/enum/enumEntriesMapping.kt");
}
@Test
@TestMetadata("enumEntriesMultiMapping.kt")
public void testEnumEntriesMultiMapping() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/enum/enumEntriesMultiMapping.kt");
}
@Test
@TestMetadata("kt18731.kt")
public void testKt18731() throws Exception {
@@ -19,18 +19,13 @@ import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrField
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrFunctionAccessExpression
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.expressions.impl.IrGetFieldImpl
import org.jetbrains.kotlin.ir.types.defaultType
import org.jetbrains.kotlin.ir.types.getClass
import org.jetbrains.kotlin.ir.types.typeWith
import org.jetbrains.kotlin.ir.util.createImplicitParameterDeclarationWithWrappedDescriptor
import org.jetbrains.kotlin.ir.util.defaultType
import org.jetbrains.kotlin.ir.util.isEnumClass
import org.jetbrains.kotlin.ir.util.isFromJava
import org.jetbrains.kotlin.ir.types.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.SpecialNames
internal val enumExternalEntriesPhase = makeIrFilePhase(
::EnumExternalEntriesLowering,
@@ -42,6 +37,25 @@ internal val enumExternalEntriesPhase = makeIrFilePhase(
* When this lowering encounters call to `Enum.entries` where `Enum` is either Java enum or enum pre-compiled
* with previous version of Kotlin, it generates `FileName$EntriesMapping` where it stores
* package-private `entries` static field that is used as a replacement of missing one.
*
* Basically, it lowers the following code:
* ```
* // F.kt
* JavaOrOldKotlinEnum.entries
* ```
*
* into
* ```
* synthetic class FKt$EntriesMappings {
* static final EnumEntries<JavaOrOldKotlinEnum> entries$1
* static {
* entries$1 = EnumEntries(JavaOrOldKotlinEnum::values)
* }
* }
*
* // F.kt
* FKt$EntriesMappings.entries$1
* ```
*/
class EnumExternalEntriesLowering(private val context: JvmBackendContext) : FileLoweringPass, IrElementTransformerVoidWithContext() {
@@ -78,19 +92,29 @@ class EnumExternalEntriesLowering(private val context: JvmBackendContext) : File
}
}
override fun visitFunctionAccess(expression: IrFunctionAccessExpression): IrExpression {
override fun visitCall(expression: IrCall): IrExpression {
val owner = expression.symbol.owner as? IrSimpleFunction
val parentClass = owner?.parent as? IrClass ?: return expression
val parentClass = owner?.parent as? IrClass ?: return super.visitCall(expression)
/*
* Candidates for lowering:
* * Java enums
* * Kotlin enums that have no 'getEntries' function (thus compiled with pre-1.8 LV/AV)
*/
val shouldBeLowered = parentClass.isEnumClass &&
owner.name == Name.special("<get-entries>") &&
(parentClass.isFromJava()) // TODO check metadata for prev versions
if (!shouldBeLowered) return expression
val enumClass = parentClass.symbol.defaultType.getClass()!!
val field = state!!.getEntriesFieldForEnum(enumClass)
owner.name == SpecialNames.ENUM_GET_ENTRIES &&
(parentClass.isFromJava() || !parentClass.hasEnumEntriesFunction())
if (!shouldBeLowered) return super.visitCall(expression)
val field = state!!.getEntriesFieldForEnum(parentClass)
return IrGetFieldImpl(expression.startOffset, expression.endOffset, field.symbol, field.type)
}
private fun IrClass.hasEnumEntriesFunction() = functions.any {
it.name.toString() == "<get-entries>"
&& it.dispatchReceiverParameter == null
&& it.extensionReceiverParameter == null
&& it.valueParameters.isEmpty()
}
override fun visitClassNew(declaration: IrClass): IrStatement {
val oldState = state
val mappingState = EntriesMappingState()
@@ -0,0 +1,3 @@
$TESTDATA_DIR$/enumEntriesForJavaNotEnabled.kt
-d
$TEMP_DIR$
@@ -0,0 +1,6 @@
@OptIn(ExperimentalStdlibApi::class)
fun box(): String {
val entries = java.util.concurrent.TimeUnit.entries
return "OK"
}
@@ -0,0 +1,4 @@
compiler/testData/cli/jvm/enumEntriesForJavaNotEnabled.kt:4:49: error: unresolved reference: entries
val entries = java.util.concurrent.TimeUnit.entries
^
COMPILATION_ERROR
+3
View File
@@ -0,0 +1,3 @@
$TESTDATA_DIR$/enumEntriesNotEnabled.kt
-d
$TEMP_DIR$
+9
View File
@@ -0,0 +1,9 @@
enum class MyEnum {
OK, NOPE
}
@OptIn(ExperimentalStdlibApi::class)
fun box(): String {
val entries = MyEnum.entries
return "OK"
}
+4
View File
@@ -0,0 +1,4 @@
compiler/testData/cli/jvm/enumEntriesNotEnabled.kt:7:26: error: unresolved reference: entries
val entries = MyEnum.entries
^
COMPILATION_ERROR
@@ -13,5 +13,6 @@ enum MyEnum {
fun box(): String {
val entries = MyEnum.entries
val entry = entries[0]
require(java.util.concurrent.TimeUnit.entries.size == java.util.concurrent.TimeUnit.values().size)
return entry.toString()
}
@@ -0,0 +1,17 @@
// TARGET_BACKEND: JVM_IR
// FULL_JDK
// WITH_STDLIB
// MODULE: lib
// FILE: MyEnum.kt
enum class MyEnum {
Nope, OK
}
// MODULE: caller(lib)
// !LANGUAGE: +EnumEntries
// FILE: Box.kt
@OptIn(ExperimentalStdlibApi::class)
fun box(): String {
return MyEnum.entries[1].toString()
}
@@ -0,0 +1,22 @@
// TARGET_BACKEND: JVM_IR
// FULL_JDK
// WITH_STDLIB
// MODULE: lib
// FILE: MyEnums.kt
enum class MyEnum {
N, O
}
enum class MyEnum2 {
O, K
}
// MODULE: caller(lib)
// !LANGUAGE: +EnumEntries
// FILE: Box.kt
@OptIn(ExperimentalStdlibApi::class)
fun box(): String {
return MyEnum.entries[1].toString() + MyEnum2.entries[1].toString()
}
@@ -0,0 +1,27 @@
// TARGET_BACKEND: JVM_IR
// FULL_JDK
// WITH_STDLIB
// MODULE: lib
// FILE: MyEnums.kt
enum class MyEnum {
N, O
}
enum class MyEnum2 {
O, K
}
// MODULE: caller(lib)
// !LANGUAGE: +EnumEntries
// FILE: Box.kt
@OptIn(ExperimentalStdlibApi::class)
object Container {
fun getK() = MyEnum2.entries[1].toString()
}
@OptIn(ExperimentalStdlibApi::class)
fun box(): String {
return MyEnum.entries[1].toString() + Container.getK()
}
@@ -10,3 +10,4 @@ enum class MyEnum {
// 1 private final static synthetic Lkotlin/enums/EnumEntries; \$ENTRIES
// 1 public final static getEntries\(\)Lkotlin/enums/EnumEntries;
// 0 [^\$]entries
// 0 class [a-zA-Z]+\$EntriesMappings
@@ -0,0 +1,24 @@
// TARGET_BACKEND: JVM_IR
// FULL_JDK
// WITH_STDLIB
// MODULE: lib
// FILE: MyEnums.kt
enum class MyEnum {
N, O
}
enum class MyEnum2 {
O, K
}
// MODULE: caller(lib)
// !LANGUAGE: +EnumEntries
// FILE: Box.kt
@OptIn(ExperimentalStdlibApi::class)
fun box(): String {
return MyEnum.entries[1].toString() + MyEnum2.entries[1].toString()
}
// 1 class [a-zA-Z]+\$EntriesMappings
@@ -0,0 +1,29 @@
// TARGET_BACKEND: JVM_IR
// FULL_JDK
// WITH_STDLIB
// MODULE: lib
// FILE: MyEnums.kt
enum class MyEnum {
N, O
}
enum class MyEnum2 {
O, K
}
// MODULE: caller(lib)
// !LANGUAGE: +EnumEntries
// FILE: Box.kt
@OptIn(ExperimentalStdlibApi::class)
object Container {
fun getK() = MyEnum2.entries[1].toString()
}
@OptIn(ExperimentalStdlibApi::class)
fun box(): String {
return MyEnum.entries[1].toString() + Container.getK()
}
// 2 class [a-zA-Z]+\$EntriesMappings
@@ -16293,6 +16293,24 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes
runTest("compiler/testData/codegen/box/enum/enumEntriesFromJava.kt");
}
@Test
@TestMetadata("enumEntriesMultimodule.kt")
public void testEnumEntriesMultimodule() throws Exception {
runTest("compiler/testData/codegen/box/enum/enumEntriesMultimodule.kt");
}
@Test
@TestMetadata("enumEntriesMultimoduleMultipleMappings.kt")
public void testEnumEntriesMultimoduleMultipleMappings() throws Exception {
runTest("compiler/testData/codegen/box/enum/enumEntriesMultimoduleMultipleMappings.kt");
}
@Test
@TestMetadata("enumEntriesMultimoduleMultipleMappingsForMultipleClassfiles.kt")
public void testEnumEntriesMultimoduleMultipleMappingsForMultipleClassfiles() throws Exception {
runTest("compiler/testData/codegen/box/enum/enumEntriesMultimoduleMultipleMappingsForMultipleClassfiles.kt");
}
@Test
@TestMetadata("enumEntryMembers.kt")
public void testEnumEntryMembers() throws Exception {
@@ -2200,6 +2200,18 @@ public class IrBytecodeTextTestGenerated extends AbstractIrBytecodeTextTest {
runTest("compiler/testData/codegen/bytecodeText/enum/enumEntries.kt");
}
@Test
@TestMetadata("enumEntriesMapping.kt")
public void testEnumEntriesMapping() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/enum/enumEntriesMapping.kt");
}
@Test
@TestMetadata("enumEntriesMultiMapping.kt")
public void testEnumEntriesMultiMapping() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/enum/enumEntriesMultiMapping.kt");
}
@Test
@TestMetadata("kt18731.kt")
public void testKt18731() throws Exception {
@@ -342,6 +342,16 @@ public class CliTestGenerated extends AbstractCliTest {
runTest("compiler/testData/cli/jvm/emptySources.args");
}
@TestMetadata("enumEntriesForJavaNotEnabled.args")
public void testEnumEntriesForJavaNotEnabled() throws Exception {
runTest("compiler/testData/cli/jvm/enumEntriesForJavaNotEnabled.args");
}
@TestMetadata("enumEntriesNotEnabled.args")
public void testEnumEntriesNotEnabled() throws Exception {
runTest("compiler/testData/cli/jvm/enumEntriesNotEnabled.args");
}
@TestMetadata("experimentalDeprecated.args")
public void testExperimentalDeprecated() throws Exception {
runTest("compiler/testData/cli/jvm/experimentalDeprecated.args");
@@ -70,6 +70,12 @@ object SpecialNames {
@JvmField
val RECEIVER = Name.special("<receiver>")
/**
* Kotlin-generated `entries` read-only property
*/
@JvmField
val ENUM_GET_ENTRIES = Name.special("<get-entries>")
/**
* Special name to store expressions used as indexes in subscription operators (`get` and `set`).
*
@@ -95,4 +101,4 @@ object SpecialNames {
fun isSafeIdentifier(name: Name): Boolean {
return name.asString().isNotEmpty() && !name.isSpecial
}
}
}