another MI bug fixed, added tests for MI serialization

This commit is contained in:
Sergey Chernov 2025-11-17 15:09:02 +01:00
parent 1e6dd89778
commit 41657b3558
4 changed files with 106 additions and 13 deletions

View File

@ -64,7 +64,7 @@ open class ObjClass(
/** /**
* All ancestors as a Set for fast `isInstanceOf` checks. Order is not guaranteed here and * 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<ObjClass> = val allParentsSet: Set<ObjClass> =
buildSet { buildSet {
@ -173,7 +173,19 @@ open class ObjClass(
if (isRoot) { if (isRoot) {
c.constructorMeta?.let { meta -> c.constructorMeta?.let { meta ->
val argsHere = argsForThis ?: Arguments.EMPTY val argsHere = argsForThis ?: Arguments.EMPTY
// Assign constructor params into instance scope (unmangled)
meta.assignToContext(instance.instanceScope, argsHere) 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 // Initialize direct parents first, in order
@ -203,6 +215,15 @@ open class ObjClass(
c.constructorMeta?.let { meta -> c.constructorMeta?.let { meta ->
val argsHere = argsForThis ?: Arguments.EMPTY val argsHere = argsForThis ?: Arguments.EMPTY
meta.assignToContext(instance.instanceScope, argsHere) 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) val execScope = instance.instanceScope.createChildScope(args = argsForThis ?: Arguments.EMPTY, newThisObj = instance)
ctor.execute(execScope) ctor.execute(execScope)
@ -259,7 +280,7 @@ open class ObjClass(
fun addFn( fun addFn(
name: String, name: String,
isOpen: Boolean = false, isOpen: Boolean = false,
visibility: net.sergeych.lyng.Visibility = net.sergeych.lyng.Visibility.Public, visibility: Visibility = Visibility.Public,
code: suspend Scope.() -> Obj code: suspend Scope.() -> Obj
) { ) {
val stmt = statement { code() } val stmt = statement { code() }

View File

@ -159,7 +159,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult) ?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
private val publicFields: Map<String, ObjRecord> private val publicFields: Map<String, ObjRecord>
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 { override fun toString(): String {
val fields = publicFields.map { "${it.key}=${it.value.value}" }.joinToString(",") 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 // Visibility: declaring class is the qualified ancestor for mangled storage
val decl = rec.declaringClass ?: startClass val decl = rec.declaringClass ?: startClass
val caller = scope.currentClassCtx 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})")) scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})"))
return rec return rec
} }
@ -246,7 +249,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
instance.instanceScope[name]?.let { rec -> instance.instanceScope[name]?.let { rec ->
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx 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 ?: "?"})")) scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl?.className ?: "?"})"))
return rec 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 r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
val decl = r.declaringClass ?: startClass val decl = r.declaringClass ?: startClass
val caller = scope.currentClassCtx 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})")) scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})"))
return when (val value = r.value) { return when (val value = r.value) {
is net.sergeych.lyng.Statement -> ObjRecord(value.execute(instance.instanceScope.createChildScope(scope.pos, newThisObj = instance)), r.isMutable) 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 -> instance.instanceScope.objects[mangled]?.let { f ->
val decl = f.declaringClass ?: startClass val decl = f.declaringClass ?: startClass
val caller = scope.currentClassCtx 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() 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.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null) f.value = newValue 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 -> instance.instanceScope[name]?.let { f ->
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx 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() 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.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null) f.value = newValue 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 r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
val decl = r.declaringClass ?: startClass val decl = r.declaringClass ?: startClass
val caller = scope.currentClassCtx 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() 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.isMutable) scope.raiseError("can't assign to read-only field: $name")
if (r.value.assign(scope, newValue) == null) r.value = newValue 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 -> memberFromAncestor(name)?.let { rec ->
val decl = rec.declaringClass ?: startClass val decl = rec.declaringClass ?: startClass
val caller = scope.currentClassCtx 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})")) scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl.className})"))
val saved = instance.instanceScope.currentClassCtx val saved = instance.instanceScope.currentClassCtx
instance.instanceScope.currentClassCtx = decl instance.instanceScope.currentClassCtx = decl
@ -316,7 +319,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
instance.instanceScope[name]?.let { rec -> instance.instanceScope[name]?.let { rec ->
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name) val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx 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 ?: "?"})")) scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})"))
val saved = instance.instanceScope.currentClassCtx val saved = instance.instanceScope.currentClassCtx
instance.instanceScope.currentClassCtx = decl instance.instanceScope.currentClassCtx = decl

View File

@ -150,5 +150,49 @@ assertEquals(null, (buzz as? Foo)?.runA())
// - Foo.protectedInFoo() is accessible inside Foo and any subclass bodies (including FooBar), // - Foo.protectedInFoo() is accessible inside Foo and any subclass bodies (including FooBar),
// but not from unrelated classes/instances. // but not from unrelated classes/instances.
""".trimIndent()) """.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)
""")
}
} }

View File

@ -332,7 +332,9 @@ class LynonTests {
val encoded = Lynon.encode(value) val encoded = Lynon.encode(value)
println(encoded.toDump()) println(encoded.toDump())
println("Encoded size %d: %s"(encoded.size, value)) println("Encoded size %d: %s"(encoded.size, value))
assertEquals( value, Lynon.decode(encoded) ) Lynon.decode(encoded).also {
assertEquals( value, it )
}
} }
""".trimIndent() """.trimIndent()
) )
@ -698,6 +700,29 @@ class Wallet( id, ownerKey, balance=0, createdAt=Instant.now().truncateToSecond(
// println(t2.readField(s, "balance")) // 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)
""")
}
} }