diff --git a/.idea/dictionaries/skuzmich.xml b/.idea/dictionaries/skuzmich.xml
index 04438696854..635f81a6c01 100644
--- a/.idea/dictionaries/skuzmich.xml
+++ b/.idea/dictionaries/skuzmich.xml
@@ -4,6 +4,7 @@
anyref
dataref
ushr
+ wasi
wasm
diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt
index 7b37d256a54..dfc303fda3b 100644
--- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt
+++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt
@@ -211,7 +211,6 @@ class WasmSymbols(
val nullableDoubleIeee754Equals = getInternalFunction("nullableDoubleIeee754Equals")
val unsafeGetScratchRawMemory = getInternalFunction("unsafeGetScratchRawMemory")
- val unsafeGetScratchRawMemorySize = getInternalFunction("unsafeGetScratchRawMemorySize")
val startCoroutineUninterceptedOrReturnIntrinsics =
(0..2).map { getInternalFunction("startCoroutineUninterceptedOrReturnIntrinsic$it") }
diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt
index 8643cada378..b5d1829d399 100644
--- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt
+++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt
@@ -568,15 +568,9 @@ class BodyGenerator(
}
wasmSymbols.unsafeGetScratchRawMemory -> {
- // TODO: This drops size of the allocated segment. Instead we can check that it's in bounds for better error messages.
- body.buildDrop()
body.buildConstI32Symbol(context.scratchMemAddr)
}
- wasmSymbols.unsafeGetScratchRawMemorySize -> {
- body.buildConstI32Symbol(WasmSymbol(context.scratchMemSizeInBytes))
- }
-
wasmSymbols.wasmArrayCopy -> {
val immediate = WasmImmediate.GcType(
context.referenceGcType(call.getTypeArgument(0)!!.getRuntimeClass(irBuiltIns).symbol)
@@ -891,6 +885,8 @@ class BodyGenerator(
WasmImmediate.HeapType(WasmHeapType.Type(getReferenceGcType()))
WasmImmediateKind.TYPE_IDX ->
WasmImmediate.TypeIdx(getReferenceGcType())
+ WasmImmediateKind.MEMORY_IDX ->
+ WasmImmediate.MemoryIdx(0)
else ->
error("Immediate $imm is unsupported")
diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmCompiledModuleFragment.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmCompiledModuleFragment.kt
index 55621c9781a..b3652dd575c 100644
--- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmCompiledModuleFragment.kt
+++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmCompiledModuleFragment.kt
@@ -65,7 +65,6 @@ class WasmCompiledModuleFragment(val irBuiltIns: IrBuiltIns) {
val initFunctions = mutableListOf()
val scratchMemAddr = WasmSymbol()
- val scratchMemSizeInBytes = 65_536
val stringPoolSize = WasmSymbol()
@@ -127,11 +126,8 @@ class WasmCompiledModuleFragment(val irBuiltIns: IrBuiltIns) {
currentDataSectionAddress += typeInfoElement.sizeInBytes
}
- // Reserve some memory to pass complex exported types (like strings). It's going to be accessible through 'unsafeGetScratchRawMemory'
- // runtime call from stdlib.
currentDataSectionAddress = alignUp(currentDataSectionAddress, INT_SIZE_BYTES)
scratchMemAddr.bind(currentDataSectionAddress)
- currentDataSectionAddress += scratchMemSizeInBytes
bind(classIds.unbound, klassIds)
interfaceId.unbound.onEachIndexed { index, entry -> entry.value.bind(index) }
@@ -176,9 +172,10 @@ class WasmCompiledModuleFragment(val irBuiltIns: IrBuiltIns) {
val typeInfoSize = currentDataSectionAddress
val memorySizeInPages = (typeInfoSize / 65_536) + 1
- val memory = WasmMemory(WasmLimits(memorySizeInPages.toUInt(), memorySizeInPages.toUInt()))
+ val memory = WasmMemory(WasmLimits(memorySizeInPages.toUInt(), null /* "unlimited" */))
// Need to export the memory in order to pass complex objects to the host language.
+ // Export name "memory" is a WASI ABI convention.
exports += WasmExport.Memory("memory", memory)
val importedFunctions = functions.elements.filterIsInstance()
diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContext.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContext.kt
index 4553f97cb22..8bc96c804b5 100644
--- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContext.kt
+++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/WasmModuleCodegenContext.kt
@@ -28,9 +28,6 @@ class WasmModuleCodegenContext(
val stringPoolSize: WasmSymbol
get() = wasmFragment.stringPoolSize
- val scratchMemSizeInBytes: Int
- get() = wasmFragment.scratchMemSizeInBytes
-
fun transformType(irType: IrType): WasmType {
return with(typeTransformer) { irType.toWasmValueType() }
}
diff --git a/gradle/versions.properties b/gradle/versions.properties
index 63987763ae5..310ab1de0d8 100644
--- a/gradle/versions.properties
+++ b/gradle/versions.properties
@@ -64,4 +64,4 @@ versions.protobuf-relocated=2.6.1-1
versions.r8=2.2.64
versions.robolectric=4.0
versions.nodejs=18.12.1
-versions.v8=10.9.194
+versions.v8=11.1.31
diff --git a/js/js.translator/testData/box/native/long.kt b/js/js.translator/testData/box/native/long.kt
index 5168c447234..4fd222347e0 100644
--- a/js/js.translator/testData/box/native/long.kt
+++ b/js/js.translator/testData/box/native/long.kt
@@ -1,4 +1,4 @@
-// EXPECTED_REACHABLE_NODES: 1273
+// EXPECTED_REACHABLE_NODES: 1490
// DONT_TARGET_EXACT_BACKEND: WASM
// WASM_MUTE_REASON: UNSUPPORTED_JS_INTEROP
// KJS_WITH_FULL_RUNTIME
@@ -9,7 +9,10 @@ import kotlin.js.Date
fun box(): String {
assertEquals("1970-01-01T00:00:00.000Z", Date(0L).toISOString())
- assertEquals("10/4/1995, 12:00:00 AM", Date(1995, 9, 4, 0, 0, 0, 0L).toLocaleString("en-US"))
+ assertTrue(Date(1995, 9, 4, 0, 0, 0, 0L).toLocaleString("en-US") in listOf(
+ "10/4/1995, 12:00:00 AM",
+ "10/4/1995, 12:00:00 AM" // New v8 uses NNBSP
+ ))
assertEquals(812764800000.0, Date.UTC(1995, 9, 4, 0, 0, 0, 0L))
return "OK"
diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt
index 48306d7697e..d5e4a35b215 100644
--- a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt
+++ b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt
@@ -6,6 +6,8 @@
package kotlin.wasm.internal
import kotlin.wasm.internal.reftypes.anyref
+import kotlin.wasm.unsafe.withScopedMemoryAllocator
+import kotlin.wasm.unsafe.UnsafeWasmMemoryApi
internal external interface ExternalInterfaceType
@@ -188,20 +190,23 @@ internal fun kotlinToJsStringAdapter(x: String?): ExternalInterfaceType? {
val srcArray = x.chars
val stringLength = srcArray.len()
- val maxStringLength = unsafeGetScratchRawMemorySize() / CHAR_SIZE_BYTES
+ val maxStringLength = STRING_INTEROP_MEM_BUFFER_SIZE / CHAR_SIZE_BYTES
- val memBuffer = unsafeGetScratchRawMemory(stringLength.coerceAtMost(maxStringLength) * CHAR_SIZE_BYTES)
+ @OptIn(UnsafeWasmMemoryApi::class)
+ withScopedMemoryAllocator { allocator ->
+ val memBuffer = allocator.allocate(stringLength.coerceAtMost(maxStringLength) * CHAR_SIZE_BYTES).address.toInt()
- var result: ExternalInterfaceType? = null
- var srcStartIndex = 0
- while (srcStartIndex < stringLength - maxStringLength) {
- unsafeWasmCharArrayToRawMemory(srcArray, srcStartIndex, maxStringLength, memBuffer)
- result = importStringFromWasm(memBuffer, maxStringLength, result)
- srcStartIndex += maxStringLength
+ var result: ExternalInterfaceType? = null
+ var srcStartIndex = 0
+ while (srcStartIndex < stringLength - maxStringLength) {
+ unsafeWasmCharArrayToRawMemory(srcArray, srcStartIndex, maxStringLength, memBuffer)
+ result = importStringFromWasm(memBuffer, maxStringLength, result)
+ srcStartIndex += maxStringLength
+ }
+
+ unsafeWasmCharArrayToRawMemory(srcArray, srcStartIndex, stringLength - srcStartIndex, memBuffer)
+ return importStringFromWasm(memBuffer, stringLength - srcStartIndex, result)
}
-
- unsafeWasmCharArrayToRawMemory(srcArray, srcStartIndex, stringLength - srcStartIndex, memBuffer)
- return importStringFromWasm(memBuffer, stringLength - srcStartIndex, result)
}
internal fun jsCheckIsNullOrUndefinedAdapter(x: ExternalInterfaceType?): ExternalInterfaceType? =
@@ -225,25 +230,31 @@ internal fun jsCheckIsNullOrUndefinedAdapter(x: ExternalInterfaceType?): Externa
)
internal external fun jsExportStringToWasm(src: ExternalInterfaceType, srcOffset: Int, srcLength: Int, dstAddr: Int)
+private const val STRING_INTEROP_MEM_BUFFER_SIZE = 65_536 // 1 page 4KiB
+
internal fun jsToKotlinStringAdapter(x: ExternalInterfaceType): String {
val stringLength = stringLength(x)
val dstArray = WasmCharArray(stringLength)
if (stringLength == 0) {
return dstArray.createString()
}
- val maxStringLength = unsafeGetScratchRawMemorySize() / CHAR_SIZE_BYTES
- val memBuffer = unsafeGetScratchRawMemory(stringLength.coerceAtMost(maxStringLength) * CHAR_SIZE_BYTES)
+ @OptIn(UnsafeWasmMemoryApi::class)
+ withScopedMemoryAllocator { allocator ->
+ val maxStringLength = STRING_INTEROP_MEM_BUFFER_SIZE / CHAR_SIZE_BYTES
+ val memBuffer = allocator.allocate(stringLength.coerceAtMost(maxStringLength) * CHAR_SIZE_BYTES).address.toInt()
- var srcStartIndex = 0
- while (srcStartIndex < stringLength - maxStringLength) {
- jsExportStringToWasm(x, srcStartIndex, maxStringLength, memBuffer)
- unsafeRawMemoryToWasmCharArray(memBuffer, srcStartIndex, maxStringLength, dstArray)
- srcStartIndex += maxStringLength
+ var srcStartIndex = 0
+ while (srcStartIndex < stringLength - maxStringLength) {
+ jsExportStringToWasm(x, srcStartIndex, maxStringLength, memBuffer)
+ unsafeRawMemoryToWasmCharArray(memBuffer, srcStartIndex, maxStringLength, dstArray)
+ srcStartIndex += maxStringLength
+ }
+
+ jsExportStringToWasm(x, srcStartIndex, stringLength - srcStartIndex, memBuffer)
+ unsafeRawMemoryToWasmCharArray(memBuffer, srcStartIndex, stringLength - srcStartIndex, dstArray)
}
- jsExportStringToWasm(x, srcStartIndex, stringLength - srcStartIndex, memBuffer)
- unsafeRawMemoryToWasmCharArray(memBuffer, srcStartIndex, stringLength - srcStartIndex, dstArray)
return dstArray.createString()
}
diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt
index d6f69a9a9d9..66be68b8090 100644
--- a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt
+++ b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/Runtime.kt
@@ -19,14 +19,10 @@ internal fun unsafeRawMemoryToWasmCharArray(srcAddr: Int, dstOffset: Int, dstLen
}
}
-// Returns a pointer into a temporary scratch segment in the raw wasm memory. Aligned by 4.
-// Note: currently there is single such segment for a whole wasm module, so use with care.
+// Returns starting address of unused linear memory.
@ExcludedFromCodegen
-internal fun unsafeGetScratchRawMemory(sizeBytes: Int): Int =
- implementedAsIntrinsic
-
-@ExcludedFromCodegen
-internal fun unsafeGetScratchRawMemorySize(): Int =
+@PublishedApi
+internal fun unsafeGetScratchRawMemory(): Int =
implementedAsIntrinsic
// Assumes there is enough space at the destination, fails with wasm trap otherwise.
diff --git a/libraries/stdlib/wasm/src/kotlin/wasm/unsafe/Annotations.kt b/libraries/stdlib/wasm/src/kotlin/wasm/unsafe/Annotations.kt
new file mode 100644
index 00000000000..c32852de077
--- /dev/null
+++ b/libraries/stdlib/wasm/src/kotlin/wasm/unsafe/Annotations.kt
@@ -0,0 +1,18 @@
+/*
+ * 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.wasm.unsafe
+
+import kotlin.annotation.AnnotationTarget.*
+
+/**
+ * This annotation marks APIs for working with unmanaged WebAssembly linear memory.
+ *
+ * Any usage of a declaration annotated with `@UnsafeWasmMemoryApi` must be accepted either by
+ * annotating that usage with the [OptIn] annotation, e.g. `@OptIn(UnsafeWasmMemoryApi::class)`,
+ * or by using the compiler argument `-opt-in=kotlin.wasm.unsafe.UnsafeWasmMemoryApi`.
+ */
+@RequiresOptIn("Unsafe APIs to access to WebAssembly linear memory")
+public annotation class UnsafeWasmMemoryApi
\ No newline at end of file
diff --git a/libraries/stdlib/wasm/src/kotlin/wasm/unsafe/MemoryAccess.kt b/libraries/stdlib/wasm/src/kotlin/wasm/unsafe/MemoryAccess.kt
new file mode 100644
index 00000000000..de4cc3d0733
--- /dev/null
+++ b/libraries/stdlib/wasm/src/kotlin/wasm/unsafe/MemoryAccess.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.wasm.unsafe
+
+import kotlin.wasm.internal.WasmOp
+import kotlin.wasm.internal.implementedAsIntrinsic
+
+/**
+ * Linear memory pointer type.
+ * Corresponds to `i32` type on 32-bit Wasm architecture.
+ */
+@UnsafeWasmMemoryApi
+public value class Pointer public constructor(public val address: UInt) {
+
+ /** Adds an [Int] to the address of this [Pointer] */
+ public operator fun plus(other: Int): Pointer =
+ Pointer(address + other.toUInt())
+
+ /** Subtracts an [Int] from the address of this [Pointer] */
+ public operator fun minus(other: Int): Pointer =
+ Pointer(address - other.toUInt())
+
+ /** Adds an [UInt] to the address of this [Pointer] */
+ public operator fun plus(other: UInt): Pointer =
+ Pointer(address + other)
+
+ /** Subtracts an [UInt] from the address of this [Pointer] */
+ public operator fun minus(other: UInt): Pointer =
+ Pointer(address - other)
+
+ /** Load a Byte (8 bit) value */
+ @WasmOp(WasmOp.I32_LOAD8_S)
+ public fun loadByte(): Byte =
+ implementedAsIntrinsic
+
+ /** Load a Short (16 bit) value */
+ @WasmOp(WasmOp.I32_LOAD16_S)
+ public fun loadShort(): Short =
+ implementedAsIntrinsic
+
+ /** Load an Int (32 bit) value */
+ @WasmOp(WasmOp.I32_LOAD)
+ public fun loadInt(): Int =
+ implementedAsIntrinsic
+
+ /** Load a Long (64 bit) value */
+ @WasmOp(WasmOp.I64_LOAD)
+ public fun loadLong(): Long =
+ implementedAsIntrinsic
+
+ /** Store a Byte (8 bit) [value] */
+ @WasmOp(WasmOp.I32_STORE8)
+ public fun storeByte(value: Byte): Unit =
+ implementedAsIntrinsic
+
+ /** Store a Short (16 bit) [value] */
+ @WasmOp(WasmOp.I32_STORE16)
+ public fun storeShort(value: Short): Unit =
+ implementedAsIntrinsic
+
+ /** Store an Int (32 bit) [value] */
+ @WasmOp(WasmOp.I32_STORE)
+ public fun storeInt(value: Int): Unit =
+ implementedAsIntrinsic
+
+ /** Store a Long (64 bit) [value] */
+ @WasmOp(WasmOp.I64_STORE)
+ public fun storeLong(value: Long): Unit =
+ implementedAsIntrinsic
+}
\ No newline at end of file
diff --git a/libraries/stdlib/wasm/src/kotlin/wasm/unsafe/MemoryAllocation.kt b/libraries/stdlib/wasm/src/kotlin/wasm/unsafe/MemoryAllocation.kt
new file mode 100644
index 00000000000..ec35e6706f5
--- /dev/null
+++ b/libraries/stdlib/wasm/src/kotlin/wasm/unsafe/MemoryAllocation.kt
@@ -0,0 +1,156 @@
+/*
+ * 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.wasm.unsafe
+
+import kotlin.wasm.internal.WasmOp
+import kotlin.wasm.internal.implementedAsIntrinsic
+import kotlin.wasm.internal.unsafeGetScratchRawMemory
+import kotlin.contracts.*
+
+/**
+ * WebAssembly linear memory allocator.
+ */
+@UnsafeWasmMemoryApi
+public abstract class MemoryAllocator {
+ /**
+ * Allocates a block of uninitialized linear memory of the given [size] in bytes.
+ *
+ * @return an address of allocated memory. It is guaranteed to be a multiple of 8.
+ */
+ public abstract fun allocate(size: Int): Pointer
+}
+
+/**
+ * Runs the [block] of code, providing it a temporary [MemoryAllocator] as an argument, and returns the result of this block.
+ *
+ * Frees all memory allocated with the provided allocator after running the [block].
+ *
+ * This function is intened to facilitate the exchange of values with outside world through linear memory.
+ * For example:
+ *
+ * ```
+ * val buffer_size = ...
+ * withScopedMemoryAllocator { allocator ->
+ * val buffer_address = allocator.allocate(buffer_size)
+ * importedWasmFunctionThatWritesToBuffer(buffer_address, buffer_size)
+ * return readDataFromBufferIntoManagedKotlinMemory(buffer_address, buffer_size)
+ * }
+ * ```
+ *
+ * WARNING! Addresses allocated inside the [block] function become invalid after exiting the function.
+ *
+ * WARNING! A nested call to [withScopedMemoryAllocator] will temporarily disable the allocator from the outer scope
+ * for the duration of the call. Calling [MemoryAllocator.allocate] on a disabled allocator
+ * will throw [IllegalStateException].
+ *
+ * WARNING! Accessing the allocator outside of the [block] scope will throw [IllegalStateException].
+ */
+@UnsafeWasmMemoryApi
+public inline fun withScopedMemoryAllocator(
+ block: (allocator: MemoryAllocator) -> T
+): T {
+ contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
+ val allocator = createAllocatorInTheNewScope()
+ val result = try {
+ block(allocator)
+ } finally {
+ allocator.destroy()
+ currentAllocator = allocator.parent
+ }
+ return result
+}
+
+@PublishedApi
+@UnsafeWasmMemoryApi
+internal fun createAllocatorInTheNewScope(): ScopedMemoryAllocator {
+ val allocator = currentAllocator?.createChild() ?:
+ ScopedMemoryAllocator(unsafeGetScratchRawMemory(), parent = null)
+ currentAllocator = allocator
+ return allocator
+}
+
+
+@PublishedApi
+@UnsafeWasmMemoryApi
+internal var currentAllocator: ScopedMemoryAllocator? = null
+
+@PublishedApi
+@UnsafeWasmMemoryApi
+internal class ScopedMemoryAllocator(
+ startAddress: Int,
+ // Allocator from parent scope or null for top-level scope.
+ @PublishedApi
+ internal var parent: ScopedMemoryAllocator?,
+) : MemoryAllocator() {
+ // true if allocator is out of scope
+ private var destroyed = false
+ // true if child allocator is active
+ private var suspended = false
+ // all memory is available starting from this address
+ private var availableAddress: ULong = startAddress.toULong()
+
+ override fun allocate(size: Int): Pointer {
+ check(!destroyed) { "ScopedMemoryAllocator is destroyed when out of scope" }
+ check(!suspended) { "ScopedMemoryAllocator is suspended when nested allocators are used" }
+
+ // Pad available address to align it to 8
+ // 8 is a max alignment number currently needed for Wasm component model canonical ABI
+ val align = 8uL
+ val result = (availableAddress + align - 1uL) and (align - 1uL).inv()
+ check(result % 8uL == 0uL)
+
+ availableAddress = result + size.toULong()
+
+ if (availableAddress > UInt.MAX_VALUE.toULong()) {
+ error("Out of linear memory. All available address space (4gb) is used.")
+ }
+
+ val currentMaxSize = wasmMemorySize().toULong() * WASM_PAGE_SIZE_IN_BYTES.toULong()
+ if (availableAddress >= currentMaxSize) {
+
+ val numPagesToGrow =
+ (availableAddress - currentMaxSize) / WASM_PAGE_SIZE_IN_BYTES.toULong() + 2uL
+
+ if (wasmMemoryGrow(numPagesToGrow.toInt()) == -1) {
+ error("Out of linear memory. memory.grow returned -1")
+ }
+ }
+
+ check(availableAddress < wasmMemorySize().toULong() * WASM_PAGE_SIZE_IN_BYTES.toULong())
+
+ return Pointer(result.toUInt())
+ }
+
+ @PublishedApi
+ internal fun createChild(): ScopedMemoryAllocator {
+ val child = ScopedMemoryAllocator(availableAddress.toInt(), parent = this)
+ suspended = true
+ return child
+ }
+
+ @PublishedApi
+ internal fun destroy() {
+ destroyed = true
+ parent?.suspended = false
+ }
+}
+
+private const val WASM_PAGE_SIZE_IN_BYTES = 65_536 // 64 KiB
+
+/**
+ * Current linear memory size in pages
+ */
+@WasmOp(WasmOp.MEMORY_SIZE)
+internal fun wasmMemorySize(): Int =
+ implementedAsIntrinsic
+
+/**
+ * Grow memory by a given delta (in pages).
+ * Return the previous size, or -1 if enough memory cannot be allocated.
+ */
+@WasmOp(WasmOp.MEMORY_GROW)
+internal fun wasmMemoryGrow(delta: Int): Int =
+ implementedAsIntrinsic
\ No newline at end of file
diff --git a/libraries/stdlib/wasm/test/unsafe/MemoryAccessTest.kt b/libraries/stdlib/wasm/test/unsafe/MemoryAccessTest.kt
new file mode 100644
index 00000000000..7b3e89bc632
--- /dev/null
+++ b/libraries/stdlib/wasm/test/unsafe/MemoryAccessTest.kt
@@ -0,0 +1,184 @@
+package test.wasm.unsafe
+
+import kotlin.wasm.unsafe.*
+import kotlin.test.*
+
+@OptIn(UnsafeWasmMemoryApi::class)
+class MemoryAccessTestTest {
+ @Test
+ fun testPointer() {
+ val p: Pointer = Pointer(19u)
+ assertEquals(p.address, 19u)
+ assertEquals((p + 10u).address, 29u)
+ assertEquals((p - 10u).address, 9u)
+ assertEquals((p + 10).address, 29u)
+ assertEquals((p - 10).address, 9u)
+ }
+
+ fun testLoadStore(values: List, typeSize: Int, store: (Pointer, T) -> Unit, load: (Pointer) -> T) {
+ withScopedMemoryAllocator { a ->
+ // Memory layout: [Long1][T][Long2]
+ val ptrToLong1 = a.allocate(24)
+ val ptrToT = ptrToLong1 + 8
+ val ptrToLong2 = ptrToT + typeSize
+
+ for (x in values) {
+ val prevLong = ptrToLong1.loadLong()
+ val nextLong = ptrToLong2.loadLong()
+
+ store(ptrToT, x)
+ assertEquals(load(ptrToT), x)
+
+ assertEquals(ptrToLong1.loadLong(), prevLong)
+ assertEquals(ptrToLong2.loadLong(), nextLong)
+ }
+ }
+ }
+
+ @Test
+ fun testByte() {
+ val bytes = listOf(0, Byte.MIN_VALUE, Byte.MAX_VALUE, -91, -21, -47, -72, -42, 118, 120, 125, 21, -43)
+ testLoadStore(bytes, 1, { p, v -> p.storeByte(v) }, { it.loadByte() })
+ }
+
+ @Test
+ fun testShort() {
+ val shorts = listOf(
+ 0, Short.MIN_VALUE, Short.MAX_VALUE, 26350, 17667, 5437, 1381,
+ 21183, 26042, -25961, -22913, 9128, -10684
+ )
+ testLoadStore(shorts, 2, { p, v -> p.storeShort(v) }, { it.loadShort() })
+ }
+
+ @Test
+ fun testInt() {
+ val ints = listOf(
+ 0, Int.MIN_VALUE, Int.MAX_VALUE,
+ 1348618689, -299556943, -394977414, -621300994, 1034622853, -1010496662,
+ -2102993550, 199131417, 407819728, -1093382545
+ )
+ testLoadStore(ints, 4, { p, v -> p.storeInt(v) }, { it.loadInt() })
+ }
+
+ @Test
+ fun testLong() {
+ val longs = listOf(
+ 0, Long.MIN_VALUE, Long.MAX_VALUE,
+ 6964777768087685094, -3965399897925814666, 6876207943944046195, 7675081221595661767,
+ -3388229176969119769, 4265730675328983821, -4893379785828386453, -7516879919690485136,
+ 8512965883914804069, 6155050932825287650
+ )
+ testLoadStore(longs, 8, { p, v -> p.storeLong(v) }, { it.loadLong() })
+ }
+
+ @Test
+ fun testAccessingWithDifferentTypes() {
+ withScopedMemoryAllocator { a ->
+ val size = 16
+ val sizeU = size.toUInt()
+ val pointer = a.allocate(size)
+ val addr = pointer.address
+
+ fun fillWith(value: Byte) {
+ for (ptr in addr..,
+ shorts: List,
+ ints: List,
+ longs: List
+ ) {
+ assertEquals(bytes.size, size)
+ assertEquals(shorts.size, size / 2)
+ assertEquals(ints.size, size / 4)
+ assertEquals(longs.size, size / 8)
+ for (i in 0..().also { list ->
+ repeat(size / 2) {
+ // little-endian
+ list += 0xCD.toByte()
+ list += 0xAB.toByte()
+ }
+ },
+ shorts = List(size / 2) { 0xABCDu.toShort() },
+ ints = List(size / 4) { 0xABCDABCDu.toInt() },
+ longs = List(size / 8) { 0xABCDABCDABCDABCDuL.toLong() }
+ )
+ fillWith(0.toInt())
+ checkZero()
+ fillWith(0xFFFFFFFFu.toInt())
+ checkMem(
+ bytes = List(size) { 0xFFu.toByte() },
+ shorts = List(size / 2) { 0xFFFFu.toShort() },
+ ints = List(size / 4) { 0xFFFFFFFFu.toInt() },
+ longs = List(size / 8) { 0xFFFFFFFFFFFFFFFFuL.toLong() }
+ )
+ fillWith(0L)
+ checkZero()
+ fillWith(0x1212121212121212L)
+ checkMem(
+ bytes = List(size) { 0x012 },
+ shorts = List(size / 2) { 0x1212 },
+ ints = List(size / 4) { 0x12121212 },
+ longs = List(size / 8) { 0x1212121212121212L }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/libraries/stdlib/wasm/test/unsafe/MemoryAllocationTest.kt b/libraries/stdlib/wasm/test/unsafe/MemoryAllocationTest.kt
new file mode 100644
index 00000000000..94836072550
--- /dev/null
+++ b/libraries/stdlib/wasm/test/unsafe/MemoryAllocationTest.kt
@@ -0,0 +1,217 @@
+package test.wasm.unsafe
+
+import kotlin.wasm.unsafe.*
+import kotlin.test.*
+
+@JsFun("(a, b) => a + b")
+private external fun jsConcatStrings(a: String, b: String): String
+
+@OptIn(UnsafeWasmMemoryApi::class)
+class MemoryAllocationTest {
+ val pageSize = 65_536
+
+ @Test
+ fun testWasmMemorySizeGrow() {
+ val s1 = wasmMemorySize()
+ val grow_res = wasmMemoryGrow(10)
+ val s2 = wasmMemorySize()
+ assertNotEquals(grow_res, -1)
+ assertEquals(grow_res, s1)
+ assertEquals(s2 - s1, 10)
+ }
+
+ @Test
+ fun testScopedAllocator() {
+ val sizes = listOf(1, 1, 2, 3, 8, 10, 305, 12_747, 31_999)
+ val allocations = mutableListOf()
+ withScopedMemoryAllocator { a ->
+ for (size in sizes) {
+ allocations += a.allocate(size)
+ }
+ }
+
+ val allocations2 = mutableListOf()
+ withScopedMemoryAllocator { a ->
+ for (size in sizes) {
+ allocations2 += a.allocate(size)
+ }
+ }
+
+ // Test that we run withScopedMemoryAllocator body
+ assertEquals(allocations.size, sizes.size)
+
+ // Memory should be reusing in different scopes
+ // NOTE: This is current impl detail and can be changed
+ assertEquals(allocations, allocations2)
+
+ // Allocations are aligned
+ assertTrue(allocations.all { it.address % 8u == 0u })
+
+ // Allocations are different
+ assertTrue(allocations.distinct().size == allocations.size)
+
+ // Allocations do not intersect
+ for (i1 in 0..(wasmMemorySize())
+ withScopedMemoryAllocator { a ->
+ var allocatedAddress = a.allocate(pageSize)
+ var allocationSize = pageSize
+
+ repeat(10) {
+ var currPagesUsed = (allocatedAddress.address.toInt() + allocationSize + 1) / pageSize
+ var currPagesAvailable = wasmMemorySize()
+ assertTrue(currPagesAvailable > currPagesUsed)
+ // Allocate 10 pages past max page
+ allocationSize = (currPagesAvailable - currPagesUsed + 10) * pageSize
+ allocatedAddress = a.allocate(allocationSize)
+ memSizes += wasmMemorySize()
+ }
+ }
+ assertTrue(memSizes.size == 11)
+ assertEquals(memSizes.distinct(), memSizes)
+ assertEquals(memSizes.sorted(), memSizes)
+ }
+
+ @Test
+ fun nestedAllocators() {
+ val sizes = listOf(1, 1, 2, 3, 8, 10, 305, 12_747, 31_999)
+ var allocations1: List
+ var allocations1_1: List
+ var allocations1_2: List
+ var allocations2: List
+
+ withScopedMemoryAllocator { allocator0 ->
+ allocations1 = sizes.map { size -> allocator0.allocate(size) }
+ withScopedMemoryAllocator { allocator0_0 ->
+ allocations1_1 = sizes.map { size -> allocator0_0.allocate(size) }
+ }
+ withScopedMemoryAllocator { allocator0_1 ->
+ allocations1_2 = sizes.map { size -> allocator0_1.allocate(size) }
+ }
+ }
+ withScopedMemoryAllocator { allocator0 ->
+ allocations2 = sizes.map { size -> allocator0.allocate(size) }
+ }
+
+ // Check that all allocations happened and distinct
+ listOf(
+ allocations1,
+ allocations1_1,
+ allocations1_2,
+ allocations2
+ ).forEach { allocation ->
+ assertEquals(allocation.size, sizes.size)
+ assertEquals(allocation.distinct().size, allocation.size)
+ }
+
+ // Impl detail: sibling scopes use the same memory
+ assertEquals(allocations1, allocations2)
+ assertEquals(allocations1_1, allocations1_2)
+
+ // Impl detaiol: allocator in child scope allocates new memory
+ val max1: UInt = allocations1.maxOf { it.address }
+ val min1_1: UInt = allocations1_1.minOf { it.address }
+ assertTrue(max1 < min1_1)
+ }
+
+ @Test
+ fun testJsIntropInsideAllocations() {
+ withScopedMemoryAllocator { allocator ->
+ assertEquals(jsConcatStrings("str1", "str2"), "str1str2")
+ allocator.allocate(10)
+ }
+ }
+
+ @Test
+ fun testNestedAllocatorThrows() {
+ var leakedAllocator1: MemoryAllocator? = null
+ var leakedAllocator2: MemoryAllocator? = null
+ var leakedAllocator3: MemoryAllocator? = null
+
+ withScopedMemoryAllocator { allocator1 ->
+ leakedAllocator1 = allocator1
+ allocator1.allocate(100)
+ // 2-level nesting
+ withScopedMemoryAllocator { allocator2 ->
+ leakedAllocator2 = allocator2
+ allocator2.allocate(100)
+ assertFailsWith {
+ allocator1.allocate(100)
+ }
+ // 3-level nesting
+ withScopedMemoryAllocator { allocator3 ->
+ leakedAllocator3 = allocator3
+ allocator3.allocate(100)
+ assertFailsWith {
+ allocator1.allocate(100)
+ }
+ assertFailsWith {
+ allocator2.allocate(100)
+ }
+ allocator3.allocate(100)
+ }
+ assertFailsWith {
+ leakedAllocator3?.allocate(100)
+ }
+ // now it is legal to use allocator1 since we're in its immediate scope
+ allocator2.allocate(100)
+ }
+ assertFailsWith {
+ leakedAllocator2?.allocate(100)
+ }
+ allocator1.allocate(100)
+ }
+
+ for (leakedAllocator in listOf(leakedAllocator1, leakedAllocator2, leakedAllocator3)) {
+ assertFailsWith {
+ leakedAllocator?.allocate(100)
+ }
+ }
+ }
+
+ @Test
+ fun testScopedAllocatorThrows() {
+ assertFailsWith {
+ var leakedAllocator: MemoryAllocator? = null
+ withScopedMemoryAllocator { allocator ->
+ leakedAllocator = allocator
+ }
+ leakedAllocator?.allocate(10)
+ }
+
+ assertFailsWith {
+ var leakedAllocator: MemoryAllocator? = null
+ try {
+ withScopedMemoryAllocator { allocator ->
+ leakedAllocator = allocator
+ throw Error()
+ }
+ } catch (e: Throwable) {
+ }
+ leakedAllocator?.allocate(10)
+ }
+
+ assertFailsWith {
+ fun foo(): MemoryAllocator {
+ withScopedMemoryAllocator { allocator ->
+ return allocator // non-local return
+ }
+ }
+ foo().allocate(10)
+ }
+ }
+}
\ No newline at end of file
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-wasm-test/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-wasm-test/build.gradle.kts
index d9cc0134725..9dce6a98e69 100644
--- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-wasm-test/build.gradle.kts
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/new-mpp-wasm-test/build.gradle.kts
@@ -14,7 +14,9 @@ with(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.apply(rootPr
}
with(org.jetbrains.kotlin.gradle.targets.js.d8.D8RootPlugin.apply(rootProject)) {
- version = "10.9.194"
+ // Test that we can set the version and it is a String.
+ // But use the default version since update this place every time anyway.
+ version = (version as String)
}
with(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin.apply(rootProject)) {
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8RootExtension.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8RootExtension.kt
index 78fdc2e493e..d4399b40351 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8RootExtension.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/d8/D8RootExtension.kt
@@ -41,7 +41,7 @@ open class D8RootExtension(@Transient val rootProject: Project) : ConfigurationP
fi;
done;
*/
- var version by Property("10.9.194")
+ var version by Property("11.1.31")
var edition by Property("rel") // rel or dbg
val setupTaskProvider: TaskProvider
diff --git a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/Operators.kt b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/Operators.kt
index 7810ef26614..8499010054c 100644
--- a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/Operators.kt
+++ b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/Operators.kt
@@ -69,9 +69,7 @@ sealed class WasmImmediate {
class ValTypeVector(val value: List) : WasmImmediate()
- class MemoryIdx(val value: WasmSymbol) : WasmImmediate() {
- constructor(value: WasmMemory) : this(WasmSymbol(value))
- }
+ class MemoryIdx(val value: Int) : WasmImmediate()
class DataIdx(val value: WasmSymbol) : WasmImmediate() {
constructor(value: Int) : this(WasmSymbol(value))
diff --git a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmBinaryToIR.kt b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmBinaryToIR.kt
index f05904e1363..cbb29e9a0f4 100644
--- a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmBinaryToIR.kt
+++ b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmBinaryToIR.kt
@@ -425,7 +425,7 @@ class WasmBinaryToIR(val b: MyByteReader) {
WasmImmediateKind.LOCAL_IDX -> WasmImmediate.LocalIdx(locals[b.readVarUInt32AsInt()])
WasmImmediateKind.GLOBAL_IDX -> WasmImmediate.GlobalIdx(globalByIdx(b.readVarUInt32AsInt()))
WasmImmediateKind.TYPE_IDX -> WasmImmediate.TypeIdx(functionTypes[b.readVarUInt32AsInt()])
- WasmImmediateKind.MEMORY_IDX -> WasmImmediate.MemoryIdx(memoryByIdx(b.readVarUInt32AsInt()))
+ WasmImmediateKind.MEMORY_IDX -> WasmImmediate.MemoryIdx(b.readVarUInt32AsInt())
WasmImmediateKind.DATA_IDX -> WasmImmediate.DataIdx(b.readVarUInt32AsInt())
WasmImmediateKind.TABLE_IDX -> WasmImmediate.TableIdx(b.readVarUInt32AsInt())
WasmImmediateKind.LABEL_IDX -> WasmImmediate.LabelIdx(b.readVarUInt32AsInt())
diff --git a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToBinary.kt b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToBinary.kt
index 803d01b7e02..564695a2094 100644
--- a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToBinary.kt
+++ b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToBinary.kt
@@ -243,7 +243,7 @@ class WasmIrToBinary(
is WasmImmediate.LocalIdx -> appendLocalReference(x.value.owner)
is WasmImmediate.GlobalIdx -> appendModuleFieldReference(x.value.owner)
is WasmImmediate.TypeIdx -> appendModuleFieldReference(x.value.owner)
- is WasmImmediate.MemoryIdx -> appendModuleFieldReference(x.value.owner)
+ is WasmImmediate.MemoryIdx -> b.writeVarUInt32(x.value)
is WasmImmediate.DataIdx -> b.writeVarUInt32(x.value.owner)
is WasmImmediate.TableIdx -> b.writeVarUInt32(x.value.owner)
is WasmImmediate.LabelIdx -> b.writeVarUInt32(x.value)
diff --git a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToText.kt b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToText.kt
index 1a83d0d6460..6e349301f98 100644
--- a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToText.kt
+++ b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToText.kt
@@ -124,7 +124,7 @@ class WasmIrToText : SExpressionBuilder() {
is WasmImmediate.LocalIdx -> appendLocalReference(x.value.owner)
is WasmImmediate.GlobalIdx -> appendModuleFieldReference(x.value.owner)
is WasmImmediate.TypeIdx -> sameLineList("type") { appendModuleFieldReference(x.value.owner) }
- is WasmImmediate.MemoryIdx -> appendModuleFieldIdIfNotNull(x.value.owner)
+ is WasmImmediate.MemoryIdx -> appendIdxIfNotZero(x.value)
is WasmImmediate.DataIdx -> appendElement(x.value.toString())
is WasmImmediate.TableIdx -> appendElement(x.value.toString())
is WasmImmediate.LabelIdx -> appendElement(x.value.toString())
@@ -523,9 +523,7 @@ class WasmIrToText : SExpressionBuilder() {
appendElement("$${local.id}_${sanitizeWatIdentifier(local.name)}")
}
- fun appendModuleFieldIdIfNotNull(field: WasmNamedModuleField) {
- val id = field.id
- ?: error("${field::class} ${field.name} ID is unlinked")
+ fun appendIdxIfNotZero(id: Int) {
if (id != 0) appendElement(id.toString())
}