diff --git a/docs/embedding.md b/docs/embedding.md index 5869017..d541581 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -298,11 +298,12 @@ To simplify handling these objects from Kotlin, several extension methods are pr | :--- | :--- | | `obj.isLyngException()` | Returns `true` if the object is an instance of `Exception`. | | `obj.isInstanceOf("ClassName")` | Returns `true` if the object is an instance of the named Lyng class or its ancestors. | -| `obj.getLyngExceptionMessage(scope)` | Returns the exception message as a Kotlin `String`. | +| `obj.getLyngExceptionMessage(scope?=null)` | Returns the exception message as a Kotlin `String`. | +| `obj.getLyngExceptionMessageWithStackTrace(scope?=null)` | Returns a detailed message with a formatted stack trace. | | `obj.getLyngExceptionString(scope)` | Returns a formatted string including the class name, message, and primary throw site. | | `obj.getLyngExceptionStackTrace(scope)` | Returns the stack trace as an `ObjList` of `StackTraceEntry`. | | `obj.getLyngExceptionExtraData(scope)` | Returns the extra data associated with the exception. | -| `obj.raiseAsExecutionError(scope)` | Rethrows the object as a Kotlin `ExecutionError`. | +| `obj.raiseAsExecutionError(scope?=null)` | Rethrows the object as a Kotlin `ExecutionError`. | #### Example: Serialization and Rethrowing diff --git a/docs/exceptions_handling.md b/docs/exceptions_handling.md index 58725c8..ed0190e 100644 --- a/docs/exceptions_handling.md +++ b/docs/exceptions_handling.md @@ -132,6 +132,8 @@ Serializable class that conveys information about the exception. Important membe | stackTrace() | lyng stack trace, list of `StackTraceEntry`, see below | | printStackTrace() | format and print stack trace using println() | +> **Note for Kotlin users**: When working with Lyng exceptions from Kotlin, you can use extension methods like `getLyngExceptionMessageWithStackTrace()`. See [Embedding Lyng](embedding.md#12-handling-and-serializing-exceptions) for the full API. + ## StackTraceEntry A simple structire that stores single entry in Lyng stack, it is created automatically on exception creation: diff --git a/docs/whats_new.md b/docs/whats_new.md index a069007..0a118b5 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -183,3 +183,6 @@ lyng fmt --check MyFile.lyng # Check if file needs formatting Experimental lightweight autocompletion is now available in the IntelliJ plugin. It features type-aware member suggestions and inheritance-aware completion. You can enable it in **Settings | Lyng Formatter | Enable Lyng autocompletion**. + +### Kotlin API: Exception Handling +The `Obj.getLyngExceptionMessageWithStackTrace()` extension method has been added to simplify retrieving detailed error information from Lyng exception objects in Kotlin. Additionally, `getLyngExceptionMessage()` and `raiseAsExecutionError()` now accept an optional `Scope`, making it easier to use them when a scope is not immediately available. 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 b633a50..7ffb05c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt @@ -337,8 +337,37 @@ fun Obj.isLyngException(): Boolean = isInstanceOf("Exception") /** * Get the exception message. */ -suspend fun Obj.getLyngExceptionMessage(scope: Scope): String = - invokeInstanceMethod(scope, "message").toString(scope).value +suspend fun Obj.getLyngExceptionMessage(scope: Scope?=null): String { + require( this.isLyngException() ) + val s = scope ?: Script.newScope() + return invokeInstanceMethod(s, "message").toString(s).value +} + +/** + * Retrieves a detailed exception message including the stack trace for a Lyng exception. + * This function is designed to handle objects identified as Lyng exceptions. + * + * @param scope the scope to be used for fetching the exception message and stack trace. + * If null, a new scope will be created. + * @return a string combining the exception message, the location ("at"), + * and the formatted stack trace information. + * The stack trace details each frame using indentation for clarity. + * @throws IllegalArgumentException if the object is not a Lyng exception. + */ +suspend fun Obj.getLyngExceptionMessageWithStackTrace(scope: Scope?=null): String { + require( this.isLyngException() ) + val s = scope ?: Script.newScope() + val msg = getLyngExceptionMessage(s) + val trace = getLyngExceptionStackTrace(s) + var at = "unknown" + val stack = if( !trace.list.isEmpty() ) { + val first = trace.list[0] + at = (first.readField(s, "at").value as ObjString).value + "\n" + trace.list.map { " at " + it.toString(s).value }.joinToString("\n") + } + else "" + return "$at: $msg$stack" +} /** * Get the exception stack trace. diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 5be7ae8..6cc59ba 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -4671,5 +4671,17 @@ class ScriptTest { // source name, in our case, is is "tc2": assertContains(x1.message!!, "tc2") } + @Test + fun testLyngToKotlinExceptionHelpers() = runTest { + var x = evalNamed( "tc1",""" + IllegalArgumentException("test3") + """.trimIndent()) + assertEquals(""" + tc1:1:1: test3 + at tc1:1:1: IllegalArgumentException("test3") + """.trimIndent(), + x.getLyngExceptionMessageWithStackTrace() + ) + } } diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index af619c2..3e0c672 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -256,9 +256,10 @@ class StackTraceEntry( val column: Int, val sourceString: String ) { + val at by lazy { "%s:%s:%s"(sourceName,line+1,column+1) } /* Formatted representation: source:line:column: text. */ override fun toString() { - "%s:%d:%d: %s"(sourceName, line, column, sourceString.trim()) + "%s: %s"(at, sourceString.trim()) } }