diff --git a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardAndroid.kt b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardAndroid.kt index df4ab5d..640d2ba 100644 --- a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardAndroid.kt +++ b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardAndroid.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt index 8fc1065..fcaef69 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Pos.kt @@ -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 "" + 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 { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt index ad83801..c0c86e7 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScriptError.kt @@ -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) \ No newline at end of file +class ImportException(pos: Pos, message: String) : ScriptError(pos, message) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index af93b44..72415d5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index c6bc6e1..485cf3b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -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 { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt index 7574bab..eab741f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt @@ -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 diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 9e024bb..5833a15 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -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( diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/PrintlnOverrideTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/PrintlnOverrideTest.kt index c68ab8c..93250d9 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/PrintlnOverrideTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/PrintlnOverrideTest.kt @@ -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() + + 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]" }) + } } diff --git a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardJvm.kt b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardJvm.kt index df4ab5d..640d2ba 100644 --- a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardJvm.kt +++ b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardJvm.kt @@ -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 diff --git a/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardNative.kt b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardNative.kt index df4ab5d..640d2ba 100644 --- a/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardNative.kt +++ b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardNative.kt @@ -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 diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index a79458f..561c534 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -441,9 +441,25 @@ static fun List.fill(size: Int, block: (Int)->T): List { /* 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..