Skip to content

KotlinToJsSpikeTest.kt

src/jvmMain/kotlin/afield/examples/KotlinToJsSpikeTest.kt · 123 lines

package afield.examples

import afield.lang.remotejs.KotlinToJsCompiler
import java.io.File
import java.nio.file.Files
import afield.media.image.util.NodeTool

FIELD_TEST id: remotejs.kotlin-to-js-spike runner: jvm-main entrypoint: afield.examples.KotlinToJsSpikeTest tags: [smoke, remotejs, kotlinjs] timeoutSec: 180 expect: exitCode: 0 stdoutContains: - "KotlinToJsSpikeTest ok"

object KotlinToJsSpikeTest

Proves the Kotlin->JS RemoteJS seam end to end, in-process from a Field JVM: 1. A self-contained Kotlin snippet that uses operator fun plus (the thing ASTA exists to fake in raw JS) compiles to a single plain-module JS string via KotlinToJsCompiler. 2. A deliberately broken snippet surfaces a structured compile diagnostic (line + message), i.e. errors are catchable at the Kotlin source before the browser ever runs anything. 3. If node is on PATH, the produced JS is actually executed and its output verified — proving the module is directly eval-able (which is what the head's global.eval(code) needs).

object KotlinToJsSpikeTest {

fun main(args: Array<String>)

body — 91 lines, from line 32
    @JvmStatic
    fun main(args: Array<String>) {
        // --- 1. good snippet: native Kotlin operator overloading -> JS ---
        val good = """
            external val console: dynamic
            data class V(val x: Double, val y: Double) {
                operator fun plus(o: V) = V(x + o.x, y + o.y)
            }
            fun main() {
                val a = V(1.0, 2.0) + V(10.0, 20.0)
                console.log("field-kotlinjs:" + a.x + "," + a.y)
            }
        """.trimIndent()

        val ok = KotlinToJsCompiler.compile(good, moduleName = "spike_ok")
        require(ok.success && ok.js != null) { "expected a successful compile, log:\n${ok.rawLog.takeLast(1200)}" }
        require(ok.diagnostics.none { it.severity == "error" }) { "unexpected errors: ${ok.diagnostics}" }
        // The default path is in-process; if it silently fell back to a subprocess the env clash is
        // back and we want to know.
        require(ok.mode == KotlinToJsCompiler.Mode.IN_PROCESS) {
            "expected in-process compile, got ${ok.mode}; log:\n${ok.rawLog.takeLast(1200)}"
        }
        val js = ok.js
        require(js.isNotBlank() && js.contains("field-kotlinjs")) { "produced JS missing the literal" }
        // plain module self-invokes main and is eval-able
        require(js.contains("mainWrapper()") || js.contains("main(")) { "plain module does not invoke main" }
        // DCE must tree-shake the stdlib out: a trivial program is a few KB, not ~600 KB.
        require(js.length < 50_000) { "DCE did not shrink output (got ${js.length} chars) — is -Xir-dce set?" }
        println("compiled ${good.length} chars of Kotlin -> ${js.length} chars of JS (mode=${ok.mode}, DCE'd)")

        // --- 1b. implicit main: script-like box code (no `fun main`), external hoisted ---
        val implicit = """
            external val console: dynamic
            data class V(val x: Double, val y: Double) {
                operator fun plus(o: V) = V(x + o.x, y + o.y)
            }
            val a = V(1.0, 2.0) + V(10.0, 20.0)
            console.log("field-implicit-main:" + a.x + "," + a.y)
        """.trimIndent()
        val wrapped = KotlinToJsCompiler.ensureImplicitMain(implicit)
        require(wrapped.contains("fun main()")) { "no synthesized main:\n$wrapped" }
        require(Regex("(?m)^external val console").containsMatchIn(wrapped)) { "external not hoisted to file scope:\n$wrapped" }
        val impl = KotlinToJsCompiler.compile(implicit, moduleName = "spike_implicit")
        require(impl.success && impl.js != null) { "implicit-main compile failed:\n${impl.rawLog.takeLast(1200)}" }
        // a pre-existing `fun main` must be left untouched (no double-wrap)
        require(KotlinToJsCompiler.ensureImplicitMain(good) == good) { "wrapping should be a no-op when fun main is present" }
        println("implicit-main wrapped ${implicit.length} chars → compiled ${impl.js.length} chars JS")

        // --- 2. broken snippet: structured diagnostic at the right line ---
        val bad = """
            fun main() {
                val x: Int = "not an int"
            }
        """.trimIndent()
        val err = KotlinToJsCompiler.compile(bad, moduleName = "spike_bad")
        require(!err.success) { "broken snippet unexpectedly compiled" }
        val errors = err.diagnostics.filter { it.severity == "error" }
        require(errors.isNotEmpty()) { "no compile diagnostic produced, log:\n${err.rawLog.takeLast(1200)}" }
        require(errors.any { it.line >= 1 }) { "diagnostic has no source line: $errors" }
        println("broken snippet -> ${errors.size} diagnostic(s), first: ${errors.first()}")

        // --- 3. optional: actually run the JS if node is available ---
        val node = NodeTool.resolveNode()
        if (node != null) {
            val tmp = Files.createTempFile("field-ktjs-spike", ".js").toFile()
            try {
                tmp.writeText(js)
                val proc = ProcessBuilder(node, tmp.absolutePath).redirectErrorStream(true).start()
                val out = proc.inputStream.bufferedReader().readText().trim()
                proc.waitFor()
                require(proc.exitValue() == 0) { "node run failed: $out" }
                require(out.contains("field-kotlinjs:11,22")) { "unexpected node output: $out" }
                println("node executed the module -> $out")

                // implicit-main output must run identically
                tmp.writeText(impl.js)
                val p2 = ProcessBuilder(node, tmp.absolutePath).redirectErrorStream(true).start()
                val out2 = p2.inputStream.bufferedReader().readText().trim()
                p2.waitFor()
                require(p2.exitValue() == 0 && out2.contains("field-implicit-main:11,22")) { "implicit-main node run: $out2" }
                println("node executed the implicit-main module -> $out2")
            } finally {
                tmp.delete()
            }
        } else {
            println("node not on PATH; skipped runtime execution (compile proof still holds)")
        }

        println("KotlinToJsSpikeTest ok")
    }
}