From bede039c086a0249fb071df7728f5638a7e02052 Mon Sep 17 00:00:00 2001 From: Igor Laevsky Date: Fri, 8 Oct 2021 19:36:18 +0300 Subject: [PATCH] [Wasm] Add compiler support for the kotlin.test library --- .../jetbrains/kotlin/cli/js/K2JsIrCompiler.kt | 3 + .../ir/backend/js/JsCommonBackendContext.kt | 5 ++ .../ir/backend/js/JsIrBackendContext.kt | 6 +- .../ir/backend/js/lower/TestGenerator.kt | 7 ++- .../kotlin/backend/wasm/WasmBackendContext.kt | 58 +++++++++++++++++-- .../kotlin/backend/wasm/WasmLoweringPhases.kt | 7 +++ .../kotlin/backend/wasm/WasmSymbols.kt | 19 +++++- .../backend/wasm/lower/TestGenerator.kt | 34 +++++++++++ .../kotlin/js/test/BasicWasmBoxTest.kt | 2 + libraries/stdlib/wasm/stubs/jsStubs.kt | 12 ++++ 10 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/TestGenerator.kt create mode 100644 libraries/stdlib/wasm/stubs/jsStubs.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 c8c3aec17ec..55b5d93e1c7 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 @@ -323,7 +323,10 @@ class K2JsIrCompiler : CLICompiler() { val runner = """ export default WebAssembly.instantiateStreaming(fetch('${outputWasmFile.name}'), { runtime, js_code }).then((it) => { wasmInstance = it.instance; + + wasmInstance.exports.startUnitTests?.(); wasmInstance.exports.main?.(); + return it.instance.exports; }); """.trimIndent() diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsCommonBackendContext.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsCommonBackendContext.kt index 84522185910..41b89672245 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsCommonBackendContext.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsCommonBackendContext.kt @@ -41,6 +41,11 @@ interface JsCommonBackendContext : CommonBackendContext { val es6mode: Boolean get() = false + + val suiteFun: IrSimpleFunctionSymbol? + val testFun: IrSimpleFunctionSymbol? + + fun createTestContainerFun(module: IrModuleFragment): IrSimpleFunction } // TODO: investigate if it could be removed diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIrBackendContext.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIrBackendContext.kt index 20f2d20b20c..6ab6c2bf7cd 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIrBackendContext.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIrBackendContext.kt @@ -123,7 +123,7 @@ class JsIrBackendContext( private val testContainerFuns = mutableMapOf() - fun createTestContainerFun(module: IrModuleFragment): IrSimpleFunction { + override fun createTestContainerFun(module: IrModuleFragment): IrSimpleFunction { return testContainerFuns.getOrPut(module) { val file = syntheticFile("tests", module) irFactory.addFunction(file) { @@ -325,8 +325,8 @@ class JsIrBackendContext( val throwISEsymbol = symbolTable.referenceSimpleFunction(getFunctions(kotlinPackageFqn.child(Name.identifier("THROW_ISE"))).single()) val throwIAEsymbol = symbolTable.referenceSimpleFunction(getFunctions(kotlinPackageFqn.child(Name.identifier("THROW_IAE"))).single()) - val suiteFun = getFunctions(FqName("kotlin.test.suite")).singleOrNull()?.let { symbolTable.referenceSimpleFunction(it) } - val testFun = getFunctions(FqName("kotlin.test.test")).singleOrNull()?.let { symbolTable.referenceSimpleFunction(it) } + override val suiteFun = getFunctions(FqName("kotlin.test.suite")).singleOrNull()?.let { symbolTable.referenceSimpleFunction(it) } + override val testFun = getFunctions(FqName("kotlin.test.test")).singleOrNull()?.let { symbolTable.referenceSimpleFunction(it) } val primitiveClassProperties by lazy2 { primitiveClassesObject.owner.declarations.filterIsInstance() diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt index 21fb5e8eb31..185937b4b5e 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TestGenerator.kt @@ -11,6 +11,7 @@ import org.jetbrains.kotlin.descriptors.ClassKind import org.jetbrains.kotlin.descriptors.DescriptorVisibilities import org.jetbrains.kotlin.descriptors.Modality import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET +import org.jetbrains.kotlin.ir.backend.js.JsCommonBackendContext import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder import org.jetbrains.kotlin.ir.builders.declarations.buildFun @@ -26,7 +27,7 @@ import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name -fun generateTests(context: JsIrBackendContext, moduleFragment: IrModuleFragment) { +fun generateTests(context: JsCommonBackendContext, moduleFragment: IrModuleFragment) { val generator = TestGenerator(context) { context.createTestContainerFun(moduleFragment) } moduleFragment.files.toList().forEach { @@ -34,7 +35,7 @@ fun generateTests(context: JsIrBackendContext, moduleFragment: IrModuleFragment) } } -class TestGenerator(val context: JsIrBackendContext, val testContainerFactory: () -> IrSimpleFunction) : FileLoweringPass { +class TestGenerator(val context: JsCommonBackendContext, val testContainerFactory: () -> IrSimpleFunction) : FileLoweringPass { override fun lower(irFile: IrFile) { irFile.declarations.forEach { @@ -61,7 +62,7 @@ class TestGenerator(val context: JsIrBackendContext, val testContainerFactory: ( val function = context.irFactory.buildFun { this.name = Name.identifier("$name test fun") - this.returnType = context.irBuiltIns.anyNType + this.returnType = if (this@createInvocation == context.suiteFun!!) context.irBuiltIns.unitType else context.irBuiltIns.anyNType this.origin = JsIrBuilder.SYNTHESIZED_DECLARATION } function.parent = parentFunction diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmBackendContext.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmBackendContext.kt index 155b5194f60..08f3ae4b650 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmBackendContext.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmBackendContext.kt @@ -17,14 +17,14 @@ import org.jetbrains.kotlin.ir.* import org.jetbrains.kotlin.ir.backend.js.JsCommonBackendContext import org.jetbrains.kotlin.ir.backend.js.JsCommonCoroutineSymbols import org.jetbrains.kotlin.ir.backend.js.JsMapping +import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder import org.jetbrains.kotlin.ir.backend.js.lower.JsInnerClassesSupport +import org.jetbrains.kotlin.ir.builders.declarations.addFunction import org.jetbrains.kotlin.ir.builders.declarations.buildFun -import org.jetbrains.kotlin.ir.declarations.IrFactory -import org.jetbrains.kotlin.ir.declarations.IrFile -import org.jetbrains.kotlin.ir.declarations.IrModuleFragment -import org.jetbrains.kotlin.ir.declarations.IrPackageFragment +import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.declarations.impl.IrExternalPackageFragmentImpl import org.jetbrains.kotlin.ir.declarations.impl.IrFileImpl +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol import org.jetbrains.kotlin.ir.symbols.impl.DescriptorlessExternalPackageFragmentSymbol import org.jetbrains.kotlin.ir.types.IrTypeSystemContext import org.jetbrains.kotlin.ir.types.IrTypeSystemContextImpl @@ -119,4 +119,54 @@ class WasmBackendContext( /*TODO*/ print(message) } + + // + // Unit test support, mostly borrowed from the JS implementation + // + + override val suiteFun: IrSimpleFunctionSymbol? + get() = wasmSymbols.suiteFun + override val testFun: IrSimpleFunctionSymbol? + get() = wasmSymbols.testFun + + private fun syntheticFile(name: String, module: IrModuleFragment): IrFile { + return IrFileImpl(object : IrFileEntry { + override val name = "<$name>" + override val maxOffset = UNDEFINED_OFFSET + + override fun getSourceRangeInfo(beginOffset: Int, endOffset: Int) = + SourceRangeInfo( + "", + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + UNDEFINED_OFFSET, + UNDEFINED_OFFSET + ) + + override fun getLineNumber(offset: Int) = UNDEFINED_OFFSET + override fun getColumnNumber(offset: Int) = UNDEFINED_OFFSET + }, internalPackageFragmentDescriptor, module).also { + module.files += it + } + } + + private val testContainerFuns = mutableMapOf() + + val testEntryPoints: Collection + get() = testContainerFuns.values + + override fun createTestContainerFun(module: IrModuleFragment): IrSimpleFunction { + return testContainerFuns.getOrPut(module) { + val file = syntheticFile("tests", module) + irFactory.addFunction(file) { + name = Name.identifier("testContainer") + returnType = irBuiltIns.unitType + origin = JsIrBuilder.SYNTHESIZED_DECLARATION + }.apply { + body = irFactory.createBlockBody(UNDEFINED_OFFSET, UNDEFINED_OFFSET, emptyList()) + } + } + } } diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt index 7bad69b90db..27a0c051817 100644 --- a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/WasmLoweringPhases.kt @@ -53,6 +53,12 @@ private val validateIrAfterLowering = makeCustomWasmModulePhase( description = "Validate IR after lowering" ) +private val generateTests = makeCustomWasmModulePhase( + { context, module -> generateWasmTests(context, module) }, + name = "GenerateTests", + description = "Generates code to execute kotlin.test cases" +) + private val expectDeclarationsRemovingPhase = makeWasmModulePhase( ::ExpectDeclarationsRemoveLowering, name = "ExpectDeclarationsRemoving", @@ -469,6 +475,7 @@ val wasmPhases = NamedCompilerPhase( name = "IrModuleLowering", description = "IR module lowering", lower = validateIrBeforeLowering then + generateTests then excludeDeclarationsFromCodegenPhase then expectDeclarationsRemovingPhase then 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 583ffdb9673..56de317f6f3 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 @@ -27,6 +27,7 @@ import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.resolve.scopes.MemberScope import org.jetbrains.kotlin.util.OperatorNameConventions +import java.lang.IllegalArgumentException class WasmSymbols( context: WasmBackendContext, @@ -39,7 +40,8 @@ class WasmSymbols( context.module.getPackage(StandardNames.COLLECTIONS_PACKAGE_FQ_NAME) private val builtInsPackage: PackageViewDescriptor = context.module.getPackage(StandardNames.BUILT_INS_PACKAGE_FQ_NAME) - + private val kotlinTestPackage: PackageViewDescriptor = + context.module.getPackage(FqName("kotlin.test")) override val throwNullPointerException = getInternalFunction("THROW_NPE") override val throwISE = getInternalFunction("THROW_ISE") @@ -131,6 +133,10 @@ class WasmSymbols( val stringGetLiteral = getFunction("stringLiteral", builtInsPackage) + val testFun = maybeGetFunction("test", kotlinTestPackage) + val suiteFun = maybeGetFunction("suite", kotlinTestPackage) + val startUnitTests = maybeGetFunction("startUnitTests", kotlinTestPackage) + val wasmClassId = getInternalFunction("wasmClassId") val wasmInterfaceId = getInternalFunction("wasmInterfaceId") @@ -211,10 +217,17 @@ class WasmSymbols( findProperty(context.module.getPackage(fqName.parent()).memberScope, fqName.shortName()).single() private fun getFunction(name: String, ownerPackage: PackageViewDescriptor): IrSimpleFunctionSymbol { - val tmp = findFunctions(ownerPackage.memberScope, Name.identifier(name)).single() - return symbolTable.referenceSimpleFunction(tmp) + return maybeGetFunction(name, ownerPackage) ?: throw IllegalArgumentException("Function $name not found") } + private fun maybeGetFunction(name: String, ownerPackage: PackageViewDescriptor): IrSimpleFunctionSymbol? { + val tmp = findFunctions(ownerPackage.memberScope, Name.identifier(name)) + if (tmp.isEmpty()) + return null + return symbolTable.referenceSimpleFunction(tmp.single()) + } + + private fun getInternalFunction(name: String) = getFunction(name, wasmInternalPackage) private fun getIrClass(fqName: FqName): IrClassSymbol = symbolTable.referenceClass(getClass(fqName)) diff --git a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/TestGenerator.kt b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/TestGenerator.kt new file mode 100644 index 00000000000..b67a3a365e9 --- /dev/null +++ b/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/lower/TestGenerator.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.backend.wasm.lower + +import org.jetbrains.kotlin.backend.common.lower.createIrBuilder +import org.jetbrains.kotlin.backend.wasm.WasmBackendContext +import org.jetbrains.kotlin.ir.backend.js.lower.TestGenerator +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.declarations.IrModuleFragment +import org.jetbrains.kotlin.ir.expressions.IrBlockBody + +fun generateWasmTests(context: WasmBackendContext, moduleFragment: IrModuleFragment) { + val generator = TestGenerator(context) { context.createTestContainerFun(moduleFragment) } + + moduleFragment.files.toList().forEach { + generator.lower(it) + } + + if (context.testEntryPoints.isEmpty()) + return + require(context.wasmSymbols.startUnitTests != null) { "kotlin.test package must be present" } + + val builder = context.createIrBuilder(context.wasmSymbols.startUnitTests) + val startFunctionBody = context.wasmSymbols.startUnitTests.owner.body as IrBlockBody + + context.testEntryPoints.forEach { testEntry -> + startFunctionBody.statements.add( + builder.irCall(testEntry) + ) + } +} diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicWasmBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicWasmBoxTest.kt index ce8717bacbe..2052e74baea 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicWasmBoxTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicWasmBoxTest.kt @@ -192,6 +192,8 @@ abstract class BasicWasmBoxTest( const ${sanitizeName(config.moduleId)} = wasmInstance.exports; + wasmInstance.exports.startUnitTests?.(); + const actualResult = importStringFromWasm(wasmInstance.exports.$testFunction()); if (actualResult !== "OK") throw `Wrong box result '${'$'}{actualResult}'; Expected "OK"`; diff --git a/libraries/stdlib/wasm/stubs/jsStubs.kt b/libraries/stdlib/wasm/stubs/jsStubs.kt new file mode 100644 index 00000000000..37b00845ff6 --- /dev/null +++ b/libraries/stdlib/wasm/stubs/jsStubs.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.js + +@ExperimentalStdlibApi +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.PROPERTY) +@SinceKotlin("1.6") +public actual annotation class JsEagerInitialization