diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt index e2c8838d06a..2ee10afb325 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt @@ -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 diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt index 3c5e7ba8da8..f48d63a2c1a 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmSymbols.kt @@ -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") diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/FieldInitializersLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/FieldInitializersLowering.kt index b20b965844d..75f7c4f0b94 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/FieldInitializersLowering.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/FieldInitializersLowering.kt @@ -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 { diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/UnhandledExceptionLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/UnhandledExceptionLowering.kt new file mode 100644 index 00000000000..e7ae6a4568f --- /dev/null +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/UnhandledExceptionLowering.kt @@ -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) + } + } + } +} \ No newline at end of file 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 da2ccf04462..663ea7e5738 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 @@ -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() diff --git a/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExceptionHelpers.kt b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExceptionHelpers.kt new file mode 100644 index 00000000000..0f0fab2c09f --- /dev/null +++ b/libraries/stdlib/wasm/internal/kotlin/wasm/internal/ExceptionHelpers.kt @@ -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 \ No newline at end of file