diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt index 8016547..97ed68a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt @@ -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, 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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index e61c81c..d2eee33 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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 } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index f66ab97..ac1018b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -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 { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index d4c6420..d264833 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -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(0) + val b = requiredArg(1) + if( a.compareTo(this, b) == 0 ) + raiseError(ObjAssertionFailedException(this,"Assertion failed: ${a.inspect()} != ${b.inspect()}")) + } addFn("assertThrows") { val code = requireOnlyArg() val result =try { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index 972595f..50ce923 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -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() } 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 fd108f3..6044101 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -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() 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 337b06e..6138ace 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -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 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 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 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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstanceClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstanceClass.kt new file mode 100644 index 0000000..c86e1d9 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstanceClass.kt @@ -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) + } + } + +} \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt index cc56a81..0b049b1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt @@ -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" diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 2d1aaca..a06084d 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -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( diff --git a/lynglib/src/commonTest/kotlin/TypesTest.kt b/lynglib/src/commonTest/kotlin/TypesTest.kt index 5c37f36..c89d749 100644 --- a/lynglib/src/commonTest/kotlin/TypesTest.kt +++ b/lynglib/src/commonTest/kotlin/TypesTest.kt @@ -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()) + } } \ No newline at end of file diff --git a/lynglib/src/jvmTest/kotlin/LynonTests.kt b/lynglib/src/jvmTest/kotlin/LynonTests.kt index 67353e3..e0aa777 100644 --- a/lynglib/src/jvmTest/kotlin/LynonTests.kt +++ b/lynglib/src/jvmTest/kotlin/LynonTests.kt @@ -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()) + } + }