Skip to content

Commit 248135a

Browse files
skuzmichSpace Team
authored and
Space Team
committed
[Wasm][stdlib] Improve Float.toString()
Before this change Float.toString() was producing unnecessarily long strings because it was implemented as Float.toDouble().toString() For example, 0.3f.toString() was evaluated to "0.30000001192092896". This change parametrizes the code with `isSinglePrecision: Boolean`, which calculates a wider range of values, rounded to the original Float value. Wider range allows the algorithm to find shorter decimal strings. ^KT-69107 Fixed ^KT-68948 Fixed ^KT-59118 Fixed
1 parent 8076d0a commit 248135a

File tree

4 files changed

+72
-16
lines changed

4 files changed

+72
-16
lines changed

generators/builtins/primitives/WasmPrimitivesGenerator.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ class WasmPrimitivesGenerator(writer: PrintWriter) : BasePrimitivesGenerator(wri
233233

234234
override fun MethodBuilder.modifyGeneratedToString(thisKind: PrimitiveType) {
235235
when (thisKind) {
236-
in PrimitiveType.floatingPoint -> "dtoa(this${thisKind.castToIfNecessary(PrimitiveType.DOUBLE)})"
236+
in PrimitiveType.floatingPoint -> "dtoa(this${thisKind.castToIfNecessary(PrimitiveType.DOUBLE)}, isSinglePrecision = ${thisKind == PrimitiveType.FLOAT})"
237237
PrimitiveType.INT, PrimitiveType.LONG -> "itoa${thisKind.bitSize}(this)"
238238
else -> "this.toInt().toString()"
239239
}.setAsExpressionBody()

libraries/stdlib/test/text/StringNumberConversionTest.kt

+34
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ package test.text
88
import test.TestPlatform
99
import test.testExceptOn
1010
import test.testOn
11+
import kotlin.math.pow
12+
import kotlin.math.round
1113
import kotlin.test.*
1214

1315
private fun testOnNativeAndJvm(action: () -> Unit) {
@@ -802,4 +804,36 @@ class FpNumberToStringTest {
802804
assertEquals(Float.POSITIVE_INFINITY.toString(), "Infinity")
803805
assertEquals(Float.NEGATIVE_INFINITY.toString(), "-Infinity")
804806
}
807+
808+
@Test
809+
fun kt69107() {
810+
val a = identity(0.30000001192092F)
811+
assertEquals("0.3", (round(a * 10f) / 10f).toString())
812+
assertEquals("0.3", (0.3f as Any).toString())
813+
}
814+
815+
@Test
816+
fun kt68948() {
817+
val inlineTemplate = "${identity(3.4f)}"
818+
assertEquals("3.4", inlineTemplate)
819+
820+
val floatVariable = identity(3.4f)
821+
val variableTemplate = "$floatVariable"
822+
assertEquals("3.4", variableTemplate)
823+
}
824+
825+
@Test
826+
fun kt59118() {
827+
fun Float.roundDecimalPlaces(places: Int): Float {
828+
if (places < 0) return this
829+
val placesFactor: Float = 10f.pow(places.toFloat())
830+
return round(this * placesFactor) / placesFactor
831+
}
832+
833+
assertEquals("0.031398475", "" + identity(0.031398475f))
834+
assertEquals("0.03", "" + 0.031398475f.roundDecimalPlaces(2))
835+
}
836+
837+
// Prevent compiler constant folding
838+
private fun <T> identity(x: T): T = x
805839
}

libraries/stdlib/wasm/builtins/kotlin/Primitives.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -2265,7 +2265,7 @@ public actual class Float private constructor(private val value: Float) : Number
22652265

22662266
@kotlin.internal.IntrinsicConstEvaluation
22672267
public actual override fun toString(): String =
2268-
dtoa(this.toDouble())
2268+
dtoa(this.toDouble(), isSinglePrecision = true)
22692269

22702270
@kotlin.internal.IntrinsicConstEvaluation
22712271
public actual override fun equals(other: Any?): Boolean =
@@ -2667,7 +2667,7 @@ public actual class Double private constructor(private val value: Double) : Numb
26672667

26682668
@kotlin.internal.IntrinsicConstEvaluation
26692669
public actual override fun toString(): String =
2670-
dtoa(this)
2670+
dtoa(this, isSinglePrecision = false)
26712671

26722672
@kotlin.internal.IntrinsicConstEvaluation
26732673
public actual override fun equals(other: Any?): Boolean =

libraries/stdlib/wasm/internal/kotlin/wasm/internal/Number2String.kt

+35-13
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ private fun decimalCount64High(value: ULong): Int {
158158

159159
private const val MAX_DOUBLE_LENGTH = 28
160160

161-
internal fun dtoa(value: Double): String {
161+
internal fun dtoa(value: Double, isSinglePrecision: Boolean): String {
162162
if (value == 0.0) {
163163
return if (value.toRawBits() == 0L) "0.0" else "-0.0"
164164
}
@@ -169,21 +169,21 @@ internal fun dtoa(value: Double): String {
169169
}
170170

171171
val buf = WasmCharArray(MAX_DOUBLE_LENGTH)
172-
val size = dtoaCore(buf, value)
172+
val size = dtoaCore(buf, value, isSinglePrecision)
173173
val ret = WasmCharArray(size)
174174
buf.copyInto(ret, 0, 0, size)
175175
return ret.createString()
176176
}
177177

178-
private fun dtoaCore(buffer: WasmCharArray, valueInp: Double): Int {
178+
private fun dtoaCore(buffer: WasmCharArray, valueInp: Double, isSinglePrecision: Boolean): Int {
179179
var value = valueInp
180180

181181
val sign = (value < 0).toInt()
182182
if (sign == 1) {
183183
value = -value
184184
buffer.set(0, CharCodes.MINUS.code.toChar())
185185
}
186-
var len = grisu2(value, buffer, sign)
186+
var len = grisu2(value, buffer, sign, isSinglePrecision)
187187
len = prettify(BufferWithOffset(buffer, sign), len - sign, _K)
188188
return len + sign
189189
}
@@ -235,15 +235,36 @@ private val FRC_POWERS = longArrayOf(
235235
0x9E19DB92B4E31BA9UL.toLong(), 0xEB96BF6EBADF77D9UL.toLong(), 0xAF87023B9BF0EE6BUL.toLong()
236236
)
237237

238-
private fun grisu2(value: Double, buffer: WasmCharArray, sign: Int): Int {
238+
private const val SINGLE_SIGNIFICANT_MASK = 0x007FFFFF
239+
private const val SINGLE_EXPONENT_MASK = 0x7F800000
240+
private const val SINGLE_SIGNIFICANT_SIZE = 23 // Excluding hidden bit
241+
private const val SINGLE_EXPONENT_BIAS = 0x7F + SINGLE_SIGNIFICANT_SIZE
242+
243+
private const val DOUBLE_SIGNIFICANT_MASK = 0x000FFFFFFFFFFFFFL
244+
private const val DOUBLE_EXPONENT_MASK = 0x7FF0000000000000L
245+
private const val DOUBLE_SIGNIFICANT_SIZE = 52 // Excluding hidden bit
246+
private const val DOUBLE_EXPONENT_BIAS = 0x3FF + DOUBLE_SIGNIFICANT_SIZE
247+
248+
private fun grisu2(value: Double, buffer: WasmCharArray, sign: Int, isSinglePrecision: Boolean): Int {
249+
var frc: Long
250+
var exp: Int
251+
239252
// frexp routine
240-
val uv = value.toBits()
241-
var exp = ((uv and 0x7FF0000000000000) ushr 52).toInt()
242-
val sid = uv and 0x000FFFFFFFFFFFFF
243-
var frc = ((exp != 0).toLong() shl 52) + sid
244-
exp = (if (exp != 0) exp else 1) - (0x3FF + 52)
253+
if (isSinglePrecision) {
254+
val uv = value.toFloat().toBits()
255+
exp = (uv and SINGLE_EXPONENT_MASK) ushr SINGLE_SIGNIFICANT_SIZE
256+
val sid = uv and SINGLE_SIGNIFICANT_MASK
257+
frc = ((exp != 0).toLong() shl SINGLE_SIGNIFICANT_SIZE) + sid
258+
exp = (if (exp != 0) exp else 1) - SINGLE_EXPONENT_BIAS
259+
} else {
260+
val uv = value.toBits()
261+
exp = ((uv and DOUBLE_EXPONENT_MASK) ushr DOUBLE_SIGNIFICANT_SIZE).toInt()
262+
val sid = uv and DOUBLE_SIGNIFICANT_MASK
263+
frc = ((exp != 0).toLong() shl DOUBLE_SIGNIFICANT_SIZE) + sid
264+
exp = (if (exp != 0) exp else 1) - DOUBLE_EXPONENT_BIAS
265+
}
245266

246-
normalizedBoundaries(frc, exp)
267+
normalizedBoundaries(frc, exp, isSinglePrecision)
247268
getCachedPower(_exp)
248269

249270
// normalize
@@ -288,14 +309,15 @@ private fun umul64e(e1: Int, e2: Int): Int {
288309
return e1 + e2 + 64 // where 64 is significand size
289310
}
290311

291-
private fun normalizedBoundaries(f: Long, e: Int) {
312+
private fun normalizedBoundaries(f: Long, e: Int, isSinglePrecision: Boolean) {
292313
var frc = (f shl 1) + 1
293314
var exp = e - 1
294315
val off = frc.countLeadingZeroBits()
295316
frc = frc shl off
296317
exp -= off
297318

298-
val m = 1 + (f == 0x0010000000000000).toInt()
319+
val smallestNormalizedSignificand: Long = if (isSinglePrecision) 0x00800000 else 0x0010000000000000
320+
val m = 1 + (f == smallestNormalizedSignificand).toInt()
299321

300322
_frc_plus = frc
301323
_frc_minus = ((f shl m) - 1) shl e - m - exp

0 commit comments

Comments
 (0)