Add Lyng deferred cancellation support

This commit is contained in:
sergeych committed 2026-03-31 22:50:46 +03:00
1 parent 3e338f3d53
commit 311cf6ee44
8 files changed
+180 -66

No files matched your search

@@ -343,6 +343,73 @@ class Compiler(
}
return t
}
fun handleTopLevelKeyword(keyword: Token): Boolean {
when (keyword.value) {
"fun", "fn" -> {
val nameToken = nextNonWs()
if (nameToken.type != Token.Type.ID) return true
val afterName = cc.peekNextNonWhitespace()
if (afterName.type == Token.Type.DOT) {
cc.nextNonWhitespace()
val actual = cc.nextNonWhitespace()
if (actual.type == Token.Type.ID) {
extensionNames.add(actual.value)
registerExtensionName(nameToken.value, actual.value)
declareSlotNameIn(plan, extensionCallableName(nameToken.value, actual.value), isMutable = false, isDelegated = false)
moduleDeclaredNames.add(extensionCallableName(nameToken.value, actual.value))
}
return true
}
declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false)
moduleDeclaredNames.add(nameToken.value)
return true
}
"val", "var" -> {
val nameToken = nextNonWs()
if (nameToken.type != Token.Type.ID) return true
val afterName = cc.peekNextNonWhitespace()
if (afterName.type == Token.Type.DOT) {
cc.nextNonWhitespace()
val actual = cc.nextNonWhitespace()
if (actual.type == Token.Type.ID) {
extensionNames.add(actual.value)
registerExtensionName(nameToken.value, actual.value)
declareSlotNameIn(plan, extensionPropertyGetterName(nameToken.value, actual.value), isMutable = false, isDelegated = false)
moduleDeclaredNames.add(extensionPropertyGetterName(nameToken.value, actual.value))
if (keyword.value == "var") {
declareSlotNameIn(plan, extensionPropertySetterName(nameToken.value, actual.value), isMutable = false, isDelegated = false)
moduleDeclaredNames.add(extensionPropertySetterName(nameToken.value, actual.value))
}
}
return true
}
declareSlotNameIn(plan, nameToken.value, isMutable = keyword.value == "var", isDelegated = false)
moduleDeclaredNames.add(nameToken.value)
predeclaredTopLevelValueNames.add(nameToken.value)
return true
}
"class", "object" -> {
val nameToken = nextNonWs()
if (nameToken.type == Token.Type.ID) {
declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false)
scopeSeedNames.add(nameToken.value)
moduleDeclaredNames.add(nameToken.value)
}
return true
}
"enum" -> {
val next = nextNonWs()
val nameToken = if (next.type == Token.Type.ID && next.value == "class") nextNonWs() else next
if (nameToken.type == Token.Type.ID) {
declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false)
scopeSeedNames.add(nameToken.value)
moduleDeclaredNames.add(nameToken.value)
}
return true
}
}
return false
}
try {
while (cc.hasNext()) {
val t = cc.next()
@@ -355,66 +422,11 @@ class Compiler(
Token.Type.RBRACKET -> if (bracketDepth > 0) bracketDepth--
Token.Type.ID -> if (depth == 0) {
if (parenDepth > 0 || bracketDepth > 0) continue
when (t.value) {
"fun", "fn" -> {
val nameToken = nextNonWs()
if (nameToken.type != Token.Type.ID) continue
val afterName = cc.peekNextNonWhitespace()
if (afterName.type == Token.Type.DOT) {
cc.nextNonWhitespace()
val actual = cc.nextNonWhitespace()
if (actual.type == Token.Type.ID) {
extensionNames.add(actual.value)
registerExtensionName(nameToken.value, actual.value)
declareSlotNameIn(plan, extensionCallableName(nameToken.value, actual.value), isMutable = false, isDelegated = false)
moduleDeclaredNames.add(extensionCallableName(nameToken.value, actual.value))
}
continue
}
declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false)
moduleDeclaredNames.add(nameToken.value)
}
"val", "var" -> {
val nameToken = nextNonWs()
if (nameToken.type != Token.Type.ID) continue
val afterName = cc.peekNextNonWhitespace()
if (afterName.type == Token.Type.DOT) {
cc.nextNonWhitespace()
val actual = cc.nextNonWhitespace()
if (actual.type == Token.Type.ID) {
extensionNames.add(actual.value)
registerExtensionName(nameToken.value, actual.value)
declareSlotNameIn(plan, extensionPropertyGetterName(nameToken.value, actual.value), isMutable = false, isDelegated = false)
moduleDeclaredNames.add(extensionPropertyGetterName(nameToken.value, actual.value))
if (t.value == "var") {
declareSlotNameIn(plan, extensionPropertySetterName(nameToken.value, actual.value), isMutable = false, isDelegated = false)
moduleDeclaredNames.add(extensionPropertySetterName(nameToken.value, actual.value))
}
}
continue
}
declareSlotNameIn(plan, nameToken.value, isMutable = t.value == "var", isDelegated = false)
moduleDeclaredNames.add(nameToken.value)
predeclaredTopLevelValueNames.add(nameToken.value)
}
"class", "object" -> {
val nameToken = nextNonWs()
if (nameToken.type == Token.Type.ID) {
declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false)
scopeSeedNames.add(nameToken.value)
moduleDeclaredNames.add(nameToken.value)
}
}
"enum" -> {
val next = nextNonWs()
val nameToken = if (next.type == Token.Type.ID && next.value == "class") nextNonWs() else next
if (nameToken.type == Token.Type.ID) {
declareSlotNameIn(plan, nameToken.value, isMutable = false, isDelegated = false)
scopeSeedNames.add(nameToken.value)
moduleDeclaredNames.add(nameToken.value)
}
}
if (t.value == "extern") {
handleTopLevelKeyword(nextNonWs())
continue
}
handleTopLevelKeyword(t)
}
else -> {}
}
@@ -9070,7 +9082,10 @@ class Compiler(
val effectiveEqToken = if (isProperty) null else eqToken
// Register the local name at compile time so that subsequent identifiers can be emitted as fast locals
if (!isStatic && declaringClassNameCaptured == null && !actualExtern) declareLocalName(name, isMutable)
val isTopLevelExtern = actualExtern && declaringClassNameCaptured == null && slotPlanStack.size == 1
if (!isStatic && declaringClassNameCaptured == null && (!actualExtern || isTopLevelExtern)) {
declareLocalName(name, isMutable)
}
val declKind = if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
SymbolKind.MEMBER
} else {
@@ -540,7 +540,8 @@ private fun buildStdlibDocs(): List<MiniDecl> {
// Concurrency helpers
mod.classDoc(name = "Deferred", doc = "Represents a value that will be available in the future.", bases = listOf(type("Obj"))) {
method(name = "await", doc = "Suspend until the value is available and return it.")
method(name = "cancel", doc = "Cancel the deferred if it is still active.")
method(name = "await", doc = "Suspend until the value is available and return it. Throws `CancellationException` if cancelled.")
}
mod.funDoc(
name = "launch",
@@ -18,6 +18,7 @@
package net.sergeych.lyng.obj
import kotlinx.coroutines.Deferred
import kotlin.coroutines.cancellation.CancellationException as KotlinCancellationException
import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
@@ -33,12 +34,27 @@ open class ObjDeferred(val deferred: Deferred<Obj>): Obj() {
scope.raiseError("Deferred constructor is not directly callable")
}
}.apply {
addFnDoc(
name = "cancel",
doc = "Cancel this deferred if it is still active. Safe to call multiple times.",
returns = type("lyng.Void"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjDeferred>().deferred.cancel()
ObjVoid
}
addFnDoc(
name = "await",
doc = "Suspend until completion and return the result value (or throw if failed).",
doc = "Suspend until completion and return the result value. Throws `CancellationException` if cancelled.",
returns = type("lyng.Any"),
moduleName = "lyng.stdlib"
) { thisAs<ObjDeferred>().deferred.await() }
) {
try {
thisAs<ObjDeferred>().deferred.await()
} catch (e: KotlinCancellationException) {
requireScope().raiseError(ObjCancellationException(requireScope(), e.message ?: "Deferred was cancelled"))
}
}
addPropertyDoc(
name = "isCompleted",
doc = "Whether this deferred has completed (successfully or with an error).",
@@ -66,4 +82,3 @@ open class ObjDeferred(val deferred: Deferred<Obj>): Obj() {
}
}
}
@@ -278,6 +278,7 @@ open class ObjException(
"AssertionFailedException",
"ClassCastException",
"IndexOutOfBoundsException",
"CancellationException",
"IllegalArgumentException",
"IllegalStateException",
"NoSuchElementException",
@@ -311,6 +312,9 @@ class ObjClassCastException(scope: Scope, message: String) : ObjException("Class
class ObjIndexOutOfBoundsException(scope: Scope, message: String = "index out of bounds") :
ObjException("IndexOutOfBoundsException", scope, message)
class ObjCancellationException(scope: Scope, message: String = "cancelled") :
ObjException("CancellationException", scope, message)
class ObjIllegalArgumentException(scope: Scope, message: String = "illegal argument") :
ObjException("IllegalArgumentException", scope, message)
@@ -58,6 +58,30 @@ class TestCoroutines {
)
}
@Test
fun testDeferredCancel() = runTest {
eval(
"""
var reached = false
val d = launch {
delay(100)
reached = true
"ok"
}
d.cancel()
d.cancel()
assertThrows(CancellationException) { d.await() }
delay(150)
assert(d.isCancelled)
assert(!d.isActive)
assert(!reached)
""".trimIndent()
)
}
@Test
fun testMutex() = runTest {
eval(
+37
View File
@@ -1,11 +1,48 @@
package lyng.stdlib
/* Launch `code` asynchronously and return its result handle. */
extern fun launch(code): Deferred
/* Yield execution so other scheduled coroutines can run. */
extern fun yield(): void
/* Build a lazy asynchronous sequence. */
extern fun flow(builder: FlowBuilder.()->void): Flow
/* Built-in exception type. */
extern class Exception
extern class IllegalArgumentException
extern class NotImplementedException
/* Raised when an awaited asynchronous task was cancelled before producing a value. */
extern class CancellationException : Exception
/* A handle to a running asynchronous task. */
extern class Deferred {
/* Cancel the task if it is still active. Safe to call multiple times. */
fun cancel(): void
/* Suspend until the task finishes and return its value.
Throws `CancellationException` if the task was cancelled. */
fun await(): Object
/* True when the task has finished, failed, or otherwise reached a terminal state. */
val isCompleted: Bool
/* True while the task is still running and has not been cancelled. */
val isActive: Bool
/* True when the task was cancelled. */
val isCancelled: Bool
}
/* A deferred result that can be completed manually. */
extern class CompletableDeferred : Deferred {
fun complete(value: Object): void
}
/* Receiver passed into `flow { ... }` builders. */
extern class FlowBuilder {
fun emit(value: Object): void
}
/* A cold asynchronous iterable sequence. */
extern class Flow<T> : Iterable<T> {
}
extern class Delegate
extern class Iterable<T> {
fun iterator(): Iterator<T>