diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/ExpressionCodegen.java b/compiler/backend/src/org/jetbrains/kotlin/codegen/ExpressionCodegen.java index 878e732ddfe..7d5fb0e18dc 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/ExpressionCodegen.java +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/ExpressionCodegen.java @@ -364,23 +364,27 @@ public class ExpressionCodegen extends KtVisitor impleme private void putStackValue(@Nullable KtElement expr, @NotNull Type type, @Nullable KotlinType kotlinType, @NotNull StackValue value) { // for repl store the result of the last line into special field - if (value.type != Type.VOID_TYPE && state.getReplSpecific().getShouldGenerateScriptResultValue()) { + if (value.type != Type.VOID_TYPE) { ScriptContext context = getScriptContext(); - if (expr == context.getLastStatement()) { - StackValue.Field resultValue = StackValue.field(context.getResultFieldInfo(), StackValue.LOCAL_0); - resultValue.store(value, v); - state.getReplSpecific().setHasResult(true); - return; + if (context != null && expr == context.getLastStatement()) { + FieldInfo resultFieldInfo = context.getResultFieldInfo(); + if (resultFieldInfo != null) { + StackValue.Field resultValue = StackValue.field(resultFieldInfo, StackValue.LOCAL_0); + resultValue.store(value, v); + state.getScriptSpecific().setResultType(resultFieldInfo.getFieldKotlinType()); + state.getScriptSpecific().setResultFieldName(resultFieldInfo.getFieldName()); + return; + } } } value.put(type, kotlinType, v); } - @NotNull + @Nullable private ScriptContext getScriptContext() { CodegenContext context = getContext(); - while (!(context instanceof ScriptContext)) { + while (context != null && !(context instanceof ScriptContext)) { context = context.getParentContext(); } return (ScriptContext) context; diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/ScriptCodegen.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/ScriptCodegen.kt index b8b87b86214..5c8cf9e3a4b 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/ScriptCodegen.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/ScriptCodegen.kt @@ -23,7 +23,6 @@ import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassOrAny import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperInterfaces import org.jetbrains.kotlin.resolve.jvm.AsmTypes import org.jetbrains.kotlin.resolve.jvm.AsmTypes.OBJECT_TYPE -import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin.Companion.NO_ORIGIN import org.jetbrains.kotlin.resolve.jvm.diagnostics.OtherOrigin import org.jetbrains.kotlin.serialization.DescriptorSerializer @@ -86,14 +85,14 @@ class ScriptCodegen private constructor( ) val asmMethod = jvmSignature.asmMethod - if (state.replSpecific.shouldGenerateScriptResultValue) { - val resultFieldInfo = scriptContext.resultFieldInfo + scriptContext.resultFieldInfo?.let { resultFieldInfo -> classBuilder.newField( - JvmDeclarationOrigin.NO_ORIGIN, - ACC_PUBLIC or ACC_FINAL, - resultFieldInfo.fieldName, - resultFieldInfo.fieldType.descriptor, - null, null) + NO_ORIGIN, + ACC_PUBLIC or ACC_FINAL, + resultFieldInfo.fieldName, + resultFieldInfo.fieldType.descriptor, + null, null + ) } val mv = classBuilder.newMethod( @@ -277,7 +276,7 @@ class ScriptCodegen private constructor( val builder = state.factory.newVisitor( OtherOrigin(declaration, scriptDescriptor), classType, declaration.containingFile) - val earlierScripts = state.replSpecific.earlierScriptsForReplInterpreter + val earlierScripts = state.scriptSpecific.earlierScriptsForReplInterpreter val scriptContext = parentContext.intoScript( scriptDescriptor, diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/context/ScriptContext.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/context/ScriptContext.kt index 8e7f491626b..83da50fd224 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/context/ScriptContext.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/context/ScriptContext.kt @@ -42,15 +42,15 @@ class ScriptContext( ) : ScriptLikeContext(typeMapper, contextDescriptor, parentContext) { val lastStatement: KtExpression? - val resultFieldInfo: FieldInfo + val resultFieldInfo: FieldInfo? get() { - assert(state.replSpecific.shouldGenerateScriptResultValue) { "Should not be called unless 'resultFieldName' is set" } - val scriptResultFieldName = state.replSpecific.scriptResultFieldName!! - val fieldType = state.replSpecific.resultType?.let { state.typeMapper.mapType(it) } ?: AsmTypes.OBJECT_TYPE + val resultValue = scriptDescriptor.resultValue ?: return null + val scriptResultFieldName = resultValue.name.identifier + val fieldType = resultValue.returnType?.let { state.typeMapper.mapType(it) } ?: AsmTypes.OBJECT_TYPE return FieldInfo.createForHiddenField( state.typeMapper.mapClass(scriptDescriptor), fieldType, - state.replSpecific.resultType, + resultValue.returnType, scriptResultFieldName ) } diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/SourceCompilerForInline.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/SourceCompilerForInline.kt index 5f3baeb7d13..73ff8e4680f 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/SourceCompilerForInline.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/inline/SourceCompilerForInline.kt @@ -391,7 +391,7 @@ class PsiSourceCompilerForInline(private val codegen: ExpressionCodegen, overrid return when (descriptor) { is ScriptDescriptor -> { - val earlierScripts = state.replSpecific.earlierScriptsForReplInterpreter + val earlierScripts = state.scriptSpecific.earlierScriptsForReplInterpreter containerContext.intoScript( descriptor, earlierScripts ?: emptyList(), 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 d66cf6a0bc8..5f2ff365599 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/state/GenerationState.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/state/GenerationState.kt @@ -214,15 +214,15 @@ class GenerationState private constructor( val factory: ClassFileFactory private lateinit var duplicateSignatureFactory: BuilderFactoryForDuplicateSignatureDiagnostics - val replSpecific = ForRepl() + val scriptSpecific = ForScript() - //TODO: should be refactored out - class ForRepl { + // TODO: review usages and consider replace mutability with explicit passing of input and output + class ForScript { + // quite a mess, this one is an input from repl interpreter var earlierScriptsForReplInterpreter: List? = null - var scriptResultFieldName: String? = null - val shouldGenerateScriptResultValue: Boolean get() = scriptResultFieldName != null + // and the rest is an output from the codegen + var resultFieldName: String? = null var resultType: KotlinType? = null - var hasResult: Boolean = false } val isCallAssertionsDisabled: Boolean = configuration.getBoolean(JVMConfigurationKeys.DISABLE_CALL_ASSERTIONS) diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplEvaluator.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplEvaluator.kt index 162799db033..aef8fd0526f 100644 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplEvaluator.kt +++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplEvaluator.kt @@ -108,12 +108,16 @@ open class GenericReplEvaluator( historyActor.addFinal(compileResult.lineId, EvalClassWithInstanceAndLoader(scriptClass.kotlin, scriptInstance, classLoader, invokeWrapper)) - val resultFieldName = scriptResultFieldName(compileResult.lineId.no) - val resultField = scriptClass.getDeclaredField(resultFieldName).apply { isAccessible = true } - val resultValue: Any? = resultField.get(scriptInstance) + return if (compileResult.hasResult) { + val resultFieldName = scriptResultFieldName(compileResult.lineId.no) + val resultField = scriptClass.declaredFields.find { it.name == resultFieldName }?.apply { isAccessible = true } + assert(resultField != null) { "compileResult.hasResult == true but resultField is null" } + val resultValue: Any? = resultField!!.get(scriptInstance) - return if (compileResult.hasResult) ReplEvalResult.ValueResult(resultFieldName, resultValue, compileResult.type) - else ReplEvalResult.UnitResult() + ReplEvalResult.ValueResult(resultFieldName, resultValue, compileResult.type) + } else { + ReplEvalResult.UnitResult() + } } } } diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt index fe3de5bd9b3..47c0eab97ad 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt @@ -7,7 +7,9 @@ package kotlin.script.experimental.api +import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream import java.io.File +import java.io.PrintStream /** * The single script diagnostic report @@ -28,11 +30,28 @@ data class ScriptDiagnostic( */ enum class Severity { FATAL, ERROR, WARNING, INFO, DEBUG } - override fun toString(): String = buildString { - append(severity.name) - append(' ') + override fun toString(): String = render() + + /** + * Render diagnostics message as a string in a form: + * "[SEVERITY ]message[ (file:line:column)][: exception message[\n exception stacktrace]]" + * @param withSeverity add severity prefix, true by default + * @param withLocation add error location in the compiled script, if present, true by default + * @param withException add exception message, if present, true by default + * @param withStackTrace add exception stacktrace, if exception is present and [withException] is true, false by default + */ + fun render( + withSeverity: Boolean = true, + withLocation: Boolean = true, + withException: Boolean = true, + withStackTrace: Boolean = false + ): String = buildString { + if (withSeverity) { + append(severity.name) + append(' ') + } append(message) - if (sourcePath != null || location != null) { + if (withLocation && (sourcePath != null || location != null)) { append(" (") sourcePath?.let { append(it.substringAfterLast(File.separatorChar)) } location?.let { @@ -43,9 +62,18 @@ data class ScriptDiagnostic( } append(')') } - if (exception != null) { + if (withException && exception != null) { append(": ") append(exception) + if (withStackTrace) { + ByteOutputStream().use { os -> + val ps = PrintStream(os) + exception.printStackTrace(ps) + ps.flush() + append("\n") + append(os.toString()) + } + } } } } diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/replData.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/replData.kt index 126b1697972..f5008ee8ef7 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/replData.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/replData.kt @@ -6,6 +6,7 @@ package kotlin.script.experimental.api import java.io.Serializable +import kotlin.script.experimental.util.PropertiesCollection const val REPL_SNIPPET_FIRST_NO = 1 const val REPL_SNIPPET_FIRST_GEN = 1 @@ -29,3 +30,21 @@ data class ReplSnippetIdImpl(override val no: Int, override val generation: Int, private val serialVersionUID: Long = 1L } } + +interface ReplScriptCompilationConfigurationKeys + +open class ReplScriptCompilationConfigurationBuilder : PropertiesCollection.Builder(), + ReplScriptCompilationConfigurationKeys { + companion object : ReplScriptCompilationConfigurationKeys +} + +val ScriptCompilationConfigurationKeys.repl + get() = ReplScriptCompilationConfigurationBuilder() + + +/** + * The prefix of the name of the generated script class field to assign the snipped results to, empty means disabled + * see also ScriptCompilationConfigurationKeys.resultField + */ +val ReplScriptCompilationConfigurationKeys.resultFieldPrefix by PropertiesCollection.key("res") + diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt index 50942839f23..f70b9c16401 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt @@ -99,6 +99,12 @@ val ScriptCompilationConfigurationKeys.defaultImports by PropertiesCollection.ke */ val ScriptCompilationConfigurationKeys.importScripts by PropertiesCollection.key>() +/** + * The name of the generated script class field to assign the script results to, empty means disabled + * see also ReplScriptCompilationConfigurationKeys.resultFieldPrefix + */ +val ScriptCompilationConfigurationKeys.resultField by PropertiesCollection.key("\$\$result") + /** * The list of script dependencies - platform specific */ diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptEvaluation.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptEvaluation.kt index cb761a0ff55..7f7c6220c44 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptEvaluation.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptEvaluation.kt @@ -121,17 +121,40 @@ data class RefineEvaluationConfigurationData( /** * The script evaluation result value */ -sealed class ResultValue { - class Value(val name: String, val value: Any?, val type: String, val scriptInstance: Any) : ResultValue() { +sealed class ResultValue(val scriptInstance: Any? = null) { + + /** + * The result value representing a script return value - the value of the last expression in the script + * @param name assigned name of the result field - used e.g. in REPL + * @param value actual result value + * @param type name of the result type + * @param scriptInstance instance of the script class + */ + class Value(val name: String, val value: Any?, val type: String, scriptInstance: Any) : ResultValue(scriptInstance) { override fun toString(): String = "$name: $type = $value" } - class UnitValue(val scriptInstance: Any) : ResultValue() { + /** + * The result value representing unit result, e.g. when the script ends with a statement + * @param scriptInstance instance of the script class + */ + class Unit(scriptInstance: Any) : ResultValue(scriptInstance) { override fun toString(): String = "Unit" } - // TODO: obsolete it, use differently named value in the saving evaluators - object Unit : ResultValue() + /** + * The result value representing an exception from script itself + * @param error the actual exception thrown on script evaluation + * @param wrappingException the wrapping exception e.g. InvocationTargetException, sometimes useful for calculating the relevant stacktrace + */ + class Error(val error: Throwable, val wrappingException: Throwable? = null) : ResultValue() { + override fun toString(): String = error.toString() + } + + /** + * The result value used in non-evaluating "evaluators" + */ + object NotEvaluated : ResultValue() } /** diff --git a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ReplTest.kt b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ReplTest.kt index 8d298418d41..cbd3756e408 100644 --- a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ReplTest.kt +++ b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ReplTest.kt @@ -27,7 +27,7 @@ class ReplTest : TestCase() { @Test fun testCompileAndEval() { val out = captureOut { - chechEvaluateInReplNoErrors( + chechEvaluateInRepl( simpleScriptompilationConfiguration, simpleScriptEvaluationConfiguration, sequenceOf( @@ -41,6 +41,32 @@ class ReplTest : TestCase() { Assert.assertEquals("x = 3", out) } + @Test + fun testEvalWithResult() { + chechEvaluateInRepl( + simpleScriptompilationConfiguration, + simpleScriptEvaluationConfiguration, + sequenceOf( + "val x = 5", + "x + 6", + "res1 * 2" + ), + sequenceOf(null, 11, 22) + ) + } + + @Test + fun testEvalWithError() { + chechEvaluateInRepl( + simpleScriptompilationConfiguration, + simpleScriptEvaluationConfiguration, + sequenceOf( + "throw RuntimeException(\"abc\")" + ), + sequenceOf(RuntimeException("abc")) + ) + } + fun evaluateInRepl( compilationConfiguration: ScriptCompilationConfiguration, evaluationConfiguration: ScriptEvaluationConfiguration, @@ -62,15 +88,12 @@ class ReplTest : TestCase() { } } .onSuccess { - val snippetInstance = when (val retVal = it.returnValue) { - is ResultValue.Value -> retVal.scriptInstance - is ResultValue.UnitValue -> retVal.scriptInstance - else -> throw IllegalStateException("Expecting value with script instance, got $it") - } - currentEvalConfig = ScriptEvaluationConfiguration(currentEvalConfig) { - previousSnippets.append(snippetInstance) - jvm { - baseClassLoader(snippetInstance::class.java.classLoader) + it.returnValue.scriptInstance?.let { snippetInstance -> + currentEvalConfig = ScriptEvaluationConfiguration(currentEvalConfig) { + previousSnippets.append(snippetInstance) + jvm { + baseClassLoader(snippetInstance::class.java.classLoader) + } } } it.asSuccess() @@ -78,7 +101,7 @@ class ReplTest : TestCase() { } } - fun chechEvaluateInReplNoErrors( + fun chechEvaluateInRepl( compilationConfiguration: ScriptCompilationConfiguration, evaluationConfiguration: ScriptEvaluationConfiguration, snippets: Sequence, @@ -90,11 +113,19 @@ class ReplTest : TestCase() { is ResultWithDiagnostics.Failure -> Assert.fail("#$index: Expected result, got $res") is ResultWithDiagnostics.Success -> { val expectedVal = expectedIter.next() - val resVal = res.value.returnValue - if (resVal is ResultValue.Value && resVal.type.isNotBlank()) // TODO: the latter check is temporary while the result is used to return the instance too - Assert.assertEquals("#$index: Expected $expectedVal, got $resVal", expectedVal, resVal.value) - else - Assert.assertTrue("#$index: Expected $expectedVal, got Unit", expectedVal == null) + when (val resVal = res.value.returnValue) { + is ResultValue.Value -> Assert.assertEquals( + "#$index: Expected $expectedVal, got $resVal", + expectedVal, + resVal.value + ) + is ResultValue.Unit -> Assert.assertTrue("#$index: Expected $expectedVal, got Unit", expectedVal == null) + is ResultValue.Error -> Assert.assertTrue( + "#$index: Expected $expectedVal, got Error: ${resVal.error}", + expectedVal is Throwable && expectedVal.message == resVal.error.message + ) + else -> Assert.assertTrue("#$index: Expected $expectedVal, got unknown result $resVal", expectedVal == null) + } } } } diff --git a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt index 37279b995ae..83db8965fcd 100644 --- a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt +++ b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt @@ -15,6 +15,7 @@ import org.jetbrains.org.objectweb.asm.Opcodes import org.junit.Assert import org.junit.Test import java.io.* +import java.lang.RuntimeException import java.net.URLClassLoader import java.nio.file.Files import java.security.MessageDigest @@ -51,6 +52,41 @@ class ScriptingHostTest : TestCase() { Assert.assertEquals(greeting, output2) } + @Test + fun testValueResult() { + val resVal = evalScriptWithResult("42") as ResultValue.Value + Assert.assertEquals(42, resVal.value) + Assert.assertEquals("\$\$result", resVal.name) + Assert.assertEquals("kotlin.Int", resVal.type) + val resField = resVal.scriptInstance!!::class.java.getDeclaredField("\$\$result") + Assert.assertEquals(42, resField.get(resVal.scriptInstance!!)) + } + + @Test + fun testUnitResult() { + val resVal = evalScriptWithResult("val x = 42") + Assert.assertTrue(resVal is ResultValue.Unit) + } + + @Test + fun testErrorResult() { + val resVal = evalScriptWithResult("throw RuntimeException(\"abc\")") + Assert.assertTrue(resVal is ResultValue.Error) + val resValError = (resVal as ResultValue.Error).error + Assert.assertTrue(resValError is RuntimeException) + Assert.assertEquals("abc", resValError.message) + } + + @Test + fun testCustomResultField() { + val resVal = evalScriptWithResult("42") { + resultField("outcome") + } as ResultValue.Value + Assert.assertEquals("outcome", resVal.name) + val resField = resVal.scriptInstance!!::class.java.getDeclaredField("outcome") + Assert.assertEquals(42, resField.get(resVal.scriptInstance!!)) + } + @Test fun testSaveToClasses() { val greeting = "Hello from script classes!" @@ -366,7 +402,8 @@ class ScriptingHostTest : TestCase() { val evaluator = BasicJvmScriptEvaluator() val host = BasicJvmScriptingHost(compiler = compiler, evaluator = evaluator) - val scriptCompilationConfiguration = createJvmCompilationConfigurationFromTemplate(body = configurationBuilder) + val scriptCompilationConfiguration = + createJvmCompilationConfigurationFromTemplate(body = configurationBuilder) Assert.assertEquals(0, cache.storedScripts) var compiledScript: CompiledScript<*>? = null @@ -453,6 +490,13 @@ fun ResultWithDiagnostics.throwOnFailure(): ResultWithDiagnostics = ap private fun evalScript(script: String, host: BasicScriptingHost = BasicJvmScriptingHost()): ResultWithDiagnostics<*> = evalScriptWithConfiguration(script, host) +private fun evalScriptWithResult( + script: String, + host: BasicScriptingHost = BasicJvmScriptingHost(), + body: ScriptCompilationConfiguration.Builder.() -> Unit = {} +): ResultValue = + evalScriptWithConfiguration(script, host, body).throwOnFailure().valueOrNull()!!.returnValue + private fun evalScriptWithConfiguration( script: String, host: BasicScriptingHost = BasicJvmScriptingHost(), diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptSaving.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptSaving.kt index a1d0288d543..fa9207c35a4 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptSaving.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptSaving.kt @@ -41,7 +41,7 @@ open class BasicJvmScriptClassFilesGenerator(val outputDir: File) : ScriptEvalua writeBytes(bytes) } } - return ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Unit, scriptEvaluationConfiguration)) + return ResultWithDiagnostics.Success(EvaluationResult(ResultValue.NotEvaluated, scriptEvaluationConfiguration)) } catch (e: Throwable) { return ResultWithDiagnostics.Failure( e.asDiagnostics("Cannot generate script classes: ${e.message}", path = compiledScript.sourceLocationId) @@ -96,7 +96,7 @@ open class BasicJvmScriptJarGenerator(val outputJar: File) : ScriptEvaluator { if (compiledScript !is KJvmCompiledScript<*>) return failure("Cannot generate jar: unsupported compiled script type $compiledScript") compiledScript.saveToJar(outputJar) - return ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Unit, scriptEvaluationConfiguration)) + return ResultWithDiagnostics.Success(EvaluationResult(ResultValue.NotEvaluated, scriptEvaluationConfiguration)) } catch (e: Throwable) { return ResultWithDiagnostics.Failure( e.asDiagnostics("Cannot generate script jar: ${e.message}", path = compiledScript.sourceLocationId) diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/repl/legacyReplEvaluation.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/repl/legacyReplEvaluation.kt index 3e7dc8333f7..d29ef3c55fb 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/repl/legacyReplEvaluation.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/repl/legacyReplEvaluation.kt @@ -56,20 +56,24 @@ class JvmReplEvaluator( val res = runBlocking { scriptEvaluator(compiledScript, currentConfiguration) } when (res) { - is ResultWithDiagnostics.Success -> when (val retVal = res.value.returnValue) { - is ResultValue.Value -> { - history.replaceOrPush(compileResult.lineId, retVal.scriptInstance) - // TODO: the latter check is temporary while the result is used to return the instance too - if (retVal.type.isNotBlank()) + is ResultWithDiagnostics.Success -> { + when (val retVal = res.value.returnValue) { + is ResultValue.Error -> { + ReplEvalResult.Error.Runtime( + retVal.error.message ?: "unknown error", + (retVal.error as? Exception) ?: (retVal.wrappingException as? Exception) + ) + } + is ResultValue.Value -> { + history.replaceOrPush(compileResult.lineId, retVal.scriptInstance!!) ReplEvalResult.ValueResult(retVal.name, retVal.value, retVal.type) - else + } + is ResultValue.Unit -> { + history.replaceOrPush(compileResult.lineId, retVal.scriptInstance!!) ReplEvalResult.UnitResult() + } + else -> throw IllegalStateException("Unexpected snippet result value $retVal") } - is ResultValue.UnitValue -> { - history.replaceOrPush(compileResult.lineId, retVal.scriptInstance) - ReplEvalResult.UnitResult() - } - else -> throw IllegalStateException("Expecting value with script instance, got $retVal") } else -> ReplEvalResult.Error.Runtime( diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/BasicJvmScriptEvaluator.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/BasicJvmScriptEvaluator.kt index f74fbfc2bdb..95752935941 100644 --- a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/BasicJvmScriptEvaluator.kt +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/BasicJvmScriptEvaluator.kt @@ -5,6 +5,7 @@ package kotlin.script.experimental.jvm +import java.lang.reflect.InvocationTargetException import kotlin.reflect.KClass import kotlin.script.experimental.api.* import kotlin.script.experimental.jvm.impl.getConfigurationWithClassloader @@ -43,13 +44,18 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator { ?.valueOrNull() ?: configuration - val instance = - scriptClass.evalWithConfigAndOtherScriptsResults(refinedEvalConfiguration, importedScriptsEvalResults) + val resultValue = try { + val instance = + scriptClass.evalWithConfigAndOtherScriptsResults(refinedEvalConfiguration, importedScriptsEvalResults) - val resultValue = compiledScript.resultField?.let { (resultFieldName, resultType) -> - val resultField = scriptClass.java.getDeclaredField(resultFieldName).apply { isAccessible = true } - ResultValue.Value(resultFieldName, resultField.get(instance), resultType.typeName, instance) - } ?: ResultValue.Value("", instance, "", instance) + compiledScript.resultField?.let { (resultFieldName, resultType) -> + val resultField = scriptClass.java.getDeclaredField(resultFieldName).apply { isAccessible = true } + ResultValue.Value(resultFieldName, resultField.get(instance), resultType.typeName, instance) + } ?: ResultValue.Unit(instance) + + } catch (e: InvocationTargetException) { + ResultValue.Error(e.targetException ?: e, e) + } EvaluationResult(resultValue, refinedEvalConfiguration).let { sharedScripts?.put(scriptClass, it) @@ -59,10 +65,7 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator { } } catch (e: Throwable) { ResultWithDiagnostics.Failure( - e.asDiagnostics( - "Error evaluating script", - path = compiledScript.sourceLocationId - ) + e.asDiagnostics(path = compiledScript.sourceLocationId) ) } @@ -89,13 +92,12 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator { } importedScriptsEvalResults.forEach { - args.add((it.returnValue as ResultValue.Value).scriptInstance) + args.add(it.returnValue.scriptInstance) } val ctor = java.constructors.single() - val instance = ctor.newInstance(*args.toArray()) - return instance + return ctor.newInstance(*args.toArray()) } } diff --git a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/repl/GenericReplCompiler.kt b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/repl/GenericReplCompiler.kt index 1fe24a87366..7985fc7fd26 100644 --- a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/repl/GenericReplCompiler.kt +++ b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/repl/GenericReplCompiler.kt @@ -77,13 +77,16 @@ open class GenericReplCompiler( val analysisResult = compilerState.analyzerEngine.analyzeReplLine(psiFile, codeLine) AnalyzerWithCompilerReport.reportDiagnostics(analysisResult.diagnostics, errorHolder) val scriptDescriptor = when (analysisResult) { - is ReplCodeAnalyzer.ReplLineAnalysisResult.WithErrors -> return ReplCompileResult.Error(errorHolder.renderMessage()) - is ReplCodeAnalyzer.ReplLineAnalysisResult.Successful -> analysisResult.scriptDescriptor + is ReplCodeAnalyzer.ReplLineAnalysisResult.WithErrors -> { + return ReplCompileResult.Error(errorHolder.renderMessage()) + } + is ReplCodeAnalyzer.ReplLineAnalysisResult.Successful -> { + (analysisResult.scriptDescriptor as? ScriptDescriptor) + ?: error("Unexpected script descriptor type ${analysisResult.scriptDescriptor::class}") + } else -> error("Unexpected result ${analysisResult::class.java}") } - val type = (scriptDescriptor as ScriptDescriptor).resultValue?.returnType - val generationState = GenerationState.Builder( psiFile.project, ClassBuilderFactories.BINARIES, @@ -93,9 +96,7 @@ open class GenericReplCompiler( compilerConfiguration ).build() - generationState.replSpecific.resultType = type - generationState.replSpecific.scriptResultFieldName = scriptResultFieldName(codeLine.no) - generationState.replSpecific.earlierScriptsForReplInterpreter = compilerState.history.map { it.item } + generationState.scriptSpecific.earlierScriptsForReplInterpreter = compilerState.history.map { it.item } generationState.beforeCompile() KotlinCodegenFacade.generatePackage( generationState, @@ -114,9 +115,9 @@ open class GenericReplCompiler( compilerState.history.map { it.id }, generatedClassname, classes, - generationState.replSpecific.hasResult, + generationState.scriptSpecific.resultFieldName != null, classpathAddendum ?: emptyList(), - generationState.replSpecific.resultType?.let { + generationState.scriptSpecific.resultType?.let { DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(it) }, null diff --git a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/LazyScriptDescriptor.kt b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/LazyScriptDescriptor.kt index 98fb3e598f4..303cb5f7dc0 100644 --- a/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/LazyScriptDescriptor.kt +++ b/plugins/scripting/scripting-compiler-impl/src/org/jetbrains/kotlin/scripting/resolve/LazyScriptDescriptor.kt @@ -90,14 +90,23 @@ class LazyScriptDescriptor( } fun resultFieldName(): String? { - val scriptPriority = scriptInfo.script.getUserData(ScriptPriorities.PRIORITY_KEY) - if (scriptPriority != null) { - return "res$scriptPriority" + // TODO: implement robust REPL/script selection + val replSnippetId = + scriptInfo.script.getUserData(ScriptPriorities.PRIORITY_KEY)?.toString() + ?: run { + val scriptName = name.asString() + if (scriptName.startsWith("Line_")) + scriptName.split("_")[1] + else null + } + return if (replSnippetId != null) { + // assuming repl + scriptCompilationConfiguration()[ScriptCompilationConfiguration.repl.resultFieldPrefix]?.takeIf { it.isNotBlank() }?.let { + "$it$replSnippetId" + } + } else { + scriptCompilationConfiguration()[ScriptCompilationConfiguration.resultField]?.takeIf { it.isNotBlank() } } - val scriptName = name.asString() - return if (scriptName.startsWith("Line_")) { - "res${scriptName.split("_")[1]}" - } else "\$\$result" } private val sourceElement = scriptInfo.script.toSourceElement() diff --git a/plugins/scripting/scripting-compiler/build.gradle.kts b/plugins/scripting/scripting-compiler/build.gradle.kts index d6feec8c83c..0a52f37f51f 100644 --- a/plugins/scripting/scripting-compiler/build.gradle.kts +++ b/plugins/scripting/scripting-compiler/build.gradle.kts @@ -51,6 +51,8 @@ runtimeJar() sourcesJar() javadocJar() +testsJar() + projectTest { workingDir = rootDir } diff --git a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/JvmCliScriptEvaluationExtension.kt b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/JvmCliScriptEvaluationExtension.kt index 9f7ab703881..ed053f88129 100644 --- a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/JvmCliScriptEvaluationExtension.kt +++ b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/JvmCliScriptEvaluationExtension.kt @@ -14,7 +14,7 @@ import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments import org.jetbrains.kotlin.cli.common.config.addKotlinSourceRoot import org.jetbrains.kotlin.cli.common.extensions.ScriptEvaluationExtension -import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.ERROR +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.config.CompilerConfiguration @@ -23,9 +23,8 @@ import org.jetbrains.kotlin.scripting.compiler.plugin.impl.ScriptJvmCompilerFrom import org.jetbrains.kotlin.scripting.configuration.ScriptingConfigurationKeys import org.jetbrains.kotlin.scripting.definitions.ScriptDefinitionProvider import java.io.File -import kotlin.script.experimental.api.constructorArgs -import kotlin.script.experimental.api.valueOr -import kotlin.script.experimental.api.with +import java.io.PrintStream +import kotlin.script.experimental.api.* import kotlin.script.experimental.host.toScriptSource import kotlin.script.experimental.jvm.BasicJvmScriptEvaluator @@ -41,7 +40,7 @@ class JvmCliScriptEvaluationExtension : ScriptEvaluationExtension { val messageCollector = configuration.getNotNull(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY) val scriptDefinitionProvider = ScriptDefinitionProvider.getInstance(projectEnvironment.project) if (scriptDefinitionProvider == null) { - messageCollector.report(ERROR, "Unable to process the script, scripting plugin is not configured") + messageCollector.report(CompilerMessageSeverity.ERROR, "Unable to process the script, scripting plugin is not configured") return COMPILATION_ERROR } val sourcePath = arguments.freeArgs.first() @@ -56,7 +55,7 @@ class JvmCliScriptEvaluationExtension : ScriptEvaluationExtension { val extensionHint = if (configuration.get(ScriptingConfigurationKeys.SCRIPT_DEFINITIONS)?.let { it.size == 1 && it.first().isDefault } == true) " (.kts)" else "" - messageCollector.report(ERROR, "Specify path to the script file$extensionHint as the first argument") + messageCollector.report(CompilerMessageSeverity.ERROR, "Specify path to the script file$extensionHint as the first argument") return COMPILATION_ERROR } @@ -76,16 +75,56 @@ class JvmCliScriptEvaluationExtension : ScriptEvaluationExtension { return runBlocking { val compiledScript = scriptCompiler.compile(script, scriptCompilationConfiguration) - .valueOr { return@runBlocking COMPILATION_ERROR } - /*val evalResult = */ - BasicJvmScriptEvaluator().invoke(compiledScript, evaluationConfiguration) + .valueOr { + for (report in it.reports) { + messageCollector.report(report.severity.toCompilerMessageSeverity(), report.render(withSeverity = false)) + } + return@runBlocking COMPILATION_ERROR + } + val evalResult = BasicJvmScriptEvaluator().invoke(compiledScript, evaluationConfiguration) .valueOr { for (report in it.reports) { - messageCollector.report(ERROR, report.toString()) + messageCollector.report(report.severity.toCompilerMessageSeverity(), report.render(withSeverity = false)) } - return@runBlocking ExitCode.SCRIPT_EXECUTION_ERROR + return@runBlocking ExitCode.INTERNAL_ERROR } - ExitCode.OK + when (evalResult.returnValue) { + is ResultValue.Value -> { + println((evalResult.returnValue as ResultValue.Value).value) + ExitCode.OK + } + is ResultValue.Error -> { + val errorValue = evalResult.returnValue as ResultValue.Error + errorValue.renderError(System.err) + ExitCode.SCRIPT_EXECUTION_ERROR + } + else -> ExitCode.OK + } } } -} \ No newline at end of file +} + +private fun ScriptDiagnostic.Severity.toCompilerMessageSeverity(): CompilerMessageSeverity = + when (this) { + ScriptDiagnostic.Severity.FATAL -> CompilerMessageSeverity.EXCEPTION + ScriptDiagnostic.Severity.ERROR -> CompilerMessageSeverity.ERROR + ScriptDiagnostic.Severity.WARNING -> CompilerMessageSeverity.WARNING + ScriptDiagnostic.Severity.INFO -> CompilerMessageSeverity.INFO + ScriptDiagnostic.Severity.DEBUG -> CompilerMessageSeverity.LOGGING + } + +private fun ResultValue.Error.renderError(stream: PrintStream) { + val fullTrace = error.stackTrace + if (wrappingException == null || fullTrace.size < wrappingException!!.stackTrace.size) { + error.printStackTrace(stream) + } else { + // subtracting wrapping message stacktrace from error stacktrace to show only user-specific part of it + // TODO: consider more reliable logic, e.g. comparing traces, fallback to full error printing in case of mismatch + // TODO: write tests + stream.println(error) + val scriptTraceSize = fullTrace.size - wrappingException!!.stackTrace.size + for (i in 0 until scriptTraceSize) { + stream.println("\tat " + fullTrace[i]) + } + } +} diff --git a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/KJvmReplCompilerImpl.kt b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/KJvmReplCompilerImpl.kt index 2fb1a4c056f..d39a0b884da 100644 --- a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/KJvmReplCompilerImpl.kt +++ b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/KJvmReplCompilerImpl.kt @@ -13,7 +13,6 @@ import org.jetbrains.kotlin.cli.common.messages.MessageCollectorBasedReporter import org.jetbrains.kotlin.cli.common.repl.IReplStageHistory import org.jetbrains.kotlin.cli.common.repl.LineId import org.jetbrains.kotlin.cli.common.repl.ReplCodeLine -import org.jetbrains.kotlin.cli.common.repl.scriptResultFieldName import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.codegen.ClassBuilderFactories import org.jetbrains.kotlin.codegen.CompilationErrorHandler @@ -112,7 +111,14 @@ class KJvmReplCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : is ReplCodeAnalyzer.ReplLineAnalysisResult.WithErrors -> return failure( messageCollector ) - is ReplCodeAnalyzer.ReplLineAnalysisResult.Successful -> analysisResult.scriptDescriptor + is ReplCodeAnalyzer.ReplLineAnalysisResult.Successful -> { + (analysisResult.scriptDescriptor as? ScriptDescriptor) + ?: return failure( + snippet, + messageCollector, + "Unexpected script descriptor type ${analysisResult.scriptDescriptor::class}" + ) + } else -> return failure( snippet, messageCollector, @@ -120,8 +126,6 @@ class KJvmReplCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : ) } - val type = (scriptDescriptor as ScriptDescriptor).resultValue?.returnType - val generationState = GenerationState.Builder( snippetKtFile.project, ClassBuilderFactories.BINARIES, @@ -130,9 +134,7 @@ class KJvmReplCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : sourceFiles, compilationState.environment.configuration ).build().apply { - replSpecific.resultType = type - replSpecific.scriptResultFieldName = scriptResultFieldName(codeLine.no) - replSpecific.earlierScriptsForReplInterpreter = history.map { it.item } + scriptSpecific.earlierScriptsForReplInterpreter = history.map { it.item } beforeCompile() } KotlinCodegenFacade.generatePackage( 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 70a6e02c01e..054e64c40f2 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 @@ -147,10 +147,9 @@ internal fun makeCompiledScript( val module = makeCompiledModule(generationState) - val resultField = with(generationState.replSpecific) { - // TODO: pass it in the configuration instead - if (!hasResult || resultType == null || scriptResultFieldName == null) null - else scriptResultFieldName!! to KotlinType(DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(resultType!!)) + val resultField = with(generationState.scriptSpecific) { + if (resultType == null || resultFieldName == null) null + else resultFieldName!! to KotlinType(DescriptorRenderer.FQ_NAMES_IN_TYPES.renderType(resultType!!)) } return KJvmCompiledScript( diff --git a/plugins/scripting/scripting-compiler/testData/integration/intResult.kts b/plugins/scripting/scripting-compiler/testData/integration/intResult.kts new file mode 100644 index 00000000000..069c2ae6b8e --- /dev/null +++ b/plugins/scripting/scripting-compiler/testData/integration/intResult.kts @@ -0,0 +1,2 @@ + +10 diff --git a/plugins/scripting/scripting-compiler/tests/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingWithCliCompilerTest.kt b/plugins/scripting/scripting-compiler/tests/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingWithCliCompilerTest.kt new file mode 100644 index 00000000000..d824b673931 --- /dev/null +++ b/plugins/scripting/scripting-compiler/tests/org/jetbrains/kotlin/scripting/compiler/plugin/ScriptingWithCliCompilerTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.scripting.compiler.plugin + +import java.io.File +import junit.framework.Assert.* +import org.jetbrains.kotlin.cli.common.CLITool +import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +class ScriptingWithCliCompilerTest { + + companion object { + const val TEST_DATA_DIR = "plugins/scripting/scripting-compiler/testData" + } + + @Test + fun testResultValue() { + runWithK2JVMCompiler("$TEST_DATA_DIR/integration/intResult.kts", listOf("10")) + } +} + +fun runWithK2JVMCompiler( + scriptPath: String, + expectedOutPatterns: List = emptyList(), + expectedExitCode: Int = 0 +) { + val mainKtsJar = File("dist/kotlinc/lib/kotlin-main-kts.jar") + assertTrue("kotlin-main-kts.jar not found, run dist task: ${mainKtsJar.absolutePath}", mainKtsJar.exists()) + + val (out, err, ret) = captureOutErrRet { + CLITool.doMainNoExit( + K2JVMCompiler(), + arrayOf("-kotlin-home", "dist/kotlinc", "-cp", mainKtsJar.absolutePath, "-script", scriptPath) + ) + } + try { + val outLines = out.lines() + assertEquals(expectedOutPatterns.size, outLines.size) + for ((expectedPattern, actualLine) in expectedOutPatterns.zip(outLines)) { + assertTrue( + "line \"$actualLine\" do not match with expected pattern \"$expectedPattern\"", + Regex(expectedPattern).matches(actualLine) + ) + } + assertEquals(expectedExitCode, ret.code) + } catch (e: Throwable) { + println("OUT:\n$out") + println("ERR:\n$err") + throw e + } +} + + +internal fun captureOutErrRet(body: () -> T): Triple { + val outStream = ByteArrayOutputStream() + val errStream = ByteArrayOutputStream() + val prevOut = System.out + val prevErr = System.err + System.setOut(PrintStream(outStream)) + System.setErr(PrintStream(errStream)) + val ret = try { + body() + } finally { + System.out.flush() + System.err.flush() + System.setOut(prevOut) + System.setErr(prevErr) + } + return Triple(outStream.toString().trim(), errStream.toString().trim(), ret) +}