Skip to content

RectSLExamples.kt

src/jvmMain/kotlin/afield/examples/rectsl/RectSLExamples.kt · 571 lines

package afield.examples.rectsl

import afield.runtime.kotlin.KotlinInterface2
import afield.runtime.kotlin.KotlinScriptEngine
import afield.lang.rectsl.RectAnimationOracle
import afield.lang.rectsl.compileRectSL
import afield.lang.rectsl.lowerSampledScene
import afield.lang.rectsl.rectMaterialShaderCode
import afield.math.functions.Functions
import java.io.File

object RectSLExamples

FIELD_TEST id: rectsl.examples.smoke runner: jvm-main entrypoint: afield.examples.rectsl.RectSLExamples tags: [smoke, examples, rectsl] timeoutSec: 180 expect: exitCode: 0 stdoutContains: - "rectsl examples passed"

val minimalMirror: String

    val minimalMirror: String = """
        #rectsl
        ->wall 30 4 meters
        ->duration 10
        ->fps 60

        "left" rect
          -5 0 ->xy
          1 ->square
          0xff3300 solid ->fill
          '( 0=-5   5=0   10=5 ) ->x
        ;

        dup "right" ->rename
          ->mirror-x
          0x3366ff solid ->fill
        ;
    """.trimIndent()

val layeredTracks: String

    val layeredTracks: String = """
        #rectsl
        ->wall 16 9 meters
        ->duration 6
        ->fps 30

        linear-track "rise" '( 0=-3   3=0   6=3 )
        hold-track "pulse" '( 0=0.2   2=1.0   4=0.45 )

        "bar-template" rect
          0 -2 ->xy
          0.5 1.4 ->wh
          "rise" track-ref ->y
          "pulse" track-ref smooth ->alpha
          0xeeeeee solid ->fill
        ;

        8 '(
          "bar-template" ->select
            clone
            i ->layer
            i 0.1 * ->z
            i 3.5 - 0.8 * -2 ->xy
        ) repeat
    """.trimIndent()

val groupsAndStackSLMaterial: String

    val groupsAndStackSLMaterial: String = """
        #rectsl
        ->wall 10 10 meters
        ->duration 8
        ->fps 30

        material petals-tint '(
          uv 4.0 * sin abs
        )

        "orbit" group
          "cell" rect
            0 0 ->xy
            0.75 ->square
            "petal" ->tag
            '( 0 0   4 3.14159   8 6.28318 ) ->rot
            0xffffff solid ->fill
          ;

          0 6 '(
            "cell" ->select
              clone
              i 2 * 5 - 0 ->xy
          ) range
        end-group

        "orbit" ->select descendants
          "petal" tagged
          "petals" ->group-selected
          selected descendants rects '(
            0.85 ->alpha
            petals-tint ->material
          ) each
        ;
    """.trimIndent()

fun main(args: Array<String>)

    @JvmStatic
    fun main(args: Array<String>) {
        listOf(
            "minimalMirror" to minimalMirror,
            "layeredTracks" to layeredTracks,
            "groupsAndStackSLMaterial" to groupsAndStackSLMaterial,
        ).forEach { (name, source) ->
            val result = RectAnimationOracle.compile(source)
            check(result.ok && result.programOrNull != null) {
                "$name failed:\n" + result.diagnostics.joinToString("\n")
            }
        }

        checkFaceTravelHeading()
        checkLifeWindows()
        checkSplit()
        checkRecursiveSplit()
        checkArrayGenerators()
        checkGalleryWorkspace()
        checkPublicDocsExamples()
        checkCheatsheetSnippets()
        checkCheatsheetCoverage()
        println("rectsl examples passed")
    }

