From 1b6b44c805c1884b2e4ac38c48b0d6bc39fd2bfa Mon Sep 17 00:00:00 2001 From: Abduqodiri Qurbonzoda Date: Mon, 28 Jan 2019 14:49:23 +0300 Subject: [PATCH] Implement conversion between floating point numbers and unsigned integers KT-27108 --- generators/builtins/common.kt | 1 + generators/builtins/unsignedTypes.kt | 28 ++++ libraries/stdlib/test/unsigned/UIntTest.kt | 88 +++++++++++ libraries/stdlib/test/unsigned/ULongTest.kt | 142 +++++++++++++++++- libraries/stdlib/unsigned/src/kotlin/UByte.kt | 14 ++ libraries/stdlib/unsigned/src/kotlin/UInt.kt | 14 ++ libraries/stdlib/unsigned/src/kotlin/ULong.kt | 14 ++ .../stdlib/unsigned/src/kotlin/UShort.kt | 14 ++ .../unsigned/src/kotlin/UnsignedUtils.kt | 44 ++++++ .../kotlin-stdlib-runtime-merged.txt | 6 + 10 files changed, 364 insertions(+), 1 deletion(-) diff --git a/generators/builtins/common.kt b/generators/builtins/common.kt index 0884939ebfc..94a8dd96e92 100644 --- a/generators/builtins/common.kt +++ b/generators/builtins/common.kt @@ -34,6 +34,7 @@ enum class PrimitiveType(val byteSize: Int?) { companion object { val exceptBoolean = PrimitiveType.values().filterNot { it == BOOLEAN } val onlyNumeric = PrimitiveType.values().filterNot { it == BOOLEAN || it == CHAR } + val floatingPoint = listOf(FLOAT, DOUBLE) } } diff --git a/generators/builtins/unsignedTypes.kt b/generators/builtins/unsignedTypes.kt index e5d6e2359c4..0fa89724021 100644 --- a/generators/builtins/unsignedTypes.kt +++ b/generators/builtins/unsignedTypes.kt @@ -6,6 +6,7 @@ package org.jetbrains.kotlin.generators.builtins.unsigned +import org.jetbrains.kotlin.generators.builtins.PrimitiveType import org.jetbrains.kotlin.generators.builtins.UnsignedType import org.jetbrains.kotlin.generators.builtins.convert import org.jetbrains.kotlin.generators.builtins.generateBuiltIns.BuiltInsSourceGenerator @@ -81,6 +82,7 @@ class UnsignedTypeGenerator(val type: UnsignedType, out: PrintWriter) : BuiltIns generateBitwiseOperators() generateMemberConversions() + generateFloatingConversions() generateToStringHashCode() @@ -223,6 +225,21 @@ class UnsignedTypeGenerator(val type: UnsignedType, out: PrintWriter) : BuiltIns out.println() } + private fun generateFloatingConversions() { + for (otherType in PrimitiveType.floatingPoint) { + val otherName = otherType.capitalized + out.println(" @kotlin.internal.InlineOnly") + out.print(" public inline fun to$otherName(): $otherName = ") + when (type) { + UnsignedType.UINT, UnsignedType.ULONG -> + out.println(if (otherType == PrimitiveType.FLOAT) "this.toDouble().toFloat()" else className.toLowerCase() + "ToDouble(data)") + else -> + out.println("this.toInt().to$otherName()") + } + } + out.println() + } + private fun generateExtensionConversions() { for (otherType in UnsignedType.values()) { val otherSigned = otherType.asSigned.capitalized @@ -236,6 +253,17 @@ class UnsignedTypeGenerator(val type: UnsignedType, out: PrintWriter) : BuiltIns else -> "$className(this.to$thisSigned())" }) } + out.println() + + for (otherType in PrimitiveType.floatingPoint) { + val otherName = otherType.capitalized + out.println("@SinceKotlin(\"1.3\")") + out.println("@ExperimentalUnsignedTypes") + out.println("@kotlin.internal.InlineOnly") + out.print("public inline fun $otherName.to$className(): $className = ") + val conversion = if (otherType == PrimitiveType.DOUBLE) "" else ".toDouble()" + out.println("doubleTo$className(this$conversion)") + } } diff --git a/libraries/stdlib/test/unsigned/UIntTest.kt b/libraries/stdlib/test/unsigned/UIntTest.kt index c0475cdefc3..f0e63bcf95c 100644 --- a/libraries/stdlib/test/unsigned/UIntTest.kt +++ b/libraries/stdlib/test/unsigned/UIntTest.kt @@ -5,6 +5,9 @@ package test.unsigned +import kotlin.math.nextDown +import kotlin.math.nextUp +import kotlin.math.pow import kotlin.math.sign import kotlin.random.Random import kotlin.test.* @@ -99,6 +102,91 @@ class UIntTest { } + @Test + fun convertToFloat() { + fun testEquals(v1: Float, v2: UInt) = assertEquals(v1, v2.toFloat()) + testEquals(0.0f, zero) + testEquals(1.0f, one) + testEquals(0xFFFF_FFFF.toFloat(), max) + repeat(100) { + val long = Random.nextLong(0, 0xFFFF_FFFF) + testEquals(long.toFloat(), long.toUInt()) + } + } + + @Test + fun convertToDouble() { + fun testEquals(v1: Double, v2: UInt) = assertEquals(v1, v2.toDouble()) + + testEquals(0.0, zero) + testEquals(1.0, one) + testEquals(max.toLong().toDouble(), max) + + repeat(100) { + val long = Random.nextLong(0, max.toLong()) + testEquals(long.toDouble(), long.toUInt()) + } + + fun testRounding(from: UInt, count: UInt) { + for (x in from..(from + count)) { + val double = x.toDouble() + val v = double.toUInt() + val down = double.nextDown().toUInt() + val up = double.nextUp().toUInt() + + assertTrue(down <= x && down <= v) + assertTrue(up >= x && up >= v) + + if (v > x) { + assertTrue(v - x <= x - down, "Expected $x being closer to $v than to $down") + } else { + assertTrue(x - v <= up - x, "Expected $x being closer to $v than to $up") + } + } + } + + testRounding(0u, 100u) + testRounding(Int.MAX_VALUE.toUInt() - 10u, 100u) + testRounding(UInt.MAX_VALUE - 100u, 100u) + } + + @Test + fun convertDoubleToUInt() { + fun testEquals(v1: Double, v2: UInt) = assertEquals(v1.toUInt(), v2) + + testEquals(0.0, zero) + testEquals(-1.0, zero) + + testEquals(-2_000_000_000_000.0, zero) + testEquals(-(2.0.pow(UInt.SIZE_BITS + 12)), zero) + testEquals(Double.MIN_VALUE, zero) + testEquals(Double.NEGATIVE_INFINITY, zero) + testEquals(Double.NaN, zero) + + testEquals(1.0, one) + + testEquals(2_000_000_000_000.0, max) + testEquals(max.toDouble(), max) + testEquals(2.0.pow(UInt.SIZE_BITS), max) + testEquals(2.0.pow(UInt.SIZE_BITS + 12), max) + testEquals(Double.MAX_VALUE, max) + testEquals(Double.POSITIVE_INFINITY, max) + + repeat(100) { + val v = -Random.nextDouble(until = 2.0.pow(UInt.SIZE_BITS + 8)) + testEquals(v, zero) + } + + repeat(100) { + val v = Random.nextDouble(from = max.toDouble(), until = 2.0.pow(UInt.SIZE_BITS + 8)) + testEquals(v, max) + } + + repeat(100) { + val v = Random.nextDouble(until = max.toDouble()) + testEquals(v, v.toLong().toUInt()) + } + } } \ No newline at end of file diff --git a/libraries/stdlib/test/unsigned/ULongTest.kt b/libraries/stdlib/test/unsigned/ULongTest.kt index 5c72b171921..8a8429d0b98 100644 --- a/libraries/stdlib/test/unsigned/ULongTest.kt +++ b/libraries/stdlib/test/unsigned/ULongTest.kt @@ -5,7 +5,7 @@ package test.unsigned -import kotlin.math.sign +import kotlin.math.* import kotlin.random.Random import kotlin.test.* @@ -99,6 +99,146 @@ class ULongTest { } + @Test + fun convertToFloat() { + fun testEquals(v1: Float, v2: ULong) = assertEquals(v1, v2.toFloat()) + testEquals(0.0f, zero) + testEquals(1.0f, one) + testEquals(2.0f.pow(ULong.SIZE_BITS) - 1, max) + testEquals(2.0f * Long.MAX_VALUE + 1, max) + + repeat(100) { + val long = Random.nextLong(from = 0, until = Long.MAX_VALUE) + testEquals(long.toFloat(), long.toULong()) + } + + repeat(100) { + val long = Random.nextLong(from = 0, until = Long.MAX_VALUE) + val float = Long.MAX_VALUE.toFloat() + long.toFloat() // We lose accuracy here, hence `eps` is used. + val ulong = Long.MAX_VALUE.toULong() + long.toULong() + + val eps = 1e+13 + assertTrue(abs(float - ulong.toFloat()) < eps) + } + } + + @Test + fun convertToDouble() { + fun testEquals(v1: Double, v2: ULong) = assertEquals(v1, v2.toDouble()) + + testEquals(0.0, zero) + testEquals(1.0, one) + + testEquals(2.0.pow(ULong.SIZE_BITS) - 1, max) + testEquals(2.0 * Long.MAX_VALUE + 1, max) + + repeat(100) { + val long = Random.nextLong(from = 0, until = Long.MAX_VALUE) + testEquals(long.toDouble(), long.toULong()) + } + + repeat(100) { + val long = Random.nextLong(from = 0, until = Long.MAX_VALUE) + val double = Long.MAX_VALUE.toDouble() + long.toDouble() // We lose accuracy here, hence `eps` is used. + val ulong = Long.MAX_VALUE.toULong() + long.toULong() + + val eps = 1e+4 + assertTrue(abs(double - ulong.toDouble()) < eps) + } + + fun testRounding(from: ULong, count: UInt) { + for (x in from..(from + count)) { + val double = x.toDouble() + val v = double.toULong() + val down = double.nextDown().toULong() + val up = double.nextUp().toULong() + + assertTrue(down <= x && down <= v) + assertTrue(up >= x && up >= v) + + if (v > x) { + assertTrue(v - x <= x - down, "Expected $x being closer to $v than to $down") + } else { + assertTrue(x - v <= up - x, "Expected $x being closer to $v than to $up") + } + } + } + + testRounding(0u, 100u) + testRounding(Long.MAX_VALUE.toULong() - 520u, 100u) + testRounding(ULong.MAX_VALUE - 10000u, 10000u) + } + + @Test + fun convertDoubleToULong() { + fun testEquals(v1: Double, v2: ULong) = assertEquals(v1.toULong(), v2) + + testEquals(0.0, zero) + testEquals(-1.0, zero) + + testEquals(-2_000_000_000_000.0, zero) + testEquals(-(2.0.pow(ULong.SIZE_BITS + 5)), zero) + testEquals(Double.MIN_VALUE, zero) + testEquals(Double.NEGATIVE_INFINITY, zero) + testEquals(Double.NaN, zero) + + testEquals(1.0, one) + + testEquals(2_000_000_000_000_000_000_000.0, max) + testEquals(2.0.pow(ULong.SIZE_BITS), max) + testEquals(2.0.pow(ULong.SIZE_BITS + 5), max) + testEquals(Double.MAX_VALUE, max) + testEquals(Double.POSITIVE_INFINITY, max) + + repeat(100) { + val v = -Random.nextDouble(until = 2.0.pow(ULong.SIZE_BITS + 8)) + testEquals(v, zero) + } + + repeat(100) { + val v = Random.nextDouble(from = max.toDouble(), until = 2.0.pow(ULong.SIZE_BITS + 8)) + testEquals(v, max) + } + + repeat(100) { + val v = Random.nextDouble() * Long.MAX_VALUE + testEquals(v, v.toLong().toULong()) + } + + repeat(100) { + val diff = Random.nextDouble() * Long.MAX_VALUE + val d = Long.MAX_VALUE.toDouble() + diff + val ul = Long.MAX_VALUE.toULong() + diff.toLong().toULong() + val eps = 1e-6 + + assertTrue(d.toULong() / ul <= 1u) + assertTrue(d / ul.toDouble() < 1 + eps) + assertTrue(d / ul.toDouble() > 1 - eps) + } + + fun testTrailingBits(v: Double, count: Int) { + val mask = (1uL shl count) - 1uL + assertEquals(0uL, v.toULong() and mask) + } + + var withTrailingZeros = 2.0.pow(64) + repeat(10) { + withTrailingZeros = withTrailingZeros.nextDown() + testTrailingBits(withTrailingZeros, 11) + } + + withTrailingZeros = 2.0.pow(63) + repeat(10) { + testTrailingBits(withTrailingZeros, 11) + withTrailingZeros = withTrailingZeros.nextUp() + } + + repeat(100) { + val msb = Random.nextInt(53, 64) + val v = 2.0.pow(msb) * (1.0 + Random.nextDouble()) + testTrailingBits(v, msb - 52) + } + } } \ No newline at end of file diff --git a/libraries/stdlib/unsigned/src/kotlin/UByte.kt b/libraries/stdlib/unsigned/src/kotlin/UByte.kt index 3f9c019181f..af8c8cbad19 100644 --- a/libraries/stdlib/unsigned/src/kotlin/UByte.kt +++ b/libraries/stdlib/unsigned/src/kotlin/UByte.kt @@ -176,6 +176,11 @@ public inline class UByte @PublishedApi internal constructor(@PublishedApi inter @kotlin.internal.InlineOnly public inline fun toULong(): ULong = ULong(data.toLong() and 0xFF) + @kotlin.internal.InlineOnly + public inline fun toFloat(): Float = this.toInt().toFloat() + @kotlin.internal.InlineOnly + public inline fun toDouble(): Double = this.toInt().toDouble() + public override fun toString(): String = toInt().toString() } @@ -196,3 +201,12 @@ public inline fun Int.toUByte(): UByte = UByte(this.toByte()) @ExperimentalUnsignedTypes @kotlin.internal.InlineOnly public inline fun Long.toUByte(): UByte = UByte(this.toByte()) + +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun Float.toUByte(): UByte = doubleToUByte(this.toDouble()) +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun Double.toUByte(): UByte = doubleToUByte(this) diff --git a/libraries/stdlib/unsigned/src/kotlin/UInt.kt b/libraries/stdlib/unsigned/src/kotlin/UInt.kt index ca3207535d2..a82edb94549 100644 --- a/libraries/stdlib/unsigned/src/kotlin/UInt.kt +++ b/libraries/stdlib/unsigned/src/kotlin/UInt.kt @@ -182,6 +182,11 @@ public inline class UInt @PublishedApi internal constructor(@PublishedApi intern @kotlin.internal.InlineOnly public inline fun toULong(): ULong = ULong(data.toLong() and 0xFFFF_FFFF) + @kotlin.internal.InlineOnly + public inline fun toFloat(): Float = this.toDouble().toFloat() + @kotlin.internal.InlineOnly + public inline fun toDouble(): Double = uintToDouble(data) + public override fun toString(): String = toLong().toString() } @@ -202,3 +207,12 @@ public inline fun Int.toUInt(): UInt = UInt(this) @ExperimentalUnsignedTypes @kotlin.internal.InlineOnly public inline fun Long.toUInt(): UInt = UInt(this.toInt()) + +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun Float.toUInt(): UInt = doubleToUInt(this.toDouble()) +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun Double.toUInt(): UInt = doubleToUInt(this) diff --git a/libraries/stdlib/unsigned/src/kotlin/ULong.kt b/libraries/stdlib/unsigned/src/kotlin/ULong.kt index e2859f3ca52..fee6fe66539 100644 --- a/libraries/stdlib/unsigned/src/kotlin/ULong.kt +++ b/libraries/stdlib/unsigned/src/kotlin/ULong.kt @@ -182,6 +182,11 @@ public inline class ULong @PublishedApi internal constructor(@PublishedApi inter @kotlin.internal.InlineOnly public inline fun toULong(): ULong = this + @kotlin.internal.InlineOnly + public inline fun toFloat(): Float = this.toDouble().toFloat() + @kotlin.internal.InlineOnly + public inline fun toDouble(): Double = ulongToDouble(data) + public override fun toString(): String = ulongToString(data) } @@ -202,3 +207,12 @@ public inline fun Int.toULong(): ULong = ULong(this.toLong()) @ExperimentalUnsignedTypes @kotlin.internal.InlineOnly public inline fun Long.toULong(): ULong = ULong(this) + +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun Float.toULong(): ULong = doubleToULong(this.toDouble()) +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun Double.toULong(): ULong = doubleToULong(this) diff --git a/libraries/stdlib/unsigned/src/kotlin/UShort.kt b/libraries/stdlib/unsigned/src/kotlin/UShort.kt index b7b592233d2..be98314c7da 100644 --- a/libraries/stdlib/unsigned/src/kotlin/UShort.kt +++ b/libraries/stdlib/unsigned/src/kotlin/UShort.kt @@ -176,6 +176,11 @@ public inline class UShort @PublishedApi internal constructor(@PublishedApi inte @kotlin.internal.InlineOnly public inline fun toULong(): ULong = ULong(data.toLong() and 0xFFFF) + @kotlin.internal.InlineOnly + public inline fun toFloat(): Float = this.toInt().toFloat() + @kotlin.internal.InlineOnly + public inline fun toDouble(): Double = this.toInt().toDouble() + public override fun toString(): String = toInt().toString() } @@ -196,3 +201,12 @@ public inline fun Int.toUShort(): UShort = UShort(this.toShort()) @ExperimentalUnsignedTypes @kotlin.internal.InlineOnly public inline fun Long.toUShort(): UShort = UShort(this.toShort()) + +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun Float.toUShort(): UShort = doubleToUShort(this.toDouble()) +@SinceKotlin("1.3") +@ExperimentalUnsignedTypes +@kotlin.internal.InlineOnly +public inline fun Double.toUShort(): UShort = doubleToUShort(this) diff --git a/libraries/stdlib/unsigned/src/kotlin/UnsignedUtils.kt b/libraries/stdlib/unsigned/src/kotlin/UnsignedUtils.kt index adfc1bbb269..913bd8c8b0b 100644 --- a/libraries/stdlib/unsigned/src/kotlin/UnsignedUtils.kt +++ b/libraries/stdlib/unsigned/src/kotlin/UnsignedUtils.kt @@ -63,6 +63,50 @@ internal fun ulongRemainder(v1: ULong, v2: ULong): ULong { } +@PublishedApi +internal fun doubleToUByte(v: Double): UByte = when { + v.isNaN() -> 0u + v <= UByte.MIN_VALUE.toDouble() -> UByte.MIN_VALUE + v >= UByte.MAX_VALUE.toDouble() -> UByte.MAX_VALUE + else -> v.toInt().toUByte() +} + +@PublishedApi +internal fun doubleToUShort(v: Double): UShort = when { + v.isNaN() -> 0u + v <= UShort.MIN_VALUE.toDouble() -> UShort.MIN_VALUE + v >= UShort.MAX_VALUE.toDouble() -> UShort.MAX_VALUE + else -> v.toInt().toUShort() +} + +@PublishedApi +internal fun doubleToUInt(v: Double): UInt = when { + v.isNaN() -> 0u + v <= UInt.MIN_VALUE.toDouble() -> UInt.MIN_VALUE + v >= UInt.MAX_VALUE.toDouble() -> UInt.MAX_VALUE + v <= Int.MAX_VALUE -> v.toInt().toUInt() + else -> (v - Int.MAX_VALUE).toInt().toUInt() + Int.MAX_VALUE.toUInt() // Int.MAX_VALUE < v < UInt.MAX_VALUE +} + +@PublishedApi +internal fun doubleToULong(v: Double): ULong = when { + v.isNaN() -> 0u + v <= ULong.MIN_VALUE.toDouble() -> ULong.MIN_VALUE + v >= ULong.MAX_VALUE.toDouble() -> ULong.MAX_VALUE + v < Long.MAX_VALUE -> v.toLong().toULong() + + // Real values from Long.MAX_VALUE to (Long.MAX_VALUE + 1) are not representable in Double, so don't handle them. + else -> (v - 9223372036854775808.0).toLong().toULong() + 9223372036854775808uL // Long.MAX_VALUE + 1 < v < ULong.MAX_VALUE +} + + +@PublishedApi +internal fun uintToDouble(v: Int): Double = (v and Int.MAX_VALUE).toDouble() + (v ushr 31 shl 30).toDouble() * 2 + +@PublishedApi +internal fun ulongToDouble(v: Long): Double = (v ushr 11).toDouble() * 2048 + (v and 2047) + + internal fun ulongToString(v: Long): String = ulongToString(v, 10) internal fun ulongToString(v: Long, base: Int): String { 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 b2cf892cb4c..0956eb76ab2 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 @@ -459,12 +459,18 @@ public abstract interface annotation class kotlin/UnsafeVariance : java/lang/ann } public final class kotlin/UnsignedKt { + public static final fun doubleToUByte (D)B + public static final fun doubleToUInt (D)I + public static final fun doubleToULong (D)J + public static final fun doubleToUShort (D)S public static final fun uintCompare (II)I public static final fun uintDivide-J1ME1BU (II)I public static final fun uintRemainder-J1ME1BU (II)I + public static final fun uintToDouble (I)D public static final fun ulongCompare (JJ)I public static final fun ulongDivide-eb3DHEI (JJ)J public static final fun ulongRemainder-eb3DHEI (JJ)J + public static final fun ulongToDouble (J)D } public abstract interface annotation class kotlin/UseExperimental : java/lang/annotation/Annotation {