[WASM] Implemented Wasm unhandled exceptions on JS site
This commit is contained in:
@@ -530,6 +530,12 @@ private val removeInitializersForLazyProperties = makeWasmModulePhase(
|
||||
description = "Remove property initializers if they was initialized lazily"
|
||||
)
|
||||
|
||||
private val unhandledExceptionLowering = makeWasmModulePhase(
|
||||
::UnhandledExceptionLowering,
|
||||
name = "UnhandledExceptionLowering",
|
||||
description = "Wrap JsExport functions with try-catch to convert unhandled Wasm exception into Js exception",
|
||||
)
|
||||
|
||||
private val propertyAccessorInlinerLoweringPhase = makeWasmModulePhase(
|
||||
::PropertyAccessorInlineLowering,
|
||||
name = "PropertyAccessorInlineLowering",
|
||||
@@ -608,6 +614,8 @@ val wasmPhases = NamedCompilerPhase(
|
||||
addContinuationToFunctionCallsLoweringPhase then
|
||||
addMainFunctionCallsLowering then
|
||||
|
||||
unhandledExceptionLowering then
|
||||
|
||||
tryCatchCanonicalization then
|
||||
returnableBlockLoweringPhase then
|
||||
|
||||
|
||||
@@ -65,6 +65,11 @@ class WasmSymbols(
|
||||
|
||||
internal val eagerInitialization: IrClassSymbol = getIrClass(FqName("kotlin.EagerInitialization"))
|
||||
|
||||
internal val isNotFirstWasmExportCall: IrPropertySymbol = symbolTable.referenceProperty(
|
||||
getProperty(FqName.fromSegments(listOf("kotlin", "wasm", "internal", "isNotFirstWasmExportCall")))
|
||||
)
|
||||
internal val throwAsJsException: IrSimpleFunctionSymbol = getInternalFunction("throwAsJsException")
|
||||
|
||||
override val throwNullPointerException = getInternalFunction("THROW_NPE")
|
||||
override val throwISE = getInternalFunction("THROW_ISE")
|
||||
override val throwTypeCastException = getInternalFunction("THROW_CCE")
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid
|
||||
* WebAssembly allows only constant expressions to be used directly in
|
||||
* field initializers.
|
||||
*
|
||||
* TODO: Don't move constant expression initializers
|
||||
* TODO: Don't move null and nullable constant initializers
|
||||
* TODO: Make field initialization lazy. Needs design.
|
||||
*/
|
||||
class FieldInitializersLowering(val context: WasmBackendContext) : FileLoweringPass {
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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 org.jetbrains.kotlin.backend.wasm.lower
|
||||
|
||||
import org.jetbrains.kotlin.backend.common.FileLoweringPass
|
||||
import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
|
||||
import org.jetbrains.kotlin.backend.common.lower.irCatch
|
||||
import org.jetbrains.kotlin.backend.common.lower.irThrow
|
||||
import org.jetbrains.kotlin.backend.wasm.WasmBackendContext
|
||||
import org.jetbrains.kotlin.backend.wasm.ir2wasm.isExported
|
||||
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
|
||||
import org.jetbrains.kotlin.ir.builders.*
|
||||
import org.jetbrains.kotlin.ir.builders.declarations.buildVariable
|
||||
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
|
||||
import org.jetbrains.kotlin.ir.declarations.IrFile
|
||||
import org.jetbrains.kotlin.ir.declarations.IrFunction
|
||||
import org.jetbrains.kotlin.ir.expressions.IrBlockBody
|
||||
import org.jetbrains.kotlin.ir.expressions.IrExpressionBody
|
||||
import org.jetbrains.kotlin.ir.interpreter.toIrConst
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
|
||||
// This pass needed to wrap around unhandled exceptions from JsExport functions and throw JS exception for call from JS site
|
||||
// @JsExport
|
||||
// fun someExportedMethod() {
|
||||
// error("some error message")
|
||||
// }
|
||||
//
|
||||
// converts into
|
||||
//
|
||||
// @JsExport
|
||||
// fun someExportedMethod() {
|
||||
// val currentIsNotFirstWasmExportCall = isNotFirstWasmExportCall
|
||||
// try {
|
||||
// isNotFirstWasmExportCall = true
|
||||
// error("some error message")
|
||||
// } catch (e: Throwable) {
|
||||
// if (currentIsNotFirstWasmExportCall) throw e else throwAsJsException(e)
|
||||
// } finally {
|
||||
// isNotFirstWasmExportCall = currentIsNotFirstWasmExportCall
|
||||
// }
|
||||
// }
|
||||
// TODO Wrap fieldInitializer function (now it building by later FieldInitializersLowering)
|
||||
|
||||
internal class UnhandledExceptionLowering(val context: WasmBackendContext) : FileLoweringPass {
|
||||
private val throwableType = context.irBuiltIns.throwableType
|
||||
private val irBooleanType = context.wasmSymbols.irBuiltIns.booleanType
|
||||
private val throwAsJsException = context.wasmSymbols.throwAsJsException
|
||||
private val isNotFirstWasmExportCallGetter = context.wasmSymbols.isNotFirstWasmExportCall.owner.getter!!.symbol
|
||||
private val isNotFirstWasmExportCallSetter = context.wasmSymbols.isNotFirstWasmExportCall.owner.setter!!.symbol
|
||||
|
||||
private fun processExportFunction(irFunction: IrFunction) {
|
||||
val body = irFunction.body ?: return
|
||||
if (body is IrBlockBody && body.statements.isEmpty()) return
|
||||
|
||||
val bodyType = when (body) {
|
||||
is IrExpressionBody -> body.expression.type
|
||||
is IrBlockBody -> context.irBuiltIns.unitType
|
||||
else -> TODO(this::class.qualifiedName!!)
|
||||
}
|
||||
|
||||
with(context.createIrBuilder(irFunction.symbol)) {
|
||||
val currentIsNotFirstWasmExportCall = buildVariable(
|
||||
parent = irFunction,
|
||||
startOffset = UNDEFINED_OFFSET, endOffset = UNDEFINED_OFFSET,
|
||||
origin = IrDeclarationOrigin.IR_TEMPORARY_VARIABLE,
|
||||
name = Name.identifier("currentIsNotFirstWasmExportCall"),
|
||||
type = irBooleanType
|
||||
)
|
||||
|
||||
val e = buildVariable(
|
||||
parent = irFunction,
|
||||
startOffset = UNDEFINED_OFFSET, endOffset = UNDEFINED_OFFSET,
|
||||
origin = IrDeclarationOrigin.CATCH_PARAMETER,
|
||||
name = Name.identifier("e"),
|
||||
type = throwableType
|
||||
)
|
||||
|
||||
currentIsNotFirstWasmExportCall.initializer =
|
||||
irGet(irBooleanType, null, isNotFirstWasmExportCallGetter)
|
||||
|
||||
val tryBody = irComposite {
|
||||
+irSet(irBooleanType, null, isNotFirstWasmExportCallSetter, true.toIrConst(irBooleanType))
|
||||
when (body) {
|
||||
is IrBlockBody -> body.statements.forEach { +it }
|
||||
is IrExpressionBody -> +body.expression
|
||||
else -> TODO(this::class.qualifiedName!!)
|
||||
}
|
||||
}
|
||||
|
||||
val catch = irCatch(
|
||||
catchParameter = e,
|
||||
result = irIfThenElse(
|
||||
type = bodyType,
|
||||
condition = irGet(currentIsNotFirstWasmExportCall, irBooleanType),
|
||||
thenPart = irThrow(irGet(e, throwableType)),
|
||||
elsePart = irCall(throwAsJsException).apply { putValueArgument(0, irGet(e, throwableType)) }
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
val finally = irSet(
|
||||
type = irBooleanType,
|
||||
receiver = null,
|
||||
setterSymbol = isNotFirstWasmExportCallSetter,
|
||||
value = irGet(currentIsNotFirstWasmExportCall, irBooleanType)
|
||||
)
|
||||
|
||||
val tryWrap = irTry(
|
||||
type = bodyType,
|
||||
tryResult = tryBody,
|
||||
catches = listOf(catch),
|
||||
finallyExpression = finally
|
||||
)
|
||||
|
||||
when (body) {
|
||||
is IrExpressionBody -> body.expression = irComposite {
|
||||
+currentIsNotFirstWasmExportCall
|
||||
+tryWrap
|
||||
}
|
||||
is IrBlockBody -> with(body.statements) {
|
||||
clear()
|
||||
add(currentIsNotFirstWasmExportCall)
|
||||
add(tryWrap)
|
||||
}
|
||||
else -> TODO(this::class.qualifiedName!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun lower(irFile: IrFile) {
|
||||
for (declaration in irFile.declarations) {
|
||||
if (declaration is IrFunction && declaration.isExported()) {
|
||||
processExportFunction(declaration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ abstract class BasicWasmBoxTest(
|
||||
val outputWasmFile = File("$outputFileBase.wasm")
|
||||
val outputJsFile = File("$outputFileBase.js")
|
||||
val outputBrowserDir = File("$outputFileBase.browser")
|
||||
val outputFileNoDceBase = outputDir.absolutePath + "/" + getTestName(true) + "/noDce"
|
||||
val outputFileNoDceBase = "${outputFileBase}NoDce"
|
||||
val outputWatNoDceFile = File("$outputFileNoDceBase.wat")
|
||||
val outputWasmNoDceFile = File("$outputFileNoDceBase.wasm")
|
||||
val outputJsNoDceFile = File("$outputFileNoDceBase.js")
|
||||
@@ -118,7 +118,6 @@ abstract class BasicWasmBoxTest(
|
||||
println(" ------ WAT file://$outputWatFile")
|
||||
println(" ------ WASM file://$outputWasmFile")
|
||||
println(" ------ JS file://$outputJsFile")
|
||||
println(" ------ (No-DCE files in noDce sub-directory)")
|
||||
PhaseConfig(
|
||||
wasmPhases,
|
||||
dumpToDirectory = dumpOutputDir.path,
|
||||
@@ -140,7 +139,6 @@ abstract class BasicWasmBoxTest(
|
||||
AnalyzerWithCompilerReport(config.configuration)
|
||||
)
|
||||
|
||||
File(outputFileNoDceBase).mkdirs()
|
||||
compileAndRun(
|
||||
phaseConfig = phaseConfig,
|
||||
sourceModule = sourceModule,
|
||||
@@ -157,7 +155,6 @@ abstract class BasicWasmBoxTest(
|
||||
jsFilesAfter = jsFilesAfter
|
||||
)
|
||||
|
||||
File(outputFileBase).mkdirs()
|
||||
compileAndRun(
|
||||
phaseConfig = phaseConfig,
|
||||
sourceModule = sourceModule,
|
||||
@@ -222,15 +219,8 @@ abstract class BasicWasmBoxTest(
|
||||
const wasmBinary = read(String.raw`${outputWasmFile.absoluteFile}`, 'binary');
|
||||
const wasmModule = new WebAssembly.Module(wasmBinary);
|
||||
wasmInstance = new WebAssembly.Instance(wasmModule, { runtime, js_code });
|
||||
wasmInstance.exports.__init();
|
||||
|
||||
const ${sanitizeName(config.moduleId)} = wasmInstance.exports;
|
||||
|
||||
wasmInstance.exports.startUnitTests?.();
|
||||
|
||||
const actualResult = wasmInstance.exports.$testFunction();
|
||||
if (actualResult !== "OK")
|
||||
throw `Wrong box result '${'$'}{actualResult}' (with DCE=${dceEnabled}); Expected "OK"`;
|
||||
${createJsRun(wasmInstance = "wasmInstance", testFunction = testFunction, dceEnabled = dceEnabled)}
|
||||
""".trimIndent()
|
||||
outputJsFile.write(compilerResult.js + "\n" + testRunner)
|
||||
|
||||
@@ -249,18 +239,30 @@ abstract class BasicWasmBoxTest(
|
||||
)
|
||||
}
|
||||
|
||||
private fun createJsRun(wasmInstance: String, testFunction: String, dceEnabled: Boolean) = """
|
||||
let actualResult
|
||||
try {
|
||||
$wasmInstance.exports.__init();
|
||||
$wasmInstance.exports.startUnitTests?.();
|
||||
actualResult = $wasmInstance.exports.$testFunction();
|
||||
} 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, { runtime, js_code })).instance;
|
||||
wasmInstance.exports.__init();
|
||||
wasmInstance.exports.startUnitTests?.();
|
||||
|
||||
const actualResult = wasmInstance.exports.box();
|
||||
if (actualResult !== "OK")
|
||||
throw `Wrong box result '${'$'}{actualResult}' (with DCE=${dceEnabled}); Expected "OK"`;
|
||||
${createJsRun(wasmInstance = "wasmInstance", testFunction = "box", dceEnabled = dceEnabled)}
|
||||
console.log("Test passed!");
|
||||
""".trimIndent()
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2010-2022 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.wasm.internal
|
||||
|
||||
@JsFun(
|
||||
"""(message, wasmTypeName, stack) => {
|
||||
const error = new Error();
|
||||
error.message = message;
|
||||
error.name = wasmTypeName;
|
||||
error.stack = stack;
|
||||
throw error;
|
||||
}"""
|
||||
)
|
||||
private external fun throwJsError(message: String?, wasmTypeName: String?, stack: String)
|
||||
|
||||
internal fun throwAsJsException(t: Throwable): Nothing {
|
||||
throwJsError(t.message, t::class.simpleName, t.stackTraceToString())
|
||||
return error("Unreachable")
|
||||
}
|
||||
|
||||
internal var isNotFirstWasmExportCall: Boolean = false
|
||||
Reference in New Issue
Block a user