Compare commits

...

6 Commits

Author SHA1 Message Date
Svyatoslav Kuzmich
db0aa9d8e7 [Wasm] Enable for loops optimization 2020-11-09 19:17:41 +03:00
Svyatoslav Kuzmich
f93653cff6 [Wasm] Enable maven-publish plugin in wasm stdlib 2020-11-09 19:17:16 +03:00
Svyatoslav Kuzmich
1000f9d97b [Wasm] Don't generate unnecessary unreachable instructions
Helps with WAT readability
2020-11-09 18:09:18 +03:00
Svyatoslav Kuzmich
67033e0fde [Wasm] Indent blocks of produced WAT
Makes it more readable
2020-11-09 18:08:00 +03:00
Svyatoslav Kuzmich
f2fb24600c [Wasm] Add interop annotation @JsFun 2020-11-09 18:07:06 +03:00
Svyatoslav Kuzmich
6800c25fa5 [Wasm] Basic CLI
-Xwasm option that will produce wasm instead of JS when used with -Xir-produce-js
Does not affect klib production
2020-11-09 17:53:10 +03:00
21 changed files with 205 additions and 121 deletions

View File

@@ -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
irProduceKlibDir || irProduceJs || irProduceKlibFile || wasm

View File

@@ -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"))

View File

