diff --git a/docs/README.md b/docs/README.md index 055271ef6..f9312fa3a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -124,6 +124,7 @@ python -m kotlin_kernel add-kernel --name "JDK 15 Big 2 GPU" --jdk ~/.jdks/openj The following REPL commands are supported: - `:help` - display help - `:classpath` - show current classpath + - `:vars` - get visible variables values ### Dependencies resolving annotations diff --git a/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/Notebook.kt b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/Notebook.kt index 577a506be..65caa72c6 100644 --- a/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/Notebook.kt +++ b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/Notebook.kt @@ -1,7 +1,5 @@ package org.jetbrains.kotlinx.jupyter.api -import kotlin.jvm.Throws - /** * [Notebook] is a main entry point for Kotlin Jupyter API */ @@ -11,6 +9,19 @@ interface Notebook { */ val cellsList: Collection + /** + * Current state of visible variables + */ + val variablesState: Map + + /** + * Stores info about useful variables in a cell. + * Key: cellId; + * Value: set of variable names. + * Useful <==> declarations + modifying references + */ + val cellVariables: Map> + /** * Mapping allowing to get cell by execution number */ diff --git a/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/VariableState.kt b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/VariableState.kt new file mode 100644 index 000000000..d88ea7086 --- /dev/null +++ b/jupyter-lib/api/src/main/kotlin/org/jetbrains/kotlinx/jupyter/api/VariableState.kt @@ -0,0 +1,33 @@ +package org.jetbrains.kotlinx.jupyter.api + +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 +import kotlin.reflect.jvm.isAccessible + +interface VariableState { + val property: KProperty<*> + val scriptInstance: Any? + val stringValue: String? + val value: Any? +} + +data class VariableStateImpl( + override val property: KProperty1, + override val scriptInstance: Any, +) : VariableState { + private var cachedValue: Any? = null + + fun update() { + val wasAccessible = property.isAccessible + property.isAccessible = true + val fieldValue = property.get(scriptInstance) + property.isAccessible = wasAccessible + cachedValue = fieldValue + } + + override val stringValue: String? + get() = cachedValue?.toString() + + override val value: Any? + get() = cachedValue +} 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 388d8dbb8..7aa139a35 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 @@ -25,6 +25,7 @@ class EvaluatedSnippetMetadata( val newClasspath: Classpath = emptyList(), val compiledData: SerializedCompiledScriptsData = SerializedCompiledScriptsData.EMPTY, val newImports: List = emptyList(), + val evaluatedVariablesState: Map = mutableMapOf() ) { companion object { val EMPTY = EvaluatedSnippetMetadata() diff --git a/kotlin-jupyter-plugin/common-dependencies/src/main/kotlin/org/jetbrains/kotlinx/jupyter/common/ReplCommand.kt b/kotlin-jupyter-plugin/common-dependencies/src/main/kotlin/org/jetbrains/kotlinx/jupyter/common/ReplCommand.kt index 2cc342711..bbf156966 100644 --- a/kotlin-jupyter-plugin/common-dependencies/src/main/kotlin/org/jetbrains/kotlinx/jupyter/common/ReplCommand.kt +++ b/kotlin-jupyter-plugin/common-dependencies/src/main/kotlin/org/jetbrains/kotlinx/jupyter/common/ReplCommand.kt @@ -2,7 +2,8 @@ package org.jetbrains.kotlinx.jupyter.common enum class ReplCommand(val desc: String) { HELP("display help"), - CLASSPATH("show current classpath"); + CLASSPATH("show current classpath"), + VARS("get visible variables values"); val nameForUser = getNameForUser(name) diff --git a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/apiImpl.kt b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/apiImpl.kt index 852bc39bc..f233ac330 100644 --- a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/apiImpl.kt +++ b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/apiImpl.kt @@ -9,7 +9,8 @@ import org.jetbrains.kotlinx.jupyter.api.JREInfoProvider import org.jetbrains.kotlinx.jupyter.api.KotlinKernelVersion import org.jetbrains.kotlinx.jupyter.api.Notebook import org.jetbrains.kotlinx.jupyter.api.RenderersProcessor -import java.lang.IllegalStateException +import org.jetbrains.kotlinx.jupyter.api.VariableState +import org.jetbrains.kotlinx.jupyter.repl.InternalEvaluator class DisplayResultWrapper private constructor( val display: DisplayResult, @@ -104,6 +105,11 @@ class NotebookImpl( override val cellsList: Collection get() = cells.values + override val variablesState = mutableMapOf() + + override val cellVariables: Map> + get() = currentCellVariables + override fun getCell(id: Int): CodeCellImpl { return cells[id] ?: throw ArrayIndexOutOfBoundsException( "There is no cell with number '$id'" @@ -114,6 +120,7 @@ class NotebookImpl( return getCell(id).result } + private var currentCellVariables = mapOf>() private val history = arrayListOf() private var mainCellCreated = false @@ -132,6 +139,32 @@ class NotebookImpl( override val jreInfo: JREInfoProvider get() = JavaRuntime + fun updateVariablesState(evaluator: InternalEvaluator) { + variablesState += evaluator.variablesHolder + currentCellVariables = evaluator.cellVariables + } + + fun updateVariablesState(varsStateUpdate: Map) { + variablesState += varsStateUpdate + } + + fun variablesReportAsHTML(): String { + return generateHTMLVarsReport(variablesState) + } + + fun variablesReport(): String { + return if (variablesState.isEmpty()) "" + else { + buildString { + append("Visible vars: \n") + variablesState.forEach { (name, currentState) -> + append("\t$name : ${currentState.stringValue}\n") + } + append('\n') + } + } + } + fun addCell( internalId: Int, preprocessedCode: String, diff --git a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/commands.kt b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/commands.kt index 9a21ec805..25e86d2e1 100644 --- a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/commands.kt +++ b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/commands.kt @@ -1,5 +1,6 @@ package org.jetbrains.kotlinx.jupyter +import org.jetbrains.kotlinx.jupyter.api.htmlResult import org.jetbrains.kotlinx.jupyter.api.textResult import org.jetbrains.kotlinx.jupyter.common.ReplCommand import org.jetbrains.kotlinx.jupyter.common.ReplLineMagic @@ -56,6 +57,9 @@ fun runCommand(code: String, repl: ReplForJupyter): Response { val cp = repl.currentClasspath OkResponseWithMessage(textResult("Current classpath (${cp.count()} paths):\n${cp.joinToString("\n")}")) } + ReplCommand.VARS -> { + OkResponseWithMessage(htmlResult(repl.notebook.variablesReportAsHTML())) + } ReplCommand.HELP -> { val commands = ReplCommand.values().asIterable().joinToStringIndented { ":${it.nameForUser} - ${it.desc}" } val magics = ReplLineMagic.values().asIterable().filter { it.visibleInHelp }.joinToStringIndented { diff --git a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/htmlUtil.kt b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/htmlUtil.kt new file mode 100644 index 000000000..943255f4f --- /dev/null +++ b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/htmlUtil.kt @@ -0,0 +1,64 @@ +package org.jetbrains.kotlinx.jupyter + +import org.jetbrains.kotlinx.jupyter.api.VariableState + +const val varsTableStyleClass = "variables_table" + +fun generateHTMLVarsReport(variablesState: Map): String { + return buildString { + append(generateStyleSection()) + if (variablesState.isEmpty()) { + append("

