[Wasm] Loader improvements

- Output ES modules instead of plain files
- Support -Xwasm-launcher=d8 for d8 shell used in tests and benchmarks.
- Reuse launcher generation logic in CLI and box tests runners.
- Create separate output directory for each box since
  there are multiple output files generated for each test.
- Stop using absolute paths in generate JS files
  to simplify running generated code on different machine
- Remove ">>>" from println output


Merge-request: KT-MR-5729
Merged-by: Svyatoslav Kuzmich <svyatoslav.kuzmich@jetbrains.com>
diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt
index 0c4f47b..fd1d9e5 100644
--- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt
+++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt
@@ -242,7 +242,7 @@
 
     @Argument(
             value = "-Xwasm-launcher",
-            valueDescription = "esm|nodejs",
+            valueDescription = "esm|nodejs|d8",
             description = "Picks flavor for the wasm launcher. Default is ESM."
     )
     var wasmLauncher: String? by NullableStringFreezableVar("esm")
diff --git a/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt b/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt
index 2dfcfe4..00431d2 100644
--- a/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt
+++ b/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt
@@ -11,9 +11,11 @@
 import org.jetbrains.kotlin.backend.common.CompilationException
 import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
 import org.jetbrains.kotlin.backend.common.serialization.metadata.KlibMetadataVersion
+import org.jetbrains.kotlin.backend.wasm.WasmLoaderKind
 import org.jetbrains.kotlin.backend.wasm.compileWasm
 import org.jetbrains.kotlin.backend.wasm.compileToLoweredIr
 import org.jetbrains.kotlin.backend.wasm.wasmPhases
+import org.jetbrains.kotlin.backend.wasm.writeCompilationResult
 import org.jetbrains.kotlin.cli.common.*
 import org.jetbrains.kotlin.cli.common.ExitCode.*
 import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments
@@ -38,6 +40,7 @@
 import org.jetbrains.kotlin.incremental.js.IncrementalDataProvider
 import org.jetbrains.kotlin.incremental.js.IncrementalNextRoundChecker
 import org.jetbrains.kotlin.incremental.js.IncrementalResultsConsumer
+import org.jetbrains.kotlin.backend.wasm.dce.eliminateDeadDeclarations
 import org.jetbrains.kotlin.ir.backend.js.*
 import org.jetbrains.kotlin.ir.backend.js.codegen.JsGenerationGranularity
 import org.jetbrains.kotlin.ir.backend.js.ic.actualizeCaches
@@ -326,49 +329,30 @@
                     exportedDeclarations = setOf(FqName("main")),
                     propertyLazyInitialization = arguments.irPropertyLazyInitialization,
                 )
+                if (arguments.irDce) {
+                    eliminateDeadDeclarations(allModules, backendContext)
+                }
                 val res = compileWasm(
                     allModules = allModules,
                     backendContext = backendContext,
                     emitNameSection = arguments.wasmDebug,
-                    dceEnabled = arguments.irDce,
+                    allowIncompleteImplementations = arguments.irDce,
                 )
-                val outputWasmFile = outputFile.withReplacedExtensionOrNull(outputFile.extension, "wasm")!!
-                outputWasmFile.writeBytes(res.wasm)
-                val outputWatFile = outputFile.withReplacedExtensionOrNull(outputFile.extension, "wat")!!
-                outputWatFile.writeText(res.wat)
 
