Add new experimental scripting API

This commit is contained in:
Ilya Chernikov
2018-02-05 15:39:26 +01:00
parent 4aec9499b5
commit aac7f97121
16 changed files with 509 additions and 0 deletions
@@ -0,0 +1,27 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package kotlin.script.experimental.annotations
import kotlin.reflect.KClass
import kotlin.script.experimental.api.ScriptConfigurator
import kotlin.script.experimental.api.ScriptRunner
import kotlin.script.experimental.api.ScriptSelector
import kotlin.script.experimental.basic.DefaultScriptSelector
import kotlin.script.experimental.basic.DummyRunner
import kotlin.script.experimental.basic.PassThroughConfigurator
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class KotlinScript(
val selector: KClass<out ScriptSelector> = DefaultScriptSelector::class,
val configurator: KClass<out ScriptConfigurator> = PassThroughConfigurator::class,
val runner: KClass<out ScriptRunner<*>> = DummyRunner::class
)
@@ -0,0 +1,22 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package kotlin.script.experimental.api
interface ScriptCompiler {
suspend fun compile(
configuration: ScriptCompileConfiguration,
configurator: ScriptConfigurator? = null
): ResultWithDiagnostics<CompiledScript<*>>
}
interface CompiledScript<out ScriptBase : Any> {
val configuration: ScriptCompileConfiguration
suspend fun instantiate(scriptEvaluationEnvironment: ScriptEvaluationEnvironment): ResultWithDiagnostics<ScriptBase>
}
@@ -0,0 +1,60 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("unused")
package kotlin.script.experimental.api
import kotlin.reflect.KClass
object ScriptCompileConfigurationParams {
val scriptSourceFragments by typedKey<ScriptSourceFragments>()
val scriptSignature by typedKey<ScriptSignature>()
val importedPackages by typedKey<Iterable<String>>()
val restrictions by typedKey<ResolvingRestrictions>()
val importedScripts by typedKey<Iterable<ScriptSource>>()
val dependencies by typedKey<Iterable<ScriptDependency>>()
val compilerOptions by typedKey<Iterable<String>>() // Q: CommonCompilerOptions instead?
val updateConfigurationOnAnnotations by typedKey<Iterable<KClass<out Annotation>>>()
val updateConfigurationOnSections by typedKey<Iterable<String>>()
}
typealias ScriptCompileConfiguration = HeterogeneousMap
fun ScriptSource.toScriptCompileConfiguration(vararg pairs: Pair<TypedKey<*>, Any?>) =
ScriptCompileConfiguration(ScriptCompileConfigurationParams.scriptSourceFragments to ScriptSourceFragments(this, null), *pairs)
object ProcessedScriptDataParams {
val annotations by typedKey<Iterable<Annotation>>()
val fragments by typedKey<Iterable<ScriptSourceNamedFragment>>()
}
typealias ProcessedScriptData = HeterogeneousMap
interface ScriptConfigurator {
// with null scriptSource should return a generic configuration for the script type
suspend fun baseConfiguration(scriptSource: ScriptSource?) : ResultWithDiagnostics<ScriptCompileConfiguration>
suspend fun refineConfiguration(
configuration: ScriptCompileConfiguration,
processedScriptData: ProcessedScriptData = ProcessedScriptData()
): ResultWithDiagnostics<ScriptCompileConfiguration>
}
fun ScriptSource?.toConfigEntry(): Pair<TypedKey<*>, Any?> =
ScriptCompileConfigurationParams.scriptSourceFragments to this?.let { ScriptSourceFragments(this, null) }
@@ -0,0 +1,58 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("unused")
package kotlin.script.experimental.api
import java.net.URL
import kotlin.reflect.KClass
import kotlin.reflect.KType
interface ScriptSource {
val location: URL?
val text: String?
data class Position(val line: Int, val col: Int, val absolutePos: Int? = null)
data class Range(val start: Position, val end: Position)
data class Location(val start: Position, val end: Position? = null)
}
data class ScriptSourceNamedFragment(val name: String?, val range: ScriptSource.Range)
open class ScriptSourceFragments(
val originalSource: ScriptSource,
val fragments: List<ScriptSourceNamedFragment>?)
open class ProvidedDeclarations(
val implicitReceivers: List<KType> = emptyList(), // previous scripts, etc.
val contextVariables: Map<String, KType> = emptyMap() // external variables
// Q: do we need context constants and/or types here, e.g.
// val contextConstants: Map<String, Any?> // or with KType as well
// val contextTypes: List<KType> // additional (to the classpath) types provided by the environment
// alternatively:
// val contextDeclarations: List<Tuple<DeclarationKind, String?, KType, Any?> // kind, name, type, value
// OR: it should be a HeterogeneousMap too
) {
object Empty : ProvidedDeclarations()
}
open class ScriptSignature(
val scriptBase: KClass<*>,
val providedDeclarations: ProvidedDeclarations
)
open class ResolvingRestrictions {
data class Rule(
val allow: Boolean,
val pattern: String // FQN wildcard
)
val rules: Iterable<Rule> = arrayListOf()
}
interface ScriptDependency {
// Q: anything generic here?
}
@@ -0,0 +1,18 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("unused")
package kotlin.script.experimental.api
import kotlin.reflect.KClass
interface ScriptDefinition {
val baseClass: KClass<*>
val selector: ScriptSelector
val configurator: ScriptConfigurator
val runner: ScriptRunner<*>?
}
@@ -0,0 +1,51 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("unused")
package kotlin.script.experimental.api
data class ScriptDiagnostic(
val message: String,
val severity: Severity = Severity.ERROR,
val location: ScriptSource.Location? = null,
val exception: Throwable? = null
) {
enum class Severity { ERROR, WARNING, INFO, DEBUG }
}
sealed class ResultWithDiagnostics<out R> {
abstract val reports: List<ScriptDiagnostic>
data class Success<out R>(
val value: R,
override val reports: List<ScriptDiagnostic> = listOf()
) : ResultWithDiagnostics<R>()
data class Failure(
override val reports: List<ScriptDiagnostic>
) : ResultWithDiagnostics<Nothing>() {
constructor(vararg reports: ScriptDiagnostic) : this(reports.asList())
}
}
operator fun <R> List<ScriptDiagnostic>.plus(res: ResultWithDiagnostics<R>): ResultWithDiagnostics<R> = when (res) {
is ResultWithDiagnostics.Success -> ResultWithDiagnostics.Success(res.value, this + res.reports)
is ResultWithDiagnostics.Failure -> ResultWithDiagnostics.Failure(this + res.reports)
}
fun <R : Any> R.asSuccess(reports: List<ScriptDiagnostic> = listOf()): ResultWithDiagnostics.Success<R> =
ResultWithDiagnostics.Success(this, reports)
fun Throwable.asDiagnostics(customMessage: String? = null, location: ScriptSource.Location? = null): ScriptDiagnostic =
ScriptDiagnostic(customMessage ?: message ?: "$this", ScriptDiagnostic.Severity.ERROR, location, this)
fun String.asErrorDiagnostics(location: ScriptSource.Location? = null): ScriptDiagnostic =
ScriptDiagnostic(this, ScriptDiagnostic.Severity.ERROR, location)
fun<R> ResultWithDiagnostics<R>.resultOrNull(): R? = when (this) {
is ResultWithDiagnostics.Success<R> -> value
else -> null
}
@@ -0,0 +1,28 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("unused")
package kotlin.script.experimental.api
object ScriptEvaluationEnvironmentParams {
val implicitReceivers by typedKey<List<Any>>()
val contextVariables by typedKey<Map<String, Any?>>() // external variables
val constructorArgs by typedKey<List<Any?>>()
val runArgs by typedKey<List<Any?>>()
}
typealias ScriptEvaluationEnvironment = HeterogeneousMap
data class EvaluationResult(val returnValue: Any?, val environment: ScriptEvaluationEnvironment)
// NOTE: name inconsistency: run vs evaluate
interface ScriptRunner<in ScriptBase : Any> {
suspend fun run(
compiledScript: CompiledScript<ScriptBase>,
scriptEvaluationEnvironment: ScriptEvaluationEnvironment
): ResultWithDiagnostics<EvaluationResult>
}
@@ -0,0 +1,19 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("unused")
package kotlin.script.experimental.api
interface ScriptSelector {
val name: String
val fileExtension: String // for preliminary selection by file type, e.g. in ide
fun makeScriptName(scriptFileName: String?): String
fun isKnownScript(script: ScriptSource): Boolean
}
@@ -0,0 +1,29 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package kotlin.script.experimental.api
import kotlin.reflect.KProperty
data class TypedKey<T>(val name: String)
class TypedKeyDelegate<T> {
operator fun getValue(thisRef: Any?, property: KProperty<*>): TypedKey<T> = TypedKey(property.name)
}
fun <T> typedKey() = TypedKeyDelegate<T>()
class HeterogeneousMap(val data: Map<TypedKey<*>, Any?> = hashMapOf()) {
constructor(vararg pairs: Pair<TypedKey<*>, Any?>) : this(hashMapOf(*pairs))
}
fun HeterogeneousMap.cloneWith(vararg pairs: Pair<TypedKey<*>, Any?>) = HeterogeneousMap(HashMap(data).apply { putAll(pairs) })
operator fun <T> HeterogeneousMap.get(key: TypedKey<T>): T =
if (data.containsKey(key)) data[key] as T
else throw IllegalArgumentException("Unknown key $key")
fun <T> HeterogeneousMap.getOrNull(key: TypedKey<T>): T? = data[key] as T?
@@ -0,0 +1,57 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package kotlin.script.experimental.basic
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.script.experimental.api.*
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class KotlinScriptFileExtension(val extension: String)
open class DefaultScriptSelector(val baseClass: KClass<Any>? = null) : ScriptSelector {
override val name: String = "Kotlin script"
override val fileExtension: String = baseClass?.findAnnotation<KotlinScriptFileExtension>()?.extension ?: "kts"
override fun makeScriptName(scriptFileName: String?): String = scriptFileName?.removeSuffix(".$fileExtension") ?: "Kotlin script"
override fun isKnownScript(script: ScriptSource): Boolean =
script.location?.file?.endsWith(fileExtension) ?: true
}
class PassThroughConfigurator(val baseClass: KClass<Any>? = null) : ScriptConfigurator {
override suspend fun baseConfiguration(scriptSource: ScriptSource?): ResultWithDiagnostics<ScriptCompileConfiguration> =
(when (scriptSource) {
null -> ScriptCompileConfiguration()
else -> ScriptCompileConfiguration(scriptSource.toConfigEntry())
}).asSuccess()
override suspend fun refineConfiguration(
configuration: ScriptCompileConfiguration,
processedScriptData: ProcessedScriptData
): ResultWithDiagnostics<ScriptCompileConfiguration> =
configuration.asSuccess()
}
class DummyRunner<ScriptBase : Any>(val baseClass: KClass<ScriptBase>? = null) : ScriptRunner<ScriptBase> {
override suspend fun run(
compiledScript: CompiledScript<ScriptBase>,
scriptEvaluationEnvironment: ScriptEvaluationEnvironment
): ResultWithDiagnostics<EvaluationResult> =
ResultWithDiagnostics.Failure("not implemented".asErrorDiagnostics())
}
// TODO: from org.jetbrains.kotlin.utils.addToStdlib, take it from the stdlib when available
private inline fun <reified T : Any> Iterable<*>.firstIsInstanceOrNull(): T? {
for (element in this) if (element is T) return element
return null
}
@@ -0,0 +1,28 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package kotlin.script.experimental.definitions
import kotlin.reflect.KClass
import kotlin.reflect.full.primaryConstructor
import kotlin.script.experimental.annotations.KotlinScript
import kotlin.script.experimental.api.ScriptDefinition
open class ScriptDefinitionFromAnnotatedBaseClass(final override val baseClass: KClass<*>) : ScriptDefinition {
private val annotation = baseClass.java.getAnnotation(KotlinScript::class.java)
?: throw IllegalArgumentException("Expecting KotlinScript on the $baseClass")
override val selector by lazy { annotation.selector.instantiateScriptHandler() }
override val configurator by lazy { annotation.configurator.instantiateScriptHandler() }
override val runner by lazy { annotation.runner.instantiateScriptHandler() }
private fun <T : Any> KClass<T>.instantiateScriptHandler(): T {
val fqn = this.qualifiedName!!
val klass: KClass<T> = (baseClass.java.classLoader.loadClass(fqn) as Class<T>).kotlin
// TODO: fix call after deciding on constructor parameters
return klass.objectInstance ?: klass.primaryConstructor!!.call(baseClass)
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:Suppress("unused")
package kotlin.script.experimental.host
import kotlinx.coroutines.experimental.CoroutineScope
import kotlinx.coroutines.experimental.runBlocking
import kotlin.script.experimental.api.*
abstract class BasicScriptingHost<ScriptBase : Any>(
val configurator: ScriptConfigurator,
val compiler: ScriptCompiler,
val runner: ScriptRunner<ScriptBase>
) {
open fun <T> runInCoroutineContext(block: suspend CoroutineScope.() -> T): T = runBlocking { block() }
open fun eval(configuration: ScriptCompileConfiguration, environment: ScriptEvaluationEnvironment): ResultWithDiagnostics<EvaluationResult> =
runInCoroutineContext {
val compiled = compiler.compile(configuration, configurator)
when (compiled) {
is ResultWithDiagnostics.Failure -> compiled
is ResultWithDiagnostics.Success -> {
val compiledScript = compiled.value!! as CompiledScript<ScriptBase>
runner.run(compiledScript, environment)
}
}
}
}
@@ -0,0 +1,63 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package kotlin.script.experimental.host
import kotlin.script.experimental.api.ScriptSource
import kotlin.script.experimental.api.ScriptSourceFragments
import kotlin.script.experimental.api.ScriptSourceNamedFragment
import java.io.File
import java.net.URL
fun ScriptSourceFragments.isWholeFile(): Boolean = fragments?.isEmpty() ?: true
fun ScriptSource.getScriptText(): String = when {
text != null -> text!!
location != null ->
location!!.openStream().bufferedReader().readText()
else -> throw RuntimeException("unable to get text from null script")
}
fun ScriptSourceFragments.getMergedScriptText(): String {
val originalScriptText = originalSource.getScriptText()
return if (isWholeFile()) {
originalScriptText
} else {
val sb = StringBuilder(originalScriptText.length)
var prevFragment: ScriptSourceNamedFragment? = null
for (fragment in fragments!!) {
val fragmentStartPos = fragment.range.start.absolutePos
val fragmentEndPos = fragment.range.end.absolutePos
if (fragmentStartPos == null || fragmentEndPos == null)
throw RuntimeException("Script fragments require absolute positions (received: $fragment)")
val curPos = if (prevFragment == null) 0 else prevFragment.range.end.absolutePos!!
if (prevFragment != null && prevFragment.range.end.absolutePos!! > fragmentStartPos) throw RuntimeException("Unsorted or overlapping fragments: previous: $prevFragment, current: $fragment")
if (curPos < fragmentStartPos) {
sb.append(
originalScriptText.subSequence(
curPos,
fragmentStartPos
).map { if (it == '\r' || it == '\n') it else ' ' }) // preserving lines layout
}
sb.append(originalScriptText.subSequence(fragmentStartPos, fragmentEndPos))
prevFragment = fragment
}
sb.toString()
}
}
open class FileScriptSource(val file: File) : ScriptSource {
override val location: URL? get() = file.toURI().toURL()
override val text: String? get() = null
}
fun File.toScriptSource(): ScriptSource = FileScriptSource(this)
open class StringScriptSource(val source: String) : ScriptSource {
override val location: URL? get() = null
override val text: String? get() = source
}
fun String.toScriptSource(): ScriptSource = StringScriptSource(this)