KotlinJs.kt
src/jvmMain/kotlin/afield/lang/remotejs/KotlinJs.kt · 531 lines
package and imports — 89 lines, from line 1
package afield.lang.remotejs
import afield.ai.agent.*
import afield.ai.conversation.*
import afield.ai.tools.*
import afield.animation.gesture.*
import afield.animation.interpolation.*
import afield.animation.keyframes.*
import afield.app.diagnostics.*
import afield.app.notifications.*
import afield.app.protocol.*
import afield.app.server.*
import afield.app.settings.*
import afield.audio.synth.*
import afield.data.table.*
import afield.document.context.*
import afield.document.graph.*
import afield.document.history.*
import afield.document.persistence.*
import afield.document.properties.*
import afield.document.workspace.*
import afield.graphics.color.*
import afield.graphics.paint.*
import afield.graphics.vulkan.*
import afield.math.functions.*
import afield.media.capture.*
import afield.media.stream.*
import afield.media.video.*
import afield.runtime.completion.*
import afield.runtime.execution.*
import afield.runtime.kotlin.*
import afield.runtime.lifecycle.*
import afield.tools.automation.*
import afield.tools.docs.*
import afield.ui.controls.*
import afield.ui.layout.*
import afield.ui.notation.*
import afield.ui.output.*
import afield.ui.output.templates.*
import afield.ui.slides.*
import afield.ui.visualization.*
import afield.app.diagnostics.logging.lprintln
import afield.runtime.MainLanguageRuntime
import afield.runtime.TopologyWatcher
import afield.runtime.applyRuntimeToggle
import afield.runtime.isMainLanguage
import afield.runtime.registerMainLanguageRuntime
import afield.ai.learning.*
import afield.ai.retrieval.*
import afield.animation.motion.*
import afield.app.commands.*
import afield.audio.analysis.*
import afield.data.collections.*
import afield.data.formats.*
import afield.data.graph.*
import afield.data.sorting.*
import afield.geometry.mesh.*
import afield.geometry.path.*
import afield.geometry.spatial.*
import afield.graphics.interaction.*
import afield.graphics.render.*
import afield.graphics.shader.*
import afield.graphics.text.*
import afield.graphics.vulkan.buffers.*
import afield.io.osc.*
import afield.io.process.*
import afield.lang.lambda.*
import afield.lang.python.*
import afield.lang.remotejs.*
import afield.lang.stacksl.*
import afield.math.autodiff.*
import afield.math.control.*
import afield.math.estimation.*
import afield.math.linalg.*
import afield.math.optimization.*
import afield.math.statistics.*
import afield.math.units.*
import afield.media.image.*
import afield.runtime.languages.*
import afield.simulation.agents.*
import afield.simulation.automata.*
import afield.simulation.particles.*
import afield.study.build.*
import afield.study.evidence.*
import afield.study.search.*
import afield.study.source.*
import afield.study.ui.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
SPIKE — a "Kotlin in the head" main language. A box flagged mainLanguage = "kotlinjs" (or its
legacy isKotlinJsRoot root flag) compiles its Kotlin source to JavaScript via
KotlinToJsCompiler (PLAIN_DCE: self-contained, tree-shaken, eval-able) and ships it to the
connected browser / visionOS head over the same WebSocket transport RemoteJS uses.
This reuses RemoteJS's transport wholesale (compileAndSendKotlin → executeRemoteJS with
doASTA = false); it only swaps what runs the box (Kotlin→JS compile) for the JS path. Kotlin
gets native operator overloading (no ASTA), real types, and compile-time diagnostics at the
source line. See dev_docs/kotlin_to_js_spike.md.
var Box.isKotlinJsRoot by Delegation<Boolean?>()
Kotlin→JS compiles are CPU-heavy (~0.6–1.2s); keep them off the main runloop.
private val kotlinJsCompilePool = Executors.newSingleThreadExecutor { r ->
Thread(r, "kotlinjs-compile").apply { isDaemon = true }
}
Completion hits the out-of-process AA sidecar over HTTP; keep it off the runloop and separate from the compile pool so completion never queues behind a compile.
private val kotlinJsCompletionPool = Executors.newFixedThreadPool(2) { r ->
Thread(r, "kotlinjs-complete").apply { isDaemon = true }
}
fun kotlinJsTopologyNeedsUpdating(values: Collection<Box>)
fun kotlinJsTopologyNeedsUpdating(values: Collection<Box>) {
applyRuntimeToggle(
boxes = values,
isRoot = { it.isKotlinJsRoot == true || it.isMainLanguage("kotlinjs") },
runtimeClass = KotlinJsExecution::class,
newExecution = { _ -> KotlinJsExecution() },
// code_language: drives the editor's Monaco highlighter. Lowercase "kotlinjs" matches the
// registered Monaco language (kotlinjs.js) across all editor resolution paths (one lowercases
// it, others use it raw) — reuses Kotlin highlighting without the JVM-Kotlin providers.
language = "kotlinjs",
customization = "",
notifyCategory = "kotlinjs",
)
}
fun installKotlinJs()
@RunsAt(plugins)
fun installKotlinJs() {
TopologyWatcher("kotlinjs") { kotlinJsTopologyNeedsUpdating(it) }.start()
}
fun installKotlinJsMainLanguage()
@RunsAt(early)
fun installKotlinJsMainLanguage() {
Box.ioPersistentProperties.add("isKotlinJsRoot")
registerMainLanguageRuntime(
MainLanguageRuntime(
id = "kotlinjs",
label = "Kotlin → head",
runtimeClass = KotlinJsExecution::class,
setLegacyRoot = { box, enabled -> box.isKotlinJsRoot = enabled },
applyTopology = ::kotlinJsTopologyNeedsUpdating,
),
)
}
class KotlinJsExecution : BoxExecution
class KotlinJsExecution : BoxExecution {
override val languageName: String = "KotlinJS"
private val launches = ConcurrentHashMap<String, KotlinJsLaunchTracker>()
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit) =
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit) =
runKotlin(inside, execute, launchable = false, code = execute.code, strand = "__fragment__", reply = reply)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit) =
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit) =
runKotlin(inside, execute, launchable = false, code = execute.code, strand = "__fragment__", reply = reply)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit) =
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit) =
runKotlin(
inside,
execute,
launchable = true,
code = code,
strand = strand.ifBlank { "default" },
reply = reply,
)
override fun stop(inside: Box)
override fun stop(inside: Box) {
val active = launches.entries.filter { it.value.box === inside }
if (active.isNotEmpty()) {
active.groupBy { it.value.selection.profile }.forEach { (profile, entries) ->
entries.forEach { (key, tracker) ->
launches.remove(key, tracker)
tracker.cancel()
}
profile.stop(
inside,
entries.first().value.selection,
entries.map { it.value.lifecycleName },
)
}
return
}
val selection = try {
KotlinJsHeadProfiles.resolveForBox(inside, "")
} catch (e: IllegalArgumentException) {
inside.output.errorProxy(e.message ?: "Invalid Kotlin/JS head marker", "code", 0)
return
}
if (selection.profile.connectedHeadCount() == 0 && selection.profile != KotlinJsHeadProfiles.visionMetal)
notify("Warning! no connected ${selection.profile.id} heads", "kotlinjs")
selection.profile.stop(inside, selection, listOf(kotlinJsLifecycleName(inside, "default")))
}
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit) {
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit) {
val exec = Execute(
uid = executeSymbol.uid, type = "fragment", line = executeSymbol.line,
code = "${executeSymbol.symbol}()", singleLine = true,
)
executeFragment(inside, exec, reply)
}
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit) {
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit) {
val pos = completionRequest.position
fun empty() = Completion(boundsStart = pos, boundsEnd = pos, metadata = emptyList())
// This is called on the Netty event loop serving the browser's websocket (Server.kt launches
// every handler in the /ws session's scope), so nothing here runs before the hand-off to the
// pool. Every path replies, a throw included, so the popup never waits out its timeout. The
// pool, not Dispatchers.IO, because it bounds how many completions sit in the sidecar at once
// while the user types, and its threads show up by name in a thread dump.
kotlinJsCompletionPool.execute {
val completion = try {
completeOffLoop(inside, completionRequest) ?: empty()
} catch (t: Throwable) {
lprintln("warning,kotlinjs", "completion failed: ${t.javaClass.simpleName}: ${t.message.orEmpty()}")
empty()
}
reply(Message("complete").payload(completion))
}
}
The completion for completionRequest, or null when there is nothing to offer. Blocks for up
to four sidecar round trips, so it runs on kotlinJsCompletionPool.
source — 193 lines, from line 229
private fun completeOffLoop(inside: Box, completionRequest: CompletionRequest): Completion? {
val pos = completionRequest.position
val selection = try {
KotlinJsHeadProfiles.resolveForBox(inside, completionRequest.allCode)
} catch (e: IllegalArgumentException) {
notify(e.message ?: "Invalid Kotlin/JS head marker", "kotlinjs_error")
return null
}
// Stage 1: reuse the rich K2 Analysis-API completion, but scope the analysis classpath to the
// Kotlin stdlib only (KotlinJsExecution.kotlinStdlibRoots). The box has no JVM classpath — its
// symbol universe is stdlib + its own declarations (present in `allCode`) + the selected head
// profile's JVM-analysis-safe declaration model + keywords. Profile declarations stay at file
// scope so nested types and member/callback inference work; their public callables are also
// represented as local stubs for discoverability.
val stdlibRoots = AaCompletionClient.kotlinStdlibRoots()
if (stdlibRoots.isEmpty()) return null
// The sidecar builds a fresh Analysis-API session per distinct classpath and answers
// "warming up" (empty) on the first call(s) for a cold stdlib session. Retry a few
// times so a user's first completion in a session isn't silently empty.
var success: afield.runtime.kotlin.CompletionResult.Success? = null
var lastDetail = ""
repeat(4) { attempt ->
if (success != null) return@repeat
val outcome = AaCompletionClient.complete(
snippet = completionRequest.allCode,
cursor = pos,
preamble = "",
declarations = selection.profile.completionDeclarations,
implicitReceivers = emptyList(),
classpathRootsOverride = stdlibRoots,
jsTarget = true,
)
lastDetail = outcome.detail
success = outcome.result as? afield.runtime.kotlin.CompletionResult.Success
if (success == null && attempt < 3) Thread.sleep(300)
}
val built = success
if (built == null) {
lprintln("info,kotlinjs", "completion backend=AA(stdlib) → empty ($lastDetail)")
return null
}
return Completion(
boundsStart = built.bounds.start,
boundsEnd = built.bounds.end,
metadata = built.metadata.map {
Metadata(
icon = it.icon,
tail = it.tail,
text = it.text,
displayText = it.displayText,
documentation = "",
)
},
)
}
private fun runKotlin(
inside: Box,
execute: Execute,
launchable: Boolean,
code: String,
strand: String,
reply: (Message) -> Unit,
) {
// The receipt every engine owes an awaited run (ExecutionVerdict.kt). Before 2026-10-24 a
// Kotlin→JS run published none, so every backdoor `run` of one timed out, errors included.
val trace = ExecutionTrace.forEngine(inside, execute, if (launchable) "launch" else "run", reply)
configureOutputProxies(inside, trace.reply)
val settle = KotlinJsReceipt(trace)
val selection = try {
KotlinJsHeadProfiles.resolveForBox(inside, code)
} catch (e: IllegalArgumentException) {
inside.output.errorProxy(e.message ?: "Invalid Kotlin/JS head marker", "code", 0)
settle.finish(false, e.message ?: "Invalid Kotlin/JS head marker")
return
}
val connected = selection.profile.connectedHeadCount()
if (connected == 0 && selection.profile != KotlinJsHeadProfiles.visionMetal) {
// Nothing would run it; this used to compile, warn in a toast and report success.
notify("Warning! no connected ${selection.profile.id} heads", "kotlinjs")
inside.output.errorProxy(RemoteJsBoxSurface.NO_HEAD, "code", -1)
settle.finish(false, RemoteJsBoxSurface.NO_HEAD)
return
} else if (connected == 0)
notify("compiling Kotlin → ${selection.profile.id}; program will replay when the head connects", "kotlinjs")
else
notify("compiling Kotlin → ${selection.profile.id} for $connected head(s)", "kotlinjs")
if (!launchable) {
runFragment(inside, execute, code, selection, settle)
return
}
val lifecycleName = kotlinJsLifecycleName(inside, strand)
val key = "${inside.id}\u0000$strand"
launches.remove(key)?.let { previous ->
previous.cancel()
previous.selection.profile.stop(inside, previous.selection, listOf(previous.lifecycleName))
}
val generation = synchronized(inside) {
val next = (inside.runGeneration[strand] ?: -1) + 1
inside.runGeneration[strand] = next
inside.currentRunningStates[strand] = RunningState(next, "launch")
next
}
send(Message("is-running").payload(IsRunning(execute.uid, true, generation, strand, "launch")))
lateinit var tracker: KotlinJsLaunchTracker
tracker = KotlinJsLaunchTracker(
box = inside,
strand = strand,
generation = generation,
lifecycleName = lifecycleName,
selection = selection,
notifyStopped = {
send(Message("is-running").payload(IsRunning(execute.uid, false, generation, strand, "launch")))
},
onFinished = { launches.remove(key, tracker) },
)
launches[key] = tracker
kotlinJsCompilePool.submit {
try {
val sent = compileAndSendKotlin(
inside,
code,
launchable = true,
selectedHead = selection,
lifecycleName = lifecycleName,
onResponse = { response -> settle.observe(response, launch = true); tracker.onResponse(response) },
sendGate = tracker::sendIfActive,
)
if (!sent) settle.finish(false, "Kotlin → JS did not compile, or the launch was replaced")
else if (selection.profile == KotlinJsHeadProfiles.visionMetal && connected == 0) settle.finish(true)
tracker.deliveryFinished(
sent = sent,
durableWithoutResponse = false,
)
} catch (e: Exception) {
lprintln("error,kotlinjs", e.stackTraceToString())
notify("Kotlin → JS failed: ${e.message}", "kotlinjs_error")
inside.output.errorProxy(e.message ?: "compile error", "code", 0)
settle.finish(false, e.message ?: "compile error")
tracker.cancel()
}
}
}
private fun runFragment(
inside: Box,
execute: Execute,
code: String,
selection: KotlinJsHeadSelection,
settle: KotlinJsReceipt,
) {
val watermark = ++inside.fragmentGeneration
send(Message("is-running").payload(IsRunning(execute.uid, true, watermark, "__fragment__")))
val boxSource = inside.getPropertyRaw(execute.property) as? String
val fragmentContext = boxSource?.takeIf(String::isNotBlank)?.let {
val firstLine = execute.line.coerceAtLeast(0) + 1
KotlinJsFragmentContext(
boxSource = it,
activeUserLines = firstLine..(firstLine + code.count { c -> c == '\n' }),
)
}
kotlinJsCompilePool.submit {
try {
val sent = compileAndSendKotlin(
inside,
code,
launchable = false,
selectedHead = selection,
fragmentContext = fragmentContext,
onResponse = { response -> settle.observe(response, launch = false); response.kind != "ERROR" && response.kind != "RUN-STOP" },
)
if (!sent) settle.finish(false, "Kotlin → JS did not compile")
else if (selection.profile == KotlinJsHeadProfiles.visionMetal) settle.finish(true)
} catch (e: Exception) {
lprintln("error,kotlinjs", e.stackTraceToString())
notify("Kotlin → JS failed: ${e.message}", "kotlinjs_error")
inside.output.errorProxy(e.message ?: "compile error", "code", 0)
settle.finish(false, e.message ?: "compile error")
} finally {
send(Message("is-running").payload(IsRunning(execute.uid, false, watermark, "__fragment__")))
}
}
}
}
internal class KotlinJsReceipt(private val trace: ExecutionTrace)
Finishes a Kotlin→JS run's ExecutionTrace once, from the first head reply that ends it: the module's own terminal SUCCESS (or, for a Launch, its RUN-START), or an ERROR.
internal class KotlinJsReceipt(private val trace: ExecutionTrace) {
private var done = false
fun observe(response: KotlinJsHeadResponse, launch: Boolean)
fun observe(response: KotlinJsHeadResponse, launch: Boolean) {
when {
response.kind == "ERROR" -> finish(false)
response.kind == "SUCCESS" && response.terminal -> finish(true)
launch && response.kind == "RUN-START" -> finish(true)
}
}
fun finish(success: Boolean, reason: String = "")
@Synchronized
fun finish(success: Boolean, reason: String = "") {
if (done) return
done = true
trace.finish(success, reason)
}
}
internal class KotlinJsLaunchTracker
internal class KotlinJsLaunchTracker(
val box: Box,
private val strand: String,
private val generation: Int,
val lifecycleName: String,
val selection: KotlinJsHeadSelection,
private val notifyStopped: () -> Unit,
private val onFinished: () -> Unit,
) {
private val expectedHeads = mutableSetOf<String>()
private val activeHeads = mutableSetOf<String>()
private val terminalHeads = mutableSetOf<String>()
private var finished = false
fun onResponse(response: KotlinJsHeadResponse): Boolean
@Synchronized
fun onResponse(response: KotlinJsHeadResponse): Boolean {
if (finished || !isCurrent()) return false
expectedHeads += response.expectedHeadIds
expectedHeads += response.headId
when (response.kind) {
"RUN-START" -> {
activeHeads += response.headId
terminalHeads -= response.headId
}
"RUN-STOP", "ERROR" -> {
activeHeads -= response.headId
terminalHeads += response.headId
}
"SUCCESS" -> if (response.terminal && response.headId !in activeHeads) {
terminalHeads += response.headId
}
}
if (activeHeads.isEmpty() && expectedHeads.isNotEmpty() && terminalHeads.containsAll(expectedHeads)) {
finish()
}
return !finished
}
fun deliveryFinished(sent: Boolean, durableWithoutResponse: Boolean)
@Synchronized
fun deliveryFinished(sent: Boolean, durableWithoutResponse: Boolean) {
if (finished || !isCurrent()) return
if (!sent) {
finish()
} else if (durableWithoutResponse) {
activeHeads += "durable-head"
}
}
fun sendIfActive(send: () -> Boolean): Boolean {
Make the final active-generation check and publication one atomic operation with respect to cancel. If stop wins, the compiled module is discarded. If publication wins, stop blocks until publication returns and then sends the matching profile stop, so a launch cannot appear remotely after its stop has already passed.
@Synchronized
fun sendIfActive(send: () -> Boolean): Boolean {
if (finished || !isCurrent()) return false
return send()
}
fun cancel()
@Synchronized
fun cancel() {
if (!finished) finish()
}
private fun isCurrent(): Boolean = synchronized(box) {
box.runGeneration[strand] == generation &&
box.currentRunningStates[strand]?.watermark == generation
}
private fun finish() {
if (finished) return
finished = true
synchronized(box) {
if (box.currentRunningStates[strand]?.watermark == generation) {
box.currentRunningStates.remove(strand)
notifyStopped()
}
if (box.runGeneration[strand] == generation) {
box.runGeneration[strand] = generation + 1
}
}
onFinished()
}
}