From b5eafb9eb76311b9507786242a3f2b3b6021f0d0 Mon Sep 17 00:00:00 2001 From: Igor Yakovlev Date: Wed, 5 Jul 2023 12:55:10 +0200 Subject: [PATCH] [Wasm] WasmExport implementation --- .../jetbrains/kotlin/backend/wasm/dce/Dce.kt | 4 +- .../dce/WasmUsefulDeclarationProcessor.kt | 2 +- .../wasm/ir2wasm/DeclarationGenerator.kt | 13 ++++-- .../wasm/lower/JsInteropFunctionsLowering.kt | 4 +- .../kotlin/backend/wasm/utils/Annotations.kt | 7 +++ .../codegen/boxWasmJsInterop/wasmExport.kt | 19 ++++++++ .../wasmTests/wasmInterop/wasmExport.kt | 23 ++++++++++ .../wasm/src/kotlin/wasm/Annotations.kt | 16 ++++++- .../wasm/resolve/WasmPlatformConfigurator.kt | 1 + .../diagnostics/DefaultErrorMessagesWasm.kt | 4 ++ .../wasm/resolve/diagnostics/ErrorsWasm.java | 4 ++ .../diagnostics/WasmInteropTypesChecker.kt | 44 +++++++++++++++++++ ...WasmCodegenWasmJsInteropTestGenerated.java | 6 +++ ...WasmCodegenWasmJsInteropTestGenerated.java | 6 +++ .../DiagnosticsWasmTestGenerated.java | 6 +++ 15 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 compiler/testData/codegen/boxWasmJsInterop/wasmExport.kt create mode 100644 compiler/testData/diagnostics/wasmTests/wasmInterop/wasmExport.kt create mode 100644 wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmInteropTypesChecker.kt diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/Dce.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/Dce.kt index 272c6e76847..6f822d3c78c 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/Dce.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/Dce.kt @@ -6,6 +6,8 @@ package org.jetbrains.kotlin.backend.wasm.dce import org.jetbrains.kotlin.backend.wasm.WasmBackendContext +import org.jetbrains.kotlin.backend.wasm.ir2wasm.isExported +import org.jetbrains.kotlin.backend.wasm.utils.getWasmExportNameIfWasmExport import org.jetbrains.kotlin.ir.IrElement import org.jetbrains.kotlin.ir.backend.js.dce.DceDumpNameCache import org.jetbrains.kotlin.ir.backend.js.utils.* @@ -51,7 +53,7 @@ private fun buildRoots(modules: List, context: WasmBackendCont modules.onAllFiles { declarations.forEach { declaration -> - if (declaration.isJsExport()) { + if (declaration is IrFunction && declaration.isExported()) { declaration.acceptVoid(declarationsCollector) } } diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/WasmUsefulDeclarationProcessor.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/WasmUsefulDeclarationProcessor.kt index 8aebe4690cf..43dadecf809 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/WasmUsefulDeclarationProcessor.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/dce/WasmUsefulDeclarationProcessor.kt @@ -207,5 +207,5 @@ internal class WasmUsefulDeclarationProcessor( } } - override fun isExported(declaration: IrDeclaration): Boolean = declaration.isJsExport() + override fun isExported(declaration: IrDeclaration): Boolean = (declaration is IrFunction && declaration.isExported()) } diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt index cb90ea12fbc..2d119f5060f 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/ir2wasm/DeclarationGenerator.kt @@ -23,6 +23,7 @@ import org.jetbrains.kotlin.ir.types.getClass import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid import org.jetbrains.kotlin.ir.visitors.acceptVoid +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.parentOrNull import org.jetbrains.kotlin.wasm.ir.* import org.jetbrains.kotlin.wasm.ir.source.location.SourceLocation @@ -182,11 +183,16 @@ class DeclarationGenerator( if (initPriority != null) context.registerInitFunction(function, initPriority) - if (declaration.isExported()) { + val nameIfExported = when { + declaration.isJsExport() -> declaration.getJsNameOrKotlinName().identifier + else -> declaration.getWasmExportNameIfWasmExport() + } + + if (nameIfExported != null) { context.addExport( WasmExport.Function( field = function, - name = declaration.getJsNameOrKotlinName().identifier + name = nameIfExported ) ) } @@ -500,8 +506,7 @@ fun IrFunction.getEffectiveValueParameters(): List { } fun IrFunction.isExported(): Boolean = - isJsExport() - + isJsExport() || getWasmExportNameIfWasmExport() != null fun generateConstExpression( expression: IrConst<*>, diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt index c0fa36c020c..92c7462f83a 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/JsInteropFunctionsLowering.kt @@ -12,13 +12,13 @@ import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder import org.jetbrains.kotlin.backend.common.lower.createIrBuilder import org.jetbrains.kotlin.backend.wasm.WasmBackendContext import org.jetbrains.kotlin.backend.wasm.ir2wasm.isBuiltInWasmRefType -import org.jetbrains.kotlin.backend.wasm.ir2wasm.isExported import org.jetbrains.kotlin.backend.wasm.ir2wasm.isExternalType import org.jetbrains.kotlin.backend.wasm.ir2wasm.toJsStringLiteral import org.jetbrains.kotlin.backend.wasm.utils.getJsFunAnnotation import org.jetbrains.kotlin.backend.wasm.utils.getWasmImportDescriptor import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName +import org.jetbrains.kotlin.ir.backend.js.utils.isJsExport import org.jetbrains.kotlin.ir.builders.* import org.jetbrains.kotlin.ir.builders.declarations.* import org.jetbrains.kotlin.ir.declarations.* @@ -49,7 +49,7 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT override fun transformFlat(declaration: IrDeclaration): List? { if (declaration.isFakeOverride) return null if (declaration !is IrSimpleFunction) return null - val isExported = declaration.isExported() + val isExported = declaration.isJsExport() val isExternal = declaration.isExternal || declaration.getJsFunAnnotation() != null if (declaration.isPropertyAccessor) return null if (declaration.parent !is IrPackageFragment) return null diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/utils/Annotations.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/utils/Annotations.kt index ea4938ff20a..ee91d5f5a33 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/utils/Annotations.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/utils/Annotations.kt @@ -61,3 +61,10 @@ fun IrAnnotationContainer.getJsFunAnnotation(): String? = fun IrAnnotationContainer.getJsPrimitiveType(): String? = getAnnotation(FqName("kotlin.wasm.internal.JsPrimitive"))?.getSingleConstStringArgument() + +@Suppress("UNCHECKED_CAST") +fun IrFunction.getWasmExportNameIfWasmExport(): String? { + val annotation = getAnnotation(FqName("kotlin.wasm.WasmExport")) ?: return null + if (annotation.valueArgumentsCount == 0) return name.identifier + return (annotation.getValueArgument(0) as? IrConst)?.value ?: name.identifier +} \ No newline at end of file diff --git a/compiler/testData/codegen/boxWasmJsInterop/wasmExport.kt b/compiler/testData/codegen/boxWasmJsInterop/wasmExport.kt new file mode 100644 index 00000000000..ed42c007418 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/wasmExport.kt @@ -0,0 +1,19 @@ +// TARGET_BACKEND: WASM + +import kotlin.wasm.WasmExport + +@WasmExport +fun exportDefaultName(): String = "some string" + +fun checkDefaultName(): Int = js("typeof wasmExports.exportDefaultName() === 'object'") + +@WasmExport("exportOverriddenName") +fun exportWithName(): String = "some string" + +fun checkOverriddenName(): Int = js("typeof wasmExports.exportOverriddenName() === 'object'") + +fun box(): String { + if (checkDefaultName() != 1) return "checkDefaultName fail" + if (checkOverriddenName() != 1) return "checkOverriddenName fail" + return "OK" +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/wasmTests/wasmInterop/wasmExport.kt b/compiler/testData/diagnostics/wasmTests/wasmInterop/wasmExport.kt new file mode 100644 index 00000000000..f1ae75722cb --- /dev/null +++ b/compiler/testData/diagnostics/wasmTests/wasmInterop/wasmExport.kt @@ -0,0 +1,23 @@ +import kotlin.wasm.WasmExport + +@WasmExport("a") +external fun foo0(): Unit + +@WasmExport("a") +fun foo1(): Int = js("42") + +class C() { + @WasmExport("a") + fun foo2(): Int = 42 +} + +@WasmExport("a") +fun foo3(): Int = 42 + +@WasmExport() +fun foo4(): Int = 42 + +@OptIn(kotlin.js.ExperimentalJsExport::class) +@JsExport() +@WasmExport() +fun foo6(): Int = 42 diff --git a/libraries/stdlib/wasm/src/kotlin/wasm/Annotations.kt b/libraries/stdlib/wasm/src/kotlin/wasm/Annotations.kt index aa1ef991c40..71aa306a6f0 100644 --- a/libraries/stdlib/wasm/src/kotlin/wasm/Annotations.kt +++ b/libraries/stdlib/wasm/src/kotlin/wasm/Annotations.kt @@ -11,11 +11,23 @@ package kotlin.wasm * * Can only be used on top-level external functions. * - * In JavaScript environment, - * function will be imported from ES module without type adapters. + * The annotated function will be imported into Wasm module without type adapters. */ @Target(AnnotationTarget.FUNCTION) public annotation class WasmImport( val module: String, val name: String = "" +) + +/** + * Exports a function with the given optional [name]. + * The declaration name will be used if the [name] argument is not provided. + * + * Can only be used on top-level non-external functions. + * + * The annotated function will be exported from Wasm module without type adapters. + */ +@Target(AnnotationTarget.FUNCTION) +public annotation class WasmExport( + val name: String = "" ) \ No newline at end of file diff --git a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/WasmPlatformConfigurator.kt b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/WasmPlatformConfigurator.kt index 39e38f3d4e4..08ff838b3e4 100644 --- a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/WasmPlatformConfigurator.kt +++ b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/WasmPlatformConfigurator.kt @@ -32,6 +32,7 @@ object WasmPlatformConfigurator : PlatformConfiguratorBase( WasmImportAnnotationChecker, WasmJsFunAnnotationChecker, WasmJsInteropTypesChecker, + WasmInteropTypesChecker, ), additionalCallCheckers = listOf( JsModuleCallChecker, diff --git a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/DefaultErrorMessagesWasm.kt b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/DefaultErrorMessagesWasm.kt index 79a25d2b9e2..3475302332d 100644 --- a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/DefaultErrorMessagesWasm.kt +++ b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/DefaultErrorMessagesWasm.kt @@ -25,6 +25,10 @@ private val DIAGNOSTIC_FACTORY_TO_RENDERER by lazy { Renderers.RENDER_TYPE ) + put(ErrorsWasm.NESTED_WASM_EXPORT, "Only top-level functions can be exported with @WasmExport") + put(ErrorsWasm.WASM_EXPORT_ON_EXTERNAL_DECLARATION, "Functions annotated with @WasmExport must not be external") + put(ErrorsWasm.JS_AND_WASM_EXPORTS_ON_SAME_DECLARATION, "Cannot use @WasmExport and @JsExport for same function") + put(ErrorsWasm.NESTED_WASM_IMPORT, "Only top-level functions can be imported with @WasmImport") put(ErrorsWasm.WASM_IMPORT_ON_NON_EXTERNAL_DECLARATION, "Functions annotated with @WasmImport must be external") put(ErrorsWasm.WASM_IMPORT_PARAMETER_DEFAULT_VALUE, "Default parameter values are not supported with @WasmImport") diff --git a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/ErrorsWasm.java b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/ErrorsWasm.java index 01153e41631..60a4746518f 100644 --- a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/ErrorsWasm.java +++ b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/ErrorsWasm.java @@ -19,6 +19,10 @@ public interface ErrorsWasm { DiagnosticFactory2 WRONG_JS_INTEROP_TYPE = DiagnosticFactory2.create(ERROR, PositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT); + DiagnosticFactory0 NESTED_WASM_EXPORT = DiagnosticFactory0.create(ERROR); + DiagnosticFactory0 WASM_EXPORT_ON_EXTERNAL_DECLARATION = DiagnosticFactory0.create(ERROR); + DiagnosticFactory0 JS_AND_WASM_EXPORTS_ON_SAME_DECLARATION = DiagnosticFactory0.create(ERROR); + DiagnosticFactory0 NESTED_WASM_IMPORT = DiagnosticFactory0.create(ERROR); DiagnosticFactory0 WASM_IMPORT_ON_NON_EXTERNAL_DECLARATION = DiagnosticFactory0.create(ERROR); DiagnosticFactory0 WASM_IMPORT_PARAMETER_DEFAULT_VALUE = DiagnosticFactory0.create(ERROR); diff --git a/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmInteropTypesChecker.kt b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmInteropTypesChecker.kt new file mode 100644 index 00000000000..db78b450d6b --- /dev/null +++ b/wasm/wasm.frontend/src/org/jetbrains/kotlin/wasm/resolve/diagnostics/WasmInteropTypesChecker.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2023 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.wasm.resolve.diagnostics + +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker +import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext +import org.jetbrains.kotlin.resolve.descriptorUtil.isEffectivelyExternal +import org.jetbrains.kotlin.wasm.util.hasValidJsCodeBody + +// TODO: Implement in K2: KT-56849 +object WasmInteropTypesChecker : DeclarationChecker { + override fun check(declaration: KtDeclaration, descriptor: DeclarationDescriptor, context: DeclarationCheckerContext) { + if (descriptor !is FunctionDescriptor) return + + if (!descriptor.annotations.hasAnnotation(FqName("kotlin.wasm.WasmExport"))) return + + val trace = context.trace + val bindingContext = trace.bindingContext + + if (descriptor.annotations.hasAnnotation(FqName("kotlin.js.JsExport"))) { + val reportOn = descriptor.findPsi() ?: declaration + trace.report(ErrorsWasm.JS_AND_WASM_EXPORTS_ON_SAME_DECLARATION.on(reportOn)) + } + + if (descriptor.isEffectivelyExternal() || descriptor.hasValidJsCodeBody(bindingContext)) { + val reportOn = descriptor.findPsi() ?: declaration + trace.report(ErrorsWasm.WASM_EXPORT_ON_EXTERNAL_DECLARATION.on(reportOn)) + } + + if (!DescriptorUtils.isTopLevelDeclaration(descriptor)) { + val reportOn = descriptor.findPsi() ?: declaration + trace.report(ErrorsWasm.NESTED_WASM_EXPORT.on(reportOn)) + } + } +} \ No newline at end of file diff --git a/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/FirWasmCodegenWasmJsInteropTestGenerated.java b/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/FirWasmCodegenWasmJsInteropTestGenerated.java index 6138fcdd515..191d375f50d 100644 --- a/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/FirWasmCodegenWasmJsInteropTestGenerated.java +++ b/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/FirWasmCodegenWasmJsInteropTestGenerated.java @@ -151,6 +151,12 @@ public class FirWasmCodegenWasmJsInteropTestGenerated extends AbstractFirWasmCod runTest("compiler/testData/codegen/boxWasmJsInterop/vararg.kt"); } + @Test + @TestMetadata("wasmExport.kt") + public void testWasmExport() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/wasmExport.kt"); + } + @Test @TestMetadata("wasmImport.kt") public void testWasmImport() throws Exception { 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 5d2fdf0423a..7a9de889d73 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 @@ -151,6 +151,12 @@ public class K1WasmCodegenWasmJsInteropTestGenerated extends AbstractK1WasmCodeg runTest("compiler/testData/codegen/boxWasmJsInterop/vararg.kt"); } + @Test + @TestMetadata("wasmExport.kt") + public void testWasmExport() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/wasmExport.kt"); + } + @Test @TestMetadata("wasmImport.kt") public void testWasmImport() throws Exception { diff --git a/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/diagnostics/DiagnosticsWasmTestGenerated.java b/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/diagnostics/DiagnosticsWasmTestGenerated.java index 9952677de28..a86cd006d60 100644 --- a/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/diagnostics/DiagnosticsWasmTestGenerated.java +++ b/wasm/wasm.tests/tests-gen/org/jetbrains/kotlin/wasm/test/diagnostics/DiagnosticsWasmTestGenerated.java @@ -85,6 +85,12 @@ public class DiagnosticsWasmTestGenerated extends AbstractDiagnosticsWasmTest { KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/diagnostics/wasmTests/wasmInterop"), Pattern.compile("^(.+)\\.kt$"), null, true); } + @Test + @TestMetadata("wasmExport.kt") + public void testWasmExport() throws Exception { + runTest("compiler/testData/diagnostics/wasmTests/wasmInterop/wasmExport.kt"); + } + @Test @TestMetadata("wasmImport.kt") public void testWasmImport() throws Exception {