face-travel must set rotation to the heading of the object's own x/y tracks. A dart travelling the +x/+y diagonal has a constant heading of atan2(1,1)=pi/4, an unambiguous oracle for the finite-difference HeadingTrack. face-travel-at adds a fixed offset, checked here as pi/4 + pi/2.

    private fun checkFaceTravelHeading() {
        fun rotationAtMid(word: String): Double {
            val src = """
                #rectsl
                ->wall 10 10 abstract
                ->duration 4
                ->fps 30
                "dart" rect
                  1 0.3 ->wh
                  0xffffff solid ->fill
                  '( 0=-5  4=5 ) ->x
                  '( 0=-5  4=5 ) ->y
                  $word
                ;
            """.trimIndent()
            val r = RectAnimationOracle.compile(src)
            check(r.ok && r.programOrNull != null) { "$word failed:\n" + r.diagnostics.joinToString("\n") }
            return r.program.sample(2.0).rects.first().rotation
        }
        val quarter = Math.PI / 4.0
        val plain = rotationAtMid("->face-travel")
        check(Math.abs(plain - quarter) < 1e-3) { "face-travel heading wrong: got $plain expected $quarter" }
        val offset = rotationAtMid("1.5707963 ->face-travel-at")
        check(Math.abs(offset - (quarter + Math.PI / 2.0)) < 1e-3) {
            "face-travel-at heading wrong: got $offset expected ${quarter + Math.PI / 2.0}"
        }
        println("rectsl face-travel heading check: face-travel=$plain, face-travel-at=$offset")
    }

appear/vanish hard-gate alpha at fixed times; live birth life additionally shifts the object's tracks so local time 0 is its birth. Checked with exact 0/1 alpha at window edges and the trajectory's start value landing at the birth time.

    private fun checkLifeWindows() {
        val src = """
            #rectsl
            ->wall 10 10 abstract
            ->duration 6
            ->fps 30
            "beacon" rect
              0 0 ->xy
              1 ->square
              0xffffff solid ->fill
              1 ->appear
              4 ->vanish
            ;
            "mover" rect
              1 ->square
              0xffffff solid ->fill
              '( 0=-4  2=4 ) ->x
              2 2 ->live
            ;
        """.trimIndent()
        val r = RectAnimationOracle.compile(src)
        check(r.ok && r.programOrNull != null) { "life windows failed:\n" + r.diagnostics.joinToString("\n") }
        val p = r.program
        fun rect(t: Double, id: String) = p.sample(t).rects.first { it.id == id }
        check(rect(0.5, "beacon").alpha == 0.0) { "beacon should be invisible before appear" }
        check(rect(2.0, "beacon").alpha == 1.0) { "beacon should be visible inside window" }
        check(rect(4.5, "beacon").alpha == 0.0) { "beacon should be invisible after vanish" }
        check(rect(1.0, "mover").alpha == 0.0) { "mover should be invisible before birth" }
        check(rect(2.0, "mover").alpha == 1.0) { "mover should be visible at birth" }
        val bornX = rect(2.0, "mover").center.x
        check(Math.abs(bornX - (-4.0)) < 1e-9) { "mover trajectory should start (-4) at its birth t=2, got $bornX" }
        check(rect(4.5, "mover").alpha == 0.0) { "mover should be invisible after death" }
        println("rectsl life-window check: beacon+mover gating OK (born x=$bornX)")
    }

split must (a) vanish the template at the split time, (b) birth count shards exactly at the template's split-point position, and (c) let each shard's authored flight carry it outward from there. The shell rises to (0,0) by t=2, so all shards start at the origin and later fly out to a nonzero radius.

    private fun checkSplit() {
        val src = """
            #rectsl
            ->wall 10 10 abstract
            ->duration 6
            ->fps 30
            "shell" rect
              0.5 ->square
              0xffffff solid ->fill
              '( 0=-4  2=0 ) ->y
              4 2 '(
                '( 0=0  2=5 )  f 6.28318 * cos  value*
                '( 0=0  2=5 )  f 6.28318 * sin  value*
                ->translate
              ) ->split
            ;
        """.trimIndent()
        val r = RectAnimationOracle.compile(src)
        check(r.ok && r.programOrNull != null) { "split failed:\n" + r.diagnostics.joinToString("\n") }
        val p = r.program
        fun shards(t: Double) = p.sample(t).rects.filter { it.id != "shell" }
        fun shell(t: Double) = p.sample(t).rects.first { it.id == "shell" }
        check(shell(1.0).alpha == 1.0) { "shell should be alive before the split" }
        val before = shards(1.0)
        check(before.size == 4) { "expected 4 shards, got ${before.size}" }
        check(before.all { it.alpha == 0.0 }) { "shards must be invisible before the split" }
        check(shell(2.0).alpha == 0.0) { "shell must vanish at the split" }
        val atSplit = shards(2.0)
        check(atSplit.all { it.alpha == 1.0 }) { "shards must be visible at the split" }
        check(atSplit.all { Math.hypot(it.center.x, it.center.y) < 1e-9 }) { "shards must start at the split point" }
        check(shards(4.0).all { Math.hypot(it.center.x, it.center.y) > 1.0 }) { "shards should fly outward after the split" }
        println("rectsl split check: shell -> 4 shards born at split point, flying outward")
    }

