diff --git a/libraries/stdlib/js-ir-minimal-for-test/build.gradle.kts b/libraries/stdlib/js-ir-minimal-for-test/build.gradle.kts index e7c12398003..de75e93f9eb 100644 --- a/libraries/stdlib/js-ir-minimal-for-test/build.gradle.kts +++ b/libraries/stdlib/js-ir-minimal-for-test/build.gradle.kts @@ -48,7 +48,7 @@ val commonMainSources by task { "libraries/stdlib/src/kotlin/time/**", "libraries/stdlib/src/kotlin/util/KotlinVersion.kt", "libraries/stdlib/src/kotlin/util/Tuples.kt", - "libraries/stdlib/src/kotlin/enums/EnumEntries.kt" + "libraries/stdlib/src/kotlin/enums/**" ) ) fullCommonMainSources.outputs.files.singleFile @@ -96,7 +96,8 @@ val jsMainSources by task { "libraries/stdlib/js/src/kotlin/dom/**", "libraries/stdlib/js/src/kotlin/browser/**", "libraries/stdlib/js/src/kotlinx/dom/**", - "libraries/stdlib/js/src/kotlinx/browser/**" + "libraries/stdlib/js/src/kotlinx/browser/**", + "libraries/stdlib/js/src/kotlin/enums/**" ) ) fullJsMainSources.outputs.files.singleFile diff --git a/libraries/stdlib/js/src/kotlin/enums/EnumEntriesSerializationProxy.kt b/libraries/stdlib/js/src/kotlin/enums/EnumEntriesSerializationProxy.kt new file mode 100644 index 00000000000..65ab65c3d54 --- /dev/null +++ b/libraries/stdlib/js/src/kotlin/enums/EnumEntriesSerializationProxy.kt @@ -0,0 +1,9 @@ +/* + * 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 kotlin.enums + +// Unused stub +internal actual class EnumEntriesSerializationProxy> actual constructor(entries: Array) diff --git a/libraries/stdlib/jvm/src/kotlin/enums/EnumEntriesSerializationProxy.kt b/libraries/stdlib/jvm/src/kotlin/enums/EnumEntriesSerializationProxy.kt new file mode 100644 index 00000000000..ee9190ea835 --- /dev/null +++ b/libraries/stdlib/jvm/src/kotlin/enums/EnumEntriesSerializationProxy.kt @@ -0,0 +1,20 @@ +/* + * 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 kotlin.enums + +@Suppress("UNCHECKED_CAST", "unused") +internal actual class EnumEntriesSerializationProxy> actual constructor(entries: Array) : Serializable { + private val c: Class = entries.javaClass.componentType!! as Class + + private companion object { + private const val serialVersionUID: Long = 0L + } + + @OptIn(ExperimentalStdlibApi::class) + private fun readResolve(): Any { + return enumEntries(c.enumConstants) + } +} diff --git a/libraries/stdlib/jvm/test/enums/EnumEntriesJvmTest.kt b/libraries/stdlib/jvm/test/enums/EnumEntriesJvmTest.kt new file mode 100644 index 00000000000..b5fe0a9a812 --- /dev/null +++ b/libraries/stdlib/jvm/test/enums/EnumEntriesJvmTest.kt @@ -0,0 +1,80 @@ +/* + * 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. + */ + +@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + +package test.enums + +import org.junit.Test +import test.collections.behaviors.listBehavior +import test.collections.compare +import test.io.deserializeFromByteArray +import test.io.serializeAndDeserialize +import kotlin.enums.EnumEntries +import kotlin.enums.enumEntries +import kotlin.test.assertEquals + +@Suppress("UNUSED_EXPRESSION") +class EnumEntriesJvmTest { + enum class EmptyEnum + + enum class NonEmptyEnum { + A, B, C + } + + @Test + fun testEmptyEnumSerialization() { + val entries = serializeAndDeserialize(enumEntries(EmptyEnum::values)) + compare(EmptyEnum.values().toList(), entries) { listBehavior() } + } + + @Test + fun testNonEmptyEnumSerialization() { + val entries = serializeAndDeserialize(enumEntries(NonEmptyEnum::values)) + compare(NonEmptyEnum.values().toList(), entries) { listBehavior() } + } + + @Test + fun testLambdaIsNotSerialized() { + val nonSerializable = object {} // Deliberately non-serializable + val entries = enumEntries { + nonSerializable // Capture it + EmptyEnum.values() + } + + val newEntries = serializeAndDeserialize(entries) + assertEquals(entries, newEntries) + } + + // Declarations for enum evolution test + + /* + * This is the serialized contents of + * ``` + * enum class Evolved {} + * ``` + * without ANY entries. + */ + private val bytes = + ("-84,-19,0,5,115,114,0,42,107,111,116,108,105,110,46,101,110,117,109,115,46,69,110,117,109,69,110,116,114," + + "105,101,115,83,101,114,105,97,108,105,122,97,116,105,111,110,80,114,111,120,121,0,0,0,0,0,0,0,0,2," + + "0,1,76,0,1,99,116,0,17,76,106,97,118,97,47,108,97,110,103,47,67,108,97,115,115,59,120,112,118,114,0," + + "37,116,101,115,116,46,101,110,117,109,115,46,69,110,117,109,69,110,116,114,105,101,115,74,118,109,84," + + "101,115,116,36,69,118,111,108,118,101,100,0,0,0,0,0,0,0,0,18,0,0,120,114,0,14,106,97,118,97,46,108,97," + + "110,103,46,69,110,117,109,0,0,0,0,0,0,0,0,18,0,0,120,112") + .split(",").map { it.toInt().toByte() }.toByteArray() + + // Emulate enum evolution + enum class Evolved { + E1, E2, E3 + } + + @Test + fun testEnumEvolution() { + // Test checks that if the enum has new members after being serialized, they are all still deserialized properly + val list = deserializeFromByteArray>(bytes) + assertEquals(enumEntries(Evolved::values), list) + } +} diff --git a/libraries/stdlib/native-wasm/src/kotlin/enums/EnumEntriesSerializationProxy.kt b/libraries/stdlib/native-wasm/src/kotlin/enums/EnumEntriesSerializationProxy.kt new file mode 100644 index 00000000000..65ab65c3d54 --- /dev/null +++ b/libraries/stdlib/native-wasm/src/kotlin/enums/EnumEntriesSerializationProxy.kt @@ -0,0 +1,9 @@ +/* + * 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 kotlin.enums + +// Unused stub +internal actual class EnumEntriesSerializationProxy> actual constructor(entries: Array) diff --git a/libraries/stdlib/src/Module.md b/libraries/stdlib/src/Module.md index b80c129b2e6..a2c8548fde6 100644 --- a/libraries/stdlib/src/Module.md +++ b/libraries/stdlib/src/Module.md @@ -51,6 +51,10 @@ Low-level building blocks for libraries that provide coroutine-based APIs. Utility functions for working with the browser DOM. +# Package kotlin.enum + +Utilities for working with Kotlin enum classes. + # Package kotlin.experimental Experimental APIs, subject to change in future versions of Kotlin. diff --git a/libraries/stdlib/src/kotlin/enums/EnumEntries.kt b/libraries/stdlib/src/kotlin/enums/EnumEntries.kt index 5d4adecbab6..d13534a4f52 100644 --- a/libraries/stdlib/src/kotlin/enums/EnumEntries.kt +++ b/libraries/stdlib/src/kotlin/enums/EnumEntries.kt @@ -21,9 +21,14 @@ public sealed interface EnumEntries> : List @PublishedApi @ExperimentalStdlibApi -@SinceKotlin("1.8") +@SinceKotlin("1.8") // Used by JVM compiler internal fun > enumEntries(entriesProvider: () -> Array): EnumEntries = EnumEntriesList(entriesProvider) +@PublishedApi +@ExperimentalStdlibApi +@SinceKotlin("1.8") // Used by Native/JS compilers and Java serialization +internal fun > enumEntries(entries: Array): EnumEntries = EnumEntriesList { entries } + /* * For enum class E, this class is instantiated in the following manner (NB it's pseudocode that does not * reflect code generation strategy precisely): @@ -55,24 +60,7 @@ internal fun > enumEntries(entriesProvider: () -> Array): EnumEnt */ @SinceKotlin("1.8") @ExperimentalStdlibApi -private class EnumEntriesList>(private val entriesProvider: () -> Array) : EnumEntries, AbstractList() { - - /* - * Open questions to implementation: - * - * - Are we allowed to use e.ordinal as an index? - * - e.g. indexOf(e) = e.ordinal - * - * - Are we allowed to short-circuit methods? - * - e.g. `EEL.contains(anyE)` is always true as long as no reflection is involved - * - * - Should it be Java-serializable? (then we definitely can suffer from short-circuiting and should be extra-careful around read-resolve) - * - * - Should it be sealed or just a class with internal constructor? TODO discuss on design to align this policy over all the language - * - Probably should to avoid exposing AbstractList superclass directly? - * - * - TODO package-info for kotlinlang - */ +private class EnumEntriesList>(private val entriesProvider: () -> Array) : EnumEntries, AbstractList(), Serializable { @Volatile // Volatile is required for safe publication of the array. It doesn't incur any real-world penalties private var _entries: Array? = null @@ -93,4 +81,33 @@ private class EnumEntriesList>(private val entriesProvider: () -> Ar checkElementIndex(index, entries.size) return entries[index] } + + // By definition, EnumEntries contains **all** enums in declaration order, + // thus we are able to short-circuit the implementation here + + override fun contains(element: E): Boolean { + @Suppress("SENSELESS_COMPARISON") + if (element === null) return false // WA for JS IR bug + // Check identity due to UnsafeVariance + val target = entries.getOrNull(element.ordinal) + return target === element + } + + override fun indexOf(element: E): Int { + @Suppress("SENSELESS_COMPARISON") + if (element === null) return -1 // WA for JS IR bug + // Check identity due to UnsafeVariance + val ordinal = element.ordinal + val target = entries.getOrNull(ordinal) + return if (target === element) ordinal else -1 + } + + override fun lastIndexOf(element: E): Int = indexOf(element) + + @Suppress("unused") // Used for Java serialization + private fun writeReplace(): Any { + return EnumEntriesSerializationProxy(entries) + } } + +internal expect class EnumEntriesSerializationProxy>(entries: Array) diff --git a/libraries/stdlib/test/collections/CollectionBehaviors.kt b/libraries/stdlib/test/collections/CollectionBehaviors.kt index 087a011e5a1..1ee27b1ce75 100644 --- a/libraries/stdlib/test/collections/CollectionBehaviors.kt +++ b/libraries/stdlib/test/collections/CollectionBehaviors.kt @@ -24,6 +24,18 @@ public fun CompareContext>.listBehavior() { propertyEquals { indexOf(elementAtOrNull(0)) } propertyEquals { lastIndexOf(elementAtOrNull(0)) } + for (element in expected) { + propertyEquals { this.indexOf(element) } + propertyEquals { this.lastIndexOf(element) } + } + + val nonExisting = object {} + propertyEquals { this.indexOf(nonExisting as Any?) } + propertyEquals { this.lastIndexOf(nonExisting as Any?) } + + propertyEquals { this.indexOf(null as Any?) } + propertyEquals { this.lastIndexOf(null as Any?) } + propertyFails { subList(0, size + 1) } propertyFails { subList(-1, 0) } propertyEquals { subList(0, size) } diff --git a/libraries/stdlib/test/enums/EnumEntriesListTest.kt b/libraries/stdlib/test/enums/EnumEntriesListTest.kt index 8742591d8be..13b143b28aa 100644 --- a/libraries/stdlib/test/enums/EnumEntriesListTest.kt +++ b/libraries/stdlib/test/enums/EnumEntriesListTest.kt @@ -18,6 +18,12 @@ class EnumEntriesListTest { A, B, C } + @Test + fun testCannotBeCasted() { + val list = enumEntries(EmptyEnum::values) + assertTrue { list !is MutableList<*> } + } + @Test fun testForEmptyEnum() { val list = enumEntries(EmptyEnum::values) @@ -54,4 +60,33 @@ class EnumEntriesListTest { val list = enumEntries(NonEmptyEnum::values) compare(NonEmptyEnum.values().toList(), list) { listBehavior() } } + + enum class E1 { + A + } + + enum class E2 { + A, B + } + + @Test + fun testVariantEnumBehaviour() { + val list = enumEntries(E1::values) + + // Index of + val enumList: List> = list + assertEquals(0, enumList.indexOf(E1.A)) + assertEquals(-1, enumList.indexOf(E2.A)) + assertEquals(-1, enumList.indexOf(E2.B)) + + // Last index of + assertEquals(0, enumList.lastIndexOf(E1.A)) + assertEquals(-1, enumList.lastIndexOf(E2.A)) + assertEquals(-1, enumList.lastIndexOf(E2.B)) + + // Contains + assertTrue(enumList.contains(E1.A)) + assertFalse(enumList.contains(E2.A)) + assertFalse(enumList.contains(E2.B)) + } } diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt index b92e92a73eb..381e0a1b6a0 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt @@ -3143,6 +3143,7 @@ public abstract interface class kotlin/enums/EnumEntries : java/util/List, kotli public final class kotlin/enums/EnumEntriesKt { public static final fun enumEntries (Lkotlin/jvm/functions/Function0;)Lkotlin/enums/EnumEntries; + public static final fun enumEntries ([Ljava/lang/Enum;)Lkotlin/enums/EnumEntries; } public abstract interface annotation class kotlin/experimental/ExperimentalObjCName : java/lang/annotation/Annotation {