Improve decodeClassObj class resolution in LynonDecoder, add fallback lookup mechanisms, and refine related tests

This commit is contained in:
Sergey Chernov 2025-12-13 17:12:44 +01:00
parent fba44622e5
commit dd1a1544c6
3 changed files with 152 additions and 11 deletions

View File

@ -1,3 +1,20 @@
<!--
~ Copyright 2025 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.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
~
-->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="lyng:site [jsBrowserDevelopmentRun]" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
@ -17,8 +34,11 @@
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<GradleProfilingDisabled>true</GradleProfilingDisabled>
<GradleCoverageDisabled>true</GradleCoverageDisabled>
<method v="2" />
</configuration>
</component>

View File

@ -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<Long>(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<Obj> {

View File

@ -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))