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.Obj
import net.sergeych.lyng.obj.ObjList import net.sergeych.lyng.obj.ObjList
import net.sergeych.lyng.obj.ObjRecord
/** /**
* List of argument declarations in the __definition__ of the lambda, class constructor, * 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( suspend fun assignToContext(
scope: Scope, scope: Scope,
arguments: Arguments = scope.args, arguments: Arguments = scope.args,
defaultAccessType: AccessType = AccessType.Var, defaultAccessType: AccessType = AccessType.Var,
defaultVisibility: Visibility = Visibility.Public defaultVisibility: Visibility = Visibility.Public,
defaultRecordType: ObjRecord.Type = ObjRecord.Type.ConstructorField
) { ) {
fun assign(a: Item, value: Obj) { fun assign(a: Item, value: Obj) {
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, value, 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 // will be used with last lambda arg fix

View File

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

View File

@ -116,9 +116,10 @@ open class Scope(
name: String, name: String,
isMutable: Boolean, isMutable: Boolean,
value: Obj, value: Obj,
visibility: Visibility = Visibility.Public visibility: Visibility = Visibility.Public,
recordType: ObjRecord.Type = ObjRecord.Type.Other
): ObjRecord { ): 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 { fun getOrCreateNamespace(name: String): ObjClass {

View File

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

View File

@ -57,13 +57,20 @@ open class Obj {
args: Arguments = Arguments.EMPTY args: Arguments = Arguments.EMPTY
): T = invokeInstanceMethod(scope, name, args) as T ): 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( open suspend fun invokeInstanceMethod(
scope: Scope, scope: Scope,
name: String, name: String,
args: Arguments = Arguments.EMPTY args: Arguments = Arguments.EMPTY,
onNotFoundResult: Obj?=null
): Obj = ): Obj =
// note that getInstanceMember traverses the hierarchy objClass.getInstanceMemberOrNull(name)?.value?.invoke(scope, this, args)
objClass.getInstanceMember(scope.pos, name).value.invoke(scope, this, args) ?: onNotFoundResult
?: scope.raiseSymbolNotFound(name)
open suspend fun getInstanceMethod( open suspend fun getInstanceMethod(
scope: Scope, scope: Scope,
@ -341,7 +348,7 @@ object ObjNull : Obj() {
scope.raiseNPE() 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() scope.raiseNPE()
} }

View File

@ -97,8 +97,10 @@ open class ObjClass(
return super.readField(scope, name) return super.readField(scope, name)
} }
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments): Obj { override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
return classMembers[name]?.value?.invoke(scope, this, args) ?: super.invokeInstanceMethod(scope, name, args) 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() 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.Arguments
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
@ -29,14 +30,15 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} ?: super.writeField(scope, name, newValue) } ?: 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 { instanceScope[name]?.let {
if (it.visibility.isPublic) if (it.visibility.isPublic)
it.value.invoke(scope, this, args) it.value.invoke(scope, this, args)
else else
scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name")) 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> private val publicFields: Map<String, ObjRecord>
get() = instanceScope.objects.filter { it.value.visibility.isPublic } 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?) { override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
val meta = objClass.constructorMeta val meta = objClass.constructorMeta
?: scope.raiseError("can't serialize non-serializable object (no constructor meta)") ?: scope.raiseError("can't serialize non-serializable object (no constructor meta)")
for( p in meta.params) { // actual constructor can vary, for example, adding new fields with default
val r = readField(scope, p.name) // values, so we save size of the construction:
println("serialize ${p.name}=${r.value}")
TODO() // using objlist allow for some optimizations:
// encoder.encodeObj(scope, r.value) 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 { override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjInstance) return -1 if (other !is ObjInstance) return -1
if (other.objClass != objClass) return -1 if (other.objClass != objClass) return -1
for (f in publicFields) { for (f in comparableVars) {
val a = f.value.value val a = f.value.value
val b = other.instanceScope[f.key]!!.value val b = other.instanceScope[f.key]!!.value
val d = a.compareTo(scope, b) 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, var value: Obj,
val isMutable: Boolean, val isMutable: Boolean,
val visibility: Visibility = Visibility.Public, 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") @Suppress("unused")
fun qualifiedName(name: String): String = fun qualifiedName(name: String): String =
"${importedFrom?.packageName ?: "anonymous"}.$name" "${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 @Test
fun testLet() = runTest { fun testLet() = runTest {
eval( eval(

View File

@ -50,4 +50,27 @@ class TypesTest {
""".trimIndent()) """.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()) """.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())
}
} }