fix endless recursion in scope resolution in some specific cases
This commit is contained in:
parent
c0fab3d60e
commit
bcabfc8962
@ -117,25 +117,30 @@ kotlin {
|
||||
}
|
||||
|
||||
// ---- Build-time generation of stdlib text from .lyng files into a Kotlin constant ----
|
||||
// The .lyng source of the stdlib lives here (module-relative path):
|
||||
val lyngStdlibDir = layout.projectDirectory.dir("stdlib/lyng")
|
||||
// The generated Kotlin source will be placed here and added to commonMain sources:
|
||||
val generatedLyngStdlibDir = layout.buildDirectory.dir("generated/source/lyngStdlib/commonMain/kotlin")
|
||||
// Implemented as a proper task type compatible with Gradle Configuration Cache
|
||||
|
||||
val generateLyngStdlib by tasks.registering {
|
||||
group = "build"
|
||||
description = "Generate Kotlin source with embedded lyng stdlib text"
|
||||
inputs.dir(lyngStdlibDir)
|
||||
outputs.dir(generatedLyngStdlibDir)
|
||||
// Simpler: opt out of configuration cache for this ad-hoc generator task
|
||||
notCompatibleWithConfigurationCache("Uses dynamic file IO in doLast; trivial generator")
|
||||
abstract class GenerateLyngStdlib : DefaultTask() {
|
||||
@get:InputDirectory
|
||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||
abstract val sourceDir: DirectoryProperty
|
||||
|
||||
doLast {
|
||||
@get:OutputDirectory
|
||||
abstract val outputDir: DirectoryProperty
|
||||
|
||||
@TaskAction
|
||||
fun generate() {
|
||||
val targetPkg = "net.sergeych.lyng.stdlib_included"
|
||||
val targetDir = generatedLyngStdlibDir.get().asFile.resolve(targetPkg.replace('.', '/'))
|
||||
val pkgPath = targetPkg.replace('.', '/')
|
||||
val outBase = outputDir.get().asFile
|
||||
val targetDir = outBase.resolve(pkgPath)
|
||||
targetDir.mkdirs()
|
||||
|
||||
val files = lyngStdlibDir.asFileTree.matching { include("**/*.lyng") }.files.sortedBy { it.name }
|
||||
val srcDir = sourceDir.get().asFile
|
||||
val files = srcDir.walkTopDown()
|
||||
.filter { it.isFile && it.extension == "lyng" }
|
||||
.sortedBy { it.name }
|
||||
.toList()
|
||||
|
||||
val content = if (files.isEmpty()) "" else buildString {
|
||||
files.forEachIndexed { idx, f ->
|
||||
val text = f.readText()
|
||||
@ -144,13 +149,12 @@ val generateLyngStdlib by tasks.registering {
|
||||
}
|
||||
}
|
||||
|
||||
// Emit as a regular quoted Kotlin string to avoid triple-quote edge cases
|
||||
fun escapeForQuoted(s: String): String = buildString {
|
||||
for (ch in s) when (ch) {
|
||||
'\\' -> append("\\\\")
|
||||
'"' -> append("\\\"")
|
||||
'\n' -> append("\\n")
|
||||
'\r' -> {} // drop CR
|
||||
'\r' -> {}
|
||||
'\t' -> append("\\t")
|
||||
else -> append(ch)
|
||||
}
|
||||
@ -168,6 +172,18 @@ val generateLyngStdlib by tasks.registering {
|
||||
}
|
||||
}
|
||||
|
||||
// The .lyng source of the stdlib lives here (module-relative path):
|
||||
val lyngStdlibDir = layout.projectDirectory.dir("stdlib/lyng")
|
||||
// The generated Kotlin source will be placed here and added to commonMain sources:
|
||||
val generatedLyngStdlibDir = layout.buildDirectory.dir("generated/source/lyngStdlib/commonMain/kotlin")
|
||||
|
||||
val generateLyngStdlib by tasks.registering(GenerateLyngStdlib::class) {
|
||||
group = "build"
|
||||
description = "Generate Kotlin source with embedded lyng stdlib text"
|
||||
sourceDir.set(lyngStdlibDir)
|
||||
outputDir.set(generatedLyngStdlibDir)
|
||||
}
|
||||
|
||||
// Add the generated directory to commonMain sources
|
||||
kotlin.sourceSets.named("commonMain") {
|
||||
kotlin.srcDir(generatedLyngStdlibDir)
|
||||
|
||||
@ -38,35 +38,80 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
|
||||
}
|
||||
|
||||
override fun get(name: String): ObjRecord? {
|
||||
// Fast-path built-ins
|
||||
if (name == "this") return thisObj.asReadonly
|
||||
|
||||
// Priority:
|
||||
// 1) Locals and arguments declared in this lambda frame (including values defined before suspension)
|
||||
// 2) Instance/class members of the captured receiver (`closureScope.thisObj`), e.g., fields like `coll`, `factor`
|
||||
// 3) Symbols from the captured closure scope (its locals and parents)
|
||||
// 2) Instance/class members of the captured receiver (`closureScope.thisObj`)
|
||||
// 3) Symbols from the captured closure scope chain (locals/parents), ignoring nested ClosureScope overrides
|
||||
// 4) Instance members of the caller's `this` (e.g., FlowBuilder.emit)
|
||||
// 5) Fallback to the standard chain (this frame -> parent (callScope) -> class members)
|
||||
// 5) Symbols from the caller chain (locals/parents), ignoring nested ClosureScope overrides
|
||||
// 6) Special fallback for module pseudo-symbols (e.g., __PACKAGE__)
|
||||
|
||||
// First, prefer locals/arguments bound in this frame
|
||||
// 1) Locals/arguments in this closure frame
|
||||
super.objects[name]?.let { return it }
|
||||
super.localBindings[name]?.let { return it }
|
||||
|
||||
// Prefer instance fields/methods declared on the captured receiver:
|
||||
// First, resolve real instance fields stored in the instance scope (constructor vars like `coll`, `factor`)
|
||||
// 2) Members on the captured receiver instance
|
||||
(closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)
|
||||
?.instanceScope
|
||||
?.objects
|
||||
?.get(name)
|
||||
?.let { return it }
|
||||
|
||||
// Then, try class-declared members (methods/properties declared in the class body)
|
||||
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
|
||||
// Then delegate to the full closure scope chain (locals, parents, etc.)
|
||||
closureScope.get(name)?.let { return it }
|
||||
// 3) Closure scope chain (locals/parents + members), ignore ClosureScope overrides to prevent recursion
|
||||
closureScope.chainLookupWithMembers(name)?.let { return it }
|
||||
|
||||
// Allow resolving instance members of the caller's `this` (e.g., FlowBuilder.emit)
|
||||
// 4) Caller `this` members
|
||||
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
|
||||
// Fallback to the standard lookup chain: this frame -> parent (callScope) -> class members
|
||||
return super.get(name)
|
||||
// 5) Caller chain (locals/parents + members)
|
||||
callScope.chainLookupWithMembers(name)?.let { return it }
|
||||
|
||||
// 6) Module pseudo-symbols (e.g., __PACKAGE__) — walk caller ancestry and query ModuleScope directly
|
||||
if (name.startsWith("__")) {
|
||||
var s: Scope? = callScope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ModuleScope) return s.get(name)
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
|
||||
// 7) Direct module/global fallback: try to locate nearest ModuleScope and check its own locals
|
||||
fun lookupInModuleAncestry(from: Scope): ObjRecord? {
|
||||
var s: Scope? = from
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ModuleScope) {
|
||||
s.objects[name]?.let { return it }
|
||||
s.localBindings[name]?.let { return it }
|
||||
// check immediate parent (root scope) locals/constants for globals like `delay`
|
||||
val p = s.parent
|
||||
if (p != null) {
|
||||
p.objects[name]?.let { return it }
|
||||
p.localBindings[name]?.let { return it }
|
||||
p.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
lookupInModuleAncestry(closureScope)?.let { return it }
|
||||
lookupInModuleAncestry(callScope)?.let { return it }
|
||||
|
||||
// 8) Global root scope constants/functions (e.g., delay, yield) via current import provider
|
||||
runCatching { this.currentImportProvider.rootScope.objects[name] }.getOrNull()?.let { return it }
|
||||
|
||||
// Final safe fallback: base scope lookup from this frame walking raw parents
|
||||
return baseGetIgnoreClosure(name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -62,6 +62,108 @@ open class Scope(
|
||||
*/
|
||||
internal val localBindings: MutableMap<String, ObjRecord> = mutableMapOf()
|
||||
|
||||
/** Debug helper: ensure assigning [candidateParent] does not create a structural cycle. */
|
||||
private fun ensureNoCycle(candidateParent: Scope?) {
|
||||
if (candidateParent == null) return
|
||||
var s: Scope? = candidateParent
|
||||
var hops = 0
|
||||
while (s != null && hops++ < 1024) {
|
||||
if (s === this) {
|
||||
// In production we silently ignore; for debugging throw an error to signal misuse
|
||||
throw IllegalStateException("cycle detected in scope parent chain assignment")
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal lookup helpers that deliberately avoid invoking overridden `get` implementations
|
||||
* (notably in ClosureScope) to prevent accidental ping-pong and infinite recursion across
|
||||
* intertwined closure frames. They traverse the plain parent chain and consult only locals
|
||||
* and bindings of each frame. Instance/class member fallback must be decided by the caller.
|
||||
*/
|
||||
private fun tryGetLocalRecord(s: Scope, name: String): ObjRecord? {
|
||||
s.objects[name]?.let { return it }
|
||||
s.localBindings[name]?.let { return it }
|
||||
s.getSlotIndexOf(name)?.let { return s.getSlotRecord(it) }
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun chainLookupIgnoreClosure(name: String): ObjRecord? {
|
||||
var s: Scope? = this
|
||||
// use frameId to detect unexpected structural cycles in the parent chain
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
tryGetLocalRecord(s, name)?.let { return it }
|
||||
s = s.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform base Scope.get semantics for this frame without delegating into parent.get
|
||||
* virtual dispatch. This checks:
|
||||
* - locals/bindings in this frame
|
||||
* - walks raw parent chain for locals/bindings (ignoring ClosureScope-specific overrides)
|
||||
* - finally falls back to this frame's `thisObj` instance/class members
|
||||
*/
|
||||
internal fun baseGetIgnoreClosure(name: String): ObjRecord? {
|
||||
// 1) locals/bindings in this frame
|
||||
tryGetLocalRecord(this, name)?.let { return it }
|
||||
// 2) walk parents for plain locals/bindings only
|
||||
var s = parent
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
tryGetLocalRecord(s, name)?.let { return it }
|
||||
s = s.parent
|
||||
}
|
||||
// 3) fallback to instance/class members of this frame's thisObj
|
||||
return thisObj.objClass.getInstanceMemberOrNull(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the ancestry starting from this scope and try to resolve [name] against:
|
||||
* - locals/bindings of each frame
|
||||
* - then instance/class members of each frame's `thisObj`.
|
||||
* This completely avoids invoking overridden `get` implementations, preventing
|
||||
* ping-pong recursion between `ClosureScope` frames.
|
||||
*/
|
||||
internal fun chainLookupWithMembers(name: String): ObjRecord? {
|
||||
var s: Scope? = this
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
tryGetLocalRecord(s, name)?.let { return it }
|
||||
s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
s = s.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a non-pooled snapshot of this scope suitable for capturing as a closure environment.
|
||||
* Copies locals, slots, and localBindings; preserves parent chain and class context.
|
||||
*/
|
||||
fun snapshotForClosure(): Scope {
|
||||
val snap = Scope(parent, args, pos, thisObj)
|
||||
snap.currentClassCtx = this.currentClassCtx
|
||||
// copy locals and bindings
|
||||
snap.objects.putAll(this.objects)
|
||||
snap.localBindings.putAll(this.localBindings)
|
||||
// copy slots map preserving indices
|
||||
if (this.slotCount() > 0) {
|
||||
var i = 0
|
||||
while (i < this.slotCount()) {
|
||||
val rec = this.getSlotRecord(i)
|
||||
snap.allocateSlotFor(this.nameToSlot.entries.firstOrNull { it.value == i }?.key ?: "slot${'$'}i", rec)
|
||||
i++
|
||||
}
|
||||
}
|
||||
return snap
|
||||
}
|
||||
|
||||
/**
|
||||
* Hint internal collections to reduce reallocations for upcoming parameter/local assignments.
|
||||
* Only effective for ArrayList-backed slots; maps are left as-is (rehashed lazily by JVM).
|
||||
@ -200,6 +302,7 @@ open class Scope(
|
||||
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
|
||||
*/
|
||||
fun resetForReuse(parent: Scope?, args: Arguments, pos: Pos, thisObj: Obj) {
|
||||
ensureNoCycle(parent)
|
||||
this.parent = parent
|
||||
this.args = args
|
||||
this.pos = pos
|
||||
@ -220,7 +323,10 @@ open class Scope(
|
||||
* Creates a new child scope using the provided arguments and optional `thisObj`.
|
||||
*/
|
||||
fun createChildScope(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
||||
Scope(this, args, pos, newThisObj ?: thisObj).also { it.reserveLocalCapacity(args.list.size + 4) }
|
||||
Scope(this, args, pos, newThisObj ?: thisObj).also {
|
||||
it.ensureNoCycle(it.parent)
|
||||
it.reserveLocalCapacity(args.list.size + 4)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a block inside a child frame. Guarded for future pooling via [PerfFlags.SCOPE_POOL].
|
||||
@ -249,7 +355,7 @@ open class Scope(
|
||||
* @return A new instance of [Scope] initialized with the specified arguments and `thisObj`.
|
||||
*/
|
||||
fun createChildScope(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
||||
Scope(this, args, pos, newThisObj ?: thisObj)
|
||||
Scope(this, args, pos, newThisObj ?: thisObj).also { it.ensureNoCycle(it.parent) }
|
||||
|
||||
/**
|
||||
* @return A child scope with the same arguments, position and [thisObj]
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.ClosureScope
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.Statement
|
||||
|
||||
@ -54,13 +55,16 @@ class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
|
||||
open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: Statement? = null) : Obj() {
|
||||
|
||||
override val objClass: ObjClass = type
|
||||
// Capture the lexical scope used to build this dynamic so callbacks can see outer locals
|
||||
internal var builderScope: Scope? = null
|
||||
|
||||
/**
|
||||
* Use read callback to dynamically resolve the field name. Note that it does not work
|
||||
* with method invocation which is implemented separately in [invokeInstanceMethod] below.
|
||||
*/
|
||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||
return readCallback?.execute(scope.createChildScope(Arguments(ObjString(name))))?.let {
|
||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||
return readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name))))?.let {
|
||||
if (writeCallback != null)
|
||||
it.asMutable
|
||||
else
|
||||
@ -79,28 +83,27 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St
|
||||
args: Arguments,
|
||||
onNotFoundResult: (() -> Obj?)?
|
||||
): Obj {
|
||||
val over = readCallback?.execute(
|
||||
scope.createChildScope(
|
||||
Arguments(ObjString(name)
|
||||
)
|
||||
)
|
||||
)
|
||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||
val over = readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name))))
|
||||
return over?.invoke(scope, scope.thisObj, args)
|
||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||
}
|
||||
|
||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||
writeCallback?.execute(scope.createChildScope(Arguments(ObjString(name), newValue)))
|
||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||
writeCallback?.execute(execBase.createChildScope(Arguments(ObjString(name), newValue)))
|
||||
?: super.writeField(scope, name, newValue)
|
||||
}
|
||||
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
return readCallback?.execute(scope.createChildScope(Arguments(index)))
|
||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||
return readCallback?.execute(execBase.createChildScope(Arguments(index)))
|
||||
?: super.getAt(scope, index)
|
||||
}
|
||||
|
||||
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||
writeCallback?.execute(scope.createChildScope(Arguments(index, newValue)))
|
||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||
writeCallback?.execute(execBase.createChildScope(Arguments(index, newValue)))
|
||||
?: super.putAt(scope, index, newValue)
|
||||
}
|
||||
|
||||
@ -109,7 +112,12 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St
|
||||
suspend fun create(scope: Scope, builder: Statement): ObjDynamic {
|
||||
val delegate = ObjDynamic()
|
||||
val context = ObjDynamicContext(delegate)
|
||||
builder.execute(scope.createChildScope(newThisObj = context))
|
||||
// Capture the function's lexical scope (scope) so callbacks can see outer locals like parameters.
|
||||
// Build the dynamic in a child scope purely to set `this` to context, but keep captured closure at parent.
|
||||
val buildScope = scope.createChildScope(newThisObj = context)
|
||||
// Snapshot the caller scope to capture locals/args even if the runtime pools/reuses frames
|
||||
delegate.builderScope = scope.snapshotForClosure()
|
||||
builder.execute(buildScope)
|
||||
return delegate
|
||||
}
|
||||
|
||||
|
||||
@ -198,6 +198,10 @@ open class ObjException(
|
||||
)) {
|
||||
scope.addConst(name, getOrCreateExceptionClass(name))
|
||||
}
|
||||
// Backward compatibility alias used in older tests/docs
|
||||
val snd = getOrCreateExceptionClass("SymbolNotDefinedException")
|
||||
scope.addConst("SymbolNotFound", snd)
|
||||
existingErrorClasses["SymbolNotFound"] = snd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1240,7 +1240,28 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
|
||||
// 2) Fallback to current-scope object or field on `this`
|
||||
scope[name]?.let { return it }
|
||||
return scope.thisObj.readField(scope, name)
|
||||
// 2a) Try nearest ClosureScope's closure ancestry explicitly
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { return it }
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
// 2b) Try raw ancestry local/binding lookup (cycle-safe), including slots in parents
|
||||
scope.chainLookupIgnoreClosure(name)?.let { return it }
|
||||
try {
|
||||
return scope.thisObj.readField(scope, name)
|
||||
} catch (e: ExecutionError) {
|
||||
// Map missing symbol during unqualified lookup to SymbolNotFound (SymbolNotDefinedException)
|
||||
// to preserve legacy behavior expected by tests.
|
||||
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||
@ -1253,7 +1274,24 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
|
||||
// 2) Fallback name in scope or field on `this`
|
||||
scope[name]?.let { return it }
|
||||
return scope.thisObj.readField(scope, name)
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { return it }
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
scope.chainLookupIgnoreClosure(name)?.let { return it }
|
||||
try {
|
||||
return scope.thisObj.readField(scope, name)
|
||||
} catch (e: ExecutionError) {
|
||||
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun evalValue(scope: Scope): Obj {
|
||||
@ -1262,14 +1300,48 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it).value }
|
||||
// fallback to current-scope object or field on `this`
|
||||
scope[name]?.let { return it.value }
|
||||
return scope.thisObj.readField(scope, name).value
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { return it.value }
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
scope.chainLookupIgnoreClosure(name)?.let { return it.value }
|
||||
return try {
|
||||
scope.thisObj.readField(scope, name).value
|
||||
} catch (e: ExecutionError) {
|
||||
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||
if (slot >= 0) return scope.getSlotRecord(slot).value
|
||||
// Fallback name in scope or field on `this`
|
||||
scope[name]?.let { return it.value }
|
||||
return scope.thisObj.readField(scope, name).value
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { return it.value }
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
scope.chainLookupIgnoreClosure(name)?.let { return it.value }
|
||||
return try {
|
||||
scope.thisObj.readField(scope, name).value
|
||||
} catch (e: ExecutionError) {
|
||||
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
@ -1286,6 +1358,26 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
||||
if (stored.isMutable) stored.value = newValue
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
||||
if (stored.isMutable) stored.value = newValue
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
// Fallback: write to field on `this`
|
||||
scope.thisObj.writeField(scope, name, newValue)
|
||||
return
|
||||
@ -1302,6 +1394,26 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
||||
if (stored.isMutable) stored.value = newValue
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
||||
if (stored.isMutable) stored.value = newValue
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
scope.thisObj.writeField(scope, name, newValue)
|
||||
return
|
||||
}
|
||||
|
||||
@ -61,6 +61,74 @@ class ScriptTest {
|
||||
println("version = ${LyngVersion}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClosureSeesCallerLocalsInLaunch() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val res = scope.eval(
|
||||
"""
|
||||
var counter = 0
|
||||
val d = launch {
|
||||
val c = counter
|
||||
delay(1)
|
||||
counter = c + 1
|
||||
}
|
||||
d.await()
|
||||
counter
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals(1L, (res as ObjInt).value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClosureResolvesGlobalsInLaunch() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val res = scope.eval(
|
||||
"""
|
||||
val d = launch {
|
||||
delay(1)
|
||||
yield()
|
||||
}
|
||||
d.await()
|
||||
42
|
||||
""".trimIndent()
|
||||
)
|
||||
assertEquals(42L, (res as ObjInt).value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClosureSeesModulePseudoSymbol() = runTest {
|
||||
val scope = Script.newScope()
|
||||
val res = scope.eval(
|
||||
"""
|
||||
val s = { __PACKAGE__ }
|
||||
s()
|
||||
""".trimIndent()
|
||||
)
|
||||
// __PACKAGE__ is a string; just ensure it's a string and non-empty
|
||||
assertTrue(res is ObjString && res.value.isNotEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoInfiniteRecursionOnUnknownInNestedClosure() = runTest {
|
||||
val scope = Script.newScope()
|
||||
withTimeout(1.seconds) {
|
||||
// Access an unknown symbol inside nested closures; should throw quickly, not hang
|
||||
try {
|
||||
scope.eval(
|
||||
"""
|
||||
val f = { { unknown_symbol_just_for_test } }
|
||||
f()()
|
||||
""".trimIndent()
|
||||
)
|
||||
fail("Expected exception not thrown")
|
||||
} catch (_: ExecutionError) {
|
||||
// ok
|
||||
} catch (_: ScriptError) {
|
||||
// ok
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helpers to test iterator cancellation semantics ---
|
||||
class ObjTestIterable : Obj() {
|
||||
|
||||
@ -4070,4 +4138,25 @@ class ScriptTest {
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHangOnNonexistingMethod() = runTest {
|
||||
eval("""
|
||||
class T(someList) {
|
||||
fun f() {
|
||||
nonExistingMethod()
|
||||
}
|
||||
}
|
||||
val t = T([1,2])
|
||||
try {
|
||||
for( i in 1..10 ) {
|
||||
t.f()
|
||||
}
|
||||
}
|
||||
catch(t: SymbolNotFound) {
|
||||
println(t::class)
|
||||
// ok
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user