diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt index e2ec981..c001ff7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt @@ -51,6 +51,32 @@ data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) defaultVisibility: Visibility = Visibility.Public, declaringClass: net.sergeych.lyng.obj.ObjClass? = scope.currentClassCtx ) { + // Fast path for simple positional-only calls with no ellipsis and no defaults + if (arguments.named.isEmpty() && !arguments.tailBlockMode) { + var hasComplex = false + for (p in params) { + if (p.isEllipsis || p.defaultValue != null) { + hasComplex = true + break + } + } + if (!hasComplex) { + if (arguments.list.size != params.size) + scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}") + + for (i in params.indices) { + val a = params[i] + val value = arguments.list[i] + scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, + value.byValueCopy(), + a.visibility ?: defaultVisibility, + recordType = ObjRecord.Type.Argument, + declaringClass = declaringClass) + } + return + } + } + fun assign(a: Item, value: Obj) { scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value.byValueCopy(), diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index aaa0e99..083530f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -70,9 +70,8 @@ open class Scope( internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? { var s: Scope? = this - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) break + var hops = 0 + while (s != null && hops++ < 1024) { // Proximity rule: check all extensions in the current scope before going to parent. // Priority within scope: more specific class in MRO wins. for (cls in receiverClass.mro) { @@ -108,7 +107,7 @@ open class Scope( */ internal fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? { caller?.let { ctx -> - s.objects["${ctx.className}::$name"]?.let { rec -> + s.objects[ctx.mangledName(name)]?.let { rec -> if (rec.visibility == Visibility.Private) return rec } } @@ -116,7 +115,7 @@ open class Scope( if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec } caller?.let { ctx -> - s.localBindings["${ctx.className}::$name"]?.let { rec -> + s.localBindings[ctx.mangledName(name)]?.let { rec -> if (rec.visibility == Visibility.Private) return rec } } @@ -132,11 +131,10 @@ open class Scope( internal fun chainLookupIgnoreClosure(name: String, followClosure: Boolean = false, caller: net.sergeych.lyng.obj.ObjClass? = null): ObjRecord? { var s: Scope? = this - // use frameId to detect unexpected structural cycles in the parent chain - val visited = HashSet(4) + // use hop counter to detect unexpected structural cycles in the parent chain + var hops = 0 val effectiveCaller = caller ?: currentClassCtx - while (s != null) { - if (!visited.add(s.frameId)) return null + while (s != null && hops++ < 1024) { tryGetLocalRecord(s, name, effectiveCaller)?.let { return it } s = if (followClosure && s is ClosureScope) s.closureScope else s.parent } @@ -155,9 +153,8 @@ open class Scope( tryGetLocalRecord(this, name, currentClassCtx)?.let { return it } // 2) walk parents for plain locals/bindings only var s = parent - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) return null + var hops = 0 + while (s != null && hops++ < 1024) { tryGetLocalRecord(s, name, currentClassCtx)?.let { return it } s = s.parent } @@ -182,9 +179,8 @@ open class Scope( */ internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx, followClosure: Boolean = false): ObjRecord? { var s: Scope? = this - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) return null + var hops = 0 + while (s != null && hops++ < 1024) { tryGetLocalRecord(s, name, caller)?.let { return it } for (cls in s.thisObj.objClass.mro) { s.extensions[cls]?.get(name)?.let { return it } @@ -396,6 +392,20 @@ open class Scope( nameToSlot[name]?.let { slots[it] = record } } + /** + * Clear all references and maps to prevent memory leaks when pooled. + */ + fun scrub() { + this.parent = null + this.skipScopeCreation = false + this.currentClassCtx = null + objects.clear() + slots.clear() + nameToSlot.clear() + localBindings.clear() + extensions.clear() + } + /** * Reset this scope instance so it can be safely reused as a fresh child frame. * Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj. @@ -414,7 +424,6 @@ open class Scope( nameToSlot.clear() localBindings.clear() extensions.clear() - this.currentClassCtx = parent?.currentClassCtx // Now safe to validate and re-parent ensureNoCycle(parent) this.parent = parent @@ -534,11 +543,15 @@ open class Scope( } } // Map to a slot for fast local access (ensure consistency) - val idx = getSlotIndexOf(name) - if (idx == null) { + if (nameToSlot.isEmpty()) { allocateSlotFor(name, rec) } else { - slots[idx] = rec + val idx = nameToSlot[name] + if (idx == null) { + allocateSlotFor(name, rec) + } else { + slots[idx] = rec + } } return rec } diff --git a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt index 421145e..713a394 100644 --- a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt +++ b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * 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. @@ -18,25 +18,25 @@ package net.sergeych.lyng actual object PerfDefaults { - actual val LOCAL_SLOT_PIC: Boolean = false - actual val EMIT_FAST_LOCAL_REFS: Boolean = false + actual val LOCAL_SLOT_PIC: Boolean = true + actual val EMIT_FAST_LOCAL_REFS: Boolean = true - actual val ARG_BUILDER: Boolean = false - actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = false + actual val ARG_BUILDER: Boolean = true + actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true actual val SCOPE_POOL: Boolean = false - actual val FIELD_PIC: Boolean = false - actual val METHOD_PIC: Boolean = false - actual val FIELD_PIC_SIZE_4: Boolean = false - actual val METHOD_PIC_SIZE_4: Boolean = false - actual val PIC_ADAPTIVE_2_TO_4: Boolean = false - actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = false - actual val PIC_ADAPTIVE_HEURISTIC: Boolean = false + actual val FIELD_PIC: Boolean = true + actual val METHOD_PIC: Boolean = true + actual val FIELD_PIC_SIZE_4: Boolean = true + actual val METHOD_PIC_SIZE_4: Boolean = true + actual val PIC_ADAPTIVE_2_TO_4: Boolean = true + actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = true + actual val PIC_ADAPTIVE_HEURISTIC: Boolean = true actual val PIC_DEBUG_COUNTERS: Boolean = false - actual val PRIMITIVE_FASTOPS: Boolean = false - actual val RVAL_FASTPATH: Boolean = false + actual val PRIMITIVE_FASTOPS: Boolean = true + actual val RVAL_FASTPATH: Boolean = true // Regex caching (JVM-first): enabled by default on JVM actual val REGEX_CACHE: Boolean = true diff --git a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/ScopePoolJvm.kt b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/ScopePoolJvm.kt index 214c82a..6dce0b8 100644 --- a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/ScopePoolJvm.kt +++ b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/ScopePoolJvm.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * 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. @@ -18,7 +18,6 @@ package net.sergeych.lyng import net.sergeych.lyng.obj.Obj -import net.sergeych.lyng.obj.ObjVoid /** * JVM actual: per-thread scope frame pool backed by ThreadLocal. @@ -32,26 +31,31 @@ actual object ScopePool { actual fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope { val pool = threadLocalPool.get() - val s = if (pool.isNotEmpty()) pool.removeLast() else Scope(parent, args, pos, thisObj) - return try { - // Always reset state on borrow to guarantee fresh-frame semantics - s.resetForReuse(parent, args, pos, thisObj) - s - } catch (e: IllegalStateException) { - // Defensive fallback: if a cycle in scope parent chain is detected during reuse, - // discard pooled instance for this call frame and allocate a fresh scope instead. - if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) { - Scope(parent, args, pos, thisObj) - } else { - throw e + if (pool.isNotEmpty()) { + val s = pool.removeLast() + try { + // Re-initialize pooled instance + s.resetForReuse(parent, args, pos, thisObj) + return s + } catch (e: IllegalStateException) { + // Defensive fallback: if a cycle in scope parent chain is detected during reuse, + // discard pooled instance for this call frame and allocate a fresh scope instead. + if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) { + return Scope(parent, args, pos, thisObj) + } else { + throw e + } } } + return Scope(parent, args, pos, thisObj) } actual fun release(scope: Scope) { val pool = threadLocalPool.get() - // Scrub sensitive references to avoid accidental retention - scope.resetForReuse(parent = null, args = Arguments.EMPTY, pos = Pos.builtIn, thisObj = ObjVoid) - if (pool.size < MAX_POOL_SIZE) pool.addLast(scope) + if (pool.size < MAX_POOL_SIZE) { + // Scrub sensitive references to avoid accidental retention before returning to pool + scope.scrub() + pool.addLast(scope) + } } } diff --git a/lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt b/lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt index c8cb031..128a832 100644 --- a/lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt +++ b/lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * 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. @@ -88,20 +88,57 @@ class PicBenchmarkTest { // PIC OFF PerfFlags.METHOD_PIC = false + PerfFlags.SCOPE_POOL = false val scope1 = Scope() val t0 = System.nanoTime() val r1 = (scope1.eval(script) as ObjInt).value val t1 = System.nanoTime() - println("[DEBUG_LOG] [BENCH] Method PIC=OFF: ${(t1 - t0) / 1_000_000.0} ms") + println("[DEBUG_LOG] [BENCH] Method PIC=OFF, POOL=OFF: ${(t1 - t0) / 1_000_000.0} ms") assertEquals(iterations.toLong(), r1) // PIC ON PerfFlags.METHOD_PIC = true + PerfFlags.SCOPE_POOL = true val scope2 = Scope() val t2 = System.nanoTime() val r2 = (scope2.eval(script) as ObjInt).value val t3 = System.nanoTime() - println("[DEBUG_LOG] [BENCH] Method PIC=ON: ${(t3 - t2) / 1_000_000.0} ms") + println("[DEBUG_LOG] [BENCH] Method PIC=ON, POOL=ON: ${(t3 - t2) / 1_000_000.0} ms") + assertEquals(iterations.toLong(), r2) + } + + @Test + fun benchmarkLoopScopePooling() = runBlocking { + val iterations = 500_000 + val script = """ + var x = 0 + var i = 0 + while(i < $iterations) { + if(true) { + var y = 1 + x = x + y + } + i = i + 1 + } + x + """.trimIndent() + + // POOL OFF + PerfFlags.SCOPE_POOL = false + val scope1 = Scope() + val t0 = System.nanoTime() + val r1 = (scope1.eval(script) as ObjInt).value + val t1 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] Loop Pool=OFF: ${(t1 - t0) / 1_000_000.0} ms") + assertEquals(iterations.toLong(), r1) + + // POOL ON + PerfFlags.SCOPE_POOL = true + val scope2 = Scope() + val t2 = System.nanoTime() + val r2 = (scope2.eval(script) as ObjInt).value + val t3 = System.nanoTime() + println("[DEBUG_LOG] [BENCH] Loop Pool=ON: ${(t3 - t2) / 1_000_000.0} ms") assertEquals(iterations.toLong(), r2) } }