From a55c65e3e2f931f0e646672943d94ba4119a740b Mon Sep 17 00:00:00 2001 From: Artem Kobzar Date: Tue, 16 Jan 2024 11:10:27 +0000 Subject: [PATCH] [K/Wasm] Add simple TypeScript definitions generating ^KT-65009 Fixed --- .../jetbrains/kotlin/cli/js/K2JsIrCompiler.kt | 11 +- .../ir/backend/js/export/ExportModel.kt | 10 +- .../backend/js/export/ExportModelGenerator.kt | 31 +- .../js/export/ExportModelToJsStatements.kt | 6 +- .../js/export/ExportModelToTsDeclarations.kt | 5 +- .../kotlin/backend/wasm/WasmSymbols.kt | 9 +- .../jetbrains/kotlin/backend/wasm/compiler.kt | 34 +- .../wasm/export/ExportModelGenerator.kt | 318 ++++++++++++++++++ .../wasm/lower/JsInteropFunctionsLowering.kt | 3 + .../wasm/lower/WasmBridgesConstruction.kt | 7 + .../externalDeclarations.d.ts | 29 ++ .../externalDeclarations.kt | 38 +++ .../typeScriptDeclarations/generics.d.ts | 20 ++ .../typeScriptDeclarations/generics.kt | 39 +++ .../typeScriptDeclarations/jsPrimitives.d.ts | 11 + .../typeScriptDeclarations/jsPrimitives.kt | 51 +++ .../nullableJsPrimitives.d.ts | 11 + .../nullableJsPrimitives.kt | 51 +++ .../nullablePrimitives.d.ts | 17 + .../nullablePrimitives.kt | 76 +++++ .../nullableUnisnged.d.ts | 11 + .../nullableUnisnged.kt | 52 +++ .../typeScriptDeclarations/primitives.d.ts | 19 ++ .../typeScriptDeclarations/primitives.kt | 89 +++++ .../typeScriptDeclarations/unisnged.d.ts | 11 + .../typeScriptDeclarations/unisnged.kt | 52 +++ .../WasmEnvironmentConfigurationDirectives.kt | 4 + ...irJsCodegenWasmJsInteropTestGenerated.java | 10 + ...sES6CodegenWasmJsInteropTestGenerated.java | 10 + ...IrCodegenWasmJsInteropJsTestGenerated.java | 10 + .../AbstractWasmBlackBoxCodegenTestBase.kt | 2 + .../wasm/test/converters/WasmBackendFacade.kt | 13 +- .../wasm/test/handlers/WasmDtsHandler.kt | 32 ++ ...WasmCodegenWasmJsInteropTestGenerated.java | 58 ++++ ...WasmCodegenWasmJsInteropTestGenerated.java | 58 ++++ 35 files changed, 1176 insertions(+), 32 deletions(-) create mode 100644 compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/export/ExportModelGenerator.kt create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/externalDeclarations.d.ts create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/externalDeclarations.kt create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/generics.d.ts create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/generics.kt create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/jsPrimitives.d.ts create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/jsPrimitives.kt create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableJsPrimitives.d.ts create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableJsPrimitives.kt create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullablePrimitives.d.ts create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullablePrimitives.kt create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableUnisnged.d.ts create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableUnisnged.kt create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/primitives.d.ts create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/primitives.kt create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/unisnged.d.ts create mode 100644 compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/unisnged.kt create mode 100644 wasm/wasm.tests/test/org/jetbrains/kotlin/wasm/test/handlers/WasmDtsHandler.kt diff --git a/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt b/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt index 31bd01d5cb2..313f9a570cd 100644 --- a/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt +++ b/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt @@ -353,11 +353,15 @@ class K2JsIrCompiler : CLICompiler() { } if (arguments.wasm) { - val (allModules, backendContext) = compileToLoweredIr( + val generateDts = configuration.getBoolean(JSConfigurationKeys.GENERATE_DTS) + val generateSourceMaps = configuration.getBoolean(JSConfigurationKeys.SOURCE_MAP) + + val (allModules, backendContext, typeScriptFragment) = compileToLoweredIr( depsDescriptors = module, phaseConfig = createPhaseConfig(wasmPhases, arguments, messageCollector), irFactory = IrFactoryImpl, exportedDeclarations = setOf(FqName("main")), + generateTypeScriptFragment = generateDts, propertyLazyInitialization = arguments.irPropertyLazyInitialization, ) val dceDumpNameCache = DceDumpNameCache() @@ -367,16 +371,15 @@ class K2JsIrCompiler : CLICompiler() { dumpDeclarationIrSizesIfNeed(arguments.irDceDumpDeclarationIrSizesToFile, allModules, dceDumpNameCache) - val generateSourceMaps = configuration.getBoolean(JSConfigurationKeys.SOURCE_MAP) - val res = compileWasm( allModules = allModules, backendContext = backendContext, + typeScriptFragment = typeScriptFragment, baseFileName = outputName, emitNameSection = arguments.wasmDebug, allowIncompleteImplementations = arguments.irDce, generateWat = configuration.get(JSConfigurationKeys.WASM_GENERATE_WAT, false), - generateSourceMaps = generateSourceMaps + generateSourceMaps = generateSourceMaps, ) writeCompilationResult( diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt index 40badb49b56..52822067e32 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModel.kt @@ -27,6 +27,7 @@ data class ExportedModule( class ExportedNamespace( val name: String, val declarations: List, + val isPrivate: Boolean = false ) : ExportedDeclaration() data class ExportedFunction( @@ -100,7 +101,7 @@ data class ExportedObject( override val members: List, override val nestedClasses: List, override val ir: IrClass, - val irGetter: IrSimpleFunction + val irGetter: IrSimpleFunction? = null ) : ExportedClass() class ExportedParameter( @@ -113,6 +114,7 @@ sealed class ExportedType { sealed class Primitive(val typescript: kotlin.String) : ExportedType() { object Boolean : Primitive("boolean") object Number : Primitive("number") + object BigInt : Primitive("bigint") object ByteArray : Primitive("Int8Array") object ShortArray : Primitive("Int16Array") object IntArray : Primitive("Int32Array") @@ -121,11 +123,14 @@ sealed class ExportedType { object String : Primitive("string") object Throwable : Primitive("Error") object Any : Primitive("any") - object Unknown : Primitive("unknown") object Undefined : Primitive("undefined") object Unit : Primitive("void") object Nothing : Primitive("never") object UniqueSymbol : Primitive("unique symbol") + object Unknown : Primitive("unknown") { + override fun withNullability(nullable: kotlin.Boolean) = + if (nullable) this else NonNullable(this) + } } sealed class LiteralType(val value: T) : ExportedType() { @@ -142,6 +147,7 @@ sealed class ExportedType { class ClassType(val name: String, val arguments: List, val ir: IrClass) : ExportedType() class TypeParameter(val name: String, val constraint: ExportedType? = null) : ExportedType() class Nullable(val baseType: ExportedType) : ExportedType() + class NonNullable(val baseType: ExportedType) : ExportedType() class ErrorType(val comment: String) : ExportedType() class TypeOf(val name: String) : ExportedType() diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt index 737a200066e..602cdb48f1e 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelGenerator.kt @@ -524,7 +524,7 @@ class ExportModelGenerator(val context: JsIrBackendContext, val generateNamespac return ExportedType.ErrorType("UnknownType ${type.render()}") } - private fun exportTypeParameter(typeParameter: IrTypeParameter): ExportedType.TypeParameter { + fun exportTypeParameter(typeParameter: IrTypeParameter): ExportedType.TypeParameter { val constraint = typeParameter.superTypes.asSequence() .filter { it != context.irBuiltIns.anyNType } .map { @@ -550,12 +550,6 @@ class ExportModelGenerator(val context: JsIrBackendContext, val generateNamespac ) } - private fun ExportedDeclaration.withAttributesFor(declaration: IrDeclaration): ExportedDeclaration { - declaration.getDeprecated()?.let { attributes.add(ExportedAttribute.DeprecatedAttribute(it)) } - - return this - } - private val currentlyProcessedTypes = hashSetOf() private fun exportType(type: IrType, shouldCalculateExportedSupertypeForImplicit: Boolean = true): ExportedType { @@ -639,13 +633,6 @@ class ExportModelGenerator(val context: JsIrBackendContext, val generateNamespac .also { currentlyProcessedTypes.remove(type) } } - private fun IrDeclarationWithName.getExportedIdentifier(): String = - with(getJsNameOrKotlinName()) { - if (isSpecial) - error("Cannot export special name: ${name.asString()} for declaration $fqNameWhenAvailable") - else identifier - } - private fun functionExportability(function: IrSimpleFunction): Exportability { if (function.isInline && function.typeParameters.any { it.isReified }) return Exportability.Prohibited("Inline reified function") @@ -811,7 +798,7 @@ fun IrDeclaration.isExportedImplicitlyOrExplicitly(context: JsIrBackendContext): return shouldDeclarationBeExportedImplicitlyOrExplicitly(candidate, context) } -private fun DescriptorVisibility.toExportedVisibility() = +fun DescriptorVisibility.toExportedVisibility() = when (this) { DescriptorVisibilities.PROTECTED -> ExportedVisibility.PROTECTED else -> ExportedVisibility.DEFAULT @@ -870,3 +857,17 @@ val strictModeReservedWords = setOf( ) private val allReservedWords = reservedWords + strictModeReservedWords + +fun ExportedDeclaration.withAttributesFor(declaration: IrDeclaration): ExportedDeclaration { + declaration.getDeprecated()?.let { attributes.add(ExportedAttribute.DeprecatedAttribute(it)) } + + return this +} + +fun IrDeclarationWithName.getExportedIdentifier(): String = + with(getJsNameOrKotlinName()) { + if (isSpecial) + error("Cannot export special name: ${name.asString()} for declaration $fqNameWhenAvailable") + else identifier + } + diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt index 0890d9e21d4..b7badfa5cc6 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToJsStatements.kt @@ -18,6 +18,7 @@ import org.jetbrains.kotlin.ir.backend.js.utils.emptyScope import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.util.companionObject +import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable import org.jetbrains.kotlin.ir.util.isObject import org.jetbrains.kotlin.js.backend.ast.* import org.jetbrains.kotlin.utils.filterIsInstanceAnd @@ -140,7 +141,10 @@ class ExportModelToJsStatements( defineProperty( namespace, declaration.name, - staticContext.getNameForStaticDeclaration(declaration.irGetter).makeRef(), + staticContext.getNameForStaticDeclaration( + declaration.irGetter + ?: error("Expect to have an object getter in its export model, but ${declaration.ir.fqNameWhenAvailable ?: declaration.name} doesn't have it") + ).makeRef(), null, staticContext ).makeStmt() diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt index c85636dd468..6a9d0b165a9 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/export/ExportModelToTsDeclarations.kt @@ -8,7 +8,6 @@ package org.jetbrains.kotlin.ir.backend.js.export import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.ir.backend.js.JsLoweredDeclarationOrigin import org.jetbrains.kotlin.ir.backend.js.lower.isEs6PrimaryConstructorReplacement -import org.jetbrains.kotlin.ir.backend.js.lower.isSyntheticPrimaryConstructor import org.jetbrains.kotlin.ir.backend.js.utils.JsAnnotations import org.jetbrains.kotlin.ir.backend.js.utils.getFqNameWithJsNameWhenAvailable import org.jetbrains.kotlin.ir.backend.js.utils.getJsNameOrKotlinName @@ -25,6 +24,7 @@ import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull import org.jetbrains.kotlin.utils.addToStdlib.runIf import org.jetbrains.kotlin.utils.findIsInstanceAnd +private const val NonNullable = "NonNullable" private const val Nullable = "Nullable" private const val objects = "_objects_" private const val declare = "declare " @@ -127,7 +127,7 @@ class ExportModelToTsDeclarations { } private fun ExportedNamespace.generateTypeScriptString(indent: String, prefix: String): String { - return "${prefix}namespace $name {\n" + declarations.toTypeScript("$indent ") + "$indent}" + return "${prefix.takeIf { !isPrivate } ?: "declare "}namespace $name {\n" + declarations.toTypeScript("$indent ") + "$indent}" } private fun ExportedConstructor.generateTypeScriptString(indent: String): String { @@ -445,6 +445,7 @@ class ExportModelToTsDeclarations { is ExportedType.ErrorType -> if (isInCommentContext) comment else "any /*$comment*/" is ExportedType.Nullable -> "$Nullable<" + baseType.toTypeScript(indent, isInCommentContext) + ">" + is ExportedType.NonNullable -> "$NonNullable<" + baseType.toTypeScript(indent, isInCommentContext) + ">" is ExportedType.InlineInterfaceType -> { members.joinToString(prefix = "{\n", postfix = "$indent}", separator = "") { it.toTypeScript("$indent ") + "\n" } } 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 1944811eb30..c4a9989f09c 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 @@ -365,7 +365,13 @@ class WasmSymbols( val jsCode = getFunction("js", kotlinJsPackage) - val jsAnyType: IrType by lazy { getIrClass(FqName("kotlin.js.JsAny")).defaultType } + val jsReferenceClass by lazy { getIrClass(FqName("kotlin.js.JsReference")) } + + val jsAnyType: IrType by lazy { getIrType("kotlin.js.JsAny") } + val jsBooleanType: IrType by lazy { getIrType("kotlin.js.JsBoolean") } + val jsStringType: IrType by lazy { getIrType("kotlin.js.JsString") } + val jsNumberType: IrType by lazy { getIrType("kotlin.js.JsNumber") } + val jsBigIntType: IrType by lazy { getIrType("kotlin.js.JsBigInt") } val newJsArray = getInternalFunction("newJsArray") @@ -424,6 +430,7 @@ class WasmSymbols( private fun getEnumsFunction(name: String) = getFunction(name, enumsInternalPackage) private fun getIrClass(fqName: FqName): IrClassSymbol = symbolTable.descriptorExtension.referenceClass(getClass(fqName)) + private fun getIrType(fqName: String): IrType = getIrClass(FqName(fqName)).defaultType private fun getInternalClass(name: String): IrClassSymbol = getIrClass(FqName("kotlin.wasm.internal.$name")) fun getKFunctionType(type: IrType, list: List): IrType { return irBuiltIns.functionN(list.size).typeWith(list + type) diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt index d9fdef06025..62154a6583f 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/compiler.kt @@ -14,10 +14,14 @@ import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmModuleFragmentGenerator import org.jetbrains.kotlin.backend.wasm.ir2wasm.toJsStringLiteral import org.jetbrains.kotlin.backend.wasm.lower.markExportedDeclarations import org.jetbrains.kotlin.backend.wasm.utils.SourceMapGenerator +import org.jetbrains.kotlin.backend.wasm.export.ExportModelGenerator import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.ir.backend.js.MainModule import org.jetbrains.kotlin.ir.backend.js.ModulesStructure import org.jetbrains.kotlin.ir.backend.js.SourceMapsInfo +import org.jetbrains.kotlin.ir.backend.js.export.ExportModelToTsDeclarations +import org.jetbrains.kotlin.ir.backend.js.export.ExportedModule +import org.jetbrains.kotlin.ir.backend.js.export.TypeScriptFragment import org.jetbrains.kotlin.ir.backend.js.loadIr import org.jetbrains.kotlin.ir.declarations.IrFactory import org.jetbrains.kotlin.ir.declarations.IrModuleFragment @@ -28,6 +32,7 @@ import org.jetbrains.kotlin.js.config.WasmTarget import org.jetbrains.kotlin.js.sourceMap.SourceFilePathResolver import org.jetbrains.kotlin.js.sourceMap.SourceMap3Builder import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.serialization.js.ModuleKind import org.jetbrains.kotlin.utils.addToStdlib.runIf import org.jetbrains.kotlin.wasm.ir.convertors.WasmIrToBinary import org.jetbrains.kotlin.wasm.ir.convertors.WasmIrToText @@ -35,13 +40,15 @@ import org.jetbrains.kotlin.wasm.ir.source.location.SourceLocation import org.jetbrains.kotlin.wasm.ir.source.location.SourceLocationMapping import java.io.ByteArrayOutputStream import java.io.File +import kotlin.math.exp class WasmCompilerResult( val wat: String?, val jsUninstantiatedWrapper: String?, val jsWrapper: String, val wasm: ByteArray, - val debugInformation: DebugInformation? + val debugInformation: DebugInformation?, + val dts: String? ) class DebugInformation( @@ -49,13 +56,20 @@ class DebugInformation( val sourceMapForText: String?, ) +data class LoweredIrWithExtraArtifacts( + val loweredIr: List, + val backendContext: WasmBackendContext, + val typeScriptFragment: TypeScriptFragment? +) + fun compileToLoweredIr( depsDescriptors: ModulesStructure, phaseConfig: PhaseConfig, irFactory: IrFactory, exportedDeclarations: Set = emptySet(), + generateTypeScriptFragment: Boolean, propertyLazyInitialization: Boolean, -): Pair, WasmBackendContext> { +): LoweredIrWithExtraArtifacts { val mainModule = depsDescriptors.mainModule val configuration = depsDescriptors.compilerConfiguration val (moduleFragment, dependencyModules, irBuiltIns, symbolTable, irLinker) = loadIr( @@ -90,6 +104,13 @@ fun compileToLoweredIr( for (file in module.files) markExportedDeclarations(context, file, exportedDeclarations) + val typeScriptFragment = runIf(generateTypeScriptFragment) { + val exportModel = ExportModelGenerator(context).generateExport(allModules) + val exportModelToDtsTranslator = ExportModelToTsDeclarations() + val fragment = exportModelToDtsTranslator.generateTypeScriptFragment(ModuleKind.ES, exportModel.declarations) + TypeScriptFragment(exportModelToDtsTranslator.generateTypeScript("", ModuleKind.ES, listOf(fragment))) + } + val phaserState = PhaserState() loweringList.forEachIndexed { _, lowering -> allModules.forEach { module -> @@ -97,12 +118,13 @@ fun compileToLoweredIr( } } - return Pair(allModules, context) + return LoweredIrWithExtraArtifacts(allModules, context, typeScriptFragment) } fun compileWasm( allModules: List, backendContext: WasmBackendContext, + typeScriptFragment: TypeScriptFragment?, baseFileName: String, emitNameSection: Boolean = false, allowIncompleteImplementations: Boolean = false, @@ -160,6 +182,7 @@ fun compileWasm( jsWrapper = compiledWasmModule.generateAsyncWasiWrapper("./$baseFileName.wasm") } + return WasmCompilerResult( wat = wat, jsUninstantiatedWrapper = jsUninstantiatedWrapper, @@ -169,6 +192,7 @@ fun compileWasm( sourceMapGeneratorForBinary?.generate(), sourceMapGeneratorForText?.generate(), ), + dts = typeScriptFragment?.raw ) } @@ -355,4 +379,8 @@ fun writeCompilationResult( result.debugInformation?.sourceMapForText?.let { File(dir, "$fileNameBase.wat.map").writeText(it) } + + if (result.dts != null) { + File(dir, "$fileNameBase.d.ts").writeText(result.dts) + } } diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/export/ExportModelGenerator.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/export/ExportModelGenerator.kt new file mode 100644 index 00000000000..7fc5b68c229 --- /dev/null +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/export/ExportModelGenerator.kt @@ -0,0 +1,318 @@ +/* + * 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.export + +import org.jetbrains.kotlin.backend.wasm.WasmBackendContext +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.descriptors.ClassKind +import org.jetbrains.kotlin.descriptors.DescriptorVisibilities +import org.jetbrains.kotlin.descriptors.Modality +import org.jetbrains.kotlin.ir.backend.js.export.* +import org.jetbrains.kotlin.ir.backend.js.utils.getFqNameWithJsNameWhenAvailable +import org.jetbrains.kotlin.ir.backend.js.utils.isJsExport +import org.jetbrains.kotlin.ir.backend.js.utils.realOverrideTarget +import org.jetbrains.kotlin.ir.declarations.* +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol +import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.ir.util.* +import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid +import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid +import org.jetbrains.kotlin.ir.visitors.acceptVoid +import org.jetbrains.kotlin.serialization.js.ModuleKind +import org.jetbrains.kotlin.utils.addToStdlib.runIf +import org.jetbrains.kotlin.utils.memoryOptimizedFilter +import org.jetbrains.kotlin.utils.memoryOptimizedMap +import org.jetbrains.kotlin.utils.memoryOptimizedMapNotNull + +private const val NOT_EXPORTED_NAMESPACE = "not.exported" + +class ExportModelGenerator(val context: WasmBackendContext) { + private val excludedFromExport = setOf( + context.wasmSymbols.jsRelatedSymbols.jsReferenceClass.owner, + context.wasmSymbols.jsRelatedSymbols.jsAnyType.classOrFail.owner, + context.wasmSymbols.jsRelatedSymbols.jsNumberType.classOrFail.owner, + context.wasmSymbols.jsRelatedSymbols.jsStringType.classOrFail.owner, + context.wasmSymbols.jsRelatedSymbols.jsBooleanType.classOrFail.owner, + context.wasmSymbols.jsRelatedSymbols.jsBigIntType.classOrFail.owner + ) + + private fun collectAllTheDeclarationsToExport(modules: Iterable): Iterable { + val declarationsToExport = mutableSetOf() + val queue = ArrayDeque().apply { + modules.asSequence() + .flatMap { it.files } + .flatMap { it.declarations } + .filter { it.isJsExport() } + .forEach { + declarationsToExport.add(it) + addLast(it) + } + } + val declarationVisitor = object : IrElementVisitorVoid { + override fun visitFunction(declaration: IrFunction) { + visitType(declaration.returnType) + declaration.typeParameters.forEach(::visitTypeParameter) + declaration.valueParameters.forEach(::visitValueParameter) + } + + override fun visitClass(declaration: IrClass) { + declaration.superTypes.forEach(::visitType) + declaration.acceptChildrenVoid(this) + } + + override fun visitField(declaration: IrField) { + visitType(declaration.type) + } + + override fun visitValueParameter(declaration: IrValueParameter) { + visitType(declaration.type) + } + + override fun visitTypeParameter(declaration: IrTypeParameter) { + declaration.superTypes.forEach(::visitType) + } + + private fun visitType(type: IrType) { + if (type !is IrSimpleType) return + val classifier = type.classifier as? IrClassSymbol ?: return + val klass = classifier.owner + if (!klass.isExternal || klass in excludedFromExport || klass in declarationsToExport) return + queue.add(klass) + declarationsToExport.add(klass) + type.arguments.forEach { it.typeOrNull?.let(::visitType) } + } + } + + while (queue.isNotEmpty()) { + val declaration = queue.removeFirst() + declaration.acceptVoid(declarationVisitor) + } + + return declarationsToExport + } + + fun generateExport(modules: Iterable): ExportedModule = + ExportedModule( + context.configuration[CommonConfigurationKeys.MODULE_NAME]!!, + ModuleKind.ES, + collectAllTheDeclarationsToExport(modules).mapNotNull(::exportDeclaration) + ) + + private fun exportDeclaration(declaration: IrDeclaration): ExportedDeclaration? { + return when (declaration) { + is IrSimpleFunction -> exportFunction(declaration) + is IrClass -> exportClass(declaration) + else -> error("Can't export declaration $declaration") + }?.withAttributesFor(declaration) + } + + private fun exportFunction(function: IrSimpleFunction): ExportedFunction? = + runIf(function.correspondingPropertySymbol == null && function.realOverrideTarget.parentClassOrNull?.symbol != context.irBuiltIns.anyClass) { + val parentClass = function.parentClassOrNull + ExportedFunction( + function.getExportedIdentifier(), + returnType = exportType(function.returnType), + typeParameters = function.typeParameters.memoryOptimizedMap(::exportTypeParameter), + ir = function, + isMember = parentClass != null, + isStatic = function.isStaticMethodOfClass, + isProtected = function.visibility == DescriptorVisibilities.PROTECTED, + isAbstract = parentClass != null && !parentClass.isInterface && function.modality == Modality.ABSTRACT, + parameters = (listOfNotNull(function.extensionReceiverParameter) + function.valueParameters) + .memoryOptimizedMap { exportParameter(it) }, + ) + } + + private fun exportConstructor(constructor: IrConstructor): ExportedDeclaration { + assert(constructor.isPrimary) { "Can't export not-primary constructor" } + val allValueParameters = listOfNotNull(constructor.extensionReceiverParameter) + constructor.valueParameters + return ExportedConstructor( + parameters = allValueParameters.memoryOptimizedMap { exportParameter(it) }, + visibility = constructor.visibility.toExportedVisibility() + ) + } + + private fun exportProperty( + property: IrProperty, + specializeType: ExportedType? = null + ): ExportedDeclaration { + val parentClass = property.parent as? IrClass + val isOptional = parentClass != null && + property.getter?.returnType?.isNullable() == true + + return ExportedProperty( + name = property.getExportedIdentifier(), + type = specializeType ?: exportType(property.getter!!.returnType), + mutable = property.isVar, + isMember = parentClass != null, + isAbstract = parentClass?.isInterface == false && property.modality == Modality.ABSTRACT, + isProtected = property.visibility == DescriptorVisibilities.PROTECTED, + isField = parentClass?.isInterface == true, + irGetter = property.getter, + irSetter = property.setter, + isOptional = isOptional, + isStatic = (property.getter ?: property.setter)?.isStaticMethodOfClass == true, + ) + } + + private fun exportParameter(parameter: IrValueParameter): ExportedParameter = + ExportedParameter( + parameter.name.asString(), + exportType(parameter.type), + parameter.defaultValue != null + ) + + private val currentlyProcessedTypes = hashSetOf() + + private fun exportType(type: IrType): ExportedType { + if (type in currentlyProcessedTypes) + return ExportedType.Primitive.Unknown + + if (type !is IrSimpleType) + return ExportedType.ErrorType("NonSimpleType ${type.render()}") + + currentlyProcessedTypes.add(type) + + val classifier = type.classifier + val isMarkedNullable = type.isMarkedNullable() + val nonNullType = type.makeNotNull() as IrSimpleType + val jsRelatedSymbols = context.wasmSymbols.jsRelatedSymbols + + val exportedType = when { + nonNullType.isBoolean() || nonNullType == jsRelatedSymbols.jsBooleanType -> ExportedType.Primitive.Boolean + nonNullType.isLong() || nonNullType.isULong() || nonNullType == jsRelatedSymbols.jsBigIntType -> ExportedType.Primitive.BigInt + nonNullType.isPrimitiveType() || nonNullType.isUByte() || nonNullType.isUShort() || nonNullType.isUInt() || nonNullType == jsRelatedSymbols.jsNumberType -> + ExportedType.Primitive.Number + nonNullType.isString() || nonNullType == jsRelatedSymbols.jsStringType -> ExportedType.Primitive.String + nonNullType == jsRelatedSymbols.jsAnyType -> ExportedType.Primitive.Unknown + nonNullType.isUnit() || nonNullType == context.wasmSymbols.voidType -> ExportedType.Primitive.Unit + nonNullType.isFunction() -> ExportedType.Function( + parameterTypes = nonNullType.arguments.dropLast(1).memoryOptimizedMap { exportTypeArgument(it) }, + returnType = exportTypeArgument(nonNullType.arguments.last()) + ) + + classifier is IrTypeParameterSymbol -> ExportedType.TypeParameter(classifier.owner.name.identifier) + + classifier is IrClassSymbol -> { + val klass = classifier.owner + if (klass.symbol == jsRelatedSymbols.jsReferenceClass) return ExportedType.Primitive.Unknown + + require(klass.isExternal) { "Unexpected non-external class: ${klass.fqNameWhenAvailable}" } + + val name = "$NOT_EXPORTED_NAMESPACE.${klass.getFqNameWithJsNameWhenAvailable(shouldIncludePackage = true).asString()}" + + when (klass.kind) { + ClassKind.OBJECT -> + ExportedType.TypeOf(name) + + ClassKind.CLASS, + ClassKind.INTERFACE -> + ExportedType.ClassType( + name, + type.arguments.memoryOptimizedMap { exportTypeArgument(it) }, + klass + ) + else -> error("Unexpected class kind ${klass.kind}") + } + } + + else -> error("Unexpected classifier $classifier") + } + + return exportedType.withNullability(isMarkedNullable) + .also { currentlyProcessedTypes.remove(type) } + } + + private fun exportTypeArgument(type: IrTypeArgument): ExportedType { + if (type is IrTypeProjection) + return exportType(type.type) + + if (type is IrType) + return exportType(type) + + return ExportedType.ErrorType("UnknownType ${type.render()}") + } + + private fun exportTypeParameter(typeParameter: IrTypeParameter): ExportedType.TypeParameter { + val constraint = typeParameter.superTypes.asSequence() + .filter { !it.isNullable() || it.makeNotNull() != context.wasmSymbols.jsRelatedSymbols.jsAnyType } + .map { exportType(it) } + .filter { it !is ExportedType.ErrorType } + .toList() + + return ExportedType.TypeParameter( + typeParameter.name.identifier, + constraint.run { + when (size) { + 0 -> null + 1 -> single() + else -> reduce(ExportedType::IntersectionType) + } + } + ) + } + + private fun exportMemberDeclaration(declaration: IrDeclaration): ExportedDeclaration? { + if (declaration !is IrDeclarationWithVisibility || declaration.visibility == DescriptorVisibilities.PRIVATE) return null + return when (declaration) { + is IrSimpleFunction -> exportFunction(declaration) + is IrConstructor -> exportConstructor(declaration) + is IrProperty -> exportProperty(declaration) + else -> null + }?.withAttributesFor(declaration) + } + + private fun exportClass(declaration: IrClass): ExportedDeclaration { + val typeParameters = declaration.typeParameters.memoryOptimizedMap(::exportTypeParameter) + + val superClass = declaration.superTypes + .find { it != context.irBuiltIns.anyType && !it.classifierOrFail.isInterface } + ?.let(::exportType) + ?.takeIf { it !is ExportedType.ErrorType } + + val superInterfaces = declaration.superTypes + .filter { it != context.wasmSymbols.jsRelatedSymbols.jsAnyType && it.classifierOrFail.isInterface } + .map(::exportType) + .memoryOptimizedFilter { it !is ExportedType.ErrorType } + + val name = declaration.getExportedIdentifier() + val members = declaration.declarations.memoryOptimizedMapNotNull(::exportMemberDeclaration) + + val exportedDeclaration = if (declaration.kind == ClassKind.OBJECT) { + ExportedObject( + ir = declaration, + name = name, + members = members, + superClasses = listOfNotNull(superClass), + nestedClasses = emptyList(), + superInterfaces = superInterfaces + ) + } else { + ExportedRegularClass( + name = name, + isInterface = declaration.isInterface, + isAbstract = declaration.modality == Modality.ABSTRACT || declaration.modality == Modality.SEALED, + superClasses = listOfNotNull(superClass), + superInterfaces = superInterfaces, + typeParameters = typeParameters, + members = members, + nestedClasses = emptyList(), + ir = declaration + ) + } + + return ExportedNamespace( + name = "$NOT_EXPORTED_NAMESPACE${declaration.packageFqName?.asString()?.takeIf { it.isNotEmpty() }?.let { ".$it" }.orEmpty()}", + declarations = listOf(exportedDeclaration), + isPrivate = true + ) + } + + private val IrClassifierSymbol.isInterface + get() = (owner as? IrClass)?.isInterface == true +} \ No newline at end of file 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 063e26b8d00..a6551cb921c 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 @@ -37,6 +37,8 @@ import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.util.OperatorNameConventions +val KOTLIN_TO_JS_CLOSURE_ORIGIN by IrDeclarationOriginImpl + /** * Create wrappers for external and @JsExport functions when type adaptation is needed */ @@ -466,6 +468,7 @@ class JsInteropFunctionsLowering(val context: WasmBackendContext) : DeclarationT val result = context.irFactory.buildFun { name = Name.identifier("__callFunction_${info.signatureString}") returnType = info.adaptedResultType + origin = KOTLIN_TO_JS_CLOSURE_ORIGIN } result.parent = currentParent result.addValueParameter { diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/WasmBridgesConstruction.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/WasmBridgesConstruction.kt index 6818dbe7e01..c237b586e36 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/WasmBridgesConstruction.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/WasmBridgesConstruction.kt @@ -9,8 +9,10 @@ import org.jetbrains.kotlin.backend.wasm.ir2wasm.WasmSignature import org.jetbrains.kotlin.backend.wasm.ir2wasm.wasmSignature import org.jetbrains.kotlin.ir.backend.js.JsCommonBackendContext import org.jetbrains.kotlin.ir.backend.js.lower.BridgesConstruction +import org.jetbrains.kotlin.ir.declarations.IrDeclaration import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction +import org.jetbrains.kotlin.ir.util.isEffectivelyExternal class WasmBridgesConstruction(context: JsCommonBackendContext) : BridgesConstruction(context) { override fun getFunctionSignature(function: IrSimpleFunction): WasmSignature = @@ -20,4 +22,9 @@ class WasmBridgesConstruction(context: JsCommonBackendContext) : BridgesConstruc override val shouldCastDispatchReceiver: Boolean = true override fun getBridgeOrigin(bridge: IrSimpleFunction): IrDeclarationOrigin = IrDeclarationOrigin.BRIDGE + + override fun transformFlat(declaration: IrDeclaration): List? { + if (declaration.isEffectivelyExternal()) return null + return super.transformFlat(declaration) + } } \ No newline at end of file diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/externalDeclarations.d.ts b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/externalDeclarations.d.ts new file mode 100644 index 00000000000..56ed17170e4 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/externalDeclarations.d.ts @@ -0,0 +1,29 @@ +type Nullable = T | null | undefined +export declare function getResult(): not.exported.org.second.Result; +declare namespace not.exported.org.second { + class Result> extends not.exported.org.second.BaseResult { + constructor(); + } +} +declare namespace not.exported.org.second { + abstract class BaseResult> { + constructor(foo: typeof not.exported.org.second.Foo); + } +} +declare namespace not.exported.org.second { + const Foo: { + get bar(): number; + get baz(): string; + } & not.exported.Baz; +} +declare namespace not.exported { + interface Baz extends not.exported.Bar { + readonly baz?: T; + readonly bar: number; + } +} +declare namespace not.exported { + interface Bar { + readonly bar: number; + } +} \ No newline at end of file diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/externalDeclarations.kt b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/externalDeclarations.kt new file mode 100644 index 00000000000..9586f7f2945 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/externalDeclarations.kt @@ -0,0 +1,38 @@ +// CHECK_TYPESCRIPT_DECLARATIONS +// TARGET_BACKEND: WASM +// MODULE: main + +// FILE: first.kt +external interface Bar { + val bar: Int +} + +external interface Baz : Bar { + val baz: T +} + +// FILE: second.kt +package org.second + +import Bar +import Baz + +external object Foo : Baz { + override val bar: Int + override val baz: JsString +} + +external abstract class BaseResult(foo: Foo) + +external class Result : BaseResult + +fun getResultInternal(): Result = js("({})") + +@JsExport +fun getResult(): Result = getResultInternal() + +// FILE: entry.mjs + +import main from "./index.mjs"; + +if (JSON.stringify(main.getResult()) != "{}") throw new Error("Unexpected result") \ No newline at end of file diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/generics.d.ts b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/generics.d.ts new file mode 100644 index 00000000000..962d61057bd --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/generics.d.ts @@ -0,0 +1,20 @@ +type Nullable = T | null | undefined +export declare function simple(x: T): T; +export declare function second(a: A, b: B): B; +export declare function simpleWithConstraint(x: T): T; +export declare function complexConstraint & not.exported.Bar, B extends typeof not.exported.Baz>(x: A): B; +declare namespace not.exported { + interface Foo> { + readonly foo: T; + } +} +declare namespace not.exported { + interface Bar { + readonly bar: string; + } +} +declare namespace not.exported { + const Baz: { + get baz(): boolean; + }; +} diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/generics.kt b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/generics.kt new file mode 100644 index 00000000000..5c82453f6f2 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/generics.kt @@ -0,0 +1,39 @@ +// CHECK_TYPESCRIPT_DECLARATIONS +// TARGET_BACKEND: WASM +// MODULE: main + +// FILE: generics.kt +@JsExport +fun simple(x: T): T = x + +@JsExport +fun second(a: A, b: B): B = b + +@JsExport +fun simpleWithConstraint(x: T): T = x + +external interface Foo : JsAny { + val foo: T +} + +external interface Bar : JsAny { + val bar: JsString +} + +external object Baz : JsAny { + val baz: JsBoolean +} + +fun getBaz(x: Foo): JsAny = js("({ baz: x.foo > 0n })") + +@JsExport +fun complexConstraint(x: A): B where A: Foo, A: Bar, B: Baz = getBaz(x).unsafeCast() + +// FILE: entry.mjs + +import main from "./index.mjs"; + +if (main.simple("OK") != "OK") throw new Error("Unexpected result from `simple` function") +if (main.second(1, "OK") != "OK") throw new Error("Unexpected result from `second` function") +if (main.simpleWithConstraint(42) != 42) throw new Error("Unexpected result from `simpleConstraint` function") +if (JSON.stringify(main.complexConstraint({ foo: 1n, bar: "bar" })) != "{\"baz\":true}") throw new Error("Unexpected result from `complexConstraint` function") diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/jsPrimitives.d.ts b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/jsPrimitives.d.ts new file mode 100644 index 00000000000..bf95f59da5d --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/jsPrimitives.d.ts @@ -0,0 +1,11 @@ +type Nullable = T | null | undefined +export declare function produceBoolean(): boolean; +export declare function produceNumber(): number; +export declare function produceBigInt(): bigint; +export declare function produceString(): string; +export declare function produceAny(): NonNullable; +export declare function consumeBoolean(x: boolean): string; +export declare function consumeNumber(x: number): string; +export declare function consumeBigInt(x: bigint): string; +export declare function consumeString(x: string): string; +export declare function consumeAny(x: NonNullable): string; \ No newline at end of file diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/jsPrimitives.kt b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/jsPrimitives.kt new file mode 100644 index 00000000000..1bbe55c3508 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/jsPrimitives.kt @@ -0,0 +1,51 @@ +// CHECK_TYPESCRIPT_DECLARATIONS +// TARGET_BACKEND: WASM +// MODULE: main +// FILE: jsPrimitives.kt + +@JsExport +fun produceBoolean(): JsBoolean = true.toJsBoolean() + +@JsExport +fun produceNumber(): JsNumber = Int.MAX_VALUE.toJsNumber() + +@JsExport +fun produceBigInt(): JsBigInt = Long.MAX_VALUE.toJsBigInt() + +@JsExport +fun produceString(): JsString = "OK".toJsString() + +@JsExport +fun produceAny(): JsAny = 42.toJsNumber() + +@JsExport +fun consumeBoolean(x: JsBoolean): String = x.toBoolean().toString() + +@JsExport +fun consumeNumber(x: JsNumber): String = x.toInt().toString() + +@JsExport +fun consumeBigInt(x: JsBigInt): String = x.toLong().toString() + +@JsExport +fun consumeString(x: JsString): String = x.toString() + +@JsExport +fun consumeAny(x: JsAny): String = x.toString() + +// FILE: entry.mjs + +import main from "./index.mjs" + +// PRODUCING +if (!main.produceBoolean()) throw new Error("Unexpected value was returned from the `produceBoolean` function") +if (main.produceNumber() != 2147483647) throw new Error("Unexpected value was returned from the `produceNumber` function") +if (main.produceBigInt() != 9223372036854775807n) throw new Error("Unexpected value was returned from the `produceBigInt` function") +if (main.produceString() != "OK") throw new Error("Unexpected value was returned from the `produceString` function") +if (main.produceAny() != 42) throw new Error("Unexpected value was returned from the `produceAny` function") + +// CONSUMPTION +if (main.consumeBoolean(false) != "false") throw new Error("Unexpected value was returned from the `consumeBoolean` function") +if (main.consumeNumber(-2147483648) != "-2147483648") throw new Error("Unexpected value was returned from the `consumeNumber` function") +if (main.consumeBigInt(-9223372036854775808n) != "-9223372036854775808") throw new Error("Unexpected value was returned from the `consumeBigInt` function") +if (main.consumeAny(24) != 24) throw new Error("Unexpected value was returned from the `consumeAny` function") diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableJsPrimitives.d.ts b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableJsPrimitives.d.ts new file mode 100644 index 00000000000..11a55f705c5 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableJsPrimitives.d.ts @@ -0,0 +1,11 @@ +type Nullable = T | null | undefined +export declare function produceBoolean(): Nullable; +export declare function produceNumber(): Nullable; +export declare function produceBigInt(): Nullable; +export declare function produceString(): Nullable; +export declare function produceAny(): unknown; +export declare function consumeBoolean(x: Nullable): Nullable; +export declare function consumeNumber(x: Nullable): Nullable; +export declare function consumeBigInt(x: Nullable): Nullable; +export declare function consumeString(x: Nullable): Nullable; +export declare function consumeAny(x: unknown): Nullable; \ No newline at end of file diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableJsPrimitives.kt b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableJsPrimitives.kt new file mode 100644 index 00000000000..d3a63458cc4 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableJsPrimitives.kt @@ -0,0 +1,51 @@ +// CHECK_TYPESCRIPT_DECLARATIONS +// TARGET_BACKEND: WASM +// MODULE: main +// FILE: nullableJsPrimitives.kt + +@JsExport +fun produceBoolean(): JsBoolean? = true.toJsBoolean() + +@JsExport +fun produceNumber(): JsNumber? = Int.MAX_VALUE.toJsNumber() + +@JsExport +fun produceBigInt(): JsBigInt? = Long.MAX_VALUE.toJsBigInt() + +@JsExport +fun produceString(): JsString? = "OK".toJsString() + +@JsExport +fun produceAny(): JsAny? = 42.toJsNumber() + +@JsExport +fun consumeBoolean(x: JsBoolean?): String? = x?.toBoolean()?.toString() + +@JsExport +fun consumeNumber(x: JsNumber?): String? = x?.toInt()?.toString() + +@JsExport +fun consumeBigInt(x: JsBigInt?): String? = x?.toLong()?.toString() + +@JsExport +fun consumeString(x: JsString?): String? = x?.toString() + +@JsExport +fun consumeAny(x: JsAny?): String? = x?.toString() + +// FILE: entry.mjs + +import main from "./index.mjs" + +// PRODUCING +if (!main.produceBoolean()) throw new Error("Unexpected value was returned from the `produceBoolean` function") +if (main.produceNumber() != 2147483647) throw new Error("Unexpected value was returned from the `produceNumber` function") +if (main.produceBigInt() != 9223372036854775807n) throw new Error("Unexpected value was returned from the `produceBigInt` function") +if (main.produceString() != "OK") throw new Error("Unexpected value was returned from the `produceString` function") +if (main.produceAny() != 42) throw new Error("Unexpected value was returned from the `produceAny` function") + +// CONSUMPTION +if (main.consumeBoolean(false) != "false") throw new Error("Unexpected value was returned from the `consumeBoolean` function") +if (main.consumeNumber(-2147483648) != "-2147483648") throw new Error("Unexpected value was returned from the `consumeNumber` function") +if (main.consumeBigInt(-9223372036854775808n) != "-9223372036854775808") throw new Error("Unexpected value was returned from the `consumeBigInt` function") +if (main.consumeAny(24) != 24) throw new Error("Unexpected value was returned from the `consumeAny` function") diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullablePrimitives.d.ts b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullablePrimitives.d.ts new file mode 100644 index 00000000000..04f5805b515 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullablePrimitives.d.ts @@ -0,0 +1,17 @@ +type Nullable = T | null | undefined +export declare function produceBoolean(): Nullable; +export declare function produceByte(): Nullable; +export declare function produceShort(): Nullable; +export declare function produceInt(): Nullable; +export declare function produceLong(): Nullable; +export declare function produceChar(): Nullable; +export declare function produceString(): Nullable; +export declare function produceFunction(): Nullable<() => number>; +export declare function consumeBoolean(x: Nullable): Nullable; +export declare function consumeByte(x: Nullable): Nullable; +export declare function consumeShort(x: Nullable): Nullable; +export declare function consumeInt(x: Nullable): Nullable; +export declare function consumeLong(x: Nullable): Nullable; +export declare function consumeChar(x: Nullable): Nullable; +export declare function consumeString(x: Nullable): Nullable; +export declare function consumeFunction(fn: Nullable<(p0: string) => number>): Nullable; \ No newline at end of file diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullablePrimitives.kt b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullablePrimitives.kt new file mode 100644 index 00000000000..51dbf4d542c --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullablePrimitives.kt @@ -0,0 +1,76 @@ +// CHECK_TYPESCRIPT_DECLARATIONS +// TARGET_BACKEND: WASM +// MODULE: main +// FILE: nullablePrimitives.kt + +@JsExport +fun produceBoolean(): Boolean? = true + +@JsExport +fun produceByte(): Byte? = Byte.MAX_VALUE + +@JsExport +fun produceShort(): Short? = Short.MAX_VALUE + +@JsExport +fun produceInt(): Int? = Int.MAX_VALUE + +@JsExport +fun produceLong(): Long? = Long.MAX_VALUE + +@JsExport +fun produceChar(): Char? = 'a' + +@JsExport +fun produceString(): String? = "OK" + +@JsExport +fun produceFunction(): (() -> Int)? = { 42 } + +@JsExport +fun consumeBoolean(x: Boolean?): String? = x?.toString() + +@JsExport +fun consumeByte(x: Byte?): String? = x?.toString() + +@JsExport +fun consumeShort(x: Short?): String? = x?.toString() + +@JsExport +fun consumeInt(x: Int?): String? = x?.toString() + +@JsExport +fun consumeLong(x: Long?): String? = x?.toString() + +@JsExport +fun consumeChar(x: Char?): String? = x?.toString() + +@JsExport +fun consumeString(x: String?): String? = x + +@JsExport +fun consumeFunction(fn: ((String) -> Int)?): Int? = fn?.invoke("42") + +// FILE: entry.mjs + +import main from "./index.mjs" + +// PRODUCING +if (!main.produceBoolean()) throw new Error("Unexpected value was returned from the `produceBoolean` function") +if (main.produceByte() != 127) throw new Error("Unexpected value was returned from the `produceByte` function") +if (main.produceShort() != 32767) throw new Error("Unexpected value was returned from the `produceShort` function") +if (main.produceInt() != 2147483647) throw new Error("Unexpected value was returned from the `produceInt` function") +if (main.produceLong() != 9223372036854775807n) throw new Error("Unexpected value was returned from the `produceLong` function") +if (String.fromCharCode(main.produceChar()) != "a") throw new Error("Unexpected value was returned from the `produceChar` function") +if (main.produceString() != "OK") throw new Error("Unexpected value was returned from the `produceString` function") +if (main.produceFunction()() != 42) throw new Error("Unexpected value was returned from the `produceFunction` function") + +// CONSUMPTION +if (main.consumeBoolean(false) != "false") throw new Error("Unexpected value was returned from the `consumeBoolean` function") +if (main.consumeByte(-128) != "-128") throw new Error("Unexpected value was returned from the `consumeByte` function") +if (main.consumeShort(-32768) != "-32768") throw new Error("Unexpected value was returned from the `consumeShort` function") +if (main.consumeInt(-2147483648) != "-2147483648") throw new Error("Unexpected value was returned from the `consumeInt` function") +if (main.consumeLong(-9223372036854775808n) != "-9223372036854775808") throw new Error("Unexpected value was returned from the `consumeLong` function") +if (main.consumeChar("b".charCodeAt()) != "b") throw new Error("Unexpected value was returned from the `consumeChar` function") +if (main.consumeString("🙂") != "🙂") throw new Error("Unexpected value was returned from the `consumeString` function") +if (main.consumeFunction(parseInt) != 42) throw new Error("Unexpected value was returned from the `consumeFunction` function") diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableUnisnged.d.ts b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableUnisnged.d.ts new file mode 100644 index 00000000000..772a1ed8af5 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableUnisnged.d.ts @@ -0,0 +1,11 @@ +type Nullable = T | null | undefined +export declare function produceUByte(): Nullable; +export declare function produceUShort(): Nullable; +export declare function produceUInt(): Nullable; +export declare function produceULong(): Nullable; +export declare function produceFunction(): () => Nullable; +export declare function consumeUByte(x: Nullable): Nullable; +export declare function consumeUShort(x: Nullable): Nullable; +export declare function consumeUInt(x: Nullable): Nullable; +export declare function consumeULong(x: Nullable): Nullable; +export declare function consumeFunction(fn: (p0: string) => Nullable): Nullable; diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableUnisnged.kt b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableUnisnged.kt new file mode 100644 index 00000000000..1f423537013 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableUnisnged.kt @@ -0,0 +1,52 @@ +// CHECK_TYPESCRIPT_DECLARATIONS +// TARGET_BACKEND: WASM +// MODULE: main +// FILE: nullableUnsigned.kt + +@JsExport +fun produceUByte(): UByte? = UByte.MAX_VALUE + +@JsExport +fun produceUShort(): UShort? = UShort.MAX_VALUE + +@JsExport +fun produceUInt(): UInt? = UInt.MAX_VALUE + +@JsExport +fun produceULong(): ULong? = ULong.MAX_VALUE + +@JsExport +fun produceFunction(): () -> UInt? = ::produceUInt + +@JsExport +fun consumeUByte(x: UByte?): String? = x?.toString() + +@JsExport +fun consumeUShort(x: UShort?): String? = x?.toString() + +@JsExport +fun consumeUInt(x: UInt?): String? = x?.toString() + +@JsExport +fun consumeULong(x: ULong?): String? = x?.toString() + +@JsExport +fun consumeFunction(fn: (String) -> UInt?): UInt? = fn("42") + +// FILE: entry.mjs + +import main from "./index.mjs" + +// PRODUCING +if (main.produceUByte() != 255) throw new Error("Unexpected value was returned from the `produceUByte` function") +if (main.produceUShort() != 65535) throw new Error("Unexpected value was returned from the `produceUShort` function") +if (main.produceUInt() != 4294967295) throw new Error("Unexpected value was returned from the `produceUInt` function") +if (main.produceULong() != 18446744073709551615n) throw new Error("Unexpected value was returned from the `produceULong` function") +if (main.produceFunction()() != 4294967295) throw new Error("Unexpected value was returned from the `produceFunction` function") + +// CONSUMPTION +if (main.consumeUByte(-128) != "128") throw new Error("Unexpected value was returned from the `consumeUByte` function") +if (main.consumeUShort(-32768) != "32768") throw new Error("Unexpected value was returned from the `consumeUShort` function") +if (main.consumeUInt(-2147483648) != "2147483648") throw new Error("Unexpected value was returned from the `consumeUInt` function") +if (main.consumeULong(-9223372036854775808n) != "9223372036854775808") throw new Error("Unexpected value was returned from the `consumeULong` function") +if (main.consumeFunction(parseInt) != 42) throw new Error("Unexpected value was returned from the `consumeFunction` function") diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/primitives.d.ts b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/primitives.d.ts new file mode 100644 index 00000000000..e7a01316ea5 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/primitives.d.ts @@ -0,0 +1,19 @@ +type Nullable = T | null | undefined +export declare function produceBoolean(): boolean; +export declare function produceByte(): number; +export declare function produceShort(): number; +export declare function produceInt(): number; +export declare function produceLong(): bigint; +export declare function produceChar(): number; +export declare function produceString(): string; +export declare function getState(): string; +export declare function mutateState(): void; +export declare function produceFunction(): () => number; +export declare function consumeBoolean(x: boolean): string; +export declare function consumeByte(x: number): string; +export declare function consumeShort(x: number): string; +export declare function consumeInt(x: number): string; +export declare function consumeLong(x: bigint): string; +export declare function consumeChar(x: number): string; +export declare function consumeString(x: string): string; +export declare function consumeFunction(fn: (p0: string) => number): number; diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/primitives.kt b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/primitives.kt new file mode 100644 index 00000000000..63c7af07011 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/primitives.kt @@ -0,0 +1,89 @@ +// CHECK_TYPESCRIPT_DECLARATIONS +// TARGET_BACKEND: WASM +// MODULE: main +// FILE: primitives.kt + +@JsExport +fun produceBoolean(): Boolean = true + +@JsExport +fun produceByte(): Byte = Byte.MAX_VALUE + +@JsExport +fun produceShort(): Short = Short.MAX_VALUE + +@JsExport +fun produceInt(): Int = Int.MAX_VALUE + +@JsExport +fun produceLong(): Long = Long.MAX_VALUE + +@JsExport +fun produceChar(): Char = 'a' + +@JsExport +fun produceString(): String = "OK" + +private var state = "INITIAL" + +@JsExport +fun getState(): String = state + +@JsExport +fun mutateState() { + state = "MUTATED" +} + +@JsExport +fun produceFunction(): () -> Int = ::produceInt + +@JsExport +fun consumeBoolean(x: Boolean): String = x.toString() + +@JsExport +fun consumeByte(x: Byte): String = x.toString() + +@JsExport +fun consumeShort(x: Short): String = x.toString() + +@JsExport +fun consumeInt(x: Int): String = x.toString() + +@JsExport +fun consumeLong(x: Long): String = x.toString() + +@JsExport +fun consumeChar(x: Char): String = x.toString() + +@JsExport +fun consumeString(x: String): String = x + +@JsExport +fun consumeFunction(fn: (String) -> Int): Int = fn("42") + +// FILE: entry.mjs + +import main from "./index.mjs" + +// PRODUCING +if (!main.produceBoolean()) throw new Error("Unexpected value was returned from the `produceBoolean` function") +if (main.produceByte() != 127) throw new Error("Unexpected value was returned from the `produceByte` function") +if (main.produceShort() != 32767) throw new Error("Unexpected value was returned from the `produceShort` function") +if (main.produceInt() != 2147483647) throw new Error("Unexpected value was returned from the `produceInt` function") +if (main.produceLong() != 9223372036854775807n) throw new Error("Unexpected value was returned from the `produceLong` function") +if (String.fromCharCode(main.produceChar()) != "a") throw new Error("Unexpected value was returned from the `produceChar` function") +if (main.produceString() != "OK") throw new Error("Unexpected value was returned from the `produceString` function") +if (main.getState() != "INITIAL") throw new Error("Unexpected value was returned from the `getState` function before the mutation") +main.mutateState() +if (main.getState() != "MUTATED") throw new Error("Unexpected value was returned from the `getState` function after the mutation") +if (main.produceFunction()() != 2147483647) throw new Error("Unexpected value was returned from the `produceFunction` function") + +// CONSUMPTION +if (main.consumeBoolean(false) != "false") throw new Error("Unexpected value was returned from the `consumeBoolean` function") +if (main.consumeByte(-128) != "-128") throw new Error("Unexpected value was returned from the `consumeByte` function") +if (main.consumeShort(-32768) != "-32768") throw new Error("Unexpected value was returned from the `consumeShort` function") +if (main.consumeInt(-2147483648) != "-2147483648") throw new Error("Unexpected value was returned from the `consumeInt` function") +if (main.consumeLong(-9223372036854775808n) != "-9223372036854775808") throw new Error("Unexpected value was returned from the `consumeLong` function") +if (main.consumeChar("b".charCodeAt()) != "b") throw new Error("Unexpected value was returned from the `consumeChar` function") +if (main.consumeString("🙂") != "🙂") throw new Error("Unexpected value was returned from the `consumeString` function") +if (main.consumeFunction(parseInt) != 42) throw new Error("Unexpected value was returned from the `consumeFunction` function") diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/unisnged.d.ts b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/unisnged.d.ts new file mode 100644 index 00000000000..d97baa47af6 --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/unisnged.d.ts @@ -0,0 +1,11 @@ +type Nullable = T | null | undefined +export declare function produceUByte(): number; +export declare function produceUShort(): number; +export declare function produceUInt(): number; +export declare function produceULong(): bigint; +export declare function produceFunction(): () => number; +export declare function consumeUByte(x: number): string; +export declare function consumeUShort(x: number): string; +export declare function consumeUInt(x: number): string; +export declare function consumeULong(x: bigint): string; +export declare function consumeFunction(fn: (p0: string) => number): number; diff --git a/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/unisnged.kt b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/unisnged.kt new file mode 100644 index 00000000000..9b1abfb65fa --- /dev/null +++ b/compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/unisnged.kt @@ -0,0 +1,52 @@ +// CHECK_TYPESCRIPT_DECLARATIONS +// TARGET_BACKEND: WASM +// MODULE: main +// FILE: unsigned.kt + +@JsExport +fun produceUByte(): UByte = UByte.MAX_VALUE + +@JsExport +fun produceUShort(): UShort = UShort.MAX_VALUE + +@JsExport +fun produceUInt(): UInt = UInt.MAX_VALUE + +@JsExport +fun produceULong(): ULong = ULong.MAX_VALUE + +@JsExport +fun produceFunction(): () -> UInt = ::produceUInt + +@JsExport +fun consumeUByte(x: UByte): String = x.toString() + +@JsExport +fun consumeUShort(x: UShort): String = x.toString() + +@JsExport +fun consumeUInt(x: UInt): String = x.toString() + +@JsExport +fun consumeULong(x: ULong): String = x.toString() + +@JsExport +fun consumeFunction(fn: (String) -> UInt): UInt = fn("42") + +// FILE: entry.mjs + +import main from "./index.mjs" + +// PRODUCING +if (main.produceUByte() != 255) throw new Error("Unexpected value was returned from the `produceUByte` function") +if (main.produceUShort() != 65535) throw new Error("Unexpected value was returned from the `produceUShort` function") +if (main.produceUInt() != 4294967295) throw new Error("Unexpected value was returned from the `produceUInt` function") +if (main.produceULong() != 18446744073709551615n) throw new Error("Unexpected value was returned from the `produceULong` function") +if (main.produceFunction()() != 4294967295) throw new Error("Unexpected value was returned from the `produceFunction` function") + +// CONSUMPTION +if (main.consumeUByte(-128) != "128") throw new Error("Unexpected value was returned from the `consumeUByte` function") +if (main.consumeUShort(-32768) != "32768") throw new Error("Unexpected value was returned from the `consumeUShort` function") +if (main.consumeUInt(-2147483648) != "2147483648") throw new Error("Unexpected value was returned from the `consumeUInt` function") +if (main.consumeULong(-9223372036854775808n) != "9223372036854775808") throw new Error("Unexpected value was returned from the `consumeULong` function") +if (main.consumeFunction(parseInt) != 42) throw new Error("Unexpected value was returned from the `consumeFunction` function") diff --git a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/WasmEnvironmentConfigurationDirectives.kt b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/WasmEnvironmentConfigurationDirectives.kt index fd3ec4df2f9..b3e8201bde1 100644 --- a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/WasmEnvironmentConfigurationDirectives.kt +++ b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/WasmEnvironmentConfigurationDirectives.kt @@ -43,4 +43,8 @@ object WasmEnvironmentConfigurationDirectives : SimpleDirectivesContainer() { val RUN_THIRD_PARTY_OPTIMIZER by directive( description = "Also run third-party optimizer (for now, only binaryen is supported) after the main compilation", ) + + val CHECK_TYPESCRIPT_DECLARATIONS by directive( + description = "Check typescript declarations generated by the compiler", + ) } diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java index eb866efc99b..994bdaf1629 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsCodegenWasmJsInteropTestGenerated.java @@ -108,4 +108,14 @@ public class FirJsCodegenWasmJsInteropTestGenerated extends AbstractFirJsCodegen public void testVararg() throws Exception { runTest("compiler/testData/codegen/boxWasmJsInterop/vararg.kt"); } + + @Nested + @TestMetadata("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations") + @TestDataPath("$PROJECT_ROOT") + public class TypeScriptDeclarations { + @Test + public void testAllFilesPresentInTypeScriptDeclarations() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JS_IR, true); + } + } } diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsES6CodegenWasmJsInteropTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsES6CodegenWasmJsInteropTestGenerated.java index 97af42f6523..148ec0d69db 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsES6CodegenWasmJsInteropTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/fir/FirJsES6CodegenWasmJsInteropTestGenerated.java @@ -108,4 +108,14 @@ public class FirJsES6CodegenWasmJsInteropTestGenerated extends AbstractFirJsES6C public void testVararg() throws Exception { runTest("compiler/testData/codegen/boxWasmJsInterop/vararg.kt"); } + + @Nested + @TestMetadata("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations") + @TestDataPath("$PROJECT_ROOT") + public class TypeScriptDeclarations { + @Test + public void testAllFilesPresentInTypeScriptDeclarations() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JS_IR_ES6, true); + } + } } diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java index 15169ab57d9..9b983b2f915 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrCodegenWasmJsInteropJsTestGenerated.java @@ -108,4 +108,14 @@ public class IrCodegenWasmJsInteropJsTestGenerated extends AbstractIrCodegenWasm public void testVararg() throws Exception { runTest("compiler/testData/codegen/boxWasmJsInterop/vararg.kt"); } + + @Nested + @TestMetadata("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations") + @TestDataPath("$PROJECT_ROOT") + public class TypeScriptDeclarations { + @Test + public void testAllFilesPresentInTypeScriptDeclarations() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JS_IR, true); + } + } } diff --git a/wasm/wasm.tests/test/org/jetbrains/kotlin/wasm/test/AbstractWasmBlackBoxCodegenTestBase.kt b/wasm/wasm.tests/test/org/jetbrains/kotlin/wasm/test/AbstractWasmBlackBoxCodegenTestBase.kt index 0fa5554e281..4b987fa9f1a 100644 --- a/wasm/wasm.tests/test/org/jetbrains/kotlin/wasm/test/AbstractWasmBlackBoxCodegenTestBase.kt +++ b/wasm/wasm.tests/test/org/jetbrains/kotlin/wasm/test/AbstractWasmBlackBoxCodegenTestBase.kt @@ -25,6 +25,7 @@ import org.jetbrains.kotlin.test.services.AdditionalSourceProvider import org.jetbrains.kotlin.test.services.EnvironmentConfigurator import org.jetbrains.kotlin.test.services.LibraryProvider import org.jetbrains.kotlin.test.services.sourceProviders.CoroutineHelpersSourceFilesProvider +import org.jetbrains.kotlin.wasm.test.handlers.WasmDtsHandler abstract class AbstractWasmBlackBoxCodegenTestBase, I : ResultingArtifact.BackendInput, A : ResultingArtifact.Binary>( private val targetFrontend: FrontendKind, @@ -94,6 +95,7 @@ abstract class AbstractWasmBlackBoxCodegenTestBase= DebugMode.DEBUG val baseFileName = "index" @@ -106,11 +108,12 @@ class WasmBackendFacade( val compilerResult = compileWasm( allModules = allModules, backendContext = backendContext, + typeScriptFragment = typeScriptFragment, baseFileName = baseFileName, emitNameSection = true, allowIncompleteImplementations = false, generateWat = generateWat, - generateSourceMaps = generateSourceMaps + generateSourceMaps = generateSourceMaps, ) val dceDumpNameCache = DceDumpNameCache() @@ -121,11 +124,12 @@ class WasmBackendFacade( val compilerResultWithDCE = compileWasm( allModules = allModules, backendContext = backendContext, + typeScriptFragment = typeScriptFragment, baseFileName = baseFileName, emitNameSection = true, allowIncompleteImplementations = true, generateWat = generateWat, - generateSourceMaps = generateSourceMaps + generateSourceMaps = generateSourceMaps, ) return BinaryArtifacts.Wasm( @@ -148,7 +152,8 @@ class WasmBackendFacade( jsUninstantiatedWrapper = jsUninstantiatedWrapper, jsWrapper = jsWrapper, wasm = newWasm, - debugInformation = null + debugInformation = null, + dts = dts ) } } diff --git a/wasm/wasm.tests/test/org/jetbrains/kotlin/wasm/test/handlers/WasmDtsHandler.kt b/wasm/wasm.tests/test/org/jetbrains/kotlin/wasm/test/handlers/WasmDtsHandler.kt new file mode 100644 index 00000000000..2df73c75daf --- /dev/null +++ b/wasm/wasm.tests/test/org/jetbrains/kotlin/wasm/test/handlers/WasmDtsHandler.kt @@ -0,0 +1,32 @@ +/* + * 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.wasm.test.handlers + +import org.jetbrains.kotlin.test.KotlinTestUtils +import org.jetbrains.kotlin.test.model.TestModule +import org.jetbrains.kotlin.test.backend.handlers.WasmBinaryArtifactHandler +import org.jetbrains.kotlin.test.directives.WasmEnvironmentConfigurationDirectives +import org.jetbrains.kotlin.test.model.BinaryArtifacts +import org.jetbrains.kotlin.test.services.TestServices +import org.jetbrains.kotlin.test.services.moduleStructure +import org.jetbrains.kotlin.utils.fileUtils.withReplacedExtensionOrNull + +class WasmDtsHandler(testServices: TestServices) : WasmBinaryArtifactHandler(testServices) { + override fun processAfterAllModules(someAssertionWasFailed: Boolean) {} + + override fun processModule(module: TestModule, info: BinaryArtifacts.Wasm) { + val globalDirectives = testServices.moduleStructure.allDirectives + if (WasmEnvironmentConfigurationDirectives.CHECK_TYPESCRIPT_DECLARATIONS !in globalDirectives) return + + val referenceDtsFile = module.files.first().originalFile.withReplacedExtensionOrNull(".kt", ".d.ts") + ?: error("Can't find reference .d.ts file") + + val generatedDts = info.compilerResult.dts + ?: error("Can't find generated .d.ts file") + + KotlinTestUtils.assertEqualsToFile(referenceDtsFile, generatedDts) + } +} 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 3e7167260c9..57203687055 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 @@ -186,4 +186,62 @@ public class FirWasmCodegenWasmJsInteropTestGenerated extends AbstractFirWasmCod public void testWasmImport() throws Exception { runTest("compiler/testData/codegen/boxWasmJsInterop/wasmImport.kt"); } + + @Nested + @TestMetadata("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations") + @TestDataPath("$PROJECT_ROOT") + public class TypeScriptDeclarations { + @Test + public void testAllFilesPresentInTypeScriptDeclarations() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.WASM, true); + } + + @Test + @TestMetadata("externalDeclarations.kt") + public void testExternalDeclarations() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/externalDeclarations.kt"); + } + + @Test + @TestMetadata("generics.kt") + public void testGenerics() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/generics.kt"); + } + + @Test + @TestMetadata("jsPrimitives.kt") + public void testJsPrimitives() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/jsPrimitives.kt"); + } + + @Test + @TestMetadata("nullableJsPrimitives.kt") + public void testNullableJsPrimitives() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableJsPrimitives.kt"); + } + + @Test + @TestMetadata("nullablePrimitives.kt") + public void testNullablePrimitives() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullablePrimitives.kt"); + } + + @Test + @TestMetadata("nullableUnisnged.kt") + public void testNullableUnisnged() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableUnisnged.kt"); + } + + @Test + @TestMetadata("primitives.kt") + public void testPrimitives() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/primitives.kt"); + } + + @Test + @TestMetadata("unisnged.kt") + public void testUnisnged() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/unisnged.kt"); + } + } } 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 3e60e2364a2..765f5e22791 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 @@ -186,4 +186,62 @@ public class K1WasmCodegenWasmJsInteropTestGenerated extends AbstractK1WasmCodeg public void testWasmImport() throws Exception { runTest("compiler/testData/codegen/boxWasmJsInterop/wasmImport.kt"); } + + @Nested + @TestMetadata("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations") + @TestDataPath("$PROJECT_ROOT") + public class TypeScriptDeclarations { + @Test + public void testAllFilesPresentInTypeScriptDeclarations() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.WASM, true); + } + + @Test + @TestMetadata("externalDeclarations.kt") + public void testExternalDeclarations() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/externalDeclarations.kt"); + } + + @Test + @TestMetadata("generics.kt") + public void testGenerics() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/generics.kt"); + } + + @Test + @TestMetadata("jsPrimitives.kt") + public void testJsPrimitives() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/jsPrimitives.kt"); + } + + @Test + @TestMetadata("nullableJsPrimitives.kt") + public void testNullableJsPrimitives() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableJsPrimitives.kt"); + } + + @Test + @TestMetadata("nullablePrimitives.kt") + public void testNullablePrimitives() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullablePrimitives.kt"); + } + + @Test + @TestMetadata("nullableUnisnged.kt") + public void testNullableUnisnged() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/nullableUnisnged.kt"); + } + + @Test + @TestMetadata("primitives.kt") + public void testPrimitives() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/primitives.kt"); + } + + @Test + @TestMetadata("unisnged.kt") + public void testUnisnged() throws Exception { + runTest("compiler/testData/codegen/boxWasmJsInterop/typeScriptDeclarations/unisnged.kt"); + } + } }