added LaunchPool
This commit is contained in:
1 parent
145c3ae34a
commit
a8f73dc8bd
9 files changed
+533
-30
No files matched your search
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* 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.
|
||||
@@ -24,7 +24,10 @@ sealed class CodeContext {
|
||||
val implicitThisMembers: Boolean = false,
|
||||
val implicitThisTypeName: String? = null,
|
||||
val typeParams: Set<String> = emptySet(),
|
||||
val typeParamDecls: List<TypeDecl.TypeParam> = emptyList()
|
||||
val typeParamDecls: List<TypeDecl.TypeParam> = emptyList(),
|
||||
/** True for static methods and top-level functions: they have no implicit `this`,
|
||||
* so class-body field initializers inside them should not inherit the class name. */
|
||||
val noImplicitThis: Boolean = false
|
||||
): CodeContext()
|
||||
class ClassBody(val name: String, val isExtern: Boolean = false): CodeContext() {
|
||||
var typeParams: Set<String> = emptySet()
|
||||
|
||||
@@ -812,8 +812,18 @@ class Compiler(
|
||||
|
||||
private fun currentImplicitThisTypeName(): String? {
|
||||
for (ctx in codeContexts.asReversed()) {
|
||||
val fn = ctx as? CodeContext.Function ?: continue
|
||||
if (fn.implicitThisTypeName != null) return fn.implicitThisTypeName
|
||||
when (ctx) {
|
||||
is CodeContext.Function -> {
|
||||
if (ctx.implicitThisTypeName != null) return ctx.implicitThisTypeName
|
||||
// A static method or top-level function explicitly has no implicit `this`.
|
||||
// Stop here — do not fall through to an enclosing ClassBody.
|
||||
if (ctx.noImplicitThis) return null
|
||||
}
|
||||
// Class field initializers are compiled directly under ClassBody with no wrapping
|
||||
// Function. Lambdas inside those initializers must still see `this` as the class.
|
||||
is CodeContext.ClassBody -> return ctx.name
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -9214,13 +9224,17 @@ class Compiler(
|
||||
parentContext is CodeContext.ClassBody && !isStatic -> parentContext.name
|
||||
else -> null
|
||||
}
|
||||
// Static methods and top-level functions have no implicit `this`; mark them so that
|
||||
// currentImplicitThisTypeName() does not fall through to an enclosing ClassBody.
|
||||
val noImplicitThis = implicitThisTypeName == null && extTypeName == null
|
||||
return inCodeContext(
|
||||
CodeContext.Function(
|
||||
name,
|
||||
implicitThisMembers = implicitThisMembers,
|
||||
implicitThisTypeName = implicitThisTypeName,
|
||||
typeParams = typeParams,
|
||||
typeParamDecls = typeParamDecls
|
||||
typeParamDecls = typeParamDecls,
|
||||
noImplicitThis = noImplicitThis
|
||||
)
|
||||
) {
|
||||
cc.labels.add(name)
|
||||
|
||||
@@ -5869,17 +5869,22 @@ class BytecodeCompiler(
|
||||
}
|
||||
|
||||
private fun compileCatchClassSlot(name: String): CompiledValue? {
|
||||
val ref = LocalVarRef(name, Pos.builtIn)
|
||||
val compiled = compileRef(ref)
|
||||
if (compiled != null) {
|
||||
return ensureObjSlot(compiled)
|
||||
// resolveTypeNameClass always returns the correct Lyng class for built-in type names.
|
||||
// nameObjClass[name] tracks variable type inference — for class declarations it stores
|
||||
// ObjClassType (the "Class" meta-type), NOT the actual class object. Using nameObjClass
|
||||
// for catch clause matching would cause CHECK_IS to compare against the wrong class,
|
||||
// silently failing to catch exceptions. Always prefer resolveTypeNameClass first.
|
||||
val cls = resolveTypeNameClass(name) ?: nameObjClass[name]?.takeIf { it != ObjClassType }
|
||||
if (cls != null) {
|
||||
val id = builder.addConst(BytecodeConst.ObjRef(cls))
|
||||
val slot = allocSlot()
|
||||
builder.emit(Opcode.CONST_OBJ, id, slot)
|
||||
updateSlotType(slot, SlotType.OBJ)
|
||||
return CompiledValue(slot, SlotType.OBJ)
|
||||
}
|
||||
val cls = nameObjClass[name] ?: resolveTypeNameClass(name) ?: return null
|
||||
val id = builder.addConst(BytecodeConst.ObjRef(cls))
|
||||
val slot = allocSlot()
|
||||
builder.emit(Opcode.CONST_OBJ, id, slot)
|
||||
updateSlotType(slot, SlotType.OBJ)
|
||||
return CompiledValue(slot, SlotType.OBJ)
|
||||
val ref = LocalVarRef(name, Pos.builtIn)
|
||||
val compiled = compileRef(ref) ?: return null
|
||||
return ensureObjSlot(compiled)
|
||||
}
|
||||
|
||||
private fun emitInlineStatements(statements: List<Statement>, needResult: Boolean): CompiledValue? {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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.runBlocking
|
||||
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) }
|
||||
|
||||
@Test
|
||||
fun testBasicExecution() = runBlocking<Unit> {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
val d1 = pool.launch { 1 + 1 }
|
||||
val d2 = pool.launch { "hello" }
|
||||
pool.closeAndJoin()
|
||||
assertEquals(2, d1.await())
|
||||
assertEquals("hello", d2.await())
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testResultsCollected() = runBlocking<Unit> {
|
||||
eval("""
|
||||
val pool = LaunchPool(4)
|
||||
val jobs = (1..10).map { n -> pool.launch { n * n } }
|
||||
pool.closeAndJoin()
|
||||
val results = jobs.map { (it as Deferred).await() }
|
||||
assertEquals([1,4,9,16,25,36,49,64,81,100], results)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConcurrencyLimit() = runBlocking<Unit> {
|
||||
eval("""
|
||||
// With maxWorkers=2, at most 2 tasks run at the same time.
|
||||
val mu = Mutex()
|
||||
var active = 0
|
||||
var maxSeen = 0
|
||||
|
||||
val pool = LaunchPool(2)
|
||||
val jobs = (1..8).map {
|
||||
pool.launch {
|
||||
mu.withLock { active++; if (active > maxSeen) maxSeen = active }
|
||||
delay(5)
|
||||
mu.withLock { active-- }
|
||||
}
|
||||
}
|
||||
pool.closeAndJoin()
|
||||
|
||||
assert(maxSeen <= 2) { "maxSeen was " + maxSeen + ", expected <= 2" }
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExceptionCapturedInDeferred() = runBlocking<Unit> {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
val good = pool.launch { 42 }
|
||||
val bad = pool.launch { throw IllegalArgumentException("boom") }
|
||||
pool.closeAndJoin()
|
||||
|
||||
assertEquals(42, good.await())
|
||||
assertThrows(IllegalArgumentException) { bad.await() }
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPoolContinuesAfterLambdaException() = runBlocking<Unit> {
|
||||
eval("""
|
||||
val pool = LaunchPool(1)
|
||||
val bad = pool.launch { throw IllegalArgumentException("fail") }
|
||||
val good = pool.launch { "ok" }
|
||||
pool.closeAndJoin()
|
||||
|
||||
assertThrows(IllegalArgumentException) { bad.await() }
|
||||
assertEquals("ok", good.await())
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchAfterCloseAndJoinThrows() = runBlocking<Unit> {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
pool.launch { 1 }
|
||||
pool.closeAndJoin()
|
||||
|
||||
assertThrows(IllegalStateException) { pool.launch { 2 } }
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLaunchAfterCancelThrows() = runBlocking<Unit> {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
pool.cancel()
|
||||
|
||||
assertThrows(IllegalStateException) { pool.launch { 1 } }
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCancelAndJoinWaitsForWorkers() = runBlocking<Unit> {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
pool.launch { delay(5) }
|
||||
pool.cancelAndJoin()
|
||||
// pool is now closed — further launches must throw
|
||||
assertThrows(IllegalStateException) { pool.launch { 1 } }
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCloseAndJoinDrainsQueue() = runBlocking<Unit> {
|
||||
eval("""
|
||||
val mu = Mutex()
|
||||
val results = []
|
||||
val pool = LaunchPool(1) // single worker to force sequential execution
|
||||
|
||||
(1..5).forEach { n ->
|
||||
pool.launch {
|
||||
delay(1)
|
||||
mu.withLock { results += n }
|
||||
}
|
||||
}
|
||||
pool.closeAndJoin() // waits for all 5 to complete
|
||||
|
||||
assertEquals(5, results.size)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBoundedQueueSuspendsProducer() = runBlocking<Unit> {
|
||||
eval("""
|
||||
// queue of 2 + 1 worker; producer can only be 1 ahead of what's running
|
||||
val pool = LaunchPool(1, 2)
|
||||
val order = []
|
||||
val mu = Mutex()
|
||||
|
||||
// fill the queue
|
||||
val d1 = pool.launch { delay(5); mu.withLock { order += 1 } }
|
||||
val d2 = pool.launch { delay(3); mu.withLock { order += 2 } }
|
||||
val d3 = pool.launch { delay(3); mu.withLock { order += 3 } }
|
||||
|
||||
pool.closeAndJoin()
|
||||
assertEquals(3, order.size)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnlimitedQueueDefault() = runBlocking<Unit> {
|
||||
eval("""
|
||||
val pool = LaunchPool(4)
|
||||
val jobs = (1..50).map { n -> pool.launch { n } }
|
||||
pool.closeAndJoin()
|
||||
var sum = 0
|
||||
for (j in jobs) { sum += (j as Deferred).await() }
|
||||
assertEquals(1275, sum) // 1+2+...+50
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIdempotentClose() = runBlocking<Unit> {
|
||||
eval("""
|
||||
val pool = LaunchPool(2)
|
||||
pool.closeAndJoin()
|
||||
pool.closeAndJoin() // calling again must not throw
|
||||
pool.cancel() // mixing close calls must not throw either
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.runBlocking
|
||||
import net.sergeych.lyng.eval
|
||||
import kotlin.test.Test
|
||||
|
||||
/**
|
||||
* Regression tests for type inference of class fields accessed inside nested closures.
|
||||
*
|
||||
* A class field declared as `val foo = SomeType(...)` should have its type inferred as
|
||||
* `SomeType` everywhere inside the class body, including inside lambdas and closures
|
||||
* that capture the field via the implicit `this` receiver.
|
||||
*/
|
||||
class TypeInferenceTest {
|
||||
|
||||
/** Channel field type inferred from constructor — accessed in a launch closure */
|
||||
@Test
|
||||
fun testChannelFieldInLaunchClosure() = runBlocking<Unit> {
|
||||
eval("""
|
||||
class Foo {
|
||||
private val ch = Channel(Channel.UNLIMITED)
|
||||
private val worker = launch {
|
||||
var item = ch.receive()
|
||||
while (item != null) {
|
||||
item = ch.receive()
|
||||
}
|
||||
}
|
||||
fun start() {
|
||||
ch.send(1)
|
||||
ch.close()
|
||||
(worker as Deferred).await()
|
||||
}
|
||||
}
|
||||
Foo().start()
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
/** Mutex field type inferred from constructor — used directly in a method body */
|
||||
@Test
|
||||
fun testMutexFieldDirectUse() = runBlocking<Unit> {
|
||||
eval("""
|
||||
class Bar {
|
||||
private val mu = Mutex()
|
||||
private var count = 0
|
||||
fun inc() { mu.withLock { count++ } }
|
||||
fun get() { count }
|
||||
}
|
||||
val b = Bar()
|
||||
b.inc()
|
||||
b.inc()
|
||||
assertEquals(2, b.get())
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
/** CompletableDeferred field type inferred — complete/await used directly */
|
||||
@Test
|
||||
fun testCompletableDeferredFieldDirectUse() = runBlocking<Unit> {
|
||||
eval("""
|
||||
class Baz {
|
||||
private val d = CompletableDeferred()
|
||||
fun complete(v) { d.complete(v) }
|
||||
fun result() { (d as Deferred).await() }
|
||||
}
|
||||
val baz = Baz()
|
||||
launch { baz.complete(42) }
|
||||
assertEquals(42, baz.result())
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
/** Channel field accessed inside a map closure within class initializer */
|
||||
@Test
|
||||
fun testChannelFieldInMapAndLaunchClosure() = runBlocking<Unit> {
|
||||
eval("""
|
||||
class Pool(n) {
|
||||
private val ch = Channel(Channel.UNLIMITED)
|
||||
private val workers = (1..n).map {
|
||||
launch {
|
||||
var item = ch.receive()
|
||||
while (item != null) {
|
||||
item = ch.receive()
|
||||
}
|
||||
}
|
||||
}
|
||||
fun closeAll() {
|
||||
ch.close()
|
||||
for (w in workers) { (w as Deferred).await() }
|
||||
}
|
||||
}
|
||||
Pool(2).closeAll()
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user