Variables State's Empty

\n") + return toString() + } + + append("

Variables State

\n") + append(generateVarsTable(variablesState)) + } +} + +fun generateStyleSection(borderPx: Int = 1, paddingPx: Int = 5): String { + //language=HTML + val styleSection = """ + + + """.trimIndent() + return styleSection +} + +fun generateVarsTable(variablesState: Map): String { + return buildString { + append( + """ + + + + + + + """.trimIndent() + ) + + variablesState.entries.forEach { + append( + """ + + + + + """.trimIndent() + ) + } + + append("\n
VariableValue
${it.key}${it.value.stringValue}
\n") + } +} diff --git a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl.kt b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl.kt index a18d1a531..96bebd0e9 100644 --- a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl.kt +++ b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl.kt @@ -17,8 +17,9 @@ import org.jetbrains.kotlinx.jupyter.codegen.FieldsProcessor import org.jetbrains.kotlinx.jupyter.codegen.FieldsProcessorImpl import org.jetbrains.kotlinx.jupyter.codegen.FileAnnotationsProcessor import org.jetbrains.kotlinx.jupyter.codegen.FileAnnotationsProcessorImpl -import org.jetbrains.kotlinx.jupyter.codegen.ResultsRenderersProcessor import org.jetbrains.kotlinx.jupyter.codegen.RenderersProcessorImpl +import org.jetbrains.kotlinx.jupyter.codegen.ResultsRenderersProcessor +import org.jetbrains.kotlinx.jupyter.common.ReplCommand import org.jetbrains.kotlinx.jupyter.common.looksLikeReplCommand import org.jetbrains.kotlinx.jupyter.compiler.CompilerArgsConfigurator import org.jetbrains.kotlinx.jupyter.compiler.DefaultCompilerArgsConfigurator @@ -360,6 +361,27 @@ class ReplForJupyterImpl( else context.compilationConfiguration.asSuccess() } + /** + * Used for debug purposes. + * @see ReplCommand + */ + private fun printVariables(isHtmlFormat: Boolean = false) = log.debug( + if (isHtmlFormat) notebook.variablesReportAsHTML() else notebook.variablesReport() + ) + + private fun printUsagesInfo(cellId: Int, usedVariables: Set?) { + log.debug(buildString { + if (usedVariables == null || usedVariables.isEmpty()) { + append("No usages for cell $cellId") + return@buildString + } + append("Usages for cell $cellId:\n") + usedVariables.forEach { + append(it + "\n") + } + }) + } + override fun eval(code: Code, displayHandler: DisplayHandler?, jupyterId: Int): EvalResult { return withEvalContext { rethrowAsLibraryException(LibraryProblemPart.BEFORE_CELL_CALLBACKS) { @@ -371,14 +393,14 @@ class ReplForJupyterImpl( val compiledData: SerializedCompiledScriptsData val newImports: List val result = try { - executor.execute(code, displayHandler) { internalId, codeToExecute -> + log.debug("Current cell id: $jupyterId") + executor.execute(code, displayHandler, currentCellId = jupyterId - 1) { internalId, codeToExecute -> cell = notebook.addCell(internalId, codeToExecute, EvalData(jupyterId, code)) } } finally { compiledData = internalEvaluator.popAddedCompiledScripts() newImports = importsCollector.popAddedImports() } - cell?.resultVal = result.result.value val rendered = result.result.let { @@ -395,7 +417,13 @@ class ReplForJupyterImpl( updateClasspath() } ?: emptyList() - EvalResult(rendered, EvaluatedSnippetMetadata(newClasspath, compiledData, newImports)) + notebook.updateVariablesState(internalEvaluator) + // printVars() + // printUsagesInfo(jupyterId, cellVariables[jupyterId - 1]) + + + val variablesStateUpdate = notebook.variablesState.mapValues { it.value.stringValue } + EvalResult(rendered, EvaluatedSnippetMetadata(newClasspath, compiledData, newImports, variablesStateUpdate)) } } diff --git a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/CellExecutor.kt b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/CellExecutor.kt index ffa0e03c5..dc4fccf80 100644 --- a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/CellExecutor.kt +++ b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/CellExecutor.kt @@ -17,6 +17,7 @@ interface CellExecutor : ExecutionHost { processAnnotations: Boolean = true, processMagics: Boolean = true, invokeAfterCallbacks: Boolean = true, + currentCellId: Int = -1, callback: ExecutionStartedCallback? = null ): InternalEvalResult } diff --git a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/InternalEvaluator.kt b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/InternalEvaluator.kt index 3dc3246e4..aa546bf41 100644 --- a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/InternalEvaluator.kt +++ b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/InternalEvaluator.kt @@ -1,6 +1,7 @@ package org.jetbrains.kotlinx.jupyter.repl import org.jetbrains.kotlinx.jupyter.api.Code +import org.jetbrains.kotlinx.jupyter.api.VariableState import org.jetbrains.kotlinx.jupyter.compiler.util.SerializedCompiledScriptsData import kotlin.reflect.KClass @@ -12,11 +13,15 @@ interface InternalEvaluator { val lastKClass: KClass<*> val lastClassLoader: ClassLoader + val variablesHolder: Map + + val cellVariables: Map> + /** * Executes code snippet * @throws IllegalStateException if this method was invoked recursively */ - fun eval(code: Code, onInternalIdGenerated: ((Int) -> Unit)? = null): InternalEvalResult + fun eval(code: Code, cellId: Int = -1, onInternalIdGenerated: ((Int) -> Unit)? = null): InternalEvalResult /** * Pop a serialized form of recently added compiled scripts diff --git a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/impl/CellExecutorImpl.kt b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/impl/CellExecutorImpl.kt index 62a46b7db..7b5dc0942 100644 --- a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/impl/CellExecutorImpl.kt +++ b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/impl/CellExecutorImpl.kt @@ -38,6 +38,7 @@ internal class CellExecutorImpl(private val replContext: SharedReplContext) : Ce processAnnotations: Boolean, processMagics: Boolean, invokeAfterCallbacks: Boolean, + currentCellId: Int, callback: ExecutionStartedCallback? ): InternalEvalResult { with(replContext) { @@ -60,7 +61,7 @@ internal class CellExecutorImpl(private val replContext: SharedReplContext) : Ce } val result = baseHost.withHost(context) { - evaluator.eval(preprocessedCode) { internalId -> + evaluator.eval(preprocessedCode, currentCellId) { internalId -> if (callback != null) callback(internalId, preprocessedCode) } } diff --git a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/impl/InternalEvaluatorImpl.kt b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/impl/InternalEvaluatorImpl.kt index 32dc35e93..0ea2f74a1 100644 --- a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/impl/InternalEvaluatorImpl.kt +++ b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/repl/impl/InternalEvaluatorImpl.kt @@ -2,8 +2,11 @@ package org.jetbrains.kotlinx.jupyter.repl.impl import kotlinx.coroutines.runBlocking import org.jetbrains.kotlinx.jupyter.ReplEvalRuntimeException +import org.jetbrains.kotlinx.jupyter.VariablesUsagesPerCellWatcher import org.jetbrains.kotlinx.jupyter.api.Code import org.jetbrains.kotlinx.jupyter.api.FieldValue +import org.jetbrains.kotlinx.jupyter.api.VariableState +import org.jetbrains.kotlinx.jupyter.api.VariableStateImpl import org.jetbrains.kotlinx.jupyter.compiler.CompiledScriptsSerializer import org.jetbrains.kotlinx.jupyter.compiler.util.SerializedCompiledScript import org.jetbrains.kotlinx.jupyter.compiler.util.SerializedCompiledScriptsData @@ -12,6 +15,9 @@ import org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException import org.jetbrains.kotlinx.jupyter.repl.ContextUpdater import org.jetbrains.kotlinx.jupyter.repl.InternalEvalResult import org.jetbrains.kotlinx.jupyter.repl.InternalEvaluator +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.declaredMemberProperties import kotlin.script.experimental.api.ResultValue import kotlin.script.experimental.api.ResultWithDiagnostics import kotlin.script.experimental.jvm.BasicJvmReplEvaluator @@ -57,9 +63,16 @@ internal class InternalEvaluatorImpl( override val lastClassLoader get() = compiler.lastClassLoader + override val variablesHolder = mutableMapOf() + + override val cellVariables: Map> + get() = variablesWatcher.cellVariables + + private val variablesWatcher: VariablesUsagesPerCellWatcher = VariablesUsagesPerCellWatcher() + private var isExecuting = false - override fun eval(code: Code, onInternalIdGenerated: ((Int) -> Unit)?): InternalEvalResult { + override fun eval(code: Code, cellId: Int, onInternalIdGenerated: ((Int) -> Unit)?): InternalEvalResult { try { if (isExecuting) { error("Recursive execution is not supported") @@ -92,19 +105,22 @@ internal class InternalEvaluatorImpl( resultValue.error.message.orEmpty(), resultValue.error ) - is ResultValue.Unit -> { + is ResultValue.Unit, is ResultValue.Value -> { serializeAndRegisterScript(compiledScript) - InternalEvalResult( - FieldValue(Unit, null), - resultValue.scriptInstance!! - ) - } - is ResultValue.Value -> { - serializeAndRegisterScript(compiledScript) - InternalEvalResult( - FieldValue(resultValue.value, pureResult.compiledSnippet.resultField?.first), // TODO: replace with resultValue.name - resultValue.scriptInstance!! - ) + updateDataAfterExecution(cellId, resultValue) + + if (resultValue is ResultValue.Unit) { + InternalEvalResult( + FieldValue(Unit, null), + resultValue.scriptInstance!! + ) + } else { + resultValue as ResultValue.Value + InternalEvalResult( + FieldValue(resultValue.value, resultValue.name), + resultValue.scriptInstance!! + ) + } } is ResultValue.NotEvaluated -> { throw ReplEvalRuntimeException( @@ -124,4 +140,49 @@ internal class InternalEvaluatorImpl( isExecuting = false } } + + private fun updateVariablesState(cellId: Int) { + variablesWatcher.removeOldUsages(cellId) + + variablesHolder.forEach { + val state = it.value as VariableStateImpl + val oldValue = state.stringValue + state.update() + + if (state.stringValue != oldValue) { + variablesWatcher.addUsage(cellId, it.key) + } + } + } + + private fun getVisibleVariables(target: ResultValue, cellId: Int): Map { + val kClass = target.scriptClass ?: return emptyMap() + val cellClassInstance = target.scriptInstance!! + + val fields = kClass.declaredMemberProperties + val ans = mutableMapOf() + fields.forEach { property -> + property as KProperty1 + val state = VariableStateImpl(property, cellClassInstance) + variablesWatcher.addDeclaration(cellId, property.name) + + // it was val, now it's var + if (property is KMutableProperty1) { + variablesHolder.remove(property.name) + } else { + variablesHolder[property.name] = state + return@forEach + } + + ans[property.name] = state + } + return ans + } + + private fun updateDataAfterExecution(lastExecutionCellId: Int, resultValue: ResultValue) { + variablesWatcher.ensureStorageCreation(lastExecutionCellId) + variablesHolder += getVisibleVariables(resultValue, lastExecutionCellId) + + updateVariablesState(lastExecutionCellId) + } } diff --git a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/util.kt b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/util.kt index 745bbef29..724cb6bce 100644 --- a/src/main/kotlin/org/jetbrains/kotlinx/jupyter/util.kt +++ b/src/main/kotlin/org/jetbrains/kotlinx/jupyter/util.kt @@ -68,3 +68,43 @@ fun Int.toSourceCodePositionWithNewAbsolute(code: SourceCode, newCode: SourceCod fun ResultsRenderersProcessor.registerDefaultRenderers() { register(bufferedImageRenderer) } + +/** + * Stores info about where a variable Y was declared and info about what are they at the address X. + * K: key, stands for a way of addressing variables, e.g. address. + * V: value, from Variable, choose any suitable type for your variable reference. + * Default: T=Int, V=String + */ +class VariablesUsagesPerCellWatcher { + val cellVariables = mutableMapOf>() + + /** + * Tells in which cell a variable was declared + */ + private val variablesDeclarationInfo: MutableMap = mutableMapOf() + + fun addDeclaration(address: K, variableRef: V) { + ensureStorageCreation(address) + + // redeclaration of any type + if (variablesDeclarationInfo.containsKey(variableRef)) { + val oldCellId = variablesDeclarationInfo[variableRef] + if (oldCellId != address) { + cellVariables[oldCellId]?.remove(variableRef) + } + } + variablesDeclarationInfo[variableRef] = address + cellVariables[address]?.add(variableRef) + } + + fun addUsage(address: K, variableRef: V) = cellVariables[address]?.add(variableRef) + + fun removeOldUsages(newAddress: K) { + // remove known modifying usages in this cell + cellVariables[newAddress]?.removeIf { + variablesDeclarationInfo[it] != newAddress + } + } + + fun ensureStorageCreation(address: K) = cellVariables.putIfAbsent(address, mutableSetOf()) +} diff --git a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/ApiTest.kt b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/ApiTest.kt index ee0de8375..2bf659d3a 100644 --- a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/ApiTest.kt +++ b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/ApiTest.kt @@ -1,8 +1,10 @@ package org.jetbrains.kotlinx.jupyter.test import org.jetbrains.kotlinx.jupyter.EvalResult +import org.jetbrains.kotlinx.jupyter.generateHTMLVarsReport import org.jetbrains.kotlinx.jupyter.repl.impl.getSimpleCompiler import org.jetbrains.kotlinx.jupyter.test.repl.AbstractSingleReplTest +import org.jetbrains.kotlinx.jupyter.varsTableStyleClass import org.junit.jupiter.api.Test import kotlin.script.experimental.api.ScriptCompilationConfiguration import kotlin.script.experimental.api.ScriptEvaluationConfiguration @@ -41,4 +43,56 @@ class ApiTest : AbstractSingleReplTest() { val version = jCompiler.version assertTrue(version.major >= 0) } + + @Test + fun testVarsReportFormat() { + val res = eval( + """ + val x = 1 + val y = "abc" + val z = 47 + """.trimIndent() + ) + + val varsUpdate = mutableMapOf( + "x" to "1", + "y" to "abc", + "z" to "47" + ) + assertEquals(res.metadata.evaluatedVariablesState, varsUpdate) + val htmlText = generateHTMLVarsReport(repl.notebook.variablesState) + assertEquals( + """ + +

