From 9735774efdca532a9dd2f2efdb8e2b6dc1a21c01 Mon Sep 17 00:00:00 2001 From: sergeych Date: Thu, 23 Apr 2026 15:18:47 +0300 Subject: [PATCH] v1.5.5 release --- CHANGELOG.md | 52 +++++-- README.md | 6 +- docs/Iterable.md | 18 +++ docs/LaunchPool.md | 4 +- docs/ai_stdlib_reference.md | 1 + docs/parallelism.md | 13 +- docs/whats_new.md | 19 ++- examples/error2.lyng | 0 lynglib/build.gradle.kts | 2 +- .../lyng/bytecode/BytecodeCompiler.kt | 38 ++++- .../commonTest/kotlin/WebsiteSamplesTest.kt | 4 +- .../sergeych/lyng/OperatorOverloadingTest.kt | 144 ++++++++++++++++++ site/src/jsMain/kotlin/HomePage.kt | 2 +- 13 files changed, 269 insertions(+), 34 deletions(-) create mode 100644 examples/error2.lyng diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f0ff36..e302c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,24 +9,46 @@ History note: ## Unreleased -### Database access -- Added the portable `lyng.io.db` SQL contract and the first concrete provider, `lyng.io.db.sqlite`. -- Added SQLite support on JVM and Linux Native with: - - generic `openDatabase("sqlite:...")` dispatch - - typed `openSqlite(...)` helper - - real nested transactions via savepoints - - generated keys through `ExecutionResult.getGeneratedKeys()` - - strict schema-driven value conversion for `Bool`, `Decimal`, `Date`, `DateTime`, and `Instant` - - documented option handling for `readOnly`, `createIfMissing`, `foreignKeys`, and `busyTimeoutMillis` -- Added public docs for database usage and SQLite provider behavior. +- No unreleased entries yet. -### Time -- Added `Date` to `lyng.time` and the core library as a first-class calendar-date type. -- Added `Instant.toDate(...)`, `DateTime.date`, `DateTime.toDate()`, `Date.toDateTime(...)`, and related date arithmetic. -- Added docs, stdlib reference updates, serialization support, and comprehensive tests for `Date`. +## 1.5.5 (2026-04-23) + +### Concurrency and collections +- Added coroutine coordination primitives and helpers for everyday parallel code: + - `Channel` for coroutine-to-coroutine communication + - `LaunchPool` for bounded-concurrency task execution + - `Iterable.joinAll()` to await a whole collection of deferreds in input order + - `CompletableDeferred.completeExceptionally(...)` and `Deferred.cancelAndJoin()` +- Added docs and examples for the new concurrency APIs, including `joinAll()` coverage in iterable and parallelism references. + +### Database and time APIs +- Added the portable `lyng.io.db` SQL contract and the first concrete providers: + - `lyng.io.db.sqlite` on JVM and Linux Native + - `lyng.io.db.jdbc` on JVM +- Added SQLite/JDBC release hardening: + - nested transactions via savepoints + - detached materialized rows + - generated-key support through `ExecutionResult.getGeneratedKeys()` + - schema-driven value conversion for `Bool`, `Decimal`, `Date`, `DateTime`, and `Instant` + - portable SQLite linker/deployment fixes and documented runtime options +- Added `Date` to `lyng.time` and the core runtime as a first-class calendar-date type, plus conversions and arithmetic across `Instant`, `DateTime`, and `Date`. + +### Language, stdlib, and tooling +- Added extensions on singleton `object` declarations, including object-scoped indexer overrides for bracket syntax. +- Added backtick string literals and formatter support. +- Added `lyng.legacy_digest` for SHA-1 compatibility work, `String.replace`, and `buffer.base64std`. +- Improved CLI/runtime behavior with `atExit` shutdown handlers, native release-binary work, and follow-up CLI packaging/import fixes. +- Expanded docs across the tutorial, stdlib references, database docs, networking docs, and release notes. + +### Runtime/compiler stability and performance +- Extended exact-call and higher-order lambda inlining through the bytecode compiler, including compiled fast paths for simple lambdas, wrappers, captures, and common higher-order helpers. +- Fixed import caching and class/object bytecode dispatch on JVM. +- Fixed immutable `val` compound assignments so true mutating `*Assign` operations continue to work while fallback reassignments report the correct read-only error. +- Fixed closure/capture and import regressions across launched loops, singleton/object extensions, aliasing, transitive re-exports, and immutable capture escaping. +- Improved list-fill/list-append fast paths, nullable-let inference, Decimal/Complex interop, and related regression coverage. ### Release notes -- Full `:lyngio:jvmTest` and `:lyngio:linuxX64Test` pass on the release tree after SQLite hardening. +- Release metadata, homepage samples, docs, and README now point to `1.5.5`. ## 1.5.4 (2026-04-03) diff --git a/README.md b/README.md index 4eaca2b..d9e1515 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ assertEquals(A.E.One, A.One) - [Language home](https://lynglang.com) - [introduction and tutorial](docs/tutorial.md) - start here please -- [Latest release notes (1.5.4)](docs/whats_new.md) +- [Latest release notes (1.5.5)](docs/whats_new.md) - [What's New in 1.5](docs/whats_new_1_5.md) - [Testing and Assertions](docs/Testing.md) - [Filesystem and Processes (lyngio)](docs/lyngio.md) @@ -66,7 +66,7 @@ assertEquals(A.E.One, A.One) ### Add dependency to your project ```kotlin -val lyngVersion = "1.5.4" +val lyngVersion = "1.5.5" repositories { // ... @@ -186,7 +186,7 @@ Designed to add scripting to kotlin multiplatform application in easy and effici # Language Roadmap -The current stable release is **v1.5.4**: the 1.5 cycle is feature-complete, compiler/runtime stabilization work is in, and the language, tooling, and site are aligned around the current release. +The current stable release is **v1.5.5**: the 1.5 cycle now includes the database/date/concurrency additions as well as the latest compiler/runtime stabilization work, and the language, tooling, and site are aligned around this release. Ready features: diff --git a/docs/Iterable.md b/docs/Iterable.md index f84ba74..a9f83bd 100644 --- a/docs/Iterable.md +++ b/docs/Iterable.md @@ -55,6 +55,23 @@ Here is the sample: assertEquals( (1..3).joinToString { it * 10 }, "10 20 30") >>> void +## joinAll + +`joinAll()` is an `Iterable` helper that awaits every deferred in iteration order and returns a `List` +with the collected results. + + val jobs = (1..4).map { n -> + launch { n * n } + } + assertEquals([1, 4, 9, 16], jobs.joinAll()) + >>> void + +Notes: + +- it does not start any task by itself; it only awaits the deferreds already present in the iterable. +- awaiting happens in iteration order, so the result list keeps the same order as the input iterable. +- if any deferred fails or was cancelled, that `await()` error is propagated from `joinAll()`. + ## `sum` and `sumOf` These, again, does the thing: @@ -184,6 +201,7 @@ Search for the first element that satisfies the given predicate: | sortedWith(comparator) | sort using a comparator that compares elements (1) | | sortedBy(predicate) | sort by comparing results of the predicate function | | joinToString(s,t) | convert iterable to string, see (2) | +| joinAll() | for `Iterable`, await all items in order and collect results to [List] | | reversed() | create a list containing items from this in reverse order | | shuffled() | create a list of shuffled elements | diff --git a/docs/LaunchPool.md b/docs/LaunchPool.md index 1d0ca5d..d904b82 100644 --- a/docs/LaunchPool.md +++ b/docs/LaunchPool.md @@ -73,13 +73,13 @@ pool.closeAndJoin() ## Collecting all results -`launch` returns a `Deferred`, so you can collect results via `map`: +`launch` returns a `Deferred`, so you can collect results with `joinAll()`: ```lyng val pool = LaunchPool(4) val jobs = (1..10).map { n -> pool.launch { n * n } } pool.closeAndJoin() -val results = jobs.map { (it as Deferred).await() } +val results = jobs.joinAll() // results == [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] ``` diff --git a/docs/ai_stdlib_reference.md b/docs/ai_stdlib_reference.md index 8508336..2f3fe11 100644 --- a/docs/ai_stdlib_reference.md +++ b/docs/ai_stdlib_reference.md @@ -18,6 +18,7 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s - Async/concurrency: `launch`, `yield`, `flow`, `delay`. - `Deferred.cancel()` cancels an active task. - `Deferred.await()` throws `CancellationException` if that task was cancelled. + - `Iterable.joinAll()` awaits every deferred in iteration order and returns a `List` of results. - 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. diff --git a/docs/parallelism.md b/docs/parallelism.md index 3b5b88d..90a48e6 100644 --- a/docs/parallelism.md +++ b/docs/parallelism.md @@ -36,6 +36,17 @@ This example shows how to launch a coroutine with `launch` which returns [Deferr 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. +When you have an iterable of deferreds, use `joinAll()` to await all of them and collect results in input order: + + val jobs = (1..4).map { n -> + launch { + delay(1) + n * 10 + } + } + assertEquals([10, 20, 30, 40], jobs.joinAll()) + >>> void + If you no longer need the result, cancel the deferred. Awaiting a cancelled deferred throws `CancellationException`: var reached = false @@ -269,7 +280,7 @@ val jobs = (1..20).map { n -> } pool.closeAndJoin() // wait for all tasks to complete -val results = jobs.map { (it as Deferred).await() } +val results = jobs.joinAll() ``` Exceptions thrown inside a submitted lambda are captured in the returned `Deferred` and do not crash the pool, so other tasks continue running normally. diff --git a/docs/whats_new.md b/docs/whats_new.md index 45a4e1c..00ac065 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -1,26 +1,29 @@ # What's New in Lyng -This document highlights the current Lyng release, **1.5.4**, and the broader additions from the 1.5 cycle. +This document highlights the current Lyng release, **1.5.5**, and the broader additions from the 1.5 cycle. It is intentionally user-facing: new language features, new modules, new tools, and the practical things you can build with them. For a programmer-focused migration summary across 1.5.x, see `docs/whats_new_1_5.md`. -## Release 1.5.4 Highlights +## Release 1.5.5 Highlights -- `1.5.4` is the stabilization release for the 1.5 feature set. +- `1.5.5` extends the 1.5 line with practical database APIs, first-class calendar dates, and better coroutine building blocks. - The 1.5 line now brings together richer ranges and loops, interpolation, math modules, immutable and observable collections, richer `lyngio`, and much better CLI/IDE support. -- `1.5.4` specifically fixes user-visible issues around decimal arithmetic, mixed numeric flows, list behavior, and observable list hooks. -- `1.5.4` also fixes extension-member registration for named singleton `object` declarations, so `fun X.foo()` and `val X.bar` now work as expected. -- `1.5.4` also lets named singleton `object` declarations use scoped indexer extensions with bracket syntax, so patterns like `Storage["name"]` can be implemented with `override fun Storage.getAt(...)` / `putAt(...)`. +- `1.5.5` adds `Channel`, `LaunchPool`, and `joinAll()` so coroutine-heavy scripts can coordinate work more directly. +- `1.5.5` adds `Date`, the portable `lyng.io.db` layer, SQLite/JDBC providers, and a compatibility `lyng.legacy_digest` module. +- `1.5.5` also continues runtime/compiler hardening with better import dispatch, faster exact lambda calls, and correct `val +=`/`-=` behavior for mutating types versus real reassignment. - The docs, homepage samples, and release metadata now point at the current stable version. ## User Highlights Across 1.5.x - Descending ranges and loops with `downTo` / `downUntil` - String interpolation with `$name` and `${expr}` +- Backtick string literals for raw-ish string text - Decimal arithmetic, matrices/vectors, and complex numbers - Calendar `Date` support in `lyng.time` +- `Channel`, `LaunchPool`, and `joinAll()` for coroutine workflows - Immutable collections and opt-in `ObservableList` -- Rich `lyngio` modules for SQLite databases, console, HTTP, WebSocket, TCP, and UDP +- Rich `lyngio` modules for SQLite/JDBC databases, console, HTTP, WebSocket, TCP, and UDP +- Legacy SHA-1 compatibility helpers in `lyng.legacy_digest` - CLI improvements including the built-in formatter `lyng fmt` - Better IDE support and stronger docs around the released feature set @@ -324,7 +327,7 @@ Singleton objects are declared using the `object` keyword. They provide a conven ```lyng object Config { - val version = "1.5.4" + val version = "1.5.5" fun show() = println("Config version: " + version) } diff --git a/examples/error2.lyng b/examples/error2.lyng new file mode 100644 index 0000000..e69de29 diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 1056564..20a9c09 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.5.5-SNAPSHOT" +version = "1.5.5" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index cc7f558..7abef8d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -2881,14 +2881,18 @@ class BytecodeCompiler( val slot = resolveCapturedOwnerScopeSlot(localTarget) ?: resolveSlot(localTarget) ?: return null val targetType = slotTypes[slot] ?: SlotType.OBJ if (!localTarget.isMutable) { - if (targetType != SlotType.OBJ && targetType != SlotType.UNKNOWN) return compileEvalRef(ref) val rhs = compileRef(ref.value) ?: return compileEvalRef(ref) + if (!shouldUseAssignOpForImmutableLocal(slot, targetType, ref.op)) { + emitImmutableLocalReassignError(localTarget.name, localTarget.pos()) + return rhs + } val rhsObj = ensureObjSlot(rhs) val nameId = builder.addConst(BytecodeConst.StringVal(localTarget.name)) if (nameId > 0xFFFF) return compileEvalRef(ref) val dst = allocSlot() builder.emit(Opcode.ASSIGN_OP_OBJ, ref.op.ordinal, slot, rhsObj.slot, dst, nameId) updateSlotType(dst, SlotType.OBJ) + slotObjClass[slot]?.let { slotObjClass[dst] = it } return CompiledValue(dst, SlotType.OBJ) } var rhs = compileRef(ref.value) ?: return compileEvalRef(ref) @@ -8446,6 +8450,38 @@ class BytecodeCompiler( builder.emit(Opcode.THROW, posId, msgSlot) return msgSlot } + + private fun assignOpMethodName(op: BinOp): String? = when (op) { + BinOp.PLUS -> "plusAssign" + BinOp.MINUS -> "minusAssign" + BinOp.STAR -> "mulAssign" + BinOp.SLASH -> "divAssign" + BinOp.PERCENT -> "modAssign" + else -> null + } + + private fun knownBuiltinAssignOpSupport(cls: ObjClass, op: BinOp): Boolean? = when (cls) { + ObjList.type -> op == BinOp.PLUS || op == BinOp.MINUS + ObjObservableList.type -> op == BinOp.PLUS || op == BinOp.MINUS + ObjSet.type -> op == BinOp.PLUS + ObjMap.type -> op == BinOp.PLUS + ObjRingBuffer.type -> op == BinOp.PLUS + else -> null + } + + private fun shouldUseAssignOpForImmutableLocal(slot: Int, targetType: SlotType, op: BinOp): Boolean { + return when (targetType) { + SlotType.INT, SlotType.REAL, SlotType.BOOL -> false + SlotType.OBJ, SlotType.UNKNOWN -> { + val cls = slotObjClass[slot] ?: return true + val methodName = assignOpMethodName(op) + if (methodName != null && cls.getInstanceMemberOrNull(methodName, includeStatic = false) != null) { + return true + } + knownBuiltinAssignOpSupport(cls, op) ?: true + } + } + } private fun binaryLeft(ref: BinaryOpRef): ObjRef = ref.left private fun binaryRight(ref: BinaryOpRef): ObjRef = ref.right private fun binaryOp(ref: BinaryOpRef): BinOp = ref.op diff --git a/lynglib/src/commonTest/kotlin/WebsiteSamplesTest.kt b/lynglib/src/commonTest/kotlin/WebsiteSamplesTest.kt index aa82f0b..e3cb77b 100644 --- a/lynglib/src/commonTest/kotlin/WebsiteSamplesTest.kt +++ b/lynglib/src/commonTest/kotlin/WebsiteSamplesTest.kt @@ -104,14 +104,14 @@ class WebsiteSamplesTest { val name = "Lyng" val base = { id:, name: } // Shorthand for id: id, name: name - val full = { ...base, version: "1.5.4", status: "stable" } + val full = { ...base, version: "1.5.5", status: "stable" } full """.trimIndent()) assertTrue(result is ObjMap) val m = result.map assertEquals(101L, (m[ObjString("id")] as ObjInt).value) assertEquals("Lyng", (m[ObjString("name")] as ObjString).value) - assertEquals("1.5.4", (m[ObjString("version")] as ObjString).value) + assertEquals("1.5.5", (m[ObjString("version")] as ObjString).value) assertEquals("stable", (m[ObjString("status")] as ObjString).value) } diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/OperatorOverloadingTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/OperatorOverloadingTest.kt index 02a8924..1e47410 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/OperatorOverloadingTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/OperatorOverloadingTest.kt @@ -19,6 +19,8 @@ package net.sergeych.lyng import kotlinx.coroutines.test.runTest import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertFailsWith class OperatorOverloadingTest { @Test @@ -63,6 +65,24 @@ class OperatorOverloadingTest { """.trimIndent()) } + @Test + fun testBuiltinListPlusAssignOnVal() = runTest { + eval(""" + val list = [1, 2] + list += 3 + assertEquals([1, 2, 3], list) + """.trimIndent()) + } + + @Test + fun testBuiltinListMinusAssignOnVal() = runTest { + eval(""" + val list = [1, 2, 3] + list -= 2 + assertEquals([1, 3], list) + """.trimIndent()) + } + @Test fun testPlusAssignFallback() = runTest { eval(""" @@ -76,6 +96,130 @@ class OperatorOverloadingTest { """.trimIndent()) } + @Test + fun testPlusAssignFallbackOnValReportsReadonlyError() = runTest { + val ex = assertFailsWith { + eval(""" + class Vector(var x: Int, var y: Int) { + fun plus(other: Vector) = Vector(this.x + other.x, this.y + other.y) + fun equals(other: Vector) = this.x == other.x && this.y == other.y + } + val v = Vector(1, 2) + v += Vector(3, 4) + """.trimIndent()) + } + + assertContains(ex.errorMessage, "can't reassign val v") + } + + @Test + fun testMinusAssignOverloadingOnVal() = runTest { + eval(""" + class Counter(var n: Int) { + fun minusAssign(x: Int) { this.n = this.n - x } + } + val c = Counter(10) + c -= 3 + assertEquals(7, c.n) + """.trimIndent()) + } + + @Test + fun testMinusAssignFallbackOnValReportsReadonlyError() = runTest { + val ex = assertFailsWith { + eval(""" + class Counter(var n: Int) { + fun minus(x: Int) = Counter(this.n - x) + } + val c = Counter(10) + c -= 3 + """.trimIndent()) + } + + assertContains(ex.errorMessage, "can't reassign val c") + } + + @Test + fun testMulAssignOverloadingOnVal() = runTest { + eval(""" + class Counter(var n: Int) { + fun mulAssign(x: Int) { this.n = this.n * x } + } + val c = Counter(10) + c *= 3 + assertEquals(30, c.n) + """.trimIndent()) + } + + @Test + fun testMulAssignFallbackOnValReportsReadonlyError() = runTest { + val ex = assertFailsWith { + eval(""" + class Counter(var n: Int) { + fun times(x: Int) = Counter(this.n * x) + } + val c = Counter(10) + c *= 3 + """.trimIndent()) + } + + assertContains(ex.errorMessage, "can't reassign val c") + } + + @Test + fun testDivAssignOverloadingOnVal() = runTest { + eval(""" + class Counter(var n: Int) { + fun divAssign(x: Int) { this.n = this.n / x } + } + val c = Counter(21) + c /= 3 + assertEquals(7, c.n) + """.trimIndent()) + } + + @Test + fun testDivAssignFallbackOnValReportsReadonlyError() = runTest { + val ex = assertFailsWith { + eval(""" + class Counter(var n: Int) { + fun div(x: Int) = Counter(this.n / x) + } + val c = Counter(21) + c /= 3 + """.trimIndent()) + } + + assertContains(ex.errorMessage, "can't reassign val c") + } + + @Test + fun testModAssignOverloadingOnVal() = runTest { + eval(""" + class Counter(var n: Int) { + fun modAssign(x: Int) { this.n = this.n % x } + } + val c = Counter(23) + c %= 5 + assertEquals(3, c.n) + """.trimIndent()) + } + + @Test + fun testModAssignFallbackOnValReportsReadonlyError() = runTest { + val ex = assertFailsWith { + eval(""" + class Counter(var n: Int) { + fun mod(x: Int) = Counter(this.n % x) + } + val c = Counter(23) + c %= 5 + """.trimIndent()) + } + + assertContains(ex.errorMessage, "can't reassign val c") + } + @Test fun testCompareOverloading() = runTest { eval(""" diff --git a/site/src/jsMain/kotlin/HomePage.kt b/site/src/jsMain/kotlin/HomePage.kt index 0d594ab..dfed20a 100644 --- a/site/src/jsMain/kotlin/HomePage.kt +++ b/site/src/jsMain/kotlin/HomePage.kt @@ -159,7 +159,7 @@ fun HomePage() { val id = 101 val name = "Lyng" val base = { id:, name: } - val full = { ...base, version: "1.5.4", status: "stable", tags: ["typed", "portable"] } + val full = { ...base, version: "1.5.5", status: "stable", tags: ["typed", "portable"] } println(full) """.trimIndent()