Add shared script instances support, fix and refactor evaluation accordingly
also fix arguments order in the evaluator
This commit is contained in:
@@ -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<List<Any?>>()
|
||||
|
||||
/**
|
||||
* 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<MutableMap<KClass<*>, 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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+15
-7
@@ -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
|
||||
}
|
||||
+44
-46
@@ -28,66 +28,60 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator {
|
||||
scriptEvaluationConfiguration: ScriptEvaluationConfiguration?
|
||||
): ResultWithDiagnostics<EvaluationResult> =
|
||||
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<Any?>()
|
||||
|
||||
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<ScriptDiagnostic>()
|
||||
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))
|
||||
|
||||
+43
@@ -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<String> {
|
||||
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<SimpleScriptTemplate> {
|
||||
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!\")"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
println("Hi from common")
|
||||
|
||||
var sharedVar = 2
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
println("Hi from middle")
|
||||
|
||||
sharedVar *= 2
|
||||
Reference in New Issue
Block a user