Implement Invocable on base evaluator and locally-evaluating JSR223 sample engines
fixes #KT-14707
This commit is contained in:
+17
-15
@@ -24,13 +24,12 @@ import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.write
|
||||
import kotlin.reflect.*
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
open class GenericReplCompiledEvaluator(baseClasspath: Iterable<File>,
|
||||
baseClassloader: ClassLoader?,
|
||||
val scriptArgs: Array<Any?>? = null,
|
||||
val scriptArgsTypes: Array<Class<*>>? = null
|
||||
) : ReplCompiledEvaluator, ReplInvoker {
|
||||
) : ReplCompiledEvaluator, ReplScriptInvoker {
|
||||
|
||||
private var classLoader: org.jetbrains.kotlin.cli.common.repl.ReplClassLoader = makeReplClassLoader(baseClassloader, baseClasspath)
|
||||
private val classLoaderLock = ReentrantReadWriteLock()
|
||||
@@ -101,36 +100,39 @@ open class GenericReplCompiledEvaluator(baseClasspath: Iterable<File>,
|
||||
return if (hasResult) ReplEvalResult.ValueResult(compiledLoadedClassesHistory.lines, rv) else ReplEvalResult.UnitResult(compiledLoadedClassesHistory.lines)
|
||||
}
|
||||
|
||||
override fun <T: Any> getInterface(klass: KClass<T>): ReplInvokeResult = evalStateLock.read {
|
||||
val (_, instance) = compiledLoadedClassesHistory.values.lastOrNull() ?: return ReplInvokeResult.Error.NoSuchEntity("no script ")
|
||||
override fun <T: Any> getInterface(klass: KClass<T>): ReplScriptInvokeResult = evalStateLock.read {
|
||||
val (_, instance) = compiledLoadedClassesHistory.values.lastOrNull() ?: return ReplScriptInvokeResult.Error.NoSuchEntity("no script ")
|
||||
return getInterface(instance, klass)
|
||||
}
|
||||
|
||||
override fun <T: Any> getInterface(receiver: Any, klass: KClass<T>): ReplInvokeResult = evalStateLock.read {
|
||||
return ReplInvokeResult.ValueResult(klass.safeCast(receiver))
|
||||
override fun <T: Any> getInterface(receiver: Any, klass: KClass<T>): ReplScriptInvokeResult = evalStateLock.read {
|
||||
return ReplScriptInvokeResult.ValueResult(klass.safeCast(receiver))
|
||||
}
|
||||
|
||||
override fun invokeMethod(receiver: Any, name: String, vararg args: Any?): ReplInvokeResult = evalStateLock.read {
|
||||
override fun invokeMethod(receiver: Any, name: String, vararg args: Any?): ReplScriptInvokeResult = evalStateLock.read {
|
||||
return invokeImpl(receiver.javaClass.kotlin, receiver, name, args)
|
||||
}
|
||||
|
||||
override fun invokeFunction(name: String, vararg args: Any?): ReplInvokeResult = evalStateLock.read {
|
||||
val (klass, instance) = compiledLoadedClassesHistory.values.lastOrNull() ?: return ReplInvokeResult.Error.NoSuchEntity("no script ")
|
||||
override fun invokeFunction(name: String, vararg args: Any?): ReplScriptInvokeResult = evalStateLock.read {
|
||||
val (klass, instance) = compiledLoadedClassesHistory.values.lastOrNull() ?: return ReplScriptInvokeResult.Error.NoSuchEntity("no script ")
|
||||
return invokeImpl(klass.kotlin, instance, name, args)
|
||||
}
|
||||
|
||||
private fun invokeImpl(receiverClass: KClass<*>, receiverInstance: Any, name: String, args: Array<out Any?>): ReplInvokeResult {
|
||||
val (fn, mapping) = receiverClass.functions.filter { it.name == name }.findMapping(args.toList()) ?:
|
||||
receiverClass.memberExtensionFunctions.filter { it.name == name }.findMapping(listOf<Any?>(receiverInstance) + args) ?:
|
||||
return ReplInvokeResult.Error.NoSuchEntity("no suitable function '$name' found")
|
||||
private fun invokeImpl(receiverClass: KClass<*>, receiverInstance: Any, name: String, args: Array<out Any?>): ReplScriptInvokeResult {
|
||||
|
||||
val candidates = receiverClass.memberFunctions.filter { it.name == name } +
|
||||
receiverClass.memberExtensionFunctions.filter { it.name == name }
|
||||
val (fn, mapping) = candidates.findMapping(args.toList()) ?:
|
||||
candidates.findMapping(listOf<Any?>(receiverInstance) + args) ?:
|
||||
return ReplScriptInvokeResult.Error.NoSuchEntity("no suitable function '$name' found")
|
||||
val res = try {
|
||||
evalWithIO { fn.callBy(mapping) }
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
// ignore everything in the stack trace until this constructor call
|
||||
return ReplInvokeResult.Error.Runtime(renderReplStackTrace(e.cause!!, startFromMethodName = "${fn.name}"), e as? Exception)
|
||||
return ReplScriptInvokeResult.Error.Runtime(renderReplStackTrace(e.cause!!, startFromMethodName = "${fn.name}"), e as? Exception)
|
||||
}
|
||||
return if (fn.returnType.classifier == Unit::class) ReplInvokeResult.UnitResult else ReplInvokeResult.ValueResult(res)
|
||||
return if (fn.returnType.classifier == Unit::class) ReplScriptInvokeResult.UnitResult else ReplScriptInvokeResult.ValueResult(res)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+41
-16
@@ -60,23 +60,48 @@ abstract class KotlinJsr223JvmScriptEngineBase(protected val myFactory: ScriptEn
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun invokeMethod(p0: Any?, p1: String?, vararg p2: Any?): Any {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun <T : Any?> getInterface(p0: Class<T>?): T {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun <T : Any?> getInterface(p0: Any?, p1: Class<T>?): T {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun invokeFunction(p0: String?, vararg p1: Any?): Any {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun createBindings(): Bindings = SimpleBindings()
|
||||
|
||||
override fun getFactory(): ScriptEngineFactory = myFactory
|
||||
}
|
||||
|
||||
|
||||
@Suppress("unused") // used externally (kotlin.script.utils)
|
||||
interface KotlinJsr223JvmInvocableScriptEngine : Invocable {
|
||||
|
||||
val replScriptInvoker: ReplScriptInvoker
|
||||
|
||||
override fun invokeFunction(name: String?, vararg args: Any?): Any? {
|
||||
if (name == null) throw java.lang.NullPointerException("function name cannot be null")
|
||||
return processInvokeResult(replScriptInvoker.invokeFunction(name, *args), isMethod = true)
|
||||
}
|
||||
|
||||
override fun invokeMethod(thiz: Any?, name: String?, vararg args: Any?): Any? {
|
||||
if (name == null) throw java.lang.NullPointerException("method name cannot be null")
|
||||
if (thiz == null) throw IllegalArgumentException("cannot invoke method on the null object")
|
||||
return processInvokeResult(replScriptInvoker.invokeMethod(thiz, name, *args), isMethod = true)
|
||||
}
|
||||
|
||||
override fun <T : Any> getInterface(clasz: Class<T>?): T? {
|
||||
if (clasz == null) throw IllegalArgumentException("class object cannot be null")
|
||||
if (!clasz.isInterface) throw IllegalArgumentException("expecting interface")
|
||||
return processInvokeResult(replScriptInvoker.getInterface(clasz.kotlin), isMethod = false) as? T
|
||||
}
|
||||
|
||||
override fun <T : Any> getInterface(thiz: Any?, clasz: Class<T>?): T? {
|
||||
if (thiz == null) throw IllegalArgumentException("object cannot be null")
|
||||
if (clasz == null) throw IllegalArgumentException("class object cannot be null")
|
||||
if (!clasz.isInterface) throw IllegalArgumentException("expecting interface")
|
||||
return processInvokeResult(replScriptInvoker.getInterface(thiz, clasz.kotlin), isMethod = false) as? T
|
||||
}
|
||||
|
||||
private fun processInvokeResult(res: ReplScriptInvokeResult, isMethod: Boolean): Any? =
|
||||
when (res) {
|
||||
is ReplScriptInvokeResult.Error.NoSuchEntity -> throw if (isMethod) NoSuchMethodException(res.message) else IllegalArgumentException(res.message)
|
||||
is ReplScriptInvokeResult.Error.CompileTime -> throw IllegalArgumentException(res.message) // should not happen in the current code, so leaving it here despite the contradiction with Invocable's specs
|
||||
is ReplScriptInvokeResult.Error.Runtime -> throw ScriptException(res.message)
|
||||
is ReplScriptInvokeResult.Error -> throw ScriptException(res.message)
|
||||
is ReplScriptInvokeResult.UnitResult -> Unit // TODO: check if it is suitable replacement for java's Void
|
||||
is ReplScriptInvokeResult.ValueResult -> res.value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,12 +73,12 @@ sealed class ReplCompileResult(val updatedHistory: List<ReplCodeLine>) : Seriali
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ReplInvokeResult : Serializable {
|
||||
class ValueResult(val value: Any?) : ReplInvokeResult() {
|
||||
sealed class ReplScriptInvokeResult : Serializable {
|
||||
class ValueResult(val value: Any?) : ReplScriptInvokeResult() {
|
||||
override fun toString(): String = "Result: $value"
|
||||
}
|
||||
object UnitResult : ReplInvokeResult()
|
||||
sealed class Error(val message: String) : ReplInvokeResult() {
|
||||
object UnitResult : ReplScriptInvokeResult()
|
||||
sealed class Error(val message: String) : ReplScriptInvokeResult() {
|
||||
class Runtime(message: String, val cause: Exception? = null) : Error(message)
|
||||
class NoSuchEntity(message: String) : Error(message)
|
||||
class CompileTime(message: String, val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION) : Error(message)
|
||||
@@ -117,11 +117,16 @@ interface ReplCompiler : ReplChecker {
|
||||
fun compile(codeLine: ReplCodeLine, history: List<ReplCodeLine>): ReplCompileResult
|
||||
}
|
||||
|
||||
interface ReplInvoker {
|
||||
fun <T: Any> getInterface(clasz: KClass<T>): ReplInvokeResult
|
||||
fun <T: Any> getInterface(receiver: Any, clasz: KClass<T>): ReplInvokeResult
|
||||
fun invokeMethod(receiver: Any, name: String, vararg args: Any?): ReplInvokeResult
|
||||
fun invokeFunction(name: String, vararg args: Any?): ReplInvokeResult
|
||||
interface ReplScriptInvoker {
|
||||
fun <T: Any> getInterface(clasz: KClass<T>): ReplScriptInvokeResult
|
||||
fun <T: Any> getInterface(receiver: Any, clasz: KClass<T>): ReplScriptInvokeResult
|
||||
fun invokeMethod(receiver: Any, name: String, vararg args: Any?): ReplScriptInvokeResult
|
||||
fun invokeFunction(name: String, vararg args: Any?): ReplScriptInvokeResult
|
||||
}
|
||||
|
||||
// TODO this is a bit cumbersome, consider some other ways to access an invoker
|
||||
interface ReplScriptInvokerProxy {
|
||||
val scriptInvoker: ReplScriptInvoker
|
||||
}
|
||||
|
||||
interface ReplCompiledEvaluator {
|
||||
|
||||
@@ -181,10 +181,12 @@ open class GenericRepl(
|
||||
baseClassloader: ClassLoader?,
|
||||
scriptArgs: Array<Any?>? = null,
|
||||
scriptArgsTypes: Array<Class<*>>? = null
|
||||
) : ReplEvaluator, GenericReplCompiler(disposable, scriptDefinition, compilerConfiguration, messageCollector) {
|
||||
) : ReplEvaluator, ReplScriptInvokerProxy, GenericReplCompiler(disposable, scriptDefinition, compilerConfiguration, messageCollector) {
|
||||
|
||||
private val compiledEvaluator = GenericReplCompiledEvaluator(compilerConfiguration.jvmClasspathRoots, baseClassloader, scriptArgs, scriptArgsTypes)
|
||||
|
||||
override val scriptInvoker: ReplScriptInvoker get() = compiledEvaluator
|
||||
|
||||
@Synchronized
|
||||
override fun eval(codeLine: ReplCodeLine, history: List<ReplCodeLine>): ReplEvalResult =
|
||||
compileAndEval(this, compiledEvaluator, codeLine, history)
|
||||
|
||||
+38
@@ -19,6 +19,7 @@ package org.jetbrains.kotlin.script.jsr223
|
||||
import org.jetbrains.kotlin.config.KotlinCompilerVersion
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import javax.script.Invocable
|
||||
import javax.script.ScriptEngine
|
||||
import javax.script.ScriptEngineManager
|
||||
import javax.script.SimpleBindings
|
||||
@@ -65,4 +66,41 @@ class KotlinJsr223ScriptEngineIT {
|
||||
val res2 = engine.eval("x + 2")
|
||||
Assert.assertEquals(5, res2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvocable() {
|
||||
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
|
||||
val res1 = engine.eval("""
|
||||
fun fn(x: Int) = x + 2
|
||||
val obj = object {
|
||||
fun fn1(x: Int) = x + 3
|
||||
}
|
||||
obj
|
||||
""")
|
||||
Assert.assertNotNull(res1)
|
||||
val invocator = engine as? Invocable
|
||||
Assert.assertNotNull(invocator)
|
||||
assertThrows(NoSuchMethodException::class.java) {
|
||||
invocator!!.invokeFunction("fn1", 3)
|
||||
}
|
||||
val res2 = invocator!!.invokeFunction("fn", 3)
|
||||
Assert.assertEquals(5, res2)
|
||||
assertThrows(NoSuchMethodException::class.java) {
|
||||
invocator!!.invokeMethod(res1, "fn", 3)
|
||||
}
|
||||
val res3 = invocator!!.invokeMethod(res1, "fn1", 3)
|
||||
Assert.assertEquals(6, res3)
|
||||
}
|
||||
}
|
||||
|
||||
fun assertThrows(exceptionClass: Class<*>, body: () -> Unit) {
|
||||
try {
|
||||
body()
|
||||
Assert.fail("Expecting an exception of type ${exceptionClass.name}")
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
if (!exceptionClass.isAssignableFrom(e.javaClass)) {
|
||||
Assert.fail("Expecting an exception of type ${exceptionClass.name} but got ${e.javaClass.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
@@ -19,6 +19,7 @@ package org.jetbrains.kotlin.script.jsr223
|
||||
import org.jetbrains.kotlin.config.KotlinCompilerVersion
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import javax.script.Invocable
|
||||
import javax.script.ScriptEngine
|
||||
import javax.script.ScriptEngineManager
|
||||
import javax.script.SimpleBindings
|
||||
@@ -65,4 +66,41 @@ class KotlinJsr223ScriptEngineIT {
|
||||
val res2 = engine.eval("x + 2")
|
||||
Assert.assertEquals(5, res2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvocable() {
|
||||
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
|
||||
val res1 = engine.eval("""
|
||||
fun fn(x: Int) = x + 2
|
||||
val obj = object {
|
||||
fun fn1(x: Int) = x + 3
|
||||
}
|
||||
obj
|
||||
""")
|
||||
Assert.assertNotNull(res1)
|
||||
val invocator = engine as? Invocable
|
||||
Assert.assertNotNull(invocator)
|
||||
assertThrows(NoSuchMethodException::class.java) {
|
||||
invocator!!.invokeFunction("fn1", 3)
|
||||
}
|
||||
val res2 = invocator!!.invokeFunction("fn", 3)
|
||||
Assert.assertEquals(5, res2)
|
||||
assertThrows(NoSuchMethodException::class.java) {
|
||||
invocator!!.invokeMethod(res1, "fn", 3)
|
||||
}
|
||||
val res3 = invocator!!.invokeMethod(res1, "fn1", 3)
|
||||
Assert.assertEquals(6, res3)
|
||||
}
|
||||
}
|
||||
|
||||
fun assertThrows(exceptionClass: Class<*>, body: () -> Unit) {
|
||||
try {
|
||||
body()
|
||||
Assert.fail("Expecting an exception of type ${exceptionClass.name}")
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
if (!exceptionClass.isAssignableFrom(e.javaClass)) {
|
||||
Assert.fail("Expecting an exception of type ${exceptionClass.name} but got ${e.javaClass.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-runtime</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-test-junit</artifactId>
|
||||
|
||||
+4
-1
@@ -37,7 +37,7 @@ class KotlinJsr223JvmDaemonLocalEvalScriptEngine(
|
||||
getScriptArgs: (ScriptContext) -> Array<Any?>?,
|
||||
scriptArgsTypes: Array<Class<*>>?,
|
||||
compilerOut: OutputStream = System.err
|
||||
) : KotlinJsr223JvmScriptEngineBase(factory) {
|
||||
) : KotlinJsr223JvmScriptEngineBase(factory), KotlinJsr223JvmInvocableScriptEngine {
|
||||
|
||||
private val daemon by lazy { connectToCompileService(compilerJar) }
|
||||
|
||||
@@ -57,6 +57,9 @@ class KotlinJsr223JvmDaemonLocalEvalScriptEngine(
|
||||
// TODO: bindings passing works only once on the first eval, subsequent setContext/setBindings call have no effect. Consider making it dynamic, but take history into account
|
||||
val localEvaluator by lazy { GenericReplCompiledEvaluator(templateClasspath, Thread.currentThread().contextClassLoader, getScriptArgs(getContext()), scriptArgsTypes) }
|
||||
|
||||
override val replScriptInvoker: ReplScriptInvoker
|
||||
get() = localEvaluator
|
||||
|
||||
override fun eval(codeLine: ReplCodeLine, history: List<ReplCodeLine>): ReplEvalResult {
|
||||
|
||||
fun ReplCompileResult.Error.locationString() = if (location == CompilerMessageLocation.NO_LOCATION) ""
|
||||
|
||||
+6
-4
@@ -21,9 +21,7 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation
|
||||
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
|
||||
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
|
||||
import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
|
||||
import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineBase
|
||||
import org.jetbrains.kotlin.cli.common.repl.ReplCodeLine
|
||||
import org.jetbrains.kotlin.cli.common.repl.ReplEvalResult
|
||||
import org.jetbrains.kotlin.cli.common.repl.*
|
||||
import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
|
||||
import org.jetbrains.kotlin.cli.jvm.repl.GenericRepl
|
||||
import org.jetbrains.kotlin.config.CommonConfigurationKeys
|
||||
@@ -44,7 +42,7 @@ class KotlinJsr223JvmLocalScriptEngine(
|
||||
templateClassName: String,
|
||||
getScriptArgs: (ScriptContext) -> Array<Any?>?,
|
||||
scriptArgsTypes: Array<Class<*>>?
|
||||
) : KotlinJsr223JvmScriptEngineBase(factory) {
|
||||
) : KotlinJsr223JvmScriptEngineBase(factory), KotlinJsr223JvmInvocableScriptEngine {
|
||||
|
||||
data class MessageCollectorReport(val severity: CompilerMessageSeverity, val message: String, val location: CompilerMessageLocation)
|
||||
|
||||
@@ -105,6 +103,10 @@ class KotlinJsr223JvmLocalScriptEngine(
|
||||
put(CommonConfigurationKeys.MODULE_NAME, "kotlin-script")
|
||||
}
|
||||
|
||||
override val replScriptInvoker: ReplScriptInvoker
|
||||
get() = repl.scriptInvoker
|
||||
|
||||
|
||||
override fun eval(codeLine: ReplCodeLine, history: List<ReplCodeLine>): ReplEvalResult {
|
||||
val evalResult = repl.eval(codeLine, history)
|
||||
messageCollector.resetAndThrowOnErrors()
|
||||
|
||||
Reference in New Issue
Block a user