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 ----
|
// ---- Build-time generation of stdlib text from .lyng files into a Kotlin constant ----
|
||||||
// The .lyng source of the stdlib lives here (module-relative path):
|
// Implemented as a proper task type compatible with Gradle Configuration Cache
|
||||||
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 {
|
abstract class GenerateLyngStdlib : DefaultTask() {
|
||||||
group = "build"
|
@get:InputDirectory
|
||||||
description = "Generate Kotlin source with embedded lyng stdlib text"
|
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||||
inputs.dir(lyngStdlibDir)
|
abstract val sourceDir: DirectoryProperty
|
||||||
outputs.dir(generatedLyngStdlibDir)
|
|
||||||
// Simpler: opt out of configuration cache for this ad-hoc generator task
|
|
||||||
notCompatibleWithConfigurationCache("Uses dynamic file IO in doLast; trivial generator")
|
|
||||||
|
|
||||||
doLast {
|
@get:OutputDirectory
|
||||||
|
abstract val outputDir: DirectoryProperty
|
||||||
|
|
||||||
|
@TaskAction
|
||||||
|
fun generate() {
|
||||||
val targetPkg = "net.sergeych.lyng.stdlib_included"
|
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()
|
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 {
|
val content = if (files.isEmpty()) "" else buildString {
|
||||||
files.forEachIndexed { idx, f ->
|
files.forEachIndexed { idx, f ->
|
||||||
val text = f.readText()
|
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 {
|
fun escapeForQuoted(s: String): String = buildString {
|
||||||
for (ch in s) when (ch) {
|
for (ch in s) when (ch) {
|
||||||
'\\' -> append("\\\\")
|
'\\' -> append("\\\\")
|
||||||
'"' -> append("\\\"")
|
'"' -> append("\\\"")
|
||||||
'\n' -> append("\\n")
|
'\n' -> append("\\n")
|
||||||
'\r' -> {} // drop CR
|
'\r' -> {}
|
||||||
'\t' -> append("\\t")
|
'\t' -> append("\\t")
|
||||||
else -> append(ch)
|
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
|
// Add the generated directory to commonMain sources
|
||||||
kotlin.sourceSets.named("commonMain") {
|
kotlin.sourceSets.named("commonMain") {
|
||||||
kotlin.srcDir(generatedLyngStdlibDir)
|
kotlin.srcDir(generatedLyngStdlibDir)
|
||||||
|
|||||||
@ -38,35 +38,80 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun get(name: String): ObjRecord? {
|
override fun get(name: String): ObjRecord? {
|
||||||
|
// Fast-path built-ins
|
||||||
|
if (name == "this") return thisObj.asReadonly
|
||||||
|
|
||||||
// Priority:
|
// Priority:
|
||||||
// 1) Locals and arguments declared in this lambda frame (including values defined before suspension)
|
// 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`
|
// 2) Instance/class members of the captured receiver (`closureScope.thisObj`)
|
||||||
// 3) Symbols from the captured closure scope (its locals and parents)
|
// 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)
|
// 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.objects[name]?.let { return it }
|
||||||
|
super.localBindings[name]?.let { return it }
|
||||||
|
|
||||||
// Prefer instance fields/methods declared on the captured receiver:
|
// 2) Members on the captured receiver instance
|
||||||
// First, resolve real instance fields stored in the instance scope (constructor vars like `coll`, `factor`)
|
|
||||||
(closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)
|
(closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)
|
||||||
?.instanceScope
|
?.instanceScope
|
||||||
?.objects
|
?.objects
|
||||||
?.get(name)
|
?.get(name)
|
||||||
?.let { return it }
|
?.let { return it }
|
||||||
|
|
||||||
// Then, try class-declared members (methods/properties declared in the class body)
|
|
||||||
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||||
|
|
||||||
// Then delegate to the full closure scope chain (locals, parents, etc.)
|
// 3) Closure scope chain (locals/parents + members), ignore ClosureScope overrides to prevent recursion
|
||||||
closureScope.get(name)?.let { return it }
|
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 }
|
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||||
|
|
||||||
// Fallback to the standard lookup chain: this frame -> parent (callScope) -> class members
|
// 5) Caller chain (locals/parents + members)
|
||||||
return super.get(name)
|
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()
|
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.
|
* 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).
|
* 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.
|
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
|
||||||
*/
|
*/
|
||||||
fun resetForReuse(parent: Scope?, args: Arguments, pos: Pos, thisObj: Obj) {
|
fun resetForReuse(parent: Scope?, args: Arguments, pos: Pos, thisObj: Obj) {
|
||||||
|
ensureNoCycle(parent)
|
||||||
this.parent = parent
|
this.parent = parent
|
||||||
this.args = args
|
this.args = args
|
||||||
this.pos = pos
|
this.pos = pos
|
||||||
@ -220,7 +323,10 @@ open class Scope(
|
|||||||
* Creates a new child scope using the provided arguments and optional `thisObj`.
|
* Creates a new child scope using the provided arguments and optional `thisObj`.
|
||||||
*/
|
*/
|
||||||
fun createChildScope(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
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].
|
* 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`.
|
* @return A new instance of [Scope] initialized with the specified arguments and `thisObj`.
|
||||||
*/
|
*/
|
||||||
fun createChildScope(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
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]
|
* @return A child scope with the same arguments, position and [thisObj]
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
import net.sergeych.lyng.Arguments
|
import net.sergeych.lyng.Arguments
|
||||||
|
import net.sergeych.lyng.ClosureScope
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.Statement
|
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() {
|
open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: Statement? = null) : Obj() {
|
||||||
|
|
||||||
override val objClass: ObjClass = type
|
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
|
* 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.
|
* with method invocation which is implemented separately in [invokeInstanceMethod] below.
|
||||||
*/
|
*/
|
||||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
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)
|
if (writeCallback != null)
|
||||||
it.asMutable
|
it.asMutable
|
||||||
else
|
else
|
||||||
@ -79,28 +83,27 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St
|
|||||||
args: Arguments,
|
args: Arguments,
|
||||||
onNotFoundResult: (() -> Obj?)?
|
onNotFoundResult: (() -> Obj?)?
|
||||||
): Obj {
|
): Obj {
|
||||||
val over = readCallback?.execute(
|
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||||
scope.createChildScope(
|
val over = readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name))))
|
||||||
Arguments(ObjString(name)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return over?.invoke(scope, scope.thisObj, args)
|
return over?.invoke(scope, scope.thisObj, args)
|
||||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
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)
|
?: super.writeField(scope, name, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
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)
|
?: super.getAt(scope, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
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)
|
?: 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 {
|
suspend fun create(scope: Scope, builder: Statement): ObjDynamic {
|
||||||
val delegate = ObjDynamic()
|
val delegate = ObjDynamic()
|
||||||
val context = ObjDynamicContext(delegate)
|
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
|
return delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -198,6 +198,10 @@ open class ObjException(
|
|||||||
)) {
|
)) {
|
||||||
scope.addConst(name, getOrCreateExceptionClass(name))
|
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++
|
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
|
||||||
// 2) Fallback to current-scope object or field on `this`
|
// 2) Fallback to current-scope object or field on `this`
|
||||||
scope[name]?.let { return it }
|
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 hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
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++
|
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
|
||||||
// 2) Fallback name in scope or field on `this`
|
// 2) Fallback name in scope or field on `this`
|
||||||
scope[name]?.let { return it }
|
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 {
|
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 }
|
scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it).value }
|
||||||
// fallback to current-scope object or field on `this`
|
// fallback to current-scope object or field on `this`
|
||||||
scope[name]?.let { return it.value }
|
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 hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||||
if (slot >= 0) return scope.getSlotRecord(slot).value
|
if (slot >= 0) return scope.getSlotRecord(slot).value
|
||||||
// Fallback name in scope or field on `this`
|
// Fallback name in scope or field on `this`
|
||||||
scope[name]?.let { return it.value }
|
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) {
|
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")
|
else scope.raiseError("Cannot assign to immutable value")
|
||||||
return
|
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`
|
// Fallback: write to field on `this`
|
||||||
scope.thisObj.writeField(scope, name, newValue)
|
scope.thisObj.writeField(scope, name, newValue)
|
||||||
return
|
return
|
||||||
@ -1302,6 +1394,26 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
else scope.raiseError("Cannot assign to immutable value")
|
else scope.raiseError("Cannot assign to immutable value")
|
||||||
return
|
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)
|
scope.thisObj.writeField(scope, name, newValue)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,6 +61,74 @@ class ScriptTest {
|
|||||||
println("version = ${LyngVersion}")
|
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 ---
|
// --- Helpers to test iterator cancellation semantics ---
|
||||||
class ObjTestIterable : Obj() {
|
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