KotlinInterface.kt
src/jvmMain/kotlin/afield/runtime/kotlin/KotlinInterface.kt · 1085 lines
package afield.runtime.kotlin
import afield.app.diagnostics.named
import afield.data.table.div
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.spreadsheet
import afield.data.table.times
import afield.runtime.execution.plus
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem
import afield.runtime.lifecycle.then
import afield.ui.notation.string
import afield.ui.visualization.typed
import afield.app.notifications.notify
import afield.app.protocol.put
import afield.app.settings.needsRerun
import afield.audio.synth.*
import afield.data.table.get
import afield.document.graph.Box
import afield.document.graph.HasProperties
import afield.document.graph.box
import afield.document.graph.doResync
import afield.document.graph.intro
import afield.document.graph.metaroot
import afield.document.graph.preamble
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.runtime.lifecycle.end
import afield.runtime.lifecycle.map
import afield.ui.output.templates.filter
import afield.ui.output.update
import org.jetbrains.kotlin.scripting.ide_services.compiler.KJvmReplCompilerWithIdeServices
package and imports — 44 lines, from line 37
import afield.app.diagnostics.logging.formatMillis
import afield.app.diagnostics.logging.lblock
import afield.app.diagnostics.logging.lprintln
import afield.runtime.macros.ContainingFunction
import afield.runtime.macros.MacroRegistry
import afield.graphics.shader.applyInsertToPreamble
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.jetbrains.kotlin.backend.common.pop
import org.jetbrains.kotlin.com.intellij.lang.Language
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.com.intellij.psi.PsiFileFactory
import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.scripting.ide_services.compiler.KJvmReplCompilerWithIdeServices
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.reflect.Field
import java.lang.reflect.Proxy
import java.net.URL
import java.net.URLClassLoader
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.reflect.*
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.kotlinFunction
import kotlin.reflect.jvm.kotlinProperty
import kotlin.script.experimental.api.*
import kotlin.script.experimental.dependencies.*
import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver
import kotlin.script.experimental.host.with
import kotlin.script.experimental.impl._languageVersion
import kotlin.script.experimental.jvm.*
import kotlin.script.experimental.jvm.impl.KJvmCompiledScript
import kotlin.script.experimental.jvm.util.determineSep
import kotlin.script.experimental.jvm.util.toSourceCodePosition
import kotlin.script.experimental.util.LinkedSnippet
import kotlin.system.measureNanoTime
The older K1 implementation of KotlinScriptEngine, built on the K1 REPL compiler.
KotlinInterface2 is the K2 replacement. createKotlinScriptEngine decides which of the two a box gets.
class KotlinInterface
class KotlinInterface(
val propertyClasses: Map<String, KClass<*>> = emptyMap(), val properties: Map<String, Any?> = emptyMap(),
override val implicitReceivers: List<KotlinType> = emptyList(),
override var classLoader: ClassLoader? = null, val filePrefix: String = "EVAL_"
) : KotlinScriptEngine {
override val compilationDispatcher get() = compilationThread
override var sourceProperty: String = "code"
internal inner class SourceCodeImpl(number: Int, override val text: String) : SourceCode
override val name: String? = fieldSourceName(filePrefix, sourceProperty, number)
override val locationId: String? = "LOCATION_$number"
override fun equals(other: Any?): Boolean
override fun equals(other: Any?): Boolean {
// `this === other`, not `==`: `==` is a call to this same equals, and the self-call
// recursed until the stack ran out. Latent until a compiler that compares SourceCode
// instances came along (2.4.20 does), and then EVERY compile died in here.
return this === other || (other as? SourceCodeImpl)?.let { text == it.text } == true
}
override fun hashCode(): Int
override fun hashCode(): Int {
return text.hashCode()
}
}
var evaluator: BasicJvmReplEvaluator
var compiler: KJvmReplCompilerWithIdeServices
Kotlin 2.5 raised the K1 IDE-services REPL compiler to DeprecationLevel.ERROR. This is the legacy K1 backend (still selectable, and used by the spreadsheet), so it is suppressed here and at the construction site below rather than removed.
@Suppress("DEPRECATION_ERROR")
var compiler: KJvmReplCompilerWithIdeServices
var compiler: K2KJvmReplCompilerWithCompletion
override var compilerConfig: ScriptCompilationConfiguration
var evalConfig: ScriptEvaluationConfiguration
val contextUpdater: ContextUpdater
override val context: KotlinContext
override var associatedBox: Any? = null
Reference to the box this KotlinInterface is associated with (for @exported support)
private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver())
init
body — 48 lines, from line 134
init {
assert(properties.size == propertyClasses.size)
assert(properties.keys.containsAll(propertyClasses.keys))
if (classLoader == null) classLoader = Thread.currentThread().contextClassLoader
if (sharedClassLoader == null) {
sharedClassLoader = URLClassLoader(arrayOf(), classLoader)
}
classLoader = sharedClassLoader
MavenClasspathManager.attachResolvedTo(classLoader)
val myHostConfiguration = defaultJvmScriptingHostConfiguration.with {
jvm {
baseClassLoader(classLoader!!)
}
}
compilerConfig = ScriptCompilationConfiguration {
defaultImports(DependsOn::class, Repository::class, exported::class, rootExported::class, metarootExported::class)
hostConfiguration.update {
myHostConfiguration
}
this@KotlinInterface.implicitReceivers.forEach { implicitReceivers.append(it) }
jvm {
jvmTarget.put("24")
_languageVersion.put(fieldKotlinLanguageVersion)
compilerOptions.put(fieldKotlinJvmCompilerOptions)
dependenciesFromClassloader(
classLoader = classLoader!!, wholeClasspath = true, unpackJarCollections = true
)
providedProperties(*propertyClasses.map { it.key to it.value }.toTypedArray())
}
refineConfiguration {
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
}
}
compiler = @Suppress("DEPRECATION_ERROR") KJvmReplCompilerWithIdeServices()
evaluator = BasicJvmReplEvaluator()
context = KotlinContext()
contextUpdater = ContextUpdater(context, evaluator, this)
}
override fun dispose()
Release the compiler's project and everything it analysed.
Nothing else frees it. The compilation state's Disposable hangs off IntelliJ's static
Disposer tree, which reaches back through the project to this engine, so a dropped engine is
never collected and its finalize never runs. Measured 2026-10-27: KotlinInterface2 rebuilt
its K1 completion delegate on every Kotlin edit of a launched graphics box, each one kept
~60 MB of deserialized library metadata, and an 8 GB heap ran out after ~120 edits.
As in KotlinInterface2.dispose, the state is released only once no compile, completion or
macro pass is using it (usingCompilationState); work that starts afterwards is refused rather
than allowed to build a new state.
override fun dispose() {
disposed = true
if (compilationStateUsers.get() == 0) releaseCompilationState()
}
@Volatile
private var disposed = false
override val isDisposed: Boolean get() = disposed
private val compilationStateUsers = AtomicInteger(0)
private val compilationStateReleased = AtomicBoolean(false)
Run body with the compilation state held live, or onDisposed once dispose has run.
private inline fun <T> usingCompilationState(onDisposed: () -> T, body: () -> T): T {
compilationStateUsers.incrementAndGet()
try {
return if (disposed) onDisposed() else body()
} finally {
if (compilationStateUsers.decrementAndGet() == 0 && disposed) releaseCompilationState()
}
}
private fun releaseCompilationState() {
if (compilationStateReleased.compareAndSet(false, true)) compiler.state.dispose()
}
fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration>
fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> {
notify("configuring dependencies in script", "execution")
val annotations =
context.collectedData?.get(ScriptCollectedData.collectedAnnotations)?.takeIf { it.isNotEmpty() }
?: return context.compilationConfiguration.asSuccess() // If no action is performed, the original configuration should be returned
return runBlocking {
notify("running resolver for dependencies", "execution")
// resolving maven artifacts using annotation arguments
resolver.resolveFromScriptSourceAnnotations(annotations)
}.onSuccess {
notify("finished running resolver", "execution")
context.compilationConfiguration.with {
// updating the original configurations with the newly resolved artifacts as compilation dependencies
try {
it.forEach {
val target = (classLoader!! as URLClassLoader)
lprintln("warning,kotlin", "sketchy url classloader front running ${it}")
val m = URLClassLoader::class.java.getDeclaredMethod("addURL", URL::class.java)
m.isAccessible = true
m.invoke(target, it.toURI().toURL())
}
} catch (e: Throwable) {
lprintln("error,kotlin", "sketchy url classloader front running threw")
e.printStackTrace()
}
jvm {
dependenciesFromClassloader(
classLoader = classLoader!!, wholeClasspath = true, unpackJarCollections = true
)
}
// dependencies.append(JvmDependency(it))
}.asSuccess()
}
}
var id = 0
var cache = mutableMapOf<String, ResultWithDiagnostics<LinkedSnippet<KJvmCompiledScript>>?>()
private fun compileSnippetKind(s: String): String {
return when {
s == intro -> "field-startup-imports"
s.isBlank() -> "blank"
s.trimStart().startsWith("import ") -> "imports-or-preamble"
else -> "user-code"
}
}
private fun compileSnippetPreview(s: String, maxLength: Int = 120): String {
val compact = s.lineSequence()
.map { it.trim() }
.filter { it.isNotEmpty() }
.take(3)
.joinToString(" | ")
return if (compact.length <= maxLength) compact else compact.take(maxLength) + "..."
}
private fun resolveBoxContext(implicits: List<Any> = emptyList()): Box? {
val fromImplicits = implicits.firstOrNull { it is Box } as? Box
if (fromImplicits != null) return fromImplicits
val fromAssociated = associatedBox as? Box
if (fromAssociated != null) return fromAssociated
return Box.context.get()
}
suspend fun compile(s: String): ResultWithDiagnostics<LinkedSnippet<KJvmCompiledScript>>
body — 41 lines, from line 291
suspend fun compile(s: String): ResultWithDiagnostics<LinkedSnippet<KJvmCompiledScript>> {
val code = SourceCodeImpl(id++, s)
val snapshot = context.debugScripts.recordCompile(code.name ?: "snippet_${id - 1}", s)
var r: ResultWithDiagnostics<LinkedSnippet<KJvmCompiledScript>>? = null
lprintln(
"info,kotlin,perf",
"\t\t compile request engine=KotlinInterface filePrefix=$filePrefix source=${code.name} kind=${
compileSnippetKind(
s
)
} length=${s.length} preview=${compileSnippetPreview(s)}"
)
lprintln("info,kotlin,perf", "\t\t compile code $s")
val ns = measureNanoTime {
r = usingCompilationState(onDisposed = { disposedFailure(s) }) {
if (KotlinEngineConfig.offloadCompilation) {
// K1 is NOT thread-safe — always use the single compilationThread
withContext(compilationDispatcher) {
compiler.compile(code, compilerConfig)
}
} else {
compiler.compile(code, compilerConfig)
}
}
}
lprintln("info,kotlin,compile", "compile=${formatMillis(ns)}")
if (r is ResultWithDiagnostics.Failure) {
context.debugScripts.discard(snapshot)
}
return r!!
}
private fun disposedFailure(s: String): ResultWithDiagnostics<LinkedSnippet<KJvmCompiledScript>> {
lprintln("info,kotlin", "compile skipped: engine filePrefix=$filePrefix was disposed, preview=${compileSnippetPreview(s)}")
return ResultWithDiagnostics.Failure(
ScriptDiagnostic(ScriptDiagnostic.unspecifiedError, "This Kotlin engine was disposed before the code compiled.")
)
}
suspend fun evaluate
suspend fun evaluate(
e: LinkedSnippet<KJvmCompiledScript>, updateContext: Boolean = false
): ResultWithDiagnostics<LinkedSnippet<KJvmEvaluatedSnippet>> {
return Box.withBoxContext(resolveBoxContext()) {
var r: ResultWithDiagnostics<LinkedSnippet<KJvmEvaluatedSnippet>>
val ns = measureNanoTime {
val evalConfig = ScriptEvaluationConfiguration {
jvm {
baseClassLoader(classLoader!!)
}
providedProperties(properties)
}
r = evaluator.eval(e, evalConfig)
if (r is ResultWithDiagnostics.Success) {
val scriptInstance = r.value.get().result.scriptInstance
context.debugScripts.attachLatestRuntimeClass(scriptInstance?.javaClass?.name)
}
if (updateContext) contextUpdater.update()
}
lprintln("info,kotlin,evaluate", "evaluate=${formatMillis(ns)}")
r
}
}
suspend fun evaluate
suspend fun evaluate(
e: LinkedSnippet<KJvmCompiledScript>, updateContext: Boolean = true, implicits: List<Any>
): ResultWithDiagnostics<LinkedSnippet<KJvmEvaluatedSnippet>> {
return Box.withBoxContext(resolveBoxContext(implicits)) {
var r: ResultWithDiagnostics<LinkedSnippet<KJvmEvaluatedSnippet>>
val ns = measureNanoTime {
val evalConfig = ScriptEvaluationConfiguration {
implicits.forEach { implicitReceivers.append(it) }
jvm {
baseClassLoader(classLoader!!)
}
providedProperties(properties)
}
r = evaluator.eval(e, evalConfig)
if (r is ResultWithDiagnostics.Success) {
val scriptInstance = r.value.get().result.scriptInstance
context.debugScripts.attachLatestRuntimeClass(scriptInstance?.javaClass?.name)
}
if (updateContext) contextUpdater.update()
}
lprintln("info,kotlin,evaluate", "evaluate=${formatMillis(ns)}")
r
}
}
fun String.findNthSubstring(s: String, n: Int, start: Int = 0): Int
fun String.findNthSubstring(s: String, n: Int, start: Int = 0): Int {
if (n < 1 || start == -1) return -1
var i = start
for (k in 1..n) {
i = indexOf(s, i)
if (i == -1) return -1
i += s.length
}
return i - s.length
}
fun Int.toSourceCodePositionWithNewAbsolute(code: SourceCode, newCode: SourceCode): SourceCode.Position?
fun Int.toSourceCodePositionWithNewAbsolute(code: SourceCode, newCode: SourceCode): SourceCode.Position? {
val pos = toSourceCodePosition(code)
val sep = code.text.determineSep()
val absLineStart = if (pos.line == 1) 0
else newCode.text.findNthSubstring(sep, pos.line - 1) + sep.length
var nextNewLinePos = newCode.text.indexOf(sep, absLineStart)
if (nextNewLinePos == -1) nextNewLinePos = newCode.text.length
val abs = absLineStart + pos.col - 1
if (abs > nextNewLinePos) return null
return SourceCode.Position(pos.line, abs - absLineStart + 1, abs)
}
override suspend fun complete(s: String, cursor: Int): CompletionResult
override suspend fun complete(s: String, cursor: Int): CompletionResult {
val code = SourceCodeImpl(id++, s)
return try {
val cc = usingCompilationState(onDisposed = { return CompletionResult.Empty(s, cursor) }) {
compiler.complete(code, cursor.toSourceCodePositionWithNewAbsolute(code, code)!!, compilerConfig)
}
cc.valueOrNull()?.toList()?.let { completionList ->
getResult(s, cursor, completionList)
} ?: CompletionResult.Empty(s, cursor)
} catch (e: Exception) {
val sw = StringWriter()
e.printStackTrace(PrintWriter(sw))
CompletionResult.Error(e.javaClass.simpleName, e.message ?: "", sw.toString())
}
}
override fun completeAndWait(s: String, cursor: Int): CompletionResult
override fun completeAndWait(s: String, cursor: Int): CompletionResult {
return runBlocking { complete(s, cursor) }
}
override fun applyMacros(box: Box, s: String, skipMacros: Set<String>, previewOnly: Boolean): String
body — 112 lines, from line 442
override fun applyMacros(box: Box, s: String, skipMacros: Set<String>, previewOnly: Boolean): String =
usingCompilationState(onDisposed = { s }) { applyMacrosLive(box, s, skipMacros, previewOnly) }
private fun applyMacrosLive(box: Box, s: String, skipMacros: Set<String>, previewOnly: Boolean): String {
var rewritten = s
val needsRerun = false
lblock("debug,kotlin", "rewriting AST analysis") {
do {
val cs = compiler.state.getCompilationState(compilerConfig)
val env = cs.environment
val project = env.project
val factory = PsiFileFactory.getInstance(project)
val lang = Language.findLanguageByID("kotlin")!!
val instance = factory.createFileFromText(lang, rewritten)
val edits = mutableListOf<Pair<Pair<Int, Int>, String>>()
instance.forEachDescendantOfType<PsiElement> { expression ->
when (expression) {
is KtCallExpression -> {
expression.children.forEach {
when (it) {
is KtNameReferenceExpression -> {
if (it.text in skipMacros) return@forEach
lprintln(
"debug,kotlin",
"child (named): ${it} ${it.text} ${it.javaClass} ${it.startOffset} -> ${it.endOffset} ${it.reference}"
)
var ns = it.nextSibling
val t = mutableListOf<String>()
while (ns != null) {
t.add(ns.text)
ns = ns.nextSibling
}
// won't work for inferred return types
val containingFunction =
PsiTreeUtil.getParentOfType(expression, KtNamedFunction::class.java)
val containingFunctionName = containingFunction?.name
val valueParameterList = containingFunction?.valueParameterList?.text
val returnParameterText = containingFunction?.typeReference?.text
val containingFunctionInfo =
if (containingFunctionName != null && valueParameterList != null && returnParameterText != null)
ContainingFunction(
containingFunctionName,
valueParameterList,
returnParameterText
)
else null
val e = MacroRegistry.rewrite(box, it.text, t, containingFunctionInfo)
if (e != null) {
if (!previewOnly) {
if (e.start.length > 0) {
edits.add((0 to 0) to e.preamble)
}
if (e.afterImports.length > 0) {
val a = findLastImport(rewritten)
edits.add((a to a) to e.afterImports)
}
}
val replacement =
if (expression.parent is KtBlockExpression || expression.parent is KtScriptInitializer)
e.standaloneHere ?: e.here
else
e.here
edits.add((expression.startOffset to expression.endOffset) to replacement)
if (!previewOnly) {
if (e.preamble.isNotBlank() || e.preambleAfterImports.isNotBlank()) {
applyInsertToPreamble(box, e)
}
}
}
}
else -> {
// lprintln("child: ${it} ${it.javaClass} ${it.startOffset} -> ${it.endOffset}")
}
}
}
}
is KtNameReferenceExpression -> {
}
}
}
while (edits.size > 0) {
lprintln("debug,kotlin", "applying edits")
val e = edits.pop()
rewritten = rewritten.replaceRange(e.first.first, e.first.second, e.second)
val shrink = (e.first.second - e.first.first) - e.second.length
edits.forEachIndexed { index, it ->
val a = if (it.first.first > e.first.first) it.first.first - shrink else it.first.first
val b = if (it.first.second > e.first.first) it.first.second - shrink else it.first.second
edits[index] = (a to b) to it.second
}
}
} while (needsRerun)
lprintln("debug,kotlin", "final string is:\n$rewritten")
}
return rewritten
}
override fun findLastImport(s: String): Int
override fun findLastImport(s: String): Int {
if (s.contains("import ")) {
val m = s.findLastAnyOf(listOf("import "))!!.first
val m2 = s.findAnyOf(listOf("\n"), startIndex = m)
if (m2 == null) return s.length
else return m2.first + 1
} else return 0
}
override fun parseAndRewrite(s: String, rules: List<(PsiElement, String) -> String>): String {
override fun parseAndRewrite(s: String, rules: List<(PsiElement, String) -> String>): String {
return s;
}
override var execNumber = 0
override suspend fun exec
body — 48 lines, from line 572
override suspend fun exec(
s: String,
onCompileError: (List<ScriptDiagnostic>) -> Unit,
onEvalError: (List<ScriptDiagnostic>) -> Unit,
onEvalException: (Throwable) -> Unit,
implicits: List<Any>,
): ResultValue? {
return when (val r = compile(s)) {
is ResultWithDiagnostics.Success -> {
when (val r2 = evaluate(r.value, implicits = implicits)) {
is ResultWithDiagnostics.Success -> {
when (val e = r2.value.get().result) {
is ResultValue.Error -> {
lprintln("error,kotlin", "Evaluation Error ${e.error} / ${e.error.javaClass}")
onEvalException(translateKotlinEvalException(e.error, s))
e
}
is ResultValue.NotEvaluated -> {
lprintln("warning,kotlin", "not evaluated for ${r2.reports}")
onEvalError(r2.reports)
e
}
else -> {
execNumber++
e
}
}
}
is ResultWithDiagnostics.Failure -> {
lprintln("error,kotlin", "evaluator error ${r2}")
onEvalError(r2.reports)
null
}
}
}
is ResultWithDiagnostics.Failure -> {
lprintln("error,kotlin", "compiler error ${r}")
onCompileError(r.reports)
null
}
}
}
override fun execAndWait
override fun execAndWait(
s: String,
onCompileError: (List<ScriptDiagnostic>) -> Unit,
onEvalError: (List<ScriptDiagnostic>) -> Unit,
onEvalException: (Throwable) -> Unit,
implicits: List<Any>
): ResultValue? {
return runBlocking {
exec(s, onCompileError, onEvalError, onEvalException, implicits)
}
}
inner class CompiledSnippet internal constructor
K1 implementation of PreparedSnippet; see that interface for the evaluation contract.
inner class CompiledSnippet internal constructor(
private val compiled: LinkedSnippet<KJvmCompiledScript>,
private val defaultImplicits: List<Any>
) : PreparedSnippet {
override suspend fun eval
override suspend fun eval(
updateContext: Boolean,
implicits: List<Any>,
onEvalError: (List<ScriptDiagnostic>) -> Unit,
onEvalException: (Throwable) -> Unit
): ResultValue? {
@Suppress("NAME_SHADOWING")
val implicits = implicits.ifEmpty { defaultImplicits }
return when (val r = evaluate(compiled, updateContext, implicits)) {
is ResultWithDiagnostics.Success -> {
when (val e = r.value.get().result) {
is ResultValue.Error -> {
lprintln("error,kotlin", "Evaluation Error ${e.error} / ${e.error.javaClass}")
onEvalException(translateKotlinEvalException(e.error))
e
}
is ResultValue.NotEvaluated -> {
lprintln("warning,kotlin", "not evaluated for ${r.reports}")
onEvalError(r.reports)
e
}
else -> {
execNumber++
e
}
}
}
is ResultWithDiagnostics.Failure -> {
lprintln("error,kotlin", "evaluator error ${r}")
onEvalError(r.reports)
null
}
}
}
override fun evalAndWait
override fun evalAndWait(
updateContext: Boolean,
implicits: List<Any>,
onEvalError: (List<ScriptDiagnostic>) -> Unit,
onEvalException: (Throwable) -> Unit
): ResultValue? {
return runBlocking {
eval(updateContext, implicits, onEvalError, onEvalException)
}
}
}
override suspend fun compileSnippet
Compile a K1 CompiledSnippet; the shared contract is on the overridden interface method.
override suspend fun compileSnippet(
s: String,
implicits: List<Any>,
onCompileError: (List<ScriptDiagnostic>) -> Unit
): CompiledSnippet? {
return when (val r = compile(s)) {
is ResultWithDiagnostics.Success -> {
CompiledSnippet(r.value, implicits)
}
is ResultWithDiagnostics.Failure -> {
lprintln("error,kotlin", "compiler error ${r}")
onCompileError(r.reports)
null
}
}
}
override fun compileSnippetAndWait
Blocking wrapper around compileSnippet.
override fun compileSnippetAndWait(
s: String,
implicits: List<Any>,
onCompileError: (List<ScriptDiagnostic>) -> Unit
): CompiledSnippet? {
return runBlocking {
compileSnippet(s, implicits, onCompileError)
}
}
companion object
companion object {
var sharedClassLoader: ClassLoader? = null
var compilationThread
multi-threaded compilation broken in current kotlin compiler.
Daemon-threaded so this shared compile dispatcher never keeps a JVM alive
(it leaked as a non-daemon pool-N thread in headless smoke tests).
var compilationThread = Executors.newFixedThreadPool(1) { runnable ->
Thread(runnable, "afield-ki-compile").apply { isDaemon = true }
}.asCoroutineDispatcher()
fun functionSignature(function: KFunction<*>)
Delegates to shared top-level functions for backwards compatibility
fun functionSignature(function: KFunction<*>) = afield.runtime.kotlin.functionSignature(function)
fun shortenType(name: String) = afield.runtime.kotlin.shortenType(name)
val CompletionTokenBounds
Keep CompletionTokenBounds as a typealias-like accessor for backwards compatibility
@Suppress("unused")
val CompletionTokenBounds = object {
operator fun invoke(start: Int, end: Int) = afield.runtime.kotlin.CompletionTokenBounds(start, end)
}
fun getTokenBounds(buf: String, cursor: Int) = afield.runtime.kotlin.getTokenBounds(buf, cursor)
fun getResult
fun getResult(
code: String, cursor: Int, completions: List<SourceCodeCompletionVariant>
) = getCompletionResult(code, cursor, completions)
}
class ContextUpdater
class ContextUpdater(
val context: KotlinContext,
private val evaluator: BasicJvmReplEvaluator,
private val ki: KotlinInterface
) {
private var lastProcessedSnippet: LinkedSnippet<KJvmEvaluatedSnippet>? = null
Track exported declarations by alias for idempotent preamble updates
private val exportedDeclarations = mutableMapOf<String, String>()
fun update()
body — 320 lines, from line 758
fun update() {
try {
var lastSnippet = evaluator.lastEvaluatedSnippet
val newSnippets = mutableListOf<Any>()
while (lastSnippet != lastProcessedSnippet && lastSnippet != null) {
val line = lastSnippet.get().result.scriptInstance
if (line != null) newSnippets.add(line)
lastSnippet = lastSnippet.previous
}
newSnippets.reverse()
refreshVariables(newSnippets)
refreshMethods(newSnippets)
if (newSnippets.isNotEmpty()) {
context.generation++
}
lastProcessedSnippet = evaluator.lastEvaluatedSnippet
} catch (e: ReflectiveOperationException) {
lprintln("error,kotlin", "Exception updating current variables $e")
} catch (e: NullPointerException) {
lprintln("error,kotlin", "Exception updating current variables $e")
}
}
private fun refreshMethods(lines: List<Any>) {
for (line in lines) {
val methods = line.javaClass.methods
for (method in methods) {
if (objectMethods.contains(method) || method.name == "main") {
continue
}
val function = method.kotlinFunction ?: continue
context.functions[function.name] = KotlinFunctionInfo(function, line)
val exportTargets = exportTargetsForFunction(method)
if (exportTargets.isNotEmpty()) {
processExportedFunction(function, line, exportTargets)
}
}
}
}
private enum class ExportTarget {
Local,
Root,
Metaroot
}
private fun exportTargetsForFunction(method: java.lang.reflect.Method): Set<ExportTarget> {
val targets = linkedSetOf<ExportTarget>()
if (method.isAnnotationPresent(exported::class.java)) targets += ExportTarget.Local
if (method.isAnnotationPresent(rootExported::class.java)) targets += ExportTarget.Root
if (method.isAnnotationPresent(metarootExported::class.java)) targets += ExportTarget.Metaroot
return targets.withLocalExport()
}
private fun exportTargetsForProperty(field: Field?, descriptor: KProperty<*>): Set<ExportTarget> {
val targets = linkedSetOf<ExportTarget>()
val annotations = descriptor.annotations + descriptor.getter.annotations
if (field?.isAnnotationPresent(exported::class.java) == true ||
annotations.any { it.annotationClass == exported::class }
) targets += ExportTarget.Local
if (field?.isAnnotationPresent(rootExported::class.java) == true ||
annotations.any { it.annotationClass == rootExported::class }
) targets += ExportTarget.Root
if (field?.isAnnotationPresent(metarootExported::class.java) == true ||
annotations.any { it.annotationClass == metarootExported::class }
) targets += ExportTarget.Metaroot
return targets.withLocalExport()
}
private fun Set<ExportTarget>.withLocalExport(): Set<ExportTarget> =
if (isEmpty() || contains(ExportTarget.Local)) this else this + ExportTarget.Local
private fun boxesForExportTargets(targets: Set<ExportTarget>): List<Box> {
val box = ki.associatedBox as? Box ?: return emptyList()
return targets.mapNotNull {
when (it) {
ExportTarget.Local -> box
ExportTarget.Root -> box.root
ExportTarget.Metaroot -> metaroot
}
}.distinctBy { it.id }
}
private fun processExportedFunction(function: KFunction<*>, line: Any, targets: Set<ExportTarget>) {
try {
val exportedName = exportedAlias(function.name)
val declaration = generateFunctionDeclaration(function, exportedName)
boxesForExportTargets(targets).forEach { box ->
try {
val functionReference = createBoundFunctionReference(function, line)
box.ki.context.functions[function.name] = KotlinFunctionInfo(function, line)
box.setPropertyRaw(exportedName, functionReference, doResync = false)
upsertExportedDeclaration(box, exportedName, declaration)
} catch (e: Exception) {
lprintln("error,kotlin", "Failed to export @exported function '${function.name}': ${e.message}")
}
}
} catch (e: Exception) {
lprintln("error,kotlin", "Failed to process @exported function: ${e.message}")
}
}
private fun generateFunctionDeclaration(function: KFunction<*>, exportedName: String): String {
// Generate a property declaration with function type
// e.g., "val Box._myFunc by InheritanceFromContext<(Int, String) -> Double>()"
val params = function.parameters.filter { it.kind != kotlin.reflect.KParameter.Kind.INSTANCE }
val paramTypes = params.joinToString(", ") { param ->
ExportedTypeNames.render(param.type, ki.classLoader)
}
val returnType = ExportedTypeNames.render(function.returnType, ki.classLoader)
val functionType = if (params.isEmpty()) {
"() -> $returnType"
} else {
"($paramTypes) -> $returnType"
}
// Add the inherit marker /**/ at the end
return "val Box.$exportedName by InheritanceFromContext<$functionType>() /*from:self*/ /**/"
}
private fun processExportedProperty(
name: String,
descriptor: KProperty<*>,
value: Any?,
line: Any,
targets: Set<ExportTarget>
) {
try {
val exportedName = exportedAlias(name)
val declaration = generatePropertyDeclaration(descriptor, exportedName)
boxesForExportTargets(targets).forEach { box ->
try {
box.ki.context.vars[name] = KotlinVariableInfo(value, descriptor, line)
box.ki.context.exportedVariables += name
if (value != null) {
box.setPropertyRaw(exportedName, value, doResync = true)
}
upsertExportedDeclaration(box, exportedName, declaration)
} catch (e: Exception) {
lprintln("error,kotlin", "Failed to export @exported property '$name': ${e.message}")
}
}
} catch (e: Exception) {
lprintln("error,kotlin", "Failed to process @exported property '$name': ${e.message}")
}
}
private fun generatePropertyDeclaration(property: KProperty<*>, exportedName: String): String {
val keyword = if (property is KMutableProperty<*>) "var" else "val"
val typeName = ExportedTypeNames.render(property.returnType, ki.classLoader)
return "$keyword Box.$exportedName by InheritanceFromContext<$typeName>() /*from:self*/ /**/"
}
private fun exportedAlias(name: String): String {
return if (name.startsWith("_")) name else "_$name"
}
private fun upsertExportedDeclaration(box: HasProperties, exportedName: String, declaration: String) {
val currentPreamble = box.getPropertyRaw("preamble", "") as? String ?: ""
val currentLines = currentPreamble
.lines()
.map { it.trimEnd() }
.filter { it.isNotBlank() }
val filteredLines = currentLines.filterNot {
isExportedDeclarationLineFor(it, exportedName)
}
val nextLines = filteredLines + declaration
val newPreamble = nextLines.joinToString("\n")
val previous = exportedDeclarations[exportedName]
if (newPreamble != currentPreamble || previous != declaration) {
box.setPropertyRaw("preamble", newPreamble, doResync = true)
exportedDeclarations[exportedName] = declaration
lprintln("info,kotlin", "Added @exported declaration to preamble: $declaration")
}
}
private fun isExportedDeclarationLineFor(line: String, exportedName: String): Boolean {
val withoutPrefixComment = line.trim()
.replaceFirst(Regex("""^\s*/\*.*?\*/\s*"""), "")
.trim()
if (!withoutPrefixComment.endsWith("/**/")) return false
val nameEscaped = Regex.escape(exportedName)
val patterns = listOf(
// Current form: val/var Box._name by ...
Regex("""^(val|var)\s+Box\.$nameEscaped\b.*\bby\b.*$"""),
// Legacy form: val/var _name by ...
Regex("""^(val|var)\s+$nameEscaped\b.*\bby\b.*$"""),
// Legacy typed forms: val/var Box._name : ...
Regex("""^(val|var)\s+Box\.$nameEscaped\b\s*:\s*.*$"""),
// Legacy typed forms: val/var _name : ...
Regex("""^(val|var)\s+$nameEscaped\b\s*:\s*.*$""")
)
return patterns.any { it.matches(withoutPrefixComment) }
}
private fun createBoundFunctionReference(function: KFunction<*>, line: Any): Any {
val arity = function.parameters.count { it.kind != KParameter.Kind.INSTANCE }
val functionInterface = when (arity) {
0 -> Function0::class.java
1 -> Function1::class.java
2 -> Function2::class.java
3 -> Function3::class.java
4 -> Function4::class.java
5 -> Function5::class.java
6 -> Function6::class.java
7 -> Function7::class.java
8 -> Function8::class.java
9 -> Function9::class.java
10 -> Function10::class.java
11 -> Function11::class.java
12 -> Function12::class.java
13 -> Function13::class.java
14 -> Function14::class.java
15 -> Function15::class.java
16 -> Function16::class.java
17 -> Function17::class.java
18 -> Function18::class.java
19 -> Function19::class.java
20 -> Function20::class.java
21 -> Function21::class.java
22 -> Function22::class.java
else -> throw IllegalArgumentException("Cannot export function '${function.name}': arity $arity is not supported")
}
return Proxy.newProxyInstance(
line.javaClass.classLoader,
arrayOf(functionInterface)
) { proxy, method, args ->
when (method.name) {
"invoke" -> {
val values = args?.toList() ?: emptyList()
val callArgs = ArrayList<Any?>(function.parameters.size)
var valueIndex = 0
for (parameter in function.parameters) {
when (parameter.kind) {
KParameter.Kind.INSTANCE -> callArgs.add(line)
else -> {
if (valueIndex >= values.size) {
throw IllegalArgumentException(
"Missing argument ${valueIndex + 1} for @exported function '${function.name}'"
)
}
callArgs.add(values[valueIndex++])
}
}
}
function.call(*callArgs.toTypedArray())
}
"toString" -> functionSignature(function)
"hashCode" -> System.identityHashCode(proxy)
"equals" -> proxy === args?.getOrNull(0)
else -> throw UnsupportedOperationException(
"Unsupported proxy method '${method.name}' for @exported function '${function.name}'"
)
}
}
}
@Throws(ReflectiveOperationException::class)
private fun refreshVariables(lines: List<Any>) {
for (line in lines) {
val seen = mutableSetOf<String>()
val fields = line.javaClass.declaredFields
findVariables(fields, line, seen)
findKotlinProperties(line, seen)
}
}
@Throws(IllegalAccessException::class)
private fun findVariables(fields: Array<Field>, o: Any, seen: MutableSet<String>) {
for (field in fields) {
val fieldName = field.name
if (fieldName.contains("$\$implicitReceiver") || fieldName.contains("script$") || fieldName.endsWith("\$delegate")) {
continue
}
field.isAccessible = true
val value = field.get(o)
val descriptor = field.kotlinProperty
if (descriptor != null) {
context.vars[descriptor.name] = KotlinVariableInfo(value, descriptor, o)
seen += descriptor.name
val exportTargets = exportTargetsForProperty(field, descriptor)
if (exportTargets.isNotEmpty()) {
processExportedProperty(descriptor.name, descriptor, value, o, exportTargets)
}
}
}
}
private fun findKotlinProperties(o: Any, seen: MutableSet<String>) {
for (descriptor in o::class.memberProperties) {
val name = descriptor.name
if (name in seen || name.contains("$\$implicitReceiver") || name.contains("script$")) continue
if (descriptor.parameters.size != 1) continue
val value = runCatching {
descriptor.isAccessible = true
descriptor.getter.call(o)
}.getOrNull() ?: continue
context.vars[name] = KotlinVariableInfo(value, descriptor, o)
seen += name
val exportTargets = exportTargetsForProperty(null, descriptor)
if (exportTargets.isNotEmpty()) {
processExportedProperty(name, descriptor, value, o, exportTargets)
}
}
}
companion object
companion object {
private val objectMethods = HashSet(listOf(*Any::class.java.methods))
}
}
}