ClojureInterface.kt
src/jvmMain/kotlin/afield/lang/clojure/ClojureInterface.kt · 1496 lines
package and imports — 64 lines, from line 1
@file:Suppress("UNCHECKED_CAST")
package afield.lang.clojure
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.document.graph.Box
import afield.app.diagnostics.logging.lprintln
import afield.runtime.TopologyWatcher
import afield.runtime.MainLanguageRuntime
import afield.runtime.applyRuntimeToggle
import afield.runtime.isMainLanguage
import afield.runtime.registerMainLanguageRuntime
import afield.runtime.lifecycle.Frames
import afield.runtime.lifecycle.eachFrame
import afield.geometry.path.FLine
import afield.ui.visualization.lines
import afield.document.properties.Delegation
import afield.graphics.render.color
import afield.graphics.render.thickness
import afield.math.linalg.vec
import clojure.java.api.Clojure
import clojure.lang.*
import java.io.PrintWriter
import java.io.Writer
import java.util.concurrent.ConcurrentHashMap
import kotlin.concurrent.thread
Clojure as a Field main language: a box whose source is Clojure, evaluated by the real Clojure runtime on this JVM.
Nothing here implements a Lisp. clojure.jar is on the classpath and this file is the adapter
between Field's box-execution contract and clojure.java.api.Clojure — which is why the whole
language is one file while afield.data.query.datalog and afield.lang.rectsl, whose compilers Field owns, are
packages.
Read it in four layers:
- ClojureHost is the interpreter, and the only part with no Field in it. It reads one top-level
form at a time and evaluates it, which is
load-string's own loop written out so that the line each form started on is available while that form runs: that line is what lets printed output land beside the form that printed it, the way it does in every other kind of Field box. It returns the value of the last form, andexecuteIfFnwill invoke that value when it is anIFn— a host convenience, off on the box path (ClojureExecution passesfalse), so a box whose last form evaluates to a function prints the function rather than calling it.compileOnlyis the same read loop with evaluation withheld, which is where the editor's syntax errors come from, carrying line and column. - ClojureInterface is the host facade: the box currently running, and the handful of Field
verbs that are awkward to reach by raw interop because they are Kotlin extension functions
with default arguments.
field/core.clj(insrc/jvmMain/resources) is the Clojure side of it,required andreferred into every box namespace, so a box can say(lines ls :fit true)rather than namingSomeSkikoKt. - ClojureExecution is the
BoxExecution, so it is the half that knows about boxes: output proxying,is-running, per-fragment generation counting, andcomplete, whose candidates come from Compliment if that namespace loads. Without it on the classpathcompletereturns an empty list rather than throwing, so a box runs and completes nothing. - installClojure and clojureTopologyNeedsUpdating are the registration, and they are
identical in shape to every other main language: one
registerMainLanguageRuntimecall, oneTopologyWatcherthat swaps a child box's execution when the graph changes.
One namespace, not one per box: every Clojure box in every open document evaluates in live.user
(Clojure namespaces are per-JVM, and ClojureExecution asks for that one name), so two Clojure
boxes are two cells of one notebook. public/docs/clojure.md is the page that says so to a
reader; dev_docs/small_language_anatomy.md is the map of the integration points.
class StreamingWriter
class StreamingWriter(
private val onText: (String) -> Unit
) : Writer() {
private val buf = StringBuilder()
override fun write(cbuf: CharArray, off: Int, len: Int)
override fun write(cbuf: CharArray, off: Int, len: Int) {
buf.append(cbuf, off, len)
// flush on newline to reduce latency in consoles
var i = 0
while (true) {
val idx = buf.indexOf("\n", i)
if (idx == -1) break
onText(buf.substring(0, idx + 1))
buf.delete(0, idx + 1)
i = 0
}
}
override fun flush()
override fun flush() {
if (buf.isNotEmpty()) {
onText(buf.toString())
buf.setLength(0)
}
}
override fun close()
override fun close() {
flush()
}
}
data class CompileError
Result types
data class CompileError(
val message: String, val line: Int? = null, val column: Int? = null, val causeClass: String? = null
)
sealed class CompileOnlyResult
data class Ok(val formsCount: Int) : CompileOnlyResult()
data class Err(val errors: List<CompileError>) : CompileOnlyResult()
data class EvalValue
data class EvalValue(
val value: Any?,
val valueClass: String? = value?.let { it::class.java.name },
/**
* 1-based line, within the evaluated text, of the last top-level form — the one whose value
* this is. `-1` when nothing was evaluated. The box path anchors the echoed value to it so the
* answer appears beside the form that produced it rather than under the whole box.
*/
val line: Int = -1,
/** `pr-str` of [value], so a Clojure box prints `[1 2 3]` and `nil` rather than Java's `toString`. */
val printed: String? = null
)
sealed class EvalResult
data class Ok(val result: EvalValue) : EvalResult()
data class Err(val error: CompileError) : EvalResult()
internal fun clojureCompletionCategory(type: String?): String
Compliment's kind of thing, said in the completion popup's own vocabulary.
Metadata.category is not a label: it is the key CATEGORY_WEIGHT in
src/commonMain/resources/fieldSuggest.js is looked up by, and a name that table does not know
scores 0 — the same as "other". Handing Compliment's own words over unmapped therefore looked
fine, because the popup will title-case anything into a group header (class got a group called
"Class"), while ranking every Clojure candidate as bottom-of-the-list filler. local is the one
that matters most: that table scores it 28, above every other category, on the grounds that a name
the reader bound two lines up is the best answer a popup can give. It was arriving with no weight
at all.
A type this does not know keeps its own spelling rather than collapsing to "other", so a new
Compliment source appears under its own header instead of joining a pile. ClojureHostTest pins
every mapping here against the shipped table, so a rename on either side is a test failure rather
than a silent demotion.
internal fun clojureCompletionCategory(type: String?): String = when (val kind = type?.trim()?.lowercase()) {
null, "" -> ""
"local" -> "local"
"function", "macro", "special-form", "method", "static-method" -> "functions"
"var", "keyword", "field", "static-field" -> "values"
"class", "import" -> "types"
"namespace" -> "imports"
else -> kind
}
Helper to safely call Clojure Vars
private fun varv(ns: String, name: String): Var = Clojure.`var`(ns, name) as Var
private fun <T> withClojureIO(
onStdout: ((String) -> Unit)? = null, onStderr: ((String) -> Unit)? = null, body: () -> T
): T {
// Vars to bind
val outVar: Var = Clojure.`var`("clojure.core", "*out*") as Var
val errVar: Var = Clojure.`var`("clojure.core", "*err*") as Var
val printReadably: Var = Clojure.`var`("clojure.core", "*print-readably*") as Var
val flushOnNewline: Var = Clojure.`var`("clojure.core", "*flush-on-newline*") as Var
// Writers -> PrintWriter (Clojure prints to java.io.Writer via PrintWriter)
val outWriter = onStdout?.let { PrintWriter(StreamingWriter(it), /*autoFlush=*/true) }
val errWriter = onStderr?.let { PrintWriter(StreamingWriter(it), true) }
// Build binding map (only include when provided)
val pairs = mutableListOf<Any>()
if (outWriter != null) {
pairs += outVar; pairs += outWriter
}
if (errWriter != null) {
pairs += errVar; pairs += errWriter
}
// Nice defaults for UIs
pairs += flushOnNewline; pairs += java.lang.Boolean.TRUE
// Usually you want human-friendly printed values
pairs += printReadably; pairs += java.lang.Boolean.TRUE
Var.pushThreadBindings(RT.map(*pairs.toTypedArray()))
return try {
body()
} finally {
try {
outWriter?.flush(); errWriter?.flush()
} catch (_: Throwable) {
}
Var.popThreadBindings()
}
}
internal fun flushClojureIO()
Flush the bound *out* and *err*, so what a form printed is delivered before the next one starts.
internal fun flushClojureIO() {
for (name in listOf("*out*", "*err*")) {
runCatching { ((Clojure.`var`("clojure.core", name) as Var).deref() as? java.io.Writer)?.flush() }
}
}
Extract line/column from Clojure's nested exceptions if present, and the message that says what went wrong.
A CompilerException's own message is only the place — Syntax error compiling at (3:1). — and
the reason (Unable to resolve symbol: x in this context) is its cause's message, which Clojure's
REPL prints on the next line. A box shows one message beside one line, so the reason goes first
and the place follows it.
private fun extractCompilerError(t: Throwable): CompileError {
// clojure.lang.Compiler$CompilerException exposes line/column; fall back to cause chain
var cur: Throwable? = t
var line: Int? = null
var col: Int? = null
var reason: String? = null
while (cur != null) {
val cls = cur.javaClass
if (cls.name.startsWith("clojure.lang.Compiler\$CompilerException")) {
try {
// getLine/getColumn are available on recent versions
val getLine = cls.methods.firstOrNull { it.name == "getLine" && it.parameterCount == 0 }
val getCol = cls.methods.firstOrNull { it.name == "getColumn" && it.parameterCount == 0 }
line = (getLine?.invoke(cur) as? Int) ?: line
col = (getCol?.invoke(cur) as? Int) ?: col
} catch (_: Throwable) { /* ignore */
}
reason = cur.cause?.let { it.message ?: it.toString() }?.takeIf { it.isNotBlank() }
break
}
cur = cur.cause
}
val place = t.message ?: t.toString()
return CompileError(
message = if (reason != null && reason !in place) "$reason — $place" else place,
line = line, column = col, causeClass = t.javaClass.name
)
}
error with line filled in when the exception did not carry one of its own.
Clojure reports a line for anything the reader or the compiler rejected, and nothing for a
runtime throw from inside a function — (/ 1 0) is an ArithmeticException with no position at
all. The line of the top-level form that was running is the best answer available there, and it
is the one a reader of the box wants: it points at the form they ran.
private fun atLine(error: CompileError, line: Int): CompileError =
if (error.line != null || line < 1) error else error.copy(line = line)
class ClojureHost(private val defaultNs: String = "user")
A Clojure evaluator for one namespace: read a form, evaluate it, repeat, report the last value.
The part of the language with no Field in it, so it is unit-testable on its own
(afield.lang.clojure.ClojureHostTest) and knows nothing about boxes. Its one unusual commitment is
that everything it reports — printed text, the last value, an error — carries the line of the
top-level form responsible. See evalPage.
boot loads Compliment if it is there and shrugs if it is not. Namespaces are initialised lazily
and once, with clojure.core and field.core referred in: see initNsIfNeeded.
class ClojureHost(private val defaultNs: String = "user") {
Synchronized: a completion arriving while warmInBackground loads Compliment waits for it.
private val booted = lazy { try { requireNs("compliment.core") } catch (_: Throwable) { /* completions optional */ } }
private fun boot() = booted.value
fun warmInBackground()
Load Compliment off the runloop; its first load is about the popup's 15 s reply timeout.
fun warmInBackground() {
thread(isDaemon = true, name = "clojure-compliment-warm") { boot() }
}
private fun ensureNs(ns: String): Namespace = Namespace.findOrCreate(Symbol.intern(ns))
private fun v(ns: String, name: String): Var = Clojure.`var`(ns, name) as Var
private fun <T> withNs(ns: String, f: () -> T): T {
val nsObj = ensureNs(ns)
Var.pushThreadBindings(RT.map(RT.CURRENT_NS, nsObj)) // dynamic *ns*
return try {
f()
} finally {
Var.popThreadBindings()
}
}
Optional helper: require a namespace by symbol, in the current dynamic ns. Safer than trying to bindRoot on ns.
private fun requireNs(sym: String) {
val require = Clojure.`var`("clojure.core", "require")
require.invoke(Clojure.read(sym))
}
One top-level form, and the 1-based line of the evaluated text it started on.
The line is read from the form's own reader metadata rather than counted afterwards. A form
that carries none — a bare literal, so 42 or "s" or :k — falls back to the reader's
current line, which for a literal that fits on one line is the same number.
private data class ReadForm(val form: Any?, val line: Int, val endLine: Int = line)
Reads code one top-level form at a time, with line numbers.
Read and eval are interleaved rather than read-all-then-eval-all, which is what load-string
does and what a box wants: the forms above a bad one have already run and printed by the time
the bad one is reported. Construct inside withNs — ::keywords and reader aliases resolve
against the current namespace.
private inner class FormSource(code: String) {
// First, and before any `Clojure.var` below: `var` resolves a name in a namespace, it does
// not load the namespace, so resolving `clojure.tools.reader/read` on a fresh JVM without
// this hands back an unbound var that throws on invoke.
init {
requireNs("clojure.tools.reader")
requireNs("clojure.tools.reader.reader-types")
}
private val readFn = Clojure.`var`("clojure.tools.reader", "read")
private val getLineNumber = Clojure.`var`("clojure.tools.reader.reader-types", "get-line-number")
private val metaFn = Clojure.`var`("clojure.core", "meta")
private val eof = Any()
private val opts = PersistentArrayMap.create(mapOf(Keyword.intern("eof") to eof))
private val reader = Clojure
.`var`("clojure.tools.reader.reader-types", "indexing-push-back-reader")
.invoke(java.io.StringReader(code))
/** The next form, or `null` at end of input. Throws what the reader throws. */
fun next(): ReadForm? {
val form = readFn.invoke(opts, reader)
if (form === eof) return null
val fromMeta = ((metaFn.invoke(form) as? ILookup)?.valAt(Keyword.intern("line")) as? Number)?.toInt()
val end = (getLineNumber.invoke(reader) as? Number)?.toInt()
val line = fromMeta ?: end ?: 1
return ReadForm(form, line, maxOf(line, end ?: line))
}
}
fun compileOnly(code: String, ns: String = defaultNs): CompileOnlyResult
Read code without evaluating any of it: the editor's syntax check.
Reports the number of top-level forms, or the first read error with its line and column. It cannot report anything a compiler would say — an unresolvable symbol, a macro that throws, a wrong arity — because none of that is known until the form is evaluated.
fun compileOnly(code: String, ns: String = defaultNs): CompileOnlyResult {
boot()
initNsIfNeeded(ns)
var forms = 0
return try {
withNs(ns) {
val source = FormSource(code)
while (source.next() != null) forms++
}
CompileOnlyResult.Ok(forms)
} catch (t: Throwable) {
CompileOnlyResult.Err(listOf(extractCompilerError(t)))
}
}
private val initted = ConcurrentHashMap.newKeySet<String>()
private fun initNsIfNeeded(ns: String) {
if (initted.add(ns)) {
withNs(ns) {
// (refer 'clojure.core)
v("clojure.core", "refer").invoke(Clojure.read("clojure.core"))
// (require 'field.core) (refer 'field.core) — the box's view of Field, from
// `src/jvmMain/resources/field/core.clj`. Referred rather than aliased so that
// `(lines …)` and `(display …)` read the way the Kotlin equivalents do. Guarded
// the same way Compliment is: a Field whose resources did not ship still runs
// Clojure, it just has no `field/core` in it.
try {
requireNs("field.core")
v("clojure.core", "refer").invoke(Clojure.read("field.core"))
} catch (t: Throwable) {
lprintln("error,clojure", "field.core unavailable in $ns: $t")
}
}
}
}
fun evalPage
Evaluate every top-level form in code, in order, and report the value of the last one.
onStdout and onStderr are handed the text and the line of the form that was running when
it was written, which is the whole reason this is a read-eval loop rather than one
load-string call. *out* and *err* are flushed after every form, so a bare (print …)
with no newline is still attributed to its own form. onForm is the same line
announced before the form runs, for a caller that needs it during evaluation rather than
after: ClojureInterface.animate reads it to remember where a launch was written.
A form that throws stops the run: the forms above it have already printed, the ones below it do not run, and the error carries the failing form's line when the exception does not carry one of its own.
body — 60 lines, from line 440
fun evalPage(
code: String,
ns: String = defaultNs,
executeIfFn: Boolean = true,
onStdout: ((String, Int) -> Unit)? = null,
onStderr: ((String, Int) -> Unit)? = null,
onForm: ((Int) -> Unit)? = null,
onFormValue: ((firstLine: Int, lastLine: Int, value: Any?, printed: String) -> Unit)? = null
): EvalResult {
boot()
val evalFn = Clojure.`var`("clojure.core", "eval")
val prStr = Clojure.`var`("clojure.core", "pr-str")
// Read by the IO callbacks below, written by the loop: "the line Clojure is on right now".
var activeLine = -1
return try {
initNsIfNeeded(ns)
var last: Any? = null
var lastLine = -1
withNs(ns) {
withClojureIO(
onStdout = onStdout?.let { f -> { text: String -> f(text, activeLine) } },
onStderr = onStderr?.let { f -> { text: String -> f(text, activeLine) } }
) {
val source = FormSource(code)
while (true) {
val read = try {
source.next() ?: break
} catch (t: Throwable) {
return@withClojureIO EvalResult.Err(atLine(extractCompilerError(t), activeLine))
}
activeLine = read.line
lastLine = read.line
onForm?.invoke(read.line)
try {
last = withSourceName { evalFn.invoke(read.form) }
} catch (t: Throwable) {
flushClojureIO()
return@withClojureIO EvalResult.Err(atLine(atRaisingLine(extractCompilerError(t), t), read.line))
}
// Text this form printed without a newline belongs to this form, so it goes out
// now, while activeLine still says so, rather than with the next form's output.
flushClojureIO()
onFormValue?.let { record -> runCatching { record(read.line, read.endLine, last, noteForm(prStr, last)) } }
}
if (executeIfFn && last is IFn) {
try {
last = (last as IFn).invoke()
} catch (t: Throwable) {
return@withClojureIO EvalResult.Err(atLine(atRaisingLine(extractCompilerError(t), t), lastLine))
}
}
null
}
}?.let { return it }
EvalResult.Ok(EvalValue(last, line = lastLine, printed = printedForm(prStr, last)))
} catch (t: Throwable) {
EvalResult.Err(atLine(extractCompilerError(t), activeLine))
}
}
The file name every fn this host compiles is stamped with, unique to this host.
Clojure writes *file* into each class it compiles as the class's source file, and the
reader's :line metadata into its line table. So with this bound while a form is evaluated, a
throw from inside a defn written in the box has a stack frame naming this file and the line
inside the box that raised it, which is what atRaisingLine looks for.
private val sourceName = "field-clojure-box-${System.identityHashCode(this).toString(16)}.clj"
private fun <T> withSourceName(f: () -> T): T {
Var.pushThreadBindings(PersistentHashMap.create(mapOf(Compiler.SOURCE to sourceName)))
try {
return f()
} finally {
Var.popThreadBindings()
}
}
error moved to the line that raised it, when that line is in this host's code.
A runtime throw comes out of eval wrapped in a CompilerException that names the top-level
form being run, (mean xs) on line 6, not the (/ …) inside mean on line 4 that divided by
zero. The innermost stack frame compiled from this code says where it really happened, the way
a Python cell's traceback does. Only execution-phase errors move: a reader or compiler error
already names its own line, and a throw from a fn defined elsewhere keeps the calling form.
private fun atRaisingLine(error: CompileError, t: Throwable): CompileError {
val compileTime = generateSequence(t) { it.cause }.any {
it is Compiler.CompilerException && (it.data as? ILookup)?.valAt(Compiler.CompilerException.ERR_PHASE) in
setOf(Compiler.CompilerException.PHASE_READ, Compiler.CompilerException.PHASE_MACROEXPANSION,
Compiler.CompilerException.PHASE_COMPILE_SYNTAX_CHECK, Compiler.CompilerException.PHASE_COMPILATION)
}
if (compileTime) return error
// Innermost cause first: its trace is the one that starts where the throw happened.
val line = generateSequence(t) { it.cause }.toList().asReversed().asSequence()
.flatMap { it.stackTrace.asSequence() }
.firstOrNull { it.fileName == sourceName && it.lineNumber >= 1 }
?.lineNumber ?: return error
return error.copy(line = line)
}
value as a line note shows it: pr-str under a *print-length* and *print-level*, cut to one
short line. A lazy sequence nothing has realised is named rather than printed — printing would
realise it, running code the program never ran.
private fun noteForm(prStr: IFn, value: Any?): String {
if (value is IPending && value is ISeq && !value.isRealized) return "an unrealised lazy sequence (a note does not force it)"
val printLength = Clojure.`var`("clojure.core", "*print-length*") as Var
val printLevel = Clojure.`var`("clojure.core", "*print-level*") as Var
Var.pushThreadBindings(PersistentHashMap.create(mapOf(printLength to 12, printLevel to 4)))
return try {
afield.ui.output.LastRunNotes.oneLine(printedForm(prStr, value), 140)
} finally {
Var.popThreadBindings()
}
}
pr-str of value, falling back to Java's toString if printing it throws.
private fun printedForm(prStr: IFn, value: Any?): String = try {
prStr.invoke(value) as? String ?: value.toString()
} catch (t: Throwable) {
value.toString()
}
data class CompletionItem
One candidate, as Field's popup needs it.
candidate is the remainder — what is left of the name once what the user has typed is
taken off the front — because the box path inserts it at the caret rather than replacing a
range. displayText is the whole name, which is what the popup shows.
data class CompletionItem(
val candidate: String,
val type: String? = null,
val doc: String? = null,
val ns: String? = null,
val displayText: String? = null,
/** Argument lists, as Clojure prints them: `([coll] [n coll])`. Empty for a non-function. */
val arglists: String? = null
)
fun complete(code: String, cursor: Int, ns: String = defaultNs): List<CompletionItem>
Candidates at cursor, from Compliment.
Three things here are not obvious and all three are what makes the popup useful rather than merely present:
- The namespace is initialised, not just created. Compliment completes what a namespace can
see, and a bare
create-nssees nothing at all — not evenclojure.core— so completion in a box that has never been run used to returnmapanddefnandlinesfor nobody.initNsIfNeededis the same call the evaluator makes. :extra-metadatais asked for. Without it Compliment returns names and types and no documentation whatsoever, which is whyCompletionItem.docused to be uniformly null and the documentation pane beside the popup uniformly empty.- The surrounding form is passed as
:context, with the prefix replaced by Compliment's__prefix__marker. That is what lets it offer local names — aletbinding, afnparameter, adoseqvariable — which no amount of namespace introspection can know. It is also the part that can fail, because half-typed code does not read: on any throw this retries once without the context, so the answer degrades to "globals only" rather than to nothing.
Returns an empty list when Compliment is not on the classpath, rather than throwing. Completion is the one part of this language that is allowed to be absent.
body — 54 lines, from line 609
fun complete(code: String, cursor: Int, ns: String = defaultNs): List<CompletionItem> {
boot()
val completionsVar = try {
varv("compliment.core", "completions")
} catch (t: Throwable) {
lprintln("error,clojure", "no Compliment on the classpath, so no completions: $t")
return emptyList()
}
// Determine effective namespace WITHOUT evaluating the buffer.
val effectiveNs = extractNsNameFromCode(code) ?: ns
val prefix = extractPrefix(code, cursor)
initNsIfNeeded(effectiveNs)
val candidates = withNs(effectiveNs) {
val withContext = try {
completionsVar.invoke(prefix, completionOptions(effectiveNs, completionContext(code, cursor, prefix)))
} catch (t: Throwable) {
lprintln("debug,clojure", "completion context rejected, retrying without it: $t")
null
}
withContext ?: try {
completionsVar.invoke(prefix, completionOptions(effectiveNs, null))
} catch (t: Throwable) {
lprintln("error,clojure", "Compliment threw: $t")
null
}
}
val seq: ISeq = RT.seq(candidates) ?: return emptyList()
val out = mutableListOf<CompletionItem>()
var cur: ISeq? = seq
while (cur != null) {
val m = cur.first() as? ILookup
if (m != null) {
fun k(key: String): String? = m.valAt(Keyword.intern(key))?.toString()
val cand = k("candidate")
if (cand != null) {
out += CompletionItem(
candidate = if (cand.startsWith(prefix)) cand.substring(prefix.length) else cand,
type = k("type")?.removePrefix(":"),
doc = k("doc"),
ns = k("ns"),
displayText = cand,
arglists = arglistsText(m.valAt(Keyword.intern("arglists")))
)
}
}
cur = cur.next()
}
return out
}
Compliment's :arglists as one line of the thing a reader wants: `coll [f coll]`.
It arrives as a Clojure collection of strings, each string being a printed argument vector,
so toString on it gives ("coll" "[f coll]") — quotes, parens and all, which is what the
completion popup was showing in the grey tail beside every function name. null for a
candidate that is not callable, which is most of them.
private fun arglistsText(raw: Any?): String? {
val items = when (raw) {
null -> return null
is Iterable<*> -> raw.mapNotNull { it?.toString() }
else -> listOf(raw.toString())
}
return items.joinToString(" ").takeIf { it.isNotBlank() }
}
{:ns 'the.ns :extra-metadata #{:doc :arglists} :context "…"} for compliment.core/completions.
private fun completionOptions(ns: String, context: String?): IPersistentMap {
val extras = PersistentHashSet.create(Keyword.intern("doc"), Keyword.intern("arglists"))
val pairs = mutableMapOf<Any, Any>(
Keyword.intern("ns") to Symbol.intern(ns),
Keyword.intern("extra-metadata") to extras
)
if (context != null) pairs[Keyword.intern("context")] = context
return PersistentArrayMap.create(pairs)
}
The code up to the cursor, with the prefix replaced by __prefix__, closed.
Compliment reads this to work out what the caret is inside, which is the only way it can know
about a local: a let binding, a fn parameter, a doseq variable. It has to read it, and
code being typed rarely reads — a caret at the end of (let [spiral 1]\n (lines [sp sits
inside three unclosed brackets. So the brackets still open at the caret are closed, by walking
the text with a bracket stack and appending their closers.
The text after the caret is dropped, and that is the load-bearing half. Every form the
caret is inside opens before it, so the tail carries no scope — and in a real editor the
tail is not the reader's text at all, it is whatever Monaco's auto-closing left there. The
buffer measured from a live Clojure box on 2026-09-13 was
(let [spline (polyline [[0 0] [10 10]])]\n (lines [sp) — note the final ), auto-inserted
for (lines and now sitting where the [ wants its ]. Kept, that tail makes the context
[__prefix__))), which does not read, and Compliment answers a context it cannot read with
globals only and no complaint: the popup listed functions, a class and a var, and not the
local two lines up. Dropped, the same caret gets (linesprefix)) and the local is
there. Every unit test that passes a string ending at the caret was already exercising the
good case, which is why this survived one.
Strings, character literals and ; comments are skipped rather than scanned, so a "(" or a
\( does not open anything. The caller still treats a rejection as "no context" — this makes
the good case common, it does not promise the string reads.
private fun completionContext(code: String, cursor: Int, prefix: String): String {
val caret = cursor.coerceIn(0, code.length)
val start = (caret - prefix.length).coerceIn(0, caret)
val marked = code.substring(0, start) + "__prefix__"
return marked + closersFor(marked)
}
The closing brackets text is missing, innermost first: "))", "])", "".
private fun closersFor(text: String): String {
val open = ArrayDeque<Char>()
var i = 0
while (i < text.length) {
when (val c = text[i]) {
';' -> while (i < text.length && text[i] != '\n') i++
'"' -> {
i++
while (i < text.length && text[i] != '"') {
if (text[i] == '\\') i++
i++
}
i++
}
// A character literal: `\(`, `\"`, `\newline`. Only the first character after the
// backslash can be a bracket, and skipping it is enough — the rest of a named
// character is ordinary symbol text.
'\\' -> i += 2
'(', '[', '{' -> {
open.addLast(c)
i++
}
')', ']', '}' -> {
open.removeLastOrNull()
i++
}
else -> i++
}
}
return open.reversed().joinToString("") {
when (it) {
'(' -> ")"
'[' -> "]"
else -> "}"
}
}
}
Very small parser: find (ns foo.bar …) and return "foo.bar". This is tolerant of leading whitespace/comments and line breaks, but intentionally does NOT read/eval the form.
private fun extractNsNameFromCode(code: String): String? {
// Regex looks for: (ns <sym or dotted-sym>
// Keep it conservative to avoid false positives in comments/strings.
val nsRegex = Regex("""\(\s*ns\s+([a-zA-Z_][\w\.\-\+]*)""")
val match = nsRegex.find(code)
return match?.groups?.get(1)?.value
}
Extract a completion prefix ending at cursor.
Supports qualified symbols like "str/joi" and member access ".toStr".
Purely lexical: no reading or eval.
private fun extractPrefix(code: String, cursor: Int): String {
val i = cursor.coerceIn(0, code.length)
if (i == 0) return ""
// Allowed chars inside a Clojure symbol segment
fun isSymChar(ch: Char): Boolean =
ch.isLetterOrDigit() || ch in listOf('-', '_', '!', '?', '*', '\'', '+', '=', '<', '>', '$')
// Walk left to include "/seg" for aliases/qualified, and "." for member/method start
var start = i - 1
var sawSlash = false
var sawDot = false
while (start >= 0) {
val ch = code[start]
if (isSymChar(ch)) {
start--
continue
}
if (ch == '/' && !sawSlash) {
sawSlash = true
start--
continue
}
if (ch == '.' && !sawDot) {
sawDot = true
start--
continue
}
break
}
val s = (start + 1).coerceAtMost(i)
return code.substring(s, i)
}
}
object ClojureInterface
What a Clojure box can reach of Field, and the Kotlin half of field/core.clj.
Clojure's Java interop is good enough that most of Field needs nothing here: (afield.geometry.path.FLine.)
constructs an FLine, (.moveTo f 0 0 0) builds it, and (.getChildren (box)) walks the
document. What interop is bad at is the two shapes Field's Kotlin API uses most — an extension
function (compiled to a static whose class name is a file name, SomeSkikoKt) and a default
argument (not a default at all from outside Kotlin, so every parameter has to be passed). So this
is a thin facade over exactly those and nothing else: everything below is a one-liner, and a verb
that reads well as raw interop does not belong here.
thisBox is the same device as PythonInterface.thisBox and carries the same caveat: box code
runs on the main coroutine, one fragment at a time, so a single slot is enough — but it is a
single slot, set for the duration of one fragment and restored afterwards.
object ClojureInterface {
var thisBox: Box?
The box whose code is running, or null outside a box.
A @JvmField rather than a property so that Clojure can read it as ClojureInterface/thisBox
instead of (ClojureInterface/getThisBox), which is what (box) in field.core does.
@JvmField
var thisBox: Box? = null
var thisLine: Int
The box line of the top-level form being evaluated, or -1 outside one.
Written by ClojureExecution as it walks the forms, and read by anything host-side that has to report a position later — animate is the one that needs it, because a function that throws on frame 900 has a stack trace full of Clojure's own source and nothing that points back at the box.
@JvmField
var thisLine: Int = -1
var currentFrames: Frames?
The Frames whose body is running, or null outside a frame.
The receiver a Kotlin animate { } block gets for free. Clojure's function has no receiver,
so the two things a body actually reaches for — the frame number and "stop now" — come from
here, via (frame) and (end) in field.core.
@JvmField
var currentFrames: Frames? = null
fun animate(body: IFn): Frames
A Frames that calls body once per frame: animate { … } for a Clojure box.
Returning one of these from a box is what launches it, by exactly the rule a Kotlin box
follows — ReturnValueRegistry sees a Frames come back and drives it from the Runloop. Code
before the (animate …) form runs once, on the run; body runs once per frame after that.
A throw from body is caught here rather than by the launcher, for the sake of the line
number: the launcher digs one out of the stack trace by matching the box id against
StackTraceElement.fileName, which works for compiled box Kotlin and never matches a Clojure
fn, leaving it to report the first frame of Clojure's internals instead. So this reports
against the line the (animate …) form was on and then ends the animation, which is what a
Kotlin launch does when its body throws.
@JvmStatic
fun animate(body: IFn): Frames {
// Captured when the animation is *built*, which is while the box is running and these are
// set; re-established below for each frame, because by then the run has finished and put
// them back.
val box = thisBox
val line = thisLine
return eachFrame {
val previousFrames = currentFrames
val previousBox = thisBox
val previousLine = thisLine
currentFrames = this
thisBox = box
thisLine = line
try {
body.invoke()
} catch (t: Throwable) {
box?.displayError(t.message ?: t.javaClass.name, "code", line)
end()
} finally {
currentFrames = previousFrames
thisBox = previousBox
thisLine = previousLine
}
}
}
fun endAnimation()
Stop the animation whose frame is running — (end) in field.core.
Frames.end() is a suspend function, so Clojure cannot call it. Cancelling the job is what it
does for a launch that has started, and inside a frame body it always has. Outside a frame
body this does nothing rather than throwing: "end what is running" and nothing is.
@JvmStatic
fun endAnimation() {
currentFrames?.job?.cancel()
}
fun frameNumber(): Int
The frame number of the animation being run, or 0 outside one.
@JvmStatic
fun frameNumber(): Int = currentFrames?.frameNumber ?: 0
fun lines(box: Box, m: List<FLine>, channel: String, w: Double, h: Double, fitToSize: Boolean)
Box.lines(…) with every argument spelled out, for (lines …) in field.core.
@param m the lines to draw; a Clojure vector of FLines arrives as a List.
@param channel output channel (disclosure panel) to draw into.
@param w output width in pixels, -1.0 to derive it from the geometry.
@param h output height in pixels, -1.0 to derive it from the geometry.
@param fitToSize scale and centre the geometry into w x h.
@JvmStatic
fun lines(box: Box, m: List<FLine>, channel: String, w: Double, h: Double, fitToSize: Boolean) {
box.lines(m, channel = channel, w = w, h = h, fitToSize = fitToSize)
}
fun polyline(points: List<*>): FLine
An FLine through points, each a 2- or 3-element sequence of numbers.
The one piece of geometry building that is worth a facade, because it is the one every example
starts with and (doto (FLine.) (.moveTo 0 0 0) (.lineTo 1 1 0)) puts the shape of the loop
ahead of the shape of the line. Anything richer — cubics, per-node colour — is FLine's own
API, reached by interop.
@throws IllegalArgumentException if a point is not a sequence of two or three numbers.
@JvmStatic
fun polyline(points: List<*>): FLine {
val f = FLine()
points.forEachIndexed { index, point ->
val xyz = (point as? Iterable<*>)?.map { it as? Number ?: badPoint(index, point) }
?: badPoint(index, point)
if (xyz.size !in 2..3) badPoint(index, point)
val z = xyz.getOrNull(2) ?: 0
if (index == 0) f.moveTo(xyz[0], xyz[1], z) else f.lineTo(xyz[0], xyz[1], z)
}
return f
}
private fun badPoint(index: Int, point: Any?): Nothing = throw IllegalArgumentException(
"point $index is $point; polyline wants a sequence of [x y] or [x y z] numbers"
)
fun colour(line: FLine, r: Double, g: Double, b: Double, a: Double): FLine
line with FLine.color set, returned so it can be threaded. Alpha included: lines blend.
Spelled colour here and color in field.core, because the extension property this sets
is color and a facade that renames the thing it wraps is a facade you have to look up.
@JvmStatic
fun colour(line: FLine, r: Double, g: Double, b: Double, a: Double): FLine {
line.color = vec(r, g, b, a)
return line
}
fun thickness(line: FLine, width: Double): FLine
line with FLine.thickness set, returned so it can be threaded.
@JvmStatic
fun thickness(line: FLine, width: Double): FLine {
line.thickness = width
return line
}
class PendingControl
One (slider …) or (checkbox …) a run asked for, waiting for flushControls.
Controls are collected rather than shown one by one because Box.controlGroup draws a whole
channel at once: a second group in the same channel replaces the first. So a run's controls are
gathered per box and drawn as one group per channel when the run ends, the approach Python
boxes took on 2026-10-15 (PythonInterface.flushPythonControls).
class PendingControl(
val kind: String,
val label: String,
val channel: String,
val min: Double,
val max: Double,
val step: Double,
val value: Any,
val onChange: Any?,
val line: Int,
)
private val pendingControls = ConcurrentHashMap<String, MutableList<PendingControl>>()
True while flushControls runs, when a restored value is delivered synchronously.
private var flushing = false
fun slider(box: Box, label: String, min: Double, max: Double, step: Double, value: Double, channel: String, onChange: Any?): Double
Ask for a slider in channel of box; drawn when the run ends. Returns value.
onChange is a one-argument fn, called with each new value, or an atom, which is reset! to it.
@JvmStatic
fun slider(box: Box, label: String, min: Double, max: Double, step: Double, value: Double, channel: String, onChange: Any?): Double {
require(max > min) { "slider \"$label\": max ($max) must be greater than min ($min)" }
pendingControls.getOrPut(box.id) { mutableListOf() } +=
PendingControl("slider", label, channel, min, max, step, value.coerceIn(min, max), onChange, thisLine)
return value
}
fun checkbox(box: Box, label: String, value: Boolean, channel: String, onChange: Any?): Boolean
Ask for a checkbox in channel of box; drawn when the run ends. See slider for onChange.
@JvmStatic
fun checkbox(box: Box, label: String, value: Boolean, channel: String, onChange: Any?): Boolean {
pendingControls.getOrPut(box.id) { mutableListOf() } +=
PendingControl("checkbox", label, channel, 0.0, 1.0, 1.0, value, onChange, thisLine)
return value
}
fun clearControls(box: Box)
Forget whatever an earlier run of box asked for and did not get to draw.
@JvmStatic
fun clearControls(box: Box) {
pendingControls.remove(box.id)
}
fun flushControls(box: Box)
Draw the controls box's run asked for, one controlGroup per channel, in the order asked.
A control whose label was on screen before keeps the value it was moved to (ControlGroupBuilder
caches by channel and label), and its callback is called with that value right here, so an atom
or a def that a callback writes agrees with the control after a re-run.
@JvmStatic
fun flushControls(box: Box) {
val asked = pendingControls.remove(box.id) ?: return
val previous = flushing
flushing = true
try {
asked.groupBy { it.channel }.forEach { (channel, group) ->
box.controlGroup(channel) {
for (c in group) when (c.kind) {
"slider" -> slider(c.label, c.min, c.max, c.step, c.value as Double) { v -> deliverControl(box, c, v) }
"checkbox" -> checkbox(c.label, c.value as Boolean) { v -> deliverControl(box, c, v) }
}
}
}
} finally {
flushing = previous
}
}
Hand value to a control's callback. Moves from the page arrive off the runloop and are sent
onto it, where box code runs; a restore during flushControls is already there and runs at once.
private fun deliverControl(box: Box, control: PendingControl, value: Any) {
if (flushing) invokeControl(box, control, value)
else controlDispatch { invokeControl(box, control, value) }
}
internal var controlDispatch: (() -> Unit) -> Unit = { work -> inMain { work() } }
How a move from the page reaches the runloop; a test without a runloop runs it in place.
fun invokeControl(box: Box, control: PendingControl, value: Any)
Call control's callback now, with box as (box) so its display and println land in the
box that drew the control. A throw is reported on the line the control was asked for.
@JvmStatic
fun invokeControl(box: Box, control: PendingControl, value: Any) {
val previousBox = thisBox
val previousLine = thisLine
thisBox = box
thisLine = control.line
try {
when (val f = control.onChange) {
null -> Unit
is IAtom -> f.reset(value)
is IFn -> f.invoke(value)
else -> throw IllegalArgumentException(
"the change handler is a ${f.javaClass.simpleName}; pass a fn of one argument or an atom"
)
}
} catch (t: Throwable) {
box.displayError("${control.label}: ${t.message ?: t.javaClass.name}", "code", control.line)
} finally {
thisBox = previousBox
thisLine = previousLine
}
}
fun context(box: Box, query: Any): Any?
fromContext for a Clojure box: the nearest value, looking at box's properties and then at
each generation of parents (their properties, then their Kotlin top-level variables).
query is either a Class, matching the nearest value that is an instance of it, or a name
(string, keyword or symbol), matching the nearest non-null property or variable called that,
which is how a Clojure box reads a property a parent exports. Returns null when nothing matches.
@JvmStatic
fun context(box: Box, query: Any): Any? {
val resolution = when (query) {
is Class<*> -> ContextResolution.walk(
box,
ContextResolution.Query.byType(query) { query.isInstance(it) },
ContextResolution.Policy.fromContext(false),
ContextResolution.Mechanism.FROM_CONTEXT,
)
is String, is clojure.lang.Named -> {
val name = (query as? clojure.lang.Named)?.name ?: query as String
ContextResolution.walk(
box,
ContextResolution.Query(name, listOf(name), { it != null }, { it.value != null }),
ContextResolution.Policy.fromContext(false, selfCandidate = false),
ContextResolution.Mechanism.FROM_CONTEXT_NAMED,
)
}
else -> throw IllegalArgumentException("context wants a class or a name, not ${query.javaClass.simpleName} $query")
}
return resolution?.value
}
}
fun install()
@RunsAt(plugins)
fun install() {
TopologyWatcher("clojure") { clojureTopologyNeedsUpdating(it) }.start()
}
A Clojure line, in the box's coordinates.
Clojure counts the lines of the text it was handed from 1; execute.line is where that text
starts in the box, counted from 0. So the sum is the box line the editor draws against, which is
the same arithmetic PythonInterface.resultLineFor does (startLine + count of the lines) and
the reason running one form reports beside that form rather than at the top of the box.
Returns -1 — "no line, put it under the box" — for a line Clojure could not supply.
private fun boxLineFor(execute: Execute): (Int?) -> Int =
{ line -> if (line != null && line >= 1) execute.line + line else -1 }
The two launch modes the front end knows, spelled the way every other language spells them.
private const val PENDING_LAUNCH_MODE = "launching"
private const val ACTIVE_LAUNCH_MODE = "launch"
class ClojureExecution(val inside: Box) : BoxExecution
val clj = ClojureHost(defaultNs = "live.user").also { it.warmInBackground() }
What one evaluation of a box came to: whether it got to the end, and the value of its last form with the text and line the echo would use.
printedAnything is the one non-obvious member and it is why this type exists rather than a
bare Any?: the conditional echo ("show the last value, unless the box already said
something") is a decision each caller makes, and it cannot be made from the value alone.
private class EvalOutcome(
val ok: Boolean,
val value: Any? = null,
val printed: String? = null,
val line: Int = -1,
val printedAnything: Boolean = false
)
Read, then evaluate, execute.code, reporting output and errors against inside.
Everything a Clojure box does, both halves of it: the reader check that turns an unbalanced paren into a marker on the right line, and the evaluation whose printed output goes beside the form that printed it. The one thing it does not do is publish the last value — that is the only thing executeFragment and launchAll disagree about, so it is theirs to decide.
source — 75 lines, from line 1179
private fun evaluateReporting(inside: Box, execute: Execute): EvalOutcome {
val boxLine = boxLineFor(execute)
val compiled = clj.compileOnly(execute.code)
if (compiled is CompileOnlyResult.Err) {
// Unreadable, so there is nothing to evaluate — and evaluating anyway would report the
// identical reader error a second time.
lprintln("error,clojure", "Syntax errs: ${compiled.errors}")
val first = compiled.errors[0]
inside.displayError(first.message, line = boxLine(first.line), property = "code")
afield.ui.output.LastRunNotes.record(inside, "code", execute.code, execute.line, emptyList(), first.line, first.message)
return EvalOutcome(ok = false)
}
lprintln("info,clojure", "OK, ${(compiled as CompileOnlyResult.Ok).formsCount} forms")
var hasOutput = false
var wroteToStderr = false
val previousBox = ClojureInterface.thisBox
val previousLine = ClojureInterface.thisLine
ClojureInterface.thisBox = inside
ClojureInterface.clearControls(inside)
// What each top-level form came to, for the box's line notes (afield.ui.output.LastRunNotes).
val produced = ArrayList<afield.ui.output.LastRunNotes.Produced>()
try {
return when (val e = clj.evalPage(
execute.code,
executeIfFn = false,
onStderr = { text, line ->
hasOutput = true
wroteToStderr = true
inside.displayError(text, "code", boxLine(line))
},
onStdout = { text, line ->
hasOutput = true
inside.output.proxy(text, false, boxLine(line))
},
onForm = { line -> ClojureInterface.thisLine = boxLine(line) },
onFormValue = { first, last, value, printed ->
val type = value?.javaClass?.simpleName?.let { " · $it" }.orEmpty()
produced += afield.ui.output.LastRunNotes.Produced(first, last, "evaluated to $printed$type",
value = listOf(afield.ui.output.LineNotes.plain("evaluated to "), afield.ui.output.LineNotes.strong(printed), afield.ui.output.LineNotes.plain(type)))
}
).also { result ->
val error = (result as? EvalResult.Err)?.error
afield.ui.output.LastRunNotes.record(inside, "code", execute.code, execute.line, produced, error?.line, error?.message)
}) {
is EvalResult.Ok -> {
// Clear the marker a *previous* failed run left, so fixing a typo and re-running
// takes the red away. Skipped when this run wrote to `*err*`, which arrives as
// an error and would otherwise be cleared by its own success.
if (!wroteToStderr) inside.displayError(null, "code", -1)
EvalOutcome(
ok = true,
value = e.result.value,
printed = e.result.printed ?: e.result.value.toString(),
line = boxLine(e.result.line),
printedAnything = hasOutput
)
}
is EvalResult.Err -> {
lprintln("error,clojure", "Runtime err: ${e.error}")
inside.displayError(e.error.message, line = boxLine(e.error.line), property = "code")
EvalOutcome(ok = false, printedAnything = hasOutput)
}
}
} finally {
// Drawn even after a throw, as a Python cell does: the controls asked for before the
// failing form are real, and a box half-built is easier to fix with them on screen.
runCatching { ClojureInterface.flushControls(inside) }
.onFailure { inside.displayError("controls: ${it.message ?: it.javaClass.name}", "code", -1) }
ClojureInterface.thisBox = previousBox
ClojureInterface.thisLine = previousLine
}
}
The last value, beside the form that produced it — but only if there is one worth showing.
Two suppressions, both of them the ones a Python cell makes. A box that has already printed
does not also get told what its last form returned. And a box whose last form returned nil
gets nothing: nil is what println and (lines …) evaluate to, so echoing it would put
the word "nil" beside the last line of almost every box that draws something. (def is not
one of those — it evaluates to the var — so a box that only defines things still echoes.)
private fun echoValue(inside: Box, outcome: EvalOutcome) {
if (outcome.ok && !outcome.printedAnything && outcome.value != null) {
inside.output.proxy(outcome.printed ?: outcome.value.toString(), false, outcome.line)
}
}
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit) {
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit) {
// Captured out here, like DatalogExecution does: `thisSession` is per-thread and `inMain`
// resumes on the runloop's thread, where it is empty.
evaluateOnce(inside, execute, "fragment", reply)
}
One evaluation of the box, reported under mode.
executeFragment and executeAll do the same thing here — Clojure has no separate
whole-buffer path — but they must not claim the same thing: ExecutionReports only records a
setup from a mode that is not "fragment", so running a box would otherwise never register
as having been set up.
private fun evaluateOnce(inside: Box, execute: Execute, mode: String, reply: (Message) -> Unit) {
val originSession = thisSession.get()
val trace = ExecutionTrace.forEngine(inside, execute, mode, reply)
inMain {
val wm = ++inside.fragmentGeneration
configureOutputProxies(inside, trace.reply, originSession)
send(Message("is-running").payload(IsRunning(execute.uid, true, wm, "__fragment__")))
var outcome: EvalOutcome? = null
try {
outcome = evaluateReporting(inside, execute)
echoValue(inside, outcome)
} finally {
trace.finish(outcome?.ok == true)
send(Message("is-running").payload(IsRunning(execute.uid, false, wm, "__fragment__")))
}
}
}
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit) {
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit) {
evaluateOnce(inside, execute, "run", reply)
}
override fun launchAll
Launch: run the box once, then hand its last value to the same registry a Kotlin box's return value goes through.
So (animate (fn [] …)) as the last form animates, for exactly the reason it does in Kotlin —
the value is a Frames and ReturnValueRegistry knows how to drive one from the Runloop —
and no special case here says the word "animate". A value the registry does not recognise is
not an error: the box reports it the way a run would and goes quiet, which is what a launch of
a box that turned out not to be an animation should look like.
body — 56 lines, from line 1316
override fun launchAll(
inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit
) {
val originSession = thisSession.get()
val launchEpoch = inside.captureLaunchEpoch()
val trace = ExecutionTrace.forEngine(inside, execute, "launch", reply)
inMain {
// A stop that landed between the request and here cancels the launch rather than
// racing it. Same guard, same reason, as FieldMLExecution.launchAll.
if (!inside.isLaunchEpochCurrent(launchEpoch)) {
// The awaiting caller still needs an answer, and "cancelled" is the honest one.
trace.finish(false, "launch cancelled by a stop before it started")
return@inMain
}
configureOutputProxies(inside, trace.reply, originSession)
val generation = inside.runGeneration.getOrPut(strand) { -1 } + 1
inside.runGeneration[strand] = generation
inside.currentRunningStates[strand] = RunningState(generation, PENDING_LAUNCH_MODE)
send(Message("is-running").payload(IsRunning(execute.uid, true, generation, strand, PENDING_LAUNCH_MODE)))
val outcome = evaluateReporting(inside, execute.copy(code = code))
val consumed = outcome.ok && ReturnValueRegistry.launchWithGeneration(
inside,
outcome.value,
strand,
generation,
launchEpoch,
) { isRunning, notifiedStrand, watermark ->
if (isRunning) {
inside.currentRunningStates[notifiedStrand] = RunningState(watermark, ACTIVE_LAUNCH_MODE)
} else if (inside.currentRunningStates[notifiedStrand]?.watermark == watermark) {
inside.currentRunningStates.remove(notifiedStrand)
}
send(
Message("is-running")
.payload(IsRunning(execute.uid, isRunning, watermark, notifiedStrand, ACTIVE_LAUNCH_MODE))
)
}
if (!consumed) {
echoValue(inside, outcome)
if (inside.currentRunningStates[strand]?.watermark == generation) {
inside.currentRunningStates.remove(strand)
}
send(
Message("is-running")
.payload(IsRunning(execute.uid, false, generation, strand, PENDING_LAUNCH_MODE))
)
}
// Setup is what a launch receipt is about, exactly as it is for Kotlin: the box
// evaluated and its value was either taken up by the registry or echoed. Whether an
// animation the registry is now driving stays healthy is not something this can claim.
trace.finish(outcome.ok && inside.isLaunchEpochCurrent(launchEpoch))
}
}
override fun stop(inside: Box)
Stop: cancel the jobs this box launched.
It does not interrupt an evaluation. A Clojure form runs to completion on the runloop, so a
box in an infinite loop is not stoppable by this or by anything else short of killing
Field — the same as a Kotlin box in a while (true) that never suspends. What this does stop
is every launched animation, by the standard barrier: bump the launch epoch, bump every
strand's generation (which is what each Frames's shouldend is watching), cancel the jobs.
override fun stop(inside: Box) {
inside.stopTrackedJobs()
}
override fun complete
body — 49 lines, from line 1386
override fun complete(
inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit
) {
// Convert (line, column) to absolute cursor index for clj.complete
val code = completionRequest.allCode
val cursor = try {
val pos = completionRequest.position
if (pos >= 0 && pos <= code.length) {
pos
} else {
val lineIdx = (completionRequest.line - 1).coerceAtLeast(0)
val col0 = (completionRequest.column - 1).coerceAtLeast(0)
val parts = code.split("\n", limit = -1)
val safeLineIdx = lineIdx.coerceAtMost(parts.size - 1)
val priorChars = if (safeLineIdx <= 0) 0 else (0 until safeLineIdx).sumOf { parts[it].length + 1 }
val safeCol = if (safeLineIdx in parts.indices) col0.coerceAtMost(parts[safeLineIdx].length) else 0
priorChars + safeCol
}
} catch (_: Throwable) { 0 }
val metadata = clj.complete(code, cursor).map {
Metadata(
icon = it.type ?: "keyword",
// The tail is the grey text after the name in the popup, so it wants the most
// useful short thing available: how to call it beats what kind of thing it is.
tail = it.arglists ?: it.type ?: "keyword",
documentation = completionDocumentation(it),
text = it.candidate,
displayText = it.displayText ?: "",
category = clojureCompletionCategory(it.type),
prefix = it.ns ?: ""
)
}
if (metadata.isNotEmpty()) {
reply(
Message("complete").payload(
Completion(
boundsStart = -1,
boundsEnd = -1,
lineStart = completionRequest.line,
columnsStart = completionRequest.column - 1,
useLineColumns = true,
metadata = metadata
)
)
)
}
}
What the documentation pane beside the popup shows: where the name lives, how to call it, and
its docstring, in that order — the three lines clojure.repl/doc prints, minus the ceremony.
private fun completionDocumentation(item: ClojureHost.CompletionItem): String = buildString {
val qualified = listOfNotNull(item.ns?.takeIf { it.isNotBlank() }, item.displayText).joinToString("/")
if (qualified.isNotBlank()) appendLine(qualified)
item.arglists?.takeIf { it.isNotBlank() }?.let { appendLine(it) }
item.doc?.takeIf { it.isNotBlank() }?.let { append(it) }
}.trim()
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit) {
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit) {
// For Clojure, construct a function call: (symbolName)
val functionCall = "(${executeSymbol.symbol})"
// Create an Execute object for executeFragment
val exec = Execute(
uid = executeSymbol.uid,
type = "fragment",
line = executeSymbol.line,
code = functionCall,
singleLine = true
)
// Execute as a fragment
executeFragment(inside, exec, reply)
}
override val languageName: String
override val languageName: String
get() = "clojure"
}
var Box.isClojureRoot by Delegation<Boolean?>()
fun installClojure()
@RunsAt(early)
fun installClojure() {
Box.ioPersistentProperties.add("isClojureRoot")
registerMainLanguageRuntime(
MainLanguageRuntime(
id = "clojure",
label = "Clojure",
runtimeClass = ClojureExecution::class,
setLegacyRoot = { box, enabled -> box.isClojureRoot = enabled },
applyTopology = ::clojureTopologyNeedsUpdating,
),
)
}
fun clojureTopologyNeedsUpdating(values: Collection<Box>)
fun clojureTopologyNeedsUpdating(values: Collection<Box>) {
applyRuntimeToggle(
boxes = values,
isRoot = { it.isClojureRoot == true || it.isMainLanguage("clojure") },
runtimeClass = ClojureExecution::class,
newExecution = { child -> ClojureExecution(child) },
language = "Clojure",
customization = "",
notifyCategory = "clojure"
)
}