@@ -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,34 @@ class K2JsIrCompiler : CLICompiler<K2JSCompilerArguments>() {
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,

View File

@@ -382,7 +382,9 @@ internal class IndexedGetLoopHeader(
with(builder) {
// loopVariable = objectVariable[inductionVariable]
val indexedGetFun = with(headerInfo.expressionHandler) { headerInfo.objectVariable.type.getFunction }
val get = irCall(indexedGetFun.symbol).apply {
// Making sure that expression type has type of the variable when it exists.
// Return type of get function can be a type parameter (for example Array<T>::get) which is not a subtype of loopVariable type.
val get = irCall(indexedGetFun.symbol, type = loopVariable?.type ?: indexedGetFun.returnType).apply {
dispatchReceiver = irGet(headerInfo.objectVariable)
putValueArgument(0, irGet(inductionVariable))
}

View File

@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.backend.wasm
import org.jetbrains.kotlin.backend.common.FileLoweringPass
import org.jetbrains.kotlin.backend.common.lower.*
import org.jetbrains.kotlin.backend.common.lower.inline.FunctionInlining
import org.jetbrains.kotlin.backend.common.lower.loops.ForLoopsLowering
import org.jetbrains.kotlin.backend.common.phaser.*
import org.jetbrains.kotlin.backend.wasm.lower.*
import org.jetbrains.kotlin.ir.backend.js.lower.*
@@ -399,6 +400,12 @@ private val virtualDispatchReceiverExtractionPhase = makeWasmModulePhase(
description = "Eliminate side-effects in dispatch receivers of virtual function calls"
)
private val forLoopsLoweringPhase = makeWasmModulePhase(
::ForLoopsLowering,
name = "ForLoopsLowering",
description = "[Optimization] For loops lowering"
)
val wasmPhases = NamedCompilerPhase(
name = "IrModuleLowering",
description = "IR module lowering",
@@ -447,6 +454,9 @@ val wasmPhases = NamedCompilerPhase(
stringConstructorLowering then
returnableBlockLoweringPhase then
forLoopsLoweringPhase then
defaultArgumentStubGeneratorPhase then
defaultArgumentPatchOverridesPhase then
defaultParameterInjectorPhase then

View File

@@ -32,7 +32,7 @@ class WasmCompilerResult(val wat: String, val js: String, val wasm: ByteArray)
fun compileWasm(
project: Project,
files: List<KtFile>,
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,81 @@ 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_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()
val jsCode =
"\nconst js_code = {${jsFuns.joinToString(",\n") { "\"" + it.importName + "\" : " + it.jsCode }}};"
return runtime + generateStringLiteralsSupport(stringLiterals) + jsCode
}

View File

@@ -208,7 +208,7 @@ class BodyGenerator(val context: WasmFunctionCodegenContext) : IrElementVisitorV
// Return types of imported functions cannot have concrete struct/array references.
// Non-primitive return types are represented as eqref which need to be casted back to expected type on call site.
if (function.getWasmImportAnnotation() != null) {
if (function.getWasmImportAnnotation() != null || function.getJsFunAnnotation() != null) {
val resT = context.transformResultType(function.returnType)
if (resT is WasmRefNullType) {
generateTypeRTT(function.returnType)

View File

@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.backend.wasm.ir2wasm
import org.jetbrains.kotlin.backend.common.ir.isOverridableOrOverrides
import org.jetbrains.kotlin.backend.wasm.WasmBackendContext
import org.jetbrains.kotlin.backend.wasm.lower.wasmSignature
import org.jetbrains.kotlin.backend.wasm.utils.*
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
import org.jetbrains.kotlin.descriptors.Modality
@@ -35,12 +36,23 @@ class DeclarationGenerator(val context: WasmModuleCodegenContext) : IrElementVis
// Type aliases are not material
}
private fun jsCodeName(declaration: IrFunction): String {
return declaration.fqNameWhenAvailable!!.asString() + "_" + (declaration as IrSimpleFunction).wasmSignature(irBuiltIns).hashCode()
}
override fun visitFunction(declaration: IrFunction) {
// Inline class constructors are currently empty
if (declaration is IrConstructor && backendContext.inlineClassesUtils.isClassInlineLike(declaration.parentAsClass))
return
val importedName = declaration.getWasmImportAnnotation()
val jsCode = declaration.getJsFunAnnotation()
val importedName = if (jsCode != null) {
val jsCodeName = jsCodeName(declaration)
context.addJsFun(jsCodeName, jsCode)
WasmImportPair("js_code", jsCodeName(declaration))
} else {
declaration.getWasmImportAnnotation()
}
val isIntrinsic = declaration.hasWasmReinterpretAnnotation() || declaration.getWasmOpAnnotation() != null
if (isIntrinsic) {

View File

@@ -46,7 +46,10 @@ class WasmCompiledModuleFragment {
ReferencableAndDefinable<IrClassSymbol, ConstantDataElement>()
val exports = mutableListOf<WasmExport<*>>()
//
class JsCodeSnippet(val importName: String, val jsCode: String)
val jsFuns = mutableListOf<JsCodeSnippet>()
var startFunction: WasmFunction? = null
open class ReferencableElements<Ir, Wasm : Any> {

View File

@@ -22,6 +22,7 @@ interface WasmModuleCodegenContext : WasmBaseCodegenContext {
fun defineStructType(irClass: IrClassSymbol, wasmStruct: WasmStructDeclaration)
fun defineRTT(irClass: IrClassSymbol, wasmGlobal: WasmGlobal)
fun defineFunctionType(irFunction: IrFunctionSymbol, wasmFunctionType: WasmFunctionType)
fun addJsFun(importName: String, jsCode: String)
fun setStartFunction(wasmFunction: WasmFunction)
fun addExport(wasmExport: WasmExport<*>)

View File

@@ -166,4 +166,9 @@ class WasmModuleCodegenContextImpl(
val fieldId = metadata.fields.indexOf(field)
return WasmSymbol(fieldId)
}
override fun addJsFun(importName: String, jsCode: String) {
wasmFragment.jsFuns +=
WasmCompiledModuleFragment.JsCodeSnippet(importName = importName, jsCode = jsCode)
}
}

View File

@@ -35,3 +35,6 @@ fun IrAnnotationContainer.getWasmImportAnnotation(): WasmImportPair? =
(it.getValueArgument(1) as IrConst<*>).value as String
)
}
fun IrAnnotationContainer.getJsFunAnnotation(): String? =
getAnnotation(FqName("kotlin.JsFun"))?.getSingleConstStringArgument()

View File

@@ -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.wasm.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,19 +151,17 @@ 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);
const wasmInstance = new WebAssembly.Instance(wasmModule, { runtime });
const wasmInstance = new WebAssembly.Instance(wasmModule, { runtime, js_code });
const actualResult = wasmInstance.exports.$testFunction();
if (actualResult !== "OK")
throw `Wrong box result '${'$'}{actualResult}'; Expected "OK"`;
""".trimIndent()
outputJsFile.write(runtime + "\n" + compilerResult.js + "\n" + testRunner)
outputJsFile.write(compilerResult.js + "\n" + testRunner)
}

View File

@@ -1,6 +1,7 @@
import org.jetbrains.kotlin.gradle.dsl.KotlinCompile
plugins {
`maven-publish`
kotlin("multiplatform")
}

View File

@@ -51,11 +51,11 @@ public class String constructor(public val string: String) : Comparable<String>,
public override fun hashCode(): Int = 10
}
@WasmImport("runtime", "String_plus")
@JsFun("(it, other) => it + String(other)")
private fun stringPlusImpl(it: String, other: String): String =
implementedAsIntrinsic
@WasmImport("runtime", "String_getLength")
@JsFun("(it) => it.length")
private fun stringLengthImpl(it: String): Int =
implementedAsIntrinsic

View File

@@ -72,11 +72,11 @@ internal fun JsArray_set_WasmExternRef(array: WasmExternRef, index: Int, value:
internal fun JsArray_getSize(array: WasmExternRef): Int =
implementedAsIntrinsic
@WasmImport("runtime", "identity")
@JsFun("(x) => x")
internal fun Any?.toWasmExternRef(): WasmExternRef =
implementedAsIntrinsic
@WasmImport("runtime", "identity")
@JsFun("(x) => x")
internal fun WasmExternRefToAny(ref: WasmExternRef): Any? =
implementedAsIntrinsic

View File

@@ -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)
}
};

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2010-2020 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.
*/
package kotlin
/**
* Implements annotated function in JavaScript and automatically imports is to Wasm.
* [code] string must contain JS expression that evaluates to JS function with signature that matches annotated kotlin function
*
* For example, a function that adds two Doubles via JS:
*
* @JsFun("(x, y) => x + y")
* fun jsAdd(x: Double, y: Double): Double =
* error("...")
*
* This is a temporary annotation because K/Wasm <-> JS interop is not designed yet.
*/
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.BINARY)
public annotation class JsFun(val code: String)

View File

@@ -9,6 +9,8 @@ abstract class WasmExpressionBuilder {
abstract fun buildInstr(op: WasmOp, vararg immediates: WasmImmediate)
abstract var numberOfNestedBlocks: Int
abstract val lastInstr: WasmOp?
fun buildConstI32(value: Int) {
buildInstr(WasmOp.I32_CONST, WasmImmediate.ConstI32(value))
}
@@ -30,6 +32,10 @@ abstract class WasmExpressionBuilder {
}
fun buildUnreachable() {
// Unreachable is not needed
if (lastInstr == WasmOp.UNREACHABLE || lastInstr == WasmOp.RETURN)
return
buildInstr(WasmOp.UNREACHABLE)
}

View File

@@ -19,4 +19,7 @@ class WasmIrExpressionBuilder(
assert(value >= 0) { "end without matching block" }
field = value
}
override val lastInstr: WasmOp?
get() = expression.lastOrNull()?.operator
}

View File

@@ -59,8 +59,16 @@ class WasmIrToText : SExpressionBuilder() {
}
private fun appendInstr(wasmInstr: WasmInstr) {
val op = wasmInstr.operator
if (op == WasmOp.END || op == WasmOp.ELSE)
indent--
newLine()
stringBuilder.append(wasmInstr.operator.mnemonic)
if (op == WasmOp.BLOCK || op == WasmOp.LOOP || op == WasmOp.IF || op == WasmOp.ELSE)
indent++
if (wasmInstr.operator in setOf(WasmOp.CALL_INDIRECT, WasmOp.TABLE_INIT)) {
wasmInstr.immediates.reversed().forEach {
appendImmediate(it)