A nested split must compose: a grand-shard is born at t1 + t2 (its split time adds onto its parent's) and starts exactly on its parent shard's split-point. Here the seed splits at t=1 into a shard flying +x; that shard splits again at local t=1 (global t=2) into a grand-shard flying +y. At t=1 only the shard is alive at (0,0); at t=2 the shard has died and the grand-shard is born at (1,0) — where the shard was.

    private fun checkRecursiveSplit() {
        val src = """
            #rectsl
            ->wall 10 10 abstract
            ->duration 8
            ->fps 30
            "seed" rect
              0.4 ->square
              0xffffff solid ->fill
              1 1 '(
                '( 0=0  4=4 ) linear ->x
                1 1 '(
                  '( 0=0  4=4 ) linear ->y
                ) ->split
              ) ->split
            ;
        """.trimIndent()
        val r = RectAnimationOracle.compile(src)
        check(r.ok && r.programOrNull != null) { "recursive split failed:\n" + r.diagnostics.joinToString("\n") }
        val p = r.program
        fun near(a: Double, b: Double) = Math.abs(a - b) < 1e-9
        fun visibleNonSeed(t: Double) = p.sample(t).rects.filter { it.id != "seed" && it.alpha > 0.5 }
        check(p.sample(1.0).rects.size == 3) { "recursive split should make seed + shard + grand = 3 rects" }
        val atSplit1 = visibleNonSeed(1.0)
        check(atSplit1.size == 1 && near(atSplit1[0].center.x, 0.0) && near(atSplit1[0].center.y, 0.0)) {
            "at t=1 exactly the shard should be alive at (0,0), got ${atSplit1.map { it.center }}"
        }
        val atSplit2 = visibleNonSeed(2.0)
        check(atSplit2.size == 1 && near(atSplit2[0].center.x, 1.0) && near(atSplit2[0].center.y, 0.0)) {
            "at t=2 exactly the grand-shard should be born at (1,0), got ${atSplit2.map { it.center }}"
        }
        println("rectsl recursive-split check: grand-shard born at t1+t2 on the parent split point")
    }

arc, spiral, and phyllotaxis place copies at known polar positions. Checked against closed-form expectations: an arc across [0, pi] hits (2,0)/(0,2)/(-2,0); a one-turn spiral lands its endpoints on +x at radii 1 and 3; the sunflower's radii follow scale·√i = 0, 1, √2.

    private fun checkArrayGenerators() {
        fun header(body: String) = """
            #rectsl
            ->wall 12 12 abstract
            ->duration 4
            ->fps 30
            "t" rect
              0.5 ->square
              0xffffff solid ->fill
              $body
            ;
        """.trimIndent()
        fun rectsOf(src: String) = run {
            val r = RectAnimationOracle.compile(src)
            check(r.ok && r.programOrNull != null) { "generator failed:\n" + r.diagnostics.joinToString("\n") }
            r.program.sample(0.0).rects
        }
        fun near(a: Double, b: Double) = Math.abs(a - b) < 1e-6

        val arc = rectsOf(header("3 2 0 3.141592653589793 '( ) arc"))
        check(arc.size == 3) { "arc expected 3 copies, got ${arc.size}" }
        check(arc.any { near(it.center.x, 2.0) && near(it.center.y, 0.0) }) { "arc missing (2,0)" }
        check(arc.any { near(it.center.x, 0.0) && near(it.center.y, 2.0) }) { "arc missing (0,2)" }
        check(arc.any { near(it.center.x, -2.0) && near(it.center.y, 0.0) }) { "arc missing (-2,0)" }

        val spiral = rectsOf(header("2 1 3 1 '( ) spiral")).map { Math.hypot(it.center.x, it.center.y) }.sorted()
        check(near(spiral[0], 1.0) && near(spiral[1], 3.0)) { "spiral radii wrong: $spiral" }

        val phyllo = rectsOf(header("3 1 '( ) sunflower")).map { Math.hypot(it.center.x, it.center.y) }.sorted()
        check(near(phyllo[0], 0.0) && near(phyllo[1], 1.0) && near(phyllo[2], Math.sqrt(2.0))) {
            "phyllotaxis radii wrong: $phyllo"
        }
        println("rectsl array-generator check: arc/spiral/phyllotaxis placement OK")
    }

Every RectSL docs-gallery program (loose *_rectsl_.rectsl files under public/docs/assets/rectsl_gallery/, rendered by RectSLGalleryHarness) must compile, sample, lower to a non-empty mesh, and have its StackSL materials compile — all CPU-only, so this guards the gallery without a GPU. Silently skips if the workspace is absent (partial checkout).

Files using a {{ … }} Kotlin escape are compiled with a real Kotlin engine (lazily created, shared, disposed at the end). jvm-main is not a render loop, so the lazy synchronous compile path is safe here — no pre-warm needed.

source — 48 lines, from line 339
    private fun checkGalleryWorkspace() {
        val dir = File(System.getProperty("user.dir"), "public/docs/assets/rectsl_gallery")
        val files = dir.listFiles { f -> f.name.endsWith("_rectsl_.rectsl") } ?: return
        check(files.isNotEmpty()) { "rectsl gallery workspace has no programs: ${dir.absolutePath}" }
        var escapeEngine: KotlinScriptEngine? = null
        try {
            val names = mutableSetOf<String>()
            files.sortedBy { it.name }.forEach { f ->
                val name = f.name.removeSuffix("_rectsl_.rectsl")
                names += name
                val source = f.readText()
                val result = if ("{{" in source) {
                    val engine = escapeEngine ?: KotlinInterface2(filePrefix = "RECTSL_SMOKE_ESCAPE_").also { escapeEngine = it }
                    compileRectSL(source, engine = engine)
                } else {
                    RectAnimationOracle.compile(source)
                }
                val program = result.programOrNull
                check(result.ok && program != null) {
                    "gallery '$name' failed to compile:\n" + result.diagnostics.joinToString("\n")
                }
                val mesh = lowerSampledScene(program.sample(program.duration * 0.5))
                check(mesh.vertices.isNotEmpty()) { "gallery '$name' lowered to an empty mesh (nothing to draw)" }
                val shader = rectMaterialShaderCode(program)
                check(shader.ok) {
                    "gallery '$name' materials failed to compile:\n" + shader.diagnostics.joinToString("\n")
                }
            }
            checkGalleryEdn(dir, names)
        } finally {
            escapeEngine?.dispose()
        }
    }

    private fun checkGalleryEdn(dir: File, names: Set<String>) {
        // The workspace is also a real openable Field document (offline viewer +
        // download/inspect). Parse metadata.edn through Field's own EDN reader so a
        // malformed document fails here rather than only in the viewer, and confirm
        // it carries a box per example.
        val edn = File(dir, "metadata.edn")
        check(edn.exists()) { "gallery workspace metadata.edn missing: ${edn.absolutePath}" }
        val transfers = afield.document.persistence.SavingContext().readEdnTransfersFromString(edn.readText())
        val boxIds = transfers.filter { it.id != "root" }.map { it.id }.toSet()
        check(boxIds.containsAll(names)) {
            "metadata.edn is missing boxes for workspace examples: ${names - boxIds}"
        }
        println("rectsl gallery workspace compiled ${names.size} program(s); metadata.edn boxes=${boxIds.size}")
    }

Compile every complete RectSL listing shown on the public examples page.

    private fun checkPublicDocsExamples() =
        compileDocsPage("public/docs/rectsl_examples.md", "example", wrapFragments = false)

Compile the short snippets on the cheatsheet page too.

The cheatsheet's job is to be scanned, so its snippets are fragments: three or four lines showing one section's words in use, with no scene header. That is the reason they had rotted into a spelling the VM never had (1 square for 1 ->square) — nothing compiled them. A fragment gets SNIPPET_PREAMBLE prepended and is then held to exactly the same standard as a full listing, so a cheatsheet snippet cannot claim a syntax that does not parse.

    private fun checkCheatsheetSnippets() =
        compileDocsPage("public/docs/rectsl_cheatsheet.md", "cheatsheet snippet", wrapFragments = true)

The scene a cheatsheet fragment is compiled inside. A snippet may override any of it.

source — 47 lines, from line 406
    private val SNIPPET_PREAMBLE = """
        #rectsl
        ->wall 16 9 abstract
        ->duration 4
        ->fps 30
    """.trimIndent() + "\n"

    private fun compileDocsPage(relative: String, label: String, wrapFragments: Boolean) {
        val file = File(System.getProperty("user.dir"), relative)
        if (!file.exists()) return
        val blocks = rectslCodeBlocks(file.readText())
        check(blocks.isNotEmpty()) { "$relative has no `data-lang=rectsl` code blocks" }

        val placeholderCurves = with(Functions) {
            val curve = gaussian(mean = 0.5, sigma = 0.16, amplitude = 1.0)
            mapOf("target" to curve, "seed" to curve, "fit" to curve)
        }
        var escapeEngine: KotlinScriptEngine? = null
        try {
            blocks.forEachIndexed { index, (fence, body) ->
                val wrapped = wrapFragments && "->wall" !in body
                val source = if (wrapped) SNIPPET_PREAMBLE + body else body
                val result = if ("{{" in source) {
                    val engine = escapeEngine
                        ?: KotlinInterface2(filePrefix = "RECTSL_DOCS_ESCAPE_").also { escapeEngine = it }
                    compileRectSL(source, curves = placeholderCurves, engine = engine)
                } else {
                    compileRectSL(source, curves = placeholderCurves)
                }
                check(result.ok && result.programOrNull != null) {
                    // Name the fence and quote its first line: a block that is not meant
                    // to be a program at all is the likeliest reason to land here, and
                    // the diagnostics alone read as a language error.
                    "public RectSL $label ${index + 1} failed to compile.\n" +
                        "  page: $relative\n" +
                        "  fence: ```$fence\n" +
                        "  first line: ${body.lineSequence().firstOrNull { it.isNotBlank() }?.trim()}\n" +
                        (if (wrapped) "  (compiled inside the standard snippet preamble)\n" else "") +
                        "  ${result.diagnostics.joinToString("\n  ")}\n" +
                        "If this block is not a RectSL program, do not label it `data-lang=rectsl`."
                }
            }
        } finally {
            escapeEngine?.dispose()
        }
        println("rectsl $relative compiled ${blocks.size} block(s)")
    }

cheatsheet coverage guard

Every operator documented in rectsl_cheatsheet.md must appear in at least one worked, animated example — a gallery *_rectsl_.rectsl program or a data-lang=rectsl code block in rectsl_examples.md. This makes "every operator is demonstrated" an enforced invariant, not a fact that silently rots the next time a word is added.

Aliases and alternate spellings collapse to one representative (see ALIAS_GROUPS): demonstrating + covers add, <= covers lte, clone covers copy, etc. The Model (literals/syntax) and Kotlin entry points cheatsheet sections are API surface, not stack operators, so they are excluded. Silently skips if the docs are absent (partial checkout).

    private fun checkCheatsheetCoverage() {
        val root = File(System.getProperty("user.dir"))
        val cheatsheet = File(root, "public/docs/rectsl_cheatsheet.md").takeIf { it.exists() } ?: return
        val examples = File(root, "public/docs/rectsl_examples.md")
        val galleryDir = File(root, "public/docs/assets/rectsl_gallery")

        val operators = cheatsheetOperators(cheatsheet.readText())
        check(operators.size > 100) { "cheatsheet operator scrape looks wrong: only ${operators.size} found" }

        val corpus = buildString {
            galleryDir.listFiles { f -> f.name.endsWith("_rectsl_.rectsl") }?.forEach { append(it.readText()).append('\n') }
            if (examples.exists()) {
                rectslCodeBlocks(examples.readText()).forEach { (_, body) -> append(body).append('\n') }
            }
        }
        val used = corpusTokens(corpus).map(::canonicalOperator).toSet()

        val missing = operators.filter { canonicalOperator(it) !in used }.map(::canonicalOperator).distinct()
        check(missing.isEmpty()) {
            "these cheatsheet operators have no worked/animated example (add one to the RectSL gallery or " +
                "rectsl_examples.md, or note the alias): ${missing.joinToString(", ")}"
        }
        println("rectsl cheatsheet coverage: all ${operators.size} operators demonstrated")
    }

First-column backticked tokens of every operator table, phrase words expanded.

    private fun cheatsheetOperators(text: String): List<String> {
        // Sections whose tables are not lists of RectSL words. "The looks, rendered"
        // tabulates `RectSLSdfLook` presets — `fill`, `relief`, `motion-blur` and the
        // rest are Kotlin-side names for ways of *shading* an emitted distance field,
        // not words the RectSL VM has ever heard of, so demanding a worked example for
        // each of them is a category error. (Their evidence is a rendered still apiece
        // on the examples page, which is a stronger guarantee than this guard gives.)
        //
        // "Fidelity" is the same category error arriving from the other end. Its table
        // is keyed on the three `RectSLSdfBake` classifications — `STEPPED`, `SAMPLED`,
        // `ALIASED` — with `fidelity` itself as the column head. Those are values the
        // emitter *reports* about a bake it just made, not words the VM parses, so there
        // is no gallery program that could "demonstrate `ALIASED`". What documents them
        // is `sdf.notes`, which that section exists to read aloud.
        val skip = setOf("Model", "Kotlin entry points", "The looks, rendered", "Fidelity")
        val phrase = mapOf(
            "->wall meters" to listOf("->wall", "meters"), "->wall pixels" to listOf("->wall", "pixels"),
            "->wall abstract" to listOf("->wall", "abstract"), "->fit contain" to listOf("->fit", "contain"),
            "->fit cover" to listOf("->fit", "cover"), "->fit stretch" to listOf("->fit", "stretch"),
            "material name '( ... )" to listOf("material"), "name ->material" to listOf("->material"),
        )
        val backtick = Regex("`([^`]+)`")
        val out = LinkedHashSet<String>()
        var section: String? = null
        text.lineSequence().forEach { line ->
            val header = Regex("^##+\\s+(.*)").find(line)
            if (header != null) { section = header.groupValues[1].trim(); return@forEach }
            if (section in skip) return@forEach
            val cell = Regex("^\\|\\s*(.+?)\\s*\\|").find(line)?.groupValues?.get(1) ?: return@forEach
            if (cell in setOf("Word", "Syntax", "API") || cell.all { it == '-' || it == '|' || it == ' ' }) return@forEach
            backtick.findAll(cell).forEach { m -> out.addAll(phrase[m.groupValues[1]] ?: listOf(m.groupValues[1])) }
        }
        return out.toList()
    }

internal fun rectslCodeBlocks(text: String): List<Pair<String, String>>

Every fenced block on a public page that this harness will try to compile as RectSL. RectSL has its own fence label; data-lang=text is prose or observed output and must not become a program merely because it happens to live on this page.

The fence header is returned alongside the body because that is what a failure usually turns out to be about. A numeric report written as data-lang=text is compiled as a program and fails with "Unknown word: near", which names a word rather than the fence; the fix is to choose a different data-lang, and nothing in the message used to say so.

    internal fun rectslCodeBlocks(text: String): List<Pair<String, String>> =
        Regex("```([^\\n]*)\\n(.*?)```", RegexOption.DOT_MATCHES_ALL).findAll(text)
            .filter { "data-lang=rectsl" in it.groupValues[1] }
            .map { it.groupValues[1].trim() to it.groupValues[2] }
            .toList()

Whitespace-split tokens with #-to-end-of-line comments stripped.

    private fun corpusTokens(text: String): Set<String> =
        text.lineSequence()
            .map { it.substringBefore('#') }
            .flatMap { it.trim().split(Regex("\\s+")) }
            .filter { it.isNotEmpty() }
            .toSet()

    private val ALIAS_GROUPS: List<List<String>> = listOf(
        listOf("+", "add"), listOf("-", "sub"), listOf("*", "mul"), listOf("/", "div"),
        listOf("->rot", "->rotation"), listOf("rot-track", "rotation-track"),
        listOf("neg", "negate"), listOf("sat", "saturate"), listOf("mix", "lerp"),
        listOf("copy", "clone"), listOf("copy-deep", "clone-deep"), listOf("end", "end-group"),
        listOf("filter", "where"), listOf("loop-count", "n"),
        listOf("<", "lt"), listOf("<=", "lte"), listOf(">", "gt"), listOf(">=", "gte"),
        listOf("=", "eq"), listOf("!=", "<>", "neq"), listOf("ease-smoothstep", "smooth"),
        listOf("colormap", "cmap"), listOf("heat", "heatmap"),
        listOf("line", "plot"), listOf("phyllotaxis", "sunflower"),
    )
    private val CANONICAL: Map<String, String> =
        ALIAS_GROUPS.flatMap { g -> g.map { it to g.first() } }.toMap()

    private fun canonicalOperator(word: String): String = CANONICAL[word] ?: word
}