From d3635010819068ad6be6577621ebb1010e027f53 Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 30 Jan 2026 23:22:02 +0300 Subject: [PATCH] Fix exception class lookup and add lazy delegate --- .../kotlin/net/sergeych/lyng/Compiler.kt | 31 +++++++--- .../kotlin/net/sergeych/lyng/Scope.kt | 8 +++ .../kotlin/net/sergeych/lyng/Script.kt | 32 ++++++++++- .../lyng/bytecode/BytecodeStatement.kt | 1 + .../net/sergeych/lyng/obj/ObjException.kt | 36 +++++++++--- .../net/sergeych/lyng/obj/ObjLazyDelegate.kt | 57 +++++++++++++++++++ lynglib/src/commonTest/kotlin/ScriptTest.kt | 5 -- 7 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 039bf97..07596ed 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -458,10 +458,14 @@ class Compiler( private fun seedResolutionFromScope(scope: Scope, pos: Pos) { val sink = resolutionSink ?: return - for ((name, record) in scope.objects) { - if (!record.visibility.isPublic) continue - if (!resolutionPredeclared.add(name)) continue - sink.declareSymbol(name, SymbolKind.LOCAL, record.isMutable, pos) + var current: Scope? = scope + while (current != null) { + for ((name, record) in current.objects) { + if (!record.visibility.isPublic) continue + if (!resolutionPredeclared.add(name)) continue + sink.declareSymbol(name, SymbolKind.LOCAL, record.isMutable, pos) + } + current = current.parent } } @@ -2635,6 +2639,13 @@ class Compiler( if (stmt.captureSlots.isEmpty()) return stmt return BlockStatement(stmt.block, stmt.slotPlan, emptyList(), stmt.pos) } + fun resolveExceptionClass(scope: Scope, name: String): ObjClass { + val rec = scope[name] + val cls = rec?.value as? ObjClass + if (cls != null) return cls + if (name == "Exception") return ObjException.Root + scope.raiseSymbolNotFound("error class does not exist or is not a class: $name") + } val body = unwrapBytecodeDeep(parseBlock()) val catches = mutableListOf() @@ -2742,8 +2753,7 @@ class Compiler( for (cdata in catches) { var match: Obj? = null for (exceptionClassName in cdata.classNames) { - val exObj = scope[exceptionClassName]?.value as? ObjClass - ?: scope.raiseSymbolNotFound("error class does not exist or is not a class: $exceptionClassName") + val exObj = resolveExceptionClass(scope, exceptionClassName) if (caughtObj.isInstanceOf(exObj)) { match = caughtObj break @@ -3131,9 +3141,12 @@ class Compiler( // accessors, constructor registration, etc. // Resolve parent classes by name at execution time val parentClasses = baseSpecs.map { baseSpec -> - val rec = - scope[baseSpec.name] ?: throw ScriptError(nameToken.pos, "unknown base class: ${baseSpec.name}") - (rec.value as? ObjClass) ?: throw ScriptError(nameToken.pos, "${baseSpec.name} is not a class") + val rec = scope[baseSpec.name] + val cls = rec?.value as? ObjClass + if (cls != null) return@map cls + if (baseSpec.name == "Exception") return@map ObjException.Root + if (rec == null) throw ScriptError(nameToken.pos, "unknown base class: ${baseSpec.name}") + throw ScriptError(nameToken.pos, "${baseSpec.name} is not a class") } val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray()).also { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index 90ce1cf..bbf3043 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -126,6 +126,14 @@ open class Scope( } s.getSlotIndexOf(name)?.let { idx -> val rec = s.getSlotRecord(idx) + val hasDirectBinding = + s.objects.containsKey(name) || + s.localBindings.containsKey(name) || + (caller?.let { ctx -> + s.objects.containsKey(ctx.mangledName(name)) || + s.localBindings.containsKey(ctx.mangledName(name)) + } ?: false) + if (!hasDirectBinding && rec.value === ObjUnset) return null if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec } return null diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 75d9f85..1a79639 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -55,10 +55,36 @@ class Script( scope.updateSlotFor(name, scope.objects[name]!!) continue } - parent.get(name)?.let { scope.updateSlotFor(name, it) } + val seed = findSeedRecord(parent, name) + if (seed != null) { + if (name == "Exception" && seed.value !is ObjClass) { + scope.updateSlotFor(name, ObjRecord(ObjException.Root, isMutable = false)) + } else { + scope.updateSlotFor(name, seed) + } + continue + } + if (name == "Exception") { + scope.updateSlotFor(name, ObjRecord(ObjException.Root, isMutable = false)) + } } } + private fun findSeedRecord(scope: Scope?, name: String): ObjRecord? { + var s = scope + var hops = 0 + while (s != null && hops++ < 1024) { + s.objects[name]?.let { return it } + s.localBindings[name]?.let { return it } + s.getSlotIndexOf(name)?.let { idx -> + val rec = s.getSlotRecord(idx) + if (rec.value !== ObjUnset) return rec + } + s = s.parent + } + return null + } + internal fun debugStatements(): List = statements suspend fun execute() = execute( @@ -363,6 +389,10 @@ class Script( } thunk } + addFn("lazy") { + val builder = requireOnlyArg() + ObjLazyDelegate(builder, this) + } addVoidFn("delay") { val a = args.firstAndOnly() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt index a44a5f2..802a1ff 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -36,6 +36,7 @@ class BytecodeStatement private constructor( override val pos: Pos = original.pos override suspend fun execute(scope: Scope): Obj { + scope.pos = pos return CmdVm().execute(function, scope, scope.args.list) } 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 9843b42..af24a2d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt @@ -344,7 +344,11 @@ fun Obj.isLyngException(): Boolean = isInstanceOf("Exception") */ suspend fun Obj.getLyngExceptionMessage(scope: Scope? = null): String { require(this.isLyngException()) - val s = scope ?: Script.newScope() + val s = scope ?: when (this) { + is ObjException -> this.scope + is ObjInstance -> this.instanceScope + else -> Script.newScope() + } return invokeInstanceMethod(s, "message").toString(s).value } @@ -361,16 +365,25 @@ suspend fun Obj.getLyngExceptionMessage(scope: Scope? = null): String { */ suspend fun Obj.getLyngExceptionMessageWithStackTrace(scope: Scope? = null,showDetails:Boolean=true): String { require(this.isLyngException()) - val s = scope ?: Script.newScope() + val s = scope ?: when (this) { + is ObjException -> this.scope + is ObjInstance -> this.instanceScope + else -> Script.newScope() + } val msg = getLyngExceptionMessage(s) val trace = getLyngExceptionStackTrace(s) var at = "unknown" -// var firstLine = true 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 "" + } else { + val pos = s.pos + if (pos.source.fileName.isNotEmpty() && pos.currentLine.isNotEmpty()) { + at = "${pos.source.fileName}:${pos.line + 1}:${pos.column + 1}" + } + "" + } return "$at: $msg$stack" } @@ -396,9 +409,16 @@ suspend fun Obj.getLyngExceptionString(scope: Scope): String = * 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 + val sc = scope ?: when (this) { + is ObjException -> this.scope + is ObjInstance -> this.instanceScope + else -> Script.newScope() + } + val msg = getLyngExceptionMessageWithStackTrace(sc) + val pos = when (this) { + is ObjException -> this.scope.pos + is ObjInstance -> this.instanceScope.pos + else -> Pos.builtIn + } throw ExecutionError(this, pos, msg) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt new file mode 100644 index 0000000..bfac5da --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjLazyDelegate.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * 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 + +import net.sergeych.lyng.Arguments +import net.sergeych.lyng.Scope +import net.sergeych.lyng.Statement + +/** + * Lazy delegate used by `val x by lazy { ... }`. + */ +class ObjLazyDelegate( + private val builder: Statement, + private val capturedScope: Scope, +) : Obj() { + override val objClass: ObjClass = type + + private var calculated = false + private var cachedValue: Obj = ObjVoid + + override suspend fun invokeInstanceMethod( + scope: Scope, + name: String, + args: Arguments, + onNotFoundResult: (suspend () -> Obj?)?, + ): Obj { + return when (name) { + "getValue" -> { + if (!calculated) { + cachedValue = builder.execute(capturedScope) + calculated = true + } + cachedValue + } + "setValue" -> scope.raiseIllegalAssignment("lazy delegate is read-only") + else -> super.invokeInstanceMethod(scope, name, args, onNotFoundResult) + } + } + + companion object { + val type = ObjClass("LazyDelegate") + } +} diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index cb977a0..f93911c 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -3696,7 +3696,6 @@ class ScriptTest { ) } - @Ignore("incremental enable: stackTrace member not resolved in bytecode path") @Test fun testExceptionSerialization() = runTest { eval( @@ -3854,7 +3853,6 @@ class ScriptTest { ) } - @Ignore("incremental enable: extension resolution for toList in chained call") @Test fun binarySearchTest2() = runTest { eval( @@ -4829,7 +4827,6 @@ class ScriptTest { ) } - @Ignore("incremental enable: raiseAsExecutionError missing source name in trace") @Test fun testRaiseAsError() = runTest { var x = evalNamed( @@ -4891,7 +4888,6 @@ class ScriptTest { } - @Ignore("incremental enable: exception helper missing source info") @Test fun testLyngToKotlinExceptionHelpers() = runTest { var x = evalNamed( @@ -4949,7 +4945,6 @@ class ScriptTest { ) } - @Ignore("incremental enable: lazy delegate not resolved in new compiler") @Test fun testLazyLocals() = runTest() { eval(