From d881f0d2a6c8d9d8e3d66e630b70d48c784f35c5 Mon Sep 17 00:00:00 2001 From: Igor Yakovlev Date: Wed, 2 Mar 2022 20:50:19 +0100 Subject: [PATCH] [WASM] Replace String storage type from CharArray to WasmCharArray --- libraries/stdlib/wasm/builtins/kotlin/Char.kt | 2 +- .../stdlib/wasm/builtins/kotlin/String.kt | 41 +++++++--- .../kotlin/wasm/internal/ExternalWrapper.kt | 2 +- .../kotlin/wasm/internal/ImportExport.kt | 2 +- .../kotlin/wasm/internal/Number2String.kt | 74 ++++++++++++------- .../internal/kotlin/wasm/internal/Runtime.kt | 18 ++--- .../wasm/src/kotlin/text/StringBuilderWasm.kt | 9 ++- .../wasm/src/kotlin/text/StringsWasm.kt | 20 ++++- 8 files changed, 113 insertions(+), 55 deletions(-) diff --git a/libraries/stdlib/wasm/builtins/kotlin/Char.kt b/libraries/stdlib/wasm/builtins/kotlin/Char.kt index 3b8192f6b7b..7b1d9838385 100644 --- a/libraries/stdlib/wasm/builtins/kotlin/Char.kt +++ b/libraries/stdlib/wasm/builtins/kotlin/Char.kt @@ -93,7 +93,7 @@ public class Char private constructor(public val value: Char) : Comparable this.toInt().toDouble() override fun toString(): String = - String.unsafeFromCharArray(charArrayOf(this)) + String.unsafeFromCharArray(WasmCharArray(1).also { it.set(0, this) }) override fun hashCode(): Int = this.toInt().hashCode() diff --git a/libraries/stdlib/wasm/builtins/kotlin/String.kt b/libraries/stdlib/wasm/builtins/kotlin/String.kt index 19d26756565..83cd884b8fe 100644 --- a/libraries/stdlib/wasm/builtins/kotlin/String.kt +++ b/libraries/stdlib/wasm/builtins/kotlin/String.kt @@ -12,20 +12,27 @@ import kotlin.math.min * The `String` class represents character strings. All string literals in Kotlin programs, such as `"abc"`, are * implemented as instances of this class. */ -public class String private constructor(internal val chars: CharArray) : Comparable, CharSequence { +public class String private constructor(internal val chars: WasmCharArray) : Comparable, CharSequence { public companion object { // Note: doesn't copy the array, use with care. - internal fun unsafeFromCharArray(chars: CharArray) = String(chars) + internal fun unsafeFromCharArray(chars: WasmCharArray) = String(chars) } /** * Returns a string obtained by concatenating this string with the string representation of the given [other] object. */ - public operator fun plus(other: Any?): String = - String(chars + other.toString().chars) + public operator fun plus(other: Any?): String { + val otherChars = if (other is String) other.chars else other.toString().chars + val newCharsLen = chars.len() + otherChars.len() + val newChars = WasmCharArray(newCharsLen) + newChars.fill(newCharsLen) { i -> + if (i < chars.len()) chars.get(i) else otherChars.get(i - chars.len()) + } + return String(newChars) + } public override val length: Int - get() = chars.size + get() = chars.len() /** * Returns the character of this string at the specified [index]. @@ -33,10 +40,20 @@ public class String private constructor(internal val chars: CharArray) : Compara * If the [index] is out of bounds of this string, throws an [IndexOutOfBoundsException] except in Kotlin/JS * where the behavior is unspecified. */ - public override fun get(index: Int): Char = chars[index] + public override fun get(index: Int): Char { + if (index < 0 || index >= chars.len()) throw IndexOutOfBoundsException() + return chars.get(index) + } public override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { - return String(chars.sliceArray(startIndex until endIndex)) + val actualStartIndex = startIndex.coerceAtLeast(0) + val actualEndIndex = endIndex.coerceAtMost(chars.len()) + val newCharsLen = actualEndIndex - actualStartIndex + val newChars = WasmCharArray(newCharsLen) + newChars.fill(newCharsLen) { i -> + chars.get(actualStartIndex + i) + } + return String(newChars) } public override fun compareTo(other: String): Int { @@ -62,12 +79,16 @@ public class String private constructor(internal val chars: CharArray) : Compara public override fun hashCode(): Int { if (_hashCode != 0 || this.isEmpty()) return _hashCode + var hash = 0 - for (c in chars) - hash = 31 * hash + c.toInt() + var i = 0 + while (i < chars.len()) { + hash = 31 * hash + chars.get(i).toInt() + i++ + } _hashCode = hash return _hashCode } } -internal fun stringLiteral(startAddr: Int, length: Int) = String.unsafeFromCharArray(unsafeRawMemoryToCharArray(startAddr, length)) +internal fun stringLiteral(startAddr: Int, length: Int) = String.unsafeFromCharArray(unsafeRawMemoryToWasmCharArray(startAddr, length)) diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt index bfd1a8d667b..ff5a5558396 100644 --- a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt +++ b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt @@ -156,7 +156,7 @@ internal fun convertJsStringToKotlinString(x: ExternalInterfaceType): String { val length = stringLength(x) val addr = unsafeGetScratchRawMemory(INT_SIZE_BYTES + length * CHAR_SIZE_BYTES) jsWriteStringIntoMemory(x, addr) - return String.unsafeFromCharArray(unsafeRawMemoryToCharArray(addr, length)) + return String.unsafeFromCharArray(unsafeRawMemoryToWasmCharArray(addr, length)) } diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ImportExport.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ImportExport.kt index 11f6c4471ef..c43082c8fcc 100644 --- a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ImportExport.kt +++ b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ImportExport.kt @@ -14,7 +14,7 @@ internal fun exportString(src: String?): Int { val retAddr = unsafeGetScratchRawMemory(INT_SIZE_BYTES + src.length * CHAR_SIZE_BYTES) wasm_i32_store(retAddr, src.length) - unsafeCharArrayToRawMemory(src.chars, retAddr + INT_SIZE_BYTES) + unsafeWasmCharArrayToRawMemory(src.chars, retAddr + INT_SIZE_BYTES) return retAddr } diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Number2String.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Number2String.kt index dd23b076cf8..5dc9f69ff61 100644 --- a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Number2String.kt +++ b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Number2String.kt @@ -59,18 +59,18 @@ internal fun itoa32(inputValue: Int, radix: Int): String { val absValue = if (sign == 1) -inputValue else inputValue val decimals = decimalCount32(absValue) + sign - val buf = CharArray(decimals) + val buf = WasmCharArray(decimals) utoaDecSimple(buf, absValue, decimals) if (sign == 1) - buf[0] = CharCodes.MINUS.code.toChar() + buf.set(0, CharCodes.MINUS.code.toChar()) return String.unsafeFromCharArray(buf) } -private fun utoaDecSimple(buffer: CharArray, numInput: Int, offsetInput: Int) { +private fun utoaDecSimple(buffer: WasmCharArray, numInput: Int, offsetInput: Int) { assert(numInput != 0) - assert(buffer.isNotEmpty()) - assert(offsetInput > 0 && offsetInput <= buffer.size) + assert(buffer.len() > 0) + assert(offsetInput > 0 && offsetInput <= buffer.len()) var num = numInput var offset = offsetInput @@ -79,14 +79,14 @@ private fun utoaDecSimple(buffer: CharArray, numInput: Int, offsetInput: Int) { val r = num % 10 num = t offset-- - buffer[offset] = digitToChar(r) + buffer.set(offset, digitToChar(r)) } while (num > 0) } -private fun utoaDecSimple64(buffer: CharArray, numInput: Long, offsetInput: Int) { +private fun utoaDecSimple64(buffer: WasmCharArray, numInput: Long, offsetInput: Int) { assert(numInput != 0L) - assert(buffer.isNotEmpty()) - assert(offsetInput > 0 && offsetInput <= buffer.size) + assert(buffer.len() > 0) + assert(offsetInput > 0 && offsetInput <= buffer.len()) var num = numInput var offset = offsetInput @@ -95,7 +95,7 @@ private fun utoaDecSimple64(buffer: CharArray, numInput: Long, offsetInput: Int) val r = (num % 10).toInt() num = t offset-- - buffer[offset] = digitToChar(r) + buffer.set(offset, digitToChar(r)) } while (num > 0) } @@ -139,10 +139,10 @@ internal fun itoa64(inputValue: Long, radix: Int): String { val absValue = if (sign == 1) -inputValue else inputValue val decimals = decimalCount64High(absValue) + sign - val buf = CharArray(decimals) + val buf = WasmCharArray(decimals) utoaDecSimple64(buf, absValue, decimals) if (sign == 1) - buf[0] = CharCodes.MINUS.code.toChar() + buf.set(0, CharCodes.MINUS.code.toChar()) return String.unsafeFromCharArray(buf) } @@ -174,22 +174,21 @@ internal fun dtoa(value: Double): String { return if (value < 0) "-Infinity" else "Infinity" } - val buf = CharArray(MAX_DOUBLE_LENGTH) + val buf = WasmCharArray(MAX_DOUBLE_LENGTH) val size = dtoaCore(buf, value) - val ret = CharArray(size) + val ret = WasmCharArray(size) buf.copyInto(ret, 0, 0, size) return String.unsafeFromCharArray(ret) } -private fun dtoaCore(buffer: CharArray, valueInp: Double): Int { +private fun dtoaCore(buffer: WasmCharArray, valueInp: Double): Int { var value = valueInp val sign = (value < 0).toInt() if (sign == 1) { value = -value - buffer[0] = CharCodes.MINUS.code.toChar() + buffer.set(0, CharCodes.MINUS.code.toChar()) } - var len = grisu2(value, buffer, sign) len = prettify(BufferWithOffset(buffer, sign), len - sign, _K) return len + sign @@ -242,7 +241,7 @@ private val FRC_POWERS = longArrayOf( 0x9E19DB92B4E31BA9UL.toLong(), 0xEB96BF6EBADF77D9UL.toLong(), 0xAF87023B9BF0EE6BUL.toLong() ) -private fun grisu2(value: Double, buffer: CharArray, sign: Int): Int { +private fun grisu2(value: Double, buffer: WasmCharArray, sign: Int): Int { // frexp routine val uv = value.toBits() var exp = ((uv and 0x7FF0000000000000) ushr 52).toInt() @@ -322,7 +321,7 @@ private fun getCachedPower(minExp: Int) { _exp_pow = EXP_POWERS[index].toInt() } -private fun genDigits(buffer: CharArray, w_frc: Long, w_exp: Int, mp_frc: Long, mp_exp: Int, deltaInp: Long, sign: Int): Int { +private fun genDigits(buffer: WasmCharArray, w_frc: Long, w_exp: Int, mp_frc: Long, mp_exp: Int, deltaInp: Long, sign: Int): Int { var delta = deltaInp val one_exp = -mp_exp val one_frc = 1L shl one_exp @@ -354,7 +353,7 @@ private fun genDigits(buffer: CharArray, w_frc: Long, w_exp: Int, mp_frc: Long, } if (d or len != 0) - buffer[len++] = digitToChar(d) + buffer.set(len++, digitToChar(d)) --kappa val tmp = (p1.toLong() shl one_exp) + p2 @@ -373,7 +372,7 @@ private fun genDigits(buffer: CharArray, w_frc: Long, w_exp: Int, mp_frc: Long, val d = p2 ushr one_exp if (d or len.toLong() != 0L) - buffer[len++] = digitToChar(d.toInt()) + buffer.set(len++, digitToChar(d.toInt())) p2 = p2 and mask --kappa @@ -385,10 +384,10 @@ private fun genDigits(buffer: CharArray, w_frc: Long, w_exp: Int, mp_frc: Long, } } -private fun grisuRound(buffer: CharArray, len: Int, delta: Long, restInp: Long, ten_kappa: Long, wp_w: Long) { +private fun grisuRound(buffer: WasmCharArray, len: Int, delta: Long, restInp: Long, ten_kappa: Long, wp_w: Long) { var rest = restInp val lastp = len - 1 - var digit = buffer[lastp] + var digit = buffer.get(lastp) while ( rest < wp_w && delta - rest >= ten_kappa && ( @@ -399,17 +398,38 @@ private fun grisuRound(buffer: CharArray, len: Int, delta: Long, restInp: Long, --digit rest += ten_kappa; } - buffer[lastp] = digit + buffer.set(lastp, digit) } -private class BufferWithOffset(val buf: CharArray, val off: Int) { +private fun WasmCharArray.copyInto(destination: WasmCharArray, destinationOffset: Int, sourceOffset: Int, len: Int) { + var srcIndex: Int + var dstIndex: Int + var increment: Int + if (destinationOffset <= sourceOffset) { + srcIndex = sourceOffset + dstIndex = destinationOffset + increment = 1 + } else { + srcIndex = sourceOffset + len - 1 + dstIndex = destinationOffset + len - 1 + increment = -1 + } + + repeat(len) { + destination.set(dstIndex, this.get(srcIndex)) + srcIndex += increment + dstIndex += increment + } +} + +private class BufferWithOffset(val buf: WasmCharArray, val off: Int) { operator fun set(addr: Int, value: Char) { - buf[off + addr] = value + buf.set(off + addr, value) } fun memoryCopy(destAddr: Int, srcAddr: Int, len: Int) { val startIdx = off + srcAddr - buf.copyInto(buf, off + destAddr, startIdx, startIdx + len) + buf.copyInto(buf, off + destAddr, startIdx, len) } fun offsetABitMore(anotherOff: Int) = BufferWithOffset(buf, off + anotherOff) diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt index 58ccddf171b..cd71e09f55d 100644 --- a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt +++ b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt @@ -10,12 +10,10 @@ internal const val INT_SIZE_BYTES = 4 internal fun unsafeRawMemoryToChar(addr: Int) = wasm_i32_load16_u(addr).toChar() -internal fun unsafeRawMemoryToCharArray(startAddr: Int, length: Int): CharArray { - val ret = CharArray(length) - for (i in 0 until length) { - ret[i] = unsafeRawMemoryToChar(startAddr + i * CHAR_SIZE_BYTES) - } - return ret +internal fun unsafeRawMemoryToWasmCharArray(startAddr: Int, length: Int): WasmCharArray { + val result = WasmCharArray(length) + result.fill(length) { unsafeRawMemoryToChar(startAddr + it * CHAR_SIZE_BYTES) } + return result } // Returns a pointer into a temporary scratch segment in the raw wasm memory. Aligned by 4. @@ -25,11 +23,13 @@ internal fun unsafeGetScratchRawMemory(sizeBytes: Int): Int = implementedAsIntrinsic // Assumes there is enough space at the destination, fails with wasm trap otherwise. -internal fun unsafeCharArrayToRawMemory(src: CharArray, dstAddr: Int) { +internal fun unsafeWasmCharArrayToRawMemory(src: WasmCharArray, dstAddr: Int) { var curAddr = dstAddr - for (i in src) { - wasm_i32_store16(curAddr, i) + var i = 0 + while (i < src.len()) { + wasm_i32_store16(curAddr, src.get(i)) curAddr += CHAR_SIZE_BYTES + i++ } } diff --git a/libraries/stdlib/wasm/src/kotlin/text/StringBuilderWasm.kt b/libraries/stdlib/wasm/src/kotlin/text/StringBuilderWasm.kt index e6cacd0a48f..b5477b0bf4c 100644 --- a/libraries/stdlib/wasm/src/kotlin/text/StringBuilderWasm.kt +++ b/libraries/stdlib/wasm/src/kotlin/text/StringBuilderWasm.kt @@ -5,6 +5,8 @@ package kotlin.text +import kotlin.wasm.internal.* + internal fun insertString(array: CharArray, distIndex: Int, value: String, sourceIndex: Int, count: Int): Int { var arrayIdx = distIndex var stringIdx = sourceIndex @@ -14,8 +16,11 @@ internal fun insertString(array: CharArray, distIndex: Int, value: String, sourc return count } -internal fun unsafeStringFromCharArray(array: CharArray, start: Int, size: Int): String = - String.unsafeFromCharArray(array.copyOfRange(start, start + size)) +internal fun unsafeStringFromCharArray(array: CharArray, start: Int, size: Int): String { + val copy = WasmCharArray(size) + copy.fill(size) { array[it + start] } + return String.unsafeFromCharArray(copy) +} internal fun insertInt(array: CharArray, start: Int, value: Int): Int { val valueString = value.toString() diff --git a/libraries/stdlib/wasm/src/kotlin/text/StringsWasm.kt b/libraries/stdlib/wasm/src/kotlin/text/StringsWasm.kt index d9ef3544e53..a5703862013 100644 --- a/libraries/stdlib/wasm/src/kotlin/text/StringsWasm.kt +++ b/libraries/stdlib/wasm/src/kotlin/text/StringsWasm.kt @@ -5,6 +5,8 @@ package kotlin.text +import kotlin.wasm.internal.* + /** * Returns the index within this string of the first occurrence of the specified character, starting from the specified offset. */ @@ -70,7 +72,10 @@ public actual fun String(chars: CharArray): String = public actual fun String(chars: CharArray, offset: Int, length: Int): String { if (offset < 0 || length < 0 || offset + length > chars.size) throw IndexOutOfBoundsException() - return String.unsafeFromCharArray(chars.copyOfRange(offset, offset + length)) + + val copy = WasmCharArray(length) + copy.fill(length) { chars[it + offset] } + return String.unsafeFromCharArray(copy) } /** @@ -78,8 +83,11 @@ public actual fun String(chars: CharArray, offset: Int, length: Int): String { */ @SinceKotlin("1.4") @WasExperimental(ExperimentalStdlibApi::class) -public actual fun CharArray.concatToString(): String = - String.unsafeFromCharArray(this.copyOf()) +public actual fun CharArray.concatToString(): String { + val copy = WasmCharArray(this.size) + copy.fill(this.size) { this[it] } + return String.unsafeFromCharArray(copy) +} /** * Concatenates characters in this [CharArray] or its subrange into a String. @@ -95,7 +103,11 @@ public actual fun CharArray.concatToString(): String = @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun CharArray.concatToString(startIndex: Int = 0, endIndex: Int = this.size): String { AbstractList.checkBoundsIndexes(startIndex, endIndex, this.size) - return String.unsafeFromCharArray(this.copyOfRange(startIndex, endIndex)) + + val length = endIndex - startIndex + val copy = WasmCharArray(length) + copy.fill(length) { this[it + startIndex] } + return String.unsafeFromCharArray(copy) } /**