Variables State

+ + + + + + + + + + + + + + + +
VariableValue
x1
yabc
z47
+ + """.trimIndent(), + htmlText + ) + } } 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 de1b82655..19cce98c6 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 @@ -5,12 +5,15 @@ import kotlinx.coroutines.runBlocking import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.jetbrains.kotlinx.jupyter.OutputConfig +import org.jetbrains.kotlinx.jupyter.api.VariableStateImpl import org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException import org.jetbrains.kotlinx.jupyter.generateDiagnostic import org.jetbrains.kotlinx.jupyter.generateDiagnosticFromAbsolute import org.jetbrains.kotlinx.jupyter.repl.CompletionResult import org.jetbrains.kotlinx.jupyter.repl.ListErrorsResult import org.jetbrains.kotlinx.jupyter.test.getOrFail +import org.jetbrains.kotlinx.jupyter.test.getStringValue +import org.jetbrains.kotlinx.jupyter.test.getValue import org.jetbrains.kotlinx.jupyter.withPath import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -19,6 +22,7 @@ import java.io.File import kotlin.script.experimental.api.SourceCode import kotlin.test.assertEquals import kotlin.test.assertFails +import kotlin.test.assertFalse import kotlin.test.fail class ReplTests : AbstractSingleReplTest() { @@ -439,3 +443,335 @@ class ReplTests : AbstractSingleReplTest() { assertEquals("org.RDKit.RWMol", res!!::class.qualifiedName) } } + +class ReplVarsTest : AbstractSingleReplTest() { + override val repl = makeSimpleRepl() + + @Test + fun testVarsStateConsistency() { + assertTrue(repl.notebook.variablesState.isEmpty()) + val res = eval( + """ + val x = 1 + val y = 0 + val z = 47 + """.trimIndent() + ) + + val varsUpdate = mutableMapOf( + "x" to "1", + "y" to "0", + "z" to "47" + ) + assertEquals(res.metadata.evaluatedVariablesState, varsUpdate) + assertFalse(repl.notebook.variablesState.isEmpty()) + val varsState = repl.notebook.variablesState + assertEquals("1", varsState.getStringValue("x")) + assertEquals("0", varsState.getStringValue("y")) + assertEquals(47, varsState.getValue("z")) + + (varsState["z"]!! as VariableStateImpl).update() + repl.notebook.updateVariablesState(varsState) + assertEquals(47, varsState.getValue("z")) + } + + @Test + fun testVarsEmptyState() { + val res = eval("3+2") + val state = repl.notebook.variablesState + val strState = mutableMapOf() + state.forEach { + strState[it.key] = it.value.stringValue ?: return@forEach + } + assertTrue(state.isEmpty()) + assertEquals(res.metadata.evaluatedVariablesState, strState) + } + + @Test + fun testVarsCapture() { + val res = eval( + """ + val x = 1 + val y = "abc" + val z = x + """.trimIndent() + ) + val varsState = repl.notebook.variablesState + assertTrue(varsState.isNotEmpty()) + val strState = mutableMapOf() + varsState.forEach { + strState[it.key] = it.value.stringValue ?: return@forEach + } + + val returnedState = res.metadata.evaluatedVariablesState + assertEquals(strState, returnedState) + assertEquals(1, varsState.getValue("x")) + assertEquals("abc", varsState.getStringValue("y")) + assertEquals("1", varsState.getStringValue("z")) + } + + @Test + fun testVarsCaptureSeparateCells() { + eval( + """ + val x = 1 + val y = "abc" + val z = x + """.trimIndent() + ) + val varsState = repl.notebook.variablesState + assertTrue(varsState.isNotEmpty()) + eval( + """ + val x = "abc" + var y = 123 + val z = x + """.trimIndent(), + jupyterId = 1 + ) + assertTrue(varsState.isNotEmpty()) + assertEquals(3, varsState.size) + assertEquals("abc", varsState.getStringValue("x")) + assertEquals(123, varsState.getValue("y")) + assertEquals("abc", varsState.getStringValue("z")) + + eval( + """ + val x = 1024 + y += 123 + """.trimIndent(), + jupyterId = 2 + ) + + assertTrue(varsState.isNotEmpty()) + assertEquals(3, varsState.size) + assertEquals("1024", varsState.getStringValue("x")) + assertEquals("${123 * 2}", varsState.getStringValue("y")) + assertEquals("abc", varsState.getValue("z")) + } + + @Test + fun testPrivateVarsCapture() { + val res = eval( + """ + private val x = 1 + private val y = "abc" + val z = x + """.trimIndent() + ) + val varsState = repl.notebook.variablesState + assertTrue(varsState.isNotEmpty()) + val strState = mutableMapOf() + varsState.forEach { + strState[it.key] = it.value.stringValue ?: return@forEach + } + + val returnedState = res.metadata.evaluatedVariablesState + assertEquals(strState, returnedState) + assertEquals(1, varsState.getValue("x")) + assertEquals("abc", varsState.getStringValue("y")) + assertEquals("1", varsState.getStringValue("z")) + } + + @Test + fun testPrivateVarsCaptureSeparateCells() { + eval( + """ + private val x = 1 + private val y = "abc" + private val z = x + """.trimIndent() + ) + val varsState = repl.notebook.variablesState + assertTrue(varsState.isNotEmpty()) + eval( + """ + private val x = "abc" + var y = 123 + private val z = x + """.trimIndent(), + jupyterId = 1 + ) + assertTrue(varsState.isNotEmpty()) + assertEquals(3, varsState.size) + assertEquals("abc", varsState.getStringValue("x")) + assertEquals(123, varsState.getValue("y")) + assertEquals("abc", varsState.getStringValue("z")) + + eval( + """ + private val x = 1024 + y += x + """.trimIndent(), + jupyterId = 2 + ) + + assertTrue(varsState.isNotEmpty()) + assertEquals(3, varsState.size) + assertEquals("1024", varsState.getStringValue("x")) + assertEquals(123 + 1024, varsState.getValue("y")) + assertEquals("abc", varsState.getStringValue("z")) + } + + @Test + fun testVarsUsageConsistency() { + eval("3+2") + val state = repl.notebook.cellVariables + assertTrue(state.values.size == 1) + assertTrue(state.values.first().isEmpty()) + val setOfNextCell = setOf() + assertEquals(state.values.first(), setOfNextCell) + } + + @Test + fun testVarsDefsUsage() { + eval( + """ + val x = 1 + val z = "abcd" + var f = 47 + """.trimIndent() + ) + val state = repl.notebook.cellVariables + assertTrue(state.isNotEmpty()) + assertTrue(state.values.first().isNotEmpty()) + val setOfCell = setOf("z", "f", "x") + assertTrue(state.containsValue(setOfCell)) + } + + @Test + fun testVarsDefNRefUsage() { + eval( + """ + val x = "abcd" + var f = 47 + """.trimIndent() + ) + val state = repl.notebook.cellVariables + assertTrue(state.isNotEmpty()) + eval( + """ + val z = 1 + f += f + """.trimIndent() + ) + assertTrue(state.isNotEmpty()) + + val setOfCell = setOf("z", "f", "x") + assertTrue(state.containsValue(setOfCell)) + } + + @Test + fun testPrivateVarsDefNRefUsage() { + eval( + """ + val x = 124 + private var f = "abcd" + """.trimIndent() + ) + val state = repl.notebook.cellVariables + assertTrue(state.isNotEmpty()) + eval( + """ + private var z = 1 + z += x + """.trimIndent() + ) + assertTrue(state.isNotEmpty()) + + val setOfCell = setOf("z", "f", "x") + assertTrue(state.containsValue(setOfCell)) + } + + @Test + fun testSeparateDefsUsage() { + eval( + """ + val x = "abcd" + var f = 47 + """.trimIndent(), + jupyterId = 1 + ) + val state = repl.notebook.cellVariables + assertTrue(state[0]!!.contains("x")) + + eval( + """ + val x = 341 + var f = "abcd" + """.trimIndent(), + jupyterId = 2 + ) + assertTrue(state.isNotEmpty()) + assertTrue(state[0]!!.isEmpty()) + assertTrue(state[1]!!.contains("x")) + + val setOfPrevCell = setOf() + val setOfNextCell = setOf("x", "f") + assertEquals(state[0], setOfPrevCell) + assertEquals(state[1], setOfNextCell) + } + + @Test + fun testSeparatePrivateDefsUsage() { + eval( + """ + private val x = "abcd" + private var f = 47 + """.trimIndent(), + jupyterId = 1 + ) + val state = repl.notebook.cellVariables + assertTrue(state[0]!!.contains("x")) + + eval( + """ + val x = 341 + private var f = "abcd" + """.trimIndent(), + jupyterId = 2 + ) + assertTrue(state.isNotEmpty()) + assertTrue(state[0]!!.isEmpty()) + assertTrue(state[1]!!.contains("x")) + + val setOfPrevCell = setOf() + val setOfNextCell = setOf("x", "f") + assertEquals(state[0], setOfPrevCell) + assertEquals(state[1], setOfNextCell) + } + + @Test + fun testSeparatePrivateCellsUsage() { + eval( + """ + private val x = "abcd" + var f = 47 + internal val z = 47 + """.trimIndent(), + jupyterId = 1 + ) + val state = repl.notebook.cellVariables + assertTrue(state[0]!!.contains("x")) + assertTrue(state[0]!!.contains("z")) + + eval( + """ + private val x = 341 + f += x + protected val z = "abcd" + """.trimIndent(), + jupyterId = 2 + ) + assertTrue(state.isNotEmpty()) + assertTrue(state[0]!!.isNotEmpty()) + assertFalse(state[0]!!.contains("x")) + assertFalse(state[0]!!.contains("z")) + assertTrue(state[1]!!.contains("x")) + + val setOfPrevCell = setOf("f") + val setOfNextCell = setOf("x", "f", "z") + assertEquals(state[0], setOfPrevCell) + assertEquals(state[1], setOfNextCell) + } +} diff --git a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/TrackedCellExecutor.kt b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/TrackedCellExecutor.kt index 6a62fbd14..de2f6d249 100644 --- a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/TrackedCellExecutor.kt +++ b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/repl/TrackedCellExecutor.kt @@ -3,6 +3,7 @@ package org.jetbrains.kotlinx.jupyter.test.repl import org.jetbrains.kotlinx.jupyter.ReplForJupyterImpl import org.jetbrains.kotlinx.jupyter.api.Code import org.jetbrains.kotlinx.jupyter.api.FieldValue +import org.jetbrains.kotlinx.jupyter.api.VariableState import org.jetbrains.kotlinx.jupyter.repl.CellExecutor import org.jetbrains.kotlinx.jupyter.repl.InternalEvalResult import org.jetbrains.kotlinx.jupyter.repl.InternalEvaluator @@ -44,10 +45,13 @@ internal class MockedInternalEvaluator : TrackedInternalEvaluator { override val lastClassLoader: ClassLoader = ClassLoader.getSystemClassLoader() override val executedCodes = mutableListOf() + override val variablesHolder = mutableMapOf() + override val cellVariables = mutableMapOf>() + override val results: List get() = executedCodes.map { null } - override fun eval(code: Code, onInternalIdGenerated: ((Int) -> Unit)?): InternalEvalResult { + override fun eval(code: Code, cellId: Int, onInternalIdGenerated: ((Int) -> Unit)?): InternalEvalResult { executedCodes.add(code.trimIndent()) return InternalEvalResult(FieldValue(null, null), Unit) } @@ -59,9 +63,9 @@ internal class TrackedInternalEvaluatorImpl(private val baseEvaluator: InternalE override val results = mutableListOf() - override fun eval(code: Code, onInternalIdGenerated: ((Int) -> Unit)?): InternalEvalResult { + override fun eval(code: Code, cellId: Int, onInternalIdGenerated: ((Int) -> Unit)?): InternalEvalResult { executedCodes.add(code.trimIndent()) - val res = baseEvaluator.eval(code, onInternalIdGenerated) + val res = baseEvaluator.eval(code, onInternalIdGenerated = onInternalIdGenerated) results.add(res.result.value) return res } diff --git a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/testUtil.kt b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/testUtil.kt index 3d2ff5ea1..dfbae5cde 100644 --- a/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/testUtil.kt +++ b/src/test/kotlin/org/jetbrains/kotlinx/jupyter/test/testUtil.kt @@ -13,6 +13,8 @@ import org.jetbrains.kotlinx.jupyter.api.JREInfoProvider import org.jetbrains.kotlinx.jupyter.api.KotlinKernelVersion import org.jetbrains.kotlinx.jupyter.api.Notebook import org.jetbrains.kotlinx.jupyter.api.RenderersProcessor +import org.jetbrains.kotlinx.jupyter.api.VariableState +import org.jetbrains.kotlinx.jupyter.api.VariableStateImpl import org.jetbrains.kotlinx.jupyter.api.libraries.ExecutionHost import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration import org.jetbrains.kotlinx.jupyter.api.libraries.LibraryDefinition @@ -102,6 +104,14 @@ fun CompletionResult.getOrFail(): CompletionResult.Success = when (this) { else -> fail("Result should be success") } +fun Map.getStringValue(variableName: String): String? { + return get(variableName)?.stringValue +} + +fun Map.getValue(variableName: String): Any? { + return get(variableName)?.value +} + class InMemoryLibraryResolver( parent: LibraryResolver?, initialDescriptorsCache: Map? = null, @@ -147,6 +157,8 @@ object NotebookMock : Notebook { override val cellsList: Collection get() = emptyList() + override val variablesState = mutableMapOf() + override val cellVariables = mapOf>() override fun getCell(id: Int): CodeCellImpl { return cells[id] ?: throw ArrayIndexOutOfBoundsException(