From 6af47ad7b3f7350e519b2ae30ee236a9f4efd352 Mon Sep 17 00:00:00 2001 From: Ivan Kylchik Date: Tue, 4 Feb 2020 19:59:43 +0300 Subject: [PATCH] Implement basic stack trace reporting if unhandled exception appear --- .../common/interpreter/IrInterpreter.kt | 40 +++++++++++-------- .../backend/common/interpreter/stack/State.kt | 35 ++++++++++++---- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/interpreter/IrInterpreter.kt b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/interpreter/IrInterpreter.kt index 638ba813d5c..51f64efa7bf 100644 --- a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/interpreter/IrInterpreter.kt +++ b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/interpreter/IrInterpreter.kt @@ -36,7 +36,7 @@ class IrInterpreter(irModule: IrModuleFragment) { .filter { it.isSubclassOf(irBuiltIns.throwableClass.owner) } private val classCastException = irExceptions.first { it.name.asString() == ClassCastException::class.java.simpleName } - private var stackSize = 0 + private val stackTrace = mutableListOf() private fun Any?.getType(defaultType: IrType): IrType { return when (this) { @@ -60,8 +60,8 @@ class IrInterpreter(irModule: IrModuleFragment) { return@runBlocking when (val code = withContext(this.coroutineContext) { expression.interpret(data) }) { Code.NEXT -> data.popReturnValue().toIrExpression(expression) Code.EXCEPTION -> { - val message = (data.popReturnValue() as ExceptionState).getMessage().value - IrErrorExpressionImpl(expression.startOffset, expression.endOffset, expression.type, message) + val message = (data.popReturnValue() as ExceptionState).getFullDescription() + IrErrorExpressionImpl(expression.startOffset, expression.endOffset, expression.type, "\n" + message) } else -> TODO("$code not supported as result of interpretation") } @@ -125,29 +125,37 @@ class IrInterpreter(irModule: IrModuleFragment) { val exceptionName = e::class.java.simpleName val irExceptionClass = irExceptions.firstOrNull { it.name.asString() == exceptionName } ?: irBuiltIns.throwableClass.owner - if (irExceptionClass == irBuiltIns.throwableClass.owner && exceptionName == "KotlinNullPointerException") { + val exceptionState = when { // this block is used to replace jvm null pointer exception with common one // TODO figure something better - data.pushReturnValue(ExceptionState(e, irExceptions.first { it.name.asString() == "NullPointerException" })) - } else { - irExceptionClass.let { data.pushReturnValue(ExceptionState(e, it)) } + irExceptionClass == irBuiltIns.throwableClass.owner && exceptionName == "KotlinNullPointerException" -> + ExceptionState(e, irExceptions.first { it.name.asString() == "NullPointerException" }, stackTrace) + else -> ExceptionState(e, irExceptionClass, stackTrace) } + data.pushReturnValue(exceptionState) return Code.EXCEPTION } } // this method is used to get stack trace after exception private suspend fun interpretFunction(irFunction: IrFunctionImpl, data: Frame): Code { - try { + return try { yield() - stackSize++ - if (stackSize == MAX_STACK_SIZE) { - throw StackOverflowError("Error!!!") + + if (irFunction.fileOrNull != null) { + val fileName = irFunction.file.name + val lineNum = irFunction.fileEntry.getLineNumber(irFunction.startOffset) + 1 + stackTrace += "at ${fileName.replace(".kt", "Kt")}.${irFunction.fqNameForIrSerialization}($fileName:$lineNum)" } - return irFunction.body?.interpret(data) ?: throw AssertionError("Ir function must be with body") + + if (stackTrace.size == MAX_STACK_SIZE) { + throw StackOverflowError("") + } + + irFunction.body?.interpret(data) ?: throw AssertionError("Ir function must be with body") } finally { - stackSize-- + if (irFunction.fileOrNull != null) stackTrace.removeAt(stackTrace.lastIndex) } } @@ -498,7 +506,7 @@ class IrInterpreter(irModule: IrModuleFragment) { val convertibleClassName = data.popReturnValue().irClass.fqNameForIrSerialization val castClassName = expression.type.classOrNull?.owner?.fqNameForIrSerialization val message = "$convertibleClassName cannot be cast to $castClassName" - data.pushReturnValue(ExceptionState(ClassCastException(message), classCastException)) + data.pushReturnValue(ExceptionState(ClassCastException(message), classCastException, stackTrace)) Code.EXCEPTION } else { code @@ -563,8 +571,8 @@ class IrInterpreter(irModule: IrModuleFragment) { private suspend fun interpretThrow(expression: IrThrow, data: Frame): Code { expression.value.interpret(data).also { if (it != Code.NEXT) return it } when (val exception = data.popReturnValue()) { - is Common -> data.pushReturnValue(ExceptionState(exception)) - is Wrapper -> data.pushReturnValue(ExceptionState(exception)) + is Common -> data.pushReturnValue(ExceptionState(exception, stackTrace)) + is Wrapper -> data.pushReturnValue(ExceptionState(exception, stackTrace)) is ExceptionState -> data.pushReturnValue(exception) else -> throw AssertionError("${exception::class} cannot be used as exception state") } diff --git a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/interpreter/stack/State.kt b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/interpreter/stack/State.kt index be6cd52e164..9e7958ed6e2 100644 --- a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/interpreter/stack/State.kt +++ b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/interpreter/stack/State.kt @@ -23,6 +23,7 @@ import org.jetbrains.kotlin.name.FqNameUnsafe import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType +import kotlin.math.min interface State { val fields: MutableList @@ -285,24 +286,35 @@ class Lambda(val irFunction: IrFunction, override val irClass: IrClass) : Comple } } -class ExceptionState private constructor(override val irClass: IrClass, override val fields: MutableList) : Complex(irClass, fields) { +class ExceptionState private constructor( + override val irClass: IrClass, override val fields: MutableList, stackTrace: List +) : Complex(irClass, fields) { + private lateinit var exceptionFqName: String private val exceptionHierarchy = mutableListOf() private val messageProperty = irClass.getPropertyByName("message") private val causeProperty = irClass.getPropertyByName("cause") + private val stackTrace: List + init { instance = this + this.stackTrace = stackTrace.reversed() + if (!this::exceptionFqName.isInitialized) this.exceptionFqName = irClass.fqNameForIrSerialization.asString() } - constructor(common: Common) : this(common.irClass, common.fields) - constructor(wrapper: Wrapper) : this(wrapper.value as Throwable, wrapper.irClass) + constructor(common: Common, stackTrace: List) : this(common.irClass, common.fields, stackTrace) + constructor(wrapper: Wrapper, stackTrace: List) : this(wrapper.value as Throwable, wrapper.irClass, stackTrace) - constructor(exception: Throwable, irClass: IrClass) : this(irClass, evaluateFields(exception, irClass)) { + constructor( + exception: Throwable, irClass: IrClass, stackTrace: List + ) : this(irClass, evaluateFields(exception, irClass), stackTrace) { if (irClass.name.asString() == "Throwable" && exception::class.java.name != "Throwable") { // ir class wasn't found in classpath, a stub was passed => need to save java class hierarchy exceptionHierarchy += exception::class.java.name generateSequence(exception::class.java.superclass) { it.superclass }.forEach { exceptionHierarchy += it.name } - exceptionHierarchy.removeAt(exceptionHierarchy.lastIndex) + exceptionHierarchy.removeAt(exceptionHierarchy.lastIndex) // remove unnecessary java.lang.Object + + this.exceptionFqName = exception::class.java.name } } @@ -317,8 +329,16 @@ class ExceptionState private constructor(override val irClass: IrClass, override fun getCause(): ExceptionState = getState(causeProperty.descriptor) as ExceptionState + fun getFullDescription(): String { + val message = getMessage().value.let { if (it.isNotEmpty()) ": $it" else ""} + val prefix = if (stackTrace.isNotEmpty()) "\n\t" else "" + val postfix = if (stackTrace.size > 10) "\n\t..." else "" + return "Exception $exceptionFqName$message" + + stackTrace.subList(0, min(stackTrace.size, 10)).joinToString(separator = "\n\t", prefix = prefix, postfix = postfix) + } + override fun copy(): State { - return ExceptionState(irClass, fields).apply { this@apply.instance = this@ExceptionState.instance } + return ExceptionState(irClass, fields, stackTrace).apply { this@apply.instance = this@ExceptionState.instance } } companion object { @@ -333,7 +353,8 @@ class ExceptionState private constructor(override val irClass: IrClass, override val causeProperty = irClass.getPropertyByName("cause") val messageVar = Variable(messageProperty.descriptor, exception.message.toState(messageProperty.getter!!.returnType)) - val causeVar = exception.cause?.let { Variable(causeProperty.descriptor, ExceptionState(it, irClass)) } + // TODO load stack trace from cause + val causeVar = exception.cause?.let { Variable(causeProperty.descriptor, ExceptionState(it, irClass, listOf())) } return listOfNotNull(messageVar, causeVar).toMutableList() } }