MorphoInterface.kt
src/jvmMain/kotlin/afield/lang/morpho/MorphoInterface.kt · 559 lines
package and imports — 49 lines, from line 1
package afield.lang.morpho
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.document.properties.Delegation
import io.ktor.websocket.WebSocketSession
Field4 language adapter for MorphoHDL, a Python-syntax hardware-description language.
MorphoHDL programs describe recursive circuit structures that Field compiles via morpho's
own compiler on GraalJS and lays out with its Barnes–Hut kernel on GraalWasm. The result
lands on Box.morphoCircuit; a neighbouring Kotlin box reads it and draws with
circuit.flines().
MorphoExecution is the BoxExecution implementation. Setting a parent box's isMorphoRoot
to true turns its children into MorphoHDL boxes. Execute mode grows the circuit to
completion; launch mode expands a few cells per frame while relaxing the layout between
expansions, so the recursive division is visible in motion. installMorpho registers the
runtime; installMorphoPlugin starts the topology watcher.
class MorphoExecution(val inside: Box) : BoxExecution
MorphoHDL as a first-class Field language.
The runtime underneath is not Field's — it is morpho's own compiler on GraalJS and morpho's own Barnes–Hut kernel on GraalWasm (see MorphoRuntime). This file is only the adapter: it teaches Field how to run a box whose text is MorphoHDL, and it is deliberately the same shape as afield.lang.forth.ForthExecution and afield.lang.nano.NanoExecution, because "add a language" should be one recognisable move rather than six bespoke ones.
What a morpho box contains
MorphoHDL source, plus one Field directive saying which design to build and how wide:
# main ripple_adder(8, 8, 1)
Xor3 = LUT(3, 0b1001_0110)
Maj3 = LUT(3, 0b1110_1000)
@morpho
def full_adder(a, b, c_in):
sum = Xor3(a, b, c_in)
c_out = Maj3(a, b, c_in)
return sum, c_out
@morpho(fallback=full_adder)
def ripple_adder(a, b, c):
a0, a1 = SPLIT(a)
b0, b1 = SPLIT(b)
s0, c_mid = ripple_adder(a0, b0, c)
s1, c_out = ripple_adder(a1, b1, c_mid)
sum = CAT(s0, s1)
return sum, c_out
The directive rides in a # comment because morpho's tokenizer discards comments entirely, so
Field can annotate a program without the language ever seeing it — and the same file still
parses in upstream's demo. An empty box inherits morpho's standard library, so
# main brent_kung_adder(32, 32, 1) on its own is a complete program.
Execute versus launch
Execute grows the circuit to completion and reports it. Launch grows it a few cells per
frame and relaxes the layout between expansions, which is the mode worth having: a language
built on recursive division should be watched dividing. Either way the result lands on
Box.morphoCircuit, so a neighbouring Kotlin box can draw it with flines and everything
Field can already do to an FLine applies.
class MorphoExecution(val inside: Box) : BoxExecution {
private var strandJobs = mutableMapOf<String, kotlinx.coroutines.Job>()
override val languageName: String get() = "morpho"
---------------------------------------------------------------- the Field directive
data class Directive
What a morpho box is asking Field to build.
@param grow cells expanded per frame in launch mode. Larger is faster and less watchable. @param relax layout iterations per frame.
data class Directive(
val design: String,
val signature: IntArray,
val optimize: Boolean = true,
val grow: Int = 12,
val relax: Int = 6,
val line: Int = 0,
)
companion object
companion object {
private val MAIN = Regex(
"""#\s*main\s+([A-Za-z_]\w*)\s*\(([^)]*)\)(.*)""",
RegexOption.IGNORE_CASE
)
private val FLAG = Regex("""([A-Za-z_]\w*)\s*=\s*(\S+)""")
fun parseDirective(code: String): Directive?
Find the # main … directive.
Returns null rather than guessing. A morpho cell is size-agnostic by design — that is the whole conceit of the language — so there is no defensible default width, and inventing one would silently build a different circuit from the one intended.
fun parseDirective(code: String): Directive? {
code.lineSequence().forEachIndexed { index, raw ->
val m = MAIN.find(raw) ?: return@forEachIndexed
val design = m.groupValues[1]
val signature = m.groupValues[2]
.split(',')
.mapNotNull { it.trim().takeIf(String::isNotEmpty)?.toIntOrNull() }
.toIntArray()
if (signature.isEmpty()) return@forEachIndexed
var optimize = true
var grow = 12
var relax = 6
FLAG.findAll(m.groupValues[3]).forEach { f ->
val value = f.groupValues[2]
when (f.groupValues[1].lowercase()) {
"optimize" -> optimize = value.toBooleanStrictOrNull() ?: true
"grow" -> grow = value.toIntOrNull()?.coerceAtLeast(1) ?: 12
"relax" -> relax = value.toIntOrNull()?.coerceAtLeast(0) ?: 6
}
}
return Directive(design, signature, optimize, grow, relax, index)
}
return null
}
}
---------------------------------------------------------------- output plumbing
private fun report(execute: Execute, reply: (Message) -> Unit, session: WebSocketSession?, text: String) {
dispatchExecutionOutput(
inside, reply,
Output(
uid = inside.id,
line = execute.line + execute.code.split("\n").size,
kind = "ok",
html = text,
frame = Runloop.runLoop.frame.toInt()
),
originSession = session
)
}
private fun clearErrors(execute: Execute, reply: (Message) -> Unit, session: WebSocketSession?) {
dispatchExecutionOutput(
inside, reply,
Output(uid = inside.id, line = -1, kind = "err", property = "code", html = "",
frame = Runloop.runLoop.frame.toInt()),
originSession = session
)
}
Report a failure on the line that caused it.
morpho's parser attaches a line to syntax errors, and those are relative to the box's text, so they need offsetting by where the fragment started. Compiler failures usually carry no line at all, and are pinned to the directive — which is nearly always the real culprit, since the commonest compile failure is asking for a width a cell cannot divide.
private fun fail(execute: Execute, e: Throwable, directiveLine: Int) {
val reported = (e as? MorphoException)?.line
val line = if (reported != null) execute.line + reported - 1 else execute.line + directiveLine
inside.displayError(e.message ?: e.javaClass.simpleName, line = line, property = "code")
}
private fun describe(circuit: MorphoCircuit): String {
val n = circuit.netlist
val ports = n.inputWidths.joinToString(", ") + " -> " + n.outputWidths.joinToString(", ")
return """
<div style="font-family:Iosevka Web, monospace; font-size:12px; line-height:1.5">
<b>${circuit.design}</b>(${circuit.signature.joinToString(", ")})
<span style="opacity:.6">$ports</span><br>
${n.gateCount} gates in ${n.activeCount} cells ·
${n.nNets} nets · depth ${circuit.depth.toInt()} ·
${circuit.expansions} expansions
</div>
""".trimIndent()
}
---------------------------------------------------------------- line notes
internal fun lineNotes(code: String, directive: Directive, circuit: MorphoCircuit): List<LastRunNotes.Produced>
What each part of code came to in the grown circuit, as line notes: the # main line is
the circuit; a Name = LUT(…) line is how many gates it became; a def (with its decorator
and body) is how many times it was expanded, at which widths, and the gates beneath it.
body — 47 lines, from line 238
internal fun lineNotes(code: String, directive: Directive, circuit: MorphoCircuit): List<LastRunNotes.Produced> {
val n = circuit.netlist
val lines = code.split("\n")
// A cell's name is `design:[widths]` for an expanded morpho cell, the LUT's own name for a gate.
fun cellName(cell: Int) = n.name(cell).substringBefore(':')
val expansions = (0 until n.nCells).filter { n.type(it) == MorphoNetlist.MORPHO }
.groupBy({ cellName(it) }, { n.name(it).substringAfter(':', "") })
val gatesUnder = HashMap<String, Int>()
val gatesOf = HashMap<String, Int>()
for (cell in n.activeCells) {
if (!n.isLut(cell)) continue
gatesOf.merge(cellName(cell), n.slots(cell), Int::plus)
val seen = HashSet<String>()
var up = n.parent(cell)
while (up >= 0) {
if (n.type(up) == MorphoNetlist.MORPHO && seen.add(cellName(up))) gatesUnder.merge(cellName(up), n.slots(cell), Int::plus)
up = n.parent(up)
}
}
fun plural(count: Int, word: String) = "$count $word" + if (count == 1) "" else "s"
val out = mutableListOf<LastRunNotes.Produced>()
out += LastRunNotes.Produced(directive.line + 1, directive.line + 1,
"${circuit.design}(${circuit.signature.joinToString(", ")}): ${plural(n.gateCount, "gate")} in " +
"${plural(n.activeCount, "cell")}, depth ${circuit.depth.toInt()}, ${plural(circuit.expansions, "expansion")}")
val lut = Regex("""^([A-Za-z_]\w*)\s*=\s*LUT\s*\(""")
val def = Regex("""^def\s+([A-Za-z_]\w*)\s*\(""")
lines.forEachIndexed { index, line ->
lut.find(line)?.let { m ->
val name = m.groupValues[1]
out += LastRunNotes.Produced(index + 1, index + 1, "$name: ${plural(gatesOf[name] ?: 0, "gate")} in this circuit")
}
def.find(line)?.let { m ->
val name = m.groupValues[1]
val first = generateSequence(index) { (it - 1).takeIf { i -> i >= 0 && lines[i].trimStart().startsWith("@") } }.last()
var last = index
while (last + 1 < lines.size && (lines[last + 1].isBlank() || lines[last + 1].first().isWhitespace())) last++
while (last > index && lines[last].isBlank()) last--
val widths = expansions[name].orEmpty()
val caption = if (widths.isEmpty()) "$name: not used by ${circuit.design}"
else "$name: expanded ${plural(widths.size, "time")} (" +
widths.groupingBy { it }.eachCount().entries.joinToString(", ") { (w, c) -> if (c == 1) w else "$w ×$c" } +
"), ${plural(gatesUnder[name] ?: 0, "gate")} beneath it"
out += LastRunNotes.Produced(first + 1, last + 1, caption)
}
}
return out
}
---------------------------------------------------------------- execute
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit) =
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit) =
growOnce(inside, execute, "fragment", reply)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit) =
Compile and grow the whole circuit, once.
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit) =
growOnce(inside, execute, "run", reply)
One compile-and-grow of the circuit, reported under mode. See DatalogExecution.runOnce.
private fun growOnce(inside: Box, execute: Execute, mode: String, reply: (Message) -> Unit) {
val originSession = thisSession.get()
val trace = ExecutionTrace.forEngine(inside, execute, mode, reply)
inMain {
val generation = ++inside.fragmentGeneration
send(Message("is-running").payload(IsRunning(execute.uid, true, generation, "__fragment__")))
configureOutputProxies(inside, trace.reply, originSession)
var ok = false
try {
val directive = parseDirective(execute.code)
if (directive == null) {
inside.displayError(missingDirectiveMessage(execute.code), line = execute.line, property = "code")
return@inMain
}
try {
inside.morphoCircuit?.close()
val circuit = MorphoCircuit.compile(
source = sourceFor(execute.code),
design = directive.design,
signature = directive.signature,
optimize = directive.optimize,
).growFully()
inside.morphoCircuit = circuit
clearErrors(execute, trace.reply, originSession)
report(execute, trace.reply, originSession, describe(circuit))
LastRunNotes.record(inside, "code", execute.code, execute.line, lineNotes(execute.code, directive, circuit))
ok = true
} catch (e: Throwable) {
if (e is InterruptedException) throw e
fail(execute, e, directive.line)
LastRunNotes.record(inside, "code", execute.code, execute.line, emptyList(),
stoppedAt = (e as? MorphoException)?.line ?: (directive.line + 1), stopReason = e.message ?: e.javaClass.simpleName)
}
} finally {
trace.finish(ok)
send(Message("is-running").payload(IsRunning(execute.uid, false, generation, "__fragment__")))
}
}
}
---------------------------------------------------------------- launch
override fun launchAll
Grow the circuit a few cells per frame, relaxing the 3D layout between expansions.
The strand keeps running after growth completes so the layout can settle — and so a drawing box downstream keeps seeing a live circuit rather than one that stopped moving mid-flight.
body — 72 lines, from line 344
override fun launchAll(
inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit
) {
val originSession = thisSession.get()
val trace = ExecutionTrace.forEngine(inside, execute, "launch", reply)
inMain {
configureOutputProxies(inside, trace.reply, originSession)
val directive = parseDirective(code)
if (directive == null) {
inside.displayError(missingDirectiveMessage(code), line = execute.line, property = "code")
trace.finish(false, "the box named no design directive")
return@inMain
}
val circuit = try {
inside.morphoCircuit?.close()
MorphoCircuit.compile(
source = sourceFor(code),
design = directive.design,
signature = directive.signature,
optimize = directive.optimize,
)
} catch (e: Throwable) {
if (e is InterruptedException) throw e
fail(execute, e, directive.line)
trace.finish(false, "the circuit did not compile")
return@inMain
}
inside.morphoCircuit = circuit
clearErrors(execute, trace.reply, originSession)
strandJobs[strand]?.cancel()
val generation = inside.runGeneration.getOrPut(strand) { -1 } + 1
inside.runGeneration[strand] = generation
send(Message("is-running").payload(IsRunning(execute.uid, true, generation, strand)))
// Compiled, installed and the growth strand started. Whether the layout settles
// prettily over the next few hundred frames is not something a setup receipt claims.
trace.finish(true)
strandJobs[strand] = inMain {
try {
var lastReported = -1
var noted = false
while (inside.runGeneration[strand] == generation) {
try {
val growing = circuit.step(grow = directive.grow, relax = directive.relax)
// The notes describe the grown circuit, so they are recorded once it is.
if (!growing && !noted) {
noted = true
LastRunNotes.record(inside, "code", code, execute.line, lineNotes(code, directive, circuit))
}
if (circuit.expansions != lastReported) {
lastReported = circuit.expansions
inside.output.display("morpho", describe(circuit))
}
} catch (e: Throwable) {
if (e is InterruptedException) throw e
lprintln("error,morpho", e.stackTraceToString())
fail(execute, e, directive.line)
break
}
kotlinx.coroutines.yield()
}
} finally {
send(Message("is-running").payload(IsRunning(execute.uid, false, generation, strand)))
}
}
}
}
override fun stop(inside: Box)
override fun stop(inside: Box) {
strandJobs.values.forEach { it.cancel() }
strandJobs.clear()
inside.runGeneration.keys.toList().forEach { strand ->
inside.runGeneration[strand] = (inside.runGeneration[strand] ?: 0) + 1
}
}
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit) {
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit) {
executeAll(
inside,
Execute(uid = executeSymbol.uid, type = "fragment", line = executeSymbol.line,
code = executeSymbol.symbol, singleLine = true),
reply
)
}
---------------------------------------------------------------- completion
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit) {
Complete cell and LUT names out of the box's own source, plus morpho's primitives.
The names come from actually parsing the buffer with morpho's parser, so completion tracks whatever the user has just written rather than a hard-coded list — and it silently offers nothing while the buffer is mid-edit and unparseable, which is the right failure.
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit) {
val code = completionRequest.allCode
val cursor = completionRequest.position.coerceIn(0, code.length)
var start = cursor
while (start > 0 && (code[start - 1].isLetterOrDigit() || code[start - 1] == '_')) start--
val prefix = code.substring(start, cursor)
val fromSource = runCatching { MorphoRuntime.cellNames(sourceFor(code)) }.getOrDefault(emptyList())
val documentation = PRIMITIVES + fromSource.associateWith { "cell defined in this box" }
val matches = (PRIMITIVES.keys + fromSource)
.distinct()
.filter { it.startsWith(prefix, ignoreCase = true) }
.sorted()
if (matches.isEmpty()) return
reply(
Message("complete").payload(
Completion(
boundsStart = start,
boundsEnd = cursor,
metadata = matches.map { name ->
Metadata(
icon = if (name in PRIMITIVES) "primitive" else "cell",
tail = if (name in PRIMITIVES) "primitive" else "cell",
documentation = documentation[name] ?: "",
text = name,
displayText = name
)
}
)
)
)
}
A box with no cells of its own still gets morpho's standard library, so the shortest useful morpho program in Field is a single directive line.
private fun sourceFor(code: String): String =
if (MorphoRuntime.cellNames(code).isEmpty()) {
MorphoRuntime.standardLibrary + "\n" + code
} else code
private fun missingDirectiveMessage(code: String): String {
val available = runCatching { MorphoRuntime.cellNames(sourceFor(code)) }.getOrDefault(emptyList())
val example = available.firstOrNull { it == "ripple_adder" } ?: available.firstOrNull() ?: "ripple_adder"
return "no '# main …' directive. Add one, e.g. # main $example(8, 8, 1)" +
if (available.isEmpty()) "" else
" — cells available: ${available.take(12).joinToString(", ")}" +
if (available.size > 12) ", …" else ""
}
}
MorphoHDL's built-in operators, with the documentation completion shows.
private val PRIMITIVES: Map<String, String> = mapOf(
"LUT" to "LUT(arity, table) — define a gate from a truth table, e.g. LUT(3, 0b1001_0110)",
"SPLIT" to "SPLIT(x) — halve a bus; raises the boundary that ends a recursion when x is 1 bit",
"CAT" to "CAT(a, b, …) — concatenate buses",
"REPEAT" to "REPEAT(x, ref) — tile x to the width of ref",
"LSLICE" to "LSLICE(x, ref) — split off the low len(ref) bits of x",
"HSLICE" to "HSLICE(x, ref) — split off the high len(ref) bits of x",
"ZERO" to "ZERO — the constant 0 wire",
"ONE" to "ONE — the constant 1 wire",
"VOID" to "VOID — the zero-width bus",
)
Box properties
var Box.isMorphoRoot by Delegation<Boolean?>()
var Box.morphoCircuit by Delegation<MorphoCircuit?>()
The circuit this box most recently built.
This is the handoff between the two halves of the bridge. A morpho box writes it; a Kotlin box beside it reads it and draws:
val circuit = parent.children.firstNotNullOf { it.morphoCircuit }
showPlaneGrid(circuit.flines())
Registration
fun installMorpho()
@RunsAt(early)
fun installMorpho() {
Box.ioPersistentProperties.add("isMorphoRoot")
registerMainLanguageRuntime(
MainLanguageRuntime(
id = "morpho",
label = "MorphoHDL",
runtimeClass = MorphoExecution::class,
setLegacyRoot = { box, enabled -> box.isMorphoRoot = enabled },
applyTopology = ::morphoTopologyNeedsUpdating,
),
)
}
fun installMorphoPlugin()
@RunsAt(plugins)
fun installMorphoPlugin() {
TopologyWatcher("morpho") { morphoTopologyNeedsUpdating(it) }.start()
}
fun morphoTopologyNeedsUpdating(values: Collection<Box>)
fun morphoTopologyNeedsUpdating(values: Collection<Box>) {
applyRuntimeToggle(
boxes = values,
isRoot = { it.isMorphoRoot == true || it.isMainLanguage("morpho") },
runtimeClass = MorphoExecution::class,
newExecution = { child -> MorphoExecution(child) },
language = "MorphoHDL",
customization = "",
notifyCategory = "morpho"
)
}