diff --git a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardAndroid.kt b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardAndroid.kt new file mode 100644 index 0000000..df4ab5d --- /dev/null +++ b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardAndroid.kt @@ -0,0 +1,20 @@ +/* + * 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.obj + +internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = null 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 7d692cf..1d807d3 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -3663,7 +3663,11 @@ class CmdGetIndex( val target = frame.storedSlotObj(targetSlot) val index = frame.storedSlotObj(indexSlot) if (target is ObjList && target::class == ObjList::class && index is ObjInt) { - frame.storeObjResult(dst, target.getObjAtFast(index.toInt())) + val i = index.toInt() + objListBoundsViolationMessageOrNull(target.sizeFast(), i)?.let { + frame.ensureScope().raiseIndexOutOfBounds(it) + } + frame.storeObjResult(dst, target.getObjAtFast(i)) return } val result = target.getAt(frame.ensureScope(), index) @@ -3682,7 +3686,11 @@ class CmdSetIndex( val index = frame.storedSlotObj(indexSlot) val value = frame.slotToObj(valueSlot) if (target is ObjList && target::class == ObjList::class && index is ObjInt) { - target.setObjAtFast(index.toInt(), value) + val i = index.toInt() + objListBoundsViolationMessageOrNull(target.sizeFast(), i)?.let { + frame.ensureScope().raiseIndexOutOfBounds(it) + } + target.setObjAtFast(i, value) return } target.putAt(frame.ensureScope(), index, value) @@ -3699,6 +3707,9 @@ class CmdGetIndexInt( val target = frame.storedSlotObj(targetSlot) val index = frame.getInt(indexSlot).toInt() if (target is ObjList && target::class == ObjList::class) { + objListBoundsViolationMessageOrNull(target.sizeFast(), index)?.let { + frame.ensureScope().raiseIndexOutOfBounds(it) + } target.getIntAtFast(index)?.let { frame.setInt(dst, it) return @@ -3722,6 +3733,9 @@ class CmdSetIndexInt( val target = frame.storedSlotObj(targetSlot) val index = frame.getInt(indexSlot).toInt() if (target is ObjList && target::class == ObjList::class) { + objListBoundsViolationMessageOrNull(target.sizeFast(), index)?.let { + frame.ensureScope().raiseIndexOutOfBounds(it) + } target.setIntAtFast(index, frame.getInt(valueSlot)) return } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt index 7937d04..5e800f1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjList.kt @@ -171,7 +171,9 @@ open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { override suspend fun getAt(scope: Scope, index: Obj): Obj { return when (index) { is ObjInt -> { - getObjAtFast(index.toInt()) + val i = index.toInt() + objListBoundsViolationMessageOrNull(sizeFast(), i)?.let { scope.raiseIndexOutOfBounds(it) } + getObjAtFast(i) } is ObjRange -> { @@ -209,7 +211,9 @@ open class ObjList(initialList: MutableList = mutableListOf()) : Obj() { } open override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) { - setObjAtFast(index.toInt(), newValue) + val i = index.toInt() + objListBoundsViolationMessageOrNull(sizeFast(), i)?.let { scope.raiseIndexOutOfBounds(it) } + setObjAtFast(i, newValue) } override suspend fun compareTo(scope: Scope, other: Obj): Int { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuard.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuard.kt new file mode 100644 index 0000000..92092b0 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuard.kt @@ -0,0 +1,20 @@ +/* + * 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.obj + +internal expect fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? diff --git a/lynglib/src/commonTest/kotlin/StdlibTest.kt b/lynglib/src/commonTest/kotlin/StdlibTest.kt index c2e5eb6..233ca8c 100644 --- a/lynglib/src/commonTest/kotlin/StdlibTest.kt +++ b/lynglib/src/commonTest/kotlin/StdlibTest.kt @@ -16,8 +16,16 @@ */ import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.Script import net.sergeych.lyng.eval +import net.sergeych.lyng.evalNamed +import net.sergeych.lyng.obj.ObjException +import net.sergeych.lyng.obj.ObjInstance +import net.sergeych.lyng.obj.getLyngExceptionMessage +import net.sergeych.lyng.obj.getLyngExceptionStackTrace import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue class StdlibTest { @Test @@ -373,4 +381,59 @@ class StdlibTest { """.trimIndent() ) } + + @Test + fun testErrorCatching() = runTest { + val error = evalNamed("testErrorCatching", """ + val src = [1,2,3] + val d = launch { + try { + for( i in 0..3 ) src[i] + } catch(e) { + e + } + } + d.await() + """.trimIndent() + ) + + val scope = when (error) { + is ObjException -> error.scope + is ObjInstance -> error.instanceScope + else -> Script.newScope() + } + val trace = error.getLyngExceptionStackTrace(scope) + val renderedTrace = trace.list.map { it.toString(scope).value } + + assertEquals("Index 3 out of bounds for length 3", error.getLyngExceptionMessage(scope)) + assertTrue(trace.list.size >= 2, "expected at least await and coroutine frames, got ${trace.list.size}") + assertTrue( + renderedTrace.all { it.contains("testErrorCatching:") }, + "unexpected trace entries: $renderedTrace" + ) + assertTrue( + renderedTrace.any { it.contains("launch") || it.contains("src[i]") }, + "trace should include the coroutine body: $renderedTrace" + ) + assertTrue( + renderedTrace.any { it.contains("d.await()") }, + "trace should include await site: $renderedTrace" + ) + } + + @Test + fun testCatchToIt() = runTest { + eval(""" + var x = 0 + try { + throw "msg1" + x = 1 + } + catch { + assert(it.message == "msg1") + x = 2 + } + assertEquals(2, x) + """.trimIndent()) + } } diff --git a/lynglib/src/jsMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardJs.kt b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardJs.kt new file mode 100644 index 0000000..640d2ba --- /dev/null +++ b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardJs.kt @@ -0,0 +1,21 @@ +/* + * 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.obj + +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/jvmMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardJvm.kt b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardJvm.kt new file mode 100644 index 0000000..df4ab5d --- /dev/null +++ b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardJvm.kt @@ -0,0 +1,20 @@ +/* + * 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.obj + +internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = 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 new file mode 100644 index 0000000..df4ab5d --- /dev/null +++ b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardNative.kt @@ -0,0 +1,20 @@ +/* + * 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.obj + +internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = null diff --git a/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardWasmJs.kt b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardWasmJs.kt new file mode 100644 index 0000000..640d2ba --- /dev/null +++ b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/obj/ObjListBoundsGuardWasmJs.kt @@ -0,0 +1,21 @@ +/* + * 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.obj + +internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = + if (index < 0 || index >= size) "Index $index out of bounds for length $size" else null