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 4ca97eb..ab8ec7c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -83,7 +83,7 @@ open class Obj { scope: Scope, name: String, args: Arguments = Arguments.EMPTY, - onNotFoundResult: (() -> Obj?)? = null + onNotFoundResult: (suspend () -> Obj?)? = null ): Obj { val rec = objClass.getInstanceMemberOrNull(name) if (rec != null) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index 698b183..bf5ff22 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -214,7 +214,7 @@ open class ObjClass( // Avoid capturing a transient (pooled) call frame as the parent of the instance scope. // Bind instance scope to the caller's parent chain directly so name resolution (e.g., stdlib like sqrt) // remains stable even when call frames are pooled and reused. - val stableParent = scope.parent + val stableParent = classScope ?: scope.parent instance.instanceScope = Scope(stableParent, scope.args, scope.pos, instance) // Expose instance methods (and other callable members) directly in the instance scope for fast lookup // This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust @@ -451,7 +451,7 @@ open class ObjClass( override suspend fun invokeInstanceMethod( scope: Scope, name: String, args: Arguments, - onNotFoundResult: (() -> Obj?)? + onNotFoundResult: (suspend () -> Obj?)? ): Obj { return classScope?.objects?.get(name)?.value?.invoke(scope, this, args) ?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt index cd5baf3..157a200 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDynamic.kt @@ -81,7 +81,7 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St scope: Scope, name: String, args: Arguments, - onNotFoundResult: (() -> Obj?)? + onNotFoundResult: (suspend () -> Obj?)? ): Obj { val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope val over = readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name)))) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt index 80a2c7f..372bcf5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt @@ -71,7 +71,7 @@ open class ObjException( ) } else { // Fallback textual entry if StackTraceEntry class is not available in this scope - result.list += ObjString("${'$'}{pos.source.objSourceName}:${'$'}{pos.line}:${'$'}{pos.column}: ${'$'}{pos.currentLine}") + result.list += ObjString("${pos.source.objSourceName}:${pos.line}:${pos.column}: ${pos.currentLine}") } } s = s.parent diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt index c66a228..0fe857a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -34,14 +34,9 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { // Direct (unmangled) lookup first instanceScope[name]?.let { val decl = it.declaringClass ?: objClass.findDeclaringClassOf(name) - // When execution passes through suspension points (e.g., withLock), - // currentClassCtx could be lost. Fall back to the instance scope class ctx - // to preserve correct visibility semantics for private/protected members - // accessed from within the class methods. // Allow unconditional access when accessing through `this` of the same instance if (scope.thisObj === this) return it - val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx - val caller = caller0 // do not default to objClass for outsiders + val caller = scope.currentClassCtx if (!canAccessMember(it.visibility, decl, caller)) scope.raiseError( ObjAccessException( @@ -71,8 +66,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") } } if (scope.thisObj === this) return rec - val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx - val caller = caller0 // do not default to objClass for outsiders + val caller = scope.currentClassCtx if (!canAccessMember(rec.visibility, declaring, caller)) scope.raiseError( ObjAccessException( @@ -93,8 +87,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { if (scope.thisObj === this) { // direct self-assignment allowed; enforce mutability below } else { - val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx - val caller = caller0 // do not default to objClass for outsiders + val caller = scope.currentClassCtx if (!canAccessMember(f.visibility, decl, caller)) ObjIllegalAssignmentException( scope, @@ -123,8 +116,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") } } if (scope.thisObj !== this) { - val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx - val caller = caller0 // do not default to objClass for outsiders + val caller = scope.currentClassCtx if (!canAccessMember(rec.visibility, declaring, caller)) ObjIllegalAssignmentException( scope, @@ -141,12 +133,11 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { override suspend fun invokeInstanceMethod( scope: Scope, name: String, args: Arguments, - onNotFoundResult: (() -> Obj?)? + onNotFoundResult: (suspend () -> Obj?)? ): Obj = instanceScope[name]?.let { rec -> val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name) - val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx - val caller = caller0 ?: if (scope.thisObj === this) objClass else null + val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null if (!canAccessMember(rec.visibility, decl, caller)) scope.raiseError( ObjAccessException( @@ -171,8 +162,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { // fallback: class-scope function (registered during class body execution) objClass.classScope?.objects?.get(name)?.let { rec -> val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name) - val caller0 = scope.currentClassCtx ?: instanceScope.currentClassCtx - val caller = caller0 ?: if (scope.thisObj === this) objClass else null + val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null if (!canAccessMember(rec.visibility, decl, caller)) scope.raiseError( ObjAccessException( @@ -199,14 +189,14 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { override suspend fun toString(scope: Scope, calledFromLyng: Boolean): ObjString { return ObjString(buildString { - append("${objClass.className}(") - var first = true - for ((name, value) in publicFields) { - if (first) first = false else append(",") - append("$name=${value.value.toString(scope)}") - } - append(")") - }) + append("${objClass.className}(") + var first = true + for ((name, value) in publicFields) { + if (first) first = false else append(",") + append("$name=${value.value.toString(scope)}") + } + append(")") + }) } override suspend fun inspect(scope: Scope): String { @@ -246,7 +236,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { for (i in serializingVars) { // remove T:: prefix from the field name for JSON val parts = i.key.split("::") - result[if( parts.size == 1 ) parts[0] else parts.last()] = i.value.value.toJson(scope) + result[if (parts.size == 1) parts[0] else parts.last()] = i.value.value.toJson(scope) } return JsonObject(result) } @@ -259,7 +249,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { instanceScope.objects.filter { it.value.type.serializable && it.value.type == ObjRecord.Type.Field && - it.value.isMutable } + it.value.isMutable + } } internal suspend fun deserializeStateVars(scope: Scope, decoder: LynonDecoder) { @@ -392,7 +383,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla scope: Scope, name: String, args: Arguments, - onNotFoundResult: (() -> Obj?)? + onNotFoundResult: (suspend () -> Obj?)? ): Obj { // Qualified method dispatch must start from the specified ancestor, not from the instance scope. memberFromAncestor(name)?.let { rec -> diff --git a/lynglib/src/commonTest/kotlin/ScopePoolingRegressionTest.kt b/lynglib/src/commonTest/kotlin/ScopePoolingRegressionTest.kt new file mode 100644 index 0000000..1c83e64 --- /dev/null +++ b/lynglib/src/commonTest/kotlin/ScopePoolingRegressionTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2025 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.PerfFlags +import net.sergeych.lyng.eval +import kotlin.test.Test +import kotlin.test.assertEquals + +class ScopePoolingRegressionTest { + + @Test + fun testPooledScopeInstance() = runTest { + val saved = PerfFlags.SCOPE_POOL + PerfFlags.SCOPE_POOL = true + try { + val result = eval(""" + class A { + fun test() { + // println is a global function + println("Calling println from A") + "method ok" + } + } + + // Use a transient scope (lambda) to create the instance + val creator = { A() } + val a = creator() + + // Re-use the pool to ensure the scope used above is reset/repurposed + val other = { 1 + 2 } + other() + other() + + // Now call method on 'a'. It should still find global 'println' + a.test() + """.trimIndent()) + assertEquals("method ok", result.toString()) + } finally { + PerfFlags.SCOPE_POOL = saved + } + } +} diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index c3bd475..9523945 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -4298,6 +4298,19 @@ class ScriptTest { """.trimIndent()) } + @Test + fun testCached() = runTest { + eval(""" + var counter = 0 + val f = cached { ++counter } + + assertEquals(1,f()) + assertEquals(1, counter) + assertEquals(1,f()) + assertEquals(1, counter) + """.trimIndent()) + } + // @Test // fun testSplatAssignemnt() = runTest { diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index 0a910a5..fb247f2 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -210,8 +210,11 @@ class StackTraceEntry( /* Print this exception and its stack trace to standard output. */ fun Exception.printStackTrace() { println(this) + var lastEntry = null for( entry in stackTrace() ) { - println("\tat "+entry) + if( lastEntry == null || lastEntry !is StackTraceEntry || lastEntry.line != entry.line ) + println("\tat "+entry.toString()) + lastEntry = entry } }