From 92bf260057a89fb41dc857138d6ec4c3d814ee44 Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Mon, 16 May 2022 16:53:37 +0200 Subject: [PATCH] Scripting: Implement conditional conversion for REPL result values to support value types erased from runtime classes. See example in added tests for motivation. #KT-45065 fixed also refactor launcher repl test and result type rendering --- .../kotlin/codegen/state/GenerationState.kt | 1 - .../kotlin/cli/common/repl/ReplApi.kt | 2 +- .../backend/jvm/lower/ScriptLowering.kt | 3 +- .../jetbrains/kotlin/cli/LauncherReplTest.kt | 131 ++++++++++++------ .../plugin/impl/jvmCompilationUtil.kt | 2 +- .../plugin/repl/GenericReplCompiler.kt | 2 +- .../compiler/plugin/repl/ReplFromTerminal.kt | 33 ++++- .../compiler/plugin/repl/ReplInterpreter.kt | 2 +- 8 files changed, 127 insertions(+), 49 deletions(-) diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/state/GenerationState.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/state/GenerationState.kt index 1b02d5de747..05a540b2823 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/state/GenerationState.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/state/GenerationState.kt @@ -309,7 +309,6 @@ class GenerationState private constructor( // and the rest is an output from the codegen var resultFieldName: String? = null - var resultTypeString: String? = null var resultType: KotlinType? = null } diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/ReplApi.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/ReplApi.kt index 5748d958571..e204a4f6137 100644 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/ReplApi.kt +++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/ReplApi.kt @@ -126,7 +126,7 @@ interface ReplEvalAction { } sealed class ReplEvalResult : Serializable { - class ValueResult(val name: String, val value: Any?, val type: String?) : ReplEvalResult() { + class ValueResult(val name: String, val value: Any?, val type: String?, val snippetInstance: Any? = null) : ReplEvalResult() { override fun toString(): String { val v = if (value is Function<*>) "" else value return "$name: $type = $v" diff --git a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/ScriptLowering.kt b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/ScriptLowering.kt index f5104f3612e..1efeaf2b609 100644 --- a/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/ScriptLowering.kt +++ b/compiler/ir/backend.jvm/lower/src/org/jetbrains/kotlin/backend/jvm/lower/ScriptLowering.kt @@ -23,6 +23,7 @@ import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.declarations.impl.IrAnonymousInitializerImpl import org.jetbrains.kotlin.ir.declarations.impl.IrClassImpl import org.jetbrains.kotlin.ir.declarations.impl.IrExternalPackageFragmentImpl +import org.jetbrains.kotlin.ir.descriptors.toIrBasedKotlinType import org.jetbrains.kotlin.ir.expressions.* import org.jetbrains.kotlin.ir.expressions.impl.IrClassReferenceImpl import org.jetbrains.kotlin.ir.expressions.impl.IrGetFieldImpl @@ -290,7 +291,7 @@ private class ScriptsToClassesLowering(val context: JvmBackendContext, val inner irScript.resultProperty?.owner?.let { irResultProperty -> context.state.scriptSpecific.resultFieldName = irResultProperty.name.identifier - context.state.scriptSpecific.resultTypeString = irResultProperty.backingField?.type?.render() + context.state.scriptSpecific.resultType = irResultProperty.backingField?.type?.toIrBasedKotlinType() } } diff --git a/compiler/tests/org/jetbrains/kotlin/cli/LauncherReplTest.kt b/compiler/tests/org/jetbrains/kotlin/cli/LauncherReplTest.kt index dc0912a0c61..dac3518a675 100644 --- a/compiler/tests/org/jetbrains/kotlin/cli/LauncherReplTest.kt +++ b/compiler/tests/org/jetbrains/kotlin/cli/LauncherReplTest.kt @@ -5,36 +5,34 @@ package org.jetbrains.kotlin.cli -import com.intellij.openapi.util.SystemInfo import junit.framework.TestCase +import org.jetbrains.kotlin.cli.common.CompilerSystemProperties import org.jetbrains.kotlin.test.TestCaseWithTmpdir import org.jetbrains.kotlin.utils.PathUtil -import java.io.File -import java.io.InputStream +import org.junit.Assert +import java.io.* import java.util.concurrent.TimeUnit import kotlin.concurrent.thread class LauncherReplTest : TestCaseWithTmpdir() { private fun runInteractive( - executableName: String, - vararg inputs: String, - expectedOutPatterns: List = emptyList(), + vararg inputsToExpectedOutputs: Pair, expectedExitCode: Int = 0, workDirectory: File? = null ) { - val executableFileName = if (SystemInfo.isWindows) "$executableName.bat" else executableName - val launcherFile = File(PathUtil.kotlinPathsForDistDirectory.homePath, "bin/$executableFileName") - assertTrue("Launcher script not found, run dist task: ${launcherFile.absolutePath}", launcherFile.exists()) + val javaExecutable = File(File(CompilerSystemProperties.JAVA_HOME.safeValue, "bin"), "java") - val processBuilder = ProcessBuilder(launcherFile.absolutePath) + val processBuilder = ProcessBuilder( + javaExecutable.absolutePath, + "-jar", + File(PathUtil.kotlinPathsForDistDirectory.homePath, "lib/kotlin-compiler.jar").absolutePath, + ) if (workDirectory != null) { processBuilder.directory(workDirectory) } val process = processBuilder.start() - val inputIter = inputs.iterator() - data class ExceptionContainer( var value: Throwable? = null ) @@ -43,9 +41,14 @@ class LauncherReplTest : TestCaseWithTmpdir() { val out = ArrayList() val exceptionContainer = ExceptionContainer() val thread = thread { + val promptRegex = Regex("(?:\\u001B\\p{Graph}+)*(?:>>>|\\.\\.\\.)") try { - reader().forEachLine { - out.add(it.trim()) + reader().forEachLine { rawLine -> + promptRegex.split(rawLine).forEach { line -> + if (line.isNotEmpty()) { + out.add(line.trim()) + } + } } } catch (e: Throwable) { exceptionContainer.value = e @@ -57,19 +60,12 @@ class LauncherReplTest : TestCaseWithTmpdir() { val (stdoutThread, stdoutException, processOut) = process.inputStream.captureStream() val (stderrThread, stderrException, processErr) = process.errorStream.captureStream() + val inputIter = inputsToExpectedOutputs.iterator() var stdinException: Throwable? = null val stdinThread = thread { try { - val writer = process.outputStream.writer() - val eol = System.getProperty("line.separator") - while (inputIter.hasNext()) { - with(writer) { - write(inputIter.next()) - write(eol) - flush() - } - } + writeInputsToOutStream(process.outputStream, inputIter) } catch (e: Throwable) { stdinException = e } @@ -90,15 +86,7 @@ class LauncherReplTest : TestCaseWithTmpdir() { TestCase.assertNull(stderrException.value) TestCase.assertFalse("stdin thread not finished", stdinThread.isAlive) TestCase.assertNull(stdinException) - TestCase.assertEquals(expectedOutPatterns.size, processOut.size) - for (i in 0 until expectedOutPatterns.size) { - val expectedPattern = expectedOutPatterns[i] - val actualLine = processOut[i] - TestCase.assertTrue( - "line \"$actualLine\" do not match with expected pattern \"$expectedPattern\"", - Regex(expectedPattern).matches(actualLine) - ) - } + assertOutputMatches(inputsToExpectedOutputs, processOut) TestCase.assertEquals(expectedExitCode, process.exitValue()) TestCase.assertFalse(inputIter.hasNext()) @@ -110,17 +98,76 @@ class LauncherReplTest : TestCaseWithTmpdir() { } } + private fun writeInputsToOutStream(dataOutStream: OutputStream, inputIter: Iterator>) { + val writer = dataOutStream.writer() + val eol = System.getProperty("line.separator") + + fun writeNextInput(nextInput: String) { + with(writer) { + write(nextInput) + write(eol) + flush() + } + } + + while (inputIter.hasNext()) { + val nextInput = inputIter.next().first ?: continue + writeNextInput(nextInput) + } + writeNextInput(":quit") + } + + private fun assertOutputMatches( + inputsToExpectedOutputs: Array>, + actualOut: List + ) { + val inputsToExpectedOutputsIter = inputsToExpectedOutputs.iterator() + val actualIter = actualOut.iterator() + + while (true) { + if (inputsToExpectedOutputsIter.hasNext() && !actualIter.hasNext()) { + Assert.fail("missing output for expected patterns:\n${inputsToExpectedOutputsIter.asSequence().joinToString("\n") { it.second } }") + } + if (!inputsToExpectedOutputsIter.hasNext() || !actualIter.hasNext()) break + val (input, expectedPattern) = inputsToExpectedOutputsIter.next() + val actualLine = actualIter.next() + val strippedActualLine = if (input != null) actualLine.removePrefix(input) else actualLine + assertTrue( + "line \"$strippedActualLine\" do not match with expected pattern \"$expectedPattern\"", + Regex(expectedPattern).matches(strippedActualLine) + ) + } + } + + val replOutHeader = arrayOf( + null to "Welcome to Kotlin version .*", + null to "Type :help for help, :quit for quit" + ) + fun testSimpleRepl() { runInteractive( - "kotlinc", - "println(42)", - ":quit", - expectedOutPatterns = listOf( - "Welcome to Kotlin version .*", - "Type :help for help, :quit for quit", - ".*42$", - ".*" - ) + *replOutHeader, + "println(42)" to "42", ) } -} \ No newline at end of file + + fun testReplResultFormatting() { + runInteractive( + *replOutHeader, + "class C" to "", + "C()" to "res1: Line_0\\.C = Line_0\\\$C@\\p{XDigit}+", + ) + } + + fun testReplValueClassConversion() { + runInteractive( + *replOutHeader, + "import kotlin.time.Duration.Companion.nanoseconds" to "", + "Result.success(\"OK\")" to "res1: kotlin\\.Result = Success\\(OK\\)", + "0U-1U" to "res2: kotlin.UInt = 4294967295", + "10.nanoseconds" to "res3: kotlin.time.Duration = 10ns", + "@JvmInline value class Z(val x: Int)" to "", + "Z(42)" to "res5: Line_4\\.Z = Z\\(x=42\\)", + ) + } +} diff --git a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/jvmCompilationUtil.kt b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/jvmCompilationUtil.kt index 8b0afb6151d..b8d9fb0501a 100644 --- a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/jvmCompilationUtil.kt +++ b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/jvmCompilationUtil.kt @@ -160,7 +160,7 @@ internal fun makeCompiledScript( val resultField = with(generationState.scriptSpecific) { if (resultFieldName == null) null - else resultFieldName!! to KotlinType(resultTypeString ?: DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(resultType!!)) + else resultFieldName!! to KotlinType(DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(resultType!!)) } return makeOtherScripts(ktScript).onSuccess { otherScripts -> diff --git a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/GenericReplCompiler.kt b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/GenericReplCompiler.kt index 29abb967a6e..a6a573bd0b7 100644 --- a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/GenericReplCompiler.kt +++ b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/GenericReplCompiler.kt @@ -119,7 +119,7 @@ open class GenericReplCompiler( classes, generationState.scriptSpecific.resultFieldName != null, classpathAddendum ?: emptyList(), - generationState.scriptSpecific.resultTypeString ?: generationState.scriptSpecific.resultType?.let { + generationState.scriptSpecific.resultType?.let { DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(it) }, null diff --git a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/ReplFromTerminal.kt b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/ReplFromTerminal.kt index ce6cebc8053..9cd7a96446d 100644 --- a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/ReplFromTerminal.kt +++ b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/ReplFromTerminal.kt @@ -13,6 +13,7 @@ import org.jetbrains.kotlin.cli.common.repl.ReplEvalResult import org.jetbrains.kotlin.cli.common.repl.replUnescapeLineBreaks import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.jetbrains.kotlin.descriptors.runtime.components.tryLoadClass import org.jetbrains.kotlin.scripting.compiler.plugin.repl.configuration.ConsoleReplConfiguration import org.jetbrains.kotlin.scripting.compiler.plugin.repl.configuration.IdeReplConfiguration import org.jetbrains.kotlin.scripting.compiler.plugin.repl.configuration.ReplConfiguration @@ -99,13 +100,43 @@ class ReplFromTerminal( } } + private fun tryInterpretResultAsValueClass(evalResult: ReplEvalResult.ValueResult): String? { + // since value classes are inlined, simple evalResult.value.toString() may provide "incorrect" results (see e.g. #KT-45065) + // so we're trying to restore original type by the type name stored in the evalResult.type + val resultClass = evalResult.value?.javaClass + val resultClassTypeName = resultClass?.typeName ?: return null + val expectedType = evalResult.type?.substringBefore('<') ?: return null + if (expectedType == resultClassTypeName) return null + val expectedTypesPossiblyInner = generateSequence(expectedType) { + val lastDot = it.lastIndexOf('.') + if (lastDot > 0) buildString { + append(it.substring(0, lastDot)) + append('$') + append(it.substring(lastDot + 1)) + } else null + } + val classLoader = evalResult.snippetInstance?.javaClass?.classLoader + ?: resultClass.classLoader + ?: ReplFromTerminal::class.java.classLoader + val expectedClass = expectedTypesPossiblyInner.firstNotNullOfOrNull { classLoader.tryLoadClass(it) } ?: return null + val boxMethod = expectedClass.declaredMethods.find { ctor -> + ctor.name == "box-impl" + } ?: return null + return try { + val valueString = boxMethod.invoke(null, evalResult.value).toString() + "${evalResult.name}: ${evalResult.type} = $valueString" + } catch (e: Throwable) { + null + } + } + private fun eval(line: String): ReplEvalResult { val evalResult = replInterpreter.eval(line) when (evalResult) { is ReplEvalResult.ValueResult, is ReplEvalResult.UnitResult -> { writer.notifyCommandSuccess() if (evalResult is ReplEvalResult.ValueResult) { - writer.outputCommandResult(evalResult.toString()) + writer.outputCommandResult(tryInterpretResultAsValueClass(evalResult) ?: evalResult.toString()) } } is ReplEvalResult.Error.Runtime -> writer.outputRuntimeError(evalResult.message) diff --git a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/ReplInterpreter.kt b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/ReplInterpreter.kt index 7f54cf02e9b..33c46b02bba 100644 --- a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/ReplInterpreter.kt +++ b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/repl/ReplInterpreter.kt @@ -189,7 +189,7 @@ class ReplInterpreter( is ResultWithDiagnostics.Success -> { when (val evalValue = evalResult.value.get().result) { is ResultValue.Unit -> ReplEvalResult.UnitResult() - is ResultValue.Value -> ReplEvalResult.ValueResult(evalValue.name, evalValue.value, evalValue.type) + is ResultValue.Value -> ReplEvalResult.ValueResult(evalValue.name, evalValue.value, evalValue.type, evalValue.scriptInstance) is ResultValue.Error -> ReplEvalResult.Error.Runtime(evalValue.renderError()) else -> ReplEvalResult.Error.Runtime("Error: snippet is not evaluated") }