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
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
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
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
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
A body item. Heads are always a positive DlAtom.
DlAtom
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
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
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
Everything one chunk of source contributed.
| Declaration | Summary |
|---|---|
val rules: List<DlRule> |
|
val queries: List<DlQuery> |
|
val directives: List<DlDirective> |
DlException
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
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.roundOfis strictly greater than the rounds of the facts that derived it. That is what makesDatalog.whyterminate 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
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
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
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
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.
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
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
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
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
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:
ClojureHostis the interpreter, and the only part with no Field in it. It reads one top-level form at a time and evaluates it, which isload-string's own loop written out so that the line each form started on is available while that form runs: that line is what lets printed output land beside the form that printed it, the way it does in every other kind of Field box. It returns the value of the last form, andexecuteIfFnwill invoke that value when it is anIFn— a host convenience, off on the box path (ClojureExecutionpassesfalse), so a box whose last form evaluates to a function prints the function rather than calling it.compileOnlyis the same read loop with evaluation withheld, which is where the editor's syntax errors come from, carrying line and column.ClojureInterfaceis the host facade: the box currently running, and the handful of Field verbs that are awkward to reach by raw interop because they are Kotlin extension functions with default arguments.field/core.clj(insrc/jvmMain/resources) is the Clojure side of it,required andreferred into every box namespace, so a box can say(lines ls :fit true)rather than namingSomeSkikoKt.ClojureExecutionis theBoxExecution, so it is the half that knows about boxes: output proxying,is-running, per-fragment generation counting, andcomplete, whose candidates come from Compliment if that namespace loads. Without it on the classpathcompletereturns an empty list rather than throwing, so a box runs and completes nothing.installClojureandclojureTopologyNeedsUpdatingare the registration, and they are identical in shape to every other main language: oneregisterMainLanguageRuntimecall, oneTopologyWatcherthat swaps a child box's execution when the graph changes.
One namespace, not one per box: every Clojure box in every open document evaluates in live.user
(Clojure namespaces are per-JVM, and ClojureExecution asks for that one name), so two Clojure
boxes are two cells of one notebook. public/docs/clojure.md is the page that says so to a
reader; dev_docs/small_language_anatomy.md is the map of the integration points.
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
| 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
| 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
| 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.
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
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
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.
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
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 uncheckedHoststops here with a sentence rather than a default. ThehostAs…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.
propandpropOrsearch up the box graph, so a value an ancestor wrote is visible;oncereads only its own box's slot; andsetPropalways 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 aprop<Int>shared with a Kotlin box from throwingClassCastExceptionon 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Matches one constructor and its arguments, from TPConstructor.
| Declaration | Summary |
|---|---|
val name: String |
|
val arguments: List<CorePattern> |
|
override val type: Type |
CPCons
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
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
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
Everything a FieldML expression can evaluate to.
RuntimeEnv
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
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
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
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.
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
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.
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
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
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
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
Hostis 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. WhathostGetreturns 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:
Floatfor numeric,Vec2for 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
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.recursiveis true for thelet f x = ...function form, which is the form whose own name is in scope inside its body.TExprLet.parametersrepeats the parameters of the lambda that the binding's value became, and is empty when the value is not a lambda.TypedConstructorgives each constructor of a declared data type its arity and theSchemeits name was bound under, soSomeis 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
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
EInt typed, which is always Int.
TExprFloat
EFloat typed, which is always Float.
TExprBool
EBool typed, which is always Bool.
TExprString
EString typed, which is always String.
TExprVar
EVar typed: a name, and the type the environment had for it.
TExprTuple
ETuple typed, carrying a TTuple over its elements' types.
TExprList
EList typed, carrying a TList of one element type.
TExprTypeDecl
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
ELet typed, with a type beside each parameter name.
TExprLambda
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
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
EIf typed.
| Declaration | Summary |
|---|---|
val condition: TExpr |
|
val thenBranch: TExpr |
|
val elseBranch: TExpr |
|
override val range: SourceRange |
|
override val type: Type |
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
PVar typed. The type here is what the name is bound to inside the arm's body.
TPInt
PInt typed, which forces the scrutinee to Int.
TPBool
PBool typed, which forces the scrutinee to Bool.
TPString
PString typed, which forces the scrutinee to String.
TPTuple
PTuple typed, element by element against a TTuple of the same width.
TPList
PList typed: every element against the one element type of a TList.
TPConstructor
PConstructor typed.
| Declaration | Summary |
|---|---|
val name: String |
|
val arguments: List<TPattern> |
|
override val range: SourceRange |
|
override val type: Type |
TPCons
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
EMatch typed.
| Declaration | Summary |
|---|---|
val scrutinee: TExpr |
|
val cases: List<TMatchCase> |
|
override val range: SourceRange |
|
override val type: Type |
TExprRecord
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
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
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
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
An inferred FieldML type.
TInt
The type of an integer.
TFloat
The type of a floating-point number.
TBool
The type of a boolean.
TString
The type of a string.
TUnit
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
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
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
The interpreter: the data stack, the dictionary, the tokenizer and eval.
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
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
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
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
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
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); @morphoor@morpho(fallback=X), thendef 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
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
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
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
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
A MorphoHDL parse, compile or expansion failure, with a source line where morpho supplied one.
| Declaration | Summary |
|---|---|
val line: Int? |
MorphoSession
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
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
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
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
| 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
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
How MorphoSearch.evolve picks a parent.
Constants: TOURNAMENT, LEXICASE.
MorphoSearch
Search drivers. All three take the same budget and return the same shape of result.
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
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
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
The role a port plays in a width-parametric design.
Constants: BUS, BIT.
MorphoSpec
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
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
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
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
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
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
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.
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) andO(overflow) - a call stack eight deep, for
CALLandRET - 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
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.
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
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
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
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,PythonSettingsStatusandPythonInstallResultare that negotiation, andrestartRequiredmeans it: a new interpreter binding cannot be swapped into a live JVM.makeInterpreterruns 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, theBoxExecution. Beyond the usual output proxying it carries the cooperative event loop: anasyncioloop implemented in Python that yields to Field's runloop instead of blocking it, soawaitinside a box does not stop the editor.- Output. Everything a box prints funnels through one formatter, and three cases are
special: a
\rbecomes a Field progress bar (which is howtqdmrenders as a real one), an object exposingHTML/Image/Markdown/Math/JSONrenders as itself, and a library that tries to open a browser page — plotly'sfig.show(), bokeh, folium — is intercepted and the HTML delivered into the box as an iframesrcdoc. Nothing here opens a browser. - Registration.
installPythonandpythonTopologyNeedsUpdatingare the same two-call shape every main language uses; seedev_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
PythonInstallResult
| 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.
PythonExecution
class PythonExecution : BoxExecution
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
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
RectLangTokenizeris the whole of the syntax. There is no parser: the language is postfix, so tokenizing is parsing.RectLangVmis the evaluator, and where the vocabulary lives. Words spelled->somethingare mutators that write through to the active rect, group or scene; everything else transforms stack values. The arrow is part of the word.SceneProgramis 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 areTracks 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 aSampledScene, a flat z-ordered list ofSampledRects with no tracks left in it.lowerSampledSceneturns that into aRectMesh: fourRectVertexes 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
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)
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
| 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
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
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
Constants: Double, Paint.
TrackOp1
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
Constants: ADD, SUB, MUL, DIV, MOD.
| Declaration | Summary |
|---|---|
val eval: (Double, Double) -> Double |
|
val glsl: (String, String) -> String |
|
val fn: RectSLFnLowering<TrackFnBinary> |
TrackOp3
Constants: CLAMP, MIX, SMOOTHSTEP.
| Declaration | Summary |
|---|---|
val eval: (Double, Double, Double) -> Double |
|
val glsl: (String, String, String) -> String |
|
val fn: RectSLFnLowering<TrackFnTernary> |
TimeWarpKind
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
| 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
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
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
| 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
| Declaration | Summary |
|---|---|
val a: Track<Double> |
|
val b: Track<Double> |
|
val op: TrackOp2 |
|
override val kind: TrackValueKind |
|
override fun sample(t: Double): Double |
TernaryDoubleTrack
| 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
| Declaration | Summary |
|---|---|
val source: Track<Double> |
|
val warp: TimeWarpKind |
|
val amount: Double |
|
override val kind: TrackValueKind |
|
override fun sample(t: Double): Double |
GateTrack
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
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
Constants: Meters, Pixels, Abstract.
Origin
Constants: Center, TopLeft.
FitPolicy
Constants: Contain, Cover, Stretch.
WallSpec
| Declaration | Summary |
|---|---|
val width: Double |
|
val height: Double |
|
val unit: UnitKind |
|
val origin: Origin |
PaintSpec
GradientSpace
Constants: Local, World.
MaterialBinding
| Declaration | Summary |
|---|---|
val materialId: Int |
|
val shaderData: Vec4f |
|
val paramBase: Int |
|
val flags: Int |
|
val name: String |
Node
| Declaration | Summary |
|---|---|
val id: String |
RectNode
GroupNode
ShaderModuleSpec
| Declaration | Summary |
|---|---|
val name: String |
|
val materialId: Int |
|
val source: String |
|
val prefix: String |
|
val glsl: String? |
|
val diagnostics: List<String> |
MaterialTable
| Declaration | Summary |
|---|---|
val modules: List<ShaderModuleSpec> |
|
val params: FloatArray |
SceneProgram
| 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
| 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
RectVertex
| 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
| Declaration | Summary |
|---|---|
val position: String |
|
val color: String |
|
val material: String |
|
val shaderData: String |
RectMaterialShaderCode
| Declaration | Summary |
|---|---|
val insertion: String |
|
val exampleFragmentShader: String |
|
val compiledMaterials: List<StackSLRectMaterialCompileResult> |
|
val ok: Boolean |
|
val diagnostics: List<String> |
FramePacket
| Declaration | Summary |
|---|---|
val time: Double |
|
val mesh: RectMesh |
|
val materialTable: MaterialTable |
|
val semantic: SemanticFrame |
SemanticFrame
| Declaration | Summary |
|---|---|
val rects: List<SampledRect> |
|
val contacts: List<ContactPair> |
|
val metrics: FrameSemanticMetrics |
RectCollider
| 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
MotionMetrics
FrameSemanticMetrics
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.
AnimationReport
AnimationWarning
sealed interface AnimationWarning
AnalysisSettings
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
| 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
ProgramEdit
RectAnimationOracle
object RectAnimationOracle : AnimationOracle
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
RectLangVm
NodeRef
| Declaration | Summary |
|---|---|
val id: String |
|
data class Rect(override val id: String) : NodeRef |
|
data class Group(override val id: String) : NodeRef |
SceneBuilder
StackSLRectMaterialCompiler
| Declaration | Summary |
|---|---|
fun compile(module: ShaderModuleSpec): StackSLRectMaterialCompileResult |
StackSLRectMaterialConvention
| Declaration | Summary |
|---|---|
val inputExpression: String |
|
val outputExpression: String |
|
fun wrap(module: ShaderModuleSpec, result: StackSLCompileResult): String |
StackSLRectMaterialCompileResult
| Declaration | Summary |
|---|---|
val module: ShaderModuleSpec |
|
val stackSL: StackSLCompileResult |
|
val rectFragGlsl: String? |
|
val ok: Boolean |
RectSLRenderSettings
| 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
| 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
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:
- Obliquity. Every kernel in
CoverageKernelis 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 isclamp(0.5 - d), soCoverageKernel'sLINEARatbeta= 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. - 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. - Union seams.
minof 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 reasonCoverageCompositehas 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
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
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
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
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
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, anda - b·trunc(a/b)in double arithmetic is a different function once the quotient outgrows 53 bits:1e17 % 3is1.0and the reconstruction says0.0. Silently, and as a zero, which is indistinguishable from the guard having fired. A constant pair still folds.- The pattern tracks (
PatTrackand friends) — an interpreter over a mini-notation tree, not an expression of fixed shape.PatPatternis a(PatArc) -> List<PatEvent>lambda: there is nothing to inspect and no closed form to find, which is the same reasonFnNonDifferentiablefilespatitself, 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 — becausepatused as scene structure is unrolled at compile time into one gated rect per event, so no interpreter survives into the sampled IR, andmusicality-variationlowers all 1096 of its tracks despite spending sixpatverbs. 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,randall push aVSig, andVSig.asTrackis aPatTrack— or one of the two gates the pattern layer installs on a rect's alpha,->everyon the cycle count and->degradeon 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 cyclePatTrackquantises 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 onsawandisaw, whose once-a-cycle wrap a straight line would smear; randis 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
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
How a bake is written into GLSL. See RectSLSdfBakeFidelity for what it costs.
Constants: HOLD, RAMP.
RectSLSdfBake
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
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
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
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
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.
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
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
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
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
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
minunion — is written inADdirectly, transcribed from the GLSL inRectSLGlsl.PRELUDEand the composition inTransform2d.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
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
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
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
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
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
How per-rect coverage becomes one number per pixel.
Constants: UNION_SDF, OVER.
SoftRasterSpec
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
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
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
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
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
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
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
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.
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
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
What the check found about an admitted scene.
RectSLLineReport
A line of the program that makes rectangles or groups.
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
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
What a study draws: a compiled scene, the source it came from (the key a GPU pipeline is cached by), and its compiled materials.
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).
RectSLGpuRasterizer
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).
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
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
PatchRectSLStudy
| Declaration | Summary |
|---|---|
override val edits: List<StudySourceEdit>, override val description: String, override val previewSeconds: List<Double> |
RectSLNumberEdit
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
| Declaration | Summary |
|---|---|
override val numbers: List<RectSLNumberEdit>, override val description: String, override val previewSeconds: List<Double> |
ObserveRectSLStudy
| Declaration | Summary |
|---|---|
override val size: Int |
|
override val compareTo: String |
TimelineRectSLStudy
| Declaration | Summary |
|---|---|
val frames: Int |
TraceRectSLStudy
| Declaration | Summary |
|---|---|
val probeY: Double? |
KnobsRectSLStudy
SweepRectSLNumber
TryRectSLVariants
AdoptRectSLVariant
RestoreRectSLRevision
CompareRectSLStudy
| Declaration | Summary |
|---|---|
override val mode: String |
ExportRectSLStudy
| Declaration | Summary |
|---|---|
val size: Int |
|
val seconds: Double |
|
val loops: Int |
|
val maxMinutes: Double |
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
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. |
RectSLWindowLinks.kt
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
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
One rewrite pass over one fragment: set options, then call parseAndReconstruct.
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
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
| 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
JsFun
JsTypeDecl
JsTopVal
| 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
| Declaration | Summary |
|---|---|
val receiver: String |
|
val operator: Boolean |
|
val name: String |
|
val params: List<JsParam> |
|
val ret: JsType |
|
val compilerBody: String |
JsBindingModel
| 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
TypeBuilder
| 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
| 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; aliasesdefaultandremotejs, and what a box with no marker at all gets.remoteAR— the A-Frame and THREE scene head; aliasesar,remote-ar,remotear.tone— the Tone.js computer-music head; aliasestonejsandmusic.visionmetal— the visionOS Metal head; aliasesmetalandvision-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
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
| Declaration | Summary |
|---|---|
val kind: String |
|
val headId: String |
|
val expectedHeadIds: Set<String> |
|
val terminal: Boolean |
KotlinJsHeadSelection
| Declaration | Summary |
|---|---|
val profile: KotlinJsHeadProfile |
|
val source: String |
|
val options: Map<String, String> |
|
val explicit: Boolean |
|
fun optionList(name: String): List<String>? |
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
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
The compiler itself: compile is the entry point, Mode picks how it runs and Profile picks the shape of what comes out.
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
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:
doASTAroutes the source throughAstafirst, which rewrites each binary operator into a call the head'sasta.jsdefines, 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 aSyntaxErrorthe 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.lifecycleNameandlaunchableare what make a fragment restartable and time-scrubbable rather than fire-and-forget, andtimeStart/timeEndare the window it is live for.handleTois the return channel: a head can send values back, dispatched throughreturnHandlers, 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
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
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
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
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
StackSLCompileOptionsdecides 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 texturessamplemay read, and — the one that changes semantics —defaultValues, the wrap/default convention used when a word wants an operand the stack does not have.tolerantdecides whether that is a warning or an error.StackSLCompileResultis GLSL, diagnostics and an analysis.okis diagnostics with no ERROR, not "the GLSL is non-empty", because a tolerant compile emits usable GLSL and complains at the same time.StackSLAnalysisis the part worth reading before debugging a program that produced a black frame. Per function it reportsmaxStackDepth,finalStackDepth,producedValues,usedValuesandprunedValues, plusStackSLDanglingWirefor 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
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
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
| Declaration | Summary |
|---|---|
val severity: StackSLSeverity |
|
val message: String |
|
val location: StackSLSourceLocation? |
|
val token: String? |
|
override fun toString(): String |
StackSLCompileOptions
StackSLFunctionAnalysis
StackSLDanglingWire
StackSLAnalysis
| 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
| Declaration | Summary |
|---|---|
val glsl: String |
|
val diagnostics: List<StackSLDiagnostic> |
|
val analysis: StackSLAnalysis |
|
val ok: Boolean |
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
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
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
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.ktsays the same. floor,ceil,trunc,round,roundEven,sign,stepandint(...)truncation carry exactly zero derivative.fracttherefore carries derivative one, since it isx - floor(x).abs,minandmaxsplit 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,isnanandisinfproduce a derivative-freebool. 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.StackSLDifferentiabilityTestcounts 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
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
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
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,atanare 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 + cinto a fused multiply-add with one rounding while the JVM performs two. Kotlin has nofma-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
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
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
Render StackSL on the CPU.
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
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
One gallery entry as it exists on disk.
GalleryAgreement
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
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
- 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.fittedIndicesis the population that moved andWiredProgram.wiresis the larger set that exists. - The template clamps.
StackSLCpu.stillTemplate, the default, endsmain()withoutColor = vec4(clamp(c, 0.0, 1.0), 1.0), andclampcarries 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.saturatedFractionmeasures how much of the image is in that state. - 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.
StackSLFitFigureTestwrites a figure of the target, the seed and the fit side by side, which is the comparison the loss itself cannot make.StackSLFitLoss.LPIPSdescends on the perceptual distance instead, with the gradient of the LPIPS network chained through the program (StackSLLpipsLoss);StackSLFitLoss.LPIPS_PIXELon the mean of the two.
src/jvmMain/kotlin/afield/lang/stacksl/cpu/StackSLFit.kt — read the source
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
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
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
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
One square of a StackSLIntervals.certifiedTiles subdivision: pixels [x, x+size) × [y, y+size).
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
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
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:
- element i of
<prefix>_wiresbecomes a scalar global<prefix>_wire_i, a shape the subset does support, and one a caller can push a new value into per evaluation withsetGlobal, with no recompile; - the wire parameter and the wire argument disappear from every signature and call;
const int <prefix>_wire_count,<prefix>_init_wiresand 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
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
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
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
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
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
What a slot study checks a candidate program for: that it compiles into this slot, and what it says.
StackAutoSlotStudy
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
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
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
EditStackAutoSlot
| Declaration | Summary |
|---|---|
override val source: String |
|
override val description: String |
|
val alone: Boolean |
|
val moments: Int |
PatchStackAutoSlot
| Declaration | Summary |
|---|---|
override val edits: List<StudySourceEdit>, override val description: String, val alone: Boolean |
|
val moments: Int |
StackAutoNumberEdit
One number to turn: knob index from Inspect, and its new value.
SetStackAutoNumbers
| Declaration | Summary |
|---|---|
override val expectedRevision: String |
ObserveStackAutoSlot
| Declaration | Summary |
|---|---|
val moments: Int |
SweepStackAutoNumber
RestoreStackAutoRevision
| Declaration | Summary |
|---|---|
override val revision: String, override val description: String |
CompareStackAutoSlot
| Declaration | Summary |
|---|---|
override val seconds: List<Double> |
StackAutoVariant
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
AdoptStackAutoVariant
| 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
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
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
One problem with a StackSL program, as the editor shows it: an error refuses the program.
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.
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
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
What the check found about an admitted program.
StackSLNumberEdit
One knob to set, by its index in the live source's knob list, and the value to write there.
StackSLDefinitionReport
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
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
One stage of a program: a line of root words, or a single word of a one-line program.
StackSLKnob
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
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
PatchStackSLStudy
| Declaration | Summary |
|---|---|
override val edits: List<StudySourceEdit>, override val description: String, override val previewSeconds: List<Double> |
SetStackSLNumbers
| Declaration | Summary |
|---|---|
override val numbers: List<StackSLNumberEdit>, override val description: String, override val previewSeconds: List<Double> |
ObserveStackSLStudy
| Declaration | Summary |
|---|---|
override val size: Int |
|
override val compareTo: String |
TraceStackSLStudy
| Declaration | Summary |
|---|---|
val probeU: Double |
|
val probeV: Double |
KnobsStackSLStudy
SweepStackSLNumber
TryStackSLVariants
AdoptStackSLVariant
RestoreStackSLRevision
CompareStackSLStudy
| Declaration | Summary |
|---|---|
override val mode: String |
ExportStackSLStudy
| Declaration | Summary |
|---|---|
val size: Int |
|
val seconds: Double |
|
val durationSeconds: Double |
|
val maxMinutes: Double |
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
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. |