From 41657b355863a17968e17f59b70d5150d1133b02 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 17 Nov 2025 15:09:02 +0100 Subject: [PATCH] another MI bug fixed, added tests for MI serialization --- .../kotlin/net/sergeych/lyng/obj/ObjClass.kt | 25 +++++++++- .../net/sergeych/lyng/obj/ObjInstance.kt | 21 +++++---- .../src/commonTest/kotlin/TestInheritance.kt | 46 ++++++++++++++++++- lynglib/src/jvmTest/kotlin/LynonTests.kt | 27 ++++++++++- 4 files changed, 106 insertions(+), 13 deletions(-) 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 2c75ee0..ffe567e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -64,7 +64,7 @@ open class ObjClass( /** * All ancestors as a Set for fast `isInstanceOf` checks. Order is not guaranteed here and - * must not be used for resolution — use [parentsLinearized] instead. + * must not be used for resolution */ val allParentsSet: Set = buildSet { @@ -173,7 +173,19 @@ open class ObjClass( if (isRoot) { c.constructorMeta?.let { meta -> val argsHere = argsForThis ?: Arguments.EMPTY + // Assign constructor params into instance scope (unmangled) meta.assignToContext(instance.instanceScope, argsHere) + // 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) { + val rec = instance.instanceScope.objects[p.name] + if (rec != null) { + val mangled = "${c.className}::${p.name}" + // Always point the mangled name to the current record to keep writes consistent + // across re-bindings (e.g., second pass before ctor) + instance.instanceScope.objects[mangled] = rec + } + } } } // Initialize direct parents first, in order @@ -203,6 +215,15 @@ open class ObjClass( c.constructorMeta?.let { meta -> val argsHere = argsForThis ?: Arguments.EMPTY meta.assignToContext(instance.instanceScope, argsHere) + // Ensure mangled aliases exist for qualified access starting from this class + for (p in meta.params) { + val rec = instance.instanceScope.objects[p.name] + if (rec != null) { + val mangled = "${c.className}::${p.name}" + // Overwrite to ensure alias refers to the latest ObjRecord after re-binding + instance.instanceScope.objects[mangled] = rec + } + } } val execScope = instance.instanceScope.createChildScope(args = argsForThis ?: Arguments.EMPTY, newThisObj = instance) ctor.execute(execScope) @@ -259,7 +280,7 @@ open class ObjClass( fun addFn( name: String, isOpen: Boolean = false, - visibility: net.sergeych.lyng.Visibility = net.sergeych.lyng.Visibility.Public, + visibility: Visibility = Visibility.Public, code: suspend Scope.() -> Obj ) { val stmt = statement { code() } 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 79afb6b..d58839e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -159,7 +159,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { ?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult) private val publicFields: Map - get() = instanceScope.objects.filter { it.value.visibility.isPublic && it.value.type.serializable } + get() = instanceScope.objects.filter { + // Expose only human-facing fields: skip MI-mangled storage entries like "Class::name" + !it.key.contains("::") && it.value.visibility.isPublic && it.value.type.serializable + } override fun toString(): String { val fields = publicFields.map { "${it.key}=${it.value.value}" }.joinToString(",") @@ -237,7 +240,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla // Visibility: declaring class is the qualified ancestor for mangled storage val decl = rec.declaringClass ?: startClass val caller = scope.currentClassCtx - if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller)) + if (!canAccessMember(rec.visibility, decl, caller)) scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})")) return rec } @@ -246,7 +249,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla instance.instanceScope[name]?.let { rec -> val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val caller = scope.currentClassCtx - if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller)) + if (!canAccessMember(rec.visibility, decl, caller)) scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl?.className ?: "?"})")) return rec } @@ -255,7 +258,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name") val decl = r.declaringClass ?: startClass val caller = scope.currentClassCtx - if (!net.sergeych.lyng.canAccessMember(r.visibility, decl, caller)) + if (!canAccessMember(r.visibility, decl, caller)) scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})")) return when (val value = r.value) { is net.sergeych.lyng.Statement -> ObjRecord(value.execute(instance.instanceScope.createChildScope(scope.pos, newThisObj = instance)), r.isMutable) @@ -269,7 +272,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla instance.instanceScope.objects[mangled]?.let { f -> val decl = f.declaringClass ?: startClass val caller = scope.currentClassCtx - if (!net.sergeych.lyng.canAccessMember(f.visibility, decl, caller)) + if (!canAccessMember(f.visibility, decl, caller)) ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl.className})").raise() if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise() if (f.value.assign(scope, newValue) == null) f.value = newValue @@ -280,7 +283,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla instance.instanceScope[name]?.let { f -> val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val caller = scope.currentClassCtx - if (!net.sergeych.lyng.canAccessMember(f.visibility, decl, caller)) + if (!canAccessMember(f.visibility, decl, caller)) ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl?.className ?: "?"})").raise() if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise() if (f.value.assign(scope, newValue) == null) f.value = newValue @@ -290,7 +293,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name") val decl = r.declaringClass ?: startClass val caller = scope.currentClassCtx - if (!net.sergeych.lyng.canAccessMember(r.visibility, decl, caller)) + if (!canAccessMember(r.visibility, decl, caller)) ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl.className})").raise() if (!r.isMutable) scope.raiseError("can't assign to read-only field: $name") if (r.value.assign(scope, newValue) == null) r.value = newValue @@ -301,7 +304,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla memberFromAncestor(name)?.let { rec -> val decl = rec.declaringClass ?: startClass val caller = scope.currentClassCtx - if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller)) + if (!canAccessMember(rec.visibility, decl, caller)) scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl.className})")) val saved = instance.instanceScope.currentClassCtx instance.instanceScope.currentClassCtx = decl @@ -316,7 +319,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla instance.instanceScope[name]?.let { rec -> val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val caller = scope.currentClassCtx - if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller)) + if (!canAccessMember(rec.visibility, decl, caller)) scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})")) val saved = instance.instanceScope.currentClassCtx instance.instanceScope.currentClassCtx = decl diff --git a/lynglib/src/commonTest/kotlin/TestInheritance.kt b/lynglib/src/commonTest/kotlin/TestInheritance.kt index 053e00a..687eb96 100644 --- a/lynglib/src/commonTest/kotlin/TestInheritance.kt +++ b/lynglib/src/commonTest/kotlin/TestInheritance.kt @@ -150,5 +150,49 @@ assertEquals(null, (buzz as? Foo)?.runA()) // - Foo.protectedInFoo() is accessible inside Foo and any subclass bodies (including FooBar), // but not from unrelated classes/instances. """.trimIndent()) - } + } + + @Test + fun testMITypes() = runTest { + eval(""" + import lyng.serialization + + class Point(x,y) + class Color(r,g,b) + + class ColoredPoint(x, y, r, g, b): Point(x,y), Color(r,g,b) + + + val cp = ColoredPoint(1,2,30,40,50) + + // cp is Color, Point and ColoredPoint: + assert(cp is ColoredPoint) + assert(cp is Point) + assert(cp is Color) + + // Color fields must be in ColoredPoint: + assertEquals(30, cp.r) + assertEquals(40, cp.g) + assertEquals(50, cp.b) + + // point fields must be available too: + assertEquals(1, cp.x) + assertEquals(2, cp.y) + + + // if we convert type to color, the fields should be available also: + val color = cp as Color + assert(color is Color) + assertEquals(30, color.r) + assertEquals(40, color.g) + assertEquals(50, color.b) + + // converted to Point, cp fields are still available: + val p = cp as Point + assert(p is Point) + assertEquals(1, p.x) + assertEquals(2, p.y) + """) + } + } \ No newline at end of file diff --git a/lynglib/src/jvmTest/kotlin/LynonTests.kt b/lynglib/src/jvmTest/kotlin/LynonTests.kt index a4f80cc..49f263c 100644 --- a/lynglib/src/jvmTest/kotlin/LynonTests.kt +++ b/lynglib/src/jvmTest/kotlin/LynonTests.kt @@ -332,7 +332,9 @@ class LynonTests { val encoded = Lynon.encode(value) println(encoded.toDump()) println("Encoded size %d: %s"(encoded.size, value)) - assertEquals( value, Lynon.decode(encoded) ) + Lynon.decode(encoded).also { + assertEquals( value, it ) + } } """.trimIndent() ) @@ -698,6 +700,29 @@ class Wallet( id, ownerKey, balance=0, createdAt=Instant.now().truncateToSecond( // println(t2.readField(s, "balance")) } + + @Test + fun testMISerialization() = runTest { + val s = testScope() + s.eval(""" + import lyng.serialization + + class Point(x,y) + class Color(r,g,b) + + class ColoredPoint(x, y, r, g, b): Point(x,y), Color(r,g,b) + + val cp = ColoredPoint(1,2,30,40,50) + val d = testEncode( cp ) + assert(d is ColoredPoint) + assert(d is Point) + assert(d is Color) + val p = d as Point + val c = d as Color + val cp2 = ColoredPoint(p.x, p.y, c.r, c.g, c.b) + assertEquals(cp, cp2) + """) + } }