Skip to content

Languages

Field's small languages, from the host side. A box can be written in one of them — a per-box property picks the language and a BoxExecution runs it, which is how datalog, forth, nano, morpho, clojure, python, FieldML and the two remotejs runtimes work — or a program can be embedded, a triple-quoted Kotlin string whose first line names the language and whose contents are compiled on the JVM, which is how RectSL and StackSL work. What is documented below is the Kotlin that either shape hands you: the compiler you give a program to, the result and the diagnostics it gives back, and the bridge a running program calls host values through. The notation itself is on the cheatsheets, and that is where the pictures are. Read Datalog.kt first — it is the smallest complete example of the shape all of these have, a value you add source to, query, and then ask why of — then DatalogEngine.kt for what the implementation underneath actually costs, StackSL.kt for the compile-and-diagnose surface both embedded languages share, ForthInterpreter.kt and NanoVM.kt for the two implementations you can read end to end in a sitting, and HostBridge.kt for FieldML's reflective JVM bridge — the file to read before wiring any language to Kotlin values. dev_docs/ carries the map of what a new language has to touch on the way in, in small_language_anatomy.md, which is also the guarded list of which languages exist. The prose, with pictures: Language extensions for the route decision, then Datalog, RectSL, StackSL, FieldML, MorphoHDL, Nano, Forth and Python.

123 source file(s), 3103 public symbol(s), 1527 of them carrying KDoc. Everything below is the documentation that lives beside the code — the same text the completion documentation panel shows in the editor. A blank summary means the declaration has no KDoc yet, not that this page dropped it.

Datalog.kt

The facade over parser and engine: a Datalog holds a program, answers queries as DlRows, and reconstructs the proof of a derived fact.

Nothing here parses or evaluates. DatalogParser turns text into the DlUnit this file merges and DatalogEngine computes the model; what is left is the part a caller touches — holding the program, noticing when the cached model has gone stale, and sorting rows so that a query prints the same way twice, which set semantics on their own do not promise.

The exception is the top-down proof search behind why, which is here rather than in the engine because it reads a finished model instead of building one. It walks rules backwards from a derived fact, bounded by the round numbers the fixpoint recorded, and picks premises in the same order the planner would. The opposite question, why a goal has no answers at all, is DatalogWhyNot.kt.

import afield.data.query.datalog.* is in every Kotlin box's preamble, so Datalog() and the Dl types are in scope with nothing to import.

src/jvmMain/kotlin/afield/data/query/datalog/Datalog.kt — read the source

Datalog

class Datalog

Datalog for Field — a small deductive database you can point at your own data, or at the live box graph (see DatalogBoxes.kt), or write directly in a box (see afield.data.query.datalog.DatalogExecution).

Declaration Summary
val rules: List<DlRule>
val queries: List<DlQuery>
val facts: Map<String, DlRelation> Base relations exactly as given — before any rule has fired.
fun add(source: String): DlUnit Parse source and merge it in. Ground facts become base relations, everything else a rule; ?- goals are recorded in queries but not run.
fun addParsed(unit: DlUnit): DlUnit Merge an already-parsed unit. The box language parses first so it can rewrite a bare non-ground atom — path(a, Z) typed on its own — into a query before it reaches addRule, which would rightly reject it as a fact that is not ground.
fun addRule(rule: DlRule): Datalog Merge one rule (or ground fact).
fun fact(pred: String, vararg args: Any?): Datalog Add one fact. Values go through dlNormalize, so fact("age", "ada", 36) stores a Long.
fun facts(pred: String, rows: Iterable<Iterable<Any?>>): Datalog Add many rows at once.
fun <T> facts(pred: String, items: Iterable<T>, columns: (T) -> List<Any?>): Datalog Add many rows by projecting Kotlin objects into columns.
fun reset() Forget everything — facts, rules, queries, and the cached model.
fun clearFacts(pred: String) Drop a single relation's base facts (rules that derive into it are untouched).
fun solve(): DlSolution The saturated model. Cached until the program changes.
fun query(goal: String): DlRows Evaluate a goal — a body conjunction, exactly as it would be written on the right of :-.
fun ask(goal: String): Boolean True when the goal has at least one solution.
fun rowsOf(pred: String): List<DlRow> All rows of one relation in the current model, sorted.
fun why(goal: String): DlDerivation? Why is this fact true? Returns a derivation tree, or null if the fact is not in the model.
fun program(): String The program as source you could paste back in.
override fun toString(): String

DlRows

class DlRows(val goal: String, val columns: List<String>, val rows: List<DlRow>, val warnings: List<String> = emptyList()) : Iterable<DlRow>, ToHTML

A query result: named columns and sorted rows.

Declaration Summary
val goal: String
val columns: List<String>
val rows: List<DlRow>
val warnings: List<String>
override fun iterator(): Iterator<DlRow>
val size: Int
val isEmpty: Boolean
fun value(): Any? The single value of a single-column, single-row result — the common "just tell me" case.
fun column(name: String): List<Any?>
fun asMaps(): List<Map<String, Any?>>
fun table(limit: Int = 200): String A monospaced table, for a box's output area or a terminal.
override fun toString()
override fun toHTML(): String The same answer as table, as HTML — so display("main", d.query(…)) in a box shows a real table rather than a wall of monospace.

DatalogAst.kt

The abstract syntax of Field's Datalog — see Datalog for the facade, DatalogParser for the concrete syntax and DatalogEngine for what evaluates this.

Everything here is a value type with structural equality, because the engine leans on that in two places: rules are de-duplicated when the same text is executed twice (a live box re-runs its whole program on every keystroke-triggered execute), and derived rows are stored in hash sets.

Names are Dl-prefixed. Boxes get import afield.data.query.datalog.* for free (see afield.document.graph.intro), on top of the standard document, geometry and math imports, so an unprefixed Var, Atom or Rule in this package would be a name collision waiting to happen in user code.

src/jvmMain/kotlin/afield/data/query/datalog/DatalogAst.kt — read the source

Top-level functions

Declaration Summary
fun dlNormalize(value: Any?): Any? Coerce a host value into the small set of types the engine joins on: Long, Double, String, Boolean, null, or an opaque object compared by equals.
fun dlFormat(value: Any?): String How a value appears in output tables and in toString of an atom. Strings are unquoted here.
fun dlSourceFormat(value: Any?): String How a value appears when a fact is printed back as source — strings are quoted and escaped.

DlTerm

sealed interface DlTerm

A Datalog term: either a variable or a constant.

DlVar

data class DlVar(val name: String) : DlTerm

A logic variable. _ in source becomes a fresh DlVar per occurrence (named _1, _2, …), which is what makes emp(_, D, _) mean "some row" rather than "a row whose first and third columns are equal".

Declaration Summary
val isAnonymous: Boolean
override fun toString()

DlConst

data class DlConst(val value: Any?) : DlTerm

A constant. Values are normalised by dlNormalize: a bare lowercase identifier in source is a String, so edge(a, b) and edge("a", "b") are the same fact, and any integral number is a Long however it was written or computed.

Declaration Summary
override fun toString()

DlArith

sealed interface DlArith

An arithmetic/string expression, usable on either side of a comparison or bind.

DlAValue

data class DlAValue(val term: DlTerm) : DlArith

DlABinary

data class DlABinary(val op: String, val left: DlArith, val right: DlArith) : DlArith

DlAUnary

data class DlAUnary(val op: String, val operand: DlArith) : DlArith

DlACall

data class DlACall(val fn: String, val args: List<DlArith>) : DlArith

DlLiteral

sealed interface DlLiteral

A body item. Heads are always a positive DlAtom.

DlAtom

data class DlAtom(val pred: String, val terms: List<DlTerm>, val negated: Boolean = false) : DlLiteral

pred(t1, …, tn), or !pred(…) / not pred(…) when negated.

Declaration Summary
val arity: Int
override fun toString()

DlCompare

data class DlCompare(val op: String, val left: DlArith, val right: DlArith) : DlLiteral

a <op> b for < <= > >= != =.

DlAggregate

data class DlAggregate(val target: DlVar, val op: String, val over: DlArith?, val body: List<DlLiteral>) : DlLiteral

Target = <op> <over> : { body } — an aggregate over the solutions of body, grouped by whichever of body's variables are already bound by the enclosing rule.

Declaration Summary
val target: DlVar
val op: String
val over: DlArith?
val body: List<DlLiteral>

DlRule

data class DlRule(val head: DlAtom, val body: List<DlLiteral>, val source: String? = null)

head :- body., or head. when body is empty (a fact). source is for error messages.

Declaration Summary
val isFact: Boolean
override fun toString()

DlQuery

data class DlQuery(val body: List<DlLiteral>, val source: String? = null)

A parsed ?- goal. directive.

DlDirective

data class DlDirective(val name: String, val argument: String, val line: Int)

A #directive line — the box language's REPL commands (#reset, #relations, #boxes, #why …).

DlStatement

class DlStatement(val firstLine: Int, val lastLine: Int, val rule: DlRule? = null, val query: DlQuery? = null, val directive: DlDirective? = null)

One statement of a program and the lines it spans (firstLine..lastLine, 1-based): exactly one of rule (a fact is a rule with no body), query or directive. See DatalogParser.statements.

Declaration Summary
val firstLine: Int
val lastLine: Int
val rule: DlRule?
val query: DlQuery?
val directive: DlDirective?

DlUnit

data class DlUnit(val rules: List<DlRule> = emptyList(), val queries: List<DlQuery> = emptyList(), val directives: List<DlDirective> = emptyList())

Everything one chunk of source contributed.

Declaration Summary
val rules: List<DlRule>
val queries: List<DlQuery>
val directives: List<DlDirective>

DlException

class DlException(message: String, val line: Int = -1, val column: Int = -1) : RuntimeException(message)

A parse, safety or stratification failure. line is 1-based; -1 when not known.

Declaration Summary
override fun toString()

DatalogBoxes.kt

The Field bridge: the live box graph as Datalog base relations.

This is the reason for having a Datalog in here rather than shelling out to one. A Field document is a labelled graph — boxes, parent/child edges, nesting, and a property bag per box — and the questions you actually ask of it while working ("which boxes does nothing run into?", "which two boxes both bind the same property?", "what is downstream of this one?") are recursive joins, which is precisely what afield.data.query.graph's path language cannot express and this can.

val d = Datalog()
d.addBoxGraph(thisBox)
d.add("""
    reaches(A, B) :- child(A, B).
    reaches(A, C) :- reaches(A, B), child(B, C).
    orphan(X) :- box(X), !child(_, X).
""")
println(d.query("orphan(X), name(X, N)").table())

src/jvmMain/kotlin/afield/data/query/datalog/DatalogBoxes.kt — read the source

Used in: Datalog in a box

Top-level functions

Declaration Summary
fun dlBoxRelationTable(): Table dlBoxRelationSchema as a table, so a box (or #help, or the documentation) can print the schema rather than restate it — a restatement is a copy that goes stale.
fun Datalog.addBoxGraph(root: Box, includeProperties: Boolean = true, maxTextLength: Int = 4000): Datalog Load root's whole document into this program as base relations. Returns this.
fun Box.datalog(program: String = ""): Datalog A Datalog preloaded with this box's document and, optionally, a program.

Top-level values

Declaration Summary
val dlBoxRelationSchema: List<Pair<String, String>> What addBoxGraph defines, for #help in the box language and for the docs.

DatalogEditorExtension.kt

Serves datalog.js, the browser-side Datalog editor support, to the page and to the test that checks it.

The JavaScript is the implementation and this file is a reader for it. datalog.js lives in src/commonMain/resources, reaches the browser as the AMD module bithero/datalog (one of the modules bootstrapMonaco.js requires by name while the editor starts), and is read back verbatim here, so the smoke test datalog.editor-extension.workout runs the same bytes the editor does. A browser-side language that can only be tested in a browser does not stay correct.

Once installed, the extension brings syntax highlighting, lexical diagnostics, a context badge and completions to #datalog regions embedded in Kotlin triple-quoted strings, and to whole .datalog documents. Colouring is decoration-based rather than a Monarch grammar, so one tokenizer covers both contexts and there is no grammar here that can fail to compile and take Monaco's startup down with it. The same workout also compares the editor's vocabulary against DatalogParser's, since the editor is JavaScript in a browser and the parser is Kotlin on a server, and the two lists can only be kept in step at build time.

datalog.js installs itself either way it is loaded: as an AMD module when a Monaco loader is present, and immediately when it is evaluated bare, which is the path html() uses.

src/jvmMain/kotlin/afield/data/query/datalog/DatalogEditorExtension.kt — read the source

Top-level functions

Declaration Summary
fun datalogEditorExtensionJavascript(): String DatalogEditorExtension.javascript, as a top-level function.
fun datalogEditorExtensionHtml(): String DatalogEditorExtension.html, as a top-level function.

DatalogEditorExtension

object DatalogEditorExtension

The Datalog editor extension, as bytes: the resource itself, and a page fragment that installs it.

Declaration Summary
const val id: String The extension's id, which is also the extensionId datalog.js registers itself under.
fun javascript(): String The bytes of datalog.js.
fun html(): String The extension as a drop-in HTML fragment: a titled line of prose saying what it does, with the whole of datalog.js inlined in a script tag.

DatalogEngine.kt

Storage and evaluation: indexed relations, a safety-directed body planner, stratification, and a semi-naive fixpoint.

Three invariants hold the thing together, and each one buys a feature:

  • Rows are planned, not interpreted. Each rule is compiled once into a list of DlSteps over integer variable slots, and the planner decides statically whether each argument position binds or filters. Nothing consults a "is this variable bound yet" map at join time.
  • Facts derived in a round are not visible until the round ends. New rows go to a pending buffer and are merged at the end of the iteration, so every fact's DlRelation.roundOf is strictly greater than the rounds of the facts that derived it. That is what makes Datalog.why terminate without an occurs-check hack: a proof may only descend to strictly earlier rounds. The counter is global, not per-stratum, so the same ordering holds across a stratum boundary and a proof may descend through negation and aggregation too.
  • Negation and aggregation are stratified. Both are "negative" edges in the dependency graph; a negative edge inside a strongly-connected component is rejected up front with the cycle spelled out, rather than producing an answer that depends on evaluation order.

src/jvmMain/kotlin/afield/data/query/datalog/DatalogEngine.kt — read the source

Top-level functions

Declaration Summary
fun dlCompareValues(a: Any?, b: Any?): Int A total order over Datalog values, so < never throws and min/max/collect are deterministic across runs (which the smoke test depends on, and a user comparing two query outputs does too). Across kinds the order is null < boolean < number < string < anything else by class name.
fun dlAggregateValue(op: String, solutions: Int, values: List<Any?>): Any? Combine an aggregate's group into its one value. solutions is how many times the subgoal succeeded (which is what count reports) and values are the evaluations of the over expression, one per solution — empty for count, which has no over.
fun dlCallFunction(name: String, args: List<Any?>): Any? The builtin functions of DatalogParser.functions. Kept out of the engine class so the parser, the engine and the box language's hover documentation all name the same list.
fun dlEvalArith(arith: DlArith, lookup: (String) -> Any?): Any? Evaluate an expression, resolving variables through lookup. Shared by the fixpoint and Datalog.why.
fun dlCompareOp(op: String, left: Any?, right: Any?): Boolean = and != treat 2 and 2.0 as equal; the ordering comparisons use dlCompareValues.

DlRow

typealias DlRow = List<Any?>

A tuple. Elements are already dlNormalized.

DlRelation

class DlRelation(val name: String, val arity: Int)

One predicate's extension, with lazily-built hash indices.

Declaration Summary
val size: Int
fun rows(): Collection<DlRow>
fun contains(row: DlRow): Boolean
fun roundOf(row: DlRow): Int The fixpoint iteration this row first appeared in; 0 for facts stated in the source.
fun add(row: DlRow, round: Int = 0): Boolean
fun lookup(pattern: Array<Any?>, mask: Int): Collection<DlRow> Rows matching pattern at the positions set in mask.
override fun toString()

DatalogEngine

class DatalogEngine(private val rules: List<DlRule>, private val base: Map<String, DlRelation>)

Evaluates a set of rules against a set of base facts. Owned by Datalog; not usually built directly.

Declaration Summary
val warnings: List<String>
fun run(): DlSolution

DlPremiseKind

enum class DlPremiseKind

What a premise is to the step above it. A proof tree is not homogeneous, and a reader who cannot tell a rule body from an aggregate's group is reading a tree that means several different things in the same shape.

Constants: DERIVED, AGGREGATE, GROUP, ABSENCE.

DlDerivation

class DlDerivation(val fact: String, val rule: DlRule?, val premises: List<DlDerivation>, val note: String? = null, val kind: DlPremiseKind = DlPremiseKind.DERIVED, val contribution: String? = null) : ToHTML

One step of a proof: a fact, the rule that produced it, and the facts that rule consumed.

Declaration Summary
val fact: String
val rule: DlRule?
val premises: List<DlDerivation>
val note: String?
val kind: DlPremiseKind
val contribution: String?
fun render(indent: String = ""): String
fun because(limit: Int = 72): String What to write beside a derived fact: the whole rule, not just its head.
override fun toString()
override fun toHTML(): String The proof as an indented HTML tree — a fact per line, in the accent colour, with the rule that produced it beside it in grey. Not a Table: the shape is the answer here, and a table would flatten the one thing worth looking at.

DlSolution

class DlSolution(val relations: Map<String, DlRelation>, val rules: List<DlRule>, val warnings: List<String>) : ToHTML

A computed model: every relation, saturated, plus the rules that made it.

Declaration Summary
val relations: Map<String, DlRelation>
val rules: List<DlRule>
val warnings: List<String>
operator fun get(pred: String): DlRelation?
fun rows(pred: String): Collection<DlRow>
val factCount: Int
fun summary(): List<String> pred/arity -> row count, in insertion order — what #relations prints.
override fun toString()
override fun toHTML(): String Every relation and how big it got — what #relations prints, as a table.

DatalogInterface.kt

Datalog as a box language: the DatalogExecution that runs fragments inside a box, and the registration that puts Datalog in the Main Language menu.

DatalogExecution is the BoxExecution. It merges a fragment into the box's accumulated program, prints whatever that fragment asked for, and completes on the relations the box has actually defined as well as on the builtin functions, the aggregate operators and the box-graph names. installDatalog registers the runtime and the persistent isDatalogRoot flag, and datalogTopologyNeedsUpdating attaches and detaches executions as boxes are marked and unmarked.

The #directives are the Field-specific part, and the reason this is more than a thin adapter: #boxes loads the live document into the base relations of dlBoxRelationSchema, #why and #whynot reach the two provenance layers, and #program prints back what the box has accumulated. directiveHelp is the one list that #help prints and completion offers, so a directive added to it appears in both.

src/jvmMain/kotlin/afield/data/query/datalog/DatalogInterface.kt — read the source

Top-level functions

Declaration Summary
fun installDatalog()
fun installDatalogPlugin()
fun datalogTopologyNeedsUpdating(values: Collection<Box>)

Top-level values

Declaration Summary
var Box.isDatalogRoot: Boolean?

DatalogExecution

class DatalogExecution(val inside: Box) : BoxExecution

Datalog as a box language — set a box's Main Language to Datalog and its children hold Datalog programs instead of Kotlin.

Declaration Summary
val datalog
var lastRunAt: Long? When the box last ran (milliseconds since the epoch), or null: what its line notes say the model's age is (DatalogLineNotes).
override val languageName: String
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit) There is no per-frame Datalog. Launch runs the program once, like execute — a box that is "running" forever while holding a finished model would only lie in the UI.
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override fun stop(inside: Box)
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit)
fun runProgram(source: String): String Merge a fragment and report on it. Visible for the smoke test and the backdoor; returns what would be printed rather than printing, so it is testable without a UI.

DatalogLineNotes.kt

Line notes for a Datalog box (LineNotes): what a line's statement means in the model the box's runs have built.

A Datalog box accumulates: every run merges its facts and rules into one program, and the saturated model of that program is the state the box is in. So a note needs no record of the last run — it asks the model, and asking is pure. A rule's note is the facts it derives there (its body, evaluated over the model, projected onto its head), a query's its answers, a fact's whether it is in the model and which rules read its relation. A statement the model does not hold — text written or changed since the line last ran — says so, rather than answering for text the model has never seen. Directives are never run for a note: #reset and #boxes change the model.

src/jvmMain/kotlin/afield/data/query/datalog/DatalogLineNotes.kt — read the source

Top-level functions

Declaration Summary
fun installDatalogLineNotes() Datalog boxes' line notes. The model belongs to the main thread, which runs the box, so the note is worked out there; a main thread busy for more than a moment gets no note rather than a racing one.

DatalogLineNotes

object DatalogLineNotes

Computes the LineNotes.Note for a line of a Datalog box from the box's current model.

Declaration Summary
fun note(datalog: Datalog, source: String, line: Int, ranAt: Long?, now: Long = System.currentTimeMillis()): LineNotes.Note? The note for line (1-based) of source against datalog's model, which the box's last run left ranAt milliseconds since the epoch (null: never run); null when the line is in no statement.

DatalogObjects.kt

Any live object graph as Datalog base relations, by way of afield.data.query.graph.

The two layers answer different halves of the same question. graphPaths addresses slots by pattern and hands back live lenses, but a glob cannot follow an edge and then follow it again, and it has nothing to say about two addresses that turn out to be the same object. Datalog has recursion, negation and aggregation and no idea what an object is. Joining them costs one walk:

val d = Datalog()
d.addObjectGraph(rig)
d.add(dlObjectGraphRules)
d.query("alias(A, B)")                                  // two names for one object
d.query("reaches(\"carrier\", P), writable(P)")         // settable, anywhere below carrier

Identity is what makes it worth the trip. dlObjectGraphSchema's at(Path, ObjectId) gives every non-scalar value a number, so aliasing needs no special relation — it is a join of at with itself. The walk already visits a shared node under every alias (its cycle detection is path-scoped, not visit-once), so the facts are there to be joined.

src/jvmMain/kotlin/afield/data/query/datalog/DatalogObjects.kt — read the source

Used in: Datalog in a box

Top-level functions

Declaration Summary
fun dlObjectGraphTable(): afield.ui.visualization.Table dlObjectGraphSchema as a table, so nothing has to restate it.
fun Datalog.addObjectGraph(root: Any?, pattern: String = "**", options: GraphWalkOptions = GraphWalkOptions(), maxTextLength: Int = 4000): Datalog Walk root and load it into this program. Returns this.

Top-level values

Declaration Summary
val dlObjectGraphSchema: List<Pair<String, String>> What addObjectGraph defines. Rendered by dlObjectGraphTable.
val dlObjectGraphRules: String Rules worth having on top of addObjectGraph, as source you can add.

DatalogParser.kt

Source text to abstract syntax: the lexer and recursive-descent parser every Datalog program in Field goes through, and the two word lists that decide what the notation reserves.

There is no error recovery. The first thing the parser cannot read raises a DlException carrying the line and column it stopped at, and the box language turns those into an error marker on the offending line, so a program is either whole or it is a single message.

Two of the tables outlive the parse. DatalogParser.functions and DatalogParser.aggregateOps are what the box language offers in its completion popup, and the browser-side highlighter keeps its own copy of both; afield.examples.datalog.DatalogEditorExtensionWorkout runs the shipped JavaScript against these sets and fails when the two have drifted apart.

src/jvmMain/kotlin/afield/data/query/datalog/DatalogParser.kt — read the source

DatalogParser

object DatalogParser

The concrete syntax. One hand-written lexer + recursive-descent parser, no dependencies.

Declaration Summary
val aggregateOps
const val LANGUAGE_TAG The embedded-region header. #datalog on its own line marks a program, and is ignored by the parser; datalog.js looks for the same word to decide which part of a Kotlin file to highlight as Datalog.
val functions The builtin functions usable inside expressions. A name here cannot also be a predicate — see the class KDoc; the parser resolves f(...) towards an expression when f is in this set.
fun parse(source: String): DlUnit
fun statements(source: String): List<DlStatement> source's statements in order, each with the lines it spans — what a line note needs to know which statement a line belongs to. Throws, as parse does, at the first statement that does not parse.
fun parseGoal(source: String): List<DlLiteral> Parse a body conjunction on its own — how Datalog.query and #why read their goal.

DatalogSmokeTest.kt

Smoke test for the Datalog deductive-database language in afield.data.query.datalog.

Like the afield.data.query.graph smoke this is a plain jvm-main entrypoint, which works because the engine touches no Runloop, starts no threads and opens no devices, so the JVM exits naturally (see the smoke-tests-natural-exit convention). The live box-graph bridge (DatalogBoxes.kt) and the box language live outside what this file imports.

The central case is testSemiNaiveAgreesWithBfs: the incremental fixpoint must produce exactly the transitive closure an independent breadth-first search produces, over a random graph with cycles. The other cases pin individual semantics: recursion, negation and stratification, arithmetic, aggregates, safety errors and the query API.

src/jvmMain/kotlin/afield/data/query/datalog/DatalogSmokeTest.kt — read the source

DatalogSmokeTest

object DatalogSmokeTest

The datalog.core.smoke entrypoint; prints "Datalog smoke passed" on success.

Declaration Summary
fun main(args: Array<String>)
fun runTests(): Boolean

DatalogWhyNot.kt

The why-not layer: Datalog.whyNot walks a failing conjunction literal by literal and names the one that took the answer count to zero.

Datalog.why accounts for a fact that is present. This accounts for a query that is empty, which is the commoner question and the one a saturated model cannot be asked directly. DlWhyNot is the result, DlNarrowing one step of the walk, and #whynot <goal> in a Datalog box is the way in.

Everything here is written against the public Datalog surface, which is why it is a file of its own rather than more of the fixpoint's. The counts it reports are the engine's rather than a second implementation's: each prefix of the conjunction is synthesised into a rule and put through DatalogEngine, at a cost of one solve per literal.

src/jvmMain/kotlin/afield/data/query/datalog/DatalogWhyNot.kt — read the source

Top-level functions

Declaration Summary
fun Datalog.whyNot(goal: String): DlWhyNot Explain why goal has no answers.

DlWhyNot

class DlWhyNot(val goal: String, val present: Boolean, val steps: List<DlNarrowing>, val witness: Map<String, Any?>, val attempts: List<DlWhyNot> = emptyList(), val rule: DlRule? = null, val note: String? = null) : ToHTML

Why a goal has no answers — the other half of Datalog.why.

Declaration Summary
val goal: String
val present: Boolean True when the goal does have answers — ask Datalog.why instead.
val steps: List<DlNarrowing>
val witness: Map<String, Any?> One binding that survived the last step that had any answers — an example of how far the conjunction got before it emptied. Empty when nothing survived even the first literal.
val attempts: List<DlWhyNot> One entry per rule that could have produced a single-atom goal, best first.
val rule: DlRule? Which rule this attempt belongs to; null for the top-level goal.
val note: String?
val culprit: DlNarrowing? The first step whose answer count is zero — the literal that emptied the conjunction.
val depth: Int How many literals held before the walk stopped. What "got furthest" is measured on.
fun render(indent: String = ""): String
override fun toString()
override fun toHTML(): String The same walk as HTML — an indented tree in the house colours, matching DlDerivation.toHTML rather than a afield.ui.visualization.Table, because a why-not is a shape too: the attempt that got furthest is the answer, and a flat table hides which attempt each row belongs to.

DlNarrowing

class DlNarrowing(val step: Int, val literal: String, val answers: Int?)

One step of a why-not walk: a literal, and how many answers the conjunction had once it was added. answers is null when the prefix could not be evaluated on its own — a comparison written before the atom that binds its variable, most often, which is itself worth being told.

Declaration Summary
fun describe(): String

ClojureInterface.kt

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, and executeIfFn will invoke that value when it is an IFn — a host convenience, off on the box path (ClojureExecution passes false), so a box whose last form evaluates to a function prints the function rather than calling it. compileOnly is 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 (in src/jvmMain/resources) is the Clojure side of it, required and referred into every box namespace, so a box can say (lines ls :fit true) rather than naming SomeSkikoKt.
  • ClojureExecution is the BoxExecution, so it is the half that knows about boxes: output proxying, is-running, per-fragment generation counting, and complete, whose candidates come from Compliment if that namespace loads. Without it on the classpath complete returns 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 registerMainLanguageRuntime call, one TopologyWatcher that 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.

src/jvmMain/kotlin/afield/lang/clojure/ClojureInterface.kt — read the source

Top-level functions

Declaration Summary
fun install()
fun installClojure()
fun clojureTopologyNeedsUpdating(values: Collection<Box>)

Top-level values

Declaration Summary
var Box.isClojureRoot: Boolean?

StreamingWriter

class StreamingWriter(private val onText: (String) -> Unit) : Writer()

Declaration Summary
override fun write(cbuf: CharArray, off: Int, len: Int)
override fun flush()
override fun close()

CompileError

data class CompileError(val message: String, val line: Int? = null, val column: Int? = null, val causeClass: String? = null)

Declaration Summary
val message: String, val line: Int?

CompileOnlyResult

sealed class CompileOnlyResult

Declaration Summary
data class Ok(val formsCount: Int) : CompileOnlyResult()
data class Err(val errors: List<CompileError>) : CompileOnlyResult()

EvalValue

data class EvalValue(val value: Any?, val valueClass: String? = value?.let { it::class.java.name }, val line: Int = -1, val printed: String? = null)

Declaration Summary
val value: Any?
val valueClass: String?
val line: Int 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 printed: String? pr-str of value, so a Clojure box prints [1 2 3] and nil rather than Java's toString.

EvalResult

sealed class EvalResult

Declaration Summary
data class Ok(val result: EvalValue) : EvalResult()
data class Err(val error: CompileError) : EvalResult()

ClojureHost

class ClojureHost(private val defaultNs: String = "user")

A Clojure evaluator for one namespace: read a form, evaluate it, repeat, report the last value.

Declaration Summary
fun warmInBackground() Load Compliment off the runloop; its first load is about the popup's 15 s reply timeout.
fun compileOnly(code: String, ns: String = defaultNs): CompileOnlyResult Read code without evaluating any of it: the editor's syntax check.
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 Evaluate every top-level form in code, in order, and report the value of the last one.
data class CompletionItem(val candidate: String, val type: String? = null, val doc: String? = null, val ns: String? = null, val displayText: String? = null, val arglists: String? = null) One candidate, as Field's popup needs it.
fun complete(code: String, cursor: Int, ns: String = defaultNs): List<CompletionItem> Candidates at cursor, from Compliment.

ClojureInterface

object ClojureInterface

What a Clojure box can reach of Field, and the Kotlin half of field/core.clj.

Declaration Summary
var thisBox: Box? The box whose code is running, or null outside a box.
var thisLine: Int The box line of the top-level form being evaluated, or -1 outside one.
var currentFrames: Frames? The Frames whose body is running, or null outside a frame.
fun animate(body: IFn): Frames A Frames that calls body once per frame: animate { … } for a Clojure box.
fun endAnimation() Stop the animation whose frame is running — (end) in field.core.
fun frameNumber(): Int The frame number of the animation being run, or 0 outside one.
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.
fun polyline(points: List<*>): FLine An FLine through points, each a 2- or 3-element sequence of 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.
fun thickness(line: FLine, width: Double): FLine line with FLine.thickness set, returned so it can be threaded.
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) One (slider …) or (checkbox …) a run asked for, waiting for flushControls.
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.
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.
fun clearControls(box: Box) Forget whatever an earlier run of box asked for and did not get to draw.
fun flushControls(box: Box) Draw the controls box's run asked for, one controlGroup per channel, in the order asked.
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.
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).

ClojureExecution

class ClojureExecution(val inside: Box) : BoxExecution

Declaration Summary
val clj
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit) Launch: run the box once, then hand its last value to the same registry a Kotlin box's return value goes through.
override fun stop(inside: Box) Stop: cancel the jobs this box launched.
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override val languageName: String

FieldMLCompiler.kt

FieldML's front end behind one call: source text in, and whatever its four stages managed to produce out.

FieldMLCompiler runs the lexer, the parser, the inferencer and the lowerer in turn, stopping at the first one to report an error. What the earlier stages produced is kept either way, so a program that does not compile still carries a token list, usually a surface tree, and sometimes a typed tree. FieldMLCompletionEngine is built on that: it compiles a half-written line and reads the names in scope off the typed tree, or the constructor declarations off the surface tree when inference has given up.

Each stage lives in its own package under afield.lang.fieldml, and this file is where the order they go in is written down.

src/jvmMain/kotlin/afield/lang/fieldml/FieldMLCompiler.kt — read the source

Used in: FieldML stage by stage

CompilationResult

data class CompilationResult(val source: String, val tokens: List<Token>?, val surface: Expr?, val typed: TExpr?, val core: CoreExpr?, val diagnostics: List<Diagnostic>)

How far a compile got and what it found: one field per stage of the front end, null from the stage that failed onwards.

Declaration Summary
val source: String The text that was compiled. A diagnostic's range indexes into it.
val tokens: List<Token>? Lexer output. Non-null unless lexing itself failed.
val surface: Expr? Parser output: the surface tree, one node per piece of written syntax.
val typed: TExpr? Inferencer output: the same shape as surface with a type on every node.
val core: CoreExpr? Lowerer output: the curried tree the evaluator runs. Non-null exactly when the compile worked.
val diagnostics: List<Diagnostic> Everything every stage that ran had to say, in stage order.
val successful: Boolean True when the whole front end ran and nothing reported an error, so core can be evaluated.

FieldMLCompiler

object FieldMLCompiler

Lex, parse, infer, lower. The entry point for the box runtime, the completion engine and the tests.

Declaration Summary
fun compile(source: String): CompilationResult Compile source, reporting errors rather than throwing them.

FieldMLCompletionEngine.kt

Completion and hover for a FieldML box: what the editor offers under the caret, and what the card over a symbol says.

The difficulty is that the text is half-written, so the compiler will usually refuse it. Both entry points compile the box anyway and read the typed tree if one came back. When none did, they fall back to compiling the top-level statements above the caret and keeping the longest run of them that does compile, skipping the broken ones rather than stopping at the first, because the broken one is normally the line being typed. Below that is a regex over let, fun and lambda parameters, which finds the names in scope without their types.

Which kind of thing can go under the caret is decided before any candidate is gathered, and two of those contexts answer with nothing rather than falling through: inside the string literal of a host… intrinsic, and after a record field's dot, a member is the only thing that can legally follow, so an empty list is the right answer when no member matches. Falling through to the general list there is how img.wi came to offer the with keyword.

Everywhere else the list is five sources merged and deduplicated — snippets, keywords, the prelude, the bindings in scope, and the constructors this box declared — sorted by a rank that puts the closest thing first: a typed local binding at 0, a snippet at 10, a constructor at 16, the graphics prelude at 18, the rest of the prelude at 24, the host… names at 34, and a keyword last at 40. A |> to the left of the caret switches the prelude to a second table for any name whose final argument accepts what the pipe is carrying, which is what floats moveTo, map and the rest of pipePreferred to the top of a pipeline.

hostTap's completions are the one place this reads something other than the box: it lists and describes Kotlin extension functions out of Field's own source, through KotlinSourceIndex.

src/jvmMain/kotlin/afield/lang/fieldml/FieldMLCompletionEngine.kt — read the source

FieldMLCompletionEngine

object FieldMLCompletionEngine

Completion and hover for FieldML, worked out from the box's own text on every keystroke.

Declaration Summary
fun complete(request: CompletionRequest): Completion The completion list for a caret position, and the span of text it would replace.
fun describeSymbolContext(request: SymbolContextRequest): SymbolContextResponse The hover card for whatever is under the caret: a declaration line, a one-word detail saying what kind of thing it is, and whatever documentation there is for it.

FieldMLInterface.kt

FieldML as a box language: set a box's Main Language to FieldML and its children hold FieldML programs instead of Kotlin.

FieldMLExecution is the BoxExecution behind that, one per box. It hands the text to FieldMLCompiler, evaluates the core tree that came back against FieldMLRuntimePrelude, and writes the value and its inferred type into the box as one inline value : type line. A program that printed something of its own gets no inline line, which is what the file's TrackingOutput is watching for. display does not count as printing, so a box that draws still says what it evaluated to.

Nothing survives between runs. Each run compiles its text from scratch and evaluates it against a freshly built prelude, so a FieldML box is the script model rather than the accumulating one a Datalog box uses. State that has to outlive a run goes in a box property, which is what the prelude's prop and once are for.

Executing and launching differ in one thing, and it is the thing that makes animation work. Both compile and evaluate. Launch also hands the result to ReturnValueRegistry, the same machinery a Kotlin box's return value goes through, so the Sequence that animate returns is advanced once per run loop frame for as long as the box is running, while a plain number is simply the answer. The run generation counted around that is how a second launch supersedes the first rather than running beside it.

Which boxes are FieldML is decided by fieldMLTopologyNeedsUpdating, and the answer is about parents: a box runs FieldML because a parent carries isFieldMLRoot or names fieldml as its main language, not because it carries the mark itself.

src/jvmMain/kotlin/afield/lang/fieldml/FieldMLInterface.kt — read the source

Top-level functions

Declaration Summary
fun installFieldML() Register FieldML with the Main Language menu, and make isFieldMLRoot survive a save.
fun installFieldMLPlugin() Start watching the box graph, so marking a box propagates to its children without a reload.
fun fieldMLTopologyNeedsUpdating(values: Collection<Box>) Attach and detach FieldMLExecution over a set of boxes, from their parents' marks.

Top-level values

Declaration Summary
var Box.isFieldMLRoot: Boolean? The mark that makes a box's children FieldML boxes.

FieldMLExecution

class FieldMLExecution(private val owner: Box) : BoxExecution

One FieldML box's runtime: compile, evaluate, and report back to the editor, holding nothing between runs and hopping to the main thread before each one.

Declaration Summary
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit) Run the selected text and print what it evaluated to.
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit) Run the whole box and print what it evaluated to, without offering the result to the launch machinery.
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit) Run the whole box and keep whatever the result turns out to be alive.
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit) Run one symbol the editor asked about, as a single-line fragment, and print what it evaluated to.
override fun stop(inside: Box) Stop whatever this box launched.
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit) Answer the editor's completion request from FieldMLCompletionEngine, which reads the whole box rather than this execution's state.
override fun describeSymbolContext(inside: Box, request: SymbolContextRequest): SymbolContextResponse The hover card for the symbol under the caret, from FieldMLCompletionEngine.
override val languageName: String fieldml: what the AI layer and the operation transcript are told a box's code property is written in.

FieldMLMacroLayer.kt

Reaching Field's Kotlin source macros from FieldML: a table of every macro MacroRegistry registers, with the route to each one, and the expansion that follows the route.

FieldML is an interpreter, with no Kotlin compile anywhere in its path, so a macro whose output is generated Kotlin has nothing to hand that output to. Each macro therefore falls into one of three cases: it rewrites its call site to an ordinary Box extension function, which can be expanded for its side effects and then called reflectively; or it generates a declaration whose effect FieldML's own prelude implements natively; or it needs a Kotlin compiler, and only its generated text is available. dev_docs/FIELDML_HOST_BRIDGE.md calls this mechanism A.

The table is a reading of the macro layer, so it can be wrong. A test holds it to the live contents of MacroRegistry in both directions.

src/jvmMain/kotlin/afield/lang/fieldml/FieldMLMacroLayer.kt — read the source

FieldMLMacroLayer

object FieldMLMacroLayer

How a Field source macro is reached from FieldML — mechanism A of dev_docs/FIELDML_HOST_BRIDGE.md.

Declaration Summary
enum class Reach How a given source macro is reachable from FieldML, if it is.
data class Route(val macro: String, val reach: Reach, val why: String, val runtimeName: String? = null, val ownerClassName: String? = null, val nativeForm: String? = null) One row of the classification: which mechanism reaches this macro, and where.
val ledger: Map<String, Route> Every name MacroRegistry registers, and how FieldML gets at it.
fun registeredMacroNames(): Set<String> The names MacroRegistry really has, read off the live maps rather than grepped.
fun expand(box: Box, macro: String, argumentTexts: List<String>): Insert Run a source macro's expansion against box and hand back the rewrite.
fun invoke(box: Box, macro: String, argumentTexts: List<String>, arguments: List<Any?>): FieldMLValue Expand macro and then call what it rewrote the call site to, with arguments as the argument values.
fun invokeNamed(box: Box, macro: String, argumentTexts: List<String>, arguments: Map<String, Any?>): FieldMLValue invoke, by name — which is also the only shape that can leave a Kotlin default unwritten (FieldMLHostBridge.callStaticMethodNamed, and decision 6 of the design notes).
fun calleeOf(here: String): String? The leading identifier of a rewritten call site, or null when the rewrite is not a call.
fun argumentTextOf(here: String): String The argument text of a rewritten call site — everything after the callee. Empty when there is none.

FieldMLRuntimePrelude.kt

The value behind every prelude name: what a FieldML program can call before it declares anything.

FieldMLTypePrelude gives the same list of names their types; this file implements them, and the two are matched by hand rather than derived from one another. values builds the map afresh for each run, which is how print, thisBox and the prop family come to mean this box's output and this box rather than a global.

Two distinctions in here decide what a FieldML program can get away with:

  • Checking versus coercing. A native function unpacks its arguments through the as… helpers, which refuse a value of the wrong kind and name what they found instead, so anything that reached the runtime as an unchecked Host stops here with a sentence rather than a default. The hostAs… family is why there are two sets of helpers rather than one: those convert, since saying what a value that arrived through an erased JVM signature really is is the whole of their job.
  • Reading versus writing a box property. prop and propOr search up the box graph, so a value an ancestor wrote is visible; once reads only its own box's slot; and setProp always writes locally, shadowing an ancestor rather than changing it. Writing also narrows an integer or a float to the JVM width already in the slot, which is what keeps a prop<Int> shared with a Kotlin box from throwing ClassCastException on the Kotlin side.

hostTap is the one name that goes looking in Field's own source rather than at a class: it resolves a Kotlin extension function by name through KotlinSourceIndex when an index is loaded, and by walking src/jvmMain/kotlin and src/personal/kotlin when none is.

src/jvmMain/kotlin/afield/lang/fieldml/FieldMLRuntimePrelude.kt — read the source

FieldMLRuntimePrelude

object FieldMLRuntimePrelude

The prelude as values: every name a FieldML program can call before it has declared anything.

Declaration Summary
fun values(output: HasOutput? = null, box: Box? = null): Map<String, FieldMLValue> The whole prelude, bound to one run of one box.

FieldMLSourceIndex.kt

FieldML's read of the shared Kotlin source index, and the staleness check that decides whether to trust it.

KotlinSourceIndex holds a database of every declaration in the checkout, which FieldML wants for three lookups: the host functions a FieldML program may call (FieldMLRuntimePrelude), and the Kotlin class names and function names its completion offers (FieldMLCompletionEngine). That database is a snapshot, so it is handed back only when every path it records still resolves to a file. When one does not, the host-function and function-name lookups walk src/ themselves instead, and the class-name lookup goes quiet rather than offering stale names.

src/jvmMain/kotlin/afield/lang/fieldml/FieldMLSourceIndex.kt — read the source

Expr.kt

FieldML's surface syntax tree: one node per piece of written syntax, each carrying the source range it was parsed from.

Parser produces this tree and Inferencer reads it. Nothing in it has a type yet. Every node here has a counterpart in afield.lang.fieldml.type that carries an inferred type, and that typed tree is lowered again into the afield.lang.fieldml.core form the evaluator runs.

The tree is narrower than the syntax in two places. ECall takes a single argument, so application is already curried when it is parsed: add 20 22 is ECall(ECall(add, 20), 22). The pipe operator builds that same node with its left operand as the argument, so 10 |> add 5 and add 5 10 differ only in the ranges they record. And a constructor's argument types are bare names: TypeRef has one case, TypeRefName, so a run of names in type Tree a = Node Tree Tree reads as a run of separate arguments. Inference resolves each name to a built-in type, to the type being declared, to one of that type's own parameters, or to a data type with no arguments.

It is wider than the syntax in one place: ELet and ELambda keep the parameter list as it was written, so let add a b = ... is one node with two parameters. Folding those into nested arrow types is inference's job, and turning them into nested one-parameter lambdas is the lowering's.

ELet and ETypeDecl both carry a body, and for a top-level binding that body is the whole rest of the program: a box's bindings stack up by nesting inside each other. A let with nothing after it gets EVar("Unit") for a body.

src/jvmMain/kotlin/afield/lang/fieldml/ast/Expr.kt — read the source

Used in: FieldML stage by stage

Expr

sealed interface Expr

A node of the surface tree, positioned by the SourceRange it was parsed from.

Declaration Summary
val range: SourceRange

EInt

data class EInt(val value: Long, override val range: SourceRange) : Expr

An integer literal.

EFloat

data class EFloat(val value: Double, override val range: SourceRange) : Expr

A floating-point literal.

EBool

data class EBool(val value: Boolean, override val range: SourceRange) : Expr

true or false. The value comes from which keyword was read, never from the text.

EString

data class EString(val value: String, override val range: SourceRange) : Expr

A string literal, with its quotes removed and its backslash escapes already decoded.

EVar

data class EVar(val name: String, override val range: SourceRange) : Expr

A name used as an expression.

ETuple

data class ETuple(val elements: List<Expr>, override val range: SourceRange) : Expr

Two or more comma-separated expressions in parentheses.

EList

data class EList(val elements: List<Expr>, override val range: SourceRange) : Expr

A bracketed list of elements, which may be empty.

TypeDecl

data class TypeDecl(val name: String, val parameters: List<String>, val constructors: List<ConstructorDecl>, val range: SourceRange)

A type declaration as written: the name being declared, the parameters it takes, and its constructors.

Declaration Summary
val name: String
val parameters: List<String>
val constructors: List<ConstructorDecl>
val range: SourceRange

ConstructorDecl

data class ConstructorDecl(val name: String, val arguments: List<TypeRef>, val range: SourceRange)

One constructor of a TypeDecl, with its argument types as they were written.

Declaration Summary
val name: String
val arguments: List<TypeRef>
val range: SourceRange

TypeRef

sealed interface TypeRef

A type as written inside a constructor's argument list.

Declaration Summary
val name: String
val range: SourceRange

TypeRefName

data class TypeRefName(override val name: String, override val range: SourceRange) : TypeRef

TypeRef's only case: a bare name.

Declaration Summary
override val name: String
override val range: SourceRange

ETypeDecl

data class ETypeDecl(val declaration: TypeDecl, val body: Expr, override val range: SourceRange) : Expr

A type declaration together with the program that follows it, which is its body.

Declaration Summary
val declaration: TypeDecl
val body: Expr
override val range: SourceRange

ELet

data class ELet(val name: String, val parameters: List<String>, val value: Expr, val body: Expr, override val range: SourceRange) : Expr

A let binding together with the program that follows it.

Declaration Summary
val name: String
val parameters: List<String>
val value: Expr
val body: Expr
override val range: SourceRange

ELambda

data class ELambda(val parameters: List<String>, val body: Expr, override val range: SourceRange) : Expr

fun x y -> ..., keeping its parameter list as written.

Declaration Summary
val parameters: List<String>
val body: Expr
override val range: SourceRange

ECall

data class ECall(val function: Expr, val argument: Expr, override val range: SourceRange) : Expr

One argument applied to one function.

Declaration Summary
val function: Expr
val argument: Expr
override val range: SourceRange

EIf

data class EIf(val condition: Expr, val thenBranch: Expr, val elseBranch: Expr, override val range: SourceRange) : Expr

if ... then ... else .... There is no one-armed form, so both branches are always present.

Declaration Summary
val condition: Expr
val thenBranch: Expr
val elseBranch: Expr
override val range: SourceRange

Pattern

sealed interface Pattern

The left-hand side of a match arm, positioned by the SourceRange it was parsed from.

Declaration Summary
val range: SourceRange

PWildcard

data class PWildcard(override val range: SourceRange) : Pattern

_: matches anything and binds nothing.

PVar

data class PVar(val name: String, override val range: SourceRange) : Pattern

A lower-case name: matches anything and binds it for the arm's body.

PInt

data class PInt(val value: Long, override val range: SourceRange) : Pattern

An integer literal, matching that value.

PBool

data class PBool(val value: Boolean, override val range: SourceRange) : Pattern

true or false, matching that value.

PString

data class PString(val value: String, override val range: SourceRange) : Pattern

A string literal, escapes already decoded, matching that text.

PTuple

data class PTuple(val elements: List<Pattern>, override val range: SourceRange) : Pattern

Two or more comma-separated patterns in parentheses. A single one is grouping.

PList

data class PList(val elements: List<Pattern>, override val range: SourceRange) : Pattern

A bracketed pattern, matching a list of exactly that length. [] matches the empty list.

PConstructor

data class PConstructor(val name: String, val arguments: List<Pattern>, override val range: SourceRange) : Pattern

A constructor name and the patterns for its arguments.

Declaration Summary
val name: String
val arguments: List<Pattern>
override val range: SourceRange

PCons

data class PCons(val head: Pattern, val tail: Pattern, override val range: SourceRange) : Pattern

head :: tail — matches any non-empty list, binding its first element and the rest.

Declaration Summary
val head: Pattern
val tail: Pattern
override val range: SourceRange

MatchCase

data class MatchCase(val pattern: Pattern, val body: Expr, val range: SourceRange)

One | pattern -> body arm of an EMatch.

Declaration Summary
val pattern: Pattern
val body: Expr
val range: SourceRange

EMatch

data class EMatch(val scrutinee: Expr, val cases: List<MatchCase>, override val range: SourceRange) : Expr

match scrutinee with followed by its arms.

Declaration Summary
val scrutinee: Expr
val cases: List<MatchCase>
override val range: SourceRange

RecordField

data class RecordField(val name: String, val value: Expr, val range: SourceRange)

One name = value field of an ERecord.

Declaration Summary
val name: String
val value: Expr
val range: SourceRange

ERecord

data class ERecord(val fields: List<RecordField>, override val range: SourceRange) : Expr

A braced record literal.

Declaration Summary
val fields: List<RecordField>
override val range: SourceRange

EField

data class EField(val target: Expr, val field: String, override val range: SourceRange) : Expr

target.field, reading one field out of a record.

Declaration Summary
val target: Expr
val field: String
override val range: SourceRange

BinaryOp

enum class BinaryOp(val text: String)

The infix operators, each carrying the text it is written as.

Constants: ADD, SUB, MUL, DIV, CONCAT, LT, LTE, GT, GTE, EQ, NEQ, AND, OR, CONS.

EBinary

data class EBinary(val op: BinaryOp, val left: Expr, val right: Expr, override val range: SourceRange) : Expr

An infix operator applied to two operands, with precedence already resolved into the shape.

Declaration Summary
val op: BinaryOp
val left: Expr
val right: Expr
override val range: SourceRange

ENegate

data class ENegate(val value: Expr, override val range: SourceRange) : Expr

Prefix - over numbers and vectors.

Declaration Summary
val value: Expr
override val range: SourceRange

Core.kt

The tree FieldML actually evaluates: the typed tree with its source positions dropped and its multi-parameter lambdas curried into one-parameter CLam nodes.

Lowerer is the only thing that builds one and Evaluator the only thing that walks one. Every case here corresponds to a case of the typed tree, and the lowering is a case-for-case mapping apart from the lambda, so the two hierarchies are easy to read side by side.

What the lowering leaves behind is as informative as what it keeps. Types stay on every node, so a later pass can still ask what a subexpression is. Ranges do not, so an evaluation failure carries no position of its own, and a box reports it against the fragment that was run rather than the subexpression that failed. CLet keeps the recursive flag but has no parameter list, because by this point a let f x = ...'s parameters are the lambda sitting in its value. CoreConstructor keeps a constructor's name and arity and drops its type scheme, which has already done its work in inference.

src/jvmMain/kotlin/afield/lang/fieldml/core/Core.kt — read the source

Used in: FieldML stage by stage

CoreExpr

sealed interface CoreExpr

A node of the lowered tree, carrying the type inferred for it.

Declaration Summary
val type: Type

CInt

data class CInt(val value: Long, override val type: Type) : CoreExpr

An integer literal, from TExprInt.

CFloat

data class CFloat(val value: Double, override val type: Type) : CoreExpr

A floating-point literal, from TExprFloat.

CBool

data class CBool(val value: Boolean, override val type: Type) : CoreExpr

A boolean literal, from TExprBool.

CString

data class CString(val value: String, override val type: Type) : CoreExpr

A string literal, from TExprString.

CVar

data class CVar(val name: String, override val type: Type) : CoreExpr

A name to look up in the environment, from TExprVar.

CTuple

data class CTuple(val elements: List<CoreExpr>, override val type: Type) : CoreExpr

A tuple, from TExprTuple.

CList

data class CList(val elements: List<CoreExpr>, override val type: Type) : CoreExpr

A list, from TExprList.

CTypeDecl

data class CTypeDecl(val declaration: TypeDecl, val constructors: List<CoreConstructor>, val body: CoreExpr, override val type: Type) : CoreExpr

A data type declaration and the program that follows it, from TExprTypeDecl.

Declaration Summary
val declaration: TypeDecl
val constructors: List<CoreConstructor>
val body: CoreExpr
override val type: Type

CoreConstructor

data class CoreConstructor(val name: String, val arity: Int)

A constructor as the evaluator needs it: its name, and how many arguments it takes before it is a value.

Declaration Summary
val name: String
val arity: Int

CLet

data class CLet(val name: String, val value: CoreExpr, val body: CoreExpr, override val type: Type, val recursive: Boolean = false) : CoreExpr

A binding and the program that follows it, from TExprLet.

Declaration Summary
val name: String
val value: CoreExpr
val body: CoreExpr
override val type: Type
val recursive: Boolean See afield.lang.fieldml.type.TExprLet.recursive.

CLam

data class CLam(val parameter: String, val parameterType: Type, val body: CoreExpr, override val type: Type) : CoreExpr

A lambda of exactly one parameter, so the evaluator only ever binds one at a time.

Declaration Summary
val parameter: String
val parameterType: Type
val body: CoreExpr
override val type: Type

CApp

data class CApp(val function: CoreExpr, val argument: CoreExpr, override val type: Type) : CoreExpr

One argument applied to one function, from TExprCall.

Declaration Summary
val function: CoreExpr
val argument: CoreExpr
override val type: Type

CIf

data class CIf(val condition: CoreExpr, val thenBranch: CoreExpr, val elseBranch: CoreExpr, override val type: Type) : CoreExpr

A conditional, from TExprIf. Both branches are present and have the same type.

Declaration Summary
val condition: CoreExpr
val thenBranch: CoreExpr
val elseBranch: CoreExpr
override val type: Type

CorePattern

sealed interface CorePattern

A pattern in the lowered tree, carrying the type it matches against.

Declaration Summary
val type: Type

CPWildcard

data class CPWildcard(override val type: Type) : CorePattern

Matches anything and binds nothing, from TPWildcard.

CPVar

data class CPVar(val name: String, override val type: Type) : CorePattern

Matches anything and binds it under name, from TPVar.

CPInt

data class CPInt(val value: Long, override val type: Type) : CorePattern

Matches one integer value, from TPInt.

CPBool

data class CPBool(val value: Boolean, override val type: Type) : CorePattern

Matches one boolean value, from TPBool.

CPString

data class CPString(val value: String, override val type: Type) : CorePattern

Matches one string value, from TPString.

CPTuple

data class CPTuple(val elements: List<CorePattern>, override val type: Type) : CorePattern

Matches a tuple element by element, from TPTuple.

CPList

data class CPList(val elements: List<CorePattern>, override val type: Type) : CorePattern

Matches a list of exactly this length, from TPList.

CPConstructor

data class CPConstructor(val name: String, val arguments: List<CorePattern>, override val type: Type) : CorePattern

Matches one constructor and its arguments, from TPConstructor.

Declaration Summary
val name: String
val arguments: List<CorePattern>
override val type: Type

CPCons

data class CPCons(val head: CorePattern, val tail: CorePattern, override val type: Type) : CorePattern

Matches a non-empty list, splitting it into head and tail, from TPCons.

Declaration Summary
val head: CorePattern
val tail: CorePattern
override val type: Type

CMatchCase

data class CMatchCase(val pattern: CorePattern, val body: CoreExpr)

One arm of a CMatch: a pattern, and the expression to evaluate when it matches.

Declaration Summary
val pattern: CorePattern
val body: CoreExpr

CMatch

data class CMatch(val scrutinee: CoreExpr, val cases: List<CMatchCase>, override val type: Type) : CoreExpr

A match over its arms, from TExprMatch.

Declaration Summary
val scrutinee: CoreExpr
val cases: List<CMatchCase>
override val type: Type

CRecord

data class CRecord(val fields: List<Pair<String, CoreExpr>>, override val type: Type) : CoreExpr

A record literal, from TExprRecord, as name-and-value pairs in their written order.

Declaration Summary
val fields: List<Pair<String, CoreExpr>>
override val type: Type

CField

data class CField(val target: CoreExpr, val field: String, override val type: Type) : CoreExpr

Reads one field off a record or a host value, from TExprField.

Declaration Summary
val target: CoreExpr
val field: String
override val type: Type

CBinary

data class CBinary(val op: BinaryOp, val left: CoreExpr, val right: CoreExpr, override val type: Type) : CoreExpr

An infix operator over two operands, from TExprBinary.

Declaration Summary
val op: BinaryOp
val left: CoreExpr
val right: CoreExpr
override val type: Type

CNegate

data class CNegate(val value: CoreExpr, override val type: Type) : CoreExpr

Negation, from TExprNegate, over an integer, a float, or each component of a vector.

Declaration Summary
val value: CoreExpr
override val type: Type

Evaluator.kt

The FieldML runtime: it walks a lowered CoreExpr tree and returns the FieldMLValue the tree evaluates to.

This is the last stage of the pipeline. Lowerer hands over a tree whose lambdas are already curried and whose source ranges are already gone, so nothing here can report a position and a box blames the fragment it ran rather than the subexpression that failed. An Evaluator holds the prelude it was constructed with and builds a fresh root scope for every evaluate, so two runs of the same box share no state.

Most of the value shapes are what their names say. Two are not. HostValue wraps an arbitrary JVM object, which is how a FieldML box reaches Field at all: FieldMLHostBridge both produces and consumes these, and Vec2, Vec3 and Vec4 are HostValues holding the mutable linalg vector types rather than a FieldML shape of their own. Arithmetic on them and access to their x, y, z and w components are therefore special-cased here, component-wise between vectors of equal width and broadcast against a scalar, and always into a freshly allocated vector, because the linalg types are mutable and a FieldML value is not. FactoryValue is the other: RuntimeEnv calls its create on every lookup, so each mention of the name is a separate value while a let-bound name is one value. The prelude uses this once, for fline, and needs to, because moveTo and lineTo mutate the line they are handed and return it.

hostToFieldMLValue and fieldMLValueToHost are the conversion pair at the JVM boundary, and they are not inverses. A tuple leaves as a plain List and comes back a list, so a round trip through the host loses the distinction the type checker made; records, unit and the numeric shapes survive one. Closures, native functions, partially applied constructors and ADT values leave as themselves rather than as anything JVM-shaped, which is what lets the bridge's SAM proxy apply one later.

Scope is a chain of RuntimeEnvs with one parent each. A let always makes a child; a recursive one evaluates its own body in the scope that will hold the name, and a plain one evaluates in the parent, so let x = x + 1 reads the outer x. A match makes one child per case it tries, and a value matching no case fails at this point: nothing earlier checks exhaustiveness, and this runtime error is the only mention of it in FieldML.

Two failures here have their cause somewhere else. Tail positions loop, but every other call recurses, so deep non-tail FieldML recursion arrives as a JVM StackOverflowError and not as a FieldML diagnostic. And an unknown runtime name means the two halves of the prelude have drifted apart: Inferencer already rejects any name FieldMLTypePrelude.schemes does not list, so a name that type-checked and then went missing is one FieldMLRuntimePrelude.values does not define.

src/jvmMain/kotlin/afield/lang/fieldml/core/Evaluator.kt — read the source

Top-level functions

Declaration Summary
suspend fun applyFieldMLFunctionSuspending(function: FieldMLValue, argument: FieldMLValue): FieldMLValue applyFieldMLFunction for the suspending walk: a closure's body is walked by evalFieldMLSuspending, and a native that can suspend (FieldMLValue.NativeFunctionValue.suspendApply) is called as one. Everything else is the synchronous application.
fun applyFieldMLFunction(function: FieldMLValue, argument: FieldMLValue): FieldMLValue Applies one argument to one callable value, which is all application FieldML has.
fun fieldMLValueToHost(value: FieldMLValue): Any? Unwraps a FieldML value into the JVM value a Kotlin caller should see.
fun hostToFieldMLValue(value: Any?): FieldMLValue Wraps a JVM value as the nearest FieldML value, falling back to an opaque FieldMLValue.HostValue.
fun renderFieldMLValue(value: FieldMLValue): String Renders a value the way FieldML source would spell it, for a box's output and for show.

FieldMLValue

sealed interface FieldMLValue

Everything a FieldML expression can evaluate to.

Declaration Summary
data class IntValue(val value: Long) : FieldMLValue A FieldML Int. There is only one integer width, and it is 64-bit.
data class FloatValue(val value: Double) : FieldMLValue A FieldML Float. There is only one floating-point width, and it is 64-bit.
data class BoolValue(val value: Boolean) : FieldMLValue A FieldML Bool.
data class StringValue(val value: String) : FieldMLValue A FieldML String.
data class TupleValue(val elements: List<FieldMLValue>) : FieldMLValue A fixed-width tuple, whose elements may have different types.
data class ListValue(val elements: List<FieldMLValue>) : FieldMLValue A list of any length, whose elements all have one type.
data class RecordValue(val fields: Map<String, FieldMLValue>) : FieldMLValue A record, keyed by field name. This is also what a JVM Map arrives as.
data class AdtValue(val tag: String, val arguments: List<FieldMLValue>) : FieldMLValue A fully applied data constructor: the constructor's name and its arguments.
data object UnitValue : FieldMLValue The one value of FieldML's unit type, and what a Kotlin Unit or a null arrives as.
data class HostValue(val value: Any?) : FieldMLValue An arbitrary JVM object, opaque to FieldML itself.
data class NativeFunctionValue(val name: String, val apply: (FieldMLValue) -> FieldMLValue, val suspendApply: (suspend (FieldMLValue) -> FieldMLValue)? = null) : FieldMLValue A prelude function, implemented in Kotlin, taking exactly one argument.
data class FactoryValue(val name: String, val create: () -> FieldMLValue) : FieldMLValue A prelude entry that is a new value on every mention rather than a shared one.
data class ClosureValue(val parameter: String, val body: CoreExpr, val env: RuntimeEnv) : FieldMLValue A FieldML function: one parameter, an unevaluated body, and the scope it was written in.
data class ConstructorValue(val tag: String, val arity: Int, val arguments: List<FieldMLValue> = emptyList()) : FieldMLValue A data constructor part-way through being applied, holding the arguments it has so far.

RuntimeEnv

class RuntimeEnv(private val parent: RuntimeEnv? = null, private val values: MutableMap<String, FieldMLValue> = mutableMapOf(), val fuel: StepFuel? = parent?.fuel)

One FieldML scope: the names bound here, and the scope to fall back on for the rest.

Declaration Summary
val fuel: StepFuel? The step budget this scope's evaluation draws on; every child scope shares its parent's.
fun child(): RuntimeEnv A new scope that falls back on this one.
fun define(name: String, value: FieldMLValue) Binds a name in this scope, shadowing any binding of it further up the chain.
fun lookup(name: String): FieldMLValue The value of a name, searching up the chain, resolving a FieldMLValue.FactoryValue as it goes.

StepFuel

class StepFuel(val limit: Long)

A budget of evaluator steps, shared by every scope of one evaluation. Both walks spend one step per node they visit (a tail call that loops counts as a visit), so a program runs out at the same point on either walk.

Declaration Summary
fun spend() Spends one step, failing with a FieldML error once limit steps have been spent.

Evaluator

class Evaluator(private val prelude: Map<String, FieldMLValue> = emptyMap(), private val stepLimit: Long? = null)

Evaluates lowered FieldML trees against one prelude.

Declaration Summary
fun evaluate(expr: CoreExpr): FieldMLValue Walks expr and returns what it evaluates to, starting from a fresh scope holding the prelude.
var onTopLevelLet: ((index: Int, name: String, value: FieldMLValue) -> Unit)? Called with each top-level let binding's position in the program's spine of lets (0 first), its name and the value it bound, as evaluate makes it; the box records these for its line notes.

HostBridge.kt

How a FieldML box calls Kotlin: hostCall, hostNew and hostGet all arrive here, and so does a .field read on a host value.

FieldML has no declarations for Field's API and no generated stubs for it. A box names a method with a string, and all of the matching happens at run time against java.lang.reflect. That leaves this file three problems a compiled language would have settled at its own compile time: choosing between same-named overloads, giving a FieldML function a JVM type it can be passed as, and reconciling FieldML's one integer and one float with the JVM's six numeric widths. The object below takes them in that order.

FieldMLHostTypes answers the same three questions a second time, because typing a hostCall means predicting which member this file will pick. Where the answer is a judgement rather than a lookup the two share one implementation rather than agreeing twice: numericAffinity scores numeric fit for both, and both read a wildcard from the bound that carries the information for the position it appears in. When they drift, a box type-checks and then invokes the wrong member, which is a harder failure to read than either a type error or a refusal.

dev_docs/FIELDML_HOST_BRIDGE.md is the longer account, including the named-argument mechanisms that make Kotlin's default parameters reachable from FieldML at all.

src/jvmMain/kotlin/afield/lang/fieldml/core/HostBridge.kt — read the source

FieldMLHostBridge

object FieldMLHostBridge

The reflective half of FieldML's JVM bridge: given a name, a receiver and some already-converted FieldML arguments, find the JVM member that the call actually meant and make it happen.

Declaration Summary
fun spreadArguments(value: FieldMLValue): List<Any?> Tuple- and list-spread, the shape hostStaticCall has always used: hostCall "m" (a, b, c) t passes three arguments. Anything else is a single argument, which is what makes hostCall1 with a one-element list the way to pass a list as a list.
fun callMethod(name: String, target: Any, arguments: List<Any?>): FieldMLValue Calls an instance method on target by name, choosing among the same-named methods of the receiver's runtime class.
fun launchSuspendMember(name: String, target: Any, arguments: List<Any?>, dispatcher: kotlin.coroutines.CoroutineContext = kotlin.coroutines.EmptyCoroutineContext, onResult: (Result<Any?>) -> Unit): kotlinx.coroutines.Job Calls a Kotlin suspend member of target by name — Renderer.run, Frames.frame — and hands its eventual result to onResult. This is the calling-into half of mechanism D in dev_docs/FIELDML_HOST_BRIDGE.md; samProxy is the being-called half.
suspend fun awaitSuspendMember(name: String, target: Any, arguments: List<Any?>): FieldMLValue Calls a Kotlin suspend member of target from inside a suspending FieldML walk and returns its result — hostAwait "frame" () f. Unlike launchSuspendMember this starts no coroutine: it passes the caller's own continuation, so when Frames.frame() suspends, the FieldML body that called it suspends with it, and resumes at this call when the frame turns.
fun callStaticMethod(name: String, owner: Class<*>, arguments: List<Any?>): FieldMLValue Calls a static method of owner by name.
fun construct(owner: Class<*>, arguments: List<Any?>): FieldMLValue Constructs an owner by positional arguments, scoring its constructors the same way methods are scored.
fun callMethodNamed(name: String, target: Any, arguments: Map<String, Any?>): FieldMLValue Calls written with a record of named arguments instead of a tuple of positional ones — hostNew {window = w} Renderer rather than hostNew (w, []) Renderer.
fun callStaticMethodNamed(name: String, owner: Class<*>, extensionReceiver: Any?, arguments: Map<String, Any?>): FieldMLValue callMethodNamed for a static method, and the path an extension function with named arguments takes.
fun constructNamed(owner: Class<*>, arguments: Map<String, Any?>): FieldMLValue Constructs an owner from named arguments, letting Kotlin fill in every parameter not named.
fun instanceMethods(owner: Class<*>, name: String): List<Method> All same-named members, any arity: a named call chooses by name set, not by count.
fun staticMethods(owner: Class<*>, name: String): List<Method> instanceMethods for statics: all same-named ones, any arity.
fun kotlinFunctionOf(method: Method): KFunction<*>? The Kotlin view of some JVM methods, dropping the ones there is no Kotlin view of.
fun readMember(name: String, target: Any): FieldMLValue Members visible to hostGet: a map key, a static member of a Class, or an instance member.
fun writeMember(name: String, target: Any, value: Any?) The write side of readMember: a mutable map entry, a non-final field, or a setter.
fun instanceMethods(owner: Class<*>, name: String, arity: Int): List<Method> Same-named instance methods that could take arity arguments, which is the candidate set a positional call chooses from.
fun staticMethods(owner: Class<*>, name: String, arity: Int): List<Method> instanceMethods for statics, with the same vararg allowance.
fun constructors(owner: Class<*>, arity: Int): List<Constructor<*>> Public constructors of owner that could take arity arguments.
fun instanceField(owner: Class<*>, name: String): Field? A public instance field by name, or null.
fun staticField(owner: Class<*>, name: String): Field? instanceField for statics: a public static field, a Java constant, or a @JvmStatic one.
fun instanceGetter(owner: Class<*>, name: String): Method? Kotlin properties reach reflection as getFoo(); Java records and Kotlin's is-prefixed booleans as foo() and isFoo(). A plain foo() method is also a legitimate reader, which is what lets hostGet "pixelCount" and hostGet "copy" both work.
fun staticGetter(owner: Class<*>, name: String): Method? instanceGetter for statics, trying the same three spellings of the name.
fun <T : Executable> choose(candidates: List<T>, arguments: List<Any?>): T? The best candidate for arguments, or null when none of them can take these arguments.
fun scoreCandidate(candidate: Executable, arguments: List<Any?>): Int? Total conversion cost of calling candidate with arguments, or null if it cannot be.
fun numericAffinity(parameter: Class<*>, argument: Class<*>): Int? How well a numeric argument fits a numeric parameter, or null when either side is not numeric.
fun invocable(method: Method): Method The same method, re-resolved onto a type this code can actually invoke through.
fun coerce(parameter: Class<*>, generic: java.lang.reflect.Type?, argument: Any?): Any? Converts one already-host-side value into exactly what a JVM parameter or field wants.
fun functionalMethod(type: Class<*>): Method? The single abstract method of a functional interface, or null when type is not one. Default and static methods do not count, and neither do the Object methods an interface may redeclare — Comparator's equals is the usual example.
class SuspendShape(val parameterCount: Int, val resultType: Class<*>) A Kotlin suspend function type as plain reflection sees it, or null for an ordinary one.
fun suspendShape(iface: Class<*>, generic: java.lang.reflect.Type?): SuspendShape? The SuspendShape of a functional interface that is really a suspend function type, or null for an ordinary one.
fun boxed(type: Class<*>): Class<*> The wrapper class for a primitive, or the type unchanged.

Lowerer.kt

The last stage of the FieldML front end: the typed tree becomes the CoreExpr tree the evaluator walks.

Almost all of it is a rename. Each case of the typed tree has one case in the core tree, the inferred type is copied across, and the source range is dropped. Two things inference needed go no further: a let's parameter list, which by this point is the lambda sitting in its value, and a constructor's type scheme, which reduces to the name and arity CoreConstructor keeps.

The one real transformation is currying. A lambda written fun x y -> ... becomes two nested one-parameter CLam nodes, so the evaluator only ever binds one parameter at a time. Each of those nodes carries its own type and they are different types: the outer node's is the whole arrow, the inner node's is the arrow that starts at the second parameter. So the lowering walks the arrow outside in, one step per parameter, keeping the type each parameter starts from, and then builds the nodes from the innermost parameter back out.

Nothing here reports anything. A tree that reaches this stage has already been typed, so there is no result to check: FieldMLCompiler runs the lowerer and returns what comes back.

src/jvmMain/kotlin/afield/lang/fieldml/core/Lowerer.kt — read the source

Lowerer

class Lowerer

Turns a typed tree into the core tree the evaluator runs.

Declaration Summary
fun lower(expr: TExpr): CoreExpr The CoreExpr equivalent of expr, with its lambdas curried and its ranges dropped.

Diagnostics.kt

Where a FieldML compile says something is wrong, and how badly.

This is the package the rest of the front end shares, and the only one that depends on nothing: the lexer, the parser, the inferencer and the lowerer all import it, and it imports none of them. A stage with something to report either adds a Diagnostic to the list it returns or, where carrying on would mean inventing a tree, throws CompileAbort and lets its own entry point turn that back into a diagnostic.

Only an error stops a compile. Each stage runs if no earlier diagnostic was an error, so a Severity of Warning or Info is collected and reported without holding the run up.

Nothing outside afield.lang.fieldml imports these types. They leave the front end inside CompilationResult, and the box runtime reads them from there to put a message on a line.

src/jvmMain/kotlin/afield/lang/fieldml/diag/Diagnostics.kt — read the source

SourcePos

data class SourcePos(val index: Int, val line: Int, val column: Int)

One character position in the compiled source, in the three forms its readers need.

Declaration Summary
val index: Int Characters from the start of the source, counting from zero.
val line: Int Lines from the start of the source, counting from one.
val column: Int Characters from the start of the line, counting from one.

SourceRange

data class SourceRange(val start: SourcePos, val end: SourcePos)

A half-open span of source: start is the first character, end is the position just past the last.

Declaration Summary
val start: SourcePos The first character of the span.
val end: SourcePos The position just past the last character of the span.

Diagnostic

data class Diagnostic(val severity: Severity, val message: String, val range: SourceRange?)

One thing a stage has to say about one place in the source.

Declaration Summary
val severity: Severity How much it matters.
val message: String The sentence shown to whoever wrote the program. It is also the exception message when the diagnostic arrives as a CompileAbort.
val range: SourceRange? What the message is about, or null when there is nothing to point at.

Severity

enum class Severity

How much a Diagnostic matters.

Constants: Error, Warning, Info.

CompileAbort

class CompileAbort(val diagnostic: Diagnostic) : RuntimeException(diagnostic.message)

The exception a stage throws to stop where it stands, carrying the Diagnostic that says why.

Lexer.kt

FieldML's first stage: source text in, a flat list of Tokens out, and a diagnostic for anything that could not be read.

The scan never stops early. A character the lexer cannot use costs one diagnostic and no token, and the scan carries on, so a typo halfway down a box does not hide what is below it. Whether the compile carries on is a separate decision, made by FieldMLCompiler, which does not reach the parser at all once lexing has reported an error.

Trivia is skipped rather than emitted: // to the end of the line, and (* … *) blocks, which nest by depth count, so a region commented out this way can contain comments of its own. An unterminated block is reported at the position it was opened.

The shapes a literal can take are all decided here. A number is digits, and a . opens a fraction only when a digit follows it, which leaves . free to be the field-access token and makes 1. an INT and then a DOT; there is no exponent form, and a leading - is always a token of its own rather than part of the literal. An identifier starts with a letter or _ and may carry ' after that. A string runs to the next unescaped ", and one that reaches the end of the source first is reported rather than closed. &, : and ! are errors on their own, since only &&, :: and != exist, and the first two say so in the message.

src/jvmMain/kotlin/afield/lang/fieldml/lex/Lexer.kt — read the source

LexResult

data class LexResult(val tokens: List<Token>, val diagnostics: List<Diagnostic>)

The whole of what one scan produced.

Declaration Summary
val tokens: List<Token> The tokens, in source order, ending with EOF. Trivia is not among them.
val diagnostics: List<Diagnostic> One entry per thing the lexer could not read, in source order. All of them are errors.

Lexer

class Lexer(private val source: String)

A single-use scanner over one string.

Declaration Summary
fun lex(): LexResult Scan to the end of the source, appending the EOF token. Reports rather than throws.

Token.kt

One TokenKind for every lexeme FieldML has, and the Token that pairs a kind with the text it was read from.

There is no kind for whitespace or for a comment: the lexer skips trivia instead of emitting it, so a token list cannot be printed back as the source it came from. Keywords are kinds of their own rather than identifiers with reserved text, which is what lets the parser ask for LET or WITH by kind, and also what stops those words being used as names.

src/jvmMain/kotlin/afield/lang/fieldml/lex/Token.kt — read the source

TokenKind

enum class TokenKind

Every lexeme FieldML has: its keywords, its identifier, its three literal kinds, its punctuation, its operators, and EOF.

Constants: LET, TYPE, IN, IF, THEN, ELSE, FUN, MATCH, WITH, TRUE, FALSE, IDENT, INT, FLOAT, STRING, LPAREN, RPAREN, LBRACKET, RBRACKET, LBRACE, RBRACE, COMMA, SEMICOLON, BAR, PIPE_FORWARD, DOT, ARROW, EQ, EQEQ, NEQ, PLUS, PLUSPLUS, MINUS, STAR, SLASH, AMPAMP, BARBAR, COLONCOLON, LT, LTE, GT, GTE, EOF.

Token

data class Token(val kind: TokenKind, val text: String, val range: SourceRange)

A lexeme: what it is, the text it was read from, and where that text was.

Declaration Summary
val kind: TokenKind Which lexeme this is.
val text: String The exact slice of source the token was read from, undecoded: a STRING still has its quotes and its backslashes, and an INT is still text. Converting them is the parser's job. Empty for EOF.
val range: SourceRange Where text was. EOF gets an empty range at the end of the source, so a diagnostic about a program that stopped too early still has somewhere to point.

Parser.kt

FieldML's second stage: tokens in, the surface tree out, by recursive descent with precedence climbing for the operators.

There is no error recovery. The first token the parser cannot use throws, parse catches it, and the result is no tree and exactly one diagnostic. Where the lexer reports every character it could not read, the parser reports only the first thing it could not fit into the grammar.

Two places in the grammar are settled by columns rather than by tokens, and they are what lets a box be written as a stack of bindings with no in and a record as a block with no separators. The value of a top-level let ends at the next line that starts no further right than the let itself, so a continuation has to be indented. A record field's value ends at }, at a , or ;, or at the next name = that starts a later line. Both are carried down into the expression parser as predicates, so they apply at whatever depth the expression has reached rather than only at the top.

Function application binds tighter than every operator, and prefix - binds looser than application, so -f x negates the call. :: is the one right-associative operator. |> gets no node of its own: a |> f is parsed as the call f a, which is why nothing after this stage has a case for a pipe.

A program is a chain of let and type declarations ending in an expression. One that ends on a binding instead evaluates to Unit, and a type declaration with nothing after it is an error rather than a program.

src/jvmMain/kotlin/afield/lang/fieldml/parse/Parser.kt — read the source

ParseResult

data class ParseResult(val expr: Expr?, val diagnostics: List<Diagnostic>)

The surface tree, or nothing and the reason why.

Declaration Summary
val expr: Expr? The tree, or null when parsing gave up. Non-null means the whole token list parsed, EOF included, so nothing was left over.
val diagnostics: List<Diagnostic> Empty when expr is non-null, and one error long when it is null.

Parser

class Parser(private val tokens: List<Token>)

Recursive descent over a finished token list.

Declaration Summary
fun parse(): ParseResult Parse the token list as one FieldML program, reporting a failure rather than throwing it.

FieldMLTypePrelude.kt

The type of every name a FieldML program has before it declares anything: the prelude, as the type checker sees it.

schemes is the root environment Inferencer starts from, so a name here is in scope in every FieldML box. Each entry is a Scheme rather than a bare type, because most of the prelude is polymorphic and a bare type cannot say how polymorphic: map works over any element type, abs over Int or Float but not over a string, dot over two vectors as long as they are the same width. A scheme carries those restrictions on its quantified variables, which is what lets one entry stand in for what would otherwise be a pile of overloads.

FieldMLRuntimePrelude holds the other half, name for name: the values these types describe. The two lists are matched by hand and nothing in the build compares them. What the build does guard is the page a reader looks the prelude up in, through FieldMLCheatsheetCoverageTest.everyPreludeNameIsListedWithItsType, which fails when a name is added, removed or retyped without public/docs/fieldml_cheatsheet.md following.

The THost values at the top are the handful of host types FieldML names. Everything else the JVM offers arrives as the opaque Host, and the host… family of intrinsics is how a program says what a value of that type actually is.

src/jvmMain/kotlin/afield/lang/fieldml/type/FieldMLTypePrelude.kt — read the source

FieldMLTypePrelude

object FieldMLTypePrelude

The prelude as types: the schemes Inferencer starts from, and the five host types FieldML knows by name.

Declaration Summary
val host Host: what a value the type checker cannot say anything more specific about has.
val vec2 A two-component vector. Arithmetic, .x/.y, and every overVector scheme accept it.
val vec3 A three-component vector, and the only width cross is defined for.
val vec4 A four-component vector, which is also how a colour is written.
val fline An afield.geometry.path.FLine, threaded through the drawing prelude so moveTo/lineTo/draw chain in a pipe.
val box thisBox is the receiver most host calls start from, and it really is an afield.document.graph.Box — so typing it as one is what turns the whole hostGet/hostCall family from opaque into checked in the case a live document hits first.
fun isVectorType(type: Type): Boolean True for Vec2/Vec3/Vec4, the host types that support arithmetic and .x/.y/.z/.w.
fun vectorComponents(type: Type): List<String> Component names readable off a vector type, in order.
val schemes: Map<String, Scheme> Every prelude name and its scheme, grouped in declaration order by what it is for: output, vectors and FLines, numbers, lists, drawing, the host… intrinsics, and the prop family.

HostTypes.kt

The reflection behind FieldML's host types: what a THost names, and what can therefore be said at type-check time about a member's type, a resolved overload's result, the parameter slots a call's arguments land in, and the members a receiver actually has.

A THost's name is a real JVM class name, which is what keeps a program typed once it reaches into the JVM: a member access is inferred rather than left dynamic, a misspelled member name is reported where it is written instead of crashing on the first run, and completion has a class to enumerate.

Two rules bound what this file will claim.

Host stays the dynamic type. OPAQUE, the name Host itself, is what a host type degrades to whenever reflection cannot justify anything better: a generic erased to Object, an array, a Map, a nullable return, an overload set whose best candidates disagree about what they return. Inferencer.unifyHosts lets it stand for any class, which is the statement "unknown, checked at run time". It is a bounded dynamic type rather than a universal one, though: Inferencer.requireDynamicallyReachable refuses a Host where a function, a list, a tuple or a record is wanted, and names what to reach for instead.

A mapping only claims what the runtime actually does. fieldMLTypeOf answers with the type afield.lang.fieldml.core.hostToFieldMLValue will really produce, so reflection saying int means an IntValue arrives, and List<Double> means a list of floats. Where the declared type does not determine the converted one the answer is Host rather than something plausible: a Map really does convert to a record, but its field names are nowhere in the declared type, so the type side says Host and the program reads the fields with hostGet.

src/jvmMain/kotlin/afield/lang/fieldml/type/HostTypes.kt — read the source

FieldMLHostTypes

object FieldMLHostTypes

Reflection over a host type: the JVM class a THost names, the FieldML type a JVM type converts to, and the member, overload, constructor and parameter-slot lookups the type checker and completion ask for.

Declaration Summary
const val OPAQUE The dynamic host type: unknown JVM class, or a class whose shape reflection cannot pin down.
val curatedTypeNames: List<String> The short names FieldML understands in hostAs and hostClass, for completion.
fun valueType(className: String): THost The type of a host value of className.
fun valueType(owner: Class<*>): THost
fun classHandleType(className: String): THost The type of the class handle hostClass "…" returns, distinct from a value of that class.
fun classHandleName(type: Type): String? The class named by a Class<…> handle type, or null when type is not one.
fun jvmClassOf(type: Type): Class<*>? The JVM class behind a host value type, or null when there is none to reflect on — either because the type is OPAQUE, because it is a class handle rather than a value, or because the class is not on this classpath (a hostClass name that was checked when it was written and is being re-checked in a different build, say).
fun loadClass(className: String): Class<*>?
fun display(name: String): String How a host type reads in a signature: the simple name, with the package and any outer class dropped.
fun fieldMLTypeOf(jvm: java.lang.reflect.Type?): Type The FieldML type a value of the JVM type jvm will have once it has been through hostToFieldMLValue. OPAQUE whenever that cannot be said for certain.
sealed interface Resolution The outcome of asking reflection about a member. Unknown is not a failure: it means the receiver's class is not known, so the call has to stay dynamic and must not be reported as an error.
fun memberType(receiver: Type, name: String): Resolution
fun methodResultType(receiver: Type, name: String, arguments: List<Type>): Resolution
fun staticMethodResultType(classType: Type, name: String, arguments: List<Type>): Resolution
fun constructorResultType(classType: Type, arguments: List<Type>): Resolution
fun namedMethodResultType(receiver: Type, name: String, argumentNames: Set<String>): Resolution The type-level twin of FieldMLHostBridge.callMethodNamed, weaker than its positional counterpart in one respect: it resolves on the argument names alone and ignores their types.
fun namedStaticMethodResultType(classType: Type, name: String, argumentNames: Set<String>, wantsExtensionReceiver: Boolean): Resolution
fun namedConstructorResultType(classType: Type, argumentNames: Set<String>): Resolution
fun namedParameters(receiverOrClass: Type, method: String?, static: Boolean, argumentNames: Set<String>, wantsExtensionReceiver: Boolean): Map<String, HostParameter> The parameter slot each name of a named call lands in, for the names every candidate that accepts argumentNames agrees on. A name whose slot differs between candidates is left out, which is "no claim" — the same rule parametersOf follows for positions.
data class HostParameter(val jvmClass: Class<*>, val generic: java.lang.reflect.Type?, val nullableFunctionArguments: Set<Int> = emptySet()) One parameter slot of a resolved overload: the erased class the JVM will check, the generic type written at the use site — the only place a functional interface's type arguments survive — and, when the slot is a Kotlin function type, which of its arguments are nullable.
fun instanceParameters(receiver: Type, name: String, arguments: List<Type>, lambdaArities: List<Int?> = emptyList()): List<HostParameter?> The parameter slots a call's arguments will land in, one per argument, or an empty list when nothing can be said. This is what lets a lambda literal be checked against the JVM signature instead of being inferred blind — see Inferencer.preInferHostSpine.
fun staticParameters(classType: Type, name: String, arguments: List<Type>, lambdaArities: List<Int?> = emptyList()): List<HostParameter?>
fun constructorParameters(classType: Type, arguments: List<Type>, lambdaArities: List<Int?> = emptyList()): List<HostParameter?>
fun parameterSlot(owner: Executable, index: Int): HostParameter The parameter slot at index of owner, for a caller that already knows which member it means.
fun samParameterTypes(parameter: HostParameter): List<Type>? The FieldML types the JVM will hand a lambda that satisfies parameter, or null when parameter is not a functional interface.
fun samResultAcceptsFunction(parameter: HostParameter): Boolean Whether a lambda satisfying parameter may legitimately have more parameters than the interface takes.
fun readableMembers(receiver: Type): List<HostMember> Members hostGet can read, for completion. Static members when receiver is a class handle.
fun callableMethods(receiver: Type): List<HostMethod> Methods hostCall can reach, for completion. Static methods when receiver is a class handle.
data class HostMember(val name: String, val type: Type, val owner: String)
data class HostMethod(val name: String, val parameters: List<String>, val resultType: Type, val owner: String)

Inferencer.kt

FieldML's type checker: a Hindley-Milner inference pass over the surface tree, producing the typed tree the lowerer consumes.

The core of it is the textbook algorithm. Every unknown is a numbered type variable; unification makes two types equal by binding variables in one substitution the whole run shares; pruning follows a bound variable to what it stands for and shortens the chain behind it; an occurs check refuses a type that would contain itself. A let generalizes over the variables its value has and its environment does not, so each use of a bound name instantiates that scheme with fresh variables.

Three things here are not the textbook's:

  • A host type carries a JVM class. Two of them unify when either class is assignable from the other, because Field's own signatures mix a writable class with its read-only interface. The bare Host is the dynamic escape, and a bounded one: it stands for a value whose class is unknown rather than one whose shape is unknown, so it unifies with a number, a string, a boolean or another object, and refuses a function, a list, a tuple or a record.
  • A saturated host… call is typed by reflection rather than by a scheme. What hostGet returns depends on the text of a string and the class of a receiver, which no one scheme can state, so the member is looked up and the call is either typed or refused where it was written instead of at the first run. A program that shadows the intrinsic is left alone. Curried application is left-nested, so the ordinary walk would reach a lambda argument before the receiver that says what its parameters are; a host call's spine is inferred ahead of that walk to put the order right, which is how a callback written as a positional argument gets its parameter types from the receiver's class.
  • Numeric and vector type variables are constrained, which is as much of a class system as FieldML has. A variable that has been through a numeric or vector requirement is recorded as such, the requirement is rechecked at the moment the variable is bound, and one that is still a variable when the tree is finalized is defaulted: Float for numeric, Vec2 for vector.

Every failure leaves through the same abort, which throws rather than returning, so an InferResult either carries a typed tree and no diagnostics at all, or no tree and exactly one error.

src/jvmMain/kotlin/afield/lang/fieldml/type/Inferencer.kt — read the source

InferResult

data class InferResult(val typed: TExpr?, val diagnostics: List<Diagnostic>)

What inference made of a program: a typed tree, or nothing and the error that stopped it.

Declaration Summary
val typed: TExpr? The surface tree with a type on every node and every pattern, or null if inference gave up.
val diagnostics: List<Diagnostic> Empty when typed is non-null, and otherwise the single error that ended the run.

Inferencer

class Inferencer

Infers the types of one FieldML program.

Declaration Summary
fun infer(expr: Expr): InferResult Types expr, returning the typed tree, or null and the error that stopped inference.

TypedAst.kt

FieldML's syntax tree after inference: the same shape as the surface tree in afield.lang.fieldml.ast, with a Type on every expression and on every pattern.

Inferencer produces it and Lowerer consumes it. Source ranges survive inference, which is what lets a type error point at the text that caused it, and this is the last stage to have them: the afield.lang.fieldml.core tree the evaluator runs keeps the types and drops the positions.

Three things here are inference's findings rather than a restatement of the syntax:

  • TExprLet.recursive is true for the let f x = ... function form, which is the form whose own name is in scope inside its body.
  • TExprLet.parameters repeats the parameters of the lambda that the binding's value became, and is empty when the value is not a lambda.
  • TypedConstructor gives each constructor of a declared data type its arity and the Scheme its name was bound under, so Some is an ordinary curried function from there on.

walkTyped is the one walker over the hierarchy, and completion is its caller: it compiles a probe program and looks for the TExprLambda whose parameter names match the lambda under the caret, which is how a callback's parameters have types before a reader has written any.

src/jvmMain/kotlin/afield/lang/fieldml/type/TypedAst.kt — read the source

Used in: FieldML stage by stage

Top-level functions

Declaration Summary
fun TExpr.walkTyped(visit: (TExpr) -> Unit) Every node of a typed tree, parents before children.

TExpr

sealed interface TExpr

A node of the typed tree: a surface node, its source range, and the type inferred for it.

Declaration Summary
val range: SourceRange
val type: Type

TExprInt

data class TExprInt(val value: Long, override val range: SourceRange, override val type: Type) : TExpr

EInt typed, which is always Int.

TExprFloat

data class TExprFloat(val value: Double, override val range: SourceRange, override val type: Type) : TExpr

EFloat typed, which is always Float.

TExprBool

data class TExprBool(val value: Boolean, override val range: SourceRange, override val type: Type) : TExpr

EBool typed, which is always Bool.

TExprString

data class TExprString(val value: String, override val range: SourceRange, override val type: Type) : TExpr

EString typed, which is always String.

TExprVar

data class TExprVar(val name: String, override val range: SourceRange, override val type: Type) : TExpr

EVar typed: a name, and the type the environment had for it.

TExprTuple

data class TExprTuple(val elements: List<TExpr>, override val range: SourceRange, override val type: Type) : TExpr

ETuple typed, carrying a TTuple over its elements' types.

TExprList

data class TExprList(val elements: List<TExpr>, override val range: SourceRange, override val type: Type) : TExpr

EList typed, carrying a TList of one element type.

TExprTypeDecl

data class TExprTypeDecl(val declaration: TypeDecl, val constructors: List<TypedConstructor>, val body: TExpr, override val range: SourceRange, override val type: Type) : TExpr

ETypeDecl typed: the declaration as written, its constructors as inference bound them, and the program that follows.

Declaration Summary
val declaration: TypeDecl
val constructors: List<TypedConstructor>
val body: TExpr
override val range: SourceRange
override val type: Type

TypedConstructor

data class TypedConstructor(val name: String, val arity: Int, val scheme: Scheme)

What inference worked out about one constructor of a declared data type.

Declaration Summary
val name: String
val arity: Int
val scheme: Scheme

TExprLet

data class TExprLet(val name: String, val parameters: List<Pair<String, Type>>, val value: TExpr, val body: TExpr, override val range: SourceRange, override val type: Type, val recursive: Boolean = false) : TExpr

ELet typed, with a type beside each parameter name.

Declaration Summary
val name: String
val parameters: List<Pair<String, Type>>
val value: TExpr
val body: TExpr
override val range: SourceRange
override val type: Type
val recursive: Boolean True for the let f x = ... function form, whose own name is in scope inside its body. Plain let x = ... bindings stay non-recursive so that let x = x + 1 still shadows an outer x rather than referring to itself.

TExprLambda

data class TExprLambda(val parameters: List<Pair<String, Type>>, val body: TExpr, override val range: SourceRange, override val type: Type) : TExpr

ELambda typed, still holding every parameter it was written with.

Declaration Summary
val parameters: List<Pair<String, Type>>
val body: TExpr
override val range: SourceRange
override val type: Type

TExprCall

data class TExprCall(val function: TExpr, val argument: TExpr, override val range: SourceRange, override val type: Type) : TExpr

ECall typed: one argument applied to one function, carrying the result type.

Declaration Summary
val function: TExpr
val argument: TExpr
override val range: SourceRange
override val type: Type

TExprIf

data class TExprIf(val condition: TExpr, val thenBranch: TExpr, val elseBranch: TExpr, override val range: SourceRange, override val type: Type) : TExpr

EIf typed.

Declaration Summary
val condition: TExpr
val thenBranch: TExpr
val elseBranch: TExpr
override val range: SourceRange
override val type: Type

TPattern

sealed interface TPattern

A pattern after inference: a surface pattern, its range, and the type it matches against.

Declaration Summary
val range: SourceRange
val type: Type

TPWildcard

data class TPWildcard(override val range: SourceRange, override val type: Type) : TPattern

PWildcard typed. It matches anything, so its type is whatever the scrutinee's is.

TPVar

data class TPVar(val name: String, override val range: SourceRange, override val type: Type) : TPattern

PVar typed. The type here is what the name is bound to inside the arm's body.

TPInt

data class TPInt(val value: Long, override val range: SourceRange, override val type: Type) : TPattern

PInt typed, which forces the scrutinee to Int.

TPBool

data class TPBool(val value: Boolean, override val range: SourceRange, override val type: Type) : TPattern

PBool typed, which forces the scrutinee to Bool.

TPString

data class TPString(val value: String, override val range: SourceRange, override val type: Type) : TPattern

PString typed, which forces the scrutinee to String.

TPTuple

data class TPTuple(val elements: List<TPattern>, override val range: SourceRange, override val type: Type) : TPattern

PTuple typed, element by element against a TTuple of the same width.

TPList

data class TPList(val elements: List<TPattern>, override val range: SourceRange, override val type: Type) : TPattern

PList typed: every element against the one element type of a TList.

TPConstructor

data class TPConstructor(val name: String, val arguments: List<TPattern>, override val range: SourceRange, override val type: Type) : TPattern

PConstructor typed.

Declaration Summary
val name: String
val arguments: List<TPattern>
override val range: SourceRange
override val type: Type

TPCons

data class TPCons(val head: TPattern, val tail: TPattern, override val range: SourceRange, override val type: Type) : TPattern

PCons typed: head against the element type and tail against the list type.

Declaration Summary
val head: TPattern
val tail: TPattern
override val range: SourceRange
override val type: Type

TMatchCase

data class TMatchCase(val pattern: TPattern, val body: TExpr)

One arm of a TExprMatch, as a typed pattern and the body it guards.

Declaration Summary
val pattern: TPattern
val body: TExpr

TExprMatch

data class TExprMatch(val scrutinee: TExpr, val cases: List<TMatchCase>, override val range: SourceRange, override val type: Type) : TExpr

EMatch typed.

Declaration Summary
val scrutinee: TExpr
val cases: List<TMatchCase>
override val range: SourceRange
override val type: Type

TExprRecord

data class TExprRecord(val fields: List<Pair<String, TExpr>>, override val range: SourceRange, override val type: Type) : TExpr

ERecord typed, carrying a TRecord of its fields' types.

Declaration Summary
val fields: List<Pair<String, TExpr>>
override val range: SourceRange
override val type: Type

TExprField

data class TExprField(val target: TExpr, val field: String, override val range: SourceRange, override val type: Type) : TExpr

EField typed: the type reading that field off that target produced.

Declaration Summary
val target: TExpr
val field: String
override val range: SourceRange
override val type: Type

TExprBinary

data class TExprBinary(val op: BinaryOp, val left: TExpr, val right: TExpr, override val range: SourceRange, override val type: Type) : TExpr

EBinary typed, with the result type the operator's own rule produced for those operands.

Declaration Summary
val op: BinaryOp
val left: TExpr
val right: TExpr
override val range: SourceRange
override val type: Type

TExprNegate

data class TExprNegate(val value: TExpr, override val range: SourceRange, override val type: Type) : TExpr

ENegate typed, over the numeric or vector type its operand turned out to have.

Declaration Summary
val value: TExpr
override val range: SourceRange
override val type: Type

Types.kt

The types FieldML infers, and the two ways one is printed back to a reader.

Type is the vocabulary inference works in: five ground types, a unification variable TVar, an arrow TFun, the structural cases TRecord, TTuple and TList, TAdt for a declared data type applied to its arguments, and THost for a JVM type reached over the host bridge, whose name is what FieldMLHostTypes reflects on. TFun takes one argument at a time, matching the curried application the parser builds, so a function of two arguments is TFun(a, TFun(b, c)).

A Scheme is a type together with the variable ids generalised over it, and it can restrict what those ids are allowed to become. An id listed in numeric only unifies with Int or Float, and one listed in vector only with Vec2, Vec3 or Vec4. That is how the prelude gives abs, min, max and clamp one scheme each rather than one per numeric type, and dot, vlength, normalize and mix one each rather than one per vector width.

TypePrinter renders a type two ways. print parenthesises every arrow, and is what diagnostics and a box's inline result use; printSignature drops the parentheses that right associativity makes redundant, and is what completion shows and what the cheatsheet's coverage guard compares against. A printer names unification variables 'a, 'b, … in order of first appearance and keeps those names for its own lifetime, so two types printed by one printer agree about which variable is which, while two printers each start again at 'a.

src/jvmMain/kotlin/afield/lang/fieldml/type/Types.kt — read the source

Type

sealed interface Type

An inferred FieldML type.

TInt

data object TInt : Type

The type of an integer.

TFloat

data object TFloat : Type

The type of a floating-point number.

TBool

data object TBool : Type

The type of a boolean.

TString

data object TString : Type

The type of a string.

TUnit

data object TUnit : Type

The type with one value, which is what () has.

TVar

data class TVar(val id: Int) : Type

A unification variable, identified by the number inference gave it.

TFun

data class TFun(val from: Type, val to: Type) : Type

A function from one argument to one result.

Declaration Summary
val from: Type
val to: Type

TRecord

data class TRecord(val fields: Map<String, Type>) : Type

A record type: a field name for each of its fields' types.

Declaration Summary
val fields: Map<String, Type>

TTuple

data class TTuple(val elements: List<Type>) : Type

A tuple type, in the order the elements were written.

Declaration Summary
val elements: List<Type>

TList

data class TList(val element: Type) : Type

A list type, which is homogeneous: every element has the one element type.

Declaration Summary
val element: Type

TAdt

data class TAdt(val name: String, val arguments: List<Type>) : Type

A data type declared by a type declaration, applied to as many arguments as it takes.

Declaration Summary
val name: String
val arguments: List<Type>

THost

data class THost(val name: String) : Type

A JVM type reached over the host bridge.

Declaration Summary
val name: String

Scheme

data class Scheme(val quantified: Set<Int>, val body: Type, val numeric: Set<Int> = emptySet(), val vector: Set<Int> = emptySet())

A type together with the variables generalised over it, and any restriction on what they may become.

Declaration Summary
val quantified: Set<Int> The TVar ids generalised over body. An empty set is a type with nothing to instantiate.
val body: Type The type being generalised.
val numeric: Set<Int> Quantified ids that may only be instantiated to Int or Float.
val vector: Set<Int> Quantified ids that may only be instantiated to Vec2/Vec3/Vec4.

TypePrinter

class TypePrinter

Renders a Type back to the two forms a reader sees.

Declaration Summary
fun printSignature(type: Type): String Like print, but without the parentheses that print puts around every arrow. Since -> is right associative, a curried type only needs them when a function is an argument: Float -> Float -> Vec2 rather than (Float -> (Float -> Vec2)), but still (Int -> 'a) -> Int -> ['a]. This is the form to show a reader — signatures in the cheatsheet and in completion — while print stays as it is for inline results and diagnostics.
fun print(type: Type): String The fully parenthesised form, with brackets around every arrow.

ForthInterface.kt

Makes ForthField a language a box can be written in.

ForthInterpreter.kt is the language itself. This file is the half that joins it to the workspace: the BoxExecution a ForthField box runs under, and the registration that puts "Set Main Language: ForthField" in front of a user.

Running code

ForthExecution answers the execution contract a box's editor talks to. Running a fragment, or the whole box, evaluates the source once and reports each diagnostic on the line that produced it, printing ok — with what the run left on the stack, the top value in bold — when a run finished cleanly and produced no visible output of its own. Completion offers the dictionary's words, each with the kind and the documentation the interpreter holds for it. Executing a symbol runs one named word as a single-line fragment.

Launching is the one that is more than an evaluation. The code runs once to define its words, and if its last token names a colon definition, that word becomes a strand: a coroutine that re-executes it until the box's run generation for that strand moves on, yielding between passes. frame is available only there, and it ends a pass rather than the strand, so a word can be written as the body of a per-frame loop. Anything else the code ends with defines words and stops. Stopping the box cancels every strand job and bumps every run generation.

Becoming a ForthField box

A box does not choose this language for itself; its parent does. installForth registers a MainLanguageRuntime under the id forth, and makes the isForthRoot flag one of the properties a document persists. installForthPlugin starts a TopologyWatcher, and forthTopologyNeedsUpdating is what that watcher calls on every graph edit: each child of a box flagged isForthRoot, or set to the forth main language, is given a ForthExecution, and a child left with no such parent goes back to Kotlin. Each child gets an interpreter of its own, so a stack and a dictionary belong to one box and outlive the run that filled them.

src/jvmMain/kotlin/afield/lang/forth/ForthInterface.kt — read the source

Top-level functions

Declaration Summary
fun ForthInterpreter.evalRecordingLineNotes(box: Box, code: String, lineOffset: Int): List<ForthDiagnostic> ForthInterpreter.eval of code, recording for box's line notes (afield.ui.output.LastRunNotes) the data stack after each line — after the last top-level step that ended on it — the words each line defined, the line a failure stopped on and the lines that never ran. lineOffset is where code starts in the box, as Execute.line counts.
fun installForth() Register ForthField as a main language, and make isForthRoot a property documents keep.
fun installForthPlugin() Start the watcher that re-applies forthTopologyNeedsUpdating whenever the graph changes.
fun forthTopologyNeedsUpdating(values: Collection<Box>) Give every child of a ForthField root a ForthExecution, and take it back from a child whose roots have all been switched off.

Top-level values

Declaration Summary
var Box.isForthRoot: Boolean? Marks a box as a ForthField root, which makes its children ForthField boxes.

ForthExecution

class ForthExecution(val inside: Box) : BoxExecution

The execution a ForthField box runs under: one ForthInterpreter, and the editor protocol around it.

Declaration Summary
val forth This box's interpreter, holding the stack, the dictionary and the variables.
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit) Evaluate execute's code once, with output and errors routed back to the box.
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit) Run the whole box, which for ForthField is executeFragment over all of it.
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit) Run code once, and then keep running its last word every pass if that word is a colon definition.
override fun stop(inside: Box) Stop every strand this box is running.
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit) Complete the word being typed against this box's dictionary.
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit) Run one named word, as though its name had been typed and evaluated on its own line.
override val languageName: String forth, the name this execution reports itself under.

ForthInterpreter.kt

ForthField — a compact, extensible Forth used by Field4 for live coding.

It is host-driven: - it executes ForthField source and keeps its own data stack/dictionary - it ships with creative-coding oriented builtins (vectors + FLine) - it can be extended at runtime from Kotlin via defineWord - it can interoperate with host objects using @? / @?! reflection calls

The host loop

Typical host loop: 1. Construct an interpreter once (val forth = ForthInterpreter()). 2. Register host bridge words with defineWord as needed. 3. Push host objects (buffers, context values, services) onto stack. 4. Run eval(...). 5. Pop resulting values (for example an FLine) and hand them to rendering/output code.

Example: basic execution and stack access:

val forth = ForthInterpreter()
val diagnostics = forth.eval("2 3 +")
check(diagnostics.isEmpty())
println(forth.popNum()) // 5.0

Example: defining words in ForthField:

: sqr ( n -- n2 ) dup * ;
: hypot2 ( x y -- d2 ) sqr swap sqr + ;
3 4 hypot2 .

Example: create an FLine shape in ForthField:

: stroke-ribbon ( -- f )
  fline
  0   0   moveto
  120 20  lineto
  160 90  210 90  260 20 cubicto
  320 0   lineto
  true  fline-stroked
  false fline-filled
  1.0 0.55 0.2 1.0 fline-color
  2.5 fline-thickness ;

stroke-ribbon

Host-side consumption of the resulting FLine:

val forth = ForthInterpreter()
val source = """
  : stroke-ribbon ( -- f )
    fline
    0 0 moveto
    120 20 lineto
    160 90 210 90 260 20 cubicto
    true fline-stroked
    1 0.55 0.2 1 fline-color
    2.5 fline-thickness ;
  stroke-ribbon
""".trimIndent()

val diagnostics = forth.eval(source)
if (diagnostics.isEmpty()) {
  val line = forth.pop() as FLine
  // Forward `line` into your output channel / renderer.
}

Example: use vector helpers with FLine convenience words:

: zig ( -- f )
  fline
  0   0   vec2 fline-moveto-v2
  40  20  vec2 fline-lineto-v2
  80 -10  vec2 fline-lineto-v2
  120 25  vec2 fline-lineto-v2
  1 0 0 1 fline-node-color ;
zig

The reflection bridge

Example: resolve classes without full FQNs. class-for consults Field's class cache (importHelp.kt) so short names usually work.

s"FLine" class-for @? getName println
s"Vec3"  class-for @? getName println

s"Fli" class-matches print   \ returns a list of FQNs
s"Fli" class-help            \ prints one-per-line suggestions

Example: reflection bridge (@? / @?!) with FLine, built entirely by class lookup. This demonstrates construction + method calls without using the fline helper.

variable f
s"FLine" class-for @? newInstance f !

\ Explicit arity call: f.moveTo(Number, Number, Number)
f @ 0   0   0   3 @?! moveTo drop

\ Explicit arity call: f.lineTo(Number, Number, Number)
f @ 120 30  0   3 @?! lineTo drop
f @ 220 10  0   3 @?! lineTo drop
f @ 260 80 0  320 80 0  380 20 0  9 @?! cubicTo drop

\ Properties are just a map on FLine
f @ @? properties  s"thickness" 2.5 2 @?! put
f @ @? properties  s"stroked" true 2 @?! put

\ Heuristic call for zero-arg methods works well with @?
f @ @? close drop

\ Leave the final FLine on the stack
f @

Example: @? reads data too (field/getter/method fallback):

f @ @? nodes   \ push internal node list
@? size .      \ call size() on that list and print node count

Example: constructor with explicit args via @?! new:

variable sb
s"StringBuilder" class-for s"hello" 1 @?! new sb !
sb @ @? toString println

Example: extending ForthField with Kotlin bridge words. Here we expose a helper that appends a vec4 worth of floats into any DynamicallyWriteable buffer (for example VulkanBuffer or FrameBufferGroup).

import afield.graphics.vulkan.buffers.DynamicallyWriteable
import afield.math.linalg.Vec4

val forth = ForthInterpreter()
forth.defineWord("append-vec4", "( buf x y z w -- buf )") {
  val w = popNum().toFloat()
  val z = popNum().toFloat()
  val y = popNum().toFloat()
  val x = popNum().toFloat()
  val buf = pop() as DynamicallyWriteable

  buf.append {
    put(Vec4(x, y, z, w))
  }
  push(buf)
}

Example: call that bridge word from ForthField after pushing a host buffer:

forth.push(myDynamicBuffer) // host object: VulkanBuffer / FrameBufferGroup / similar
forth.eval("""
  0.0 1.0 0.0 1.0 append-vec4
  1.0 0.0 0.0 1.0 append-vec4
  0.0 0.0 1.0 1.0 append-vec4
""".trimIndent())

Example: long-form output areas from ForthField:

s"<b>Inline-ish output area content</b>" html
s"<pre>Detailed log text...</pre>" s"debug" html-channel

Example: launchable animation with cooperative yielding via frame. Run this in Launch mode (or call begin("default") on the box), so spinner executes every RunLoop tick. frame yields immediately, letting the rest of Field proceed. Store evolving state in variables, because each strand tick re-enters the word from the top.

variable tick
variable line
0 tick !

: spinner ( -- )
  s"FLine" class-for @? newInstance line !

  \ center point
  line @ 0 0 0 3 @?! moveTo drop

  \ animated endpoint (simple circular motion)
  line @
  tick @ 0.05 * dup cos 120 *          ( x )
             swap sin 120 *            ( x y )
  0 3 @?! lineTo drop

  \ style via reflected property map
  line @ @? properties s"thickness" 2.0 2 @?! put
  line @ @? properties s"stroked" true 2 @?! put
  line @ s"geom" display-channel

  \ show a debug line in output area
  s"tick=" tick @ str str+ s"debug" display-channel

  1 tick +!
  frame
;

spinner

Example: same pattern with an explicit ForthField loop. frame exits the current tick immediately; on next tick the word starts again.

variable phase
0 phase !

: pulse-loop ( -- )
  begin
    s"phase=" phase @ str str+ println
    1 phase +!
    frame
  again ;

pulse-loop

Example: full overwrite of float data each tick, since write replaces the payload rather than adding to it:

import afield.graphics.vulkan.buffers.DynamicallyWriteable

forth.defineWord("write-triangle", "( buf -- buf )") {
  val buf = pop() as DynamicallyWriteable
  buf.write {
    // XYZW packed per vertex (3 vertices)
    put(-0.5f); put(-0.5f); put(0f); put(1f)
    put( 0.5f); put(-0.5f); put(0f); put(1f)
    put( 0.0f); put( 0.5f); put(0f); put(1f)
  }
  push(buf)
}

Notes for dynamic-buffer bridging: - defineWord closures execute on the interpreter thread/context used by your host. - Prefer write { ... } when replacing the payload; append { ... } when accumulating. - DynamicBuffer.put(Vec4) follows std430-friendly alignment rules used by Field's buffer code. - If you need explicit control, call putScalar(...) repeatedly instead of vector helpers.

Builtins

Builtins included by default: - stack ops (dup, swap, over, pick, roll, ...) - math/logic/control flow (if/else/then, begin/until, do/loop, ...) - variables/constants (variable, !, @, constant) - strings/output (."...", s"...", ., println, ...) - dictionary tools (words, see, forget, recurse) - return-stack transfer (>r, r>, r@) - vectors (vec2, vec3, vec4 + arithmetic helpers) - FLine primitives (fline, moveto, lineto, cubicto, styling words) - small visualization words (html, watch, stack-display, bar, plot-stack) - frame/yield primitive (frame) for cooperative launch-mode animations - host reflection hooks (@?, @?!, class-for, class-matches, class-help)

Dictionary, recursion and the return stack

Dictionary / debugging examples:

: sqr dup * ;
words              \ print all currently known words
see sqr            \ print ": sqr dup * ;"
forget sqr         \ remove the user-defined word

Recursion example:

: countdown ( n -- )
  dup 0> if
    dup println
    1- recurse
  else
    drop
  then ;
5 countdown

Infinite loop example. Use again mainly in launched words where frame yields back to Field each tick:

variable tick
: ticker begin tick @ println 1 tick +! frame again ;
ticker

Return-stack examples. >r moves a temporary value away from the data stack, r@ copies it back, and r> restores/removes it:

: add-twice ( a b -- a+b+b )
  >r        \ return stack: b ; data stack: a
  r@ +      \ data stack: a+b, b is still saved
  r> + ;    \ data stack: a+b+b

Visualization words

Visualization examples:

s"<b>Hello from Forth</b>" html
0.72 1.0 s"progress" bar
42 s"answer" watch
stack-display
1 4 9 16 plot-stack

src/jvmMain/kotlin/afield/lang/forth/ForthInterpreter.kt — read the source

ForthInterpreter

class ForthInterpreter

The interpreter: the data stack, the dictionary, the tokenizer and eval.

Declaration Summary
data class Token(val text: String, val line: Int)
data class CallFrame(val word: String, val line: Int)
var currentLine: Int
var currentToken: String
val stack
val returnStack
val callStack
val dictionary
val variables
var onOutput: (String) -> Unit
var onDisplay: (String, Any?) -> Unit
var onFrame: () -> Unit
val strands
var onTopLevelStep: ((firstLine: Int, lastLine: Int, defined: String?, endsLine: Boolean) -> Unit)? Called after each top-level step of eval — a word, a literal, a whole : … ; definition or if … then — with the first and last source lines the step's tokens sat on (1-based, in the evaluated text), the name it defined, if it was a definition, and whether it is the last step to end on its line (the next step starts on a later line, or there is none) — the moment a line's note is about. Steps inside a word's body do not count. The box records the stack here for its line notes; null costs nothing.
fun push(v: Any)
fun pop(): Any
fun peek(): Any
fun popNum(): Double
fun popInt(): Int
fun anyToDouble(v: Any): Double
fun anyToBool(v: Any): Boolean
fun stackSnapshot(): List<String> The data stack, bottom first, each value as a diagnostic shows it.
fun stackTop(n: Int): List<String> The top n values of the data stack, bottom first, as stackSnapshot shows them: cheap however deep the stack is.
fun eval(source: String): List<ForthDiagnostic> Evaluate a string of ForthField code. Returns a list of diagnostics (empty = success).
fun diagnosticFor(e: ForthError): ForthDiagnostic
fun tokenize(source: String): List<Token>
fun execTokens(tokens: List<Token>, start: Int, end: Int, topLevel: Boolean = false): Int Execute tokens[start..end) — the core interpreter loop. Returns the index one past the last token consumed (= end, normally).
fun execWord(fw: ForthWord)
fun complete(code: String, cursor: Int): List<ForthCompletion>
fun describeWord(name: String): String
fun forgetWord(name: String, line: Int = -1)
fun defineWord(name: String, doc: String = "", impl: ForthInterpreter.() -> Unit)
fun addStrand(strandName: String, wordName: String)
fun removeStrand(strandName: String)
fun tickStrands()
fun reflectMember(obj: Any, memberName: String, line: Int = -1) @? implementation: access a field, property, or method on obj by name.
fun reflectCallN(obj: Any, memberName: String, args: List<Any>, line: Int = -1) @?! implementation: call a method on obj with an explicit argument list.

ForthWord

sealed class ForthWord

Declaration Summary
data class Builtin(val name: String, val doc: String, val impl: ForthInterpreter.() -> Unit) : ForthWord()
data class Compiled(val name: String, val body: List<ForthInterpreter.Token>, val sourceLine: Int = -1) : ForthWord()
data class Variable(val name: String) : ForthWord()
data class Constant(val name: String, val value: Any) : ForthWord()
data class Lambda(val name: String, val doc: String, val impl: ForthInterpreter.() -> Unit) : ForthWord()

ForthError

class ForthError(message: String, val line: Int = -1) : RuntimeException(message)

ForthFrameYield

class ForthFrameYield : Throwable()

ForthDiagnostic

data class ForthDiagnostic(val message: String, val line: Int)

ForthCompletion

data class ForthCompletion(val word: String, val kind: String, val documentation: String)

Declaration Summary
val word: String
val kind: String
val documentation: String

LuaCatalog.kt

The signatures and one-line descriptions that completion shows for Lua names, held to a live LuaJIT by LuaCatalogTest.

src/jvmMain/kotlin/afield/lang/lua/LuaCatalog.kt — read the source

LuaCatalog

object LuaCatalog

What completion says about a Lua name: its signature and one line on what it does.

Declaration Summary
class Entry(val name: String, val signature: String, val doc: String, val fromField: Boolean) One catalogued function: name as it is reached (string.format, Line.moveTo, print).
val stdlib: Map<String, Entry> by lazy The Lua and LuaJIT library.
val field: Map<String, Entry> by lazy What Field's prelude adds.
fun lookup(path: String, name: String): Entry? The entry for name reached at path ("" for a global, "string", "field", "field.Line"). Field's entry wins where both have one — print goes to the box, not stdout.

LuaLanguage.kt

Lua as a Field box language: LuaExecution runs a box's text in that box's own LuaJIT state, and installLuaLanguage registers Lua as a Main Language.

LuaBoxTest drives Lua boxes end to end.

src/jvmMain/kotlin/afield/lang/lua/LuaLanguage.kt — read the source

Top-level functions

Declaration Summary
fun installLuaLanguage()
fun installLuaLanguagePlugin()
fun luaTopologyNeedsUpdating(values: Collection<Box>)

Top-level values

Declaration Summary
var Box.isLuaRoot: Boolean?

LuaExecution

class LuaExecution(val inside: Box) : BoxExecution

Lua as a box language, over LuaJIT: set a box's Main Language to Lua and its children hold Lua.

Declaration Summary
override val languageName: String
fun luaState(): LuaState The box's Lua, opened on first use. Main thread only.
val hasState: Boolean Whether this box has a Lua state open now; for tests and the census.
fun close() Close the box's Lua now; the next run opens a fresh one.
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit)
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override fun stop(inside: Box)
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit)
fun luaCompletions(path: String, prefix: String): List<Triple<String, String, String>> Names to offer at path ("" for the globals) that start with prefix: text, kind, documentation.

LuaNative.kt

Loading LuaJIT and binding its C API: LuaLibrary finds the optional native library, and the FFM downcalls that LuaState uses are bound here.

src/jvmMain/kotlin/afield/lang/lua/LuaNative.kt — read the source

LuaLibrary

object LuaLibrary

Where LuaJIT is, and what to do when it is not there.

Declaration Summary
val fileNames: List<String> The platform's file names for LuaJIT, most specific first.
val searchedPaths: List<Path> Every path resolvedPath tries, in order — quoted in the "not installed" error.
val resolvedPath: Path? Not cached: installing LuaJIT during a live session is picked up by the next box run.
val isAvailable: Boolean True when a LuaJIT is where searchedPaths looks.
val HOW_TO_INSTALL: String The one instruction a missing library needs.
fun describe(): String One line for a box or a test to print.

LuaState.kt

The LuaJIT state behind a Lua box: LuaState opens a lua_State, runs chunks under lua_pcall, and marshals values between Lua and Kotlin.

LuaStateTest exercises it against a live LuaJIT.

src/jvmMain/kotlin/afield/lang/lua/LuaState.kt — read the source

LuaState

class LuaState(raw: Map<String, Handler> = emptyMap()) : AutoCloseable

One LuaJIT lua_State: open, evaluate, marshal values both ways, let Lua call Kotlin, close.

Declaration Summary
fun interface Handler A Kotlin function Lua can call. args are marshalled Lua values; the returned list is pushed as the results.
class LuaFunction internal constructor(val state: LuaState, internal val ref: Int) A Lua function held from Kotlin: a reference in the registry, released when the state closes or release is called.
sealed class Result What an evaluation came to.
val isClosed: Boolean True once close has run; every other method then refuses.
val version: String jit.version — "LuaJIT 2.1.…" — or _VERSION for a plain Lua.
val memoryKb: Int Kilobytes the Lua heap holds.
fun setGlobal(name: String, value: Any?) Set global name to value, marshalled. A Handler becomes a Lua function that raises on failure.
fun getGlobal(name: String): Any? Global name, marshalled; raw access, so a strict-mode metatable on _G is not consulted.
fun run(code: String, chunkName: String = CHUNK): Result Compile and run code as a chunk called chunkName (the name Lua puts in front of an error's line number: =box gives box:3: …). Returns what the chunk returned, or the error.
fun load(code: String, chunkName: String = CHUNK): Any Compile code without running it: a LuaFunction, or the syntax error as a Result.Err.
class Outcome(val result: Result, val lines: Map<Int, LineRecord>, val truncated: Boolean) What one guarded run came to: the result, and when lines were recorded, for each 1-based line of the chunk that ran, how often and (for the main chunk) the names it assigned with their values.
class LineRecord(val count: Int, val main: Boolean, val values: List<Pair<String, String>>) One recorded line: count times it ran, main whether in the chunk's top level, values name → value as shown.
fun runGuarded(code: String, lineNames: Map<Int, List<String>>?, asExpression: Boolean = false): Outcome Run code under the prelude's guard — stopped with an error after field.budget seconds — and, when lineNames is given (1-based chunk line → names assigned on it), record its lines as it runs. With asExpression, return <code> is tried first, the way the Lua REPL treats a line.
fun tick(now: Double, dt: Double): Result One frame of a launched box: update(dt) and the due tasks, guarded by field.frameBudget.
fun callGuarded(fn: LuaFunction, vararg args: Any?): Result Call fn under the frame budget: a callback from Field (a slider moving) into the box's Lua.
fun call(name: String, vararg args: Any?): Result Call the global function name with args.
fun call(fn: LuaFunction, vararg args: Any?): Result Call fn with args.
override fun close() Close the state: every LuaFunction and Handler it handed out goes with it. Idempotent.
data class BoxRef(val id: String) A Lua proxy for a Field box, as it arrives in Kotlin: the box's id.

MorphoCircuit.kt

The grow-and-draw handle: a compiled MorphoHDL circuit that expands a few cells per frame while its layout relaxes between expansions.

MorphoCircuit.compile takes a design name and a bus signature and returns something compiled but not yet expanded. step is shaped to be the condition of a run loop, so the recursive division is watchable rather than already finished; growFully is the same circuit with the watching skipped. Either way the current MorphoNetlist and MorphoTiming are readable between steps, which is what lets a neighbouring Kotlin box draw a half-built circuit.

A morpho box publishes one of these on Box.morphoCircuit; a Kotlin box builds its own with morphoCircuit(...) from MorphoFLines.kt. It holds a guest-side session and a WASM layout, so it has to be closed.

src/jvmMain/kotlin/afield/lang/morpho/MorphoCircuit.kt — read the source

MorphoCircuit

class MorphoCircuit private constructor(val design: String, val signature: IntArray, private val session: MorphoSession, val layout: MorphoLayout3D) : Closeable

A MorphoHDL circuit that is being grown and laid out at the same time — the thing a morpho box holds, and the thing a Kotlin box draws.

Declaration Summary
val design: String
val signature: IntArray
val layout: MorphoLayout3D
var netlist: MorphoNetlist The circuit as it stands. Replaced, not mutated, on every step.
var timing: MorphoTiming Signal arrival times for netlist.
var growing: Boolean False once every cell has been expanded.
val expansions: Int Expansions performed so far.
fun step(grow: Int = 16, relax: Int = 6, params: MorphoLayout3D.Params = MorphoLayout3D.Params()): Boolean Advance one frame: expand up to grow cells, then relax the layout relax iterations.
fun growFully(relaxSteps: Int = 200): MorphoCircuit Expand to completion, then settle the layout for relaxSteps iterations.
fun evaluate(vararg samples: LongArray): Array<LongArray> Run the circuit on a batch of integer test vectors. See MorphoSim.evaluate.
fun evaluateOne(vararg ports: Long): LongArray Run the circuit on one test vector.
val depth: Float Critical-path depth: the number that separates a ripple adder from a Brent–Kung.
fun summary(): String A one-line description with every number that matters.
override fun close()

MorphoFLines.kt

Drawing a MorphoHDL circuit with Field's FLines.

This is the half of the bridge morpho does not supply and Field does. Upstream renders with SwissGL and a Canvas 2D fallback (viewer.js, layout_renderer.js, bloom.js — about 1,900 lines), none of which is vendored. Field already draws graphs, so a circuit becomes one FLine per wire and one point-set per gate, and everything downstream — SVG export, the plane grid, animation, the docs capture pipeline — comes along for free.

Colour carries the simulation

By default wires are coloured by signal arrival time, which is what makes the picture worth looking at rather than a hairball: the inputs are one end of the ramp, the critical path is the other, and the difference between a ripple adder and a Brent–Kung is visible as a gradient rather than inferable from a gate count.

src/jvmMain/kotlin/afield/lang/morpho/MorphoFLines.kt — read the source

Used in: MorphoHDL in Field, Searching for MorphoHDL Circuits

Top-level functions

Declaration Summary
fun morphoCircuit(design: String, vararg signature: Int, source: String = MorphoRuntime.standardLibrary, optimize: Boolean = true, seed: Int = 1337): MorphoCircuit Compile a MorphoHDL design, ready to grow.
fun morphoOrbit(azimuth: Double, elevation: Double = 0.5235988): MorphoProjector A view from an arbitrary angle around the vertical axis.
fun MorphoCircuit.planeBounds(projection: MorphoProjector = MorphoProjection.ISOMETRIC, padFraction: Double = 0.08): DoubleArray Projected bounds of a laid-out circuit as [xMin, yMin, xMax, yMax], square and padded.
fun morphoWindowUnion(a: DoubleArray?, b: DoubleArray): DoubleArray The smallest window containing both, for a frame that grows with the circuit but never shrinks.
fun morphoUnitWindow(): Rect The window for panels drawn with fit = true, which normalise themselves into the unit square.
fun morphoPlaneWindow(window: DoubleArray): Rect A planeBounds result as the Rect afield.geometry.spatial.showPlaneGrid wants.
fun MorphoCircuit.flines(projection: MorphoProjector = MorphoProjection.ISOMETRIC, colormap: String = "magma", byDepth: Boolean = true, minIntensity: Double = 0.15, thinWire: Double = 0.7, thickWire: Double = 1.6, gateSize: Double = 0.45, portSize: Double = 1.1, fit: Boolean = false): List<FLine> One FLine per wire, plus one point-set per cell type.
fun morphoFLines(netlist: MorphoNetlist, timing: MorphoTiming?, positions: FloatArray, cellIndices: IntArray, projection: MorphoProjector = MorphoProjection.ISOMETRIC, colormap: String = "magma", byDepth: Boolean = true, minIntensity: Double = 0.15, thinWire: Double = 0.7, thickWire: Double = 1.6, gateSize: Double = 0.45, portSize: Double = 1.1, fit: Boolean = false): List<FLine> The same drawing from raw parts, for callers holding a netlist and positions from somewhere other than MorphoCircuit — a recorded layout, say, or Field's own graph systems standing in for the WASM kernel.

MorphoProjector

fun interface MorphoProjector

Flattens a 3D layout position to the plane a drawing lives in.

Declaration Summary
fun project(x: Float, y: Float, z: Float): Pair<Double, Double>

MorphoProjection

enum class MorphoProjection : MorphoProjector

The three fixed views worth having names.

Constants: XY, ISOMETRIC, XZ.

Declaration Summary
override fun project(x: Float, y: Float, z: Float): Pair<Double, Double> Look down the y axis — the view in which a "flat" layout would be a horizontal line.

MorphoGenerator.kt

Where MorphoHDL programs come from when nobody writes them.

A sketch fixes everything about a cell that the search is not looking for — its name, its ports, what it falls back into — and leaves the body open. That split is deliberate: "find a circuit" with nothing fixed is a search over parse errors, and the interesting question is not whether a random walk can discover the token def.

Repair rather than reject

Mutation operators here are allowed to produce nonsense, and MorphoProgramGenerator.repair projects it back into the region the width model calls plausible: an argument that names a wire out of scope is rewired, a SPLIT of a one-bit bus is deleted, a recursive call that would not narrow has its arguments resampled. Rejection-and-resample would be simpler and much worse — with a rejection rate near one the search stops being a search.

The compiler still has the last word. Repair only makes candidates likely to compile; the fraction that does not is reported as MorphoSearchRun.failureRate rather than hidden.

src/jvmMain/kotlin/afield/lang/morpho/MorphoGenerator.kt — read the source

MorphoCellSketch

class MorphoCellSketch(val name: String, val inputs: List<String>, val inputRoles: List<MorphoPortRole>, val outputRoles: List<MorphoPortRole>, val palette: MorphoPalette, val fallback: String? = null, val designWidth: Int = 8, val allowSplit: Boolean = true, val allowCat: Boolean = true, val allowSelf: Boolean = true, val minStatements: Int = 2, val maxStatements: Int = 10, val prologue: List<MorphoStatement> = emptyList())

The fixed frame around one evolvable cell.

Declaration Summary
val name: String
val inputs: List<String>
val inputRoles: List<MorphoPortRole>
val outputRoles: List<MorphoPortRole>
val palette: MorphoPalette
val fallback: String?
val designWidth: Int
val allowSplit: Boolean
val allowCat: Boolean
val allowSelf: Boolean
val minStatements: Int
val maxStatements: Int
val prologue: List<MorphoStatement> Statements always present at the top of the body, which mutation cannot remove.
val inputWidths: IntArray
val outputWidths: IntArray
fun selfShape(argWidths: IntArray): IntArray What a recursive call returns, given what it is passed.
val opcodes: List<MorphoOpcode> The generator's opcode set for this cell: the palette, plus whichever structural ops are on.
fun trace(cell: MorphoCellGenome): MorphoWidthModel.Trace

MorphoProgramGenerator

class MorphoProgramGenerator(val sketches: List<MorphoCellSketch>, val preamble: String)

Random programs, and small changes to programs.

Declaration Summary
val sketches: List<MorphoCellSketch>
val preamble: String
fun random(rng: Random): MorphoGenome A fresh program drawn from the sketch's distribution.
fun mutate(genome: MorphoGenome, rng: Random): MorphoGenome One point change, repaired and pruned.
fun mutate(genome: MorphoGenome, rng: Random, n: Int): MorphoGenome n independent point changes — a bigger jump, for restarting a stuck climb.
fun crossover(a: MorphoGenome, b: MorphoGenome, rng: Random): MorphoGenome Take whole cells from either parent.
fun repair(cell: MorphoCellGenome, sketch: MorphoCellSketch, rng: Random): MorphoCellGenome Rebuild a mutated cell so the width model has no complaints left, dropping what cannot be saved.
fun settle(cell: MorphoCellGenome, sketch: MorphoCellSketch, rng: Random): MorphoCellGenome Repair, then guarantee the recursion stops — dropping self-calls, latest first, until MorphoWidthModel.terminates agrees.

MorphoGenome.kt

The thing a search actually mutates: a MorphoHDL program, as text.

A generator could build netlists directly and skip the compiler, and it would be faster. It would also optimise against Field's idea of what morpho compiles a design into rather than against MorphoHDL. So a genome renders to source, upstream's parse() reads it, upstream's compileModule() compiles it, and anything the search finds can be pasted into a morpho box and will still be there tomorrow.

The grammar this has to hit

parser.js is 190 lines and its accepted language is small (parser.js:23-188):

  • top-level Name = LUT(arity, value);
  • @morpho or @morpho(fallback=X), then def name(a, b, c):;
  • SSA lines t0, t1 = OP(x, y), one per line;
  • return x, y.

That is all MorphoGenome.render emits, and it is why the rendered text is legible: it is not a serialisation format, it is the language.

Where the widths come from

MorphoHDL infers bus widths and never declares them, so a generator that ignores widths produces mostly-invalid programs. MorphoWidthModel is Field's model of that inference — it mirrors primitives.js's SPLIT/CAT and compiler.js's materializeLut broadcast rule — and it is used only to steer generation. The compiler remains the judge; the model's accuracy shows up as MorphoSearchRun.failureRate and nowhere else.

src/jvmMain/kotlin/afield/lang/morpho/MorphoGenome.kt — read the source

Used in: Searching for MorphoHDL Circuits

MorphoLut

data class MorphoLut(val name: String, val arity: Int, val value: Int)

A LUT standard cell: a truth table and the number of inputs it reads.

Declaration Summary
fun declaration(): String As parser.js:165 wants to read it back.

MorphoOpKind

enum class MorphoOpKind

What a generated SSA line can be.

Constants: LUT, SPLIT, CAT, SELF.

MorphoOpcode

data class MorphoOpcode(val name: String, val arity: Int, val results: Int, val kind: MorphoOpKind)

One callable thing, with its shape.

MorphoPalette

class MorphoPalette(val luts: List<MorphoLut>)

The standard cells a search is allowed to build out of.

Declaration Summary
val ops: List<MorphoOpcode>
fun declarations(): String The palette's declarations, ready to head a rendered program.
fun withArities(arities: Set<Int>): MorphoPalette Only the cells with the given arities — a 1-bit base case has no use for a 4-input mux.
fun only(vararg names: String): MorphoPalette Only the named cells, in the order named.

MorphoStatement

data class MorphoStatement(val targets: List<String>, val op: String, val args: List<String>)

One SSA line: targets = op(args).

Declaration Summary
fun render(): String

MorphoCellGenome

data class MorphoCellGenome(val name: String, val inputs: List<String>, val statements: List<MorphoStatement>, val outputs: List<String>, val fallback: String?)

One @morpho def — a cell body plus the ports it presents.

Declaration Summary
val name: String
val inputs: List<String>
val statements: List<MorphoStatement>
val outputs: List<String>
val fallback: String?
fun render(): String
fun pruned(): MorphoCellGenome Drop statements nothing reachable from outputs consumes.
fun normalized(): MorphoCellGenome Rename every target to t0, t1, … in order, so two structurally identical cells render identically.

MorphoGenome

data class MorphoGenome(val cells: List<MorphoCellGenome>, val preamble: String)

A whole searchable program: one or more cells plus whatever fixed text they are compiled against.

Declaration Summary
val cells: List<MorphoCellGenome>
val preamble: String LUT declarations and any fixed helper cells, emitted verbatim above the evolved ones.
val rootName: String
fun render(): String The program as the compiler sees it: dead statements dropped, wires renumbered.
fun phenotype(): MorphoGenome The live part of the program, which is what render emits and what gets compiled.
fun pruned(): MorphoGenome
fun normalized(): MorphoGenome
val statementCount: Int Statements across every cell, introns included — what the genome costs.
val liveStatementCount: Int Statements that survive into the compiled circuit.

MorphoWidthModel

object MorphoWidthModel

Field's model of MorphoHDL's width inference, used to steer generation and never to judge it.

Declaration Summary
val CONSTANTS Widths morpho gives its two built-in constants — one net each (compiler.js:949).
fun trace(cell: MorphoCellGenome, inputWidths: IntArray, palette: MorphoPalette, selfShape: (IntArray) -> IntArray): Trace Walk cell with inputWidths and report the width of every wire it defines.
class Trace(val widths: Map<String, Int>, val problems: List<String>, val hitsBoundary: Boolean = false)
fun terminates(cell: MorphoCellGenome, inputWidths: IntArray, palette: MorphoPalette, selfShape: (IntArray) -> IntArray, widthCap: Int = 64, signatureBudget: Int = 256): Boolean Does this cell's recursion stop?

MorphoGraal.kt

Hosting MorphoHDL's own compiler: one GraalJS context, one lock, and a growth handle per circuit.

Nothing about the language is reimplemented on this side. MorphoRuntime evaluates morpho's parser, compiler and expansion as ES modules, at the upstream commit recorded in morpho/PROVENANCE.md, and MorphoSession holds one compiled design that is expanded a budget at a time. The rest of the package consumes what comes back out: MorphoNetlist is the snapshot, MorphoSim evaluates it, MorphoLayout3D places it, MorphoFLines.kt draws it.

Crossing the boundary is the expensive part, so both guest scripts exist to make each crossing carry more. morpho-field-driver.mjs flattens the compiler's graph into typed arrays; morpho-field-layout.mjs drives the Barnes–Hut kernel from JavaScript, where its buffers can be wrapped in views rather than read an element at a time. A guest failure arrives as MorphoException, carrying morpho's own source line whenever its parser supplied one.

src/jvmMain/kotlin/afield/lang/morpho/MorphoGraal.kt — read the source

MorphoRuntime

object MorphoRuntime

Hosts MorphoHDL inside Field by running morpho's own compiler on GraalJS.

Declaration Summary
val root: Path by lazy Where the vendored tree lives as real files.
val standardLibrary: String by lazy The MorphoHDL standard library — 8 LUTs and 26 cells covering adders, multipliers, shifters, muxes and the growth shapes.
fun inspect(source: String): String Parse source and report what it defines, without compiling anything.
fun cellNames(source: String): List<String> Names of the cells a source defines, in declaration order.
fun compile(source: String = standardLibrary, design: String, signature: IntArray, optimize: Boolean = true, maxFanout: Int = 0): MorphoSession Compile design at signature and return a handle that has not been expanded yet.

MorphoException

class MorphoException(message: String, val line: Int?, cause: Throwable? = null) : RuntimeException(message, cause)

A MorphoHDL parse, compile or expansion failure, with a source line where morpho supplied one.

Declaration Summary
val line: Int?

MorphoSession

class MorphoSession(val design: String, val signature: IntArray, internal val state: Value) : Closeable

One circuit being grown.

Declaration Summary
val design: String
val signature: IntArray
fun grow(budget: Int = Int.MAX_VALUE, method: String = "largest"): Int Expand up to budget cells and return how many actually expanded; 0 means fully grown.
fun growFully(method: String = "largest"): Int Expand to completion, returning the total number of expansions.
fun isFullyGrown(): Boolean
fun netlist(): MorphoNetlist Copy the current circuit out of the guest. Safe to call between grow steps.
fun timing(): MorphoTiming Signal arrival times from morpho's own updateTimes().
override fun close()

MorphoInterface.kt

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.

src/jvmMain/kotlin/afield/lang/morpho/MorphoInterface.kt — read the source

Top-level functions

Declaration Summary
fun installMorpho()
fun installMorphoPlugin()
fun morphoTopologyNeedsUpdating(values: Collection<Box>)

Top-level values

Declaration Summary
var Box.isMorphoRoot: Boolean?
var Box.morphoCircuit: MorphoCircuit? The circuit this box most recently built.

MorphoExecution

class MorphoExecution(val inside: Box) : BoxExecution

MorphoHDL as a first-class Field language.

Declaration Summary
override val languageName: String
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) What a morpho box is asking Field to build.
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit) Compile and grow the whole circuit, once.
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit) Grow the circuit a few cells per frame, relaxing the 3D layout between expansions.
override fun stop(inside: Box)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
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.

MorphoLayout3D.kt

Placement for a MorphoHDL circuit: every active cell gets a 3D position, from a force-directed layout running on morpho's own Barnes–Hut kernel.

One of the two things Field does with a MorphoNetlist snapshot, the other being MorphoSim; MorphoFLines.kt turns the positions that come out of here into FLines. Layout is stepped alongside growth rather than run to convergence at the end, and a cell born in a growth step is seeded at its parent's position, so a circuit that is still expanding is drawable on every frame.

The parameters are upstream's defaults, and the only behavioural difference from morpho's own demo is that Field leaves the z axis free.

src/jvmMain/kotlin/afield/lang/morpho/MorphoLayout3D.kt — read the source

MorphoLayout3D

class MorphoLayout3D(private val session: MorphoSession, seed: Int = 1337, maxPoints: Int = 65_536) : Closeable

Force-directed 3D layout for a growing MorphoHDL circuit, computed by morpho's own Barnes–Hut kernel (graphs_engine/main.wasm) on GraalWasm.

Declaration Summary
data class Params(val repulsionForce: Double = 0.05, val wireForce: Double = 1.0, val velocityDecay: Double = 0.05, val theta: Double = 0.5, val leafSize: Int = 64, val maxLevel: Int = 10, val linkDistance: Double = 2.0, val maxDist: Double = 80.0, val maxSpeed: Double = 1.0, val gravity: Double = 0.1) Barnes–Hut and integration parameters. Defaults are upstream's, from force_layout.js's constructor, so a Field layout and a morpho layout of the same circuit are comparable.
var particleCount: Int Number of particles, i.e. active cells, after re-mapping onto the current graph.
fun sync(): Int Re-map particles onto the circuit's currently active cells. Call after each growth step.
fun relax(steps: Int = 1, params: Params = Params()): Int Advance the simulation by steps Barnes–Hut iterations.
fun positions(): FloatArray Particle positions, stride 3 (x, y, z), in particle order.
fun cellIndices(): IntArray Cell index for each particle, joining positions back to a MorphoNetlist.
fun bounds(): FloatArray [minX, minY, minZ, maxX, maxY, maxZ].
fun flatness(): Double How far the layout actually used the third dimension, as z extent over the larger of the x and y extents. Zero would mean it had collapsed to a plane, as upstream's forces it to.
override fun close()

MorphoNetlist.kt

The compiled circuit as a Kotlin value: packed per-cell metadata, one array per relation, and morpho's own numbering carried across the boundary unchanged.

MorphoSession.netlist builds one of these out of the GraalJS guest in a handful of bulk copies, and everything Field does with a circuit reads it from here: MorphoSim evaluates it, MorphoLayout3D places it, MorphoFLines.kt draws it. It is a snapshot rather than a view, so growing the circuit produces a new one.

Cell indices are morpho's, so deactivated cells are still counted and isActive decides what is real. A LUT cell is broadcast across a bus, so the count of active cells, the count of LUTs and the count of gates are three different numbers.

src/jvmMain/kotlin/afield/lang/morpho/MorphoNetlist.kt — read the source

MorphoNetlist

class MorphoNetlist(val nCells: Int, val nNets: Int, val nPins: Int, val steps: Int, private val meta: IntArray, private val cellPinCount: IntArray, val pinNet: IntArray, val netDriver: IntArray, val inputCells: IntArray, val outputCells: IntArray, val names: List<String>)

A snapshot of a MorphoHDL circuit, as produced by morpho's own compiler and copied across the GraalJS boundary by morpho-field-driver.mjs.

Declaration Summary
val nCells: Int
val nNets: Int
val nPins: Int
val steps: Int Expansions performed to reach this snapshot.
val pinNet: IntArray Per input pin, the net it currently reads. Pins are contiguous per cell from pinStart.
val netDriver: IntArray Per net, the cell driving it, or -1 for constants and dangling nets.
val inputCells: IntArray Cell indices of the design's input ports, in declaration order.
val outputCells: IntArray Cell indices of the design's output ports, in declaration order.
val names: List<String>
fun isActive(cell: Int): Boolean
fun type(cell: Int): Int
fun lutValue(cell: Int): Int
fun lutArity(cell: Int): Int
fun parent(cell: Int): Int
fun netStart(cell: Int): Int
fun slots(cell: Int): Int Output wires driven by this cell, occupying nets netStart until netStart + slots.
fun pinStart(cell: Int): Int
fun pinCount(cell: Int): Int
fun name(cell: Int): String
fun isLut(cell: Int): Boolean
val activeCells: IntArray by lazy Every cell index that survived expansion and optimisation.
val activeCount: Int How many cells survived expansion and optimisation: the length of activeCells.
val lutCount: Int Active LUT cells. Not the gate count — see slots.
val gateCount: Int Total gates, counting each broadcast LUT slot separately.
fun lutInputNet(cell: Int, slot: Int, port: Int): Int The input port index of a LUT's pin. LUT input pins are stored slot-major (pinStart + slot * arity + port) so that splitting a broadcast cell is a contiguous copy.
val inputWidths: IntArray Widths of the design's input ports, in declaration order.
val outputWidths: IntArray Widths of the design's output ports, in declaration order.
fun edges(): IntArray Driver-to-consumer edges between active cells, one per connected input pin.
fun topologicalOrder(): IntArray Active cells in dependency order: every cell appears after the cells driving its inputs.
override fun toString(): String

MorphoProblems.kt

The search problems this layer was built to ask, and how much of the answer each one gives away.

Each one is a MorphoSpec (what the circuit must compute, at which widths) plus a MorphoProgramGenerator (what a candidate is allowed to be). They live here rather than inside a test, so a Field box, the measurement run and the regression tests all ask the same question, and a number quoted in the documentation is a number an assertion also checks.

They range from bitwiseXor, where one broadcast LUT is enough and a failure implicates the harness rather than the search, through fullAdderCell's eight-vector exhaustive truth table, to adderFromScratch, where the recursion and the base case it falls into are evolved together and nothing about addition is supplied but the oracle.

src/jvmMain/kotlin/afield/lang/morpho/MorphoProblems.kt — read the source

Top-level functions

Declaration Summary
fun morphoCellSource(name: String, source: String = MorphoRuntime.standardLibrary): String One cell's source, lifted out of a MorphoHDL module by name.

MorphoProblem

class MorphoProblem(val name: String, val spec: MorphoSpec, val generator: MorphoProgramGenerator, val about: String)

One search problem: what the circuit has to compute, and what a candidate is allowed to be.

Declaration Summary
val name: String
val spec: MorphoSpec
val generator: MorphoProgramGenerator
val about: String One sentence on what is being searched for and what is being handed over.
fun reference(design: String): MorphoFitness Score a design from morpho's own standard library against this problem's spec.
override fun toString(): String

MorphoProblems

object MorphoProblems

Declaration Summary
const val ROOT Every candidate cell is called this, so MorphoSpec.name and the rendered root agree.
const val BASE The base case a recursion falls into when MorphoProblems.ROOT is the recursion.
val ODD_FAMILY: List<Int> The default width family, and not a power-of-two one. This is a measured decision.
fun fullAdderCell(maxStatements: Int = 6): MorphoProblem One bit, no recursion. Find sum, c_out = a + b + c_in as straight-line logic over morpho's standard cells.
fun recursiveAdder(widths: List<Int> = listOf(2, 4, 8), maxStatements: Int = 10, designWidth: Int = 8): MorphoProblem The recursion. The 1-bit base case is handed over — it is morpho's own full_adder, lifted verbatim out of tiny_morpho.py — and the divide-and-conquer body is searched for.
fun guidedRecursiveAdder(widths: List<Int> = listOf(2, 4, 8), maxStatements: Int = 10, designWidth: Int = 8): MorphoProblem The conquer, with the divide given. Same as recursiveAdder, except that a0, a1 = SPLIT(a) and b0, b1 = SPLIT(b) are already in the body and cannot be removed.
fun adderFromScratch(widths: List<Int> = listOf(2, 4, 8), maxStatements: Int = 10, designWidth: Int = 8): MorphoProblem Both at once. The recursion and the base case it falls into, evolved together.
fun orReduce(widths: List<Int> = ODD_FAMILY, maxStatements: Int = 6, designWidth: Int = 8): MorphoProblem The recursion, on a target where a half-built one already pays. Reduce an N-bit bus to one bit: y = (x != 0).
fun andReduce(widths: List<Int> = ODD_FAMILY, maxStatements: Int = 6, designWidth: Int = 8): MorphoProblem orReduce's dual: y = (x == all ones). A second point on the same claim.
fun bitwiseXor(widths: List<Int> = listOf(2, 4, 8)): MorphoProblem The easy parametric one. Bitwise XOR of two buses — one statement is enough, and no recursion is required, because morpho broadcasts a LUT across a bus for free.

MorphoSearch.kt

Searching the space of MorphoHDL programs for one that computes something.

Three drivers, one evaluator, one generator. That is the arrangement the whole file exists for: MorphoSearch.random is a control arm drawing from exactly the distribution MorphoSearch.hillClimb and MorphoSearch.evolve mutate within, so a difference between their results is a difference in selection. Reporting an evolutionary result without the control next to it would say nothing at all — a generator good enough to make valid adders occasionally would look like a successful search.

Every candidate is scored by compiling it with upstream's compiler and running the netlist that comes out. See MorphoEvaluator.

src/jvmMain/kotlin/afield/lang/morpho/MorphoSearch.kt — read the source

Used in: Searching for MorphoHDL Circuits

MorphoIndividual

class MorphoIndividual(val genome: MorphoGenome, val fitness: MorphoFitness)

A program and what it scored.

Declaration Summary
val gates: Int
val depth: Int
override fun toString(): String

MorphoSearchRun

class MorphoSearchRun(val label: String, val spec: MorphoSpec, val evaluations: Int, val failures: Int, val millis: Double, val best: MorphoIndividual?, val trace: DoubleArray, val gateTrace: IntArray, val firstCorrectAt: Int, val front: List<MorphoIndividual>, val solutions: List<MorphoIndividual> = emptyList(), val survivors: List<MorphoIndividual> = emptyList(), val proposals: Int = evaluations)

What one search run did.

Declaration Summary
val label: String
val spec: MorphoSpec
val evaluations: Int
val failures: Int
val millis: Double
val best: MorphoIndividual?
val trace: DoubleArray
val gateTrace: IntArray Best-so-far gate count among correct candidates; -1 at evaluations where none had been found.
val firstCorrectAt: Int Evaluation index at which the first fully correct design appeared, or -1.
val front: List<MorphoIndividual> Non-dominated correct designs on (gates, depth).
val solutions: List<MorphoIndividual> Every distinct correct program found, capped. The population a generalisation test is run over.
val survivors: List<MorphoIndividual> The final population (or the climber's last point), for seeding the next stage of a curriculum.
val proposals: Int Programs proposed, including ones already evaluated. Well above evaluations means re-treading.
val failureRate: Double Fraction of candidates the compiler, the simulator or the cell budget rejected.
val solved: Boolean
fun summary(): String

MorphoSelection

enum class MorphoSelection

How MorphoSearch.evolve picks a parent.

Constants: TOURNAMENT, LEXICASE.

MorphoSearch

object MorphoSearch

Search drivers. All three take the same budget and return the same shape of result.

Declaration Summary
fun random(spec: MorphoSpec, generator: MorphoProgramGenerator, budget: Int, seed: Int = 1, optimize: Boolean = true, label: String = "random search"): MorphoSearchRun Draw budget independent programs and keep the best.
fun hillClimb(spec: MorphoSpec, generator: MorphoProgramGenerator, budget: Int, seed: Int = 1, patience: Int = 80, optimize: Boolean = true, seeds: List<MorphoGenome> = emptyList(), label: String = "hill climb"): MorphoSearchRun Accept any mutation that is not worse, restart when stuck.
fun evolve(spec: MorphoSpec, generator: MorphoProgramGenerator, budget: Int, seed: Int = 1, mu: Int = 12, lambda: Int = 24, tournament: Int = 2, selection: MorphoSelection = MorphoSelection.LEXICASE, crossoverRate: Double = 0.25, immigrantRate: Double = 0.12, optimize: Boolean = true, seeds: List<MorphoGenome> = emptyList(), label: String = "evolution"): MorphoSearchRun A (μ+λ) population with tournament selection and whole-cell crossover.
fun reference(spec: MorphoSpec, design: String, source: String = MorphoRuntime.standardLibrary, optimize: Boolean = true): MorphoFitness Score a hand-written design under the same spec, so a search result has something to be measured against.
fun staged(spec: MorphoSpec, generator: MorphoProgramGenerator, budgetPerStage: Int, seed: Int = 1, mu: Int = 12, lambda: Int = 24, optimize: Boolean = true): List<MorphoSearchRun> A width curriculum: search at two bits, then carry the survivors up to four, then eight.
fun generalization(genome: MorphoGenome, spec: MorphoSpec, widths: List<Int>, optimize: Boolean = true): MorphoFitness Re-score a program at widths it was never selected on.
const val SOLUTION_CAP How many distinct correct programs one run will remember.
const val PROPOSAL_LIMIT Proposals a driver may make per unit of budget before it is judged to be stuck.

MorphoSearchFLines.kt

Drawing what a search did, as vector FLines for afield.geometry.spatial.showPlaneGrid.

Two figures, and between them they are the whole argument:

  • morphoConvergenceFLines — best-so-far error against compiler invocations, one line per driver. This is where the control arm lives: the random-search line and the evolution line are drawn on the same axes at the same budget, so "the search found it" is a statement about the gap between two curves rather than about one number.
  • morphoParetoFLines — gates against depth for every correct design found, with morpho's own hand-written designs marked. A search result that lands outside the human front is a result; one that lands on it is a different and much smaller result; and the picture says which.

Everything here is geometry. Nothing rasterises, so a figure scales, exports to SVG, and can be redrawn every frame of a launched box while the search is still running.

src/jvmMain/kotlin/afield/lang/morpho/MorphoSearchFLines.kt — read the source

Used in: Searching for MorphoHDL Circuits

Top-level functions

Declaration Summary
fun morphoPlotWindow(): Rect The window every figure in this file is drawn into: a unit plot box with room for the frame.
fun morphoConvergenceFLines(runs: List<MorphoSearchRun>, logX: Boolean = true, yMax: Double = -1.0, colours: List<Vec4> = morphoRunColours): List<FLine> Best-so-far error against evaluations, one polyline per run.
fun morphoParetoFLines(points: List<MorphoCostPoint>, gateMax: Int = -1, depthMax: Int = -1): List<FLine> Gates against depth, with the non-dominated staircase drawn through the search's own points.
fun MorphoSearchRun.costPoints(): List<MorphoCostPoint> Every correct design a run found, as cost points.
fun morphoRunCaption(runs: List<MorphoSearchRun>): String A caption carrying the numbers the picture cannot: budget, control, and whether it was solved.

Top-level values

Declaration Summary
val morphoRunColours: List<Vec4> Default line colours, in the order runs are passed. Distinguishable at hairline width.

MorphoCostPoint

class MorphoCostPoint(val label: String, val gates: Int, val depth: Int, val reference: Boolean = false)

One point on a cost figure: a circuit, what it cost, and whether a person or a search made it.

Declaration Summary
val label: String
val gates: Int
val depth: Int
val reference: Boolean Drawn larger and off the run palette — the hand-written designs a result is measured against.

MorphoSim.kt

What a compiled MorphoHDL circuit computes, and how deep it is: a bit-parallel logic simulator, plus the per-cell arrival times morpho's own compiler already hands over.

MorphoSim evaluates a MorphoNetlist with 64 test vectors packed into the bits of a machine word, which makes exhausting a small adder's whole input space cheap enough for a unit test. Morpho's JavaScript has no logic simulator at all, so this side is Field's, written to the LUT convention of the numpy evaluator in upstream's tiny_morpho.py. MorphoTiming is the other way round: a thin reading of morpho's own updateTimes(), whose deepest cell is the circuit's critical path. MorphoEvaluator reads both, the simulator for correctness and the arrival times for depth.

src/jvmMain/kotlin/afield/lang/morpho/MorphoSim.kt — read the source

MorphoSim

object MorphoSim

Logic simulation for a compiled MorphoHDL circuit — does the adder actually add?

Declaration Summary
const val LANES Samples evaluated per pass.
fun simulatePlanes(net: MorphoNetlist, inputs: Array<LongArray>): Array<LongArray> Evaluate one pass of up to 64 samples held as bit planes.
fun evaluate(net: MorphoNetlist, samples: Array<LongArray>): Array<LongArray> Evaluate a batch of integer test vectors.
fun evaluateOne(net: MorphoNetlist, vararg ports: Long): LongArray Single-sample convenience wrapper.

MorphoTiming

class MorphoTiming(private val times: FloatArray, val nCells: Int)

Per-cell signal arrival, straight out of morpho's CompiledGraph.updateTimes().

Declaration Summary
fun first(cell: Int): Float
fun last(cell: Int): Float
fun depth(net: MorphoNetlist): Float Critical-path depth over the active cells of net.

MorphoSpec.kt

What a MorphoHDL design is supposed to do, and how much it is allowed to cost — the half of a search that morpho itself has no opinion about.

Everything in this file evaluates candidate designs by running them: MorphoRuntime compiles the candidate's source with upstream's parser and upstream's compiler, MorphoSim runs the resulting netlist, and MorphoTiming — morpho's own updateTimes() — supplies the depth. Nothing here models a circuit. A search whose fitness function was Field's idea of what a netlist would look like would be searching Field's imagination, and the point of the bridge is that it does not have to.

src/jvmMain/kotlin/afield/lang/morpho/MorphoSpec.kt — read the source

Used in: Searching for MorphoHDL Circuits

MorphoPortRole

enum class MorphoPortRole

The role a port plays in a width-parametric design.

Constants: BUS, BIT.

MorphoSpec

class MorphoSpec(val name: String, val inputRoles: List<MorphoPortRole>, val outputRoles: List<MorphoPortRole>, val widths: List<Int>, val exhaustiveLimit: Int = 1 shl 13, val sampleCount: Int = 384, val seed: Int = 20260801, val oracle: (LongArray, Int) -> LongArray)

A functional oracle plus the widths it must hold at.

Declaration Summary
val name: String
val inputRoles: List<MorphoPortRole>
val outputRoles: List<MorphoPortRole>
val widths: List<Int>
val exhaustiveLimit: Int
val sampleCount: Int
val seed: Int
val oracle: (LongArray, Int) -> LongArray
fun signature(width: Int): IntArray The signature create() wants: MorphoPortRole.BUS ports at width, MorphoPortRole.BIT at 1.
fun outputWidths(width: Int): IntArray The output widths a correct design must produce at width.
fun inputSpace(width: Int): Long How many distinct input vectors exist at width, saturating rather than overflowing.
fun isExhaustiveAt(width: Int): Boolean True when samples at width is the whole input space rather than a sample of it.
fun samples(width: Int): Array<LongArray> samples[port](workspace.md#stdlib-kt--i), cached: a search asks for the same vectors thousands of times.
fun expected(width: Int): Array<LongArray> expected[port](workspace.md#stdlib-kt--i), the oracle's answer for samples.
fun at(widths: List<Int>): MorphoSpec The same specification checked at a different width family.
fun outputBits(width: Int): Int Total oracle output bits per width — the denominator of the bit-error rate.
val caseCount: Int by lazy One case per (width, output port, bit position) — the granularity lexicase selection needs.
fun caseName(index: Int): String Human-readable name of case i, for reporting.

MorphoCost

data class MorphoCost(val cells: Int, val luts: Int, val gates: Int, val depth: Int, val maxFanout: Int)

What a compiled candidate costs, entirely in numbers the real netlist and morpho's own timing produced.

Declaration Summary
val cells: Int
val luts: Int
val gates: Int
val depth: Int
val maxFanout: Int
override fun toString(): String

MorphoFitness

class MorphoFitness(val error: Double, val perWidthError: DoubleArray, val cost: MorphoCost?, val failure: String?, val millis: Double = 0.0, private val caseErrors: DoubleArray = DoubleArray(0))

How a candidate scored: how wrong it is, what it costs, and — when it did not compile at all — why.

Declaration Summary
val error: Double
val perWidthError: DoubleArray Bit-error rate at each of MorphoSpec.widths, in order.
val cost: MorphoCost?
val failure: String? Non-null when the candidate did not compile, did not simulate, or was rejected as too big.
val millis: Double
val valid: Boolean
val cases: DoubleArray Error rate per (width, port, bit) case — see MorphoSpec.caseCount.
val worstError: Double The width that went worst — the number a claim of correctness has to answer to.
val correct: Boolean
override fun toString(): String

MorphoEvaluator

class MorphoEvaluator(val spec: MorphoSpec, val optimize: Boolean = true, val cellBudget: Int = 40_000, val timeoutMillis: Long = 5_000)

Compiles a candidate at every width in the spec and scores it.

Declaration Summary
val spec: MorphoSpec
val optimize: Boolean
val cellBudget: Int Candidates whose netlist exceeds this many active cells are rejected unsimulated.
val timeoutMillis: Long
var evaluations: Int
var failures: Int
var totalMillis: Double
fun exhaustiveAt(width: Int): Boolean True when the width's check was the whole input space rather than a sample of it.
fun evaluate(source: String, design: String = spec.name): MorphoFitness Compile source at every spec width, simulate it, and score it.

NanoAssembler.kt

Turns Nano assembly source text into a bytecode ByteArray: one pass over the lines emitting bytes, then a patch pass that fills in the labels the first pass left as placeholders.

NanoVM.kt has the machine and the opcode map; this is the text side of the same instruction set. The listing below is the whole language: one directive, no macros, and operands that are registers, integer literals or labels.

Syntax

; line comment (rest of line)
label:            ; defines a label at the current address

; Data directive – place raw bytes into the program stream
.data 0x1A 0x2B 42 ...

; Instructions
nop
hlt
ret
inc r0
dec r1
not r2
neg r3
clr r0
out r1           ; print reg value
rng r2           ; fill with random byte

mov r0, r1       ; r0 = r1
add r0, r1       ; r0 += r1
sub r0, r1
mul r0, r1
and r0, r1
or  r0, r1
xor r0, r1
cmp r0, r1       ; flags = r0 <=> r1  (no reg change)
ldr r0, r1       ; r0 = the RAM byte at address r1  (load indirect)
str r0, r1       ; the RAM byte at address r0 = r1  (store indirect)
div r0, r1       ; r0 = r0 / r1  (integer; r1==0 → r0=0xFF)

ldi r0, 42       ; r0 = 42   (immediate, 0–255)
ldi r0, 0xFF
lda r0, 5        ; r0 = RAM[5]  (absolute load)
sta r0, 5        ; RAM[5] = r0  (absolute store)
addi r0, -3      ; r0 += -3     (signed immediate, -128..127)
ldrr r0, -2      ; r0 = RAM[PC + (-2)]  (PC-relative load)
strr r0, 4       ; RAM[PC + 4] = r0     (PC-relative store)
shl r0, 2        ; r0 <<= 2
shr r0, 1        ; r0 >>= 1

jmp  target      ; unconditional (relative, or label)
jz   target
jnz  target
jgt  target
jlt  target
jge  target
jle  target
jc   target
jnc  target
call target      ; relative call
jmpa target      ; absolute jump (0–255)
calla target     ; absolute call

Labels can appear on their own line (start:) or before a mnemonic on the same line (start: ldi r0, 0). A label used by a relative branch resolves to the signed 8-bit offset from the byte following the branch (PC+2 for the two-byte forms like jmp), and an offset that will not fit in −128..+127 is an error on that line. jmpa and calla take the absolute address instead, so there the error is an address past 255.

Results and errors

Every bad register, number, mnemonic, duplicate label and out-of-range branch goes into a list that assembly keeps filling, so one run reports all of them with the line each came from rather than stopping at the first. assemble hands back AssemblyResult, which is either the bytes or that list. assembleWithSourceMap hands back the same bytes plus a program-counter-to-line array, which is what NanoInterface.kt and NanoVisualization.kt use to mark the source line a running program is on. The throwing wrapper a live-coding box calls is the top-level nanoAssemble in NanoInterface.kt.

src/jvmMain/kotlin/afield/lang/nano/NanoAssembler.kt — read the source

NanoAssembler

class NanoAssembler

The assembler. It keeps no state between calls, so one instance can assemble any number of programs.

Declaration Summary
data class AssemblyError(val line: Int, val message: String) One assembly failure: the 1-based source line it was found on, and what was wrong.
sealed class AssemblyResult The bytecode, or every error the source produced.
data class AssemblyDebugInfo(val bytes: ByteArray, val pcToLine: IntArray) Bytecode plus the source map: pcToLine is one entry per emitted byte, holding the 1-based line that byte came from, so a program counter indexes straight into it.
sealed class AssemblyDebugResult AssemblyDebugInfo, or every error the source produced.
fun assemble(source: String): AssemblyResult Assemble source into bytecode, or into the list of everything wrong with it.
fun assembleWithSourceMap(source: String): AssemblyDebugResult Assemble source and keep the byte-to-line map, for a caller that has to point at the line a program counter is sitting on.
fun parseNumber(s: String): Int? Parse an integer literal: decimal, 0x hex, or 0b binary. Returns null on failure.
fun extractLabels(source: String): List<String> Returns all label names defined in source. Useful for completion.

NanoComputeShader.kt

The host side of nano_compute.glsl, the GLSL compute port of the Nano VM: the buffer layout a caller has to honour, the mapping struct, and the packing helpers for getting bytes in and out.

The shader runs one independent Nano VM instance per GPU invocation, decoding the same opcode map NanoVM.kt documents. Three of its four storage buffers hold bytes and are cut into non-overlapping per-instance slices; the fourth, at binding 0, holds one NanoMapping per dispatch id and says where that instance's three slices begin. Nothing in the tree dispatches it. SOURCE reads the GLSL off the classpath and the helpers below build the buffers, so what is missing is the Vulkan plumbing: pipeline, descriptor sets, submit and readback.

Buffer bindings

binding layout access description
0 NanoMapping[] r-only dispatch-id → slice offsets
1 uint[] (bytes) r/w RAM: persistent working memory
2 uint[] (bytes) r-only code: assembled Nano programs
3 uint[] (bytes) r/w OUT stream: results of OUT rX

Byte-packing

All three byte buffers pack 4 bytes per uint: the byte at logical offset B lives in word B >> 2 at bit-shift (B & 3) << 3. A host has to honour that when uploading code and RAM and when reading OUT results back, which is what packBytes, packPrograms and unpackBytes are for.

Push constants (20 bytes, std430)

field type description
numInstances uint32 total instances (over-dispatch guard)
defaultMaxSteps uint32 step limit when mapping.maxSteps == 0
ramSize uint32 RAM bytes per instance; must be power-of-2
codeSize uint32 code bytes per instance (≤ 256)

Mapping buffer layout (NanoMapping struct, 20 bytes each)

field type description
ramBase uint32 byte offset into RAM buffer for this instance
codeBase uint32 byte offset into code buffer for this instance
outputBase uint32 byte offset into OUT buffer for this instance
maxSteps uint32 per-instance step cap (0 → use push-constant)
outputCapacity uint32 maximum OUT bytes; overflow halts this VM

Minimal dispatch example (pseudocode)

val N         = 1024               // number of Nano instances
val ramSize   = 32                 // bytes per VM
val codeSize  = 64                 // bytes per program
val outSize   = 64                 // max OUT bytes per VM

val mappings  = NanoComputeShader.buildMappings(N, ramSize, codeSize, outSize)
// upload mappings, ramBytes (N*ramSize), codeBytes (N*codeSize), outBytes (N*outSize)
// bind as buffers 0..3
// dispatch ceil(N / 64) groups of 64

src/jvmMain/kotlin/afield/lang/nano/NanoComputeShader.kt — read the source

NanoComputeShader

object NanoComputeShader

The GLSL source, the mapping-buffer builder and the byte-packing helpers, and nothing that talks to a device.

Declaration Summary
val SOURCE: String by lazy Source of the nano_compute.glsl compute shader.
data class NanoMapping(val ramBase: Int, val codeBase: Int, val outputBase: Int, val maxSteps: Int = 0, val outputCapacity: Int = 0) Kotlin mirror of the NanoMapping GPU struct (20 bytes, std430).
fun buildMappings(numInstances: Int, ramSize: Int = NanoVM.RAM_SIZE, codeSize: Int = 64, outSize: Int = 64, maxSteps: Int = 0): ByteBuffer Build a flat ByteBuffer containing one NanoMapping per instance, with each instance allocated contiguous, non-overlapping slices.
fun packBytes(bytes: ByteArray): ByteBuffer Pack bytes into a uint[]-compatible ByteBuffer using the shader's 4-bytes-per-uint little-endian layout.
fun unpackBytes(buf: ByteBuffer, baseOffset: Int, length: Int): ByteArray Unpack length bytes from a uint[]-packed ByteBuffer starting at byte offset baseOffset.
fun packPrograms(programs: List<ByteArray>, codeSize: Int = 64): ByteBuffer Pack programs (each up to codeSize bytes) into a single flat ByteBuffer ready for binding as the code SSBO.
fun assemble(source: String): ByteArray Assemble source with NanoAssembler and return the raw bytes, or throw NanoAssemblyException on error. Useful for building code buffers inline.

NanoDisassembler.kt

Reads a Nano bytecode ByteArray back as assembly text, one DisasmLine per instruction.

It walks the program twice. The first walk collects every branch target, so that the second can put a name on each one and a branch reads as a label rather than as a raw offset. The name is the target address in hex, so L00 and L0A, and it arrives as its own DisasmLine with an empty bytes list, which is how NanoVisualization.kt tells a label apart from an instruction when it is deciding which line to highlight.

DisasmLine.toString is the printable form:

addr:  hex  hex   mnemonic  operands    ; optional annotation

The annotation is the part the bytes do not say: the signed offset behind a relative branch, and the absolute address behind jmpa or calla. A byte that decodes to no instruction prints as ??? with its hex value, which is the five of the 256 that NanoVM.kt lists as executing nothing: 0x03, and 0xFC–0xFF past DIV's last register pair.

src/jvmMain/kotlin/afield/lang/nano/NanoDisassembler.kt — read the source

NanoDisassembler

class NanoDisassembler

The disassembler. Like the assembler it keeps no state between calls.

Declaration Summary
data class DisasmLine(val addr: Int, val bytes: List<Int>, val text: String) One output line: its addr, the program bytes it decoded from, and the text to show.
fun disassemble(program: ByteArray, startAddr: Int = 0): List<DisasmLine> Disassemble program starting at offset startAddr. Returns a list of DisasmLines.
fun disassembleToString(program: ByteArray, startAddr: Int = 0): String Disassemble and join into a single printable string.

NanoInterface.kt

Field4 language adapter for the Nano 8-bit assembly language.

Nano is a compact virtual machine: 4 registers (r0–r3), 32 bytes of RAM with address wrap, a small call stack, and an instruction set sized for emergence and artificial-life grid experiments. NanoExecution is the BoxExecution implementation; setting a parent box's isNanoRoot to true turns its children into Nano assembly boxes.

installNano registers the runtime with Field's language dispatch; installNanoPlugin starts the topology watcher that activates it when the box tree changes. nanoAssemble and Box.nanoDisassemble are available to neighbouring Kotlin boxes that need to inspect or construct the VM's byte representation directly.

src/jvmMain/kotlin/afield/lang/nano/NanoInterface.kt — read the source

Used in: What a document is on disk

Top-level functions

Declaration Summary
fun installNano()
fun installNanoPlugin()
fun nanoTopologyNeedsUpdating(values: Collection<Box>)
fun Box.nanoDisassemble(bytes: ByteArray) Disassemble Nano bytecode bytes and display the result in this box's output area.
fun nanoAssemble(source: String): ByteArray Assemble Nano source and return the bytecode, or throw NanoAssemblyException on error. Convenient from Kotlin live-coding boxes:

Top-level values

Declaration Summary
var Box.isNanoRoot: Boolean?
var Box.nanoLaunchDelayMs: Number?
var Box.nanoMemoryInit: Any?
var Box.nanoMemoryBefore: List<Int>?
var Box.nanoMemoryAfter: List<Int>?

NanoExecution

class NanoExecution(val inside: Box) : BoxExecution

Field4 BoxExecution implementation for the Nano micro-computer language.

Declaration Summary
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit)
override fun stop(inside: Box)
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override val languageName: String

NanoAssemblyException

class NanoAssemblyException(message: String) : RuntimeException(message)

NanoVM.kt

Nano is a minimal 8-bit virtual machine designed for emergence and artificial-life simulations: four registers, 32 bytes of RAM, and all but five of the 256 possible opcode bytes decoding to an instruction.

The whole machine is:

  • r0–r3, 8-bit unsigned, so 0–255
  • 32 bytes of RAM, addressed modulo 32, so no address is out of range
  • flags Z (zero), C (carry), S (sign) and O (overflow)
  • a call stack eight deep, for CALL and RET
  • unsigned-byte arithmetic throughout, wrapping mod 256

Instructions are fetched from the program byte array passed to NanoVM.run, and ram is separate data. NanoVM.load also mirrors the program's first 32 bytes into RAM, which is what lets a program read its own text. NanoInterface's launch mode calls NanoVM.resetRegisters instead, leaving RAM alone so it carries state from one per-tick run to the next.

Opcode map

Opcode is the constant table, and Opcode.twoReg and Opcode.oneReg are the encoders.

form encoding instructions opcodes
no operand one byte NOP, HLT, RET 0x00–0x02
one register base + reg, 4 per op INC, DEC, NOT, NEG, CLR, OUT, RNG 0x04–0x1F
two registers base + (dst shl 2) + src, 16 per op MOV, ADD, SUB, MUL, AND, OR, XOR, CMP, LDR, STR 0x20–0xBF
register and immediate or address two bytes, 4 per op LDI, LDA, STA, ADDI, LDRR, STRR, SHL, SHR 0xC0–0xDF
relative branch two bytes, signed offset JMP, JZ, JNZ, JGT, JLT, JGE, JLE, JC, JNC, CALL 0xE0–0xE9
absolute branch two bytes, absolute address JMPA, CALLA 0xEA–0xEB
two registers base + (dst shl 2) + src DIV 0xEC–0xFB

Five bytes decode to nothing — 0x03, and 0xFC–0xFF past DIV's last (dst, src) pair. Each reports an unknown opcode and halts. That path matters here more than it would in a VM with a fixed program: artificial-life work mutates programs, and NanoAssembler's raw-byte directive puts an arbitrary byte in front of the decoder too, so every one of the 256 has to either execute or report. The GPU port in nano_compute.glsl decodes the same map.

A program

; comment
start:
    ldi r0, 10     ; r0 = 10
    ldi r1, 0      ; r1 = 0
loop:
    add r1, r0     ; r1 += r0
    dec r0         ; r0--  (sets Z when r0 reaches 0)
    jnz loop       ; jump back if not zero
    out r1         ; print r1
    hlt

NanoAssembler.kt has the rest of the syntax, including the directive that places raw bytes in the program stream and the fact that a label resolves to an offset for a relative branch and to an address for JMPA and CALLA.

src/jvmMain/kotlin/afield/lang/nano/NanoVM.kt — read the source

NanoVM

class NanoVM(val maxSteps: Int = 100_000, var rng: () -> Int = { Random.nextInt(256) })

The machine itself: the register file, RAM, the flags and the fetch-decode-execute step.

Declaration Summary
val maxSteps: Int
var rng: () -> Int
val reg
val ram
var pc
var flagZ
var flagC
var flagS
var flagO
var halted
var onOutput: (Int) -> Unit
var onError: (String, Int) -> Unit
data class StepResult(val executedPc: Int, val currentPc: Int, val halted: Boolean, val executed: Boolean)
fun reset()
fun resetRegisters() Reset only registers and execution state, preserving RAM contents. Used in NanoInterface launch mode so RAM acts as persistent working memory that survives across per-tick executions (important for ALife state machines).
fun load(program: ByteArray) Load a program into a fresh machine (resets full state including RAM).
fun run(program: ByteArray): Int Run up to maxSteps instructions from current pc. Returns the number of steps executed.
fun stepOnce(program: ByteArray): StepResult Execute exactly one instruction at the current pc.
fun stateString(): String

Opcode

object Opcode

Opcode constants for the Nano VM, with the two encoders that build a byte out of a base and a register index. These are the ranges NanoVM.stepOnce decodes.

Declaration Summary
const val NOP
const val HLT
const val RET
const val INC_BASE
const val DEC_BASE
const val NOT_BASE
const val NEG_BASE
const val CLR_BASE
const val OUT_BASE
const val RNG_BASE
const val MOV_BASE
const val ADD_BASE
const val SUB_BASE
const val MUL_BASE
const val AND_BASE
const val OR_BASE
const val XOR_BASE
const val CMP_BASE
const val LDR_BASE
const val STR_BASE
const val LDI_BASE
const val LDA_BASE
const val STA_BASE
const val ADDI_BASE
const val LDRR_BASE
const val STRR_BASE
const val SHL_BASE
const val SHR_BASE
const val JMP
const val JZ
const val JNZ
const val JGT
const val JLT
const val JGE
const val JLE
const val JC
const val JNC
const val CALL
const val JMPA
const val CALLA
const val DIV_BASE
fun twoReg(base: Int, dst: Int, src: Int) Encode a two-register opcode: op_base + (dst << 2) + src
fun oneReg(base: Int, reg: Int) Encode a single-register opcode: op_base + regIndex

NanoVisualization.kt

Runs a Nano program and hands back its final machine state, either as data or as a self-contained HTML panel for a box's output area.

Nothing in the repository calls these; they exist for a Kotlin box sitting next to a Nano box, so a document can show a register file and 32 bytes of RAM without reaching into NanoVM itself. nanoRunReportHtml is the one-liner and nanoRunReport is the same run with the state left as a NanoRunReport to read or render differently.

The two kinds of failure a Nano program can have come back by different routes. Source that will not assemble throws NanoAssemblyException from either entry point, because there is no machine state to report. A program that assembles and then faults at run time fills runtimeError and returns everything else, because the registers and RAM at the point of the fault are what a reader wants; nanoRunReportHtml turns that into a thrown IllegalStateException by default, and throwOnRuntimeError = false gets the panel with the error drawn into it instead. A program that never halts is neither: maxSteps bounds the run and halted comes back false.

RNG draws from Random unless rngSeed is given, so a program using it is reproducible only when seeded.

The rendered panel distinguishes three things about a RAM cell, and only two of them survive this route. A cell whose value differs from the program byte loaded there is drawn as mutated, and the cell under the program counter is drawn as current. changedRegisters and changedRam draw what the most recent step touched, and only a single-step caller fills them, so a panel from nanoRunReport shows accumulated mutation and never per-step change.

src/jvmMain/kotlin/afield/lang/nano/NanoVisualization.kt — read the source

Top-level functions

Declaration Summary
fun nanoRunReport(source: String, maxSteps: Int = 100_000, rngSeed: Int? = null): NanoRunReport Assemble and execute Nano source, returning a structured snapshot of the final machine state.
fun nanoRunReportHtml(source: String, maxSteps: Int = 100_000, rngSeed: Int? = null, title: String? = null, includeSource: Boolean = true, includeDisassembly: Boolean = true, includeProgramBytes: Boolean = true, throwOnRuntimeError: Boolean = true): String Assemble, execute, and render Nano source as a compact HTML report.
fun NanoRunReport.toHtml(title: String? = null, includeSource: Boolean = true, includeDisassembly: Boolean = true, includeProgramBytes: Boolean = true): String Renders a report as one self-contained HTML block, ready to hand to a box's output area.

NanoRunReport

data class NanoRunReport(val source: String?, val program: ByteArray, val steps: Int, val outputs: List<Int>, val registers: List<Int>, val ram: List<Int>, val pc: Int, val focusPc: Int = pc, val focusLine: Int? = null, val halted: Boolean, val flagZ: Boolean, val flagC: Boolean, val flagS: Boolean, val flagO: Boolean, val changedRegisters: Set<Int> = emptySet(), val changedRam: Set<Int> = emptySet(), val runtimeError: String? = null)

Snapshot of a Nano execution, suitable for HTML rendering in Field output areas.

Declaration Summary
val source: String? The assembly this ran, kept so the panel can show it with a line highlighted. Null when unknown.
val program: ByteArray The assembled bytes. The first 32 of these are also RAM's initial contents.
val steps: Int Instructions executed. Reaching the step bound is how a program that never stops shows up here.
val outputs: List<Int> Every byte OUT emitted, in order, masked to 0–255.
val registers: List<Int> The four registers r0 to r3, each 0–255.
val ram: List<Int> All 32 bytes of RAM. Nano's addresses wrap, so there is no out-of-range cell.
val pc: Int The program counter where the machine stopped.
val focusPc: Int The address the panel highlights, which defaults to pc but need not be it for a single step.
val focusLine: Int? The source line the panel highlights, resolved through the assembler's map, or null.
val halted: Boolean Whether the machine stopped on its own, by HLT or by a fault.
val flagZ: Boolean Zero flag as the last arithmetic left it.
val flagC: Boolean Carry flag as the last arithmetic left it.
val flagS: Boolean Sign flag as the last arithmetic left it.
val flagO: Boolean Overflow flag as the last arithmetic left it.
val changedRegisters: Set<Int> Registers the most recent step wrote, for the panel to highlight.
val changedRam: Set<Int> RAM addresses the most recent step wrote. Empty for a whole-program run, like changedRegisters.
val runtimeError: String? The VM fault that stopped the run, already formatted with its address, or null.

NumpyShared.kt

Shared memory between Python boxes and Kotlin boxes: images and line sets that numpy writes in place and Kotlin reads without copying through a serializer.

NumpyShared hands out named SharedImage and SharedLines buffers; the fieldcv package in python/ is the Python side. NumpySharedTest covers the publish/consume handoff.

src/jvmMain/kotlin/afield/lang/python/NumpyShared.kt — read the source

NumpyShared

object NumpyShared

Named buffers that a Python box writes with numpy and a Kotlin box reads, without serializing.

Declaration Summary
fun image(name: String, height: Int = 0, width: Int = 0): SharedImage The shared image called name, creating it at height x width if it does not exist or has a different size. A size of 0 attaches to whatever exists and fails if nothing does.
fun lines(name: String): SharedLines The shared polylines called name, created empty on first use.
fun draw(box: afield.document.graph.Box, lines: SharedLines, channel: String = "lines", width: Double = 480.0, height: Double = 270.0) Draw lines into box's output on channel as SVG, scaled from normalized coordinates to width x height pixels. This is a preview in the box itself; the renderer path is SharedLines.lines.
fun names(): List<String> Every shared buffer by name, for the status panel and for debugging.

SharedImage

class SharedImage internal constructor(val name: String, val height: Int, val width: Int)

A height x width BGRA8 image: OpenCV's channel order with an alpha byte, so a BGR frame becomes one cvtColor into array and the GPU reads it as B8G8R8A8_SRGB with no swizzle.

Declaration Summary
val bytes: Int
val front: ByteBuffer The copy a reader sees: only changes inside consume.
var version: Long
fun array(): DirectNDArray<ByteBuffer> The back copy as a writable (height, width, 4) uint8 numpy array (JEP converts it on return).
fun publish() Make what has been written to array visible to the next consume. Safe from any thread.
fun consume(): Boolean Bring front up to date; true if it changed since the last call. Call it on the reading thread.
fun texture(renderer: Renderer = fromContext()): StreamingTexture A texture showing this image, uploaded on frames where something new was published.
override fun toString()

SharedLines

class SharedLines internal constructor(val name: String)

Polylines in normalized image coordinates — tracks, contours, skeleton edges — written by Python as one flat (points, 2) float32 array plus a count per line.

Declaration Summary
var version: Long
val pointCapacity: Int
val lineCapacity: Int
fun reserve(points: Int, lines: Int) Grow the back arrays to hold at least points points in lines lines. Re-fetch the arrays after.
fun points(): DirectNDArray<FloatBuffer> The back point array, (pointCapacity, 2) float32.
fun counts(): DirectNDArray<IntBuffer> The back count array, (lineCapacity,) int32.
fun publish(pointCount: Int, lineCount: Int) Publish the first lineCount lines, pointCount points in all.
fun lines(width: Double = 1.0, height: Double = 1.0): List<FLine> The newest polylines as FLines, scaled from normalized coordinates by width and height. The list is shared until something new is published, so don't mutate it.
override fun toString()

PythonErrors.kt

Error locations for Python boxes: the cell line that raised, read from the exception's traceback.

PythonErrors holds the Python helper spliced into the interpreter and the Kotlin arithmetic that maps its answer onto a box line. PythonErrorsTest runs the helper under a plain python3.

src/jvmMain/kotlin/afield/lang/python/PythonErrors.kt — read the source

PythonErrors

object PythonErrors

Where a Python box's error happened, found from the exception itself rather than from JEP's message.

Declaration Summary
fun boxLine(pythonLine: Int, fragmentStart: Int, droppedLeadingBlank: Int = 0): Int The fragment line pythonLine as a box line, or -1 (the fragment's end) when unknown.
fun describe(type: String, message: String): String The text shown beside the marker: NameError: name 'q' is not defined.
val SOURCE: String

PythonInterface.kt

Python as a Field main language: a box whose source is Python, executed in this process by CPython through JEP.

There is no subprocess and no socket, so a Python box and a Kotlin box exchange objects rather than text; and there is exactly one interpreter and one namespace per Field process, so two Python boxes share globals whether or not you wanted them to. That is a deliberate trade — it is what makes a cell in one box see a DataFrame another box built — and it is the first thing to know before debugging "why does this variable already exist".

Read it in four layers:

  • Configuration. JEP needs a real CPython to bind against, chosen once and persisted: PythonSettingsRequest, PythonSettingsStatus and PythonInstallResult are that negotiation, and restartRequired means it: a new interpreter binding cannot be swapped into a live JVM. makeInterpreter runs early and returns silently when nothing is configured, which is why a checkout with no Python simply has no Python boxes rather than a broken startup.
  • PythonExecution, the BoxExecution. Beyond the usual output proxying it carries the cooperative event loop: an asyncio loop implemented in Python that yields to Field's runloop instead of blocking it, so await inside a box does not stop the editor.
  • Output. Everything a box prints funnels through one formatter, and three cases are special: a \r becomes a Field progress bar (which is how tqdm renders as a real one), an object exposing HTML/Image/Markdown/Math/JSON renders as itself, and a library that tries to open a browser page — plotly's fig.show(), bokeh, folium — is intercepted and the HTML delivered into the box as an iframe srcdoc. Nothing here opens a browser.
  • Registration. installPython and pythonTopologyNeedsUpdating are the same two-call shape every main language uses; see dev_docs/small_language_anatomy.md.

A large part of this file is Python source held in Kotlin string literals — the event loop, the display types, the output shims. It is code that has to run inside the interpreter, so it lives here rather than in a .py on disk, where a fresh checkout could be missing it.

Python in Field is the prose.

src/jvmMain/kotlin/afield/lang/python/PythonInterface.kt — read the source

Top-level functions

Declaration Summary
fun makeInterpreter()
fun installPython()
fun install()
fun pythonTopologyNeedsUpdating(values: Collection<Box>)

Top-level values

Declaration Summary
var Box.isPythonRoot: Boolean?
var Box._pythonLocals: LinkedHashMap<*,*>
var interpretersForBoxes
var globalInterpreter: SharedInterpreter?
var resolvedVenvSitePackagesPath: String? Venv site-packages path resolved during init, used to fix sys.path before JEP's import

PythonSettingsRequest

data class PythonSettingsRequest(val kind: String = "venv", val path: String = "")

Declaration Summary
val kind: String
val path: String

PythonSettingsStatus

data class PythonSettingsStatus(val configured: Boolean, val source: String, val kind: String, val path: String, val pythonExecutable: String? = null, val pythonHome: String? = null, val jepLibrary: String? = null, val sitePackages: String? = null, val configPath: String, val message: String, val restartRequired: Boolean = false)

Declaration Summary
val configured: Boolean
val source: String
val kind: String
val path: String
val pythonExecutable: String?
val pythonHome: String?
val jepLibrary: String?
val sitePackages: String?
val configPath: String
val message: String
val restartRequired: Boolean

PythonInstallResult

data class PythonInstallResult(val success: Boolean, val message: String, val log: String, val status: PythonSettingsStatus? = null)

Declaration Summary
val success: Boolean
val message: String
val log: String
val status: PythonSettingsStatus? The status after a successful install (settings are persisted so a restart binds JEP).

PythonUnavailableExecution

class PythonUnavailableExecution : BoxExecution

The Python runtime when no interpreter with JEP was found at startup: every run reports one error naming the problem and the fix, and the box's verdict fails.

Declaration Summary
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override fun stop(inside: Box)
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit)
override val languageName: String

PythonExecution

class PythonExecution : BoxExecution

Declaration Summary
fun interpreter(b: Box? = null): SharedInterpreter
val syntaxError
val otherError
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit)
override fun stop(inside: Box)
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override val languageName: String

PythonInterface

class PythonInterface

Declaration Summary
fun helloWorld()

PythonLineNotes.kt

Line notes for Python boxes: a rewriter that records what each top-level statement of a cell evaluated to, for display beside the code.

PythonLineNotes carries the rewriter as Python source and the environment switch that disables it. PythonLineNotesTest runs that source under a plain python3.

src/jvmMain/kotlin/afield/lang/python/PythonLineNotes.kt — read the source

PythonLineNotes

object PythonLineNotes

The Python half of a Python box's line notes (afield.ui.output.LastRunNotes): the rewriter that records, as a cell runs, what each top-level statement came to.

Declaration Summary
val enabled: Boolean Whether Python boxes record line notes; see the class KDoc for the switch.
val SOURCE: String The rewriter, as Python source. See the class KDoc.

RectSL.kt

RectSL — Field's rectangle scene language, and the largest single file in the tree.

A RectSL program is a finite animated scene made of rectangles, written as a small stack language inside a Kotlin triple-quoted string whose first line is #rectsl. It is not a simulation and not a renderer: the language decides where rectangles are over a fixed duration and what material metadata each one carries, and StackSL (afield.lang.stacksl) decides how a material shades. That division is the reason both are small.

The pipeline, in reading order

Four types, and every entry point in this file is a step between two of them:

String  --tokenize-->  List<Token>  --eval-->  SceneProgram  --sample(t)-->  SampledScene  --lower-->  RectMesh
  • RectLangTokenizer is the whole of the syntax. There is no parser: the language is postfix, so tokenizing is parsing.
  • RectLangVm is the evaluator, and where the vocabulary lives. Words spelled ->something are mutators that write through to the active rect, group or scene; everything else transforms stack values. The arrow is part of the word.
  • SceneProgram is the compiled result and the value worth understanding first. It is time-independent — a wall, a duration, an fps and a list of nodes whose properties are Tracks rather than numbers — which is what makes a scene something you can ask questions of rather than only play. sample(t) walks it and returns a SampledScene, a flat z-ordered list of SampledRects with no tracks left in it.
  • lowerSampledScene turns that into a RectMesh: four RectVertexes and six indices per rect, with colour, material index and per-object shader data interleaved, ready for a Vulkan buffer.

RectAnimationOracle is the one object that names all five steps, and compileRectSL is the same compile with host-built curves bound in by name — the afield.math.functions.Functions half of the bridge, so a program can lay out an array from a curve the host fitted or evolved.

Why a property is a track and not a number

Half this file is Track and its implementations, because a property being a function of time rather than a number is what the rest of the package is built on. A track is symbolic and composable — TcbTrack for Kochanek–Bartels keys, EasedScalarTrack, TimeWarpTrack, GateTrack for life windows, HeadingTrack for face-travel, plus unary/binary/ternary combinators — so a scene can be differentiated, fitted (RectSLFit.kt), measured (RectSLCoverage.kt computes exact pixel coverage in closed form) and translated to GLSL (RectSLGlsl.kt) rather than only evaluated frame by frame.

Where the rest of the package sits

RectSLPattern.kt / RectSLPatternOps.kt make a Tidal pattern the scene's structure — a pattern is a track and an event is a live window. RectSLSoftRaster.kt is the differentiable rasterizer, RectSLFn.kt lowers scenes into the Functions algebra, RectSLSdfLook.kt and RectSLGlsl.kt are the two GPU paths, RectSLSimplify.kt shrinks a program, and RectSLEditorExtension.kt is the three lines that register the browser-side editor mode.

RectSL cheatsheet is the word-by-word reference with worked pictures; RectSL examples is the gallery.

src/jvmMain/kotlin/afield/lang/rectsl/RectSL.kt — read the source

Used in: RectSL Inverse Animation, RectSL examples, RectSL cheatsheet

Top-level functions

Declaration Summary
fun Color4.toVec4f(): Vec4f Bridge a Color4 to RectSL's local Vec4f. Color4 and Colormaps live in package afield.graphics.color, so general colormap and visualization code does not depend on this microlanguage; this extension keeps the RGBA→Vec4f conversion where its only caller, the renderer path, lives.
fun constantTrack(x: Double): Track<Double>
fun constantPaintTrack(x: PaintSpec): Track<PaintSpec>
fun compileRectSL(source: String, curves: Map<String, Functions.Fn> = emptyMap(), engine: KotlinScriptEngine? = null, precompiledEscapes: Map<String, (RectEscapeContext) -> Any?> = emptyMap()): CompileResult Compile a RectSL program with host-supplied distribution curves bound by name. Each entry becomes a bare word that pushes its Functions.Fn (like a named track), so a program can sample a curve the host built — including one that fitBest/evolve optimised toward a target. This is the Functions→RectSL half of the bridge: the host searches curve-space, RectSL lays out the array from the winner.
suspend fun prewarmRectSLEscapes(source: String, engine: KotlinScriptEngine): Map<String, (RectEscapeContext) -> Any?> Compile every {{ … }} escape in source to its lambda ahead of time, using the engine's suspend API so the compile runs off the caller's thread (on the compilation dispatcher) rather than blocking it. Call this before a graphics render loop — whose runloop is non-reentrant, so a lazy compileSnippetAndWait on first use during the render would deadlock — then hand the result to compileRectSL as precompiledEscapes, so the render-time compile is pure (only cached lambda invokes).
fun evalRectSLStack(source: String): List<Value> Evaluate a RectSL program and return its residual stack — for tooling and tests that need the values a program leaves behind (e.g. a VFn distribution curve built through the Functions bridge, to inspect or evolve its graph).
fun rectCornersWorld(r: SampledRect): Array<Vec2d>
fun lowerSampledScene(scene: SampledScene): RectMesh
fun SceneProgram.lowerFrameToMesh(time: Double): RectMesh
fun SceneProgram.writeFrameToMesh(time: Double, mesh: Mesh, attributes: RectMeshAttributeNames = RectMeshAttributeNames()): RectMesh
fun SampledScene.writeToMesh(mesh: Mesh, attributes: RectMeshAttributeNames = RectMeshAttributeNames()): RectMesh
fun RectMesh.writeTo(mesh: Mesh, attributes: RectMeshAttributeNames = RectMeshAttributeNames()): Mesh
fun sampledFramePacket(program: SceneProgram, time: Double): FramePacket
fun colliderFor(rect: SampledRect): RectCollider
fun detectContacts(rects: List<SampledRect>): List<ContactPair>
fun analyzeAnimation(program: SceneProgram, settings: AnalysisSettings = AnalysisSettings()): AnimationReport
fun compileStackSLMaterials(program: SceneProgram, compiler: StackSLRectMaterialCompiler = StackSLRectMaterialCompiler()): List<StackSLRectMaterialCompileResult>
fun rectMaterialShaderCode(program: SceneProgram, compiler: StackSLRectMaterialCompiler = StackSLRectMaterialCompiler()): RectMaterialShaderCode
fun rectMaterialShaderInsertion(compiledModules: List<StackSLRectMaterialCompileResult> = emptyList()): String
fun rectAnimationFragmentShaderWrapper(materialInsertion: String): String
fun rectAnimationFragmentShader(compiledModules: List<StackSLRectMaterialCompileResult> = emptyList()): String
suspend fun renderRectSLJpgSequence(source: String, outputDirectory: String, pixelsPerMeter: Double, fps: Double, renderer: Renderer = fromContext(), configure: RectSLRenderSettings.() -> Unit = {}): RectSLRenderResult+1 overload · also Path
suspend fun renderRectSLJpgSequence(source: String, outputDirectory: String, renderer: Renderer = fromContext(), configure: RectSLRenderSettings.() -> Unit): RectSLRenderResult+1 overload · also Path
fun renderRectSLJpgSequenceBlocking(source: String, outputDirectory: Path, pixelsPerMeter: Double, fps: Double, renderer: Renderer = fromContext(), configure: RectSLRenderSettings.() -> Unit = {}): RectSLRenderResult
fun renderRectSLJpgSequenceBlocking(source: String, outputDirectory: Path, renderer: Renderer = fromContext(), configure: RectSLRenderSettings.() -> Unit): RectSLRenderResult
fun WallSpec.worldToClip(): Matrix4f Orthographic world-to-clip projection respecting this wall's dimensions and origin.

Top-level values

Declaration Summary
const val RECT_ANIMATION_VERTEX_SHADER: String
const val RECT_MATERIAL_SHADER_INSERTION_PREAMBLE: String
const val RECT_ANIMATION_FRAGMENT_WRAPPER_PREAMBLE: String

Vec2d

data class Vec2d(val x: Double, val y: Double)

Declaration Summary
operator fun plus(other: Vec2d): Vec2d
operator fun minus(other: Vec2d): Vec2d
operator fun times(scale: Double): Vec2d
infix fun dot(other: Vec2d): Double
fun length(): Double
fun normalized(): Vec2d

Vec4f

data class Vec4f(val x: Float, val y: Float, val z: Float, val w: Float)

Declaration Summary
val x: Float
val y: Float
val z: Float
val w: Float

Token

sealed interface Token

Declaration Summary
data class Word(val name: String) : Token
data class Number(val value: Double) : Token
data class StringLit(val value: String) : Token
data class ColorLit(val color: Color4) : Token
data class Quote(val tokens: List<Token>, val source: String) : Token
data class KotlinEscape(val source: String) : Token {{ kotlin }} — a Kotlin expression evaluated in a with(Functions){…} scope.

RectLangException

class RectLangException(message: String) : IllegalArgumentException(message)

RectEscapeContext

class RectEscapeContext(@JvmField val i: Double, @JvmField val n: Double, @JvmField val f: Double, @JvmField val f0: Double, @JvmField val fc: Double, @JvmField val ic: Double)

The array/loop context a {{ … }} Kotlin escape sees, so escaped Kotlin can vary per copy: i (index), n (count), f (fraction 0..1), f0 (cyclic), fc (centered -1..1), ic (centered index). All zero (n=1) outside a loop.

Declaration Summary
val i: Double
val n: Double
val f: Double
val f0: Double
val fc: Double
val ic: Double

Value

sealed interface Value

VNum

data class VNum(val x: Double) : Value

VStr

data class VStr(val x: String) : Value

VColor

data class VColor(val rgba: Color4) : Value

VVec2

data class VVec2(val x: Double, val y: Double) : Value

VPaint

data class VPaint(val paint: PaintSpec) : Value

VTrack

data class VTrack<T>(val track: Track<T>) : Value

VRect

data class VRect(val id: String) : Value

VGroup

data class VGroup(val id: String) : Value

VSelection

data class VSelection(val refs: List<NodeRef>) : Value

VQuote

data class VQuote(val tokens: List<Token>, val source: String) : Value

VMaterialRef

data class VMaterialRef(val name: String) : Value

VFn

data class VFn(val fn: afield.math.functions.Functions.Fn) : Value

VData

data class VData(val values: List<Double>) : Value

RectLangTokenizer

class RectLangTokenizer(private val source: String)

Declaration Summary
val offsets: java.util.IdentityHashMap<Token, Int> Where each token began in the source, by identity. Tokens are data classes — two 1s are equal — so only identity tells them apart; compileRectSLWithSource reads this to put a problem, and each rectangle, on the line it came from.
var failureOffset: Int? Where the tokenizer gave up, when tokenize threw: the opening of an unclosed quote, string or escape, or the unexpected character.
fun tokenize(): List<Token>

TrackValueKind

enum class TrackValueKind

Constants: Double, Paint.

TrackOp1

enum class TrackOp1(val eval: (Double) -> Double, val glsl: (String) -> String, val fn: RectSLFnLowering<TrackFnUnary>)

The closed vocabulary of scalar operations that can appear inside a track.

Constants: NEG, ABS, FLOOR, CEIL, FRACT, SQRT, SIN, COS, SATURATE.

Declaration Summary
val eval: (Double) -> Double
val glsl: (String) -> String
val fn: RectSLFnLowering<TrackFnUnary>

TrackOp2

enum class TrackOp2(val eval: (Double, Double) -> Double, val glsl: (String, String) -> String, val fn: RectSLFnLowering<TrackFnBinary>)

Constants: ADD, SUB, MUL, DIV, MOD.

Declaration Summary
val eval: (Double, Double) -> Double
val glsl: (String, String) -> String
val fn: RectSLFnLowering<TrackFnBinary>

TrackOp3

enum class TrackOp3(val eval: (Double, Double, Double) -> Double, val glsl: (String, String, String) -> String, val fn: RectSLFnLowering<TrackFnTernary>)

Constants: CLAMP, MIX, SMOOTHSTEP.

Declaration Summary
val eval: (Double, Double, Double) -> Double
val glsl: (String, String, String) -> String
val fn: RectSLFnLowering<TrackFnTernary>

TimeWarpKind

enum class TimeWarpKind(val eval: (Double, Double) -> Double, val glsl: (String, Double) -> String, val fn: RectSLFnLowering<TrackFnWarp>)

How a TimeWarpTrack rewrites the time it hands to its source. Each kind takes one scalar parameter, fixed when the warp is built, so the whole warp is (kind, amount) rather than a closure over that amount.

Constants: SHIFT, SCALE, REVERSE, LOOP, PINGPONG.

Declaration Summary
val eval: (Double, Double) -> Double
val glsl: (String, Double) -> String
val fn: RectSLFnLowering<TrackFnWarp>

Track

sealed interface Track<out T>

Declaration Summary
val kind: TrackValueKind
fun sample(t: Double): T

ConstantTrack

data class ConstantTrack<T>(val value: T, override val kind: TrackValueKind) : Track<T>

Declaration Summary
val value: T
override val kind: TrackValueKind
override fun sample(t: Double): T

TcbKey

data class TcbKey(val time: Double, val valueTrack: Track<Double>, val tension: Double? = null, val continuity: Double? = null, val bias: Double? = null)

One key of a keyed track: a time, and the value the track takes there.

Declaration Summary
val time: Double
val valueTrack: Track<Double>
val tension: Double?
val continuity: Double?
val bias: Double?
val value: Double The number this key currently stands at — valueTrack folded once, at construction.
val isConstantValue: Boolean True when this key is a plain number, with no free parameter under it for a fit to move.

HermiteSegment

data class HermiteSegment(val t0: Double, val t1: Double, val p0: Double, val p1: Double, val m0: Double, val m1: Double)

One cubic Hermite span of a TcbTrack, tangents already resolved.

Declaration Summary
val t0: Double
val t1: Double
val p0: Double
val p1: Double
val m0: Double
val m1: Double
val dt: Double

TcbTrack

data class TcbTrack(val keys: List<TcbKey>, val defaultTension: Double = 0.0, val defaultContinuity: Double = 0.0, val defaultBias: Double = 0.0) : Track<Double>

Declaration Summary
val keys: List<TcbKey>
val defaultTension: Double
val defaultContinuity: Double
val defaultBias: Double
override val kind: TrackValueKind
override fun sample(t: Double): Double
fun withTcb(tension: Double, continuity: Double, bias: Double): TcbTrack
fun hermiteSegments(): List<HermiteSegment> The track as plain cubic Hermite segments, with the TCB tangents already resolved. The tension/continuity/bias parameters only ever influence the two slopes at a segment's ends, so once those are computed the curve is an ordinary Hermite spline — which is what makes a TCB track emittable as a handful of constants rather than as a solver.

LinearScalarTrack

data class LinearScalarTrack(val keys: List<TcbKey>) : Track<Double>

Declaration Summary
override val kind: TrackValueKind
override fun sample(t: Double): Double

HoldScalarTrack

data class HoldScalarTrack(val keys: List<TcbKey>) : Track<Double>

Declaration Summary
override val kind: TrackValueKind
override fun sample(t: Double): Double

EasedScalarTrack

data class EasedScalarTrack(val keys: List<TcbKey>, val easeName: String) : Track<Double>

A keyed track whose segment parameter is shaped by a named easing.

Declaration Summary
val keys: List<TcbKey>
val easeName: String
override val kind: TrackValueKind
val ease: (Double) -> Double
override fun sample(t: Double): Double

MappedDoubleTrack

data class MappedDoubleTrack(val source: Track<Double>, val op: TrackOp1) : Track<Double>

Declaration Summary
val source: Track<Double>
val op: TrackOp1
override val kind: TrackValueKind
override fun sample(t: Double): Double

BinaryDoubleTrack

data class BinaryDoubleTrack(val a: Track<Double>, val b: Track<Double>, val op: TrackOp2) : Track<Double>

Declaration Summary
val a: Track<Double>
val b: Track<Double>
val op: TrackOp2
override val kind: TrackValueKind
override fun sample(t: Double): Double

TernaryDoubleTrack

data class TernaryDoubleTrack(val a: Track<Double>, val b: Track<Double>, val c: Track<Double>, val op: TrackOp3) : Track<Double>

Declaration Summary
val a: Track<Double>
val b: Track<Double>
val c: Track<Double>
val op: TrackOp3
override val kind: TrackValueKind
override fun sample(t: Double): Double

TimeWarpTrack

data class TimeWarpTrack(val source: Track<Double>, val warp: TimeWarpKind, val amount: Double) : Track<Double>

Declaration Summary
val source: Track<Double>
val warp: TimeWarpKind
val amount: Double
override val kind: TrackValueKind
override fun sample(t: Double): Double

GateTrack

data class GateTrack(val lo: Double = Double.NEGATIVE_INFINITY, val hi: Double = Double.POSITIVE_INFINITY) : Track<Double>

A hard visibility window — 1.0 while lo <= t < hi, else 0.0. Multiplied into an object's alpha, it makes the object pop into and out of existence at fixed times (bullet spawn/despawn). lo/hi default to ±infinity for one-sided gates.

Declaration Summary
val lo: Double
val hi: Double
override val kind: TrackValueKind
override fun sample(t: Double): Double

HeadingTrack

data class HeadingTrack(val x: Track<Double>, val y: Track<Double>, val eps: Double = 1.0 / 240.0, val offset: Double = 0.0) : Track<Double>

Rotation derived from a pair of position tracks: the heading (direction of travel) of the point (x(t), y(t)). Because RectSL tracks are pure closed-form functions of time, the velocity is recovered by central finite difference — no simulation state — so a rect can auto-orient to its own trajectory. offset corrects for how the rect's art is drawn (0 = local +x points along travel).

Declaration Summary
val x: Track<Double>
val y: Track<Double>
val eps: Double
val offset: Double
override val kind: TrackValueKind
override fun sample(t: Double): Double

FreeParamTrack

data class FreeParamTrack(val name: String, val value: Double) : Track<Double>

A named unknown: a constant whose value is currently value but which is meant to be solved for rather than authored.

Declaration Summary
val name: String
val value: Double
override val kind: TrackValueKind
override fun sample(t: Double): Double

UnitKind

enum class UnitKind

Constants: Meters, Pixels, Abstract.

Origin

enum class Origin

Constants: Center, TopLeft.

FitPolicy

enum class FitPolicy

Constants: Contain, Cover, Stretch.

WallSpec

data class WallSpec(val width: Double, val height: Double, val unit: UnitKind, val origin: Origin = Origin.Center)

Declaration Summary
val width: Double
val height: Double
val unit: UnitKind
val origin: Origin

PaintSpec

sealed interface PaintSpec

Declaration Summary
data class Solid(val color: Color4) : PaintSpec
data class LinearGradient(val c0: Color4, val c1: Color4, val p0: Vec2d, val p1: Vec2d, val space: GradientSpace = GradientSpace.Local) : PaintSpec

GradientSpace

enum class GradientSpace

Constants: Local, World.

MaterialBinding

data class MaterialBinding(val materialId: Int, val shaderData: Vec4f = Vec4f.ZERO, val paramBase: Int = -1, val flags: Int = 0, val name: String = "plain")

Declaration Summary
val materialId: Int
val shaderData: Vec4f
val paramBase: Int
val flags: Int
val name: String

Node

sealed interface Node

Declaration Summary
val id: String

RectNode

data class RectNode(override val id: String, val x: Track<Double>, val y: Track<Double>, val w: Track<Double>, val h: Track<Double>, val rotation: Track<Double>, val scaleX: Track<Double>, val scaleY: Track<Double>, val alpha: Track<Double>, val z: Track<Double>, val fill: Track<PaintSpec>, val material: MaterialBinding, val tags: Set<String>) : Node

Declaration Summary
override val id: String
val x: Track<Double>
val y: Track<Double>
val w: Track<Double>
val h: Track<Double>
val rotation: Track<Double>
val scaleX: Track<Double>
val scaleY: Track<Double>
val alpha: Track<Double>
val z: Track<Double>
val fill: Track<PaintSpec>
val material: MaterialBinding
val tags: Set<String>

GroupNode

data class GroupNode(override val id: String, val children: List<String>, val x: Track<Double> = constantTrack(0.0), val y: Track<Double> = constantTrack(0.0), val rotation: Track<Double> = constantTrack(0.0), val scaleX: Track<Double> = constantTrack(1.0), val scaleY: Track<Double> = constantTrack(1.0), val alpha: Track<Double> = constantTrack(1.0), val tags: Set<String> = emptySet()) : Node

Declaration Summary
override val id: String
val children: List<String>
val x: Track<Double>
val y: Track<Double>
val rotation: Track<Double>
val scaleX: Track<Double>
val scaleY: Track<Double>
val alpha: Track<Double>
val tags: Set<String>

ShaderModuleSpec

data class ShaderModuleSpec(val name: String, val materialId: Int, val source: String, val prefix: String, val glsl: String? = null, val diagnostics: List<String> = emptyList())

Declaration Summary
val name: String
val materialId: Int
val source: String
val prefix: String
val glsl: String?
val diagnostics: List<String>

MaterialTable

data class MaterialTable(val modules: List<ShaderModuleSpec> = emptyList(), val params: FloatArray = FloatArray(0))

Declaration Summary
val modules: List<ShaderModuleSpec>
val params: FloatArray

SceneProgram

data class SceneProgram(val wall: WallSpec, val duration: Double, val fps: Double, val nodes: List<Node>, val fit: FitPolicy = FitPolicy.Contain, val background: Color4 = Color4.BLACK, val materialTable: MaterialTable = MaterialTable())

Declaration Summary
val wall: WallSpec
val duration: Double
val fps: Double
val nodes: List<Node>
val fit: FitPolicy
val background: Color4
val materialTable: MaterialTable
fun sample(t: Double): SampledScene

Transform2d

data class Transform2d(val translation: Vec2d, val rotation: Double, val scale: Vec2d, val alpha: Double)

Declaration Summary
val translation: Vec2d
val rotation: Double
val scale: Vec2d
val alpha: Double
infix fun then(child: Transform2d): Transform2d

SampledScene

data class SampledScene(val time: Double, val rects: List<SampledRect>)

Declaration Summary
val time: Double
val rects: List<SampledRect>

SampledRect

data class SampledRect(val id: String, val objectId: Int, val center: Vec2d, val size: Vec2d, val rotation: Double, val scale: Vec2d, val alpha: Double, val z: Double, val fill: PaintSpec, val material: MaterialBinding, val tags: Set<String>)

Declaration Summary
val id: String
val objectId: Int
val center: Vec2d
val size: Vec2d
val rotation: Double
val scale: Vec2d
val alpha: Double
val z: Double
val fill: PaintSpec
val material: MaterialBinding
val tags: Set<String>

RectVertex

data class RectVertex(val position: Vec4f, val color: Vec4f, val material: Vec4f, val shaderData: Vec4f)

Declaration Summary
val position: Vec4f
val color: Vec4f
val material: Vec4f
val shaderData: Vec4f

RectMesh

data class RectMesh(val vertices: List<RectVertex>, val indices: IntArray)

Declaration Summary
val vertices: List<RectVertex>
val indices: IntArray

RectMeshAttributeNames

data class RectMeshAttributeNames(val position: String = "a_position", val color: String = "a_color", val material: String = "a_material", val shaderData: String = "a_shader")

Declaration Summary
val position: String
val color: String
val material: String
val shaderData: String

RectMaterialShaderCode

data class RectMaterialShaderCode(val insertion: String, val exampleFragmentShader: String, val compiledMaterials: List<StackSLRectMaterialCompileResult>)

Declaration Summary
val insertion: String
val exampleFragmentShader: String
val compiledMaterials: List<StackSLRectMaterialCompileResult>
val ok: Boolean
val diagnostics: List<String>

FramePacket

data class FramePacket(val time: Double, val mesh: RectMesh, val materialTable: MaterialTable, val semantic: SemanticFrame)

Declaration Summary
val time: Double
val mesh: RectMesh
val materialTable: MaterialTable
val semantic: SemanticFrame

SemanticFrame

data class SemanticFrame(val rects: List<SampledRect>, val contacts: List<ContactPair>, val metrics: FrameSemanticMetrics)

Declaration Summary
val rects: List<SampledRect>
val contacts: List<ContactPair>
val metrics: FrameSemanticMetrics

RectCollider

data class RectCollider(val id: String, val corners: Array<Vec2d>, val center: Vec2d, val axes: Array<Vec2d>, val halfExtents: Vec2d)

Declaration Summary
val id: String
val corners: Array<Vec2d>
val center: Vec2d
val axes: Array<Vec2d>
val halfExtents: Vec2d

ContactPair

data class ContactPair(val a: String, val b: String, val overlapDepth: Double)

Declaration Summary
val a: String
val b: String
val overlapDepth: Double

VisualEvent

sealed interface VisualEvent

Declaration Summary
val time: Double
val confidence: Double
data class TouchStart(override val time: Double, val a: String, val b: String, override val confidence: Double) : VisualEvent
data class TouchEnd(override val time: Double, val a: String, val b: String, override val confidence: Double) : VisualEvent
data class DirectionChange(override val time: Double, val id: String, val fromAngle: Double, val toAngle: Double, override val confidence: Double) : VisualEvent
data class Stop(override val time: Double, val id: String, override val confidence: Double) : VisualEvent
data class StartMoving(override val time: Double, val id: String, override val confidence: Double) : VisualEvent
data class EnterScreen(override val time: Double, val id: String, override val confidence: Double) : VisualEvent
data class LeaveScreen(override val time: Double, val id: String, override val confidence: Double) : VisualEvent
data class RotationReversal(override val time: Double, val id: String, override val confidence: Double) : VisualEvent
data class OpacityCrossing(override val time: Double, val id: String, val threshold: Double, val rising: Boolean, override val confidence: Double) : VisualEvent

MotionMetrics

data class MotionMetrics(val objectId: String, val time0: Double, val time1: Double, val dxMeters: Double, val dyMeters: Double, val speedMetersPerSecond: Double, val directionRadians: Double, val dRotationRadians: Double, val angularVelocityRadiansPerSecond: Double, val visibleAreaSquareMeters: Double, val opacityChange: Double)

Declaration Summary
val objectId: String
val time0: Double
val time1: Double
val dxMeters: Double
val dyMeters: Double
val speedMetersPerSecond: Double
val directionRadians: Double
val dRotationRadians: Double
val angularVelocityRadiansPerSecond: Double
val visibleAreaSquareMeters: Double
val opacityChange: Double

FrameSemanticMetrics

data class FrameSemanticMetrics(val time: Double, val visibleObjectCount: Int, val totalVisibleAreaSquareMeters: Double, val averageVelocity: Vec2d, val averageSpeedMetersPerSecond: Double, val averageAngularVelocityRadiansPerSecond: Double, val totalOpacityWeightedArea: Double)

What one sampled frame shows. A rectangle counts as visible when it is not transparent and meets the wall, and the areas count only the part of it inside the wall (its bounding box's share, for a rotated one) — so a rectangle waiting below the wall to rise into it is not counted. Until 2026-09-27 every opaque rectangle counted wherever it was, which made a scene whose rectangles had all left the wall look fully occupied, and AnimationWarning.MostlyBlank never fire for it.

Declaration Summary
val time: Double
val visibleObjectCount: Int
val totalVisibleAreaSquareMeters: Double
val averageVelocity: Vec2d
val averageSpeedMetersPerSecond: Double
val averageAngularVelocityRadiansPerSecond: Double
val totalOpacityWeightedArea: Double

AnimationReport

data class AnimationReport(val duration: Double, val frameMetrics: List<FrameSemanticMetrics>, val objectMetrics: Map<String, List<MotionMetrics>>, val events: List<VisualEvent>, val warnings: List<AnimationWarning>)

Declaration Summary
val duration: Double
val frameMetrics: List<FrameSemanticMetrics>
val objectMetrics: Map<String, List<MotionMetrics>>
val events: List<VisualEvent>
val warnings: List<AnimationWarning>

AnimationWarning

sealed interface AnimationWarning

Declaration Summary
data class MostlyBlank(val start: Double, val end: Double) : AnimationWarning
data class TooMuchChange(val time: Double, val amount: Double) : AnimationWarning
data class ObjectNeverVisible(val id: String) : AnimationWarning
data class ObjectLeavesTooEarly(val id: String, val time: Double) : AnimationWarning
data class ExcessiveOverlap(val time: Double, val ids: List<String>) : AnimationWarning
data class AspectMismatch(val wallAspect: Double, val renderAspect: Double) : AnimationWarning

AnalysisSettings

data class AnalysisSettings(val fps: Double? = null, val renderWidth: Double? = null, val renderHeight: Double? = null, val blankAreaThreshold: Double = 1e-4, val tooMuchChangeSpeed: Double = 8.0, val stopSpeed: Double = 0.002, val startMovingSpeed: Double = 0.006, val directionChangeRadians: Double = PI / 3.0, val opacityThresholds: List<Double> = listOf(0.01, 0.5))

Declaration Summary
val fps: Double?
val renderWidth: Double?
val renderHeight: Double?
val blankAreaThreshold: Double
val tooMuchChangeSpeed: Double
val stopSpeed: Double
val startMovingSpeed: Double
val directionChangeRadians: Double
val opacityThresholds: List<Double>

ParseResult

data class ParseResult(val tokens: List<Token>, val diagnostics: List<String> = emptyList())

Declaration Summary
val tokens: List<Token>
val diagnostics: List<String>
val ok: Boolean

CompileResult

data class CompileResult(val programOrNull: SceneProgram?, val diagnostics: List<String> = emptyList())

Declaration Summary
val programOrNull: SceneProgram?
val diagnostics: List<String>
val ok: Boolean
val program: SceneProgram The compiled program, or a RectLangException carrying the diagnostics if compilation failed. Prefer this (and the copy-paste-friendly .program!!, where the !! is now a harmless no-op) over reaching for programOrNull: a failed compile reports why — e.g. "Unknown word: arra" — instead of an opaque NullPointerException. Inspect ok/diagnostics first if you'd rather handle failure than throw.

AnimationOracle

interface AnimationOracle

Declaration Summary
fun parse(source: String): ParseResult
fun compile(source: String): CompileResult
fun sample(program: SceneProgram, time: Double): SampledScene
fun lower(scene: SampledScene): RectMesh
fun analyze(program: SceneProgram, settings: AnalysisSettings = AnalysisSettings()): AnimationReport
fun mutate(program: SceneProgram, edit: ProgramEdit): SceneProgram

ProgramEdit

sealed interface ProgramEdit

RectAnimationOracle

object RectAnimationOracle : AnimationOracle

Declaration Summary
override fun parse(source: String): ParseResult
override fun compile(source: String): CompileResult
override fun sample(program: SceneProgram, time: Double): SampledScene
override fun lower(scene: SampledScene): RectMesh
override fun analyze(program: SceneProgram, settings: AnalysisSettings): AnimationReport
override fun mutate(program: SceneProgram, edit: ProgramEdit): SceneProgram

RectWordEffect

enum class RectWordEffect

Constants: Ordinary, Mutator.

RectWordMetadata

data class RectWordMetadata(val spelling: String, val effect: RectWordEffect)

Declaration Summary
val spelling: String
val effect: RectWordEffect

Word

class Word(val metadata: RectWordMetadata, private val action: (RectLangVm) -> Unit)

Declaration Summary
val metadata: RectWordMetadata
fun apply(vm: RectLangVm)

LoopFrame

data class LoopFrame(val index: Int, val count: Int, val ordinal: Int = index, val col: Int = -1, val row: Int = -1, val cols: Int = -1, val rows: Int = -1, val pat: PatEventContext? = null)

Declaration Summary
val index: Int
val count: Int
val ordinal: Int
val col: Int
val row: Int
val cols: Int
val rows: Int
val pat: PatEventContext? Set only by pat: the musical identity of the event this copy came from, which is what t / dur / cyc / step / atom / param read. It rides on LoopFrame rather than a parallel stack so that i, n, f, f0, fc and ic keep working inside a pat body exactly as they do inside array.

RectLangVm

class RectLangVm(private val words: MutableMap<String, Word> = mutableMapOf(), private val scene: SceneBuilder = SceneBuilder(), private val namedCurves: Map<String, Functions.Fn> = emptyMap(), private val escapeEngine: KotlinScriptEngine? = null, precompiledEscapes: Map<String, (RectEscapeContext) -> Any?> = emptyMap())

Declaration Summary
var onLiteralTaken: ((token: Token, word: String, depth: Int) -> Unit)? Told when a number or colour typed in the program is taken off the stack: its token, the word taking it, and how many values that word had already taken (0 for the top of the stack). What a study's knob labels rest on, in place of guessing from the tokens around a number; null costs nothing.
var failedAt: Token? The innermost token that was being evaluated when evaluation last failed, or null once an enclosing token finished normally (a word that caught the failure and carried on).
fun evaluatingTokens(): List<Token> The tokens being evaluated right now, outermost first: a root-level word, the definition it runs, a word inside that, …
fun wordMetadata(): List<RectWordMetadata>
fun eval(tokens: List<Token>): SceneProgram
fun evalBlock(block: List<Token>)
fun push(v: Value)
fun pop(): Value
fun peek(): Value
fun peekOrNull(): Value?
fun stackValues(): List<Value> The stack after evaluation — lets tooling/tests extract values (e.g. a built VFn).
fun replaceStackRefs(oldId: String, newRef: Value)
fun popNum(): Double
fun popString(): String
fun popQuote(): VQuote
fun popFn(label: String): Functions.Fn
fun popData(label: String): List<Double>
fun stackDepth(): Int Current stack depth — mark it before an evalBlock, then drainNumbersAbove the mark.
fun drainNumbersAbove(mark: Int, label: String): List<Double> Pop everything pushed above mark as a list of Numbers, preserving push order.
fun popColor(): Color4
fun popPaint(): PaintSpec
fun popScalarTrackOrConstant(word: String): Track<Double>
fun popBoolean(word: String): Boolean
fun scalarTrackFrom(value: Value, word: String): Track<Double>
fun pushUnaryScalar(word: String, op: TrackOp1)
fun pushBinaryScalar(word: String, op: TrackOp2)
fun pushTernaryScalar(word: String, op: TrackOp3)
fun pushCompare(word: String, compareNum: (Double, Double) -> Boolean, compareStr: ((String, String) -> Boolean)? = null)
fun builder(): SceneBuilder
fun defineTrack(name: String, track: Track<Double>)
fun namedTrack(name: String): Track<Double>
fun withLoop(index: Int, count: Int, ordinal: Int = index, block: () -> Unit)
fun withGrid(index: Int, count: Int, col: Int, row: Int, cols: Int, rows: Int, block: () -> Unit)
fun loopIndex(): Int
fun loopCount(): Int
fun loopFraction(): Double Normalized fraction across the array, i/(n-1) in [0,1]; 0 for a single element.
fun loopFraction0(): Double Cyclic fraction ordinal/n in [0,1); handy for rings (angle = f0 tau *).
fun loopFractionCentered(): Double Centered fraction 2f-1 in [-1,1]; 0 for a single element.
fun loopCenteredIndex(): Double Centered index ordinal-(n-1)/2, symmetric around 0.
fun gridCol(): Int
fun gridRow(): Int
fun gridCols(): Int
fun gridRows(): Int
fun popTemplateRef(label: String): NodeRef Template for array/radial: an explicit rect/group/id on the stack, else the active object.
fun nextTokenOrNull(): Token?
fun consumeToken(): Token
fun consumeWordName(label: String): String
fun consumeName(label: String): String
fun consumeNumber(label: String): Double
fun consumeQuote(label: String): VQuote
fun popObjectRefOrActive(): NodeRef
fun objectRefFromValue(value: Value): NodeRef
fun popSelectionOrActive(label: String): VSelection
fun selectionFrom(value: Value, label: String): VSelection
fun withSelected(ref: NodeRef, block: () -> Unit)
enum class TrackMode
fun quoteToScalarTrack(quote: VQuote, mode: TrackMode): Track<Double> A track quote — '( 0=0 2=4 ) — as keys.

NodeRef

sealed interface NodeRef

Declaration Summary
val id: String
data class Rect(override val id: String) : NodeRef
data class Group(override val id: String) : NodeRef

SceneBuilder

class SceneBuilder

Declaration Summary
var wall: WallSpec
var duration: Double
var fps: Double
var cps: Double Cycles per second — the scene's musical clock, and the one number that converts between the pattern language's cycles and RectSL's seconds. The default 1.0 is Tidal's own, and makes a scene that never says ->tempo behave as if a cycle were a second. Nothing outside the musical words reads it.
var beatsPerCycle: Double Beats per cycle — the denominator ->tempo divides a bpm by. Four is a 4/4 bar.
var bpm: Double? The last bpm ->tempo was given, remembered so that ->tempo and ->beats-per-cycle commute. Null when cps was set directly, which is not a tempo divided by anything.
var fit: FitPolicy
var background: Color4
fun build(): SceneProgram
fun hasMaterial(name: String): Boolean
fun createRect(id: String?): VRect
fun createGroup(id: String?): VGroup
fun groupSelected(id: String?, refs: List<NodeRef>): VGroup
fun cloneNode(ref: NodeRef): Value
fun cloneNodeDeep(ref: NodeRef): Value
fun cloneSelection(refs: List<NodeRef>, deep: Boolean): Value
fun rename(ref: NodeRef, newId: String): Value
fun select(ref: NodeRef)
fun endScope()
fun endGroup()
fun activeRef(): NodeRef?
fun withActive(ref: NodeRef, block: () -> Unit)
fun refFor(id: String): NodeRef
fun allRefs(): List<NodeRef>
fun rootRefs(): List<NodeRef>
fun childrenOf(refs: List<NodeRef>): List<NodeRef>
fun descendantsOf(refs: List<NodeRef>): List<NodeRef>
fun tag(tag: String)
fun hasTag(tag: String): Boolean
fun hasTag(ref: NodeRef, tag: String): Boolean
fun scalarTrack(name: String): Track<Double>
fun setX(track: Track<Double>)
fun setY(track: Track<Double>)
fun setW(track: Track<Double>)
fun setH(track: Track<Double>)
fun setRotation(track: Track<Double>)
fun addRotation(track: Track<Double>)
fun faceTravel(offset: Double) Set rotation to the heading of the active object's own x/y tracks (+offset).
fun gateVisibility(lo: Double, hi: Double) Gate the active object's alpha so it is only visible while lo <= t < hi.
fun live(birth: Double, life: Double) The emitter primitive: play the active object's animation from its birth (local time 0 becomes birth, like delay-all-time) and make it visible only during [birth, birth + life]. Authoring a trajectory in local time and firing it at birth is how one template becomes a staggered stream of bullets.
fun setScaleX(track: Track<Double>)
fun setScaleY(track: Track<Double>)
fun setAlpha(track: Track<Double>)
fun setZ(track: Track<Double>)
fun setLayer(name: String)
fun moveFront()
fun moveBack()
fun multiplyAlpha(track: Track<Double>)
fun setFill(track: Track<PaintSpec>)
fun setMaterial(name: String)
fun setShaderData(data: Vec4f)
fun defineStackSLMaterial(name: String, source: String)
fun mirrorX(axis: Double)
fun mirrorY(axis: Double)
fun translate(dx: Track<Double>, dy: Track<Double>)
fun mapActiveScalarTracks(transform: (Track<Double>) -> Track<Double>)
fun staggerTime(refs: List<NodeRef>, step: Double)
fun instantiateArray(template: NodeRef, count: Int): List<NodeRef> Element 0 is the template itself; the remaining count-1 are fresh clones (deep for groups).
fun captureNewNodes(block: () -> Unit): List<NodeRef> Run block, then return the nodes it created, in creation order.
fun stagger(refs: List<NodeRef>, dx: Double, dy: Double) Increasing positional offset over selection order: element k moves by k*(dx,dy).
fun positionAt(ref: NodeRef, t: Double): Vec2d Sample a node's own position tracks at time t (the split point for split).
fun cloneShardAtOrigin(parent: NodeRef): NodeRef Clone parent (deep for groups) and move the clone's origin to (0, 0) so a shard's flight can be authored around the origin, then translated onto the split point. Every other channel (size, fill, scale, rotation) is inherited from the parent.
fun setPolar(angle: Double, radius: Double) Place the active object on a circle of the given radius at the given angle (radians).

StackSLRectMaterialCompiler

class StackSLRectMaterialCompiler(private val convention: StackSLRectMaterialConvention = StackSLRectMaterialConvention())

Declaration Summary
fun compile(module: ShaderModuleSpec): StackSLRectMaterialCompileResult

StackSLRectMaterialConvention

data class StackSLRectMaterialConvention(val inputExpression: String = "vec4(r.uv, r.material.z, r.time)", val outputExpression: String = "vec4(stackColor.rgb * r.color.rgb, stackColor.a * r.color.a)")

Declaration Summary
val inputExpression: String
val outputExpression: String
fun wrap(module: ShaderModuleSpec, result: StackSLCompileResult): String

StackSLRectMaterialCompileResult

data class StackSLRectMaterialCompileResult(val module: ShaderModuleSpec, val stackSL: StackSLCompileResult, val rectFragGlsl: String?)

Declaration Summary
val module: ShaderModuleSpec
val stackSL: StackSLCompileResult
val rectFragGlsl: String?
val ok: Boolean

RectSLRenderSettings

data class RectSLRenderSettings(var pixelsPerMeter: Double, var fps: Double, var outputDirectory: Path, var renderer: Renderer, var renderTarget: RenderTarget = renderer.renderList, var name: String = "rectsl-${UUID.randomUUID().toString().take(8)}", var jpegQuality: Int = 92, var includeFinalFrame: Boolean = false, var frameName: (Int, Double) -> String = { index, _ -> "frame_%05d.jpg".format(index) }, var timeoutMsPerFrame: Long = 15_000L, var waitPollMs: Long = 8L, var driveRendererWhileWaiting: Boolean = false)

Declaration Summary
var pixelsPerMeter: Double
var fps: Double
var outputDirectory: Path
var renderer: Renderer
var renderTarget: RenderTarget
var name: String
var jpegQuality: Int
var includeFinalFrame: Boolean
var frameName: (Int, Double) -> String
var timeoutMsPerFrame: Long
var waitPollMs: Long
var driveRendererWhileWaiting: Boolean
fun validate()

RectSLRenderResult

data class RectSLRenderResult(val program: SceneProgram, val width: Int, val height: Int, val fps: Double, val frameTimes: List<Double>, val files: List<Path>)

Declaration Summary
val program: SceneProgram
val width: Int
val height: Int
val fps: Double
val frameTimes: List<Double>
val files: List<Path>
val frameCount: Int

RectSLRenderProgress

fun interface RectSLRenderProgress

Declaration Summary
fun frameSaved(frameIndex: Int, time: Double, file: Path)

RectSLRenderRunner

object RectSLRenderRunner

Declaration Summary
suspend fun renderJpgSequence(source: String, settings: RectSLRenderSettings, progress: RectSLRenderProgress? = null): RectSLRenderResult
suspend fun renderJpgSequence(program: SceneProgram, settings: RectSLRenderSettings, progress: RectSLRenderProgress? = null): RectSLRenderResult

RectSLSmokeTest

object RectSLSmokeTest

Declaration Summary
val minimalAcceptanceSource: String
fun main(args: Array<String>)

RectSLCoverage.kt

The exact coverage a soft raster is approximating, and the measurement of how far off it is.

Why the reference has to be exact

A soft rasterizer is only useful if you know what it is wrong about. The claim a differentiable renderer makes is "coverage is a smooth function of the parameters"; the claim it must not make is "coverage is correct". Between those two sits a bias, and a bias you have not measured is a bias your fit has silently absorbed into its answer.

So this file computes the thing there is no argument about: the area of a pixel covered by the scene, in closed form. A rect is a convex quadrilateral and a pixel is a convex box, so a single rect's coverage is a convex-convex clip (clipConvexToBox, Sutherland–Hodgman) followed by the shoelace formula (polygonArea) — exact in the arithmetic, with no sampling anywhere. A union of rects is not convex, so that case goes to JTS, an outside implementation of the same quantity, which makes it a check on this file rather than a restatement of it. RectSLSoftRasterTest cross-checks the two on the single-rect case where both apply.

The three biases, named

The measurement in measureCoverageBias is designed around the fact that there are three different reasons soft coverage departs from area coverage, and lumping them together produces a number that means nothing:

  1. Obliquity. Every kernel in CoverageKernel is a function of a single signed distance, so it is implicitly assuming the edge is axis-aligned. For an axis-aligned edge the exact area of a unit pixel on one side of it is clamp(0.5 - d), so CoverageKernel's LINEAR at beta = one pixel has zero obliquity bias there, which is itself a check: if that case does not come out at machine precision, either the kernel or the exact reference is wrong. As the edge rotates the true coverage curve becomes piecewise quadratic and the ramp cannot follow it.
  2. Feature scale. A rect thinner than the band never lets coverage saturate. As width/beta → 0 the error tends to the whole of the feature's coverage, which is the regime where a soft raster is not an approximation of the picture at all.
  3. Union seams. min of two signed distances is the union's distance outside the union and an under-estimate inside it, so where two rects abut the field reads about zero and coverage collapses towards 0.5 across a region that is entirely covered. This one is a property of the SDF, not of the kernel, and it is the reason CoverageComposite has two members.

Each is reported separately, over a population that is counted so that an empty population cannot read as a pass: see CoverageBiasRow.partialPixels and CoverageBiasRow.edgePixels.

Scope of the reference

Area coverage is defined for opaque geometry. A rect at alpha 0.4 has no area interpretation — the exact answer to "what fraction of this pixel is covered" is still 1 if the rect fills it — so exactRaster treats every visible rect as opaque and measureCoverageBias throws on a scene whose rects are translucent, instead of returning a number that compares two different quantities.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLCoverage.kt — read the source

Top-level functions

Declaration Summary
fun AdShapeFrame.toCoverageRect(): CoverageRect This frame's shape as the coverage reference sees it, with the tape taken off.
fun clipConvexToBox(polygon: DoubleArray, x0: Double, y0: Double, x1: Double, y1: Double): DoubleArray Clips a convex polygon to an axis-aligned box by Sutherland–Hodgman, returning the clipped ring as flat x, y pairs — empty when nothing survives.
fun polygonArea(polygon: DoubleArray): Double The unsigned area of a flat x, y ring by the shoelace formula; fewer than three points is 0.
fun exactUnionCoverage(rects: List<CoverageRect>, box: DoubleArray): Double The exact fraction of the box (x0, y0, x1, y1) covered by the union of rects, via JTS.
fun exactRaster(field: SceneAdField, spec: SoftRasterSpec, time: Double): DoubleArray The exact area-coverage raster of field at time, row-major, to compare a soft raster against.
fun measureCoverageBias(field: SceneAdField, spec: SoftRasterSpec, time: Double, cornerGuard: Double = 2.0): CoverageBiasRow Measures field's soft raster against exact area coverage at time, and reports the three biases.
fun CoverageKernel.reachInPixels(beta: Double, pixelWidth: Double, threshold: Double = RECTSL_REACH_THRESHOLD): Double How far from a surface, in units of pixelWidth, this kernel's coverage still has a slope of at least threshold — the scene-independent reach.
fun CoverageKernel.slope(d: Double, beta: Double): Double d(coverage)/d(distance) for this kernel, in closed form — the number CoverageBiasRow.gradientReach thresholds.

Top-level values

Declaration Summary
const val RECTSL_REACH_THRESHOLD: Double Coverage slope below which a pixel is treated as handing a fit nothing. See CoverageBiasRow.gradientReach.

CoverageRect

data class CoverageRect(val centreX: Double, val centreY: Double, val halfX: Double, val halfY: Double, val rotation: Double, val alpha: Double = 1.0)

A rect as the coverage reference sees it: a centre, half-extents that already include scale, and a rotation — i.e. exactly what an AdShapeFrame holds, with the tape taken off.

Declaration Summary
val centreX: Double World-space centre x.
val centreY: Double World-space centre y.
val halfX: Double Half-width, including accumulated scale.
val halfY: Double Half-height, including accumulated scale.
val rotation: Double Rotation in radians about the centre.
val alpha: Double Accumulated alpha in 0..1, carried so a caller can refuse a translucent scene.
fun corners(): DoubleArray The four corners as x0, y0, x1, y1, x2, y2, x3, y3, counter-clockwise in an axis-aligned frame and therefore counter-clockwise after any rotation.
fun signedDistance(x: Double, y: Double): Double The exact signed distance from (x, y) to this rect in plain arithmetic — positive outside, negative inside.
fun exactCoverage(box: DoubleArray): Double The exact fraction of the box (x0, y0, x1, y1) this rect covers, by convex clipping.

CoverageBiasRow

data class CoverageBiasRow(val kernel: CoverageKernel, val composite: CoverageComposite, val betaInPixels: Double, val pixels: Int, val partialPixels: Int, val edgePixels: Int, val cornerPixels: Int, val worstBias: Double, val worstPartialBias: Double, val worstEdgeBias: Double, val worstCornerBias: Double, val meanPartialBias: Double, val gradientReach: Double, val reachInPixels: Double)

One row of the bias table: what a given kernel, band width and geometry cost in coverage error.

Declaration Summary
val kernel: CoverageKernel Which kernel produced the soft raster.
val composite: CoverageComposite How per-rect coverage was combined.
val betaInPixels: Double The band width, in pixel widths — the unit the finding is stated in.
val pixels: Int How many pixels the raster had.
val partialPixels: Int Pixels whose exact coverage is strictly between 0 and 1: the genuinely partial ones.
val edgePixels: Int Partial pixels further than the corner guard from every rect corner.
val cornerPixels: Int Partial pixels within the corner guard of some rect corner.
val worstBias: Double The worst \|soft - exact\| anywhere in the image.
val worstPartialBias: Double The worst \|soft - exact\| over partialPixels.
val worstEdgeBias: Double The worst \|soft - exact\| over edgePixels — obliquity bias, with corners excluded.
val worstCornerBias: Double The worst \|soft - exact\| over cornerPixels — its own error source, and the measurement says the larger one.
val meanPartialBias: Double The mean \|soft - exact\| over partialPixels.
val gradientReach: Double The fraction of pixels whose coverage has a slope of at least RECTSL_REACH_THRESHOLD — how much of this image can push a fit at all.
val reachInPixels: Double How far from a surface, in pixel widths, coverage still has a slope of at least RECTSL_REACH_THRESHOLD — the scene-independent form of gradientReach.
override fun toString(): String A single line, for a log or a docs table.

RectSLEditorExtension.kt

Serves rectsl.js, the browser-side RectSL editor support, to the page and to the test that checks it.

RectSL runs in Kotlin, in RectSL.kt; a RectSL program is a triple-quoted string beginning #rectsl inside an ordinary Kotlin box. So there is no execution path to hook, and this file is the whole of the language's presence in the editor. rectsl.js lives in src/commonMain/resources, reaches the browser as the AMD module bithero/rectsl (one of the modules bootstrapMonaco.js requires by name while the editor starts), and is read back verbatim here, so the smoke test rectsl.editor-extension.workout runs the same bytes the editor does. A browser-side language that can only be tested in a browser does not stay correct.

Once installed, the extension brings syntax highlighting, flow decorations, lexical diagnostics, a context badge and completions to a #rectsl region. Colouring is decoration-based rather than a Monarch grammar, so there is no grammar here that can fail to compile and take Monaco's startup down with it.

rectsl.js installs itself either way it is loaded: as an AMD module when a Monaco loader is present, and immediately when it is evaluated bare, which is the path html() uses. The two differ in one respect. The AMD path also registers a standalone rectsl Monaco language, so that a loose .rectsl file opened in the editor gets # comment toggling and bracket handling; it carries no tokenizer of its own, because the decoration highlighter already treats a whole rectsl model as one region.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLEditorExtension.kt — read the source

Top-level functions

Declaration Summary
fun rectSLEditorExtensionJavascript(): String RectSLEditorExtension.javascript, as a top-level function.
fun rectSLEditorExtensionHtml(): String RectSLEditorExtension.html, as a top-level function.

RectSLEditorExtension

object RectSLEditorExtension

The RectSL editor extension, as bytes: the resource itself, and a page fragment that installs it.

Declaration Summary
const val id: String The extension's id, which is also the extensionId rectsl.js registers itself under.
fun javascript(): String The bytes of rectsl.js.
fun html(): String The extension as a drop-in HTML fragment: a titled line of prose saying what it does, with the whole of rectsl.js inlined in a script tag.

RectSLFit.kt

Inverse animation: say where a rect should be and when, and solve for the numbers.

This is what the differentiable backend is for. A RectSL program is normally read forwards — you write the tracks and the sampler tells you where the rects went. Once a track lowers to an Fn the arrow turns round: FreeParamTrack marks the numbers you are willing to have chosen for you, TrackFitTarget says what the track should read at particular instants, and the exact gradient of the lowered graph walks the parameters to the answer.

# a bar whose height is unknown
1.0 free-param height
val fit = track.fitParameters(listOf(TrackFitTarget(0.5, 3.0), TrackFitTarget(1.5, 0.0)))
fit.initialResidual   // where the seed was
fit.finalResidual     // where the fit ended
fit.fitted            // the same track, with the parameters substituted

Why this is not simply fitGradient

fitGradient is the library's fitter and it does the right thing for a whole graph: it moves every numeric leaf. A lowered track's leaves are mostly not parameters at all — they are the key times, the clamp bounds, the 0.5 in an easing, the -1.0 that mirrors a reversed clock. Letting a descent move those would not fit a track, it would dismantle one. So the loss and the Adam update here are the same shape as fitGradient's, over a mask: only the θ slots that freeParameterSlots identified as named parameters ever move — slots, plural, because a lowering may use one sub-Fn more than once and each occurrence gets its own θ index. A heading reads each of its position tracks twelve times. The slots of one name are one variable here: gradients summed, one Adam moment, one step written to all of them.

Everything under the mask is borrowed rather than reimplemented. The gradient is Functions.Fn.gradientAt — one forward interpretation and one backward sweep through the same FnFormulas table Fn.eval is pinned against — and θ is read and written by extractParameters / applyParameters, so index i means the same leaf here as it does there.

A track that does not lower cannot be fitted, and says so with the lowering's own reason rather than a generic failure. A track that lowers but has no free parameters is refused too: descending on an empty mask would report "converged" without having moved, which is the failure mode FnNonDifferentiable exists to prevent.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLFit.kt — read the source

Top-level functions

Declaration Summary
fun Track<Double>.fitParameters(targets: List<TrackFitTarget>, over: Functions.Domain = defaultFitWindow(targets), iterations: Int = 5000, lr: Double = 0.05, beta1: Double = 0.9, beta2: Double = 0.999, epsilon: Double = 1e-8): TrackFitResult Solves this track's named free parameters so it passes as near targets as it can.
fun defaultFitWindow(targets: List<TrackFitTarget>): Functions.Domain The window a fit lowers over when the caller does not say: the targets' own span, widened to include time 0 and padded by 10% so a target sitting exactly on the end of a gate or a loop is not on the boundary of validity.
fun Track<Double>.gradientAgreement(times: List<Double>, over: Functions.Domain = Functions.Domain(minOf(0.0, times.minOrNull() ?: 0.0), maxOf(1.0, times.maxOrNull() ?: 1.0)), step: Double = 1e-6): Double The exact gradient of this track's value at time with respect to its named free parameters, against a central finite difference of the same thing.

TrackFitTarget

data class TrackFitTarget(val time: Double, val value: Double, val weight: Double = 1.0)

One thing the fit is being asked for: the value the track should read at time.

Declaration Summary
val time: Double
val value: Double
val weight: Double Relative importance. Targets with a larger weight are matched more closely.

TrackFitStep

data class TrackFitStep(val iteration: Int, val residual: Double)

One descent step: the residual after it was applied.

TrackFitResult

data class TrackFitResult(val parameters: Map<String, Double>, val initialParameters: Map<String, Double>, val initialResidual: Double, val finalResidual: Double, val residuals: List<Double>, val steps: List<TrackFitStep>, val fitted: Track<Double>, val fittedFn: Functions.Fn, val stuckParameters: Set<String> = emptySet(), val converged: Boolean = true)

What a fit did, including where it started — because "the residual is 0.03" means nothing without the number it came down from, and a fit that did not move should be visible as such.

Declaration Summary
val parameters: Map<String, Double> The solved value of each named parameter.
val initialParameters: Map<String, Double> What each parameter started at.
val initialResidual: Double Weighted RMS distance from the targets before any step.
val finalResidual: Double Weighted RMS distance from the targets at the end.
val residuals: List<Double> The whole trajectory, losses.first() == initialResidual.
val steps: List<TrackFitStep>
val fitted: Track<Double> The input track with parameters substituted — a real track, samplable and renderable.
val fittedFn: Functions.Fn The lowered graph at the fitted parameters, for plotting or further composition.
val stuckParameters: Set<String> Free parameters whose exact derivative was zero at every target when the fit started — the ones sitting under a floor, a gate or a hold track, which have no differentiable path to the value and therefore cannot have moved.
val converged: Boolean True when the descent stopped because the residual stopped changing, false when it ran out of iterations still moving. A fit that did not converge is not finished: give it more iterations, or a larger lr, before trusting parameters.
val improved: Boolean True when the fit ended strictly closer to the targets than it started.
val partlyStuck: Boolean True when some — not all — free parameters could not move. The fit is real; it just solved for fewer parameters than were named. Read stuckParameters for which.
val residualReduction: Double How much of the starting residual was removed, in 0..1.

RectSLFitFigure.kt

A fit, as a picture, with its counterfactual beside it.

The figure stacks three bands: where the rect was asked to be, where the seed parameters put it, and where the solved parameters put it. The miss and its correction are visible in one image.

It is a CPU raster. The GPU path needs a device, a window and a render target; this runs in a unit test and inside a Field box. Every rectangle on the canvas is one SampledRect, sampled from the fitted tracks at the drawn times, so a failed fit shows in the third band.

RectSL has no text primitive (its axis labels are grids of rects), so the band labels use the 3×5 dot-matrix font the plotting words use for tick numerals.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLFitFigure.kt — read the source

RectSLFitFigure

object RectSLFitFigure

Renders a fit's target, seed and solved tracks as a three-band PNG.

Declaration Summary
data class Band(val label: String, val x: Track<Double>, val y: Track<Double>, val colour: Color4, val note: String = "", val strobe: Boolean = true, val rotation: ((Double) -> Double)? = null) One band of the figure: a pair of position tracks, strobed across time in one colour.
data class Marker(val time: Double, val x: Double, val y: Double) A place the animation was asked to pass through, drawn as an outlined square on every band.
fun render(bands: List<Band>, markers: List<Marker>, wall: WallSpec, rectSize: Vec2d, times: List<Double>, width: Int = 980, bandHeight: Int = 250, pad: Int = 14, viewScale: Double = 1.2): ByteArray Renders bands as stacked panels over the wall, strobing each at times and marking markers on all of them.

RectSLFn.kt

RectSL tracks as differentiable Fn graphs — the third backend.

Why there is a third backend at all

The track IR is symbolic rather than a closure, because a closure cannot be printed, compared, rewritten, or emitted as GLSL. TrackOp1, TrackOp2, TrackOp3 and TimeWarpKind each carry an eval: and a glsl:; this file is the third column, fn:, and it is the one that changes what RectSL can do rather than where it can run.

afield.math.functions.Functions.Fn is Field's own expression type, and autodiff_fn.kt differentiates an Fn graph exactly with respect to its numeric leaves. So a track that lowers to an Fn is a track you can take the derivative of, and therefore a track you can solve: declare where a rect should be at some instant and let a gradient find the numbers that put it there. That is Track.fitParameters, and it is the point of this file.

The two identities that make it possible

Fn was not designed as a general expression language, and two gaps look fatal and are not.

There is no "apply a unary function to another Fn's value" op. The differentiable value transforms are flip, abs, exp, log, pow, clamp, scaleY, offsetY and pointwise arithmetic — no sin. Functions.sin is an oscillator over the sample position, offset + amplitude·sin(2π·frequency·x + phase), which is not a composition. But its phase child is evaluated at x, so

sin(frequency = 0, phase = u, amplitude = 1, offset = 0)   ==   sin(u(x))

exactly (0 + 1·sin(2π·0·x + u) collapses with no rounding). That single identity gives the whole trigonometric family, which is most of the Penner easings.

There is no piecewise-in-x op, and every keyframed track is piecewise. A clamped-ramp sum fixes it. For keys (tᵢ, vᵢ) and any segment shape Sᵢ with Sᵢ(0) = vᵢ and Sᵢ(1) = vᵢ₊₁,

v(t)  =  v₀ + Σᵢ [ Sᵢ(clamp01((t - tᵢ) / (tᵢ₊₁ - tᵢ))) - vᵢ ]

is identically the clamped piecewise interpolant: for t inside span j every earlier term is vᵢ₊₁ - vᵢ and every later one is 0, so the sum telescopes to vⱼ + (vⱼ₊₁ - vⱼ)·Sⱼ(u). Before the first key everything is 0 and after the last everything is 1, which is exactly the sampler's clamping. It needs only clamp / divide / multiply / add, all differentiable, and it is exact to a few ulps rather than approximate. LinearScalarTrack, EasedScalarTrack and TcbTrack (whose tangents TcbTrack.hermiteSegments has already resolved) all lower through it.

Refusals are in the type

A wrong derivative is worse than no derivative, so the lowering never guesses. Every op declares either a lowering or a refusal with a reason — RectSLFnLowering — and Track.toFn propagates the first refusal it meets. This is the same discipline FnNonDifferentiable already applies inside the stdlib, and for the same reason: the gap has to be readable, or it grows in silence.

What is refused:

  • TrackOp2.MOD. Kotlin's % on doubles is the exact remainder, and a - b·trunc(a/b) in double arithmetic is a different function once the quotient outgrows 53 bits: 1e17 % 3 is 1.0 and the reconstruction says 0.0. Silently, and as a zero, which is indistinguishable from the guard having fired. A constant pair still folds.
  • The pattern tracks (PatTrack and friends) — an interpreter over a mini-notation tree, not an expression of fixed shape. PatPattern is a (PatArc) -> List<PatEvent> lambda: there is nothing to inspect and no closed form to find, which is the same reason FnNonDifferentiable files pat itself, and makes this the one refusal here that cannot be closed. It has a name, RECTSL_PATTERN_REFUSAL, and a predicate that decides it from the track rather than from the sentence: dependsOnPatternTrack. It costs far less than four rhythm scenes in the gallery would suggest — 27 of 5020 tracks — because pat used as scene structure is unrolled at compile time into one gated rect per event, so no interpreter survives into the sampled IR, and musicality-variation lowers all 1096 of its tracks despite spending six pat verbs. What survives is a pattern still being asked a value of at sample time: a signal word read as a channel — sine, cosine, saw, isaw, tri, square, perlin, rand all push a VSig, and VSig.asTrack is a PatTrack — or one of the two gates the pattern layer installs on a rect's alpha, ->every on the cycle count and ->degrade on a per-event coin flip. Those are the same tracks the GLSL/SDF emitter has to bake rather than lower: the one place both backends meet the same wall.
  • Degenerate keyed tracks: no keys at all (nothing to sample), or two keys sharing a time (a step between two named values, which the clamped-ramp identity cannot express).

Everything else lowers exactly, hard edges included. Fn has a first-class differentiable floor, from which ceil is -floor(-x) and fract is x - floor(x), both exact in IEEE, and the loop and pingpong time warps follow for any window. clamp01(floor(x) + 1) is an exact step — 1 at and above 0, 0 below, at every double including -0.0 — which gives HoldScalarTrack as v₀ + Σ (vᵢ - vᵢ₋₁)·[t ≥ tᵢ] and GateTrack straddling an edge as [t ≥ lo]·(1 - [t ≥ hi]), the sampler's half-open boundary included. Zero-almost-everywhere is the truth about a staircase rather than an approximation of one, so Track.fitParameters refuses a parameter reachable only through a step instead of descending a gradient that cannot move.

withExactEndpoints below covers the expo and elastic easings, whose closed forms do not reach their own endpoints. lowerHeading covers HeadingTrack through atan2, and records the one signed-zero corner where it parts company with the sampler. fnGuardedQuotient guards the divisor of a near-zero division, as b·live + (1 - live), so nothing ever divides by a near-zero and no infinity is created to mask; the sign of the suppressed zero is repaired by adding fnZeroSelector's -0.0/+0.0, since x + -0.0 is the true additive identity on doubles and x + +0.0 is not.

dev_docs/stdlib_behaviour_changes.md carries the dates on which each of those stopped being a refusal, and what it means for a scene written before then.

How the fidelity is checked

RectSLFnBackendTest is the reason to believe any of this. It reads src/jvmTest/resources/rectsl/track_characterization.txt — every sampled rect of the gallery's programs at 41 times, to full double precision — as data, and recomputes each rect's channels through Fn-lowered tracks, so the oracle shares no code path with the thing under test. It reports the worst disagreement, and it reports coverage, how much of the corpus lowered at all, because a differential test that refused everything would otherwise read as a pass.

The corpus is not uniform in what it proves, and its rows differ in what they rule out. The rows for the programs that predate this backend were dumped from code that knew nothing about it, so a disagreement on one of those can only be this file's. The rows for the four musicality-* programs were produced by the current SceneProgram.sample: still a real differential, since the symbolic IR shares no code path with the sampler, but not independent of a sampler bug introduced since. Nothing should be re-derived from those four as if it were.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLFn.kt — read the source

Used in: RectSL Inverse Animation

Top-level functions

Declaration Summary
fun <T> RectSLFnLowering<T>.valueOrNull(): T? The lowered value, or null for a refusal.
fun RectSLFnLowering<*>.refusalOrNull(): String? The refusal reason, or null when this did lower.
fun <T> RectSLFnLowering<T>.orThrow(): T The lowered value, or a thrown RectLangException carrying the refusal.
fun Track<Double>.toFn(over: Functions.Domain = Functions.Domain.ALL): RectSLFnLowering<Functions.Fn> This track as a differentiable function of time, or a refusal saying why not.
fun Track<*>.dependsOnPatternTrack(): Boolean True when this track is, or is built over, one of the mini-notation tracks — the one class toFn refuses permanently.
fun freeParameterIndices(fn: Functions.Fn): Map<String, Int> Which θ index each named free parameter ended up at, for the graph fn.
fun freeParameterSlots(fn: Functions.Fn): Map<String, List<Int>> Every θ index each named free parameter occupies, in graph order. freeParameterIndices returns the first of them and no more, so this is the one to read when a parameter can occupy several.
fun Track<Double>.freeParameters(): Map<String, Double> Every named free parameter in this track tree, with its current value, in first-seen order.
fun Track<Double>.substituteParameters(values: Map<String, Double>): Track<Double> This track with each named free parameter set to the value values gives for it, leaving anything the map does not mention alone.
fun Track<Double>.describe(): String The track tree as one line of prose, showing the algebra rather than a data class's fields.

Top-level values

Declaration Summary
const val RECTSL_PATTERN_REFUSAL: String Why a pattern track has no differentiable form.

TrackFnUnary

typealias TrackFnUnary = (Functions.Fn) -> Functions.Fn

A track lowered to an Fn, or an explanation of why it was not.

TrackFnBinary

typealias TrackFnBinary = (Functions.Fn, Functions.Fn) -> Functions.Fn

Two Fn operands to one — the shape of TrackOp2's lowering.

TrackFnTernary

typealias TrackFnTernary = (Functions.Fn, Functions.Fn, Functions.Fn) -> Functions.Fn

Three Fn operands to one — the shape of TrackOp3's lowering.

TrackFnWarp

typealias TrackFnWarp = (Functions.Fn, Double) -> Functions.Fn

A source Fn and a fixed scalar amount to one — the shape of TimeWarpKind's lowering.

RectSLFnLowering

sealed interface RectSLFnLowering<out T>

Either a lowering into the Fn world, or a refusal that says why.

Declaration Summary
data class Lowered<out T>(val value: T) : RectSLFnLowering<T> The lowering exists: value is the builder (or, at track level, the Fn itself).
data class Refused(val why: String, val closable: Boolean = false, val missing: String = "") : RectSLFnLowering<Nothing> There is no faithful differentiable form, why is the reason in one sentence, and closable says whether that is permanent.

RectSLGlsl.kt

RectSL scenes as GLSL signed distance fields.

Why this is a small file

RectSL looks like a big language to port, and almost none of it needs porting. The stack VM is a build-time machine: array, grid, radial, every selection word, each/filter, the chart and plotting layers, the {{ }} Kotlin escapes — all of it runs once and is gone, leaving a SceneProgram: a flat list of nodes whose channels are Tracks. Only that residue has to reach the GPU, and it is eleven track shapes and a 2D transform.

What the SDF is, and is not

This emits shape, not picture. A RectSL scene is painter's-algorithm 2D — z-sorted rects composited with per-rect alpha, fill, and material. An SDF is a set, and min is union, which has no notion of layering. So the emitted float field answers "how far is this point from the nearest visible rectangle" and drops fill, material, and layer order. What that buys is everything a distance field is good at and a rasterized quad is not: outlines and glows at any width, smin blending between rectangles, boolean ops, per-pixel normals, and sub-frame motion blur — because time is a uniform and every track is closed-form, so a fragment can sample the scene at as many instants as it likes.

Visibility is not dropped. A rect whose alpha has been gated to zero by ->appear / ->vanish / ->live must not contribute a silhouette, so each rect's alpha track is evaluated and tested before its distance is unioned in.

None of that list is aspirational, and it is more useful to say where the evidence is than to restate the claim. RectSLSdfLook is that vocabulary as data with one preset per capability, RectSLSdfShader assembles a whole fragment shader from it, and RectSLGalleryHarness.renderSdf renders each preset — plus a side-by-side against the raster path — into public/docs/assets/rectsl/rectsl-sdf-*.png. Two of them need the emitter's cooperation rather than the look layer's, because they change the set: see RectSLSdfOptions. To get it on screen and moving, replay dev_docs/agents/transcripts/rectsl_sdf_live.yaml.

Fidelity

Tracks are emitted symbolically, never sampled into tables: keyed tracks become branch chains, TCB tracks become cubic Hermite spans with their tangents precomputed on the CPU (TcbTrack.hermiteSegments), and the scalar ops carry their own GLSL (TrackOp1, TrackOp2, TimeWarpKind). The guards are reproduced rather than approximated — RectSL's divide-by-zero returns 0, and its mod truncates where GLSL's floors.

The one unavoidable difference is precision: the CPU samples in double and a shader runs in float. Expect agreement to about 1e-6 of a wall unit, not to the ulp. RectSLGalleryHarness.measureSdfAgreement reads the compiled shader's own answers back out of a framebuffer and compares them against SceneProgram.sample across eleven programs, and the worst disagreement is 2.5e-06 wall units.

What is approximated is now two tracks, not three. ->degrade's 64-bit integer hash reads like something a shader cannot do and is not: it lowers exactly, in uvec2 lanes, with the threshold comparison done in integers so no float enters the decision (see emitDegradeGate and INT64_HELPER). The two that remain baked are the pattern queries, which are rational arithmetic over a mini-notation tree — an interpreter, not a formula, and a different kind of problem. There is nothing to lower symbolically even in principle: a PatPattern is a (PatArc) -> List<PatEvent> lambda, so the emitter cannot tell a "bd*4" from a sine from the outside, let alone find a closed form for it. They go through bakeTrack, and what that costs depends on the track:

  • a rhythm is piecewise constant, so the bake is exact except for where its edges are, and each edge is bisected to BAKE_EDGE_TOLERANCE — a microsecond, the same order as the millionth of a cycle PatTrack quantises its own queries to, which is the resolution the edge exists at in the first place;
  • a signal — sine, tri, perlin — has no plateaus and no exactness to claim, so both a hold chain and a ramp through the samples are built and the one that measures closer wins. That is the ramp by 100x to 3000x, except on saw and isaw, whose once-a-cycle wrap a straight line would smear;
  • rand is neither. It hashes the reduced rational instant it is handed, so it has no value between samples: a finer bake is a different signal, not a better approximation of this one. The emitter detects that by baking it twice — halving the rate leaves the error where it was — says so, and stops claiming otherwise.

Which of the three, and what it measured, is RectSLSdfResult.bakes; the same thing as a sentence is RectSLSdfResult.notes. RectSLSdfBakeTest measures each regime on the timeline, and RectSLGlslSdfTest holds the whole gallery to the bound its own classification implies.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLGlsl.kt — read the source

Used in: RectSL cheatsheet

RectSLGlsl

object RectSLGlsl

Lowering host: compiles a SceneProgram to self-contained GLSL and measures what each approximation costs.

Declaration Summary
val PRELUDE: String Shared GLSL every emitted scene needs: an exact box field, and the inverse-transform that carries a world point into a rect's local frame.
val supportedEasings: Set<String> Every easing name this emitter can spell — the coverage guard's oracle.
fun emitSdf(program: SceneProgram, functionName: String = "rectslScene", options: RectSLSdfOptions = RectSLSdfOptions()): RectSLSdfResult Lower a program to a float <name>(vec2 p, float t) distance field plus everything it depends on. The result is self-contained GLSL: no uniforms, no buffers, no samplers, so it can be pasted into any shader that has a world-space point and a time.

RectSLSdfOptions

data class RectSLSdfOptions(val blendRadius: Double = 0.0, val namePrefix: String = "rectsl")

How a scene is lowered — the choices a consumer of the field cannot make for itself, because they happen inside scene.

Declaration Summary
val blendRadius: Double
val namePrefix: String

RectSLSdfBakeFidelity

enum class RectSLSdfBakeFidelity

What a bake can claim, which is a property of the track rather than of the bake.

Constants: STEPPED, SAMPLED, ALIASED.

RectSLSdfBakeShape

enum class RectSLSdfBakeShape

How a bake is written into GLSL. See RectSLSdfBakeFidelity for what it costs.

Constants: HOLD, RAMP.

RectSLSdfBake

data class RectSLSdfBake(val what: String, val fidelity: RectSLSdfBakeFidelity, val shape: RectSLSdfBakeShape, val knotCount: Int, val sampleCount: Int, val sampleRate: Double, val duration: Double, val edgeTolerance: Double, val worstError: Double, val valueRange: Double, val halfRateError: Double)

One track the SDF emitter had to sample, and what that measurably cost.

Declaration Summary
val what: String Which query was baked: "pattern" or "pattern (sample-and-hold)".
val fidelity: RectSLSdfBakeFidelity What this bake can claim, and therefore which bound below applies.
val shape: RectSLSdfBakeShape How it was written into GLSL, which follows from whichever measured closer.
val knotCount: Int Steps in the emitted chain, or samples in the emitted ramp.
val sampleCount: Int Samples taken of the track, before compression.
val sampleRate: Double
val duration: Double The bake covers [0, duration]; outside it the emitted function is constant.
val edgeTolerance: Double How far an emitted edge can sit from the track's own, in seconds.
val worstError: Double Worst deviation from the track, in the track's own units, measured at emit time.
val valueRange: Double The track's own spread over the bake, so worstError can be read as a fraction.
val halfRateError: Double worstError again, from a bake at half the sample rate — the control that separates a signal being approximated from a track that sampling cannot converge on at all. NaN for a stepped bake, which is exact and has no need of it.
val relativeError: Double worstError as a fraction of what the track does at all.
val convergenceRatio: Double How much worse the same bake is at half the rate. Above ~1.5 the samples are converging on something and worstError is a real approximation error; at ~1 they are not, and RectSLSdfBakeFidelity.ALIASED is what that means.
val note: String The sentence that appears in RectSLSdfResult.notes and above the emitted function.

RectSLSdfResult

data class RectSLSdfResult(val glsl: String, val globals: Map<String, String> = emptyMap(), val sceneGlsl: String = glsl, val sceneFunctionName: String = "rectslScene", val rectCount: Int, val trackFunctionCount: Int, val notes: List<String> = emptyList(), val bakes: List<RectSLSdfBake> = emptyList())

What RectSLGlsl.emitSdf produced, and anything it could not carry across.

Declaration Summary
val glsl: String globals then sceneGlsl: the whole self-contained field, ready to paste.
val globals: Map<String, String> Function name -> definition, for the helpers whose bodies depend on nothing in this program. Merging the maps of two emissions and writing each entry once is what lets two scenes share one shader; see RectSLSdfOptions.
val sceneGlsl: String Just this program's own functions — tracks, shapes, unions, and scene.
val sceneFunctionName: String The name the scene function was emitted under.
val rectCount: Int
val trackFunctionCount: Int
val notes: List<String> Fidelity caveats — empty means nothing was approximated or dropped.
val bakes: List<RectSLSdfBake> The measured half of notes: one entry per track the emitter had to sample. notes.size == bakes.size means sampling a pattern was the only thing it could not carry across.
val ok: Boolean

RectSLImg.kt

A RectSL scene as a sequence of Imgs: the bridge from the rectangle language to everything in Field that reads an image — afield.media.image filters, showImg, and Patch.mapImage, which puts a scene on a light rig.

Frames come from the CPU reference rasterizer the RectSL study tools use (CpuRectSLRasterizer), so no window or graphics context is needed: the scene's background, its rectangles blended as the GPU blends them into an 8-bit target, and its StackSL materials run through the StackSL GLSL interpreter. One sample per pixel, at the pixel's centre; the wall fills the frame whatever its ->fit says, row 0 at the top of the wall.

The source is compiled once, here, including any {{ … }} Kotlin escapes; a scene that does not compile throws, naming its errors.

val scene = RectSLImager(source)
val img = scene.frame(1.5, 256, 128)          // the colours as typed, 0..1
patch.mapImage(img.resize(16, 8))             // one fixture per cell, area-averaged

sRGB or light. By default a frame holds the colours as typed: 0x808080 is 0.502, which is what the RectSL pictures show and what an 8-bit target stores. Pass linear = true for the light those values stand for (0.216 for that grey). Averaging, blurring or down-sizing a frame is only physically right on linear values, and an LED's PWM is linear in light; so a rig driven from a linear frame at gamma 1 and a rig driven from an as-typed frame at withGamma(2.2) both show roughly the right brightness, but only the first averages a thin line to the right energy.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLImg.kt — read the source

Top-level functions

Declaration Summary
fun rectSLImage(source: String, time: Double, width: Int, height: Int? = null, linear: Boolean = false): Img source at time as an Img — RectSLImager for one frame. Compiles the scene every call.

RectSLImager

class RectSLImager(val source: String)

A compiled RectSL scene that renders Img frames at any time and size.

Declaration Summary
val scene: SceneProgram The compiled scene: its wall, duration, fps and rectangles.
val duration: Double The scene's ->duration, in seconds.
val fps: Double The scene's ->fps.
fun heightFor(width: Int): Int Rows for a frame width pixels across that keeps the wall's aspect (at least 1).
fun frame(time: Double, width: Int, height: Int = heightFor(width), linear: Boolean = false): Img The scene at time seconds as a 3-channel Img, width × height pixels, row 0 at the top. Colours are as typed unless linear (see the class notes). time is not wrapped: past the scene's duration its tracks hold their last values, as SceneProgram.sample does.
fun frames(count: Int, width: Int, height: Int = heightFor(width), linear: Boolean = false): List<Img> count frames evenly spaced over the scene's duration, the last one at duration.
override fun toString()

RectSLPattern.kt

RectSL's musical layer — the Tidal pattern language as scene structure

RectSL already had time: tracks, ->live birth life, ->stagger-time, ->split. What it did not have was meter — a shared clock, subdivision, and (the one that actually matters) any way for repetition n+1 to differ from repetition n. The array primitives distribute copies in space and every copy plays the same clip. That is the difference between a loop and a groove, and it is exactly the gap afield.audio.synth's pattern language fills.

The two languages turn out to be the same shape underneath, which is why this bridge is thin rather than a translation layer:

RectSL   interface Track<out T> { fun sample(t: Double): T }
Pattern  class PatPattern<A>    { fun events(arc: PatArc): List<PatEvent<A>> }

Both are pure functions of time, queryable out of order, with no simulation state — PatContractTest's purity and query-order properties assert it on one side, and RectSL's closed-form tracks (the reason HeadingTrack can recover velocity by finite difference) assert it on the other. So composing them needs no scheduler and no frame loop: a pattern is a track (PatTrack), and an event is a life window (patArray).

What lives here

§ words
clock ->cps ->tempo ->beats-per-cycle ->duration-cycles beats cycles cycle-secs
pat pat and its per-event context t dur cyc step atom param param?
patterns as values mini sine cosine saw isaw tri square rand perlin nums texts segment pat-range quantise bipolar euclid kolakoski kolakoski-gaps
variation ->every ->off ->jux ->degrade ->swing ->press ->iter ->chunk
layout strip ->euclid ->kolakoski
out pat-events

Why a separate file

RectSL.kt is already 4.6k lines, and this layer is the only part of the language that depends on afield.audio.synth. Keeping the dependency in one file means the rest of RectSL still compiles and reads without knowing the pattern language exists. What it reaches back into RectSL.kt for is a short list: RectLangVm.word/RectLangVm.mutator (so the musical words register through the same one path as every other word), RectLangVm.loopStack and LoopFrame.pat (so i/n/f keep working inside a pat body), SceneBuilder.mapScalarTracks, and SceneBuilder.cps.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLPattern.kt — read the source

VPat

data class VPat(val pattern: PatPattern<String>) : Value

An event pattern on the RectSL stack — a rhythm. Holds PatPattern<String>, the same type pat("bd*4") produces, so the whole mini-notation surface (polymeter, Euclid, <a b>, ? degrade, . grouping) arrives intact rather than being re-parsed here.

VSig

data class VSig(val signal: PatPattern<Double>) : Value

A numeric pattern on the RectSL stack — a signal. Separate from VPat because the round trip through texts()/nums() formats to nine decimal places, and a value that is about to become a track should not be quantised on the way. Coerces to a PatTrack wherever RectSL wants a Track<Double>.

PatTrack

data class PatTrack(val signal: PatPattern<Double>, val cps: Double, val fallback: Double = 0.0) : Track<Double>

A pattern sampled as a RectSL scalar track. t is scene seconds; cps converts to the pattern's cycles.

Declaration Summary
val signal: PatPattern<Double>
val cps: Double
val fallback: Double
override val kind: TrackValueKind
override fun sample(t: Double): Double

PatHoldTrack

data class PatHoldTrack(val signal: PatPattern<Double>, val cps: Double, val fallback: Double = 0.0, val lookBackCycles: Long = 2L) : Track<Double>

Sample-and-hold: like PatTrack but a rest keeps the previous event's value instead of falling back to zero. Pure, not stateful — it looks backwards from t over a bounded window and takes the latest event that has already started, so it still answers any time in any order with the same value.

Declaration Summary
val signal: PatPattern<Double>
val cps: Double
val fallback: Double
val lookBackCycles: Long
override val kind: TrackValueKind
override fun sample(t: Double): Double

PatEventContext

data class PatEventContext(val t: Double, val dur: Double, val cycle: Int, val step: Int, val atom: String, val params: Map<String, String>)

The musical identity of one event, readable inside a pat body through t, dur, cyc, step, atom and param. Times are already in scene seconds, converted once here, so nothing downstream has to remember the tempo.

Declaration Summary
val t: Double Onset, in scene seconds.
val dur: Double Length, in scene seconds — the event's whole, not the fragment a query happened to see.
val cycle: Int Which cycle the onset falls in.
val step: Int Index of this event within its own cycle, counting from 0.
val atom: String The event's mini-notation value: "bd", "808:4", "c'maj7".
val params: Map<String, String> Named parameters carried on the event — gain, pan, n, note, …

RectSLPatternOps.kt

The variation and layout halves of RectSL's musical layer. See installPatternWords in RectSLPattern.kt for the overview; this file holds §4 (variation) and §5 (layout).

src/jvmMain/kotlin/afield/lang/rectsl/RectSLPatternOps.kt — read the source

CycleSwitchTrack

data class CycleSwitchTrack(val before: Track<Double>, val after: Track<Double>, val k: Int, val cps: Double, val phase: Long = 0L) : Track<Double>

Choose between two versions of a track depending on the cycle — the mechanism behind ->every. Tidal's every k fires on cycles 0, k, 2k, …, and so does this.

Declaration Summary
val before: Track<Double>
val after: Track<Double>
val k: Int
val cps: Double
val phase: Long
override val kind: TrackValueKind
override fun sample(t: Double): Double

CycleGateTrack

data class CycleGateTrack(val k: Int, val cps: Double, val phase: Long = 0L) : Track<Double>

1.0 on the cycles ->every fires on, 0.0 elsewhere — gates whatever the body created.

Declaration Summary
val k: Int
val cps: Double
val phase: Long
override val kind: TrackValueKind
override fun sample(t: Double): Double

DegradeGateTrack

data class DegradeGateTrack(val ordinal: Int, val probability: Double, val cps: Double, val salt: Int = 0) : Track<Double>

Deterministic per-object, per-cycle dropout — the visual degradeBy. The same scene always drops the same copies on the same cycles, which is the point: a groove you cannot reproduce is not a groove. The hash is a plain integer mix rather than afield.audio.synth.rand because it is keyed on object ordinal and cycle, not on a rational time instant.

Declaration Summary
val ordinal: Int
val probability: Double
val cps: Double
val salt: Int
override val kind: TrackValueKind
override fun sample(t: Double): Double

RectSLSdfLook.kt

What to do with a RectSL distance field once you have one.

RectSLGlsl.emitSdf answers a single question per point — how far to the nearest visible rectangle, at this instant — and its KDoc lists what that buys: outlines and glows at any width, blending between rectangles, boolean ops, per-pixel normals, sub-frame motion blur. Each of those is a handful of GLSL lines over the same float, and writing them once per demo is how a capability stays a claim. This file is the vocabulary instead: a look is data, RectSLSdfShader turns (field, look) into a complete fragment shader, and RectSLSdfLook.PRESETS is one worked example of every advertised capability.

What is a look and what is not

Two of the capabilities are not looks, and the split matters. Smooth-union between rects changes the set itself, so it happens in the emitter (RectSLSdfOptions.blendRadius) — a look can only carry the number and hand it over. Boolean subtraction needs a second field, so a carving look is only meaningful when the caller supplies one; RectSLSdfShader.fragment requires it and says so.

Why the edges are measured in pixels

Every softness here is expressed as a multiple of one screen pixel, recovered per-fragment with fwidth on the world coordinate. That is the whole point of a distance field over a rasterized quad: the same scene at 512px and at 4096px has one-pixel edges at both, and no geometry was re-tessellated to get there. Widths that describe the scene rather than the screen — an outline's thickness, a contour's spacing, a bevel's depth — are in wall units, because those are properties of the drawing and should zoom with it.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLSdfLook.kt — read the source

Used in: RectSL cheatsheet, RectSL examples

RectSLSdfLook

data class RectSLSdfLook(val name: String, val about: String, val background: RectSLSdfColor = RectSLSdfColor(0x0b0d12), val fillColor: RectSLSdfColor? = RectSLSdfColor(0x2f9fe8), val outlineColor: RectSLSdfColor? = null, val outlineWidth: Double = 0.0, val glowColor: RectSLSdfColor? = null, val glowFalloff: Double = 6.0, val contourColor: RectSLSdfColor = RectSLSdfColor(0x39536e), val contourSpacing: Double = 0.0, val reliefDepth: Double = 0.0, val lightDirection: Triple<Double, Double, Double> = Triple(-0.45, 0.55, 0.70), val motionBlurSamples: Int = 1, val shutterSeconds: Double = 0.0, val blendRadius: Double = 0.0, val carves: Boolean = false, val edgeSoftnessPixels: Double = 1.2)

Rendering parameters for one RectSL distance-field presentation: fill, outline, glow, contours, relief, motion blur, and blending radius. RectSLSdfShader turns a (field, look) pair into a complete fragment shader; PRESETS is one worked example of every capability.

Declaration Summary
val name: String Short slug; used for asset filenames and as the shader's leading comment.
val about: String One line saying what this look demonstrates. Rendered into the docs table.
val background: RectSLSdfColor
val fillColor: RectSLSdfColor? Solid interior colour, or null to leave the inside unpainted.
val outlineColor: RectSLSdfColor? Band centred on the zero set, outlineWidth wall units to each side.
val outlineWidth: Double
val glowColor: RectSLSdfColor? Exponential falloff outside the shape; glowFalloff is per wall unit.
val glowFalloff: Double
val contourColor: RectSLSdfColor Iso-distance lines every contourSpacing wall units. Zero disables them.
val contourSpacing: Double
val reliefDepth: Double Depth in wall units of the rounded bevel lit by lightDirection. Zero disables.
val lightDirection: Triple<Double, Double, Double>
val motionBlurSamples: Int Instants sampled inside one frame. 1 disables motion blur.
val shutterSeconds: Double Width of the open shutter, in seconds of scene time.
val blendRadius: Double Handed to RectSLSdfOptions — the union becomes a smooth minimum of this radius.
val carves: Boolean True when this look subtracts a second field; RectSLSdfShader.fragment then needs one.
val edgeSoftnessPixels: Double Antialiasing width, in screen pixels.
fun emitOptions(namePrefix: String = "rectsl"): RectSLSdfOptions The emitter options this look implies. Only the union operator is the emitter's business.

RectSLSdfColor

value class RectSLSdfColor(val rgb: Int)

A colour written the way RectSL writes one — RectSLSdfColor(0xff3300) — so a look reads like the ->fill it is standing in for.

Declaration Summary
val r: Double
val g: Double
val b: Double
fun glsl(): String As a GLSL literal, in the linear-ish space the render target expects.

RectSLSdfShader

object RectSLSdfShader

Assembles a complete, compilable pair of shaders around an emitted field.

Declaration Summary
val PUSH_BLOCK: String The push block every shader in this file declares, in both stages.
val VERTEX: String Fullscreen triangle, no vertex buffers: three gl_VertexIndex corners covering the clip cube, with v_world the wall-space point under the fragment. Draw it with mesh.resize(3, 0, module).
fun fragment(scene: RectSLSdfResult, look: RectSLSdfLook, carve: RectSLSdfResult? = null): String The whole fragment shader for one field and one look.
fun probeFragment(scene: RectSLSdfResult): String A fragment shader that does not draw the field but reports it.
fun unpack24(r: Int, g: Int, b: Int, range: Double): Double The inverse of rectslPack24, for the CPU side of the probe.
fun quantisation(range: Double): Double The width of one 24-bit step at range — the probe's own quantisation floor.

RectSLSimplify.kt

An algebraic simplifier over the track IR, under a requirement that every rewrite be bit-exact.

The IR is data classes, so two tracks built by the same array body compare ==. The GLSL emitter's function deduplication rests on that, keying emitted functions by the track itself so that a hundred array copies share one function, and so does the min(x, x) rule below, which compares two subtrees directly. The narrower rewrites over the same IR are describe(), which prints a track, and substituteParameters, which sets named free parameters to numbers; this is the general one.

Nothing in afield.lang.rectsl writes a track out or reads one back, so a track cannot be serialized, although being data rather than closures would allow it.

What it is for

Not speed, primarily. RectSL's array words build per-copy channels by offsetting a constant, so a hundred-copy array arrives at a backend as a hundred BinaryDoubleTrack(const, const, ADD) nodes, and each backend has had to fold that itself: trackConstantValue exists because the GLSL emitter needed it, and the Fn lowering calls the same function for the same reason. A fold is the smallest rewrite there is, and both emitters had to reach for it themselves because there was nowhere over the IR to apply it once.

The bigger reason is that a simplified tree is a smaller Fn graph, and an Fn graph's size is the cost of both a sample and a gradient — Fn.eval walks every node, and gradientAt records every node on a tape. A fit runs that thousands of times.

The contract: every rule is bit-exact

A simplified track must sample bit-identically to the original, not merely equal-to-tolerance, which rules out several rewrites everyone writes by reflex. The gallery corpus caught the first one: 0 - x rewritten to -x disagrees at x = 0, because 0.0 - 0.0 is +0.0 and -(0.0) is -0.0.

A signed zero is not a curiosity in this IR. HeadingTrack takes atan2 of a velocity, and atan2(+0.0, -1.0) is +pi while atan2(-0.0, -1.0) is -pi — half a turn apart. A rewrite that "only" changes the sign of a zero can therefore spin a rectangle through 180 degrees. So the rules kept here are the ones exact for every double including both zeros, and the reflexive ones are dropped with their reasons:

rewrite kept? why
constant folding yes exact by construction — the folded value is what the chain evaluated to
x - 0 -> x yes x - (+0.0) == x for every x, both zeros included
x * 1, x / 1, x ^ 1 yes multiplicative identity preserves sign
min(x, x), max(x, x) -> x yes structural equality; exact whatever x is
-(-x) -> x, abs abs, saturate saturate yes involution / idempotence, sign-exact
clamp(x, v, v) -> v yes coerceIn(x, v, v) is v
shift 0, scale 1, degenerate loop/pingpong yes the lookup is t unchanged
x + 0 -> x no (-0.0) + 0.0 is +0.0, so this flips a signed zero
0 - x -> -x no the rule the corpus caught, above
mix(a, b, 1) -> b no a + (b - a) is not b in floating point at all
mix(a, b, 0) -> a, mix(a, a, t) -> a no both end in a + (+/-0.0), signed-zero unsafe
x * 0 -> 0 no only valid for finite x, and nothing here proves x is finite

Losing the additive identity costs less than it looks like it should: RectSL's array words build per-copy channels by offsetting a constant, and the constant fold takes those whole.

Free parameters are never folded, for the same reason trackConstantValue takes a flag: a FreeParamTrack holds a number but is not a number, and folding it away would delete the thing a fit is meant to solve for.

How it is checked

RectSLSimplifyTest runs it over the gallery and requires every rewritten channel to sample bit-identically to the original at 41 times each, and to be a fixed point of a second pass. The node-count reduction it prints is reported rather than asserted at any figure, because that figure moves with the gallery and with the IR — a key value became a node when TcbKey began holding a track, for instance. Run the test to read the current one.

A plausible-looking rule that is wrong in one corner is this file's characteristic failure, and bit-identity over the corpus already assembled for the differentiable backend is what catches it.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLSimplify.kt — read the source

Top-level functions

Declaration Summary
fun Track<Double>.nodeCount(): Int Total nodes in this track's tree, counting a shared subtree once per path — the size a backend pays.
fun Track<Double>.simplify(): Track<Double> This track with constants folded and the algebraic identities applied, bottom-up.

RectSLSoftRaster.kt

The third lowering: a RectSL scene as a differentiable raster, so a picture can be fitted.

Why an image fit needs a rasterizer

Track.fitParameters turns the arrow round for one track: mark a number free-param, say what the track should read at three instants, and the exact gradient of the lowered Fn walks the number to the answer. What it cannot do is take a target image, because the derivative an image fit needs is d(pixel)/d(parameter) and a per-track scalar lowering has no pixels in it. That was recognised on 2026-08-06 and the image fit was abandoned rather than half-landed. This file is the retry, on the terms the abandonment set: a soft rasterizer whose coverage bias is measured. The exact reference that bias is measured against is exactCoverage; the numbers, and where they say this is and is not usable, are in dev_docs/RECTSL_SOFT_RASTER.md.

The one design decision worth arguing about

The obvious plan is "lower the scene to an Fn-valued signed distance field over (x, y, t)". Functions.Fn cannot be that: it is a (Double) -> Double, one input and one output, and every existing consumer of a lowered track — freeParameterSlots, Track.substituteParameters, Track.fitParameters — reads its θ-vector as the track's own leaves. Smuggling x and y in as two more θ leaves would make all of them wrong about what they are solving for, and nothing would report it.

So the differentiable layer here is not Fn, it is AD on an ADTape — which is what Fn's own gradient is built out of anyway. FnAutoDiff.interpret already re-reads an Fn graph in AD arithmetic at an AD-tracked x, so:

  • every track goes through the existing lowering, Track.toFn, unchanged;
  • every track is then interpreted on the tape at the frame's time;
  • the geometry — the ancestor-group composition, the rotate-into-local-frame, the exact box field, the min union — is written in AD directly, transcribed from the GLSL in RectSLGlsl.PRELUDE and the composition in Transform2d.then;
  • the coverage kernel and the image loss go on the same tape.

One backward sweep then yields d(loss)/dθ for every parameter in the scene at once, with nothing finite-differenced anywhere in the chain. That is strictly more than an Fn could have given.

The scene-level θ ledger

Track.fitParameters masks θ within one Fn. A scene has eight tracks per rect and 183 rects in the largest gallery scene, so it has hundreds of independent θ-vectors, and "index i" means nothing until they are concatenated. SceneAdField does that concatenation once: every channel gets a contiguous block, SceneAdField.theta is the whole scene's leaves in block order, and SceneAdField.freeParameterSlots re-maps each free-param name to its global slots — unioned across channels, because one name legitimately reaches several of them. The gradient-summed, step-applied-to-every-slot discipline of Track.fitParameters carries over unchanged; only the index space is new.

Why a frame is a separate object

ADTape records five parallel arrays per operation and never shrinks. A 128² raster over a 30-rect scene, naively taped, is ~14M nodes — half a gigabyte. But a shape's transform depends only on (θ, t) and not on the pixel, so SceneAdField.frameAt evaluates all of it once and SceneAdFrame hands back only what varies per pixel: centre, rotation, half-extent, alpha. The per-pixel cost drops to about twenty tape nodes per rect, and because an image loss is a sum over pixels its gradient is additive — so softRasterLossAndGradient walks the image in row chunks, one tape per chunk, and accumulates. Memory becomes a choice rather than a limit.

Fidelity between the two interpreters

A second interpreter of the same graph is exactly the situation where a slightly different function gets differentiated confidently. autodiff_fn.kt has a name for the guard against that — FnAutoDiff.worstValueDisagreement — and toAdField runs it on every channel it lowers, recording the worst in SceneAdField.worstChannelDisagreement. A channel whose AD reading parts company with its own Fn.eval is refused, not averaged in.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLSoftRaster.kt — read the source

Used in: A RectSL animation, as a solid object

Top-level functions

Declaration Summary
fun coverageAt(frame: SceneAdFrame, spec: SoftRasterSpec, i: Int, j: Int): AD The soft coverage of one pixel of spec, tracked, from an already-transformed frame.
fun softRaster(field: SceneAdField, spec: SoftRasterSpec, time: Double, at: DoubleArray = field.theta, rowsPerChunk: Int = 8): DoubleArray The whole soft raster as coverage in 0..1, row-major, untracked.
fun softRasterLossAndGradient(field: SceneAdField, spec: SoftRasterSpec, time: Double, target: DoubleArray, at: DoubleArray = field.theta, rowsPerChunk: Int = 8): RasterLoss The mean squared difference between the soft raster and target, and its exact gradient with respect to every numeric leaf in the scene.
fun SceneProgram.toAdField(over: Functions.Domain = Functions.Domain(0.0, maxOf(duration, 1e-9))): SceneAdField Lowers every rect of this scene into a SceneAdField, skipping — and reporting — the rects whose channels have no differentiable form.

Top-level values

Declaration Summary
const val RECTSL_INVISIBLE_DISTANCE: Double The sentinel an invisible rect's distance returns, matching the emitted GLSL's 1e9 exactly.
const val RECTSL_CHANNEL_TOLERANCE: Double How far apart the AD reading of a channel and its own Fn.eval may be before it is refused.

AdShape

class AdShape internal constructor(val id: String, internal val levels: List<LoweredLevel>, internal val w: LoweredChannel, internal val h: LoweredChannel)

One rect, with its ancestor groups flattened into an ordered transform stack.

Declaration Summary
val id: String The RectNode.id this came from, so a disagreement can be attributed to a rect.

AdShapeFrame

class AdShapeFrame internal constructor(val id: String, val centreX: AD, val centreY: AD, val rotation: AD, val halfX: AD, val halfY: AD, val alpha: AD)

A rect's world transform at one instant, on one tape — the part that does not vary per pixel.

Declaration Summary
val id: String The rect this is a frame of.
val centreX: AD World-space centre x, tracked.
val centreY: AD World-space centre y, tracked.
val rotation: AD Accumulated rotation in radians, tracked.
val halfX: AD Half-width including accumulated scale, tracked and non-negative.
val halfY: AD Half-height including accumulated scale, tracked and non-negative.
val alpha: AD Accumulated alpha, clamped to 0..1, tracked.
val visible: Boolean True when this rect contributes a silhouette at all — the emitted shader's own test.
fun distanceAt(x: Double, y: Double): AD The exact signed distance from (x, y) to this rect, tracked: positive outside, negative inside, transcribed from rectslRect / rectslBox in RectSLGlsl.PRELUDE.

SceneAdFrame

class SceneAdFrame internal constructor(val tape: ADTape, val theta: List<AD>, val time: Double, val shapes: List<AdShapeFrame>)

Every rect of a scene, transformed at one instant, on one tape.

Declaration Summary
val tape: ADTape The tape every value in this frame lives on.
val theta: List<AD> The scene's whole θ-vector as tape variables, in SceneAdField.theta order.
val time: Double The instant this frame was transformed at.
val shapes: List<AdShapeFrame> Every rect, in scene order, visible or not.
val visible: List<AdShapeFrame> by lazy(LazyThreadSafetyMode.NONE) The rects not gated invisible at this instant.
fun distanceAt(x: Double, y: Double): AD The scene's signed distance at (x, y): the min over visible rects, or RECTSL_INVISIBLE_DISTANCE when nothing is visible.

SceneAdField

class SceneAdField internal constructor(val program: SceneProgram, val shapes: List<AdShape>, val theta: DoubleArray, val freeParameterSlots: Map<String, List<Int>>, val skipped: Map<String, String>, val worstChannelDisagreement: Double, val window: Functions.Domain)

A whole scene, lowered: its shapes, its concatenated θ, and what it had to leave out.

Declaration Summary
val program: SceneProgram The scene this was lowered from.
val shapes: List<AdShape> The rects that lowered, in scene order.
val theta: DoubleArray Every numeric leaf of every lowered channel, in channel-block order.
val freeParameterSlots: Map<String, List<Int>> Each free-param name's global θ slots, unioned across the channels that use it.
val skipped: Map<String, String> Rect id to refusal reason, for the rects that could not be lowered.
val worstChannelDisagreement: Double The worst AD-versus-Fn.eval disagreement across every channel that was accepted.
val window: Functions.Domain The time window every channel was lowered over.
val rectCount: Int How many rects lowered.
val freeParameterNames: List<String> The scene's free-parameter names in first-seen order.
fun frameAt(time: Double, at: DoubleArray = theta): SceneAdFrame Transforms every shape at time on a fresh tape, with at as the θ-vector.
fun signedDistanceAt(x: Double, y: Double, time: Double, at: DoubleArray = theta): Double This scene's signed distance at one point and instant, untracked — the shape of call the differential check against SceneProgram.sample makes.
fun freeParameters(at: DoubleArray = theta): Map<String, Double> The value of each named free parameter in at, read from its first global slot.
fun withFreeParameters(values: Map<String, Double>, at: DoubleArray = theta): DoubleArray A copy of at with each name in values written to every slot it occupies.

CoverageKernel

enum class CoverageKernel

The three soft-coverage kernels, normalised to be comparable: each reads 0.5 at the surface and has slope 1/beta there.

Constants: LINEAR, SMOOTHSTEP, SIGMOID.

Declaration Summary
fun coverage(d: AD, beta: Double): AD This kernel's coverage for a tracked signed distance d at band width beta.
fun coverage(d: Double, beta: Double): Double This kernel's coverage for a plain signed distance — the same formula, untracked.

CoverageComposite

enum class CoverageComposite

How per-rect coverage becomes one number per pixel.

Constants: UNION_SDF, OVER.

SoftRasterSpec

data class SoftRasterSpec(val wall: WallSpec, val width: Int, val height: Int, val beta: Double, val kernel: CoverageKernel = CoverageKernel.LINEAR, val composite: CoverageComposite = CoverageComposite.UNION_SDF)

A rasterization window: a pixel grid over a WallSpec, plus the coverage kernel and the band width it is allowed.

Declaration Summary
val wall: WallSpec The wall the grid covers.
val width: Int Pixel columns.
val height: Int Pixel rows.
val beta: Double Coverage band width, in wall units.
val kernel: CoverageKernel Which normalised kernel turns distance into coverage.
val composite: CoverageComposite How per-rect coverage becomes one number.
val pixelWidth: Double Wall units across one pixel.
val pixelHeight: Double Wall units down one pixel.
val pixelCount: Int How many pixels there are.
val betaInPixels: Double beta expressed in pixel widths — the unit the bias measurement is reported in.
fun centreX(i: Int): Double The wall x of pixel column i's centre.
fun centreY(j: Int): Double The wall y of pixel row j's centre. y grows up in wall space and down in the image.
fun pixelBox(i: Int, j: Int): DoubleArray The wall-space box of pixel (i, j) as (x0, y0, x1, y1), with y0 below y1.

RasterLoss

class RasterLoss internal constructor(val loss: Double, val gradient: DoubleArray)

A raster loss and its exact gradient with respect to the scene's whole θ.

Declaration Summary
val loss: Double Mean squared coverage error over the image.
val gradient: DoubleArray d(loss)/dθ, one entry per SceneAdField.theta slot.
val rmse: Double Root mean squared coverage error — the number worth printing.

RectSLSoftRasterFigure.kt

A soft raster, its exact reference, and the difference between them — as a picture.

The third panel is |soft - exact| on a heat ramp, with its own peak printed in the corner. It shows where a soft raster's bias lives: a thin fringe along an oblique edge, a bright bar down a union seam, a whole feature glowing when it is thinner than the band.

The figure is a CPU raster, for the reason RectSLFitFigure gives: per-pixel coverage is the data here, so a GPU would add only a device, a window and a render target. The labels use the same 3×5 dot-matrix font RectSL's own plotting words use.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLSoftRasterFigure.kt — read the source

RectSLSoftRasterFigure

object RectSLSoftRasterFigure

Renders soft-versus-exact coverage figures as PNG panels.

Declaration Summary
data class Panel(val label: String, val values: DoubleArray, val heat: Boolean = false, val gain: Double = 1.0) One panel of the figure: a coverage image, a label, and how to colour it.
fun renderBias(field: SceneAdField, spec: SoftRasterSpec, time: Double, scale: Int = 2): ByteArray The three-panel figure for one scene and one raster spec: soft, exact, and their difference.
fun render(field: SceneAdField, spec: SoftRasterSpec, time: Double, scale: Int = 2): ByteArray The soft raster alone, at scale screen pixels per raster pixel.
fun renderPanels(panels: List<Panel>, width: Int, height: Int, scale: Int = 2): ByteArray Lays panels out in a row, each width x height raster pixels at scale screen pixels per pixel, with a one-pixel rule between them and the label drawn over the top-left corner.

RectSLSource.kt

RectSL read as source: what the editor and the study tools need that a plain compile does not give — where things are.

compileRectSL answers "does it compile, and to what". A person editing a scene, or a model talking one into existence, also needs to know which line a problem is on, which line made a given rectangle, and which numbers in the text are worth turning. The tokenizer records where each token began (RectLangTokenizer.offsets), the evaluator records what it was evaluating when it failed (RectLangVm.failedAt), and the scene reports each node as it is made (SceneBuilder.onNode); this file turns those into lines.

Nothing here changes what a program means: compileRectSLWithSource runs the same tokenizer and evaluator as compileRectSL, with the same curves, engine and pre-compiled escapes.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLSource.kt — read the source

Top-level functions

Declaration Summary
fun rectSLLineAndColumn(source: String, offset: Int): Pair<Int, Int> The 1-based line and column of offset in source.
fun compileRectSLWithSource(source: String, curves: Map<String, Functions.Fn> = emptyMap(), engine: KotlinScriptEngine? = null, precompiledEscapes: Map<String, (RectEscapeContext) -> Any?> = emptyMap()): RectSLCompiled Compile source as compileRectSL does, and say where: each problem on its line, and which line made each rectangle and group. A material whose StackSL does not compile is a problem on the material line that defines it, because the GPU would draw it magenta.
fun rectSLKnobs(source: String, takenBy: Map<Int, Pair<String, Int>> = emptyMap()): List<RectSLKnob> Every knob in source: each number and hex colour, in source order, with what reads it. Numbers inside {{ … }} Kotlin escapes are not knobs; numbers inside a material quote are (they are StackSL numbers, and turning one re-shades the material). A source that does not tokenize has none.
fun formatRectSLKnob(value: Double, original: String): String value written the way original was: as many decimals as it had (at least enough to show the change), no exponent.
fun setRectSLKnobs(source: String, values: Map<Int, Double>): String source with knobs rewritten in place (knob index to new value). A colour knob takes its value as the 24-bit number of its hex (0xff8800 is 16746496). Refuses an index that is not a knob, and a value the number's reader cannot hold — a wall, duration or frame rate that is not positive, an rgb channel outside 0..1, a colour outside 24 bits — saying why, rather than writing a scene that fails or silently clamps. The same knobs must exist afterwards.

RectSLProblem

data class RectSLProblem(val message: String, val line: Int?, val column: Int?, val error: Boolean = true)

A problem with a RectSL program, as the editor shows it. line and column are 1-based.

Declaration Summary
val summary: String The problem in one line, with its place, as tool results quote it.

RectSLOrigin

data class RectSLOrigin(val line: Int, val column: Int, val statementLine: Int)

Where a node came from: the line and column of the innermost word that made it (a rect, the array that cloned it, a word inside a definition), and the line of the root-level statement that was running — the same for a rectangle made directly, the call site for one made inside a definition.

RectSLCompiled

class RectSLCompiled(val source: String, val program: SceneProgram?, val problems: List<RectSLProblem>, val origins: Map<String, RectSLOrigin>, val materials: List<StackSLRectMaterialCompileResult>, val takenBy: Map<Int, Pair<String, Int>> = emptyMap())

compileRectSLWithSource's answer.

Declaration Summary
val source: String
val program: SceneProgram? The compiled scene, or null when it did not compile.
val problems: List<RectSLProblem> Every problem, errors first; material (StackSL) problems included.
val origins: Map<String, RectSLOrigin> Which line made each node, by node id.
val materials: List<StackSLRectMaterialCompileResult> The materials the scene's StackSL compiled to, when it compiled.
val takenBy: Map<Int, Pair<String, Int>> What took each literal off the stack, by the literal's offset in source: the word, and how many values that word had taken before it — the evaluator's own record (RectLangVm.onLiteralTaken).
val ok: Boolean

RectSLKnob

data class RectSLKnob(val knob: Int, val value: Double, val text: String, val line: Int, val column: Int, val start: Int, val end: Int, val role: String, val context: String, val colour: String? = null)

A number (or a colour) typed in a RectSL program, which a person or a model can turn without rewriting the line around it.

Declaration Summary
val knob: Int
val value: Double
val text: String
val line: Int
val column: Int
val start: Int
val end: Int
val role: String
val context: String
val colour: String?

RectSLVocabulary.kt

Every word the RectSL evaluator knows, with what the cheatsheet says it does — the vocabulary the editor offers in completion and explains beside the cursor.

The words are the evaluator's own (RectLangVm.wordMetadata, the set its "did you mean" searches), so a word the evaluator knows is offered even when the editor's hand-written table (rectsl.js) lacks it. Their stack effect and description come from the RectSL words guide, which tools/rectsl_conversation_documents.py generates from public/docs/rectsl_cheatsheet.md and holds equal to it. The editor asks for completion through Field's complete request, which a RectSL scene answers here (RectSLExecution), as does a Kotlin box for its rectSL property.

src/jvmMain/kotlin/afield/lang/rectsl/RectSLVocabulary.kt — read the source

RectSLVocabulary

object RectSLVocabulary

The RectSL word list and the completion it answers.

Declaration Summary
val words: List<VocabularyWord> by lazy Every built-in word, alphabetically.
fun complete(source: String, position: Int): Completion Completion at position in the RectSL scene source: the vocabulary and the scene's own : name definitions.

RectslBoxTools.kt

Two tools a document can hand to its AI4 agents: one syntax-checks a RectSL program, the other renders it in the document's live Field 4 window and returns the captured frames to the conversation.

Both are built with aiTool. A box registers the pair once in its code, and running that again replaces them:

registerRectslStudioTools()
The agent then sees rectsl.check and rectsl.run. {{ … }} escapes in the RectSL source are compiled against this box's Kotlin engine, so they can read the box's live state (ki-vars, thisBox members).

src/jvmMain/kotlin/afield/lang/rectsl/RectslBoxTools.kt — read the source

Top-level functions

Declaration Summary
fun Box.registerRectslStudioTools() Registers rectsl.check and rectsl.run against this box, replacing either if it already has them.
suspend fun Box.renderRectslToImages(source: String, pixelsPerMeter: Double = 100.0, fps: Double = 8.0, maxImages: Int = 6, renderer: Renderer = fromContext(), configure: RectSLRenderSettings.() -> Unit = {}): List<String> Render source in renderer (the document's live renderer by default) and return the captured frames as data:image/jpeg;base64,… URLs, subsampled to at most maxImages.

EmbeddedRectSL.kt

Completion and line notes for RectSL written inside a Kotlin box's code, as a string whose first line is #rectsl (EmbeddedRectSL).

src/jvmMain/kotlin/afield/lang/rectsl/study/EmbeddedRectSL.kt — read the source

Top-level functions

Declaration Summary
fun installEmbeddedRectSLLineNotes() Line notes for a Kotlin box's code inside a #rectsl scene. A line outside one is left to the other providers of code (EmbeddedStackSL's among them): LineNotes asks each in turn.

EmbeddedRectSL

object EmbeddedRectSL

RectSL written inside a Kotlin box's code, as a string whose first line is #rectsl (the convention the editor already highlights): the RectSL vocabulary when completion opens inside it, and each line's scene-so-far as a line note — the help a rectSL property has, where the scene sits in Kotlin. The StackSL counterpart, EmbeddedStackSL, also finds strings assigned to stack.auto slots; RectSL has no such slot, so only the header marks it.

Declaration Summary
fun regions(code: String): List<EmbeddedStackSL.Region> Every #rectsl string in code: its body (after the header line) and where it starts.
fun complete(code: String, position: Int): Completion? Completion at position of code, when it is inside a #rectsl string: the vocabulary, with bounds in the code's offsets.
fun lineNote(box: Box, code: String, line: Int): LineNotes.Note? line (1-based) of code's #rectsl scene, as a note: the scene built up to it; null outside one.

RectSLPrograms.kt

RectSL as a main language in Field: running a scene box compiles its rectSL, draws it live in the window above it, and checks it in to the box's study.

RectSLPrograms holds the per-box studies, RectSLLive keeps a box's window following its edits, RectSLExecution is the box execution, and the install... functions register the language, its line notes, its topology watcher and its scene action.

src/jvmMain/kotlin/afield/lang/rectsl/study/RectSLPrograms.kt — read the source

Top-level functions

Declaration Summary
fun installRectSLLanguage() Register RectSL as a main language, so "Set Main Language: RectSL" is offered on a root box.
fun installRectSLLineNotes() A RectSL scene's lines, for the editor's completion popup and cursor notes: the scene built up to each, at the time its window is showing (RectSLStudy.lineNote).
fun installRectSLLanguageWatcher() Re-apply rectSLTopologyNeedsUpdating whenever the graph changes.
fun installRectSLSceneAction(root: Box) "Make a RectSL Scene": the selected box becomes a scene beneath a RectSL root — a new one just above it, if it has none, which opens a window of its own — and starts playing. A selected root gets a new scene beneath it.
fun rectSLTopologyNeedsUpdating(values: Collection<Box>) Give every child of a RectSL root a RectSLExecution that edits rectSL, and hand a child back to Kotlin when no RectSL root claims it any more.

Top-level values

Declaration Summary
const val RECTSL_STARTER The scene a new RectSL box starts with: a wall, a background, and one rectangle to change.

RectSLPrograms

object RectSLPrograms

RectSL as one of Field's main languages, and the home of every RectSL scene's study.

Declaration Summary
const val PROPERTY The property a RectSL scene lives in.
const val CHANNEL The output channel a scene's study card is shown in.
const val LANGUAGE The main-language id, as mainLanguage stores it.
val WINDOW Where the window a RectSL root opens for itself goes, and its size: a 16:9 wall at 960 by 540.
fun sessionOf(box: Box): SessionInformation? The open document box belongs to, or null.
fun rendererFor(box: Box): Renderer? The running renderer a scene draws in; see BoxStudies.rendererAbove.
fun study(box: Box): RectSLStudy box's study, created on first use; publishing writes the box's rectSL on the main thread.
fun readyStudy(box: Box): RectSLStudy study, with a first revision checked in if it has none. Draws probe frames, so call it off the main thread.
fun viewer(box: Box): RectSLStudyViewer The card showing box's study, created (and shown) on first use.
fun live(box: Box): RectSLLive box's binding to the window above it.
fun run(box: Box) Run box as the editor's run does: draw it, follow its edits, and check it in.
fun isScene(box: Box, property: String): Boolean Whether a conversation focused on box's property is about a RectSL scene.

RectSLLive

class RectSLLive internal constructor(private val box: Box)

A RectSL scene drawn live in a window, looping over its duration: the lowered rectangles through RectSLStudyShader's shaders into an 8-bit target the size of the window, shown by a fullscreen pass that hands the swapchain exactly those bytes (see RectSLStudyShader for why not straight into the window) — the whole wall fitted in, with black around it.

Declaration Summary
var held: Double? Seconds to hold the window at, or null to play.
var preview: RectSLDrawable? A scene the card is previewing in place of the box's, or null.
var pinnedLine: Int? A line the card pinned the window to: it shows the scene as it stands after that line. Null for the whole scene.
var followCursor: Boolean Whether the window shows the scene as it stands at the editor's line, rather than the whole of it.
fun shownLine(cursor: Int?): Int? The line the window is showing the scene up to, given the editor's cursor; null for the whole scene.
var outlined: Set<String> The rectangles to outline in the window: those the editor's line makes (see RectSLWindowLinks).
var drawing: RectSLDrawable? The scene the window is drawing, with the lines its rectangles came from, or null.
val showing: String? The source the window above the box is drawing, or null — also when that window closed, changed, or was resized since (so the next present rebuilds its target).
fun time(): Double The time the window is drawing now, in the scene's seconds.
fun hold(seconds: Double?) Hold the window at seconds, or play again from there with null.
fun restart() Play from the start.
fun present(good: RectSLDrawable?): String? Show good — or the card's preview while there is one — if it is not what the window is drawing.
fun show(drawable: RectSLDrawable): String? Draw drawable in the window above the box. Returns null when it is drawing, otherwise why not: no window, or a shader the GPU refused (the previous scene keeps drawing).
fun hide() Stop drawing in the window. Nothing to hide costs nothing, and does not wait for the main thread.
fun pick(u: Double, v: Double, aspect: Double): Int? What a click at (u, v) of the window does — u across from the left, v down from the top, both 0..1, in a window aspect wide for each unit high: find the topmost rectangle under it now and put the editor on the line that made it. Returns that line, or null when the click missed.

RectSLExecution

class RectSLExecution(val inside: Box) : BoxExecution

How a child of a RectSL root runs: see RectSLPrograms. Run and launch both draw the scene and follow its edits until stopped; run also checks it in as a revision. Compiling happens off the main thread (a {{ … }} escape compiles Kotlin); drawing and reporting on it.

Declaration Summary
override val languageName: String
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override fun stop(inside: Box)
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit) The evaluator's vocabulary and the scene's own definitions (afield.lang.rectsl.RectSLVocabulary.complete).

RectSLStudy.kt

The RectSL study: checked revisions of a box's rectSL scene, and the frames, timelines, traces, knob sweeps, variants and comparisons that an AI4 chat and a person use to look at it.

The file holds RectSLStudy itself, the RectSLCheck and RectSLLineReport results it returns, and RectSLEscapes.

src/jvmMain/kotlin/afield/lang/rectsl/study/RectSLStudy.kt — read the source

RectSLStudy

class RectSLStudy(val directory: File, readSource: () -> String, replaceSource: (expected: String, replacement: String) -> Boolean)

Talking a scene into existence: the RectSL study a box's AI4 chat, and the person beside it, work on together.

Declaration Summary
val directory: File
var rasterizer: RectSLRasterizer What draws the study's frames: the CPU reference until the runtime hands it a RectSLGpuRasterizer.
val revisions The revision history this study publishes through.
var onChange: (() -> Unit)? Called after anything that changes what the card should show.
var editorLine: () -> Int? The line the person's editor cursor is on in this study's box, or null — see afield.study.source.BoxStudies.personAt.
fun resume(): String Resume from HEAD, and the variants a previous session was holding against the revision still current.
fun currentId(): String The checked revision's id, or empty.
fun inspect(id: String = "current"): StudyReport Everything a model needs before editing: source, live-edit state, history, the scene's shape, lines and knobs.
fun edit(expectedRevision: String, expectedSourceHash: String, source: String, description: String, previewSeconds: List<Double> = emptyList()): StudyReport Publish a complete program. See SourceStudy.edit for the guards.
fun patch(expectedRevision: String, expectedSourceHash: String, edits: List<StudySourceEdit>, description: String, previewSeconds: List<Double> = emptyList()): StudyReport Publish exact replacements against the live source.
fun setNumbers(expectedRevision: String, expectedSourceHash: String, numbers: Map<Int, Double>, description: String, previewSeconds: List<Double> = emptyList()): StudyReport Publish new values for knobs of the live source (knob index to value), rewritten in place.
fun checkIn(description: String = "edited by hand"): StudyReport Check in whatever is in the editor now — a person's own edit — as a revision.
fun restore(expectedRevision: String, expectedSourceHash: String, id: String, description: String = ""): StudyReport Make an earlier revision current again, as a new revision.
fun observe(id: String = "current", seconds: List<Double> = emptyList(), size: Int = 0, compareTo: String = ""): StudyReport Frames of a revision at seconds (a still scene is drawn once), with measurements, and optionally the same frames of compareTo and a difference map beneath.
fun timeline(id: String = "current", frames: Int = 8, size: Int = 0): StudyReport The whole clip: frames frames evenly across the duration, then a chart of how many rectangles are visible, how much of the wall they cover and how fast they move, over time — with the events the scene's analysis finds (entering, leaving, stopping, starting, touching), each named with the line that made the rectangle.
fun trace(id: String = "current", seconds: Double = 0.0, mode: String = "identify", size: Int = 0, probeX: Double = Double.NaN, probeY: Double = Double.NaN): StudyReport Which line made what. identify draws the frame at seconds with every rectangle outlined in its line's colour and labelled L<line>, and lists the lines; build shows the scene as it stands after each line that makes something, so a program reads as a sequence of pictures. With a wall point (probeX, probeY) it also says what is there at seconds: every rectangle covering it, topmost first, with the line that made it — the way to answer "what is this, and why is it here?".
fun lineNote(source: String, line: Int, seconds: Double): afield.ui.output.LineNotes.Note? What line of source shows, for the editor (afield.ui.output.LineNotes): the scene built up to the end of the statement the line is in — the program cut there, as Trace's build mode cuts it — at seconds, with the rectangles the line itself makes outlined. So a line reads as how far the scene has got, as a StackSL line reads as its stage. Null when the scene does not compile up to the line. Recent notes are kept.
fun knobs(id: String = "current", seconds: List<Double> = emptyList(), sensitivity: Boolean = true): StudyReport The knobs of a revision, and — unless sensitivity is false — how much a small nudge to each changes the frames.
fun sweep(id: String = "current", knob: Int, values: List<Double>, seconds: Double = 0.0, size: Int = 0): StudyReport One knob at several values, side by side at seconds. Nothing is published.
data class Variant(override val label: String, override val source: String = "", override val edits: List<StudySourceEdit> = emptyList(), override val note: String = "") : afield.study.source.StudyRequest.Variant A candidate for tryVariants: a complete program, or exact edits to the live source.
fun tryVariants(variants: List<Variant>, seconds: List<Double> = emptyList(), size: Int = 0): StudyReport Check and draw several candidates beside the current scene, and hold them — unpublished — for adoptVariant.
fun heldVariants(): List<Triple<String, String, String>> The candidates tryVariants is holding: label, note, source.
fun adoptVariant(expectedRevision: String, expectedSourceHash: String, label: String, description: String = ""): StudyReport Publish a held candidate.
fun compare(before: String, after: String = "current", seconds: List<Double> = emptyList(), size: Int = 0, mode: String = "rgb", region: List<Double>? = null): StudyReport Test what an edit preserved: pixels (rgb, or luma for brightness only), held to one level outside region (x0, y0, x1, y1 in wall units), and which rectangles appeared, went, or changed — by id, with the line that made them.
data class ExportJob(val id: String, val kind: String, val path: String, val revision: String, val frames: Int, val estimatedSeconds: Double = 0.0) One export running or finished in the background — see startExport.
fun exports(): List<Map<String, Any?>> Every export this session started, newest last.
fun startExport(id: String, expectedSourceHash: String, file: File, kind: String, size: Int = 0, seconds: Double = 0.0, loops: Int = 1, overwrite: Boolean = false, maxMinutes: Double = 10.0): StudyReport Export a checked revision in the background and return at once: png (one frame at seconds) or mp4 (the whole clip at the scene's fps, looped loops times), with an estimate made by timing one real frame at the export's size. An export estimated past maxMinutes is refused, with the size that would fit. Progress appears in exports, inspect and the card.
fun cancelExport(jobId: String): StudyReport Stop a running export after its current frame.
fun ffmpeg(): File? Where ffmpeg is, or null.
fun previewPng(id: String = "current", long: Int = 240, seconds: Double = 0.0): ByteArray A PNG of a revision at long px along its longer side and time seconds — what the card shows. Frames the GPU drew are cached; frames the CPU reference drew because the window was not up yet are not, so the card's next redraw, once it is, draws them on the GPU.

RectSLCheck

data class RectSLCheck(val wallWidth: Double, val wallHeight: Double, val unit: String, val origin: String, val duration: Double, val fps: Double, val background: String, val rects: Int, val groups: Int, val visibleAtProbes: List<Map<String, Any>>, val animated: Boolean, val materials: List<String>, val warnings: List<String>, val knobs: List<RectSLKnob>, val lines: List<RectSLLineReport>)

What the check found about an admitted scene.

Declaration Summary
val wallWidth: Double
val wallHeight: Double
val unit: String
val origin: String centre, +y up or top-left, +y down.
val duration: Double
val fps: Double
val background: String
val rects: Int
val groups: Int
val visibleAtProbes: List<Map<String, Any>> Rectangles on the wall at the check's probe times.
val animated: Boolean Whether anything changes over time: a track, or a material that reads time.
val materials: List<String>
val warnings: List<String> Material warnings and what the animation analysis noticed; kept, not refused.
val knobs: List<RectSLKnob>
val lines: List<RectSLLineReport> The lines that make rectangles or groups, with how many each makes.

RectSLLineReport

data class RectSLLineReport(val line: Int, val text: String, val comment: String?, val nodes: Int, val rects: Int, val ids: List<String>)

A line of the program that makes rectangles or groups.

RectSLEscapes

object RectSLEscapes

{{ … }} Kotlin escapes for study and runtime compiles: each distinct escape is compiled to its lambda once, on a dedicated Kotlin engine created on first use, and reused by every later compile. Not the box's own engine — a box engine busy running code would be re-entered — and never on Field's main thread, which a compile could block.

Declaration Summary
fun precompiled(source: String): Map<String, (RectEscapeContext) -> Any?> The lambdas for every escape in source, compiling the ones not seen before.

RectSLStudyRender.kt

Drawing a RectSL scene for a study, on the GPU in a running window (RectSLGpuRasterizer) or by the CPU reference (CpuRectSLRasterizer), through the one shader setup in RectSLStudyShader so that both give the same frames.

src/jvmMain/kotlin/afield/lang/rectsl/study/RectSLStudyRender.kt — read the source

RectSLStudyShader

object RectSLStudyShader

How a RectSL scene is drawn for a study, in the window and in every piece of evidence — the one place those two are made to agree.

Declaration Summary
val VERTEX: String The vertex shader: RectSL's own, unchanged.
val COMPOSITE_VERTEX: String A fullscreen triangle with fragUV running 0..1, y up — the window pass that shows the scene's target.
val COMPOSITE_FRAGMENT: String Shows the scene's 8-bit target in an sRGB window: each texel decoded to linear light, which the swapchain encodes back to the same byte. The target's first row is the top of the picture and fragUV.y grows upwards, hence the flip.
fun fragment(materials: List<StackSLRectMaterialCompileResult>): String The fragment shader for a scene with these compiled materials.
fun linear(v: Double): Double The sRGB transfer function's inverse: a drawn byte's value (0..1) as linear light.
fun mesh(scene: SceneProgram, time: Double, outlined: Set<String> = emptySet(), pixel: Double = 0.0): RectMesh scene at time, with its background as the first rectangle — the mesh every study draw writes. The rectangles named in outlined get an outline drawn over everything, two pixels wide (in wall units): the live window's way of showing which rectangles the editor's line makes.
fun visible(scene: SceneProgram, width: Int, height: Int): DoubleArray The part of the wall a width by height target shows, as its centre and size — cx, cy, w, h in wall units — under the scene's ->fit (see contain).
fun wallPoint(scene: SceneProgram, width: Int, height: Int, u: Double, v: Double): Vec2d The wall point under (u, v) of a width by height target, u across from the left and v down from the top, both 0..1: where a click in the window lands in the scene.
fun contain(scene: SceneProgram, width: Int, height: Int): Matrix4f The world-to-clip transform that fits the wall into a width by height target the way the scene's ->fit asks: contain (the default) shows the whole wall centred with black around it, cover fills the target and crops the wall, stretch fills it and distorts. When the aspects agree — every study frame — all three are exactly the wall.
fun frameSize(scene: SceneProgram, longSide: Int): Pair<Int, Int> A frame's pixel size: longSide along the wall's longer side, the other from its aspect, both even and at least 2.

RectSLDrawable

class RectSLDrawable(val source: String, val scene: SceneProgram, val materials: List<StackSLRectMaterialCompileResult>, val origins: Map<String, afield.lang.rectsl.RectSLOrigin> = emptyMap())

What a study draws: a compiled scene, the source it came from (the key a GPU pipeline is cached by), and its compiled materials.

Declaration Summary
val source: String
val scene: SceneProgram
val materials: List<StackSLRectMaterialCompileResult>
val origins: Map<String, afield.lang.rectsl.RectSLOrigin> Which line made each rectangle, by id — what the live window outlines and picks by.

RectSLRasterizer

typealias RectSLRasterizer = StudyFrames<RectSLDrawable>

Where a RectSL study's frames come from: the GPU when a window is running (RectSLGpuRasterizer), the CPU reference when it is not (CpuRectSLRasterizer). Both return linear colour, rows top to bottom, the wall filling the frame. A StudyFrames of a compiled scene.

CpuRectSLRasterizer

object CpuRectSLRasterizer : RectSLRasterizer

The CPU reference: the lowered mesh rasterized quad by quad at pixel centres, blended as the GPU blends into its 8-bit target (source alpha over, on the typed values), shaded as RectSLStudyShader.fragment shades — the three built-in materials in Kotlin, a StackSL material through the StackSL GLSL interpreter — and decoded to linear light at the end, as the GPU's bytes are. It needs no graphics context, so tests and headless runs see the scene the window shows, a few levels apart at rectangle edges (it samples one point per pixel, with a top-left fill rule like the GPU's).

Declaration Summary
override val description
override val fast
override fun render(input: RectSLDrawable, width: Int, height: Int, seconds: List<Double>, each: (index: Int, raster: CpuRaster) -> Unit)

RectSLGpuRasterizer

class RectSLGpuRasterizer(private val rendererOf: () -> Renderer?, private val fallback: RectSLRasterizer = CpuRectSLRasterizer, private val frameTimeoutMs: Long = 4000) : RectSLRasterizer

A study's frames from the GPU: the scene drawn by a real Field 4 graphics module — RectSLStudyShader's shaders, the scene's materials — into an offscreen 8-bit target on a running renderer, as the window draws it, and read back; the bytes are decoded to linear light (see RectSLStudyShader).

Declaration Summary
override var description: String
override val fast: Boolean
override fun render(input: RectSLDrawable, width: Int, height: Int, seconds: List<Double>, each: (index: Int, raster: CpuRaster) -> Unit)

RectSLStudyTools.kt

The tools an AI4 conversation gets when it is opened on a RectSL scene's rectSL property.

Each works on the focused box's own study (see RectSLPrograms), so nothing here is per document. A document can still add tools of its own from a box: AgentToolExtensions.register with its document as the scope.

src/jvmMain/kotlin/afield/lang/rectsl/study/RectSLStudyTools.kt — read the source

Used in: Talking a scene into existence

Top-level functions

Declaration Summary
fun installRectSLStudyTools() Register the RectSL study tools once, for every conversation opened on a scene's rectSL property.

RectSLStudyGuide

data class RectSLStudyGuide(@JsonPropertyDescription("A word or phrase to look up in the RectSL words guide; empty returns the working guide") val search: String = "")

The working guide for RectSL studies, or the words guide's entries matching a search term.

Declaration Summary
val search: String A word or phrase to look up in the words guide; empty returns the working guide.

InspectRectSLStudy

data class InspectRectSLStudy(override val revision: String = "current") : StudyRequest.Inspect

EditRectSLStudy

data class EditRectSLStudy(override val expectedRevision: String, override val expectedSourceHash: String, @JsonPropertyDescription("Complete RectSL source. Keep it human-editable: scene words first, one object per paragraph with a # comment saying what it is, named rectangles (\"sun\" rect), : name ... ; definitions for repeated motifs.") override val source: String, override val description: String, @JsonPropertyDescription("Times in seconds for the preview frames; empty chooses four evenly across the clip.") override val previewSeconds: List<Double> = emptyList()) : StudyRequest.Edit, StudyRequest.Preview

Declaration Summary
override val expectedRevision: String
override val expectedSourceHash: String
override val source: String
override val description: String
override val previewSeconds: List<Double>

PatchRectSLStudy

data class PatchRectSLStudy(override val expectedRevision: String, override val expectedSourceHash: String, override val edits: List<StudySourceEdit>, override val description: String, override val previewSeconds: List<Double> = emptyList()) : StudyRequest.Patch, StudyRequest.Preview

Declaration Summary
override val edits: List<StudySourceEdit>, override val description: String, override val previewSeconds: List<Double>

RectSLNumberEdit

data class RectSLNumberEdit(override val knob: Int, @JsonPropertyDescription("The new value; for a hex colour knob, the colour as a 24-bit number (0xff8800 is 16746496)") override val value: Double) : afield.study.ui.NumberEdit

One knob to set, by its index in the live source's knob list, and the value to write there.

Declaration Summary
override val knob: Int

SetRectSLNumbers

data class SetRectSLNumbers(override val expectedRevision: String, override val expectedSourceHash: String, override val numbers: List<RectSLNumberEdit>, override val description: String, override val previewSeconds: List<Double> = emptyList()) : StudyRequest.SetNumbers, StudyRequest.Preview

Declaration Summary
override val numbers: List<RectSLNumberEdit>, override val description: String, override val previewSeconds: List<Double>

ObserveRectSLStudy

data class ObserveRectSLStudy(override val revision: String = "current", override val seconds: List<Double> = emptyList(), @JsonPropertyDescription("Pixels along the wall's longer side, 48..1024; 0 chooses 512 for one frame and 320 for several") override val size: Int = 0, override val compareTo: String = "") : StudyRequest.Observe, StudyRequest.Sized

Declaration Summary
override val size: Int
override val compareTo: String

TimelineRectSLStudy

data class TimelineRectSLStudy(val revision: String = "current", @JsonPropertyDescription("How many frames, 2..16") val frames: Int = 8)

Declaration Summary
val frames: Int

TraceRectSLStudy

data class TraceRectSLStudy(val revision: String = "current", val seconds: Double = 0.0, val mode: String = "identify", @JsonPropertyDescription("x of a wall point to probe, in wall units (the frame's convention); omit for none") val probeX: Double? = null, @JsonPropertyDescription("y of a wall point to probe, in wall units; omit for none") val probeY: Double? = null)

Declaration Summary
val probeY: Double?

KnobsRectSLStudy

data class KnobsRectSLStudy(val revision: String = "current", val seconds: List<Double> = emptyList(), val sensitivity: Boolean = true)

SweepRectSLNumber

data class SweepRectSLNumber(override val revision: String = "current", override val knob: Int, override val values: List<Double>, override val seconds: Double = 0.0)

TryRectSLVariants

data class TryRectSLVariants(override val variants: List<RectSLStudy.Variant>, override val seconds: List<Double> = emptyList()) : StudyRequest.TryVariants

AdoptRectSLVariant

data class AdoptRectSLVariant(override val expectedRevision: String, override val expectedSourceHash: String, override val label: String, override val description: String = "")

RestoreRectSLRevision

data class RestoreRectSLRevision(override val expectedRevision: String, override val expectedSourceHash: String, override val revision: String, override val description: String = "")

CompareRectSLStudy

data class CompareRectSLStudy(override val beforeRevision: String, override val afterRevision: String = "current", override val seconds: List<Double> = emptyList(), override val mode: String = "rgb", override val allowedRegion: List<Double> = emptyList()) : StudyRequest.Compare

Declaration Summary
override val mode: String

ExportRectSLStudy

data class ExportRectSLStudy(val revision: String, val expectedSourceHash: String, val filename: String, val kind: String = "png", @JsonPropertyDescription("Pixels along the wall's longer side; 0 means 1920 for png and 1280 for mp4") val size: Int = 0, @JsonPropertyDescription("The still's time in seconds") val seconds: Double = 0.0, val loops: Int = 1, val overwrite: Boolean = false, @JsonPropertyDescription("Refuse an export estimated to take longer than this many minutes; the refusal names a size that fits. Raise it only if the person agrees to wait") val maxMinutes: Double = 10.0)

Declaration Summary
val size: Int
val seconds: Double
val loops: Int
val maxMinutes: Double

RectSLStudyGuides

object RectSLStudyGuides

The working guide and the words guide the RectSLStudyGuide tool serves.

Declaration Summary
val working: String by lazy How to work on a RectSL study.
val words: String by lazy Every RectSL word with its signature and a line of description, as Markdown sections.
fun search(query: String, limit: Int = 40): String The words guide's entries (lines, with their section heading) that mention query, at most limit of them.

RectSLGuideText

data class RectSLGuideText(val text: String) : afield.ai.tools.SummarisesForToolLog

The RectSLStudyGuide tool's answer.

Declaration Summary
override fun toolLogSummary(): String

RectSLStudyViewer.kt

The output card a person uses on a RectSL study: a filmstrip, a scrubbed window, knobs for the scene's numbers and colours, and the keep, adopt, restore and check-in actions (RectSLStudyViewer).

src/jvmMain/kotlin/afield/lang/rectsl/study/RectSLStudyViewer.kt — read the source

RectSLStudyViewer

class RectSLStudyViewer(val study: RectSLStudy, val owner: Box, val channel: String = RectSLPrograms.CHANNEL)

The person's side of a RectSL study: one output card that shows the scene over time, lets them turn its numbers and colours with the window following as they drag, and lets them keep, adopt, restore or check in — the same transactions the model's tools make, on the same study.

Declaration Summary
val study: RectSLStudy
val owner: Box
val channel: String
var drawnBy: String Who drew the card's frames last time — the rasterizer's description when it was built.
fun redrawIfDrawnWithoutWindow() Redraw the card if it was drawn without the window — at a document's opening the scene can run before its window's renderer exists — so its frames, and what it says drew them, become the GPU's. Once per such card.
fun refreshLater() Rebuild the card soon, on the card's own thread; repeated requests while one is pending collapse into it.
fun show() Rebuild the card and open its channel; from the main thread, hands the work to the card's thread (its frames wait on the GPU).
fun html(): String The card's HTML for the study's current state.

Links between the editor and a RectSL scene's window: the cursor's line is outlined in the window, and clicking a rectangle in the window puts the editor on the line that made it (RectSLWindowLinks).

src/jvmMain/kotlin/afield/lang/rectsl/study/RectSLWindowLinks.kt — read the source

object RectSLWindowLinks

The editor and a scene's window, each pointing at the other.

Declaration Summary
fun cursorLine(box: Box): Int? The editor's cursor line in box's rectSL, while the box is selected with that property showing; else null.
fun idsForLine(drawable: RectSLDrawable, line: Int): Set<String> The rectangles line of source makes, by id: those whose own word is on it and those of the statement that starts on it (a group's line outlines every part), with their copies — so "dot" rect … outlines the dots an array below it makes too — or, for a comment or a blank line, those of the statement just below.
fun baseId(id: String): String The id id was copied from, through any number of copies: dot-copy-3 and dot-copy-copy are dot's.
fun hit(scene: SceneProgram, time: Double, x: Double, y: Double): SampledRect? The topmost visible rectangle of scene at time under wall point (x, y), or null.
fun reveal(box: Box, line: Int) Select box and put its editor on line of rectSL, in every page open on its document.

Asta.kt

Source-to-source rewriting of a JavaScript box, over Nashorn's parse tree.

JavaScript has no operator overloading, so in a RemoteJS box a + b on two vectors concatenates their string forms and a - b is NaN. This pass rewrites the source before it ships: each binary +, -, *, / and ^ becomes a call to a global named for the operator, so a + b leaves here as __PLUS__(a,b). The head supplies those globals in field/asta.js, where __PLUS__, __MINUS__, __MULTIPLY__ and __DIVIDE__ work componentwise on THREE vectors, promote an array of two or three numbers to a vector first, let an operand join the protocol by defining its own __PLUS__ (or __RPLUS__, for the right-hand side), and throw a named error where plain JavaScript would hand back NaN or "[object Object]". XOR is in the trap set here and has no counterpart there.

executeRemoteJS runs the pass when its doASTA flag is set, which is the default. The Kotlin/JS path sets it false, because Kotlin has operator fun plus of its own and a compiled box needs no rewriting.

The pass also recovers a source location. Nashorn parses at ES6 while the browser is the authority on whether newer syntax is valid, so a diagnostic raised here is held rather than shown, and used only to fill in the line and column of a SyntaxError that the head reported without one; an error the head raises at run time keeps its own location. A parser AssertionError is caught, notified, and the source returned unrewritten, so a construct Nashorn cannot read costs the operators rather than the run.

Rewriting is textual, so offsets do the work, and Nashorn's need correcting before they can index the string. A string literal's quotes fall outside its reported extent, and a node that reports a narrower span than its own children is widened to cover them. Replacements run back-to-front so an earlier edit cannot move a later one, and the header of a for loop is excluded from rewriting entirely, because an edit inside it shifts the offsets of everything the loop spans. Field's //# sourceURL= trailer is split off before parsing and appended again afterwards.

Options gates the whole pass. overloads is on, and is the operator rewrite above; numbers is off, and rewrites each numeric literal to __NUMBER_LITERAL__(start, end, text), for which this tree carries no head-side definition; functionRewrite is off and has no effect beyond making the pass run at all. With all three off, parseAndReconstruct returns its input unchanged.

src/jvmMain/kotlin/afield/lang/remotejs/Asta.kt — read the source

Asta

class Asta

One rewrite pass over one fragment: set options, then call parseAndReconstruct.

Declaration Summary
val disabled
class Options
var options
var currentMapping
var lastReturn
var currentContents: String?
var baseContents: String?
val accessorMap: Map<Tree.Kind, List<Method>>
val trapKinds
fun parse(v: String)
var hash
fun parseAndReconstruct(v: String, d: (Diagnostic) -> Unit = { print("diagnostic: $it") }): String
fun options(): Options
var srcURL
fun trimSrcURL(src: String): Pair<String, String>

KotlinJs.kt

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.

src/jvmMain/kotlin/afield/lang/remotejs/KotlinJs.kt — read the source

Top-level functions

Declaration Summary
fun kotlinJsTopologyNeedsUpdating(values: Collection<Box>)
fun installKotlinJs()
fun installKotlinJsMainLanguage()

Top-level values

Declaration Summary
var Box.isKotlinJsRoot: Boolean?

KotlinJsExecution

class KotlinJsExecution : BoxExecution

Declaration Summary
override val languageName: String
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit)
override fun stop(inside: Box)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit)

KotlinJsBinding.kt

One source of truth for a Kotlin/JS façade surface.

A KotlinJS head profile needs the same API described twice: once as compiler-facing external declarations (the JS target the box compiles against) and once as a JVM-analysis-safe mirror (what the out-of-process K2 Analysis-API completion sidecar analyzes — it has no dynamic, and external declarations behave differently there). Maintaining those two by hand drifts — e.g. a property that's var in one and val in the other, or a member completion offers that compilation rejects.

So describe the surface ONCE with jsBinding and let JsBindingModel.compilerDeclarations and JsBindingModel.completionDeclarations render both. The two rendered forms differ only mechanically:

aspect compiler (external) completion (plain, JVM-analysis)
declaration external object/class plain object/class
member fun body none = TODO()
property init none = <init> (default TODO())
ctor param default = definedExternally = <real default>
supertype : Base : Base()
dynamic dynamic Any?
callback type () -> Unit Function0<Unit>
operator/fun body real body = TODO()

A field that is both a constructor argument and a mutable property (THREE's Vector3(x,y,z) with mutable .x/.y/.z, or Mesh(geometry, material) with mutable .geometry/.material) is one TypeBuilder.field call — it emits a plain ctor param plus a var property in BOTH renderings, so the two can never disagree on mutability or on whether the property exists.

src/jvmMain/kotlin/afield/lang/remotejs/KotlinJsBinding.kt — read the source

Top-level functions

Declaration Summary
fun jsBinding(build: BindingBuilder.() -> Unit): JsBindingModel

JsType

class JsType(val compiler: String, val completion: String = compiler)

A type reference that may render differently for the JS compiler vs the JVM completion session.

JsParam

class JsParam(val name: String, val type: JsType, val default: String? = null, val vararg: Boolean = false)

Declaration Summary
val name: String
val type: JsType
val default: String? Completion/real default; the compiler uses definedExternally inside external declarations.
val vararg: Boolean

JsProp

class JsProp(val name: String, val type: JsType, val mutable: Boolean, val completionInit: String = "TODO()")

JsFun

class JsFun(val name: String, val params: List<JsParam>, val ret: JsType, val compilerBody: String? = null, val external: Boolean = false, val operator: Boolean = false, val compilerOnly: Boolean = false)

Declaration Summary
val name: String
val params: List<JsParam>
val ret: JsType
val compilerBody: String? Real body for top-level funs / operators; member funs of external classes carry none.
val external: Boolean A top-level installed global (external fun, no body) rather than a wrapper with a body.
val operator: Boolean Member operator fun (e.g. a stage get/set indexer).
val compilerOnly: Boolean Rendered into the compiler façade ONLY (never the completion mirror). Used for raw runtime members like FLine's __PLUS__/__MINUS__ that a public operator delegates to but that should not surface as completion suggestions.

JsTypeDecl

class JsTypeDecl(val name: String, val isObject: Boolean, val open: Boolean, val constructible: Boolean, val superType: String?, val ctor: List<JsParam>, val props: List<JsProp>, val funs: List<JsFun>, val nested: List<JsTypeDecl>, val completionOnly: Boolean = false)

Declaration Summary
val name: String
val isObject: Boolean
val open: Boolean
val constructible: Boolean
val superType: String?
val ctor: List<JsParam>
val props: List<JsProp>
val funs: List<JsFun>
val nested: List<JsTypeDecl>
val completionOnly: Boolean Emitted only into the completion mirror — lets a value be dynamic for the JS compiler (maximally permissive) yet a real type for completion (so its members are suggested).

JsTopVal

class JsTopVal(val name: String, val type: JsType, val mutable: Boolean, val compilerGetter: String? = null)

Declaration Summary
val name: String
val type: JsType
val mutable: Boolean
val compilerGetter: String? external val (compiler) vs a computed get() = <compilerGetter> when compilerGetter is set.

JsExtFun

class JsExtFun(val receiver: String, val operator: Boolean, val name: String, val params: List<JsParam>, val ret: JsType, val compilerBody: String)

Declaration Summary
val receiver: String
val operator: Boolean
val name: String
val params: List<JsParam>
val ret: JsType
val compilerBody: String

JsBindingModel

class JsBindingModel(val types: List<JsTypeDecl>, val topVals: List<JsTopVal>, val topFuns: List<JsFun>, val extFuns: List<JsExtFun>, val rawCompiler: String)

Declaration Summary
val types: List<JsTypeDecl>
val topVals: List<JsTopVal>
val topFuns: List<JsFun>
val extFuns: List<JsExtFun>
val rawCompiler: String Compiler-only glue emitted verbatim (private @JsName externals the public funs delegate to).
val compilerDeclarations: String
val completionDeclarations: String

BindingBuilder

class BindingBuilder

Declaration Summary
fun obj(name: String, build: TypeBuilder.() -> Unit) external object Name { … } — a namespace like THREE.
fun cls(name: String, build: TypeBuilder.() -> Unit) A top-level external class Name { … } (e.g. FLine).
fun completionCls(name: String, build: TypeBuilder.() -> Unit) A class emitted ONLY into the completion mirror — used to give a dynamic compiler value (e.g. the stage proxy) real, suggestable members for completion without constraining the permissive JS-target type.
fun externalFun(name: String, ret: JsType = JsType.of("Unit"), build: ParamsBuilder.() -> Unit = {})+1 overload · also String An installed JS global function (external fun, no body).
fun externalVal(name: String, type: JsType, mutable: Boolean = false)+1 overload · also String
fun computedVal(name: String, type: String, compiler: String) A read-only alias resolved to compiler in the JS target (get() = compiler).
fun function(name: String, ret: JsType = JsType.of("Unit"), compilerBody: String, build: ParamsBuilder.() -> Unit = {})
fun operator(receiver: String, name: String, ret: String, compilerBody: String, build: ParamsBuilder.() -> Unit)
fun rawCompiler(text: String) Compiler-only glue (private @JsName externals). Not part of the completion surface.
fun build()

TypeBuilder

class TypeBuilder(private val name: String, private val isObject: Boolean = false, private val completionOnly: Boolean = false)

Declaration Summary
fun open()
fun privateConstructor() Model a runtime-owned singleton/result type that user code may receive but cannot construct.
fun extends(base: String)
fun ctor(build: ParamsBuilder.() -> Unit)
fun varProp(name: String, type: String, completionInit: String = "TODO()")+1 overload · also JsType Mutable property whose compiler/completion types differ (e.g. a dynamic ↔ typed value).
fun valProp(name: String, type: String, completionInit: String = "TODO()")+1 overload · also JsType Property whose compiler/completion types differ (e.g. dynamic proxy ↔ typed indexer).
fun field(name: String, type: String, default: String? = null) A mutable field that is BOTH a ctor argument and a property — the single call keeps the two renderings in lockstep (plain ctor param + var property in each).
fun fn(name: String, ret: String = "Unit", build: ParamsBuilder.() -> Unit = {})+1 overload · also JsType Member fun whose return type differs across façades (e.g. dynamic ↔ Any?).
fun operatorFn(name: String, ret: JsType, build: ParamsBuilder.() -> Unit = {}) A member operator fun (e.g. get/set indexers). Param/return types may be JsType.
fun compilerFn(name: String, ret: JsType, build: ParamsBuilder.() -> Unit = {})+1 overload · also String A member fun rendered into the COMPILER façade only — a raw runtime method (e.g. FLine's __PLUS__) that a public operator/extension delegates to, kept out of the completion surface.
fun cls(name: String, build: TypeBuilder.() -> Unit = {}) Nested class inside this object/class.
fun build()

ParamsBuilder

class ParamsBuilder

Declaration Summary
val params
fun param(name: String, type: String, default: String? = null)+1 overload · also JsType
fun vararg(name: String, type: String)

KotlinJsHeadProfiles.kt

Which remote head a Kotlin/JS box runs on, chosen by a marker on its first line.

A box whose main language is kotlinjs compiles to JavaScript and ships to a head over the RemoteJS transport, but the heads are not interchangeable: each has a different set of globals loaded, and Kotlin can only be typed against the globals its target actually has. A profile is that pairing. It carries the external declarations spliced in front of the author's source before compiling, a second surface used for completion, and the transport and lifecycle calls that put the compiled JavaScript in front of a head.

KotlinJsHeadProfiles.resolve reads a // {{ name }} marker on the first non-blank line and returns a KotlinJsHeadSelection. A name is matched case-insensitively against an id or an alias:

  • remote — the plain RemoteJS head; aliases default and remotejs, and what a box with no marker at all gets.
  • remoteAR — the A-Frame and THREE scene head; aliases ar, remote-ar, remotear.
  • tone — the Tone.js computer-music head; aliases tonejs and music.
  • visionmetal — the visionOS Metal head; aliases metal and vision-metal.

An unknown name raises instead of falling back, because a box compiled against the wrong globals type-checks and then fails in the head. The same reasoning gates delivery at run time: a head has to report the capability keys its profile requires in its initialize payload before it is counted or sent to, so // {{ tone }} reaches a head with Tone loaded and no other.

Two further things resolve does, both about line numbers. It blanks the marker's line rather than removing it, so the lines the compiler reports still line up with what the author sees. And resolveForBox re-reads the marker from the box's stored code when the source handed to it carries none, which is what fragment and symbol execution pass when only a selection is being run.

The two implementations behind the sealed class mean different things by sending. The WebSocket one hands its JavaScript to executeRemoteJS with doASTA = false, broadcasts to every connected head that matches, and reports results back through the box's output. The visionOS one publishes a Metal program on the box instead and reports success without waiting, because that publication is durable and replays for a native head that subscribes later. Its uniforms option is the only marker option here: slot order is what the head turns into #define u_NAME, so it is declared rather than inferred, and the inference that covers an absent declaration says what it guessed.

src/jvmMain/kotlin/afield/lang/remotejs/KotlinJsHeadProfiles.kt — read the source

KotlinJsHeadProfile

sealed class KotlinJsHeadProfile(val id: String, val declarations: String, val completionDeclarations: String, val aliases: Set<String> = emptySet(), val optionNames: Set<String> = emptySet())

A compiler surface plus the transport and lifecycle semantics of one remote JavaScript head.

Declaration Summary
val id: String
val declarations: String
val completionDeclarations: String
val aliases: Set<String>
val optionNames: Set<String>
abstract fun connectedHeadCount(): Int
abstract fun send(box: Box, selection: KotlinJsHeadSelection, javascript: String, launchable: Boolean, lifecycleName: String, onResponse: ((KotlinJsHeadResponse) -> Boolean)? = null): Boolean
abstract fun stop(box: Box, selection: KotlinJsHeadSelection, lifecycleNames: Collection<String>)
fun prepareSource(userSource: String): String

KotlinJsHeadResponse

data class KotlinJsHeadResponse(val kind: String, val headId: String, val expectedHeadIds: Set<String>, val terminal: Boolean = false)

Declaration Summary
val kind: String
val headId: String
val expectedHeadIds: Set<String>
val terminal: Boolean

KotlinJsHeadSelection

data class KotlinJsHeadSelection(val profile: KotlinJsHeadProfile, val source: String, val options: Map<String, String>, val explicit: Boolean)

Declaration Summary
val profile: KotlinJsHeadProfile
val source: String
val options: Map<String, String>
val explicit: Boolean
fun optionList(name: String): List<String>?

KotlinJsHeadProfiles

object KotlinJsHeadProfiles

Declaration Summary
val remote: KotlinJsHeadProfile
val remoteAr: KotlinJsHeadProfile
val tone: KotlinJsHeadProfile Tone.js computer-music head. Tone is installed as the global Tone by the RemoteAR head (index.html loads vendor/tone/…/Tone.js), so this profile shares that transport but requires the head to actually report the tone capability — a bound-but-absent name would compile then fail at runtime, so we route // {{ tone }} only to a head that has Tone loaded.
val visionMetal: KotlinJsHeadProfile
val all: List<KotlinJsHeadProfile>
fun resolve(source: String): KotlinJsHeadSelection Resolve only the first non-blank line. An absent marker preserves the original RemoteJS broadcast behavior; an unknown explicit marker is an error instead of silently using a head with the wrong globals.
fun resolveForBox(box: Box, source: String): KotlinJsHeadSelection Fragment/symbol execution often contains only the selected text; inherit the marker from code.

KotlinJsSourceMap.kt

The source map of a Kotlin→JS box's compiled module, remembered per box so that output arriving after the run can still be given a line.

The map is a by-product of the compile, and the output that needs it usually arrives once the run has finished: an error from a timer, a console.log from an event handler, each carrying a line of generated JavaScript and nothing else. RemoteJS.kt builds a map the moment KotlinToJsCompiler returns one and remembers it under the box id; KotlinJsHeadProfiles.kt asks for it when a reply needs a line; the next compile of that box replaces it.

None of this reports a failure to the box. A compile that produced no map, or a map whose mappings the decoder rejects, leaves the box with none, and its output keeps the generated line it came with. A position that maps somewhere other than the box's own Kotlin is a null from editorLine, and editorLineForStack goes on to the next frame of the stack.

src/jvmMain/kotlin/afield/lang/remotejs/KotlinJsSourceMap.kt — read the source

KotlinJsSourceMap

class KotlinJsSourceMap(json: String, private val userSourceSuffix: String, private val toEditorLine: (Int) -> Int)

Maps a line in the JavaScript a Kotlin→JS box compiled to back to the line in the box's editor.

Declaration Summary
fun editorLine(line: Int, column: Int = 1): Int? The one-based editor line for generated (one-based) line / column, or null when that position is not in the box's own Kotlin.
fun editorLineForStack(stack: String, codeName: String): Int? The editor line of the innermost frame of stack in codeName that is the box's own code.

KotlinToJsCompiler.kt

SPIKE — Kotlin source -> JavaScript, for RemoteJS boxes.

This mirrors what JetBrains' kotlin-compiler-server (KotlinToJSTranslator) does: a two-phase K2 compile of a self-contained Kotlin snippet into a single, eval-able JS module that the existing RemoteJS transport ships to the browser / visionOS "graphics head" over the WebSocket bus.

phase 1:  source.kt      --( -Xir-produce-klib-file )-->                 <module>.klib
phase 2:  <module>.klib  --( -Xir-produce-js, -Xinclude, -Xir-dce )-->   <module>.js

What this buys a box author is Kotlin: real types, completion, and native operator fun plus vector and matrix math, so a Kotlin box needs none of the operator rewriting Asta.kt does for a JavaScript one. The result runs unchanged in the head via executeRemoteJS with doASTA = false.

Output is a plain module that self-invokes main, so the head's global.eval(code) runs it with no changes. Dead-code elimination is what keeps that module small enough to ship on every compile: it tree-shakes the stdlib, taking a trivial program well under the ~600 KB an unshaken one contributes, and KotlinToJsSpikeTest holds the emitted JS under 50 000 characters.

Two execution modes (Mode): - IN_PROCESS: calls K2JSCompiler().exec(...) directly, with no JVM launch to pay for. The K2 REPL (KotlinInterface2) already holds a live IntelliJ application environment in this JVM and K2JSCompiler reuses the process-wide getOrCreateApplicationEnvironment; the two coexist, and a throw from that call falls back to SUBPROCESS rather than failing the compile. - SUBPROCESS: spawns java -cp <this app's classpath> K2JSCompiler. Fully isolated, and reuses the embeddable compiler already on the runtime classpath. Slower, by a cold JVM per compile.

Follow-up (see dev_docs/kotlin_to_js_spike.md): incremental compiles via JsIrCompilerWithIC, and a typed external façade over the head runtime (THREE / A-Frame / Field JS).

src/jvmMain/kotlin/afield/lang/remotejs/KotlinToJsCompiler.kt — read the source

KotlinToJsCompiler

object KotlinToJsCompiler

The compiler itself: compile is the entry point, Mode picks how it runs and Profile picks the shape of what comes out.

Declaration Summary
enum class Mode
enum class Profile Output shape. - PLAIN_DCE: one self-contained, tree-shaken plain module (~5 KB for a trivial program). Eval-able as-is; DCE runs the whole-program reachability pass on every compile. - ESM_SHARED: per-module ES output — a tiny box module (~2 KB) that imports from a kotlin-kotlin-stdlib.mjs shipped to the head once. No per-compile DCE over the stdlib.
data class Diagnostic(val severity: String, val message: String, val line: Int, val column: Int)
data class Result(val js: String?, val diagnostics: List<Diagnostic>, val success: Boolean, val mode: Mode, val rawLog: String, val sourceMap: String? = null)
fun compile(kotlinSource: String, moduleName: String = "field_box", mode: Mode = defaultMode, profile: Profile = Profile.PLAIN_DCE, cacheDir: File? = null, wrapImplicitMain: Boolean = true): Result Compile kotlinSource to a single plain-module, DCE'd JS string.
fun emitSharedStdlibMjs(@Suppress("UNUSED_PARAMETER") destDir: File): File OPEN BLOCKER — producing the shared kotlin-kotlin-stdlib.mjs for ESM_SHARED.

RemoteArKotlinJs.kt

The Kotlin/JS declarations a box sees when it runs Kotlin in the browser RemoteAR head.

RemoteArKotlinJs describes the THREE globals the extensions/remote_ar head installs (scene objects, vectors, FLine construction, the stage proxy and the animation task loop) with jsBinding. From that one description it produces RemoteArKotlinJs.declarations, the external declarations the Kotlin/JS compiler sees, and RemoteArKotlinJs.completionDeclarations, the stub model the completion sidecar analyses on the JVM. It also adds FLine operators (fline + rotate(0.5), fline * 2.0) and named scene slots (SCENEname= obj).

src/jvmMain/kotlin/afield/lang/remotejs/RemoteArKotlinJs.kt — read the source

RemoteArKotlinJs

object RemoteArKotlinJs

Typed Kotlin/JS view of the stable, useful core of the browser RemoteAR head.

Declaration Summary
const val surfaceId: String
val declarations: String Compiler-facing external declarations, generated from binding.
val completionDeclarations: String JVM-analysis-safe model used by the out-of-process completion sidecar, generated from binding.

RemoteJS.kt

RemoteJS — JavaScript that runs in a browser somewhere else, written in a Field box here.

The value proposition is not "Field can run JavaScript". It is that the box is on this machine and the execution is on a head: a browser tab, an Electron window, a phone, a Vision Pro. A head connects inbound over the WebSocket Field already has, announces an id and its capabilities, and from then on a box can be pointed at one head, at all of them, or at a filtered subset. knownOpenRemoteClients and remoteClientCapabilities are that registry, keyed by head id, and they are the reason nearly everything here takes a filterSession rather than a connection.

The one function to read

executeRemoteJS is the language. Its parameter list is the semantics, and three of them carry most of the weight:

  • doASTA routes the source through Asta first, which rewrites each binary operator into a call the head's asta.js defines, so vector arithmetic in a box means what it looks like. That pass also parses, and its diagnostics fill in the line and column of a SyntaxError the browser reported without one; an error raised at run time keeps its own location. Turn it off, as the Kotlin/JS path does, and a box gets neither.
  • lifecycleName and launchable are what make a fragment restartable and time-scrubbable rather than fire-and-forget, and timeStart/timeEnd are the window it is live for.
  • handleTo is the return channel: a head can send values back, dispatched through returnHandlers, so this is a duplex language and not a one-way pipe.

RemoteJSExectution is the BoxExecution for a box written in JavaScript; installRemoteJSMainLanguage registers it, and becomeRemote is the imperative version for a box that is being converted. Note the two ids: this file registers remotejs, and KotlinJs.kt beside it registers kotlinjs — the same transport, but the box is written in Kotlin and cross-compiled, with KotlinToJsCompiler.kt doing the compiling and afield.lang.remotejs.KotlinJsHeadProfile describing what each kind of head can be asked to do. Two main languages, one package.

dev_docs/small_language_anatomy.md is the map of the integration points every main language touches.

src/jvmMain/kotlin/afield/lang/remotejs/RemoteJS.kt — read the source

Top-level functions

Declaration Summary
fun topologyNeedsUpdating(values: Collection<Box>)
fun install()
fun installRemoteJSMainLanguage()
fun executeRemoteJS(_s: String, filename: String = "unknown", lifecycleName: String? = null, launchable: Boolean = false, requiresSandbox: Boolean = false, lineOffset: Int = 0, timeStart: Double? = null, timeEnd: Double? = null, doASTA: Boolean = true, __var0: Any? = null, filterSession: (String, WebSocketSession) -> Boolean = { _, _ -> true }, diagnosticsTo: (Diagnostic) -> Unit = { print("unhandled diagnostic output from asta\n $it") }, handleTo: ((JSONObject) -> Boolean)? = null): Boolean
fun executeFragment(b: Box, code: String, lineOffset: Int = 0, onComplete: (() -> Unit)? = null)
fun compileAndSendKotlin(b: Box, kotlinSource: String, launchable: Boolean = false, selectedHead: KotlinJsHeadSelection? = null, fragmentContext: KotlinJsFragmentContext? = null, lifecycleName: String = kotlinJsLifecycleName(b, "default"), onResponse: ((KotlinJsHeadResponse) -> Boolean)? = null, sendGate: ((() -> Boolean) -> Boolean)? = null): Boolean SPIKE — compile a Kotlin snippet to JS and ship it to the head, reusing the JS transport.
fun launchAll(b: Box, code: String, lineOffset: Int = 0)
fun target(box: Box): String
fun stop(b: Box)
fun complete(inside: Box, file: String, position: Int, success: (Completion) -> Unit)
fun becomeRemote(b: Box)

Top-level values

Declaration Summary
val knownOpenRemoteClients
val remoteClientCapabilities
val returnHandlers
var Box.isRemoteJSRoot: Boolean?
var Box.code_language: String
var lastBoxExec : Box?

KotlinJsFragmentContext

data class KotlinJsFragmentContext(val boxSource: String, val activeUserLines: IntRange)

Declaration Summary
val boxSource: String
val activeUserLines: IntRange One-based editor lines occupied by the selected fragment.

RemoteJSExectution

class RemoteJSExectution : BoxExecution

Declaration Summary
override val languageName: String
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code : String, strand: String, reply: (Message) -> Unit)
override fun stop(inside: Box)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit)

RemoteJsBoxSurface.kt

Where a message from a browser head lands when it names a box rather than a run.

A head reaches Field over the WebSocket Field already has, and every message it sends carries an address. RemoteJS.kt's dispatcher tries those addresses in turn and handle is its last claimant, so a false from here is what makes a message unrecognised. What reaches it is whatever the head has to say about a box while no reply channel is open: display, the slider and checkbox a box asked for, and output a timer or an event handler printed after the run returned. A Kotlin→JS box arrives here as well, and routeLateOutput maps its generated-JavaScript line back to the editor on the way through.

None of those messages carries a box, only the code name the head evaluated the source under. handle and routeLateOutput resolve it through boxFor, and take that lookup as a parameter so a test can supply a box directly.

In the other direction this file writes JavaScript as text for the transport to evaluate in the head: stopScript when a box is stopped, controlScript when a control drawn here is moved. dev_docs/REMOTEJS_BOX_PARITY.md measures the whole surface against a Kotlin box's.

src/jvmMain/kotlin/afield/lang/remotejs/RemoteJsBoxSurface.kt — read the source

RemoteJsBoxSurface

object RemoteJsBoxSurface

The parts of the box contract a RemoteJS box gets from messages its head sends outside a run's reply channel: display(channel, html), slider / checkbox, and output printed after the run finished (a timer, an event handler, an animation frame). See dev_docs/REMOTEJS_BOX_PARITY.md.

Declaration Summary
const val NO_HEAD The error a run gets when no browser head is connected to run it.
fun uidOf(codeName: String?): String? The box uid inside a target code name, or null for a name that is not a box's.
fun boxFor(codeName: String?): Box? The Box whose uid a target code name carries, looked for across the boxes of every open session, or null when none of them has it.
fun stopScript(codeName: String): String Javascript that stops every task the box started: its _r / generator task, named exactly codeName, and each launch(fn, name), named codeName + name. Stopping only the exact name left launch(...) loops running after Stop.
fun handle(o: JSONObject, find: (String?) -> Box? = ::boxFor, invokeControl: (id: String, value: Any) -> Unit = ::sendControlValue): Boolean Handles a head message this file owns; returns false for any other address. invokeControl sends a control's new value back to the heads (overridden by tests).
fun routeLateOutput(payload: JSONObject, find: (String?) -> Box? = ::boxFor): Boolean Output a box printed after its run's reply channel closed. The head tags every print (and routed console.log) with the box's code name; without this the JVM dropped it as an unknown address.
fun applyControls(box: Box, specs: List<JSONObject>, invokeControl: (String, Any) -> Unit) Draws the controls a head asked for on box, one controlGroup per channel, and hands each move to invokeControl. A spec whose kind is neither slider nor checkbox draws nothing, and the rest of its group is drawn as usual.
fun isTerminal(payload: JSONObject): Boolean Whether a reply ends a run: an error, a bare "OK", or the run's own value (the head marks that terminal). Before 2026-10-24 a run ending in a value never settled its receipt.
fun controlScript(id: String, value: Any): String Javascript that hands value to the head-side callback of control id.

RemoteLifecycleKotlinJs.kt

The onStart / onFrame / onStop / keepAlive callbacks that every remote Kotlin/JS head shares.

RemoteLifecycleKotlinJs supplies them, with the HeadFrame timing record, as compiler declarations and as a completion mirror. Callbacks register only during a Launch; a launched box stays active until relaunch, error or Stop, and its onStop callbacks then run once.

src/jvmMain/kotlin/afield/lang/remotejs/RemoteLifecycleKotlinJs.kt — read the source

RemoteLifecycleKotlinJs

object RemoteLifecycleKotlinJs

Head-neutral lifecycle surface shared by remote Kotlin/JS profiles.

Declaration Summary
val declarations: String
val completionDeclarations: String

PhaseNoise.kt

StackSL's phase noise family: qnoise, phnoise, phrot — noise whose phase is a control, per frequency.

What it is for

Two fields made from the same random kernels, differing only in phase, are related in a precise way that no amount of offsetting or reseeding gives you. Rotate every kernel's phase by θ turns and the new field correlates with the old one by cos(2πθ): 0 is the same field, 0.25 an uncorrelated partner with the same spectrum, 0.5 its negative. Rotate only the kernels above some frequency and the two fields share their coarse structure exactly while their fine detail decorrelates — stereo pairs, RGB channels or particle species that belong together, and noise that churns at small scales while its large forms hold still.

Value noise (noise, fbm, fbm3) cannot do this. It has no phase to rotate: shifting its output is a translation or an offset, not a phase change. These words are built from kernels that have one — localised sinusoids, which is sparse Gabor noise.

The construction

Each octave is a jittered lattice with one kernel per cell: a Gaussian window exp(-2 r²), truncated to zero at r = 1.5 cells, times a sinusoid with a random direction, a random phase, and a carrier of PhaseNoiseConstants.CARRIER cycles per cell, jittered by up to ±PhaseNoiseConstants.CARRIER_JITTER_OCTAVES/2 octaves per kernel so the spectrum is continuous across a band rather than a comb. The same kernels are summed twice, once through cos and once through sin — a quadrature pair — so the result is (Nc, Ns) and every phase rotation of it is Nc·cos θ − Ns·sin θ, exactly. Each octave is divided by the square root of its own window energy, which is independent of phase, so the identity survives normalisation.

Octave o is the lattice scaled by lacunarity^o and rotated once per octave by the fixed (PhaseNoiseConstants.OCTAVE_ROTATION_COS, PhaseNoiseConstants.OCTAVE_ROTATION_SIN) pair, so a kernel's frequency in cycles per unit of the input coordinate is its carrier times lacunarity^o. That number is what a phase response sees.

Output and range

vec4(Nc, Ns, |N|, 1): the quadrature pair, its envelope (phase-invariant, so still right after a phrot), and an opaque alpha. Nc and Ns are signed and zero-mean, with an RMS near PhaseNoiseConstants.OUTPUT_SCALE·0.7 ≈ 0.35, so they sit mostly inside -1..1; PhaseNoiseTest pins those statistics.

Cost

Sixteen kernels per octave, each a sin/cos pair, two hashes and an exp: five octaves is eighty kernels. That is more than fbm3 and less than curl3. The family is 2D over input.xy; input.w reaches a phase response as time.

Phase is in turns throughout, like phacelle's offset and unlike rotate's radians: 0.25 is a quarter cycle.

src/jvmMain/kotlin/afield/lang/stacksl/PhaseNoise.kt — read the source

PhaseNoiseConstants

object PhaseNoiseConstants

The fixed numbers of the phase noise construction, read by the emitted GLSL.

Declaration Summary
const val MAX_OCTAVES Loop bound in the emitted GLSL, shared with the volumetric family.
const val CARRIER Cycles of the carrier per lattice cell, before jitter. With a window of standard deviation half a cell the band is about ±0.32 cycles wide around this, so at lacunarity 2 consecutive octaves' bands meet rather than leaving gaps.
const val CARRIER_JITTER_OCTAVES Per-kernel carrier jitter, as a total width in octaves (±half of it).
const val WINDOW_FALLOFF The window exp(-WINDOW_FALLOFF r²).
const val WINDOW_FLOOR exp(-WINDOW_FALLOFF * 1.5²), subtracted from the window so it reaches zero at 1.5 cells. A 4x4 neighbourhood with jitter of ±half a cell is complete for that radius, so no kernel that should contribute is ever skipped and the field has no seams.
const val ENERGY_EPSILON Added under the energy square root so an octave never divides by zero.
const val OUTPUT_SCALE Final scale on the quadrature pair; see the class notes for the resulting RMS.
const val OCTAVE_ROTATION_COS Per-octave lattice rotation (cos, sin) — a 3-4-5 triangle, so it is exactly orthonormal.
const val OCTAVE_ROTATION_SIN
const val OCTAVE_OFFSET_X Translation after each octave's rotate-and-scale, so the lattices do not share an origin.
const val OCTAVE_OFFSET_Y
const val DEFAULT_OCTAVES
const val DEFAULT_LACUNARITY
const val DEFAULT_GAIN

StackSL.kt

StackSL — a concatenative shading language that compiles to GLSL, in one file with no runtime.

A StackSL program is a sequence of words over a vec4 stack, and it lowers to exactly one GLSL function: vec4 stack(vec4 stack_in), conventionally called with vec4(x, y, z_or_hash, time). There is no interpreter here and nothing executes on the JVM — StackSLCompiler.compile is a source-to-source translation, which is why the whole language is a compiler and a diagnostic type and nothing else.

The three values to know

  • StackSLCompileOptions decides the shape of the emitted GLSL rather than the meaning of the program: the symbol prefix, the entry point name, whether a header and samplers are declared, which textures sample may read, and — the one that changes semantics — defaultValues, the wrap/default convention used when a word wants an operand the stack does not have. tolerant decides whether that is a warning or an error.
  • StackSLCompileResult is GLSL, diagnostics and an analysis. ok is diagnostics with no ERROR, not "the GLSL is non-empty", because a tolerant compile emits usable GLSL and complains at the same time.
  • StackSLAnalysis is the part worth reading before debugging a program that produced a black frame. Per function it reports maxStackDepth, finalStackDepth, producedValues, usedValues and prunedValues, plus StackSLDanglingWire for each value that was pushed and never consumed. A definition whose arity does not work out is pruned silently in the emitted shader — the diagnostic and this analysis are the only places it says so.

compileStackSL and compileStackSLToGlsl are the two convenience entry points; the second throws away everything except the source, which is usually the wrong trade.

Where StackSL sits relative to its neighbours

afield.stdlib/stacksl.kt — on the Graphics page, because it is host API rather than language — is how a box actually reaches a StackSL string. afield.lang.rectsl compiles through this file: RectSL decides where rectangles are, StackSL decides how their materials shade, and RectSLGlsl.kt is the join. VolumetricNoise.kt beside this file is the noise vocabulary the emitted GLSL calls into.

StackSL cheatsheet is the word-by-word reference; StackSL examples is the gallery.

src/jvmMain/kotlin/afield/lang/stacksl/StackSL.kt — read the source

Used in: StackSL examples

Top-level functions

Declaration Summary
fun compileStackSL(source: String, options: StackSLCompileOptions = StackSLCompileOptions()): StackSLCompileResult
fun compileStackSLToGlsl(source: String, prefix: String = "stacksl", entrypoint: String = "main", sampleTextures: List<String> = listOf("source"), tolerant: Boolean = false, declareSamplers: Boolean = true, externalizeConstants: Boolean = false): String

Top-level values

Declaration Summary
val stackSLDocumentableWords: Set<String> The words a human is expected to look up: the literal vocabulary plus the named colors, but WITHOUT the mechanically generated surface — arity variants like fbm3.2 and the ~1100 swizzles — which would drown any documentation page that tried to list them.
val stackSLAllWords: Set<String> Everything the compiler will accept as a word, including the mechanically generated arity variants and swizzles. Used by the coverage guard's reverse direction, where a cheatsheet row naming rgba or .xy is legitimate and must not be reported as a typo.

StackSLSeverity

enum class StackSLSeverity

Constants: ERROR, WARNING, INFO.

StackSLSourceLocation

data class StackSLSourceLocation(val line: Int, val column: Int, val index: Int)

Declaration Summary
val line: Int
val column: Int
val index: Int
override fun toString(): String

StackSLDiagnostic

data class StackSLDiagnostic(val severity: StackSLSeverity, val message: String, val location: StackSLSourceLocation? = null, val token: String? = null)

Declaration Summary
val severity: StackSLSeverity
val message: String
val location: StackSLSourceLocation?
val token: String?
override fun toString(): String

StackSLCompileOptions

data class StackSLCompileOptions(val prefix: String = "stacksl", val entrypoint: String = "main", val tolerant: Boolean = false, val includeHeader: Boolean = true, val declareSamplers: Boolean = true, val sampleTextures: List<String> = listOf("source"), val externalizeConstants: Boolean = false, val defaultValues: List<String> = listOf("input", "vec4(0.0)", "vec4(1.0)", "vec4(0.5, 0.5, 0.5, 1.0)"), val useEntrypointAsRoot: Boolean = false)

Declaration Summary
val prefix: String
val entrypoint: String
val tolerant: Boolean
val includeHeader: Boolean
val declareSamplers: Boolean
val sampleTextures: List<String>
val externalizeConstants: Boolean
val defaultValues: List<String>
val useEntrypointAsRoot: Boolean

StackSLFunctionAnalysis

data class StackSLFunctionAnalysis(val name: String, val emittedName: String, val tokenCount: Int, val producedValues: Int, val usedValues: Int, val prunedValues: Int, val maxStackDepth: Int, val finalStackDepth: Int, val calls: Set<String>, val channelWrites: Set<Int> = emptySet())

Declaration Summary
val name: String
val emittedName: String
val tokenCount: Int
val producedValues: Int
val usedValues: Int
val prunedValues: Int
val maxStackDepth: Int
val finalStackDepth: Int
val calls: Set<String>
val channelWrites: Set<Int>

StackSLDanglingWire

data class StackSLDanglingWire(val index: Int, val initial: String, val line: Int? = null, val column: Int? = null, val token: String? = null, val note: String = "constant", val consumedAs: String? = null)

Declaration Summary
val index: Int
val initial: String
val line: Int?
val column: Int?
val token: String?
val note: String
val consumedAs: String? The operand the value holding this wire was first taken as — the word's name for it, such as "mix amount", "smoothstep high" or "multiply b" — or null when nothing took it (a number left on the stack as the result).

StackSLAnalysis

data class StackSLAnalysis(val prefix: String, val entrypoint: String, val functions: List<StackSLFunctionAnalysis>, val requiredHelpers: Set<String>, val requiredSamplers: Set<String>, val danglingWires: List<StackSLDanglingWire> = emptyList())

Declaration Summary
val prefix: String
val entrypoint: String
val functions: List<StackSLFunctionAnalysis>
val requiredHelpers: Set<String>
val requiredSamplers: Set<String>
val danglingWires: List<StackSLDanglingWire>
val prunedValues: Int

StackSLCompileResult

data class StackSLCompileResult(val glsl: String, val diagnostics: List<StackSLDiagnostic>, val analysis: StackSLAnalysis)

Declaration Summary
val glsl: String
val diagnostics: List<StackSLDiagnostic>
val analysis: StackSLAnalysis
val ok: Boolean

StackSLCompiler

object StackSLCompiler

Declaration Summary
fun compile(source: String, options: StackSLCompileOptions = StackSLCompileOptions()): StackSLCompileResult
fun outline(source: String): StackSLOutline source's words and definitions, as the tokenizer and parser behind compile see them, with nothing compiled.

StackSLWord

data class StackSLWord(val text: String, val location: StackSLSourceLocation)

One word of a StackSL program: its text exactly as written, and where it starts.

Declaration Summary
val range: IntRange The characters of the source this word occupies.

StackSLOutlineDefinition

data class StackSLOutlineDefinition(val name: String, val location: StackSLSourceLocation, val body: List<StackSLWord>)

A : name … ; definition: its name, where that name is written, and the words of its body.

Declaration Summary
val name: String
val location: StackSLSourceLocation
val body: List<StackSLWord>

StackSLOutline

data class StackSLOutline(val root: List<StackSLWord>, val definitions: List<StackSLOutlineDefinition>)

A StackSL program's shape: the root words in order, and the definitions they may call.

Declaration Summary
fun rootPrefix(source: String, count: Int): String source with every root word after the first count blanked out. source must be the text this outline came from.
fun storedMark(count: Int): String? The mark the root's word number count (1-based) stores into, if it is a move into a local mark (>name) — null for any other word, including the lane words >x >y >z >w.
fun stageProgram(source: String, count: Int): String What the first count root words produce, as a program: rootPrefix, and when the last of those words moves a value into a mark, that mark read back. A line ending in >name leaves only the program's input on top of the stack, which says nothing about the line; the value it stored does.

StackSLVocabulary.kt

The words of a small language as an editor offers them: names, stack effects and descriptions read from the language's generated words guide, served as completions.

vocabularyFromGuide reads the guide's tables, vocabularyCompletion offers the words at a cursor, and StackSLVocabulary is StackSL's own list. StackSLVocabularyCoverageTest and LanguageVocabularyTest cover it.

src/jvmMain/kotlin/afield/lang/stacksl/StackSLVocabulary.kt — read the source

Top-level functions

Declaration Summary
fun vocabularyFromGuide(words: Collection<String>, guide: String): List<VocabularyWord> words, each with what guide — a words guide generated from a language's cheatsheet — says it does, alphabetically. A table whose header has a Stack, Pushes, Reads or Stack / reads column gives the effect there, and its last column is the description; a row naming several words (`sat`, `saturate`) describes each of them. A word the guide does not describe is kept, with nothing said about it.
fun vocabularyCompletion(source: String, position: Int, language: String, vocabulary: List<VocabularyWord>, ownWords: List<Metadata>): Completion What Field's editor offers at position in source, a program in language: every word of vocabulary, and every name the program gives itself (ownWords), that begins with the word being typed — which runs back from position to whitespace or a bracket. Nothing inside a # comment or a string. Each row carries the word's stack effect as its tail and the cheatsheet's description.

VocabularyWord

data class VocabularyWord(val word: String, val effect: String, val detail: String)

One word of a small language as an editor offers it: effect as its cheatsheet writes it (t a b c d -- color), and detail the cheatsheet's prose, as Markdown. Either may be empty.

StackSLVocabulary

object StackSLVocabulary

Every word the StackSL compiler accepts that a person would type, with what the cheatsheet says it does — the vocabulary the editor offers in completion and explains beside the cursor.

Declaration Summary
val words: List<VocabularyWord> by lazy Every documentable word, alphabetically.
fun complete(source: String, position: Int): Completion Completion at position in the StackSL program source: the vocabulary, the program's own : name definitions, and <name for every mark it stores with >name.

VolumetricNoise.kt

StackSL's volumetric noise family: noise3, fbm3, fbmd3, warp3, curl3, worley3, noise4, fbm4 — eight words that work as true 3D and 4D fields.

Why a separate family

StackSL's original noise and fbm are two-dimensional. The word lowers to _noise(input.xy + input.zw * 0.07), so a vec4 carrying a position and a time is squashed into a plane: z enters at one fourteenth the weight of x, as a shear rather than as a third dimension. On a volume seen down z that reads as one fixed pattern imprinted at every depth. The two-slice-sum workaround (f(x,y) + f(y,z) + f(z,x)) is what people write instead, and it costs three fbms and does not give a 3D field — it gives three sheared 2D ones.

Three properties the 2D words lack

The range really is [0,1). _fbm(vec2) sums five octaves at amplitudes 0.5 0.25 0.125 0.0625 0.03125, which total 0.96875, not 1 — so the usual 0.5 - recentering leaves a constant bias of about -0.031. A constant force integrates into a drift instead of averaging out. Here the amplitude sum is accumulated at runtime and divided out, so the range is exactly [0,1) for any octave count, lacunarity and gain.

The hashes contain no sin(). _hash11(float) uses fract(sin(p * 127.1) * 43758.5453123), and sin is not bit-portable: its precision is implementation-defined, it loses all meaning for large arguments, and no CPU implementation can be trusted to agree with a GPU's. _hash21(vec2) — the hash that actually feeds _noise and _fbm — has no sin at all; it is fract(p * vec2(123.34, 456.21)), p += dot(p, p + 45.32), fract(p.x * p.y), so it is already multiply/add/fract and already IEEE-754 exact. (Exact is not the same as stable: fract of a large product amplifies a last-bit difference in its argument to O(1), which is why an exactly-specified noise can still disagree visibly across backends.) The reason this family could not use _hash21 is dimensionality: _hash21 takes a vec2, and a 3D/4D family needs a hash over 3 and 4 inputs. The hashes below are multiply/add/fract only, which makes VolumetricNoiseReference a real reference rather than an approximation. StackSLCpuVolumetricCrossCheckTest evaluates the emitted GLSL against every function here, bit-for-bit.

curl3 is divergence-free by construction. It is the curl of a vector potential taken with central differences at one shared epsilon. Discrete shift operators commute, so the discrete divergence of the discrete curl cancels term for term and is zero up to float rounding — see VolumetricNoiseTest. Particles advected through it swirl instead of piling into sinks.

Constants

VolumetricNoiseConstants is the single source of truth for the family's shared constants — the octave defaults, lacunarity, gain, the finite-difference epsilons, the hash multipliers and the per-octave rotation. The Kotlin reference reads them and the emitted GLSL interpolates them, so the two cannot drift apart. VolumetricNoiseTest's every shared constant appears in the emitted GLSL fails if one is ever typed into the shader text by hand. The algebraic coefficients inside VolumetricNoiseReference — the mix weights, the smoothstep polynomial — are spelled out at their call sites instead, because they are the definition of the operation rather than a tunable value.

src/jvmMain/kotlin/afield/lang/stacksl/VolumetricNoise.kt — read the source

NoiseVec3

data class NoiseVec3(val x: Float, val y: Float, val z: Float)

A float triple. Float rather than Double, because the GLSL this mirrors works in float.

Declaration Summary
operator fun plus(o: NoiseVec3)
operator fun minus(o: NoiseVec3)
operator fun times(s: Float)
val length: Float

NoiseVec4

data class NoiseVec4(val x: Float, val y: Float, val z: Float, val w: Float)

A float quadruple, for the 4D words and for the packed worley3 result.

VolumetricNoiseConstants

object VolumetricNoiseConstants

Declaration Summary
const val MAX_OCTAVES Loop bound in the emitted GLSL. An octave request above this is clamped, not an error.
const val DEFAULT_OCTAVES
const val DEFAULT_CURL_OCTAVES curl3 defaults to FEWER octaves than everything else, and the reason is visible the moment you render it.
const val DEFAULT_LACUNARITY
const val DEFAULT_GAIN
const val DEFAULT_CURL_EPSILON curl3's finite-difference step, in the same units as the input coordinate. Large enough that (f(p+e) - f(p-e)) / 2e is not dominated by float cancellation, small enough that the stencil still resolves the finest octave. At the default lacunarity the fifth octave has a feature size around 1/16, so 0.05 straddles roughly a third of it.
const val DEFAULT_GRADIENT_EPSILON fbmd3's gradient step. Same trade-off, one field rather than three.
const val DEFAULT_WARP_STRENGTH warp3's displacement strength, in input units.
const val HASH_SEED Added before every hash so the origin is not a fixed point. Without it hash31(0,0,0) is exactly 0 — fract(0 * 0.1031) is 0, the dot product of zero with anything is 0, and fract((0 + 0) * 0) is 0 — which puts a black dot on the origin of every field. The offset is non-integral, so no lattice corner can ever land on the new fixed point at -HASH_SEED.
const val HASH3_SCALE_X
const val HASH3_SCALE_Y
const val HASH3_SCALE_Z
const val HASH4_SCALE_W
const val HASH31_ADD
const val HASH33_ADD
const val HASH41_ADD
val OCTAVE_ROTATION_COLUMNS: Array<NoiseVec3> Per-octave rotation. Orthonormal (asserted in the tests), so it changes the direction of the lattice without changing the scale — the lacunarity does all the scaling. Without a rotation, every octave's lattice lines up with the last and the sum shows axis-aligned grid streaks. Stored as COLUMNS, which is what GLSL's mat3(...) constructor takes.
val OCTAVE_OFFSET Translation applied after each octave's rotate-and-scale, to decorrelate the lattices.
const val OCTAVE_OFFSET_W
val FIELD_OFFSET_A The three offsets that turn one scalar fbm into a vector potential for curl3, and that separate warp3's three displacement fields. Far apart and non-integral, so the three components are decorrelated samples of the same field rather than three views of one blob.
val FIELD_OFFSET_B
val FIELD_OFFSET_C
fun allScalars(): Map<String, Float> Every float literal that must appear in the emitted GLSL, for the anti-drift test.

VolumetricNoiseReference

object VolumetricNoiseReference

The CPU twin of the GLSL emitted by volumetricNoiseHelperSource. Every function here is a statement-for-statement transliteration of its shader counterpart, in Float, using only operations GLSL and the JVM both round identically. It exists so the family's semantics — ranges, continuity, worley3's F1 <= F2, curl3's divergence — can be asserted without a device, which is the only kind of test StackSL has ever been able to have.

Declaration Summary
fun hash31(px: Float, py: Float, pz: Float): Float vec3 -> [0,1). Multiply/add/fract only; see the header on why that matters.
fun hash33(px: Float, py: Float, pz: Float): NoiseVec3 vec3 -> three independent values in [0,1), for worley3's feature-point jitter.
fun hash41(px: Float, py: Float, pz: Float, pw: Float): Float vec4 -> [0,1).
fun noise3(p: NoiseVec3): Float Trilinearly interpolated value noise on the integer lattice, smoothstep-faded. The result is a convex combination of eight hashes each in [0,1), so it is in [0,1) exactly — no scaling, no bias, nothing to get wrong.
fun noise4(p: NoiseVec4): Float The 4D lattice, 16 corners. w is the animation axis when you want a volume that moves.
fun rotateOctave(p: NoiseVec3): NoiseVec3 OCTAVE_ROTATION_COLUMNS applied as GLSL applies a mat3: sum of columns times components.
fun fbm3(p: NoiseVec3, octaves: Float = K.DEFAULT_OCTAVES, lacunarity: Float = K.DEFAULT_LACUNARITY, gain: Float = K.DEFAULT_GAIN): Float Fractal sum. The amplitude sum is accumulated into norm and divided out at the end, so the range is exactly [0,1) whatever the octave count, lacunarity or gain — this is the structural fix for the 2D fbm's 0.96875 ceiling.
fun fbm4(p: NoiseVec4, octaves: Float = K.DEFAULT_OCTAVES, lacunarity: Float = K.DEFAULT_LACUNARITY, gain: Float = K.DEFAULT_GAIN): Float
fun octaveCount(octaves: Float): Int GLSL clamps then truncates; mirror that exactly rather than rounding.
fun potential3(p: NoiseVec3, octaves: Float, lacunarity: Float, gain: Float) The vector potential curl3 differentiates: one scalar fbm read at three widely separated offsets. Using one field three times rather than three fields is deliberate — it is cheaper, and the offsets are far enough apart that the components are uncorrelated.
fun curl3(p: NoiseVec3, epsilon: Float = K.DEFAULT_CURL_EPSILON, octaves: Float = K.DEFAULT_CURL_OCTAVES, lacunarity: Float = K.DEFAULT_LACUNARITY, gain: Float = K.DEFAULT_GAIN): NoiseVec3 Curl of potential3, central differences at one shared epsilon.
fun worley3(p: NoiseVec3): NoiseVec4 Cellular noise over a jittered lattice, packed so that the useful channels are all present at once: .x is F1, the distance to the nearest feature point; .y is F2, the second nearest; .z is F2 - F1, which is near zero exactly on the cell walls and is the crack or Voronoi-edge field; .w is a per-cell random constant, for flat-shading the cells.
fun fbmd3(p: NoiseVec3, epsilon: Float = K.DEFAULT_GRADIENT_EPSILON, octaves: Float = K.DEFAULT_OCTAVES, lacunarity: Float = K.DEFAULT_LACUNARITY, gain: Float = K.DEFAULT_GAIN): NoiseVec4 fbm3 and its gradient in one word: .x is the value, .yzw the gradient. The gradient of a scalar field is a normal — of an isosurface, of a cloud edge — so this is the word to reach for when shading one, rather than differencing fbm3 by hand.
fun warp3(p: NoiseVec3, strength: Float = K.DEFAULT_WARP_STRENGTH, octaves: Float = K.DEFAULT_OCTAVES, lacunarity: Float = K.DEFAULT_LACUNARITY, gain: Float = K.DEFAULT_GAIN): Float Domain warping: fbm3 evaluated at a position that has itself been displaced by an fbm3 vector field. One line of arithmetic that turns the characteristic cloudy fbm into something with filaments and folds. Still in [0,1), because the outer call is still fbm3.

GlslAd.kt

Reverse-mode automatic differentiation for the GLSL interpreter: d(pixel)/d(literal).

GlslAdScalars is a GlslScalars instance, so a shader differentiated here runs through the interpreter's one copy of the control flow, over the same parse tree the Float reference path walks. Nothing about a GLSL builtin's meaning is restated; what is added is a local partial beside each operation.

Why reverse mode

An image loss over a wire vector is many-inputs-one-output, which is reverse mode's shape: one forward pass plus one backward sweep answers for every wire at once, so the cost does not grow with the number of free parameters. The loss is a sum over pixels, so the gradient is a sum of per-pixel backward sweeps, one short tape per pixel, thrown away before the next. That bounds the tape: a 64x64 fit never holds more than one pixel's operations at a time, where a whole-image tape would hold 4096 times as many.

The primal is bit-identical to the Float path

AD computes in Double, and a Double shader is the wrong shader. GlslValue.kt's overview sets out why: widening an intermediate makes the CPU result more accurate than the GPU's and therefore a worse reference, and fract of a large product amplifies a last-bit difference to O(1). On a hash-bearing program a Double primal would not be slightly different, it would be a different image.

So every node this file pushes stores its value rounded to Float, at exactly the points the Float path rounds. Double rounding is safe for + - * / and sqrt: Double has more than twice Float's significand, so rounding a Double-exact sum to Float gives the same answer as the Float operation. The differentiable pass's primal image is therefore the same image the reference path produces, which StackSLGradientTest asserts bit for bit over a named population, while the adjoint is the derivative of the smooth relaxation, which is the quantity a fit needs.

Derivative conventions

  • a branch, a loop bound and an index carry no derivative: differentiated as taken, which is correct for the piecewise function away from the switch-over. autodiff.kt says the same.
  • floor, ceil, trunc, round, roundEven, sign, step and int(...) truncation carry exactly zero derivative. fract therefore carries derivative one, since it is x - floor(x).
  • abs, min and max split evenly at an exact tie, which is what a central difference reports at a kink, so the analytic gradient and the finite-difference oracle agree there.
  • a comparison, any, all, isnan and isinf produce a derivative-free bool.
  • texture() is refused. A bilinear CPU texture fetch is differentiable in principle, but the two feedback gallery programs are already excluded from every asserted comparison in this backend, so a derivative through a sampler would have no oracle to check it against. StackSLDifferentiabilityTest counts it as unsupported rather than as zero.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/GlslAd.kt — read the source

GlslAdSurrogate.kt

The surrogate vocabulary a differentiable StackSL fit descends through: a mollified delta, the jump families it may be applied to, and the schedule that narrows it.

Three types. AdSurrogate is a temperature plus a set of families — the one object GlslAdScalars consults at each jump site. AdSurrogateFamily partitions the jump-discontinuous builtins by where their jumps are rather than by name, because that is what decides the partial, and because floor is not one thing: as a quantiser it is a wall worth softening, and as a noise lattice's cell index its zero is the true derivative and softening it destroys a wire that already worked. AdAnnealing is the schedule, and the measurements say a fit does not need it.

Nothing here is on unless a caller asks: AdSurrogate.OFF is the default everywhere, and even switched on it replaces only local partials and never a primal value. The measurements — including the two that argue against enabling it globally — are dev_docs/STACKSL_SURROGATE.md.

What is not here is GlslAdScalars' mask tracking, which is what stops a nested predicate being charged a second kernel. That is a property of the AD path rather than of the surrogate, so it sits in GlslAd.kt beside the jump sites it corrects.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/GlslAdSurrogate.kt — read the source

AdSurrogate

class AdSurrogate private constructor(val temperature: Double, val families: Set<AdSurrogateFamily>)

A differentiable surrogate for the jump-discontinuous GLSL operations, as a mode of the one AD evaluation rather than a second one.

Declaration Summary
val temperature: Double The bump width, in the units of the quantity being floored. 0.0 means off.
val families: Set<AdSurrogateFamily> Which jump families get a surrogate partial; the rest keep their exact zero.
val enabled: Boolean True when any partial in this object differs from the exact zero.
fun covers(family: AdSurrogateFamily): Boolean Is family surrogated here?
fun at(temperature: Double): AdSurrogate The same families at a different temperature — what an annealing schedule steps.
fun kernel(u: Double, jump: Double): Double The mollified delta: jump · sech²(u / 2t) / 4t, with u the signed distance to the jump.
val unitPeriodMass: Double The fraction of a unit-period jump this kernel actually carries: tanh(1 / 4t).
override fun toString(): String
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

AdSurrogateFamily

enum class AdSurrogateFamily

The three jump families, split by where the jumps are rather than by builtin name — because that is what decides the surrogate partial, and two builtins with the same jump lattice want the same treatment.

Constants: STAIRCASE, STEP, SIGN.

AdAnnealing

class AdAnnealing(val start: Double, val end: Double, val budget: Int, val families: Set<AdSurrogateFamily>)

A geometric annealing schedule from start down to end, plus the driver that steps it.

Declaration Summary
val start: Double The temperature at the first evaluation.
val end: Double The temperature at the last evaluation of the budget.
val budget: Int The evaluation count the schedule is scaled to — normally the iteration count.
val families: Set<AdSurrogateFamily> Which families the schedule anneals.
fun temperatureAt(index: Int): Double The temperature at evaluation index, counting from zero. Pure — no state is touched.
fun next(): AdSurrogate The surrogate for the next evaluation, and advance. Call once per loss evaluation.
val used: Int How many times next has been called.
override fun toString(): String

GlslInterpreter.kt

The evaluator for parsed GLSL shaders, tree-walking and single-threaded per invocation.

It holds no state between invocations except the globals the caller supplies, so a raster can be rendered by running one GlslInvocation per row across as many threads as there are cores — see StackSLCpu.

Three design choices

Control flow as a return value, not an exception. Each statement reports what it did to control flow — normal, break, continue, return or discard — as an ordinary return value, and the caller unwinds. The tree-walking pattern normally uses a control-flow exception, but the volumes here are large: _fbm returns from inside a five-iteration loop, and a 512×512 render evaluates that return 1.3 million times per program, which is too many to assume a stack-trace-bearing exception is free.

One flat frame per function call, with block scopes as marks into it. Entering a block records the current top of the frame and leaving truncates back to it, giving correct GLSL shadowing with no per-block allocation. Lookup is a backwards linear scan, which for the emitted subset (never more than about a dozen live names) beats a hash map.

Generic in its scalar, with exactly one copy of the control flow. GlslMachine holds the statements, the branches, the loops, the frames and the lvalue writes; what a value is comes from a GlslScalars. GlslScalars<GlslValue> is the binary32 reference path and GlslScalars<AdValue> makes the same shader differentiable, so d(pixel)/d(literal) flows through this file rather than through a second evaluator — which would agree with itself while both copies drifted from the spec. GlslInvocation is the Float face of it, and is what every pre-existing caller uses, unchanged.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/GlslInterpreter.kt — read the source

Top-level functions

Declaration Summary
fun GlslShader.newInvocation(initialGlobals: Map<String, GlslValue> = emptyMap(), samplers: GlslSamplers = GlslSamplers.REFUSE): GlslInvocation Prepare a shader for repeated invocation over binary32 floats.

GlslInvocation

class GlslInvocation internal constructor(internal val machine: GlslMachine<GlslValue>)

One invocation of a shader over binary32 floats — the CPU backend's reference path.

Declaration Summary
val discarded: Boolean True when this invocation executed discard. The caller decides what that means.
fun global(name: String): GlslValue? Read a global — outColor after running main, most usefully.
fun setGlobal(name: String, value: GlslValue) Set a global before running, or between runs of the same invocation.
fun call(name: String, args: List<GlslValue>): GlslValue Call a function by name with already-evaluated arguments.
fun runMain(): Boolean Run main(). Returns whether it completed without discard.

GlslInterval.kt

Interval arithmetic for the GLSL interpreter: a shader run over a whole region of fragUV at once, returning a bound on every colour any pixel in it can produce.

GlslIntervalScalars is the third GlslScalars instance, beside the binary32 reference and the reverse-mode one, so it runs the emitted GLSL through the interpreter's one copy of the control flow. Nothing about a StackSL word is restated here.

What is contained

The contract is the one afield.math.functions.FnInterval states for Fn, moved to binary32: for every input inside the intervals, the value the Float reference path computes lies inside the result (or is NaN, and the result says NaN is possible). Each operation is bounded by FnRangeMath in Double and the endpoints rounded outward to Float. That is sound for the correctly rounded operations because rounding to nearest is monotone, and because a Double product or sum of two floats rounds to Float the way the Float operation does (the argument GlslAd.kt makes for its primal). The library transcendentals are already widened by FnRangeMath, and the Float path rounds their Double result to nearest, which is monotone too.

The composite builtins (mod, fract, mix, smoothstep, dot, length) are bounded as the Float path spells them, operation by operation, so the bound is of that arithmetic and not of a real-number ideal. fract(x) gets the one structural rule FnInterval also has: inside one unit cell it is endpoint-exact, across a boundary it is [0, 1].

Branches

A comparison of two overlapping intervals is neither true nor false. truth answers GlslTruth.UNKNOWN, and GlslMachine then runs both arms from the same state and joins what each wrote, with join. The machine refuses (with GlslUnsupported) the cases a join cannot express: arms that leave by different routes (one returns, the other falls through), and a loop whose condition is undecided. An integer index must be a single value.

What is refused

Builtins with no rule here are refused by name, never guessed: textures, cross, reflect, refract, faceforward, the matrix builtins, tan, the inverse trigonometric and hyperbolic functions except tanh, and the bit operators.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/GlslInterval.kt — read the source

GlslParser.kt

A parser for the GLSL that StackSL's emitter actually produces.

Why a parser and not a second backend in the compiler

StackSLCompiler has no symbolic node to hang a CPU arm off: its one internal node is StackValue(id, expression: String, …) and expression is already GLSL text, built by String-returning lambdas in StackSL.kt. Adding an eval: beside every glsl: — the RectSL TrackOp shape — would mean rewriting the emitter word by word, and every one of those rewrites is a chance to make the CPU arm disagree with the GPU arm. That disagreement is the exact defect this backend exists to rule out: a hand-written CPU twin that diverged from its own emitter.

Parsing the emitter's output means the CPU backend consumes the same text the GPU consumes, byte for byte, and cannot drift from the shipping emitter by construction. It also needs zero edits to StackSL.kt, so it cannot regress the GPU path at all.

What the subset is, measured

Dumped for all 31 committed gallery programs (1472 lines of emitted GLSL), the entire surface is: scalar/vector/matrix declarations, =/+=/-=/*=//=, if, for (int i = …; …; i++), return, function definitions, and about 30 builtins. No ternary, no while, no switch, no structs, no arrays, no bit operators. This parser covers a little more than that (ternary, while, do, break/continue, interface blocks) so the first shader that reaches slightly further does not fail; anything genuinely outside throws GlslUnsupported with the token quoted rather than guessing.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/GlslParser.kt — read the source

Top-level functions

Declaration Summary
fun parseGlslShader(source: String): GlslShader Parse a whole fragment shader.

GlslShader

class GlslShader internal constructor(internal val functions: Map<String, GFunction>, internal val overloads: Map<String, List<GFunction>>, internal val globals: List<GlslGlobal>, internal val samplers: List<String>, internal val blocks: List<GlslBlock>, val preprocessorLines: List<String>)

A parsed fragment shader.

Declaration Summary
val preprocessorLines: List<String> Preprocessor lines seen and skipped, for reporting; #version is the only expected one.
val functionNames: Set<String> Names of every function this shader defines — the emitter's helpers plus main.
val globalNames: List<String> Names of in/out/uniform variables, samplers included.

GlslScalars.kt

The value type the GLSL interpreter is generic over, so that one interpreter serves both the Float reference path and the differentiable one.

One interpreter, two algebras

The interface factors out the values, leaving GlslInterpreter.kt's machine (the statements, the branches, the loops, the call frames, the lvalue writes) as one copy of the control flow, shared by every scalar type. GlslFloatScalars is the Float instance, and each of its methods is a single GlslValue expression, so going through the interface neither widens nor reorders any arithmetic. StackSLCpuFloatPathManifestTest holds all 31 gallery programs to committed Float bits.

Sharing the control flow is what lets a derivative run over the emitted GLSL, the artifact the GPU consumes, rather than over a second evaluator written beside it.

V is a reference type in both instances (GlslValue and AdValue), so the Float path pays no boxing for the generic parameter: one interface dispatch per operation is the entire cost.

Why the methods take whole values

Every method here takes and returns a whole GLSL value rather than a component, because normalize, dot, length and the matrix products are not component-wise, and an interface at component granularity would force them to be restated per scalar. As it is, the Float instance delegates to glslMul, glslConstruct, glslSwizzle and GlslBuiltins.call unchanged.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/GlslScalars.kt — read the source

GlslValue.kt

The value model for the GLSL-subset interpreter behind StackSL's CPU backend.

Why Float, not Double

The thing being mirrored is a GPU fragment shader whose float is IEEE-754 binary32. Every intermediate here is therefore a Float, and every builtin rounds to Float at every step, including the ones Kotlin would rather do in Double (kotlin.math.sin takes and returns Double, so those are computed wide and narrowed once — see f1). Widening an intermediate would make the CPU result more accurate than the shader and therefore a worse reference: the goal is to reproduce the shader, not to improve on it.

Two consequences bound what any CPU implementation can agree with a GPU on, and neither is a defect in this file:

  • sin, cos, exp, log, pow, atan are IMPLEMENTATION-DEFINED in GLSL. The spec gives them no accuracy requirement at all beyond monotonicity in places. So no CPU implementation can be bit-exact with a GPU's, even in principle, and any program that reaches one of them can only be compared under a tolerance.
  • Even the exact operations do not compose exactly, because a GPU may CONTRACT a * b + c into a fused multiply-add with one rounding while the JVM performs two. Kotlin has no fma-free guarantee to offer here either way; what it does have is no automatic contraction, so this interpreter is the unfused reading of the shader.

The interesting failure is neither of those. fract of a large product amplifies a last-bit difference in its argument to O(1) — so _hash21, which is multiply/add/fract only and therefore exact operation by operation, can still produce a visibly different image from a backend that differed by one ulp three operations earlier. That is chaos, not error, and the gallery characterisation reports it separately from tolerance.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/GlslValue.kt — read the source

GlslBase

enum class GlslBase

The scalar element type of a GlslType. GLSL's uint is folded into INT (nothing in the emitted subset relies on unsigned wraparound), and SAMPLER is here because a sampler is a value the emitter passes to helper functions — _blur5(sampler2D tex, …) — so it has to be something the frame can hold.

Constants: FLOAT, INT, BOOL, SAMPLER.

GlslType

data class GlslType(val base: GlslBase, val rows: Int, val cols: Int = 1)

A GLSL type in the subset: a scalar, a vector, or a square matrix.

Declaration Summary
val components: Int
val isMatrix: Boolean
val isScalar: Boolean
override fun toString(): String

GlslValue

class GlslValue(val type: GlslType, val c: FloatArray)

A GLSL value: a GlslType and its components, always stored as Float.

Declaration Summary
val x: Float
val asFloat: Float
val asInt: Int
val asBool: Boolean
fun withComponent(index: Int, value: Float): GlslValue This value with component index replaced — the primitive behind swizzle assignment.
fun withComponents(indices: IntArray, values: FloatArray): GlslValue This value with the components at indices replaced by values, in order.
override fun toString(): String

GlslUnsupported

class GlslUnsupported(message: String) : RuntimeException(message)

Thrown for anything the subset does not cover, with the offending text quoted.

GlslEvalError

class GlslEvalError(message: String) : RuntimeException(message)

Thrown when a shader is syntactically or semantically wrong for this interpreter.

GlslSamplers

fun interface GlslSamplers

Where texture(sampler, uv) gets its answer.

Declaration Summary
fun sample(sampler: GlslValue, uv: GlslValue): GlslValue sampler is the value the sampler name evaluated to; uv the coordinate.
fun size(sampler: GlslValue): GlslValue textureSize(s, lod) as an ivec2. Defaults to 1x1, which makes texelsize 1.0.
fun gather(sampler: GlslValue, uv: GlslValue, channel: Int): GlslValue textureGather(s, uv, channel): component channel of the four texels in the footprint.

CpuTexture2D

class CpuTexture2D(val width: Int, val height: Int, val rgba: FloatArray) : GlslSamplers

A CPU texture a StackSL feedback program can read: RGBA floats, bilinear, clamp-to-edge.

Declaration Summary
override fun size(sampler: GlslValue): GlslValue
override fun sample(sampler: GlslValue, uv: GlslValue): GlslValue

StackSLCpu.kt

StackSL's CPU backend: the same GLSL a GPU would run, evaluated one pixel at a time on the JVM, so a StackSL program can be rendered, unit-tested, diffed and documented with no graphics context at all.

StackSLCpu is the entry point and CpuRaster is what comes back: RGBA floats with row 0 at the top, quantised the way a VK_FORMAT_R8G8B8A8_UNORM attachment does, and encodable straight to PNG. StackSLCpu.renderStill is the one-liner a box wants.

What the backend is exact about is the program. StackSLCpu.renderFragment takes a complete fragment shader as text and knows nothing about StackSL; the StackSL half compiles a program with StackSLCompiler and substitutes the result into a {{ stack.auto.<slot> }} template, which is the same tag the production expansion path in afield.graphics.shader.shaders fills. Both paths therefore receive the same string, and nothing about a word's behaviour is restated here: change what the emitter makes of fbm and this backend changes with it on the next call, with no edit. Tests pin the two copies that make that true: StackSLCpuExpansionTest compares the compile options against the production path's, and StackSLCpuStillTemplateTest compares the gallery's fragment template against the committed one in the gallery workspace.

It is a reference, not a renderer. It is an interpreter, so it is orders of magnitude short of a GPU, and StackSLCpu.renderStill states a cost for one program at one size. It is not bit-exact either: GLSL leaves sin, cos, exp, log, pow and atan implementation-defined, so a program that reaches one of them can only be compared under a tolerance. GlslValue has that argument in full, including the case where operations that are exact still diverge visibly.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/StackSLCpu.kt — read the source

CpuRaster

class CpuRaster(val width: Int, val height: Int, val rgba: FloatArray)

A rendered raster: RGBA floats in row-major order, row 0 at the top, matching both a PNG and the framebuffer convention the gallery harness reads back under (see StackSLCpu.FRAG_UV_ORIGIN_IS_TOP_LEFT).

Declaration Summary
fun at(x: Int, y: Int, channel: Int): Float Component channel (0..3) of the pixel at x, y.
fun toArgb(): IntArray Quantise to 8-bit ARGB the way a VK_FORMAT_R8G8B8A8_UNORM attachment does: clamp to [0,1] then round to nearest, which is what the Vulkan spec requires for a float to unorm conversion. Truncating instead would bias every channel down by half a level and turn an exact match into a one-level miss across the whole image.
fun toSrgbArgb(): IntArray Quantise to 8-bit ARGB the way a VK_FORMAT_R8G8B8A8_SRGB attachment does — the window swapchain's encoding, and the StackSL gallery's: clamp to [0,1], put red, green and blue through the sRGB transfer function (IEC 61966-2-1), leave alpha linear, then round. This is the picture a person sees; toArgb is the value the shader wrote, which is markedly darker (0.5 is 128 there and 188 here).
fun toPngBytes(): ByteArray

StackSLCpu

object StackSLCpu

Render StackSL on the CPU.

Declaration Summary
const val FRAG_UV_ORIGIN_IS_TOP_LEFT Which framebuffer row fragUV.y == 0 lands on, for the gallery harness's fullscreen triangle.
fun fragUvAt(x: Int, y: Int, width: Int, height: Int): GlslValue fragUV for pixel (x, y) of a width by height raster, at the pixel centre — which is where a rasteriser evaluates a fullscreen triangle's interpolants.
fun stackAutoOptions(slot: String, sampleTexture: String = "source"): StackSLCompileOptions The compile options a {{ stack.auto.<slot> }} expansion uses.
fun compile(program: String, slot: String = "program", sampleTexture: String = "source"): StackSLCompileResult Compile program exactly as a stack.auto slot named slot would.
fun expandStackAuto(template: String, program: String, slot: String = "program", sampleTexture: String = "source"): String Substitute program's compiled GLSL into template at {{ stack.auto.<slot> }}.
fun substituteSlot(template: String, glsl: String, slot: String = "program"): String Put glsl into template at {{ stack.auto.<slot> }}.
fun renderFragment(source: String, width: Int, height: Int, uniforms: Map<String, GlslValue> = emptyMap(), samplers: GlslSamplers = GlslSamplers.REFUSE, outName: String = "outColor", fragUvName: String = "fragUV", parallel: Boolean = true, perPixel: ((x: Int, y: Int, invocation: GlslInvocation) -> Unit)? = null, onDiscard: FloatArray = floatArrayOf(0f, 0f, 0f, 0f)): CpuRaster Run a whole fragment shader over a width by height raster.
fun stillTemplate(time: Float = 1.75f): String The still template the docs gallery renders through, with time baked into input.w.
fun renderStill(program: String, dim: Int = 256, time: Float = 1.75f, parallel: Boolean = true): CpuRaster Render a StackSL program on the CPU, at dim square, exactly as the docs gallery renders it.
fun renderProgram(program: String, template: String, width: Int, height: Int, slot: String = "program", sampleTexture: String = "source", pushParams: FloatArray? = null, samplers: GlslSamplers = GlslSamplers.REFUSE, parallel: Boolean = true): CpuRaster The one-call path: compile program, substitute it into template, render it.

StackSLCpuContactSheet.kt

src/jvmMain/kotlin/afield/lang/stacksl/cpu/StackSLCpuContactSheet.kt — read the source

Top-level functions

Declaration Summary
fun main(args: Array<String>) Command line: repoRoot` `outputDir.

StackSLCpuContactSheet

object StackSLCpuContactSheet

Renders every gallery program on the CPU, compares it to the committed GPU still, and writes a contact sheet plus a markdown table.

Declaration Summary
const val TILE Side of one tile on the sheet. 512 decimated by 4; see decimateAverage.
fun build(repoRoot: File, outputDir: File, only: Set<String>? = null): List<GalleryAgreement> Build the sheet. Returns the per-program statistics in the same order as the sheet's cells, so the caller can print them beside the picture rather than recomputing.

StackSLCpuGallery.kt

Compares StackSL's CPU renderer against the GPU: 31 committed gallery programs, each with its fragment template and the 512x512 PNG a real GPU drew for it.

public/docs/assets/stacksl_gallery/ holds, per gallery entry <name>, the program (<name>_stackSL_.stacksl), its fragment shader with the literal {{ stack.auto.program }} slot (<name>_fragment_.glsl), and its docOrder / animate / timeSpeed / feedback settings (<name>_metadata_.yaml). public/docs/assets/stacksl/stacksl-<name>.png is the GPU's still.

The fragment file is a template, not expanded GLSL, so a CPU render that reads it goes through the live StackSLCompiler, and both backends are compared against today's emitter. 29 of the 31 templates are byte-identical (fragmentTemplate(1.75f) in StackSLGalleryHarness); the other 2 are the feedback template.

The ground truth is 8-bit. The harness renders into VK_FORMAT_R8G8B8A8_SRGB and clamps to [0,1] first, so the GPU's answer is known to one sRGB level. Agreement is therefore measured in 8-bit sRGB levels, with the CPU side encoded the same way (StackSLCpuGallery.galleryArgb), and "exact" means "quantises to the same byte".

src/jvmMain/kotlin/afield/lang/stacksl/cpu/StackSLCpuGallery.kt — read the source

GalleryEntry

class GalleryEntry(val name: String, val program: String, val fragmentTemplate: String, val docOrder: Int, val animate: Boolean, val feedback: Boolean, val gpuPng: File?)

One gallery entry as it exists on disk.

Declaration Summary
val name: String
val program: String
val fragmentTemplate: String
val docOrder: Int
val animate: Boolean
val feedback: Boolean
val gpuPng: File? The committed GPU still, or null if there is none.

GalleryAgreement

class GalleryAgreement(val name: String, val tier: AgreementTier, val meanLevels: Double, val maxLevels: Int, val withinOneFraction: Double, val exactFraction: Double, val worstPixel: Pair<Int, Int>, val pixels: Int, val overTwoPixels: Int = 0)

What the two backends did to each other, per program, in 8-bit levels.

Declaration Summary
val name: String
val tier: AgreementTier
val meanLevels: Double
val maxLevels: Int
val withinOneFraction: Double Fraction of pixels whose three channels are all within one 8-bit level.
val exactFraction: Double Fraction of pixels that quantise to exactly the same three bytes.
val worstPixel: Pair<Int, Int>
val pixels: Int
val overTwoPixels: Int How many pixels have a channel more than two 8-bit levels apart.
override fun toString(): String

AgreementTier

enum class AgreementTier(val label: String, val why: String)

Why a program is or is not entitled to agree tightly, decided from the emitted GLSL text rather than from a hand-kept list, so a program that gains an fbm gets reclassified without anyone remembering to.

Constants: EXACT_OPS, SMOOTH_TRANSCENDENTAL, HASH21_AMPLIFIED, HASH11_UNBOUNDED, FEEDBACK.

StackSLCpuGallery

object StackSLCpuGallery

Load the gallery from a checkout and compare the CPU backend against the committed GPU stills.

Declaration Summary
const val WORKSPACE_DIR Where the programs and templates live, relative to a checkout root.
const val GPU_STILL_DIR Where the committed GPU stills live, relative to a checkout root.
const val DIM The side of the committed GPU stills; StackSLGalleryHarness.OUTPUT_DIM.
fun load(repoRoot: File): List<GalleryEntry> Load every entry, in the docs' own order.
fun render(entry: GalleryEntry, dim: Int = DIM, parallel: Boolean = true): CpuRaster Render one entry on the CPU at dim square, through the committed template.
fun tierOf(entry: GalleryEntry): AgreementTier The tier an entry falls in, from its emitted GLSL.
fun galleryArgb(cpu: CpuRaster): IntArray A CPU raster as the gallery's GPU target stores it: 8-bit sRGB (StackSLGalleryHarness.GALLERY_FORMAT). Anything compared with or drawn beside a committed still goes through here, so the two sides are never in different encodings.
fun compare(name: String, tier: AgreementTier, cpu: CpuRaster, gpuArgb: IntArray, gpuWidth: Int): GalleryAgreement Compare a CPU raster against a committed GPU PNG.
val blackSampler A sampler that reports black with alpha 1.

StackSLFit.kt

Gradient descent on a StackSL program's own literals: fit a shader to a picture, then write the fitted numbers back into the program.

Why a StackSL program is differentiable

StackSL is not geometry. It is a per-pixel field: main() runs once per pixel with fragUV as its only spatial input, and the output is an ordinary composition of arithmetic and GLSL builtins. So d(pixel)/d(wire) is the chain rule. There is no rasteriser in the pipeline, no coverage term, no approximation and so no bias to characterise.

RectSL, a shape language, is the harder case. There a pixel's value depends on a parameter through coverage, so d(pixel)/d(parameter) needs a differentiable rasteriser, and a coverage approximation is a modelling choice with a bias to characterise. RectSLSoftRaster.kt is that choice, made there.

Limits on what a fit can do

  1. A fit can only move literal-backed wires. See StackSLWires: a wire noted "optional default palette a" is a default the emitter supplied, with no literal in the source to rewrite. StackSLFitResult.fittedIndices is the population that moved and WiredProgram.wires is the larger set that exists.
  2. The template clamps. StackSLCpu.stillTemplate, the default, ends main() with outColor = vec4(clamp(c, 0.0, 1.0), 1.0), and clamp carries zero derivative outside its range. A pixel that is saturated at the seed therefore contributes nothing to the gradient, so a seed whose image is mostly blown out is a seed with almost no signal. StackSLFitResult.saturatedFraction measures how much of the image is in that state.
  3. The default loss is a per-pixel sum of squares and rewards exactly that. It has no term for structure, so a fit can lower it a long way by matching a target's colour and luminance statistics while the image does not resemble the target at all. StackSLFitFigureTest writes a figure of the target, the seed and the fit side by side, which is the comparison the loss itself cannot make. StackSLFitLoss.LPIPS descends on the perceptual distance instead, with the gradient of the LPIPS network chained through the program (StackSLLpipsLoss); StackSLFitLoss.LPIPS_PIXEL on the mean of the two.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/StackSLFit.kt — read the source

StackSLFit

object StackSLFit

The fit itself: seed program in, fitted program out, with the descent's provenance attached.

Declaration Summary
const val DEFAULT_DIM The default fit resolution: 64 square, sized from the measured cost — see dev_docs.
fun fit(program: String, target: CpuRaster, template: String = StackSLCpu.stillTemplate(), dim: Int = DEFAULT_DIM, iterations: Int = 60, learningRate: Double = 0.05, method: OptimizeMethod = OptimizeMethod.ADAM, slot: String = "program", indices: List<Int>? = null, parallel: Boolean = true, annealing: AdAnnealing? = null, loss: StackSLFitLoss = StackSLFitLoss.MSE, lpipsSide: Int = maxOf(StackSLLpipsLoss.DEFAULT_SIDE / dim, 1) * dim, lpipsGrid: Int = 0): StackSLFitResult Fit program's literals to target.

StackSLImageLoss

class StackSLImageLoss(val wired: WiredProgram, template: String, val dim: Int, val target: CpuRaster, private val parallel: Boolean = true, private val outName: String = "outColor", private val fragUvName: String = "fragUV")

A mean-squared-error image loss over a wired program's parameter vector, with both an exact reverse-mode gradient and the plain Float render the gradient is checked against.

Declaration Summary
val wired: WiredProgram
val dim: Int
val target: CpuRaster
val source: String The complete fragment shader, wires and all — the artifact both paths read.
fun render(values: DoubleArray): CpuRaster Render on the Float reference path at values.
fun value(values: DoubleArray): Double The loss at values, via the Float path — the finite-difference oracle's evaluator.
fun valueAndGradient(values: DoubleArray, surrogate: AdSurrogate = AdSurrogate.OFF): ADGradient The loss and its exact gradient at values, by reverse-mode AD through the same parse tree the Float path walks.
fun seededGradient(values: DoubleArray, seed: FloatArray, surrogate: AdSurrogate = AdSurrogate.OFF): DoubleArray The vector-seeded backward pass: sum over pixels and channels of seed[ch, y, x] * d outch/ d wire, for an image-space gradient seed that something else computed.
fun renderPrimal(values: DoubleArray, surrogate: AdSurrogate = AdSurrogate.OFF): CpuRaster The primal image the differentiable pass computed, as a raster.
fun saturatedFraction(values: DoubleArray): Double Fraction of RGB samples that sit exactly on a clamp bound, and so carry no gradient.

StackSLFitResult

class StackSLFitResult(val wired: WiredProgram, val loss: StackSLImageLoss, val fittedIndices: List<Int>, val fittedWires: DoubleArray, val run: OptimizeRun, val fittedProgram: String, val annealing: AdAnnealing? = null, val lossKind: StackSLFitLoss = StackSLFitLoss.MSE)

What a fit produced, including the program a human can paste back into a box.

Declaration Summary
val wired: WiredProgram The wired program that was fitted.
val loss: StackSLImageLoss The image loss: the renderer, and the MSE whatever lossKind the descent ran on.
val fittedIndices: List<Int> The wire indices that were allowed to move, in the optimiser's own order.
val fittedWires: DoubleArray The whole wire vector after the fit, indexed by wire index.
val run: OptimizeRun The descent's provenance: losses, trajectory, gradient norms.
val fittedProgram: String wired's source with the fitted literals substituted at their own token positions.
val annealing: AdAnnealing? The jump surrogate's schedule, or null for an exact fit.
val lossKind: StackSLFitLoss What the descent ran on; seedLoss and fittedLoss are its values.
val seedWires: DoubleArray The wire vector the fit started from.
val seedLoss: Double The loss before any step.
val fittedLoss: Double The loss at the end.
val lossRatio: Double How far the loss fell, as a ratio. 1.0 means the descent achieved nothing.
val saturatedFraction: Double Fraction of RGB samples clamped at the seed — see StackSLFit's second caveat.
fun report(): List<String> One line per moved literal: what the human wrote, and what the fit made of it.

StackSLIntervals.kt

Interval bounds on a fragment shader's colours: a certified range for every pixel in a rectangle, without rendering the pixels.

StackSLIntervals encloses a rectangle (StackSLRegionBound) or splits the frame into a quadtree of tiles (StackSLTile) proven flat to a tolerance; StackSLTilePicture draws the result.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/StackSLIntervals.kt — read the source

StackSLRegionBound

class StackSLRegionBound(val lo: FloatArray, val hi: FloatArray, val maybeNaN: Boolean, val maybeDiscarded: Boolean, val refusal: String? = null)

A certified bound on the colour of every pixel in a rectangle of a fragment shader's output.

Declaration Summary
val lo: FloatArray
val hi: FloatArray
val maybeNaN: Boolean
val maybeDiscarded: Boolean
val refusal: String?
val certified: Boolean
val width: Float The widest channel's hi - lo, or infinity when refused.
fun contains(rgba: FloatArray): Boolean Whether rgba (one pixel) lies inside, channel by channel.
override fun toString(): String

StackSLTile

class StackSLTile(val x: Int, val y: Int, val size: Int, val bound: StackSLRegionBound, val settled: Boolean)

One square of a StackSLIntervals.certifiedTiles subdivision: pixels [x, x+size) × [y, y+size).

StackSLIntervals

object StackSLIntervals

Interval evaluation of StackSL programs: the range a shader can output over a rectangle of pixels, bounded without drawing any of them. The rules are in GlslInterval.kt; this is the face a box or a test uses.

Declaration Summary
fun enclosePixels(glsl: String, x0: Int, y0: Int, x1: Int, y1: Int, width: Int, height: Int, uniforms: Map<String, GlslValue> = emptyMap()): StackSLRegionBound Bound every pixel x0..x1 × y0..y1 (inclusive) of a width × height render of glsl, a complete fragment shader writing outColor from fragUV, as StackSLCpu.renderFragment runs it.
fun certifiedTiles(glsl: String, dim: Int, tolerance: Float = 1f / 255f, minSize: Int = 1, uniforms: Map<String, GlslValue> = emptyMap(), onEnclosure: (() -> Unit)? = null): List<StackSLTile> Split a dim × dim render of glsl into squares until each one's colour is pinned to within tolerance in every channel, or the square is minSize pixels across.
fun tilePicture(program: String, dim: Int = 128, tolerance: Float = 2f / 255f, time: Float = 1.75f): StackSLTilePicture Render program through the gallery's still template at dim square, prove it into certifiedTiles, and return both as one picture.
fun tilePng(tiles: List<StackSLTile>, dim: Int, scale: Int = 2): ByteArray The tiles drawn as a picture: each settled tile filled with its proven colour and outlined, each unsettled pixel shown magenta. The quadtree is the proof, so its shape shows where the program is simple and where it is not.

StackSLTilePicture

class StackSLTilePicture(val program: String, val dim: Int, val tolerance: Float, val tiles: List<StackSLTile>, val render: CpuRaster, val enclosures: Int, val millis: Long) : afield.ui.visualization.ToHTML

A StackSL program's render beside its proof: the Float render on the left, the StackSLIntervals.certifiedTiles quadtree on the right, and a line of counts under both.

Declaration Summary
val program: String
val dim: Int
val tolerance: Float
val tiles: List<StackSLTile>
val render: CpuRaster
val enclosures: Int Interval runs the subdivision took, one per tile it visited.
val millis: Long
val settledPixels: Int Pixels inside a settled tile: proven to within tolerance without being rendered.
val provenByRegion: Int Pixels inside a settled tile larger than one pixel — the ones proven by a region rather than by evaluating them one at a time, which is the saving the quadtree buys.
val refusal: String? Tiles the subdivision gave up on because a rule was missing, with the first reason.
override fun toHTML(): String

StackSLLpipsLoss.kt

The losses a StackSLFit can descend on: mean squared RGB error, LPIPS perceptual distance, and mixtures of the two.

StackSLFitLoss names the choices; StackSLLpipsLoss computes the LPIPS value and gradient of a render against its target.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/StackSLLpipsLoss.kt — read the source

StackSLFitLoss

enum class StackSLFitLoss(val label: String)

What a StackSLFit descends on.

Constants: MSE, LPIPS, LPIPS_PIXEL.

StackSLLpipsLoss

class StackSLLpipsLoss(val image: StackSLImageLoss, val side: Int = DEFAULT_SIDE, val regionGrid: Int = 0, val kind: StackSLFitLoss = StackSLFitLoss.LPIPS)

An LPIPS loss over a wired program's parameter vector, with its gradient: the LPIPS gradient with respect to the image (Lpips.distanceAndGradientPlanar) chained through the program by StackSLImageLoss.seededGradient.

Declaration Summary
val image: StackSLImageLoss
val side: Int
val regionGrid: Int
val kind: StackSLFitLoss
val dim: Int
fun value(values: DoubleArray): Double The loss at values (the kind's value, no gradient).
fun valueAndGradient(values: DoubleArray, surrogate: AdSurrogate = AdSurrogate.OFF): ADGradient The loss and its gradient with respect to every wire.

StackSLWires.kt

StackSL's dangling wires, made runnable on the CPU: the free-parameter vector a fit descends.

StackSLCompileOptions.externalizeConstants already lifts every numeric literal out of a program into an emitted float array called <prefix>_wires, with a <prefix>_init_wires initialiser, and records each one as a StackSLDanglingWire carrying its index, its initial, and the source token it came from. So the parameter vector needs no new syntax.

Why the emitted GLSL has to be rewritten first

This backend cannot parse the externalised GLSL at all. GlslParser refuses arrays outright ("GLSL arrays are not in the StackSL subset", on both the global and the local declaration path) and StackSLCpu.stackAutoOptions pins externalizeConstants = false. Two further things break even if arrays are granted: every emitted function gains a trailing , float <prefix>_wires[<prefix>_wire_count] parameter, and the entrypoint gains a second, overloaded one-argument wrapper that declares the array, calls the initialiser and forwards. GlslShader.functions is keyed by name, so the wrapper simply replaces the real function.

lower closes that with one source-to-source rewrite of the emitted GLSL, so what the CPU runs is still the artifact the GPU consumes rather than a restatement of it:

  1. element i of <prefix>_wires becomes a scalar global <prefix>_wire_i, a shape the subset does support, and one a caller can push a new value into per evaluation with setGlobal, with no recompile;
  2. the wire parameter and the wire argument disappear from every signature and call;
  3. const int <prefix>_wire_count, <prefix>_init_wires and the forwarding wrapper are dropped, the initial values having moved into the globals.

StackSLWireLoweringTest renders the lowered program with every wire at its recorded initial and asserts the raster is bit-identical to the ordinary non-externalised compile of the same program.

Which wires can be written back

A wire's note says where it came from, and that is what governs the round trip. A "number" wire stands for a literal the human typed, so a fitted value can be substituted at the recorded line/column and the program still reads as the program. A wire noted "optional default palette a" is a default the emitter supplied for a word that was given no operand: there is no literal in the source to rewrite, and inventing one would mean pushing four vec4s in front of palette. So rewrite fits and rewrites literal-backed wires only, literalBacked is the population, and the rest are held at their initials. A program's wire count is therefore an upper bound on its fittable parameter count rather than the count itself, and StackSLFit reports both.

src/jvmMain/kotlin/afield/lang/stacksl/cpu/StackSLWires.kt — read the source

StackSLWires

object StackSLWires

Lifting a StackSL program's literals into addressable globals, and writing fitted values back.

Declaration Summary
fun wiredOptions(slot: String, sampleTexture: String = "source", entrypoint: String? = null): StackSLCompileOptions The compile options for a wired program: StackSLCpu.stackAutoOptions plus externalisation, and — for a stack.auto.slot:word slot — entrypoint as the root.
fun globalName(prefix: String, index: Int): String The global a lowered wire lives in.
fun lift(program: String, slot: String = "program", sampleTexture: String = "source", entrypoint: String? = null): WiredProgram Compile program with its literals lifted, and lower the result to something the CPU backend can parse.
fun lower(glsl: String, prefix: String, wireCount: Int): String Rewrite the wire array in glsl into scalar globals. See the file KDoc for the three steps and for why this is a rewrite of the emitted artifact rather than a second emitter.
fun literalBacked(wires: List<StackSLDanglingWire>): List<StackSLDanglingWire> The wires a fitted value can be written back into the program source: the ones whose note is "number", i.e. the ones that stand for a literal the human typed.
fun rewrite(program: String, wires: List<StackSLDanglingWire>, values: DoubleArray): String Substitute values back into program's source at the recorded token positions.
fun formatLiteral(v: Double): String %.8f, trimmed, with a decimal point kept — the same shape StackSL.kt emits.

WiredProgram

class WiredProgram(val program: String, val slot: String, val prefix: String, val glsl: String, val wires: List<StackSLDanglingWire>)

A StackSL program whose numeric literals have become addressable free parameters.

Declaration Summary
val program: String The StackSL source this came from.
val slot: String The {{ stack.auto.<slot> }} slot the lowered GLSL is named for.
val prefix: String The emitted prefix, which is also slot unless the compiler renamed it.
val glsl: String The lowered emitted GLSL: parseable by parseGlslShader, wires as scalar globals.
val wires: List<StackSLDanglingWire> Every wire the emitter lifted, literal-backed or not, in index order.
val literalWires: List<StackSLDanglingWire> The wires a fitted value can be written back into the source — see StackSLWires.rewrite.
val initial: DoubleArray Every wire's emitted initial value, indexed by wire index.
fun globals(values: DoubleArray): Map<String, GlslValue> The globals map a render needs for a given parameter vector.
fun describe(): String A one-line report of what this program's parameter vector actually is.

EmbeddedStackSL.kt

Editor help for StackSL written as a string inside a Kotlin box: completion with the StackSL vocabulary, and each line's stage as a line note.

EmbeddedStackSL finds those strings by a leading #stacksl line or by assignment to one of the box's stack.auto slots, as the text is typed.

src/jvmMain/kotlin/afield/lang/stacksl/study/EmbeddedStackSL.kt — read the source

Top-level functions

Declaration Summary
fun installEmbeddedStackSLLineNotes() StackSL inside a Kotlin box's code gets its line notes from EmbeddedStackSL.

EmbeddedStackSL

object EmbeddedStackSL

StackSL written inside a Kotlin box's code: where it is, and the editor's help for it there — the StackSL vocabulary when completion opens inside it, and each line's stage as a line note.

Declaration Summary
data class Region(val bodyStart: Int, val text: String, val slot: String?) A StackSL string in Kotlin: its text from bodyStart (an offset of the code) and the slot it is assigned to, if any.
fun slots(box: Box): Set<String> The names of box's stack.auto slots: those its shaders declare and those it has bound.
fun regions(code: String, slots: Set<String>): List<Region> Every StackSL string in code, given the box's slots.
fun regionAt(box: Box, code: String, position: Int): Region? The StackSL string of box's code that offset position is in, or null.
fun complete(box: Box, code: String, position: Int): Completion? Completion at position of code, when it is inside StackSL: the vocabulary, with bounds in the code's offsets.
fun lineNote(box: Box, code: String, line: Int): LineNotes.Note? line (1-based) of code's StackSL, as a note: the stage it ends, drawn alone; null outside StackSL.

StackAutoCard.kt

Sliders under a box for the numbers in its stack.auto slots, redrawing the box's own shader without a recompile.

StackAutoCard writes each slider to the slot's uniform block, can keep the numbers back into the box's code, and shows the slot either in its host shader or alone on the study frame. installStackAutoCard registers it.

src/jvmMain/kotlin/afield/lang/stacksl/study/StackAutoCard.kt — read the source

Top-level functions

Declaration Summary
fun installStackAutoCard() The box's stack.auto numbers card redraws whenever one of its slots compiles new StackSL.

StackAutoCard

object StackAutoCard

The numbers of a box's stack.auto slots, as sliders under the box: turn one and the box's own shader — the whole of it, in its own window — redraws with it, because a slider writes the slot's uniform block (StackAutoNumbers) and nothing is recompiled.

Declaration Summary
const val CHANNEL The output channel the card is drawn in.
fun refreshLater(box: Box) Redraw box's card soon, on the card's own thread; requests while one waits collapse into it. Boxes in no open document have none.
fun html(box: Box): String The card's HTML: a section per slot with numbers.

StackAutoStudy.kt

Studies of the stack.auto slots in a box's own shaders: checked edits to a slot's StackSL, and pictures of it in its host shader and alone.

StackAutoStudies finds or makes the StackAutoSlotStudy for a slot, and StackAutoView draws the two pictures: where the slot's module is drawn, and the slot's program by itself on the study frame.

src/jvmMain/kotlin/afield/lang/stacksl/study/StackAutoStudy.kt — read the source

StackAutoView

object StackAutoView

Pictures of a stack.auto slot: inShader the end result — wherever its module is drawn, found by walking what the renderer draws, else the module drawn alone into a target of its own (ModuleDestinations) — and alone, the slot's program by itself on the StackSL study's frame. The numbers card and the slot study's tools both show them.

Declaration Summary
data class Picture(val png: ByteArray?, val note: String) A picture (PNG bytes, or null when there is none) and what it is.
fun inShader(box: Box, slot: String): Picture slot of box in the host shader, as it is drawing now; see the object docs.
fun overTime(box: Box, slot: String, seconds: Double, moments: Int): Picture slot in the host shader at moments moments spread evenly across the next seconds seconds, as it really draws, on one numbered sheet (read left to right, then down). A host that animates the slot — by time it pushes, say — cannot be moved to a chosen time from here, so this waits for it. Run it off the main thread: every capture waits for a frame the main thread draws.
fun alone(box: Box, slot: String, program: String, entrypoint: String?, numbers: Map<Int, Double> = emptyMap()): Picture program (a slot's, at numbers by knob index) drawn alone on the StackSL study's frame, for debugging.
fun fitWithin(png: ByteArray, long: Int): ByteArray png scaled so its longer side is at most long pixels: a window capture is the display's full resolution.

StackAutoSlotCheck

data class StackAutoSlotCheck(val problems: List<String>, val warnings: List<String>, val knobs: List<StackSLKnob>)

What a slot study checks a candidate program for: that it compiles into this slot, and what it says.

StackAutoSlotStudy

class StackAutoSlotStudy internal constructor(val box: Box, val slot: String, private val directory: File, publisher: ((expected: String, replacement: String) -> Boolean)? = null)

A model's study of one stack.auto slot, inside a box's own shader: the StackSL study's loop — inspect, edit, turn numbers, observe, sweep, restore — where the program lives in someone else's GLSL.

Declaration Summary
val box: Box, val slot: String, private val directory: File
val revisions
fun compare(before: String, after: String = "current", mode: String = "rgb", region: List<Double>? = null): StudyReport What an edit preserved, in the host shader: before and after each bound live in turn and captured where the module draws, then the slot's own program put back. Displayed colour (rgb) or brightness (luma), held to one 8-bit level outside region (u0, v0, u1, v1 of the capture).
fun tryVariants(variants: List<StudyRequest.Variant>): StudyReport Try candidates in the host shader without keeping any: each is checked, bound live, captured where the module draws, and held for adoptVariant; then the slot's own program is put back.
fun adoptVariant(expectedRevision: String, expectedSourceHash: String, label: String, description: String = ""): StudyReport Publish a held candidate: bound live and written back into the code like an edit.
fun heldVariants(): List<Triple<String, String, String>> The candidates tryVariants is holding: label, note, source.
fun inspect(id: String = "current"): StudyReport Everything a model needs before editing: the slot's program, how the host uses it, its numbers, where it draws, history.
fun edit(expectedRevision: String, expectedSourceHash: String, source: String, description: String, alone: Boolean = false, over: Double = 0.0, moments: Int = 4): StudyReport Replace the slot's program: checked, bound live, written back into the code, and shown in the shader.
fun patch(expectedRevision: String, expectedSourceHash: String, edits: List<StudySourceEdit>, description: String, alone: Boolean = false, over: Double = 0.0, moments: Int = 4): StudyReport Exact text replacements in the slot's program, through the same checks.
fun restore(expectedRevision: String, expectedSourceHash: String, id: String, description: String): StudyReport Make an earlier revision the slot's program again, as a new revision.
fun setNumbers(numbers: Map<Int, Double>, keep: Boolean, expectedRevision: String, expectedSourceHash: String, description: String): StudyReport Turn numbers live, through the slot's block — no recompile — and show the result in the shader. With keep, write them into the program as a new revision too; without, they stay turned but unkept.
fun sweep(knob: Int, values: List<Double>): StudyReport One number at several values, each drawn in the shader for real, then put back as it was.
fun observe(alone: Boolean, over: Double = 0.0, moments: Int = 4): StudyReport The slot as it is drawing now, in the shader, or across over seconds — and, with alone, its program by itself.
fun checkIn(description: String) Check the slot's live program in as a revision, if it has none or has changed outside the study.

StackAutoStudies

object StackAutoStudies

A box's slot studies, one per slot, made on first use.

Declaration Summary
fun study(box: Box, slot: String): StackAutoSlotStudy box's study of slot, with the slot's live program checked in.
fun slots(box: Box): List<String> The slots of box a study can take: those with a program bound.

StackAutoStudyTools.kt

The tools an AI4 conversation gets when it is opened on the code of a box whose shaders have {{ stack.auto.<slot> }} slots with programs bound.

Each works on one slot's study (see StackAutoStudies), inside the box's own shader: the end result is what the tools show.

src/jvmMain/kotlin/afield/lang/stacksl/study/StackAutoStudyTools.kt — read the source

Used in: Talking to the StackSL in your shader, StackSL examples

Top-level functions

Declaration Summary
fun installStackAutoStudyTools() Register the stack.auto slot tools, for every conversation opened on a box's code that has bound slots.

StackAutoSlotGuide

data class StackAutoSlotGuide(@JsonPropertyDescription("A word or phrase to look up in the StackSL words guide; empty returns how to work on a slot") val search: String = "")

How to work on a stack.auto slot, or the StackSL words guide's entries matching a search term.

Declaration Summary
val search: String A word or phrase to look up in the StackSL words guide; empty returns how to work on a slot.

InspectStackAutoSlot

data class InspectStackAutoSlot(override val slot: String = "", override val revision: String = "current") : StackAutoSlotRequest, StudyRequest.Inspect

EditStackAutoSlot

data class EditStackAutoSlot(override val slot: String = "", override val expectedRevision: String, override val expectedSourceHash: String, @JsonPropertyDescription("Complete StackSL source: one transform per line, # comments naming what each stage does") override val source: String, override val description: String, @JsonPropertyDescription("Also draw the program alone on StackSL's own frame, a debugging view whose input is not the host's") val alone: Boolean = false, @JsonPropertyDescription("Seconds to watch the shader for; above 0 the picture is a numbered sheet of real frames across that long — use it when the host animates the slot (by time it pushes), as one frame shows one moment") val over: Double = 0.0, @JsonPropertyDescription("How many moments the sheet holds, 2..9, evenly spaced across over") val moments: Int = 4) : StackAutoSlotRequest, StudyRequest.Edit

Declaration Summary
override val source: String
override val description: String
val alone: Boolean
val moments: Int

PatchStackAutoSlot

data class PatchStackAutoSlot(override val slot: String = "", override val expectedRevision: String, override val expectedSourceHash: String, override val edits: List<StudySourceEdit>, override val description: String, val alone: Boolean = false, @JsonPropertyDescription("Seconds to watch the shader for; above 0 the picture is a numbered sheet of real frames across that long — use it when the host animates the slot (by time it pushes), as one frame shows one moment") val over: Double = 0.0, @JsonPropertyDescription("How many moments the sheet holds, 2..9, evenly spaced across over") val moments: Int = 4) : StackAutoSlotRequest, StudyRequest.Patch

Declaration Summary
override val edits: List<StudySourceEdit>, override val description: String, val alone: Boolean
val moments: Int

StackAutoNumberEdit

data class StackAutoNumberEdit(override val knob: Int, override val value: Double) : afield.study.ui.NumberEdit

One number to turn: knob index from Inspect, and its new value.

SetStackAutoNumbers

data class SetStackAutoNumbers(override val slot: String = "", override val numbers: List<StackAutoNumberEdit>, val keep: Boolean = false, override val expectedRevision: String = "", override val expectedSourceHash: String = "", override val description: String = "") : StackAutoSlotRequest, StudyRequest.SetNumbers

Declaration Summary
override val expectedRevision: String

ObserveStackAutoSlot

data class ObserveStackAutoSlot(val slot: String = "", val alone: Boolean = false, @JsonPropertyDescription("Seconds to watch the shader for; above 0 the picture is a numbered sheet of real frames across that long — use it when the host animates the slot (by time it pushes), as one frame shows one moment") val over: Double = 0.0, @JsonPropertyDescription("How many moments the sheet holds, 2..9, evenly spaced across over") val moments: Int = 4)

Declaration Summary
val moments: Int

SweepStackAutoNumber

data class SweepStackAutoNumber(override val slot: String = "", override val knob: Int, override val values: List<Double>) : StackAutoSlotRequest, StudyRequest.Sweep

RestoreStackAutoRevision

data class RestoreStackAutoRevision(override val slot: String = "", override val expectedRevision: String, override val expectedSourceHash: String, override val revision: String, override val description: String = "") : StackAutoSlotRequest, StudyRequest.Restore

Declaration Summary
override val revision: String, override val description: String

CompareStackAutoSlot

data class CompareStackAutoSlot(override val slot: String = "", override val beforeRevision: String, override val afterRevision: String = "current", override val seconds: List<Double> = emptyList(), override val mode: String = "rgb", override val allowedRegion: List<Double> = emptyList())

Declaration Summary
override val seconds: List<Double>

StackAutoVariant

data class StackAutoVariant(override val label: String, override val source: String = "", override val edits: List<StudySourceEdit> = emptyList(), override val note: String = "") : StudyRequest.Variant

A candidate program for a slot: a complete source, or exact edits to the slot's live program.

Declaration Summary
override val note: String

TryStackAutoVariants

data class TryStackAutoVariants(override val slot: String = "", override val variants: List<StackAutoVariant>, override val seconds: List<Double> = emptyList())

AdoptStackAutoVariant

data class AdoptStackAutoVariant(override val slot: String = "", override val expectedRevision: String, override val expectedSourceHash: String, override val label: String, override val description: String = "") : StackAutoSlotRequest, StudyRequest.Adopt

Declaration Summary
override val label: String, override val description: String

StackAutoSlotRequest

interface StackAutoSlotRequest

A stack.auto request names the slot it is about; empty when the box has only one.

StackAutoGuideText

data class StackAutoGuideText(val text: String) : afield.ai.tools.SummarisesForToolLog

The StackAutoSlotGuide tool's answer.

Declaration Summary
override fun toolLogSummary(): String

StackSLGpu.kt

Where a StackSL study's frames come from: the GPU when a window is running, the CPU reference otherwise.

Both sides implement StackSLRasterizer and hand back raw-output rasters with rows top to bottom and uv.y upwards, so every measurement a study makes is independent of the renderer: StackSLGpuRasterizer and CpuStackSLRasterizer.

src/jvmMain/kotlin/afield/lang/stacksl/study/StackSLGpu.kt — read the source

Top-level functions

Declaration Summary
fun StackSLRasterizer.renderAll(program: String, size: Int, seconds: List<Double>): List<CpuRaster> StackSLRasterizer.render, collected.

StackSLRasterizer

interface StackSLRasterizer : StudyFrames<String>

Where a StackSL study's frames come from.

Declaration Summary
fun render(program: String, size: Int, seconds: List<Double>, each: (index: Int, raster: CpuRaster) -> Unit) program's raw output at size square, once per time in seconds, handed to each in order.
override fun render(input: String, width: Int, height: Int, seconds: List<Double>, each: (index: Int, raster: CpuRaster) -> Unit) A StackSL study frame is square: width must equal height.

CpuStackSLRasterizer

object CpuStackSLRasterizer : StackSLRasterizer

The CPU reference: the same GLSL interpreted pixel by pixel. Exact and everywhere, and slow.

Declaration Summary
override val description
override val fast
override fun render(program: String, size: Int, seconds: List<Double>, each: (index: Int, raster: CpuRaster) -> Unit)

StackSLGpuRasterizer

class StackSLGpuRasterizer(private val rendererOf: () -> Renderer?, private val onMain: (() -> Unit) -> Unit = ::onRunloopMain, private val fallback: StackSLRasterizer = CpuStackSLRasterizer, private val frameTimeoutMs: Long = 4000) : StackSLRasterizer

A study's frames from the GPU: the program in a real Field 4 fragment shader, drawn into an offscreen float target on a running renderer and read back exactly.

Declaration Summary
override var description: String
override val fast: Boolean
override fun render(program: String, size: Int, seconds: List<Double>, each: (index: Int, raster: CpuRaster) -> Unit)

StackSLPrograms.kt

StackSL as a main language for a box's children: compile on run, draw in the window above, and check each run in as a study revision.

StackSLPrograms is the entry point; StackSLLive follows a running program's edits in its window, and StackSLExecution is the box execution that runs it. installStackSLLanguage registers the language.

src/jvmMain/kotlin/afield/lang/stacksl/study/StackSLPrograms.kt — read the source

Top-level functions

Declaration Summary
fun installStackSLLanguage() Register StackSL as a main language, so "Set Main Language: StackSL" is offered on a root box.
fun installStackSLLineNotes() A StackSL program's lines, for the editor's completion popup and cursor notes: the stage each ends (StackSLStudy.lineNote).
fun installStackSLLanguageWatcher() Re-apply stackSLTopologyNeedsUpdating whenever the graph changes.
fun installStackSLProgramAction(root: Box) "Make a StackSL Program": the selected box becomes a program beneath a StackSL root — a new one just above it, if it has none, which opens a window of its own — and starts drawing. A selected root gets a new program beneath it.
fun stackSLTopologyNeedsUpdating(values: Collection<Box>) Give every child of a StackSL root a StackSLExecution that edits stackSL, and hand a child back to Kotlin when no StackSL root claims it any more.

Top-level values

Declaration Summary
const val STACKSL_STARTER The program a new StackSL box starts with: layered noise over slowly drifting coordinates, coloured.

StackSLPrograms

object StackSLPrograms

StackSL as one of Field's main languages, and the home of every StackSL program's study.

Declaration Summary
const val PROPERTY The property a StackSL program lives in.
const val CHANNEL The output channel a program's study card is shown in.
const val LANGUAGE The main-language id, as mainLanguage stores it.
val WINDOW Where the window a StackSL root opens for itself goes, and its size: square, 720 by 720.
fun sessionOf(box: Box): SessionInformation? The open document box belongs to, or null for a box in no open document.
fun rendererFor(box: Box): Renderer? The running renderer a program draws in: the nearest box, starting at box and walking up its parents, that holds an open renderer — the same population prop<Renderer>(renderer) searches.
fun study(box: Box): StackSLStudy box's study, created on first use. Publishing an accepted edit writes the box's stackSL property on the main thread, refusing if the editor changed it since the edit was based on it.
fun readyStudy(box: Box): StackSLStudy study, with a first revision checked in if it has none. Renders probe frames, so call it off the main thread (the GPU waits for a frame the main thread draws).
fun run(box: Box) Run box as the editor's run does: draw it, follow its edits, and check it in.
fun viewer(box: Box): StackSLStudyViewer The card showing box's study, created (and shown) on first use.
fun live(box: Box): StackSLLive box's binding to the window above it.
fun isProgram(box: Box, property: String): Boolean Whether a conversation focused on box's property is about a StackSL program.

StackSLProblem

data class StackSLProblem(val error: Boolean, val message: String, val line: Int?, val summary: String)

One problem with a StackSL program, as the editor shows it: an error refuses the program.

Declaration Summary
val error: Boolean
val message: String
val line: Int? 1-based line in the program, when the problem has one.
val summary: String The problem in one line, as tool results and logs quote it.

StackSLLive

class StackSLLive internal constructor(private val box: Box)

A StackSL program drawn live in a window: a fullscreen triangle whose fragment shader is the study template with the program in its {{ stack.auto.program }} slot, and the seconds since the binding began in pc.params.x — the same shader the study's evidence is rendered with.

Declaration Summary
val showing: String? The program the window above the box is drawing, or null — also when that window closed or changed.
var pinnedStage: Int? A stage the card holds the window at — how many root words have run — or null for the whole program.
var followCursor: Boolean Whether the window shows the stage the editor's cursor line ends, rather than the whole program.
fun wanted(program: String, cursorLine: Int?): String What the window should draw for program: the stage the card holds it at, else — when it follows the cursor — the stage cursorLine ends, else the whole program. A stage is the program up to that line (afield.lang.stacksl.StackSLOutline.stageProgram): what that line leaves on the stack, drawn.
fun show(source: String): String? Draw source in the window above the box. Returns null when it is drawing, otherwise why not: no window, or a shader the GPU refused (the previous program keeps drawing).
fun hide() Stop drawing in the window. Nothing to hide costs nothing, and does not wait for the main thread.

StackSLExecution

class StackSLExecution(val inside: Box) : BoxExecution

How a child of a StackSL root runs: see StackSLPrograms. Run and launch both draw the program and follow its edits until stopped; run also checks it in as a revision.

Declaration Summary
override val languageName: String
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit)
override fun stop(inside: Box)
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit) The compiler's vocabulary and the program's own names (afield.lang.stacksl.StackSLVocabulary.complete).

StackSLStudy.kt

The revisioned study behind every StackSL program: checked edits, history on disk, and the measurements a model and a person work from.

StackSLStudy holds one program's revisions and renders them through the CPU reference, so observing, tracing, sweeping a knob and comparing two revisions give the same pixels in a test, a headless run and a live session.

src/jvmMain/kotlin/afield/lang/stacksl/study/StackSLStudy.kt — read the source

StackSLStudy

class StackSLStudy(val directory: File, readSource: () -> String, replaceSource: (expected: String, replacement: String) -> Boolean)

Talking a shader into existence: the StackSL study a document's AI4 tools, and the person beside them, work on together.

Declaration Summary
val directory: File
var rasterizer: StackSLRasterizer What draws the study's frames. The CPU reference until a tools box hands it a StackSLGpuRasterizer for a running window — then everything below renders on the GPU, and falls back, saying why, whenever the window cannot draw.
val revisions The revision history this study publishes through.
fun lineNote(source: String, line: Int): afield.ui.output.LineNotes.Note? What line of source shows, for the editor (afield.ui.output.LineNotes): the stage the line ends — the program up to it, or the value it stores for a line ending >name — drawn at the card's reference time and scaled to its own range when it runs outside 0..1, as the card's stage tiles are. Null for a line that ends no stage (a comment, a definition's body) or one whose prefix does not compile. Recent notes are kept, so moving the cursor back is free.
var onChange: (() -> Unit)? Called after anything that changes what the human view should show — a publication, a new batch of variants.
var editorLine: () -> Int? The line the person's editor cursor is on in this study's box, or null — see afield.study.source.BoxStudies.personAt.
fun resume(): String Resume from HEAD if a previous session left one, and pick up the variants it was holding if they were tried against the revision that is still current.
fun currentId(): String The checked revision's id, or empty.
fun inspect(id: String = "current"): StudyReport Everything a model needs before editing: source, live-edit state, history, checks, stages and knobs.
fun edit(expectedRevision: String, expectedSourceHash: String, source: String, description: String, previewSeconds: List<Double> = emptyList()): StudyReport Publish a complete program. See SourceStudy.edit for the guards.
fun patch(expectedRevision: String, expectedSourceHash: String, edits: List<StudySourceEdit>, description: String, previewSeconds: List<Double> = emptyList()): StudyReport Publish exact replacements against the live source.
fun setNumbers(expectedRevision: String, expectedSourceHash: String, numbers: Map<Int, Double>, description: String, previewSeconds: List<Double> = emptyList()): StudyReport Publish new values for knobs of the live source (knob index to value), rewritten in place.
fun checkIn(description: String = "edited by hand"): StudyReport Check in whatever is in the editor now — a person's own edit — as a revision.
fun restore(expectedRevision: String, expectedSourceHash: String, id: String, description: String = ""): StudyReport Make an earlier revision current again, as a new revision.
fun observe(id: String = "current", seconds: List<Double> = emptyList(), size: Int = 0, compareTo: String = ""): StudyReport Frames of a revision at seconds (a still program is rendered once), with measurements, and optionally the same frames of compareTo and a difference map beneath.
fun trace(id: String = "current", seconds: Double = StackSLStudyShader.REFERENCE_TIME, size: Int = 0, probeU: Double? = null, probeV: Double? = null): StudyReport What each stage leaves on top of the stack: one tile per line (or per word, for a one-line program), shown as colour when it already is one and stretched per channel into view when not. With probeU/probeV, also the top four stack values at that point after every root word.
fun knobs(id: String = "current", seconds: Double = StackSLStudyShader.REFERENCE_TIME, sensitivity: Boolean = true): StudyReport The knobs of a revision, and — unless sensitivity is false — how many levels a small nudge to each moves the frame.
fun sweep(id: String = "current", knob: Int, values: List<Double>, seconds: Double = StackSLStudyShader.REFERENCE_TIME, size: Int = 0): StudyReport One knob at several values, side by side. Nothing is published.
data class Variant(override val label: String, override val source: String = "", override val edits: List<StudySourceEdit> = emptyList(), override val note: String = "") : afield.study.source.StudyRequest.Variant A candidate for tryVariants: a complete program, or exact edits to the live source.
fun tryVariants(variants: List<Variant>, seconds: List<Double> = emptyList(), size: Int = 0): StudyReport Check and render several candidates beside the current program, and hold them — unpublished — for adoptVariant. A rejected candidate shows its reason in its row.
fun heldVariants(): List<Triple<String, String, String>> The candidates tryVariants is holding, label to source — what the human view offers to adopt.
fun adoptVariant(expectedRevision: String, expectedSourceHash: String, label: String, description: String = ""): StudyReport Publish a held candidate.
fun compare(before: String, after: String = "current", seconds: List<Double> = emptyList(), size: Int = 0, mode: String = "rgb", region: List<Double>? = null, beforeStage: Int? = null, afterStage: Int? = null): StudyReport Test what an edit preserved. mode "rgb" compares displayed colour, "luma" brightness only, "stage" what the two programs leave on the stack after beforeStage and afterStage. region (u0, v0, u1, v1) is where change is allowed; everything else is held to one level.
data class ExportJob(val id: String, val kind: String, val path: String, val revision: String, val frames: Int, val estimatedSeconds: Double) One export running or finished in the background — see startExport.
fun exports(): List<Map<String, Any?>> Every export this session started, newest last.
fun startExport(id: String, expectedSourceHash: String, file: File, kind: String, size: Int = 0, seconds: Double = StackSLStudyShader.REFERENCE_TIME, duration: Double = 6.0, fps: Int = 24, overwrite: Boolean = false, maxMinutes: Double = 10.0): StudyReport Start exporting a checked revision in the background and return at once with an estimate — png (one frame, at seconds) or mp4 (a clip duration long, starting at seconds). The CPU reference renders a noise-heavy program at a few thousand pixels a second, so a clip can take minutes; progress appears in exports, in inspect, and in the shader card.
fun cancelExport(jobId: String): StudyReport Stop a running export after its current frame.
fun exportPng(id: String, expectedSourceHash: String, file: File, size: Int = 1024, seconds: Double = StackSLStudyShader.REFERENCE_TIME, overwrite: Boolean = false): StudyReport Write one frame of a checked revision as a PNG.
fun ffmpeg(): File? Where ffmpeg is, or null.
fun exportVideo(id: String, expectedSourceHash: String, file: File, size: Int = 256, seconds: Double = 6.0, fps: Int = 24, start: Double = 0.0, overwrite: Boolean = false, progress: (Int) -> Unit = {}): StudyReport Render seconds of a checked revision at fps and encode an H.264 MP4 with ffmpeg.
fun previewPng(id: String = "current", size: Int = 192, seconds: Double = StackSLStudyShader.REFERENCE_TIME, parallel: Boolean = true): ByteArray A cached PNG of a revision at size — what the human view shows for history and the evidence still.

StackSLCheck

data class StackSLCheck(val warnings: List<String>, val notes: List<String>, val definitions: List<StackSLDefinitionReport>, val rootFinalDepth: Int, val prunedValues: Int, val stages: List<StackSLStage>, val knobs: List<StackSLKnob>, val animated: Boolean, val nonFiniteFraction: Double, val words: Int, val glslLines: Int)

What the check found about an admitted program.

Declaration Summary
val warnings: List<String> The compiler's warnings — each one is worth reading; reusing wrapped stack values is almost always a mistake.
val notes: List<String>
val definitions: List<StackSLDefinitionReport>
val rootFinalDepth: Int Stack depth at the end of the root; it counts the program's input, so a simple program ends at 2.
val prunedValues: Int
val stages: List<StackSLStage>
val knobs: List<StackSLKnob>
val animated: Boolean Whether frames differ over time — whether the program reads t/time in a way that shows.
val nonFiniteFraction: Double Fraction of pixels whose colour is NaN or infinite at the reference time.
val words: Int
val glslLines: Int

StackSLNumberEdit

data class StackSLNumberEdit(override val knob: Int, override val value: Double) : afield.study.ui.NumberEdit

One knob to set, by its index in the live source's knob list, and the value to write there.

StackSLDefinitionReport

data class StackSLDefinitionReport(val name: String, val finalDepth: Int, val maxDepth: Int, val pruned: Int)

A definition's stack bookkeeping: a called definition should end at depth 1.

StackSLStudyShader.kt

The single fragment shader a StackSL study renders through, with its CPU, WebGL2 and knob-finding uses.

StackSLStudyShader fixes the study frame, program(vec4(uv, hash, seconds)) on a unit square with y upwards, and finds the numbers a person typed so they can be turned and written back.

src/jvmMain/kotlin/afield/lang/stacksl/study/StackSLStudyShader.kt — read the source

StackSLStudyShader

object StackSLStudyShader

The one shader a StackSL study renders through, and the three things done with it: render on the CPU (the evidence a model is shown), translate to WebGL2 (the live preview a person turns knobs on), and find the numbers a person typed (the knobs).

Declaration Summary
const val REFERENCE_TIME The time a single still is rendered at, matching the StackSL gallery's stills.
val FRAGMENT: String The study's fragment shader: the StackSL gallery's animated template. A GPU module that uses this text as its fragment and pushes seconds into pc.params.x renders what the study renders.
val RAW_FRAGMENT: String FRAGMENT without its last two lines: the program's raw vec4, unclamped and with NaN left in. Every measurement is taken from this — clipping cannot be counted after the clamp — and the displayed colour is derived from it by displayed, which applies exactly those two lines.
val VERTEX: String The fullscreen-triangle vertex shader the GPU module uses with FRAGMENT.
fun renderRaw(program: String, size: Int, seconds: Double, parallel: Boolean = true): CpuRaster Render program's raw output at size square and time seconds. Throws on a program that does not compile.
fun displayed(raw: CpuRaster): CpuRaster What FRAGMENT draws for a raw raster: see afield.study.evidence.StudyEvidence.displayed.
fun presented(raw: CpuRaster): CpuRaster What a person sees of a raw raster in the window: see afield.study.evidence.StudyEvidence.presented.
fun linearToSrgb(v: Float): Float The sRGB transfer function (IEC 61966-2-1), for a linear value in 0..1.
fun sample(program: String, u: Double, v: Double, seconds: Double): FloatArray The raw vec4 program leaves at one point, (u, v) in the frame's 0..1 coordinates. One shader invocation — cheap enough to call once per word of a program.
data class WebGl2Program(val vertex: String, val fragment: String, val wires: List<Pair<String, Double>>) A WebGL2 translation of a program: shader text, and the uniforms that stand for its literals.
fun webgl2(program: String): WebGl2Program program as a GLSL ES 3.00 fragment shader for a browser preview: FRAGMENT's main, the program compiled with its literals externalised, and each literal a uniform — so a slider can move a number at display rate without a round trip to the JVM. The CPU reference stays the evidence; this is a view.
fun stages(program: String, outline: StackSLOutline = StackSLCompiler.outline(program)): List<StackSLStage> The program's stages: one per source line that holds root words, or — for a program written on a single line — one per root word. Each stage names how many root words run through its end, which is the count StackSLOutline.rootPrefix takes.
fun splitComment(line: String): Pair<String, String?> A source line split into its words and its comment, by the tokenizer's rules: # and // run to the end of the line and ( … ) is inline. Words never contain # or (, so the first one of those outside a comment starts one.
fun knobs(program: String, entrypoint: String? = null): List<StackSLKnob> The literals a person typed in program — rooted at entrypoint when given — each located to its digits. See the object docs.
fun colourKnobs(program: String, knobs: List<StackSLKnob> = knobs(program)): List<StackSLColourKnobs> The vector literals in program that look like colours — three components, or four with the last 1, the first three in 0..1 — each with the three knobs that hold its red, green and blue. A literal whose components share a knob (equal values share a compiler wire) is left out, since a colour control could not set them apart.
fun formatKnob(value: Double, original: String): String A knob value written the way a person would: no float noise, and integer-looking where the original was.
fun setKnobs(program: String, values: Map<Int, Double>, entrypoint: String? = null): String program with each knob in values (knob index to new value) rewritten in place, re-read and checked: the same knobs must exist afterwards and each must now hold its requested value. A value a word cannot hold — a negative frequency after a modifier letter, say — is refused with the reason, never written half-way.

StackSLStage

data class StackSLStage(val stage: Int, val line: Int, val text: String, val wordsThrough: Int, val granularity: String, val comment: String? = null)

One stage of a program: a line of root words, or a single word of a one-line program.

Declaration Summary
val stage: Int 1-based stage number.
val line: Int The source line the stage ends on.
val text: String The line's text, or the word's.
val wordsThrough: Int How many root words have run by the end of this stage.
val granularity: String "line" or "word".
val comment: String? The comment on that line, if any — what the author said the stage is for.

StackSLKnob

data class StackSLKnob(val knob: Int, val wire: Int, val value: Double, val token: String, val line: Int, val spans: List<StackSLSpan>, val role: String, val feeds: String?, val context: String, val takenAs: String? = null)

A number a person typed, located to its digits so it can be rewritten without changing its word.

Declaration Summary
val knob: Int Index among this program's knobs, in source order — the number a tool call names.
val wire: Int The compiler's wire index for this literal, which the WebGL preview's uniforms are named by.
val value: Double
val token: String The whole word the number is part of.
val line: Int 1-based line of the digits.
val spans: List<StackSLSpan> Where the digits are on that line — usually one span, several when equal components of a vector literal share one compiler wire ([3,3,3,0.08] has a knob for the three 3s together).
val role: String "number" for a plain literal, else what the compiler says the number does ("t amount", "wr frequency").
val feeds: String? For a plain literal, the next word — usually the one that consumes it.
val context: String The source line, trimmed.
val takenAs: String? The operand the compiler took the number as, in its own words — "mix amount", "smoothstep high", "multiply b" — or null when nothing took it (afield.lang.stacksl.StackSLDanglingWire.consumedAs).
val column: Int 1-based column of the first digit.

StackSLSpan

data class StackSLSpan(val column: Int, val length: Int)

A run of digits within a source line: 1-based column and length in characters.

StackSLColourKnobs

data class StackSLColourKnobs(val knobs: List<Int>, val linear: List<Double>)

A colour literal's red, green and blue knobs, and its colour as linear light: see StackSLStudyShader.colourKnobs.

Declaration Summary
val hex: String The colour as the window shows it: sRGB-encoded, #rrggbb.

StackSLStudyTools.kt

The tools an AI4 conversation gets when it is opened on a StackSL program's stackSL property.

Each works on the focused box's own study (see StackSLPrograms), so nothing here is per document. A document can still add tools of its own from a box: AgentToolExtensions.register with its document as the scope.

src/jvmMain/kotlin/afield/lang/stacksl/study/StackSLStudyTools.kt — read the source

Used in: Talking a shader into existence

Top-level functions

Declaration Summary
fun installStackSLStudyTools() Register the StackSL study tools once, for every conversation opened on a program's stackSL property. Each call works on the focused box's own study.

StackSLStudyGuide

data class StackSLStudyGuide(@JsonPropertyDescription("A word or phrase to look up in the StackSL words guide; empty returns the working guide") val search: String = "")

The working guide for StackSL studies, or the words guide's entries matching a search term.

Declaration Summary
val search: String A word or phrase to look up in the words guide; empty returns the working guide.

InspectStackSLStudy

data class InspectStackSLStudy(override val revision: String = "current") : StudyRequest.Inspect

EditStackSLStudy

data class EditStackSLStudy(override val expectedRevision: String, override val expectedSourceHash: String, @JsonPropertyDescription("Complete StackSL source. Keep it human-editable: one transform per line, # comments naming what each stage does, : name ... ; definitions for motifs.") override val source: String, override val description: String, @JsonPropertyDescription("Times in seconds for the preview frames; empty chooses 0, 1.75, 3.5, 7 for animated programs.") override val previewSeconds: List<Double> = emptyList()) : StudyRequest.Edit, StudyRequest.Preview

Declaration Summary
override val expectedRevision: String
override val expectedSourceHash: String
override val source: String
override val description: String
override val previewSeconds: List<Double>

PatchStackSLStudy

data class PatchStackSLStudy(override val expectedRevision: String, override val expectedSourceHash: String, override val edits: List<StudySourceEdit>, override val description: String, override val previewSeconds: List<Double> = emptyList()) : StudyRequest.Patch, StudyRequest.Preview

Declaration Summary
override val edits: List<StudySourceEdit>, override val description: String, override val previewSeconds: List<Double>

SetStackSLNumbers

data class SetStackSLNumbers(override val expectedRevision: String, override val expectedSourceHash: String, override val numbers: List<StackSLNumberEdit>, override val description: String, override val previewSeconds: List<Double> = emptyList()) : StudyRequest.SetNumbers, StudyRequest.Preview

Declaration Summary
override val numbers: List<StackSLNumberEdit>, override val description: String, override val previewSeconds: List<Double>

ObserveStackSLStudy

data class ObserveStackSLStudy(override val revision: String = "current", override val seconds: List<Double> = emptyList(), @JsonPropertyDescription("Pixels square, at most 256; 0 chooses 256 for a still program and 160 for an animated one") override val size: Int = 0, override val compareTo: String = "") : StudyRequest.Observe, StudyRequest.Sized

Declaration Summary
override val size: Int
override val compareTo: String

TraceStackSLStudy

data class TraceStackSLStudy(val revision: String = "current", val seconds: Double = 1.75, @JsonPropertyDescription("uv x of a point to probe, 0..1; negative for none") val probeU: Double = -1.0, @JsonPropertyDescription("uv y of a point to probe, 0..1 with y up; negative for none") val probeV: Double = -1.0)

Declaration Summary
val probeU: Double
val probeV: Double

KnobsStackSLStudy

data class KnobsStackSLStudy(val revision: String = "current", val seconds: Double = 1.75, val sensitivity: Boolean = true)

SweepStackSLNumber

data class SweepStackSLNumber(override val revision: String = "current", override val knob: Int, override val values: List<Double>, override val seconds: Double = 1.75)

TryStackSLVariants

data class TryStackSLVariants(override val variants: List<StackSLStudy.Variant>, override val seconds: List<Double> = emptyList()) : StudyRequest.TryVariants

AdoptStackSLVariant

data class AdoptStackSLVariant(override val expectedRevision: String, override val expectedSourceHash: String, override val label: String, override val description: String = "")

RestoreStackSLRevision

data class RestoreStackSLRevision(override val expectedRevision: String, override val expectedSourceHash: String, override val revision: String, override val description: String = "")

CompareStackSLStudy

data class CompareStackSLStudy(override val beforeRevision: String, override val afterRevision: String = "current", override val seconds: List<Double> = emptyList(), override val mode: String = "rgb", override val allowedRegion: List<Double> = emptyList(), val beforeStage: Int = 0, val afterStage: Int = 0) : StudyRequest.Compare

Declaration Summary
override val mode: String

ExportStackSLStudy

data class ExportStackSLStudy(val revision: String, val expectedSourceHash: String, val filename: String, val kind: String = "png", @JsonPropertyDescription("Pixels square; 0 means 1024 for png and 256 for mp4") val size: Int = 0, @JsonPropertyDescription("The still's time, or the clip's first frame, in seconds") val seconds: Double = 1.75, val durationSeconds: Double = 6.0, val fps: Int = 24, val overwrite: Boolean = false, @JsonPropertyDescription("Refuse, with a size that fits, if the estimate exceeds this; raise it only when the person agrees to wait") val maxMinutes: Double = 10.0)

Declaration Summary
val size: Int
val seconds: Double
val durationSeconds: Double
val maxMinutes: Double

StackSLStudyGuides

object StackSLStudyGuides

The working guide and the words guide the StackSLStudyGuide tool serves.

Declaration Summary
val working: String by lazy How to work on a StackSL study.
val words: String by lazy Every StackSL word with its signature and a line of description, as Markdown sections.
fun search(query: String, limit: Int = 40): String The words guide's entries (lines, with their section heading) that mention query, at most limit of them.

StackSLGuideText

data class StackSLGuideText(val text: String) : afield.ai.tools.SummarisesForToolLog

The StackSLStudyGuide tool's answer.

Declaration Summary
override fun toolLogSummary(): String

StackSLStudyViewer.kt

The output card a person uses on a StackSL study: a live WebGL2 preview, sliders for the typed numbers, per-line stages, variants and history.

StackSLStudyViewer makes the same transactions on the study as the model's tools, so a knob kept on the card becomes a checked revision like any other edit.

src/jvmMain/kotlin/afield/lang/stacksl/study/StackSLStudyViewer.kt — read the source

StackSLStudyViewer

class StackSLStudyViewer(val study: StackSLStudy, val owner: Box, val channel: String = "shader", val previewSize: Int = 320)

The person's side of a StackSL study: one output card that shows the program running, lets them turn its numbers, and lets them keep, adopt, restore or check in — the same transactions the model's tools make, from the same study.

Declaration Summary
val study: StackSLStudy
val owner: Box
val channel: String
val previewSize: Int
fun refreshLater() Rebuild the card soon, on the viewer's own thread; repeated requests while one is pending collapse into it.
fun show() Rebuild the card and open its channel. Called on Field's main thread — as the tools box does — it hands the work to the viewer's thread instead: the card's images render on the window's GPU, which waits for a frame the main thread draws, so rendering here would fall back to the CPU reference and hold up the main thread while it ran.
fun html(): String The card's HTML for the study's current state.