diff --git a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/BenchmarksAndroid.kt b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/BenchmarksAndroid.kt new file mode 100644 index 0000000..27ec22e --- /dev/null +++ b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/BenchmarksAndroid.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.sergeych.lyng + +actual object Benchmarks { + actual val enabled: Boolean = run { + val p = System.getProperty("LYNG_BENCHMARKS")?.lowercase() + val e = System.getenv("BENCHMARKS")?.lowercase() + fun parse(v: String?): Boolean = + v == "true" || v == "1" || v == "yes" + parse(p) || parse(e) + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt index 1f91482..2791333 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Arguments.kt @@ -37,7 +37,7 @@ data class ParsedArgument( count++ if (count > limit) break } - if (!hasSplatOrNamed && count == this.size) { + if (!hasSplatOrNamed && count == this.size) { val quick = when (count) { 0 -> Arguments.EMPTY 1 -> Arguments(listOf(this.elementAt(0).value.execute(scope)), tailBlockMode) @@ -154,11 +154,11 @@ data class ParsedArgument( else -> null } if (quick != null) return quick - } + } } // General path: build positional list and named map, enforcing ordering rules - val positional: MutableList = mutableListOf() + val positional: MutableList = mutableListOf() var named: MutableMap? = null var namedSeen = false for ((idx, x) in this.withIndex()) { @@ -254,4 +254,3 @@ data class ParsedArgument( fun from(values: Collection) = Arguments(values.toList()) } } - diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Benchmarks.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Benchmarks.kt new file mode 100644 index 0000000..3fd2373 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Benchmarks.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.sergeych.lyng + +expect object Benchmarks { + val enabled: Boolean +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 35a982a..01f2e90 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -413,11 +413,7 @@ class Compiler( private suspend fun parseExpression(): Statement? { val pos = cc.currentPos() return parseExpressionLevel()?.let { ref -> - val stmtPos = pos - object : Statement() { - override val pos: Pos = stmtPos - override suspend fun execute(scope: Scope): Obj = ref.evalValue(scope) - } + ExpressionStatement(ref, pos) } } @@ -722,11 +718,18 @@ class Compiler( null else parseExpression() - operand = RangeRef( - left, - right?.let { StatementRef(it) }, - isEndInclusive - ) + val rightRef = right?.let { StatementRef(it) } + if (left != null && rightRef != null) { + val lConst = constIntValueOrNull(left) + val rConst = constIntValueOrNull(rightRef) + if (lConst != null && rConst != null) { + operand = ConstRef(ObjRange(ObjInt.of(lConst), ObjInt.of(rConst), isEndInclusive).asReadonly) + } else { + operand = RangeRef(left, rightRef, isEndInclusive) + } + } else { + operand = RangeRef(left, rightRef, isEndInclusive) + } } Token.Type.LBRACE, Token.Type.NULL_COALESCE_BLOCKINVOKE -> { @@ -2467,39 +2470,68 @@ class Compiler( // So we parse an expression explicitly and wrap it into a StatementRef. val exprAfterIn = parseExpression() ?: throw ScriptError(start, "Bad for statement: expected expression") val source: Statement = exprAfterIn + val constRange = (exprAfterIn as? ExpressionStatement)?.ref?.let { ref -> + constIntRangeOrNull(ref) + } ensureRparen() // Expose the loop variable name to the parser so identifiers inside the loop body // can be emitted as FastLocalVarRef when enabled. val namesForLoop = (currentLocalNames?.toSet() ?: emptySet()) + tVar.value - val (canBreak, body, elseStatement) = withLocalNames(namesForLoop) { - val loopParsed = cc.parseLoop { - if (cc.current().type == Token.Type.LBRACE) parseBlock() - else parseStatement() ?: throw ScriptError(start, "Bad for statement: expected loop body") + val loopSlotPlan = SlotPlan(mutableMapOf(), 0) + slotPlanStack.add(loopSlotPlan) + declareSlotName(tVar.value) + val (canBreak, body, elseStatement) = try { + withLocalNames(namesForLoop) { + val loopParsed = cc.parseLoop { + if (cc.current().type == Token.Type.LBRACE) parseBlock() + else parseStatement() ?: throw ScriptError(start, "Bad for statement: expected loop body") + } + // possible else clause + cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) + val elseStmt = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) { + parseStatement() + } else { + cc.previous() + null + } + Triple(loopParsed.first, loopParsed.second, elseStmt) } - // possible else clause - cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) - val elseStmt = if (cc.next().let { it.type == Token.Type.ID && it.value == "else" }) { - parseStatement() - } else { - cc.previous() - null - } - Triple(loopParsed.first, loopParsed.second, elseStmt) + } finally { + slotPlanStack.removeLast() } + val loopSlotPlanSnapshot = if (loopSlotPlan.slots.isEmpty()) emptyMap() else loopSlotPlan.slots.toMap() return object : Statement() { override val pos: Pos = body.pos override suspend fun execute(scope: Scope): Obj { val forContext = scope.createChildScope(start) + if (loopSlotPlanSnapshot.isNotEmpty()) { + forContext.applySlotPlan(loopSlotPlanSnapshot) + } // loop var: StoredObject val loopSO = forContext.addItem(tVar.value, true, ObjNull) + if (constRange != null && PerfFlags.PRIMITIVE_FASTOPS) { + val loopSlotIndex = forContext.getSlotIndexOf(tVar.value) ?: -1 + return loopIntRange( + forContext, + constRange.start, + constRange.endExclusive, + loopSO, + loopSlotIndex, + body, + elseStatement, + label, + canBreak + ) + } // insofar we suggest source object is enumerable. Later we might need to add checks val sourceObj = source.execute(forContext) if (sourceObj is ObjRange && sourceObj.isIntRange && PerfFlags.PRIMITIVE_FASTOPS) { + val loopSlotIndex = forContext.getSlotIndexOf(tVar.value) ?: -1 return loopIntRange( forContext, sourceObj.start!!.toLong(), @@ -2508,6 +2540,7 @@ class Compiler( else sourceObj.end!!.toLong(), loopSO, + loopSlotIndex, body, elseStatement, label, @@ -2571,32 +2604,91 @@ class Compiler( } private suspend fun loopIntRange( - forScope: Scope, start: Long, end: Long, loopVar: ObjRecord, + forScope: Scope, start: Long, end: Long, loopVar: ObjRecord, loopSlotIndex: Int, body: Statement, elseStatement: Statement?, label: String?, catchBreak: Boolean ): Obj { var result: Obj = ObjVoid + val cacheLow = ObjInt.CACHE_LOW + val cacheHigh = ObjInt.CACHE_HIGH + val useCache = start >= cacheLow && end <= cacheHigh + 1 + val cache = if (useCache) ObjInt.cacheArray() else null + val useSlot = loopSlotIndex >= 0 if (catchBreak) { - for (i in start.. (ref.constValue as? ObjInt)?.value + is StatementRef -> { + val stmt = ref.statement + if (stmt is ExpressionStatement) constIntValueOrNull(stmt.ref) else null + } + else -> null + } + } + private suspend fun loopIterable( forScope: Scope, sourceObj: Obj, loopVar: ObjRecord, body: Statement, elseStatement: Statement?, label: String?, 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 0671102..ae21cac 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt @@ -55,7 +55,11 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu override suspend fun compareTo(scope: Scope, other: Obj): Int { if (other !is Numeric) return -2 - return value.compareTo(other.doubleValue) + return if (other is ObjInt) { + value.compareTo(other.value) + } else { + doubleValue.compareTo(other.doubleValue) + } } override fun toString(): String = value.toString() @@ -159,13 +163,19 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu } companion object { - private val cache = Array(256) { ObjInt((it - 128).toLong(), true) } + internal const val CACHE_LOW: Long = -1024L + internal const val CACHE_HIGH: Long = 1023L + private val cache = Array((CACHE_HIGH - CACHE_LOW + 1).toInt()) { + ObjInt((it + CACHE_LOW).toLong(), true) + } fun of(value: Long): ObjInt { - return if (value in -128L..127L) cache[(value + 128).toInt()] + return if (value in CACHE_LOW..CACHE_HIGH) cache[(value - CACHE_LOW).toInt()] else ObjInt(value) } + internal fun cacheArray(): Array = cache + val Zero = of(0) val One = of(1) val type = object : ObjClass("Int") { @@ -192,4 +202,4 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu } fun Int.toObj() = ObjInt.of(this.toLong()) -fun Long.toObj() = ObjInt.of(this) \ No newline at end of file +fun Long.toObj() = ObjInt.of(this) 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 0c631d0..dc61c66 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt @@ -96,6 +96,30 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob if (other is ObjRange) return containsRange(scope, other) + if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) { + if (start is ObjInt && end is ObjInt && other is ObjInt) { + val s = start.value + val e = end.value + val v = other.value + if (v < s) return false + return if (isEndInclusive) v <= e else v < e + } + if (start is ObjChar && end is ObjChar && other is ObjChar) { + val s = start.value + val e = end.value + val v = other.value + if (v < s) return false + return if (isEndInclusive) v <= e else v < e + } + if (start is ObjString && end is ObjString && other is ObjString) { + val s = start.value + val e = end.value + val v = other.value + if (v < s) return false + return if (isEndInclusive) v <= e else v < e + } + } + if (start == null && end == null) return true if (start != null) { if (start.compareTo(scope, other) > 0) return false @@ -241,4 +265,3 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob } } } - diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt index faa5dd5..5a38285 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRangeIterator.kt @@ -58,7 +58,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() { start.value.code.toLong() + nextIndex++ else scope.raiseError("iterator error: unsupported range start") - if( isCharRange ) ObjChar(x.toInt().toChar()) else ObjInt(x) + if (isCharRange) ObjChar(x.toInt().toChar()) else ObjInt.of(x) } else { scope.raiseError(ObjIterationFinishedException(scope)) @@ -83,13 +83,18 @@ class ObjRangeIterator(val self: ObjRange) : Obj() { class ObjFastIntRangeIterator(private val start: Int, private val endExclusive: Int) : Obj() { private var cur: Int = start + private val cacheLow = ObjInt.CACHE_LOW.toInt() + private val useCache = start >= cacheLow && endExclusive <= ObjInt.CACHE_HIGH.toInt() + 1 + private val cache = if (useCache) ObjInt.cacheArray() else null override val objClass: ObjClass get() = type fun hasNext(): Boolean = cur < endExclusive fun next(scope: Scope): Obj = - if (cur < endExclusive) ObjInt(cur++.toLong()) + if (cur < endExclusive) { + if (useCache && cache != null) cache[cur++ - cacheLow] else ObjInt(cur++.toLong()) + } else scope.raiseError(ObjIterationFinishedException(scope)) companion object { @@ -98,4 +103,4 @@ class ObjFastIntRangeIterator(private val start: Int, private val endExclusive: addFn("next") { thisAs().next(this) } } } -} \ No newline at end of file +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index 2dd0ae6..1e7f770 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -152,6 +152,80 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r // Primitive fast paths for common cases (guarded by PerfFlags.PRIMITIVE_FASTOPS) if (PerfFlags.PRIMITIVE_FASTOPS) { + // Fast membership for common containers + if (op == BinOp.IN || op == BinOp.NOTIN) { + val inResult: Boolean? = when (b) { + is ObjList -> { + if (a is ObjInt) { + var i = 0 + val sz = b.list.size + var found = false + while (i < sz) { + val v = b.list[i] + if (v is ObjInt && v.value == a.value) { + found = true + break + } + i++ + } + found + } else { + b.list.contains(a) + } + } + is ObjSet -> b.set.contains(a) + is ObjMap -> b.map.containsKey(a) + is ObjRange -> { + when (a) { + is ObjInt -> { + val s = b.start as? ObjInt + val e = b.end as? ObjInt + val v = a.value + if (s == null && e == null) null + else { + if (s != null && v < s.value) false + else if (e != null) if (b.isEndInclusive) v <= e.value else v < e.value else true + } + } + is ObjChar -> { + val s = b.start as? ObjChar + val e = b.end as? ObjChar + val v = a.value + if (s == null && e == null) null + else { + if (s != null && v < s.value) false + else if (e != null) if (b.isEndInclusive) v <= e.value else v < e.value else true + } + } + is ObjString -> { + val s = b.start as? ObjString + val e = b.end as? ObjString + val v = a.value + if (s == null && e == null) null + else { + if (s != null && v < s.value) false + else if (e != null) if (b.isEndInclusive) v <= e.value else v < e.value else true + } + } + else -> null + } + } + is ObjString -> when (a) { + is ObjString -> b.value.contains(a.value) + is ObjChar -> b.value.contains(a.value) + else -> null + } + else -> null + } + if (inResult != null) { + if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++ + return if (op == BinOp.IN) { + if (inResult) ObjTrue else ObjFalse + } else { + if (inResult) ObjFalse else ObjTrue + } + } + } // Fast boolean ops when both operands are ObjBool if (a is ObjBool && b is ObjBool) { val r: Obj? = when (op) { @@ -604,7 +678,37 @@ class AssignOpRef( else -> null } if (inPlace != null) return inPlace.asReadonly - val result: Obj = when (op) { + val fast: Obj? = if (PerfFlags.PRIMITIVE_FASTOPS) { + when { + x is ObjInt && y is ObjInt -> { + val xv = x.value + val yv = y.value + when (op) { + BinOp.PLUS -> ObjInt.of(xv + yv) + BinOp.MINUS -> ObjInt.of(xv - yv) + BinOp.STAR -> ObjInt.of(xv * yv) + BinOp.SLASH -> if (yv != 0L) ObjInt.of(xv / yv) else null + BinOp.PERCENT -> if (yv != 0L) ObjInt.of(xv % yv) else null + else -> null + } + } + (x is ObjInt || x is ObjReal) && (y is ObjInt || y is ObjReal) -> { + val xv = if (x is ObjInt) x.doubleValue else (x as ObjReal).value + val yv = if (y is ObjInt) y.doubleValue else (y as ObjReal).value + when (op) { + BinOp.PLUS -> ObjReal.of(xv + yv) + BinOp.MINUS -> ObjReal.of(xv - yv) + BinOp.STAR -> ObjReal.of(xv * yv) + BinOp.SLASH -> ObjReal.of(xv / yv) + BinOp.PERCENT -> ObjReal.of(xv % yv) + else -> null + } + } + x is ObjString && op == BinOp.PLUS -> ObjString(x.value + y.toString()) + else -> null + } + } else null + val result: Obj = fast ?: when (op) { BinOp.PLUS -> x.plus(scope, y) BinOp.MINUS -> x.minus(scope, y) BinOp.STAR -> x.mul(scope, y) @@ -632,7 +736,15 @@ class IncDecRef( // We now treat numbers as immutable and always perform write-back via setAt. // This avoids issues where literals are shared and mutated in-place. // For post-inc: return ORIGINAL value; for pre-inc: return NEW value. - val result = if (isIncrement) v.plus(scope, one) else v.minus(scope, one) + val result = if (PerfFlags.PRIMITIVE_FASTOPS) { + when (v) { + is ObjInt -> if (isIncrement) ObjInt.of(v.value + 1L) else ObjInt.of(v.value - 1L) + is ObjReal -> if (isIncrement) ObjReal.of(v.value + 1.0) else ObjReal.of(v.value - 1.0) + else -> if (isIncrement) v.plus(scope, one) else v.minus(scope, one) + } + } else { + if (isIncrement) v.plus(scope, one) else v.minus(scope, one) + } target.setAt(atPos, scope, result) return (if (isPost) v else result).asReadonly } @@ -1447,7 +1559,7 @@ class IndexRef( /** * R-value reference that wraps a Statement (used during migration for expressions parsed as Statement). */ -class StatementRef(private val statement: Statement) : ObjRef { +class StatementRef(internal val statement: Statement) : ObjRef { override suspend fun get(scope: Scope): ObjRecord = statement.execute(scope).asReadonly } @@ -2283,7 +2395,19 @@ class LocalSlotRef( private var cachedOwnerVerified: Boolean = false private fun resolveOwner(scope: Scope): Scope? { - if (cachedOwner != null && cachedFrameId == scope.frameId && cachedOwnerVerified) return cachedOwner + if (cachedOwner != null && cachedFrameId == scope.frameId && cachedOwnerVerified) { + val cached = cachedOwner!! + val candidate = if (depth == 0) scope else { + var s: Scope? = scope + var remaining = depth + while (s != null && remaining > 0) { + s = s.parent + remaining-- + } + s + } + if (candidate === cached && candidate?.getSlotIndexOf(name) == slot) return cached + } var s: Scope? = scope var remaining = depth while (s != null && remaining > 0) { @@ -2475,9 +2599,9 @@ class MapLiteralRef(private val entries: List) : ObjRef { * Range literal: left .. right or left ..< right. Right may be omitted in certain contexts. */ class RangeRef( - private val left: ObjRef?, - private val right: ObjRef?, - private val isEndInclusive: Boolean + internal val left: ObjRef?, + internal val right: ObjRef?, + internal val isEndInclusive: Boolean ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { return evalValue(scope).asReadonly diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt index f24b3ca..d22068b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt @@ -63,6 +63,13 @@ abstract class Statement( } +class ExpressionStatement( + val ref: net.sergeych.lyng.obj.ObjRef, + override val pos: Pos +) : Statement() { + override suspend fun execute(scope: Scope): Obj = ref.evalValue(scope) +} + fun Statement.raise(text: String): Nothing { throw ScriptError(pos, text) } diff --git a/lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt b/lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt new file mode 100644 index 0000000..aaa975b --- /dev/null +++ b/lynglib/src/commonTest/kotlin/NestedRangeBenchmarkTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.Benchmarks +import net.sergeych.lyng.eval +import net.sergeych.lyng.obj.ObjInt +import kotlin.time.TimeSource +import kotlin.test.Test +import kotlin.test.assertEquals + +class NestedRangeBenchmarkTest { + @Test + fun benchmarkHappyNumbersNestedRanges() = runTest { + if (!Benchmarks.enabled) return@runTest + val script = """ + fun naiveCountHappyNumbers() { + var count = 0 + for( n1 in 0..9 ) + for( n2 in 0..9 ) + for( n3 in 0..9 ) + for( n4 in 0..9 ) + for( n5 in 0..9 ) + for( n6 in 0..9 ) + if( n1 + n2 + n3 == n4 + n5 + n6 ) count++ + count + } + naiveCountHappyNumbers() + """.trimIndent() + + val start = TimeSource.Monotonic.markNow() + val result = eval(script) as ObjInt + val elapsedMs = start.elapsedNow().inWholeMilliseconds + println("[DEBUG_LOG] [BENCH] nested-happy elapsed=${elapsedMs} ms") + assertEquals(55252L, result.value) + } +} diff --git a/lynglib/src/jsMain/kotlin/net/sergeych/lyng/BenchmarksJs.kt b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/BenchmarksJs.kt new file mode 100644 index 0000000..81501e1 --- /dev/null +++ b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/BenchmarksJs.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.sergeych.lyng + +actual object Benchmarks { + actual val enabled: Boolean = false +} diff --git a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/BenchmarksJvm.kt b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/BenchmarksJvm.kt new file mode 100644 index 0000000..27ec22e --- /dev/null +++ b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/BenchmarksJvm.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.sergeych.lyng + +actual object Benchmarks { + actual val enabled: Boolean = run { + val p = System.getProperty("LYNG_BENCHMARKS")?.lowercase() + val e = System.getenv("BENCHMARKS")?.lowercase() + fun parse(v: String?): Boolean = + v == "true" || v == "1" || v == "yes" + parse(p) || parse(e) + } +} diff --git a/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/BenchmarksNative.kt b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/BenchmarksNative.kt new file mode 100644 index 0000000..64f445b --- /dev/null +++ b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/BenchmarksNative.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.sergeych.lyng + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import platform.posix.getenv + +@OptIn(ExperimentalForeignApi::class) +actual object Benchmarks { + actual val enabled: Boolean = run { + fun parse(v: String?): Boolean = + v == "true" || v == "1" || v == "yes" + val b = getenv("BENCHMARKS")?.toKString()?.lowercase() + val l = getenv("LYNG_BENCHMARKS")?.toKString()?.lowercase() + parse(b) || parse(l) + } +} diff --git a/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/BenchmarksWasmJs.kt b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/BenchmarksWasmJs.kt new file mode 100644 index 0000000..81501e1 --- /dev/null +++ b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/BenchmarksWasmJs.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.sergeych.lyng + +actual object Benchmarks { + actual val enabled: Boolean = false +}