Implement import test, fix import support in compiler and evaluator

This commit is contained in:
Ilya Chernikov
2018-12-09 22:35:39 +01:00
parent fce68102bd
commit 1e74ab5a62
10 changed files with 133 additions and 49 deletions
@@ -234,6 +234,9 @@ interface CompiledScript<out ScriptBase : Any> {
*/
suspend fun getClass(scriptEvaluationConfiguration: ScriptEvaluationConfiguration?): ResultWithDiagnostics<KClass<*>>
val importedScripts: List<CompiledScript<*>>?
get() = null
/**
* The scripts compiled along with this one in one module, imported or otherwise included into compilation
*/
val otherScripts: List<CompiledScript<*>>
get() = emptyList()
}
@@ -42,9 +42,9 @@ fun getMergedScriptText(script: SourceCode, configuration: ScriptCompilationConf
/**
* The implementation of the SourceCode for a script located in a file
*/
open class FileScriptSource(val file: File) : ExternalSourceCode {
open class FileScriptSource(val file: File, private val preloadedText: String? = null) : ExternalSourceCode {
override val externalLocation: URL get() = file.toURI().toURL()
override val text: String by lazy { file.readText() }
override val text: String by lazy { preloadedText ?: file.readText() }
}
/**
@@ -14,37 +14,49 @@ import kotlin.reflect.KClass
import kotlin.script.experimental.api.*
import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvmhost.JvmScriptEvaluationConfiguration
import kotlin.script.experimental.jvmhost.actualClassLoader
import kotlin.script.experimental.jvmhost.baseClassLoader
class KJvmCompiledModule(
generationState: GenerationState
) : Serializable {
val compilerOutputFiles: Map<String, ByteArray> =
generationState.factory.asList()
.associateTo(sortedMapOf<String, ByteArray>()) { it.relativePath to it.asByteArray() }
companion object {
@JvmStatic
private val serialVersionUID = 0L
}
}
class KJvmCompiledScript<out ScriptBase : Any>(
compilationConfiguration: ScriptCompilationConfiguration,
generationState: GenerationState,
private var scriptClassFQName: String,
override val importedScripts: List<CompiledScript<*>>? = null
override val otherScripts: List<CompiledScript<*>> = emptyList(),
private var compiledModule: KJvmCompiledModule? = null
) : CompiledScript<ScriptBase>, Serializable {
private var _compilationConfiguration: ScriptCompilationConfiguration? = compilationConfiguration
private var compilerOutputFiles: Map<String, ByteArray> = run {
val res = sortedMapOf<String, ByteArray>()
for (it in generationState.factory.asList()) {
res[it.relativePath] = it.asByteArray()
}
res
}
override val compilationConfiguration: ScriptCompilationConfiguration
get() = _compilationConfiguration!!
override suspend fun getClass(scriptEvaluationConfiguration: ScriptEvaluationConfiguration?): ResultWithDiagnostics<KClass<*>> = try {
val baseClassLoader = scriptEvaluationConfiguration?.get(JvmScriptEvaluationConfiguration.baseClassLoader)
?: Thread.currentThread().contextClassLoader
val dependencies = compilationConfiguration[ScriptCompilationConfiguration.dependencies]
?.flatMap { (it as? JvmDependency)?.classpath?.map { it.toURI().toURL() } ?: emptyList() }
// TODO: previous dependencies and classloaders should be taken into account here
val classLoaderWithDeps =
if (dependencies == null) baseClassLoader
else URLClassLoader(dependencies.toTypedArray(), baseClassLoader)
val classLoader = CompiledScriptClassLoader(classLoaderWithDeps, compilerOutputFiles)
val classLoader = scriptEvaluationConfiguration?.get(JvmScriptEvaluationConfiguration.actualClassLoader)
?: run {
if (compiledModule == null)
return ResultWithDiagnostics.Failure("Unable to load class $scriptClassFQName: no compiled module is provided".asErrorDiagnostics())
val baseClassLoader = scriptEvaluationConfiguration?.get(JvmScriptEvaluationConfiguration.baseClassLoader)
?: Thread.currentThread().contextClassLoader
val dependencies = compilationConfiguration[ScriptCompilationConfiguration.dependencies]
?.flatMap { (it as? JvmDependency)?.classpath?.map { it.toURI().toURL() } ?: emptyList() }
// TODO: previous dependencies and classloaders should be taken into account here
val classLoaderWithDeps =
if (dependencies == null) baseClassLoader
else URLClassLoader(dependencies.toTypedArray(), baseClassLoader)
CompiledScriptClassLoader(classLoaderWithDeps, compiledModule!!.compilerOutputFiles)
}
val clazz = classLoader.loadClass(scriptClassFQName).kotlin
clazz.asSuccess()
@@ -65,13 +77,13 @@ class KJvmCompiledScript<out ScriptBase : Any>(
}
private fun writeObject(outputStream: ObjectOutputStream) {
outputStream.writeObject(compilerOutputFiles)
outputStream.writeObject(compiledModule)
outputStream.writeObject(scriptClassFQName)
}
private fun readObject(inputStream: ObjectInputStream) {
_compilationConfiguration = null
compilerOutputFiles = inputStream.readObject() as Map<String, ByteArray>
compiledModule = inputStream.readObject() as KJvmCompiledModule
scriptClassFQName = inputStream.readObject() as String
}
@@ -38,6 +38,7 @@ import org.jetbrains.kotlin.script.KotlinScriptDefinition
import org.jetbrains.kotlin.script.util.KotlinJars
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import java.io.File
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.full.starProjectedType
@@ -172,20 +173,38 @@ class KJvmCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : KJvm
sourceFiles,
kotlinCompilerConfiguration
).build()
KotlinCodegenFacade.compileCorrectFiles(generationState, CompilationErrorHandler.THROW_EXCEPTION)
fun makeCompiledScript(script: KtScript): KJvmCompiledScript<*> {
val scriptDependenciesStack = ArrayDeque<KtScript>()
val importedScripts = sourceDependencies.find { it.script == script }?.sourceDependencies?.mapNotNull { sourceFile ->
sourceFile.declarations.firstIsInstanceOrNull<KtScript>()?.let { makeCompiledScript(it) }
} ?: emptyList()
fun makeOtherScripts(script: KtScript): List<KJvmCompiledScript<*>> {
return KJvmCompiledScript<Any>(updatedConfiguration, generationState, script.fqName.asString(), importedScripts)
// TODO: ensure that it is caught earlier (as well) since it would be more economical
if (scriptDependenciesStack.contains(script))
throw IllegalArgumentException("Unable to handle recursive script dependencies")
scriptDependenciesStack.push(script)
val containingKtFile = script.containingKtFile
val otherScripts: List<KJvmCompiledScript<*>> =
sourceDependencies.find { it.scriptFile == containingKtFile }?.sourceDependencies?.mapNotNull { sourceFile ->
sourceFile.declarations.firstIsInstanceOrNull<KtScript>()?.let {
KJvmCompiledScript<Any>(updatedConfiguration, it.fqName.asString(), makeOtherScripts(it))
}
} ?: emptyList()
scriptDependenciesStack.pop()
return otherScripts
}
val res = makeCompiledScript(ktScript)
val compiledScript = KJvmCompiledScript<Any>(
updatedConfiguration,
ktScript.fqName.asString(),
makeOtherScripts(ktScript),
KJvmCompiledModule(generationState)
)
return ResultWithDiagnostics.Success(res, messageCollector.diagnostics)
return ResultWithDiagnostics.Success(compiledScript, messageCollector.diagnostics)
} catch (ex: Throwable) {
return failure(ex.asDiagnostics())
}
@@ -17,6 +17,8 @@ open class JvmScriptEvaluationConfiguration : PropertiesCollection.Builder() {
val JvmScriptEvaluationConfiguration.baseClassLoader by PropertiesCollection.key<ClassLoader?>(Thread.currentThread().contextClassLoader)
val JvmScriptEvaluationConfiguration.actualClassLoader by PropertiesCollection.key<ClassLoader?>()
val ScriptEvaluationConfiguration.jvm get() = JvmScriptEvaluationConfiguration()
open class BasicJvmScriptEvaluator : ScriptEvaluator {
@@ -43,12 +45,30 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator {
}
val importedScriptsReports = ArrayList<ScriptDiagnostic>()
var importedScriptsLoadingFailed = false
compiledScript.importedScripts?.forEach {
val importedScriptEvalRes = invoke(it, scriptEvaluationConfiguration)
// 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.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 -> {
args.add(importedScriptEvalRes.value)
// TODO: checks and diagnostics
args.add((importedScriptEvalRes.value.returnValue as ResultValue.Value).value)
}
else -> {
importedScriptsLoadingFailed = true
@@ -60,14 +80,14 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator {
ResultWithDiagnostics.Failure(importedScriptsReports)
} else {
scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.constructorArgs)?.let {
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, ""), scriptEvaluationConfiguration))
ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Value("", instance, ""), updatedEvalConfiguration))
}
}
}
@@ -15,6 +15,7 @@ import java.security.MessageDigest
import kotlin.reflect.KClass
import kotlin.script.experimental.api.*
import kotlin.script.experimental.host.BasicScriptingHost
import kotlin.script.experimental.host.FileScriptSource
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
import kotlin.script.experimental.jvmhost.*
@@ -50,6 +51,31 @@ class ScriptingHostTest : TestCase() {
Assert.assertEquals(greeting, output)
}
@Test
fun testSimpleImport() {
val greeting = "Hello from helloWithVal script!\nHello from imported helloWithVal script!"
val script = "println(\"Hello from imported \$helloScriptName script!\")"
val compilationConfiguration = createJvmCompilationConfigurationFromTemplate<SimpleScriptTemplate> {
refineConfiguration {
beforeCompiling { ctx ->
val importedScript = File(TEST_DATA_DIR, "importTest/helloWithVal.kts")
if ((ctx.script as? FileScriptSource)?.file?.canonicalFile == importedScript.canonicalFile) {
ctx.compilationConfiguration
} else {
ScriptCompilationConfiguration(ctx.compilationConfiguration) {
importScripts(importedScript.toScriptSource())
}
}.asSuccess()
}
}
}
val output = captureOut {
BasicJvmScriptingHost().eval(script.toScriptSource(), compilationConfiguration, null).throwOnFailure()
}
Assert.assertEquals(greeting, output)
}
@Test
fun testMemoryCache() {
val script = "val x = 1\nprintln(\"x = \$x\")"
@@ -159,7 +185,11 @@ class ScriptingHostTest : TestCase() {
fun ResultWithDiagnostics<*>.throwOnFailure(): ResultWithDiagnostics<*> = apply {
if (this is ResultWithDiagnostics.Failure) {
throw Exception("Compilation/evaluation failed:\n ${reports.joinToString("\n ") { it.exception?.toString() ?: it.message }}")
val firstExceptionFromReports = reports.find { it.exception != null }?.exception
throw Exception(
"Compilation/evaluation failed:\n ${reports.joinToString("\n ") { it.exception?.toString() ?: it.message }}",
firstExceptionFromReports
)
}
}
@@ -0,0 +1,4 @@
val helloScriptName = "helloWithVal"
println("Hello from $helloScriptName script!")
@@ -14,6 +14,7 @@ import kotlin.script.experimental.dependencies.AsyncDependenciesResolver
import kotlin.script.experimental.dependencies.DependenciesResolver
import kotlin.script.experimental.dependencies.ScriptDependencies
import kotlin.script.experimental.dependencies.ScriptReport
import kotlin.script.experimental.host.FileScriptSource
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvm.compat.mapToLegacyScriptReportPosition
@@ -94,8 +95,8 @@ internal fun List<ScriptDiagnostic>.mapScriptReportsToDiagnostics() =
map { ScriptReport(it.message, mapToLegacyScriptReportSeverity(it.severity), mapToLegacyScriptReportPosition(it.location)) }
internal fun ScriptContents.toScriptSource(): SourceCode = when {
file != null -> FileScriptSource(file!!, text?.toString())
text != null -> text!!.toString().toScriptSource()
file != null -> file!!.toScriptSource()
else -> throw IllegalArgumentException("Unable to convert script contents $this into script source")
}