Properly handle REPL snippets with exceptions ...

so the REPL remain operational after exception in one of the snippets:
- separately return script class and script instance on evaluation (
  because in case of an exception the class is valid, while the instance
  is not).
- store both the class and the instance in the history
- handle this data accordingly
This commit is contained in:
Ilya Chernikov
2019-07-25 15:23:58 +02:00
parent 288fdc0952
commit ec3ccf1ba8
6 changed files with 52 additions and 26 deletions
@@ -69,7 +69,7 @@ val ScriptEvaluationConfigurationKeys.constructorArgs by PropertiesCollection.ke
* For the first snippet in a REPL an empty list should be passed explicitly * For the first snippet in a REPL an empty list should be passed explicitly
* An array of the previous snippets will be passed to the current snippet constructor * An array of the previous snippets will be passed to the current snippet constructor
*/ */
val ScriptEvaluationConfigurationKeys.previousSnippets by PropertiesCollection.key<List<Any>>(isTransient = true) val ScriptEvaluationConfigurationKeys.previousSnippets by PropertiesCollection.key<List<Any?>>(isTransient = true)
@Deprecated("use scriptsInstancesSharing flag instead", level = DeprecationLevel.ERROR) @Deprecated("use scriptsInstancesSharing flag instead", level = DeprecationLevel.ERROR)
val ScriptEvaluationConfigurationKeys.scriptsInstancesSharingMap by PropertiesCollection.key<MutableMap<KClass<*>, EvaluationResult>>(isTransient = true) val ScriptEvaluationConfigurationKeys.scriptsInstancesSharingMap by PropertiesCollection.key<MutableMap<KClass<*>, EvaluationResult>>(isTransient = true)
@@ -134,24 +134,28 @@ fun ScriptEvaluationConfiguration.refineBeforeEvaluation(
/** /**
* The script evaluation result value * The script evaluation result value
*/ */
sealed class ResultValue(val scriptInstance: Any? = null) { sealed class ResultValue(val scriptClass: KClass<*>? = null, val scriptInstance: Any? = null) {
/** /**
* The result value representing a script return value - the value of the last expression in the script * 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 name assigned name of the result field - used e.g. in REPL
* @param value actual result value * @param value actual result value
* @param type name of the result type * @param type name of the result type
* @param scriptClass the loaded class of the script
* @param scriptInstance instance of the script class * @param scriptInstance instance of the script class
*/ */
class Value(val name: String, val value: Any?, val type: String, scriptInstance: Any) : ResultValue(scriptInstance) { class Value(val name: String, val value: Any?, val type: String, scriptClass: KClass<*>, scriptInstance: Any) :
ResultValue(scriptClass, scriptInstance) {
override fun toString(): String = "$name: $type = $value" override fun toString(): String = "$name: $type = $value"
} }
/** /**
* The result value representing unit result, e.g. when the script ends with a statement * The result value representing unit result, e.g. when the script ends with a statement
* @param scriptClass the loaded class of the script
* @param scriptInstance instance of the script class * @param scriptInstance instance of the script class
*/ */
class Unit(scriptInstance: Any) : ResultValue(scriptInstance) { class Unit(scriptClass: KClass<*>, scriptInstance: Any) : ResultValue(scriptClass, scriptInstance) {
override fun toString(): String = "Unit" override fun toString(): String = "Unit"
} }
@@ -159,8 +163,9 @@ sealed class ResultValue(val scriptInstance: Any? = null) {
* The result value representing an exception from script itself * The result value representing an exception from script itself
* @param error the actual exception thrown on script evaluation * @param error the actual exception thrown on script evaluation
* @param wrappingException the wrapping exception e.g. InvocationTargetException, sometimes useful for calculating the relevant stacktrace * @param wrappingException the wrapping exception e.g. InvocationTargetException, sometimes useful for calculating the relevant stacktrace
* @param scriptClass the loaded class of the script, if any
*/ */
class Error(val error: Throwable, val wrappingException: Throwable? = null) : ResultValue() { class Error(val error: Throwable, val wrappingException: Throwable? = null, scriptClass: KClass<*>? = null) : ResultValue(scriptClass) {
override fun toString(): String = error.toString() override fun toString(): String = error.toString()
} }
@@ -85,6 +85,23 @@ class KotlinJsr223ScriptEngineIT {
Assert.assertEquals(5, res3) Assert.assertEquals(5, res3)
} }
@Test
fun testEvalWithException() {
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
try {
engine.eval("throw Exception(\"!!\")")
Assert.fail("Expecting exception to propagate")
} catch (e: ScriptException) {
Assert.assertEquals("!!", e.cause?.message)
}
// engine should remain operational
val res1 = engine.eval("val x = 3")
Assert.assertNull(res1)
val res2 = engine.eval("x + 4")
Assert.assertEquals(7, res2)
}
@Test @Test
fun testEngineRepeatWithReset() { fun testEngineRepeatWithReset() {
val code = "open class A {}\n" + val code = "open class A {}\n" +
@@ -80,9 +80,11 @@ class ReplTest : TestCase() {
simpleScriptompilationConfiguration, simpleScriptompilationConfiguration,
simpleScriptEvaluationConfiguration, simpleScriptEvaluationConfiguration,
sequenceOf( sequenceOf(
"throw RuntimeException(\"abc\")" "throw RuntimeException(\"abc\")",
"val x = 3",
"x + 1"
), ),
sequenceOf(RuntimeException("abc")) sequenceOf(RuntimeException("abc"), null, 4)
) )
} }
@@ -107,11 +109,12 @@ class ReplTest : TestCase() {
} }
} }
.onSuccess { .onSuccess {
it.returnValue.scriptInstance?.let { snippetInstance -> val snippetClass = it.returnValue.scriptClass
currentEvalConfig = ScriptEvaluationConfiguration(currentEvalConfig) { currentEvalConfig = ScriptEvaluationConfiguration(currentEvalConfig) {
previousSnippets.append(snippetInstance) previousSnippets.append(it.returnValue.scriptInstance)
if (snippetClass != null) {
jvm { jvm {
baseClassLoader(snippetInstance::class.java.classLoader) baseClassLoader(snippetClass.java.classLoader)
} }
} }
} }
@@ -7,7 +7,6 @@ package kotlin.script.experimental.jvmhost.jsr223
import org.jetbrains.kotlin.cli.common.repl.* import org.jetbrains.kotlin.cli.common.repl.*
import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.concurrent.locks.ReentrantReadWriteLock
import javax.script.Bindings
import javax.script.ScriptContext import javax.script.ScriptContext
import javax.script.ScriptEngineFactory import javax.script.ScriptEngineFactory
import kotlin.script.experimental.api.ScriptCompilationConfiguration import kotlin.script.experimental.api.ScriptCompilationConfiguration
@@ -71,7 +70,7 @@ class KotlinJsr223ScriptEngineImpl(
get() = null get() = null
override val backwardInstancesHistory: Sequence<Any> override val backwardInstancesHistory: Sequence<Any>
get() = getCurrentState(getContext()).asState(JvmReplEvaluatorState::class.java).history.asReversed().asSequence().map { it.item } get() = getCurrentState(getContext()).asState(JvmReplEvaluatorState::class.java).history.asReversed().asSequence().map { it.item.second }.filterNotNull()
override val baseClassLoader: ClassLoader override val baseClassLoader: ClassLoader
get() = evaluationConfiguration[ScriptEvaluationConfiguration.jvm.baseClassLoader]!! get() = evaluationConfiguration[ScriptEvaluationConfiguration.jvm.baseClassLoader]!!
@@ -9,6 +9,7 @@ import kotlinx.coroutines.runBlocking
import org.jetbrains.kotlin.cli.common.repl.* import org.jetbrains.kotlin.cli.common.repl.*
import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write import kotlin.concurrent.write
import kotlin.reflect.KClass
import kotlin.script.experimental.api.* import kotlin.script.experimental.api.*
import kotlin.script.experimental.jvm.BasicJvmScriptEvaluator import kotlin.script.experimental.jvm.BasicJvmScriptEvaluator
import kotlin.script.experimental.jvm.baseClassLoader import kotlin.script.experimental.jvm.baseClassLoader
@@ -37,15 +38,15 @@ class JvmReplEvaluator(
val compiledScript = (compileResult.data as? KJvmCompiledScript<*>) val compiledScript = (compileResult.data as? KJvmCompiledScript<*>)
?: return ReplEvalResult.Error.CompileTime("Unable to access compiled script: ${compileResult.data}") ?: return ReplEvalResult.Error.CompileTime("Unable to access compiled script: ${compileResult.data}")
val lastSnippetInstance = history.peek()?.item val lastSnippetClass = history.peek()?.item?.first
val historyBeforeSnippet = history.previousItems(compileResult.lineId) val historyBeforeSnippet = history.previousItems(compileResult.lineId).map { it.second }.toList()
val currentConfiguration = ScriptEvaluationConfiguration(baseScriptEvaluationConfiguration) { val currentConfiguration = ScriptEvaluationConfiguration(baseScriptEvaluationConfiguration) {
if (historyBeforeSnippet.any()) { if (historyBeforeSnippet.isNotEmpty()) {
previousSnippets.put(historyBeforeSnippet.toList()) previousSnippets.put(historyBeforeSnippet)
} }
if (lastSnippetInstance != null) { if (lastSnippetClass != null) {
jvm { jvm {
baseClassLoader(lastSnippetInstance::class.java.classLoader) baseClassLoader(lastSnippetClass.java.classLoader)
} }
} }
if (scriptArgs != null) { if (scriptArgs != null) {
@@ -59,17 +60,18 @@ class JvmReplEvaluator(
is ResultWithDiagnostics.Success -> { is ResultWithDiagnostics.Success -> {
when (val retVal = res.value.returnValue) { when (val retVal = res.value.returnValue) {
is ResultValue.Error -> { is ResultValue.Error -> {
history.replaceOrPush(compileResult.lineId, retVal.scriptClass to null)
ReplEvalResult.Error.Runtime( ReplEvalResult.Error.Runtime(
retVal.error.message ?: "unknown error", retVal.error.message ?: "unknown error",
(retVal.error as? Exception) ?: (retVal.wrappingException as? Exception) (retVal.error as? Exception) ?: (retVal.wrappingException as? Exception)
) )
} }
is ResultValue.Value -> { is ResultValue.Value -> {
history.replaceOrPush(compileResult.lineId, retVal.scriptInstance!!) history.replaceOrPush(compileResult.lineId, retVal.scriptClass to retVal.scriptInstance)
ReplEvalResult.ValueResult(retVal.name, retVal.value, retVal.type) ReplEvalResult.ValueResult(retVal.name, retVal.value, retVal.type)
} }
is ResultValue.Unit -> { is ResultValue.Unit -> {
history.replaceOrPush(compileResult.lineId, retVal.scriptInstance!!) history.replaceOrPush(compileResult.lineId, retVal.scriptClass to retVal.scriptInstance)
ReplEvalResult.UnitResult() ReplEvalResult.UnitResult()
} }
else -> throw IllegalStateException("Unexpected snippet result value $retVal") else -> throw IllegalStateException("Unexpected snippet result value $retVal")
@@ -87,8 +89,8 @@ class JvmReplEvaluator(
open class JvmReplEvaluatorState( open class JvmReplEvaluatorState(
scriptEvaluationConfiguration: ScriptEvaluationConfiguration, scriptEvaluationConfiguration: ScriptEvaluationConfiguration,
override val lock: ReentrantReadWriteLock = ReentrantReadWriteLock() override val lock: ReentrantReadWriteLock = ReentrantReadWriteLock()
) : IReplStageState<Any> { ) : IReplStageState<Pair<KClass<*>?, Any?>> {
override val history: IReplStageHistory<Any> = ReplStageHistoryWithReplace(lock) override val history: IReplStageHistory<Pair<KClass<*>?, Any?>> = ReplStageHistoryWithReplace(lock)
override val currentGeneration: Int get() = (history as BasicReplStageHistory<*>).currentGeneration.get() override val currentGeneration: Int get() = (history as BasicReplStageHistory<*>).currentGeneration.get()
} }
@@ -41,11 +41,11 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator {
compiledScript.resultField?.let { (resultFieldName, resultType) -> compiledScript.resultField?.let { (resultFieldName, resultType) ->
val resultField = scriptClass.java.getDeclaredField(resultFieldName).apply { isAccessible = true } val resultField = scriptClass.java.getDeclaredField(resultFieldName).apply { isAccessible = true }
ResultValue.Value(resultFieldName, resultField.get(instance), resultType.typeName, instance) ResultValue.Value(resultFieldName, resultField.get(instance), resultType.typeName, scriptClass, instance)
} ?: ResultValue.Unit(instance) } ?: ResultValue.Unit(scriptClass, instance)
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
ResultValue.Error(e.targetException ?: e, e) ResultValue.Error(e.targetException ?: e, e, scriptClass)
} }
EvaluationResult(resultValue, refinedEvalConfiguration).let { EvaluationResult(resultValue, refinedEvalConfiguration).let {