Fix exception class lookup and add lazy delegate

This commit is contained in:
Sergey Chernov 2026-01-30 23:22:02 +03:00
parent ffb22d0875
commit d363501081
7 changed files with 147 additions and 23 deletions

View File

@ -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<CatchBlockData>()
@ -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 {

View File

@ -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

View File

@ -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<Statement> = statements
suspend fun execute() = execute(
@ -363,6 +389,10 @@ class Script(
}
thunk
}
addFn("lazy") {
val builder = requireOnlyArg<Statement>()
ObjLazyDelegate(builder, this)
}
addVoidFn("delay") {
val a = args.firstAndOnly()

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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")
}
}

View File

@ -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(