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
This commit is contained in:
@@ -309,7 +309,6 @@ class GenerationState private constructor(
|
|||||||
|
|
||||||
// and the rest is an output from the codegen
|
// and the rest is an output from the codegen
|
||||||
var resultFieldName: String? = null
|
var resultFieldName: String? = null
|
||||||
var resultTypeString: String? = null
|
|
||||||
var resultType: KotlinType? = null
|
var resultType: KotlinType? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ interface ReplEvalAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed class ReplEvalResult : Serializable {
|
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 {
|
override fun toString(): String {
|
||||||
val v = if (value is Function<*>) "<function${TypeIntrinsics.getFunctionArity(value)}>" else value
|
val v = if (value is Function<*>) "<function${TypeIntrinsics.getFunctionArity(value)}>" else value
|
||||||
return "$name: $type = $v"
|
return "$name: $type = $v"
|
||||||
|
|||||||
+2
-1
@@ -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.IrAnonymousInitializerImpl
|
||||||
import org.jetbrains.kotlin.ir.declarations.impl.IrClassImpl
|
import org.jetbrains.kotlin.ir.declarations.impl.IrClassImpl
|
||||||
import org.jetbrains.kotlin.ir.declarations.impl.IrExternalPackageFragmentImpl
|
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.*
|
||||||
import org.jetbrains.kotlin.ir.expressions.impl.IrClassReferenceImpl
|
import org.jetbrains.kotlin.ir.expressions.impl.IrClassReferenceImpl
|
||||||
import org.jetbrains.kotlin.ir.expressions.impl.IrGetFieldImpl
|
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 ->
|
irScript.resultProperty?.owner?.let { irResultProperty ->
|
||||||
context.state.scriptSpecific.resultFieldName = irResultProperty.name.identifier
|
context.state.scriptSpecific.resultFieldName = irResultProperty.name.identifier
|
||||||
context.state.scriptSpecific.resultTypeString = irResultProperty.backingField?.type?.render()
|
context.state.scriptSpecific.resultType = irResultProperty.backingField?.type?.toIrBasedKotlinType()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,36 +5,34 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.cli
|
package org.jetbrains.kotlin.cli
|
||||||
|
|
||||||
import com.intellij.openapi.util.SystemInfo
|
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
|
import org.jetbrains.kotlin.cli.common.CompilerSystemProperties
|
||||||
import org.jetbrains.kotlin.test.TestCaseWithTmpdir
|
import org.jetbrains.kotlin.test.TestCaseWithTmpdir
|
||||||
import org.jetbrains.kotlin.utils.PathUtil
|
import org.jetbrains.kotlin.utils.PathUtil
|
||||||
import java.io.File
|
import org.junit.Assert
|
||||||
import java.io.InputStream
|
import java.io.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
class LauncherReplTest : TestCaseWithTmpdir() {
|
class LauncherReplTest : TestCaseWithTmpdir() {
|
||||||
|
|
||||||
private fun runInteractive(
|
private fun runInteractive(
|
||||||
executableName: String,
|
vararg inputsToExpectedOutputs: Pair<String?, String>,
|
||||||
vararg inputs: String,
|
|
||||||
expectedOutPatterns: List<String> = emptyList(),
|
|
||||||
expectedExitCode: Int = 0,
|
expectedExitCode: Int = 0,
|
||||||
workDirectory: File? = null
|
workDirectory: File? = null
|
||||||
) {
|
) {
|
||||||
val executableFileName = if (SystemInfo.isWindows) "$executableName.bat" else executableName
|
val javaExecutable = File(File(CompilerSystemProperties.JAVA_HOME.safeValue, "bin"), "java")
|
||||||
val launcherFile = File(PathUtil.kotlinPathsForDistDirectory.homePath, "bin/$executableFileName")
|
|
||||||
assertTrue("Launcher script not found, run dist task: ${launcherFile.absolutePath}", launcherFile.exists())
|
|
||||||
|
|
||||||
val processBuilder = ProcessBuilder(launcherFile.absolutePath)
|
val processBuilder = ProcessBuilder(
|
||||||
|
javaExecutable.absolutePath,
|
||||||
|
"-jar",
|
||||||
|
File(PathUtil.kotlinPathsForDistDirectory.homePath, "lib/kotlin-compiler.jar").absolutePath,
|
||||||
|
)
|
||||||
if (workDirectory != null) {
|
if (workDirectory != null) {
|
||||||
processBuilder.directory(workDirectory)
|
processBuilder.directory(workDirectory)
|
||||||
}
|
}
|
||||||
val process = processBuilder.start()
|
val process = processBuilder.start()
|
||||||
|
|
||||||
val inputIter = inputs.iterator()
|
|
||||||
|
|
||||||
data class ExceptionContainer(
|
data class ExceptionContainer(
|
||||||
var value: Throwable? = null
|
var value: Throwable? = null
|
||||||
)
|
)
|
||||||
@@ -43,9 +41,14 @@ class LauncherReplTest : TestCaseWithTmpdir() {
|
|||||||
val out = ArrayList<String>()
|
val out = ArrayList<String>()
|
||||||
val exceptionContainer = ExceptionContainer()
|
val exceptionContainer = ExceptionContainer()
|
||||||
val thread = thread {
|
val thread = thread {
|
||||||
|
val promptRegex = Regex("(?:\\u001B\\p{Graph}+)*(?:>>>|\\.\\.\\.)")
|
||||||
try {
|
try {
|
||||||
reader().forEachLine {
|
reader().forEachLine { rawLine ->
|
||||||
out.add(it.trim())
|
promptRegex.split(rawLine).forEach { line ->
|
||||||
|
if (line.isNotEmpty()) {
|
||||||
|
out.add(line.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
exceptionContainer.value = e
|
exceptionContainer.value = e
|
||||||
@@ -57,19 +60,12 @@ class LauncherReplTest : TestCaseWithTmpdir() {
|
|||||||
val (stdoutThread, stdoutException, processOut) = process.inputStream.captureStream()
|
val (stdoutThread, stdoutException, processOut) = process.inputStream.captureStream()
|
||||||
val (stderrThread, stderrException, processErr) = process.errorStream.captureStream()
|
val (stderrThread, stderrException, processErr) = process.errorStream.captureStream()
|
||||||
|
|
||||||
|
val inputIter = inputsToExpectedOutputs.iterator()
|
||||||
var stdinException: Throwable? = null
|
var stdinException: Throwable? = null
|
||||||
val stdinThread =
|
val stdinThread =
|
||||||
thread {
|
thread {
|
||||||
try {
|
try {
|
||||||
val writer = process.outputStream.writer()
|
writeInputsToOutStream(process.outputStream, inputIter)
|
||||||
val eol = System.getProperty("line.separator")
|
|
||||||
while (inputIter.hasNext()) {
|
|
||||||
with(writer) {
|
|
||||||
write(inputIter.next())
|
|
||||||
write(eol)
|
|
||||||
flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
stdinException = e
|
stdinException = e
|
||||||
}
|
}
|
||||||
@@ -90,15 +86,7 @@ class LauncherReplTest : TestCaseWithTmpdir() {
|
|||||||
TestCase.assertNull(stderrException.value)
|
TestCase.assertNull(stderrException.value)
|
||||||
TestCase.assertFalse("stdin thread not finished", stdinThread.isAlive)
|
TestCase.assertFalse("stdin thread not finished", stdinThread.isAlive)
|
||||||
TestCase.assertNull(stdinException)
|
TestCase.assertNull(stdinException)
|
||||||
TestCase.assertEquals(expectedOutPatterns.size, processOut.size)
|
assertOutputMatches(inputsToExpectedOutputs, processOut)
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TestCase.assertEquals(expectedExitCode, process.exitValue())
|
TestCase.assertEquals(expectedExitCode, process.exitValue())
|
||||||
TestCase.assertFalse(inputIter.hasNext())
|
TestCase.assertFalse(inputIter.hasNext())
|
||||||
|
|
||||||
@@ -110,17 +98,76 @@ class LauncherReplTest : TestCaseWithTmpdir() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun writeInputsToOutStream(dataOutStream: OutputStream, inputIter: Iterator<Pair<String?, String>>) {
|
||||||
|
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<out Pair<String?, String>>,
|
||||||
|
actualOut: List<String>
|
||||||
|
) {
|
||||||
|
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() {
|
fun testSimpleRepl() {
|
||||||
runInteractive(
|
runInteractive(
|
||||||
"kotlinc",
|
*replOutHeader,
|
||||||
"println(42)",
|
"println(42)" to "42",
|
||||||
":quit",
|
)
|
||||||
expectedOutPatterns = listOf(
|
}
|
||||||
"Welcome to Kotlin version .*",
|
|
||||||
"Type :help for help, :quit for quit",
|
fun testReplResultFormatting() {
|
||||||
".*42$",
|
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<kotlin\\.String> = 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\\)",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -160,7 +160,7 @@ internal fun makeCompiledScript(
|
|||||||
|
|
||||||
val resultField = with(generationState.scriptSpecific) {
|
val resultField = with(generationState.scriptSpecific) {
|
||||||
if (resultFieldName == null) null
|
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 ->
|
return makeOtherScripts(ktScript).onSuccess { otherScripts ->
|
||||||
|
|||||||
+1
-1
@@ -119,7 +119,7 @@ open class GenericReplCompiler(
|
|||||||
classes,
|
classes,
|
||||||
generationState.scriptSpecific.resultFieldName != null,
|
generationState.scriptSpecific.resultFieldName != null,
|
||||||
classpathAddendum ?: emptyList(),
|
classpathAddendum ?: emptyList(),
|
||||||
generationState.scriptSpecific.resultTypeString ?: generationState.scriptSpecific.resultType?.let {
|
generationState.scriptSpecific.resultType?.let {
|
||||||
DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(it)
|
DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(it)
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
|
|||||||
+32
-1
@@ -13,6 +13,7 @@ import org.jetbrains.kotlin.cli.common.repl.ReplEvalResult
|
|||||||
import org.jetbrains.kotlin.cli.common.repl.replUnescapeLineBreaks
|
import org.jetbrains.kotlin.cli.common.repl.replUnescapeLineBreaks
|
||||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||||
import org.jetbrains.kotlin.config.KotlinCompilerVersion
|
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.ConsoleReplConfiguration
|
||||||
import org.jetbrains.kotlin.scripting.compiler.plugin.repl.configuration.IdeReplConfiguration
|
import org.jetbrains.kotlin.scripting.compiler.plugin.repl.configuration.IdeReplConfiguration
|
||||||
import org.jetbrains.kotlin.scripting.compiler.plugin.repl.configuration.ReplConfiguration
|
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 {
|
private fun eval(line: String): ReplEvalResult {
|
||||||
val evalResult = replInterpreter.eval(line)
|
val evalResult = replInterpreter.eval(line)
|
||||||
when (evalResult) {
|
when (evalResult) {
|
||||||
is ReplEvalResult.ValueResult, is ReplEvalResult.UnitResult -> {
|
is ReplEvalResult.ValueResult, is ReplEvalResult.UnitResult -> {
|
||||||
writer.notifyCommandSuccess()
|
writer.notifyCommandSuccess()
|
||||||
if (evalResult is ReplEvalResult.ValueResult) {
|
if (evalResult is ReplEvalResult.ValueResult) {
|
||||||
writer.outputCommandResult(evalResult.toString())
|
writer.outputCommandResult(tryInterpretResultAsValueClass(evalResult) ?: evalResult.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ReplEvalResult.Error.Runtime -> writer.outputRuntimeError(evalResult.message)
|
is ReplEvalResult.Error.Runtime -> writer.outputRuntimeError(evalResult.message)
|
||||||
|
|||||||
+1
-1
@@ -189,7 +189,7 @@ class ReplInterpreter(
|
|||||||
is ResultWithDiagnostics.Success -> {
|
is ResultWithDiagnostics.Success -> {
|
||||||
when (val evalValue = evalResult.value.get().result) {
|
when (val evalValue = evalResult.value.get().result) {
|
||||||
is ResultValue.Unit -> ReplEvalResult.UnitResult()
|
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())
|
is ResultValue.Error -> ReplEvalResult.Error.Runtime(evalValue.renderError())
|
||||||
else -> ReplEvalResult.Error.Runtime("Error: snippet is not evaluated")
|
else -> ReplEvalResult.Error.Runtime("Error: snippet is not evaluated")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user