-                val esmRunner = """
-                    export default WebAssembly.instantiateStreaming(fetch('${outputWasmFile.name}'), { runtime, js_code }).then((it) => {
-                        wasmInstance = it.instance;
-                        wasmInstance.exports.__init?.();
-                        wasmInstance.exports.startUnitTests?.();
-                        
-                        return it.instance.exports;
-                    });
-                """.trimIndent()
-
-                val nodeRunner = """
-                    const fs = require('fs');
-                    var path = require('path');
-                    const wasmBuffer = fs.readFileSync(path.resolve(__dirname, './${outputWasmFile.name}'));
-                    
-                    module.exports = WebAssembly.instantiate(wasmBuffer, { runtime, js_code }).then(wasm => {
-                        wasmInstance = wasm.instance;
-                    
-                        wasmInstance.exports.__init?.();
-                        wasmInstance.exports.startUnitTests?.();
-                    
-                        return wasmInstance.exports
-                    });
-                """.trimIndent()
-
-                val runner = when (arguments.wasmLauncher) {
-                    "esm" -> esmRunner
-                    "nodejs" -> nodeRunner
+                val launcherKind = when (arguments.wasmLauncher) {
+                    "esm" -> WasmLoaderKind.BROWSER
+                    "nodejs" -> WasmLoaderKind.NODE
+                    "d8" -> WasmLoaderKind.D8
                     else -> throw IllegalArgumentException("Unrecognized flavor for the wasm launcher")
                 }
 
-                outputFile.writeText(res.js + "\n" + runner)
+                writeCompilationResult(
+                    result = res,
+                    dir = outputFile.parentFile,
+                    loaderKind = launcherKind,
+                    fileNameBase = outputFile.nameWithoutExtension
+                )
+
                 return OK
             }
 
diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt
index ea0eda7..a995685 100644
--- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt
+++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt
@@ -7,7 +7,6 @@
 
 import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
 import org.jetbrains.kotlin.backend.common.phaser.invokeToplevel
-import org.jetbrains.kotlin.backend.wasm.dce.eliminateDeadDeclarations
 import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmCompiledModuleFragment
 import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmModuleFragmentGenerator
 import org.jetbrains.kotlin.backend.wasm.lower.markExportedDeclarations
@@ -23,6 +22,7 @@
 import org.jetbrains.kotlin.wasm.ir.convertors.WasmIrToBinary
 import org.jetbrains.kotlin.wasm.ir.convertors.WasmIrToText
 import java.io.ByteArrayOutputStream
+import java.io.File
 
 class WasmCompilerResult(val wat: String, val js: String, val wasm: ByteArray)
 
@@ -75,15 +75,10 @@
     allModules: List<IrModuleFragment>,
     backendContext: WasmBackendContext,
     emitNameSection: Boolean = false,
