diff --git a/libraries/scripting/common/build.gradle.kts b/libraries/scripting/common/build.gradle.kts new file mode 100644 index 00000000000..e9b283b3d62 --- /dev/null +++ b/libraries/scripting/common/build.gradle.kts @@ -0,0 +1 @@ +import org.jetbrains.kotlin.gradle.dsl.Coroutines apply { plugin("kotlin") } jvmTarget = "1.6" dependencies { compile(projectDist(":kotlin-stdlib")) compile(projectDist(":kotlin-reflect")) compile(commonDep("org.jetbrains.kotlinx", "kotlinx-coroutines-core")) { isTransitive = false } } sourceSets { "main" { projectDefault() } "test" {} } kotlin.experimental.coroutines = Coroutines.ENABLE dist() ideaPlugin() standardPublicJars() publish() \ No newline at end of file diff --git a/libraries/scripting/common/src/kotlin/script/experimental/annotations/scriptAnnotations.kt b/libraries/scripting/common/src/kotlin/script/experimental/annotations/scriptAnnotations.kt new file mode 100644 index 00000000000..cd5258ad930 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/annotations/scriptAnnotations.kt @@ -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 = DefaultScriptSelector::class, + val configurator: KClass = PassThroughConfigurator::class, + val runner: KClass> = DummyRunner::class +) diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt new file mode 100644 index 00000000000..d542927287e --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptCompilation.kt @@ -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> +} + + +interface CompiledScript { + + val configuration: ScriptCompileConfiguration + + suspend fun instantiate(scriptEvaluationEnvironment: ScriptEvaluationEnvironment): ResultWithDiagnostics +} diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptConfiguration.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptConfiguration.kt new file mode 100644 index 00000000000..7f1bbdefc49 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptConfiguration.kt @@ -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() + + val scriptSignature by typedKey() + + val importedPackages by typedKey>() + + val restrictions by typedKey() + + val importedScripts by typedKey>() + + val dependencies by typedKey>() + + val compilerOptions by typedKey>() // Q: CommonCompilerOptions instead? + + val updateConfigurationOnAnnotations by typedKey>>() + + val updateConfigurationOnSections by typedKey>() +} + +typealias ScriptCompileConfiguration = HeterogeneousMap + +fun ScriptSource.toScriptCompileConfiguration(vararg pairs: Pair, Any?>) = + ScriptCompileConfiguration(ScriptCompileConfigurationParams.scriptSourceFragments to ScriptSourceFragments(this, null), *pairs) + +object ProcessedScriptDataParams { + val annotations by typedKey>() + + val fragments by typedKey>() +} + +typealias ProcessedScriptData = HeterogeneousMap + + +interface ScriptConfigurator { + + // with null scriptSource should return a generic configuration for the script type + suspend fun baseConfiguration(scriptSource: ScriptSource?) : ResultWithDiagnostics + + suspend fun refineConfiguration( + configuration: ScriptCompileConfiguration, + processedScriptData: ProcessedScriptData = ProcessedScriptData() + ): ResultWithDiagnostics +} + +fun ScriptSource?.toConfigEntry(): Pair, Any?> = + ScriptCompileConfigurationParams.scriptSourceFragments to this?.let { ScriptSourceFragments(this, null) } + diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptData.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptData.kt new file mode 100644 index 00000000000..92685151689 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptData.kt @@ -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?) + +open class ProvidedDeclarations( + val implicitReceivers: List = emptyList(), // previous scripts, etc. + val contextVariables: Map = emptyMap() // external variables + // Q: do we need context constants and/or types here, e.g. + // val contextConstants: Map // or with KType as well + // val contextTypes: List // additional (to the classpath) types provided by the environment + // alternatively: + // val contextDeclarations: List // 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 = arrayListOf() +} + +interface ScriptDependency { + // Q: anything generic here? +} diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptDefinition.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptDefinition.kt new file mode 100644 index 00000000000..fbca9b07902 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptDefinition.kt @@ -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<*>? +} + diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptErrorHandling.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptErrorHandling.kt new file mode 100644 index 00000000000..bc3af7fd340 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptErrorHandling.kt @@ -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 { + abstract val reports: List + + data class Success( + val value: R, + override val reports: List = listOf() + ) : ResultWithDiagnostics() + + data class Failure( + override val reports: List + ) : ResultWithDiagnostics() { + constructor(vararg reports: ScriptDiagnostic) : this(reports.asList()) + } +} + +operator fun List.plus(res: ResultWithDiagnostics): ResultWithDiagnostics = when (res) { + is ResultWithDiagnostics.Success -> ResultWithDiagnostics.Success(res.value, this + res.reports) + is ResultWithDiagnostics.Failure -> ResultWithDiagnostics.Failure(this + res.reports) +} + +fun R.asSuccess(reports: List = listOf()): ResultWithDiagnostics.Success = + 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 ResultWithDiagnostics.resultOrNull(): R? = when (this) { + is ResultWithDiagnostics.Success -> value + else -> null +} diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptEvaluation.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptEvaluation.kt new file mode 100644 index 00000000000..ebeeddae4ff --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptEvaluation.kt @@ -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>() + val contextVariables by typedKey>() // external variables + val constructorArgs by typedKey>() + val runArgs by typedKey>() +} + +typealias ScriptEvaluationEnvironment = HeterogeneousMap + +data class EvaluationResult(val returnValue: Any?, val environment: ScriptEvaluationEnvironment) + +// NOTE: name inconsistency: run vs evaluate +interface ScriptRunner { + + suspend fun run( + compiledScript: CompiledScript, + scriptEvaluationEnvironment: ScriptEvaluationEnvironment + ): ResultWithDiagnostics +} diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptSelection.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptSelection.kt new file mode 100644 index 00000000000..e976969cdcd --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptSelection.kt @@ -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 +} diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/scriptUtil.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptUtil.kt new file mode 100644 index 00000000000..fd222e486d1 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/scriptUtil.kt @@ -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(val name: String) + +class TypedKeyDelegate { + operator fun getValue(thisRef: Any?, property: KProperty<*>): TypedKey = TypedKey(property.name) +} + +fun typedKey() = TypedKeyDelegate() + +class HeterogeneousMap(val data: Map, Any?> = hashMapOf()) { + constructor(vararg pairs: Pair, Any?>) : this(hashMapOf(*pairs)) +} + +fun HeterogeneousMap.cloneWith(vararg pairs: Pair, Any?>) = HeterogeneousMap(HashMap(data).apply { putAll(pairs) }) + +operator fun HeterogeneousMap.get(key: TypedKey): T = + if (data.containsKey(key)) data[key] as T + else throw IllegalArgumentException("Unknown key $key") + +fun HeterogeneousMap.getOrNull(key: TypedKey): T? = data[key] as T? + diff --git a/libraries/scripting/common/src/kotlin/script/experimental/basic/basicScript.kt b/libraries/scripting/common/src/kotlin/script/experimental/basic/basicScript.kt new file mode 100644 index 00000000000..7e96750fe78 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/basic/basicScript.kt @@ -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? = null) : ScriptSelector { + + override val name: String = "Kotlin script" + + override val fileExtension: String = baseClass?.findAnnotation()?.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? = null) : ScriptConfigurator { + + override suspend fun baseConfiguration(scriptSource: ScriptSource?): ResultWithDiagnostics = + (when (scriptSource) { + null -> ScriptCompileConfiguration() + else -> ScriptCompileConfiguration(scriptSource.toConfigEntry()) + }).asSuccess() + + override suspend fun refineConfiguration( + configuration: ScriptCompileConfiguration, + processedScriptData: ProcessedScriptData + ): ResultWithDiagnostics = + configuration.asSuccess() +} + +class DummyRunner(val baseClass: KClass? = null) : ScriptRunner { + override suspend fun run( + compiledScript: CompiledScript, + scriptEvaluationEnvironment: ScriptEvaluationEnvironment + ): ResultWithDiagnostics = + ResultWithDiagnostics.Failure("not implemented".asErrorDiagnostics()) +} + +// TODO: from org.jetbrains.kotlin.utils.addToStdlib, take it from the stdlib when available +private inline fun Iterable<*>.firstIsInstanceOrNull(): T? { + for (element in this) if (element is T) return element + return null +} + diff --git a/libraries/scripting/common/src/kotlin/script/experimental/definitions/scriptDefinitions.kt b/libraries/scripting/common/src/kotlin/script/experimental/definitions/scriptDefinitions.kt new file mode 100644 index 00000000000..b81dde08e80 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/definitions/scriptDefinitions.kt @@ -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 KClass.instantiateScriptHandler(): T { + val fqn = this.qualifiedName!! + val klass: KClass = (baseClass.java.classLoader.loadClass(fqn) as Class).kotlin + // TODO: fix call after deciding on constructor parameters + return klass.objectInstance ?: klass.primaryConstructor!!.call(baseClass) + } +} + diff --git a/libraries/scripting/common/src/kotlin/script/experimental/host/BasicScriptingHost.kt b/libraries/scripting/common/src/kotlin/script/experimental/host/BasicScriptingHost.kt new file mode 100644 index 00000000000..bb2a8dacec8 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/host/BasicScriptingHost.kt @@ -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( + val configurator: ScriptConfigurator, + val compiler: ScriptCompiler, + val runner: ScriptRunner +) { + open fun runInCoroutineContext(block: suspend CoroutineScope.() -> T): T = runBlocking { block() } + + open fun eval(configuration: ScriptCompileConfiguration, environment: ScriptEvaluationEnvironment): ResultWithDiagnostics = + runInCoroutineContext { + val compiled = compiler.compile(configuration, configurator) + when (compiled) { + is ResultWithDiagnostics.Failure -> compiled + is ResultWithDiagnostics.Success -> { + val compiledScript = compiled.value!! as CompiledScript + runner.run(compiledScript, environment) + } + } + } +} diff --git a/libraries/scripting/common/src/kotlin/script/experimental/host/scriptHostUtil.kt b/libraries/scripting/common/src/kotlin/script/experimental/host/scriptHostUtil.kt new file mode 100644 index 00000000000..b8c2eb448b5 --- /dev/null +++ b/libraries/scripting/common/src/kotlin/script/experimental/host/scriptHostUtil.kt @@ -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) diff --git a/libraries/tools/script-runtime/build.gradle b/libraries/tools/script-runtime/build.gradle index 8ead68723c7..4619691bff4 100644 --- a/libraries/tools/script-runtime/build.gradle +++ b/libraries/tools/script-runtime/build.gradle @@ -45,3 +45,6 @@ compileKotlin { "-module-name", project.name ] } + +kotlin.experimental.coroutines 'enable' + diff --git a/settings.gradle b/settings.gradle index 4c76aacae90..be47408f663 100644 --- a/settings.gradle +++ b/settings.gradle @@ -150,6 +150,7 @@ include ":kotlin-build-common", ":examples:kotlin-jsr223-daemon-local-eval-example", ":kotlin-annotations-jvm", ":kotlin-annotations-android", + ":kotlin-scripting-common", ":pill:generate-all-tests", ":include:kotlin-compiler", @@ -243,6 +244,7 @@ project(':examples:kotlin-jsr223-local-example').projectDir = "$rootDir/librarie project(':examples:kotlin-jsr223-daemon-local-eval-example').projectDir = "$rootDir/libraries/examples/kotlin-jsr223-daemon-local-eval-example" as File project(':kotlin-annotations-jvm').projectDir = "$rootDir/libraries/tools/kotlin-annotations-jvm" as File project(':kotlin-annotations-android').projectDir = "$rootDir/libraries/tools/kotlin-annotations-android" as File +project(':kotlin-scripting-common').projectDir = "$rootDir/libraries/scripting/common" as File project(':pill:generate-all-tests').projectDir = "$rootDir/plugins/pill/generate-all-tests" as File // plugin markers: