Skip to content

Commit 7bb0638

Browse files
authored
Ensure that non-local returns work properly in inline functions (Kotlin#3986)
Also, ensure that the workaround for KT-58685 still works. It was enough to move the `return` a tiny bit to fix the issue. Fixes Kotlin#3985
1 parent b05e15e commit 7bb0638

File tree

5 files changed

+161
-19
lines changed

5 files changed

+161
-19
lines changed

kotlinx-coroutines-core/common/src/sync/Mutex.kt

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,19 +121,12 @@ public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T
121121
contract {
122122
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
123123
}
124-
125-
// Cannot use 'finally' in this function because of KT-58685
126-
// See kotlinx.coroutines.sync.MutexTest.testWithLockJsMiscompilation
127-
128124
lock(owner)
129-
val result = try {
125+
return try {
130126
action()
131-
} catch (e: Throwable) {
127+
} finally {
132128
unlock(owner)
133-
throw e
134129
}
135-
unlock(owner)
136-
return result
137130
}
138131

139132

kotlinx-coroutines-core/common/src/sync/Semaphore.kt

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,12 @@ public suspend inline fun <T> Semaphore.withPermit(action: () -> T): T {
8282
contract {
8383
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
8484
}
85-
86-
// Cannot use 'finally' in this function because of KT-58685
87-
// See kotlinx.coroutines.sync.SemaphoreTest.testWithPermitJsMiscompilation
88-
8985
acquire()
90-
val result = try {
86+
return try {
9187
action()
92-
} catch (e: Throwable) {
88+
} finally {
9389
release()
94-
throw e
9590
}
96-
release()
97-
return result
9891
}
9992

10093
@Suppress("UNCHECKED_CAST")
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2016-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:OptIn(DelicateCoroutinesApi::class)
6+
package kotlinx.coroutines.channels
7+
8+
import kotlinx.coroutines.*
9+
import kotlin.test.*
10+
11+
class ConsumeTest: TestBase() {
12+
13+
/** Check that [ReceiveChannel.consume] does not suffer from KT-58685 */
14+
@Test
15+
fun testConsumeJsMiscompilation() = runTest {
16+
val channel = Channel<Int>()
17+
assertFailsWith<IndexOutOfBoundsException> {
18+
try {
19+
channel.consume { null } ?: throw IndexOutOfBoundsException() // should throw…
20+
} catch (e: Exception) {
21+
throw e // …but instead fails here
22+
}
23+
}
24+
}
25+
26+
/** Checks that [ReceiveChannel.consume] closes the channel when the block executes successfully. */
27+
@Test
28+
fun testConsumeClosesOnSuccess() = runTest {
29+
val channel = Channel<Int>()
30+
channel.consume { }
31+
assertTrue(channel.isClosedForReceive)
32+
}
33+
34+
/** Checks that [ReceiveChannel.consume] closes the channel when the block executes successfully. */
35+
@Test
36+
fun testConsumeClosesOnFailure() = runTest {
37+
val channel = Channel<Int>()
38+
try {
39+
channel.consume { throw TestException() }
40+
} catch (e: TestException) {
41+
// Expected
42+
}
43+
assertTrue(channel.isClosedForReceive)
44+
}
45+
46+
/** Checks that [ReceiveChannel.consume] closes the channel when the block does an early return. */
47+
@Test
48+
fun testConsumeClosesOnEarlyReturn() = runTest {
49+
val channel = Channel<Int>()
50+
fun f() {
51+
try {
52+
channel.consume { return }
53+
} catch (e: TestException) {
54+
// Expected
55+
}
56+
}
57+
f()
58+
assertTrue(channel.isClosedForReceive)
59+
}
60+
61+
/** Checks that [ReceiveChannel.consume] closes the channel when the block executes successfully. */
62+
@Test
63+
fun testConsumeEachClosesOnSuccess() = runTest {
64+
val channel = Channel<Int>(Channel.UNLIMITED)
65+
launch { channel.close() }
66+
channel.consumeEach { fail("unreached") }
67+
assertTrue(channel.isClosedForReceive)
68+
}
69+
70+
/** Checks that [ReceiveChannel.consume] closes the channel when the block executes successfully. */
71+
@Test
72+
fun testConsumeEachClosesOnFailure() = runTest {
73+
val channel = Channel<Unit>(Channel.UNLIMITED)
74+
channel.send(Unit)
75+
try {
76+
channel.consumeEach { throw TestException() }
77+
} catch (e: TestException) {
78+
// Expected
79+
}
80+
assertTrue(channel.isClosedForReceive)
81+
}
82+
83+
/** Checks that [ReceiveChannel.consume] closes the channel when the block does an early return. */
84+
@Test
85+
fun testConsumeEachClosesOnEarlyReturn() = runTest {
86+
val channel = Channel<Unit>(Channel.UNLIMITED)
87+
channel.send(Unit)
88+
suspend fun f() {
89+
channel.consumeEach {
90+
return@f
91+
}
92+
}
93+
f()
94+
assertTrue(channel.isClosedForReceive)
95+
}
96+
97+
/** Check that [BroadcastChannel.consume] does not suffer from KT-58685 */
98+
@OptIn(ObsoleteCoroutinesApi::class)
99+
@Suppress("DEPRECATION")
100+
@Test
101+
fun testBroadcastChannelConsumeJsMiscompilation() = runTest {
102+
val channel = BroadcastChannel<Int>(1)
103+
assertFailsWith<IndexOutOfBoundsException> {
104+
try {
105+
channel.consume { null } ?: throw IndexOutOfBoundsException() // should throw…
106+
} catch (e: Exception) {
107+
throw e // …but instead fails here
108+
}
109+
}
110+
}
111+
}

kotlinx-coroutines-core/common/test/sync/MutexTest.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,27 @@ class MutexTest : TestBase() {
6666
try {
6767
mutex.withLock {
6868
expect(1)
69+
assertTrue(mutex.isLocked)
6970
throw TestException()
7071
}
7172
} catch (e: TestException) {
72-
finish(2)
73+
expect(2)
74+
}
75+
assertFalse(mutex.isLocked)
76+
finish(3)
77+
}
78+
79+
@Test
80+
fun withLockOnEarlyReturnTest() = runTest {
81+
val mutex = Mutex()
82+
assertFalse(mutex.isLocked)
83+
suspend fun f() {
84+
mutex.withLock {
85+
assertTrue(mutex.isLocked)
86+
return@f
87+
}
7388
}
89+
f()
7490
assertFalse(mutex.isLocked)
7591
}
7692

kotlinx-coroutines-core/common/test/sync/SemaphoreTest.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,35 @@ class SemaphoreTest : TestBase() {
6969
assertEquals(1, semaphore.availablePermits)
7070
}
7171

72+
@Test
73+
fun withSemaphoreOnFailureTest() = runTest {
74+
val semaphore = Semaphore(1)
75+
assertEquals(1, semaphore.availablePermits)
76+
try {
77+
semaphore.withPermit {
78+
assertEquals(0, semaphore.availablePermits)
79+
throw TestException()
80+
}
81+
} catch (e: TestException) {
82+
// Expected
83+
}
84+
assertEquals(1, semaphore.availablePermits)
85+
}
86+
87+
@Test
88+
fun withSemaphoreOnEarlyReturnTest() = runTest {
89+
val semaphore = Semaphore(1)
90+
assertEquals(1, semaphore.availablePermits)
91+
suspend fun f() {
92+
semaphore.withPermit {
93+
assertEquals(0, semaphore.availablePermits)
94+
return@f
95+
}
96+
}
97+
f()
98+
assertEquals(1, semaphore.availablePermits)
99+
}
100+
72101
@Test
73102
fun fairnessTest() = runTest {
74103
val semaphore = Semaphore(1)

0 commit comments

Comments
 (0)