diff --git a/.gitignore b/.gitignore index 42c27ae..7f6a84e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ debug.log /check_output.txt /compile_jvm_output.txt /compile_metadata_output.txt +test_output*.txt diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 162385d..0d3ce33 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.1.1-SNAPSHOT" +version = "1.2.-SNAPSHOT" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt index f49377a..9cb7358 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClosureScope.kt @@ -17,6 +17,8 @@ package net.sergeych.lyng +import net.sergeych.lyng.obj.Obj +import net.sergeych.lyng.obj.ObjClass import net.sergeych.lyng.obj.ObjRecord /** @@ -38,138 +40,34 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) : } override fun get(name: String): ObjRecord? { - // Fast-path built-ins if (name == "this") return thisObj.asReadonly - // Priority: - // 1) Locals and arguments declared in this lambda frame (including values defined before suspension) - // 2) Instance/class members of the captured receiver (`closureScope.thisObj`) - // 3) Symbols from the captured closure scope chain (locals/parents), ignoring nested ClosureScope overrides - // 4) Instance members of the caller's `this` (e.g., FlowBuilder.emit) - // 5) Symbols from the caller chain (locals/parents), ignoring nested ClosureScope overrides - // 6) Special fallback for module pseudo-symbols (e.g., __PACKAGE__) + // 1. Current frame locals (parameters, local variables) + tryGetLocalRecord(this, name, currentClassCtx)?.let { return it } - // 1) Locals/arguments in this closure frame - super.objects[name]?.let { return it } - super.localBindings[name]?.let { return it } - - // 1a) Priority: if we are in a class context, prefer our own private members to support - // non-virtual private dispatch. This prevents a subclass from accidentally - // capturing a private member call from a base class. - // We only return non-field/non-property members (methods) here; fields must - // be resolved via instance storage in priority 2. - currentClassCtx?.let { ctx -> - ctx.members[name]?.let { rec -> - if (rec.visibility == Visibility.Private && - rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field && - rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property) return rec - } - } - - // 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. + // 2. Lexical environment (captured locals from entire ancestry) 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) - inst.instanceScope.objects[name]?.let { rec -> - if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property && - canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec - } - // Check mangled names for fields along MRO - for (cls in inst.objClass.mro) { - inst.instanceScope.objects["${cls.className}::$name"]?.let { rec -> - if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property && - canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) return rec + // 3. Lexical this members (captured receiver) + val receiver = thisObj + val effectiveClass = receiver as? ObjClass ?: receiver.objClass + for (cls in effectiveClass.mro) { + val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) + if (rec != null && !rec.isAbstract) { + if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) { + return rec.copy(receiver = receiver) } } } - - findExtension(closureScope.thisObj.objClass, name)?.let { return it } - closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> + // Finally, root object fallback + Obj.rootObjectType.members[name]?.let { rec -> if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) { - // Return only non-field/non-property members (methods) from class-level records. - // Fields and properties must be resolved via instance storage (mangled) or readField. - if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field && - rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property && - !rec.isAbstract) return rec + return rec.copy(receiver = receiver) } } - // 3) Closure scope chain (locals/parents + members), ignore ClosureScope overrides to prevent recursion - closureScope.chainLookupWithMembers(name, currentClassCtx, followClosure = true)?.let { return it } - - // 4) Caller `this` members - (callScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)?.let { inst -> - // Check direct locals in instance scope (unmangled) - inst.instanceScope.objects[name]?.let { rec -> - if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property && - canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec - } - // Check mangled names for fields along MRO - for (cls in inst.objClass.mro) { - inst.instanceScope.objects["${cls.className}::$name"]?.let { rec -> - if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property && - canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) return rec - } - } - } - findExtension(callScope.thisObj.objClass, name)?.let { return it } - callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> - if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) { - if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field && - rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property && - !rec.isAbstract) return rec - } - } - - // 5) Caller chain (locals/parents + members) - callScope.chainLookupWithMembers(name, currentClassCtx)?.let { return it } - - // 6) Module pseudo-symbols (e.g., __PACKAGE__) — walk caller ancestry and query ModuleScope directly - if (name.startsWith("__")) { - var s: Scope? = callScope - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) break - if (s is ModuleScope) return s.get(name) - s = s.parent - } - } - - // 7) Direct module/global fallback: try to locate nearest ModuleScope and check its own locals - fun lookupInModuleAncestry(from: Scope): ObjRecord? { - var s: Scope? = from - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) break - if (s is ModuleScope) { - s.objects[name]?.let { return it } - s.localBindings[name]?.let { return it } - // check immediate parent (root scope) locals/constants for globals like `delay` - val p = s.parent - if (p != null) { - p.objects[name]?.let { return it } - p.localBindings[name]?.let { return it } - p.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it } - } - return null - } - s = s.parent - } - return null - } - - lookupInModuleAncestry(closureScope)?.let { return it } - lookupInModuleAncestry(callScope)?.let { return it } - - // 8) Global root scope constants/functions (e.g., delay, yield) via current import provider - runCatching { this.currentImportProvider.rootScope.objects[name] }.getOrNull()?.let { return it } - - // Final safe fallback: base scope lookup from this frame walking raw parents - return baseGetIgnoreClosure(name) + // 4. Call environment (caller locals, caller this, and global fallback) + return callScope.get(name) } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 37809f0..87b940b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -667,19 +667,25 @@ class Compiler( "lambda must have either valid arguments declaration with '->' or no arguments" ) + val paramNames = argsDeclaration?.params?.map { it.name } ?: emptyList() + label?.let { cc.labels.add(it) } - val body = parseBlock(skipLeadingBrace = true) + val body = inCodeContext(CodeContext.Function("")) { + withLocalNames(paramNames.toSet()) { + parseBlock(skipLeadingBrace = true) + } + } label?.let { cc.labels.remove(it) } return ValueFnRef { closureScope -> - statement { + statement(body.pos) { scope -> // and the source closure of the lambda which might have other thisObj. - val context = this.applyClosure(closureScope) + val context = scope.applyClosure(closureScope) // Execute lambda body in a closure-aware context. Blocks inside the lambda // will create child scopes as usual, so re-declarations inside loops work. if (argsDeclaration == null) { // no args: automatic var 'it' - val l = args.list + val l = scope.args.list val itValue: Obj = when (l.size) { // no args: it == void 0 -> ObjVoid @@ -2192,20 +2198,6 @@ class Compiler( for (s in initScope) s.execute(classScope) } - // Fallback: ensure any functions declared in class scope are also present as instance methods - // (defensive in case some paths skipped cls.addFn during parsing/execution ordering) - for ((k, rec) in classScope.objects) { - val v = rec.value - if (v is Statement) { - if (newClass.members[k] == null) { - newClass.addFn(k, isMutable = true, pos = rec.importedFrom?.pos ?: nameToken.pos) { - (thisObj as? ObjInstance)?.let { i -> - v.execute(ClosureScope(this, i.instanceScope)) - } ?: v.execute(thisObj.autoInstanceScope(this)) - } - } - } - } newClass.checkAbstractSatisfaction(nameToken.pos) // Debug summary: list registered instance methods and class-scope functions for this class newClass @@ -2292,7 +2284,7 @@ class Compiler( } else if (sourceObj.isInstanceOf(ObjIterable)) { loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak) } else { - val size = runCatching { sourceObj.invokeInstanceMethod(forContext, "size").toInt() } + val size = runCatching { sourceObj.readField(forContext, "size").value.toInt() } .getOrElse { throw ScriptError( tOp.pos, @@ -3079,31 +3071,88 @@ class Compiler( val mark = cc.savePos() cc.restorePos(markBeforeEq) cc.skipWsTokens() - val next = cc.peekNextNonWhitespace() - if (next.isId("get") || next.isId("set") || next.isId("private") || next.isId("protected")) { + + // Heuristic: if we see 'get(' or 'set(' or 'private set(' or 'protected set(', + // look ahead for a body. + fun hasAccessorWithBody(): Boolean { + val t = cc.peekNextNonWhitespace() + if (t.isId("get") || t.isId("set")) { + val saved = cc.savePos() + cc.next() // consume get/set + val nextToken = cc.peekNextNonWhitespace() + if (nextToken.type == Token.Type.LPAREN) { + cc.next() // consume ( + var depth = 1 + while (cc.hasNext() && depth > 0) { + val tt = cc.next() + if (tt.type == Token.Type.LPAREN) depth++ + else if (tt.type == Token.Type.RPAREN) depth-- + } + val next = cc.peekNextNonWhitespace() + if (next.type == Token.Type.LBRACE || next.type == Token.Type.ASSIGN) { + cc.restorePos(saved) + return true + } + } else if (nextToken.type == Token.Type.LBRACE || nextToken.type == Token.Type.ASSIGN) { + cc.restorePos(saved) + return true + } + cc.restorePos(saved) + } else if (t.isId("private") || t.isId("protected")) { + val saved = cc.savePos() + cc.next() // consume modifier + if (cc.skipWsTokens().isId("set")) { + cc.next() // consume set + val nextToken = cc.peekNextNonWhitespace() + if (nextToken.type == Token.Type.LPAREN) { + cc.next() // consume ( + var depth = 1 + while (cc.hasNext() && depth > 0) { + val tt = cc.next() + if (tt.type == Token.Type.LPAREN) depth++ + else if (tt.type == Token.Type.RPAREN) depth-- + } + val next = cc.peekNextNonWhitespace() + if (next.type == Token.Type.LBRACE || next.type == Token.Type.ASSIGN) { + cc.restorePos(saved) + return true + } + } else if (nextToken.type == Token.Type.LBRACE || nextToken.type == Token.Type.ASSIGN) { + cc.restorePos(saved) + return true + } + } + cc.restorePos(saved) + } + return false + } + + if (hasAccessorWithBody()) { isProperty = true cc.restorePos(markBeforeEq) - cc.skipWsTokens() + // Do not consume eqToken if it's an accessor keyword } else { cc.restorePos(mark) } } + val effectiveEqToken = if (isProperty) null else eqToken + // Register the local name at compile time so that subsequent identifiers can be emitted as fast locals if (!isStatic) declareLocalName(name) val isDelegate = if (isAbstract || actualExtern) { - if (!isProperty && (eqToken.type == Token.Type.ASSIGN || eqToken.type == Token.Type.BY)) - throw ScriptError(eqToken.pos, "${if (isAbstract) "abstract" else "extern"} variable $name cannot have an initializer or delegate") + if (!isProperty && (effectiveEqToken?.type == Token.Type.ASSIGN || effectiveEqToken?.type == Token.Type.BY)) + throw ScriptError(effectiveEqToken.pos, "${if (isAbstract) "abstract" else "extern"} variable $name cannot have an initializer or delegate") // Abstract or extern variables don't have initializers cc.restorePos(markBeforeEq) cc.skipWsTokens() setNull = true false - } else if (!isProperty && eqToken.type == Token.Type.BY) { + } else if (!isProperty && effectiveEqToken?.type == Token.Type.BY) { true } else { - if (!isProperty && eqToken.type != Token.Type.ASSIGN) { + if (!isProperty && effectiveEqToken?.type != Token.Type.ASSIGN) { if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null)) throw ScriptError(start, "val must be initialized") else if (!isMutable && declaringClassNameCaptured != null && extTypeName == null) { @@ -3123,12 +3172,12 @@ class Compiler( val initialExpression = if (setNull || isProperty) null else parseStatement(true) - ?: throw ScriptError(eqToken.pos, "Expected initializer expression") + ?: throw ScriptError(effectiveEqToken!!.pos, "Expected initializer expression") // Emit MiniValDecl for this declaration (before execution wiring), attach doc if any run { val declRange = MiniRange(pendingDeclStart ?: start, cc.currentPos()) - val initR = if (setNull || isProperty) null else MiniRange(eqToken.pos, cc.currentPos()) + val initR = if (setNull || isProperty) null else MiniRange(effectiveEqToken!!.pos, cc.currentPos()) val node = MiniValDecl( range = declRange, name = name, @@ -3193,17 +3242,24 @@ class Compiler( if (t.isId("get")) { val getStart = cc.currentPos() cc.next() // consume 'get' - cc.requireToken(Token.Type.LPAREN) - cc.requireToken(Token.Type.RPAREN) + if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) { + cc.next() // consume ( + cc.requireToken(Token.Type.RPAREN) + } miniSink?.onEnterFunction(null) getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { cc.skipWsTokens() - parseBlock() + inCodeContext(CodeContext.Function("")) { + parseBlock() + } } else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) { cc.skipWsTokens() cc.next() // consume '=' - val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected getter expression") - expr + inCodeContext(CodeContext.Function("")) { + val expr = parseExpression() + ?: throw ScriptError(cc.current().pos, "Expected getter expression") + expr + } } else { throw ScriptError(cc.current().pos, "Expected { or = after get()") } @@ -3211,26 +3267,34 @@ class Compiler( } else if (t.isId("set")) { val setStart = cc.currentPos() cc.next() // consume 'set' - cc.requireToken(Token.Type.LPAREN) - val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name") - cc.requireToken(Token.Type.RPAREN) + var setArgName = "it" + if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) { + cc.next() // consume ( + setArgName = cc.requireToken(Token.Type.ID, "Expected setter argument name").value + cc.requireToken(Token.Type.RPAREN) + } miniSink?.onEnterFunction(null) setter = 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) + scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument) body.execute(scope) } } 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 expr = inCodeContext(CodeContext.Function("")) { + parseExpression() + ?: throw ScriptError(cc.current().pos, "Expected setter expression") + } val st = expr statement(st.pos) { scope -> val value = scope.args.list.firstOrNull() ?: ObjNull - scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument) + scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument) st.execute(scope) } } else { @@ -3274,6 +3338,8 @@ class Compiler( throw ScriptError(cc.current().pos, "Expected { or = after set(...)") } miniSink?.onExitFunction(cc.currentPos()) + } else { + // private set without body: setter remains null, visibility is restricted } } else { cc.restorePos(mark) @@ -3525,7 +3591,7 @@ class Compiler( } else { // Not in class body: regular local/var declaration val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull - context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field) + context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Other) initValue } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 7d10fc4..be45358 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -106,7 +106,7 @@ open class Scope( * intertwined closure frames. They traverse the plain parent chain and consult only locals * and bindings of each frame. Instance/class member fallback must be decided by the caller. */ - private fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? { + internal fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? { s.objects[name]?.let { rec -> if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec } @@ -330,28 +330,38 @@ open class Scope( internal val objects = mutableMapOf() - open operator fun get(name: String): ObjRecord? = - if (name == "this") thisObj.asReadonly - else { - // Prefer direct locals/bindings declared in this frame - (objects[name]?.let { rec -> - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null - } - // Then, check known local bindings in this frame (helps after suspension) - ?: localBindings[name]?.let { rec -> - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null - } - // Walk up ancestry - ?: parent?.get(name) - // Finally, fallback to class members on thisObj - ?: thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> - if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) { - if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null - else rec - } else null - } - ) + open operator fun get(name: String): ObjRecord? { + 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 + } + + // 2. Then, check members of thisObj + val receiver = thisObj + val effectiveClass = receiver as? ObjClass ?: receiver.objClass + for (cls in effectiveClass.mro) { + val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) + if (rec != null && !rec.isAbstract) { + if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) { + return rec.copy(receiver = receiver) + } + } + } + // Finally, root object fallback + Obj.rootObjectType.members[name]?.let { rec -> + if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) { + return rec.copy(receiver = receiver) + } + } + + // 3. Finally, walk up ancestry + return parent?.get(name) + } // Slot fast-path API fun getSlotRecord(index: Int): ObjRecord = slots[index] @@ -380,6 +390,7 @@ open class Scope( // that could interact badly with the new parent and produce a cycle. this.parent = null this.skipScopeCreation = false + this.currentClassCtx = parent?.currentClassCtx // fresh identity for PIC caches this.frameId = nextFrameId() // clear locals and slot maps @@ -388,6 +399,7 @@ 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 @@ -628,46 +640,34 @@ open class Scope( } suspend fun resolve(rec: ObjRecord, name: String): Obj { - if (rec.type == ObjRecord.Type.Delegated) { - val del = rec.delegate ?: run { - if (thisObj is ObjInstance) { - val res = (thisObj as ObjInstance).resolveRecord(this, rec, name, rec.declaringClass).value - rec.value = res - return res - } - raiseError("Internal error: delegated property $name has no delegate") - } - val th = if (thisObj === ObjVoid) ObjNull else thisObj - val res = del.invokeInstanceMethod(this, "getValue", Arguments(th, ObjString(name)), onNotFoundResult = { - // If getValue not found, return a wrapper that calls invoke - object : Statement() { - override val pos: Pos = Pos.builtIn - override suspend fun execute(scope: Scope): Obj { - val th2 = if (scope.thisObj === ObjVoid) ObjNull else scope.thisObj - val allArgs = (listOf(th2, ObjString(name)) + scope.args.list).toTypedArray() - return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs)) - } - } - }) - rec.value = res - return res - } - return rec.value + val receiver = rec.receiver ?: thisObj + return receiver.resolveRecord(this, rec, name, rec.declaringClass).value } suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) { if (rec.type == ObjRecord.Type.Delegated) { + val receiver = rec.receiver ?: thisObj val del = rec.delegate ?: run { - if (thisObj is ObjInstance) { - (thisObj as ObjInstance).writeField(this, name, newValue) + if (receiver is ObjInstance) { + (receiver as ObjInstance).writeField(this, name, newValue) return } raiseError("Internal error: delegated property $name has no delegate") } - val th = if (thisObj === ObjVoid) ObjNull else thisObj + val th = if (receiver === ObjVoid) ObjNull else receiver del.invokeInstanceMethod(this, "setValue", Arguments(th, ObjString(name), newValue)) return } + if (rec.value is ObjProperty) { + (rec.value as ObjProperty).callSetter(this, rec.receiver ?: thisObj, newValue, rec.declaringClass) + return + } + // If it's a member (explicitly tracked by receiver or declaringClass), use writeField. + // Important: locals have receiver == null and declaringClass == null (enforced in addItem). + if (rec.receiver != null || (rec.declaringClass != null && (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property))) { + (rec.receiver ?: thisObj).writeField(this, name, newValue) + return + } if (!rec.isMutable && rec.value !== ObjUnset) raiseIllegalAssignment("can't reassign val $name") rec.value = newValue } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 97a743b..3a1275e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -293,10 +293,14 @@ class Script( ObjVoid } - // Delay in milliseconds (plain numeric). For time-aware variants use lyng.time.Duration API. addVoidFn("delay") { - val ms = (this.args.firstAndOnly().toDouble()).roundToLong() - delay(ms) + val a = args.firstAndOnly() + when (a) { + is ObjInt -> delay(a.value) + is ObjReal -> delay((a.value * 1000).roundToLong()) + is ObjDuration -> delay(a.duration) + else -> raiseIllegalArgument("Expected Int, Real or Duration, got ${a.inspect(this)}") + } } addConst("Object", rootObjectType) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocRegistrationHelpers.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocRegistrationHelpers.kt index c61c5ad..62d963e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocRegistrationHelpers.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocRegistrationHelpers.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. @@ -145,6 +145,23 @@ fun ObjClass.addClassFnDoc( } } +fun ObjClass.addPropertyDoc( + name: String, + doc: String, + type: TypeDoc? = null, + visibility: Visibility = Visibility.Public, + moduleName: String? = null, + getter: (suspend Scope.() -> Obj)? = null, + setter: (suspend Scope.(Obj) -> Unit)? = null +) { + addProperty(name, getter, setter, visibility) + BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) { + classDoc(this@addPropertyDoc.className, doc = "") { + field(name = name, doc = doc, type = type, mutable = setter != null) + } + } +} + fun ObjClass.addClassConstDoc( name: String, value: Obj, 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 1ca5205..ffd3d9b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -93,23 +93,45 @@ open class Obj { args: Arguments = Arguments.EMPTY, onNotFoundResult: (suspend () -> Obj?)? = null ): Obj { + // 0. Prefer private member of current class context + scope.currentClassCtx?.let { caller -> + caller.members[name]?.let { rec -> + if (rec.visibility == Visibility.Private && !rec.isAbstract) { + if (rec.type == ObjRecord.Type.Property) { + if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, caller) + } else if (rec.type != ObjRecord.Type.Delegated) { + return rec.value.invoke(scope, this, args, caller) + } + } + } + } + // 1. Hierarchy members (excluding root fallback) for (cls in objClass.mro) { if (cls.className == "Obj") break val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) - if (rec != null && !rec.isAbstract && rec.type != ObjRecord.Type.Property) { + if (rec != null && !rec.isAbstract) { val decl = rec.declaringClass ?: cls val caller = scope.currentClassCtx if (!canAccessMember(rec.visibility, decl, caller)) scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})")) - return rec.value.invoke(scope, this, args, decl) + + if (rec.type == ObjRecord.Type.Property) { + if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl) + } else if (rec.type != ObjRecord.Type.Delegated) { + return rec.value.invoke(scope, this, args, decl) + } } } // 2. Extensions in scope val extension = scope.findExtension(objClass, name) if (extension != null) { - return extension.value.invoke(scope, this, args) + if (extension.type == ObjRecord.Type.Property) { + if (args.isEmpty()) return (extension.value as ObjProperty).callGetter(scope, this, extension.declaringClass) + } else if (extension.type != ObjRecord.Type.Delegated) { + return extension.value.invoke(scope, this, args) + } } // 3. Root object fallback @@ -120,7 +142,12 @@ open class Obj { val caller = scope.currentClassCtx if (!canAccessMember(rec.visibility, decl, caller)) scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})")) - return rec.value.invoke(scope, this, args, decl) + + if (rec.type == ObjRecord.Type.Property) { + if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl) + } else if (rec.type != ObjRecord.Type.Delegated) { + return rec.value.invoke(scope, this, args, decl) + } } } } @@ -415,29 +442,52 @@ open class Obj { // suspend fun sync(block: () -> T): T = monitor.withLock { block() } open suspend fun readField(scope: Scope, name: String): ObjRecord { + // 0. Prefer private member of current class context + scope.currentClassCtx?.let { caller -> + caller.members[name]?.let { rec -> + if (rec.visibility == Visibility.Private && !rec.isAbstract) { + val resolved = resolveRecord(scope, rec, name, caller) + if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement) + return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, caller)) + return resolved + } + } + } + // 1. Hierarchy members (excluding root fallback) for (cls in objClass.mro) { if (cls.className == "Obj") break val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) - if (rec != null) { - if (!rec.isAbstract) { - return resolveRecord(scope, rec, name, rec.declaringClass) - } + if (rec != null && !rec.isAbstract) { + val decl = rec.declaringClass ?: cls + val resolved = resolveRecord(scope, rec, name, decl) + if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement) + return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, decl)) + return resolved } } // 2. Extensions val extension = scope.findExtension(objClass, name) if (extension != null) { - return resolveRecord(scope, extension, name, extension.declaringClass) + val resolved = resolveRecord(scope, extension, name, extension.declaringClass) + if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement) + return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, extension.declaringClass)) + return resolved } // 3. Root fallback for (cls in objClass.mro) { if (cls.className == "Obj") { - cls.members[name]?.let { - val decl = it.declaringClass ?: cls - return resolveRecord(scope, it, name, decl) + cls.members[name]?.let { rec -> + val decl = rec.declaringClass ?: cls + val caller = scope.currentClassCtx + if (!canAccessMember(rec.visibility, decl, caller)) + scope.raiseError(ObjIllegalAccessException(scope, "can't access field ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})")) + val resolved = resolveRecord(scope, rec, name, decl) + if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement) + return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, decl)) + return resolved } } } @@ -450,17 +500,29 @@ open class Obj { open suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord { if (obj.type == ObjRecord.Type.Delegated) { val del = obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate") + val th = if (this === ObjVoid) ObjNull else this + val res = del.invokeInstanceMethod(scope, "getValue", Arguments(th, ObjString(name)), onNotFoundResult = { + // If getValue not found, return a wrapper that calls invoke + object : Statement() { + override val pos: Pos = Pos.builtIn + override suspend fun execute(s: Scope): Obj { + val th2 = if (s.thisObj === ObjVoid) ObjNull else s.thisObj + val allArgs = (listOf(th2, ObjString(name)) + s.args.list).toTypedArray() + return del.invokeInstanceMethod(s, "invoke", Arguments(*allArgs)) + } + } + }) return obj.copy( - value = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))), + value = res, type = ObjRecord.Type.Other ) } val value = obj.value - if (value is ObjProperty) { - return ObjRecord(value.callGetter(scope, this, decl), obj.isMutable) - } - if (value is Statement && decl != null) { - return ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable) + if (value is ObjProperty || obj.type == ObjRecord.Type.Property) { + val prop = if (value is ObjProperty) value else (value as? Statement)?.execute(scope.createChildScope(scope.pos, newThisObj = this)) as? ObjProperty + ?: scope.raiseError("Expected ObjProperty for property member $name, got ${value::class}") + val res = prop.callGetter(scope, this, decl) + return ObjRecord(res, obj.isMutable) } val caller = scope.currentClassCtx // Check visibility for non-property members here if they weren't checked before @@ -472,13 +534,24 @@ open class Obj { open suspend fun writeField(scope: Scope, name: String, newValue: Obj) { willMutate(scope) var field: ObjRecord? = null + // 0. Prefer private member of current class context + scope.currentClassCtx?.let { caller -> + caller.members[name]?.let { rec -> + if (rec.visibility == Visibility.Private && !rec.isAbstract) { + field = rec + } + } + } + // 1. Hierarchy members (excluding root fallback) - for (cls in objClass.mro) { - if (cls.className == "Obj") break - val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) - if (rec != null && !rec.isAbstract) { - field = rec - break + if (field == null) { + for (cls in objClass.mro) { + if (cls.className == "Obj") break + val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) + if (rec != null && !rec.isAbstract) { + field = rec + break + } } } // 2. Extensions @@ -512,12 +585,19 @@ open class Obj { } open suspend fun getAt(scope: Scope, index: Obj): Obj { + if (index is ObjString) { + return readField(scope, index.value).value + } scope.raiseNotImplemented("indexing") } suspend fun getAt(scope: Scope, index: Int): Obj = getAt(scope, ObjInt(index.toLong())) open suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) { + if (index is ObjString) { + writeField(scope, index.value, newValue) + return + } scope.raiseNotImplemented("indexing") } @@ -683,9 +763,7 @@ open class Obj { } - @Suppress("NOTHING_TO_INLINE") - inline fun from(obj: Any?): Obj { - @Suppress("UNCHECKED_CAST") + fun from(obj: Any?): Obj { return when (obj) { is Obj -> obj is Double -> ObjReal(obj) @@ -694,16 +772,16 @@ open class Obj { is Long -> ObjInt.of(obj) is String -> ObjString(obj) is CharSequence -> ObjString(obj.toString()) + is Char -> ObjChar(obj) is Boolean -> ObjBool(obj) - is Set<*> -> ObjSet((obj as Set).toMutableSet()) + is Set<*> -> ObjSet(obj.map { from(it) }.toMutableSet()) + is List<*> -> ObjList(obj.map { from(it) }.toMutableList()) + is Map<*, *> -> ObjMap(obj.entries.associate { from(it.key) to from(it.value) }.toMutableMap()) + is Map.Entry<*, *> -> ObjMapEntry(from(obj.key), from(obj.value)) + is Enum<*> -> ObjString(obj.name) Unit -> ObjVoid null -> ObjNull - is Iterator<*> -> ObjKotlinIterator(obj) - is Map.Entry<*, *> -> { - obj as MutableMap.MutableEntry - ObjMapEntry(obj.key, obj.value) - } - + is Iterator<*> -> ObjKotlinIterator(obj as Iterator) else -> throw IllegalArgumentException("cannot convert to Obj: $obj") } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjArray.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjArray.kt index 3200b9d..4209d06 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjArray.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjArray.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. @@ -17,10 +17,7 @@ package net.sergeych.lyng.obj -import net.sergeych.lyng.miniast.ParamDoc -import net.sergeych.lyng.miniast.TypeGenericDoc -import net.sergeych.lyng.miniast.addFnDoc -import net.sergeych.lyng.miniast.type +import net.sergeych.lyng.miniast.* val ObjArray by lazy { @@ -52,32 +49,35 @@ val ObjArray by lazy { ObjFalse } - addFnDoc( + addPropertyDoc( name = "last", doc = "The last element of this array.", - returns = type("lyng.Any"), - moduleName = "lyng.stdlib" - ) { - thisObj.invokeInstanceMethod( - this, - "getAt", - (thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj() - ) - } + type = type("lyng.Any"), + moduleName = "lyng.stdlib", + getter = { + this.thisObj.invokeInstanceMethod( + this, + "getAt", + (this.thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj() + ) + } + ) - addFnDoc( + addPropertyDoc( name = "lastIndex", doc = "Index of the last element (size - 1).", - returns = type("lyng.Int"), - moduleName = "lyng.stdlib" - ) { (thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj() } + type = type("lyng.Int"), + moduleName = "lyng.stdlib", + getter = { (this.thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj() } + ) - addFnDoc( + addPropertyDoc( name = "indices", doc = "Range of valid indices for this array.", - returns = type("lyng.Range"), - moduleName = "lyng.stdlib" - ) { ObjRange(0.toObj(), thisObj.invokeInstanceMethod(this, "size"), false) } + type = type("lyng.Range"), + moduleName = "lyng.stdlib", + getter = { ObjRange(0.toObj(), this.thisObj.invokeInstanceMethod(this, "size"), false) } + ) addFnDoc( name = "binarySearch", diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBitBuffer.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBitBuffer.kt index fe0173d..2eaebf9 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBitBuffer.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBitBuffer.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. @@ -19,6 +19,8 @@ package net.sergeych.lyng.obj import net.sergeych.bintools.toDump import net.sergeych.lyng.Scope +import net.sergeych.lyng.miniast.addPropertyDoc +import net.sergeych.lyng.miniast.type import net.sergeych.lynon.BitArray class ObjBitBuffer(val bitArray: BitArray) : Obj() { @@ -43,12 +45,20 @@ class ObjBitBuffer(val bitArray: BitArray) : Obj() { thisAs().bitArray.asUByteArray().toDump() ) } - addFn("size") { - thisAs().bitArray.size.toObj() - } - addFn("sizeInBytes") { - ObjInt((thisAs().bitArray.size + 7) shr 3) - } + addPropertyDoc( + name = "size", + doc = "Size of the bit buffer in bits.", + type = type("lyng.Int"), + moduleName = "lyng.stdlib", + getter = { thisAs().bitArray.size.toObj() } + ) + addPropertyDoc( + name = "sizeInBytes", + doc = "Size of the bit buffer in full bytes (rounded up).", + type = type("lyng.Int"), + moduleName = "lyng.stdlib", + getter = { ObjInt((thisAs().bitArray.size + 7) shr 3) } + ) } } } \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBuffer.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBuffer.kt index 58f654e..563d232 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBuffer.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjBuffer.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. @@ -23,7 +23,8 @@ import net.sergeych.bintools.decodeHex import net.sergeych.bintools.encodeToHex import net.sergeych.bintools.toDump import net.sergeych.lyng.Scope -import net.sergeych.lyng.statement +import net.sergeych.lyng.miniast.addPropertyDoc +import net.sergeych.lyng.miniast.type import net.sergeych.lynon.BitArray import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder @@ -174,20 +175,26 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() { addClassFn("decodeHex") { ObjBuffer(requireOnlyArg().toString().decodeHex().asUByteArray()) } - createField("size", - statement { - (thisObj as ObjBuffer).byteArray.size.toObj() - } + addPropertyDoc( + name = "size", + doc = "Number of bytes in this buffer.", + type = type("lyng.Int"), + moduleName = "lyng.stdlib", + getter = { (this.thisObj as ObjBuffer).byteArray.size.toObj() } ) - createField("hex", - statement { - thisAs().hex.toObj() - } + addPropertyDoc( + name = "hex", + doc = "Hexadecimal string representation of the buffer.", + type = type("lyng.String"), + moduleName = "lyng.stdlib", + getter = { thisAs().hex.toObj() } ) - createField("base64", - statement { - thisAs().base64.toObj() - } + addPropertyDoc( + name = "base64", + doc = "Base64 (URL-safe) string representation of the buffer.", + type = type("lyng.String"), + moduleName = "lyng.stdlib", + getter = { thisAs().base64.toObj() } ) addFn("decodeUtf8") { ObjString( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjChar.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjChar.kt index 26dfbaa..fbecdce 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjChar.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjChar.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,7 @@ package net.sergeych.lyng.obj import net.sergeych.lyng.Scope -import net.sergeych.lyng.miniast.addFnDoc +import net.sergeych.lyng.miniast.addPropertyDoc import net.sergeych.lyng.miniast.type class ObjChar(val value: Char): Obj() { @@ -47,12 +47,13 @@ class ObjChar(val value: Char): Obj() { companion object { val type = ObjClass("Char").apply { - addFnDoc( + addPropertyDoc( name = "code", doc = "Unicode code point (UTF-16 code unit) of this character.", - returns = type("lyng.Int"), - moduleName = "lyng.stdlib" - ) { ObjInt(thisAs().value.code.toLong()) } + type = type("lyng.Int"), + moduleName = "lyng.stdlib", + getter = { ObjInt((this.thisObj as ObjChar).value.code.toLong()) } + ) addFn("isDigit") { thisAs().value.isDigit().toObj() } 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 0d7436f..9e3cbb0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -18,10 +18,7 @@ package net.sergeych.lyng.obj import net.sergeych.lyng.* -import net.sergeych.lyng.miniast.ParamDoc -import net.sergeych.lyng.miniast.TypeGenericDoc -import net.sergeych.lyng.miniast.addFnDoc -import net.sergeych.lyng.miniast.type +import net.sergeych.lyng.miniast.* import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonType @@ -30,47 +27,56 @@ private object ClassIdGen { var c: Long = 1L; fun nextId(): Long = c++ } val ObjClassType by lazy { ObjClass("Class").apply { - addProperty("className", getter = { thisAs().classNameObj }) - addFnDoc( + addPropertyDoc( + name = "className", + doc = "Full name of this class including package if available.", + type = type("lyng.String"), + moduleName = "lyng.stdlib", + getter = { (this.thisObj as ObjClass).classNameObj } + ) + addPropertyDoc( name = "name", doc = "Simple name of this class (without package).", - returns = type("lyng.String"), - moduleName = "lyng.stdlib" - ) { thisAs().classNameObj } + type = type("lyng.String"), + moduleName = "lyng.stdlib", + getter = { (this.thisObj as ObjClass).classNameObj } + ) - addFnDoc( + addPropertyDoc( name = "fields", doc = "Declared instance fields of this class and its ancestors (C3 order), without duplicates.", - returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))), - moduleName = "lyng.stdlib" - ) { - val cls = thisAs() - val seen = hashSetOf() - val names = mutableListOf() - for (c in cls.mro) { - for ((n, rec) in c.members) { - if (rec.value !is Statement && seen.add(n)) names += ObjString(n) + type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))), + moduleName = "lyng.stdlib", + getter = { + val cls = this.thisObj as ObjClass + val seen = hashSetOf() + val names = mutableListOf() + for (c in cls.mro) { + for ((n, rec) in c.members) { + if (rec.value !is Statement && seen.add(n)) names += ObjString(n) + } } + ObjList(names.toMutableList()) } - ObjList(names.toMutableList()) - } + ) - addFnDoc( + addPropertyDoc( name = "methods", doc = "Declared instance methods of this class and its ancestors (C3 order), without duplicates.", - returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))), - moduleName = "lyng.stdlib" - ) { - val cls = thisAs() - val seen = hashSetOf() - val names = mutableListOf() - for (c in cls.mro) { - for ((n, rec) in c.members) { - if (rec.value is Statement && seen.add(n)) names += ObjString(n) + type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))), + moduleName = "lyng.stdlib", + getter = { + val cls = this.thisObj as ObjClass + val seen = hashSetOf() + val names = mutableListOf() + for (c in cls.mro) { + for ((n, rec) in c.members) { + if (rec.value is Statement && seen.add(n)) names += ObjString(n) + } } + ObjList(names.toMutableList()) } - ObjList(names.toMutableList()) - } + ) addFnDoc( name = "get", @@ -247,21 +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 - // 1) members-defined methods - for ((k, v) in members) { - if (v.value is Statement || v.type == ObjRecord.Type.Delegated) { - instance.instanceScope.objects[k] = if (v.type == ObjRecord.Type.Delegated) v.copy() else v + + for (cls in mro) { + // 1) members-defined methods + 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 (!instance.instanceScope.objects.containsKey(key)) { + instance.instanceScope.objects[key] = if (v.type == ObjRecord.Type.Delegated) v.copy() else v + } + } } - } - // 2) class-scope methods registered during class-body execution - classScope?.objects?.forEach { (k, rec) -> - if (rec.value is Statement || rec.type == ObjRecord.Type.Delegated) { - // if not already present, copy reference for dispatch - if (!instance.instanceScope.objects.containsKey(k)) { - instance.instanceScope.objects[k] = if (rec.type == ObjRecord.Type.Delegated) rec.copy() else rec + // 2) class-scope methods 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 + // 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 + } } } } @@ -300,7 +314,13 @@ open class ObjClass( c.constructorMeta?.let { meta -> val argsHere = argsForThis ?: Arguments.EMPTY // Assign constructor params into instance scope (unmangled) - meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c) + val savedCtx = instance.instanceScope.currentClassCtx + instance.instanceScope.currentClassCtx = c + try { + meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c) + } finally { + instance.instanceScope.currentClassCtx = savedCtx + } // Also expose them under MI-mangled storage keys `${Class}::name` so qualified views can access them // and so that base-class casts like `(obj as Base).field` work. for (p in meta.params) { @@ -329,7 +349,13 @@ open class ObjClass( // parameters even if they were shadowed/overwritten by parent class initialization. c.constructorMeta?.let { meta -> val argsHere = argsForThis ?: Arguments.EMPTY - meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c) + val savedCtx = instance.instanceScope.currentClassCtx + instance.instanceScope.currentClassCtx = c + try { + meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c) + } finally { + instance.instanceScope.currentClassCtx = savedCtx + } // Re-sync mangled names to point to the fresh records to keep them consistent for (p in meta.params) { val rec = instance.instanceScope.objects[p.name] @@ -382,7 +408,8 @@ open class ObjClass( ): ObjRecord { // Validation of override rules: only for non-system declarations if (pos != Pos.builtIn) { - val existing = getInstanceMemberOrNull(name) + // Only consider TRUE instance members from ancestors for overrides + val existing = getInstanceMemberOrNull(name, includeAbstract = true, includeStatic = false) var actualOverride = false if (existing != null && existing.declaringClass != this) { // If the existing member is private in the ancestor, it's not visible for overriding. @@ -505,14 +532,21 @@ open class ObjClass( /** * Get instance member traversing the hierarchy if needed. Its meaning is different for different objects. */ - fun getInstanceMemberOrNull(name: String): ObjRecord? { + fun getInstanceMemberOrNull(name: String, includeAbstract: Boolean = false, includeStatic: Boolean = true): ObjRecord? { // Unified traversal in strict C3 order: self, then each ancestor, checking members before classScope for (cls in mro) { - cls.members[name]?.let { return it } - cls.classScope?.objects?.get(name)?.let { return it } + cls.members[name]?.let { + if (includeAbstract || !it.isAbstract) return it + } + if (includeStatic) { + cls.classScope?.objects?.get(name)?.let { + if (includeAbstract || !it.isAbstract) return it + } + } } // Finally, allow root object fallback (rare; mostly built-ins like toString) - return rootObjectType.members[name] + val rootRec = rootObjectType.members[name] + return if (rootRec != null && (includeAbstract || !rootRec.isAbstract)) rootRec else null } /** Find the declaring class where a member with [name] is defined, starting from this class along MRO. */ diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjCollection.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjCollection.kt index a73e076..18cc9e7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjCollection.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjCollection.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. @@ -21,5 +21,6 @@ package net.sergeych.lyng.obj * Collection is an iterator with `size` */ val ObjCollection = ObjClass("Collection", ObjIterable).apply { + addProperty("size", isAbstract = true) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDeferred.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDeferred.kt index 54a85b0..49ff90f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDeferred.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDeferred.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. @@ -20,6 +20,7 @@ package net.sergeych.lyng.obj import kotlinx.coroutines.Deferred import net.sergeych.lyng.Scope import net.sergeych.lyng.miniast.addFnDoc +import net.sergeych.lyng.miniast.addPropertyDoc import net.sergeych.lyng.miniast.type open class ObjDeferred(val deferred: Deferred): Obj() { @@ -38,28 +39,30 @@ open class ObjDeferred(val deferred: Deferred): Obj() { returns = type("lyng.Any"), moduleName = "lyng.stdlib" ) { thisAs().deferred.await() } - addFnDoc( + addPropertyDoc( name = "isCompleted", doc = "Whether this deferred has completed (successfully or with an error).", - returns = type("lyng.Bool"), - moduleName = "lyng.stdlib" - ) { thisAs().deferred.isCompleted.toObj() } - addFnDoc( + type = type("lyng.Bool"), + moduleName = "lyng.stdlib", + getter = { thisAs().deferred.isCompleted.toObj() } + ) + addPropertyDoc( name = "isActive", doc = "Whether this deferred is currently active (not completed and not cancelled).", - returns = type("lyng.Bool"), - moduleName = "lyng.stdlib" - ) { - val d = thisAs().deferred - (d.isActive || (!d.isCompleted && !d.isCancelled)).toObj() - } - addFnDoc( + type = type("lyng.Bool"), + moduleName = "lyng.stdlib", + getter = { + val d = thisAs().deferred + (d.isActive || (!d.isCompleted && !d.isCancelled)).toObj() + } + ) + addPropertyDoc( name = "isCancelled", doc = "Whether this deferred was cancelled.", - returns = type("lyng.Bool"), - moduleName = "lyng.stdlib" - ) { thisAs().deferred.isCancelled.toObj() } - + type = type("lyng.Bool"), + moduleName = "lyng.stdlib", + getter = { thisAs().deferred.isCancelled.toObj() } + ) } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDuration.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDuration.kt index 0761f0f..6f080fd 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDuration.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjDuration.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,7 @@ package net.sergeych.lyng.obj import net.sergeych.lyng.Scope -import net.sergeych.lyng.miniast.addFnDoc +import net.sergeych.lyng.miniast.addPropertyDoc import net.sergeych.lyng.miniast.type import kotlin.time.Duration import kotlin.time.Duration.Companion.days @@ -74,169 +74,195 @@ class ObjDuration(val duration: Duration) : Obj() { ) } }.apply { - addFnDoc( + addPropertyDoc( name = "days", doc = "Return this duration as a real number of days.", - returns = type("lyng.Real"), - moduleName = "lyng.time" - ) { thisAs().duration.toDouble(DurationUnit.DAYS).toObj() } - addFnDoc( + type = type("lyng.Real"), + moduleName = "lyng.time", + getter = { thisAs().duration.toDouble(DurationUnit.DAYS).toObj() } + ) + addPropertyDoc( name = "hours", doc = "Return this duration as a real number of hours.", - returns = type("lyng.Real"), - moduleName = "lyng.time" - ) { thisAs().duration.toDouble(DurationUnit.HOURS).toObj() } - addFnDoc( + type = type("lyng.Real"), + moduleName = "lyng.time", + getter = { thisAs().duration.toDouble(DurationUnit.HOURS).toObj() } + ) + addPropertyDoc( name = "minutes", doc = "Return this duration as a real number of minutes.", - returns = type("lyng.Real"), - moduleName = "lyng.time" - ) { thisAs().duration.toDouble(DurationUnit.MINUTES).toObj() } - addFnDoc( + type = type("lyng.Real"), + moduleName = "lyng.time", + getter = { thisAs().duration.toDouble(DurationUnit.MINUTES).toObj() } + ) + addPropertyDoc( name = "seconds", doc = "Return this duration as a real number of seconds.", - returns = type("lyng.Real"), - moduleName = "lyng.time" - ) { thisAs().duration.toDouble(DurationUnit.SECONDS).toObj() } - addFnDoc( + type = type("lyng.Real"), + moduleName = "lyng.time", + getter = { thisAs().duration.toDouble(DurationUnit.SECONDS).toObj() } + ) + addPropertyDoc( name = "milliseconds", doc = "Return this duration as a real number of milliseconds.", - returns = type("lyng.Real"), - moduleName = "lyng.time" - ) { thisAs().duration.toDouble(DurationUnit.MILLISECONDS).toObj() } - addFnDoc( + type = type("lyng.Real"), + moduleName = "lyng.time", + getter = { thisAs().duration.toDouble(DurationUnit.MILLISECONDS).toObj() } + ) + addPropertyDoc( name = "microseconds", doc = "Return this duration as a real number of microseconds.", - returns = type("lyng.Real"), - moduleName = "lyng.time" - ) { thisAs().duration.toDouble(DurationUnit.MICROSECONDS).toObj() } + type = type("lyng.Real"), + moduleName = "lyng.time", + getter = { thisAs().duration.toDouble(DurationUnit.MICROSECONDS).toObj() } + ) // extensions - ObjInt.type.addFnDoc( + ObjInt.type.addPropertyDoc( name = "seconds", doc = "Construct a `Duration` equal to this integer number of seconds.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.seconds) } + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.seconds) } + ) - ObjInt.type.addFnDoc( + ObjInt.type.addPropertyDoc( name = "second", doc = "Construct a `Duration` equal to this integer number of seconds.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.seconds) } - ObjInt.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.seconds) } + ) + ObjInt.type.addPropertyDoc( name = "milliseconds", doc = "Construct a `Duration` equal to this integer number of milliseconds.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.milliseconds) } + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.milliseconds) } + ) - ObjInt.type.addFnDoc( + ObjInt.type.addPropertyDoc( name = "millisecond", doc = "Construct a `Duration` equal to this integer number of milliseconds.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.milliseconds) } - ObjReal.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.milliseconds) } + ) + ObjReal.type.addPropertyDoc( name = "seconds", doc = "Construct a `Duration` equal to this real number of seconds.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.seconds) } + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.seconds) } + ) - ObjReal.type.addFnDoc( + ObjReal.type.addPropertyDoc( name = "second", doc = "Construct a `Duration` equal to this real number of seconds.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.seconds) } + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.seconds) } + ) - ObjReal.type.addFnDoc( + ObjReal.type.addPropertyDoc( name = "milliseconds", doc = "Construct a `Duration` equal to this real number of milliseconds.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.milliseconds) } - ObjReal.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.milliseconds) } + ) + ObjReal.type.addPropertyDoc( name = "millisecond", doc = "Construct a `Duration` equal to this real number of milliseconds.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.milliseconds) } + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.milliseconds) } + ) - ObjInt.type.addFnDoc( + ObjInt.type.addPropertyDoc( name = "minutes", doc = "Construct a `Duration` equal to this integer number of minutes.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.minutes) } - ObjReal.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.minutes) } + ) + ObjReal.type.addPropertyDoc( name = "minutes", doc = "Construct a `Duration` equal to this real number of minutes.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.minutes) } - ObjInt.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.minutes) } + ) + ObjInt.type.addPropertyDoc( name = "minute", doc = "Construct a `Duration` equal to this integer number of minutes.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.minutes) } - ObjReal.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.minutes) } + ) + ObjReal.type.addPropertyDoc( name = "minute", doc = "Construct a `Duration` equal to this real number of minutes.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.minutes) } - ObjInt.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.minutes) } + ) + ObjInt.type.addPropertyDoc( name = "hours", doc = "Construct a `Duration` equal to this integer number of hours.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.hours) } - ObjReal.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.hours) } + ) + ObjReal.type.addPropertyDoc( name = "hours", doc = "Construct a `Duration` equal to this real number of hours.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.hours) } - ObjInt.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.hours) } + ) + ObjInt.type.addPropertyDoc( name = "hour", doc = "Construct a `Duration` equal to this integer number of hours.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.hours) } - ObjReal.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.hours) } + ) + ObjReal.type.addPropertyDoc( name = "hour", doc = "Construct a `Duration` equal to this real number of hours.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.hours) } - ObjInt.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.hours) } + ) + ObjInt.type.addPropertyDoc( name = "days", doc = "Construct a `Duration` equal to this integer number of days.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.days) } - ObjReal.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.days) } + ) + ObjReal.type.addPropertyDoc( name = "days", doc = "Construct a `Duration` equal to this real number of days.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.days) } - ObjInt.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.days) } + ) + ObjInt.type.addPropertyDoc( name = "day", doc = "Construct a `Duration` equal to this integer number of days.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.days) } - ObjReal.type.addFnDoc( + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.days) } + ) + ObjReal.type.addPropertyDoc( name = "day", doc = "Construct a `Duration` equal to this real number of days.", - returns = type("lyng.Duration"), - moduleName = "lyng.time" - ) { ObjDuration(thisAs().value.days) } + type = type("lyng.Duration"), + moduleName = "lyng.time", + getter = { ObjDuration(thisAs().value.days) } + ) // addFn("epochSeconds") { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjEnum.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjEnum.kt index e48fbde..d7e9864 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjEnum.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjEnum.kt @@ -35,6 +35,8 @@ package net.sergeych.lyng.obj/* import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive import net.sergeych.lyng.Scope +import net.sergeych.lyng.miniast.addPropertyDoc +import net.sergeych.lyng.miniast.type import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonType @@ -76,8 +78,8 @@ class ObjEnumClass(val name: String) : ObjClass(name, EnumBase) { val name = requireOnlyArg() byName[name] ?: raiseSymbolNotFound("does not exists: enum ${className}.$name") } - addFn("name") { thisAs().name } - addFn("ordinal") { thisAs().ordinal } + addPropertyDoc("name", doc = "Entry name as string", type = type("lyng.String"), getter = { thisAs().name }) + addPropertyDoc("ordinal", doc = "Entry ordinal position", type = type("lyng.Int"), getter = { thisAs().ordinal }) } 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 7ffb05c..9143dce 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt @@ -17,11 +17,10 @@ package net.sergeych.lyng.obj -import net.sergeych.bintools.encodeToHex import net.sergeych.lyng.* import net.sergeych.lyng.miniast.TypeGenericDoc -import net.sergeych.lyng.miniast.addConstDoc import net.sergeych.lyng.miniast.addFnDoc +import net.sergeych.lyng.miniast.addPropertyDoc import net.sergeych.lyng.miniast.type import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder @@ -133,7 +132,7 @@ open class ObjException( return ObjException(this, scope, message) } - override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}" + override fun toString(): String = name override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj { return try { @@ -170,43 +169,44 @@ open class ObjException( ObjVoid }) instanceConstructor = statement { ObjVoid } - addConstDoc( + addPropertyDoc( name = "message", - value = statement { - when (val t = thisObj) { + doc = "Human‑readable error message.", + type = type("lyng.String"), + moduleName = "lyng.stdlib", + getter = { + when (val t = this.thisObj) { is ObjException -> t.message is ObjInstance -> t.instanceScope.get("Exception::message")?.value ?: ObjNull else -> ObjNull } - }, - doc = "Human‑readable error message.", - type = type("lyng.String"), - moduleName = "lyng.stdlib" + } ) - addConstDoc( + addPropertyDoc( name = "extraData", - value = statement { - when (val t = thisObj) { + doc = "Extra data associated with the exception.", + type = type("lyng.Any", nullable = true), + moduleName = "lyng.stdlib", + getter = { + when (val t = this.thisObj) { is ObjException -> t.extraData else -> ObjNull } - }, - doc = "Extra data associated with the exception.", - type = type("lyng.Any", nullable = true), - moduleName = "lyng.stdlib" + } ) - addFnDoc( + addPropertyDoc( name = "stackTrace", doc = "Stack trace captured at throw site as a list of `StackTraceEntry`.", - returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.StackTraceEntry"))), - moduleName = "lyng.stdlib" - ) { - when (val t = thisObj) { - is ObjException -> t.getStackTrace() - is ObjInstance -> t.instanceScope.get("Exception::stackTrace")?.value as? ObjList ?: ObjList() - else -> ObjList() + type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.StackTraceEntry"))), + moduleName = "lyng.stdlib", + getter = { + when (val t = this.thisObj) { + is ObjException -> t.getStackTrace() + is ObjInstance -> t.instanceScope.get("Exception::stackTrace")?.value as? ObjList ?: ObjList() + else -> ObjList() + } } - } + ) addFnDoc( name = "toString", doc = "Human‑readable string representation of the error.", 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 ef030fd..a58928f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -21,6 +21,7 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import net.sergeych.lyng.Arguments import net.sergeych.lyng.Scope +import net.sergeych.lyng.Visibility import net.sergeych.lyng.canAccessMember import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder @@ -31,50 +32,35 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { internal lateinit var instanceScope: Scope override suspend fun readField(scope: Scope, name: String): ObjRecord { - // 1. Direct (unmangled) lookup first - instanceScope[name]?.let { rec -> + val caller = scope.currentClassCtx + + // 0. Private mangled of current class context + caller?.let { c -> + val mangled = "${c.className}::$name" + instanceScope.objects[mangled]?.let { rec -> + if (rec.visibility == Visibility.Private) { + return resolveRecord(scope, rec, name, c) + } + } + } + + // 1. MRO mangled storage + for (cls in objClass.mro) { + if (cls.className == "Obj") break + val mangled = "${cls.className}::$name" + instanceScope.objects[mangled]?.let { rec -> + if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, cls, caller)) { + return resolveRecord(scope, rec, name, cls) + } + } + } + + // 2. Unmangled storage + instanceScope.objects[name]?.let { rec -> val decl = rec.declaringClass - // Allow unconditional access when accessing through `this` of the same instance - // BUT only if we are in the class context (not extension) - if (scope.thisObj !== this || scope.currentClassCtx == null) { - 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) - } - - // 2. MI-mangled instance scope lookup - val cls = objClass - fun findMangledInRead(): ObjRecord? { - instanceScope.objects["${cls.className}::$name"]?.let { return it } - for (p in cls.mroParents) { - instanceScope.objects["${p.className}::$name"]?.let { return it } - } - return null - } - - findMangledInRead()?.let { rec -> - val declaring = when { - instanceScope.objects.containsKey("${cls.className}::$name") -> cls - else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") } - } - if (scope.thisObj !== this || scope.currentClassCtx == null) { - val caller = scope.currentClassCtx - if (!canAccessMember(rec.visibility, declaring, caller)) - scope.raiseError( - ObjIllegalAccessException( - scope, - "can't access field $name (declared in ${declaring?.className ?: "?"})" - ) - ) - } - return resolveRecord(scope, rec, name, declaring) + // 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)) + return resolveRecord(scope, rec, name, decl) } // 3. Fall back to super (handles class members and extensions) @@ -83,6 +69,90 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { override suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord { if (obj.type == ObjRecord.Type.Delegated) { + val d = decl ?: obj.declaringClass + val storageName = "${d?.className}::$name" + var del = instanceScope[storageName]?.delegate ?: obj.delegate + if (del == null) { + for (c in objClass.mro) { + del = instanceScope["${c.className}::$name"]?.delegate + if (del != null) break + } + } + del = del ?: scope.raiseError("Internal error: delegated property $name has no delegate") + val res = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))) + obj.value = res + return obj + } + + // Map member template to instance storage if applicable + var targetRec = obj + val d = decl ?: obj.declaringClass + if (d != null) { + val mangled = "${d.className}::$name" + instanceScope.objects[mangled]?.let { + targetRec = it + } + } + if (targetRec === obj) { + instanceScope.objects[name]?.let { rec -> + // Check if this record in instanceScope is the one we want. + // For members, it must match the declaring class. + // Arguments are also preferred. + if (rec.type == ObjRecord.Type.Argument || rec.declaringClass == d || rec.declaringClass == null) { + targetRec = rec + } + } + } + + return super.resolveRecord(scope, targetRec, name, d) + } + + override suspend fun writeField(scope: Scope, name: String, newValue: Obj) { + willMutate(scope) + val caller = scope.currentClassCtx + + // 0. Private mangled of current class context + caller?.let { c -> + val mangled = "${c.className}::$name" + instanceScope.objects[mangled]?.let { rec -> + if (rec.visibility == Visibility.Private) { + updateRecord(scope, rec, name, newValue, c) + return + } + } + } + + // 1. MRO mangled storage + for (cls in objClass.mro) { + if (cls.className == "Obj") break + val mangled = "${cls.className}::$name" + instanceScope.objects[mangled]?.let { rec -> + if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, cls, caller)) { + updateRecord(scope, rec, name, newValue, cls) + return + } + } + } + + // 2. Unmangled storage + instanceScope.objects[name]?.let { rec -> + val decl = rec.declaringClass + if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, decl, caller)) { + updateRecord(scope, rec, name, newValue, decl) + return + } + } + + super.writeField(scope, name, newValue) + } + + private suspend fun updateRecord(scope: Scope, rec: ObjRecord, name: String, newValue: Obj, decl: ObjClass?) { + if (rec.type == ObjRecord.Type.Property) { + val prop = rec.value as ObjProperty + prop.callSetter(scope, this, newValue, decl) + return + } + if (rec.type == ObjRecord.Type.Delegated) { val storageName = "${decl?.className}::$name" var del = instanceScope[storageName]?.delegate if (del == null) { @@ -91,136 +161,80 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { if (del != null) break } } - del = del ?: obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)") - val res = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))) - obj.value = res - return obj - } - return super.resolveRecord(scope, obj, name, decl) - } - - override suspend fun writeField(scope: Scope, name: String, newValue: Obj) { - // Direct (unmangled) first - instanceScope[name]?.let { f -> - val decl = f.declaringClass - if (scope.thisObj !== this || scope.currentClassCtx == null) { - val caller = scope.currentClassCtx - if (!canAccessMember(f.effectiveWriteVisibility, decl, caller)) - ObjIllegalAccessException( - scope, - "can't assign to field $name (declared in ${decl?.className ?: "?"})" - ).raise() - } - if (f.type == ObjRecord.Type.Property) { - val prop = f.value as ObjProperty - prop.callSetter(scope, this, newValue, decl) - return - } - if (f.type == ObjRecord.Type.Delegated) { - val storageName = "${decl?.className}::$name" - var del = instanceScope[storageName]?.delegate - if (del == null) { - for (c in objClass.mro) { - del = instanceScope["${c.className}::$name"]?.delegate - if (del != null) break - } - } - del = del ?: f.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)") - del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue)) - return - } - if (!f.isMutable && f.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise() - if (f.value.assign(scope, newValue) == null) - f.value = newValue + del = del ?: rec.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)") + del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue)) return } - // Try MI-mangled resolution along linearization (C3 MRO) - val cls = objClass - fun findMangled(): ObjRecord? { - instanceScope.objects["${cls.className}::$name"]?.let { return it } - for (p in cls.mroParents) { - instanceScope.objects["${p.className}::$name"]?.let { return it } - } - return null - } - - val rec = findMangled() - if (rec != null) { - val declaring = when { - instanceScope.objects.containsKey("${cls.className}::$name") -> cls - else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") } - } - if (scope.thisObj !== this || scope.currentClassCtx == null) { - val caller = scope.currentClassCtx - if (!canAccessMember(rec.effectiveWriteVisibility, declaring, caller)) - ObjIllegalAccessException( - scope, - "can't assign to field $name (declared in ${declaring?.className ?: "?"})" - ).raise() - } - if (rec.type == ObjRecord.Type.Property) { - val prop = rec.value as ObjProperty - prop.callSetter(scope, this, newValue, declaring) - return - } - if (rec.type == ObjRecord.Type.Delegated) { - val storageName = "${declaring?.className}::$name" - var del = instanceScope[storageName]?.delegate - if (del == null) { - for (c in objClass.mro) { - del = instanceScope["${c.className}::$name"]?.delegate - if (del != null) break - } - } - del = del ?: rec.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)") - del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue)) - return - } - if (!rec.isMutable && rec.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise() - if (rec.value.assign(scope, newValue) == null) - rec.value = newValue - return - } - super.writeField(scope, name, newValue) + if (!rec.isMutable && rec.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise() + if (rec.value.assign(scope, newValue) == null) + rec.value = newValue } override suspend fun invokeInstanceMethod( scope: Scope, name: String, args: Arguments, onNotFoundResult: (suspend () -> Obj?)? ): Obj { + // 0. Prefer private member of current class context + scope.currentClassCtx?.let { caller -> + val mangled = "${caller.className}::$name" + instanceScope.objects[mangled]?.let { rec -> + if (rec.visibility == Visibility.Private && !rec.isAbstract) { + if (rec.type == ObjRecord.Type.Property) { + if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, caller) + } else if (rec.type == ObjRecord.Type.Fun) { + return rec.value.invoke(instanceScope, this, args, caller) + } + } + } + caller.members[name]?.let { rec -> + if (rec.visibility == Visibility.Private && !rec.isAbstract) { + if (rec.type == ObjRecord.Type.Property) { + if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, caller) + } else if (rec.type == ObjRecord.Type.Fun) { + return rec.value.invoke(instanceScope, this, args, caller) + } + } + } + } + // 1. Walk MRO to find member, handling delegation for (cls in objClass.mro) { if (cls.className == "Obj") break val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) - if (rec != null) { + if (rec != null && !rec.isAbstract) { if (rec.type == ObjRecord.Type.Delegated) { val storageName = "${cls.className}::$name" val del = instanceScope[storageName]?.delegate ?: rec.delegate ?: scope.raiseError("Internal error: delegated member $name has no delegate (tried $storageName)") + + // For delegated member, try 'invoke' first if it's a function-like call val allArgs = (listOf(this, ObjString(name)) + args.list).toTypedArray() return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs), onNotFoundResult = { - // Fallback: property delegation + // Fallback: property delegation (getValue then call result) val propVal = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))) propVal.invoke(scope, this, args, rec.declaringClass ?: cls) }) } - if (rec.type == ObjRecord.Type.Fun && !rec.isAbstract) { - val decl = rec.declaringClass ?: cls - val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null - if (!canAccessMember(rec.visibility, decl, caller)) - scope.raiseError( - ObjIllegalAccessException( - scope, - "can't invoke method $name (declared in ${decl.className})" - ) + val decl = rec.declaringClass ?: cls + val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null + if (!canAccessMember(rec.visibility, decl, caller)) + scope.raiseError( + ObjIllegalAccessException( + scope, + "can't invoke method $name (declared in ${decl.className})" ) + ) + + if (rec.type == ObjRecord.Type.Property) { + if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl) + } else if (rec.type == ObjRecord.Type.Fun) { return rec.value.invoke( instanceScope, this, args, decl ) - } else if ((rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property) && !rec.isAbstract) { + } else if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.ConstructorField || rec.type == ObjRecord.Type.Argument) { val resolved = readField(scope, name) return resolved.value.invoke(scope, this, args, resolved.declaringClass) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.kt index 7af1f02..d13ddbe 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstant.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. @@ -20,6 +20,9 @@ package net.sergeych.lyng.obj import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive import net.sergeych.lyng.Scope +import net.sergeych.lyng.miniast.addFnDoc +import net.sergeych.lyng.miniast.addPropertyDoc +import net.sergeych.lyng.miniast.type import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonSettings @@ -148,34 +151,71 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru } }.apply { - addFn("epochSeconds") { - val instant = thisAs().instant - ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9) - } - addFn("isDistantFuture") { - thisAs().instant.isDistantFuture.toObj() - } - addFn("isDistantPast") { - thisAs().instant.isDistantPast.toObj() - } - addFn("epochWholeSeconds") { - ObjInt(thisAs().instant.epochSeconds) - } - addFn("nanosecondsOfSecond") { - ObjInt(thisAs().instant.nanosecondsOfSecond.toLong()) - } - addFn("truncateToSecond") { + addPropertyDoc( + name = "epochSeconds", + doc = "Return the number of seconds since the Unix epoch as a real number (including fractions).", + type = type("lyng.Real"), + moduleName = "lyng.time", + getter = { + val instant = thisAs().instant + ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9) + } + ) + addPropertyDoc( + name = "isDistantFuture", + doc = "Whether this instant represents the distant future.", + type = type("lyng.Bool"), + moduleName = "lyng.time", + getter = { thisAs().instant.isDistantFuture.toObj() } + ) + addPropertyDoc( + name = "isDistantPast", + doc = "Whether this instant represents the distant past.", + type = type("lyng.Bool"), + moduleName = "lyng.time", + getter = { thisAs().instant.isDistantPast.toObj() } + ) + addPropertyDoc( + name = "epochWholeSeconds", + doc = "Return the number of full seconds since the Unix epoch.", + type = type("lyng.Int"), + moduleName = "lyng.time", + getter = { ObjInt(thisAs().instant.epochSeconds) } + ) + addPropertyDoc( + name = "nanosecondsOfSecond", + doc = "The number of nanoseconds within the current second.", + type = type("lyng.Int"), + moduleName = "lyng.time", + getter = { ObjInt(thisAs().instant.nanosecondsOfSecond.toLong()) } + ) + addFnDoc( + name = "truncateToSecond", + doc = "Truncate this instant to the nearest second.", + returns = type("lyng.Instant"), + moduleName = "lyng.time" + ) { val t = thisAs().instant ObjInstant(Instant.fromEpochSeconds(t.epochSeconds), LynonSettings.InstantTruncateMode.Second) } - addFn("truncateToMillisecond") { + addFnDoc( + name = "truncateToMillisecond", + doc = "Truncate this instant to the nearest millisecond.", + returns = type("lyng.Instant"), + moduleName = "lyng.time" + ) { val t = thisAs().instant ObjInstant( Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000_000 * 1_000_000), LynonSettings.InstantTruncateMode.Millisecond ) } - addFn("truncateToMicrosecond") { + addFnDoc( + name = "truncateToMicrosecond", + doc = "Truncate this instant to the nearest microsecond.", + returns = type("lyng.Instant"), + moduleName = "lyng.time" + ) { val t = thisAs().instant ObjInstant( Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000 * 1_000), diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt index 279196d..1a273b2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.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. @@ -21,6 +21,7 @@ import net.sergeych.lyng.Arguments import net.sergeych.lyng.Statement import net.sergeych.lyng.miniast.ParamDoc import net.sergeych.lyng.miniast.addFnDoc +import net.sergeych.lyng.miniast.addPropertyDoc import net.sergeych.lyng.miniast.type /** @@ -29,19 +30,20 @@ import net.sergeych.lyng.miniast.type val ObjIterable by lazy { ObjClass("Iterable").apply { - addFnDoc( + addPropertyDoc( name = "toList", doc = "Collect elements of this iterable into a new list.", - returns = type("lyng.List"), - moduleName = "lyng.stdlib" - ) { - val result = mutableListOf() - val iterator = thisObj.invokeInstanceMethod(this, "iterator") - - while (iterator.invokeInstanceMethod(this, "hasNext").toBool()) - result += iterator.invokeInstanceMethod(this, "next") - ObjList(result) - } + type = type("lyng.List"), + moduleName = "lyng.stdlib", + getter = { + val result = mutableListOf() + val it = this.thisObj.invokeInstanceMethod(this, "iterator") + while (it.invokeInstanceMethod(this, "hasNext").toBool()) { + result.add(it.invokeInstanceMethod(this, "next")) + } + ObjList(result) + } + ) // it is not effective, but it is open: addFnDoc( @@ -55,7 +57,7 @@ val ObjIterable by lazy { val obj = args.firstAndOnly() val it = thisObj.invokeInstanceMethod(this, "iterator") while (it.invokeInstanceMethod(this, "hasNext").toBool()) { - if (obj.compareTo(this, it.invokeInstanceMethod(this, "next")) == 0) + if (obj.equals(this, it.invokeInstanceMethod(this, "next"))) return@addFnDoc ObjTrue } ObjFalse @@ -73,46 +75,49 @@ val ObjIterable by lazy { var index = 0 val it = thisObj.invokeInstanceMethod(this, "iterator") while (it.invokeInstanceMethod(this, "hasNext").toBool()) { - if (obj.compareTo(this, it.invokeInstanceMethod(this, "next")) == 0) + if (obj.equals(this, it.invokeInstanceMethod(this, "next"))) return@addFnDoc ObjInt(index.toLong()) index++ } ObjInt(-1L) } - addFnDoc( + addPropertyDoc( name = "toSet", doc = "Collect elements of this iterable into a new set.", - returns = type("lyng.Set"), - moduleName = "lyng.stdlib" - ) { - if( thisObj.isInstanceOf(ObjSet.type) ) - thisObj - else { - val result = mutableSetOf() - val it = thisObj.invokeInstanceMethod(this, "iterator") - while (it.invokeInstanceMethod(this, "hasNext").toBool()) { - result += it.invokeInstanceMethod(this, "next") + type = type("lyng.Set"), + moduleName = "lyng.stdlib", + getter = { + if( this.thisObj.isInstanceOf(ObjSet.type) ) + this.thisObj + else { + val result = mutableSetOf() + val it = this.thisObj.invokeInstanceMethod(this, "iterator") + while (it.invokeInstanceMethod(this, "hasNext").toBool()) { + result.add(it.invokeInstanceMethod(this, "next")) + } + ObjSet(result) } - ObjSet(result) } - } + ) - addFnDoc( + addPropertyDoc( name = "toMap", doc = "Collect pairs into a map using [0] as key and [1] as value for each element.", - returns = type("lyng.Map"), - moduleName = "lyng.stdlib" - ) { - val result = mutableMapOf() - thisObj.toFlow(this).collect { pair -> - when (pair) { - is ObjMapEntry -> result[pair.key] = pair.value - else -> result[pair.getAt(this, 0)] = pair.getAt(this, 1) + type = type("lyng.Map"), + moduleName = "lyng.stdlib", + getter = { + val result = mutableMapOf() + this.thisObj.enumerate(this) { pair -> + when (pair) { + is ObjMapEntry -> result[pair.key] = pair.value + else -> result[pair.getAt(this, 0)] = pair.getAt(this, 1) + } + true } + ObjMap(result) } - ObjMap(result) - } + ) addFnDoc( name = "associateBy", @@ -156,7 +161,7 @@ val ObjIterable by lazy { val fn = requiredArg(0) val result = mutableListOf() thisObj.toFlow(this).collect { - result += fn.call(this, it) + result.add(fn.call(this, it)) } ObjList(result) } @@ -173,7 +178,7 @@ val ObjIterable by lazy { val result = mutableListOf() thisObj.toFlow(this).collect { val transformed = fn.call(this, it) - if( transformed != ObjNull) result += transformed + if( transformed != ObjNull) result.add(transformed) } ObjList(result) } @@ -189,25 +194,26 @@ val ObjIterable by lazy { val result = mutableListOf() if (n > 0) { thisObj.enumerate(this) { - result += it + result.add(it) --n > 0 } } ObjList(result) } - addFnDoc( + addPropertyDoc( name = "isEmpty", doc = "Whether the iterable has no elements.", - returns = type("lyng.Bool"), - moduleName = "lyng.stdlib" - ) { - ObjBool( - thisObj.invokeInstanceMethod(this, "iterator") - .invokeInstanceMethod(this, "hasNext").toBool() - .not() - ) - } + type = type("lyng.Bool"), + moduleName = "lyng.stdlib", + getter = { + ObjBool( + this.thisObj.invokeInstanceMethod(this, "iterator") + .invokeInstanceMethod(this, "hasNext").toBool() + .not() + ) + } + ) addFnDoc( name = "sortedWith", diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt index 92c888d..60d8b4b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.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. @@ -22,16 +22,30 @@ import kotlinx.serialization.json.JsonElement import net.sergeych.lyng.Scope import net.sergeych.lyng.Statement import net.sergeych.lyng.miniast.ParamDoc -import net.sergeych.lyng.miniast.addConstDoc import net.sergeych.lyng.miniast.addFnDoc +import net.sergeych.lyng.miniast.addPropertyDoc import net.sergeych.lyng.miniast.type -import net.sergeych.lyng.statement import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonType class ObjList(val list: MutableList = mutableListOf()) : Obj() { + override suspend fun equals(scope: Scope, other: Obj): Boolean { + if (this === other) return true + if (other !is ObjList) { + if (other.isInstanceOf(ObjIterable)) { + return compareTo(scope, other) == 0 + } + return false + } + if (list.size != other.list.size) return false + for (i in 0.. { @@ -77,21 +91,35 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { } override suspend fun compareTo(scope: Scope, other: Obj): Int { - if (other !is ObjList) return -2 - val mySize = list.size - val otherSize = other.list.size - val commonSize = minOf(mySize, otherSize) - for (i in 0.. -1 - mySize > otherSize -> 1 - else -> 0 + if (other.isInstanceOf(ObjIterable)) { + val it1 = this.list.iterator() + val it2 = other.invokeInstanceMethod(scope, "iterator") + val hasNext2 = it2.getInstanceMethod(scope, "hasNext") + val next2 = it2.getInstanceMethod(scope, "next") + + while (it1.hasNext()) { + if (!hasNext2.invoke(scope, it2).toBool()) return 1 // I'm longer + val v1 = it1.next() + val v2 = next2.invoke(scope, it2) + val d = v1.compareTo(scope, v2) + if (d != 0) return d + } + return if (hasNext2.invoke(scope, it2).toBool()) -1 else 0 } + return -2 } override suspend fun plus(scope: Scope, other: Obj): Obj = @@ -99,27 +127,28 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { other is ObjList -> ObjList((list + other.list).toMutableList()) - other.isInstanceOf(ObjIterable) -> { + other.isInstanceOf(ObjIterable) && other !is ObjString && other !is ObjBuffer -> { val l = other.callMethod(scope, "toList") ObjList((list + l.list).toMutableList()) } - else -> - scope.raiseError("'+': can't concatenate ${this.toString(scope)} with ${other.toString(scope)}") + else -> { + val newList = list.toMutableList() + newList.add(other) + ObjList(newList) + } } override suspend fun plusAssign(scope: Scope, other: Obj): Obj { - // optimization if (other is ObjList) { - list += other.list - return this + list.addAll(other.list) + } else if (other.isInstanceOf(ObjIterable) && other !is ObjString && other !is ObjBuffer) { + val otherList = (other.invokeInstanceMethod(scope, "toList") as ObjList).list + list.addAll(otherList) + } else { + list.add(other) } - if (other.isInstanceOf(ObjIterable)) { - val otherList = other.invokeInstanceMethod(scope, "toList") as ObjList - list += otherList.list - } else - list += other return this } @@ -222,26 +251,25 @@ class ObjList(val list: MutableList = mutableListOf()) : Obj() { return ObjList(decoder.decodeAnyList(scope)) } }.apply { - addConstDoc( + addPropertyDoc( name = "size", - value = statement { - (thisObj as ObjList).list.size.toObj() - }, doc = "Number of elements in this list.", type = type("lyng.Int"), - moduleName = "lyng.stdlib" + moduleName = "lyng.stdlib", + getter = { + val s = (this.thisObj as ObjList).list.size + s.toObj() + } ) - addConstDoc( + addFnDoc( name = "add", - value = statement { - val l = thisAs().list - for (a in args) l.add(a) - ObjVoid - }, doc = "Append one or more elements to the end of this list.", - type = type("lyng.Callable"), moduleName = "lyng.stdlib" - ) + ) { + val l = thisAs().list + for (a in args) l.add(a) + ObjVoid + } addFnDoc( name = "insertAt", doc = "Insert elements starting at the given index.", diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt index 782db82..7095494 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.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. @@ -21,10 +21,7 @@ import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import net.sergeych.lyng.Scope import net.sergeych.lyng.Statement -import net.sergeych.lyng.miniast.ParamDoc -import net.sergeych.lyng.miniast.TypeGenericDoc -import net.sergeych.lyng.miniast.addFnDoc -import net.sergeych.lyng.miniast.type +import net.sergeych.lyng.miniast.* import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonType @@ -76,24 +73,27 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() { ) } }.apply { - addFnDoc( + addPropertyDoc( name = "key", doc = "Key component of this map entry.", - returns = type("lyng.Any"), - moduleName = "lyng.stdlib" - ) { thisAs().key } - addFnDoc( + type = type("lyng.Any"), + moduleName = "lyng.stdlib", + getter = { thisAs().key } + ) + addPropertyDoc( name = "value", doc = "Value component of this map entry.", - returns = type("lyng.Any"), - moduleName = "lyng.stdlib" - ) { thisAs().value } - addFnDoc( + type = type("lyng.Any"), + moduleName = "lyng.stdlib", + getter = { thisAs().value } + ) + addPropertyDoc( name = "size", doc = "Number of components in this entry (always 2).", - returns = type("lyng.Int"), - moduleName = "lyng.stdlib" - ) { 2.toObj() } + type = type("lyng.Int"), + moduleName = "lyng.stdlib", + getter = { 2.toObj() } + ) } } @@ -106,6 +106,18 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() { class ObjMap(val map: MutableMap = mutableMapOf()) : Obj() { + override suspend fun equals(scope: Scope, other: Obj): Boolean { + if (this === other) return true + if (other !is ObjMap) return false + if (map.size != other.map.size) return false + for ((k, v) in map) { + val otherV = other.getAt(scope, k) + if (otherV === ObjNull && !other.contains(scope, k)) return false + if (!v.equals(scope, otherV)) return false + } + return true + } + override val objClass get() = type override suspend fun getAt(scope: Scope, index: Obj): Obj = @@ -120,7 +132,13 @@ class ObjMap(val map: MutableMap = mutableMapOf()) : Obj() { } override suspend fun compareTo(scope: Scope, other: Obj): Int { - if( other is ObjMap && other.map == map) return 0 + if (other is ObjMap) { + if (map == other.map) return 0 + if (map.size != other.map.size) return map.size.compareTo(other.map.size) + // for same size, if they are not equal, we don't have a stable order + // but let's try to be consistent + return map.toString().compareTo(other.map.toString()) + } return -1 } @@ -247,14 +265,13 @@ class ObjMap(val map: MutableMap = mutableMapOf()) : Obj() { lambda.execute(this) } } - addFnDoc( + addPropertyDoc( name = "size", doc = "Number of entries in the map.", - returns = type("lyng.Int"), - moduleName = "lyng.stdlib" - ) { - thisAs().map.size.toObj() - } + type = type("lyng.Int"), + moduleName = "lyng.stdlib", + getter = { (this.thisObj as ObjMap).map.size.toObj() } + ) addFnDoc( name = "remove", doc = "Remove the entry by key and return the previous value or null if absent.", @@ -273,22 +290,20 @@ class ObjMap(val map: MutableMap = mutableMapOf()) : Obj() { thisAs().map.clear() thisObj } - addFnDoc( + addPropertyDoc( name = "keys", doc = "List of keys in this map.", - returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))), - moduleName = "lyng.stdlib" - ) { - thisAs().map.keys.toObj() - } - addFnDoc( + type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))), + moduleName = "lyng.stdlib", + getter = { thisAs().map.keys.toObj() } + ) + addPropertyDoc( name = "values", doc = "List of values in this map.", - returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))), - moduleName = "lyng.stdlib" - ) { - ObjList(thisAs().map.values.toMutableList()) - } + type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))), + moduleName = "lyng.stdlib", + getter = { ObjList(thisAs().map.values.toMutableList()) } + ) addFnDoc( name = "iterator", doc = "Iterator over map entries as MapEntry objects.", @@ -328,7 +343,14 @@ class ObjMap(val map: MutableMap = mutableMapOf()) : Obj() { for (e in other.list) { val entry = when (e) { is ObjMapEntry -> e - else -> scope.raiseIllegalArgument("map can only be merged with MapEntry elements; got $e") + else -> { + if (e.isInstanceOf(ObjArray)) { + if (e.invokeInstanceMethod(scope, "size").toInt() != 2) + scope.raiseIllegalArgument("Array element to merge into map must have 2 elements, got $e") + ObjMapEntry(e.getAt(scope, 0), e.getAt(scope, 1)) + } else + scope.raiseIllegalArgument("map can only be merged with MapEntry elements; got $e") + } } map[entry.key] = entry.value } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt index 94ab8a7..0c631d0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRange.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. @@ -20,6 +20,7 @@ package net.sergeych.lyng.obj import net.sergeych.lyng.Scope import net.sergeych.lyng.miniast.TypeGenericDoc import net.sergeych.lyng.miniast.addFnDoc +import net.sergeych.lyng.miniast.addPropertyDoc import net.sergeych.lyng.miniast.type class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() { @@ -174,54 +175,48 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob companion object { val type = ObjClass("Range", ObjIterable).apply { - addFnDoc( + addPropertyDoc( name = "start", doc = "Start bound of the range or null if open.", - returns = type("lyng.Any", nullable = true), - moduleName = "lyng.stdlib" - ) { - thisAs().start ?: ObjNull - } - addFnDoc( + type = type("lyng.Any", nullable = true), + moduleName = "lyng.stdlib", + getter = { thisAs().start ?: ObjNull } + ) + addPropertyDoc( name = "end", doc = "End bound of the range or null if open.", - returns = type("lyng.Any", nullable = true), - moduleName = "lyng.stdlib" - ) { - thisAs().end ?: ObjNull - } - addFnDoc( + type = type("lyng.Any", nullable = true), + moduleName = "lyng.stdlib", + getter = { thisAs().end ?: ObjNull } + ) + addPropertyDoc( name = "isOpen", doc = "Whether the range is open on either side (no start or no end).", - returns = type("lyng.Bool"), - moduleName = "lyng.stdlib" - ) { - thisAs().let { it.start == null || it.end == null }.toObj() - } - addFnDoc( + type = type("lyng.Bool"), + moduleName = "lyng.stdlib", + getter = { thisAs().let { it.start == null || it.end == null }.toObj() } + ) + addPropertyDoc( name = "isIntRange", doc = "True if both bounds are Int values.", - returns = type("lyng.Bool"), - moduleName = "lyng.stdlib" - ) { - thisAs().isIntRange.toObj() - } - addFnDoc( + type = type("lyng.Bool"), + moduleName = "lyng.stdlib", + getter = { thisAs().isIntRange.toObj() } + ) + addPropertyDoc( name = "isCharRange", doc = "True if both bounds are Char values.", - returns = type("lyng.Bool"), - moduleName = "lyng.stdlib" - ) { - thisAs().isCharRange.toObj() - } - addFnDoc( + type = type("lyng.Bool"), + moduleName = "lyng.stdlib", + getter = { thisAs().isCharRange.toObj() } + ) + addPropertyDoc( name = "isEndInclusive", doc = "Whether the end bound is inclusive.", - returns = type("lyng.Bool"), - moduleName = "lyng.stdlib" - ) { - thisAs().isEndInclusive.toObj() - } + type = type("lyng.Bool"), + moduleName = "lyng.stdlib", + getter = { thisAs().isEndInclusive.toObj() } + ) addFnDoc( name = "iterator", doc = "Iterator over elements in this range (optimized for Int ranges).", diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt index 0b4a5a4..f86b62b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt @@ -36,6 +36,8 @@ data class ObjRecord( val isClosed: Boolean = false, val isOverride: Boolean = false, var delegate: Obj? = null, + /** The receiver object to resolve this member against (for instance fields/methods). */ + var receiver: Obj? = null, ) { val effectiveWriteVisibility: Visibility get() = writeVisibility ?: visibility enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index daec539..54c5a25 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -34,7 +34,15 @@ sealed interface ObjRef { */ suspend fun evalValue(scope: Scope): Obj { val rec = get(scope) - if (rec.type == ObjRecord.Type.Delegated) return scope.resolve(rec, "unknown") + if (rec.type == ObjRecord.Type.Delegated) { + val receiver = rec.receiver ?: scope.thisObj + // Use resolve to handle delegated property logic + return scope.resolve(rec, "unknown") + } + // Template record: must map to instance storage + if (rec.receiver != null && rec.declaringClass != null) { + return rec.receiver!!.resolveRecord(scope, rec, "unknown", rec.declaringClass).value + } return rec.value } suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { @@ -383,7 +391,7 @@ class IncDecRef( override suspend fun get(scope: Scope): ObjRecord { val rec = target.get(scope) if (!rec.isMutable) scope.raiseError("Cannot ${if (isIncrement) "increment" else "decrement"} immutable value") - val v = scope.resolve(rec, "unknown") + val v = target.evalValue(scope) val one = ObjInt.One // We now treat numbers as immutable and always perform write-back via setAt. // This avoids issues where literals are shared and mutated in-place. @@ -947,65 +955,63 @@ class IndexRef( } override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { - val fastRval = PerfFlags.RVAL_FASTPATH - val base = if (fastRval) target.evalValue(scope) else target.get(scope).value + val base = target.evalValue(scope) if (base == ObjNull && isOptional) { // no-op on null receiver for optional chaining assignment return } - val idx = if (fastRval) index.evalValue(scope) else index.get(scope).value - if (fastRval) { - // Mirror read fast-path with direct write for ObjList + ObjInt index - if (base is ObjList && idx is ObjInt) { - val i = idx.toInt() - base.list[i] = newValue - return + val idx = index.evalValue(scope) + + // Mirror read fast-path with direct write for ObjList + ObjInt index + if (base is ObjList && idx is ObjInt) { + val i = idx.toInt() + base.list[i] = newValue + return + } + // Direct write fast path for ObjMap + ObjString + if (base is ObjMap && idx is ObjString) { + base.map[idx] = newValue + return + } + if (PerfFlags.RVAL_FASTPATH && PerfFlags.INDEX_PIC) { + // Polymorphic inline cache for index write + val (key, ver) = when (base) { + is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion + is ObjClass -> base.classId to base.layoutVersion + else -> 0L to -1 } - // Direct write fast path for ObjMap + ObjString - if (base is ObjMap && idx is ObjString) { - base.map[idx] = newValue - return - } - if (PerfFlags.INDEX_PIC) { - // Polymorphic inline cache for index write - val (key, ver) = when (base) { - is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion - is ObjClass -> base.classId to base.layoutVersion - else -> 0L to -1 - } - if (key != 0L) { - wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { s(base, scope, idx, newValue); return } } - wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) { - val tk = wKey2; val tv = wVer2; val ts = wSetter2 - wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 - wKey1 = tk; wVer1 = tv; wSetter1 = ts - s(base, scope, idx, newValue); return - } } - if (PerfFlags.INDEX_PIC_SIZE_4) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) { - val tk = wKey3; val tv = wVer3; val ts = wSetter3 - wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 - wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 - wKey1 = tk; wVer1 = tv; wSetter1 = ts - s(base, scope, idx, newValue); return - } } - if (PerfFlags.INDEX_PIC_SIZE_4) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) { - val tk = wKey4; val tv = wVer4; val ts = wSetter4 - wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 - wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 - wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 - wKey1 = tk; wVer1 = tv; wSetter1 = ts - s(base, scope, idx, newValue); return - } } - // Miss: perform write and install generic handler - base.putAt(scope, idx, newValue) - if (PerfFlags.INDEX_PIC_SIZE_4) { - wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 - wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 - } + if (key != 0L) { + wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { s(base, scope, idx, newValue); return } } + wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) { + val tk = wKey2; val tv = wVer2; val ts = wSetter2 wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 - wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, ix, v -> obj.putAt(sc, ix, v) } - return + wKey1 = tk; wVer1 = tv; wSetter1 = ts + s(base, scope, idx, newValue); return + } } + if (PerfFlags.INDEX_PIC_SIZE_4) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) { + val tk = wKey3; val tv = wVer3; val ts = wSetter3 + wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 + wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 + wKey1 = tk; wVer1 = tv; wSetter1 = ts + s(base, scope, idx, newValue); return + } } + if (PerfFlags.INDEX_PIC_SIZE_4) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) { + val tk = wKey4; val tv = wVer4; val ts = wSetter4 + wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 + wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 + wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 + wKey1 = tk; wVer1 = tv; wSetter1 = ts + s(base, scope, idx, newValue); return + } } + // Miss: perform write and install generic handler + base.putAt(scope, idx, newValue) + if (PerfFlags.INDEX_PIC_SIZE_4) { + wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 + wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 } + wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 + wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, ix, v -> obj.putAt(sc, ix, v) } + return } } base.putAt(scope, idx, newValue) @@ -1266,20 +1272,6 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef { if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++ // 2) Fallback to current-scope object or field on `this` scope[name]?.let { return it } - // 2a) Try nearest ClosureScope's closure ancestry explicitly - run { - var s: Scope? = scope - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) break - if (s is ClosureScope) { - s.closureScope.chainLookupWithMembers(name)?.let { return it } - } - s = s.parent - } - } - // 2b) Try raw ancestry local/binding lookup (cycle-safe), including slots in parents - scope.chainLookupIgnoreClosure(name)?.let { return it } try { return scope.thisObj.readField(scope, name) } catch (e: ExecutionError) { @@ -1305,18 +1297,6 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef { if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++ // 2) Fallback name in scope or field on `this` scope[name]?.let { return it } - run { - var s: Scope? = scope - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) break - if (s is ClosureScope) { - s.closureScope.chainLookupWithMembers(name)?.let { return it } - } - s = s.parent - } - } - scope.chainLookupIgnoreClosure(name)?.let { return it } try { return scope.thisObj.readField(scope, name) } catch (e: ExecutionError) { @@ -1327,56 +1307,11 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef { override suspend fun evalValue(scope: Scope): Obj { scope.pos = atPos - if (!PerfFlags.LOCAL_SLOT_PIC) { - scope.getSlotIndexOf(name)?.let { return scope.resolve(scope.getSlotRecord(it), name) } - // fallback to current-scope object or field on `this` - scope[name]?.let { return scope.resolve(it, name) } - run { - var s: Scope? = scope - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) break - if (s is ClosureScope) { - s.closureScope.chainLookupWithMembers(name)?.let { return s.resolve(it, name) } - } - s = s.parent - } - } - scope.chainLookupIgnoreClosure(name)?.let { return scope.resolve(it, name) } - return try { - scope.thisObj.readField(scope, name).value - } catch (e: ExecutionError) { - if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name) - throw e - } - } - val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) - val slot = if (hit) cachedSlot else resolveSlot(scope) - if (slot >= 0) { - val rec = scope.getSlotRecord(slot) - if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) { - return scope.resolve(rec, name) - } - } - // Fallback name in scope or field on `this` - scope[name]?.let { - return scope.resolve(it, name) - } - run { - var s: Scope? = scope - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) break - if (s is ClosureScope) { - s.closureScope.chainLookupWithMembers(name)?.let { return s.resolve(it, name) } - } - s = s.parent - } - } - scope.chainLookupIgnoreClosure(name)?.let { return scope.resolve(it, name) } + scope.getSlotIndexOf(name)?.let { return scope.resolve(scope.getSlotRecord(it), name) } + // fallback to current-scope object or field on `this` + scope[name]?.let { return scope.resolve(it, name) } return try { - val res = scope.thisObj.readField(scope, name).value - res + scope.thisObj.readField(scope, name).value } catch (e: ExecutionError) { if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name) throw e @@ -1395,24 +1330,6 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef { scope.assign(stored, name, newValue) return } - run { - var s: Scope? = scope - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) break - if (s is ClosureScope) { - s.closureScope.chainLookupWithMembers(name)?.let { stored -> - s.assign(stored, name, newValue) - return - } - } - s = s.parent - } - } - scope.chainLookupIgnoreClosure(name)?.let { stored -> - scope.assign(stored, name, newValue) - return - } // Fallback: write to field on `this` scope.thisObj.writeField(scope, name, newValue) return @@ -1429,24 +1346,6 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef { scope.assign(stored, name, newValue) return } - run { - var s: Scope? = scope - val visited = HashSet(4) - while (s != null) { - if (!visited.add(s.frameId)) break - if (s is ClosureScope) { - s.closureScope.chainLookupWithMembers(name)?.let { stored -> - s.assign(stored, name, newValue) - return - } - } - s = s.parent - } - } - scope.chainLookupIgnoreClosure(name)?.let { stored -> - scope.assign(stored, name, newValue) - return - } scope.thisObj.writeField(scope, name, newValue) return } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRegex.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRegex.kt index c56ac05..c51f129 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRegex.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRegex.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. @@ -20,10 +20,7 @@ package net.sergeych.lyng.obj import net.sergeych.lyng.PerfFlags import net.sergeych.lyng.RegexCache import net.sergeych.lyng.Scope -import net.sergeych.lyng.miniast.ParamDoc -import net.sergeych.lyng.miniast.TypeGenericDoc -import net.sergeych.lyng.miniast.addFnDoc -import net.sergeych.lyng.miniast.type +import net.sergeych.lyng.miniast.* class ObjRegex(val regex: Regex) : Obj() { override val objClass get() = type @@ -123,30 +120,27 @@ class ObjRegexMatch(val match: MatchResult) : Obj() { scope.raiseError("RegexMatch can't be constructed directly") } }.apply { - addFnDoc( + addPropertyDoc( name = "groups", doc = "List of captured groups with index 0 as the whole match.", - returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))), - moduleName = "lyng.stdlib" - ) { - thisAs().objGroups - } - addFnDoc( + type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))), + moduleName = "lyng.stdlib", + getter = { thisAs().objGroups } + ) + addPropertyDoc( name = "value", doc = "The matched substring.", - returns = type("lyng.String"), - moduleName = "lyng.stdlib" - ) { - thisAs().objValue - } - addFnDoc( + type = type("lyng.String"), + moduleName = "lyng.stdlib", + getter = { thisAs().objValue } + ) + addPropertyDoc( name = "range", doc = "Range of the match in the input (end-exclusive).", - returns = type("lyng.Range"), - moduleName = "lyng.stdlib" - ) { - thisAs().objRange - } + type = type("lyng.Range"), + moduleName = "lyng.stdlib", + getter = { thisAs().objRange } + ) } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRingBuffer.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRingBuffer.kt index 09a2440..7099723 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRingBuffer.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRingBuffer.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,10 +18,7 @@ package net.sergeych.lyng.obj import net.sergeych.lyng.Scope -import net.sergeych.lyng.miniast.ParamDoc -import net.sergeych.lyng.miniast.TypeGenericDoc -import net.sergeych.lyng.miniast.addFnDoc -import net.sergeych.lyng.miniast.type +import net.sergeych.lyng.miniast.* class RingBuffer(val maxSize: Int) : Iterable { private val data = arrayOfNulls(maxSize) @@ -94,18 +91,20 @@ class ObjRingBuffer(val capacity: Int) : Obj() { return ObjRingBuffer(scope.requireOnlyArg().toInt()) } }.apply { - addFnDoc( + addPropertyDoc( name = "capacity", doc = "Maximum number of elements the buffer can hold.", - returns = type("lyng.Int"), - moduleName = "lyng.stdlib" - ) { thisAs().capacity.toObj() } - addFnDoc( + type = type("lyng.Int"), + moduleName = "lyng.stdlib", + getter = { thisAs().capacity.toObj() } + ) + addPropertyDoc( name = "size", doc = "Current number of elements in the buffer.", - returns = type("lyng.Int"), - moduleName = "lyng.stdlib" - ) { thisAs().buffer.size.toObj() } + type = type("lyng.Int"), + moduleName = "lyng.stdlib", + getter = { thisAs().buffer.size.toObj() } + ) addFnDoc( name = "iterator", doc = "Iterator over elements in insertion order (oldest to newest).", @@ -122,12 +121,16 @@ class ObjRingBuffer(val capacity: Int) : Obj() { returns = type("lyng.Void"), moduleName = "lyng.stdlib" ) { thisAs().apply { buffer.add(requireOnlyArg()) } } - addFnDoc( + addPropertyDoc( name = "first", doc = "Return the oldest element in the buffer.", - returns = type("lyng.Any"), - moduleName = "lyng.stdlib" - ) { thisAs().buffer.first() } + type = type("lyng.Any"), + moduleName = "lyng.stdlib", + getter = { + val buffer = (this.thisObj as ObjRingBuffer).buffer + if (buffer.size == 0) ObjNull else buffer.first() + } + ) } } } \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjSet.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjSet.kt index d9c72b3..11b120c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjSet.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjSet.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. @@ -28,6 +28,18 @@ import net.sergeych.lynon.LynonType class ObjSet(val set: MutableSet = mutableSetOf()) : Obj() { + override suspend fun equals(scope: Scope, other: Obj): Boolean { + if (this === other) return true + if (other !is ObjSet) return false + if (set.size != other.set.size) return false + // Sets are equal if all my elements are in other and vice versa + // contains() in ObjSet uses equals(scope, ...), so we need to be careful + for (e in set) { + if (!other.contains(scope, e)) return false + } + return true + } + override val objClass get() = type override suspend fun contains(scope: Scope, other: Obj): Boolean { @@ -113,11 +125,12 @@ class ObjSet(val set: MutableSet = mutableSetOf()) : Obj() { } override suspend fun compareTo(scope: Scope, other: Obj): Int { - return if (other !is ObjSet) -1 - else { - if (set == other.set) 0 - else -1 + if (other is ObjSet) { + if (set == other.set) return 0 + if (set.size != other.set.size) return set.size.compareTo(other.set.size) + return set.toString().compareTo(other.set.toString()) } + return -2 } override fun hashCode(): Int { @@ -126,10 +139,7 @@ class ObjSet(val set: MutableSet = mutableSetOf()) : Obj() { override fun equals(other: Any?): Boolean { if (this === other) return true - if (other == null || this::class != other::class) return false - - other as ObjSet - + if (other !is ObjSet) return false return set == other.set } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt index 8c9eb91..98ed16b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjString.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. @@ -25,7 +25,6 @@ import net.sergeych.lyng.PerfFlags import net.sergeych.lyng.RegexCache import net.sergeych.lyng.Scope import net.sergeych.lyng.miniast.* -import net.sergeych.lyng.statement import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonType @@ -124,10 +123,16 @@ data class ObjString(val value: String) : Obj() { } companion object { - val type = object : ObjClass("String") { + val type = object : ObjClass("String", ObjCollection) { override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = ObjString(decoder.unpackBinaryData().decodeToString()) }.apply { + addFnDoc( + name = "iterator", + doc = "Iterator over characters of this string.", + returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Char"))), + moduleName = "lyng.stdlib" + ) { ObjKotlinIterator(thisAs().value.iterator()) } addFnDoc( name = "toInt", doc = "Parse this string as an integer or throw if it is not a valid integer.", @@ -157,12 +162,12 @@ data class ObjString(val value: String) : Obj() { ) { ObjBool(thisAs().value.endsWith(requiredArg(0).value)) } - addConstDoc( + addPropertyDoc( name = "length", - value = statement { ObjInt.of(thisAs().value.length.toLong()) }, doc = "Number of UTF-16 code units in this string.", type = type("lyng.Int"), - moduleName = "lyng.stdlib" + moduleName = "lyng.stdlib", + getter = { ObjInt.of((this.thisObj as ObjString).value.length.toLong()) } ) addFnDoc( name = "takeLast", @@ -240,16 +245,17 @@ data class ObjString(val value: String) : Obj() { ) { thisAs().value.uppercase().let(::ObjString) } - addFnDoc( + addPropertyDoc( name = "characters", doc = "List of characters of this string.", - returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Char"))), - moduleName = "lyng.stdlib" - ) { - ObjList( - thisAs().value.map { ObjChar(it) }.toMutableList() - ) - } + type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Char"))), + moduleName = "lyng.stdlib", + getter = { + ObjList( + (this.thisObj as ObjString).value.map { ObjChar(it) }.toMutableList() + ) + } + ) addFnDoc( name = "last", doc = "The last character of this string or throw if the string is empty.", @@ -264,12 +270,13 @@ data class ObjString(val value: String) : Obj() { returns = type("lyng.Buffer"), moduleName = "lyng.stdlib" ) { ObjBuffer(thisAs().value.encodeToByteArray().asUByteArray()) } - addFnDoc( + addPropertyDoc( name = "size", doc = "Alias for length: the number of characters (code units) in this string.", - returns = type("lyng.Int"), - moduleName = "lyng.stdlib" - ) { ObjInt.of(thisAs().value.length.toLong()) } + type = type("lyng.Int"), + moduleName = "lyng.stdlib", + getter = { ObjInt.of((this.thisObj as ObjString).value.length.toLong()) } + ) addFnDoc( name = "toReal", doc = "Parse this string as a real number (floating point).", diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index bc4c617..81ff026 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -1994,12 +1994,12 @@ class ScriptTest { """ class Foo { // this means last is lambda: - fun f(e=1, f) { - "e="+e+"f="+f() + fun test_f(e=1, f_param) { + "e="+e+"f="+f_param() } } - val f = Foo() - assertEquals("e=1f=xx", f.f { "xx" }) + val f_obj = Foo() + assertEquals("e=1f=xx", f_obj.test_f { "xx" }) """.trimIndent() ) @@ -2011,13 +2011,13 @@ class ScriptTest { """ class Foo { // this means last is lambda: - fun f(e..., f) { - "e="+e+"f="+f() + fun test_f_ellipsis(e..., f_param) { + "e="+e+"f="+f_param() } } - val f = Foo() - assertEquals("e=[]f=xx", f.f { "xx" }) - assertEquals("e=[1,2]f=xx", f.f(1,2) { "xx" }) + val f_obj = Foo() + assertEquals("e=[]f=xx", f_obj.test_f_ellipsis { "xx" }) + assertEquals("e=[1,2]f=xx", f_obj.test_f_ellipsis(1,2) { "xx" }) """.trimIndent() ) @@ -4645,7 +4645,8 @@ class ScriptTest { null } catch { it } assert(caught != null) - assert(caught.message.contains("Expected DerivedEx, got MyEx")) + assertEquals("Expected DerivedEx, got MyEx", caught.message) + assert(caught.message == "Expected DerivedEx, got MyEx") """.trimIndent()) } @@ -4703,7 +4704,7 @@ class ScriptTest { // 61755f07-630c-4181-8d50-1b044d96e1f4 class T { static var f1 = null - static fun t(name=null) { + static fun testCapture(name=null) { run { // I expect it will catch the 'name' from // param? @@ -4714,11 +4715,86 @@ class ScriptTest { assert(T.f1 == null) println("-- "+T.f1::class) println("-- "+T.f1) - T.t("foo") + T.testCapture("foo") println("2- "+T.f1::class) println("2- "+T.f1) assert(T.f1 == "foo") """.trimIndent()) } + + @Test + fun testLazyLocals() = runTest() { + eval(""" + class T { + val x by lazy { + val c = "c" + c + "!" + } + } + val t = T() + assertEquals("c!", t.x) + assertEquals("c!", t.x) + """.trimIndent()) + } + @Test + fun testGetterLocals() = runTest() { + eval(""" + class T { + val x get() { + val c = "c" + c + "!" + } + } + val t = T() + assertEquals("c!", t.x) + assertEquals("c!", t.x) + """.trimIndent()) + } + + @Test + fun testMethodLocals() = runTest() { + eval(""" + class T { + fun x() { + val c = "c" + c + "!" + } + } + val t = T() + assertEquals("c!", t.x()) + assertEquals("c!", t.x()) + """.trimIndent()) + } + + @Test + fun testContrcuctorMagicIdBug() = runTest() { + eval(""" + interface SomeI { + abstract fun x() + } + class T(id): SomeI { + override fun x() { + val c = id + c + "!" + } + } + val t = T("c") + assertEquals("c!", t.x()) + assertEquals("c!", t.x()) + """.trimIndent()) + } + + @Test + fun testLambdaLocals() = runTest() { + eval(""" + class T { + val l = { x -> + val c = x + ":" + c + x + } + } + assertEquals("r:r", T().l("r")) + """.trimIndent()) + } } diff --git a/lynglib/src/jvmTest/kotlin/BookTest.kt b/lynglib/src/jvmTest/kotlin/BookTest.kt index 4e61557..076636f 100644 --- a/lynglib/src/jvmTest/kotlin/BookTest.kt +++ b/lynglib/src/jvmTest/kotlin/BookTest.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. diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index 3e0c672..97ea679 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -211,7 +211,7 @@ fun Iterable.sortedBy(predicate) { /* Return a shuffled copy of the iterable as a list. */ fun Iterable.shuffled() { - toList().apply { shuffle() } + toList.apply { shuffle() } } /* @@ -267,7 +267,7 @@ class StackTraceEntry( fun Exception.printStackTrace() { println(this) var lastEntry = null - for( entry in stackTrace() ) { + for( entry in stackTrace ) { if( lastEntry == null || lastEntry !is StackTraceEntry || lastEntry.line != entry.line ) println("\tat "+entry.toString()) lastEntry = entry