Skip to content

StackSLSmokeTest.kt

src/jvmMain/kotlin/afield/examples/stacksl/StackSLSmokeTest.kt · 1080 lines

package afield.examples.stacksl

import afield.document.graph.Box
import afield.graphics.shader.ShaderKind
import afield.graphics.shader.preprocess
import afield.graphics.shader.stackAutoShaderReport
import afield.graphics.shader.stackAutoShaderSources
import afield.lang.stacksl.StackSLCompileOptions
import afield.lang.stacksl.StackSLCompiler
import afield.lang.stacksl.StackSLSeverity

object StackSLSmokeTest

FIELD_TEST id: stacksl.compiler.smoke runner: jvm-main entrypoint: afield.examples.stacksl.StackSLSmokeTest tags: [smoke, examples, stacksl, shaders] timeoutSec: 20 expect: exitCode: 0 stdoutContains: - "StackSL compiler smoke tests passed"

fun main(args: Array<String>)

    @JvmStatic
    fun main(args: Array<String>) {
        check(runTests()) { "StackSL compiler smoke tests failed" }
    }

fun runTests(): Boolean

body — 789 lines, from line 30
    fun runTests(): Boolean {
        return try {
            compilesCreativeProgramWithPrefixedDefinitions()
            prunesDroppedValues()
            reportsStrictAndTolerantUnknownWords()
            keepsShortMutatedProgramsValid()
            emitsSamplerEnvironment()
            emitsNeighborhoodSamplerHelpers()
            usesXyzHashTimeInputConvention()
            lengthUsesSpatialChannelsByDefault()
            emitsColorSpaceHelpers()
            compilesCenteredModifierProgram()
            compilesStackedSuffixModifiers()
            externalizesDanglingWires()
            compilesStoreReadWriteWords()
            inlinesRepeatedDefinitionForCse()
            compilesExplicitArityVariants()
            compilesStackBlastOperators()
            compilesFMChirpOperator()
            compilesBitGlitchOperators()
            compilesOneMinusOperator()
            compilesGlslStyleMinMaxClampAndStackSelectors()
            compilesGeometryOperatorsAndVecLiterals()
            compilesShapingDomainAndVectorUtilityOperators()
            compilesNeighborhoodFieldOperators()
            compilesSamplerSizeAndChannelGradientOperators()
            compilesLocalNamedMarks()
            compilesWordReferences()
            compilesHigherOrderPhacelleNoise()
            compilesExternalHostGlslEscapeHatches()
            expandsStackAutoSamplerOverrides()
            reportsAStackAutoProgramsArityAndWarnings()
            compilesDocsGalleryPrograms()
            loadsGalleryWorkspaceMetadataEdn()
            compilesCreativeFreedomVariants()
            println("StackSL compiler smoke tests passed")
            true
        } catch (t: Throwable) {
            t.printStackTrace(System.err)
            false
        }
    }

    private fun compilesCreativeProgramWithPrefixedDefinitions() {
        val result = StackSLCompiler.compile(
            """
            : warp
              drop uv 8 * time + sin
              fbm palette saturate
            ;

            warp 1.15 contrast tonemap
            """.trimIndent(),
            StackSLCompileOptions(prefix = "fx")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("vec4 fx(vec4 fx_in)" in result.glsl)
        require("vec4 fx_warp(vec4 fx_in)" in result.glsl)
        require("fx_palette" in result.glsl)
        require("fx_tonemap" in result.glsl)
        require("vec4 warp(" !in result.glsl)
    }

    private fun prunesDroppedValues() {
        val result = StackSLCompiler.compile(
            """
            1 2 + drop
            uv fbm palette
            """.trimIndent(),
            StackSLCompileOptions(prefix = "prune")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require(result.analysis.prunedValues >= 1) { "Expected at least one pruned value: ${result.analysis}" }
        require("vec4(1.0) + vec4(2.0)" !in result.glsl) { "Dropped expression should not be emitted:\n${result.glsl}" }
    }

    private fun reportsStrictAndTolerantUnknownWords() {
        val strict = StackSLCompiler.compile("uv shimmerize", StackSLCompileOptions(prefix = "strict"))
        require(!strict.ok)
        require(strict.diagnostics.any { it.severity == StackSLSeverity.ERROR && "Unknown StackSL word" in it.message })

        val tolerant = StackSLCompiler.compile(
            "uv shimmerize",
            StackSLCompileOptions(prefix = "loose", tolerant = true)
        )
        require(tolerant.ok) { tolerant.diagnostics.joinToString("\n") }
        require(tolerant.diagnostics.any { it.severity == StackSLSeverity.WARNING && "Treating it as noop" in it.message })
    }

    private fun keepsShortMutatedProgramsValid() {
        val result = StackSLCompiler.compile(
            """
            drop
            1 +
            ema-diff
            """.trimIndent(),
            StackSLCompileOptions(prefix = "mutant", tolerant = true)
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("vec4 mutant(vec4 mutant_in)" in result.glsl)
        require("mutant_ema" in result.glsl)
        require(result.diagnostics.any { "wrapped stack values" in it.message || "Stack underflow" in it.message })
    }

    private fun emitsSamplerEnvironment() {
        val result = StackSLCompiler.compile(
            """
            uv 0.015 * sample
            ema saturate
            """.trimIndent(),
            StackSLCompileOptions(prefix = "ping", sampleTextures = listOf("state"))
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("uniform sampler2D state;" in result.glsl)
        require("uniform sampler2D ping_state;" !in result.glsl)
        require("ping_sample(state, ping_in" in result.glsl)
        require(result.analysis.requiredSamplers == setOf("state")) { result.analysis.requiredSamplers.toString() }
    }

    private fun emitsNeighborhoodSamplerHelpers() {
        val result = StackSLCompiler.compile(
            """
            uv 0.01 crossSample
            uv 0.02 rCrossSample.r
            +
            """.trimIndent(),
            StackSLCompileOptions(prefix = "cross", sampleTextures = listOf("state"))
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("uniform sampler2D state;" in result.glsl) { result.glsl }
        require("cross_crossSample(state" in result.glsl) { result.glsl }
        require("cross_randomCrossSampleChannel(state" in result.glsl) { result.glsl }
        require("cross_hash21" in result.glsl) { result.glsl }
        require(result.analysis.requiredSamplers == setOf("state")) { result.analysis.requiredSamplers.toString() }
    }

    private fun usesXyzHashTimeInputConvention() {
        val result = StackSLCompiler.compile(
            """
            time
            hash +
            0.5t +
            0.25c +
            0.35hash +
            0.2grain80 +
            """.trimIndent(),
            StackSLCompileOptions(prefix = "input_convention")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("vec4(input_convention_in.w)" in result.glsl) { result.glsl }
        require("vec4(input_convention_in.z)" in result.glsl) { result.glsl }
        require("vec4(0.5) * vec4(input_convention_in.w)" in result.glsl) { result.glsl }
        require("input_convention_in.w * 1.0" in result.glsl) { result.glsl }
        require("input_convention_in.z) * 1.0" in result.glsl) { result.glsl }
        require("input_convention_hash21((input_convention_in.xy" in result.glsl && "+ vec2(input_convention_in.w)" in result.glsl) {
            result.glsl
        }
    }

    private fun lengthUsesSpatialChannelsByDefault() {
        val result = StackSLCompiler.compile(
            "uv 0.5 - length pulse white *",
            StackSLCompileOptions(prefix = "ring")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("vec4(length(_ring_v3.xy))" in result.glsl) { result.glsl }
        require("vec4(length(_ring_v3))" !in result.glsl) { result.glsl }
    }

    private fun emitsColorSpaceHelpers() {
        val result = StackSLCompiler.compile(
            """
            uv ryb
            rgb2ryb ryb2rgb
            red rgb2oklab
            blue rgb2oklab
            0.5 mix
            oklab2rgb
            +
            saturate
            """.trimIndent(),
            StackSLCompileOptions(prefix = "color")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("color_ryb2rgb" in result.glsl) { result.glsl }
        require("color_rgb2ryb" in result.glsl) { result.glsl }
        require("color_rgb2oklab" in result.glsl) { result.glsl }
        require("color_oklab2rgb" in result.glsl) { result.glsl }
    }

    private fun compilesCenteredModifierProgram() {
        val result = StackSLCompiler.compile(
            """
            p ]wr18
            0.4sh4p0.1 +
            0.35n2 +
            0.2grain80 +
            palette
            0.25 |fbm3 +
            p length ]sh20 +
            p fbm ]c2p0.2 +
            0.006_0.09 median +
            saturate
            """.trimIndent(),
            StackSLCompileOptions(prefix = "mods", sampleTextures = listOf("state"))
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("mods_valueNoise1" in result.glsl) { result.glsl }
        require("mods_hash11(floor" in result.glsl) { result.glsl }
        require("mods_fbm" in result.glsl) { result.glsl }
        require("mods_palette" in result.glsl) { result.glsl }
        require("mods_median4" in result.glsl) { result.glsl }
        require("vec4(mods_in.xy - vec2(0.5), 0.0, 1.0)" in result.glsl) { result.glsl }
    }

    private fun compilesStackedSuffixModifiers() {
        val result = StackSLCompiler.compile(
            """
            0.4s3q8
            0.2 |s5q4 +
            p ]fbm4q8 +
            saturate
            """.trimIndent(),
            StackSLCompileOptions(prefix = "suffix_stack")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("sin((suffix_stack_in.w * 3.0 + 0.0))" in result.glsl) { result.glsl }
        require("floor(clamp(vec4(((sin((suffix_stack_in.w * 3.0 + 0.0)) + 1.0) * 0.5))" in result.glsl) {
            result.glsl
        }
        require("vec4(max(abs(8.0), 2.0) - 1.0)" in result.glsl) { result.glsl }
        require("suffix_stack_fbm" in result.glsl) { result.glsl }
        require(result.analysis.functions.last().finalStackDepth >= 1) { result.analysis.functions.last().toString() }
    }

    private fun externalizesDanglingWires() {
        val result = StackSLCompiler.compile(
            "p length 0.5 0.08 pulse white *",
            StackSLCompileOptions(prefix = "wires", externalizeConstants = true)
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require(result.analysis.danglingWires.isNotEmpty()) { result.analysis.toString() }
        require("const int wires_wire_count" in result.glsl) { result.glsl }
        require("void wires_init_wires" in result.glsl) { result.glsl }
        require("float wires_wires[wires_wire_count]" in result.glsl) { result.glsl }
        require("return wires(wires_in, wires_wires);" in result.glsl) { result.glsl }
    }

    private fun compilesStoreReadWriteWords() {
        val result = StackSLCompiler.compile(
            """
            0-> 0.985 *
            p length 0.5 0.08 pulse
            +
            dup ->0
            dup 0.65 * ->2
            saturate
            """.trimIndent(),
            StackSLCompileOptions(prefix = "store", sampleTextures = listOf("state"))
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("uniform sampler2D state;" in result.glsl) { result.glsl }
        require("store_sample(state, store_in, vec4(0.0))" in result.glsl) { result.glsl }
        require("vec4 _store_out =" in result.glsl) { result.glsl }
        require("_store_out.x =" in result.glsl) { result.glsl }
        require("_store_out.z =" in result.glsl) { result.glsl }
        require(result.analysis.functions.last().channelWrites == setOf(0, 2)) {
            result.analysis.functions.last().toString()
        }
    }

    private fun inlinesRepeatedDefinitionForCse() {
        val result = StackSLCompiler.compile(
            """
            : ring
              0-> 0.982 *
              p length 0.42 0.035 pulse 0.08 *
              + nip
            ;

            drop
            input ring
            input ring
            + 0.5 *
            """.trimIndent(),
            StackSLCompileOptions(prefix = "cse", sampleTextures = listOf("state"), externalizeConstants = true)
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        val rootSource = result.glsl.substringAfter("vec4 cse(vec4 cse_in, float cse_wires[cse_wire_count]) {")
        val sharedSample = "cse_sample(state, cse_in, vec4(0.0))"
        require(rootSource.indexOf(sharedSample) >= 0) { result.glsl }
        require(rootSource.indexOf(sharedSample) == rootSource.lastIndexOf(sharedSample)) { result.glsl }
        require(result.analysis.danglingWires.map { it.initial } == listOf("0.982", "0.42", "0.035", "0.08", "0.5")) {
            result.analysis.danglingWires.toString()
        }
    }

    private fun compilesExplicitArityVariants() {
        val result = StackSLCompiler.compile(
            """
            time
            uv 0.4 pulse.2
            +
            time
            uv 0.1 1.4 grade.3
            +
            time
            uv .x 2 fmchirp.2
            +
            """.trimIndent(),
            StackSLCompileOptions(prefix = "arity_variants")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require(result.diagnostics.isEmpty()) { result.diagnostics.joinToString("\n") }
    }

    private fun compilesStackBlastOperators() {
        val result = StackSLCompiler.compile(
            """
            1 2 3 4 mean.4
            5 6 7 median.3
            8 9 10 min.3
            11 12 13 max.3
            sort.4
            intermediate.3
            mean.all
            """.trimIndent(),
            StackSLCompileOptions(prefix = "blast")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("min(" in result.glsl) { result.glsl }
        require("max(" in result.glsl) { result.glsl }
        require("vec4(0.5)" in result.glsl) { result.glsl }
    }

    private fun compilesFMChirpOperator() {
        val short = StackSLCompiler.compile(
            "uv .x fmchirp.1",
            StackSLCompileOptions(prefix = "chirp_short")
        )

        require(short.ok) { short.diagnostics.joinToString("\n") }
        require("chirp_short_fmchirpSignedPow" in short.glsl) { short.glsl }
        require("chirp_short_fmchirp(" in short.glsl) { short.glsl }
        require("chirp_short_fmchirp(_chirp_short_v" in short.glsl) { short.glsl }

        val full = StackSLCompiler.compile(
            """
            uv .x
            2
            10
            0
            0
            0.25
            -0.5
            1
            0.8
            fmchirp
            """.trimIndent(),
            StackSLCompileOptions(prefix = "chirp_full")
        )

        require(full.ok) { full.diagnostics.joinToString("\n") }
        require("chirp_full_fmchirpSignedPow" in full.glsl) { full.glsl }
        require("chirp_full_fmchirp(" in full.glsl) { full.glsl }
        require("chirp_full_fmchirp(_chirp_full_v" in full.glsl) { full.glsl }
        require("vec4(2.0), vec4(10.0), vec4(0.0), vec4(0.0), vec4(0.25), vec4(-0.5), vec4(1.0), vec4(0.8))" in full.glsl) {
            full.glsl
        }

        val eightParam = StackSLCompiler.compile(
            """
            2
            10
            0
            0
            0.25
            -0.5
            1
            0.8
            fmchirp.8
            """.trimIndent(),
            StackSLCompileOptions(prefix = "chirp_params")
        )

        require(eightParam.ok) { eightParam.diagnostics.joinToString("\n") }
        require("vec4(chirp_params_in.x)" in eightParam.glsl) { eightParam.glsl }
        require("chirp_params_fmchirp(_chirp_params_v" in eightParam.glsl) { eightParam.glsl }
        require("vec4(2.0), vec4(10.0), vec4(0.0), vec4(0.0), vec4(0.25), vec4(-0.5), vec4(1.0), vec4(0.8))" in eightParam.glsl) {
            eightParam.glsl
        }

        val eightParamWithPriorStack = StackSLCompiler.compile(
            """
            uv
            2
            10
            0
            0
            0.25
            -0.5
            1
            0.8
            fmchirp.8
            +
            """.trimIndent(),
            StackSLCompileOptions(prefix = "chirp_params_plus")
        )

        require(eightParamWithPriorStack.ok) { eightParamWithPriorStack.diagnostics.joinToString("\n") }
    }

    private fun compilesBitGlitchOperators() {
        val result = StackSLCompiler.compile(
            """
            p length
            time gray-code
            xor
            dup gray-decode
            lfsr
            bitxor
            4 bitcrush.2
            bitcrush.1
            saturate
            """.trimIndent(),
            StackSLCompileOptions(prefix = "bits")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("uvec4 bits_toBits16" in result.glsl) { result.glsl }
        require("vec4 bits_xor" in result.glsl) { result.glsl }
        require("vec4 bits_grayCode" in result.glsl) { result.glsl }
        require("vec4 bits_grayDecode" in result.glsl) { result.glsl }
        require("vec4 bits_lfsr" in result.glsl) { result.glsl }
        require("vec4 bits_bitcrush" in result.glsl) { result.glsl }
        require("bits_xor(" in result.glsl) { result.glsl }
        require("bits_grayCode(" in result.glsl) { result.glsl }
        require("bits_grayDecode(" in result.glsl) { result.glsl }
        require("bits_lfsr(" in result.glsl) { result.glsl }
        require("bits_bitcrush(" in result.glsl) { result.glsl }
    }

    private fun compilesOneMinusOperator() {
        val result = StackSLCompiler.compile(
            "uv 1-",
            StackSLCompileOptions(prefix = "one_minus")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("(vec4(1.0) - " in result.glsl) { result.glsl }
    }

    private fun compilesGlslStyleMinMaxClampAndStackSelectors() {
        val result = StackSLCompiler.compile(
            """
            1 2 max
            3 min.1
            +
            0 0.25 1 clamp
            +
            10 20 30 40 [3x,2x,1x,0x]
            +
            """.trimIndent(),
            StackSLCompileOptions(prefix = "glsl_edges")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("max(vec4(1.0), vec4(2.0))" in result.glsl) { result.glsl }
        require("glsl_edges_min4(vec4(3.0))" in result.glsl) { result.glsl }
        require("clamp(vec4(0.0), vec4(0.25), vec4(1.0))" in result.glsl) { result.glsl }
        require("vec4(vec4(10.0).x, vec4(20.0).x, vec4(30.0).x, vec4(40.0).x)" in result.glsl) { result.glsl }
    }

    private fun compilesGeometryOperatorsAndVecLiterals() {
        val result = StackSLCompiler.compile(
            """
            [1,2,3,4]
            [0.25,0.5]
            rotate
            [0.5,0.5,0.25,0.25]
            [0,0,1,1]
            rotates
            [9,8,7,6] >z
            [1,2,3,4] [2,4,6,8] distance2
            [1,2,3,4] [2,4,6,8] distance3
            +
            [1,2,3,4] [2,4,6,8] distance
            +
            [3,4,5,6] [1,1,1,1] 2 standoff
            +
            [3,4,5,6] [1,1,1,1] 2 standoff2
            +
            [3,4,5,6] [1,1,1,1] 2 standoff3
            +
            +
            """.trimIndent(),
            StackSLCompileOptions(prefix = "geom")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("vec4(1.0, 2.0, 3.0, 4.0)" in result.glsl) { result.glsl }
        require("vec4(0.25, 0.5, 0.0, 1.0)" in result.glsl) { result.glsl }
        require("cos(" in result.glsl && "sin(" in result.glsl) { result.glsl }
        require(".z" in result.glsl) { result.glsl }
        require("distance(" in result.glsl && ".xy" in result.glsl && ".xyz" in result.glsl) { result.glsl }
        require("vec4 geom_safeNormalize(vec4 value)" in result.glsl) { result.glsl }
        require("vec4 geom_safeNormalize2(vec4 value)" in result.glsl) { result.glsl }
        require("vec4 geom_safeNormalize3(vec4 value)" in result.glsl) { result.glsl }
        require("geom_safeNormalize(" in result.glsl && "geom_safeNormalize2(" in result.glsl && "geom_safeNormalize3(" in result.glsl) {
            result.glsl
        }
    }

    private fun compilesShapingDomainAndVectorUtilityOperators() {
        val result = StackSLCompiler.compile(
            """
            uv 8 tile
            p 6 kaleido
            3 twist
            2 spiral
            polar cart
            fbm
            smooth
            0.62 bias
            0.35 gain
            sigmoid
            5 softplus
            0.45 threshold
            0.5 0.12 band
            0.2 0.8 ramp
            7 posterize
            dup sum
            over prod +
            over any +
            over all +
            over x+y +
            over xylen +
            rgbnorm
            saturate
            """.trimIndent(),
            StackSLCompileOptions(prefix = "new_ops")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("new_ops_bias" in result.glsl) { result.glsl }
        require("new_ops_gain" in result.glsl) { result.glsl }
        require("new_ops_kaleido" in result.glsl) { result.glsl }
        require("new_ops_twist" in result.glsl) { result.glsl }
        require("new_ops_spiral" in result.glsl) { result.glsl }
        require("new_ops_polar" in result.glsl && "new_ops_cart" in result.glsl) { result.glsl }
        require("new_ops_sum4" in result.glsl) { result.glsl }
        require("new_ops_prod4" in result.glsl) { result.glsl }
        require("new_ops_any4" in result.glsl) { result.glsl }
        require("new_ops_all4" in result.glsl) { result.glsl }
    }

    private fun compilesNeighborhoodFieldOperators() {
        val result = StackSLCompiler.compile(
            """
            laplace
            0.002 blur
            p 0.003 blur9
            p 0.004 sobel
            p 0.005 gradient
            p 0.006 divergence
            p 0.007 curl
            + + + + + +
            """.trimIndent(),
            StackSLCompileOptions(prefix = "field_ops", sampleTextures = listOf("state"))
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("uniform sampler2D state;" in result.glsl) { result.glsl }
        require("field_ops_laplace(state" in result.glsl) { result.glsl }
        require("field_ops_blur5(state" in result.glsl) { result.glsl }
        require("field_ops_blur9(state" in result.glsl) { result.glsl }
        require("field_ops_sobel(state" in result.glsl) { result.glsl }
        require("field_ops_gradient(state" in result.glsl) { result.glsl }
        require("field_ops_divergence(state" in result.glsl) { result.glsl }
        require("field_ops_curl(state" in result.glsl) { result.glsl }
        require(result.analysis.requiredSamplers == setOf("state")) { result.analysis.requiredSamplers.toString() }
    }

    private fun compilesSamplerSizeAndChannelGradientOperators() {
        val result = StackSLCompiler.compile(
            """
            texelSize gradient.r
            uv texelSize gradient.g.2
            +
            texelSize1 +
            textureSize0 0.0 * +
            """.trimIndent(),
            StackSLCompileOptions(prefix = "sizes", sampleTextures = listOf("state", "other"))
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("uniform sampler2D state;" in result.glsl) { result.glsl }
        require("uniform sampler2D other;" in result.glsl) { result.glsl }
        require("textureSize(state, 0)" in result.glsl) { result.glsl }
        require("textureSize(other, 0)" in result.glsl) { result.glsl }
        require("sizes_gradientChannel(state" in result.glsl) { result.glsl }
        require("sizes_gradient(state" !in result.glsl) { result.glsl }
        require(result.analysis.requiredSamplers == setOf("state", "other")) { result.analysis.requiredSamplers.toString() }
    }

    private fun compilesLocalNamedMarks() {
        val compact = StackSLCompiler.compile(
            """
            uv 8 * =coord
            coord fbm
            coord 0.2t + fbm
            0.5 mix
            """.trimIndent(),
            StackSLCompileOptions(prefix = "marks_compact")
        )

        require(compact.ok) { compact.diagnostics.joinToString("\n") }
        require("marks_compact_fbm" in compact.glsl) { compact.glsl }
        require("Unknown StackSL" !in compact.diagnostics.joinToString("\n")) { compact.diagnostics.joinToString("\n") }

        val explicit = StackSLCompiler.compile(
            """
            uv 8 * >coord
            <coord fbm
            <coord 0.2t + fbm
            0.5 mix
            """.trimIndent(),
            StackSLCompileOptions(prefix = "marks_explicit")
        )

        require(explicit.ok) { explicit.diagnostics.joinToString("\n") }
        require("marks_explicit_fbm" in explicit.glsl) { explicit.glsl }
        require(explicit.analysis.functions.last().finalStackDepth == 2) { explicit.analysis.functions.last().toString() }

        val strict = StackSLCompiler.compile("<missing fbm", StackSLCompileOptions(prefix = "marks_strict"))
        require(!strict.ok) { strict.glsl }
        require(strict.diagnostics.any { it.severity == StackSLSeverity.ERROR && "Unknown StackSL local mark 'missing'" in it.message }) {
            strict.diagnostics.joinToString("\n")
        }

        val tolerant = StackSLCompiler.compile(
            "<missing fbm",
            StackSLCompileOptions(prefix = "marks_tolerant", tolerant = true)
        )
        require(tolerant.ok) { tolerant.diagnostics.joinToString("\n") }
        require(tolerant.diagnostics.any { it.severity == StackSLSeverity.WARNING && "Unknown StackSL local mark 'missing'" in it.message }) {
            tolerant.diagnostics.joinToString("\n")
        }
    }

    private fun compilesWordReferences() {
        val direct = StackSLCompiler.compile(
            """
            : pattern
              normalize
            ;

            'pattern p apply
            """.trimIndent(),
            StackSLCompileOptions(prefix = "wordref_direct")
        )

        require(direct.ok) { direct.diagnostics.joinToString("\n") }
        require(Regex("""vec4 _wordref_direct_v\d+ = normalize\(_wordref_direct_v\d+\);""").containsMatchIn(direct.glsl)) {
            direct.glsl
        }
        require(direct.analysis.functions.last().calls == setOf("pattern")) { direct.analysis.functions.last().toString() }

        val passedThrough = StackSLCompiler.compile(
            """
            : pattern
              xylen
            ;

            : mapped
              apply
            ;

            'pattern uv mapped
            """.trimIndent(),
            StackSLCompileOptions(prefix = "wordref_map")
        )

        require(passedThrough.ok) { passedThrough.diagnostics.joinToString("\n") }
        require(Regex("""vec4 _wordref_map_v\d+ = vec4\(length\(_wordref_map_v\d+\.xy\)\);""").containsMatchIn(passedThrough.glsl)) {
            passedThrough.glsl
        }
        require(passedThrough.analysis.functions.last().calls.containsAll(setOf("mapped", "pattern"))) {
            passedThrough.analysis.functions.last().toString()
        }

        val builtin = StackSLCompiler.compile(
            "'normalize p apply",
            StackSLCompileOptions(prefix = "wordref_builtin")
        )

        require(builtin.ok) { builtin.diagnostics.joinToString("\n") }
        require(Regex("""vec4 _wordref_builtin_v\d+ = normalize\(_wordref_builtin_v\d+\);""").containsMatchIn(builtin.glsl)) {
            builtin.glsl
        }

        val unknown = StackSLCompiler.compile("'missing p apply", StackSLCompileOptions(prefix = "wordref_strict"))
        require(!unknown.ok) { unknown.glsl }
        require(unknown.diagnostics.any { it.severity == StackSLSeverity.ERROR && "Unknown StackSL word reference 'missing'" in it.message }) {
            unknown.diagnostics.joinToString("\n")
        }
    }

    private fun compilesHigherOrderPhacelleNoise() {
        val result = StackSLCompiler.compile(
            """
            : pattern
              input sample-abs normalize
            ;

            'pattern p 100 * 1.5 0.2 1 phacelle.4
            """.trimIndent(),
            StackSLCompileOptions(prefix = "phacelle_ref", sampleTextures = listOf("state"))
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("uniform sampler2D state;" in result.glsl) { result.glsl }
        require("vec4 phacelle_ref_phacelle_pattern(vec4 pValue, float freq, float offset, float normalization)" in result.glsl) {
            result.glsl
        }
        require("for (int i = -1; i <= 2; i++)" in result.glsl) { result.glsl }
        require("phacelle_ref_pattern(vec4(cellPoint, pValue.zw))" in result.glsl) { result.glsl }
        require("phacelle_ref_sampleAbs(state" in result.glsl) { result.glsl }
        require(result.analysis.functions.last().calls.contains("pattern")) { result.analysis.functions.last().toString() }

        val trailingReference = StackSLCompiler.compile(
            """
            : pattern
              normalize
            ;

            p 4 * 1.5 0.2 1 'pattern directional-noise.4
            """.trimIndent(),
            StackSLCompileOptions(prefix = "phacelle_trailing")
        )

        require(trailingReference.ok) { trailingReference.diagnostics.joinToString("\n") }
        require("phacelle_trailing_phacelle_pattern" in trailingReference.glsl) { trailingReference.glsl }

        val defaultPattern = StackSLCompiler.compile(
            "p 4 * phacelle",
            StackSLCompileOptions(prefix = "phacelle_default")
        )

        require(defaultPattern.ok) { defaultPattern.diagnostics.joinToString("\n") }
        require("phacelle_default_phacelle_default" in defaultPattern.glsl) { defaultPattern.glsl }
        require("normalize(vec4(cellPoint, pValue.zw))" in defaultPattern.glsl) { defaultPattern.glsl }
    }

    private fun compilesExternalHostGlslEscapeHatches() {
        val result = StackSLCompiler.compile(
            """
            ?pc.foo
            0.5 *
            dup ->?pc.foo
            ?pc.inData.p[idx].posvel.Z +
            dup ->?pc.inData.p[idx].posvel.Z
            ?pc.bar +
            """.trimIndent(),
            StackSLCompileOptions(prefix = "host")
        )

        require(result.ok) { result.diagnostics.joinToString("\n") }
        require("host_asVec4(pc.foo) * vec4(0.5)" in result.glsl) { result.glsl }
        require("+ host_asVec4(pc.inData.p[idx].posvel.Z)" in result.glsl) { result.glsl }
        require("+ host_asVec4(pc.bar)" in result.glsl) { result.glsl }
        require(Regex("""pc\.foo = _host_v\d+;""").containsMatchIn(result.glsl)) { result.glsl }
        require(Regex("""pc\.inData\.p\[idx]\.posvel\.Z = _host_v\d+;""").containsMatchIn(result.glsl)) { result.glsl }
    }

stack.auto must report what a successful compile does not say.

StackSLCompileResult.ok is errors only, and a stack language's characteristic mistake is not an error: a definition opened with =name (which copies, a -- a) instead of >name (which moves, a --) leaves its input under its result, so every call pushes two values and everything downstream takes the wrong operand. The compiler prunes the surplus and says nothing.

That cost a real debugging session, so this pins both halves: the broken shape is reported, and a correct program stays quiet. The second half is what keeps the report worth reading.

source — 249 lines, from line 831
    private fun reportsAStackAutoProgramsArityAndWarnings() {
        // `=c` copies, so `half` leaves its input under its result: net +1 instead of net 0.
        val unbalanced = """
            : half
              =c
              <c 0.5 *
            ;

            input half
        """.trimIndent()

        // Since 2026-10-16 the compiler refuses it outright, naming the definition and the fix.
        val bad = stackAutoShaderReport(unbalanced, "noiseForce")
        require(!bad.compiled) { "an unbalanced called definition is a compile error: $bad" }
        require(!bad.quiet) { "an unbalanced definition must not be reported as clean: $bad" }
        require(bad.problems.any { "half" in it && ">mark" in it }) { "the problem should name the definition and the fix: ${bad.problems}" }
        // And it renders, because an output area is where this is actually read.
        require("problem" in bad.toHTML()) { bad.toHTML() }

        // `>c` moves, so the same definition balances.
        val good = stackAutoShaderReport(unbalanced.replace("=c", ">c"), "noiseForce")
        require(good.quiet) { "the balanced program should be clean, got: $good" }
        require("clean" in good.toString()) { good.toString() }
        require(good.problems.isEmpty() && good.warnings.isEmpty()) { "$good" }

        // A blank slot compiles to the stable no-op fallback and has nothing to report.
        require(stackAutoShaderReport("", "noiseForce").quiet) { "a blank slot should be quiet" }

        // Non-ERROR diagnostics have to survive to the report: they were dropped on the success path,
        // which is how a second bug in the same session went unseen. A root's stack starts with the
        // program's input on it, so a binary word on its own finds one value where it needs two.
        val underflow = stackAutoShaderReport("*", "noiseForce")
        require(underflow.compiled) { "an arity underflow is a warning, not an error: $underflow" }
        require(underflow.warnings.isNotEmpty()) {
            "the compiler's non-error diagnostics must reach the report, got: $underflow"
        }
        require(!underflow.quiet) { "a program with warnings is not clean: $underflow" }

        // A root legitimately ends deeper than 1 — its stack starts with the program's input on it — so
        // depth is only checked for definitions. Without this the check would fire on `"input x 12 *"`.
        val root = stackAutoShaderReport("input x 12 *", "edgeForce")
        require(root.quiet) { "a root that pushes a second copy of its input is not a defect: $root" }

        // And a definition nothing calls is not a claim about behaviour. This shape exists to be used as
        // an *entrypoint* (`{{ stack.auto.foo:tap }}`), where it gets root semantics and so ends at 2.
        // Flagging it was this check's first false positive, caught by expandsStackAutoSamplerOverrides.
        val entrypointOnly = stackAutoShaderReport(
            """
            : tap 0.2 sample ;
            0.1 sample
            """.trimIndent(),
            "foo"
        )
        require(entrypointOnly.quiet) {
            "an uncalled definition must not be flagged; its shape constrains nothing: $entrypointOnly"
        }
    }

    private fun expandsStackAutoSamplerOverrides() {
        val box = Box("stack-auto-sampler-smoke")
        box.stackAutoShaderSources["foo"] = """
            : tap 0.2 sample ;
            0.1 sample
        """.trimIndent()

        val direct = preprocess(
            box,
            "{{ stack.auto.foo@noiseFieldView }}",
            ShaderKind.FRAGMENT_SHADER
        )
        require("foo_sample(noiseFieldView" in direct) { direct }
        require("foo_sample(source" !in direct) { direct }

        val entrypoint = preprocess(
            box,
            "{{ stack.auto.foo:tap@noiseFieldView }}",
            ShaderKind.FRAGMENT_SHADER
        )
        require("foo_sample(noiseFieldView" in entrypoint) { entrypoint }
        require("foo_sample(source" !in entrypoint) { entrypoint }
    }

    private fun compilesDocsGalleryPrograms() {
        val gallery = afield.graphics.scene.fabricated.StackSLGalleryHarness
        val examples = gallery.loadExamples()
        require(examples.isNotEmpty()) { "StackSL docs gallery has no examples" }

        val box = Box("stacksl-gallery-smoke")
        examples.forEach { example ->
            val result = StackSLCompiler.compile(
                example.program,
                StackSLCompileOptions(
                    prefix = "program",
                    includeHeader = false,
                    declareSamplers = false,
                    sampleTextures = listOf("source") // matches the {{ stack.auto.program }} default sampler
                )
            )
            require(result.ok) {
                "StackSL gallery program '${example.name}' failed to compile:\n" +
                    result.diagnostics.filter { it.severity == StackSLSeverity.ERROR }.joinToString("\n")
            }
            // The gallery injects each program through the documented stack.auto path,
            // so make sure the template actually emits `vec4 program(...)`.
            box.stackAutoShaderSources["program"] = example.program
            val expanded = preprocess(box, "{{ stack.auto.program }}", ShaderKind.FRAGMENT_SHADER)
            require("vec4 program(vec4 program_in)" in expanded) {
                "stack.auto expansion for gallery program '${example.name}' missing entrypoint:\n$expanded"
            }

            // A program whose expansion reads `source` can only be hosted by the
            // gallery's feedback (ping-pong) template, which is the only one that
            // declares that sampler. Getting this pairing wrong compiles fine at
            // the StackSL level and then fails inside glslang, leaving the render
            // target at its clear color — i.e. it silently produces a black still.
            // So pin `feedback:` in the workspace metadata to what the program
            // actually reads.
            val samplesSource = Regex("""\bsource\b""").containsMatchIn(expanded)
            require(samplesSource == example.feedback) {
                if (samplesSource) {
                    "gallery program '${example.name}' samples `source` but its metadata.yaml does not " +
                        "set `feedback: true` — the plain still template declares no sampler, so this " +
                        "would render an all-black PNG"
                } else {
                    "gallery program '${example.name}' is marked `feedback: true` but never samples " +
                        "`source` — it does not need the ping-pong path"
                }
            }
        }
    }

    private fun loadsGalleryWorkspaceMetadataEdn() {
        // The gallery workspace is a real canonical_graphics-style Field document.
        // Parse its metadata.edn through Field's own EDN reader so a malformed
        // document (or a dropped stackSL property) fails here rather than only in
        // the offline viewer.
        val edn = java.io.File(
            System.getProperty("user.dir"),
            "${afield.graphics.scene.fabricated.StackSLGalleryHarness.WORKSPACE_DIR}/metadata.edn"
        )
        require(edn.exists()) { "gallery workspace metadata.edn missing: ${edn.absolutePath}" }

        val transfers = afield.document.persistence.SavingContext().readEdnTransfersFromString(edn.readText())
        // Exclude the window/renderer parent box: it owns the graphics context the
        // example boxes inherit and carries no stackSL of its own.
        val boxes = transfers.filter { it.id != "root" && it.id != "renderer" }
        val expected = afield.graphics.scene.fabricated.StackSLGalleryHarness.loadExamples().map { it.name }.toSet()
        val boxIds = boxes.map { it.id }.toSet()
        require(boxIds.containsAll(expected)) {
            "metadata.edn is missing boxes for workspace examples: ${expected - boxIds}"
        }
        val missingStackSL = boxes.filter { "stackSL" !in it.properties.keys }.map { it.id }
        require(missingStackSL.isEmpty()) {
            "gallery workspace boxes lack a stackSL property: $missingStackSL"
        }

        // Field runs the loose sidecar and the offline viewer shows the inline copy, so the
        // two can disagree without either one failing. Hold them equal, and hold the file to
        // what the codec writes, so a refresh on a clean tree changes nothing.
        val saving = afield.document.persistence.SavingContext()
        val dir = edn.parentFile
        val stale = saving.readInlineFileSnapshots(dir.absolutePath)
            .filter { java.io.File(dir, it.path).readText() != it.inline }
            .map { it.path }
        val refresh = "refresh with afield.document.persistence.SavingContext().refreshInlineFilesFromDisk(\"${dir.path}\")"
        require(stale.isEmpty()) { "metadata.edn inline copies differ from their sidecars: $stale; $refresh" }
        require(edn.readText() == saving.canonicalInlineMetadata(dir.absolutePath).metadataEdn) {
            "metadata.edn is not what Field's EDN codec writes; $refresh"
        }
    }

    private fun compilesCreativeFreedomVariants() {
        val rawMix = StackSLCompiler.compile("red blue 1.5 mix", StackSLCompileOptions(prefix = "raw_mix"))
        require(rawMix.ok) { rawMix.diagnostics.joinToString("\n") }
        require("mix(vec4(1.0, 0.0, 0.0, 1.0), vec4(0.0, 0.0, 1.0, 1.0), vec4(1.5))" in rawMix.glsl) {
            rawMix.glsl
        }
        require("clamp(vec4(1.5)" !in rawMix.glsl) { rawMix.glsl }

        val clampedMix = StackSLCompiler.compile("red blue 1.5 mix.c", StackSLCompileOptions(prefix = "clamped_mix"))
        require(clampedMix.ok) { clampedMix.diagnostics.joinToString("\n") }
        require("clamp(vec4(1.5), vec4(0.0), vec4(1.0))" in clampedMix.glsl) { clampedMix.glsl }

        val softMix = StackSLCompiler.compile("red blue 1.5 mix.sc", StackSLCompileOptions(prefix = "soft_mix"))
        require(softMix.ok) { softMix.diagnostics.joinToString("\n") }
        require("soft_mix_softClamp01" in softMix.glsl) { softMix.glsl }

        val select = StackSLCompiler.compile("red blue 1 select", StackSLCompileOptions(prefix = "select_test"))
        require(select.ok) { select.diagnostics.joinToString("\n") }
        require("step(vec4(0.5), abs(vec4(1.0)))" in select.glsl) { select.glsl }

        val rawPow = StackSLCompiler.compile("-2 0.5 pow", StackSLCompileOptions(prefix = "raw_pow"))
        require(rawPow.ok) { rawPow.diagnostics.joinToString("\n") }
        require("pow(vec4(-2.0), vec4(0.5))" in rawPow.glsl) { rawPow.glsl }

        val safePow = StackSLCompiler.compile("-2 0.5 pow.safe", StackSLCompileOptions(prefix = "safe_pow"))
        require(safePow.ok) { safePow.diagnostics.joinToString("\n") }
        require("pow(max(abs(vec4(-2.0)), vec4(0.00001)), vec4(0.5))" in safePow.glsl) { safePow.glsl }

        val rawSample = StackSLCompiler.compile("0.1 sample", StackSLCompileOptions(prefix = "raw_sample", sampleTextures = listOf("state")))
        require(rawSample.ok) { rawSample.diagnostics.joinToString("\n") }
        require("texture(sourceTexture, base.xy + offset.xy)" in rawSample.glsl) { rawSample.glsl }
        require("fract(base.xy + offset.xy)" !in rawSample.glsl) { rawSample.glsl }

        val wrappedSample = StackSLCompiler.compile("0.1 sample.w", StackSLCompileOptions(prefix = "wrap_sample", sampleTextures = listOf("state")))
        require(wrappedSample.ok) { wrappedSample.diagnostics.joinToString("\n") }
        require("wrap_sample_sample_w" in wrappedSample.glsl) { wrappedSample.glsl }
        require("fract(base.xy + offset.xy)" in wrappedSample.glsl) { wrappedSample.glsl }

        val rawEma = StackSLCompiler.compile("input prev 1.25 ema.3", StackSLCompileOptions(prefix = "raw_ema", sampleTextures = listOf("state")))
        require(rawEma.ok) { rawEma.diagnostics.joinToString("\n") }
        require("return mix(previous, current, alpha);" in rawEma.glsl) { rawEma.glsl }

        val clampedEma = StackSLCompiler.compile("input prev 1.25 ema.c.3", StackSLCompileOptions(prefix = "clamped_ema", sampleTextures = listOf("state")))
        require(clampedEma.ok) { clampedEma.diagnostics.joinToString("\n") }
        require("clamp(vec4(1.25), vec4(0.0), vec4(1.0))" in clampedEma.glsl) { clampedEma.glsl }

        val softEma = StackSLCompiler.compile("input prev 1.25 ema.sc.3", StackSLCompileOptions(prefix = "soft_ema", sampleTextures = listOf("state")))
        require(softEma.ok) { softEma.diagnostics.joinToString("\n") }
        require("soft_ema_softClamp01" in softEma.glsl) { softEma.glsl }

        val rawAlpha = StackSLCompiler.compile("-0.08 0.5 8 alpha", StackSLCompileOptions(prefix = "raw_alpha"))
        require(rawAlpha.ok) { rawAlpha.diagnostics.joinToString("\n") }
        require("raw_alpha_alpha" in rawAlpha.glsl) { rawAlpha.glsl }
        require("lessThan(baseAlpha, vec4(0.0))" in rawAlpha.glsl) { rawAlpha.glsl }
        require("max(abs(baseAlpha), vec4(0.000001))" in rawAlpha.glsl) { rawAlpha.glsl }

        val clampedAlpha = StackSLCompiler.compile("-0.08 2 8 alpha.c", StackSLCompileOptions(prefix = "clamped_alpha"))
        require(clampedAlpha.ok) { clampedAlpha.diagnostics.joinToString("\n") }
        require("clamped_alpha_alpha_c" in clampedAlpha.glsl) { clampedAlpha.glsl }
        require("clamp(baseAlpha, vec4(0.000001), vec4(0.999999))" in clampedAlpha.glsl) { clampedAlpha.glsl }
        require("clamp(modulation, vec4(-1.0), vec4(1.0))" in clampedAlpha.glsl) { clampedAlpha.glsl }

        val softAlpha = StackSLCompiler.compile("0.08 2 8 alpha.sc", StackSLCompileOptions(prefix = "soft_alpha"))
        require(softAlpha.ok) { softAlpha.diagnostics.joinToString("\n") }
        require("soft_alpha_alpha_sc" in softAlpha.glsl) { softAlpha.glsl }
        require("soft_alpha_softClamp01" in softAlpha.glsl) { softAlpha.glsl }

        val rawRyb = StackSLCompiler.compile("[1.2,-0.1,0.5,1] ryb", StackSLCompileOptions(prefix = "raw_ryb"))
        require(rawRyb.ok) { rawRyb.diagnostics.joinToString("\n") }
        require("vec3 c = color.rgb;" in rawRyb.glsl) { rawRyb.glsl }
        require("vec3 c = clamp(color.rgb" !in rawRyb.glsl) { rawRyb.glsl }

        val clampedRyb = StackSLCompiler.compile("[1.2,-0.1,0.5,1] ryb.c", StackSLCompileOptions(prefix = "clamped_ryb"))
        require(clampedRyb.ok) { clampedRyb.diagnostics.joinToString("\n") }
        require("clamped_ryb_ryb2rgb_c" in clampedRyb.glsl) { clampedRyb.glsl }
        require("clamp(color.rgb, vec3(0.0), vec3(1.0))" in clampedRyb.glsl) { clampedRyb.glsl }
    }
}