From ea068e483c332db4979daf8945837bc817e79d84 Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Tue, 16 Apr 2019 21:00:02 +0200 Subject: [PATCH] Implement JSR-223 functionality on top of the "new" REPL --- .../kotlin/cli/common/repl/BasicReplState.kt | 27 ++- .../repl/GenericReplCompilingEvaluator.kt | 32 ++- .../jsr223-embeddable/build.gradle.kts | 37 +++ libraries/scripting/jsr223/build.gradle.kts | 32 +++ .../services/javax.script.ScriptEngineFactory | 2 + .../jsr223/KotlinJsr223DefaultScript.kt | 50 ++++ .../KotlinJsr223InvocableScriptEngine.kt | 93 ++++++++ .../jsr223/KotlinJsr223ScriptEngine.kt | 61 +++++ .../jsr223/KotlinJsr223ScriptEngineFactory.kt | 28 +++ .../jsr223/test/KotlinJsr223ScriptEngineIT.kt | 214 ++++++++++++++++++ .../jvmhost/repl/legacyReplEvaluation.kt | 44 +++- .../jetbrains/kotlin/gradle/SubpluginsIT.kt | 1 + settings.gradle | 4 + 13 files changed, 592 insertions(+), 33 deletions(-) create mode 100644 libraries/scripting/jsr223-embeddable/build.gradle.kts create mode 100644 libraries/scripting/jsr223/build.gradle.kts create mode 100644 libraries/scripting/jsr223/resources/META-INF/services/javax.script.ScriptEngineFactory create mode 100644 libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223DefaultScript.kt create mode 100644 libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223InvocableScriptEngine.kt create mode 100644 libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223ScriptEngine.kt create mode 100644 libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223ScriptEngineFactory.kt create mode 100644 libraries/scripting/jsr223/test/kotlin/script/experimental/jsr223/test/KotlinJsr223ScriptEngineIT.kt diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/BasicReplState.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/BasicReplState.kt index 64b15122d46..7af3d692523 100644 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/BasicReplState.kt +++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/BasicReplState.kt @@ -60,18 +60,21 @@ open class BasicReplStageHistory(override val lock: ReentrantReadWriteLock = override fun resetTo(id: ILineId): Iterable { lock.write { - val idx = indexOfFirst { it.id == id } - if (idx < 0) throw java.util.NoSuchElementException("Cannot rest to inexistent line ${id.no}") - return if (idx < lastIndex) { - val removed = asSequence().drop(idx + 1).map { it.id }.toList() - removeRange(idx + 1, size) - currentGeneration.incrementAndGet() - removed - } - else { - currentGeneration.incrementAndGet() - emptyList() - } + return tryResetTo(id) ?: throw NoSuchElementException("Cannot reset to non-existent line ${id.no}") + } + } + + protected fun tryResetTo(id: ILineId): List? { + val idx = indexOfFirst { it.id == id } + if (idx < 0) return null + return if (idx < lastIndex) { + val removed = asSequence().drop(idx + 1).map { it.id }.toList() + removeRange(idx + 1, size) + currentGeneration.incrementAndGet() + removed + } else { + currentGeneration.incrementAndGet() + emptyList() } } } diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompilingEvaluator.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompilingEvaluator.kt index 094d0bcf837..825aa14f131 100644 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompilingEvaluator.kt +++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/repl/GenericReplCompilingEvaluator.kt @@ -20,13 +20,11 @@ import java.io.File import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.write -class GenericReplCompilingEvaluator(val compiler: ReplCompiler, - baseClasspath: Iterable, - baseClassloader: ClassLoader? = Thread.currentThread().contextClassLoader, - private val fallbackScriptArgs: ScriptArgsWithTypes? = null, - repeatingMode: ReplRepeatingMode = ReplRepeatingMode.REPEAT_ONLY_MOST_RECENT +open class GenericReplCompilingEvaluatorBase( + val compiler: ReplCompiler, + val evaluator: ReplEvaluator, + private val fallbackScriptArgs: ScriptArgsWithTypes? = null ) : ReplFullEvaluator { - private val evaluator = GenericReplEvaluator(baseClasspath, baseClassloader, fallbackScriptArgs, repeatingMode) override fun createState(lock: ReentrantReadWriteLock): IReplStageState<*> = AggregatedReplStageState(compiler.createState(lock), evaluator.createState(lock), lock) @@ -75,7 +73,7 @@ class GenericReplCompilingEvaluator(val compiler: ReplCompiler, } override fun eval(state: IReplStageState<*>, compileResult: ReplCompileResult.CompiledClasses, scriptArgs: ScriptArgsWithTypes?, invokeWrapper: InvokeWrapper?): ReplEvalResult = - evaluator.eval(state, compileResult, scriptArgs, invokeWrapper) + evaluator.eval(state, compileResult, scriptArgs, invokeWrapper) override fun check(state: IReplStageState<*>, codeLine: ReplCodeLine): ReplCheckResult = compiler.check(state, codeLine) @@ -93,12 +91,24 @@ class GenericReplCompilingEvaluator(val compiler: ReplCompiler, private val evaluator: ReplEvaluator, private val defaultScriptArgs: ScriptArgsWithTypes?) : Evaluable { override fun eval(scriptArgs: ScriptArgsWithTypes?, invokeWrapper: InvokeWrapper?): ReplEvalResult = - evaluator.eval(state, compiledCode, scriptArgs ?: defaultScriptArgs, invokeWrapper) + evaluator.eval(state, compiledCode, scriptArgs ?: defaultScriptArgs, invokeWrapper) } } +class GenericReplCompilingEvaluator( + compiler: ReplCompiler, + baseClasspath: Iterable, + baseClassloader: ClassLoader? = Thread.currentThread().contextClassLoader, + fallbackScriptArgs: ScriptArgsWithTypes? = null, + repeatingMode: ReplRepeatingMode = ReplRepeatingMode.REPEAT_ONLY_MOST_RECENT +) : GenericReplCompilingEvaluatorBase( + compiler, + GenericReplEvaluator(baseClasspath, baseClassloader, fallbackScriptArgs, repeatingMode), + fallbackScriptArgs +) + private fun AggregatedReplStageState<*, *>.adjustHistories(): Iterable? = - state2.history.peek()?.let { - state1.history.resetTo(it.id) - } + state2.history.peek()?.let { + state1.history.resetTo(it.id) + } ?: state1.history.reset() diff --git a/libraries/scripting/jsr223-embeddable/build.gradle.kts b/libraries/scripting/jsr223-embeddable/build.gradle.kts new file mode 100644 index 00000000000..cc615dcb986 --- /dev/null +++ b/libraries/scripting/jsr223-embeddable/build.gradle.kts @@ -0,0 +1,37 @@ + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.jvm.tasks.Jar + +description = "Kotlin Scripting JSR-223 support" + +plugins { java } + +val packedJars by configurations.creating + +dependencies { + packedJars(project(":kotlin-scripting-jsr223")) { isTransitive = false } + runtime(project(":kotlin-script-runtime")) + runtime(kotlinStdlib()) + runtime(project(":kotlin-scripting-common")) + runtime(project(":kotlin-scripting-jvm")) + runtime(project(":kotlin-script-util")) + runtime(projectRuntimeJar(":kotlin-compiler-embeddable")) + runtime(project(":kotlin-scripting-compiler-embeddable")) +} + +sourceSets { + "main" {} + "test" {} +} + +publish() + +noDefaultJar() + +runtimeJar(rewriteDepsToShadedCompiler( + task("shadowJar") { + from(packedJars) + } +)) +sourcesJar() +javadocJar() diff --git a/libraries/scripting/jsr223/build.gradle.kts b/libraries/scripting/jsr223/build.gradle.kts new file mode 100644 index 00000000000..6aec99e48ce --- /dev/null +++ b/libraries/scripting/jsr223/build.gradle.kts @@ -0,0 +1,32 @@ + +plugins { + kotlin("jvm") +} + +jvmTarget = "1.6" + +dependencies { + compile(project(":kotlin-script-runtime")) + compile(kotlinStdlib()) + compile(project(":kotlin-scripting-common")) + compile(project(":kotlin-scripting-jvm")) + compile(project(":kotlin-scripting-jvm-host")) + compile(project(":kotlin-script-util")) + compile(project(":kotlin-scripting-compiler")) + compile(project(":kotlin-scripting-compiler-impl")) + compileOnly(project(":compiler:cli")) + compileOnly(project(":kotlin-reflect-api")) + compileOnly(intellijCoreDep()) + runtime(projectRuntimeJar(":kotlin-compiler")) + runtime(project(":kotlin-reflect")) +} + +sourceSets { + "main" { projectDefault() } + "test" { projectDefault() } +} + +publish() + +standardPublicJars() + diff --git a/libraries/scripting/jsr223/resources/META-INF/services/javax.script.ScriptEngineFactory b/libraries/scripting/jsr223/resources/META-INF/services/javax.script.ScriptEngineFactory new file mode 100644 index 00000000000..e461416460d --- /dev/null +++ b/libraries/scripting/jsr223/resources/META-INF/services/javax.script.ScriptEngineFactory @@ -0,0 +1,2 @@ +kotlin.script.experimental.jsr223.KotlinJsr223JvmScriptEngineFactory + diff --git a/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223DefaultScript.kt b/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223DefaultScript.kt new file mode 100644 index 00000000000..0b36735aac8 --- /dev/null +++ b/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223DefaultScript.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package kotlin.script.experimental.jsr223 + +import org.jetbrains.kotlin.cli.common.repl.KOTLIN_SCRIPT_ENGINE_BINDINGS_KEY +import org.jetbrains.kotlin.cli.common.repl.KOTLIN_SCRIPT_STATE_BINDINGS_KEY +import javax.script.Bindings +import javax.script.ScriptEngine +import kotlin.script.experimental.annotations.KotlinScript +import kotlin.script.templates.ScriptTemplateDefinition +import kotlin.script.templates.standard.ScriptTemplateWithBindings + +@Suppress("unused") +@KotlinScript +abstract class KotlinJsr223DefaultScript(val jsr223Bindings: Bindings) : ScriptTemplateWithBindings(jsr223Bindings) { + + private val myEngine: ScriptEngine? get() = bindings[KOTLIN_SCRIPT_ENGINE_BINDINGS_KEY]?.let { it as? ScriptEngine } + + private inline fun withMyEngine(body: (ScriptEngine) -> T): T = + myEngine?.let(body) ?: throw IllegalStateException("Script engine for `eval` call is not found") + + fun eval(script: String, newBindings: Bindings): Any? = + withMyEngine { + val savedState = + newBindings[KOTLIN_SCRIPT_STATE_BINDINGS_KEY]?.takeIf { it === this.jsr223Bindings[KOTLIN_SCRIPT_STATE_BINDINGS_KEY] } + ?.apply { + newBindings[KOTLIN_SCRIPT_STATE_BINDINGS_KEY] = null + } + val res = it.eval(script, newBindings) + savedState?.apply { + newBindings[KOTLIN_SCRIPT_STATE_BINDINGS_KEY] = savedState + } + res + } + + fun eval(script: String): Any? = + withMyEngine { + val savedState = jsr223Bindings.remove(KOTLIN_SCRIPT_STATE_BINDINGS_KEY) + val res = it.eval(script, jsr223Bindings) + savedState?.apply { + jsr223Bindings[KOTLIN_SCRIPT_STATE_BINDINGS_KEY] = savedState + } + res + } + + fun createBindings(): Bindings = withMyEngine { it.createBindings() } +} \ No newline at end of file diff --git a/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223InvocableScriptEngine.kt b/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223InvocableScriptEngine.kt new file mode 100644 index 00000000000..a2a5f7ab62c --- /dev/null +++ b/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223InvocableScriptEngine.kt @@ -0,0 +1,93 @@ +/* + * 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 kotlin.script.experimental.jsr223 + +import org.jetbrains.kotlin.cli.common.repl.InvokeWrapper +import org.jetbrains.kotlin.cli.common.repl.renderReplStackTrace +import org.jetbrains.kotlin.utils.tryCreateCallableMapping +import java.lang.reflect.Proxy +import javax.script.Invocable +import javax.script.ScriptException +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.full.functions +import kotlin.reflect.full.safeCast + +interface KotlinJsr223InvocableScriptEngine : Invocable { + + val invokeWrapper: InvokeWrapper? + + val backwardInstancesHistory: Sequence + + val baseClassLoader: ClassLoader + + fun instancesForInvokeSearch(requestedReceiver: Any) = + sequenceOf(requestedReceiver) + backwardInstancesHistory.filterNot { it == requestedReceiver } + + override fun invokeFunction(name: String?, vararg args: Any?): Any? { + if (name == null) throw java.lang.NullPointerException("function name cannot be null") + return invokeImpl(backwardInstancesHistory, name, args) + } + + 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 invokeImpl(instancesForInvokeSearch(thiz), name, args) + } + + private fun invokeImpl(possibleReceivers: Sequence, name: String, args: Array): Any? { + // TODO: cache the method lookups? + + val (fn, mapping) = possibleReceivers.mapNotNull { instance -> + val candidates = instance::class.functions.filter { it.name == name } + candidates.findMapping(listOf(instance) + args) + }.filterNotNull().firstOrNull() ?: throw NoSuchMethodException("no suitable function '$name' found") + + val res = try { + if (invokeWrapper != null) { + invokeWrapper!!.invoke { + fn.callBy(mapping) + } + } else { + fn.callBy(mapping) + } + } catch (e: Throwable) { + // ignore everything in the stack trace until this constructor call + throw ScriptException(renderReplStackTrace(e.cause!!, startFromMethodName = fn.name)) + } + return if (fn.returnType.classifier == Unit::class) Unit else res + } + + + override fun getInterface(clasz: Class?): T? { + return proxyInterface(backwardInstancesHistory, clasz) + } + + override fun getInterface(thiz: Any?, clasz: Class?): T? { + if (thiz == null) throw IllegalArgumentException("object cannot be null") + return proxyInterface(instancesForInvokeSearch(thiz), clasz) + } + + private fun proxyInterface(possibleReceivers: Sequence, clasz: Class?): T? { + if (clasz == null) throw IllegalArgumentException("class object cannot be null") + if (!clasz.isInterface) throw IllegalArgumentException("expecting interface") + + // TODO: cache the method lookups? + + val proxy = Proxy.newProxyInstance(baseClassLoader, arrayOf(clasz)) { _, method, args -> + invokeImpl(possibleReceivers, method.name, args ?: emptyArray()) + } + return clasz.kotlin.safeCast(proxy) + } +} + +private fun Iterable>.findMapping(args: List): Pair, Map>? { + for (fn in this) { + val mapping = tryCreateCallableMapping(fn, args) + if (mapping != null) return fn to mapping + } + return null +} \ No newline at end of file diff --git a/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223ScriptEngine.kt b/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223ScriptEngine.kt new file mode 100644 index 00000000000..bf0b42558b2 --- /dev/null +++ b/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223ScriptEngine.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package kotlin.script.experimental.jsr223 + +import org.jetbrains.kotlin.cli.common.repl.* +import java.util.concurrent.locks.ReentrantReadWriteLock +import javax.script.ScriptContext +import javax.script.ScriptEngineFactory +import kotlin.reflect.KClass +import kotlin.script.experimental.api.ScriptEvaluationConfiguration +import kotlin.script.experimental.jvm.baseClassLoader +import kotlin.script.experimental.jvm.dependenciesFromCurrentContext +import kotlin.script.experimental.jvm.jvm +import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate +import kotlin.script.experimental.jvmhost.createJvmEvaluationConfigurationFromTemplate +import kotlin.script.experimental.jvmhost.repl.JvmReplCompiler +import kotlin.script.experimental.jvmhost.repl.JvmReplEvaluator +import kotlin.script.experimental.jvmhost.repl.JvmReplEvaluatorState + +class KotlinJsr223ScriptEngine( + factory: ScriptEngineFactory, + val getScriptArgs: (ScriptContext, Array>?) -> ScriptArgsWithTypes?, + val scriptArgsTypes: Array>? +) : KotlinJsr223JvmScriptEngineBase(factory), KotlinJsr223InvocableScriptEngine { + + val compilationConfiguration = createJvmCompilationConfigurationFromTemplate { + jvm { + dependenciesFromCurrentContext(wholeClasspath = true) + } + } + + val evaluationConfiguration = createJvmEvaluationConfigurationFromTemplate() + + override val replCompiler: ReplCompiler by lazy { + JvmReplCompiler(compilationConfiguration) + } + private val localEvaluator by lazy { + GenericReplCompilingEvaluatorBase(replCompiler, JvmReplEvaluator(evaluationConfiguration)) + } + + override val replEvaluator: ReplFullEvaluator get() = localEvaluator + + internal val state: IReplStageState<*> get() = getCurrentState(getContext()) + + override fun createState(lock: ReentrantReadWriteLock): IReplStageState<*> = replEvaluator.createState(lock) + + override fun overrideScriptArgs(context: ScriptContext): ScriptArgsWithTypes? = getScriptArgs(context, scriptArgsTypes) + + override val invokeWrapper: InvokeWrapper? + get() = null + + override val backwardInstancesHistory: Sequence + get() = getCurrentState(getContext()).asState(JvmReplEvaluatorState::class.java).history.asReversed().asSequence().map { it.item } + + override val baseClassLoader: ClassLoader + get() = evaluationConfiguration[ScriptEvaluationConfiguration.jvm.baseClassLoader]!! +} + diff --git a/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223ScriptEngineFactory.kt b/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223ScriptEngineFactory.kt new file mode 100644 index 00000000000..5a08175d497 --- /dev/null +++ b/libraries/scripting/jsr223/src/kotlin/script/experimental/jsr223/KotlinJsr223ScriptEngineFactory.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +@file:Suppress("unused") + +// could be used externally in javax.script.ScriptEngineFactory META-INF file + +package kotlin.script.experimental.jsr223 + +import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineFactoryBase +import org.jetbrains.kotlin.cli.common.repl.ScriptArgsWithTypes +import org.jetbrains.kotlin.script.util.* +import javax.script.Bindings +import javax.script.ScriptContext +import javax.script.ScriptEngine + +class KotlinJsr223JvmScriptEngineFactory : KotlinJsr223JvmScriptEngineFactoryBase() { + + override fun getScriptEngine(): ScriptEngine = + KotlinJsr223ScriptEngine( + this, + { ctx, types -> ScriptArgsWithTypes(arrayOf(ctx.getBindings(ScriptContext.ENGINE_SCOPE)), types ?: emptyArray()) }, + arrayOf(Bindings::class) + ) +} + diff --git a/libraries/scripting/jsr223/test/kotlin/script/experimental/jsr223/test/KotlinJsr223ScriptEngineIT.kt b/libraries/scripting/jsr223/test/kotlin/script/experimental/jsr223/test/KotlinJsr223ScriptEngineIT.kt new file mode 100644 index 00000000000..3b24c352e72 --- /dev/null +++ b/libraries/scripting/jsr223/test/kotlin/script/experimental/jsr223/test/KotlinJsr223ScriptEngineIT.kt @@ -0,0 +1,214 @@ +/* + * 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 kotlin.script.experimental.jsr223.test + +import org.jetbrains.kotlin.cli.common.environment.setIdeaIoUseFallback +import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.junit.Assert +import org.junit.Test +import javax.script.* +import kotlin.script.experimental.jsr223.KotlinJsr223ScriptEngine + +class KotlinJsr223ScriptEngineIT { + + init { + setIdeaIoUseFallback() + } + + @Test + fun testEngineFactory() { + val factory = ScriptEngineManager().getEngineByExtension("kts").factory + Assert.assertNotNull(factory) + factory!!.apply { + Assert.assertEquals("kotlin", languageName) + Assert.assertEquals(KotlinCompilerVersion.VERSION, languageVersion) + Assert.assertEquals("kotlin", engineName) + Assert.assertEquals(KotlinCompilerVersion.VERSION, engineVersion) + Assert.assertEquals(listOf("kts"), extensions) + Assert.assertEquals(listOf("text/x-kotlin"), mimeTypes) + Assert.assertEquals(listOf("kotlin"), names) + Assert.assertEquals("obj.method(arg1, arg2, arg3)", getMethodCallSyntax("obj", "method", "arg1", "arg2", "arg3")) + Assert.assertEquals("print(\"Hello, world!\")", getOutputStatement("Hello, world!")) + Assert.assertEquals(KotlinCompilerVersion.VERSION, getParameter(ScriptEngine.LANGUAGE_VERSION)) + val sep = System.getProperty("line.separator") + val prog = arrayOf("val x: Int = 3", "var y = x + 2") + Assert.assertEquals(prog.joinToString(sep) + sep, getProgram(*prog)) + } + } + + @Test + fun testEngine() { + val factory = ScriptEngineManager().getEngineByExtension("kts").factory + Assert.assertNotNull(factory) + val engine = factory!!.scriptEngine + Assert.assertNotNull(engine as? KotlinJsr223ScriptEngine) + Assert.assertSame(factory, engine!!.factory) + val bindings = engine.createBindings() + Assert.assertTrue(bindings is SimpleBindings) + } + + @Test + fun testSimpleEval() { + val engine = ScriptEngineManager().getEngineByExtension("kts")!! + val res1 = engine.eval("val x = 3") + Assert.assertNull(res1) + val res2 = engine.eval("x + 2") + Assert.assertEquals(5, res2) + } + + @Test + fun testEvalWithError() { + val engine = ScriptEngineManager().getEngineByExtension("kts")!! + + try { + engine.eval("java.lang.fish") + Assert.fail("Script error expected") + } catch (e: ScriptException) {} + + val res1 = engine.eval("val x = 3") + Assert.assertNull(res1) + + try { + engine.eval("y") + Assert.fail("Script error expected") + } catch (e: ScriptException) { + Assert.assertTrue( + "Expected message to contain \"Unresolved reference: y\", actual: \"${e.message}\"", + e.message?.contains("Unresolved reference: y") ?: false + ) + } + + val res3 = engine.eval("x + 2") + Assert.assertEquals(5, res3) + } + + @Test + fun testEngineRepeatWithReset() { + val code = "open class A {}\n" + + "class B : A() {}" + val engine = ScriptEngineManager().getEngineByExtension("kts") as KotlinJsr223ScriptEngine + + val res1 = engine.eval(code) + Assert.assertNull(res1) + + engine.state.history.reset() + + engine.eval(code) + } + + @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) + // TODO: fix and restore +// assertThrows(NoSuchMethodException::class.java) { +// invocator!!.invokeMethod(res1, "fn", 3) +// } + val res3 = invocator!!.invokeMethod(res1, "fn1", 3) + Assert.assertEquals(6, res3) + } + + @Test + fun testSimpleCompilable() { + val engine = ScriptEngineManager().getEngineByExtension("kts") as KotlinJsr223ScriptEngine + val comp1 = engine.compile("val x = 3") + val comp2 = engine.compile("x + 2") + val res1 = comp1.eval() + Assert.assertNull(res1) + val res2 = comp2.eval() + Assert.assertEquals(5, res2) + } + + @Test + fun testMultipleCompilable() { + val engine = ScriptEngineManager().getEngineByExtension("kts") as KotlinJsr223ScriptEngine + val compiled1 = engine.compile("""listOf(1,2,3).joinToString(",")""") + val compiled2 = engine.compile("""val x = bindings["boundValue"] as Int + bindings["z"] as Int""") + val compiled3 = engine.compile("""x""") + + Assert.assertEquals("1,2,3", compiled1.eval()) + Assert.assertEquals("1,2,3", compiled1.eval()) + Assert.assertEquals("1,2,3", compiled1.eval()) + Assert.assertEquals("1,2,3", compiled1.eval()) + + engine.getBindings(ScriptContext.ENGINE_SCOPE).apply { + put("boundValue", 100) + put("z", 33) + } + + compiled2.eval() + + Assert.assertEquals(133, compiled3.eval()) + Assert.assertEquals(133, compiled3.eval()) + Assert.assertEquals(133, compiled3.eval()) + } + + @Test + fun testEvalWithContext() { + val engine = ScriptEngineManager().getEngineByExtension("kts")!! + + engine.put("z", 33) + + engine.eval("""val x = 10 + bindings["z"] as Int""") + + val result = engine.eval("""x + 20""") + Assert.assertEquals(63, result) + + // in the current implementation the history is shared between contexts, so "x" could also be used in this line, + // but this behaviour probably will not be preserved in the future, since contexts may become completely isolated + val result2 = engine.eval("""11 + bindings["boundValue"] as Int""", engine.createBindings().apply { + put("boundValue", 100) + }) + Assert.assertEquals(111, result2) + } + + @Test + fun testSimpleEvalInEval() { + val engine = ScriptEngineManager().getEngineByExtension("kts")!! + val res1 = engine.eval("val x = 3") + Assert.assertNull(res1) + val res2 = engine.eval("val y = eval(\"\$x + 2\") as Int\ny") + Assert.assertEquals(5, res2) + val res3 = engine.eval("y + 2") + Assert.assertEquals(7, res3) + } + + @Test + fun `kotlin script evaluation should support functional return types`() { + val scriptEngine = ScriptEngineManager().getEngineByExtension("kts")!! + + val script = "{1 + 2}" + val result = scriptEngine.eval(script) + + Assert.assertTrue(result is Function0<*>) + Assert.assertEquals(3, (result as Function0<*>).invoke()) + } +} + +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/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 7a78326e074..7fda64da0e3 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 @@ -33,19 +33,24 @@ class JvmReplEvaluator( invokeWrapper: InvokeWrapper? ): ReplEvalResult = state.lock.write { val evalState = state.asState(JvmReplEvaluatorState::class.java) + val history = evalState.history as ReplStageHistoryWithReplace val compiledScript = (compileResult.data as? KJvmCompiledScript<*>) ?: return ReplEvalResult.Error.CompileTime("Unable to access compiled script: ${compileResult.data}") - val lastSnippetInstance = evalState.history.peek()?.item + val lastSnippetInstance = history.peek()?.item + val historyBeforeSnippet = history.previousItems(compileResult.lineId) val currentConfiguration = ScriptEvaluationConfiguration(baseScriptEvaluationConfiguration) { - if (evalState.history.isNotEmpty()) { - previousSnippets.put(evalState.history.map { it.item }) + if (historyBeforeSnippet.any()) { + previousSnippets.put(historyBeforeSnippet.toList()) } if (lastSnippetInstance != null) { jvm { baseClassLoader(lastSnippetInstance::class.java.classLoader) } } + if (scriptArgs != null) { + constructorArgs(*scriptArgs.scriptArgs) + } } val res = runBlocking { scriptEvaluator(compiledScript, currentConfiguration) } @@ -53,7 +58,7 @@ class JvmReplEvaluator( when (res) { is ResultWithDiagnostics.Success -> when (val retVal = res.value.returnValue) { is ResultValue.Value -> { - evalState.history.push(compileResult.lineId, retVal.scriptInstance) + 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()) ReplEvalResult.ValueResult(retVal.name, retVal.value, retVal.type) @@ -61,12 +66,12 @@ class JvmReplEvaluator( ReplEvalResult.UnitResult() } is ResultValue.UnitValue -> { - evalState.history.push(compileResult.lineId, retVal.scriptInstance) + history.replaceOrPush(compileResult.lineId, retVal.scriptInstance) ReplEvalResult.UnitResult() } else -> throw IllegalStateException("Expecting value with script instance, got $retVal") } - else -> ReplEvalResult.Error.Runtime(res.reports.joinToString("\n") { it.message }) + else -> ReplEvalResult.Error.Runtime(res.reports.joinToString("\n") { it.message + (it.exception?.let { e -> ": $e" } ?: "") }) } } } @@ -75,10 +80,29 @@ open class JvmReplEvaluatorState( scriptEvaluationConfiguration: ScriptEvaluationConfiguration, override val lock: ReentrantReadWriteLock = ReentrantReadWriteLock() ) : IReplStageState { - - override val history: IReplStageHistory = BasicReplStageHistory(lock) + override val history: IReplStageHistory = ReplStageHistoryWithReplace(lock) override val currentGeneration: Int get() = (history as BasicReplStageHistory<*>).currentGeneration.get() - - val topClassLoader: ClassLoader = scriptEvaluationConfiguration[ScriptEvaluationConfiguration.jvm.baseClassLoader]!! } + +open class ReplStageHistoryWithReplace(lock: ReentrantReadWriteLock = ReentrantReadWriteLock()) : BasicReplStageHistory(lock) { + + fun replace(id: ILineId, item: T): Boolean = lock.write { + for (idx in indices) { + if (get(idx).id == id) { + set(idx, ReplHistoryRecord(id, item)) + return true + } + } + return false + } + + fun replaceOrPush(id: ILineId, item: T) { + if (!replace(id, item)) { + tryResetTo(id) + push(id, item) + } + } + + fun previousItems(id: ILineId): Sequence = asSequence().takeWhile { it.id.no < id.no }.map { it.item } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SubpluginsIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SubpluginsIT.kt index 38eccfa2092..db862d3a839 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SubpluginsIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/SubpluginsIT.kt @@ -160,6 +160,7 @@ class SubpluginsIT : BaseGradleIT() { assertCompiledKotlinSources(project.relativize(bobGreet, aliceGreet, worldGreet, greetScriptTemplateKt)) } } else { + assertFailed() val usedGradleVersion = GradleVersion.version( System.getProperty("kotlin.gradle.version.for.tests") diff --git a/settings.gradle b/settings.gradle index da2dc2f5f97..17f18aa8d18 100644 --- a/settings.gradle +++ b/settings.gradle @@ -185,6 +185,8 @@ include ":kotlin-build-common", ":kotlin-scripting-compiler-embeddable", ":kotlin-scripting-compiler-impl", ":kotlin-scripting-compiler-impl-embeddable", + ":kotlin-scripting-jsr223", + ":kotlin-scripting-jsr223-embeddable", ":kotlin-scripting-idea", ":kotlin-main-kts", ":kotlin-main-kts-test", @@ -357,6 +359,8 @@ project(':kotlin-scripting-common').projectDir = "$rootDir/libraries/scripting/c project(':kotlin-scripting-jvm').projectDir = "$rootDir/libraries/scripting/jvm" as File project(':kotlin-scripting-jvm-host').projectDir = "$rootDir/libraries/scripting/jvm-host" as File project(':kotlin-scripting-jvm-host-embeddable').projectDir = "$rootDir/libraries/scripting/jvm-host-embeddable" as File +project(':kotlin-scripting-jsr223').projectDir = "$rootDir/libraries/scripting/jsr223" as File +project(':kotlin-scripting-jsr223-embeddable').projectDir = "$rootDir/libraries/scripting/jsr223-embeddable" as File project(':kotlin-scripting-intellij').projectDir = "$rootDir/libraries/scripting/intellij" as File project(':kotlin-scripting-compiler').projectDir = "$rootDir/plugins/scripting/scripting-compiler" as File project(':kotlin-scripting-compiler-embeddable').projectDir = "$rootDir/plugins/scripting/scripting-compiler-embeddable" as File