Implement invokable/invoker interface on the generic repl evaluator, refactor reflection utils on the way
This commit is contained in:
@@ -16,72 +16,20 @@
|
||||
|
||||
package org.jetbrains.kotlin.cli.common
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KParameter
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.*
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
fun tryConstructScriptClass(scriptClass: Class<*>, scriptArgs: List<String>): Any? {
|
||||
|
||||
fun convertPrimitive(type: KType?, arg: String): Any? =
|
||||
when (type?.classifier) {
|
||||
String::class -> arg
|
||||
Int::class -> arg.toInt()
|
||||
Long::class -> arg.toLong()
|
||||
Short::class -> arg.toShort()
|
||||
Byte::class -> arg.toByte()
|
||||
Char::class -> arg[0]
|
||||
Float::class -> arg.toFloat()
|
||||
Double::class -> arg.toDouble()
|
||||
Boolean::class -> arg.toBoolean()
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun convertArray(type: KType?, args: List<String>): Any? =
|
||||
when (type?.classifier) {
|
||||
String::class -> args.toTypedArray()
|
||||
Int::class -> args.map(String::toInt).toTypedArray()
|
||||
Long::class -> args.map(String::toLong).toTypedArray()
|
||||
Short::class -> args.map(String::toShort).toTypedArray()
|
||||
Byte::class -> args.map(String::toByte).toTypedArray()
|
||||
Char::class -> args.map { it[0] }.toTypedArray()
|
||||
Float::class -> args.map(String::toFloat).toTypedArray()
|
||||
Double::class -> args.map(String::toDouble).toTypedArray()
|
||||
Boolean::class -> args.map(String::toBoolean).toTypedArray()
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun foldingFunc(state: Pair<List<Any>, List<String>?>, par: KParameter): Pair<List<Any>, List<String>?> {
|
||||
state.second?.let { scriptArgsLeft ->
|
||||
try {
|
||||
if (scriptArgsLeft.isNotEmpty()) {
|
||||
val primArgCandidate = convertPrimitive(par.type, scriptArgsLeft.first())
|
||||
if (primArgCandidate != null)
|
||||
return@foldingFunc Pair(state.first + primArgCandidate, scriptArgsLeft.drop(1))
|
||||
}
|
||||
|
||||
if ((par.type.classifier as? KClass<*>)?.qualifiedName == Array<Any>::class.qualifiedName) {
|
||||
val arrCompType = par.type.arguments.getOrNull(0)?.type
|
||||
val arrayArgCandidate = convertArray(arrCompType, scriptArgsLeft)
|
||||
if (arrayArgCandidate != null)
|
||||
return@foldingFunc Pair(state.first + arrayArgCandidate, null)
|
||||
}
|
||||
}
|
||||
catch (e: NumberFormatException) {
|
||||
} // just skips to return below
|
||||
}
|
||||
return state
|
||||
}
|
||||
fun tryConstructClassFromStringArgs(scriptClass: Class<*>, scriptArgs: List<String>): Any? {
|
||||
|
||||
try {
|
||||
return scriptClass.getConstructor(Array<String>::class.java).newInstance(scriptArgs.toTypedArray())
|
||||
}
|
||||
catch (e: NoSuchMethodException) {
|
||||
for (ctor in scriptClass.kotlin.constructors) {
|
||||
val (ctorArgs, scriptArgsLeft) = ctor.parameters.fold(Pair(emptyList<Any>(), scriptArgs), ::foldingFunc)
|
||||
if (ctorArgs.size <= ctor.parameters.size && (scriptArgsLeft == null || scriptArgsLeft.isEmpty())) {
|
||||
val argsMap = ctor.parameters.zip(ctorArgs).toMap()
|
||||
val mapping = tryCreateCallableMapping(ctor, scriptArgs, StringArgsConverter())
|
||||
if (mapping != null) {
|
||||
try {
|
||||
return ctor.callBy(argsMap)
|
||||
return ctor.callBy(mapping)
|
||||
}
|
||||
catch (e: Exception) { // TODO: find the exact exception type thrown then callBy fails
|
||||
}
|
||||
@@ -92,3 +40,105 @@ fun tryConstructScriptClass(scriptClass: Class<*>, scriptArgs: List<String>): An
|
||||
}
|
||||
|
||||
|
||||
fun tryCreateCallableMapping(callable: KCallable<*>, args: List<Any?>): Map<KParameter, Any?>? =
|
||||
tryCreateCallableMapping(callable, args, AnyArgsConverter())
|
||||
|
||||
|
||||
private interface ArgsConverter<T> {
|
||||
|
||||
data class Result(val v: Any?, val argsConsumed: Int)
|
||||
|
||||
fun convert(parameter: KParameter, args: List<T>, startArgIndex: Int): Result?
|
||||
}
|
||||
|
||||
private fun <T: Any?> tryCreateCallableMapping(callable: KCallable<*>, args: List<T>, converter: ArgsConverter<T>): Map<KParameter, Any?>? {
|
||||
|
||||
var argIdx = 0
|
||||
val res = mutableMapOf<KParameter, Any?>()
|
||||
for (par in callable.parameters) {
|
||||
val (compatibleArg, argsConsumed) = converter.convert(par, args, argIdx) ?: return null
|
||||
if (argsConsumed > 0) {
|
||||
res.put(par, compatibleArg)
|
||||
argIdx += argsConsumed
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
private class StringArgsConverter : ArgsConverter<String> {
|
||||
|
||||
override fun convert(parameter: KParameter, args: List<String>, startArgIndex: Int): ArgsConverter.Result? {
|
||||
|
||||
fun convertPrimitive(type: KType?, arg: String): Any? =
|
||||
when (type?.classifier) {
|
||||
String::class -> arg
|
||||
Int::class -> arg.toInt()
|
||||
Long::class -> arg.toLong()
|
||||
Short::class -> arg.toShort()
|
||||
Byte::class -> arg.toByte()
|
||||
Char::class -> arg[0]
|
||||
Float::class -> arg.toFloat()
|
||||
Double::class -> arg.toDouble()
|
||||
Boolean::class -> arg.toBoolean()
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun convertArray(type: KType?, args: List<String>): Any? =
|
||||
when (type?.classifier) {
|
||||
String::class -> args.toTypedArray()
|
||||
Int::class -> args.map(String::toInt).toTypedArray()
|
||||
Long::class -> args.map(String::toLong).toTypedArray()
|
||||
Short::class -> args.map(String::toShort).toTypedArray()
|
||||
Byte::class -> args.map(String::toByte).toTypedArray()
|
||||
Char::class -> args.map { it[0] }.toTypedArray()
|
||||
Float::class -> args.map(String::toFloat).toTypedArray()
|
||||
Double::class -> args.map(String::toDouble).toTypedArray()
|
||||
Boolean::class -> args.map(String::toBoolean).toTypedArray()
|
||||
else -> null
|
||||
}
|
||||
|
||||
try {
|
||||
if (startArgIndex >= args.size && parameter.isOptional)
|
||||
return ArgsConverter.Result(null, 0)
|
||||
|
||||
if (startArgIndex < args.size) {
|
||||
val primArgCandidate = convertPrimitive(parameter.type, args[startArgIndex])
|
||||
if (primArgCandidate != null)
|
||||
return ArgsConverter.Result(primArgCandidate, 1)
|
||||
}
|
||||
|
||||
if ((parameter.type.classifier as? KClass<*>)?.qualifiedName == Array<Any>::class.qualifiedName) {
|
||||
val arrCompType = parameter.type.arguments.getOrNull(0)?.type
|
||||
val arrayArgCandidate = convertArray(arrCompType, args.drop(startArgIndex))
|
||||
if (arrayArgCandidate != null)
|
||||
return ArgsConverter.Result(arrayArgCandidate, args.size - startArgIndex)
|
||||
}
|
||||
}
|
||||
catch (e: NumberFormatException) {}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private class AnyArgsConverter : ArgsConverter<Any?> {
|
||||
override fun convert(parameter: KParameter, args: List<Any?>, startArgIndex: Int): ArgsConverter.Result? {
|
||||
|
||||
fun convertSingle(type: KType, arg: Any?): Any? = when {
|
||||
type.classifier == arg?.javaClass?.kotlin -> arg
|
||||
type.jvmErasure.java.isAssignableFrom(arg?.javaClass) -> arg
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (startArgIndex >= args.size && parameter.isOptional)
|
||||
return ArgsConverter.Result(null, 0)
|
||||
|
||||
if (startArgIndex < args.size) {
|
||||
val primArgCandidate = convertSingle(parameter.type, args[startArgIndex])
|
||||
if (primArgCandidate != null)
|
||||
return ArgsConverter.Result(primArgCandidate, 1)
|
||||
}
|
||||
// TODO: add vararg support
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+58
-8
@@ -16,24 +16,33 @@
|
||||
|
||||
package org.jetbrains.kotlin.cli.common.repl
|
||||
|
||||
import org.jetbrains.kotlin.cli.common.tryCreateCallableMapping
|
||||
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
|
||||
import kotlin.reflect.*
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
open class GenericReplCompiledEvaluator(baseClasspath: Iterable<File>, baseClassloader: ClassLoader?, val scriptArgs: Array<Any?>? = null, val scriptArgsTypes: Array<Class<*>>? = null) : ReplCompiledEvaluator {
|
||||
open class GenericReplCompiledEvaluator(baseClasspath: Iterable<File>,
|
||||
baseClassloader: ClassLoader?,
|
||||
val scriptArgs: Array<Any?>? = null,
|
||||
val scriptArgsTypes: Array<Class<*>>? = null
|
||||
) : ReplCompiledEvaluator, ReplInvoker {
|
||||
|
||||
private var classLoader: org.jetbrains.kotlin.cli.common.repl.ReplClassLoader = makeReplClassLoader(baseClassloader, baseClasspath)
|
||||
private val classLoaderLock = ReentrantReadWriteLock()
|
||||
|
||||
private class ClassWithInstance(val klass: Class<*>, val instance: Any)
|
||||
// TODO: consider to expose it as a part of (evaluator, invoker) interface
|
||||
private val evalStateLock = ReentrantReadWriteLock()
|
||||
|
||||
private data class ClassWithInstance(val klass: Class<*>, val instance: Any)
|
||||
|
||||
private val compiledLoadedClassesHistory = ReplHistory<ClassWithInstance>()
|
||||
|
||||
override fun eval(codeLine: ReplCodeLine, history: List<ReplCodeLine>, compiledClasses: List<CompiledClassData>, hasResult: Boolean, classpathAddendum: List<File>): ReplEvalResult {
|
||||
|
||||
override fun eval(codeLine: ReplCodeLine, history: List<ReplCodeLine>, compiledClasses: List<CompiledClassData>, hasResult: Boolean, classpathAddendum: List<File>): ReplEvalResult /*= evalStateLock.write*/ {
|
||||
checkAndUpdateReplHistoryCollection(compiledLoadedClassesHistory, history)?.let {
|
||||
return@eval ReplEvalResult.HistoryMismatch(compiledLoadedClassesHistory.lines, it)
|
||||
}
|
||||
@@ -62,8 +71,9 @@ open class GenericReplCompiledEvaluator(baseClasspath: Iterable<File>, baseClass
|
||||
try {
|
||||
classLoader.loadClass(mainLineClassName!!)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
throw Exception("Error loading class $mainLineClassName: known classes: ${compiledClasses.map { classNameFromPath(it.path).fqNameForClassNameWithoutDollars.asString() }}", e)
|
||||
catch (e: Throwable) {
|
||||
return ReplEvalResult.Error.Runtime(compiledLoadedClassesHistory.lines, "Error loading class $mainLineClassName: known classes: ${compiledClasses.map { classNameFromPath(it.path).fqNameForClassNameWithoutDollars.asString() }}",
|
||||
e as? Exception)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +90,7 @@ open class GenericReplCompiledEvaluator(baseClasspath: Iterable<File>, baseClass
|
||||
}
|
||||
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>"))
|
||||
return ReplEvalResult.Error.Runtime(compiledLoadedClassesHistory.lines, renderReplStackTrace(e.cause!!, startFromMethodName = "${scriptClass.name}.<init>"), e as? Exception)
|
||||
}
|
||||
|
||||
compiledLoadedClassesHistory.add(codeLine, ClassWithInstance(scriptClass, scriptInstance))
|
||||
@@ -91,10 +101,50 @@ open class GenericReplCompiledEvaluator(baseClasspath: Iterable<File>, baseClass
|
||||
return if (hasResult) ReplEvalResult.ValueResult(compiledLoadedClassesHistory.lines, rv) else ReplEvalResult.UnitResult(compiledLoadedClassesHistory.lines)
|
||||
}
|
||||
|
||||
override fun <T: Any> getInterface(klass: KClass<T>): ReplInvokeResult = evalStateLock.read {
|
||||
val (_, instance) = compiledLoadedClassesHistory.values.lastOrNull() ?: return ReplInvokeResult.Error.NoSuchEntity("no script ")
|
||||
return getInterface(instance, klass)
|
||||
}
|
||||
|
||||
override fun <T: Any> getInterface(receiver: Any, klass: KClass<T>): ReplInvokeResult = evalStateLock.read {
|
||||
return ReplInvokeResult.ValueResult(klass.safeCast(receiver))
|
||||
}
|
||||
|
||||
override fun invokeMethod(receiver: Any, name: String, vararg args: Any?): ReplInvokeResult = evalStateLock.read {
|
||||
return invokeImpl(receiver.javaClass.kotlin, receiver, name, args)
|
||||
}
|
||||
|
||||
override fun invokeFunction(name: String, vararg args: Any?): ReplInvokeResult = evalStateLock.read {
|
||||
val (klass, instance) = compiledLoadedClassesHistory.values.lastOrNull() ?: return ReplInvokeResult.Error.NoSuchEntity("no script ")
|
||||
return invokeImpl(klass.kotlin, instance, name, args)
|
||||
}
|
||||
|
||||
private fun invokeImpl(receiverClass: KClass<*>, receiverInstance: Any, name: String, args: Array<out Any?>): ReplInvokeResult {
|
||||
val (fn, mapping) = receiverClass.functions.filter { it.name == name }.findMapping(args.toList()) ?:
|
||||
receiverClass.memberExtensionFunctions.filter { it.name == name }.findMapping(listOf<Any?>(receiverInstance) + args) ?:
|
||||
return ReplInvokeResult.Error.NoSuchEntity("no suitable function '$name' found")
|
||||
val res = try {
|
||||
evalWithIO { fn.callBy(mapping) }
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
// ignore everything in the stack trace until this constructor call
|
||||
return ReplInvokeResult.Error.Runtime(renderReplStackTrace(e.cause!!, startFromMethodName = "${fn.name}"), e as? Exception)
|
||||
}
|
||||
return if (fn.returnType.classifier == Unit::class) ReplInvokeResult.UnitResult else ReplInvokeResult.ValueResult(res)
|
||||
}
|
||||
|
||||
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))
|
||||
ReplClassLoader(URLClassLoader(baseClasspath.map { it.toURI().toURL() }.toTypedArray(), baseClassloader))
|
||||
|
||||
private fun Iterable<KFunction<*>>.findMapping(args: List<Any?>): Pair<KFunction<*>, Map<KParameter, Any?>>? {
|
||||
for (fn in this) {
|
||||
val mapping = tryCreateCallableMapping(fn, args)
|
||||
if (mapping != null) return fn to mapping
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import javax.script.*
|
||||
//
|
||||
//val Bindings.kotlinScriptHistory:
|
||||
|
||||
abstract class KotlinJsr223JvmScriptEngineBase(protected val myFactory: ScriptEngineFactory) : AbstractScriptEngine(), ScriptEngine, Compilable, Invocable {
|
||||
abstract class KotlinJsr223JvmScriptEngineBase(protected val myFactory: ScriptEngineFactory) : AbstractScriptEngine(), ScriptEngine, Compilable {
|
||||
protected var lineCount = 0
|
||||
|
||||
protected val history = arrayListOf<ReplCodeLine>()
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
data class ReplCodeLine(val no: Int, val code: String) : Serializable {
|
||||
companion object {
|
||||
@@ -27,6 +28,8 @@ data class ReplCodeLine(val no: Int, val code: String) : Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: consider storing code hash where source is not needed
|
||||
|
||||
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)
|
||||
@@ -70,6 +73,22 @@ sealed class ReplCompileResult(val updatedHistory: List<ReplCodeLine>) : Seriali
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ReplInvokeResult : Serializable {
|
||||
class ValueResult(val value: Any?) : ReplInvokeResult() {
|
||||
override fun toString(): String = "Result: $value"
|
||||
}
|
||||
object UnitResult : ReplInvokeResult()
|
||||
sealed class Error(val message: String) : ReplInvokeResult() {
|
||||
class Runtime(message: String, val cause: Exception? = null) : Error(message)
|
||||
class NoSuchEntity(message: String) : Error(message)
|
||||
class CompileTime(message: String, val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION) : Error(message)
|
||||
override fun toString(): String = "${this::class.simpleName}Error(message = \"$message\""
|
||||
}
|
||||
companion object {
|
||||
private val serialVersionUID: Long = 8228357578L
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ReplEvalResult(val updatedHistory: List<ReplCodeLine>) : Serializable {
|
||||
class ValueResult(updatedHistory: List<ReplCodeLine>, val value: Any?) : ReplEvalResult(updatedHistory) {
|
||||
override fun toString(): String = "Result: $value"
|
||||
@@ -78,7 +97,7 @@ sealed class ReplEvalResult(val updatedHistory: List<ReplCodeLine>) : Serializab
|
||||
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) : Error(updatedHistory, message)
|
||||
class Runtime(updatedHistory: List<ReplCodeLine>, message: String, val cause: Exception? = null) : Error(updatedHistory, message)
|
||||
class CompileTime(updatedHistory: List<ReplCodeLine>,
|
||||
message: String,
|
||||
val location: CompilerMessageLocation = CompilerMessageLocation.NO_LOCATION
|
||||
@@ -98,6 +117,13 @@ interface ReplCompiler : ReplChecker {
|
||||
fun compile(codeLine: ReplCodeLine, history: List<ReplCodeLine>): ReplCompileResult
|
||||
}
|
||||
|
||||
interface ReplInvoker {
|
||||
fun <T: Any> getInterface(clasz: KClass<T>): ReplInvokeResult
|
||||
fun <T: Any> getInterface(receiver: Any, clasz: KClass<T>): ReplInvokeResult
|
||||
fun invokeMethod(receiver: Any, name: String, vararg args: Any?): ReplInvokeResult
|
||||
fun invokeFunction(name: String, vararg args: Any?): ReplInvokeResult
|
||||
}
|
||||
|
||||
interface ReplCompiledEvaluator {
|
||||
|
||||
fun eval(codeLine: ReplCodeLine, history: List<ReplCodeLine>, compiledClasses: List<CompiledClassData>, hasResult: Boolean, classpathAddendum: List<File>): ReplEvalResult
|
||||
|
||||
Reference in New Issue
Block a user