From a12a31ce686ab0a0fba0f3c64d923cf727e99eda Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Thu, 21 Jul 2022 17:16:53 +0200 Subject: [PATCH] [JVM] Lower .entries calls on Java enums #KT-53236 --- .../FirBlackBoxCodegenTestGenerated.java | 6 + .../jetbrains/kotlin/backend/jvm/JvmLower.kt | 1 + .../backend/jvm/lower/EnumClassLowering.kt | 79 ++++++------ .../jvm/lower/EnumExternalEntriesLowering.kt | 115 ++++++++++++++++++ .../jvm/lower/MappedEnumWhenLowering.kt | 18 +-- .../jvm/JvmLoweredDeclarationOrigin.kt | 1 + .../codegen/box/enum/enumEntriesFromJava.kt | 17 +++ .../IrBlackBoxCodegenTestGenerated.java | 6 + 8 files changed, 196 insertions(+), 47 deletions(-) create mode 100644 compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/EnumExternalEntriesLowering.kt create mode 100644 compiler/testData/codegen/box/enum/enumEntriesFromJava.kt diff --git a/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java b/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java index 6132b435f90..d26eeeeba10 100644 --- a/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java +++ b/compiler/fir/fir2ir/tests-gen/org/jetbrains/kotlin/test/runners/codegen/FirBlackBoxCodegenTestGenerated.java @@ -16287,6 +16287,12 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT runTest("compiler/testData/codegen/box/enum/enumEntries.kt"); } + @Test + @TestMetadata("enumEntriesFromJava.kt") + public void testEnumEntriesFromJava() throws Exception { + runTest("compiler/testData/codegen/box/enum/enumEntriesFromJava.kt"); + } + @Test @TestMetadata("enumEntryMembers.kt") public void testEnumEntryMembers() throws Exception { diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt index caede5d1bca..d7ffe0f2f25 100644 --- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt +++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt @@ -353,6 +353,7 @@ private val jvmFilePhases = listOf( // makePatchParentsPhase(), enumClassPhase, + enumExternalEntriesPhase, objectClassPhase, readResolveForDataObjectsPhase, staticInitializersPhase, diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/EnumClassLowering.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/EnumClassLowering.kt index bf99ca6ea86..1b751d0bae3 100644 --- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/EnumClassLowering.kt +++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/EnumClassLowering.kt @@ -13,10 +13,7 @@ import org.jetbrains.kotlin.backend.common.lower.createIrBuilder import org.jetbrains.kotlin.backend.common.phaser.makeIrFilePhase import org.jetbrains.kotlin.backend.jvm.JvmBackendContext import org.jetbrains.kotlin.backend.jvm.JvmLoweredDeclarationOrigin -import org.jetbrains.kotlin.backend.jvm.ir.createJvmIrBuilder -import org.jetbrains.kotlin.backend.jvm.ir.getSingleAbstractMethod -import org.jetbrains.kotlin.backend.jvm.ir.irArray -import org.jetbrains.kotlin.backend.jvm.ir.javaClassReference +import org.jetbrains.kotlin.backend.jvm.ir.* import org.jetbrains.kotlin.codegen.ImplementationBodyCodegen import org.jetbrains.kotlin.config.LanguageFeature import org.jetbrains.kotlin.descriptors.DescriptorVisibilities @@ -29,9 +26,7 @@ import org.jetbrains.kotlin.ir.expressions.* import org.jetbrains.kotlin.ir.expressions.impl.* import org.jetbrains.kotlin.ir.symbols.IrConstructorSymbol import org.jetbrains.kotlin.ir.symbols.IrValueParameterSymbol -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.types.* import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid @@ -187,7 +182,7 @@ private class EnumClassLowering(private val context: JvmBackendContext) : ClassL } } - private fun buildEntriesField(@Suppress("UNUSED_PARAMETER") entriesHelper: IrFunction): IrField = irClass.addField { + private fun buildEntriesField(entriesHelper: IrFunction): IrField = irClass.addField { name = Name.identifier(ENTRIES_FIELD_NAME) type = context.ir.symbols.enumEntries.defaultType visibility = DescriptorVisibilities.PRIVATE @@ -196,37 +191,7 @@ private class EnumClassLowering(private val context: JvmBackendContext) : ClassL isStatic = true }.apply { initializer = context.createJvmIrBuilder(symbol).run { - irExprBody(irBlock { - val symbols = this@EnumClassLowering.context.ir.symbols - val samClass = context.irBuiltIns.functionN(0) - val type = samClass.typeWith(enumArrayType) - val sam = type.getClass()!!.getSingleAbstractMethod()!!.symbol - /* - * Indy to LMF call: - * INVOKEDYNAMIC get()Lkotlin/jvm/functions/Function0; [ - * // handle kind 0x6 : INVOKESTATIC - * java/lang/invoke/LambdaMetafactory.metafactory - * // arguments: - * ()Ljava/lang/Object;, - * // handle kind 0x6 : INVOKESTATIC - * $entries() [LEnum[, - * ()Ljava/util/List; - * ] - */ - val indyCall = irCall(symbols.indyLambdaMetafactoryIntrinsic, type).apply { - putTypeArgument(0, type) - putValueArgument(0, irRawFunctionReferefence(context.irBuiltIns.anyType, sam)) - putValueArgument(1, IrFunctionReferenceImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, type, entriesHelper.symbol, 0, 0)) - putValueArgument(2, irRawFunctionReferefence(context.irBuiltIns.anyType, sam)) - putValueArgument(3, irVararg(context.irBuiltIns.anyType, emptyList())) - putValueArgument(4, irBoolean(false)) - } - // Bind it to temp var and pass to ctor - val supplier = createTmpVariable(indyCall, "supplier") - +irCall(symbols.createEnumEntries).apply { - putValueArgument(0, irGet(supplier)) - } - }) + irCreateEnumEntriesIndy(entriesHelper, enumArrayType, this@EnumClassLowering.context) } } @@ -356,3 +321,39 @@ private class EnumClassLowering(private val context: JvmBackendContext) : ClassL } } } + +internal fun IrBuilderWithScope.irCreateEnumEntriesIndy( + functionThatReturnsEnumArray: IrFunction, // values() or $entries() + enumArrayType: IrType, + jvmContext: JvmBackendContext +) = irExprBody(irBlock { + val symbols = jvmContext.ir.symbols + val samClass = context.irBuiltIns.functionN(0) + val type = samClass.typeWith(enumArrayType) + val sam = type.getClass()!!.getSingleAbstractMethod()!!.symbol + /* + * Indy to LMF call: + * INVOKEDYNAMIC get()Lkotlin/jvm/functions/Function0; [ + * // handle kind 0x6 : INVOKESTATIC + * java/lang/invoke/LambdaMetafactory.metafactory + * // arguments: + * ()Ljava/lang/Object;, + * // handle kind 0x6 : INVOKESTATIC + * $entries() [LEnum[, // or values() + * ()Ljava/util/List; + * ] + */ + val indyCall = irCall(symbols.indyLambdaMetafactoryIntrinsic, type).apply { + putTypeArgument(0, type) + putValueArgument(0, irRawFunctionReferefence(context.irBuiltIns.anyType, sam)) + putValueArgument(1, IrFunctionReferenceImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, type, functionThatReturnsEnumArray.symbol, 0, 0)) + putValueArgument(2, irRawFunctionReferefence(context.irBuiltIns.anyType, sam)) + putValueArgument(3, irVararg(context.irBuiltIns.anyType, emptyList())) + putValueArgument(4, irBoolean(false)) + } + // Bind it to temp var and pass to ctor + val supplier = createTmpVariable(indyCall, "supplier") + +irCall(symbols.createEnumEntries).apply { + putValueArgument(0, irGet(supplier)) + } +}) diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/EnumExternalEntriesLowering.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/EnumExternalEntriesLowering.kt new file mode 100644 index 00000000000..7a41228a856 --- /dev/null +++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/EnumExternalEntriesLowering.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.backend.jvm.lower + +import org.jetbrains.kotlin.backend.common.FileLoweringPass +import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext +import org.jetbrains.kotlin.backend.common.lower.createIrBuilder +import org.jetbrains.kotlin.backend.common.phaser.makeIrFilePhase +import org.jetbrains.kotlin.backend.jvm.JvmBackendContext +import org.jetbrains.kotlin.backend.jvm.JvmLoweredDeclarationOrigin +import org.jetbrains.kotlin.config.LanguageFeature +import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.builders.declarations.addField +import org.jetbrains.kotlin.ir.builders.declarations.buildClass +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.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.visitors.transformChildrenVoid +import org.jetbrains.kotlin.name.Name + +internal val enumExternalEntriesPhase = makeIrFilePhase( + ::EnumExternalEntriesLowering, + name = "EnumExternalEntries", + description = "Replaces '.entries' on Java and pre-compiled Kotlin enums with access to entries in generated \$EntriesMapping " +) + +/** + * 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. + */ +class EnumExternalEntriesLowering(private val context: JvmBackendContext) : FileLoweringPass, IrElementTransformerVoidWithContext() { + + override fun lower(irFile: IrFile) { + if (!context.state.languageVersionSettings.supportsFeature(LanguageFeature.EnumEntries)) { + return + } + irFile.transformChildrenVoid(this) + } + + private var state: EntriesMappingState? = null + + private inner class EntriesMappingState { + val mappings = mutableMapOf() + val mappingsClass by lazy { + context.irFactory.buildClass { + name = Name.identifier("EntriesMappings") + origin = JvmLoweredDeclarationOrigin.ENUM_MAPPINGS_FOR_ENTRIES + }.apply { + createImplicitParameterDeclarationWithWrappedDescriptor() + } + } + + fun getEntriesFieldForEnum(enumClass: IrClass): IrField { + return mappings.getOrPut(enumClass) { + mappingsClass.addField { + name = Name.identifier("entries\$${mappings.size}") + type = context.ir.symbols.enumEntries.typeWith(enumClass.defaultType) + origin = JvmLoweredDeclarationOrigin.ENUM_MAPPINGS_FOR_ENTRIES + isFinal = true + isStatic = true + } + } + } + } + + override fun visitFunctionAccess(expression: IrFunctionAccessExpression): IrExpression { + val owner = expression.symbol.owner as? IrSimpleFunction + val parentClass = owner?.parent as? IrClass ?: return expression + val shouldBeLowered = parentClass.isEnumClass && + owner.name == Name.special("") && + (parentClass.isFromJava()) // TODO check metadata for prev versions + if (!shouldBeLowered) return expression + + val enumClass = parentClass.symbol.defaultType.getClass()!! + val field = state!!.getEntriesFieldForEnum(enumClass) + return IrGetFieldImpl(expression.startOffset, expression.endOffset, field.symbol, field.type) + } + + override fun visitClassNew(declaration: IrClass): IrStatement { + val oldState = state + val mappingState = EntriesMappingState() + state = mappingState + super.visitClassNew(declaration) + + for ((enum, field) in mappingState.mappings) { + val enumValues = enum.findEnumValuesFunction(context) + val enumArrayType = field.type + val builder = context.createIrBuilder(field.symbol) + field.initializer = builder.irCreateEnumEntriesIndy(enumValues, enumArrayType, context) + } + + if (mappingState.mappings.isNotEmpty()) { + declaration.declarations += mappingState.mappingsClass.apply { + parent = declaration + } + } + state = oldState + return declaration + } +} diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/MappedEnumWhenLowering.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/MappedEnumWhenLowering.kt index a8ce54d57dc..9c326e48ad4 100644 --- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/MappedEnumWhenLowering.kt +++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/MappedEnumWhenLowering.kt @@ -127,14 +127,7 @@ private class MappedEnumWhenLowering(override val context: JvmBackendContext) : super.visitClassNew(declaration) for ((enum, mapping) in mappingState.mappings) { - val enumValues = enum.functions.single { - it.name.toString() == "values" - && it.dispatchReceiverParameter == null - && it.extensionReceiverParameter == null - && it.valueParameters.isEmpty() - && it.returnType.isBoxedArray - && it.returnType.getArrayElementType(context.irBuiltIns).classOrNull == enum.symbol - } + val enumValues = enum.findEnumValuesFunction(context) val builder = context.createIrBuilder(mapping.field.symbol) mapping.field.initializer = builder.irExprBody(builder.irBlock { val enumSize = irCall(refArraySize).apply { dispatchReceiver = irCall(enumValues) } @@ -175,3 +168,12 @@ private class MappedEnumWhenLowering(override val context: JvmBackendContext) : return declaration } } + +internal fun IrClass.findEnumValuesFunction(context: JvmBackendContext) = functions.single { + it.name.toString() == "values" + && it.dispatchReceiverParameter == null + && it.extensionReceiverParameter == null + && it.valueParameters.isEmpty() + && it.returnType.isBoxedArray + && it.returnType.getArrayElementType(context.irBuiltIns).classOrNull == this.symbol +} diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLoweredDeclarationOrigin.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLoweredDeclarationOrigin.kt index 4a277f2b475..fa74ad9706a 100644 --- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLoweredDeclarationOrigin.kt +++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLoweredDeclarationOrigin.kt @@ -27,6 +27,7 @@ interface JvmLoweredDeclarationOrigin : IrDeclarationOrigin { object GENERATED_PROPERTY_REFERENCE : IrDeclarationOriginImpl("GENERATED_PROPERTY_REFERENCE", isSynthetic = true) object GENERATED_MEMBER_IN_CALLABLE_REFERENCE : IrDeclarationOriginImpl("GENERATED_MEMBER_IN_CALLABLE_REFERENCE", isSynthetic = false) object ENUM_MAPPINGS_FOR_WHEN : IrDeclarationOriginImpl("ENUM_MAPPINGS_FOR_WHEN", isSynthetic = true) + object ENUM_MAPPINGS_FOR_ENTRIES : IrDeclarationOriginImpl("ENUM_MAPPINGS_FOR_ENTRIES", isSynthetic = true) object SYNTHETIC_INLINE_CLASS_MEMBER : IrDeclarationOriginImpl("SYNTHETIC_INLINE_CLASS_MEMBER", isSynthetic = true) object INLINE_CLASS_GENERATED_IMPL_METHOD : IrDeclarationOriginImpl("INLINE_CLASS_GENERATED_IMPL_METHOD") object STATIC_INLINE_CLASS_REPLACEMENT : IrDeclarationOriginImpl("STATIC_INLINE_CLASS_REPLACEMENT") diff --git a/compiler/testData/codegen/box/enum/enumEntriesFromJava.kt b/compiler/testData/codegen/box/enum/enumEntriesFromJava.kt new file mode 100644 index 00000000000..43029c467c9 --- /dev/null +++ b/compiler/testData/codegen/box/enum/enumEntriesFromJava.kt @@ -0,0 +1,17 @@ +// !LANGUAGE: +EnumEntries +// TARGET_BACKEND: JVM_IR +// FULL_JDK +// WITH_STDLIB + +// FILE: MyEnum.java +enum MyEnum { + OK, NOPE +} + +// FILE: test.kt +@OptIn(ExperimentalStdlibApi::class) +fun box(): String { + val entries = MyEnum.entries + val entry = entries[0] + return entry.toString() +} diff --git a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java index db4f2155611..b9c7bb58c1e 100644 --- a/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java +++ b/compiler/tests-common-new/tests-gen/org/jetbrains/kotlin/test/runners/codegen/IrBlackBoxCodegenTestGenerated.java @@ -16287,6 +16287,12 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes runTest("compiler/testData/codegen/box/enum/enumEntries.kt"); } + @Test + @TestMetadata("enumEntriesFromJava.kt") + public void testEnumEntriesFromJava() throws Exception { + runTest("compiler/testData/codegen/box/enum/enumEntriesFromJava.kt"); + } + @Test @TestMetadata("enumEntryMembers.kt") public void testEnumEntryMembers() throws Exception {