[WIP] Web workers
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 9b67e66..c9e7a09 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
@@ -1,6 +1,6 @@ /* - * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license - * that can be found in the license/LICENSE.txt file. + * Copyright 2010-2019 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.cli.js
diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt index eb25690..bdee0a4 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsIntrinsics.kt
@@ -26,7 +26,6 @@ import org.jetbrains.kotlin.psi2ir.findSingleFunction import org.jetbrains.kotlin.types.KotlinType import java.util.* - class JsIntrinsics(private val irBuiltIns: IrBuiltIns, val context: JsIrBackendContext) { private val externalPackageFragmentSymbol = IrExternalPackageFragmentSymbolImpl(context.internalPackageFragmentDescriptor) @@ -260,7 +259,8 @@ // TODO move CharSequence-related stiff to IntrinsifyCallsLowering val charSequenceClassSymbol = context.symbolTable.referenceClass(context.getClass(FqName("kotlin.CharSequence"))) val charSequenceLengthPropertyGetterSymbol = - charSequenceClassSymbol.owner.declarations.filterIsInstance<IrProperty>().first { it.name.asString() == "length" }.getter!!.symbol + charSequenceClassSymbol.owner.declarations.filterIsInstance<IrProperty>().find { it.name.asString() == "length" }?.getter?.symbol + ?: charSequenceClassSymbol.owner.declarations.filterIsInstance<IrFunction>().single { it.name.asString() == "<get-length>" }.symbol val charSequenceGetFunctionSymbol = charSequenceClassSymbol.owner.declarations.filterIsInstance<IrFunction>().single { it.name.asString() == "get" }.symbol val charSequenceSubSequenceFunctionSymbol = @@ -339,3 +339,4 @@ private fun binOpBool(name: String) = binOp(name, irBuiltIns.bool) private fun binOpInt(name: String) = binOp(name, irBuiltIns.int) } +
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 c3aaba0..350829a 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
@@ -1,5 +1,5 @@ /* - * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2019 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. */ @@ -29,7 +29,10 @@ import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.declarations.impl.IrFileImpl import org.jetbrains.kotlin.ir.descriptors.IrBuiltIns -import org.jetbrains.kotlin.ir.symbols.* +import org.jetbrains.kotlin.ir.symbols.IrClassSymbol +import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol +import org.jetbrains.kotlin.ir.symbols.IrEnumEntrySymbol +import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol import org.jetbrains.kotlin.ir.types.IrDynamicType import org.jetbrains.kotlin.ir.types.IrSimpleType import org.jetbrains.kotlin.ir.types.classifierOrFail @@ -121,6 +124,8 @@ private val COROUTINE_PACKAGE_FQNAME = COROUTINE_PACKAGE_FQNAME_13 private val COROUTINE_INTRINSICS_PACKAGE_FQNAME = COROUTINE_PACKAGE_FQNAME.child(INTRINSICS_PACKAGE_NAME) + private val WORKER_PACKAGE_FQNAME = FqName.fromSegments(listOf("kotlin", "js", "worker")) + // TODO: due to name clash those weird suffix is required, remove it once `MemberNameGenerator` is implemented private val COROUTINE_SUSPEND_OR_RETURN_JS_NAME = "suspendCoroutineUninterceptedOrReturnJS" private val GET_COROUTINE_CONTEXT_NAME = "getCoroutineContext" @@ -277,6 +282,12 @@ val throwableConstructors by lazy { throwableClass.owner.declarations.filterIsInstance<IrConstructor>().map { it.symbol } } val defaultThrowableCtor by lazy { throwableConstructors.single { !it.owner.isPrimary && it.owner.valueParameters.size == 0 } } + // web worker specific stuff + private val workerPackage = module.getPackage(WORKER_PACKAGE_FQNAME) + val postMessage = symbolTable.referenceSimpleFunction(getFunctions(FqName("kotlin.js.worker.postMessage")).single()) + val terminateMessage = symbolTable.referenceSimpleFunction(getFunctions(FqName("kotlin.js.worker.terminateWorkers")).single()) + val workerClass = symbolTable.referenceClass(findClass(workerPackage.memberScope, Name.identifier("WebWorker"))) + private fun referenceOperators(): Map<Name, MutableMap<IrClassifierSymbol, IrSimpleFunctionSymbol>> { val primitiveIrSymbols = irBuiltIns.primitiveIrTypes.map { it.classifierOrFail as IrClassSymbol }
diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/compiler.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/compiler.kt index 3324a7c..fcfac66 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/compiler.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/compiler.kt
@@ -1,5 +1,5 @@ /* - * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2019 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. */ @@ -8,9 +8,13 @@ import com.intellij.openapi.project.Project import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig import org.jetbrains.kotlin.backend.common.phaser.invokeToplevel +import org.jetbrains.kotlin.config.CommonConfigurationKeys import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.ir.backend.js.transformers.irToJs.IrModuleToJsTransformer import org.jetbrains.kotlin.ir.backend.js.utils.JsMainFunctionDetector +import org.jetbrains.kotlin.ir.backend.js.webWorkers.moveWorkersToSeparateFiles +import org.jetbrains.kotlin.ir.backend.js.webWorkers.prepareFilePrefixForWorkers +import org.jetbrains.kotlin.ir.backend.js.webWorkers.prepareFileSuffixForWorkers import org.jetbrains.kotlin.ir.declarations.IrModuleFragment import org.jetbrains.kotlin.ir.util.ExternalDependenciesGenerator import org.jetbrains.kotlin.ir.util.patchDeclarationParents @@ -36,7 +40,8 @@ allDependencies: List<KotlinLibrary>, friendDependencies: List<KotlinLibrary>, mainArguments: List<String>?, - exportedDeclarations: Set<FqName> = emptySet() + exportedDeclarations: Set<FqName> = emptySet(), + supportWebWorkers: Boolean = false ): String { val (moduleFragment, dependencyModules, irBuiltIns, symbolTable, deserializer) = loadIr(project, files, configuration, allDependencies, friendDependencies) @@ -45,7 +50,14 @@ val mainFunction = JsMainFunctionDetector.getMainFunctionOrNull(moduleFragment) - val context = JsIrBackendContext(moduleDescriptor, irBuiltIns, symbolTable, moduleFragment, exportedDeclarations, configuration) + val context = JsIrBackendContext( + moduleDescriptor, + irBuiltIns, + symbolTable, + moduleFragment, + if (supportWebWorkers) exportedDeclarations + FqName("kotlin.js.worker.terminateWorkers") else exportedDeclarations, + configuration + ) // Load declarations referenced during `context` initialization dependencyModules.forEach { @@ -71,9 +83,16 @@ ).generateUnboundSymbolsAsDependencies() moduleFragment.patchDeclarationParents() + val workerFilesWithIndices = moduleFragment.files.flatMap { moveWorkersToSeparateFiles(it, context) } + moduleFragment.files += workerFilesWithIndices.map { it.irFile } + jsPhases.invokeToplevel(phaseConfig, context, moduleFragment) - val jsProgram = - moduleFragment.accept(IrModuleToJsTransformer(context, mainFunction, mainArguments), null) - return jsProgram.toString() + val jsProgram = moduleFragment.accept(IrModuleToJsTransformer(context, mainFunction, mainArguments, workerFilesWithIndices), null) + return if (supportWebWorkers && workerFilesWithIndices.isNotEmpty()) + prepareFilePrefixForWorkers() + jsProgram.toString() + prepareFileSuffixForWorkers( + context.configuration[CommonConfigurationKeys.MODULE_NAME]!!, + workerFilesWithIndices + ) + else jsProgram.toString() } \ No newline at end of file
diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt index 583598e..cc02acf 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/transformers/irToJs/IrModuleToJsTransformer.kt
@@ -1,5 +1,5 @@ /* - * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2019 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. */ @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.descriptors.Visibilities import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext import org.jetbrains.kotlin.ir.backend.js.utils.* +import org.jetbrains.kotlin.ir.backend.js.webWorkers.WorkerFileWithIndex import org.jetbrains.kotlin.ir.declarations.* import org.jetbrains.kotlin.ir.symbols.IrClassSymbol import org.jetbrains.kotlin.ir.util.fqNameWhenAvailable @@ -22,7 +23,8 @@ class IrModuleToJsTransformer( private val backendContext: JsIrBackendContext, private val mainFunction: IrSimpleFunction?, - private val mainArguments: List<String>? + private val mainArguments: List<String>?, + private val workerFilesWithIndices: List<WorkerFileWithIndex> ) : BaseIrElementToJsNodeTransformer<JsNode, Nothing?> { val moduleName = backendContext.configuration[CommonConfigurationKeys.MODULE_NAME]!! @@ -276,6 +278,10 @@ if (fqNameWhenAvailable in backendContext.additionalExportedDeclarations) return true + if (name.asString() in workerFilesWithIndices.map { it.functionName }) { + return true + } + // Hack to support properties val correspondingProperty = when { this is IrField -> correspondingPropertySymbol
diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/webWorkers/webWorkersSupport.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/webWorkers/webWorkersSupport.kt new file mode 100644 index 0000000..9f2f3a3 --- /dev/null +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/webWorkers/webWorkersSupport.kt
@@ -0,0 +1,226 @@ +/* + * Copyright 2010-2019 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.ir.backend.js.webWorkers + +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor +import org.jetbrains.kotlin.descriptors.impl.EmptyPackageFragmentDescriptor +import org.jetbrains.kotlin.descriptors.isTopLevelInPackage +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.SourceManager +import org.jetbrains.kotlin.ir.SourceRangeInfo +import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET +import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext +import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder +import org.jetbrains.kotlin.ir.builders.declarations.addValueParameter +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.impl.IrFileImpl +import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl +import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl +import org.jetbrains.kotlin.ir.expressions.impl.IrGetValueImpl +import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol +import org.jetbrains.kotlin.ir.types.createType +import org.jetbrains.kotlin.ir.util.constructors +import org.jetbrains.kotlin.ir.visitors.* +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.utils.sure + +private data class WorkerInfo(val call: IrCall, val lambda: IrFunction, val index: Int) + +private const val WORKER_FUNCTION_NAME_PREFIX = "__Worker_" + +class WorkerFileWithIndex(val irFile: IrFile, val index: Int) { + val functionName = WORKER_FUNCTION_NAME_PREFIX + index +} + +fun prepareFilePrefixForWorkers(): String = "var wt = require('worker_threads');\n" + +fun prepareFileSuffixForWorkers(moduleName: String, workerFileWithIndices: List<WorkerFileWithIndex>): String { + val res = StringBuffer( + """ +if (!wt.isMainThread) { + switch (wt.workerData.id) {""" + ) + for (workerFileWithIndex in workerFileWithIndices) { + res.append( + """ + case ${workerFileWithIndex.index}: + wt.parentPort.on('message', $moduleName.${workerFileWithIndex.functionName}); + break; +""" + ) + } + res.append( + """ + default: + throw Error("Worker with id: " + id + " not found") + } +} +""" + ) + return res.toString() +} + +fun moveWorkersToSeparateFiles(irFile: IrFile, context: JsIrBackendContext): List<WorkerFileWithIndex> { + val workerCalls = irFile.filterCalls { it.descriptor.isWorker() } + + if (workerCalls.isEmpty()) return emptyList() + + val infos = workerCalls.withIndex().map { (index, call) -> + WorkerInfo( + call, + (call.getValueArgument(0) as? IrFunctionExpression ?: error("worker intrinsic accepts only block, but got $call")).function, + index + ) + } + + val result = infos.map { (_, workerLambda, index) -> + replaceReturnsWithPostMessage(workerLambda, context) + WorkerFileWithIndex(moveToSeparateFile(workerLambda, context, "$WORKER_FUNCTION_NAME_PREFIX$index"), index) + } + + replaceWorkerIntrinsicCalls(infos, irFile, context) + return result +} + +private fun moveToSeparateFile( + workerLambda: IrFunction, + context: JsIrBackendContext, + fileName: String +): IrFile { + val newFile = IrFileImpl( + object : SourceManager.FileEntry { + override val name = fileName + 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 + }, context.workersPackageFragmentDescriptor + ) + val function = JsIrBuilder.buildFunction(fileName, context.irBuiltIns.unitType, newFile).also { + it.body = workerLambda.body + it.copyValueParametersAndUpdateGetValues(workerLambda) + } + newFile.declarations.add(function) + return newFile +} + +fun IrFunction.copyValueParametersAndUpdateGetValues(original: IrFunction) { + for (param in original.valueParameters) { + addValueParameter(param.name.asString(), param.type) + } + body?.transformChildrenVoid(object : IrElementTransformerVoid() { + override fun visitGetValue(expression: IrGetValue): IrExpression { + val index = original.valueParameters.single { it.symbol == expression.symbol.owner }.index + return IrGetValueImpl(expression.startOffset, expression.endOffset, valueParameters[index].symbol) + } + }) +} + +private fun replaceReturnsWithPostMessage(workerLambda: IrFunction, context: JsIrBackendContext) { + workerLambda.body.sure { "worker lambda $workerLambda shall have body" }.transformReturns { + IrCallImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, context.irBuiltIns.unitType, context.postMessage).apply { + putValueArgument(0, it.value) + } + } +} + +private fun replaceWorkerIntrinsicCalls( + infos: List<WorkerInfo>, + irFile: IrFile, + context: JsIrBackendContext +) { + irFile.transformCalls { call -> + val info = infos.find { it.call == call } + if (info != null) { + val constructor = context.workerClass.constructors.first() + IrConstructorCallImpl.fromSymbolDescriptor( + call.startOffset, call.endOffset, context.workerClass.createType(false, emptyList()), + constructor + ).apply { + putValueArgument( + 0, newCallWithUndefinedOffsets( + symbol = context.jsCodeSymbol, + arguments = listOf(context.string("new wt.Worker(__filename, { workerData: { id: ${info.index} } })")) + ) + ) + putValueArgument( + 1, newCallWithUndefinedOffsets( + symbol = context.jsCodeSymbol, + arguments = listOf(context.string("{}")) + ) + ) + } + } else call + } +} + +fun IrElement.filterCalls(predicate: (IrCall) -> Boolean): List<IrCall> { + val res = arrayListOf<IrCall>() + acceptVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) { + element.acceptChildrenVoid(this) + } + + override fun visitCall(expression: IrCall) { + if (predicate(expression)) { + res += expression + } + super.visitCall(expression) + } + }) + return res +} + +fun IrElement.transformCalls(transformer: (IrCall) -> IrExpression) { + transformChildrenVoid(object : IrElementTransformerVoid() { + override fun visitCall(expression: IrCall): IrExpression { + val visited = super.visitCall(expression) as IrCall + return transformer(visited) + } + }) +} + +fun IrElement.transformReturns(transformer: (IrReturn) -> IrExpression) { + transformChildrenVoid(object : IrElementTransformerVoid() { + override fun visitReturn(expression: IrReturn): IrExpression { + val visited = super.visitReturn(expression) as IrReturn + return transformer(visited) + } + }) +} + +fun newCallWithUndefinedOffsets( + symbol: IrFunctionSymbol, + arguments: List<IrExpression?> +) = IrCallImpl(UNDEFINED_OFFSET, UNDEFINED_OFFSET, symbol.owner.returnType, symbol).apply { + for ((index, expression) in arguments.withIndex()) { + putValueArgument(index, expression) + } +} + +private val JsIrBackendContext.workersPackageFragmentDescriptor: PackageFragmentDescriptor + get() = EmptyPackageFragmentDescriptor(builtIns.builtInsModule, FqName("")) + +private fun FunctionDescriptor.isWorker(): Boolean = isTopLevelInPackage("worker", "kotlin.js.worker") + +private fun JsIrBackendContext.string(s: String) = JsIrBuilder.buildString(irBuiltIns.stringType, s) +private val JsIrBackendContext.jsCodeSymbol + get() = symbolTable.referenceSimpleFunction(getJsInternalFunction("js")) +
diff --git a/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/declarations/IrDeclarationOrigin.kt b/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/declarations/IrDeclarationOrigin.kt index e0b1d09..5d9a1df 100644 --- a/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/declarations/IrDeclarationOrigin.kt +++ b/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/declarations/IrDeclarationOrigin.kt
@@ -1,17 +1,6 @@ /* - * Copyright 2010-2016 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Copyright 2010-2019 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.ir.declarations @@ -42,6 +31,7 @@ object IR_BUILTINS_STUB : IrDeclarationOriginImpl("IR_BUILTINS_STUB") object BRIDGE : IrDeclarationOriginImpl("BRIDGE", isSynthetic = true) object BRIDGE_SPECIAL: IrDeclarationOriginImpl("BRIDGE_SPECIAL") + object WORKER: IrDeclarationOriginImpl("WORKER") object FIELD_FOR_ENUM_ENTRY : IrDeclarationOriginImpl("FIELD_FOR_ENUM_ENTRY") object FIELD_FOR_ENUM_VALUES : IrDeclarationOriginImpl("FIELD_FOR_ENUM_VALUES")
diff --git a/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/util/PatchDeclarationParents.kt b/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/util/PatchDeclarationParents.kt index 2e2b497..489048e 100644 --- a/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/util/PatchDeclarationParents.kt +++ b/compiler/ir/ir.tree/src/org/jetbrains/kotlin/ir/util/PatchDeclarationParents.kt
@@ -67,6 +67,9 @@ } private fun patchParent(declaration: IrDeclaration) { + if (declarationParentsStack.isEmpty()) { + declaration.parent = declaration.parent + } declaration.parent = declarationParentsStack.peekFirst() } } \ No newline at end of file
diff --git a/compiler/ir/serialization.js/build.gradle.kts b/compiler/ir/serialization.js/build.gradle.kts index dcaa8d4..c3a8739 100644 --- a/compiler/ir/serialization.js/build.gradle.kts +++ b/compiler/ir/serialization.js/build.gradle.kts
@@ -125,6 +125,7 @@ "libraries/stdlib/js/src/kotlin/regexp.kt", "libraries/stdlib/js/src/kotlin/sequence.kt", "libraries/stdlib/js/src/kotlin/text/**", + "libraries/stdlib/js/src/kotlin/worker/**", "libraries/stdlib/src/kotlin/collections/**", "libraries/stdlib/src/kotlin/experimental/bitwiseOperations.kt", "libraries/stdlib/src/kotlin/properties/Delegates.kt",
diff --git a/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt b/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt index db3206d..a317c30 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt
@@ -42,6 +42,10 @@ testClass<AbstractJsLineNumberTest> { model("lineNumbers/", pattern = "^([^_](.+))\\.kt$", targetBackend = TargetBackend.JS) } + + testClass<AbstractIrJsWebWorkersTests> { + model("webworkers/", pattern = "^([^_](.+))\\.kt$", targetBackend = TargetBackend.JS_IR) + } } testGroup("js/js.tests/test", "compiler/testData", testRunnerMethodName = "runTest0") {
diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicIrBoxTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicIrBoxTest.kt index 84eb1d9..32b51bb 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicIrBoxTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/BasicIrBoxTest.kt
@@ -1,5 +1,5 @@ /* - * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2019 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. */ @@ -7,10 +7,10 @@ import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig import org.jetbrains.kotlin.backend.common.phaser.toPhaseMap -import org.jetbrains.kotlin.ir.backend.js.loadKlib import org.jetbrains.kotlin.ir.backend.js.compile import org.jetbrains.kotlin.ir.backend.js.generateKLib import org.jetbrains.kotlin.ir.backend.js.jsPhases +import org.jetbrains.kotlin.ir.backend.js.loadKlib import org.jetbrains.kotlin.js.config.JSConfigurationKeys import org.jetbrains.kotlin.js.config.JsConfig import org.jetbrains.kotlin.js.facade.MainCallParameters @@ -28,7 +28,8 @@ testGroupOutputDirPrefix: String, pathToRootOutputDir: String = BasicBoxTest.TEST_DATA_DIR_PATH, generateSourceMap: Boolean = false, - generateNodeJsRunner: Boolean = false + generateNodeJsRunner: Boolean = false, + private val supportWebWorkers: Boolean = false ) : BasicBoxTest( pathToTestDir, testGroupOutputDirPrefix, @@ -110,7 +111,8 @@ allDependencies = allDependencies, friendDependencies = emptyList(), mainArguments = mainCallParameters.run { if (shouldBeGenerated()) arguments() else null }, - exportedDeclarations = setOf(FqName.fromSegments(listOfNotNull(testPackage, testFunction))) + exportedDeclarations = setOf(FqName.fromSegments(listOfNotNull(testPackage, testFunction))), + supportWebWorkers = supportWebWorkers ) val wrappedCode = wrapWithModuleEmulationMarkers(jsCode, moduleId = config.moduleId, moduleKind = config.moduleKind)
diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/JsTestChecker.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/JsTestChecker.kt index 53836dd..4d19bc3 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/JsTestChecker.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/JsTestChecker.kt
@@ -1,5 +1,5 @@ /* - * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2019 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. */
diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/IrJsWebWorkersTestsGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/IrJsWebWorkersTestsGenerated.java new file mode 100644 index 0000000..589443b --- /dev/null +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/IrJsWebWorkersTestsGenerated.java
@@ -0,0 +1,41 @@ +/* + * Copyright 2010-2019 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.js.test.ir.semantics; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TargetBackend; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("js/js.translator/testData/webworkers") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class IrJsWebWorkersTestsGenerated extends AbstractIrJsWebWorkersTests { + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest0(this::doTest, TargetBackend.JS_IR, testDataFilePath); + } + + public void testAllFilesPresentInWebworkers() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("js/js.translator/testData/webworkers"), Pattern.compile("^([^_](.+))\\.kt$"), TargetBackend.JS_IR, true); + } + + @TestMetadata("empty.kt") + public void testEmpty() throws Exception { + runTest("js/js.translator/testData/webworkers/empty.kt"); + } + + @TestMetadata("postMessage.kt") + public void testPostMessage() throws Exception { + runTest("js/js.translator/testData/webworkers/postMessage.kt"); + } +}
diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/abstractClassesForGeneratedIrTests.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/abstractClassesForGeneratedIrTests.kt index 1f69a20..d23cdec 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/abstractClassesForGeneratedIrTests.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/abstractClassesForGeneratedIrTests.kt
@@ -38,4 +38,6 @@ abstract class AbstractIrInlineSuspendTests : BorrowedIrInlineTest("suspend/") -abstract class AbstractIrJsInlineContractsTests : BorrowedIrInlineTest("contracts/") \ No newline at end of file +abstract class AbstractIrJsInlineContractsTests : BorrowedIrInlineTest("contracts/") + +abstract class AbstractIrJsWebWorkersTests : BasicIrBoxTest(TEST_DATA_DIR_PATH + "webworkers/", "webworkers/", supportWebWorkers = true) \ No newline at end of file
diff --git a/js/js.translator/testData/package.json b/js/js.translator/testData/package.json index 6642c07..2b37b1a9d 100644 --- a/js/js.translator/testData/package.json +++ b/js/js.translator/testData/package.json
@@ -1,14 +1,16 @@ { - "dependencies" : { + "dependencies": { + "node": "^12.7.0", "require-from-string": "1.2.0" }, - "devDependencies" : { + "devDependencies": { "mocha": "3.2.0", "mocha-teamcity-reporter": "1.1.1" }, "scripts": { "runOnTeamcity": "mocha --reporter mocha-teamcity-reporter", "test": "mocha", - "runIrTestInNode" : "node $NODE_DEBUG_OPTION runIrTestInNode.js" + "runIrTestInNode": "node $NODE_DEBUG_OPTION runIrTestInNode.js", + "runWebWorkerInNode": "node $NODE_DEBUG_OPTION runWebWorkerInNode.js" } }
diff --git a/js/js.translator/testData/runWebWorkerInNode.js b/js/js.translator/testData/runWebWorkerInNode.js new file mode 100644 index 0000000..e376069 --- /dev/null +++ b/js/js.translator/testData/runWebWorkerInNode.js
@@ -0,0 +1,84 @@ +// Note right now it works only for IR tests. +// With IDEA 2018.3 or later you can just activate required js and run "run IR test in node.js" configuration. + +// Add to this array your path to test files or provide it as argument. +var anotherFiles = [""]; + +var vm = require('vm'); +var fs = require('fs'); +var wt = require('worker_threads'); + +// Change working dir to root of project +var testDataPathFromRoot = "js/js.translator/testData"; +var cwd = process.cwd(); +if (cwd.endsWith(testDataPathFromRoot)) { + process.chdir(cwd.substr(0, cwd.length - testDataPathFromRoot.length)); +} + +var filesFromArgs = process.argv.slice(2); + +function toAbsolutePath(path) { + if (fs.existsSync(path) && fs.statSync(path).isFile()) { + return fs.realpathSync(path) + } + + return ""; +} + +// TODO autodetect common js files and other js files +// Filter out all except existing js files and transform all paths to absolute +var files = [].concat(filesFromArgs, anotherFiles) + .map(toAbsolutePath) + .filter(function(path) { + return path.endsWith(".js") + }); + +// Find runtime path + +var runtimeHeader = "// RUNTIME: "; +var runtimeFiles = []; +files.forEach(function (path) { + var code = fs.readFileSync(path, 'utf8'); + var firstLine = code.substr(0, code.indexOf("\n")); + if (firstLine.startsWith(runtimeHeader)) { + runtimeFiles = JSON.parse(firstLine.slice(runtimeHeader.length)) + .map(toAbsolutePath); + } +}); + +var allFiles = [].concat(runtimeFiles, files); + +// Evaluate files and run box function + +var sandbox = { + require: require, + __filename: filesFromArgs[0] +}; + +vm.createContext(sandbox); + +allFiles.forEach(function(path) { + var code = fs.readFileSync(path, 'utf8'); + vm.runInContext(code, sandbox, { + filename: path + }) +}); + +function sleep(time) { + var start = Date.now(); + while (Date.now() < start + time) { + } +} + +function runTest() { + vm.runInContext("JS_TESTS.box()", sandbox).then(function (result) { + console.log(result); + try { + vm.runInContext("JS_TESTS.terminateWorkers()", sandbox); + } catch (e) { + console.log(e); + } + }, function (e) { console.log(e) }); +} + +runTest();
diff --git a/js/js.translator/testData/webworkers/empty.kt b/js/js.translator/testData/webworkers/empty.kt new file mode 100644 index 0000000..9409cb9 --- /dev/null +++ b/js/js.translator/testData/webworkers/empty.kt
@@ -0,0 +1,11 @@ +// KJS_WITH_FULL_RUNTIME + +import kotlin.js.worker.* +import kotlin.js.Promise + +fun box(): Promise<String> { + worker<Unit> { + // Just a smoke test: no exception should be thrown + } + return Promise<String> { resolve, _ -> resolve("OK") } +}
diff --git a/js/js.translator/testData/webworkers/postMessage.kt b/js/js.translator/testData/webworkers/postMessage.kt new file mode 100644 index 0000000..478f791 --- /dev/null +++ b/js/js.translator/testData/webworkers/postMessage.kt
@@ -0,0 +1,10 @@ +// KJS_WITH_FULL_RUNTIME + +import kotlin.js.worker.* +import kotlin.js.Promise + +fun box(): Promise<String> { + return worker { + "OK" + }.waitForReply() +}
diff --git a/libraries/stdlib/js/src/kotlin/worker/worker.kt b/libraries/stdlib/js/src/kotlin/worker/worker.kt new file mode 100644 index 0000000..51217fc --- /dev/null +++ b/libraries/stdlib/js/src/kotlin/worker/worker.kt
@@ -0,0 +1,44 @@ +/* + * Copyright 2010-2019 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.worker + +import kotlin.js.Promise + +class WebWorker<T>(private val worker: dynamic, initialMessage: dynamic) { + init { + aliveWorkers.add(WorkerWrapper(worker)) + worker.postMessage(initialMessage) + } + + fun waitForReply(): Promise<T> { + return Promise { resolve, reject -> worker.on("message") { e -> resolve(e) }} + } + + companion object { + private val aliveWorkers = arrayListOf<WorkerWrapper>() + fun terminateWorkers() { + for (worker in aliveWorkers) { + worker.worker.terminate() + } + } + } +} + +// TODO: this shall be internal and exported from module +fun terminateWorkers() { + WebWorker.terminateWorkers() +} + +private class WorkerWrapper(val worker: dynamic) + + +fun <T> worker(c: () -> T): WebWorker<T> { + throw UnsupportedOperationException("Implemented as intrinsic") +} + +fun postMessage(message: dynamic) { + js("wt.parentPort.postMessage(message)") +}