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
|
||||
|
||||
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"
|
||||
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
|
||||
|
||||
companion object {
|
||||
|
||||
@ -25,14 +25,15 @@ open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable?
|
||||
"""
|
||||
$pos: Error: $errorMessage
|
||||
|
||||
${pos.currentLine}
|
||||
${if( pos.column >= 0 ) "-".repeat(pos.column) + "^" else ""}
|
||||
${pos.currentLineTrimmedStart}
|
||||
${if( pos.column >= 0 ) "-".repeat(pos.visualColumn) + "^" else ""}
|
||||
""".trimIndent(),
|
||||
cause
|
||||
)
|
||||
|
||||
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? {
|
||||
val indexPos = refPosOrCurrent(ref.targetRef)
|
||||
setPos(indexPos)
|
||||
val receiver = compileRefWithFallback(ref.targetRef, null, Pos.builtIn) ?: return null
|
||||
val elementSlotType = indexElementSlotType(receiver.slot, ref.targetRef)
|
||||
val dst = allocSlot()
|
||||
if (!ref.optionalRef) {
|
||||
val index = compileRefWithFallback(ref.indexRef, null, Pos.builtIn) ?: return null
|
||||
setPos(indexPos)
|
||||
if (elementSlotType == SlotType.INT && index.type == SlotType.INT) {
|
||||
builder.emit(Opcode.GET_INDEX_INT, receiver.slot, index.slot, dst)
|
||||
updateSlotType(dst, SlotType.INT)
|
||||
|
||||
@ -43,9 +43,10 @@ class CmdVm {
|
||||
}
|
||||
break
|
||||
} catch (e: Throwable) {
|
||||
if (!frame.handleException(e)) {
|
||||
val throwable = frame.normalizeThrowable(e)
|
||||
if (!frame.handleException(throwable)) {
|
||||
frame.cancelIterators()
|
||||
throw e
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4432,6 +4433,19 @@ class CmdFrame(
|
||||
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 {
|
||||
val handler = tryStack.lastOrNull() ?: return false
|
||||
vmIterDebug {
|
||||
|
||||
@ -106,16 +106,18 @@ open class ObjException(
|
||||
val pos = s.pos
|
||||
if (pos != lastPos && !pos.currentLine.isEmpty()) {
|
||||
if (lastPos == null || (lastPos.source != pos.source || lastPos.line != pos.line)) {
|
||||
val sourceLine = pos.currentLineTrimmedStart
|
||||
val visualColumn = pos.visualColumn
|
||||
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) {
|
||||
try {
|
||||
result.list += maybeCls.callWithArgs(
|
||||
scope,
|
||||
pos.source.objSourceName,
|
||||
ObjInt(pos.line.toLong()),
|
||||
ObjInt(pos.column.toLong()),
|
||||
ObjString(pos.currentLine)
|
||||
ObjInt(visualColumn.toLong()),
|
||||
ObjString(sourceLine)
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
// 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
|
||||
fun testMapIteralAmbiguity() = runTest {
|
||||
eval(
|
||||
|
||||
@ -22,6 +22,8 @@ import net.sergeych.lyng.bridge.globalBinder
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class PrintlnOverrideTest {
|
||||
|
||||
@ -84,4 +86,46 @@ class PrintlnOverrideTest {
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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. */
|
||||
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 ) {
|
||||
println("\tat "+entry.toString())
|
||||
if( skipFirst ) {
|
||||
skipFirst = false
|
||||
} else {
|
||||
println(" at " + entry.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user