Add context receiver extensions for DSLs

This commit is contained in:
sergeych committed 2026-04-29 20:49:50 +03:00
1 parent e107296bca
commit b2200e71ff
22 files changed
+860 -71

No files matched your search

+18 -14
View File
@@ -15,17 +15,21 @@
*
*/
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlin.test.Test
import net.sergeych.lyng.eval as lyngEval
class LaunchPoolTest {
private suspend fun eval(code: String) = withTimeout(2_000L) { lyngEval(code) }
private suspend fun eval(code: String) = withContext(Dispatchers.Default) {
withTimeout(2_000L) { lyngEval(code) }
}
@Test
fun testBasicExecution() = runBlocking<Unit> {
fun testBasicExecution() = runTest {
eval("""
val pool = LaunchPool(2)
val d1 = pool.launch { 1 + 1 }
@@ -37,7 +41,7 @@ class LaunchPoolTest {
}
@Test
fun testResultsCollected() = runBlocking<Unit> {
fun testResultsCollected() = runTest {
eval("""
val pool = LaunchPool(4)
val jobs = (1..10).map { n -> pool.launch { n * n } }
@@ -48,7 +52,7 @@ class LaunchPoolTest {
}
@Test
fun testConcurrencyLimit() = runBlocking<Unit> {
fun testConcurrencyLimit() = runTest {
eval("""
// With maxWorkers=2, at most 2 tasks run at the same time.
val mu = Mutex()
@@ -70,7 +74,7 @@ class LaunchPoolTest {
}
@Test
fun testExceptionCapturedInDeferred() = runBlocking<Unit> {
fun testExceptionCapturedInDeferred() = runTest {
eval("""
val pool = LaunchPool(2)
val good = pool.launch { 42 }
@@ -83,7 +87,7 @@ class LaunchPoolTest {
}
@Test
fun testPoolContinuesAfterLambdaException() = runBlocking<Unit> {
fun testPoolContinuesAfterLambdaException() = runTest {
eval("""
val pool = LaunchPool(1)
val bad = pool.launch { throw IllegalArgumentException("fail") }
@@ -96,7 +100,7 @@ class LaunchPoolTest {
}
@Test
fun testLaunchAfterCloseAndJoinThrows() = runBlocking<Unit> {
fun testLaunchAfterCloseAndJoinThrows() = runTest {
eval("""
val pool = LaunchPool(2)
pool.launch { 1 }
@@ -107,7 +111,7 @@ class LaunchPoolTest {
}
@Test
fun testLaunchAfterCancelThrows() = runBlocking<Unit> {
fun testLaunchAfterCancelThrows() = runTest {
eval("""
val pool = LaunchPool(2)
pool.cancel()
@@ -117,7 +121,7 @@ class LaunchPoolTest {
}
@Test
fun testCancelAndJoinWaitsForWorkers() = runBlocking<Unit> {
fun testCancelAndJoinWaitsForWorkers() = runTest {
eval("""
val pool = LaunchPool(2)
pool.launch { delay(5) }
@@ -128,7 +132,7 @@ class LaunchPoolTest {
}
@Test
fun testCloseAndJoinDrainsQueue() = runBlocking<Unit> {
fun testCloseAndJoinDrainsQueue() = runTest {
eval("""
val mu = Mutex()
val results = []
@@ -147,7 +151,7 @@ class LaunchPoolTest {
}
@Test
fun testBoundedQueueSuspendsProducer() = runBlocking<Unit> {
fun testBoundedQueueSuspendsProducer() = runTest {
eval("""
// queue of 2 + 1 worker; producer can only be 1 ahead of what's running
val pool = LaunchPool(1, 2)
@@ -165,7 +169,7 @@ class LaunchPoolTest {
}
@Test
fun testUnlimitedQueueDefault() = runBlocking<Unit> {
fun testUnlimitedQueueDefault() = runTest {
eval("""
val pool = LaunchPool(4)
val jobs = (1..50).map { n -> pool.launch { n } }
@@ -177,7 +181,7 @@ class LaunchPoolTest {
}
@Test
fun testIdempotentClose() = runBlocking<Unit> {
fun testIdempotentClose() = runTest {
eval("""
val pool = LaunchPool(2)
pool.closeAndJoin()
@@ -190,4 +190,63 @@ class ScriptImportPreparationTest {
session.cancelAndJoin()
}
}
@Test
fun importedContextReceiverExtensionIsAvailableInReceiverDsl() = runTest {
val manager = Script.defaultImportManager.copy().apply {
addTextPackages(
"""
package imported.ctxdsl
class Tag(name: String) {
val name = name
var inner = ""
fun child(tagName: String, block: Tag.()->void) {
val child = Tag(tagName)
child.apply { block(this) }
inner += child.render()
}
fun h3(block: Tag.()->void) { child("h3", block) }
fun addText(text: String) { inner += text }
fun render() = "<" + name + ">" + inner + "</" + name + ">"
}
context(Tag)
fun String.unaryPlus() {
this@Tag.addText(this)
}
""".trimIndent()
)
}
val script = Compiler.compile(
Source(
"<ctx-dsl-import>",
"""
import imported.ctxdsl
fun html(block: Tag.()->void) {
val root = Tag("html")
root.apply { block(this) }
root.render()
}
val page = html {
h3 {
+"Imported"
}
}
assertEquals("<html><h3>Imported</h3></html>", page)
assertEquals("plain", +"plain")
page
""".trimIndent()
),
manager
)
val result = script.execute(manager.newStdScope()) as ObjString
assertEquals("<html><h3>Imported</h3></html>", result.value)
}
}
@@ -15,7 +15,7 @@
*
*/
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.eval
import kotlin.test.Test
@@ -30,7 +30,7 @@ class TypeInferenceTest {
/** Channel field type inferred from constructor — accessed in a launch closure */
@Test
fun testChannelFieldInLaunchClosure() = runBlocking<Unit> {
fun testChannelFieldInLaunchClosure() = runTest {
eval("""
class Foo {
private val ch = Channel(Channel.UNLIMITED)
@@ -52,7 +52,7 @@ class TypeInferenceTest {
/** Mutex field type inferred from constructor — used directly in a method body */
@Test
fun testMutexFieldDirectUse() = runBlocking<Unit> {
fun testMutexFieldDirectUse() = runTest {
eval("""
class Bar {
private val mu = Mutex()
@@ -69,7 +69,7 @@ class TypeInferenceTest {
/** CompletableDeferred field type inferred — complete/await used directly */
@Test
fun testCompletableDeferredFieldDirectUse() = runBlocking<Unit> {
fun testCompletableDeferredFieldDirectUse() = runTest {
eval("""
class Baz {
private val d = CompletableDeferred()
@@ -84,7 +84,7 @@ class TypeInferenceTest {
/** Channel field accessed inside a map closure within class initializer */
@Test
fun testChannelFieldInMapAndLaunchClosure() = runBlocking<Unit> {
fun testChannelFieldInMapAndLaunchClosure() = runTest {
eval("""
class Pool(n) {
private val ch = Channel(Channel.UNLIMITED)
@@ -106,7 +106,7 @@ class TypeInferenceTest {
}
@Test
fun testIterableFirstPreservesElementTypeForBlockReturnInference() = runBlocking<Unit> {
fun testIterableFirstPreservesElementTypeForBlockReturnInference() = runTest {
eval("""
class Item(title: String)
@@ -121,7 +121,7 @@ class TypeInferenceTest {
}
@Test
fun testCallableLocalInitializedFromFunctionCallPreservesReturnType() = runBlocking<Unit> {
fun testCallableLocalInitializedFromFunctionCallPreservesReturnType() = runTest {
eval("""
fun makeAdder(base) {
return { x -> x + base + 0.5 }
@@ -53,6 +53,197 @@ class OperatorOverloadingTest {
""".trimIndent())
}
@Test
fun testUnaryPlusDefaultIdentity() = runTest {
eval("""
assertEquals(42, +42)
assertEquals(3.5, +3.5)
assertEquals("abc", +"abc")
class Box(val text: String) {
fun upper() = text.upper()
}
assertEquals("ABC", (+Box("abc")).upper())
""".trimIndent())
}
@Test
fun testUnaryPlusOverloading() = runTest {
eval("""
class Counter(val n: Int) {
fun unaryPlus() = Counter(this.n + 1)
fun equals(other: Counter) = this.n == other.n
}
assertEquals(Counter(6), Counter(5).unaryPlus())
assertEquals(Counter(6), +Counter(5))
""".trimIndent())
}
@Test
fun testUnaryPlusExtensionOverloading() = runTest {
eval("""
var out = ""
fun String.unaryPlus() {
out = out + this
}
"Hello".unaryPlus()
" ".unaryPlus()
"Lyng".unaryPlus()
assertEquals("Hello Lyng", out)
out = ""
+"Hello"
+" "
+"Lyng"
assertEquals("Hello Lyng", out)
""".trimIndent())
}
@Test
fun testUnaryPlusDslBuilderStyle() = runTest {
eval("""
class Tag(name: String) {
val name = name
var inner = ""
fun child(tagName: String, block: Tag.()->void) {
val child = Tag(tagName)
with(child) { block(this) }
inner += child.render()
}
fun head(block: Tag.()->void) { child("head", block) }
fun body(block: Tag.()->void) { child("body", block) }
fun title(block: Tag.()->void) { child("title", block) }
fun h1(block: Tag.()->void) { child("h1", block) }
fun addText(text: String) {
inner += text
}
fun render() {
"<" + name + ">" + inner + "</" + name + ">"
}
}
context(Tag)
fun String.unaryPlus() {
this@Tag.addText(this)
}
fun html(block: Tag.()->void) {
val root = Tag("html")
with(root) { block(this) }
root.render()
}
val page = html {
head {
title {
+"Demo"
}
}
body {
h1 {
+"Heading 1"
}
}
}
assertEquals("<html><head><title>Demo</title></head><body><h1>Heading 1</h1></body></html>", page)
""".trimIndent())
}
@Test
fun testContextReceiverUnaryPlusDslBuilderStyle() = runTest {
eval("""
class Tag(name: String) {
val name = name
var inner = ""
fun child(tagName: String, block: Tag.()->void) {
val child = Tag(tagName)
with(child) { block(this) }
inner += child.render()
}
fun h3(block: Tag.()->void) { child("h3", block) }
fun addText(text: String) {
inner += text
}
fun render() {
"<" + name + ">" + inner + "</" + name + ">"
}
}
context(Tag)
fun String.unaryPlus() {
this@Tag.addText(this)
}
fun html(block: Tag.()->void) {
val root = Tag("html")
with(root) { block(this) }
root.render()
}
val page = html {
h3 {
+"Heading 3"
}
}
assertEquals("<html><h3>Heading 3</h3></html>", page)
assertEquals("plain", +"plain")
""".trimIndent())
}
@Test
fun testContextReceiverExtensionIsHiddenOutsideContext() = runTest {
val ex = assertFailsWith<Throwable> {
eval("""
class Tag {
fun wrap(text: String) = "[" + text + "]"
}
context(Tag)
fun String.mark() = this@Tag.wrap(this)
"x".mark()
""".trimIndent())
}
assertContains(ex.message ?: "", "no such member: mark on String")
}
@Test
fun testContextReceiverExtensionIsHiddenInWrongContext() = runTest {
val ex = assertFailsWith<Throwable> {
eval("""
class Tag {
fun wrap(text: String) = "[" + text + "]"
}
class Other
context(Tag)
fun String.mark() = this@Tag.wrap(this)
fun other(block: Other.()->void) {
with(Other()) { block(this) }
}
other {
"x".mark()
}
""".trimIndent())
}
assertContains(ex.message ?: "", "no such member: mark on String")
}
@Test
fun testPlusAssignOverloading() = runTest {
eval("""