Fix exception class lookup and add lazy delegate
This commit is contained in:
parent
ffb22d0875
commit
d363501081
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user