Skip to content

KotlinInTheHeadEvidence.kt

src/jvmMain/kotlin/afield/examples/KotlinInTheHeadEvidence.kt · 150 lines

package afield.examples

import afield.lang.remotejs.KotlinJsHeadProfiles
import afield.lang.remotejs.KotlinToJsCompiler
import java.io.File

FIELD_TEST id: docs.kotlin-in-the-head.emitted-js runner: jvm-main entrypoint: afield.examples.KotlinInTheHeadEvidence tags: [docs, remotejs, kotlinjs] timeoutSec: 180 expect: exitCode: 0 stdoutContains: - "KotlinInTheHeadEvidence ok"

object KotlinInTheHeadEvidence

Regenerates the emitted-JavaScript evidence embedded in public/docs/kotlin_in_the_head.md.

The whole honesty point of that page: the WebXR/audio payoff of a Kotlin/JS head is NOT headlessly capturable (it needs a real browser / Vision head + a user gesture), but the claim "your Kotlin really became JavaScript" IS reproducible — it is exactly what KotlinToJsCompiler emits.

This drives the REAL compiler on the exact worked-example source the doc shows (the V vector data class with operator fun plus/times, through the real remoteAR profile via KotlinJsHeadProfiles.remoteAr.prepareSource) and dumps the emitted module so the page can quote a faithful excerpt. Output goes only under build/ (never a committed asset); the page author copies the relevant slice by hand. Also compiles a Tone example so the doc's second profile is proven too.

object KotlinInTheHeadEvidence {

The vectors box from the doc, verbatim. data class + operator overloads + string templates + the RemoteAR log(...) helper — all real Kotlin, all bound by RemoteArKotlinJs.

    private val vectorsSource = """
        // {{ remoteAR }}
        data class V(val x: Double, val y: Double) {
            operator fun plus(o: V) = V(x + o.x, y + o.y)
            operator fun times(k: Double) = V(x * k, y * k)
        }

        val a = V(1.0, 2.0)
        val b = V(10.0, 20.0)
        log("sum = ${'$'}{a + b}")
        log("scaled = ${'$'}{a * 3.0}")
    """.trimIndent()

The ring box: builds real THREE geometry and animates it on the head's frame clock. Uses the bound THREE.Vector3 +/* operators, addBox/scene/onFrame — none of this is faked in raw JS.

    private val ringSource = """
        // {{ remoteAR }}
        clearAll()
        val n = 12
        val ring = (0 until n).map { i ->
            val theta = i.toDouble() / n * 2.0 * kotlin.math.PI
            val here = vec(kotlin.math.cos(theta) * 2.0, kotlin.math.sin(theta) * 2.0, 0.0)
            val box = addBox("ring-${'$'}i", 0.25).mesh
            box.position.set(here.x, here.y, here.z)
            box
        }
        onFrame { frame ->
            ring.forEachIndexed { i, box ->
                box.rotation.y = frame.time + i.toDouble() * 0.2
            }
        }
        log("ring installed: ${'$'}{ring.size} boxes")
    """.trimIndent()

The bells box (Tone.js head): a scheduled arpeggio built from real Tone nodes.

    private val bellsSource = """
        // {{ tone }}
        onStart {
            val synth = Tone.PolySynth()
            val reverb = Tone.Reverb(3.0)
            reverb.toDestination()
            synth.connect(reverb)
            val notes = listOf("C4", "E4", "G4", "B4")
            val transport = Tone.getTransport()
            transport.bpm.value = 96.0
            var step = 0
            val loop = transport.scheduleRepeat({ time ->
                synth.triggerAttackRelease(notes[step % notes.size], "8n", time)
                step += 1
            }, "8n")
            Tone.start()
            transport.start()
            onStop {
                transport.clear(loop)
                transport.stop()
                synth.dispose()
                reverb.dispose()
            }
        }
    """.trimIndent()

fun main(args: Array<String>)

body — 52 lines, from line 98
    @JvmStatic
    fun main(args: Array<String>) {
        val outDir = File("build/field-tests/kotlin_in_the_head").apply { mkdirs() }

        val vectors = compileThroughProfile(vectorsSource, KotlinJsHeadProfiles.remoteAr, "vectors")
        emit(outDir, "vectors", vectorsSource, vectors)

        val ring = compileThroughProfile(ringSource, KotlinJsHeadProfiles.remoteAr, "ring")
        emit(outDir, "ring", ringSource, ring)

        val bells = compileThroughProfile(bellsSource, KotlinJsHeadProfiles.tone, "bells")
        emit(outDir, "bells", bellsSource, bells)

        // Sanity: the `V` data class and its operators really made it into the emitted JS (not
        // tree-shaken away, not faked) — this is the exact evidence the doc quotes. The plain module
        // lowers the class to `function main$V(...)` with prototype methods carrying Kotlin's mangled
        // JS names (`plus_…_k$` / `times_…_k$`), and DCE keeps only the members `main` actually uses.
        val v = vectors.js!!
        require(v.contains("function main\$V")) { "expected `function main\$V` in the emitted JS" }
        require(v.contains("protoOf(main\$V).plus_")) { "expected the plus operator method on main\$V" }
        require(v.contains("protoOf(main\$V).times_")) { "expected the times operator method on main\$V" }

        println("KotlinInTheHeadEvidence ok")
    }

    private fun compileThroughProfile(
        source: String,
        profile: afield.lang.remotejs.KotlinJsHeadProfile,
        module: String,
    ): KotlinToJsCompiler.Result {
        val selection = KotlinJsHeadProfiles.resolve(source)
        require(selection.profile === profile) {
            "$module resolved to ${selection.profile.id}, expected ${profile.id}"
        }
        // prepareSource() does implicit-main wrapping + appends the profile's generated declarations —
        // the exact real path a live box takes before K2→JS.
        val prepared = profile.prepareSource(selection.source)
        val result = KotlinToJsCompiler.compile(prepared, moduleName = module, wrapImplicitMain = false)
        require(result.success && result.js != null) {
            "$module did not compile through the real K2→JS pipeline:\n${result.rawLog.takeLast(2000)}"
        }
        return result
    }

    private fun emit(dir: File, name: String, kotlin: String, result: KotlinToJsCompiler.Result) {
        File(dir, "$name.kt").writeText(kotlin)
        File(dir, "$name.js").writeText(result.js!!)
        println("---- $name : ${kotlin.length} chars Kotlin -> ${result.js.length} chars JS (mode=${result.mode}) ----")
        println(result.js)
        println("---- end $name ----")
    }
}