user exceptions to kotlin fixes
This commit is contained in:
parent
1d089db9ff
commit
c12804a806
@ -76,7 +76,7 @@ Now you can import lyng and use it:
|
||||
### Execute script:
|
||||
|
||||
```kotlin
|
||||
import net.sergeyh.lyng.*
|
||||
import net.sergeych.lyng.*
|
||||
|
||||
// we need a coroutine to start, as Lyng
|
||||
// is a coroutine based language, async topdown
|
||||
@ -92,9 +92,7 @@ Script is executed over some `Scope`. Create instance,
|
||||
add your specific vars and functions to it, and call:
|
||||
|
||||
```kotlin
|
||||
|
||||
import com.sun.source.tree.Scope
|
||||
import new.sergeych.lyng.*
|
||||
import net.sergeych.lyng.*
|
||||
|
||||
// simple function
|
||||
val scope = Script.newScope().apply {
|
||||
|
||||
@ -286,6 +286,45 @@ val isolated = net.sergeych.lyng.Scope.new()
|
||||
- When registering packages, names must be unique. Register before you compile/evaluate scripts that import them.
|
||||
- To debug scope content, `scope.toString()` and `scope.trace()` can help during development.
|
||||
|
||||
### 12) Handling and serializing exceptions
|
||||
|
||||
When Lyng code throws an exception, it is caught in Kotlin as an `ExecutionError`. This error wraps the actual Lyng `Obj` that was thrown (which could be a built-in `ObjException` or a user-defined `ObjInstance`).
|
||||
|
||||
To simplify handling these objects from Kotlin, several extension methods are provided on the `Obj` class. These methods work uniformly regardless of whether the exception is built-in or user-defined.
|
||||
|
||||
#### Uniform Exception API
|
||||
|
||||
| Method | Description |
|
||||
| :--- | :--- |
|
||||
| `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.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`. |
|
||||
|
||||
#### Example: Serialization and Rethrowing
|
||||
|
||||
You can serialize Lyng exception objects using `Lynon` to transmit them across boundaries and then rethrow them.
|
||||
|
||||
```kotlin
|
||||
try {
|
||||
scope.eval("throw MyUserException(404, \"Not Found\")")
|
||||
} catch (e: ExecutionError) {
|
||||
// 1. Serialize the Lyng exception object
|
||||
val encoded: UByteArray = lynonEncodeAny(scope, e.errorObject)
|
||||
|
||||
// ... (transmit 'encoded' byte array) ...
|
||||
|
||||
// 2. Deserialize it back to an Obj in a different context
|
||||
val decoded: Obj = lynonDecodeAny(scope, encoded)
|
||||
|
||||
// 3. Properly rethrow it on the Kotlin side using the uniform API
|
||||
decoded.raiseAsExecutionError(scope)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
That’s it. You now have Lyng embedded in your Kotlin app: you can expose your app’s API, evaluate user scripts, and organize your own packages to import from Lyng code.
|
||||
|
||||
@ -20,6 +20,7 @@ package net.sergeych.lyng
|
||||
import net.sergeych.lyng.Compiler.Companion.compile
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyng.pacman.ImportProvider
|
||||
|
||||
/**
|
||||
@ -3840,4 +3841,6 @@ class Compiler(
|
||||
}
|
||||
|
||||
suspend fun eval(code: String) = compile(code).execute()
|
||||
suspend fun evalNamed(name: String, code: String, importManager: ImportManager = Script.defaultImportManager) =
|
||||
compile(Source(name,code), importManager).execute()
|
||||
|
||||
|
||||
@ -67,6 +67,10 @@ open class Obj {
|
||||
someClass == rootObjectType ||
|
||||
(someClass is ObjClass && objClass.allImplementingNames.contains(someClass.className))
|
||||
|
||||
fun isInstanceOf(className: String) =
|
||||
objClass.mro.any { it.className == className } ||
|
||||
objClass.allImplementingNames.contains(className)
|
||||
|
||||
|
||||
suspend fun invokeInstanceMethod(scope: Scope, name: String, vararg args: Obj): Obj =
|
||||
invokeInstanceMethod(scope, name, Arguments(args.toList()))
|
||||
|
||||
@ -159,20 +159,17 @@ open class ObjException(
|
||||
}
|
||||
|
||||
val Root = ExceptionClass("Exception").apply {
|
||||
instanceConstructor = statement {
|
||||
val msg = args.getOrNull(0) ?: ObjString("Exception")
|
||||
if (thisObj is ObjInstance) {
|
||||
(thisObj as ObjInstance).instanceScope.addItem("Exception::message", false, msg)
|
||||
}
|
||||
ObjVoid
|
||||
}
|
||||
instanceInitializers.add(statement {
|
||||
if (thisObj is ObjInstance) {
|
||||
val msg = get("message")?.value ?: ObjString("Exception")
|
||||
(thisObj as ObjInstance).instanceScope.addItem("Exception::message", false, msg)
|
||||
|
||||
val stack = captureStackTrace(this)
|
||||
(thisObj as ObjInstance).instanceScope.addItem("Exception::stackTrace", false, stack)
|
||||
}
|
||||
ObjVoid
|
||||
})
|
||||
instanceConstructor = statement { ObjVoid }
|
||||
addConstDoc(
|
||||
name = "message",
|
||||
value = statement {
|
||||
@ -331,3 +328,43 @@ class ObjUnsetException(scope: Scope, message: String = "property is unset (not
|
||||
|
||||
class ObjNotImplementedException(scope: Scope, message: String = "not implemented") :
|
||||
ObjException("NotImplementedException", scope, message)
|
||||
|
||||
/**
|
||||
* Check if the object is an instance of Lyng Exception class.
|
||||
*/
|
||||
fun Obj.isLyngException(): Boolean = isInstanceOf("Exception")
|
||||
|
||||
/**
|
||||
* Get the exception message.
|
||||
*/
|
||||
suspend fun Obj.getLyngExceptionMessage(scope: Scope): String =
|
||||
invokeInstanceMethod(scope, "message").toString(scope).value
|
||||
|
||||
/**
|
||||
* Get the exception stack trace.
|
||||
*/
|
||||
suspend fun Obj.getLyngExceptionStackTrace(scope: Scope): ObjList =
|
||||
invokeInstanceMethod(scope, "stackTrace").cast(scope)
|
||||
|
||||
/**
|
||||
* Get the exception extra data.
|
||||
*/
|
||||
suspend fun Obj.getLyngExceptionExtraData(scope: Scope): Obj =
|
||||
invokeInstanceMethod(scope, "extraData")
|
||||
|
||||
/**
|
||||
* Get the exception as a formatted string with the primary throw site.
|
||||
*/
|
||||
suspend fun Obj.getLyngExceptionString(scope: Scope): String =
|
||||
invokeInstanceMethod(scope, "toString").toString(scope).value
|
||||
|
||||
/**
|
||||
* Rethrow this object as a Kotlin [ExecutionError] if it's an exception.
|
||||
*/
|
||||
suspend fun Obj.raiseAsExecutionError(scope: Scope?=null): Nothing {
|
||||
if (this is ObjException) raise()
|
||||
val sc = scope ?: Script.newScope()
|
||||
val msg = getLyngExceptionMessage(sc)
|
||||
val pos = (this as? ObjInstance)?.instanceScope?.pos ?: Pos.builtIn
|
||||
throw ExecutionError(this, pos, msg)
|
||||
}
|
||||
|
||||
@ -294,10 +294,12 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
// }
|
||||
|
||||
val serializingVars: Map<String, ObjRecord> by lazy {
|
||||
val metaParams = objClass.constructorMeta?.params?.map { it.name }?.toSet() ?: emptySet()
|
||||
instanceScope.objects.filter {
|
||||
it.value.type.serializable &&
|
||||
it.value.type == ObjRecord.Type.Field &&
|
||||
it.value.isMutable
|
||||
it.value.isMutable &&
|
||||
!metaParams.contains(it.key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
121
lynglib/src/commonTest/kotlin/EmbeddingExceptionTest.kt
Normal file
121
lynglib/src/commonTest/kotlin/EmbeddingExceptionTest.kt
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lynon.lynonDecodeAny
|
||||
import net.sergeych.lynon.lynonEncodeAny
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class EmbeddingExceptionTest {
|
||||
|
||||
@Test
|
||||
fun testExceptionMessageSerialization() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val ex = scope.eval("Exception(\"test message\")")
|
||||
val encoded = lynonEncodeAny(scope, ex)
|
||||
val decoded = lynonDecodeAny(scope, encoded)
|
||||
assertEquals("test message", decoded.getLyngExceptionMessage(scope))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsInstanceOfString() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval("class T")
|
||||
val t = scope.eval("T()")
|
||||
assertTrue(t.isInstanceOf("T"))
|
||||
assertTrue(t.isInstanceOf("Obj"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExceptionSerializationAndRethrow() = runTest {
|
||||
val scope = Script.newScope()
|
||||
|
||||
// 1. Define, throw and catch the exception in Lyng to get the object
|
||||
val errorObj = scope.eval("""
|
||||
class MyException(val code, m) : Exception(m)
|
||||
try {
|
||||
throw MyException(123, "something failed")
|
||||
} catch {
|
||||
it
|
||||
}
|
||||
""".trimIndent())
|
||||
|
||||
assertTrue(errorObj.isLyngException(), "Should be a Lyng exception")
|
||||
assertTrue(errorObj.isInstanceOf("MyException"), "Should be a MyException")
|
||||
|
||||
// 2. Serialize it
|
||||
val encoded = lynonEncodeAny(scope, errorObj)
|
||||
|
||||
// 3. Deserialize it
|
||||
val decodedObj = lynonDecodeAny(scope, encoded)
|
||||
|
||||
// 4. Rethrow it using the new uniform extension
|
||||
val rethrown = try {
|
||||
decodedObj.raiseAsExecutionError(scope)
|
||||
} catch (e: ExecutionError) {
|
||||
e
|
||||
}
|
||||
|
||||
// 5. Verify the rethrown exception preserves its identity and data
|
||||
val caughtObj = rethrown.errorObject
|
||||
assertTrue(caughtObj.isLyngException())
|
||||
assertTrue(caughtObj.isInstanceOf("MyException"))
|
||||
assertIs<ObjInstance>(caughtObj)
|
||||
assertEquals("MyException", caughtObj.objClass.className)
|
||||
|
||||
// Verify we can still access the custom fields
|
||||
// 'code' is a field, so we use readField
|
||||
val code = caughtObj.readField(scope, "code").value.toKotlin(scope)
|
||||
assertEquals(123L, code)
|
||||
|
||||
val message = caughtObj.getLyngExceptionMessage(scope)
|
||||
assertEquals("something failed", message)
|
||||
|
||||
// Verify stack trace is available
|
||||
val stack = caughtObj.getLyngExceptionStackTrace(scope)
|
||||
assertTrue(stack.list.isNotEmpty(), "Stack trace should not be empty")
|
||||
|
||||
val errorString = caughtObj.getLyngExceptionString(scope)
|
||||
assertTrue(errorString.contains("MyException: something failed"), "Error string should contain message")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBuiltInExceptionSerialization() = runTest {
|
||||
val scope = Script.newScope()
|
||||
|
||||
val encoded = try {
|
||||
scope.eval("throw IllegalArgumentException(\"bad arg\")")
|
||||
null
|
||||
} catch (e: ExecutionError) {
|
||||
lynonEncodeAny(scope, e.errorObject)
|
||||
}!!
|
||||
|
||||
val decodedObj = lynonDecodeAny(scope, encoded)
|
||||
assertTrue(decodedObj.isLyngException())
|
||||
assertTrue(decodedObj.isInstanceOf("IllegalArgumentException"))
|
||||
assertIs<ObjException>(decodedObj)
|
||||
|
||||
val message = decodedObj.getLyngExceptionMessage(scope)
|
||||
assertEquals("bad arg", message)
|
||||
}
|
||||
}
|
||||
@ -4558,6 +4558,7 @@ class ScriptTest {
|
||||
val x = try { throw IllegalAccessException("test1") } catch { it }
|
||||
assertEquals("test1", x.message)
|
||||
assert( x is IllegalAccessException)
|
||||
assert( x is Exception )
|
||||
assertThrows(IllegalAccessException) { throw IllegalAccessException("test2") }
|
||||
|
||||
class X : Exception("test3")
|
||||
@ -4565,6 +4566,7 @@ class ScriptTest {
|
||||
println(y)
|
||||
assertEquals("test3", y.message)
|
||||
assert( y is X)
|
||||
assert( y is Exception )
|
||||
|
||||
""".trimIndent())
|
||||
}
|
||||
@ -4601,6 +4603,17 @@ class ScriptTest {
|
||||
assert( x is UserException)
|
||||
val y = try { throw IllegalStateException() } catch { it }
|
||||
assert( y is IllegalStateException)
|
||||
|
||||
// creating exceptions as usual objects:
|
||||
val z = IllegalArgumentException()
|
||||
assert( z is Exception )
|
||||
assert( z is IllegalArgumentException )
|
||||
|
||||
class X : Exception
|
||||
val t = X()
|
||||
assert( t is X )
|
||||
assert( t is Exception )
|
||||
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@ -4635,4 +4648,28 @@ class ScriptTest {
|
||||
assert(caught.message.contains("Expected DerivedEx, got MyEx"))
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRaiseAsError() = runTest {
|
||||
var x = evalNamed( "tc1","""
|
||||
IllegalArgumentException("test3")
|
||||
""".trimIndent())
|
||||
var x1 = try { x.raiseAsExecutionError() } catch(e: ExecutionError) { e }
|
||||
println(x1.message)
|
||||
assertTrue { "tc1:1" in x1.message!! }
|
||||
assertTrue { "test3" in x1.message!! }
|
||||
|
||||
// With user exception classes it should be the same at top level:
|
||||
x = evalNamed("tc2","""
|
||||
class E: Exception("test4")
|
||||
E()
|
||||
""".trimIndent())
|
||||
x1 = try { x.raiseAsExecutionError() } catch(e: ExecutionError) { e }
|
||||
println(x1.message)
|
||||
assertContains(x1.message!!, "test4")
|
||||
// the reported error message should include proper trace, which must include
|
||||
// source name, in our case, is is "tc2":
|
||||
assertContains(x1.message!!, "tc2")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -100,6 +100,14 @@ val worker = object : Runnable {
|
||||
override fun run() = println("Working...")
|
||||
}
|
||||
worker.run()
|
||||
|
||||
// User-defined exceptions: real classes with custom fields
|
||||
class MyError(val code, m) : Exception(m)
|
||||
try {
|
||||
throw MyError(500, "Something failed")
|
||||
} catch (e: MyError) {
|
||||
println("Error " + e.code + ": " + e.message)
|
||||
}
|
||||
>>> void
|
||||
""".trimIndent()
|
||||
val mapHtml = "<pre><code>" + htmlEscape(code) + "</code></pre>"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user