[WasmJs] Support catching JS exceptions

Fixed #KT-65660
This commit is contained in:
Igor Yakovlev
2024-02-12 20:41:38 +01:00
committed by Space Team
parent c6aac835e5
commit 8fe5cf2641
18 changed files with 216 additions and 26 deletions
@@ -432,11 +432,18 @@ private val excludeDeclarationsFromCodegenPhase = makeCustomPhase<WasmBackendCon
description = "Move excluded declarations to separate place"
)
private val jsExceptionReveal = makeIrModulePhase(
::JsExceptionRevealLowering,
name = "JsExceptionRevealLowering",
description = "Wraps try statement into try with revealed JS exception",
prerequisite = setOf(functionInliningPhase)
)
private val tryCatchCanonicalization = makeIrModulePhase(
::TryCatchCanonicalization,
name = "TryCatchCanonicalization",
description = "Transforms try/catch statements into canonical form supported by the wasm codegen",
prerequisite = setOf(functionInliningPhase)
prerequisite = setOf(functionInliningPhase, jsExceptionReveal)
)
private val bridgesConstructionPhase = makeIrModulePhase(
@@ -566,6 +573,7 @@ private val unhandledExceptionLowering = makeIrModulePhase(
::UnhandledExceptionLowering,
name = "UnhandledExceptionLowering",
description = "Wrap JsExport functions with try-catch to convert unhandled Wasm exception into Js exception",
prerequisite = setOf(jsExceptionReveal)
)
private val propertyAccessorInlinerLoweringPhase = makeIrModulePhase(
@@ -699,8 +707,8 @@ val loweringList = listOf(
invokeOnExportedFunctionExitLowering,
jsExceptionReveal,
unhandledExceptionLowering,
tryCatchCanonicalization,
forLoopsLoweringPhase,
@@ -410,6 +410,9 @@ class WasmSymbols(
getInternalFunction("throwAsJsException")
val kExternalClassImpl: IrClassSymbol = getInternalClass("KExternalClassImpl")
val jsException = getIrClass(FqName("kotlin.js.JsException"))
val throwJsException = getInternalFunction("throwJsException")
}
private val wasmExportClass = getIrClass(FqName("kotlin.wasm.WasmExport"))
@@ -64,6 +64,9 @@ private fun buildRoots(modules: List<IrModuleFragment>, context: WasmBackendCont
add(context.fieldInitFunction)
add(context.findUnitInstanceField())
add(context.irBuiltIns.unitClass.owner.primaryConstructor!!)
if (context.isWasmJsTarget) {
add(context.wasmSymbols.jsRelatedSymbols.throwJsException.owner)
}
// Remove all functions used to call a kotlin closure from JS side, reachable ones will be added back later.
removeAll(context.closureCallExports.values)
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.backend.wasm.ir2wasm
import org.jetbrains.kotlin.backend.common.ir.returnType
import org.jetbrains.kotlin.backend.wasm.WasmBackendContext
import org.jetbrains.kotlin.backend.wasm.WasmSymbols
import org.jetbrains.kotlin.backend.wasm.lower.JsExceptionRevealOrigin
import org.jetbrains.kotlin.backend.wasm.utils.*
import org.jetbrains.kotlin.backend.wasm.utils.isCanonical
import org.jetbrains.kotlin.ir.IrBuiltIns
@@ -675,16 +676,7 @@ class BodyGenerator(
functionContext.stepOutLastInlinedFunction()
}
override fun visitContainerExpression(expression: IrContainerExpression) {
val statements = expression.statements
if (statements.isEmpty()) {
if (expression.type == irBuiltIns.unitType) {
body.buildGetUnit()
}
return
}
private fun processContainerExpression(expression: IrContainerExpression) {
if (expression is IrReturnableBlock) {
val inlineFunction = expression.symbol.owner.inlineFunction
val correspondingProperty = (inlineFunction as? IrSimpleFunction)?.correspondingPropertySymbol
@@ -698,6 +690,7 @@ class BodyGenerator(
)
}
val statements = expression.statements
statements.forEachIndexed { i, statement ->
if (i != statements.lastIndex) {
generateStatement(statement)
@@ -719,6 +712,32 @@ class BodyGenerator(
}
}
override fun visitContainerExpression(expression: IrContainerExpression) {
if (expression.statements.isEmpty()) {
if (expression.type == irBuiltIns.unitType) {
body.buildGetUnit()
}
return
}
if (context.backendContext.isWasmJsTarget && expression.origin == JsExceptionRevealOrigin.JS_EXCEPTION_REVEAL) {
body.buildTry(null, context.transformBlockResultType(expression.type))
processContainerExpression(expression)
val revealLocation = SourceLocation.NoLocation("JS exception reveal")
body.buildCatch(functionContext.tagIdx)
body.buildInstr(WasmOp.RETHROW, revealLocation, WasmImmediate.LabelIdx(0))
body.buildCatchAll()
body.buildCall(
symbol = context.referenceFunction(context.backendContext.wasmSymbols.jsRelatedSymbols.throwJsException),
location = revealLocation
)
body.buildUnreachable(revealLocation)
body.buildEnd()
} else {
processContainerExpression(expression)
}
}
override fun visitBreak(jump: IrBreak) {
assert(jump.type == irBuiltIns.nothingType)
body.buildBr(functionContext.referenceLoopLevel(jump.loop, LoopLabelType.BREAK), jump.getSourceLocation())
@@ -0,0 +1,82 @@
/*
* Copyright 2010-2024 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.BodyLoweringPass
import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext
import org.jetbrains.kotlin.backend.common.lower.createIrBuilder
import org.jetbrains.kotlin.backend.wasm.WasmBackendContext
import org.jetbrains.kotlin.backend.wasm.lower.JsExceptionRevealOrigin.Companion.JS_EXCEPTION_REVEAL
import org.jetbrains.kotlin.ir.builders.irComposite
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.symbols.IrSymbol
import org.jetbrains.kotlin.ir.types.defaultType
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
import org.jetbrains.kotlin.js.config.JSConfigurationKeys
/**
* Wraps try block with finalizer and/or catch block for Throwable/JsException into JS reveal intrinsic
*
* try {
* foo()
* } catch(e: JsExpression) {
* bar()
* }
*
// converts into
*
* try {
* composite { foo() } with origin JS_EXCEPTION_REVEAL
* } catch(e: JsExpression) {
* bar()
* }
*/
interface JsExceptionRevealOrigin : IrStatementOrigin {
companion object {
val JS_EXCEPTION_REVEAL by IrStatementOriginImpl
}
}
class JsExceptionRevealLowering(private val context: WasmBackendContext) : BodyLoweringPass {
override fun lower(irBody: IrBody, container: IrDeclaration) {
if (context.isWasmJsTarget && !context.configuration.getBoolean(JSConfigurationKeys.WASM_USE_TRAPS_INSTEAD_OF_EXCEPTIONS)) {
irBody.transformChildrenVoid(JsExceptionRevealTransformer(context, container.symbol))
}
}
private class JsExceptionRevealTransformer(
val context: WasmBackendContext,
val containerSymbol: IrSymbol,
) : IrElementTransformerVoidWithContext() {
private fun needToReveal(aTry: IrTry): Boolean {
if (aTry.finallyExpression != null) return true
val throwableType = context.irBuiltIns.throwableType
val jsExceptionType = context.wasmSymbols.jsRelatedSymbols.jsException.defaultType
return aTry.catches.any {
it.catchParameter.type.let { it == throwableType || it == jsExceptionType }
}
}
override fun visitTry(aTry: IrTry): IrExpression {
aTry.transformChildrenVoid(this)
if (!needToReveal(aTry)) return aTry
context.createIrBuilder(containerSymbol).run {
aTry.tryResult = irComposite(
resultType = aTry.tryResult.type,
origin = JS_EXCEPTION_REVEAL
) {
+aTry.tryResult
}
}
return aTry
}
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
// TARGET_BACKEND: WASM
// RUN_THIRD_PARTY_OPTIMIZER
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 12_773
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 13_220
// WASM_DCE_EXPECTED_OUTPUT_SIZE: mjs 5_411
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 2_640
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 2_824
// FILE: test.kt
+2 -2
View File
@@ -1,9 +1,9 @@
// TARGET_BACKEND: WASM
// RUN_THIRD_PARTY_OPTIMIZER
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 35_154
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 35_681
// WASM_DCE_EXPECTED_OUTPUT_SIZE: mjs 5_431
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 8_586
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 8_830
fun box(): String {
println("Hello, World!")
+2 -2
View File
@@ -1,9 +1,9 @@
// TARGET_BACKEND: WASM
// RUN_THIRD_PARTY_OPTIMIZER
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 14_178
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 14_584
// WASM_DCE_EXPECTED_OUTPUT_SIZE: mjs 5_968
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 4_317
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 4_570
// FILE: test.kt
+2 -2
View File
@@ -1,9 +1,9 @@
// TARGET_BACKEND: WASM
// RUN_THIRD_PARTY_OPTIMIZER
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 17_907
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 18_320
// WASM_DCE_EXPECTED_OUTPUT_SIZE: mjs 5_277
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 5_841
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 6_095
object Simple
+2 -2
View File
@@ -1,8 +1,8 @@
// TARGET_BACKEND: WASM
// RUN_THIRD_PARTY_OPTIMIZER
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 12_856
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 13_262
// WASM_DCE_EXPECTED_OUTPUT_SIZE: mjs 5_277
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 3_743
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 3_996
fun box() = "OK"
+2 -2
View File
@@ -2,9 +2,9 @@
// TARGET_BACKEND: WASM
// RUN_THIRD_PARTY_OPTIMIZER
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 13_321
// WASM_DCE_EXPECTED_OUTPUT_SIZE: wasm 13_769
// WASM_DCE_EXPECTED_OUTPUT_SIZE: mjs 5_277
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 3_900
// WASM_OPT_EXPECTED_OUTPUT_SIZE: 4_153
interface I {
fun foo() = "OK"
@@ -0,0 +1,42 @@
// TARGET_BACKEND: WASM
fun throwSomeJsException(): Int = js("{ throw 42; }")
fun withFinally(): Boolean {
try {
throwSomeJsException()
return false
} finally {
return true
}
return false
}
fun withThrowable(): Boolean {
try {
throwSomeJsException()
return false
} catch (_: Throwable) {
return true
}
return false
}
fun withJsException(): Boolean {
try {
throwSomeJsException()
return false
} catch (_: JsException) {
return true
}
return false
}
fun box(): String {
if (!withFinally()) return "FAIL1"
if (!withThrowable()) return "FAIL2"
if (!withJsException()) return "FAIL3"
return "OK"
}
@@ -19,3 +19,7 @@ private fun throwJsError(message: String?, wasmTypeName: String?, stack: Externa
internal fun throwAsJsException(t: Throwable): Nothing {
throwJsError(t.message, getSimpleName(t.typeInfo), t.jsStack)
}
internal fun throwJsException(): Nothing {
throw JsException()
}
@@ -0,0 +1,13 @@
/*
* Copyright 2010-2024 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.js
/**
* Exception thrown by the JavaScript code.
* All exceptions thrown by JS code are signalled to Wasm code as `JsException`.
* One can catch such exception in Wasm, but no details of the original exception can be retrieved from it.
* */
public class JsException : Throwable(message = "Exception was thrown while running JavaScript code")
@@ -139,6 +139,10 @@ abstract class WasmExpressionBuilder {
buildInstrWithNoLocation(WasmOp.CATCH, WasmImmediate.TagIdx(tagIdx))
}
fun buildCatchAll() {
buildInstrWithNoLocation(WasmOp.CATCH_ALL)
}
fun buildBrIf(absoluteBlockLevel: Int, location: SourceLocation) {
buildBrInstr(WasmOp.BR_IF, absoluteBlockLevel, location)
}
@@ -117,13 +117,13 @@ class WasmIrToText(
return
}
if (op == WasmOp.END || op == WasmOp.ELSE || op == WasmOp.CATCH)
if (op == WasmOp.END || op == WasmOp.ELSE || op == WasmOp.CATCH || op == WasmOp.CATCH_ALL)
indent--
newLine()
stringBuilder.append(wasmInstr.operator.mnemonic)
if (op == WasmOp.BLOCK || op == WasmOp.LOOP || op == WasmOp.IF || op == WasmOp.ELSE || op == WasmOp.CATCH || op == WasmOp.TRY)
if (op == WasmOp.BLOCK || op == WasmOp.LOOP || op == WasmOp.IF || op == WasmOp.ELSE || op == WasmOp.CATCH || op == WasmOp.CATCH_ALL || op == WasmOp.TRY)
indent++
if (wasmInstr.operator in setOf(WasmOp.CALL_INDIRECT, WasmOp.TABLE_INIT)) {
@@ -84,6 +84,12 @@ public class FirWasmJsCodegenInteropTestGenerated extends AbstractFirWasmJsCodeg
runTest("compiler/testData/codegen/boxWasmJsInterop/jsCode.kt");
}
@Test
@TestMetadata("jsException.kt")
public void testJsException() {
runTest("compiler/testData/codegen/boxWasmJsInterop/jsException.kt");
}
@Test
@TestMetadata("jsExport.kt")
public void testJsExport() {
@@ -84,6 +84,12 @@ public class K1WasmCodegenWasmJsInteropTestGenerated extends AbstractK1WasmCodeg
runTest("compiler/testData/codegen/boxWasmJsInterop/jsCode.kt");
}
@Test
@TestMetadata("jsException.kt")
public void testJsException() {
runTest("compiler/testData/codegen/boxWasmJsInterop/jsException.kt");
}
@Test
@TestMetadata("jsExport.kt")
public void testJsExport() {