fix wasmJs suspend-lambda generation and add agent guidance

This commit is contained in:
Sergey Chernov 2026-01-24 18:59:58 +03:00
parent 717a79aca2
commit 062f9e7866
5 changed files with 771 additions and 408 deletions

8
AGENTS.md Normal file
View File

@ -0,0 +1,8 @@
# AI Agent Notes
## Kotlin/Wasm generation guardrails
- Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`.
- Do not use `statement { ... }` or other inline suspend lambdas in compiler hot paths (e.g., parsing/var declarations, initializer thunks).
- If you need a wrapper for delegated properties, check for `getValue` explicitly and return a concrete `Statement` object when missing; avoid `onNotFoundResult` lambdas.
- If wasmJs browser tests hang, first run `:lynglib:wasmJsNodeTest` and look for wasm compilation errors; hangs usually mean module instantiation failed.
- Do not increase test timeouts to mask wasm generation errors; fix the invalid IR instead.

View File

@ -45,6 +45,7 @@ fun swapEnds(first, args..., last, f) {
- [Samples directory](docs/samples) - [Samples directory](docs/samples)
- [Formatter (core + CLI + IDE)](docs/formatter.md) - [Formatter (core + CLI + IDE)](docs/formatter.md)
- [Books directory](docs) - [Books directory](docs)
- [AI agent guidance](AGENTS.md)
## Integration in Kotlin multiplatform ## Integration in Kotlin multiplatform

File diff suppressed because it is too large Load Diff

View File

@ -70,9 +70,8 @@ open class Scope(
internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? { internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? {
var s: Scope? = this var s: Scope? = this
val visited = HashSet<Long>(4) var hops = 0
while (s != null) { while (s != null && hops++ < 1024) {
if (!visited.add(s.frameId)) break
// Proximity rule: check all extensions in the current scope before going to parent. // Proximity rule: check all extensions in the current scope before going to parent.
// Priority within scope: more specific class in MRO wins. // Priority within scope: more specific class in MRO wins.
for (cls in receiverClass.mro) { for (cls in receiverClass.mro) {
@ -106,28 +105,38 @@ open class Scope(
* intertwined closure frames. They traverse the plain parent chain and consult only locals * 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. * and bindings of each frame. Instance/class member fallback must be decided by the caller.
*/ */
private fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? { internal fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? {
caller?.let { ctx ->
s.objects[ctx.mangledName(name)]?.let { rec ->
if (rec.visibility == Visibility.Private) return rec
}
}
s.objects[name]?.let { rec -> s.objects[name]?.let { rec ->
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec
}
caller?.let { ctx ->
s.localBindings[ctx.mangledName(name)]?.let { rec ->
if (rec.visibility == Visibility.Private) return rec
}
} }
s.localBindings[name]?.let { rec -> s.localBindings[name]?.let { rec ->
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec
} }
s.getSlotIndexOf(name)?.let { idx -> s.getSlotIndexOf(name)?.let { idx ->
val rec = s.getSlotRecord(idx) val rec = s.getSlotRecord(idx)
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec
} }
return null return null
} }
internal fun chainLookupIgnoreClosure(name: String): ObjRecord? { internal fun chainLookupIgnoreClosure(name: String, followClosure: Boolean = false, caller: net.sergeych.lyng.obj.ObjClass? = null): ObjRecord? {
var s: Scope? = this var s: Scope? = this
// use frameId to detect unexpected structural cycles in the parent chain // use hop counter to detect unexpected structural cycles in the parent chain
val visited = HashSet<Long>(4) var hops = 0
while (s != null) { val effectiveCaller = caller ?: currentClassCtx
if (!visited.add(s.frameId)) return null while (s != null && hops++ < 1024) {
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it } tryGetLocalRecord(s, name, effectiveCaller)?.let { return it }
s = s.parent s = if (followClosure && s is ClosureScope) s.closureScope else s.parent
} }
return null return null
} }
@ -144,9 +153,8 @@ open class Scope(
tryGetLocalRecord(this, name, currentClassCtx)?.let { return it } tryGetLocalRecord(this, name, currentClassCtx)?.let { return it }
// 2) walk parents for plain locals/bindings only // 2) walk parents for plain locals/bindings only
var s = parent var s = parent
val visited = HashSet<Long>(4) var hops = 0
while (s != null) { while (s != null && hops++ < 1024) {
if (!visited.add(s.frameId)) return null
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it } tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
s = s.parent s = s.parent
} }
@ -155,7 +163,7 @@ open class Scope(
this.extensions[cls]?.get(name)?.let { return it } this.extensions[cls]?.get(name)?.let { return it }
} }
return thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> return thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) { if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx, name)) {
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null
else rec else rec
} else null } else null
@ -169,23 +177,22 @@ open class Scope(
* This completely avoids invoking overridden `get` implementations, preventing * This completely avoids invoking overridden `get` implementations, preventing
* ping-pong recursion between `ClosureScope` frames. * ping-pong recursion between `ClosureScope` frames.
*/ */
internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx): ObjRecord? { internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx, followClosure: Boolean = false): ObjRecord? {
var s: Scope? = this var s: Scope? = this
val visited = HashSet<Long>(4) var hops = 0
while (s != null) { while (s != null && hops++ < 1024) {
if (!visited.add(s.frameId)) return null
tryGetLocalRecord(s, name, caller)?.let { return it } tryGetLocalRecord(s, name, caller)?.let { return it }
for (cls in s.thisObj.objClass.mro) { for (cls in s.thisObj.objClass.mro) {
s.extensions[cls]?.get(name)?.let { return it } s.extensions[cls]?.get(name)?.let { return it }
} }
s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec -> s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, caller)) { if (canAccessMember(rec.visibility, rec.declaringClass, caller, name)) {
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) { if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) {
// ignore fields, properties and abstracts here, they will be handled by the caller via readField // ignore fields, properties and abstracts here, they will be handled by the caller via readField
} else return rec } else return rec
} }
} }
s = s.parent s = if (followClosure && s is ClosureScope) s.closureScope else s.parent
} }
return null return null
} }
@ -277,16 +284,22 @@ open class Scope(
raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name")) raiseError(ObjSymbolNotDefinedException(this, "symbol is not defined: $name"))
fun raiseError(message: String): Nothing { fun raiseError(message: String): Nothing {
throw ExecutionError(ObjException(this, message)) val ex = ObjException(this, message)
throw ExecutionError(ex, pos, ex.message.value)
} }
fun raiseError(obj: ObjException): Nothing { fun raiseError(obj: ObjException): Nothing {
throw ExecutionError(obj) throw ExecutionError(obj, obj.scope.pos, obj.message.value)
}
fun raiseError(obj: Obj, pos: Pos, message: String): Nothing {
throw ExecutionError(obj, pos, message)
} }
@Suppress("unused") @Suppress("unused")
fun raiseNotFound(message: String = "not found"): Nothing { fun raiseNotFound(message: String = "not found"): Nothing {
throw ExecutionError(ObjNotFoundException(this, message)) val ex = ObjNotFoundException(this, message)
throw ExecutionError(ex, ex.scope.pos, ex.message.value)
} }
inline fun <reified T : Obj> requiredArg(index: Int): T { inline fun <reified T : Obj> requiredArg(index: Int): T {
@ -314,37 +327,51 @@ open class Scope(
inline fun <reified T : Obj> thisAs(): T { inline fun <reified T : Obj> thisAs(): T {
var s: Scope? = this var s: Scope? = this
do { while (s != null) {
val t = s!!.thisObj val t = s.thisObj
if (t is T) return t if (t is T) return t
s = s.parent s = s.parent
} while (s != null) }
raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}") raiseClassCastError("Cannot cast ${thisObj.objClass.className} to ${T::class.simpleName}")
} }
internal val objects = mutableMapOf<String, ObjRecord>() internal val objects = mutableMapOf<String, ObjRecord>()
open operator fun get(name: String): ObjRecord? = open operator fun get(name: String): ObjRecord? {
if (name == "this") thisObj.asReadonly if (name == "this") return thisObj.asReadonly
else {
// Prefer direct locals/bindings declared in this frame // 1. Prefer direct locals/bindings declared in this frame
(objects[name]?.let { rec -> tryGetLocalRecord(this, name, currentClassCtx)?.let { return it }
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null
val p = parent
// 2. If we share thisObj with parent, delegate to parent to maintain
// "locals shadow members" priority across the this-context.
if (p != null && p.thisObj === thisObj) {
return p.get(name)
} }
// Then, check known local bindings in this frame (helps after suspension)
?: localBindings[name]?.let { rec -> // 3. Otherwise, we are the "primary" scope for this thisObj (or have no parent),
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null // so check members of thisObj before walking up to a different this-context.
val receiver = thisObj
val effectiveClass = receiver as? ObjClass ?: receiver.objClass
for (cls in effectiveClass.mro) {
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null && !rec.isAbstract) {
if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx, name)) {
return rec.copy(receiver = receiver)
} }
// Walk up ancestry
?: parent?.get(name)
// Finally, fallback to class members on thisObj
?: thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null
else rec
} else null
} }
) }
// Finally, root object fallback
Obj.rootObjectType.members[name]?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx, name)) {
return rec.copy(receiver = receiver)
}
}
// 4. Finally, walk up ancestry to a scope with a different thisObj context
return p?.get(name)
} }
// Slot fast-path API // Slot fast-path API
@ -365,6 +392,20 @@ open class Scope(
nameToSlot[name]?.let { slots[it] = record } nameToSlot[name]?.let { slots[it] = record }
} }
/**
* Clear all references and maps to prevent memory leaks when pooled.
*/
fun scrub() {
this.parent = null
this.skipScopeCreation = false
this.currentClassCtx = null
objects.clear()
slots.clear()
nameToSlot.clear()
localBindings.clear()
extensions.clear()
}
/** /**
* Reset this scope instance so it can be safely reused as a fresh child frame. * Reset this scope instance so it can be safely reused as a fresh child frame.
* 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.
@ -374,6 +415,7 @@ open class Scope(
// that could interact badly with the new parent and produce a cycle. // that could interact badly with the new parent and produce a cycle.
this.parent = null this.parent = null
this.skipScopeCreation = false this.skipScopeCreation = false
this.currentClassCtx = parent?.currentClassCtx
// fresh identity for PIC caches // fresh identity for PIC caches
this.frameId = nextFrameId() this.frameId = nextFrameId()
// clear locals and slot maps // clear locals and slot maps
@ -474,7 +516,8 @@ open class Scope(
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx, declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx,
isAbstract: Boolean = false, isAbstract: Boolean = false,
isClosed: Boolean = false, isClosed: Boolean = false,
isOverride: Boolean = false isOverride: Boolean = false,
isTransient: Boolean = false
): ObjRecord { ): ObjRecord {
val rec = ObjRecord( val rec = ObjRecord(
value, isMutable, visibility, writeVisibility, value, isMutable, visibility, writeVisibility,
@ -482,7 +525,8 @@ open class Scope(
type = recordType, type = recordType,
isAbstract = isAbstract, isAbstract = isAbstract,
isClosed = isClosed, isClosed = isClosed,
isOverride = isOverride isOverride = isOverride,
isTransient = isTransient
) )
objects[name] = rec objects[name] = rec
// Index this binding within the current frame to help resolve locals across suspension // Index this binding within the current frame to help resolve locals across suspension
@ -501,12 +545,16 @@ open class Scope(
} }
} }
// Map to a slot for fast local access (ensure consistency) // Map to a slot for fast local access (ensure consistency)
val idx = getSlotIndexOf(name) if (nameToSlot.isEmpty()) {
allocateSlotFor(name, rec)
} else {
val idx = nameToSlot[name]
if (idx == null) { if (idx == null) {
allocateSlotFor(name, rec) allocateSlotFor(name, rec)
} else { } else {
slots[idx] = rec slots[idx] = rec
} }
}
return rec return rec
} }
@ -622,31 +670,34 @@ open class Scope(
} }
suspend fun resolve(rec: ObjRecord, name: String): Obj { suspend fun resolve(rec: ObjRecord, name: String): Obj {
if (rec.type == ObjRecord.Type.Delegated) { val receiver = rec.receiver ?: thisObj
val del = rec.delegate ?: raiseError("Internal error: delegated property $name has no delegate") return receiver.resolveRecord(this, rec, name, rec.declaringClass).value
val th = if (thisObj === ObjVoid) ObjNull else thisObj
if (del.objClass.getInstanceMemberOrNull("getValue") == null) {
return object : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(scope: Scope): Obj {
val th2 = if (scope.thisObj === ObjVoid) ObjNull else scope.thisObj
val allArgs = (listOf(th2, ObjString(name)) + scope.args.list).toTypedArray()
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs))
}
}
}
return del.invokeInstanceMethod(this, "getValue", Arguments(th, ObjString(name)))
}
return rec.value
} }
suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) { suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) {
if (rec.type == ObjRecord.Type.Delegated) { if (rec.type == ObjRecord.Type.Delegated) {
val del = rec.delegate ?: raiseError("Internal error: delegated property $name has no delegate") val receiver = rec.receiver ?: thisObj
val th = if (thisObj === ObjVoid) ObjNull else thisObj val del = rec.delegate ?: run {
if (receiver is ObjInstance) {
(receiver as ObjInstance).writeField(this, name, newValue)
return
}
raiseError("Internal error: delegated property $name has no delegate")
}
val th = if (receiver === ObjVoid) ObjNull else receiver
del.invokeInstanceMethod(this, "setValue", Arguments(th, ObjString(name), newValue)) del.invokeInstanceMethod(this, "setValue", Arguments(th, ObjString(name), newValue))
return return
} }
if (rec.value is ObjProperty) {
(rec.value as ObjProperty).callSetter(this, rec.receiver ?: thisObj, newValue, rec.declaringClass)
return
}
// If it's a member (explicitly tracked by receiver or declaringClass), use writeField.
// Important: locals have receiver == null and declaringClass == null (enforced in addItem).
if (rec.receiver != null || (rec.declaringClass != null && (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property))) {
(rec.receiver ?: thisObj).writeField(this, name, newValue)
return
}
if (!rec.isMutable && rec.value !== ObjUnset) raiseIllegalAssignment("can't reassign val $name") if (!rec.isMutable && rec.value !== ObjUnset) raiseIllegalAssignment("can't reassign val $name")
rec.value = newValue rec.value = newValue
} }

View File

@ -609,17 +609,16 @@ open class Obj {
scope.raiseNotImplemented() scope.raiseNotImplemented()
} }
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj { suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj =
if (PerfFlags.SCOPE_POOL) { if (PerfFlags.SCOPE_POOL)
return scope.withChildFrame(args, newThisObj = thisObj) { child -> scope.withChildFrame(args, newThisObj = thisObj) { child ->
if (declaringClass != null) child.currentClassCtx = declaringClass if (declaringClass != null) child.currentClassCtx = declaringClass
callOn(child) callOn(child)
} }
} else
val child = scope.createChildScope(scope.pos, args = args, newThisObj = thisObj) callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj).also {
if (declaringClass != null) child.currentClassCtx = declaringClass if (declaringClass != null) it.currentClassCtx = declaringClass
return callOn(child) })
}
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj = suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
callOn( callOn(