diff --git a/libraries/stdlib/coroutines/js/src/kotlin/coroutines/SafeContinuationJs.kt b/libraries/stdlib/coroutines/js/src/kotlin/coroutines/SafeContinuationJs.kt new file mode 100644 index 00000000000..15fdbd7f4d1 --- /dev/null +++ b/libraries/stdlib/coroutines/js/src/kotlin/coroutines/SafeContinuationJs.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2010-2018 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. + */ + +package kotlin.coroutines + +import kotlin.coroutines.intrinsics.CoroutineSingletons.* + +@PublishedApi +@SinceKotlin("1.3") +internal actual class SafeContinuation +internal actual constructor( + private val delegate: Continuation, + initialResult: Any? +) : Continuation { + @PublishedApi + internal actual constructor(delegate: Continuation) : this(delegate, UNDECIDED) + + public actual override val context: CoroutineContext + get() = delegate.context + + private var result: Any? = initialResult + + public actual override fun resumeWith(result: SuccessOrFailure) { + val cur = this.result + when { + cur === UNDECIDED -> { + this.result = result + } + cur === COROUTINE_SUSPENDED -> { + this.result = RESUMED + delegate.resumeWith(result) + } + else -> throw IllegalStateException("Already resumed") + } + } + + @PublishedApi + internal actual fun getOrThrow(): Any? { + if (result === UNDECIDED) { + result = COROUTINE_SUSPENDED + return COROUTINE_SUSPENDED + } + val result = this.result + return when { + result === RESUMED -> COROUTINE_SUSPENDED // already called continuation, indicate COROUTINE_SUSPENDED upstream + result is SuccessOrFailure.Failure -> throw result.exception + else -> result // either COROUTINE_SUSPENDED or data + } + } +} diff --git a/libraries/stdlib/coroutines/js/src/kotlin/coroutines/intrinsics/IntrinsicsJs.kt b/libraries/stdlib/coroutines/js/src/kotlin/coroutines/intrinsics/IntrinsicsJs.kt new file mode 100644 index 00000000000..3d5911c6445 --- /dev/null +++ b/libraries/stdlib/coroutines/js/src/kotlin/coroutines/intrinsics/IntrinsicsJs.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2010-2018 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. + */ + +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER", "UNCHECKED_CAST") + +package kotlin.coroutines.intrinsics + +import kotlin.coroutines.* +import kotlin.internal.InlineOnly + +/** + * Starts unintercepted coroutine without receiver and with result type [T] and executes it until its first suspension. + * Returns the result of the coroutine or throws its exception if it does not suspend or [COROUTINE_SUSPENDED] if it suspends. + * In the later case, the [completion] continuation is invoked when coroutine completes with result or exception. + * + * The coroutine is started directly in the invoker's thread without going through the [ContinuationInterceptor] that might + * be present in the completion's [CoroutineContext]. It is invoker's responsibility to ensure that the proper invocation + * context is established. + * + * This function is designed to be used from inside of [suspendCoroutineUninterceptedOrReturn] to resume the execution of suspended + * coroutine using a reference to the suspending function. + */ +@SinceKotlin("1.3") +@InlineOnly +public actual inline fun (suspend () -> T).startCoroutineUninterceptedOrReturn( + completion: Continuation +): Any? = this.asDynamic()(completion, false) + +/** + * Starts unintercepted coroutine with receiver type [R] and result type [T] and executes it until its first suspension. + * Returns the result of the coroutine or throws its exception if it does not suspend or [COROUTINE_SUSPENDED] if it suspends. + * In the later case, the [completion] continuation is invoked when coroutine completes with result or exception. + * + * The coroutine is started directly in the invoker's thread without going through the [ContinuationInterceptor] that might + * be present in the completion's [CoroutineContext]. It is invoker's responsibility to ensure that the proper invocation + * context is established. + * + * This function is designed to be used from inside of [suspendCoroutineUninterceptedOrReturn] to resume the execution of suspended + * coroutine using a reference to the suspending function. + */ +@SinceKotlin("1.3") +@InlineOnly +public actual inline fun (suspend R.() -> T).startCoroutineUninterceptedOrReturn( + receiver: R, + completion: Continuation +): Any? = this.asDynamic()(receiver, completion, false) + + +/** + * Creates unintercepted coroutine without receiver and with result type [T]. + * This function creates a new, fresh instance of suspendable computation every time it is invoked. + * + * To start executing the created coroutine, invoke `resume(Unit)` on the returned [Continuation] instance. + * The [completion] continuation is invoked when coroutine completes with result or exception. + * + * This function returns unintercepted continuation. + * Invocation of `resume(Unit)` starts coroutine directly in the invoker's thread without going through the + * [ContinuationInterceptor] that might be present in the completion's [CoroutineContext]. + * It is invoker's responsibility to ensure that the proper invocation context is established. + * [Continuation.intercepted] can be used to acquire the intercepted continuation. + * + * Repeated invocation of any resume function on the resulting continuation corrupts the + * state machine of the coroutine and may result in arbitrary behaviour or exception. + */ +@SinceKotlin("1.3") +public actual fun (suspend () -> T).createCoroutineUnintercepted( + completion: Continuation +): Continuation = + // Kotlin/JS suspend lambdas have an extra parameter `suspended` + if (this.asDynamic().length == 2) { + // When `suspended` is true the continuation is created, but not executed + this.asDynamic()(completion, true) + } else { + createCoroutineFromSuspendFunction(completion) { + this.asDynamic()(completion) + } + } + +/** + * Creates unintercepted coroutine with receiver type [R] and result type [T]. + * This function creates a new, fresh instance of suspendable computation every time it is invoked. + * + * To start executing the created coroutine, invoke `resume(Unit)` on the returned [Continuation] instance. + * The [completion] continuation is invoked when coroutine completes with result or exception. + * + * This function returns unintercepted continuation. + * Invocation of `resume(Unit)` starts coroutine directly in the invoker's thread without going through the + * [ContinuationInterceptor] that might be present in the completion's [CoroutineContext]. + * It is invoker's responsibility to ensure that the proper invocation context is established. + * [Continuation.intercepted] can be used to acquire the intercepted continuation. + * + * Repeated invocation of any resume function on the resulting continuation corrupts the + * state machine of the coroutine and may result in arbitrary behaviour or exception. + */ +@SinceKotlin("1.3") +public actual fun (suspend R.() -> T).createCoroutineUnintercepted( + receiver: R, + completion: Continuation +): Continuation = + // Kotlin/JS suspend lambdas have an extra parameter `suspended` + if (this.asDynamic().length == 3) { + // When `suspended` is true the continuation is created, but not executed + this.asDynamic()(receiver, completion, true) + } else { + createCoroutineFromSuspendFunction(completion) { + this.asDynamic()(receiver, completion) + } + } + +/** + * Intercepts this continuation with [ContinuationInterceptor]. + */ +@SinceKotlin("1.3") +public actual fun Continuation.intercepted(): Continuation = + (this as? CoroutineImpl)?.intercepted() ?: this + + +private inline fun createCoroutineFromSuspendFunction( + completion: Continuation, + crossinline block: () -> Any? +): Continuation { + return object : CoroutineImpl(completion as Continuation) { + override fun doResume(): Any? { + exception?.let { throw it } + return block() + } + } +} diff --git a/libraries/stdlib/coroutines/js/src/kotlin/coroutines/js/internal/CoroutineImpl.kt b/libraries/stdlib/coroutines/js/src/kotlin/coroutines/js/internal/CoroutineImpl.kt new file mode 100644 index 00000000000..b335a1289fe --- /dev/null +++ b/libraries/stdlib/coroutines/js/src/kotlin/coroutines/js/internal/CoroutineImpl.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2010-2018 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. + */ + +package kotlin.coroutines + +import kotlin.coroutines.intrinsics.CoroutineSingletons + +@SinceKotlin("1.3") +@JsName("CoroutineImpl") +internal abstract class CoroutineImpl(private val resultContinuation: Continuation) : Continuation { + protected var state = 0 + protected var exceptionState = 0 + protected var result: Any? = null + protected var exception: Throwable? = null + protected var finallyPath: Array? = null + + public override val context: CoroutineContext = resultContinuation.context + + private var intercepted_: Continuation? = null + + public fun intercepted(): Continuation = + intercepted_ + ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this) + .also { intercepted_ = it } + + override fun resumeWith(result: SuccessOrFailure) { + var current = this + var currentResult: Any? = result.getOrNull() + var currentException: Throwable? = result.exceptionOrNull() + + // This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume + while (true) { + with(current) { + val completion = resultContinuation + + // Set result and exception fields in the current continuation + if (currentException == null) { + this.result = currentResult + } else { + state = exceptionState + exception = currentException + } + + try { + val outcome = doResume() + if (outcome === CoroutineSingletons.COROUTINE_SUSPENDED) return + currentResult = outcome + currentException = null + } catch (exception: dynamic) { // Catch all exceptions + currentResult = null + currentException = exception.unsafeCast() + } + + releaseIntercepted() // this state machine instance is terminating + + if (completion is CoroutineImpl) { + // unrolling recursion via loop + current = completion + } else { + // top-level completion reached -- invoke and return + currentException?.let { + completion.resumeWithException(it) + } ?: completion.resume(currentResult) + return + } + } + } + } + + private fun releaseIntercepted() { + val intercepted = intercepted_ + if (intercepted != null && intercepted !== this) { + context[ContinuationInterceptor]!!.releaseInterceptedContinuation(intercepted) + } + this.intercepted_ = CompletedContinuation // just in case + } + + protected abstract fun doResume(): Any? +} + +internal object CompletedContinuation : Continuation { + override val context: CoroutineContext + get() = error("This continuation is already complete") + + override fun resumeWith(result: SuccessOrFailure) { + error("This continuation is already complete") + } + + override fun toString(): String = "This continuation is already complete" +} \ No newline at end of file diff --git a/libraries/stdlib/coroutines/src/kotlin/SuccessOrFailure.kt b/libraries/stdlib/coroutines/src/kotlin/SuccessOrFailure.kt index 4a2126c2393..85876e096fd 100644 --- a/libraries/stdlib/coroutines/src/kotlin/SuccessOrFailure.kt +++ b/libraries/stdlib/coroutines/src/kotlin/SuccessOrFailure.kt @@ -10,7 +10,8 @@ "NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS", "UNSUPPORTED_FEATURE", "INVISIBLE_REFERENCE", - "INVISIBLE_MEMBER" + "INVISIBLE_MEMBER", + "CANNOT_OVERRIDE_INVISIBLE_MEMBER" ) package kotlin diff --git a/libraries/stdlib/js/build.gradle b/libraries/stdlib/js/build.gradle index 5c8eca18f54..857e5c602a8 100644 --- a/libraries/stdlib/js/build.gradle +++ b/libraries/stdlib/js/build.gradle @@ -15,10 +15,12 @@ def builtinsSrcDir = "${buildDir}/builtin-sources" def builtinsSrcDir2 = "${buildDir}/builtin-sources-for-builtins" def commonSrcDir = "${projectDir}/../src/kotlin" def commonSrcDir2 = "${projectDir}/../common/src" +def coroutinesJsSrcDir = "${projectDir}/../coroutines/js/src" def builtinsDir = "${rootDir}/core/builtins" def experimentalSrcDir = "${rootDir}/libraries/stdlib/experimental" def experimentalJsModuleName = 'kotlin-experimental' +def coroutinesJsModuleName = 'kotlin-stdlib-coroutines' def jsSrcDir = "src" def jsTestSrcDir = "test" def jsSrcJsDir = "${jsSrcDir}/js" @@ -59,6 +61,12 @@ sourceSets { srcDir jsTestSrcDir } } + + coroutines { + kotlin { + srcDir coroutinesJsSrcDir + } + } } configurations { @@ -70,6 +78,7 @@ dependencies { commonSources project(path: ":kotlin-stdlib-common", configuration: "sources") testCompile project(':kotlin-test:kotlin-test-js') merger project(":tools:kotlin-stdlib-js-merger") + coroutinesCompile project.files { sourceSets.main.output.files }.builtBy(compileKotlin2Js) } task prepareComparableSource(type: Copy) { @@ -156,6 +165,16 @@ compileExperimentalKotlin2Js { } } +compileCoroutinesKotlin2Js { + kotlinOptions { + languageVersion = "1.3" + apiVersion = "1.3" + outputFile = "${buildDir}/classes/coroutines/kotlin.js" + sourceMap = true + sourceMapPrefix = "./" + } +} + compileTestKotlin2Js { kotlinOptions { moduleKind = "umd" @@ -163,10 +182,11 @@ compileTestKotlin2Js { } task compileJs(type: JavaExec) { - dependsOn compileBuiltinsKotlin2Js, compileKotlin2Js, compileExperimentalKotlin2Js + dependsOn compileBuiltinsKotlin2Js, compileKotlin2Js, compileExperimentalKotlin2Js, compileCoroutinesKotlin2Js inputs.files(compileBuiltinsKotlin2Js.outputs.files) inputs.files(compileKotlin2Js.outputs.files) inputs.files(compileExperimentalKotlin2Js.outputs.files) + inputs.files(compileCoroutinesKotlin2Js.outputs.files) inputs.dir(jsSrcDir) outputs.file(jsOutputFile) outputs.file("${jsOutputFile}.map") @@ -179,7 +199,8 @@ task compileJs(type: JavaExec) { doFirst { args = [jsOutputFile, rootDir, "$jsSrcDir/wrapper.js"] + inputFiles.collect { it.path }.sort() + (compileBuiltinsKotlin2Js.outputs.files.collect { it.path }.sort() + - compileKotlin2Js.outputs.files.collect { it.path }.sort() /* + + compileKotlin2Js.outputs.files.collect { it.path }.sort() + + compileCoroutinesKotlin2Js.outputs.files.collect { it.path }.sort() /* + compileExperimentalKotlin2Js.outputs.files.collect { it.path }.sort() */).findAll { it.endsWith(".js") && !it.endsWith(".meta.js") } @@ -226,7 +247,8 @@ task compileJs(type: JavaExec) { sourceMapFile.text = groovy.json.JsonOutput.toJson(sourceMap) - file(jsOutputMetaFile).text = file(compileKotlin2Js.outputFile.path.replaceAll('\\.js$', '.meta.js')).text /* + + file(jsOutputMetaFile).text = file(compileKotlin2Js.outputFile.path.replaceAll('\\.js$', '.meta.js')).text + + file(compileCoroutinesKotlin2Js.outputFile.path.replaceAll('\\.js$', '.meta.js')).text /* + file(compileExperimentalKotlin2Js.outputFile.path.replaceAll('\\.js$', '.meta.js')).text .replaceFirst(experimentalJsModuleName, 'kotlin') */ } @@ -239,7 +261,7 @@ jar { enabled false } -task mergedJar(type: Jar, dependsOn: classes) { +task mergedJar(type: Jar, dependsOn: compileJs) { classifier = null manifestAttributes(manifest, project, 'Main') @@ -257,10 +279,13 @@ task mergedJar(type: Jar, dependsOn: classes) { from "${jsOutputFile}.map" from sourceSets.main.output from sourceSets.experimental.output + from("${buildDir}/classes/coroutines/kotlin") { + into coroutinesJsModuleName + } exclude "${experimentalJsModuleName}.*" } -task sourcesJar(type: Jar, dependsOn: classes) { +task sourcesJar(type: Jar, dependsOn: compileJs) { classifier = 'sources' includeEmptyDirs false from(sourceSets.builtins.allSource) { @@ -278,6 +303,9 @@ task sourcesJar(type: Jar, dependsOn: classes) { from(sourceSets.experimental.allSource) { into 'kotlin' } + from(sourceSets.coroutines.allSource) { + into 'kotlin' + } } task distSourcesJar(type: Jar) { @@ -293,6 +321,10 @@ task distSourcesJar(type: Jar) { exclude 'META-INF/*' into 'common' } + + from(project(":kotlin-stdlib-common").sourceSets.coroutines.allSource) { + into 'kotlin' + } }