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.

This commit is contained in:
Sergey Chernov 2025-12-23 08:33:34 +01:00
parent 5f3a54d08f
commit 3f235878c0
12 changed files with 220 additions and 77 deletions

View File

@ -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)

View File

@ -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

View File

@ -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>) : 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<MyCollection>().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>) : 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 `<reified T> 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.

View File

@ -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]

View File

@ -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]

View File

@ -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..<end) {
loopVar.value = ObjInt(i)
loopVar.value = ObjInt.of(i)
try {
result = body.execute(forScope)
} catch (lbe: LoopBreakContinueException) {
@ -2100,7 +2100,7 @@ class Compiler(
}
} else {
for (i in start..<end) {
loopVar.value = ObjInt(i)
loopVar.value = ObjInt.of(i)
result = body.execute(forScope)
}
}
@ -2112,38 +2112,33 @@ class Compiler(
body: Statement, elseStatement: Statement?, label: String?,
catchBreak: Boolean
): Obj {
val iterObj = sourceObj.invokeInstanceMethod(forScope, "iterator")
var result: Obj = ObjVoid
var completedNaturally = false
try {
while (iterObj.invokeInstanceMethod(forScope, "hasNext").toBool()) {
if (catchBreak)
try {
loopVar.value = iterObj.invokeInstanceMethod(forScope, "next")
result = body.execute(forScope)
} catch (lbe: LoopBreakContinueException) {
if (lbe.label == label || lbe.label == null) {
if (lbe.doContinue) continue
// premature finish, will trigger cancel in finally
return lbe.result
}
throw lbe
}
else {
loopVar.value = iterObj.invokeInstanceMethod(forScope, "next")
var breakCaught = false
sourceObj.enumerate(forScope) { item ->
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")

View File

@ -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)

View File

@ -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())
fun Int.toObj() = ObjInt.of(this.toLong())
fun Long.toObj() = ObjInt.of(this)

View File

@ -76,36 +76,3 @@ fun Obj.toFlow(scope: Scope): Flow<Obj> = 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 }
}
}
}

View File

@ -140,6 +140,12 @@ class ObjList(val list: MutableList<Obj> = 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

View File

@ -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..<e) {
if (!callback(ObjInt.of(i))) break
}
}
} else if (isCharRange) {
val s = (start as ObjChar).value
val e = (end as ObjChar).value
if (isEndInclusive) {
for (c in s..e) {
if (!callback(ObjChar(c))) break
}
} else {
for (c in s..<e) {
if (!callback(ObjChar(c))) break
}
}
} else {
super.enumerate(scope, callback)
}
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
return (other as? ObjRange)?.let {
if( start == other.start && end == other.end ) 0 else -1

View File

@ -34,6 +34,12 @@ class ObjSet(val set: MutableSet<Obj> = 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)