From 3f235878c06c8593f0d0dfd1ed3381a4d15e111e Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 23 Dec 2025 08:33:34 +0100 Subject: [PATCH] Add `EfficientIterables.md` documentation for Kotlin interop. Optimize `ObjInt` operations by integrating value caching and updating number operations. Introduce high-performance `enumerate` method for `ObjList`, `ObjSet`, `ObjRange`, and custom iterables. Update compiler and loop handling for improved enumeration logic. --- README.md | 1 + docs/Collection.md | 2 +- docs/EfficientIterables.md | 92 +++++++++++++++++++ docs/Iterable.md | 2 + docs/Iterator.md | 2 + .../kotlin/net/sergeych/lyng/Compiler.kt | 57 ++++++------ .../kotlin/net/sergeych/lyng/obj/Obj.kt | 38 +++++++- .../kotlin/net/sergeych/lyng/obj/ObjInt.kt | 28 ++++-- .../sergeych/lyng/obj/ObjKotlinIterator.kt | 33 ------- .../kotlin/net/sergeych/lyng/obj/ObjList.kt | 6 ++ .../kotlin/net/sergeych/lyng/obj/ObjRange.kt | 30 ++++++ .../kotlin/net/sergeych/lyng/obj/ObjSet.kt | 6 ++ 12 files changed, 220 insertions(+), 77 deletions(-) create mode 100644 docs/EfficientIterables.md diff --git a/README.md b/README.md index c905651..4a80c02 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ and it is multithreaded on platforms supporting it (automatically, no code chang - [Language home](https://lynglang.com) - [introduction and tutorial](docs/tutorial.md) - start here please - [Testing and Assertions](docs/Testing.md) +- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md) - [Samples directory](docs/samples) - [Formatter (core + CLI + IDE)](docs/formatter.md) - [Books directory](docs) diff --git a/docs/Collection.md b/docs/Collection.md index f4f7820..0fbd3d8 100644 --- a/docs/Collection.md +++ b/docs/Collection.md @@ -12,7 +12,7 @@ Is a [Iterable] with known `size`, a finite [Iterable]: (1) : `comparator(a,b)` should return -1 if `a < b`, +1 if `a > b` or zero. -See [List], [Set] and [Iterable] +See [List], [Set], [Iterable] and [Efficient Iterables in Kotlin Interop](EfficientIterables.md) [Iterable]: Iterable.md [List]: List.md diff --git a/docs/EfficientIterables.md b/docs/EfficientIterables.md new file mode 100644 index 0000000..55ede28 --- /dev/null +++ b/docs/EfficientIterables.md @@ -0,0 +1,92 @@ +# Efficient Iterables in Kotlin Interop + +Lyng provides high-performance iteration mechanisms that allow Kotlin-side code to interact with Lyng iterables efficiently and vice versa. + +## 1. Enumerating Lyng Objects from Kotlin + +To iterate over a Lyng object (like a `List`, `Set`, or `Range`) from Kotlin code, use the virtual `enumerate` method: + +```kotlin +val lyngList: Obj = ... +lyngList.enumerate(scope) { item -> + println("Processing $item") + true // return true to continue, false to break +} +``` + +### Why it's efficient: +- **Zero allocation**: Unlike traditional iterators, it doesn't create a `LyngIterator` object or any intermediate wrappers. +- **Direct access**: Subclasses like `ObjList` override `enumerate` to iterate directly over their internal Kotlin collections. +- **Reduced overhead**: It avoids multiple `invokeInstanceMethod` calls for `hasNext()` and `next()` on every step, which would normally involve dynamic dispatch and scope overhead. + +## 2. Reactive Enumeration with Flow + +If you prefer a reactive approach or need to integrate with Kotlin Coroutines flows, use `toFlow()`: + +```kotlin +lyngList.toFlow(scope).collect { item -> + // ... +} +``` +*Note: `toFlow()` internally uses the Lyng iterator protocol (`iterator()`, `hasNext()`, `next()`), so it's slightly less efficient than `enumerate()` for performance-critical loops, but more idiomatic for flow-based processing.* + +## 3. Creating Efficient Iterables for Lyng in Kotlin + +When implementing a custom object in Kotlin that should be iterable in Lyng (e.g., usable in `for (x in myObj) { ... }`), follow these steps to ensure maximum performance. + +### A. Inherit from `Obj` and use `ObjIterable` +Ensure your object's class has `ObjIterable` as a parent so the Lyng compiler recognizes it as an iterable. + +```kotlin +class MyCollection(val items: List) : Obj() { + override val objClass = MyCollection.type + + companion object { + val type = ObjClass("MyCollection", ObjIterable).apply { + // Provide a Lyng-side iterator for compatibility with + // manual iterator usage in Lyng scripts. + // Using ObjKotlinObjIterator if items are already Obj instances: + addFn("iterator") { + ObjKotlinObjIterator(thisAs().items.iterator()) + } + } + } +} +``` + +### B. Override `enumerate` for Maximum Performance +The Lyng compiler's `for` loops use the `enumerate` method. By overriding it in your Kotlin class, you provide a "fast path" for iteration. + +```kotlin +class MyCollection(val items: List) : Obj() { + // ... + override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) { + for (item in items) { + // If callback returns false, it means 'break' was called in Lyng + if (!callback(item)) break + } + } +} +``` + +### C. Use `ObjInt.of()` for Numeric Data +If your iterable contains integers, always use `ObjInt.of(Long)` instead of the `ObjInt(Long)` constructor. Lyng maintains a cache for small integers (-128 to 127), which significantly reduces object allocations and GC pressure during tight loops. + +```kotlin +// Efficiently creating an integer object +val obj = ObjInt.of(42L) + +// Or using extension methods which also use the cache: +val obj2 = 42.toObj() +val obj3 = 42L.toObj() +``` + +#### Note on `toObj()` extensions: +While ` T.toObj()` is convenient, using specific extensions like `Int.toObj()` or `Long.toObj()` is slightly more efficient as they use the `ObjInt` cache. + +## 4. Summary of Best Practices + +- **To Consume**: Use `enumerate(scope) { item -> ... true }`. +- **To Implement**: Override `enumerate` in your `Obj` subclass. +- **To Register**: Use `ObjIterable` (or `ObjCollection`) as a parent class in your `ObjClass` definition. +- **To Optimize**: Use `ObjInt.of()` (or `.toObj()`) for all integer object allocations. diff --git a/docs/Iterable.md b/docs/Iterable.md index 30e0070..8a992ff 100644 --- a/docs/Iterable.md +++ b/docs/Iterable.md @@ -142,6 +142,8 @@ optional function applied to each item that must return result string for an ite fun iterator(): Iterator +For high-performance Kotlin-side interop and custom iterable implementation details, see [Efficient Iterables in Kotlin Interop](EfficientIterables.md). + ## Included in interfaces: - [Collection], Array, [List] diff --git a/docs/Iterator.md b/docs/Iterator.md index b4c0cbf..2f79311 100644 --- a/docs/Iterator.md +++ b/docs/Iterator.md @@ -23,6 +23,8 @@ must throw `ObjIterationFinishedError`. Iterators are returned when implementing [Iterable] interface. +For high-performance Kotlin-side interop and custom iterable implementation details, see [Efficient Iterables in Kotlin Interop](EfficientIterables.md). + ## Implemented for classes: - [List], [Range] diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index e42743d..d5f5f27 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -2040,7 +2040,7 @@ class Compiler( var breakCaught = false if (size > 0) { - var current = runCatching { sourceObj.getAt(forContext, ObjInt(0)) } + var current = runCatching { sourceObj.getAt(forContext, ObjInt.of(0)) } .getOrElse { throw ScriptError( tOp.pos, @@ -2065,7 +2065,7 @@ class Compiler( throw lbe } if (++index >= size) break - current = sourceObj.getAt(forContext, ObjInt(index.toLong())) + current = sourceObj.getAt(forContext, ObjInt.of(index.toLong())) } } if (!breakCaught && elseStatement != null) { @@ -2087,7 +2087,7 @@ class Compiler( var result: Obj = ObjVoid if (catchBreak) { for (i in start.. + loopVar.value = item + if (catchBreak) { + try { result = body.execute(forScope) + true + } catch (lbe: LoopBreakContinueException) { + if (lbe.label == label || lbe.label == null) { + if (lbe.doContinue) true + else { + result = lbe.result + breakCaught = true + false + } + } else + throw lbe } - } - completedNaturally = true - return elseStatement?.execute(forScope) ?: result - } finally { - if (!completedNaturally) { - // Best-effort cancellation on premature termination - runCatching { - iterObj.invokeInstanceMethod(forScope, "cancelIteration") { ObjVoid } - } + } else { + result = body.execute(forScope) + true } } + return if (!breakCaught && elseStatement != null) { + elseStatement.execute(forScope) + } else result } @Suppress("UNUSED_VARIABLE") diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index 8aac2cb..0c1f573 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -127,6 +127,40 @@ open class Obj { return invokeInstanceMethod(scope, "contains", other).toBool() } + /** + * Call [callback] for each element of this obj considering it provides [Iterator] + * methods `hasNext` and `next`. + * + * IF callback returns false, iteration is stopped. + */ + open suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) { + val iterator = invokeInstanceMethod(scope, "iterator") + val hasNext = iterator.getInstanceMethod(scope, "hasNext") + val next = iterator.getInstanceMethod(scope, "next") + var closeIt = false + try { + while (hasNext.invoke(scope, iterator).toBool()) { + val nextValue = next.invoke(scope, iterator) + val shouldContinue = try { + callback(nextValue) + } catch (e: Exception) { + // iteration aborted due to exception in callback + closeIt = true + throw e + } + if (!shouldContinue) { + closeIt = true + break + } + } + } finally { + if (closeIt) { + // Best-effort cancel on premature termination + iterator.invokeInstanceMethod(scope, "cancelIteration") { ObjVoid } + } + } + } + /** * Default toString implementation: * @@ -444,8 +478,8 @@ open class Obj { is Obj -> obj is Double -> ObjReal(obj) is Float -> ObjReal(obj.toDouble()) - is Int -> ObjInt(obj.toLong()) - is Long -> ObjInt(obj) + is Int -> ObjInt.of(obj.toLong()) + is Long -> ObjInt.of(obj) is String -> ObjString(obj) is CharSequence -> ObjString(obj.toString()) is Boolean -> ObjBool(obj) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt index b930fed..d573c53 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt @@ -45,11 +45,11 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu } override suspend fun incrementAndGet(scope: Scope): Obj { - return ObjInt(value + 1) + return of(value + 1) } override suspend fun decrementAndGet(scope: Scope): Obj { - return ObjInt(value - 1) + return of(value - 1) } override suspend fun compareTo(scope: Scope, other: Obj): Int { @@ -63,29 +63,29 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu override suspend fun plus(scope: Scope, other: Obj): Obj = if (other is ObjInt) - ObjInt(this.value + other.value) + of(this.value + other.value) else ObjReal(this.doubleValue + other.toDouble()) override suspend fun minus(scope: Scope, other: Obj): Obj = if (other is ObjInt) - ObjInt(this.value - other.value) + of(this.value - other.value) else ObjReal(this.doubleValue - other.toDouble()) override suspend fun mul(scope: Scope, other: Obj): Obj = if (other is ObjInt) { - ObjInt(this.value * other.value) + of(this.value * other.value) } else ObjReal(this.value * other.toDouble()) override suspend fun div(scope: Scope, other: Obj): Obj = if (other is ObjInt) - ObjInt(this.value / other.value) + of(this.value / other.value) else ObjReal(this.value / other.toDouble()) override suspend fun mod(scope: Scope, other: Obj): Obj = if (other is ObjInt) - ObjInt(this.value % other.value) + of(this.value % other.value) else ObjReal(this.value.toDouble() % other.toDouble()) /** @@ -158,8 +158,15 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu } companion object { - val Zero = ObjInt(0, true) - val One = ObjInt(1, true) + private val cache = Array(256) { ObjInt((it - 128).toLong(), true) } + + fun of(value: Long): ObjInt { + return if (value in -128L..127L) cache[(value + 128).toInt()] + else ObjInt(value) + } + + val Zero = of(0) + val One = of(1) val type = object : ObjClass("Int") { override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = when (lynonType) { @@ -178,4 +185,5 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu } } -fun Int.toObj() = ObjInt(this.toLong()) \ No newline at end of file +fun Int.toObj() = ObjInt.of(this.toLong()) +fun Long.toObj() = ObjInt.of(this) \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjKotlinIterator.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjKotlinIterator.kt index aca67c0..8da408b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjKotlinIterator.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjKotlinIterator.kt @@ -76,36 +76,3 @@ fun Obj.toFlow(scope: Scope): Flow = flow { } } -/** - * Call [callback] for each element of this obj considering it provides [Iterator] - * methods `hasNext` and `next`. - * - * IF callback returns false, iteration is stopped. - */ -suspend fun Obj.enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) { - val iterator = invokeInstanceMethod(scope, "iterator") - val hasNext = iterator.getInstanceMethod(scope, "hasNext") - val next = iterator.getInstanceMethod(scope, "next") - var closeIt = false - try { - while (hasNext.invoke(scope, iterator).toBool()) { - val nextValue = next.invoke(scope, iterator) - val shouldContinue = try { - callback(nextValue) - } catch (e: Exception) { - // iteration aborted due to exception in callback - closeIt = true - throw e - } - if (!shouldContinue) { - closeIt = true - break - } - } - } finally { - if (closeIt) { - // Best-effort cancel on premature termination - iterator.invokeInstanceMethod(scope, "cancelIteration") { ObjVoid } - } - } -} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt index 909d60d..acbf999 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt @@ -140,6 +140,12 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { return list.contains(other) } + override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) { + for (item in list) { + if (!callback(item)) break + } + } + override val objClass: ObjClass get() = type diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt index 6bc7e59..4656cb4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt @@ -114,6 +114,36 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob start is ObjChar && end is ObjChar } + override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) { + if (isIntRange) { + val s = (start as ObjInt).value + val e = (end as ObjInt).value + if (isEndInclusive) { + for (i in s..e) { + if (!callback(ObjInt.of(i))) break + } + } else { + for (i in s.. = mutableSetOf()) : Obj() { return set.contains(other) } + override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) { + for (item in set) { + if (!callback(item)) break + } + } + override suspend fun plus(scope: Scope, other: Obj): Obj { return ObjSet( if (other is ObjSet)