SnippetClassReceiverCrash.kt
src/jvmMain/kotlin/afield/runtime/kotlin/SnippetClassReceiverCrash.kt · 117 lines
package afield.runtime.kotlin
internal object SnippetClassReceiverCrash
Turns one K2 REPL crash into something a person (or an agent) can act on.
In a box, a bare println, print, display or error is the afield.ui.output.HasOutput member
of the box, reached through the snippet's implicit receiver. Inside a class or object the box
declares, the frontend accepts that call but the script lowering has no path back to the snippet
instance and throws Unexpected script transformation state: ScriptLikeToClassTransformerContext(...),
with no source location; the box then reports its last line. class C { fun g() { println("hi") } }
is enough (found 2026-10-28 in an agent's box; kotlin.io.println compiles).
const val SIGNATURE = "Unexpected script transformation state"
Calls as the compiler sees them, mapped to what the person wrote: Field's macros rewrite
println(x) to _printlnHere(__line__+1, x) and print to _printHere before compiling.
private val boxOutputMembers = mapOf(
"println" to "println", "print" to "print", "display" to "display", "error" to "error",
"_printlnHere" to "println", "_printHere" to "print"
)
data class Site(val line: Int, val column: Int, val member: String, val declaration: String)
A bare box-output call inside a class or object: 1-based line and column, the member and the declaration.
fun matches(messages: List<String?>): Boolean = messages.any { it?.contains(SIGNATURE) == true }
fun explain(site: Site?): String
fun explain(site: Site?): String = buildString {
append("\n\nInside a class or object declared in a box, the box itself is not reachable, so its ")
append("`println`, `print`, `display` and `error` cannot be called there (a K2 REPL limitation; the compiler crashes ")
append("instead of reporting it). ")
if (site != null) append("Here: `${site.member}(...)` in `${site.declaration}`, line ${site.line}. ")
append("Use `kotlin.io.println(...)`, return the value and print it outside the class, or pass the box in ")
append("(`class W(val box: Box)` then `box.println(...)`).")
}
fun locate(source: String): Site?
The first bare box-output call inside a class or object body in source, or null.
fun locate(source: String): Site? {
val code = blankStringsAndComments(source)
val declaration = Regex("""\b(class|object)\s+([A-Za-z_][A-Za-z0-9_]*)""")
for (decl in declaration.findAll(code)) {
val open = bodyStart(code, decl.range.last + 1) ?: continue
val close = matchingBrace(code, open) ?: continue
val call = Regex("""(?<![\w.:])(?<!fun )(_printlnHere|_printHere|println|print|display|error)\s*\(""").find(code.substring(open, close)) ?: continue
val member = boxOutputMembers[call.groupValues[1]] ?: continue
val at = open + call.range.first
val line = 1 + code.substring(0, at).count { it == '\n' }
val column = at - code.lastIndexOf('\n', at - 1)
return Site(line, column, member, decl.groupValues[2])
}
return null
}
The { that opens the body after a declaration's header, past a primary constructor, type
parameters and supertypes; null when the header ends first (a bodiless class W).
private fun bodyStart(code: String, from: Int): Int? {
var depth = 0
for (i in from until code.length) {
when (code[i]) {
'(', '<' -> depth++
')', '>' -> depth--
'{' -> if (depth <= 0) return i
'\n' -> if (depth <= 0) {
val header = code.substring(from, i).trimEnd()
val next = nextNonBlank(code, i)
if (!header.endsWith(",") && !header.endsWith(":") && next != '{' && next != ':' && next != ',') return null
}
}
}
return null
}
private fun nextNonBlank(code: String, from: Int): Char? {
var i = from
while (i < code.length && code[i].isWhitespace()) i++
return code.getOrNull(i)
}
private fun matchingBrace(code: String, open: Int): Int? {
var depth = 0
for (i in open until code.length) {
if (code[i] == '{') depth++
if (code[i] == '}' && --depth == 0) return i
}
return null
}
internal fun blankStringsAndComments(source: String): String
Same length and line breaks as source, with string literals and comments replaced by spaces.
internal fun blankStringsAndComments(source: String): String {
val out = StringBuilder(source)
var i = 0
fun blank(from: Int, to: Int) { for (k in from until minOf(to, out.length)) if (out[k] != '\n') out[k] = ' ' }
while (i < source.length) {
when {
source.startsWith("//", i) -> { val end = source.indexOf('\n', i).let { if (it < 0) source.length else it }; blank(i, end); i = end }
source.startsWith("/*", i) -> { val end = source.indexOf("*/", i + 2).let { if (it < 0) source.length else it + 2 }; blank(i, end); i = end }
source.startsWith("\"\"\"", i) -> { val end = source.indexOf("\"\"\"", i + 3).let { if (it < 0) source.length else it + 3 }; blank(i, end); i = end }
source[i] == '"' -> {
var j = i + 1
while (j < source.length && source[j] != '"' && source[j] != '\n') { if (source[j] == '\\') j++; j++ }
blank(i, j + 1); i = j + 1
}
source[i] == '\'' && i + 2 < source.length -> {
val end = source.indexOf('\'', i + 1 + (if (source[i + 1] == '\\') 1 else 0)).let { if (it < 0) i + 1 else it + 1 }
blank(i, end); i = end
}
else -> i++
}
}
return out.toString()
}
}