PR-1021: Merge Keplin project scripting code into Kotlin core.

Overhauls the scripting layers (GenericRepl and related, and JSR223 and related)
Adds repeating modes (none, only latest eval'd line, or random order)
Also adds better thread-safe IO capturing, default imports, SimpleRepl wrapper, more unit tests

NOTE: the script-util part of the pull request was rejected due to various problems and incompatibilities.
It may be incorporated into the code later.

(originally cherry picked from commit 6f7d517)
This commit is contained in:
apatrida
2017-01-18 04:17:41 +01:00
committed by Ilya Chernikov
parent 8caf607378
commit 5ad06e1e92
29 changed files with 1146 additions and 548 deletions
@@ -1,124 +0,0 @@
/*
* Copyright 2010-2016 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.
*/
package org.jetbrains.kotlin.cli.common.repl
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import java.io.File
import java.net.URLClassLoader
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
open class GenericReplCompiledEvaluator(baseClasspath: Iterable<File>,
baseClassloader: ClassLoader?,
val scriptArgs: Array<Any?>? = null,
val scriptArgsTypes: Array<Class<*>>? = null
) : ReplCompiledEvaluator {
private var classLoader: org.jetbrains.kotlin.cli.common.repl.ReplClassLoader = makeReplClassLoader(baseClassloader, baseClasspath)
private val classLoaderLock = ReentrantReadWriteLock()
// TODO: consider to expose it as a part of (evaluator, invoker) interface
private val evalStateLock = ReentrantReadWriteLock()
private val compiledLoadedClassesHistory = ReplHistory<ClassWithInstance>()
override fun eval(codeLine: ReplCodeLine,
history: List<ReplCodeLine>,
compiledClasses: List<CompiledClassData>,
hasResult: Boolean,
classpathAddendum: List<File>,
invokeWrapper: InvokeWrapper?
): ReplEvalResult = evalStateLock.write {
checkAndUpdateReplHistoryCollection(compiledLoadedClassesHistory, history)?.let {
return@eval ReplEvalResult.HistoryMismatch(compiledLoadedClassesHistory.lines, it)
}
var mainLineClassName: String? = null
fun classNameFromPath(path: String) = JvmClassName.byInternalName(path.replaceFirst("\\.class$".toRegex(), ""))
fun processCompiledClasses() {
val expectedClassName = "Line${codeLine.no}"
compiledClasses.filter { it.path.endsWith(".class") }
.forEach {
val className = classNameFromPath(it.path)
if (className.internalName == expectedClassName || className.internalName.endsWith("/$expectedClassName")) {
mainLineClassName = className.internalName.replace('/', '.')
}
classLoader.addClass(className, it.bytes)
}
}
classLoaderLock.read {
if (classpathAddendum.isNotEmpty()) {
classLoaderLock.write {
classLoader = makeReplClassLoader(classLoader, classpathAddendum)
}
}
processCompiledClasses()
}
fun compiledClassesNames() = compiledClasses.map { classNameFromPath(it.path).fqNameForClassNameWithoutDollars.asString() }
val scriptClass = classLoaderLock.read {
try {
classLoader.loadClass(mainLineClassName!!)
}
catch (e: Throwable) {
return ReplEvalResult.Error.Runtime(compiledLoadedClassesHistory.lines, "Error loading class $mainLineClassName: known classes: ${compiledClassesNames()}",
e as? Exception)
}
}
fun getConstructorParams(): Array<Class<*>> =
(compiledLoadedClassesHistory.values.map { it.klass.java } +
(scriptArgs?.mapIndexed { i, it -> scriptArgsTypes?.getOrNull(i) ?: it?.javaClass ?: Any::class.java } ?: emptyList())
).toTypedArray()
fun getConstructorArgs() = (compiledLoadedClassesHistory.values.map { it.instance } + scriptArgs.orEmpty()).toTypedArray()
val constructorParams: Array<Class<*>> = getConstructorParams()
val constructorArgs: Array<Any?> = getConstructorArgs()
val scriptInstanceConstructor = scriptClass.getConstructor(*constructorParams)
val scriptInstance =
try {
invokeWrapper?.invoke { scriptInstanceConstructor.newInstance(*constructorArgs) } ?: scriptInstanceConstructor.newInstance(*constructorArgs)
}
catch (e: Throwable) {
// ignore everything in the stack trace until this constructor call
return ReplEvalResult.Error.Runtime(compiledLoadedClassesHistory.lines, renderReplStackTrace(e.cause!!, startFromMethodName = "${scriptClass.name}.<init>"), e as? Exception)
}
compiledLoadedClassesHistory.add(codeLine, ClassWithInstance(scriptClass.kotlin, scriptInstance))
val rvField = scriptClass.getDeclaredField(SCRIPT_RESULT_FIELD_NAME).apply { isAccessible = true }
val rv: Any? = rvField.get(scriptInstance)
return if (hasResult) ReplEvalResult.ValueResult(compiledLoadedClassesHistory.lines, rv) else ReplEvalResult.UnitResult(compiledLoadedClassesHistory.lines)
}
override val lastEvaluatedScript: ClassWithInstance? get() = evalStateLock.read { compiledLoadedClassesHistory.values.lastOrNull() }
companion object {
private val SCRIPT_RESULT_FIELD_NAME = "\$\$result"
}
}
private fun makeReplClassLoader(baseClassloader: ClassLoader?, baseClasspath: Iterable<File>) =
ReplClassLoader(URLClassLoader(baseClasspath.map { it.toURI().toURL() }.toTypedArray(), baseClassloader))
@@ -0,0 +1,112 @@
/*
* Copyright 2010-2017 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.
*/
package org.jetbrains.kotlin.cli.common.repl
import java.io.File
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
class GenericReplCompilingEvaluator(val compiler: ReplCompiler,
baseClasspath: Iterable<File>,
baseClassloader: ClassLoader?,
protected val fallbackScriptArgs: ScriptArgsWithTypes? = null,
repeatingMode: ReplRepeatingMode = ReplRepeatingMode.REPEAT_ONLY_MOST_RECENT,
protected val stateLock: ReentrantReadWriteLock = ReentrantReadWriteLock()) : ReplFullEvaluator {
val evaluator = GenericReplEvaluator(baseClasspath, baseClassloader, fallbackScriptArgs, repeatingMode, stateLock)
override fun compileAndEval(codeLine: ReplCodeLine, scriptArgs: ScriptArgsWithTypes?, verifyHistory: List<ReplCodeLine>?, invokeWrapper: InvokeWrapper?): ReplEvalResult {
return stateLock.write {
@Suppress("DEPRECATION")
val compiled = compiler.compile(codeLine, verifyHistory)
when (compiled) {
is ReplCompileResult.Error -> ReplEvalResult.Error.CompileTime(compiled.compiledHistory, compiled.message, compiled.location)
is ReplCompileResult.HistoryMismatch -> ReplEvalResult.HistoryMismatch(compiled.compiledHistory, compiled.lineNo)
is ReplCompileResult.Incomplete -> ReplEvalResult.Incomplete(compiled.compiledHistory)
is ReplCompileResult.CompiledClasses -> {
@Suppress("DEPRECATION")
val result = eval(compiled, scriptArgs, invokeWrapper)
when (result) {
is ReplEvalResult.Error,
is ReplEvalResult.HistoryMismatch,
is ReplEvalResult.Incomplete -> {
result.completedEvalHistory.lastOrNull()?.let { compiler.resetToLine(it) }
result
}
is ReplEvalResult.ValueResult,
is ReplEvalResult.UnitResult ->
result
else -> throw IllegalStateException("Unknown evaluator result type ${compiled}")
}
}
else -> throw IllegalStateException("Unknown compiler result type ${compiled}")
}
}
}
override fun resetToLine(lineNumber: Int): List<ReplCodeLine> {
stateLock.write {
val removedCompiledLines = compiler.resetToLine(lineNumber)
val removedEvaluatorLines = evaluator.resetToLine(lineNumber)
removedCompiledLines.zip(removedEvaluatorLines).forEach {
if (it.first != it.second) {
throw IllegalStateException("History mistmatch when resetting lines")
}
}
return removedCompiledLines
}
}
override fun resetToLine(line: ReplCodeLine): List<ReplCodeLine> = resetToLine(line.no)
override val lastEvaluatedScripts: List<EvalHistoryType> get() = evaluator.lastEvaluatedScripts
override val history: List<ReplCodeLine> get() = evaluator.history
override val currentClasspath: List<File> get() = evaluator.currentClasspath
override val compiledHistory: List<ReplCodeLine> get() = compiler.history
override val evaluatedHistory: List<ReplCodeLine> get() = evaluator.history
override fun eval(compileResult: ReplCompileResult.CompiledClasses, scriptArgs: ScriptArgsWithTypes?, invokeWrapper: InvokeWrapper?): ReplEvalResult {
return evaluator.eval(compileResult, scriptArgs, invokeWrapper)
}
override fun check(codeLine: ReplCodeLine): ReplCheckResult {
return compiler.check(codeLine)
}
override fun compileToEvaluable(codeLine: ReplCodeLine, defaultScriptArgs: ScriptArgsWithTypes?, verifyHistory: List<ReplCodeLine>?): Pair<ReplCompileResult, Evaluable?> {
val compiled = compiler.compile(codeLine, verifyHistory)
return if (compiled is ReplCompileResult.CompiledClasses) {
Pair(compiled, DelayedEvaluation(compiled, stateLock, evaluator, defaultScriptArgs ?: fallbackScriptArgs))
}
else {
Pair(compiled, null)
}
}
class DelayedEvaluation(override val compiledCode: ReplCompileResult.CompiledClasses,
private val stateLock: ReentrantReadWriteLock,
private val evaluator: ReplEvaluator,
private val defaultScriptArgs: ScriptArgsWithTypes?) : Evaluable {
override fun eval(scriptArgs: ScriptArgsWithTypes?, invokeWrapper: InvokeWrapper?): ReplEvalResult {
return stateLock.write { evaluator.eval(compiledCode, scriptArgs ?: defaultScriptArgs, invokeWrapper) }
}
}
}
@@ -0,0 +1,209 @@
/*
* Copyright 2010-2016 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.
*/
package org.jetbrains.kotlin.cli.common.repl
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import java.io.File
import java.net.URLClassLoader
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
open class GenericReplEvaluator(baseClasspath: Iterable<File>,
baseClassloader: ClassLoader?,
protected val fallbackScriptArgs: ScriptArgsWithTypes? = null,
protected val repeatingMode: ReplRepeatingMode = ReplRepeatingMode.REPEAT_ONLY_MOST_RECENT,
protected val stateLock: ReentrantReadWriteLock = ReentrantReadWriteLock()) : ReplEvaluator {
private val topClassLoader: ReplClassLoader = makeReplClassLoader(baseClassloader, baseClasspath)
private val evaluatedHistory = ReplHistory<EvalClassWithInstanceAndLoader>()
override fun resetToLine(lineNumber: Int): List<ReplCodeLine> {
return stateLock.write {
evaluatedHistory.resetToLine(lineNumber)
}.map { it.first }
}
override val history: List<ReplCodeLine> get() = stateLock.read { evaluatedHistory.copySources() }
override val currentClasspath: List<File> get() = stateLock.read {
evaluatedHistory.copyValues().lastOrNull()?.let { it.classLoader.listAllUrlsAsFiles() }
?: topClassLoader.listAllUrlsAsFiles()
}
private class HistoryActions(val effectiveHistory: List<EvalClassWithInstanceAndLoader>,
val verify: (compareHistory: SourceList?) -> Int?,
val addPlaceholder: (line: CompiledReplCodeLine, value: EvalClassWithInstanceAndLoader) -> Unit,
val removePlaceholder: (line: CompiledReplCodeLine) -> Boolean,
val addFinal: (line: CompiledReplCodeLine, value: EvalClassWithInstanceAndLoader) -> Unit,
val processClasses: (compileResult: ReplCompileResult.CompiledClasses) -> Pair<ClassLoader, Class<out Any>>)
private fun prependClassLoaderWithNewClasses(effectiveHistory: List<EvalClassWithInstanceAndLoader>, compileResult: ReplCompileResult.CompiledClasses): Pair<ClassLoader, Class<out Any>> {
return stateLock.write {
var mainLineClassName: String? = null
val classLoader = makeReplClassLoader(effectiveHistory.lastOrNull()?.classLoader ?: topClassLoader, compileResult.classpathAddendum)
fun classNameFromPath(path: String) = JvmClassName.byInternalName(path.replaceFirst("\\.class$".toRegex(), ""))
fun compiledClassesNames() = compileResult.classes.map { classNameFromPath(it.path).fqNameForClassNameWithoutDollars.asString() }
val expectedClassName = compileResult.generatedClassname
compileResult.classes.filter { it.path.endsWith(".class") }
.forEach {
val className = classNameFromPath(it.path)
if (className.internalName == expectedClassName || className.internalName.endsWith("/$expectedClassName")) {
mainLineClassName = className.internalName.replace('/', '.')
}
classLoader.addClass(className, it.bytes)
}
val scriptClass = try {
classLoader.loadClass(mainLineClassName!!)
}
catch (t: Throwable) {
throw Exception("Error loading class $mainLineClassName: known classes: ${compiledClassesNames()}", t)
}
Pair(classLoader, scriptClass)
}
}
override fun eval(compileResult: ReplCompileResult.CompiledClasses,
scriptArgs: ScriptArgsWithTypes?,
invokeWrapper: InvokeWrapper?): ReplEvalResult {
stateLock.write {
val verifyHistory = compileResult.compiledHistory.dropLast(1)
val defaultHistoryActor = HistoryActions(
effectiveHistory = evaluatedHistory.copyValues(),
verify = { line -> evaluatedHistory.firstMismatchingHistory(line) },
addPlaceholder = { line, value -> evaluatedHistory.add(line, value) },
removePlaceholder = { line -> evaluatedHistory.removeLast(line) },
addFinal = { line, value -> evaluatedHistory.add(line, value) },
processClasses = { compiled ->
prependClassLoaderWithNewClasses(evaluatedHistory.copyValues(), compiled)
})
val historyActor: HistoryActions = when (repeatingMode) {
ReplRepeatingMode.NONE -> defaultHistoryActor
ReplRepeatingMode.REPEAT_ONLY_MOST_RECENT -> {
val lastItem = evaluatedHistory.lastItem()
if (lastItem == null || lastItem.first.source != compileResult.compiledCodeLine.source) {
defaultHistoryActor
}
else {
val trimmedHistory = ReplHistory(evaluatedHistory.copyAll().dropLast(1))
HistoryActions(
effectiveHistory = trimmedHistory.copyValues(),
verify = { trimmedHistory.firstMismatchingHistory(it) },
addPlaceholder = { _, _ -> NO_ACTION() },
removePlaceholder = { NO_ACTION_THAT_RETURNS(true) },
addFinal = { line, value ->
evaluatedHistory.removeLast(line)
evaluatedHistory.add(line, value)
},
processClasses = { _ ->
Pair(lastItem.second.classLoader, lastItem.second.klass.java)
})
}
}
ReplRepeatingMode.REPEAT_ANY_PREVIOUS -> {
if (evaluatedHistory.isEmpty() || !evaluatedHistory.contains(compileResult.compiledCodeLine.source)) {
defaultHistoryActor
}
else {
val historyCopy = evaluatedHistory.copyAll()
val matchingItem = historyCopy.first { it.first.source == compileResult.compiledCodeLine.source }
val trimmedHistory = ReplHistory(evaluatedHistory.copyAll().takeWhile { it != matchingItem })
HistoryActions(
effectiveHistory = trimmedHistory.copyValues(),
verify = { trimmedHistory.firstMismatchingHistory(it) },
addPlaceholder = { _, _ -> NO_ACTION() },
removePlaceholder = { NO_ACTION_THAT_RETURNS(true) },
addFinal = { line, value ->
val extraLines = evaluatedHistory.resetToLine(line)
evaluatedHistory.removeLast(line)
evaluatedHistory.add(line, value)
extraLines.forEach {
evaluatedHistory.add(it.first, it.second)
}
},
processClasses = { _ ->
Pair(matchingItem.second.classLoader, matchingItem.second.klass.java)
})
}
}
}
val firstMismatch = historyActor.verify(verifyHistory)
if (firstMismatch != null) {
return@eval ReplEvalResult.HistoryMismatch(evaluatedHistory.copySources(), firstMismatch)
}
val (classLoader, scriptClass) = try {
historyActor.processClasses(compileResult)
}
catch (e: Exception) {
return@eval ReplEvalResult.Error.Runtime(evaluatedHistory.copySources(),
e.message!!, e)
}
val currentScriptArgs = scriptArgs ?: fallbackScriptArgs
val useScriptArgs = currentScriptArgs?.scriptArgs
val useScriptArgsTypes = currentScriptArgs?.scriptArgsTypes?.map { it.java }
val constructorParams: Array<Class<*>> = (historyActor.effectiveHistory.map { it.klass.java } +
(useScriptArgs?.mapIndexed { i, it -> useScriptArgsTypes?.getOrNull(i) ?: it?.javaClass ?: Any::class.java } ?: emptyList())
).toTypedArray()
val constructorArgs: Array<Any?> = (historyActor.effectiveHistory.map { it.instance } + useScriptArgs.orEmpty()).toTypedArray()
val scriptInstanceConstructor = scriptClass.getConstructor(*constructorParams)
historyActor.addPlaceholder(compileResult.compiledCodeLine, EvalClassWithInstanceAndLoader(scriptClass.kotlin, null, classLoader, invokeWrapper))
val scriptInstance =
try {
if (invokeWrapper != null) invokeWrapper.invoke { scriptInstanceConstructor.newInstance(*constructorArgs) }
else scriptInstanceConstructor.newInstance(*constructorArgs)
}
catch (e: Throwable) {
historyActor.removePlaceholder(compileResult.compiledCodeLine)
// ignore everything in the stack trace until this constructor call
return@eval ReplEvalResult.Error.Runtime(evaluatedHistory.copySources(),
renderReplStackTrace(e.cause!!, startFromMethodName = "${scriptClass.name}.<init>"), e as? Exception)
}
historyActor.removePlaceholder(compileResult.compiledCodeLine)
historyActor.addFinal(compileResult.compiledCodeLine, EvalClassWithInstanceAndLoader(scriptClass.kotlin, scriptInstance, classLoader, invokeWrapper))
val resultField = scriptClass.getDeclaredField(SCRIPT_RESULT_FIELD_NAME).apply { isAccessible = true }
val resultValue: Any? = resultField.get(scriptInstance)
return if (compileResult.hasResult) ReplEvalResult.ValueResult(evaluatedHistory.copySources(), resultValue)
else ReplEvalResult.UnitResult(evaluatedHistory.copySources())
}
}
override val lastEvaluatedScripts: List<EvalHistoryType> get() {
return stateLock.read { evaluatedHistory.copyAll() }
}
companion object {
private val SCRIPT_RESULT_FIELD_NAME = "\$\$result"
}
private fun makeReplClassLoader(baseClassloader: ClassLoader?, baseClasspath: Iterable<File>) =
ReplClassLoader(URLClassLoader(baseClasspath.map { it.toURI().toURL() }.toTypedArray(), baseClassloader))
}
@@ -17,60 +17,117 @@
package org.jetbrains.kotlin.cli.common.repl
import org.jetbrains.kotlin.utils.tryCreateCallableMapping
import java.lang.reflect.Proxy
import javax.script.Invocable
import javax.script.ScriptException
import kotlin.reflect.*
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.functions
import kotlin.reflect.full.safeCast
@Suppress("unused") // used externally (kotlin.script.utils)
interface KotlinJsr223JvmInvocableScriptEngine : Invocable {
val replScriptEvaluator: ReplEvaluatorBase
val replScriptEvaluator: ReplEvaluatorExposedInternalHistory
fun <T: Any> getInterface(klass: KClass<T>): Any? {
val (_, instance) = replScriptEvaluator.lastEvaluatedScript ?: throw IllegalArgumentException("no script ")
return getInterface(instance, klass)
}
fun <T: Any> getInterface(receiver: Any, klass: KClass<T>): Any? {
return klass.safeCast(receiver)
private fun prioritizedHistory(receiverClass: KClass<*>?, receiverInstance: Any?): List<EvalClassWithInstanceAndLoader> {
return replScriptEvaluator.lastEvaluatedScripts.map { it.second }.filter { it.instance != null }.reversed().assertNotEmpty("no script ").let { history ->
if (receiverInstance != null) {
val receiverKlass = receiverClass ?: receiverInstance.javaClass.kotlin
val receiverInHistory = history.find { it.instance == receiverInstance } ?:
EvalClassWithInstanceAndLoader(receiverKlass, receiverInstance, receiverKlass.java.classLoader, history.first().invokeWrapper)
listOf(receiverInHistory) + history.filterNot { it == receiverInHistory }
}
else {
history
}
}
}
override fun invokeFunction(name: String?, vararg args: Any?): Any? {
if (name == null) throw java.lang.NullPointerException("function name cannot be null")
val (klass, instance) = replScriptEvaluator.lastEvaluatedScript ?: throw IllegalArgumentException("no script ")
return invokeImpl(klass, instance, name, args, invokeWrapper = null)
return invokeImpl(prioritizedHistory(null, null), name, args)
}
override fun invokeMethod(thiz: Any?, name: String?, vararg args: Any?): Any? {
if (name == null) throw java.lang.NullPointerException("method name cannot be null")
if (thiz == null) throw IllegalArgumentException("cannot invoke method on the null object")
return invokeImpl(thiz.javaClass.kotlin, thiz, name, args, invokeWrapper = null)
return invokeImpl(prioritizedHistory(thiz.javaClass.kotlin, thiz), name, args)
}
private fun invokeImpl(prioritizedCallOrder: List<EvalClassWithInstanceAndLoader>, name: String, args: Array<out Any?>): Any? {
// TODO: cache the method lookups?
val (fn, mapping, invokeWrapper) = prioritizedCallOrder.asSequence().map { attempt ->
val candidates = attempt.klass.functions.filter { it.name == name }
candidates.findMapping(listOf<Any?>(attempt.instance) + args)?.let {
Triple(it.first, it.second, attempt.invokeWrapper)
}
}.filterNotNull().firstOrNull() ?: throw NoSuchMethodException("no suitable function '$name' found")
val res = try {
if (invokeWrapper != null) {
invokeWrapper.invoke {
fn.callBy(mapping)
}
}
else {
fn.callBy(mapping)
}
}
catch (e: Throwable) {
// ignore everything in the stack trace until this constructor call
throw ScriptException(renderReplStackTrace(e.cause!!, startFromMethodName = fn.name))
}
return if (fn.returnType.classifier == Unit::class) Unit else res
}
override fun <T : Any> getInterface(clasz: Class<T>?): T? {
val (_, instance) = replScriptEvaluator.lastEvaluatedScript ?: throw IllegalArgumentException("no script ")
return getInterface(instance, clasz)
return proxyInterface(null, clasz)
}
override fun <T : Any> getInterface(thiz: Any?, clasz: Class<T>?): T? {
if (thiz == null) throw IllegalArgumentException("object cannot be null")
return proxyInterface(thiz, clasz)
}
private fun <T : Any> proxyInterface(thiz: Any?, clasz: Class<T>?): T? {
replScriptEvaluator.lastEvaluatedScripts.assertNotEmpty("no script")
val priority = prioritizedHistory(thiz?.javaClass?.kotlin, thiz)
if (clasz == null) throw IllegalArgumentException("class object cannot be null")
if (!clasz.isInterface) throw IllegalArgumentException("expecting interface")
return clasz.kotlin.safeCast(thiz)
// TODO: cache the method lookups?
val proxy = Proxy.newProxyInstance(Thread.currentThread().contextClassLoader, arrayOf(clasz)) { _, method, args ->
invokeImpl(priority, method.name, args ?: emptyArray())
}
return clasz.kotlin.safeCast(proxy)
}
}
private fun invokeImpl(receiverClass: KClass<*>, receiverInstance: Any, name: String, args: Array<out Any?>, invokeWrapper: InvokeWrapper?): Any? {
private fun invokeImpl(prioritizedCallOrder: List<EvalClassWithInstanceAndLoader>, name: String, args: Array<out Any?>): Any? {
// TODO: cache the method lookups?
val (fn, mapping, invokeWrapper) = prioritizedCallOrder.asSequence().map { attempt ->
val candidates = attempt.klass.functions.filter { it.name == name }
candidates.findMapping(listOf<Any?>(attempt.instance) + args)?.let {
Triple(it.first, it.second, attempt.invokeWrapper)
}
}.filterNotNull().firstOrNull() ?: throw NoSuchMethodException("no suitable function '$name' found")
val candidates = receiverClass.functions.filter { it.name == name }
val (fn, mapping) = candidates.findMapping(listOf<Any?>(receiverInstance) + args) ?:
throw NoSuchMethodException("no suitable function '$name' found")
val res = try {
invokeWrapper?.invoke {
if (invokeWrapper != null) {
invokeWrapper.invoke {
fn.callBy(mapping)
}
}
else {
fn.callBy(mapping)
} ?: fn.callBy(mapping)
}
}
catch (e: Throwable) {
// ignore everything in the stack trace until this constructor call
@@ -18,25 +18,27 @@ package org.jetbrains.kotlin.cli.common.repl
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation
import java.io.Reader
import java.util.concurrent.atomic.AtomicInteger
import javax.script.*
val KOTLIN_SCRIPT_HISTORY_BINDINGS_KEY = "kotlin.script.history"
// TODO consider additional error handling
@Suppress("UNCHECKED_CAST")
val Bindings.kotlinScriptHistory: MutableList<ReplCodeLine>
get() = getOrPut(KOTLIN_SCRIPT_HISTORY_BINDINGS_KEY, { arrayListOf<ReplCodeLine>() }) as MutableList<ReplCodeLine>
abstract class KotlinJsr223JvmScriptEngineBase(protected val myFactory: ScriptEngineFactory) : AbstractScriptEngine(), ScriptEngine, Compilable {
private var lineCount = 0
protected var codeLineNumber = AtomicInteger(0)
protected abstract val replCompiler: ReplCompiler
protected abstract val replCompiler: ReplCompileAction
protected abstract val replScriptEvaluator: ReplFullEvaluator
protected abstract val replEvaluator: ReplCompiledEvaluator
override fun eval(script: String, context: ScriptContext): Any? = compileAndEval(script, context)
override fun eval(script: String, context: ScriptContext): Any? = compile(script, context).eval(context)
override fun eval(script: Reader, context: ScriptContext): Any? = compile(script.readText(), context).eval()
override fun eval(script: Reader, context: ScriptContext): Any? = compileAndEval(script.readText(), context)
override fun compile(script: String): CompiledScript = compile(script, getContext())
@@ -46,42 +48,56 @@ abstract class KotlinJsr223JvmScriptEngineBase(protected val myFactory: ScriptEn
override fun getFactory(): ScriptEngineFactory = myFactory
open fun compile(script: String, context: ScriptContext): CompiledScript {
lineCount += 1
fun nextCodeLine(code: String) = ReplCodeLine(codeLineNumber.incrementAndGet(), code)
val codeLine = ReplCodeLine(lineCount, script)
open fun overrideScriptArgs(context: ScriptContext): ScriptArgsWithTypes? = null
open fun compileAndEval(script: String, context: ScriptContext): Any? {
val codeLine = nextCodeLine(script)
val history = context.getBindings(ScriptContext.ENGINE_SCOPE).kotlinScriptHistory
val result = replScriptEvaluator.compileAndEval(codeLine, scriptArgs = overrideScriptArgs(context), verifyHistory = history)
val ret = when (result) {
is ReplEvalResult.ValueResult -> result.value
is ReplEvalResult.UnitResult -> null
is ReplEvalResult.Error -> throw ScriptException(result.message)
is ReplEvalResult.Incomplete -> throw ScriptException("error: incomplete code")
is ReplEvalResult.HistoryMismatch -> throw ScriptException("Repl history mismatch at line: ${result.lineNo}")
}
context.getBindings(ScriptContext.ENGINE_SCOPE).put(KOTLIN_SCRIPT_HISTORY_BINDINGS_KEY, ArrayList(result.completedEvalHistory))
return ret
}
open fun compile(script: String, context: ScriptContext): CompiledScript {
val codeLine = nextCodeLine(script)
val history = context.getBindings(ScriptContext.ENGINE_SCOPE).kotlinScriptHistory
val compileResult = replCompiler.compile(codeLine, history)
val compiled = when (compileResult) {
is ReplCompileResult.Error -> throw ScriptException("Error${compileResult.locationString()}: ${compileResult.message}")
val result = replCompiler.compile(codeLine, history)
val compiled = when (result) {
is ReplCompileResult.Error -> throw ScriptException("Error${result.locationString()}: ${result.message}")
is ReplCompileResult.Incomplete -> throw ScriptException("error: incomplete code")
is ReplCompileResult.HistoryMismatch -> throw ScriptException("Repl history mismatch at line: ${compileResult.lineNo}")
is ReplCompileResult.CompiledClasses -> compileResult
is ReplCompileResult.HistoryMismatch -> throw ScriptException("Repl history mismatch at line: ${result.lineNo}")
is ReplCompileResult.CompiledClasses -> result
}
context.getBindings(ScriptContext.ENGINE_SCOPE).put(KOTLIN_SCRIPT_HISTORY_BINDINGS_KEY, ArrayList(result.compiledHistory))
return CompiledKotlinScript(this, codeLine, compiled)
}
open fun eval(compiledScript: CompiledKotlinScript, context: ScriptContext): Any? {
val history = context.getBindings(ScriptContext.ENGINE_SCOPE).kotlinScriptHistory
val evalResult = try {
replEvaluator.eval(compiledScript.codeLine, history, compiledScript.compiledData.classes, compiledScript.compiledData.hasResult, compiledScript.compiledData.classpathAddendum)
val result = try {
replScriptEvaluator.eval(compiledScript.compiledData, scriptArgs = overrideScriptArgs(context))
}
catch (e: Exception) {
throw ScriptException(e)
}
val ret = when (evalResult) {
is ReplEvalResult.ValueResult -> evalResult.value
val ret = when (result) {
is ReplEvalResult.ValueResult -> result.value
is ReplEvalResult.UnitResult -> null
is ReplEvalResult.Error -> throw ScriptException(evalResult.message)
is ReplEvalResult.Error -> throw ScriptException(result.message)
is ReplEvalResult.Incomplete -> throw ScriptException("error: incomplete code")
is ReplEvalResult.HistoryMismatch -> throw ScriptException("Repl history mismatch at line: ${evalResult.lineNo}")
is ReplEvalResult.HistoryMismatch -> throw ScriptException("Repl history mismatch at line: ${result.lineNo}")
}
history.add(compiledScript.codeLine)
context.getBindings(ScriptContext.ENGINE_SCOPE).put(KOTLIN_SCRIPT_HISTORY_BINDINGS_KEY, ArrayList(result.completedEvalHistory))
return ret
}
@@ -28,105 +28,173 @@ data class ReplCodeLine(val no: Int, val code: String) : Serializable {
}
}
data class ClassWithInstance(val klass: KClass<*>, val instance: Any)
// TODO: consider storing code hash where source is not needed
data class CompiledReplCodeLine(val className: String, val source: ReplCodeLine) : Serializable {
companion object {
private val serialVersionUID: Long = 8228307678L
}
}
data class CompiledClassData(val path: String, val bytes: ByteArray) : Serializable {
override fun equals(other: Any?): Boolean = (other as? CompiledClassData)?.let { path == it.path && Arrays.equals(bytes, it.bytes) } ?: false
override fun hashCode(): Int = path.hashCode() + Arrays.hashCode(bytes)
companion object {
private val serialVersionUID: Long = 8228357578L
}
}
sealed class ReplCheckResult(val updatedHistory: List<ReplCodeLine>) : Serializable {
class Ok(updatedHistory: List<ReplCodeLine>) : ReplCheckResult(updatedHistory)
class Incomplete(updatedHistory: List<ReplCodeLine>) : ReplCheckResult(updatedHistory)
class Error(updatedHistory: List<ReplCodeLine>,
val message: String,
val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION
) : ReplCheckResult(updatedHistory)
{
interface ReplCheckAction {
fun check(codeLine: ReplCodeLine): ReplCheckResult
}
sealed class ReplCheckResult : Serializable {
class Ok : ReplCheckResult()
class Incomplete : ReplCheckResult()
class Error(val message: String,
val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION) : ReplCheckResult() {
override fun toString(): String = "Error(message = \"$message\""
}
companion object {
private val serialVersionUID: Long = 8228357578L
private val serialVersionUID: Long = 8228307678L
}
}
sealed class ReplCompileResult(val updatedHistory: List<ReplCodeLine>) : Serializable {
class CompiledClasses(updatedHistory: List<ReplCodeLine>,
interface ReplResettableCodeLine {
fun resetToLine(lineNumber: Int): List<ReplCodeLine>
fun resetToLine(line: ReplCodeLine): List<ReplCodeLine> = resetToLine(line.no)
}
interface ReplCodeLineHistory {
val history: List<ReplCodeLine>
}
interface ReplCombinedHistory {
val compiledHistory: List<ReplCodeLine>
val evaluatedHistory: List<ReplCodeLine>
}
interface ReplCompileAction {
fun compile(codeLine: ReplCodeLine, verifyHistory: List<ReplCodeLine>? = null): ReplCompileResult
}
sealed class ReplCompileResult(val compiledHistory: List<ReplCodeLine>) : Serializable {
class CompiledClasses(compiledHistory: List<ReplCodeLine>,
val compiledCodeLine: CompiledReplCodeLine,
val generatedClassname: String,
val classes: List<CompiledClassData>,
val hasResult: Boolean,
val classpathAddendum: List<File>
) : ReplCompileResult(updatedHistory)
class Incomplete(updatedHistory: List<ReplCodeLine>) : ReplCompileResult(updatedHistory)
class HistoryMismatch(updatedHistory: List<ReplCodeLine>, val lineNo: Int): ReplCompileResult(updatedHistory)
class Error(updatedHistory: List<ReplCodeLine>,
val classpathAddendum: List<File>) : ReplCompileResult(compiledHistory)
class Incomplete(compiledHistory: List<ReplCodeLine>) : ReplCompileResult(compiledHistory)
class HistoryMismatch(compiledHistory: List<ReplCodeLine>, val lineNo: Int) : ReplCompileResult(compiledHistory)
class Error(compiledHistory: List<ReplCodeLine>,
val message: String,
val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION
) : ReplCompileResult(updatedHistory)
{
val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION) : ReplCompileResult(compiledHistory) {
override fun toString(): String = "Error(message = \"$message\""
}
companion object {
private val serialVersionUID: Long = 8228357578L
private val serialVersionUID: Long = 8228307678L
}
}
sealed class ReplEvalResult(val updatedHistory: List<ReplCodeLine>) : Serializable {
class ValueResult(updatedHistory: List<ReplCodeLine>, val value: Any?) : ReplEvalResult(updatedHistory) {
interface ReplCompiler : ReplResettableCodeLine, ReplCodeLineHistory, ReplCompileAction, ReplCheckAction
typealias EvalHistoryType = Pair<CompiledReplCodeLine, EvalClassWithInstanceAndLoader>
interface ReplEvaluatorExposedInternalHistory {
val lastEvaluatedScripts: List<EvalHistoryType>
}
interface ReplClasspath {
val currentClasspath: List<File>
}
data class EvalClassWithInstanceAndLoader(val klass: KClass<*>, val instance: Any?, val classLoader: ClassLoader, val invokeWrapper: InvokeWrapper?)
interface ReplEvalAction {
fun eval(compileResult: ReplCompileResult.CompiledClasses,
scriptArgs: ScriptArgsWithTypes? = null,
invokeWrapper: InvokeWrapper? = null): ReplEvalResult
}
sealed class ReplEvalResult(val completedEvalHistory: List<ReplCodeLine>) : Serializable {
class ValueResult(completedEvalHistory: List<ReplCodeLine>, val value: Any?) : ReplEvalResult(completedEvalHistory) {
override fun toString(): String = "Result: $value"
}
class UnitResult(updatedHistory: List<ReplCodeLine>) : ReplEvalResult(updatedHistory)
class Incomplete(updatedHistory: List<ReplCodeLine>) : ReplEvalResult(updatedHistory)
class HistoryMismatch(updatedHistory: List<ReplCodeLine>, val lineNo: Int): ReplEvalResult(updatedHistory)
sealed class Error(updatedHistory: List<ReplCodeLine>, val message: String) : ReplEvalResult(updatedHistory) {
class Runtime(updatedHistory: List<ReplCodeLine>, message: String, val cause: Exception? = null) : Error(updatedHistory, message)
class CompileTime(updatedHistory: List<ReplCodeLine>,
class UnitResult(completedEvalHistory: List<ReplCodeLine>) : ReplEvalResult(completedEvalHistory)
class Incomplete(completedEvalHistory: List<ReplCodeLine>) : ReplEvalResult(completedEvalHistory)
class HistoryMismatch(completedEvalHistory: List<ReplCodeLine>, val lineNo: Int) : ReplEvalResult(completedEvalHistory)
sealed class Error(completedEvalHistory: List<ReplCodeLine>, val message: String) : ReplEvalResult(completedEvalHistory) {
class Runtime(completedEvalHistory: List<ReplCodeLine>, message: String, val cause: Exception? = null) : Error(completedEvalHistory, message)
class CompileTime(completedEvalHistory: List<ReplCodeLine>,
message: String,
val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION
) : Error(updatedHistory, message)
val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION) : Error(completedEvalHistory, message)
override fun toString(): String = "${this::class.simpleName}Error(message = \"$message\""
}
companion object {
private val serialVersionUID: Long = 8228357578L
private val serialVersionUID: Long = 8228307678L
}
}
interface ReplChecker {
fun check(codeLine: ReplCodeLine, history: List<ReplCodeLine>): ReplCheckResult
interface ReplEvaluator : ReplResettableCodeLine, ReplCodeLineHistory, ReplEvaluatorExposedInternalHistory, ReplEvalAction, ReplClasspath
interface ReplAtomicEvalAction {
fun compileAndEval(codeLine: ReplCodeLine,
scriptArgs: ScriptArgsWithTypes? = null,
verifyHistory: List<ReplCodeLine>? = null,
invokeWrapper: InvokeWrapper? = null): ReplEvalResult
}
interface ReplCompiler : ReplChecker {
fun compile(codeLine: ReplCodeLine, history: List<ReplCodeLine>): ReplCompileResult
interface ReplAtomicEvaluator : ReplResettableCodeLine, ReplCombinedHistory, ReplEvaluatorExposedInternalHistory, ReplAtomicEvalAction, ReplCheckAction, ReplClasspath
interface ReplDelayedEvalAction {
fun compileToEvaluable(codeLine: ReplCodeLine, defaultScriptArgs: ScriptArgsWithTypes? = null, verifyHistory: List<ReplCodeLine>?): Pair<ReplCompileResult, Evaluable?>
}
interface ReplEvaluatorBase {
val lastEvaluatedScript: ClassWithInstance?
interface Evaluable {
val compiledCode: ReplCompileResult.CompiledClasses
fun eval(scriptArgs: ScriptArgsWithTypes? = null, invokeWrapper: InvokeWrapper? = null): ReplEvalResult
}
interface ReplCompiledEvaluator : ReplEvaluatorBase {
interface ReplFullEvaluator : ReplEvaluator, ReplAtomicEvaluator, ReplDelayedEvalAction, ReplCombinedHistory
fun eval(codeLine: ReplCodeLine,
history: List<ReplCodeLine>,
compiledClasses: List<CompiledClassData>,
hasResult: Boolean,
classpathAddendum: List<File>,
invokeWrapper: InvokeWrapper? = null
): ReplEvalResult
/**
* Keep args and arg types together, so as a whole they are present or absent
*/
class ScriptArgsWithTypes(val scriptArgs: Array<out Any?>, val scriptArgsTypes: Array<out KClass<out Any>>) : Serializable {
companion object {
private val serialVersionUID: Long = 8529357500L
}
}
interface ReplEvaluator : ReplChecker, ReplEvaluatorBase {
fun eval(codeLine: ReplCodeLine,
history: List<ReplCodeLine>,
invokeWrapper: InvokeWrapper? = null
): ReplEvalResult
interface ScriptTemplateEmptyArgsProvider {
val defaultEmptyArgs: ScriptArgsWithTypes?
}
class SimpleScriptTemplateEmptyArgsProvider(override val defaultEmptyArgs: ScriptArgsWithTypes? = null) : ScriptTemplateEmptyArgsProvider
enum class ReplRepeatingMode {
NONE,
REPEAT_ONLY_MOST_RECENT,
REPEAT_ANY_PREVIOUS
}
interface InvokeWrapper {
operator fun<T> invoke(body: () -> T): T // e.g. for capturing io
operator fun <T> invoke(body: () -> T): T // e.g. for capturing io
}
@@ -0,0 +1,105 @@
/*
* Copyright 2010-2017 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.
*/
package org.jetbrains.kotlin.cli.common.repl
import java.io.Serializable
import java.util.concurrent.ConcurrentLinkedDeque
typealias CompiledHistoryItem<T> = Pair<CompiledReplCodeLine, T>
typealias SourceHistoryItem<T> = Pair<ReplCodeLine, T>
typealias CompiledHistoryStorage<T> = ConcurrentLinkedDeque<CompiledHistoryItem<T>>
typealias CompiledHistoryList<T> = List<CompiledHistoryItem<T>>
typealias SourceHistoryList<T> = List<SourceHistoryItem<T>>
typealias SourceList = List<ReplCodeLine>
/*
Not thread safe, the caller is assumed to lock access. Even though ConcurrentLinkedDeque is used, there are mutliple
actions that require locking.
*/
class ReplHistory<T>(startingHistory: CompiledHistoryList<T> = emptyList()) : Serializable {
private val history: CompiledHistoryStorage<T> = ConcurrentLinkedDeque(startingHistory)
fun isEmpty(): Boolean = history.isEmpty()
fun isNotEmpty(): Boolean = history.isNotEmpty()
fun add(line: CompiledReplCodeLine, value: T) {
history.add(line to value)
}
/* remove last line only if it is the line we think it is */
fun removeLast(line: CompiledReplCodeLine): Boolean {
return if (history.peekLast().first == line) {
history.removeLast()
true
}
else {
false
}
}
/* resets back to a previous line number and returns the lines removed */
fun resetToLine(lineNumber: Int): SourceHistoryList<T> {
val removed = arrayListOf<SourceHistoryItem<T>>()
while ((history.peekLast()?.first?.source?.no ?: -1) > lineNumber) {
removed.add(history.removeLast().let { Pair(it.first.source, it.second) })
}
return removed.reversed()
}
fun resetToLine(line: ReplCodeLine): SourceHistoryList<T> {
return resetToLine(line.no)
}
fun resetToLine(line: CompiledReplCodeLine): CompiledHistoryList<T> {
val removed = arrayListOf<CompiledHistoryItem<T>>()
while ((history.peekLast()?.first?.source?.no ?: -1) > line.source.no) {
removed.add(history.removeLast())
}
return removed.reversed()
}
fun contains(line: ReplCodeLine): Boolean = history.any { it.first.source == line }
fun contains(line: CompiledReplCodeLine): Boolean = history.any { it.first == line }
fun lastItem(): CompiledHistoryItem<T>? = history.peekLast()
fun lastCodeLine(): CompiledReplCodeLine? = lastItem()?.first
fun lastValue(): T? = lastItem()?.second
fun checkHistoryIsInSync(compareHistory: SourceList?): Boolean {
return firstMismatchingHistory(compareHistory) == null
}
// return from the compareHistory the first line that does not match or null
fun firstMismatchingHistory(compareHistory: SourceList?): Int? {
if (compareHistory == null) return null
val firstMismatch = history.zip(compareHistory).firstOrNull { it.first.first.source != it.second }?.second?.no
if (compareHistory.size == history.size) return firstMismatch
if (compareHistory.size > history.size) return compareHistory[history.size].no
return history.toList()[compareHistory.size].first.source.no
}
fun copySources(): SourceList = history.map { it.first.source }
fun copyValues(): List<T> = history.map { it.second }
fun copyAll(): CompiledHistoryList<T> = history.toList()
companion object {
private val serialVersionUID: Long = 8328353000L
}
}
@@ -17,44 +17,11 @@
package org.jetbrains.kotlin.cli.common.repl
import com.google.common.base.Throwables
import java.io.Serializable
import java.io.File
import java.net.URLClassLoader
// TODO: thread safety!!
data class ReplHistory<T>(
val lines: MutableList<ReplCodeLine> = arrayListOf(),
val values: MutableList<T> = arrayListOf()
) : Serializable
{
init { assert(isValid()) }
fun isValid() = lines.size == values.size
fun add(line: ReplCodeLine, value: T) {
lines.add(line)
values.add(value)
}
fun trimAt(idx: Int) {
lines.dropLast(lines.size - idx)
values.dropLast(lines.size - idx)
}
companion object {
private val serialVersionUID: Long = 8228357578L
}
}
fun <T> checkAndUpdateReplHistoryCollection(history: ReplHistory<T>, linesHistory: Iterable<ReplCodeLine>): Int? {
assert(history.isValid())
val linesHistoryIt = linesHistory.iterator()
var idx = 0
while (linesHistoryIt.hasNext()) {
val curLine = linesHistoryIt.next()
if (history.lines[idx] != curLine) return curLine.no
idx += 1
}
history.trimAt(idx)
return null
}
fun makeSriptBaseName(codeLine: ReplCodeLine, generation: Long) =
"Line_${codeLine.no}" + if (generation > 1) "_gen_${generation}" else ""
fun renderReplStackTrace(cause: Throwable, startFromMethodName: String): String {
val newTrace = arrayListOf<StackTraceElement>()
@@ -76,3 +43,22 @@ fun renderReplStackTrace(cause: Throwable, startFromMethodName: String): String
return Throwables.getStackTraceAsString(cause)
}
fun NO_ACTION(): Unit = Unit
fun <T> NO_ACTION_THAT_RETURNS(v: T): T = v
internal fun ClassLoader.listAllUrlsAsFiles(): List<File> {
val parents = generateSequence(this) { loader -> loader.parent }.filterIsInstance(URLClassLoader::class.java)
return parents.fold(emptyList<File>()) { accum, loader ->
loader.listLocalUrlsAsFiles() + accum
}.distinct()
}
internal fun URLClassLoader.listLocalUrlsAsFiles(): List<File> {
return this.urLs.map { it.toString().removePrefix("file:") }.filterNotNull().map { File(it) }
}
internal fun <T : Any> List<T>.assertNotEmpty(error: String): List<T> {
if (this.isEmpty()) throw IllegalStateException(error)
return this
}