Preserve VM throw sites in stack traces
This commit is contained in:
parent
fada848907
commit
b42ceec686
@ -17,4 +17,5 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = null
|
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? =
|
||||||
|
if (index < 0 || index >= size) "Index $index out of bounds for length $size" else null
|
||||||
|
|||||||
@ -33,6 +33,17 @@ data class Pos(val source: Source, val line: Int, val column: Int) {
|
|||||||
if( end ) "EOF"
|
if( end ) "EOF"
|
||||||
else if( line >= 0 ) source.lines[line] else "<no line information>"
|
else if( line >= 0 ) source.lines[line] else "<no line information>"
|
||||||
|
|
||||||
|
val currentLineTrimmedStart: String get() = currentLine.trimStart()
|
||||||
|
|
||||||
|
val currentLineIndentWidth: Int
|
||||||
|
get() {
|
||||||
|
val lineText = currentLine
|
||||||
|
val firstNonWhitespace = lineText.indexOfFirst { !it.isWhitespace() }
|
||||||
|
return if (firstNonWhitespace >= 0) firstNonWhitespace else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val visualColumn: Int get() = (column - currentLineIndentWidth).coerceAtLeast(0)
|
||||||
|
|
||||||
val end: Boolean get() = line >= source.lines.size
|
val end: Boolean get() = line >= source.lines.size
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@ -25,14 +25,15 @@ open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable?
|
|||||||
"""
|
"""
|
||||||
$pos: Error: $errorMessage
|
$pos: Error: $errorMessage
|
||||||
|
|
||||||
${pos.currentLine}
|
${pos.currentLineTrimmedStart}
|
||||||
${if( pos.column >= 0 ) "-".repeat(pos.column) + "^" else ""}
|
${if( pos.column >= 0 ) "-".repeat(pos.visualColumn) + "^" else ""}
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
cause
|
cause
|
||||||
)
|
)
|
||||||
|
|
||||||
class ScriptFlowIsNoMoreCollected: Exception()
|
class ScriptFlowIsNoMoreCollected: Exception()
|
||||||
|
|
||||||
class ExecutionError(val errorObject: Obj, pos: Pos, message: String) : ScriptError(pos, message)
|
class ExecutionError(val errorObject: Obj, pos: Pos, message: String, cause: Throwable? = null) :
|
||||||
|
ScriptError(pos, message, cause)
|
||||||
|
|
||||||
class ImportException(pos: Pos, message: String) : ScriptError(pos, message)
|
class ImportException(pos: Pos, message: String) : ScriptError(pos, message)
|
||||||
|
|||||||
@ -3650,11 +3650,14 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileIndexRef(ref: IndexRef): CompiledValue? {
|
private fun compileIndexRef(ref: IndexRef): CompiledValue? {
|
||||||
|
val indexPos = refPosOrCurrent(ref.targetRef)
|
||||||
|
setPos(indexPos)
|
||||||
val receiver = compileRefWithFallback(ref.targetRef, null, Pos.builtIn) ?: return null
|
val receiver = compileRefWithFallback(ref.targetRef, null, Pos.builtIn) ?: return null
|
||||||
val elementSlotType = indexElementSlotType(receiver.slot, ref.targetRef)
|
val elementSlotType = indexElementSlotType(receiver.slot, ref.targetRef)
|
||||||
val dst = allocSlot()
|
val dst = allocSlot()
|
||||||
if (!ref.optionalRef) {
|
if (!ref.optionalRef) {
|
||||||
val index = compileRefWithFallback(ref.indexRef, null, Pos.builtIn) ?: return null
|
val index = compileRefWithFallback(ref.indexRef, null, Pos.builtIn) ?: return null
|
||||||
|
setPos(indexPos)
|
||||||
if (elementSlotType == SlotType.INT && index.type == SlotType.INT) {
|
if (elementSlotType == SlotType.INT && index.type == SlotType.INT) {
|
||||||
builder.emit(Opcode.GET_INDEX_INT, receiver.slot, index.slot, dst)
|
builder.emit(Opcode.GET_INDEX_INT, receiver.slot, index.slot, dst)
|
||||||
updateSlotType(dst, SlotType.INT)
|
updateSlotType(dst, SlotType.INT)
|
||||||
|
|||||||
@ -43,9 +43,10 @@ class CmdVm {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (!frame.handleException(e)) {
|
val throwable = frame.normalizeThrowable(e)
|
||||||
|
if (!frame.handleException(throwable)) {
|
||||||
frame.cancelIterators()
|
frame.cancelIterators()
|
||||||
throw e
|
throw throwable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4432,6 +4433,19 @@ class CmdFrame(
|
|||||||
return scope
|
return scope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun normalizeThrowable(t: Throwable): Throwable {
|
||||||
|
if (t is ExecutionError || t is ReturnException || t is LoopBreakContinueException) return t
|
||||||
|
val parentScope = ensureScope()
|
||||||
|
val pos = (t as? ScriptError)?.pos ?: currentErrorPos() ?: parentScope.pos
|
||||||
|
val throwScope = parentScope.createChildScope(pos = pos)
|
||||||
|
val message = when (t) {
|
||||||
|
is ScriptError -> t.errorMessage
|
||||||
|
else -> t.message ?: t.toString()
|
||||||
|
}
|
||||||
|
val errorObject = ObjUnknownException(throwScope, message).apply { getStackTrace() }
|
||||||
|
return ExecutionError(errorObject, pos, message, t)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun handleException(t: Throwable): Boolean {
|
suspend fun handleException(t: Throwable): Boolean {
|
||||||
val handler = tryStack.lastOrNull() ?: return false
|
val handler = tryStack.lastOrNull() ?: return false
|
||||||
vmIterDebug {
|
vmIterDebug {
|
||||||
|
|||||||
@ -106,16 +106,18 @@ open class ObjException(
|
|||||||
val pos = s.pos
|
val pos = s.pos
|
||||||
if (pos != lastPos && !pos.currentLine.isEmpty()) {
|
if (pos != lastPos && !pos.currentLine.isEmpty()) {
|
||||||
if (lastPos == null || (lastPos.source != pos.source || lastPos.line != pos.line)) {
|
if (lastPos == null || (lastPos.source != pos.source || lastPos.line != pos.line)) {
|
||||||
|
val sourceLine = pos.currentLineTrimmedStart
|
||||||
|
val visualColumn = pos.visualColumn
|
||||||
val fallback =
|
val fallback =
|
||||||
ObjString("#${pos.source.objSourceName}:${pos.line+1}:${pos.column+1}: ${pos.currentLine}")
|
ObjString("#${pos.source.objSourceName}:${pos.line+1}:${visualColumn+1}: $sourceLine")
|
||||||
if (maybeCls != null) {
|
if (maybeCls != null) {
|
||||||
try {
|
try {
|
||||||
result.list += maybeCls.callWithArgs(
|
result.list += maybeCls.callWithArgs(
|
||||||
scope,
|
scope,
|
||||||
pos.source.objSourceName,
|
pos.source.objSourceName,
|
||||||
ObjInt(pos.line.toLong()),
|
ObjInt(pos.line.toLong()),
|
||||||
ObjInt(pos.column.toLong()),
|
ObjInt(visualColumn.toLong()),
|
||||||
ObjString(pos.currentLine)
|
ObjString(sourceLine)
|
||||||
)
|
)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// Fallback textual entry if StackTraceEntry fails to instantiate
|
// Fallback textual entry if StackTraceEntry fails to instantiate
|
||||||
|
|||||||
@ -5261,6 +5261,35 @@ class ScriptTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnexpectedThrowablePreservesThrowSiteStackTrace() = runTest {
|
||||||
|
val caught = evalNamed(
|
||||||
|
"baderrorstack", """
|
||||||
|
fun boom() {
|
||||||
|
val arr = [10, 20, 30]
|
||||||
|
arr[10]
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
boom()
|
||||||
|
"unreachable"
|
||||||
|
} catch (e) {
|
||||||
|
e
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
val trace = caught.getLyngExceptionMessageWithStackTrace()
|
||||||
|
assertContains(trace, "\n at baderrorstack:3:1: arr[10]")
|
||||||
|
assertContains(trace, "\n at baderrorstack:6:1: boom()")
|
||||||
|
assertFalse(trace.contains("catch (e)"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testScriptErrorMessageTrimsSourceIndent() = runTest {
|
||||||
|
val x = ScriptError(Pos(Source("trimraw", " arr[10]"), 0, 4), "boom")
|
||||||
|
assertContains(x.message!!, "\narr[10]\n")
|
||||||
|
assertFalse(x.message!!.contains("\n arr[10]\n"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMapIteralAmbiguity() = runTest {
|
fun testMapIteralAmbiguity() = runTest {
|
||||||
eval(
|
eval(
|
||||||
|
|||||||
@ -22,6 +22,8 @@ import net.sergeych.lyng.bridge.globalBinder
|
|||||||
import net.sergeych.lyng.obj.ObjVoid
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class PrintlnOverrideTest {
|
class PrintlnOverrideTest {
|
||||||
|
|
||||||
@ -84,4 +86,46 @@ class PrintlnOverrideTest {
|
|||||||
|
|
||||||
assertEquals(listOf("gb top level", "gb inside function"), output)
|
assertEquals(listOf("gb top level", "gb inside function"), output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testExceptionPrintStackTraceFormatsPrimaryFrameBlock() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
val output = mutableListOf<String>()
|
||||||
|
|
||||||
|
scope.globalBinder().bindGlobalFun("println") {
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (i in 0 until args.size) {
|
||||||
|
if (i > 0) sb.append(" ")
|
||||||
|
sb.append(string(i))
|
||||||
|
}
|
||||||
|
output.add(sb.toString())
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
fun boom() {
|
||||||
|
val arr = [10, 20, 30]
|
||||||
|
var a = 10
|
||||||
|
var b = arr[a]
|
||||||
|
b
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
boom()
|
||||||
|
} catch (e) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
assertTrue(output.isNotEmpty())
|
||||||
|
assertEquals(
|
||||||
|
"IndexOutOfBoundsException: Index 10 out of bounds for length 3 at eval:4:9:",
|
||||||
|
output[0]
|
||||||
|
)
|
||||||
|
assertEquals("var b = arr[a]", output[1])
|
||||||
|
assertEquals("--------^", output[2])
|
||||||
|
assertTrue(output.size >= 4)
|
||||||
|
assertFalse(output.any { it == " at eval:4:9: var b = arr[a]" })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,4 +17,5 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = null
|
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? =
|
||||||
|
if (index < 0 || index >= size) "Index $index out of bounds for length $size" else null
|
||||||
|
|||||||
@ -17,4 +17,5 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = null
|
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? =
|
||||||
|
if (index < 0 || index >= size) "Index $index out of bounds for length $size" else null
|
||||||
|
|||||||
@ -441,9 +441,25 @@ static fun List<T>.fill(size: Int, block: (Int)->T): List<T> {
|
|||||||
|
|
||||||
/* Print this exception and its stack trace to standard output. */
|
/* Print this exception and its stack trace to standard output. */
|
||||||
fun Exception.printStackTrace(): void {
|
fun Exception.printStackTrace(): void {
|
||||||
println(this)
|
if( stackTrace.size == 0 ) {
|
||||||
|
println(this)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val first = stackTrace[0] as StackTraceEntry
|
||||||
|
var arrow = "^"
|
||||||
|
for( i in 0..<first.column ) {
|
||||||
|
arrow = "-" + arrow
|
||||||
|
}
|
||||||
|
println("%s: %s at %s:"(this::class.className, message, first.at))
|
||||||
|
println(first.sourceString)
|
||||||
|
println(arrow)
|
||||||
|
var skipFirst = true
|
||||||
for( entry in stackTrace ) {
|
for( entry in stackTrace ) {
|
||||||
println("\tat "+entry.toString())
|
if( skipFirst ) {
|
||||||
|
skipFirst = false
|
||||||
|
} else {
|
||||||
|
println(" at " + entry.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user