diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt index bf2824a..f49377a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.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. @@ -66,6 +66,11 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) : } } + // 1b) Captured locals from the entire closure ancestry. This ensures that parameters + // and local variables shadow members of captured receivers, matching standard + // lexical scoping rules. + closureScope.chainLookupIgnoreClosure(name, followClosure = true)?.let { return it } + // 2) Members on the captured receiver instance (closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)?.let { inst -> // Check direct locals in instance scope (unmangled) @@ -94,7 +99,7 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) : } // 3) Closure scope chain (locals/parents + members), ignore ClosureScope overrides to prevent recursion - closureScope.chainLookupWithMembers(name, currentClassCtx)?.let { return it } + closureScope.chainLookupWithMembers(name, currentClassCtx, followClosure = true)?.let { return it } // 4) Caller `this` members (callScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)?.let { inst -> diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index ff98401..7d10fc4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -120,14 +120,14 @@ open class Scope( return null } - internal fun chainLookupIgnoreClosure(name: String): ObjRecord? { + internal fun chainLookupIgnoreClosure(name: String, followClosure: Boolean = false): ObjRecord? { var s: Scope? = this // use frameId to detect unexpected structural cycles in the parent chain val visited = HashSet(4) while (s != null) { if (!visited.add(s.frameId)) return null tryGetLocalRecord(s, name, currentClassCtx)?.let { return it } - s = s.parent + s = if (followClosure && s is ClosureScope) s.closureScope else s.parent } return null } @@ -169,7 +169,7 @@ open class Scope( * This completely avoids invoking overridden `get` implementations, preventing * ping-pong recursion between `ClosureScope` frames. */ - internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx): ObjRecord? { + 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) { @@ -185,7 +185,7 @@ open class Scope( } else return rec } } - s = s.parent + s = if (followClosure && s is ClosureScope) s.closureScope else s.parent } return null } diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 6cc59ba..bc4c617 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -4683,5 +4683,42 @@ class ScriptTest { x.getLyngExceptionMessageWithStackTrace() ) } + + @Test + fun testMapIteralAmbiguity() = runTest { + eval(""" + val m = { a: 1, b: { foo: "bar" } } + assertEquals(1, m["a"]) + assertEquals("bar", m["b"]["foo"]) + val bar = "foobar" + val m2 = { a: 1, b: { bar: } } + assert( m2["b"] is Map ) + assertEquals("foobar", m2["b"]["bar"]) + """.trimIndent()) + } + + @Test + fun realWorldCaptureProblem() = runTest { + eval(""" + // 61755f07-630c-4181-8d50-1b044d96e1f4 + class T { + static var f1 = null + static fun t(name=null) { + run { + // I expect it will catch the 'name' from + // param? + f1 = name + } + } + } + assert(T.f1 == null) + println("-- "+T.f1::class) + println("-- "+T.f1) + T.t("foo") + println("2- "+T.f1::class) + println("2- "+T.f1) + assert(T.f1 == "foo") + """.trimIndent()) + } }