From e51a76bc4e4e81b303d531affb92b6d4412c62a3 Mon Sep 17 00:00:00 2001 From: Svyatoslav Kuzmich Date: Mon, 19 Oct 2020 17:34:31 +0300 Subject: [PATCH] [Wasm] Basic CLI -Xwasm option that will produce wasm instead of JS when used with -Xir-produce-js Does not affect klib production --- .../common/arguments/K2JSCompilerArguments.kt | 5 +- compiler/cli/cli-js/build.gradle.kts | 1 + .../jetbrains/kotlin/cli/js/K2JsIrCompiler.kt | 31 ++++++ .../jetbrains/kotlin/backend/wasm/compiler.kt | 90 ++++++++++++++-- compiler/testData/cli/js/jsExtraHelp.out | 1 + .../kotlin/js/test/BasicWasmBoxTest.kt | 9 +- libraries/stdlib/wasm/runtime/runtime.js | 100 ------------------ 7 files changed, 125 insertions(+), 112 deletions(-) delete mode 100644 libraries/stdlib/wasm/runtime/runtime.js 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 a9e2c90a927..964e397d962 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 @@ -178,6 +178,9 @@ class K2JSCompilerArguments : CommonCompilerArguments() { @Argument(value = "-Xerror-tolerance-policy", description = "Set up error tolerance policy (NONE, SEMANTIC, SYNTAX, ALL)") var errorTolerancePolicy: String? by NullableStringFreezableVar(null) + @Argument(value = "-Xwasm", description = "Use experimental WebAssembly compiler backend") + var wasm: Boolean by FreezableVar(false) + override fun checkIrSupport(languageVersionSettings: LanguageVersionSettings, collector: MessageCollector) { if (!isIrBackendEnabled()) return @@ -196,4 +199,4 @@ fun K2JSCompilerArguments.isPreIrBackendDisabled(): Boolean = irOnly || irProduceJs || irProduceKlibFile fun K2JSCompilerArguments.isIrBackendEnabled(): Boolean = - irProduceKlibDir || irProduceJs || irProduceKlibFile \ No newline at end of file + irProduceKlibDir || irProduceJs || irProduceKlibFile || wasm \ No newline at end of file diff --git a/compiler/cli/cli-js/build.gradle.kts b/compiler/cli/cli-js/build.gradle.kts index 4fbc4db747c..70d7c6f3adc 100644 --- a/compiler/cli/cli-js/build.gradle.kts +++ b/compiler/cli/cli-js/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { compile(project(":compiler:ir.backend.common")) compile(project(":compiler:ir.serialization.js")) compile(project(":compiler:backend.js")) + compile(project(":compiler:backend.wasm")) compile(project(":js:js.translator")) compile(project(":js:js.serializer")) compile(project(":js:js.dce")) 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 2e14b5261f8..b537004fe84 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 @@ -8,7 +8,10 @@ package org.jetbrains.kotlin.cli.js import com.intellij.openapi.Disposable import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.text.StringUtil +import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig import org.jetbrains.kotlin.backend.common.serialization.metadata.KlibMetadataVersion +import org.jetbrains.kotlin.backend.wasm.compileWasm +import org.jetbrains.kotlin.backend.wasm.wasmPhases import org.jetbrains.kotlin.cli.common.* import org.jetbrains.kotlin.cli.common.ExitCode.COMPILATION_ERROR import org.jetbrains.kotlin.cli.common.ExitCode.OK @@ -34,6 +37,7 @@ import org.jetbrains.kotlin.ir.backend.js.* import org.jetbrains.kotlin.ir.declarations.persistent.PersistentIrFactory import org.jetbrains.kotlin.js.config.* import org.jetbrains.kotlin.metadata.deserialization.BinaryVersion +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.serialization.js.ModuleKind import org.jetbrains.kotlin.util.Logger @@ -216,6 +220,33 @@ class K2JsIrCompiler : CLICompiler() { MainModule.SourceFiles(sourcesFiles) } + if (arguments.wasm) { + val res = compileWasm( + projectJs, + mainModule, + AnalyzerWithCompilerReport(config.configuration), + config.configuration, + PhaseConfig(wasmPhases), + allDependencies = resolvedLibraries, + friendDependencies = friendDependencies, + exportedDeclarations = setOf(FqName("main")) + ) + val outputWasmFile = outputFile.withReplacedExtensionOrNull(outputFile.extension, "wasm")!! + outputWasmFile.writeBytes(res.wasm) + val outputWatFile = outputFile.withReplacedExtensionOrNull(outputFile.extension, "wat")!! + outputWatFile.writeText(res.wat) + + val runner = """ + const wasmBinary = read(String.raw`${outputWasmFile.absoluteFile}`, 'binary'); + const wasmModule = new WebAssembly.Module(wasmBinary); + const wasmInstance = new WebAssembly.Instance(wasmModule, { runtime, js_code }); + wasmInstance.exports.main(); + """.trimIndent() + + outputFile.writeText(res.js + "\n" + runner) + return OK + } + val compiledModule = try { compile( projectJs, 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 0c9c937fe17..81ecd222755 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 @@ -32,7 +32,7 @@ class WasmCompilerResult(val wat: String, val js: String, val wasm: ByteArray) fun compileWasm( project: Project, - files: List, + mainModule: MainModule, analyzer: AbstractAnalyzerWithCompilerReport, configuration: CompilerConfiguration, phaseConfig: PhaseConfig, @@ -42,21 +42,26 @@ fun compileWasm( ): WasmCompilerResult { val (moduleFragment, dependencyModules, irBuiltIns, symbolTable, deserializer) = loadIr( - project, MainModule.SourceFiles(files), analyzer, configuration, allDependencies, friendDependencies, + project, mainModule, analyzer, configuration, allDependencies, friendDependencies, PersistentIrFactory ) + val allModules = when (mainModule) { + is MainModule.SourceFiles -> dependencyModules + listOf(moduleFragment) + is MainModule.Klib -> dependencyModules + } + val moduleDescriptor = moduleFragment.descriptor val context = WasmBackendContext(moduleDescriptor, irBuiltIns, symbolTable, moduleFragment, exportedDeclarations, configuration) // Load declarations referenced during `context` initialization - dependencyModules.forEach { + allModules.forEach { val irProviders = generateTypicalIrProviderList(it.descriptor, irBuiltIns, symbolTable, deserializer) ExternalDependenciesGenerator(symbolTable, irProviders, configuration.languageVersionSettings) .generateUnboundSymbolsAsDependencies() } - val irFiles = dependencyModules.flatMap { it.files } + moduleFragment.files + val irFiles = allModules.flatMap { it.files } moduleFragment.files.clear() moduleFragment.files += irFiles @@ -77,13 +82,86 @@ fun compileWasm( watGenerator.appendWasmModule(linkedModule) val wat = watGenerator.toString() + val js = compiledWasmModule.generateJs() + val os = ByteArrayOutputStream() WasmIrToBinary(os, linkedModule).appendWasmModule() val byteArray = os.toByteArray() return WasmCompilerResult( - wat, - generateStringLiteralsSupport(compiledWasmModule.stringLiterals), + wat = wat, + js = js, wasm = byteArray ) } + + +fun WasmCompiledModuleFragment.generateJs(): String { + val runtime = """ + const runtime = { + String_plus(str1, str2) { + return str1 + String(str2); + }, + + String_getLength(str) { + return str.length; + }, + + String_getChar(str, index) { + return str.charCodeAt(index); + }, + + String_compareTo(str1, str2) { + if (str1 > str2) return 1; + if (str1 < str2) return -1; + return 0; + }, + + String_equals(str, other) { + return str === other; + }, + + String_subsequence(str, startIndex, endIndex) { + return str.substring(startIndex, endIndex); + }, + + String_getLiteral(index) { + return runtime.stringLiterals[index]; + }, + + coerceToString(value) { + return String(value); + }, + + Char_toString(char) { + return String.fromCharCode(char) + }, + + JsArray_new(size) { + return new Array(size); + }, + + JsArray_get(array, index) { + return array[index]; + }, + + JsArray_set(array, index, value) { + array[index] = value; + }, + + JsArray_getSize(array) { + return array.length; + }, + + identity(x) { + return x; + }, + + println(value) { + console.log(">>> " + value) + } + }; + """.trimIndent() + + return runtime + generateStringLiteralsSupport(stringLiterals) +} diff --git a/compiler/testData/cli/js/jsExtraHelp.out b/compiler/testData/cli/js/jsExtraHelp.out index 5bdb1b8e1cc..134142ad9d0 100644 --- a/compiler/testData/cli/js/jsExtraHelp.out +++ b/compiler/testData/cli/js/jsExtraHelp.out @@ -21,6 +21,7 @@ where advanced options include: -Xir-produce-klib-file Generate packed klib into file specified by -output. Disables pre-IR backend -Xmetadata-only Generate *.meta.js and *.kjsm files only -Xtyped-arrays Translate primitive arrays to JS typed arrays + -Xwasm Use experimental WebAssembly compiler backend -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 -Xcheck-phase-conditions Check pre- and postconditions on phases diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicWasmBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicWasmBoxTest.kt index 8a9c4eed27b..a23c8828dcf 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicWasmBoxTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicWasmBoxTest.kt @@ -19,6 +19,7 @@ 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.ir.backend.js.MainModule import org.jetbrains.kotlin.ir.backend.js.loadKlib import org.jetbrains.kotlin.js.config.JsConfig import org.jetbrains.kotlin.js.facade.TranslationUnit @@ -114,7 +115,7 @@ abstract class BasicWasmBoxTest( testFunction: String ) { val filesToCompile = units.map { (it as TranslationUnit.SourceFile).file } - val debugMode =getBoolean("kotlin.js.debugMode") + val debugMode = getBoolean("kotlin.js.debugMode") val phaseConfig = if (debugMode) { val allPhasesSet = wasmPhases.toPhaseMap().values.toSet() @@ -137,7 +138,7 @@ abstract class BasicWasmBoxTest( val compilerResult = compileWasm( project = config.project, - files = filesToCompile, + mainModule = MainModule.SourceFiles(filesToCompile), analyzer = AnalyzerWithCompilerReport(config.configuration), configuration = config.configuration, phaseConfig = phaseConfig, @@ -150,8 +151,6 @@ abstract class BasicWasmBoxTest( outputWatFile.write(compilerResult.wat) outputWasmFile.writeBytes(compilerResult.wasm) - val runtime = File("libraries/stdlib/wasm/runtime/runtime.js").readText() - val testRunner = """ const wasmBinary = read(String.raw`${outputWasmFile.absoluteFile}`, 'binary'); const wasmModule = new WebAssembly.Module(wasmBinary); @@ -162,7 +161,7 @@ abstract class BasicWasmBoxTest( throw `Wrong box result '${'$'}{actualResult}'; Expected "OK"`; """.trimIndent() - outputJsFile.write(runtime + "\n" + compilerResult.js + "\n" + testRunner) + outputJsFile.write(compilerResult.js + "\n" + testRunner) } diff --git a/libraries/stdlib/wasm/runtime/runtime.js b/libraries/stdlib/wasm/runtime/runtime.js deleted file mode 100644 index 85e0786bb3a..00000000000 --- a/libraries/stdlib/wasm/runtime/runtime.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. - * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. - */ - -const runtime = { - /** - * kotlin.String#plus(other: Any?): String - * @return {string} - */ - String_plus(str1, str2) { - if (typeof str1 != "string") throw `Illegal argument str1: ${str1}`; - return str1 + String(str2); - }, - - /** - * @return {number} - */ - String_getLength(str) { - if (typeof str != "string") throw `Illegal argument x: ${str}`; - return str.length; - }, - - /** - * @return {number} - */ - String_getChar(str, index) { - if (typeof str != "string") throw `Illegal argument str: ${str}`; - if (typeof index != "number") throw `Illegal argument index: ${index}`; - return str.charCodeAt(index); - }, - - /** - * @return {number} - */ - String_compareTo(str1, str2) { - if (str1 > str2) return 1; - if (str1 < str2) return -1; - return 0; - }, - - /** - * @return {boolean} - */ - String_equals(str, other) { - // if (typeof str != "string") throw `Illegal argument str: ${str}`; - return str === other; - }, - - /** - * @return {string} - */ - String_subsequence(str, startIndex, endIndex) { - return str.substring(startIndex, endIndex); - }, - - String_getLiteral(index) { - return runtime.stringLiterals[index]; - }, - - coerceToString(value) { - return String(value); - }, - - /** - * @return {string} - */ - Char_toString(char) { - return String.fromCharCode(char) - }, - - JsArray_new(size) { - if (typeof size != "number") throw `Illegal argument size: ${size}`; - return new Array(size); - }, - - JsArray_get(array, index) { - if (typeof index != "number") throw `Illegal argument index: ${index}`; - if (array.length <= index) throw `Index out of bounds: index=${index} length=${array.length}`; - return array[index]; - }, - - JsArray_set(array, index, value) { - if (typeof index != "number") throw `Illegal argument index: ${index}`; - if (array.length <= index) throw `Index out of bounds: index=${index} length=${array.length}`; - array[index] = value; - }, - - JsArray_getSize(array) { - return array.length; - }, - - identity(x) { - return x; - }, - - println(value) { - console.log(">>> " + value) - } -}; \ No newline at end of file