[WasmJs] Support catching JS exceptions
Fixed #KT-65660
This commit is contained in:
committed by
Space Team
parent
c6aac835e5
commit
8fe5cf2641
+10
-2
@@ -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)
|
||||
|
||||
+29
-10
@@ -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())
|
||||
|
||||
+82
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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,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)) {
|
||||
|
||||
Generated
+6
@@ -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() {
|
||||
|
||||
+6
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user