Improve script and REPL result handling:

- implement error result
- refactor other result classes
- implement handling in the script evaluation extension - also restores
  previous script error reporting functionality
- add possibility to customize result fileds in script and REPL
- refactor result calculation in the backend: cleanup, rename (since
  it is not only about REPL now)
This commit is contained in:
Ilya Chernikov
2019-07-04 13:33:15 +02:00
parent dc4370ff08
commit 9ae0ff03fa
23 changed files with 421 additions and 127 deletions
@@ -364,23 +364,27 @@ public class ExpressionCodegen extends KtVisitor<StackValue, StackValue> 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;
@@ -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,
@@ -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
)
}
@@ -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(),
@@ -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<ScriptDescriptor>? = 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)
@@ -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()
}
}
}
}
@@ -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())
}
}
}
}
}
@@ -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<String>("res")
@@ -99,6 +99,12 @@ val ScriptCompilationConfigurationKeys.defaultImports by PropertiesCollection.ke
*/
val ScriptCompilationConfigurationKeys.importScripts by PropertiesCollection.key<List<SourceCode>>()
/**
* 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<String>("\$\$result")
/**
* The list of script dependencies - platform specific
*/
@@ -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()
}
/**
@@ -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<String>,
@@ -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)
}
}
}
}
@@ -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<SimpleScriptTemplate>(body = configurationBuilder)
val scriptCompilationConfiguration =
createJvmCompilationConfigurationFromTemplate<SimpleScriptTemplate>(body = configurationBuilder)
Assert.assertEquals(0, cache.storedScripts)
var compiledScript: CompiledScript<*>? = null
@@ -453,6 +490,13 @@ fun <T> ResultWithDiagnostics<T>.throwOnFailure(): ResultWithDiagnostics<T> = 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(),
@@ -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)
@@ -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(
@@ -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())
}
}
@@ -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
@@ -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()
@@ -51,6 +51,8 @@ runtimeJar()
sourcesJar()
javadocJar()
testsJar()
projectTest {
workingDir = rootDir
}
@@ -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
}
}
}
}
}
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])
}
}
}
@@ -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(
@@ -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(
@@ -0,0 +1,2 @@
10
@@ -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<String> = 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 <T> captureOutErrRet(body: () -> T): Triple<String, String, T> {
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)
}