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
@@ -16,7 +16,6 @@
package org.jetbrains.kotlin.codegen.context package org.jetbrains.kotlin.codegen.context
import org.jetbrains.kotlin.codegen.AsmUtil
import org.jetbrains.kotlin.codegen.FieldInfo import org.jetbrains.kotlin.codegen.FieldInfo
import org.jetbrains.kotlin.codegen.OwnerKind import org.jetbrains.kotlin.codegen.OwnerKind
import org.jetbrains.kotlin.codegen.StackValue import org.jetbrains.kotlin.codegen.StackValue
@@ -35,7 +34,6 @@ import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny
import org.jetbrains.kotlin.resolve.jvm.AsmTypes import org.jetbrains.kotlin.resolve.jvm.AsmTypes
import org.jetbrains.kotlin.resolve.lazy.descriptors.script.ScriptProvidedPropertiesDescriptor import org.jetbrains.kotlin.resolve.lazy.descriptors.script.ScriptProvidedPropertiesDescriptor
import org.jetbrains.org.objectweb.asm.Type import org.jetbrains.org.objectweb.asm.Type
import kotlin.reflect.KClass
class ScriptContext( class ScriptContext(
val typeMapper: KotlinTypeMapper, val typeMapper: KotlinTypeMapper,
@@ -83,9 +81,8 @@ class ScriptContext(
scriptDescriptor.unsubstitutedPrimaryConstructor.valueParameters[ctorImplicitReceiversParametersStart + index].name.identifier scriptDescriptor.unsubstitutedPrimaryConstructor.valueParameters[ctorImplicitReceiversParametersStart + index].name.identifier
fun getImplicitReceiverType(index: Int): Type? { fun getImplicitReceiverType(index: Int): Type? {
val receivers = script.kotlinScriptDefinition.implicitReceivers val receiverParam = scriptDescriptor.unsubstitutedPrimaryConstructor.valueParameters[ctorImplicitReceiversParametersStart + index]
val kClass = receivers.getOrNull(index)?.classifier as? KClass<*> return state.typeMapper.mapType(receiverParam.type)
return kClass?.java?.classId?.let(AsmUtil::asmTypeByClassId)
} }
fun getProvidedPropertyName(index: Int): String = fun getProvidedPropertyName(index: Int): String =
@@ -113,10 +110,8 @@ class ScriptContext(
fun getScriptFieldName(scriptDescriptor: ScriptDescriptor): String { fun getScriptFieldName(scriptDescriptor: ScriptDescriptor): String {
val index = earlierScripts.indexOf(scriptDescriptor) val index = earlierScripts.indexOf(scriptDescriptor)
if (index < 0) { return if (index >= 0) "script$" + (index + 1)
throw IllegalStateException("Unregistered script: $scriptDescriptor") else "\$\$importedScript${scriptDescriptor.name.identifier}"
}
return "script$" + (index + 1)
} }
override fun toString(): String { override fun toString(): String {
@@ -711,7 +711,7 @@ data class ScriptsCompilationDependencies(
val sourceDependencies: List<SourceDependencies> val sourceDependencies: List<SourceDependencies>
) { ) {
data class SourceDependencies( data class SourceDependencies(
val script: KtFile, val scriptFile: KtFile,
val sourceDependencies: List<KtFile> val sourceDependencies: List<KtFile>
) )
} }
@@ -721,7 +721,7 @@ fun collectScriptsCompilationDependencies(
configuration: CompilerConfiguration, configuration: CompilerConfiguration,
project: Project, project: Project,
initialSources: Iterable<KtFile> initialSources: Iterable<KtFile>
): ScriptsCompilationDependencies{ ): ScriptsCompilationDependencies {
val collectedClassPath = ArrayList<File>() val collectedClassPath = ArrayList<File>()
val collectedSources = ArrayList<KtFile>() val collectedSources = ArrayList<KtFile>()
val collectedSourceDependencies = ArrayList<ScriptsCompilationDependencies.SourceDependencies>() val collectedSourceDependencies = ArrayList<ScriptsCompilationDependencies.SourceDependencies>()
@@ -234,6 +234,9 @@ interface CompiledScript<out ScriptBase : Any> {
*/ */
suspend fun getClass(scriptEvaluationConfiguration: ScriptEvaluationConfiguration?): ResultWithDiagnostics<KClass<*>> 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 * 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 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.api.*
import kotlin.script.experimental.jvm.JvmDependency import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvmhost.JvmScriptEvaluationConfiguration import kotlin.script.experimental.jvmhost.JvmScriptEvaluationConfiguration
import kotlin.script.experimental.jvmhost.actualClassLoader
import kotlin.script.experimental.jvmhost.baseClassLoader 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>( class KJvmCompiledScript<out ScriptBase : Any>(
compilationConfiguration: ScriptCompilationConfiguration, compilationConfiguration: ScriptCompilationConfiguration,
generationState: GenerationState,
private var scriptClassFQName: String, private var scriptClassFQName: String,
override val importedScripts: List<CompiledScript<*>>? = null override val otherScripts: List<CompiledScript<*>> = emptyList(),
private var compiledModule: KJvmCompiledModule? = null
) : CompiledScript<ScriptBase>, Serializable { ) : CompiledScript<ScriptBase>, Serializable {
private var _compilationConfiguration: ScriptCompilationConfiguration? = compilationConfiguration 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 override val compilationConfiguration: ScriptCompilationConfiguration
get() = _compilationConfiguration!! get() = _compilationConfiguration!!
override suspend fun getClass(scriptEvaluationConfiguration: ScriptEvaluationConfiguration?): ResultWithDiagnostics<KClass<*>> = try { override suspend fun getClass(scriptEvaluationConfiguration: ScriptEvaluationConfiguration?): ResultWithDiagnostics<KClass<*>> = try {
val baseClassLoader = scriptEvaluationConfiguration?.get(JvmScriptEvaluationConfiguration.baseClassLoader) val classLoader = scriptEvaluationConfiguration?.get(JvmScriptEvaluationConfiguration.actualClassLoader)
?: Thread.currentThread().contextClassLoader ?: run {
val dependencies = compilationConfiguration[ScriptCompilationConfiguration.dependencies] if (compiledModule == null)
?.flatMap { (it as? JvmDependency)?.classpath?.map { it.toURI().toURL() } ?: emptyList() } return ResultWithDiagnostics.Failure("Unable to load class $scriptClassFQName: no compiled module is provided".asErrorDiagnostics())
// TODO: previous dependencies and classloaders should be taken into account here val baseClassLoader = scriptEvaluationConfiguration?.get(JvmScriptEvaluationConfiguration.baseClassLoader)
val classLoaderWithDeps = ?: Thread.currentThread().contextClassLoader
if (dependencies == null) baseClassLoader val dependencies = compilationConfiguration[ScriptCompilationConfiguration.dependencies]
else URLClassLoader(dependencies.toTypedArray(), baseClassLoader) ?.flatMap { (it as? JvmDependency)?.classpath?.map { it.toURI().toURL() } ?: emptyList() }
val classLoader = CompiledScriptClassLoader(classLoaderWithDeps, compilerOutputFiles) // 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 val clazz = classLoader.loadClass(scriptClassFQName).kotlin
clazz.asSuccess() clazz.asSuccess()
@@ -65,13 +77,13 @@ class KJvmCompiledScript<out ScriptBase : Any>(
} }
private fun writeObject(outputStream: ObjectOutputStream) { private fun writeObject(outputStream: ObjectOutputStream) {
outputStream.writeObject(compilerOutputFiles) outputStream.writeObject(compiledModule)
outputStream.writeObject(scriptClassFQName) outputStream.writeObject(scriptClassFQName)
} }
private fun readObject(inputStream: ObjectInputStream) { private fun readObject(inputStream: ObjectInputStream) {
_compilationConfiguration = null _compilationConfiguration = null
compilerOutputFiles = inputStream.readObject() as Map<String, ByteArray> compiledModule = inputStream.readObject() as KJvmCompiledModule
scriptClassFQName = inputStream.readObject() as String 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.script.util.KotlinJars
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import java.io.File import java.io.File
import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.starProjectedType import kotlin.reflect.full.starProjectedType
@@ -172,20 +173,38 @@ class KJvmCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : KJvm
sourceFiles, sourceFiles,
kotlinCompilerConfiguration kotlinCompilerConfiguration
).build() ).build()
KotlinCodegenFacade.compileCorrectFiles(generationState, CompilationErrorHandler.THROW_EXCEPTION) 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 -> fun makeOtherScripts(script: KtScript): List<KJvmCompiledScript<*>> {
sourceFile.declarations.firstIsInstanceOrNull<KtScript>()?.let { makeCompiledScript(it) }
} ?: emptyList()
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) { } catch (ex: Throwable) {
return failure(ex.asDiagnostics()) 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.baseClassLoader by PropertiesCollection.key<ClassLoader?>(Thread.currentThread().contextClassLoader)
val JvmScriptEvaluationConfiguration.actualClassLoader by PropertiesCollection.key<ClassLoader?>()
val ScriptEvaluationConfiguration.jvm get() = JvmScriptEvaluationConfiguration() val ScriptEvaluationConfiguration.jvm get() = JvmScriptEvaluationConfiguration()
open class BasicJvmScriptEvaluator : ScriptEvaluator { open class BasicJvmScriptEvaluator : ScriptEvaluator {
@@ -43,12 +45,30 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator {
} }
val importedScriptsReports = ArrayList<ScriptDiagnostic>() val importedScriptsReports = ArrayList<ScriptDiagnostic>()
var importedScriptsLoadingFailed = false 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) importedScriptsReports.addAll(importedScriptEvalRes.reports)
when (importedScriptEvalRes) { when (importedScriptEvalRes) {
is ResultWithDiagnostics.Success -> { is ResultWithDiagnostics.Success -> {
args.add(importedScriptEvalRes.value) // TODO: checks and diagnostics
args.add((importedScriptEvalRes.value.returnValue as ResultValue.Value).value)
} }
else -> { else -> {
importedScriptsLoadingFailed = true importedScriptsLoadingFailed = true
@@ -60,14 +80,14 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator {
ResultWithDiagnostics.Failure(importedScriptsReports) ResultWithDiagnostics.Failure(importedScriptsReports)
} else { } else {
scriptEvaluationConfiguration?.get(ScriptEvaluationConfiguration.constructorArgs)?.let { updatedEvalConfiguration[ScriptEvaluationConfiguration.constructorArgs]?.let {
args.addAll(it) 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 // 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.reflect.KClass
import kotlin.script.experimental.api.* import kotlin.script.experimental.api.*
import kotlin.script.experimental.host.BasicScriptingHost import kotlin.script.experimental.host.BasicScriptingHost
import kotlin.script.experimental.host.FileScriptSource
import kotlin.script.experimental.host.toScriptSource import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
import kotlin.script.experimental.jvmhost.* import kotlin.script.experimental.jvmhost.*
@@ -50,6 +51,31 @@ class ScriptingHostTest : TestCase() {
Assert.assertEquals(greeting, output) 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 @Test
fun testMemoryCache() { fun testMemoryCache() {
val script = "val x = 1\nprintln(\"x = \$x\")" val script = "val x = 1\nprintln(\"x = \$x\")"
@@ -159,7 +185,11 @@ class ScriptingHostTest : TestCase() {
fun ResultWithDiagnostics<*>.throwOnFailure(): ResultWithDiagnostics<*> = apply { fun ResultWithDiagnostics<*>.throwOnFailure(): ResultWithDiagnostics<*> = apply {
if (this is ResultWithDiagnostics.Failure) { 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.DependenciesResolver
import kotlin.script.experimental.dependencies.ScriptDependencies import kotlin.script.experimental.dependencies.ScriptDependencies
import kotlin.script.experimental.dependencies.ScriptReport import kotlin.script.experimental.dependencies.ScriptReport
import kotlin.script.experimental.host.FileScriptSource
import kotlin.script.experimental.host.toScriptSource import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvm.JvmDependency import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvm.compat.mapToLegacyScriptReportPosition 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)) } map { ScriptReport(it.message, mapToLegacyScriptReportSeverity(it.severity), mapToLegacyScriptReportPosition(it.location)) }
internal fun ScriptContents.toScriptSource(): SourceCode = when { internal fun ScriptContents.toScriptSource(): SourceCode = when {
file != null -> FileScriptSource(file!!, text?.toString())
text != null -> text!!.toString().toScriptSource() text != null -> text!!.toString().toScriptSource()
file != null -> file!!.toScriptSource()
else -> throw IllegalArgumentException("Unable to convert script contents $this into script source") else -> throw IllegalArgumentException("Unable to convert script contents $this into script source")
} }