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) { private fun seedResolutionFromScope(scope: Scope, pos: Pos) {
val sink = resolutionSink ?: return val sink = resolutionSink ?: return
for ((name, record) in scope.objects) { var current: Scope? = scope
if (!record.visibility.isPublic) continue while (current != null) {
if (!resolutionPredeclared.add(name)) continue for ((name, record) in current.objects) {
sink.declareSymbol(name, SymbolKind.LOCAL, record.isMutable, pos) 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 if (stmt.captureSlots.isEmpty()) return stmt
return BlockStatement(stmt.block, stmt.slotPlan, emptyList(), stmt.pos) 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 body = unwrapBytecodeDeep(parseBlock())
val catches = mutableListOf<CatchBlockData>() val catches = mutableListOf<CatchBlockData>()
@ -2742,8 +2753,7 @@ class Compiler(
for (cdata in catches) { for (cdata in catches) {
var match: Obj? = null var match: Obj? = null
for (exceptionClassName in cdata.classNames) { for (exceptionClassName in cdata.classNames) {
val exObj = scope[exceptionClassName]?.value as? ObjClass val exObj = resolveExceptionClass(scope, exceptionClassName)
?: scope.raiseSymbolNotFound("error class does not exist or is not a class: $exceptionClassName")
if (caughtObj.isInstanceOf(exObj)) { if (caughtObj.isInstanceOf(exObj)) {
match = caughtObj match = caughtObj
break break
@ -3131,9 +3141,12 @@ class Compiler(
// accessors, constructor registration, etc. // accessors, constructor registration, etc.
// Resolve parent classes by name at execution time // Resolve parent classes by name at execution time
val parentClasses = baseSpecs.map { baseSpec -> val parentClasses = baseSpecs.map { baseSpec ->
val rec = val rec = scope[baseSpec.name]
scope[baseSpec.name] ?: throw ScriptError(nameToken.pos, "unknown base class: ${baseSpec.name}") val cls = rec?.value as? ObjClass
(rec.value as? ObjClass) ?: throw ScriptError(nameToken.pos, "${baseSpec.name} is not a class") 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 { val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray()).also {

View File

@ -126,6 +126,14 @@ open class Scope(
} }
s.getSlotIndexOf(name)?.let { idx -> s.getSlotIndexOf(name)?.let { idx ->
val rec = s.getSlotRecord(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 if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec
} }
return null return null

View File

@ -55,10 +55,36 @@ class Script(
scope.updateSlotFor(name, scope.objects[name]!!) scope.updateSlotFor(name, scope.objects[name]!!)
continue 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 internal fun debugStatements(): List<Statement> = statements
suspend fun execute() = execute( suspend fun execute() = execute(
@ -363,6 +389,10 @@ class Script(
} }
thunk thunk
} }
addFn("lazy") {
val builder = requireOnlyArg<Statement>()
ObjLazyDelegate(builder, this)
}
addVoidFn("delay") { addVoidFn("delay") {
val a = args.firstAndOnly() val a = args.firstAndOnly()

View File

@ -36,6 +36,7 @@ class BytecodeStatement private constructor(
override val pos: Pos = original.pos override val pos: Pos = original.pos
override suspend fun execute(scope: Scope): Obj { override suspend fun execute(scope: Scope): Obj {
scope.pos = pos
return CmdVm().execute(function, scope, scope.args.list) 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 { suspend fun Obj.getLyngExceptionMessage(scope: Scope? = null): String {
require(this.isLyngException()) 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 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 { suspend fun Obj.getLyngExceptionMessageWithStackTrace(scope: Scope? = null,showDetails:Boolean=true): String {
require(this.isLyngException()) 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 msg = getLyngExceptionMessage(s)
val trace = getLyngExceptionStackTrace(s) val trace = getLyngExceptionStackTrace(s)
var at = "unknown" var at = "unknown"
// var firstLine = true
val stack = if (!trace.list.isEmpty()) { val stack = if (!trace.list.isEmpty()) {
val first = trace.list[0] val first = trace.list[0]
at = (first.readField(s, "at").value as ObjString).value at = (first.readField(s, "at").value as ObjString).value
"\n" + trace.list.map { " at " + it.toString(s).value }.joinToString("\n") "\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" 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. * Rethrow this object as a Kotlin [ExecutionError] if it's an exception.
*/ */
suspend fun Obj.raiseAsExecutionError(scope: Scope? = null): Nothing { suspend fun Obj.raiseAsExecutionError(scope: Scope? = null): Nothing {
if (this is ObjException) raise() val sc = scope ?: when (this) {
val sc = scope ?: Script.newScope() is ObjException -> this.scope
val msg = getLyngExceptionMessage(sc) is ObjInstance -> this.instanceScope
val pos = (this as? ObjInstance)?.instanceScope?.pos ?: Pos.builtIn 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) 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 @Test
fun testExceptionSerialization() = runTest { fun testExceptionSerialization() = runTest {
eval( eval(
@ -3854,7 +3853,6 @@ class ScriptTest {
) )
} }
@Ignore("incremental enable: extension resolution for toList in chained call")
@Test @Test
fun binarySearchTest2() = runTest { fun binarySearchTest2() = runTest {
eval( eval(
@ -4829,7 +4827,6 @@ class ScriptTest {
) )
} }
@Ignore("incremental enable: raiseAsExecutionError missing source name in trace")
@Test @Test
fun testRaiseAsError() = runTest { fun testRaiseAsError() = runTest {
var x = evalNamed( var x = evalNamed(
@ -4891,7 +4888,6 @@ class ScriptTest {
} }
@Ignore("incremental enable: exception helper missing source info")
@Test @Test
fun testLyngToKotlinExceptionHelpers() = runTest { fun testLyngToKotlinExceptionHelpers() = runTest {
var x = evalNamed( var x = evalNamed(
@ -4949,7 +4945,6 @@ class ScriptTest {
) )
} }
@Ignore("incremental enable: lazy delegate not resolved in new compiler")
@Test @Test
fun testLazyLocals() = runTest() { fun testLazyLocals() = runTest() {
eval( eval(