ProvideDelegateReplSmokeTest.kt
src/jvmMain/kotlin/afield/examples/ProvideDelegateReplSmokeTest.kt · 386 lines
package afield.examples
import afield.runtime.kotlin.KotlinInterface2
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlin.script.experimental.api.ScriptDiagnostic
FIELD_TEST id: kotlin.repl.provide-delegate runner: jvm-main entrypoint: afield.examples.ProvideDelegateReplSmokeTest tags: [smoke, kotlin, repl, k2, delegate] timeoutSec: 600 expect: exitCode: 0 stdoutContains: - "provideDelegate REPL characterization passed"
object ProvideDelegateReplSmokeTest
operator fun provideDelegate in Field's K2 REPL, in every position box code can put it — the
box idiom var kc by keyCamera(camera=camera) (public/docs/afield_four_graphics.md:787) among
them. This began as a characterization of two crashes; both are fixed, and it now asserts that
every rung evaluates.
History
The test pinned the crash signatures while they existed, and was built to go red when either was fixed. That alarm fired twice:
- 2026-08-25, DEFECT_A was fixed in the fork:
cross-snippet-provideDelegate, theby keyCamera(...)shape, started working, and the five rungs that embedPROVIDER_DECLmoved from A to DEFECT_B (A had only been failing first). - 2026-10-04, DEFECT_B was fixed in Field's build of the Wither plugin, and the nine rungs
in
PROVIDE_DELEGATEmoved from failing to working.
The two causes
DEFECT_A was IrPropertySymbolImpl is unbound, thrown inside
org.jetbrains.kotlin.fir.backend.Fir2IrConverter.runSourcesConversion, with no Field frame and
no compiler-plugin frame between K2ReplCompiler.compileImpl and the throw. A REPL-level
delegated property is resolved through a disposable FIR copy of itself
(replSnippetDelegatedPropertyCopies, which upstream needs because the Analysis API forbids
resolving a sibling member mid-resolution), and the resolved delegate expression is then
installed into the real property — but with provideDelegate that expression carries a ::x
callable reference resolved against the copy, which is not a snippet-class member and never
gets IR. The fork records the original on the copy and maps back to it in
Fir2IrDeclarationStorage.prepareProperty.
DEFECT_B was IrGenerationExtensionException: IrFunctionImpl … has no callableId, thrown from
the Wither plugin's WitherReceiverTransformer. To find its own with(...) calls, that IR pass
read the callableId of every call standing as a statement, and callableId throws for a
function that is local or a member of a local or anonymous class. The v = value in
PROVIDER_DECL's anonymous ReadWriteProperty is such a call (the setter of v), which is
why that interface failed and ReadOnlyProperty, with no setter to call, did not. Wither now
checks the callee's name and parent before asking. Local functions called as statements failed
the same way; afield.runtime.kotlin.ReplSnippetDeclarationsSmokeTest cases LF1–LF9 cover those.
House style, applied deliberately:
- asserts its own premise first — assertReplItselfWorks proves plain evaluation and the
non-provideDelegate delegates yield 42 before anything else is judged.
- floored at a number — every rung must produce its exact value, not merely "not crash".
- bidirectional ledger — assertLedger checks missing, stale, and set equality across the
declared and observed rung names.
object ProvideDelegateReplSmokeTest {
const val DEFECT_A = "IrPropertySymbolImpl is unbound"
Signature of the fork-side, top-level failure. See DEFECT_A.
const val DEFECT_B = "has no callableId"
Signature of the Wither-plugin failure, fixed 2026-10-04. Its trigger was not
provideDelegate and not local-ness: it was a call, as a statement, to a function with no
callableId — here the setter of a property of an anonymous ReadWriteProperty. See
dev_docs/k2_repl_provide_delegate.md.
private const val PROVIDER_DECL = """
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class Provider {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadWriteProperty<Any?, Int> {
val n = 42
return object : ReadWriteProperty<Any?, Int> {
private var v = n
override fun getValue(thisRef: Any?, property: KProperty<*>): Int = v
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { v = value }
}
}
}
"""
A provider whose provideDelegate returns a named holder instead of an anonymous
ReadWriteProperty. It was the control that kept DEFECT_A and DEFECT_B apart while both
were open, because PROVIDER_DECL could not even be declared without tripping DEFECT_B.
private const val NAMED_PROVIDER_DECL = """
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class NamedHolder(private var v: Int) : ReadWriteProperty<Any?, Int> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Int = v
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { v = value }
}
class NamedProvider {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadWriteProperty<Any?, Int> =
NamedHolder(42)
}
"""
Rungs that must evaluate to exactly EXPECTED_VALUE. Deliberately broad: these are seven
different ways to write by without a delegate provider, and all seven work. That breadth
is what licenses the claim that the defect is provideDelegate specifically rather than
delegation generally.
private val WORKING: Map<String, String> = linkedMapOf(
"plain-val" to "val a = 40 + 2\na",
"by-lazy" to "val a: Int by lazy { 42 }\na",
"by-readwriteproperty" to """
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class Holder : ReadWriteProperty<Any?, Int> {
private var v = 42
override fun getValue(thisRef: Any?, property: KProperty<*>): Int = v
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { v = value }
}
val a: Int by Holder()
a
""",
"by-delegates-observable" to """
import kotlin.properties.Delegates
var a: Int by Delegates.observable(42) { _, _, _ -> }
a
""",
"by-delegates-notnull" to """
import kotlin.properties.Delegates
var a: Int by Delegates.notNull<Int>()
a = 42
a
""",
"by-map-delegation" to "val m = mapOf(\"a\" to 42)\nval a: Int by m\na",
"by-readonlyproperty-sam" to """
import kotlin.properties.ReadOnlyProperty
val a: Int by ReadOnlyProperty<Any?, Int> { _, _ -> 42 }
a
""",
)
Working rungs that need a prior snippet, kept apart from WORKING only because they carry one; they are asserted identically.
cross-snippet-provideDelegate moved here from the failing ledger on 2026-08-25, when DEFECT_A was
root-caused and fixed in the vendored fork. It is the by keyCamera(...) shape — the provider
is AOT-compiled outside the snippet being compiled — so this is the rung that says the
user-facing idiom works again. It used NAMED_PROVIDER_DECL because PROVIDER_DECL tripped
DEFECT_B until 2026-10-04; it keeps it, so the AOT-style named provider stays covered.
private val WORKING_WITH_PRIOR: Map<String, Pair<String, String>> = linkedMapOf(
"cross-snippet-provideDelegate" to
(NAMED_PROVIDER_DECL to "var abcd: Int by NamedProvider()\nabcd\n"),
)
Every provideDelegate position probed, as (prior snippet, source, expected value). These
were the failing ledger until 2026-10-04, all with DEFECT_B: val as well as var, an
extension provideDelegate as well as a member one, a generic provider, a factory function,
a local, and a class member, plus two rungs with no delegate machinery at all.
source — 104 lines, from line 172
private val PROVIDE_DELEGATE: Map<String, Triple<String?, String, String>> = linkedMapOf(
"toplevel-provideDelegate" to
Triple(null, PROVIDER_DECL + "var abcd: Int by Provider()\nabcd\n", EXPECTED_VALUE),
"factoryfn-provideDelegate" to
Triple(null, PROVIDER_DECL + "fun provider(): Provider = Provider()\nvar abcd: Int by provider()\nabcd\n", EXPECTED_VALUE),
"toplevel-val-provideDelegate" to
Triple(null, PROVIDER_DECL + "val abcd: Int by Provider()\nabcd\n", EXPECTED_VALUE),
"generic-provideDelegate" to Triple(
null,
"""
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class GProvider<T>(val initial: T) {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadWriteProperty<Any?, T> {
val n = initial
return object : ReadWriteProperty<Any?, T> {
private var v = n
override fun getValue(thisRef: Any?, property: KProperty<*>): T = v
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { v = value }
}
}
}
var abcd: Int by GProvider(42)
abcd
""",
EXPECTED_VALUE,
),
"extension-provideDelegate" to Triple(
null,
"""
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class Plain
operator fun Plain.provideDelegate(thisRef: Any?, property: KProperty<*>): ReadWriteProperty<Any?, Int> {
val n = 42
return object : ReadWriteProperty<Any?, Int> {
private var v = n
override fun getValue(thisRef: Any?, property: KProperty<*>): Int = v
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { v = value }
}
}
var abcd: Int by Plain()
abcd
""",
EXPECTED_VALUE,
),
// --- The DEFECT_B trigger with no delegate machinery in it at all: an anonymous object
// implementing kotlin.properties.ReadWriteProperty, created inside a function body. ---
"anon-readwriteproperty-in-fun" to Triple(
null,
"""
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
fun make(): ReadWriteProperty<Any?, Int> = object : ReadWriteProperty<Any?, Int> {
private var v = 0
override fun getValue(thisRef: Any?, property: KProperty<*>): Int = v
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { v = value }
}
val p = make()
p.setValue(null, String::length, 42)
p.getValue(null, String::length)
""",
EXPECTED_VALUE,
),
// Declaring the idiomatic provider used to be enough — it is never used with `by` here.
"declare-provideDelegate-only" to Triple(null, PROVIDER_DECL + "42\n", EXPECTED_VALUE),
"local-provideDelegate" to
Triple(null, PROVIDER_DECL + "fun demo(): Int {\n var abcd: Int by Provider()\n return abcd\n}\ndemo()\n", EXPECTED_VALUE),
"classmember-provideDelegate" to
Triple(null, PROVIDER_DECL + "class Owner { var abcd: Int by Provider() }\nOwner().abcd\n", EXPECTED_VALUE),
)
private const val EXPECTED_VALUE = "42"
private class Run(val value: String?, val diagnostics: List<String>)
private fun evaluate(label: String, src: String, priorSnippet: String? = null): Run = runBlocking {
val diags = mutableListOf<String>()
val ki = KotlinInterface2(filePrefix = "pd-${label.filter { it.isLetterOrDigit() }}")
try {
if (priorSnippet != null) {
val priorDiags = mutableListOf<String>()
withTimeout(240_000) {
ki.exec(priorSnippet, onCompileError = { ds -> ds.forEach { priorDiags += it.toString() } })
}
// The prior snippet is setup, not the thing under test. If it cannot compile, the
// rung would "fail" for the wrong reason and quietly look like the defect.
require(priorDiags.isEmpty()) {
"rung '$label': its PRIOR snippet failed to compile, so this rung proves nothing: $priorDiags"
}
}
val result = withTimeout(240_000) {
ki.exec(
src,
onCompileError = { ds: List<ScriptDiagnostic> -> ds.forEach { diags += it.toString() } },
onEvalError = { ds: List<ScriptDiagnostic> -> ds.forEach { diags += it.toString() } },
onEvalException = { t: Throwable -> diags += "THROWN ${t::class.qualifiedName}: ${t.message}" },
)
}
Run(result?.toString(), diags)
} finally {
runCatching { ki.dispose() }
}
}
Premise gate. If the REPL cannot evaluate 40 + 2, or cannot handle the non
provideDelegate delegate forms, the provideDelegate rungs below are not the problem. Fail
here, loudly, instead.
private fun assertReplItselfWorks(observedWorking: MutableMap<String, String>) {
println(" premise: the REPL and non-provideDelegate delegation still work...")
(WORKING.mapValues { (_, s) -> null to s } + WORKING_WITH_PRIOR).forEach { (label, spec) ->
val (prior, src) = spec
val run = evaluate(label, src, prior)
require(run.diagnostics.isEmpty()) {
"PREMISE FAILED: rung '$label' should compile cleanly but reported " +
"${run.diagnostics.size} diagnostic(s): ${run.diagnostics}. " +
"The REPL is broken more broadly than this test characterizes; fix that first."
}
val value = run.value
require(value != null && value.contains(EXPECTED_VALUE)) {
"PREMISE FAILED: rung '$label' should evaluate to $EXPECTED_VALUE, got: $value. " +
"A working rung that stops producing its value makes this test a tautology."
}
observedWorking[label] = value
println(" ok $label -> $value")
}
}
Every PROVIDE_DELEGATE rung must compile cleanly and produce its value. A rung that fails is
classified by signature, so a returning DEFECT_A or DEFECT_B reads as that regression
rather than as an unknown crash.
private fun assertProvideDelegateWorks(observedWorking: MutableMap<String, String>) {
println(" provideDelegate in every position...")
// Measure every rung, print the table, and only then judge. A compiler change moves rungs in
// GROUPS -- fixing DEFECT_A on 2026-08-25 moved one rung to working and re-pointed five more
// at DEFECT_B in a single step -- and failing on the first mismatch reports one rung while
// hiding the shape of the move.
val measured = PROVIDE_DELEGATE.mapValues { (_, spec) -> evaluate("measure", spec.second, spec.first) }
println(" measured ledger (what this compiler actually does):")
measured.forEach { (label, run) ->
val expected = PROVIDE_DELEGATE.getValue(label).third
val joined = run.diagnostics.joinToString("\n")
println(
" %-34s %s".format(
label,
when {
run.diagnostics.isEmpty() && run.value?.contains(expected) == true -> "works -> ${run.value}"
run.diagnostics.isEmpty() -> "WRONG VALUE: ${run.value}, expected $expected"
joined.contains(DEFECT_A) -> "REGRESSED to DEFECT_A ($DEFECT_A)"
joined.contains(DEFECT_B) -> "REGRESSED to DEFECT_B ($DEFECT_B)"
else -> "FAILS: ${run.diagnostics.first().take(140)}"
}
)
)
}
PROVIDE_DELEGATE.forEach { (label, spec) ->
val expected = spec.third
val run = measured.getValue(label)
require(run.diagnostics.isEmpty()) {
"rung '$label' should compile and evaluate cleanly but reported " +
"${run.diagnostics.size} diagnostic(s): ${run.diagnostics}"
}
val value = run.value
require(value != null && value.contains(expected)) {
"rung '$label' should evaluate to $expected, got: $value"
}
observedWorking[label] = value
}
}
Bidirectional: missing, stale, and set equality, in both directions.
private fun assertLedger(observedWorking: Map<String, String>) {
println(" ledger: missing / stale / set equality...")
val expectedWorking = WORKING.keys + WORKING_WITH_PRIOR.keys + PROVIDE_DELEGATE.keys
val missingWorking = expectedWorking - observedWorking.keys
val staleWorking = observedWorking.keys - expectedWorking
require(missingWorking.isEmpty()) { "ledger: working rungs declared but never observed: $missingWorking" }
require(staleWorking.isEmpty()) { "ledger: working rungs observed but not declared: $staleWorking" }
require(observedWorking.keys == expectedWorking) { "ledger: working set mismatch" }
// A floor, not an equality: rungs may be added, but a silently-shrinking matrix fails. 17 =
// seven non-provideDelegate spellings of `by`, the cross-snippet rung, and the nine
// provideDelegate positions that were the failing ledger until 2026-10-04.
require(observedWorking.size >= 17) { "expected at least 17 working rungs, got ${observedWorking.size}" }
println(" working=${observedWorking.size}")
}
fun runTests(): Boolean
fun runTests(): Boolean {
return try {
println("Running provideDelegate REPL characterization...")
val observedWorking = linkedMapOf<String, String>()
assertReplItselfWorks(observedWorking)
assertProvideDelegateWorks(observedWorking)
assertLedger(observedWorking)
println("✅ provideDelegate REPL characterization passed")
true
} catch (t: Throwable) {
println("❌ provideDelegate REPL characterization failed: ${t.message}")
t.printStackTrace()
false
}
}
fun main(args: Array<String>)
@JvmStatic
fun main(args: Array<String>) {
check(runTests()) { "provideDelegate REPL characterization failed" }
}
}