-    dceEnabled: Boolean = false,
+    allowIncompleteImplementations: Boolean = false,
 ): WasmCompilerResult {
-
-    if (dceEnabled) {
-        eliminateDeadDeclarations(allModules, backendContext)
-    }
-
     val compiledWasmModule = WasmCompiledModuleFragment(backendContext.irBuiltIns)
-    val codeGenerator = WasmModuleFragmentGenerator(backendContext, compiledWasmModule, allowIncompleteImplementations = dceEnabled)
+    val codeGenerator = WasmModuleFragmentGenerator(backendContext, compiledWasmModule, allowIncompleteImplementations = allowIncompleteImplementations)
     allModules.forEach { codeGenerator.generateModule(it) }
 
     val linkedModule = compiledWasmModule.linkWasmCompiledFragments()
@@ -107,13 +102,76 @@
 fun WasmCompiledModuleFragment.generateJs(): String {
     //language=js
     val runtime = """
-    var wasmInstance = null;
     
     const externrefBoxes = new WeakMap();
     """.trimIndent()
 
-    val jsFuns = jsFuns.joinToString(",\n") { "\"" + it.importName + "\" : " + it.jsCode }
-    val jsCode = "\nconst js_code = {$jsFuns};"
+    val jsCodeBody = jsFuns.joinToString(",\n") { "\"" + it.importName + "\" : " + it.jsCode }
+    val jsCodeBodyIndented = jsCodeBody.prependIndent("    ")
+    val jsCode =
+        "\nconst js_code = {\n$jsCodeBodyIndented\n};\n"
 
     return runtime + jsCode
 }
+
+enum class WasmLoaderKind {
+    D8,
+    NODE,
+    BROWSER,
+}
+
+fun generateJsWasmLoader(kind: WasmLoaderKind, wasmFilePath: String, externalJs: String): String {
+    val instantiation = when (kind) {
+        WasmLoaderKind.D8 ->
+            """
+                const wasmModule = new WebAssembly.Module(read('$wasmFilePath', 'binary'));
+                const wasmInstance = new WebAssembly.Instance(wasmModule, { js_code });
+            """.trimIndent()
+
+        WasmLoaderKind.NODE ->
+            """
+                const fs = require('fs');
+                var path = require('path');
+                const wasmBuffer = fs.readFileSync(path.resolve(__dirname, './$wasmFilePath'));
+                const wasmModule = new WebAssembly.Module(wasmBuffer);
+                const wasmInstance = new WebAssembly.Instance(wasmModule, { js_code });
+            """.trimIndent()
+
+        WasmLoaderKind.BROWSER ->
+            """
+                const { wasmInstance } = await WebAssembly.instantiateStreaming(fetch("$wasmFilePath"), { js_code });
+            """.trimIndent()
+    }
+
+    val init =
+        """
+            
+            const wasmExports = wasmInstance.exports;
+            wasmExports.__init();
+            wasmExports.startUnitTests?.();
+            
+        """.trimIndent()
+
+    val export = when (kind) {
+        WasmLoaderKind.D8, WasmLoaderKind.BROWSER ->
+            "export default wasmExports;\n"
+
+        WasmLoaderKind.NODE ->
+            "module.exports = wasmExports;\n"
+    }
+
+    return externalJs + instantiation + init + export
+}
+
+fun writeCompilationResult(
+    result: WasmCompilerResult,
+    dir: File,
+    loaderKind: WasmLoaderKind,
+    fileNameBase: String = "index",
+) {
+    dir.mkdirs()
+    File(dir, "$fileNameBase.wat").writeText(result.wat)
+    File(dir, "$fileNameBase.wasm").writeBytes(result.wasm)
+    val jsWithLoader = generateJsWasmLoader(loaderKind, "./$fileNameBase.wasm", result.js)
+    File(dir, "$fileNameBase.js").writeText(jsWithLoader)
+}
\ No newline at end of file
diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/Dce.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/Dce.kt
index 4ff4193..f889306 100644
--- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/Dce.kt
+++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/Dce.kt
@@ -15,7 +15,7 @@
 import org.jetbrains.kotlin.ir.visitors.acceptVoid
 import org.jetbrains.kotlin.js.config.JSConfigurationKeys
 
-internal fun eliminateDeadDeclarations(modules: List<IrModuleFragment>, context: WasmBackendContext) {
+fun eliminateDeadDeclarations(modules: List<IrModuleFragment>, context: WasmBackendContext) {
     val printReachabilityInfo =
         context.configuration.getBoolean(JSConfigurationKeys.PRINT_REACHABILITY_INFO) ||
                 java.lang.Boolean.getBoolean("kotlin.wasm.dce.print.reachability.info")
diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt
index 1f12062..18dad14 100644
--- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt
+++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt
@@ -364,7 +364,7 @@
         val jsCode = buildString {
             append("(f) => (")
             appendParameterList(arity)
-            append(") => wasmInstance.exports.__callFunction_")
+            append(") => wasmExports.__callFunction_")
             append(info.hashString)
             append("(f, ")
             appendParameterList(arity)
diff --git a/compiler/testData/cli/js/jsExtraHelp.out b/compiler/testData/cli/js/jsExtraHelp.out
index 3169c80..938f3bd 100644
--- a/compiler/testData/cli/js/jsExtraHelp.out
+++ b/compiler/testData/cli/js/jsExtraHelp.out
@@ -39,7 +39,7 @@
   -Xwasm                     Use experimental WebAssembly compiler backend
   -Xwasm-debug-info          Add debug info to WebAssembly compiled module
   -Xwasm-kclass-fqn          Enable support for FQ names in KClass
-  -Xwasm-launcher=esm|nodejs Picks flavor for the wasm launcher. Default is ESM.
+  -Xwasm-launcher=esm|nodejs|d8 Picks flavor for the wasm launcher. Default is ESM.
   -Xallow-kotlin-package     Allow compiling code in package 'kotlin' and allow not requiring kotlin.stdlib in module-info
   -Xallow-result-return-type Allow compiling code when `kotlin.Result` is used as a return type
   -Xbuiltins-from-sources    Compile builtIns from sources
diff --git a/compiler/testData/codegen/boxWasmJsInterop/functionTypes.kt b/compiler/testData/codegen/boxWasmJsInterop/functionTypes.kt
index 3184974..3290948 100644
--- a/compiler/testData/codegen/boxWasmJsInterop/functionTypes.kt
+++ b/compiler/testData/codegen/boxWasmJsInterop/functionTypes.kt
@@ -241,6 +241,14 @@
     return "OK"
 }
 
+// TODO: Rewrite test to use module system
+@JsFun("() => { globalThis.main = wasmExports; }")
+external fun hackNonModuleExport()
+
+fun main() {
+    hackNonModuleExport()
+}
+
 // FILE: functionTypes__after.js
 
 const exportedFres = main.exportedF()(1, 20, 300)("<", ">");
diff --git a/compiler/testData/codegen/boxWasmJsInterop/jsExport.kt b/compiler/testData/codegen/boxWasmJsInterop/jsExport.kt
index 6b48f67..7f2e65d 100644
--- a/compiler/testData/codegen/boxWasmJsInterop/jsExport.kt
+++ b/compiler/testData/codegen/boxWasmJsInterop/jsExport.kt
@@ -1,3 +1,4 @@
+// IGNORE_BACKEND: JS_IR, JS
 // MODULE: main
 // FILE: externals.kt
 
@@ -25,6 +26,15 @@
 
 fun box(): String = "OK"
 
+// TODO: Rewrite test to use module system
+
+@JsFun("() => { globalThis.main = wasmExports; }")
+external fun hackNonModuleExport()
+
+fun main() {
+    hackNonModuleExport()
+}
+
 // FILE: jsExport__after.js
 
 const c = main.makeC(300);
diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/testOld/BasicWasmBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/testOld/BasicWasmBoxTest.kt
index 24a699c..010bf48 100644
--- a/js/js.tests/test/org/jetbrains/kotlin/js/testOld/BasicWasmBoxTest.kt
+++ b/js/js.tests/test/org/jetbrains/kotlin/js/testOld/BasicWasmBoxTest.kt
@@ -11,19 +11,15 @@
 import com.intellij.psi.PsiManager
 import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
 import org.jetbrains.kotlin.backend.common.phaser.toPhaseMap
-import org.jetbrains.kotlin.backend.wasm.WasmCompilerResult
-import org.jetbrains.kotlin.backend.wasm.compileWasm
-import org.jetbrains.kotlin.backend.wasm.compileToLoweredIr
-import org.jetbrains.kotlin.backend.wasm.wasmPhases
+import org.jetbrains.kotlin.backend.wasm.*
+import org.jetbrains.kotlin.backend.wasm.dce.eliminateDeadDeclarations
 import org.jetbrains.kotlin.checkers.parseLanguageVersionSettings
 import org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport
 import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
 import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
 import org.jetbrains.kotlin.config.*
 import org.jetbrains.kotlin.idea.KotlinFileType
-import org.jetbrains.kotlin.ir.backend.js.ModulesStructure
 import org.jetbrains.kotlin.ir.backend.js.prepareAnalyzedSourceModule
-import org.jetbrains.kotlin.ir.backend.js.utils.sanitizeName
 import org.jetbrains.kotlin.ir.declarations.impl.IrFactoryImpl
 import org.jetbrains.kotlin.js.config.JsConfig
 import org.jetbrains.kotlin.js.facade.TranslationUnit
@@ -50,31 +46,17 @@
 
     private val COMMON_FILES_NAME = "_common"
 
-    @Suppress("UNUSED_PARAMETER")
-    fun doTestWithCoroutinesPackageReplacement(filePath: String, coroutinesPackage: String) {
-        TODO("TestWithCoroutinesPackageReplacement are not supported")
-    }
-
     fun doTest(filePath: String) = doTestWithTransformer(filePath) { it }
     fun doTestWithTransformer(filePath: String, transformer: java.util.function.Function<String, String>) {
         val file = File(filePath)
 
-        val outputDir = getOutputDir(file)
+        val outputDirBase = File(getOutputDir(file), getTestName(true))
         val fileContent = transformer.apply(KtTestUtil.doLoadFile(file))
 
         TestFileFactoryImpl().use { testFactory ->
             val inputFiles: MutableList<TestFile> = TestFiles.createTestFiles(file.name, fileContent, testFactory, true)
             val testPackage = testFactory.testPackage
-            val outputFileBase = outputDir.absolutePath + "/" + getTestName(true)
-            val outputWatFile = File("$outputFileBase.wat")
-            val outputWasmFile = File("$outputFileBase.wasm")
-            val outputJsFile = File("$outputFileBase.js")
-            val outputBrowserDir = File("$outputFileBase.browser")
-            val outputFileNoDceBase = "${outputFileBase}NoDce"
-            val outputWatNoDceFile = File("$outputFileNoDceBase.wat")
-            val outputWasmNoDceFile = File("$outputFileNoDceBase.wasm")
-            val outputJsNoDceFile = File("$outputFileNoDceBase.js")
-            val outputBrowserNoDceDir = File("$outputFileNoDceBase.browser")
+
             val languageVersionSettings = inputFiles.firstNotNullOfOrNull { it.languageVersionSettings }
 
             val kotlinFiles = mutableListOf<String>()
@@ -112,12 +94,9 @@
 
             val phaseConfig = if (debugMode >= DebugMode.DEBUG) {
                 val allPhasesSet = if (debugMode >= DebugMode.SUPER_DEBUG) wasmPhases.toPhaseMap().values.toSet() else emptySet()
-                val dumpOutputDir = File(outputWatFile.parent, outputWatFile.nameWithoutExtension + "-irdump")
-                println("\n ------ Dumping phases to file://$dumpOutputDir")
+                val dumpOutputDir = File(outputDirBase, "irdump")
+                println("\n ------ Dumping phases to file://${dumpOutputDir.absolutePath}")
                 println(" ------ KT   file://${file.absolutePath}")
-                println(" ------ WAT  file://$outputWatFile")
-                println(" ------ WASM file://$outputWasmFile")
-                println(" ------ JS   file://$outputJsFile")
                 PhaseConfig(
                     wasmPhases,
                     dumpToDirectory = dumpOutputDir.path,
@@ -139,33 +118,108 @@
                 AnalyzerWithCompilerReport(config.configuration)
             )
 
-            compileAndRun(
+            val (allModules, backendContext) = compileToLoweredIr(
+                depsDescriptors = sourceModule,
                 phaseConfig = phaseConfig,
-                sourceModule = sourceModule,
-                testPackage = testPackage,
-                dceEnabled = false,
-                outputWatFile = outputWatNoDceFile,
-                outputWasmFile = outputWasmNoDceFile,
-                outputJsFile = outputJsNoDceFile,
-                outputBrowserDir = outputBrowserNoDceDir,
-                debugMode = debugMode,
-                jsFilesBefore = jsFilesBefore,
-                jsFilesAfter = jsFilesAfter
+                irFactory = IrFactoryImpl,
+                exportedDeclarations = setOf(FqName.fromSegments(listOfNotNull(testPackage, TEST_FUNCTION))),
+                propertyLazyInitialization = true,
             )
 
-            compileAndRun(
-                phaseConfig = phaseConfig,
-                sourceModule = sourceModule,
-                testPackage = testPackage,
-                dceEnabled = true,
-                outputBrowserDir = outputBrowserDir,
-                debugMode = debugMode,
-                outputWatFile = outputWatFile,
-                outputWasmFile = outputWasmFile,
-                outputJsFile = outputJsFile,
-                jsFilesBefore = jsFilesBefore,
-                jsFilesAfter = jsFilesAfter
+            val compilerResult = compileWasm(
+                allModules = allModules,
+                backendContext = backendContext,
+                emitNameSection = true,
+                allowIncompleteImplementations = false,
             )
+
+            eliminateDeadDeclarations(allModules, backendContext)
+
+            val compilerResultWithDCE = compileWasm(
+                allModules = allModules,
+                backendContext = backendContext,
+                emitNameSection = true,
+                allowIncompleteImplementations = true,
+            )
+
+            val testJsQuiet = """
+                import exports from './index.js';
+        
+                let actualResult
+                try {
+                    actualResult = exports.box();
+                } catch(e) {
+                    console.log('Failed with exception!')
+                    console.log('Message: ' + e.message)
+                    console.log('Name:    ' + e.name)
+                    console.log('Stack:')
+                    console.log(e.stack)
+                }
+                if (actualResult !== "OK")
+                    throw `Wrong box result '${'$'}{actualResult}'; Expected "OK"`;
+            """.trimIndent()
+
+            val testJsVerbose = testJsQuiet + """
+                console.log('test passed');
+            """.trimIndent()
+
+            val testJs = if (debugMode >= DebugMode.DEBUG) testJsVerbose else testJsQuiet
+
+            fun compileAndRunD8Test(name: String, res: WasmCompilerResult) {
+                val dir = File(outputDirBase, name)
+                if (debugMode >= DebugMode.DEBUG) {
+                    val path = dir.absolutePath
+                    println(" ------ $name WAT  file://$path/index.wat")
+                    println(" ------ $name WASM file://$path/index.wasm")
+                    println(" ------ $name JS   file://$path/index.js")
+                    println(" ------ $name Test file://$path/test.js")
+                }
+
+                writeCompilationResult(res, dir, WasmLoaderKind.D8)
+                File(dir, "test.js").writeText(testJs)
+                ExternalTool(System.getProperty("javascript.engine.path.V8"))
+                    .run(
+                        "--experimental-wasm-typed-funcref",
+                        "--experimental-wasm-gc",
+                        "--experimental-wasm-eh",
+                        *jsFilesBefore.map { File(it).absolutePath }.toTypedArray(),
+                        "--module",
+                        "./test.js",
+                        *jsFilesAfter.map { File(it).absolutePath }.toTypedArray(),
+                        workingDirectory = dir
+                    )
+            }
+
+            compileAndRunD8Test("d8", compilerResult)
+            compileAndRunD8Test("d8-dce", compilerResultWithDCE)
+
+            if (debugMode >= DebugMode.SUPER_DEBUG) {
+                fun writeBrowserTest(name: String, res: WasmCompilerResult) {
+                    val dir = File(outputDirBase, name)
+                    writeCompilationResult(res, dir, WasmLoaderKind.BROWSER)
+                    File(dir, "test.js").writeText(testJsVerbose)
+                    File(dir, "index.html").writeText(
+                        """
+                            <!DOCTYPE html>
+                            <html lang="en">
+                            <body>
+                            <script src="test.js" type="module"></script>
+                            </body>
+                            </html>
+                        """.trimIndent()
+                    )
+                    val path = dir.absolutePath
+                    println(" ------ $name WAT  file://$path/index.wat")
+                    println(" ------ $name WASM file://$path/index.wasm")
+                    println(" ------ $name JS   file://$path/index.js")
+                    println(" ------ $name TEST file://$path/test.js")
+                    println(" ------ $name HTML file://$path/index.html")
+                }
+
+                writeBrowserTest("browser", compilerResult)
+                writeBrowserTest("browser-dce", compilerResultWithDCE)
+            }
+
         }
     }
 
@@ -178,108 +232,6 @@
             .fold(testGroupOutputDir, ::File)
     }
 
-    private fun compileAndRun(
-        phaseConfig: PhaseConfig,
-        sourceModule: ModulesStructure,
-        testPackage: String?,
-        dceEnabled: Boolean,
-        outputWatFile: File,
-        outputWasmFile: File,
-        outputJsFile: File,
-        outputBrowserDir: File,
-        debugMode: DebugMode,
-        jsFilesBefore: List<String>,
-        jsFilesAfter: List<String>,
-    ) {
-        val (allModules, backendContext) = compileToLoweredIr(
-            depsDescriptors = sourceModule,
-            phaseConfig = phaseConfig,
-            irFactory = IrFactoryImpl,
-            exportedDeclarations = setOf(FqName.fromSegments(listOfNotNull(testPackage, TEST_FUNCTION))),
-            propertyLazyInitialization = true,
-        )
-
-        val compilerResult = compileWasm(
-            allModules = allModules,
-            backendContext = backendContext,
-            emitNameSection = true,
-            dceEnabled = dceEnabled,
-        )
-
-        outputWatFile.write(compilerResult.wat)
-        outputWasmFile.writeBytes(compilerResult.wasm)
-
-        val testRunner = """
-            const wasmBinary = read(String.raw`${outputWasmFile.absoluteFile}`, 'binary');
-            const wasmModule = new WebAssembly.Module(wasmBinary);
-            wasmInstance = new WebAssembly.Instance(wasmModule, { js_code });
-            const ${sanitizeName(TEST_MODULE)} = wasmInstance.exports;
-            ${createJsRun(wasmInstance = "wasmInstance", dceEnabled = dceEnabled)}
-        """.trimIndent()
-        outputJsFile.write(compilerResult.js + "\n" + testRunner)
-
-        if (debugMode >= DebugMode.SUPER_DEBUG) {
-            createDirectoryToRunInBrowser(outputBrowserDir, compilerResult, dceEnabled)
-        }
-
-        ExternalTool(System.getProperty("javascript.engine.path.V8"))
-            .run(
-                "--experimental-wasm-typed-funcref",
-                "--experimental-wasm-gc",
-                "--experimental-wasm-eh",
-                *jsFilesBefore.toTypedArray(),
-                outputJsFile.absolutePath,
-                *jsFilesAfter.toTypedArray(),
-            )
-    }
-
-    private fun createJsRun(wasmInstance: String, dceEnabled: Boolean) = """
-            let actualResult
-            try {
-                $wasmInstance.exports.__init();
-                $wasmInstance.exports.startUnitTests?.();
-                actualResult = $wasmInstance.exports.$TEST_FUNCTION();
-            } catch(e) {
-                console.log('Failed with exception!')
-                console.log('Message: ' + e.message)
-                console.log('Name:    ' + e.name)
-                console.log('Stack:')
-                console.log(e.stack)
-            }
-            if (actualResult !== "OK")
-                throw `Wrong box result '${'$'}{actualResult}' (with DCE=${dceEnabled}); Expected "OK"`;
-    """.trimIndent()
-
-    private fun createDirectoryToRunInBrowser(directory: File, compilerResult: WasmCompilerResult, dceEnabled: Boolean) {
-        val browserRunner =
-            """
-            const response = await fetch("index.wasm");
-            const wasmBinary = await response.arrayBuffer();
-            wasmInstance = (await WebAssembly.instantiate(wasmBinary, { js_code })).instance;
-            ${createJsRun(wasmInstance = "wasmInstance", dceEnabled = dceEnabled)}
-            console.log("Test passed!");    
-            """.trimIndent()
-
-        directory.mkdirs()
-
-        File(directory, "index.html").writeText(
-            """
-            <!DOCTYPE html>
-            <html lang="en">
-            <body>
-            <script src="index.js" type="module"></script>
-            </body>
-            </html>
-            """.trimIndent()
-        )
-        File(directory, "index.js").writeText(
-            compilerResult.js + "\n" + browserRunner
-        )
-        File(directory, "index.wasm").writeBytes(
-            compilerResult.wasm
-        )
-    }
-
     private fun createConfig(languageVersionSettings: LanguageVersionSettings?): JsConfig {
         val configuration = environment.configuration.copy()
         configuration.put(CommonConfigurationKeys.MODULE_NAME, TEST_MODULE)
diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/testOld/engines/SpiderMonkey.kt b/js/js.tests/test/org/jetbrains/kotlin/js/testOld/engines/SpiderMonkey.kt
index 43f3780..da97246 100644
--- a/js/js.tests/test/org/jetbrains/kotlin/js/testOld/engines/SpiderMonkey.kt
+++ b/js/js.tests/test/org/jetbrains/kotlin/js/testOld/engines/SpiderMonkey.kt
@@ -6,6 +6,7 @@
 package org.jetbrains.kotlin.js.testOld.engines
 
 import java.io.BufferedReader
+import java.io.File
 import java.io.InputStreamReader
 import java.lang.Boolean.getBoolean
 import kotlin.test.fail
@@ -14,15 +15,27 @@
 val toolLogsEnabled: Boolean = getBoolean("kotlin.js.test.verbose")
 
 class ExternalTool(val path: String) {
-    fun run(vararg arguments: String) {
+    fun run(vararg arguments: String, workingDirectory: File? = null) {
         val command = arrayOf(path, *arguments)
-        val process = ProcessBuilder(*command)
+        val processBuilder = ProcessBuilder(*command)
             .redirectErrorStream(true)
-            .start()
+
+        if (workingDirectory != null) {
+            processBuilder.directory(workingDirectory)
+        }
+
+        val process = processBuilder.start()
+
 
         val commandString = command.joinToString(" ") { escapeShellArgument(it) }
         if (toolLogsEnabled) {
-            println(commandString)
+            println(
+                if (workingDirectory != null) {
+                    "(cd '$workingDirectory' && $commandString)"
+                } else {
+                    commandString
+                }
+            )
         }
 
         // Print process output
diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt
index 60d0f27..bfd1a8d 100644
--- a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt
+++ b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExternalWrapper.kt
@@ -139,8 +139,8 @@
 }
 
 @JsFun("""(addr) => {
-    const mem16 = new Uint16Array(wasmInstance.exports.memory.buffer);
-    const mem32 = new Int32Array(wasmInstance.exports.memory.buffer);
+    const mem16 = new Uint16Array(wasmExports.memory.buffer);
+    const mem32 = new Int32Array(wasmExports.memory.buffer);
     const len = mem32[addr / 4];
     const str_start_addr = (addr + 4) / 2;
     const slice = mem16.slice(str_start_addr, str_start_addr + len);
@@ -163,7 +163,7 @@
 //language=js
 @JsFun(
 """ (str, addr) => { 
-    const memory = new DataView(wasmInstance.exports.memory.buffer);
+    const memory = new DataView(wasmExports.memory.buffer);
     for (var i = 0; i < str.length; i++) {
         memory.setInt16(addr + i * 2, str.charCodeAt(i), true);
     }
diff --git a/libraries/stdlib/wasm/src/kotlin/io.kt b/libraries/stdlib/wasm/src/kotlin/io.kt
index 16ce6fe..6e85e8d 100644
--- a/libraries/stdlib/wasm/src/kotlin/io.kt
+++ b/libraries/stdlib/wasm/src/kotlin/io.kt
@@ -7,11 +7,11 @@
 
 import kotlin.wasm.internal.*
 
-@JsFun("(error) => console.error(\">>>  \" + error)")
+@JsFun("(error) => console.error(error)")
 internal external fun printError(error: String?): Unit
 
-@JsFun("(message) => console.log(\">>>  \" + message)")
-private external fun printlnImpl(error: String?): Unit
+@JsFun("(message) => console.log(message)")
+private external fun printlnImpl(message: String?): Unit
 
 /** Prints the line separator to the standard output stream. */
 public actual fun println() {