diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index d23ba43..aaa0e99 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -347,7 +347,16 @@ open class Scope( // 1. Prefer direct locals/bindings declared in this frame tryGetLocalRecord(this, name, currentClassCtx)?.let { return it } - // 2. Then, check members of thisObj + val p = parent + + // 2. If we share thisObj with parent, delegate to parent to maintain + // "locals shadow members" priority across the this-context. + if (p != null && p.thisObj === thisObj) { + return p.get(name) + } + + // 3. Otherwise, we are the "primary" scope for this thisObj (or have no parent), + // so check members of thisObj before walking up to a different this-context. val receiver = thisObj val effectiveClass = receiver as? ObjClass ?: receiver.objClass for (cls in effectiveClass.mro) { @@ -365,8 +374,8 @@ open class Scope( } } - // 3. Finally, walk up ancestry - return parent?.get(name) + // 4. Finally, walk up ancestry to a scope with a different thisObj context + return p?.get(name) } // Slot fast-path API 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 7047843..401bc98 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -76,6 +76,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { } override suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord { + if (obj.type.isArgument) return super.resolveRecord(scope, obj, name, decl) if (obj.type == ObjRecord.Type.Delegated) { val d = decl ?: obj.declaringClass val storageName = "${d?.className}::$name" diff --git a/lynglib/src/commonTest/kotlin/OOTest.kt b/lynglib/src/commonTest/kotlin/OOTest.kt index 3ee062e..4014420 100644 --- a/lynglib/src/commonTest/kotlin/OOTest.kt +++ b/lynglib/src/commonTest/kotlin/OOTest.kt @@ -20,6 +20,7 @@ import net.sergeych.lyng.Script import net.sergeych.lyng.eval import net.sergeych.lyng.obj.ObjInstance import net.sergeych.lyng.obj.ObjList +import net.sergeych.lyng.toSource import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFails @@ -731,4 +732,48 @@ class OOTest { """.trimIndent()) } + @Test + fun testArgsPriority() = runTest { + eval(""" + class A(id) { + var stored = null + // Arguments should have priority on + // instance fields + fun setStored(id) { stored = id } + } + val a = A(1) + assertEquals(1, a.id) + assertEquals(null, a.stored) + + // Check that arguments of the call have the priority: + a.setStored(2) + assertEquals(1, a.id) + assertEquals(2, a.stored) + """.trimIndent()) + } + + /** + * Demonstrates that function parameters are shadowed by class methods of the same name + * when accessed within a block, but not in a single expression. + */ + @Test + fun testParameterShadowingConflict() = runTest { + val scope = Script.newScope() + val result = scope.eval(""" + class Tester() { + fun id() { "method" } + // This correctly returns "success" + fun checkOk(id) = id + // This incorrectly returns the 'id' method (a Callable) instead of "success" + fun checkFail(id) { + id + } + } + val t = Tester() + if (t.checkOk("success") != "success") throw "checkOk failed" + t.checkFail("success") + """.trimIndent().toSource("repro")) + + assertEquals("success", result.toString(), "Parameter 'id' should shadow method 'id' in block") + } } \ No newline at end of file