fix #35 serialization alsso user classes. Fixed user classes comparison

This commit is contained in:
Sergey Chernov 2025-08-04 16:49:55 +03:00
parent f805e1ee82
commit 6df06a6911
12 changed files with 211 additions and 23 deletions

View File

@ -2,6 +2,7 @@ package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.ObjRecord
/**
* List of argument declarations in the __definition__ of the lambda, class constructor,
@ -22,17 +23,20 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
}
/**
* parse args and create local vars in a given context
* parse args and create local vars in a given context properly interpreting
* ellipsis args and default values
*/
suspend fun assignToContext(
scope: Scope,
arguments: Arguments = scope.args,
defaultAccessType: AccessType = AccessType.Var,
defaultVisibility: Visibility = Visibility.Public
defaultVisibility: Visibility = Visibility.Public,
defaultRecordType: ObjRecord.Type = ObjRecord.Type.ConstructorField
) {
fun assign(a: Item, value: Obj) {
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value,
a.visibility ?: defaultVisibility)
a.visibility ?: defaultVisibility,
recordType = defaultRecordType)
}
// will be used with last lambda arg fix

View File

@ -1122,7 +1122,7 @@ class Compiler(
thisObj
}
// inheritance must alter this code:
val newClass = ObjClass(className).apply {
val newClass = ObjInstanceClass(className).apply {
instanceConstructor = constructorCode
constructorMeta = constructorArgsDeclaration
}
@ -1630,7 +1630,7 @@ class Compiler(
// create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, isMutable, initValue, visibility)
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
initValue
}
}

View File

