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 7405dd97669..55ad03f73f3 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 @@ -432,11 +432,18 @@ private val excludeDeclarationsFromCodegenPhase = makeCustomPhase, 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) diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt index b402577517f..5da831510f1 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/BodyGenerator.kt @@ -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()) diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsExceptionRevealLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsExceptionRevealLowering.kt new file mode 100644 index 00000000000..d6df662970d --- /dev/null +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsExceptionRevealLowering.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/compiler/testData/codegen/box/size/add.kt b/compiler/testData/codegen/box/size/add.kt index 261136e0efc..5f683c18181 100644 --- a/compiler/testData/codegen/box/size/add.kt +++ b/compiler/testData/codegen/box/size/add.kt @@ -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 diff --git a/compiler/testData/codegen/box/size/helloWorld.kt b/compiler/testData/codegen/box/size/helloWorld.kt index 99ed027c4da..f65742e5b93 100644 --- a/compiler/testData/codegen/box/size/helloWorld.kt +++ b/compiler/testData/codegen/box/size/helloWorld.kt @@ -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!") diff --git a/compiler/testData/codegen/box/size/helloWorldDOM.kt b/compiler/testData/codegen/box/size/helloWorldDOM.kt index ea121a628e0..7e356380a31 100644 --- a/compiler/testData/codegen/box/size/helloWorldDOM.kt +++ b/compiler/testData/codegen/box/size/helloWorldDOM.kt @@ -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 diff --git a/compiler/testData/codegen/box/size/objectsOptimization.kt b/compiler/testData/codegen/box/size/objectsOptimization.kt index 3ca1c60b0ad..40c889a1a9f 100644 --- a/compiler/testData/codegen/box/size/objectsOptimization.kt +++ b/compiler/testData/codegen/box/size/objectsOptimization.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 diff --git a/compiler/testData/codegen/box/size/ok.kt b/compiler/testData/codegen/box/size/ok.kt index 649d9093f7a..606060254a1 100644 --- a/compiler/testData/codegen/box/size/ok.kt +++ b/compiler/testData/codegen/box/size/ok.kt @@ -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" \ No newline at end of file diff --git a/compiler/testData/codegen/box/size/removeUnusedOverride.kt b/compiler/testData/codegen/box/size/removeUnusedOverride.kt index 63f1390002b..3a5346678cb 100644 --- a/compiler/testData/codegen/box/size/removeUnusedOverride.kt +++ b/compiler/testData/codegen/box/size/removeUnusedOverride.kt @@ -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" diff --git a/compiler/testData/codegen/boxWasmJsInterop/jsException.kt b/compiler/testData/codegen/boxWasmJsInterop/jsException.kt new file mode 100644 index 00000000000..747eb623c34 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/jsException.kt @@ -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" +} diff --git a/libraries/stdlib/wasm/js/internal/ExceptionHelpers.kt b/libraries/stdlib/wasm/js/internal/ExceptionHelpers.kt index 241d9a27639..6944a6e1e15 100644 --- a/libraries/stdlib/wasm/js/internal/ExceptionHelpers.kt +++ b/libraries/stdlib/wasm/js/internal/ExceptionHelpers.kt @@ -18,4 +18,8 @@ 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() } \ No newline at end of file diff --git a/libraries/stdlib/wasm/js/src/kotlin/js/JsException.kt b/libraries/stdlib/wasm/js/src/kotlin/js/JsException.kt new file mode 100644 index 00000000000..10248986bed --- /dev/null +++ b/libraries/stdlib/wasm/js/src/kotlin/js/JsException.kt @@ -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") diff --git a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/WasmExpressionBuilder.kt b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/WasmExpressionBuilder.kt index 08399f430a0..997b08c16c7 100644 --- a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/WasmExpressionBuilder.kt +++ b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/WasmExpressionBuilder.kt @@ -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) } diff --git a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToText.kt b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToText.kt index 2d0707f1887..d1fa85d3478 100644 --- a/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToText.kt +++ b/wasm/wasm.ir/src/org/jetbrains/kotlin/wasm/ir/convertors/WasmIrToText.kt @@ -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)) { diff --git a/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/FirWasmJsCodegenInteropTestGenerated.java b/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/FirWasmJsCodegenInteropTestGenerated.java index e2c370fd1ef..93678477c11 100644 --- a/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/FirWasmJsCodegenInteropTestGenerated.java +++ b/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/FirWasmJsCodegenInteropTestGenerated.java @@ -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() { diff --git a/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/K1WasmCodegenWasmJsInteropTestGenerated.java b/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/K1WasmCodegenWasmJsInteropTestGenerated.java index 89908830fc0..7a92a46e64b 100644 --- a/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/K1WasmCodegenWasmJsInteropTestGenerated.java +++ b/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/K1WasmCodegenWasmJsInteropTestGenerated.java @@ -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() {