Skip to content

RectSLKotlinEscapeTest.kt

src/jvmMain/kotlin/afield/examples/rectsl/RectSLKotlinEscapeTest.kt · 106 lines

package afield.examples.rectsl

import afield.runtime.kotlin.KotlinInterface2
import afield.lang.rectsl.compileRectSL

FIELD_TEST id: rectsl.kotlin-escape runner: jvm-main entrypoint: afield.examples.rectsl.RectSLKotlinEscapeTest tags: [rectsl, kotlin, k2, functions, experimental] timeoutSec: 180 expect: exitCode: 0 stdoutContains: - "rectsl kotlin escape passed"

object RectSLKotlinEscapeTest

Experimental: {{ … }} escapes let a RectSL program drop into a live with(Functions){ … } Kotlin scope anywhere a value is expected — the escape evaluates once (K2), is cached by source, and pushes a Number, an Fn, or a Track<Double> onto the stack. This exercises the whole path with a real engine.

fun main(args: Array<String>)

body — 81 lines, from line 25
    @JvmStatic
    fun main(args: Array<String>) {
        val ki = KotlinInterface2(filePrefix = "RECTSL_ESCAPE_")

        // 1) An escape that returns a Number, used directly as a coordinate.
        val numeric = compileRectSL(
            """
            ->wall 10 10 meters
            ->duration 1
            ->fps 1
            "a" rect  {{ 1.0 + 2.0 }} 0 ->xy  1 ->square ;
            """.trimIndent(),
            engine = ki
        ).programOrNull ?: error("numeric escape failed to compile")
        val ax = numeric.sample(0.0).rects.first().center.x
        require(kotlin.math.abs(ax - 3.0) < 1e-9) { "numeric escape expected x=3.0, got $ax" }

        // 2) An escape that returns an Fn, sampled per copy across an array. The escape
        //    sits inside the loop quote — proving the per-source cache (compiled once).
        val bell = compileRectSL(
            """
            ->wall 20 8 meters
            ->duration 1
            ->fps 1
            "c" rect 0 0 ->xy 1 ->square
              5 '( ic 2 * 0 ->offset  {{ gaussian(0.5, 0.16) }} f sample ->alpha ) array
            ;
            """.trimIndent(),
            engine = ki
        ).programOrNull ?: error("Fn escape failed to compile")
        val alphas = bell.sample(0.0).rects.sortedBy { it.center.x }.map { it.alpha }
        require(alphas.size == 5) { "expected 5 rects, got ${alphas.size}" }
        require(kotlin.math.abs(alphas[2] - 1.0) < 1e-3) { "bell center should be ~1, got ${alphas[2]}" }
        require(alphas[0] < 0.05 && alphas[4] < 0.05) { "bell edges should be dark: $alphas" }

        // 2b) A per-copy escape: compiled once, then invoked per copy with a different
        //     i, so the escaped Kotlin varies across the array at ~free marginal cost.
        val perCopy = compileRectSL(
            """
            ->wall 20 8 meters
            ->duration 1
            ->fps 1
            "c" rect 0 0 ->xy 1 ->square
              5 '( {{ i * 2.0 }} 0 ->offset ) array
            ;
            """.trimIndent(),
            engine = ki
        ).programOrNull ?: error("per-copy escape failed to compile")
        val pxs = perCopy.sample(0.0).rects.map { it.center.x }.sorted()
        listOf(0.0, 2.0, 4.0, 6.0, 8.0).forEachIndexed { idx, e ->
            require(kotlin.math.abs(pxs[idx] - e) < 1e-9) { "per-copy escape expected $e at $idx, got ${pxs[idx]}" }
        }

        // 3) An escape that returns a value from the Functions transform pipeline.
        val pipe = compileRectSL(
            """
            ->wall 6 6 meters
            ->duration 1
            ->fps 1
            "p" rect 0 0 ->xy 1 ->square  {{ (sin(2) through gain(0.5)).eval(0.25) }} ->alpha ;
            """.trimIndent(),
            engine = ki
        ).programOrNull
        require(pipe != null) { "transform-pipeline escape failed to compile" }
        val pa = pipe.sample(0.0).rects.first().alpha
        require(pa in 0.0..1.0) { "pipeline alpha out of range: $pa" }

        // 4) A missing engine is an honest, specific error — never a silent zero.
        val noEngine = compileRectSL(
            """
            ->wall 4 4 meters
            "z" rect {{ 1.0 }} 0 ->xy 1 ->square ;
            """.trimIndent()
        )
        require(noEngine.programOrNull == null && noEngine.diagnostics.any { it.contains("Kotlin escape needs") }) {
            "escape without an engine should fail with a clear message, got ${noEngine.diagnostics}"
        }

        println("rectsl kotlin escape passed")
    }
}