diff --git a/docs/ai_stdlib_reference.md b/docs/ai_stdlib_reference.md index cc4863a..4f150bd 100644 --- a/docs/ai_stdlib_reference.md +++ b/docs/ai_stdlib_reference.md @@ -14,6 +14,8 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s - Assertions/tests: `assert`, `assertEquals`/`assertEqual`, `assertNotEquals`, `assertThrows`. - Preconditions: `require`, `check`. - Async/concurrency: `launch`, `yield`, `flow`, `delay`. + - `Deferred.cancel()` cancels an active task. + - `Deferred.await()` throws `CancellationException` if that task was cancelled. - Math: `floor`, `ceil`, `round`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`, `exp`, `ln`, `log10`, `log2`, `pow`, `sqrt`, `abs`, `clamp`. - These helpers also accept `lyng.decimal.Decimal`. - Exact Decimal path today: `abs`, `floor`, `ceil`, `round`, and `pow` with integral exponent. @@ -26,13 +28,14 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s - Collections/types: `Iterable`, `Iterator`, `Collection`, `Array`, `List`, `ImmutableList`, `Set`, `ImmutableSet`, `Map`, `ImmutableMap`, `MapEntry`, `Range`, `RingBuffer`. - Random: singleton `Random` and class `SeededRandom`. - Async types: `Deferred`, `CompletableDeferred`, `Mutex`, `Flow`, `FlowBuilder`. +- Async exception: `CancellationException`. - Delegation types: `Delegate`, `DelegateContext`. - Regex types: `Regex`, `RegexMatch`. - Also present: `Math.PI` namespace constant. ## 4. `lyng.stdlib` Module Surface (from `root.lyng`) ### 4.1 Extern class declarations -- Exceptions/delegation base: `Exception`, `IllegalArgumentException`, `NotImplementedException`, `Delegate`. +- Exceptions/delegation base: `Exception`, `CancellationException`, `IllegalArgumentException`, `NotImplementedException`, `Delegate`. - Collections and iterables: `Iterable`, `Iterator`, `Collection`, `Array`, `List`, `ImmutableList`, `Set`, `ImmutableSet`, `Map`, `ImmutableMap`, `MapEntry`, `RingBuffer`. - Host iterator bridge: `KotlinIterator`. - Random APIs: `extern object Random`, `extern class SeededRandom`. diff --git a/docs/parallelism.md b/docs/parallelism.md index bb90ca1..08f8cb3 100644 --- a/docs/parallelism.md +++ b/docs/parallelism.md @@ -32,10 +32,25 @@ Depending on the platform, these coroutines may be executed on different CPU and assert(xIsCalled) >>> void -This example shows how to launch a coroutine with `launch` which returns [Deferred] instance, the latter have ways to await for the coroutine completion and retrieve possible result. +This example shows how to launch a coroutine with `launch` which returns [Deferred] instance, the latter have ways to await for the coroutine completion, cancel it if it is no longer needed, and retrieve possible result. Launch has the only argument which should be a callable (lambda usually) that is run in parallel (or cooperatively in parallel), and return anything as the result. +If you no longer need the result, cancel the deferred. Awaiting a cancelled deferred throws `CancellationException`: + + var reached = false + val work = launch { + delay(100) + reached = true + "ok" + } + work.cancel() + assertThrows(CancellationException) { work.await() } + assert(work.isCancelled) + assert(!work.isActive) + assert(!reached) + >>> void + ## Synchronization: Mutex Suppose we have a resource, that could be used concurrently, a counter in our case. If we won't protect it, concurrent usage cause RC, Race Condition, providing wrong result: diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 68e980c..23153e1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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 { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt index 1e45c95..1818eb1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt @@ -540,7 +540,8 @@ private fun buildStdlibDocs(): List { // 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", diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDeferred.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDeferred.kt index 49ff90f..456c6dc 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDeferred.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDeferred.kt @@ -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() { 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().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().deferred.await() } + ) { + try { + thisAs().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() { } } } - diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt index 51596e4..7574bab 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt @@ -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) diff --git a/lynglib/src/commonTest/kotlin/CoroutinesTest.kt b/lynglib/src/commonTest/kotlin/CoroutinesTest.kt index 5383fbc..19c5475 100644 --- a/lynglib/src/commonTest/kotlin/CoroutinesTest.kt +++ b/lynglib/src/commonTest/kotlin/CoroutinesTest.kt @@ -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( diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index f92320c..0ad4b21 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -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 : Iterable { +} + extern class Delegate extern class Iterable { fun iterator(): Iterator