diff --git a/jupyter-lib/shared-compiler/src/main/kotlin/org/jetbrains/kotlinx/jupyter/compiler/util/serializedCompiledScript.kt b/jupyter-lib/shared-compiler/src/main/kotlin/org/jetbrains/kotlinx/jupyter/compiler/util/serializedCompiledScript.kt index 0bb7d9a44..efc2b4e31 100644 --- a/jupyter-lib/shared-compiler/src/main/kotlin/org/jetbrains/kotlinx/jupyter/compiler/util/serializedCompiledScript.kt +++ b/jupyter-lib/shared-compiler/src/main/kotlin/org/jetbrains/kotlinx/jupyter/compiler/util/serializedCompiledScript.kt @@ -30,7 +30,6 @@ data class SerializedVariablesState( val fieldDescriptor: MutableMap = mutableMapOf() } - @Serializable class EvaluatedSnippetMetadata( val newClasspath: Classpath = emptyList(), diff --git a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/serializationUtils.kt b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/serializationUtils.kt index 496642de3..d87902785 100644 --- a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/serializationUtils.kt +++ b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/serializationUtils.kt @@ -2,9 +2,11 @@ package org.jetbrains.kotlinx.jupyter import org.jetbrains.kotlinx.jupyter.api.VariableState import org.jetbrains.kotlinx.jupyter.compiler.util.SerializedVariablesState +import java.lang.reflect.Field import kotlin.reflect.KClass import kotlin.reflect.KProperty import kotlin.reflect.KProperty1 +import kotlin.reflect.KTypeParameter import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.full.isSubclassOf import kotlin.reflect.jvm.isAccessible @@ -15,8 +17,33 @@ typealias PropertiesData = Collection> data class ProcessedSerializedVarsState( val serializedVariablesState: SerializedVariablesState, - val propertiesData: PropertiesData? -) + val propertiesData: PropertiesData?, + val jvmOnlyFields: Array? = null +) { + // autogenerated + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ProcessedSerializedVarsState + + if (serializedVariablesState != other.serializedVariablesState) return false + if (propertiesData != other.propertiesData) return false + if (jvmOnlyFields != null) { + if (other.jvmOnlyFields == null) return false + if (!jvmOnlyFields.contentEquals(other.jvmOnlyFields)) return false + } else if (other.jvmOnlyFields != null) return false + + return true + } + + override fun hashCode(): Int { + var result = serializedVariablesState.hashCode() + result = 31 * result + (propertiesData?.hashCode() ?: 0) + result = 31 * result + (jvmOnlyFields?.contentHashCode() ?: 0) + return result + } +} data class ProcessedDescriptorsState( // perhaps, better tp make SerializedVariablesState -> PropertiesData? @@ -41,7 +68,6 @@ class VariablesSerializer(private val serializationStep: Int = 2, private val se */ private val computedDescriptorsPerCell: MutableMap = mutableMapOf() - fun serializeVariables(cellId: Int, variablesState: Map): Map { if (seenObjectsPerCell.containsKey(cellId)) { seenObjectsPerCell[cellId]!!.clear() @@ -62,12 +88,17 @@ class VariablesSerializer(private val serializationStep: Int = 2, private val se * @param evaluatedDescriptorsState - origin variable state to get value from * @param serializedVariablesState - current state of recursive state to go further */ - private fun updateVariableState(cellId: Int, propertyName: String, evaluatedDescriptorsState: ProcessedDescriptorsState, - serializedVariablesState: SerializedVariablesState): SerializedVariablesState { + private fun updateVariableState( + cellId: Int, + propertyName: String, + evaluatedDescriptorsState: ProcessedDescriptorsState, + serializedVariablesState: SerializedVariablesState + ): SerializedVariablesState { + // TODO: consider anonymous class as well and fix bug with state val value = evaluatedDescriptorsState.instancesPerState[serializedVariablesState] val propertiesData = evaluatedDescriptorsState.processedSerializedVarsState[serializedVariablesState] - if (propertiesData == null && value != null && value::class.java.isArray) { - return serializeVariableState(cellId, propertyName, null, value, false) + if (propertiesData == null && value != null && (value::class.java.isArray || value::class.java.isMemberClass)) { + return serializeVariableState(cellId, propertyName, propertiesData, value, false) } val property = propertiesData?.firstOrNull { it.name == propertyName @@ -76,14 +107,22 @@ class VariablesSerializer(private val serializationStep: Int = 2, private val se return serializeVariableState(cellId, propertyName, property, value, false) } - fun serializeVariableState(cellId: Int, name: String?, variableState: VariableState?, isOverride: Boolean = true): SerializedVariablesState { if (variableState == null || name == null) return SerializedVariablesState() return serializeVariableState(cellId, name, variableState.property, variableState.value, isOverride) } - fun serializeVariableState(cellId: Int, name: String, property: KProperty<*>?, value: Any?, isOverride: Boolean = true): SerializedVariablesState { - val processedData = createSerializeVariableState(name, property, value) + fun serializeVariableState(cellId: Int, name: String, property: Field?, value: Any?, isOverride: Boolean = true): SerializedVariablesState { + val processedData = createSerializeVariableState(name, getSimpleTypeNameFrom(property, value), value) + return doActualSerialization(cellId, processedData, value, isOverride) + } + + fun serializeVariableState(cellId: Int, name: String, property: KProperty<*>, value: Any?, isOverride: Boolean = true): SerializedVariablesState { + val processedData = createSerializeVariableState(name, getSimpleTypeNameFrom(property, value), value) + return doActualSerialization(cellId, processedData, value, isOverride) + } + + private fun doActualSerialization(cellId: Int, processedData: ProcessedSerializedVarsState, value: Any?, isOverride: Boolean = true): SerializedVariablesState { val serializedVersion = processedData.serializedVariablesState seenObjectsPerCell.putIfAbsent(cellId, mutableMapOf()) @@ -103,8 +142,7 @@ class VariablesSerializer(private val serializationStep: Int = 2, private val se return processedData.serializedVariablesState } - - private fun iterateThroughContainerMembers(cellId: Int, callInstance: Any?, descriptor: MutableFieldDescriptor, properties: PropertiesData?, currentDepth: Int = 0): Unit { + private fun iterateThroughContainerMembers(cellId: Int, callInstance: Any?, descriptor: MutableFieldDescriptor, properties: PropertiesData?, currentDepth: Int = 0) { if (properties == null || callInstance == null || currentDepth >= serializationStep) return val serializedIteration = mutableMapOf() @@ -123,7 +161,7 @@ class VariablesSerializer(private val serializationStep: Int = 2, private val se val value = tryGetValueFromProperty(it, callInstance) if (!seenObjectsPerCell!!.containsKey(value)) { - serializedIteration[name] = createSerializeVariableState(name, it, value) + serializedIteration[name] = createSerializeVariableState(name, getSimpleTypeNameFrom(it, value), value) descriptor[name] = serializedIteration[name]!!.serializedVariablesState } if (descriptor[name] != null) { @@ -155,61 +193,104 @@ class VariablesSerializer(private val serializationStep: Int = 2, private val se isArrayType -> { callInstance } - else -> { null } + else -> { + null + } } if (isArrayType) { if (callInstance is List<*>) { callInstance.forEach { arrayElem -> - iterateThroughContainerMembers(cellId, arrayElem, serializedVariablesState.fieldDescriptor, - it.value.propertiesData, currentDepth + 1) + iterateThroughContainerMembers( + cellId, + arrayElem, + serializedVariablesState.fieldDescriptor, + it.value.propertiesData, + currentDepth + 1 + ) } } else { callInstance as Array<*> callInstance.forEach { arrayElem -> - iterateThroughContainerMembers(cellId, arrayElem, serializedVariablesState.fieldDescriptor, - it.value.propertiesData, currentDepth + 1) + iterateThroughContainerMembers( + cellId, + arrayElem, + serializedVariablesState.fieldDescriptor, + it.value.propertiesData, + currentDepth + 1 + ) } } return@forEach } - iterateThroughContainerMembers(cellId, neededCallInstance, serializedVariablesState.fieldDescriptor, - it.value.propertiesData, currentDepth + 1) + + // update state with JVMFields + it.value.jvmOnlyFields?.forEach { field -> + serializedVariablesState.fieldDescriptor[field.name] = serializeVariableState(cellId, field.name, field, neededCallInstance) + instancesPerState[serializedVariablesState] = neededCallInstance + } + iterateThroughContainerMembers( + cellId, + neededCallInstance, + serializedVariablesState.fieldDescriptor, + it.value.propertiesData, + currentDepth + 1 + ) } } } + private fun getSimpleTypeNameFrom(property: Field?, value: Any?): String? { + return if (property != null) { + val returnType = property.type + returnType.simpleName + } else { + value?.toString() + } + } - private fun createSerializeVariableState(name: String, property: KProperty<*>?, value: Any?): ProcessedSerializedVarsState { - val simpleName = if (property != null) { + private fun getSimpleTypeNameFrom(property: KProperty<*>?, value: Any?): String? { + return if (property != null) { val returnType = property.returnType - val classifier = returnType.classifier as KClass<*> - classifier.simpleName + val classifier = returnType.classifier + if (classifier is KTypeParameter) { + classifier.name + } else { + (classifier as KClass<*>).simpleName + } } else { value?.toString() } + } + private fun createSerializeVariableState(name: String, simpleTypeName: String?, value: Any?): ProcessedSerializedVarsState { // make it exception-safe val membersProperties = try { if (value != null) value::class.declaredMemberProperties else null } catch (e: Throwable) { null } - val isContainer = if (membersProperties != null) (membersProperties.size > 1 || value!!::class.java.isArray) else false - val type = if (value!= null && value::class.java.isArray) { + val javaClass = value?.javaClass + val jvmFields = if (javaClass != null && javaClass.isMemberClass) { + javaClass.declaredFields + } else { null } + + val isContainer = if (membersProperties != null) ( + membersProperties.isNotEmpty() || value!!::class.java.isArray || (javaClass != null && javaClass.isMemberClass) + ) else false + val type = if (value != null && value::class.java.isArray) { "Array" } else if (isContainer && value is List<*>) { "SingletonList" } else { - simpleName.toString() + simpleTypeName.toString() } val serializedVariablesState = SerializedVariablesState(name, type, getProperString(value), isContainer) - return ProcessedSerializedVarsState(serializedVariablesState, membersProperties) + return ProcessedSerializedVarsState(serializedVariablesState, membersProperties, jvmFields) } - private fun tryGetValueFromProperty(property: KProperty1, callInstance: Any): Any? { // some fields may be optimized out like array size. Thus, calling it.isAccessible would return error val canAccess = try { @@ -243,13 +324,10 @@ class VariablesSerializer(private val serializationStep: Int = 2, private val se false } } - } -// TODO: maybe think of considering the depth? fun getProperString(value: Any?): String { - - fun print(builder: StringBuilder, containerSize:Int, index: Int, value: Any?): Unit { + fun print(builder: StringBuilder, containerSize: Int, index: Int, value: Any?) { if (index != containerSize - 1) { builder.append(value, ", ") } else { @@ -296,4 +374,4 @@ fun getProperString(value: Any?): String { } return value.toString() -} \ No newline at end of file +} diff --git a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/ReplTests.kt b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/ReplTests.kt index 263c90a41..c6fb37b3a 100644 --- a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/ReplTests.kt +++ b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/ReplTests.kt @@ -776,7 +776,6 @@ class ReplVarsTest : AbstractSingleReplTest() { } } - class ReplVarsSerializationTest : AbstractSingleReplTest() { override val repl = makeSimpleRepl() @@ -805,7 +804,7 @@ class ReplVarsSerializationTest : AbstractSingleReplTest() { val actualContainer = listDescriptors.entries.first().value!! assertEquals(2, actualContainer.fieldDescriptor.size) assertTrue(actualContainer.isContainer) - assertEquals(listOf(1,2,3,4).toString().substring(1, actualContainer.value!!.length + 1), actualContainer.value) + assertEquals(listOf(1, 2, 3, 4).toString().substring(1, actualContainer.value!!.length + 1), actualContainer.value) } @Test @@ -842,6 +841,34 @@ class ReplVarsSerializationTest : AbstractSingleReplTest() { } } + @Test + fun cyclicReferenceTest() { + val res = eval( + """ + class C { + inner class Inner; + val i = Inner() + val counter = 0 + } + val c = C() + """.trimIndent(), + jupyterId = 1 + ) + val varsData = res.metadata.evaluatedVariablesState + assertEquals(1, varsData.size) + assertTrue(varsData.containsKey("c")) + + val serializedState = varsData["c"]!! + assertTrue(serializedState.isContainer) + val descriptor = serializedState.fieldDescriptor + assertEquals(2, descriptor.size) + assertEquals("0", descriptor["counter"]!!.value) + + val serializer = repl.variablesSerializer + + val newData = serializer.doIncrementalSerialization(0, descriptor["i"]!!.name, descriptor["i"]!!) + val a = 1 + } @Test fun incrementalUpdateTest() { @@ -882,7 +909,5 @@ class ReplVarsSerializationTest : AbstractSingleReplTest() { assertTrue(state.isContainer) assertEquals("${values++}", state.value) } - } - -} \ No newline at end of file +}