From 4571e273a464efc8dba416f5ad4cd8bfbc12cd53 Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Mon, 7 Jan 2019 18:10:57 +0100 Subject: [PATCH] Add shared script instances support, fix and refactor evaluation accordingly also fix arguments order in the evaluator --- .../experimental/api/scriptEvaluation.kt | 19 +++- .../experimental/host/scriptHostUtil.kt | 5 +- .../jvmhost/impl/KJvmCompilerImpl.kt | 22 +++-- .../jvmhost/jvmScriptEvaluation.kt | 90 +++++++++---------- .../jvmhost/test/ScriptingHostTest.kt | 43 +++++++++ .../importTest/diamondImportCommon.kts | 4 + .../importTest/diamondImportMiddle.kts | 4 + 7 files changed, 130 insertions(+), 57 deletions(-) create mode 100644 libraries/scripting/jvm-host/testData/importTest/diamondImportCommon.kts create mode 100644 libraries/scripting/jvm-host/testData/importTest/diamondImportMiddle.kts diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptEvaluation.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptEvaluation.kt index 82853eafa2f..b90cbc5e209 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptEvaluation.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptEvaluation.kt @@ -7,6 +7,7 @@ package kotlin.script.experimental.api +import kotlin.reflect.KClass import kotlin.script.experimental.util.PropertiesCollection interface ScriptEvaluationConfigurationKeys @@ -46,11 +47,27 @@ val ScriptEvaluationConfigurationKeys.providedProperties by PropertiesCollection */ val ScriptEvaluationConfigurationKeys.constructorArgs by PropertiesCollection.key>() +/** + * A map that is used to store evaluated script instances; if provided - the evaluator will try to get imported script from the map and + * only create/evaluate instances if not found, and evaluator will put newly created instances into the map + * This allows to have a single instance of the script if it is imported several times via different import paths. + */ +val ScriptEvaluationConfigurationKeys.scriptsInstancesSharingMap by PropertiesCollection.key, Any>>() + +/** + * A helper to enable scriptsInstancesSharingMap with default implementation + */ +fun ScriptEvaluationConfiguration.Builder.enableScriptsInstancesSharing() { + this { + scriptsInstancesSharingMap(HashMap()) + } +} + /** * The script evaluation result value */ sealed class ResultValue { - class Value(val name: String, val value: Any?, val type: String) : ResultValue() { + class Value(val name: String, val value: Any?, val type: String, val scriptInstance: Any) : ResultValue() { override fun toString(): String = "$name: $type = $value" } diff --git a/libraries/scripting/common/src/kotlin/script/experimental/host/scriptHostUtil.kt b/libraries/scripting/common/src/kotlin/script/experimental/host/scriptHostUtil.kt index 277e740a3a4..3f5df2c9a1d 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/host/scriptHostUtil.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/host/scriptHostUtil.kt @@ -66,15 +66,14 @@ fun File.toScriptSource(): SourceCode = FileScriptSource(this) /** * The implementation of the ScriptSource for a script in a String */ -open class StringScriptSource(val source: String) : SourceCode { +open class StringScriptSource(val source: String, override val name: String? = null) : SourceCode { override val text: String get() = source - override val name: String? = null override val locationId: String? = null } /** * Converts the String into the SourceCode */ -fun String.toScriptSource(): SourceCode = StringScriptSource(this) +fun String.toScriptSource(name: String? = null): SourceCode = StringScriptSource(this, name) diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt index e60a0168cfb..67fc5dd2621 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt @@ -43,6 +43,7 @@ import kotlin.reflect.KType import kotlin.reflect.full.starProjectedType import kotlin.script.experimental.api.* import kotlin.script.experimental.dependencies.DependenciesResolver +import kotlin.script.experimental.host.FileScriptSource import kotlin.script.experimental.host.ScriptingHostConfiguration import kotlin.script.experimental.host.getMergedScriptText import kotlin.script.experimental.host.getScriptingClass @@ -136,13 +137,9 @@ class KJvmCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : KJvm val psiFileFactory: PsiFileFactoryImpl = PsiFileFactory.getInstance(environment.project) as PsiFileFactoryImpl val scriptText = getMergedScriptText(script, updatedConfiguration) val scriptFileName = script.name ?: "script.${updatedConfiguration[ScriptCompilationConfiguration.fileExtension]}" - val virtualFile = LightVirtualFile( - scriptFileName, - KotlinLanguage.INSTANCE, - StringUtil.convertLineSeparators(scriptText) - ).apply { - charset = CharsetToolkit.UTF8_CHARSET - } + + val virtualFile = ScriptLightVirtualFile(scriptFileName, (script as? FileScriptSource)?.file?.path, scriptText) + val psiFile: KtFile = psiFileFactory.trySetupPsiForFile(virtualFile, KotlinLanguage.INSTANCE, true, false) as KtFile? ?: return failure("Unable to make PSI file from script") @@ -322,3 +319,14 @@ internal class BridgeScriptDefinition( hostConfiguration ) } + +internal class ScriptLightVirtualFile(name: String, private val _path: String?, text: String) : + LightVirtualFile(name, KotlinLanguage.INSTANCE, StringUtil.convertLineSeparators(text)) { + + init { + charset = CharsetToolkit.UTF8_CHARSET + } + + override fun getPath(): String = _path ?: super.getPath() + override fun getCanonicalPath(): String? = path +} \ No newline at end of file diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptEvaluation.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptEvaluation.kt index 4703212e559..f2e68c30263 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptEvaluation.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptEvaluation.kt @@ -28,66 +28,60 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator { scriptEvaluationConfiguration: ScriptEvaluationConfiguration? ): ResultWithDiagnostics = try { - val res = compiledScript.getClass(scriptEvaluationConfiguration) - when (res) { - is ResultWithDiagnostics.Failure -> res - is ResultWithDiagnostics.Success -> { - // in the future, when (if) we'll stop to compile everything into constructor - // run as SAM - // return res - val scriptClass = res.value + compiledScript.getClass(scriptEvaluationConfiguration).onSuccess { scriptClass -> + // in the future, when (if) we'll stop to compile everything into constructor + // run as SAM + // return res + + // for other scripts we need evaluation configuration with actualClassloader set, + // so they are loaded in the same classloader as the "main" script + val updatedEvalConfiguration = when { + scriptEvaluationConfiguration == null -> ScriptEvaluationConfiguration { + // TODO: find out why dsl syntax doesn't work here + set(JvmScriptEvaluationConfiguration.actualClassLoader, scriptClass.java.classLoader) + } + scriptEvaluationConfiguration.getNoDefault(JvmScriptEvaluationConfiguration.actualClassLoader) == null -> + ScriptEvaluationConfiguration(scriptEvaluationConfiguration) { + // TODO: find out why dsl syntax doesn't work here + set(JvmScriptEvaluationConfiguration.actualClassLoader, scriptClass.java.classLoader) + } + else -> scriptEvaluationConfiguration + } + + val sharedScripts = scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.scriptsInstancesSharingMap) + + val instanceFromShared = sharedScripts?.get(scriptClass) + + if (instanceFromShared != null) { + instanceFromShared.asSuccess(updatedEvalConfiguration) + } else { + val args = ArrayList() + + updatedEvalConfiguration[ScriptEvaluationConfiguration.constructorArgs]?.let { + args.addAll(it) + } scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.providedProperties)?.forEach { args.add(it.value) } scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.implicitReceivers)?.let { args.addAll(it) } - val importedScriptsReports = ArrayList() - var importedScriptsLoadingFailed = false - // for other scripts we need evaluation configuration with actualClassloader set, - // so they are loaded in the same classloader as the "main" script - val updatedEvalConfiguration = when { - scriptEvaluationConfiguration == null -> ScriptEvaluationConfiguration { - // TODO: find out why dsl syntax doesn't work here - set(JvmScriptEvaluationConfiguration.actualClassLoader, scriptClass.java.classLoader) - } - scriptEvaluationConfiguration.getNoDefault(JvmScriptEvaluationConfiguration.actualClassLoader) == null -> ScriptEvaluationConfiguration(scriptEvaluationConfiguration) { - // TODO: find out why dsl syntax doesn't work here - set(JvmScriptEvaluationConfiguration.actualClassLoader, scriptClass.java.classLoader) - } - else -> scriptEvaluationConfiguration - } + compiledScript.otherScripts.mapSuccess { + invoke(it, updatedEvalConfiguration) + }.onSuccess { importedScriptsEvalResults -> - compiledScript.otherScripts.forEach { - // TODO: in the future other scripts could be used for other purposes, so args here should be added only for actually imported scripts - // (it means that we should keep mapping somewhere (or reuse one with source dependencies) between imported scrips and e.g. fqnames) - val importedScriptEvalRes = invoke(it, updatedEvalConfiguration) - importedScriptsReports.addAll(importedScriptEvalRes.reports) - when (importedScriptEvalRes) { - is ResultWithDiagnostics.Success -> { - // TODO: checks and diagnostics - args.add((importedScriptEvalRes.value.returnValue as ResultValue.Value).value) - } - else -> { - importedScriptsLoadingFailed = true - return@forEach - } + importedScriptsEvalResults.forEach { + args.add((it.returnValue as ResultValue.Value).scriptInstance) } - } - if (importedScriptsLoadingFailed) { - ResultWithDiagnostics.Failure(importedScriptsReports) - } else { - updatedEvalConfiguration[ScriptEvaluationConfiguration.constructorArgs]?.let { - args.addAll(it) - } val ctor = scriptClass.java.constructors.single() val instance = ctor.newInstance(*args.toArray()) - // TODO: fix result value - ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Value("", instance, ""), updatedEvalConfiguration)) + sharedScripts?.put(scriptClass, instance) + + instance.asSuccess(updatedEvalConfiguration) } } } @@ -95,3 +89,7 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator { ResultWithDiagnostics.Failure(e.asDiagnostics("Error evaluating script", path = compiledScript.sourceLocationId)) } } + +private fun Any.asSuccess(updatedEvalConfiguration: ScriptEvaluationConfiguration) = +// TODO: fix result value when ready + ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Value("", this, "", this), updatedEvalConfiguration)) diff --git a/libraries/scripting/jvm-host/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt b/libraries/scripting/jvm-host/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt index 33fe6744ab9..f4ff8c59717 100644 --- a/libraries/scripting/jvm-host/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt +++ b/libraries/scripting/jvm-host/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt @@ -75,6 +75,49 @@ class ScriptingHostTest : TestCase() { Assert.assertEquals(greeting, output) } + @Test + fun testDiamondImportWithoutSharing() { + val greeting = listOf("Hi from common", "Hi from middle", "Hi from common", "sharedVar == 3") + val output = doDiamondImportTest() + Assert.assertEquals(greeting, output) + } + + @Test + fun testDiamondImportWithSharing() { + val greeting = listOf("Hi from common", "Hi from middle", "sharedVar == 5") + val output = doDiamondImportTest( + ScriptEvaluationConfiguration { + enableScriptsInstancesSharing() + } + ) + Assert.assertEquals(greeting, output) + } + + private fun doDiamondImportTest(evaluationConfiguration: ScriptEvaluationConfiguration? = null): List { + val mainScript = "sharedVar += 1\nprintln(\"sharedVar == \$sharedVar\")".toScriptSource("main.kts") + val middleScript = File(TEST_DATA_DIR, "importTest/diamondImportMiddle.kts").toScriptSource() + val commonScript = File(TEST_DATA_DIR, "importTest/diamondImportCommon.kts").toScriptSource() + val compilationConfiguration = createJvmCompilationConfigurationFromTemplate { + refineConfiguration { + beforeCompiling { ctx -> + when (ctx.script.name) { + "main.kts" -> ScriptCompilationConfiguration(ctx.compilationConfiguration) { + importScripts(middleScript, commonScript) + } + "diamondImportMiddle.kts" -> ScriptCompilationConfiguration(ctx.compilationConfiguration) { + importScripts(commonScript) + } + else -> ctx.compilationConfiguration + }.asSuccess() + } + } + } + val output = captureOut { + BasicJvmScriptingHost().eval(mainScript, compilationConfiguration, evaluationConfiguration).throwOnFailure() + }.lines() + return output + } + @Test fun testImportError() { val script = "println(\"Hello from imported \$helloScriptName script!\")" diff --git a/libraries/scripting/jvm-host/testData/importTest/diamondImportCommon.kts b/libraries/scripting/jvm-host/testData/importTest/diamondImportCommon.kts new file mode 100644 index 00000000000..594714c1e71 --- /dev/null +++ b/libraries/scripting/jvm-host/testData/importTest/diamondImportCommon.kts @@ -0,0 +1,4 @@ + +println("Hi from common") + +var sharedVar = 2 diff --git a/libraries/scripting/jvm-host/testData/importTest/diamondImportMiddle.kts b/libraries/scripting/jvm-host/testData/importTest/diamondImportMiddle.kts new file mode 100644 index 00000000000..29884bb8083 --- /dev/null +++ b/libraries/scripting/jvm-host/testData/importTest/diamondImportMiddle.kts @@ -0,0 +1,4 @@ + +println("Hi from middle") + +sharedVar *= 2