Compare commits
No commits in common. "062f9e786619b001ecec57ca5a2fb09a7449887a" and "7b1ba71ef0b9ce9427fb0b213bcc4a160e97a39a" have entirely different histories.
062f9e7866
...
7b1ba71ef0
@ -1,8 +0,0 @@
|
||||
# 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.
|
||||
@ -45,7 +45,6 @@ fun swapEnds(first, args..., last, f) {
|
||||
- [Samples directory](docs/samples)
|
||||
- [Formatter (core + CLI + IDE)](docs/formatter.md)
|
||||
- [Books directory](docs)
|
||||
- [AI agent guidance](AGENTS.md)
|
||||
|
||||
## Integration in Kotlin multiplatform
|
||||
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
# AI notes: avoid Kotlin/Wasm invalid IR with suspend lambdas
|
||||
|
||||
## Do
|
||||
- Prefer explicit `object : Statement()` with `override suspend fun execute(...)` when building compiler statements.
|
||||
- Keep `Statement` objects non-lambda, especially in compiler hot paths like parsing/var declarations.
|
||||
- If you need conditional behavior, return early in `execute` instead of wrapping `parseExpression()` with `statement(...) { ... }`.
|
||||
- When wasmJs tests hang in the browser, first check `wasmJsNodeTest` for a compile error; hangs often mean module instantiation failed.
|
||||
|
||||
## Don't
|
||||
- Do not create suspend lambdas inside `Statement` factories (`statement { ... }`) for wasm targets.
|
||||
- Do not "fix" hangs by increasing browser timeouts; it masks invalid wasm generation.
|
||||
|
||||
## Debugging tips
|
||||
- Look for `$invokeCOROUTINE$` in wasm function names when mapping failures.
|
||||
- If node test logs a wasm compile error, the browser hang is likely the same root cause.
|
||||
@ -1,27 +0,0 @@
|
||||
# Wasm generation hang in wasmJs browser tests
|
||||
|
||||
## Summary
|
||||
The wasmJs browser test runner hung after commit 5f819dc. The root cause was invalid WebAssembly generated by the Kotlin/Wasm backend when certain compiler paths emitted suspend lambdas for `Statement` execution. The invalid module failed to instantiate in the browser, and Karma kept the browser connected but never ran tests.
|
||||
|
||||
## Symptoms
|
||||
- `:lynglib:wasmJsBrowserTest` hangs indefinitely in ChromeHeadless.
|
||||
- `:lynglib:wasmJsNodeTest` fails with a WebAssembly compile error similar to:
|
||||
- `struct.set expected type (ref null XXXX), found global.get of type (ref null YYYY)`
|
||||
- The failing function name in the wasm name section looks like:
|
||||
- `net.sergeych.lyng.$invokeCOROUTINE$.doResume`
|
||||
|
||||
## Root cause
|
||||
The delegation/var-declaration changes introduced compiler-generated suspend lambdas inside `Statement` construction (e.g., `statement { ... }` wrappers). Kotlin/Wasm generates extra coroutine state for those suspend lambdas, which in this case produced invalid wasm IR (mismatched GC reference types). The browser loader then waits forever because the module fails to instantiate.
|
||||
|
||||
## Fix
|
||||
Avoid suspend-lambda `Statement` construction in compiler code paths. Replace `statement { ... }` and other anonymous suspend lambdas with explicit `object : Statement()` implementations and move logic into `override suspend fun execute(...)`. This keeps the resulting wasm IR valid while preserving behavior.
|
||||
|
||||
## Where it was fixed
|
||||
- `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt`
|
||||
- `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt`
|
||||
|
||||
## Verification
|
||||
- `./gradlew :lynglib:wasmJsNodeTest --info`
|
||||
- `./gradlew :lynglib:wasmJsBrowserTest --info`
|
||||
|
||||
Both tests finish quickly after the change.
|
||||
@ -3267,60 +3267,37 @@ class Compiler(
|
||||
// when creating instance, but we need to execute it in the class initializer which
|
||||
// is missing as for now. Add it to the compiler context?
|
||||
|
||||
currentInitScope += object : Statement() {
|
||||
override val pos: Pos = start
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
val initValue = initialExpression?.execute(scope)?.byValueCopy() ?: ObjNull
|
||||
if (isDelegate) {
|
||||
val accessTypeStr = if (isMutable) "Var" else "Val"
|
||||
val accessType = scope.resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr")
|
||||
val finalDelegate = try {
|
||||
initValue.invokeInstanceMethod(
|
||||
scope,
|
||||
"bind",
|
||||
Arguments(ObjString(name), accessType, scope.thisObj)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
initValue
|
||||
}
|
||||
(scope.thisObj as ObjClass).createClassField(
|
||||
name,
|
||||
ObjUnset,
|
||||
isMutable,
|
||||
visibility,
|
||||
null,
|
||||
start,
|
||||
isTransient = isTransient,
|
||||
type = ObjRecord.Type.Delegated
|
||||
).apply {
|
||||
delegate = finalDelegate
|
||||
}
|
||||
// Also expose in current init scope
|
||||
scope.addItem(
|
||||
name,
|
||||
isMutable,
|
||||
ObjUnset,
|
||||
visibility,
|
||||
null,
|
||||
ObjRecord.Type.Delegated,
|
||||
isTransient = isTransient
|
||||
).apply {
|
||||
delegate = finalDelegate
|
||||
}
|
||||
} else {
|
||||
(scope.thisObj as ObjClass).createClassField(
|
||||
name,
|
||||
initValue,
|
||||
isMutable,
|
||||
visibility,
|
||||
null,
|
||||
start,
|
||||
isTransient = isTransient
|
||||
)
|
||||
scope.addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field, isTransient = isTransient)
|
||||
currentInitScope += statement {
|
||||
val initValue = initialExpression?.execute(this)?.byValueCopy() ?: ObjNull
|
||||
if (isDelegate) {
|
||||
val accessTypeStr = if (isMutable) "Var" else "Val"
|
||||
val accessType = resolveQualifiedIdentifier("DelegateAccess.$accessTypeStr")
|
||||
val finalDelegate = try {
|
||||
initValue.invokeInstanceMethod(this, "bind", Arguments(ObjString(name), accessType, thisObj))
|
||||
} catch (e: Exception) {
|
||||
initValue
|
||||
}
|
||||
return ObjVoid
|
||||
(thisObj as ObjClass).createClassField(
|
||||
name,
|
||||
ObjUnset,
|
||||
isMutable,
|
||||
visibility,
|
||||
null,
|
||||
start,
|
||||
isTransient = isTransient,
|
||||
type = ObjRecord.Type.Delegated
|
||||
).apply {
|
||||
delegate = finalDelegate
|
||||
}
|
||||
// Also expose in current init scope
|
||||
addItem(name, isMutable, ObjUnset, visibility, null, ObjRecord.Type.Delegated, isTransient = isTransient).apply {
|
||||
delegate = finalDelegate
|
||||
}
|
||||
} else {
|
||||
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, start, isTransient = isTransient)
|
||||
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field, isTransient = isTransient)
|
||||
}
|
||||
ObjVoid
|
||||
}
|
||||
return NopStatement
|
||||
}
|
||||
@ -3372,13 +3349,10 @@ class Compiler(
|
||||
val body = inCodeContext(CodeContext.Function("<setter>")) {
|
||||
parseBlock()
|
||||
}
|
||||
object : Statement() {
|
||||
override val pos: Pos = body.pos
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||
scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument)
|
||||
return body.execute(scope)
|
||||
}
|
||||
statement(body.pos) { scope ->
|
||||
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||
scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument)
|
||||
body.execute(scope)
|
||||
}
|
||||
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
|
||||
cc.skipWsTokens()
|
||||
@ -3388,13 +3362,10 @@ class Compiler(
|
||||
?: throw ScriptError(cc.current().pos, "Expected setter expression")
|
||||
}
|
||||
val st = expr
|
||||
object : Statement() {
|
||||
override val pos: Pos = st.pos
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||
scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument)
|
||||
return st.execute(scope)
|
||||
}
|
||||
statement(st.pos) { scope ->
|
||||
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||
scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument)
|
||||
st.execute(scope)
|
||||
}
|
||||
} else {
|
||||
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
||||
@ -3417,13 +3388,10 @@ class Compiler(
|
||||
val body = inCodeContext(CodeContext.Function("<setter>")) {
|
||||
parseBlock()
|
||||
}
|
||||
object : Statement() {
|
||||
override val pos: Pos = body.pos
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
|
||||
return body.execute(scope)
|
||||
}
|
||||
statement(body.pos) { scope ->
|
||||
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
|
||||
body.execute(scope)
|
||||
}
|
||||
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
|
||||
cc.skipWsTokens()
|
||||
@ -3434,13 +3402,10 @@ class Compiler(
|
||||
"Expected setter expression"
|
||||
)
|
||||
}
|
||||
object : Statement() {
|
||||
override val pos: Pos = st.pos
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
|
||||
return st.execute(scope)
|
||||
}
|
||||
statement(st.pos) { scope ->
|
||||
val value = scope.args.list.firstOrNull() ?: ObjNull
|
||||
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument)
|
||||
st.execute(scope)
|
||||
}
|
||||
} else {
|
||||
throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
|
||||
@ -4029,3 +3994,4 @@ class Compiler(
|
||||
suspend fun eval(code: String) = compile(code).execute()
|
||||
suspend fun evalNamed(name: String, code: String, importManager: ImportManager = Script.defaultImportManager) =
|
||||
compile(Source(name,code), importManager).execute()
|
||||
|
||||
|
||||
@ -501,8 +501,9 @@ open class Obj {
|
||||
if (obj.type == ObjRecord.Type.Delegated) {
|
||||
val del = obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
|
||||
val th = if (this === ObjVoid) ObjNull else this
|
||||
if (del.objClass.getInstanceMemberOrNull("getValue") == null) {
|
||||
val wrapper = object : Statement() {
|
||||
val res = del.invokeInstanceMethod(scope, "getValue", Arguments(th, ObjString(name)), onNotFoundResult = {
|
||||
// If getValue not found, return a wrapper that calls invoke
|
||||
object : Statement() {
|
||||
override val pos: Pos = Pos.builtIn
|
||||
override suspend fun execute(s: Scope): Obj {
|
||||
val th2 = if (s.thisObj === ObjVoid) ObjNull else s.thisObj
|
||||
@ -510,12 +511,7 @@ open class Obj {
|
||||
return del.invokeInstanceMethod(s, "invoke", Arguments(*allArgs))
|
||||
}
|
||||
}
|
||||
return obj.copy(
|
||||
value = wrapper,
|
||||
type = ObjRecord.Type.Other
|
||||
)
|
||||
}
|
||||
val res = del.invokeInstanceMethod(scope, "getValue", Arguments(th, ObjString(name)))
|
||||
})
|
||||
return obj.copy(
|
||||
value = res,
|
||||
type = ObjRecord.Type.Other
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user