Add Channel primitive for coroutine inter-task communication
Exposes Kotlin's Channel<Obj> to Lyng scripts as a first-class type with rendezvous, buffered, and unlimited capacity modes. Supports suspending send/receive, non-suspending tryReceive, close/drain semantics, and isClosedForSend/isClosedForReceive properties. Also fixes a pre-existing typo in BookTest.kt that blocked JVM test compilation, and adds Channel reference docs and a parallelism.md section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
30e56946a0
commit
f9a07f176a
6 files changed
+529
-1
No files matched your search
@@ -634,6 +634,7 @@ class Script(
|
||||
addConst("Deferred", ObjDeferred.type)
|
||||
addConst("CompletableDeferred", ObjCompletableDeferred.type)
|
||||
addConst("Mutex", ObjMutex.type)
|
||||
addConst("Channel", ObjChannel.type)
|
||||
addConst("Flow", ObjFlow.type)
|
||||
addConst("FlowBuilder", ObjFlowBuilder.type)
|
||||
addConst("Delegate", ObjDynamic.type)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
||||
import kotlinx.coroutines.channels.ClosedSendChannelException
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.miniast.ParamDoc
|
||||
import net.sergeych.lyng.miniast.addFnDoc
|
||||
import net.sergeych.lyng.miniast.addPropertyDoc
|
||||
import net.sergeych.lyng.miniast.type
|
||||
|
||||
/**
|
||||
* Lyng-visible wrapper around Kotlin's [Channel].
|
||||
*
|
||||
* Construction:
|
||||
* - `Channel()` – rendezvous channel (capacity 0, sender and receiver synchronise)
|
||||
* - `Channel(n)` – buffered channel with capacity *n* (n > 0)
|
||||
* - `Channel(Channel.UNLIMITED)` – unlimited-buffer channel
|
||||
*
|
||||
* Methods:
|
||||
* - `send(value)` – suspend until the value is accepted by a receiver (or buffered)
|
||||
* - `receive()` – suspend until a value is available; returns `null` when channel is closed and drained
|
||||
* - `tryReceive()` – return the next value immediately, or `null` if none is available
|
||||
* - `close()` – signal that no more values will be sent; pending receivers can still drain buffered items
|
||||
*
|
||||
* Properties:
|
||||
* - `isClosedForSend: Bool`
|
||||
* - `isClosedForReceive: Bool`
|
||||
*/
|
||||
class ObjChannel(val channel: Channel<Obj>) : Obj() {
|
||||
|
||||
override val objClass get() = type
|
||||
|
||||
companion object {
|
||||
val type = object : ObjClass("Channel") {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val capacity = scope.args.list.getOrNull(0)
|
||||
?.let { (it as? ObjInt)?.value?.toInt() ?: scope.raiseIllegalArgument("Channel capacity must be an integer") }
|
||||
?: Channel.RENDEZVOUS
|
||||
return ObjChannel(Channel(capacity))
|
||||
}
|
||||
}.apply {
|
||||
// Expose Channel.UNLIMITED as a static constant on the Channel class so scripts can write
|
||||
// Channel(Channel.UNLIMITED).
|
||||
addConst("UNLIMITED", ObjInt(Channel.UNLIMITED.toLong()))
|
||||
|
||||
addFnDoc(
|
||||
name = "send",
|
||||
doc = "Suspend until the value is accepted by a receiver (or placed into the buffer). " +
|
||||
"Throws if the channel is already closed.",
|
||||
params = listOf(ParamDoc("value", type("lyng.Any"))),
|
||||
returns = type("lyng.Void"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
val value = requiredArg<Obj>(0)
|
||||
try {
|
||||
thisAs<ObjChannel>().channel.send(value)
|
||||
} catch (e: ClosedSendChannelException) {
|
||||
raiseIllegalState("Channel is closed for send")
|
||||
}
|
||||
ObjVoid
|
||||
}
|
||||
|
||||
addFnDoc(
|
||||
name = "receive",
|
||||
doc = "Suspend until a value is available and return it, or return `null` when the channel " +
|
||||
"is closed and all buffered items have been consumed.",
|
||||
returns = type("lyng.Any"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
try {
|
||||
thisAs<ObjChannel>().channel.receive()
|
||||
} catch (_: ClosedReceiveChannelException) {
|
||||
ObjNull
|
||||
}
|
||||
}
|
||||
|
||||
addFnDoc(
|
||||
name = "tryReceive",
|
||||
doc = "Return the next buffered value immediately without suspending, or `null` if the " +
|
||||
"channel is empty or closed.",
|
||||
returns = type("lyng.Any"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
val result = thisAs<ObjChannel>().channel.tryReceive()
|
||||
result.getOrNull() ?: ObjNull
|
||||
}
|
||||
|
||||
addFnDoc(
|
||||
name = "close",
|
||||
doc = "Signal that no more values will be sent. Receivers can still drain any buffered items.",
|
||||
returns = type("lyng.Void"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) {
|
||||
thisAs<ObjChannel>().channel.close()
|
||||
ObjVoid
|
||||
}
|
||||
|
||||
addPropertyDoc(
|
||||
name = "isClosedForSend",
|
||||
doc = "Whether this channel is closed for sending (no more `send` calls are permitted).",
|
||||
type = type("lyng.Bool"),
|
||||
moduleName = "lyng.stdlib",
|
||||
getter = { thisAs<ObjChannel>().channel.isClosedForSend.toObj() }
|
||||
)
|
||||
|
||||
addPropertyDoc(
|
||||
name = "isClosedForReceive",
|
||||
doc = "Whether this channel is closed for receiving (closed and fully drained).",
|
||||
type = type("lyng.Bool"),
|
||||
moduleName = "lyng.stdlib",
|
||||
getter = { thisAs<ObjChannel>().channel.isClosedForReceive.toObj() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.eval
|
||||
import kotlin.test.Test
|
||||
|
||||
class ChannelTest {
|
||||
|
||||
@Test
|
||||
fun testRendezvousChannel() = runTest {
|
||||
eval("""
|
||||
val ch = Channel()
|
||||
val producer = launch {
|
||||
ch.send(1)
|
||||
ch.send(2)
|
||||
ch.send(3)
|
||||
ch.close()
|
||||
}
|
||||
val results = []
|
||||
var v = ch.receive()
|
||||
while (v != null) {
|
||||
results += v
|
||||
v = ch.receive()
|
||||
}
|
||||
assertEquals([1, 2, 3], results)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBufferedChannel() = runTest {
|
||||
eval("""
|
||||
val ch = Channel(4)
|
||||
ch.send(10)
|
||||
ch.send(20)
|
||||
ch.send(30)
|
||||
ch.close()
|
||||
assertEquals(10, ch.receive())
|
||||
assertEquals(20, ch.receive())
|
||||
assertEquals(30, ch.receive())
|
||||
// closed and drained
|
||||
assertEquals(null, ch.receive())
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTryReceive() = runTest {
|
||||
eval("""
|
||||
val ch = Channel(2)
|
||||
// nothing buffered yet
|
||||
assertEquals(null, ch.tryReceive())
|
||||
ch.send(42)
|
||||
assertEquals(42, ch.tryReceive())
|
||||
assertEquals(null, ch.tryReceive())
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChannelProperties() = runTest {
|
||||
eval("""
|
||||
val ch = Channel(1)
|
||||
assert(!ch.isClosedForSend)
|
||||
assert(!ch.isClosedForReceive)
|
||||
ch.send(1)
|
||||
ch.close()
|
||||
assert(ch.isClosedForSend)
|
||||
// still has one buffered item, so not yet closed for receive
|
||||
assert(!ch.isClosedForReceive)
|
||||
ch.receive()
|
||||
// now fully drained
|
||||
assert(ch.isClosedForReceive)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnlimitedChannel() = runTest {
|
||||
eval("""
|
||||
val ch = Channel(Channel.UNLIMITED)
|
||||
(1..100).forEach { ch.send(it) }
|
||||
ch.close()
|
||||
var sum = 0
|
||||
var v = ch.receive()
|
||||
while (v != null) {
|
||||
sum += v
|
||||
v = ch.receive()
|
||||
}
|
||||
assertEquals(5050, sum)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChannelProducerConsumerPattern() = runTest {
|
||||
eval("""
|
||||
val ch = Channel()
|
||||
val results = []
|
||||
val mu = Mutex()
|
||||
|
||||
val consumer = launch {
|
||||
var item = ch.receive()
|
||||
while (item != null) {
|
||||
mu.withLock { results += item }
|
||||
item = ch.receive()
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
for (i in 1..5) {
|
||||
ch.send("msg:${'$'}i")
|
||||
}
|
||||
ch.close()
|
||||
}.await()
|
||||
|
||||
consumer.await()
|
||||
assertEquals(["msg:1","msg:2","msg:3","msg:4","msg:5"], results)
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,7 @@ suspend fun runDocTests(fileName: String, bookMode: Boolean = false) {
|
||||
val bookScope = Scope()
|
||||
var count = 0
|
||||
parseDocTests(fileName, bookMode).collect { dt ->
|
||||
if (bookMode)imp dt.test(bookScope)
|
||||
if (bookMode) dt.test(bookScope)
|
||||
else dt.test()
|
||||
count++
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user