Implement invokable/invoker interface on the generic repl evaluator, refactor reflection utils on the way

This commit is contained in:
Ilya Chernikov
2016-11-30 12:37:34 +01:00
parent 7a79fff9d6
commit fbd4c6eb61
8 changed files with 205 additions and 79 deletions
@@ -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
}
}
@@ -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
}
@@ -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
@@ -34,7 +34,7 @@ import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.ExitCode
import org.jetbrains.kotlin.cli.common.messages.*
import org.jetbrains.kotlin.cli.common.output.outputUtils.writeAll
import org.jetbrains.kotlin.cli.common.tryConstructScriptClass
import org.jetbrains.kotlin.cli.common.tryConstructClassFromStringArgs
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import org.jetbrains.kotlin.cli.jvm.config.*
import org.jetbrains.kotlin.codegen.ClassBuilderFactories
@@ -227,7 +227,7 @@ object KotlinToJVMBytecodeCompiler {
try {
try {
tryConstructScriptClass(scriptClass, scriptArgs)
tryConstructClassFromStringArgs(scriptClass, scriptArgs)
?: throw RuntimeException("unable to find appropriate constructor for class ${scriptClass.name} accepting arguments $scriptArgs\n")
}
finally {
@@ -19,7 +19,7 @@ package org.jetbrains.kotlin.scripts
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.*
import org.jetbrains.kotlin.cli.common.tryConstructScriptClass
import org.jetbrains.kotlin.cli.common.tryConstructClassFromStringArgs
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler
@@ -214,7 +214,7 @@ class ScriptTemplateTest {
val aClass = compileScript("fib.kts", ScriptWithIntParam::class)
Assert.assertNotNull(aClass)
captureOut {
val anObj = tryConstructScriptClass(aClass!!, listOf("4"))
val anObj = tryConstructClassFromStringArgs(aClass!!, listOf("4"))
Assert.assertNotNull(anObj)
}.let {
assertEqualsTrimmed(NUM_4_LINE + FIB_SCRIPT_OUTPUT_TAIL, it)
@@ -227,7 +227,7 @@ class ScriptTemplateTest {
Assert.assertNotNull(aClass)
var exceptionThrown = false
try {
tryConstructScriptClass(aClass!!, emptyList())
tryConstructClassFromStringArgs(aClass!!, emptyList())
}
catch (e: InvocationTargetException) {
Assert.assertTrue(e.cause is IllegalStateException)
@@ -19,7 +19,7 @@ package org.jetbrains.kotlin.scripts
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.messages.*
import org.jetbrains.kotlin.cli.common.tryConstructScriptClass
import org.jetbrains.kotlin.cli.common.tryConstructClassFromStringArgs
import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler
@@ -44,7 +44,7 @@ class ScriptTest : KtUsefulTestCase() {
val aClass = compileScript("fib_std.kts", StandardScriptDefinition)
Assert.assertNotNull(aClass)
val out = captureOut {
val anObj = tryConstructScriptClass(aClass!!, listOf("4", "comment"))
val anObj = tryConstructClassFromStringArgs(aClass!!, listOf("4", "comment"))
Assert.assertNotNull(anObj)
}
assertEqualsTrimmed(NUM_4_LINE + " (comment)" + FIB_SCRIPT_OUTPUT_TAIL, out)
@@ -55,7 +55,7 @@ class ScriptTest : KtUsefulTestCase() {
val aClass = compileScript("fib_std.kts", StandardScriptDefinition)
Assert.assertNotNull(aClass)
val out = captureOut {
val anObj = tryConstructScriptClass(aClass!!, emptyList())
val anObj = tryConstructClassFromStringArgs(aClass!!, emptyList())
Assert.assertNotNull(anObj)
}
assertEqualsTrimmed(NUM_4_LINE + " (none)" + FIB_SCRIPT_OUTPUT_TAIL, out)
@@ -68,7 +68,7 @@ class ScriptTest : KtUsefulTestCase() {
val aClass = compileScript("fib_std.kts", StandardScriptDefinition, saveClassesDir = tmpdir)
Assert.assertNotNull(aClass)
val out1 = captureOut {
val anObj = tryConstructScriptClass(aClass!!, emptyList())
val anObj = tryConstructClassFromStringArgs(aClass!!, emptyList())
Assert.assertNotNull(anObj)
}
assertEqualsTrimmed(NUM_4_LINE + " (none)" + FIB_SCRIPT_OUTPUT_TAIL, out1)
@@ -76,7 +76,7 @@ class ScriptTest : KtUsefulTestCase() {
val aClassSaved = savedClassLoader.loadClass(aClass.name)
Assert.assertNotNull(aClassSaved)
val out2 = captureOut {
val anObjSaved = tryConstructScriptClass(aClassSaved!!, emptyList())
val anObjSaved = tryConstructClassFromStringArgs(aClassSaved!!, emptyList())
Assert.assertNotNull(anObjSaved)
}
assertEqualsTrimmed(NUM_4_LINE + " (none)" + FIB_SCRIPT_OUTPUT_TAIL, out2)
@@ -202,7 +202,7 @@ public class ExecuteKotlinScriptMojo extends AbstractMojo {
try {
Class<?> klass = classLoader.loadClass(nameForScript.asString());
ExecuteKotlinScriptMojo.INSTANCE = this;
if (ReflectionUtilKt.tryConstructScriptClass(klass, scriptArguments) == null)
if (ReflectionUtilKt.tryConstructClassFromStringArgs(klass, scriptArguments) == null)
throw new ScriptExecutionException(scriptFile, "unable to construct script");
} catch (ClassNotFoundException e) {
throw new ScriptExecutionException(scriptFile, "internal error", e);