diff --git a/libraries/stdlib/src/kotlin/random/Random.kt b/libraries/stdlib/src/kotlin/random/Random.kt index 340c48b4ad0..343576d9146 100644 --- a/libraries/stdlib/src/kotlin/random/Random.kt +++ b/libraries/stdlib/src/kotlin/random/Random.kt @@ -260,7 +260,6 @@ public abstract class Random { */ public open fun nextBytes(size: Int): ByteArray = nextBytes(ByteArray(size)) - // TODO: UInt, ULong, UByteArray /** * The default random number generator. @@ -321,6 +320,136 @@ public fun Random(seed: Int): Random = XorWowRandom(seed, seed.shr(31)) public fun Random(seed: Long): Random = XorWowRandom(seed.toInt(), seed.shr(32).toInt()) + +/** + * Gets the next random [UInt] from the random number generator. + * + * Generates a Unsigned Int random value uniformly distributed between [UInt.MIN_VALUE] and [UInt.MAX_VALUE] (inclusive) + */ +@SinceKotlin("1.3") +public fun Random.nextUInt(): UInt = nextInt().toUInt() + +/** + * Gets the next random [UInt] from the random number generator not greater than the specified [bound]. + * + * Generated an Unsigned Int random value uniformly distributed between `0`(inclusive) and the specified [bound] (exclusive). + * + * @throws IllegalArgumentException if [bound] is zero + */ +@SinceKotlin("1.3") +public fun Random.nextUInt(bound: UInt): UInt = nextUInt(0u, bound) + +/** + * Gets the next random [UInt] from the random number generator in the specified range. + * + * Generated an Unsigned Integer random value uniformly distributed between the specified [origin] (inclusive) and the specified [bound] (exclusive). + * + * @throws IllegalArgumentException if [origin] is greater than or equal to [bound] + */ +@SinceKotlin("1.3") +public fun Random.nextUInt(origin: UInt, bound: UInt): UInt { + checkUIntRangeBounds(origin, bound) + + val originTransformedToInt = origin.toInt() xor Int.MIN_VALUE + val boundTransformedToInt = bound.toInt() xor Int.MIN_VALUE + + val randomValueTransformedBack = nextInt(originTransformedToInt, boundTransformedToInt) xor Int.MIN_VALUE + + return randomValueTransformedBack.toUInt() +} + +/** + * Gets the next random [UInt] from the random number generator in the specified [range]. + * + * Generates an Unsigned Integer random value uniformly distributed in the specified [range]: + * from `range.start` inclusive to `range.endInclusive` inclusive. + * + * @throws IllegalArgumentException if [range] is empty. + */ +@SinceKotlin("1.3") +public fun Random.nextUInt(range: UIntRange): UInt = when { + range.isEmpty() -> throw IllegalArgumentException("Cannot get random in empty range: $range") + range.last < UInt.MAX_VALUE -> nextUInt(range.first, range.last + 1.toUInt()) + range.first > UInt.MIN_VALUE -> nextUInt(range.first, range.last + 1.toUInt()) + else -> nextUInt() +} + +/** + * Gets the next random [ULong] from the random number generator. + * + * Generated a Unsigned Long random value uniformly distributed between [ULong.MIN_VALUE] and [ULong.MAX_VALUE] (inclusive) + */ +@SinceKotlin("1.3") +public fun Random.nextULong(): ULong = nextLong().toULong() + +/** + * Gets the next random [ULong] from the random number generator not greater than the specified [bound]. + * + * Generated an Unsigned Long random value uniformly distributed between `0`(inclusive) and the specified [bound] (exclusive). + * + * @throws IllegalArgumentException if [bound] is zero + */ +@SinceKotlin("1.3") +public fun Random.nextULong(bound: ULong): ULong = nextULong(0uL, bound) + +/** + * Gets the next random [ULong] from the random number generator in the specified range + * + * Generated an Unsigned Long random value uniformly distributed between the specified [origin] (inclusive) and the specified [bound] (exclusive). + * + * @throws IllegalArgumentException if [origin] is greater than or equal to [bound] + */ +@SinceKotlin("1.3") +public fun Random.nextULong(origin: ULong, bound: ULong): ULong { + checkULongRangeBounds(origin, bound) + + val originTransformedToLong = origin.toLong() xor Long.MIN_VALUE + val boundTransformedToLong = bound.toLong() xor Long.MIN_VALUE + + val randomValueTransformedBack = nextLong(originTransformedToLong, boundTransformedToLong) xor Long.MIN_VALUE + return randomValueTransformedBack.toULong() +} + +/** + * Gets the next random [ULong] from the random number generator in the specified [range]. + * + * Generates an Unsigned Long random value uniformly distributed in the specified [range]: + * from `range.start` inclusive to `range.endInclusive` inclusive. + * + * @throws IllegalArgumentException if [range] is empty + */ +@SinceKotlin("1.3") +public fun Random.nextULong(range: ULongRange): ULong = when { + range.isEmpty() -> throw IllegalArgumentException("Cannot get random in empty range: $range") + range.last < ULong.MAX_VALUE -> nextULong(range.first, range.last + 1.toULong()) + range.first > ULong.MIN_VALUE -> nextULong(range.first, range.last + 1.toULong()) + else -> nextULong() +} + +/** + * Fills the specified byte [array] with random bytes and returns it. + * + * @return [array] filled with random bytes. + */ +@SinceKotlin("1.3") +public fun Random.nextUBytes(array: UByteArray) = nextUBytes(array, 0, array.size) + +/** + * Creates a byte array of the specified [size], filled with random bytes. + */ +@SinceKotlin("1.3") +public fun Random.nextUBytes(size: Int): UByteArray = UByteArray(nextBytes(size)) + +/** + * Fills a subrange of the specified UByte [array] starting from [fromIndex] inclusive and ending [toIndex] exclusive with random UBytes + * + * @returns [array] with the subrange filled with random bytes. + */ +@SinceKotlin("1.3") +public fun Random.nextUBytes(array: UByteArray, fromIndex: Int = 0, toIndex: Int = array.size): UByteArray { + return UByteArray(nextBytes(array.storage, fromIndex, toIndex)) +} + internal expect fun defaultPlatformRandom(): Random internal expect fun fastLog2(value: Int): Int // 31 - Integer.numberOfLeadingZeros(value) internal expect fun doubleFromParts(hi26: Int, low27: Int): Double @@ -330,7 +459,9 @@ internal fun Int.takeUpperBits(bitCount: Int): Int = this.ushr(32 - bitCount) and (-bitCount).shr(31) internal fun checkRangeBounds(origin: Int, bound: Int) = require(bound > origin) { boundsErrorMessage(origin, bound) } +internal fun checkUIntRangeBounds(origin: UInt, bound: UInt) = require(bound > origin) { boundsErrorMessage(origin, bound) } internal fun checkRangeBounds(origin: Long, bound: Long) = require(bound > origin) { boundsErrorMessage(origin, bound) } +internal fun checkULongRangeBounds(origin: ULong, bound: ULong) = require(bound > origin) { boundsErrorMessage(origin, bound) } internal fun checkRangeBounds(origin: Double, bound: Double) = require(bound > origin) { boundsErrorMessage(origin, bound) } private fun boundsErrorMessage(origin: Any, bound: Any) = "Random range is empty: [$origin, $bound)." diff --git a/libraries/stdlib/test/random/RandomTest.kt b/libraries/stdlib/test/random/RandomTest.kt index b83112865c0..9975e40e770 100644 --- a/libraries/stdlib/test/random/RandomTest.kt +++ b/libraries/stdlib/test/random/RandomTest.kt @@ -49,6 +49,20 @@ abstract class RandomSmokeTest { assertEquals(0, result2, "All zero bits should present") } + @Test + fun nextUInt() { + var result1 = UInt.MIN_VALUE + var result2 = UInt.MAX_VALUE + repeat(1000) { + val r = subject.nextUInt() + result1 = result1 or r + result2 = result2 and r + } + assertEquals(UInt.MAX_VALUE, result1, "All one bits should be present") + assertEquals(UInt.MIN_VALUE, result2, "All zero bits should present") + + } + @Test fun nextIntBound() { assertFailsWith { subject.nextInt(0) } @@ -68,6 +82,23 @@ abstract class RandomSmokeTest { } } + @Test + fun nextUIntBound() { + assertFailsWith { subject.nextUInt(UInt.MIN_VALUE) } + + repeat(1000) { + assertEquals(0u, subject.nextUInt(1u)) + } + + for (bound in listOf(2u, 3u, 7u, 16u, 32u, 0x4000_0000u, UInt.MAX_VALUE)) { + repeat(1000) { + val x = subject.nextUInt(bound) + if (x !in 0u..(bound - 1u)) + fail("Value $x must be in range [0, $bound)") + } + } + } + @Test fun nextIntOriginBound() { assertFailsWith { subject.nextInt(0, 0) } @@ -90,6 +121,28 @@ abstract class RandomSmokeTest { } } + @Test + fun nextUIntOriginBound() { + assertFailsWith { subject.nextUInt(0u, 0u) } + assertFailsWith { subject.nextUInt((-1).toUInt(), (-2).toUInt()) } + assertFailsWith { subject.nextUInt(UInt.MIN_VALUE, UInt.MIN_VALUE) } + + for (n in UInt.MIN_VALUE until UInt.MAX_VALUE step 0x10000) { + assertEquals(n, subject.nextUInt(n, n + 1u)) + } + (UInt.MAX_VALUE - 1u).let { n -> + assertEquals(n, subject.nextUInt(n, n + 1u)) + } + + for ((origin, bound) in listOf((0u to 2u), (1u to 6u), (0u to 32u), (0u to UInt.MAX_VALUE), (1u to (Int.MAX_VALUE.toUInt() + 1u)), (UInt.MIN_VALUE to UInt.MAX_VALUE))) { + repeat(1000) { + val x = subject.nextUInt(origin, bound) + if (x !in origin..bound - 1u) + fail("Value $x must be in range [$origin, $bound)") + } + } + } + @Suppress("EmptyRange") @Test fun nextIntInIntRange() { @@ -111,6 +164,32 @@ abstract class RandomSmokeTest { } } + @Test + fun nextUIntInUIntRange() { + assertFailsWith { subject.nextUInt(1u..0u) } + assertFailsWith { subject.nextUInt((-1).toUInt()..UInt.MIN_VALUE) } + assertFailsWith { subject.nextUInt(UInt.MAX_VALUE..(UInt.MAX_VALUE - 1u)) } + + repeat(1000) { it -> + val n = it.toUInt() + assertEquals(n, subject.nextUInt(n..n)) + } + + for (range in listOf( + (0u..1u), + (1u..5u), + (0u..31u), + (0u..UInt.MAX_VALUE - 1u), + (0u..UInt.MAX_VALUE) + )) { + repeat(1000) { + val x = subject.nextUInt(range) + if (x !in range) + fail("Value $x must be in range $range") + } + } + } + @Test fun nextLong() { var result1 = 0L @@ -124,6 +203,20 @@ abstract class RandomSmokeTest { assertEquals(0, result2, "All zero bits should present") } + @Test + fun nextULong() { + var result1 = ULong.MIN_VALUE + var result2 = ULong.MAX_VALUE + repeat(1000) { + val r = subject.nextULong() + result1 = result1 or r + result2 = result2 and r + } + + assertEquals(ULong.MAX_VALUE, result1, "All one bits should be present") + assertEquals(ULong.MIN_VALUE, result2, "All zero bits should be present") + } + @Test fun nextLongBound() { assertFailsWith { subject.nextLong(0) } @@ -143,6 +236,24 @@ abstract class RandomSmokeTest { } } + @Test + fun nextULongBound() { + assertFailsWith { subject.nextULong(ULong.MIN_VALUE) } + + repeat(1000) { + assertEquals(0uL, subject.nextULong(1uL)) + } + + for (bound in listOf(2uL, 23uL, 32uL, (0x1_0000_0000).toULong(), 0x4000_0000_0000_000.toULong(), ULong.MAX_VALUE)) { + repeat(1000) { + val x = subject.nextULong(bound) + if (x !in 0uL..(bound - 1uL)) { + fail("Value $x must be in range [0, $bound)") + } + } + } + } + @Test fun nextLongOriginBound() { assertFailsWith { subject.nextLong(0, 0) } @@ -164,6 +275,31 @@ abstract class RandomSmokeTest { } } + @Test + fun nextULongOriginBound() { + assertFailsWith { subject.nextULong(0uL, 0uL) } + assertFailsWith { subject.nextULong((-1).toULong(), (-2).toULong()) } + + for (i in 500uL..1500uL) { + val n = (0x1_0000_0000).toULong() + i + assertEquals(n, subject.nextULong(n, n + 1uL)) + } + + for ((origin, bound) in listOf( + (0uL to 32uL), + (1uL to 6uL), + (0uL to 0x1_0000_0000.toULong()), + (0uL to ULong.MAX_VALUE) + )) { + repeat(1000) { + val x = subject.nextULong(origin, bound) + if (x !in origin..(bound - 1uL)) { + fail("Value $x must be in range [$origin, $bound)") + } + } + } + } + @Suppress("EmptyRange") @Test fun nextLongInLongRange() { @@ -186,6 +322,35 @@ abstract class RandomSmokeTest { } } + @Test + fun nextULongInULongRange() { + assertFailsWith { subject.nextULong(1uL..0uL) } + assertFailsWith { subject.nextULong(ULong.MAX_VALUE..ULong.MIN_VALUE) } + assertFailsWith { subject.nextULong(ULong.MAX_VALUE..(ULong.MAX_VALUE - 1uL))} + + repeat(1000) { i -> + val n = (0x1_0000_0000).toULong() - 500uL + i.toULong() + assertEquals(n, subject.nextULong(n..n)) + + } + + for (range in listOf( + (0uL..1uL), + (1uL..5uL), + (0uL..31uL), + (0uL..(ULong.MAX_VALUE - 1uL)), + (0uL..ULong.MAX_VALUE) + )) { + repeat(1000) { + val x = subject.nextULong(range) + if (x !in range) { + fail("Value $x must be in range $range") + } + } + } + + } + @Test fun nextDouble() { @@ -291,6 +456,18 @@ abstract class RandomSmokeTest { assertFalse(bytes1 contentEquals bytes2) } + @Test + fun nextUBytes() { + val size = 20 + val ubytes1 = subject.nextUBytes(size) + assertEquals(size, ubytes1.size) + assertTrue(ubytes1.any { it != 0.toUByte() }) + + val ubytes2 = subject.nextUBytes(UByteArray(size)) + assertEquals(size, ubytes2.size) + assertTrue(ubytes2.any { it != 0.toUByte() }) + } + @Test fun nextBytesRange() { val size = 100 @@ -321,7 +498,7 @@ abstract class RandomSmokeTest { if (noChanges) { fail("Something should have changed in array after subrange [$from, $to) randomization (${1 + retries} times): " + - array.copyOfRange(from, to).contentToString()) + array.copyOfRange(from, to).contentToString()) } for (p in 0 until from) { @@ -332,6 +509,53 @@ abstract class RandomSmokeTest { } } } + + @Test + fun nextUBytesRange() { + val size = 100 + val array = subject.nextUBytes(size) + + assertFailsWith { subject.nextUBytes(array, -1, 10) } + assertFailsWith { subject.nextUBytes(array, 0, size + 10) } + assertFailsWith { subject.nextUBytes(array, 10, 0) } + + repeat(10000) { + val from = subject.nextInt(0, size - 1) + val to = subject.nextInt(from + 1, size) + + val prev = array.copyOf() + + subject.nextUBytes(array, from, to) + + var noChanges = array contentEquals prev + val rangeSize = to - from + + val retries = 4 / rangeSize + + var n = 0 + while(noChanges && n < retries) { + // there's a small chance that a small range will get the same value as before + // run randomization again + subject.nextUBytes(array, from, to) + noChanges = array contentEquals prev + n++ + } + + if(noChanges) { + fail("Something should have changed in array after subrange [$from, $to) randomization (${1 + retries} times: " + + array.copyOfRange(from, to).contentToString()) + } + + for (p in 0 until from) { + assertEquals(prev[p], array[p]) + } + + for (p in to until size) { + assertEquals(prev[p], array[p]) + } + + } + } } 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 290fd59a098..35f7eefdf70 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 @@ -3894,6 +3894,18 @@ public final class kotlin/random/Random$Companion : kotlin/random/Random { public final class kotlin/random/RandomKt { public static final fun Random (I)Lkotlin/random/Random; public static final fun Random (J)Lkotlin/random/Random; + public static final fun nextUBytes (Lkotlin/random/Random;I)[B + public static final fun nextUBytes-4zielkbi (Lkotlin/random/Random;[BII)[B + public static synthetic fun nextUBytes-4zielkbi$default (Lkotlin/random/Random;[BIIILjava/lang/Object;)[B + public static final fun nextUBytes-y7z55vk (Lkotlin/random/Random;[B)[B + public static final fun nextUInt (Lkotlin/random/Random;)I + public static final fun nextUInt (Lkotlin/random/Random;Lkotlin/ranges/UIntRange;)I + public static final fun nextUInt-5wlsgfq1 (Lkotlin/random/Random;II)I + public static final fun nextUInt-97rx7kg5 (Lkotlin/random/Random;I)I + public static final fun nextULong (Lkotlin/random/Random;)J + public static final fun nextULong (Lkotlin/random/Random;Lkotlin/ranges/ULongRange;)J + public static final fun nextULong-4sbioyd2 (Lkotlin/random/Random;J)J + public static final fun nextULong-7szwbobr (Lkotlin/random/Random;JJ)J } public class kotlin/ranges/CharProgression : java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker {