Fix concurrent TCP CLI scope and add regressions

This commit is contained in:
sergeych committed 2026-04-09 11:55:01 +03:00
1 parent c9eb3df93d
commit fbb1683ba3
10 files changed
+471 -61

No files matched your search

@@ -24,6 +24,7 @@ import net.sergeych.lyng.bridge.bind
import net.sergeych.lyng.bridge.bindObject
import net.sergeych.lyng.bytecode.CmdFunction
import net.sergeych.lyng.bytecode.CmdVm
import net.sergeych.lyng.bytecode.BytecodeLambdaCallable
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
@@ -635,16 +636,20 @@ class Script(
addConst("MapEntry", ObjMapEntry.type)
addFn("launch") {
val callable = requireOnlyArg<Obj>()
val captured = this
val rawCallable = requireOnlyArg<Obj>()
val currentScope = requireScope()
// Freeze non-module lexical state at launch time so each coroutine gets
// its own view of loop locals and other transient frame values.
val captured = if (currentScope is ModuleScope) currentScope else currentScope.snapshotForClosure()
val callable = (rawCallable as? BytecodeLambdaCallable)?.freezeForLaunch(captured) ?: rawCallable
val session = EvalSession.currentOrNull()
val deferred = if (session != null) {
session.launchTrackedDeferred {
captured.call(callable)
ScopeBridge(captured).call(callable)
}
} else {
globalDefer {
captured.call(callable)
ScopeBridge(captured).call(callable)
}
}
ObjDeferred(deferred)
@@ -1034,15 +1034,18 @@ class BytecodeCompiler(
val typeValue = compileRefWithFallback(ref.castTypeRef(), null, ref.castPos()) ?: return null
val objValue = ensureObjSlot(value)
val typeObj = ensureObjSlot(typeValue)
val sourceSlot = allocSlot()
builder.emit(Opcode.MOVE_OBJ, objValue.slot, sourceSlot)
updateSlotType(sourceSlot, SlotType.OBJ)
if (!ref.castIsNullable()) {
builder.emit(Opcode.ASSERT_IS, objValue.slot, typeObj.slot)
builder.emit(Opcode.ASSERT_IS, sourceSlot, typeObj.slot)
val resultSlot = allocSlot()
builder.emit(Opcode.MAKE_QUALIFIED_VIEW, objValue.slot, typeObj.slot, resultSlot)
builder.emit(Opcode.MAKE_QUALIFIED_VIEW, sourceSlot, typeObj.slot, resultSlot)
updateSlotType(resultSlot, SlotType.OBJ)
return CompiledValue(resultSlot, SlotType.OBJ)
}
val checkSlot = allocSlot()
builder.emit(Opcode.CHECK_IS, objValue.slot, typeObj.slot, checkSlot)
builder.emit(Opcode.CHECK_IS, sourceSlot, typeObj.slot, checkSlot)
updateSlotType(checkSlot, SlotType.BOOL)
val resultSlot = allocSlot()
val nullSlot = allocSlot()
@@ -1056,7 +1059,7 @@ class BytecodeCompiler(
builder.emit(Opcode.MOVE_OBJ, nullSlot, resultSlot)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(okLabel)
builder.emit(Opcode.MAKE_QUALIFIED_VIEW, objValue.slot, typeObj.slot, resultSlot)
builder.emit(Opcode.MAKE_QUALIFIED_VIEW, sourceSlot, typeObj.slot, resultSlot)
builder.mark(endLabel)
updateSlotType(resultSlot, SlotType.OBJ)
return CompiledValue(resultSlot, SlotType.OBJ)
@@ -3783,11 +3783,48 @@ class BytecodeLambdaCallable(
private val returnLabels: Set<String>,
override val pos: Pos,
) : Statement(), BytecodeCallable {
private fun freezeRecord(record: ObjRecord): ObjRecord {
val frozenValue = when (val raw = record.value) {
is net.sergeych.lyng.FrameSlotRef -> raw.read()
is net.sergeych.lyng.RecordSlotRef -> raw.read()
is net.sergeych.lyng.ScopeSlotRef -> raw.read()
else -> raw
}
return record.copy(value = frozenValue)
}
private fun resolveCaptureRecords(base: Scope): List<ObjRecord>? {
if (captureNames.isEmpty()) return null
return captureNames.map { name ->
base.chainLookupIgnoreClosure(
name,
followClosure = true,
caller = base.currentClassCtx
) ?: base.raiseSymbolNotFound("symbol $name not found")
}
}
fun rebindClosure(newClosureScope: Scope): BytecodeLambdaCallable {
return BytecodeLambdaCallable(
fn = fn,
closureScope = newClosureScope,
captureRecords = captureRecords,
captureRecords = resolveCaptureRecords(newClosureScope) ?: captureRecords,
captureNames = captureNames,
paramSlotPlan = paramSlotPlan,
argsDeclaration = argsDeclaration,
preferredThisType = preferredThisType,
returnLabels = returnLabels,
pos = pos
)
}
fun freezeForLaunch(newClosureScope: Scope): BytecodeLambdaCallable {
val frozenCaptures = captureRecords?.map(::freezeRecord)
?: resolveCaptureRecords(newClosureScope)?.map(::freezeRecord)
return BytecodeLambdaCallable(
fn = fn,
closureScope = newClosureScope,
captureRecords = frozenCaptures,
captureNames = captureNames,
paramSlotPlan = paramSlotPlan,
argsDeclaration = argsDeclaration,
@@ -157,4 +157,10 @@ class ImportManager(
}
}
fun invalidatePackageCache(name: String) {
op.withLock {
imports[name]?.cachedScope = null
}
}
}
@@ -139,6 +139,66 @@ class TestCoroutines {
)
}
@Test
fun testLaunchCapturesDistinctLoopValues() = runTest {
eval(
"""
val jobs = []
for( i in 0..<8 ) {
val x = i
jobs += launch {
delay(5)
x
}
}
assertEquals([0,1,2,3,4,5,6,7], jobs.joinAll())
""".trimIndent()
)
}
@Test
fun testNestedLaunchCapturesDistinctLoopValues() = runTest {
eval(
"""
val outer = launch {
val jobs = []
for( i in 0..<8 ) {
val x = i
jobs += launch {
delay(5)
x
}
}
jobs.joinAll()
}
assertEquals([0,1,2,3,4,5,6,7], outer.await())
""".trimIndent()
)
}
@Test
fun testLaunchCapturesDistinctObjectValues() = runTest {
eval(
"""
val jobs = []
for( i in 0..<8 ) {
val box = ["item:${'$'}i"]
jobs += launch {
delay(5)
box[0]
}
}
assertEquals(
["item:0", "item:1", "item:2", "item:3", "item:4", "item:5", "item:6", "item:7"],
jobs.joinAll()
)
""".trimIndent()
)
}
@Test
fun testFlows() = runTest {
eval("""