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
|
package kotlin.script.experimental.api
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
import kotlin.script.experimental.util.PropertiesCollection
|
import kotlin.script.experimental.util.PropertiesCollection
|
||||||
|
|
||||||
interface ScriptEvaluationConfigurationKeys
|
interface ScriptEvaluationConfigurationKeys
|
||||||
@@ -46,11 +47,27 @@ val ScriptEvaluationConfigurationKeys.providedProperties by PropertiesCollection
|
|||||||
*/
|
*/
|
||||||
val ScriptEvaluationConfigurationKeys.constructorArgs by PropertiesCollection.key<List<Any?>>()
|
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
|
* The script evaluation result value
|
||||||
*/
|
*/
|
||||||
sealed class ResultValue {
|
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"
|
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
|
* 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 text: String get() = source
|
||||||
|
|
||||||
override val name: String? = null
|
|
||||||
override val locationId: String? = null
|
override val locationId: String? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the String into the SourceCode
|
* 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.reflect.full.starProjectedType
|
||||||
import kotlin.script.experimental.api.*
|
import kotlin.script.experimental.api.*
|
||||||
import kotlin.script.experimental.dependencies.DependenciesResolver
|
import kotlin.script.experimental.dependencies.DependenciesResolver
|
||||||
|
import kotlin.script.experimental.host.FileScriptSource
|
||||||
import kotlin.script.experimental.host.ScriptingHostConfiguration
|
import kotlin.script.experimental.host.ScriptingHostConfiguration
|
||||||
import kotlin.script.experimental.host.getMergedScriptText
|
import kotlin.script.experimental.host.getMergedScriptText
|
||||||
import kotlin.script.experimental.host.getScriptingClass
|
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 psiFileFactory: PsiFileFactoryImpl = PsiFileFactory.getInstance(environment.project) as PsiFileFactoryImpl
|
||||||
val scriptText = getMergedScriptText(script, updatedConfiguration)
|
val scriptText = getMergedScriptText(script, updatedConfiguration)
|
||||||
val scriptFileName = script.name ?: "script.${updatedConfiguration[ScriptCompilationConfiguration.fileExtension]}"
|
val scriptFileName = script.name ?: "script.${updatedConfiguration[ScriptCompilationConfiguration.fileExtension]}"
|
||||||
val virtualFile = LightVirtualFile(
|
|
||||||
scriptFileName,
|
val virtualFile = ScriptLightVirtualFile(scriptFileName, (script as? FileScriptSource)?.file?.path, scriptText)
|
||||||
KotlinLanguage.INSTANCE,
|
|
||||||
StringUtil.convertLineSeparators(scriptText)
|
|
||||||
).apply {
|
|
||||||
charset = CharsetToolkit.UTF8_CHARSET
|
|
||||||
}
|
|
||||||
val psiFile: KtFile = psiFileFactory.trySetupPsiForFile(virtualFile, KotlinLanguage.INSTANCE, true, false) as KtFile?
|
val psiFile: KtFile = psiFileFactory.trySetupPsiForFile(virtualFile, KotlinLanguage.INSTANCE, true, false) as KtFile?
|
||||||
?: return failure("Unable to make PSI file from script")
|
?: return failure("Unable to make PSI file from script")
|
||||||
|
|
||||||
@@ -322,3 +319,14 @@ internal class BridgeScriptDefinition(
|
|||||||
hostConfiguration
|
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?
|
scriptEvaluationConfiguration: ScriptEvaluationConfiguration?
|
||||||
): ResultWithDiagnostics<EvaluationResult> =
|
): ResultWithDiagnostics<EvaluationResult> =
|
||||||
try {
|
try {
|
||||||
val res = compiledScript.getClass(scriptEvaluationConfiguration)
|
compiledScript.getClass(scriptEvaluationConfiguration).onSuccess { scriptClass ->
|
||||||
when (res) {
|
// in the future, when (if) we'll stop to compile everything into constructor
|
||||||
is ResultWithDiagnostics.Failure -> res
|
// run as SAM
|
||||||
is ResultWithDiagnostics.Success -> {
|
// return res
|
||||||
// in the future, when (if) we'll stop to compile everything into constructor
|
|
||||||
// run as SAM
|
// for other scripts we need evaluation configuration with actualClassloader set,
|
||||||
// return res
|
// so they are loaded in the same classloader as the "main" script
|
||||||
val scriptClass = res.value
|
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?>()
|
val args = ArrayList<Any?>()
|
||||||
|
|
||||||
|
updatedEvalConfiguration[ScriptEvaluationConfiguration.constructorArgs]?.let {
|
||||||
|
args.addAll(it)
|
||||||
|
}
|
||||||
scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.providedProperties)?.forEach {
|
scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.providedProperties)?.forEach {
|
||||||
args.add(it.value)
|
args.add(it.value)
|
||||||
}
|
}
|
||||||
scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.implicitReceivers)?.let {
|
scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.implicitReceivers)?.let {
|
||||||
args.addAll(it)
|
args.addAll(it)
|
||||||
}
|
}
|
||||||
val importedScriptsReports = ArrayList<ScriptDiagnostic>()
|
|
||||||
var importedScriptsLoadingFailed = false
|
|
||||||
|
|
||||||
// for other scripts we need evaluation configuration with actualClassloader set,
|
compiledScript.otherScripts.mapSuccess {
|
||||||
// so they are loaded in the same classloader as the "main" script
|
invoke(it, updatedEvalConfiguration)
|
||||||
val updatedEvalConfiguration = when {
|
}.onSuccess { importedScriptsEvalResults ->
|
||||||
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.forEach {
|
importedScriptsEvalResults.forEach {
|
||||||
// TODO: in the future other scripts could be used for other purposes, so args here should be added only for actually imported scripts
|
args.add((it.returnValue as ResultValue.Value).scriptInstance)
|
||||||
// (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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (importedScriptsLoadingFailed) {
|
|
||||||
ResultWithDiagnostics.Failure(importedScriptsReports)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
updatedEvalConfiguration[ScriptEvaluationConfiguration.constructorArgs]?.let {
|
|
||||||
args.addAll(it)
|
|
||||||
}
|
|
||||||
val ctor = scriptClass.java.constructors.single()
|
val ctor = scriptClass.java.constructors.single()
|
||||||
val instance = ctor.newInstance(*args.toArray())
|
val instance = ctor.newInstance(*args.toArray())
|
||||||
|
|
||||||
// TODO: fix result value
|
sharedScripts?.put(scriptClass, instance)
|
||||||
ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Value("", instance, ""), updatedEvalConfiguration))
|
|
||||||
|
instance.asSuccess(updatedEvalConfiguration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,3 +89,7 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator {
|
|||||||
ResultWithDiagnostics.Failure(e.asDiagnostics("Error evaluating script", path = compiledScript.sourceLocationId))
|
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)
|
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
|
@Test
|
||||||
fun testImportError() {
|
fun testImportError() {
|
||||||
val script = "println(\"Hello from imported \$helloScriptName script!\")"
|
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