diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt index 9cb7358..159f0cb 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt @@ -46,7 +46,7 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) : tryGetLocalRecord(this, name, currentClassCtx)?.let { return it } // 2. Lexical environment (captured locals from entire ancestry) - closureScope.chainLookupIgnoreClosure(name, followClosure = true)?.let { return it } + closureScope.chainLookupIgnoreClosure(name, followClosure = true, caller = currentClassCtx)?.let { return it } // 3. Lexical this members (captured receiver) val receiver = thisObj diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 87b940b..8578bd2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -1482,7 +1482,14 @@ class Compiler( miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos)) } val initStmt = statement(id.pos) { scp -> - block.execute(scp) + val cls = scp.thisObj.objClass + val saved = scp.currentClassCtx + scp.currentClassCtx = cls + try { + block.execute(scp) + } finally { + scp.currentClassCtx = saved + } ObjVoid } statement { @@ -3313,9 +3320,11 @@ class Compiler( val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name") cc.requireToken(Token.Type.RPAREN) miniSink?.onEnterFunction(null) - setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { + val finalSetter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { cc.skipWsTokens() - val body = parseBlock() + val body = inCodeContext(CodeContext.Function("")) { + parseBlock() + } statement(body.pos) { scope -> val value = scope.args.list.firstOrNull() ?: ObjNull scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument) @@ -3324,11 +3333,12 @@ class Compiler( } else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) { cc.skipWsTokens() cc.next() // consume '=' - val expr = parseExpression() ?: throw ScriptError( - cc.current().pos, - "Expected setter expression" - ) - val st = expr + val st = inCodeContext(CodeContext.Function("")) { + parseExpression() ?: throw ScriptError( + cc.current().pos, + "Expected setter expression" + ) + } statement(st.pos) { scope -> val value = scope.args.list.firstOrNull() ?: ObjNull scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument) @@ -3337,6 +3347,7 @@ class Compiler( } else { throw ScriptError(cc.current().pos, "Expected { or = after set(...)") } + setter = finalSetter miniSink?.onExitFunction(cc.currentPos()) } else { // private set without body: setter remains null, visibility is restricted @@ -3383,6 +3394,7 @@ class Compiler( // In true class bodies (not inside a function), store fields under a class-qualified key to support MI collisions // Do NOT infer declaring class from runtime thisObj here; only the compile-time captured // ClassBody qualifies for class-field storage. Otherwise, this is a plain local. + isProperty = getter != null || setter != null val declaringClassName = declaringClassNameCaptured if (declaringClassName == null) { if (context.containsLocal(name)) @@ -3488,7 +3500,8 @@ class Compiler( isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, - pos = start + pos = start, + prop = prop ) } else { cls.createField( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index be45358..d23ba43 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -107,9 +107,19 @@ open class Scope( * and bindings of each frame. Instance/class member fallback must be decided by the caller. */ internal fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? { + caller?.let { ctx -> + s.objects["${ctx.className}::$name"]?.let { rec -> + if (rec.visibility == Visibility.Private) return rec + } + } s.objects[name]?.let { rec -> if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec } + caller?.let { ctx -> + s.localBindings["${ctx.className}::$name"]?.let { rec -> + if (rec.visibility == Visibility.Private) return rec + } + } s.localBindings[name]?.let { rec -> if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec } @@ -120,13 +130,14 @@ open class Scope( return null } - internal fun chainLookupIgnoreClosure(name: String, followClosure: Boolean = false): ObjRecord? { + 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) + val effectiveCaller = caller ?: currentClassCtx while (s != null) { if (!visited.add(s.frameId)) return null - tryGetLocalRecord(s, name, currentClassCtx)?.let { return it } + tryGetLocalRecord(s, name, effectiveCaller)?.let { return it } s = if (followClosure && s is ClosureScope) s.closureScope else s.parent } return null @@ -334,12 +345,7 @@ open class Scope( if (name == "this") return thisObj.asReadonly // 1. Prefer direct locals/bindings declared in this frame - objects[name]?.let { rec -> - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec - } - localBindings[name]?.let { rec -> - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec - } + tryGetLocalRecord(this, name, currentClassCtx)?.let { return it } // 2. Then, check members of thisObj val receiver = thisObj 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 9e3cbb0..b789409 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -253,28 +253,29 @@ open class ObjClass( // remains stable even when call frames are pooled and reused. val stableParent = classScope ?: scope.parent instance.instanceScope = Scope(stableParent, scope.args, scope.pos, instance) - // println("[DEBUG_LOG] createInstance: created $instance scope@${instance.instanceScope.hashCode()}") instance.instanceScope.currentClassCtx = null // 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 for (cls in mro) { - // 1) members-defined methods + // 1) members-defined methods and fields for ((k, v) in cls.members) { - if (v.value is Statement || v.type == ObjRecord.Type.Delegated) { - val key = if (v.visibility == Visibility.Private) "${cls.className}::$k" else k + if (!v.isAbstract && (v.value is Statement || v.type == ObjRecord.Type.Delegated || v.type == ObjRecord.Type.Field)) { + val key = if (v.visibility == Visibility.Private || v.type == ObjRecord.Type.Field || v.type == ObjRecord.Type.Delegated) "${cls.className}::$k" else k if (!instance.instanceScope.objects.containsKey(key)) { - instance.instanceScope.objects[key] = if (v.type == ObjRecord.Type.Delegated) v.copy() else v + instance.instanceScope.objects[key] = if (v.type == ObjRecord.Type.Fun) v else v.copy() } } } - // 2) class-scope methods registered during class-body execution + // 2) class-scope members registered during class-body execution cls.classScope?.objects?.forEach { (k, rec) -> - if (rec.value is Statement || rec.type == ObjRecord.Type.Delegated) { - val key = if (rec.visibility == Visibility.Private) "${cls.className}::$k" else k + // ONLY copy methods and delegated members from class scope to instance scope. + // Fields in class scope are static fields and must NOT be per-instance. + if (!rec.isAbstract && (rec.value is Statement || rec.type == ObjRecord.Type.Delegated)) { + val key = if (rec.visibility == Visibility.Private || rec.type == ObjRecord.Type.Delegated) "${cls.className}::$k" else k // if not already present, copy reference for dispatch if (!instance.instanceScope.objects.containsKey(key)) { - instance.instanceScope.objects[key] = if (rec.type == ObjRecord.Type.Delegated) rec.copy() else rec + instance.instanceScope.objects[key] = if (rec.type == ObjRecord.Type.Fun) rec else rec.copy() } } } @@ -512,12 +513,13 @@ open class ObjClass( isClosed: Boolean = false, isOverride: Boolean = false, pos: Pos = Pos.builtIn, + prop: ObjProperty? = null ) { val g = getter?.let { statement { it() } } val s = setter?.let { statement { it(requiredArg(0)); ObjVoid } } - val prop = if (isAbstract) ObjNull else ObjProperty(name, g, s) + val finalProp = prop ?: if (isAbstract) ObjNull else ObjProperty(name, g, s) createField( - name, prop, false, visibility, writeVisibility, pos, declaringClass, + name, finalProp, false, visibility, writeVisibility, pos, declaringClass, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, type = ObjRecord.Type.Property ) 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 a58928f..7047843 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -36,6 +36,13 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { // 0. Private mangled of current class context caller?.let { c -> + // Check for private methods/properties + c.members[name]?.let { rec -> + if (rec.visibility == Visibility.Private) { + return resolveRecord(scope, rec, name, c) + } + } + // Check for private fields (stored in instanceScope) val mangled = "${c.className}::$name" instanceScope.objects[mangled]?.let { rec -> if (rec.visibility == Visibility.Private) { @@ -59,8 +66,9 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { instanceScope.objects[name]?.let { rec -> val decl = rec.declaringClass // Unmangled access is only allowed if it's public OR we are inside the same instance's method - if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, decl, caller)) + if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, decl, caller)) { return resolveRecord(scope, rec, name, decl) + } } // 3. Fall back to super (handles class members and extensions) @@ -113,6 +121,14 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { // 0. Private mangled of current class context caller?.let { c -> + // Check for private methods/properties + c.members[name]?.let { rec -> + if (rec.visibility == Visibility.Private) { + updateRecord(scope, resolveRecord(scope, rec, name, c), name, newValue, c) + return + } + } + // Check for private fields (stored in instanceScope) val mangled = "${c.className}::$name" instanceScope.objects[mangled]?.let { rec -> if (rec.visibility == Visibility.Private) { @@ -343,7 +359,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { if (other.objClass != objClass) return -1 for (f in comparableVars) { val a = f.value.value - val b = other.instanceScope[f.key]!!.value + val b = other.instanceScope.objects[f.key]?.value ?: scope.raiseError("Internal error: field ${f.key} not found in other instance") val d = a.compareTo(scope, b) if (d != 0) return d } @@ -370,7 +386,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla val caller = scope.currentClassCtx if (!canAccessMember(rec.visibility, decl, caller)) scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})")) - return resolveRecord(scope, rec, name, decl) + return instance.resolveRecord(scope, rec, name, decl) } // Then try instance locals (unmangled) only if startClass is the dynamic class itself if (startClass === instance.objClass) { @@ -384,7 +400,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla "can't access field $name (declared in ${decl?.className ?: "?"})" ) ) - return resolveRecord(scope, rec, name, decl) + return instance.resolveRecord(scope, rec, name, decl) } } // Finally try methods/properties starting from ancestor @@ -394,7 +410,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla if (!canAccessMember(r.visibility, decl, caller)) scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})")) - return resolveRecord(scope, r, name, decl) + return instance.resolveRecord(scope, r, name, decl) } override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {