From dd1a1544c6d49641783d221b15e23c7150010161 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sat, 13 Dec 2025 17:12:44 +0100 Subject: [PATCH] Improve `decodeClassObj` class resolution in `LynonDecoder`, add fallback lookup mechanisms, and refine related tests --- ...yng_site [jsBrowserDevelopmentRun].run.xml | 20 ++++++ .../kotlin/net/sergeych/lynon/LynonDecoder.kt | 71 ++++++++++++++++-- lynglib/src/jvmTest/kotlin/LynonTests.kt | 72 +++++++++++++++++-- 3 files changed, 152 insertions(+), 11 deletions(-) diff --git a/.run/lyng_site [jsBrowserDevelopmentRun].run.xml b/.run/lyng_site [jsBrowserDevelopmentRun].run.xml index 548e319..5518148 100644 --- a/.run/lyng_site [jsBrowserDevelopmentRun].run.xml +++ b/.run/lyng_site [jsBrowserDevelopmentRun].run.xml @@ -1,3 +1,20 @@ + + @@ -17,8 +34,11 @@ true true + false false false + true + true \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt index 6e230d1..40d3268 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lynon/LynonDecoder.kt @@ -78,15 +78,72 @@ open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSe private suspend fun decodeClassObj(scope: Scope): ObjClass { val className = decodeObject(scope, ObjString.type, null) as ObjString - return scope.get(className.value)?.value?.let { + val name = className.value + // 1) Try direct lookup in this scope (locals/parents/this members) + scope.get(name)?.value?.let { if (it !is ObjClass) scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}") - it - } ?: scope.also { - println("Class not found: $className") - println("::: ${runCatching { scope.eval("Vault")}.getOrNull() }") - println("::2 [${className}]: ${scope.get(className.value)}") - }.raiseSymbolNotFound("can't deserialize: not found type $className") + return it + } + // 2) Try ancestry lookup including instance/class members, but without invoking overridden get + scope.chainLookupWithMembers(name)?.value?.let { + if (it !is ObjClass) + scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}") + return it + } + // 3) Try to find nearest ModuleScope and check its locals and its parent (root) locals + run { + var s: Scope? = scope + val visited = HashSet(4) + while (s != null) { + if (!visited.add(s.frameId)) break + if (s is net.sergeych.lyng.ModuleScope) { + s.objects[name]?.value?.let { + if (it !is ObjClass) + scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}") + return it + } + s.localBindings[name]?.value?.let { + if (it !is ObjClass) + scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}") + return it + } + s.parent?.let { p -> + p.objects[name]?.value?.let { + if (it !is ObjClass) + scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}") + return it + } + p.localBindings[name]?.value?.let { + if (it !is ObjClass) + scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}") + return it + } + p.thisObj.objClass.getInstanceMemberOrNull(name)?.value?.let { + if (it !is ObjClass) + scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}") + return it + } + } + break + } + s = s.parent + } + } + // 4) Try ImportProvider root scope globals (e.g., stdlib) + runCatching { scope.currentImportProvider.rootScope.objects[name]?.value }.getOrNull()?.let { + if (it !is ObjClass) + scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}") + return it + } +// // 5) As a final fallback, try to evaluate the name in this scope using the compiler +// runCatching { scope.eval(name) }.getOrNull()?.let { +// if (it !is ObjClass) +// scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}") +// return it +// } + // If everything failed, raise an informative error + scope.raiseSymbolNotFound("can't deserialize: not found type ${className}") } suspend fun decodeAnyList(scope: Scope, fixedSize: Int? = null): MutableList { diff --git a/lynglib/src/jvmTest/kotlin/LynonTests.kt b/lynglib/src/jvmTest/kotlin/LynonTests.kt index f597c5d..cf8eb0e 100644 --- a/lynglib/src/jvmTest/kotlin/LynonTests.kt +++ b/lynglib/src/jvmTest/kotlin/LynonTests.kt @@ -25,10 +25,7 @@ import net.sergeych.lyng.obj.* import net.sergeych.lynon.* import java.nio.file.Files import java.nio.file.Path -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals -import kotlin.test.assertTrue +import kotlin.test.* class LynonTests { @@ -44,6 +41,73 @@ class LynonTests { assertEquals(3, sizeInTetrades(257u)) } + @Ignore("This is not yet implemented") + @Test + fun decodeClassObj_shouldResolveDottedQualifiedNames() = runTest { + // Define nested namespaces and a class with a qualified name via eval + val module = net.sergeych.lyng.Script.defaultImportManager.newModule() + module.eval( + """ + package ns.sub + class Vault() { fun toString() { "ns.sub.Vault" } } + """.trimIndent() + ) + + val child = module.createChildScope(module.pos) + + // Sanity: eval resolves both qualified and unqualified in the same module context + val qualified = child.eval("ns.sub.Vault") + assertTrue(qualified is ObjClass) + + val inst = child.eval("ns.sub.Vault()") + assertTrue(inst is ObjInstance) + + // Encode and decode instance; decoder should resolve class by its encoded name + val bout = MemoryBitOutput() + val enc = LynonEncoder(bout) + enc.encodeAny(child, inst) + val bin = MemoryBitInput(bout.toBitArray()) + val dec = LynonDecoder(bin) + + val decoded = dec.decodeAny(child) + assertTrue(decoded is ObjInstance) + val decObj = decoded as ObjInstance + assertEquals("Vault", decObj.objClass.className) + } + + @Test + fun decodeClassObj_shouldResolveWhenEvalFindsButGetMisses() = runTest { + // Build a module scope and define a class there via eval (simulating imported/user code) + val module = net.sergeych.lyng.Script.defaultImportManager.newModule() + module.eval("class Vault() { fun toString() { \"Vault\" } }") + + // Build a child scope where local bindings do not include the class name explicitly + val child = module.createChildScope(module.pos) + + // Sanity: eval in the child must resolve the class by name + val evalResolved = child.eval("Vault") + assertTrue(evalResolved is ObjClass) + + // Create an instance so that encoder will write type Other + class name and constructor args + val inst = child.eval("Vault()") + assertTrue(inst is ObjInstance) + + // Encode the instance and then decode it with our decoder that contains robust class lookup + val bout = MemoryBitOutput() + val enc = LynonEncoder(bout) + enc.encodeAny(child, inst) + val bin = MemoryBitInput(bout.toBitArray()) + val dec = LynonDecoder(bin) + + val decoded = dec.decodeAny(child) + assertTrue(decoded is ObjInstance) + // Class should be resolvable and preserved + val instObj = inst as ObjInstance + val decObj = decoded as ObjInstance + assertEquals(instObj.objClass.className, decObj.objClass.className) + assertEquals("Vault", decObj.objClass.className) + } + @Test fun testSizeInBits() { assertEquals(1, sizeInBits(0u))