@ -116,9 +116,10 @@ open class Scope(
name: String,
isMutable: Boolean,
value: Obj,
visibility: Visibility = Visibility.Public
visibility: Visibility = Visibility.Public,
recordType: ObjRecord.Type = ObjRecord.Type.Other
): ObjRecord {
return ObjRecord(value, isMutable, visibility).also { objects[name] = it }
return ObjRecord(value, isMutable, visibility,type = recordType).also { objects[name] = it }
}
fun getOrCreateNamespace(name: String): ObjClass {

View File

@ -131,6 +131,12 @@ class Script(
if( a.compareTo(this, b) != 0 )
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} == ${b.inspect()}"))
}
addVoidFn("assertNotEquals") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if( a.compareTo(this, b) == 0 )
raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} != ${b.inspect()}"))
}
addFn("assertThrows") {
val code = requireOnlyArg<Statement>()
val result =try {

View File

@ -57,13 +57,20 @@ open class Obj {
args: Arguments = Arguments.EMPTY
): T = invokeInstanceMethod(scope, name, args) as T
/**
* Invoke a method of the object if exists
* it [onNotFoundResult] is not null, it returns it when symbol is not found
* otherwise throws [ObjSymbolNotDefinedException] object exception
*/
open suspend fun invokeInstanceMethod(
scope: Scope,
name: String,
args: Arguments = Arguments.EMPTY
args: Arguments = Arguments.EMPTY,
onNotFoundResult: Obj?=null
): Obj =
// note that getInstanceMember traverses the hierarchy
objClass.getInstanceMember(scope.pos, name).value.invoke(scope, this, args)
objClass.getInstanceMemberOrNull(name)?.value?.invoke(scope, this, args)
?: onNotFoundResult
?: scope.raiseSymbolNotFound(name)
open suspend fun getInstanceMethod(
scope: Scope,
@ -341,7 +348,7 @@ object ObjNull : Obj() {
scope.raiseNPE()
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments): Obj {
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments, onNotFoundResult: Obj?): Obj {
scope.raiseNPE()
}

View File

@ -97,8 +97,10 @@ open class ObjClass(
return super.readField(scope, name)
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments): Obj {
return classMembers[name]?.value?.invoke(scope, this, args) ?: super.invokeInstanceMethod(scope, name, args)
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
onNotFoundResult: Obj?): Obj {
return classMembers[name]?.value?.invoke(scope, this, args)
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
}
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = scope.raiseNotImplemented()

View File

@ -2,6 +2,7 @@ package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
@ -29,14 +30,15 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} ?: super.writeField(scope, name, newValue)
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments): Obj =
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
onNotFoundResult: Obj?): Obj =
instanceScope[name]?.let {
if (it.visibility.isPublic)
it.value.invoke(scope, this, args)
else
scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name"))
}
?: super.invokeInstanceMethod(scope, name, args)
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
private val publicFields: Map<String, ObjRecord>
get() = instanceScope.objects.filter { it.value.visibility.isPublic }
@ -49,19 +51,51 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
val meta = objClass.constructorMeta
?: scope.raiseError("can't serialize non-serializable object (no constructor meta)")
for( p in meta.params) {
val r = readField(scope, p.name)
println("serialize ${p.name}=${r.value}")
TODO()
// encoder.encodeObj(scope, r.value)
// actual constructor can vary, for example, adding new fields with default
// values, so we save size of the construction:
// using objlist allow for some optimizations:
val params = meta.params.map { readField(scope, it.name).value }
encoder.encodeAnyList(scope, params)
serializeStateVars(scope, encoder)
}
protected val instanceVars: Map<String, ObjRecord> by lazy {
instanceScope.objects.filter { it.value.type.serializable }
}
protected suspend fun serializeStateVars(scope: Scope,encoder: LynonEncoder) {
val vars = instanceVars.values.map { it.value }
if( vars.isNotEmpty()) {
encoder.encodeAnyList(scope, vars)
println("serialized state vars $vars")
}
}
internal suspend fun deserializeStateVars(scope: Scope, decoder: LynonDecoder) {
val localVars = instanceVars.values.toList()
if( localVars.isNotEmpty() ) {
println("gonna read vars")
val vars = decoder.decodeAnyList(scope)
if (vars.size > instanceVars.size)
scope.raiseIllegalArgument("serialized vars has bigger size than instance vars")
println("deser state vars $vars")
for ((i, v) in vars.withIndex()) {
localVars[i].value = vars[i]
}
}
}
protected val comparableVars: Map<String, ObjRecord> by lazy {
instanceScope.objects.filter {
it.value.type.comparable
}
// todo: possible vars?
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjInstance) return -1
if (other.objClass != objClass) return -1
for (f in publicFields) {
for (f in comparableVars) {
val a = f.value.value
val b = other.instanceScope[f.key]!!.value
val d = a.compareTo(scope, b)

View File

@ -0,0 +1,28 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonType
/**
* Special variant of [ObjClass] to be used in [ObjInstance], e.g. for Lyng compiled classes
*/
class ObjInstanceClass(val name: String) : ObjClass(name) {
// val onDeserilaized =
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
val args = decoder.decodeAnyList(scope)
println("deserializing constructor $name, $args params")
val actualSize = constructorMeta?.params?.size ?: 0
if( args.size > actualSize )
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
val newScope = scope.copy(args = Arguments(args))
return (callOn(newScope) as ObjInstance).apply {
deserializeStateVars(scope,decoder)
invokeInstanceMethod(scope, "onDeserialized", onNotFoundResult = ObjVoid)
}
}
}

View File

@ -10,8 +10,17 @@ data class ObjRecord(
var value: Obj,
val isMutable: Boolean,
val visibility: Visibility = Visibility.Public,
var importedFrom: Scope? = null
var importedFrom: Scope? = null,
val isTransient: Boolean = false,
val type: Type = Type.Other
) {
enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) {
Field(true, true),
@Suppress("unused")
Fun,
ConstructorField(true, true),
Other
}
@Suppress("unused")
fun qualifiedName(name: String): String =
"${importedFrom?.packageName ?: "anonymous"}.$name"

View File

@ -2242,6 +2242,14 @@ class ScriptTest {
)
}
@Test
fun testSet2() = runTest {
eval("""
assertEquals( Set( ...[1,2,3]), Set(1,2,3) )
assertEquals( Set( ...[1,false,"ok"]), Set("ok", 1, false) )
""".trimIndent())
}
@Test
fun testLet() = runTest {
eval(

View File

@ -50,4 +50,27 @@ class TypesTest {
""".trimIndent())
}
@Test
fun testUserClassCompareTo() = runTest {
eval("""
class Point(val a,b)
assertEquals(Point(0,1), Point(0,1) )
assertNotEquals(Point(0,1), Point(1,1) )
""".trimIndent())
}
@Test
fun testUserClassCompareTo2() = runTest {
eval("""
class Point(val a,b) {
var c = 0
}
assertEquals(Point(0,1), Point(0,1) )
assertEquals(Point(0,1).apply { c = 2 }, Point(0,1).apply { c = 2 } )
assertNotEquals(Point(0,1), Point(1,1) )
assertNotEquals(Point(0,1), Point(0,1).apply { c = 1 } )
""".trimIndent())
}
}

View File

@ -566,6 +566,72 @@ class LynonTests {
""".trimIndent())
}
@Test
fun testClassSerializationNoInstanceVars() = runTest {
testScope().eval("""
import lyng.serialization
class Point(x,y)
// println( Lynon.encode(Point(0,0)).toDump() )
testEncode(Point(0,0))
testEncode(Point(10,11))
testEncode(Point(-1,2))
testEncode(Point(-1,-2))
testEncode(Point("point!",-2))
""".trimIndent())
}
@Test
fun testClassSerializationWithInstanceVars() = runTest {
testScope().eval("""
import lyng.serialization
class Point(x=0) {
var y = 0
}
testEncode(Point())
testEncode(Point(1))
testEncode(Point(1).apply { y = 2 })
testEncode(Point(10).also { it.y = 11 })
""".trimIndent())
}
@Test
fun testClassSerializationWithInstanceVars2() = runTest {
testScope().eval("""
import lyng.serialization
var onInitComment = null
class Point(x=0) {
var y = 0
var comment = null
fun onDeserialized() {
onInitComment = comment
}
}
testEncode(Point())
testEncode(Point(1))
testEncode(Point(1).apply { y = 2 })
testEncode(Point(10).also { it.y = 11 })
// important: class init is called before setting non-constructor fields
// this is decessary, so deserialized fields are only available
// after onDeserialized() call (if exists):
// deserialized:
testEncode(Point(10).also { it.y = 11; it.comment = "comment" })
println("-- on init comment "+onInitComment)
assertEquals("comment", onInitComment)
""".trimIndent())
}
}