diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompiledEvaluator.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompiledEvaluator.kt index 65c9d39475d..27d56412417 100644 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompiledEvaluator.kt +++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompiledEvaluator.kt @@ -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, baseClassloader: ClassLoader?, val scriptArgs: Array? = null, val scriptArgsTypes: Array>? = 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, return if (hasResult) ReplEvalResult.ValueResult(compiledLoadedClassesHistory.lines, rv) else ReplEvalResult.UnitResult(compiledLoadedClassesHistory.lines) } - override fun getInterface(klass: KClass): ReplInvokeResult = evalStateLock.read { - val (_, instance) = compiledLoadedClassesHistory.values.lastOrNull() ?: return ReplInvokeResult.Error.NoSuchEntity("no script ") + override fun getInterface(klass: KClass): ReplScriptInvokeResult = evalStateLock.read { + val (_, instance) = compiledLoadedClassesHistory.values.lastOrNull() ?: return ReplScriptInvokeResult.Error.NoSuchEntity("no script ") return getInterface(instance, klass) } - override fun getInterface(receiver: Any, klass: KClass): ReplInvokeResult = evalStateLock.read { - return ReplInvokeResult.ValueResult(klass.safeCast(receiver)) + override fun getInterface(receiver: Any, klass: KClass): 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): ReplInvokeResult { - val (fn, mapping) = receiverClass.functions.filter { it.name == name }.findMapping(args.toList()) ?: - receiverClass.memberExtensionFunctions.filter { it.name == name }.findMapping(listOf(receiverInstance) + args) ?: - return ReplInvokeResult.Error.NoSuchEntity("no suitable function '$name' found") + private fun invokeImpl(receiverClass: KClass<*>, receiverInstance: Any, name: String, args: Array): 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(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 { diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/KotlinJsr223JvmScriptEngineBase.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/KotlinJsr223JvmScriptEngineBase.kt index 4155fb810a2..486c7266d28 100644 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/KotlinJsr223JvmScriptEngineBase.kt +++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/KotlinJsr223JvmScriptEngineBase.kt @@ -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 getInterface(p0: Class?): T { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun getInterface(p0: Any?, p1: Class?): 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 getInterface(clasz: Class?): 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 getInterface(thiz: Any?, clasz: Class?): 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 + } +} diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/Repl.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/Repl.kt index 23940c7b389..5f3f249396f 100644 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/Repl.kt +++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/Repl.kt @@ -73,12 +73,12 @@ sealed class ReplCompileResult(val updatedHistory: List) : 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): ReplCompileResult } -interface ReplInvoker { - fun getInterface(clasz: KClass): ReplInvokeResult - fun getInterface(receiver: Any, clasz: KClass): ReplInvokeResult - fun invokeMethod(receiver: Any, name: String, vararg args: Any?): ReplInvokeResult - fun invokeFunction(name: String, vararg args: Any?): ReplInvokeResult +interface ReplScriptInvoker { + fun getInterface(clasz: KClass): ReplScriptInvokeResult + fun getInterface(receiver: Any, clasz: KClass): 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 { diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/GenericRepl.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/GenericRepl.kt index 650e3921c8f..e6b789b900c 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/GenericRepl.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/repl/GenericRepl.kt @@ -181,10 +181,12 @@ open class GenericRepl( baseClassloader: ClassLoader?, scriptArgs: Array? = null, scriptArgsTypes: Array>? = 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): ReplEvalResult = compileAndEval(this, compiledEvaluator, codeLine, history) diff --git a/libraries/examples/kotlin-jsr223-daemon-local-eval-example/src/test/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223ScriptEngineIT.kt b/libraries/examples/kotlin-jsr223-daemon-local-eval-example/src/test/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223ScriptEngineIT.kt index 25e1bfad58d..0a300da751f 100644 --- a/libraries/examples/kotlin-jsr223-daemon-local-eval-example/src/test/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223ScriptEngineIT.kt +++ b/libraries/examples/kotlin-jsr223-daemon-local-eval-example/src/test/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223ScriptEngineIT.kt @@ -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}") + } + } } diff --git a/libraries/examples/kotlin-jsr223-local-example/src/test/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223ScriptEngineIT.kt b/libraries/examples/kotlin-jsr223-local-example/src/test/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223ScriptEngineIT.kt index 917e0e69d83..43ec1ccc646 100644 --- a/libraries/examples/kotlin-jsr223-local-example/src/test/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223ScriptEngineIT.kt +++ b/libraries/examples/kotlin-jsr223-local-example/src/test/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223ScriptEngineIT.kt @@ -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}") + } + } } diff --git a/libraries/tools/kotlin-script-util/pom.xml b/libraries/tools/kotlin-script-util/pom.xml index f4038901169..210df64d050 100644 --- a/libraries/tools/kotlin-script-util/pom.xml +++ b/libraries/tools/kotlin-script-util/pom.xml @@ -34,6 +34,11 @@ kotlin-stdlib ${project.version} + + org.jetbrains.kotlin + kotlin-runtime + ${project.version} + org.jetbrains.kotlin kotlin-test-junit diff --git a/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223JvmDaemonScriptEngines.kt b/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223JvmDaemonScriptEngines.kt index 8e058dd6a1b..d47680d9774 100644 --- a/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223JvmDaemonScriptEngines.kt +++ b/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223JvmDaemonScriptEngines.kt @@ -37,7 +37,7 @@ class KotlinJsr223JvmDaemonLocalEvalScriptEngine( getScriptArgs: (ScriptContext) -> Array?, scriptArgsTypes: Array>?, 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): ReplEvalResult { fun ReplCompileResult.Error.locationString() = if (location == CompilerMessageLocation.NO_LOCATION) "" diff --git a/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223JvmLocalScriptEngine.kt b/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223JvmLocalScriptEngine.kt index ae6cf4fd7d8..ad97af53831 100644 --- a/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223JvmLocalScriptEngine.kt +++ b/libraries/tools/kotlin-script-util/src/main/kotlin/org/jetbrains/kotlin/script/jsr223/KotlinJsr223JvmLocalScriptEngine.kt @@ -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?, scriptArgsTypes: Array>? -) : 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): ReplEvalResult { val evalResult = repl.eval(codeLine, history) messageCollector.resetAndThrowOnErrors()