From 8463380f4659a23f29b3641ae6690f63d1e20ddf Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sat, 27 Jun 2020 19:44:51 +0200 Subject: [PATCH 01/49] roots of suspension control --- .../continuations/DelimitedContinuation.kt | 178 ++++++++++++++++++ .../kotlin/arrow/core/MonadContinuation.kt | 3 +- 2 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/DelimitedContinuation.kt diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/DelimitedContinuation.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/DelimitedContinuation.kt new file mode 100644 index 000000000..1f2858b2b --- /dev/null +++ b/arrow-core-data/src/main/kotlin/arrow/continuations/DelimitedContinuation.kt @@ -0,0 +1,178 @@ +package arrow.continuations + +import arrow.core.ArrowCoreInternalException +import arrow.core.ShortCircuit +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import kotlin.coroutines.jvm.internal.* +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.createCoroutine +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.startCoroutine +import kotlin.coroutines.suspendCoroutine +import kotlin.experimental.ExperimentalTypeInference + + +internal const val UNDECIDED = 0 +internal const val SUSPENDED = 1 + +private object NoOpContinuation : Continuation { + override val context: CoroutineContext = EmptyCoroutineContext + + override fun resumeWith(result: Result) { + // Nothing + } +} + +@Suppress( + "CANNOT_OVERRIDE_INVISIBLE_MEMBER", + "INVISIBLE_MEMBER", + "INVISIBLE_REFERENCE", + "UNCHECKED_CAST", + "EXPOSED_SUPER_CLASS" +) +abstract class DelimitedContinuation : ContinuationImpl(NoOpContinuation, EmptyCoroutineContext) { + + abstract val parent: Continuation + + fun ShortCircuit.recover(): A = + throw this + + /** + * State is either + * 0 - UNDECIDED + * 1 - SUSPENDED + * Any? (3) `resumeWith` always stores it upon UNDECIDED, and `getResult` can atomically get it. + */ + private val _decision = atomic(UNDECIDED) + + override val context: CoroutineContext = EmptyCoroutineContext + + fun resumeWithX(result: Result) { + _decision.loop { decision -> + when (decision) { + UNDECIDED -> { + val r: Any? = when { + result.isFailure -> { + val e = result.exceptionOrNull() + if (e is ShortCircuit) e.recover() else null + } + result.isSuccess -> result.getOrNull() + else -> throw ArrowCoreInternalException + } + + when { + r == null -> { + parent.resumeWithException(result.exceptionOrNull()!!) + return + } + _decision.compareAndSet(UNDECIDED, r) -> return + else -> Unit // loop again + } + } + else -> { // If not `UNDECIDED` then we need to pass result to `parent` + val res: Result = result.fold({ Result.success(it) }, { t -> + if (t is ShortCircuit) Result.success(t.recover()) + else Result.failure(t) + }) + parent.resumeWith(res) + return + } + } + } + } + + @PublishedApi // return the result + internal fun getResult(): Any? = + _decision.loop { decision -> + when (decision) { + UNDECIDED -> if (this._decision.compareAndSet(UNDECIDED, SUSPENDED)) return COROUTINE_SUSPENDED + else -> return decision + } + } + + fun > C.start(f: suspend C.() -> A): Any? = + try { + f.startCoroutineUninterceptedOrReturn(this, this)?.let { + if (it == COROUTINE_SUSPENDED) getResult() + else it + } + } catch (e: Throwable) { + if (e is ShortCircuit) e.recover() + else throw e + } + + override fun invokeSuspend(result: Result): Any? = + resumeWithX(result).also { + println("invokeSuspend: $this $result") + } + + + // Escalate visibility to manually release intercepted continuation + override fun releaseIntercepted() { + super.releaseIntercepted().also { + println("releaseIntercepted $this") + } + } + + override val callerFrame: CoroutineStackFrame? + get() = super.callerFrame.also { + println("callerFrame $it") + } + + override fun create(value: Any?, completion: Continuation<*>): Continuation { + return super.create(value, completion).also { + println("create: $value completion: $completion , created: $it") + } + } + + override fun create(completion: Continuation<*>): Continuation { + return super.create(completion).also { + println("create: $completion , created: $it") + } + } + + override fun getStackTraceElement(): StackTraceElement? { + return super.getStackTraceElement().also { + println("getStackTraceElement: $it") + } + } + +} + +class NonDeterministicContinuation : DelimitedContinuation() { + override val parent: Continuation = this +} + +operator fun List.invoke(): A = + iterator().next() + +@UseExperimental(ExperimentalTypeInference::class) +@BuilderInference +suspend fun > list(f: suspend DelimitedContinuation.() -> A): B = + suspendCoroutineUninterceptedOrReturn { c -> + val ncont = NonDeterministicContinuation() + f.startCoroutine(ncont, ncont) + when (val result = ncont.getResult()) { + COROUTINE_SUSPENDED -> COROUTINE_SUSPENDED + else -> c.resumeWith(result as Result) + } + } + +suspend fun main() { + println("1. suspend main") + val result : List = + list { + println("2. Before list bind") + val a = listOf(1, 2, 3)() + println("*. After list bind") + val result = a + 1 + result + } + println(result) +} diff --git a/arrow-core-data/src/main/kotlin/arrow/core/MonadContinuation.kt b/arrow-core-data/src/main/kotlin/arrow/core/MonadContinuation.kt index ed16db42e..607ebcb61 100644 --- a/arrow-core-data/src/main/kotlin/arrow/core/MonadContinuation.kt +++ b/arrow-core-data/src/main/kotlin/arrow/core/MonadContinuation.kt @@ -11,8 +11,7 @@ import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn @RestrictsSuspension interface EagerBind : BindSyntax -@PublishedApi -internal class ShortCircuit(val value: Any?) : RuntimeException(null, null) { +class ShortCircuit(val value: Any?) : RuntimeException(null, null) { override fun fillInStackTrace(): Throwable = this override fun toString(): String = "ShortCircuit($value)" } From eba7b60524975edf878224cc4244bdaa528984b0 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Wed, 1 Jul 2020 14:45:44 +0200 Subject: [PATCH 02/49] Computation builders first draft --- .../main/kotlin/arrow/continuations/Cont.kt | 106 +++++++++++ .../continuations/DelimitedContinuation.kt | 178 ------------------ .../main/kotlin/arrow/continuations/either.kt | 28 +++ .../arrow/continuations/examples/either.kt | 32 ++++ .../arrow/continuations/examples/nullable.kt | 29 +++ .../kotlin/arrow/continuations/nullable.kt | 26 +++ .../kotlin/arrow/core/MonadContinuation.kt | 1 + 7 files changed, 222 insertions(+), 178 deletions(-) create mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/Cont.kt delete mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/DelimitedContinuation.kt create mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/either.kt create mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/examples/either.kt create mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/examples/nullable.kt create mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/nullable.kt diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/Cont.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/Cont.kt new file mode 100644 index 000000000..bd876782f --- /dev/null +++ b/arrow-core-data/src/main/kotlin/arrow/continuations/Cont.kt @@ -0,0 +1,106 @@ +package arrow.continuations + +import arrow.core.ArrowCoreInternalException +import arrow.core.Either +import arrow.core.ShortCircuit +import arrow.core.left +import arrow.core.right +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import me.eugeniomarletti.kotlin.metadata.shadow.utils.addToStdlib.cast +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn +import kotlin.coroutines.resumeWithException +import kotlin.experimental.ExperimentalTypeInference + +interface Computation + +internal const val UNDECIDED = 0 +internal const val SUSPENDED = 1 + +sealed class ContState(val parent: Continuation<*>) : Continuation, Computation { + + @Suppress("UNCHECKED_CAST") + abstract fun ShortCircuit.recover(): F + + override fun resumeWith(result: Result) { + _decision.loop { decision -> + when (decision) { + UNDECIDED -> { + val r: Any? = when { + result.isFailure -> { + val e = result.exceptionOrNull() + if (e is ShortCircuit) e.recover() else null + } + result.isSuccess -> result.getOrNull() + else -> throw ArrowCoreInternalException + } + + when { + r == null -> { + parent.resumeWithException(result.exceptionOrNull()!!) + return + } + _decision.compareAndSet(UNDECIDED, r) -> return + else -> Unit // loop again + } + } + else -> { // If not `UNDECIDED` then we need to pass result to `parent` + val res: Result = result.fold({ Result.success(it) }, { t -> + if (t is ShortCircuit) Result.success(t.recover()) + else Result.failure(t) + }) + parent.resumeWith(res.cast()) + return + } + } + } + } + + internal fun getResult(): Any? = + _decision.loop { decision -> + when (decision) { + UNDECIDED -> if (this._decision.compareAndSet(UNDECIDED, SUSPENDED)) return COROUTINE_SUSPENDED + else -> return decision + } + } + + private val _decision = atomic(UNDECIDED) + + override val context: CoroutineContext = EmptyCoroutineContext +} + +@UseExperimental(ExperimentalTypeInference::class) +@Suppress("UNCHECKED_CAST") +sealed class Cont(parent: Continuation<*>) : ContState(parent) { + + abstract suspend fun A.just(): F + + abstract class Strict(parent: Continuation<*>) : Cont(parent) { + @UseExperimental(ExperimentalTypeInference::class) + @BuilderInference + @Suppress("UNCHECKED_CAST") + open fun > strict(computation: suspend C.() -> A): Any? = + start { computation(this as C).just() } + } + abstract class Suspend(parent: Continuation<*>) : Cont(parent) + abstract class NonDeterministic(parent: Continuation<*>) : Cont(parent) + abstract class Interleaved(parent: Continuation<*>, val ctxs: Iterable>) : Cont(parent) + + @UseExperimental(ExperimentalTypeInference::class) + @BuilderInference + fun start(@BuilderInference f: suspend () -> F): Any? = + try { + f.startCoroutineUninterceptedOrReturn(this)?.let { + if (it === COROUTINE_SUSPENDED) getResult() + else it + } + } catch (e: Throwable) { + if (e is ShortCircuit) e.recover() + else throw e + } +} + diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/DelimitedContinuation.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/DelimitedContinuation.kt deleted file mode 100644 index 1f2858b2b..000000000 --- a/arrow-core-data/src/main/kotlin/arrow/continuations/DelimitedContinuation.kt +++ /dev/null @@ -1,178 +0,0 @@ -package arrow.continuations - -import arrow.core.ArrowCoreInternalException -import arrow.core.ShortCircuit -import kotlinx.atomicfu.atomic -import kotlinx.atomicfu.loop -import kotlin.coroutines.jvm.internal.* -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.createCoroutine -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.startCoroutine -import kotlin.coroutines.suspendCoroutine -import kotlin.experimental.ExperimentalTypeInference - - -internal const val UNDECIDED = 0 -internal const val SUSPENDED = 1 - -private object NoOpContinuation : Continuation { - override val context: CoroutineContext = EmptyCoroutineContext - - override fun resumeWith(result: Result) { - // Nothing - } -} - -@Suppress( - "CANNOT_OVERRIDE_INVISIBLE_MEMBER", - "INVISIBLE_MEMBER", - "INVISIBLE_REFERENCE", - "UNCHECKED_CAST", - "EXPOSED_SUPER_CLASS" -) -abstract class DelimitedContinuation : ContinuationImpl(NoOpContinuation, EmptyCoroutineContext) { - - abstract val parent: Continuation - - fun ShortCircuit.recover(): A = - throw this - - /** - * State is either - * 0 - UNDECIDED - * 1 - SUSPENDED - * Any? (3) `resumeWith` always stores it upon UNDECIDED, and `getResult` can atomically get it. - */ - private val _decision = atomic(UNDECIDED) - - override val context: CoroutineContext = EmptyCoroutineContext - - fun resumeWithX(result: Result) { - _decision.loop { decision -> - when (decision) { - UNDECIDED -> { - val r: Any? = when { - result.isFailure -> { - val e = result.exceptionOrNull() - if (e is ShortCircuit) e.recover() else null - } - result.isSuccess -> result.getOrNull() - else -> throw ArrowCoreInternalException - } - - when { - r == null -> { - parent.resumeWithException(result.exceptionOrNull()!!) - return - } - _decision.compareAndSet(UNDECIDED, r) -> return - else -> Unit // loop again - } - } - else -> { // If not `UNDECIDED` then we need to pass result to `parent` - val res: Result = result.fold({ Result.success(it) }, { t -> - if (t is ShortCircuit) Result.success(t.recover()) - else Result.failure(t) - }) - parent.resumeWith(res) - return - } - } - } - } - - @PublishedApi // return the result - internal fun getResult(): Any? = - _decision.loop { decision -> - when (decision) { - UNDECIDED -> if (this._decision.compareAndSet(UNDECIDED, SUSPENDED)) return COROUTINE_SUSPENDED - else -> return decision - } - } - - fun > C.start(f: suspend C.() -> A): Any? = - try { - f.startCoroutineUninterceptedOrReturn(this, this)?.let { - if (it == COROUTINE_SUSPENDED) getResult() - else it - } - } catch (e: Throwable) { - if (e is ShortCircuit) e.recover() - else throw e - } - - override fun invokeSuspend(result: Result): Any? = - resumeWithX(result).also { - println("invokeSuspend: $this $result") - } - - - // Escalate visibility to manually release intercepted continuation - override fun releaseIntercepted() { - super.releaseIntercepted().also { - println("releaseIntercepted $this") - } - } - - override val callerFrame: CoroutineStackFrame? - get() = super.callerFrame.also { - println("callerFrame $it") - } - - override fun create(value: Any?, completion: Continuation<*>): Continuation { - return super.create(value, completion).also { - println("create: $value completion: $completion , created: $it") - } - } - - override fun create(completion: Continuation<*>): Continuation { - return super.create(completion).also { - println("create: $completion , created: $it") - } - } - - override fun getStackTraceElement(): StackTraceElement? { - return super.getStackTraceElement().also { - println("getStackTraceElement: $it") - } - } - -} - -class NonDeterministicContinuation : DelimitedContinuation() { - override val parent: Continuation = this -} - -operator fun List.invoke(): A = - iterator().next() - -@UseExperimental(ExperimentalTypeInference::class) -@BuilderInference -suspend fun > list(f: suspend DelimitedContinuation.() -> A): B = - suspendCoroutineUninterceptedOrReturn { c -> - val ncont = NonDeterministicContinuation() - f.startCoroutine(ncont, ncont) - when (val result = ncont.getResult()) { - COROUTINE_SUSPENDED -> COROUTINE_SUSPENDED - else -> c.resumeWith(result as Result) - } - } - -suspend fun main() { - println("1. suspend main") - val result : List = - list { - println("2. Before list bind") - val a = listOf(1, 2, 3)() - println("*. After list bind") - val result = a + 1 - result - } - println(result) -} diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/either.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/either.kt new file mode 100644 index 000000000..edcc5b303 --- /dev/null +++ b/arrow-core-data/src/main/kotlin/arrow/continuations/either.kt @@ -0,0 +1,28 @@ +package arrow.continuations + +import arrow.core.Either +import arrow.core.ShortCircuit +import arrow.core.identity +import arrow.core.left +import arrow.core.right +import kotlin.coroutines.Continuation +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.experimental.ExperimentalTypeInference + +class EitherBuilder(parent: Continuation<*>) : Cont.Strict, Any?>(parent) { + operator fun Either<*, A>.invoke(): A = + fold({ e -> throw ShortCircuit(e) }, ::identity) + + override suspend fun A.just(): Either = + right() + + override fun ShortCircuit.recover(): Either = + resolve().left() +} + +@UseExperimental(ExperimentalTypeInference::class) +@BuilderInference +suspend fun either(@BuilderInference f: suspend EitherBuilder.() -> A): Either = + suspendCoroutineUninterceptedOrReturn { + EitherBuilder(it).strict(f) + } diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/examples/either.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/examples/either.kt new file mode 100644 index 000000000..17a1dc1fc --- /dev/null +++ b/arrow-core-data/src/main/kotlin/arrow/continuations/examples/either.kt @@ -0,0 +1,32 @@ +package arrow.continuations.examples + +import arrow.continuations.either +import arrow.core.Either +import arrow.core.left +import arrow.core.right + +suspend fun main() { + + val fa: Either = 1.right() + val fb: Either = 2f.right() + val fc: Either = "not an int".left() + + val success: Either = + either { + val a: Int = fa() + val b: Float = fb() + a + b + } + + val error: Either = + either { + val a: Int = fa() + val b: Float = fb() + val c: Int = fc() + a + b + c + } + println(success) // Right(3.0) + println(error) // Left(not an int) + +} + diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/examples/nullable.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/examples/nullable.kt new file mode 100644 index 000000000..69e7aec02 --- /dev/null +++ b/arrow-core-data/src/main/kotlin/arrow/continuations/examples/nullable.kt @@ -0,0 +1,29 @@ +package arrow.continuations.examples + +import arrow.continuations.nullable + +suspend fun main() { + + val fa: Int? = 1 + val fb: Float? = 2f + val fc: Int? = null + + val success: Float? = + nullable { + val a: Int = fa() + val b: Float = fb() + a + b + } + + val error: Float? = + nullable { + val a: Int = fa() + val b: Float = fb() + val c: Int = fc() + a + b + c + } + println(success) // 3.0 + println(error) // null + +} + diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/nullable.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/nullable.kt new file mode 100644 index 000000000..76465be8e --- /dev/null +++ b/arrow-core-data/src/main/kotlin/arrow/continuations/nullable.kt @@ -0,0 +1,26 @@ +package arrow.continuations + +import arrow.core.Either +import arrow.core.ShortCircuit +import arrow.core.identity +import arrow.core.left +import arrow.core.right +import kotlin.coroutines.Continuation +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.experimental.ExperimentalTypeInference + +class NullableBuilder(parent: Continuation<*>) : Cont.Strict(parent) { + + operator fun A?.invoke(): A = this ?: throw ShortCircuit(null) + + override suspend fun A.just(): A? = this + + override fun ShortCircuit.recover(): Any? = null +} + +@UseExperimental(ExperimentalTypeInference::class) +@BuilderInference +suspend fun nullable(@BuilderInference f: suspend NullableBuilder.() -> A): A? = + suspendCoroutineUninterceptedOrReturn { + NullableBuilder(it).strict(f) + } diff --git a/arrow-core-data/src/main/kotlin/arrow/core/MonadContinuation.kt b/arrow-core-data/src/main/kotlin/arrow/core/MonadContinuation.kt index 607ebcb61..513c22a46 100644 --- a/arrow-core-data/src/main/kotlin/arrow/core/MonadContinuation.kt +++ b/arrow-core-data/src/main/kotlin/arrow/core/MonadContinuation.kt @@ -14,6 +14,7 @@ interface EagerBind : BindSyntax class ShortCircuit(val value: Any?) : RuntimeException(null, null) { override fun fillInStackTrace(): Throwable = this override fun toString(): String = "ShortCircuit($value)" + inline fun resolve(): E = value as E } @Suppress("UNCHECKED_CAST") From 19bd93f0ecfa11af9ddcf547963be25784f7da75 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Fri, 17 Jul 2020 14:53:01 +0200 Subject: [PATCH 03/49] multiprompt progress --- arrow-continuations/build.gradle | 24 ++ arrow-continuations/gradle.properties | 6 + .../arrow/continuations/contt/DelimCC.kt | 214 ++++++++++++++++++ .../arrow/continuations/contt/Delimited.kt | 180 +++++++++++++++ .../arrow/continuations/contt/Delimited2.kt | 112 +++++++++ .../main/kotlin/arrow/continuations/Cont.kt | 106 --------- .../main/kotlin/arrow/continuations/either.kt | 28 --- .../arrow/continuations/examples/either.kt | 32 --- .../arrow/continuations/examples/nullable.kt | 29 --- .../kotlin/arrow/continuations/nullable.kt | 26 --- settings.gradle | 1 + 11 files changed, 537 insertions(+), 221 deletions(-) create mode 100644 arrow-continuations/build.gradle create mode 100644 arrow-continuations/gradle.properties create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited.kt create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited2.kt delete mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/Cont.kt delete mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/either.kt delete mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/examples/either.kt delete mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/examples/nullable.kt delete mode 100644 arrow-core-data/src/main/kotlin/arrow/continuations/nullable.kt diff --git a/arrow-continuations/build.gradle b/arrow-continuations/build.gradle new file mode 100644 index 000000000..a3fb977a2 --- /dev/null +++ b/arrow-continuations/build.gradle @@ -0,0 +1,24 @@ +plugins { + id "maven-publish" + id "base" + id "org.jetbrains.kotlin.jvm" + id "org.jetbrains.kotlin.kapt" + id "org.jetbrains.dokka" + id "org.jlleitschuh.gradle.ktlint" + id "ru.vyarus.animalsniffer" +} + +apply from: "$SUBPROJECT_CONF" +apply from: "$DOC_CONF" +apply from: "$PUBLISH_CONF" +apply plugin: 'kotlinx-atomicfu' + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" + compile project(":arrow-annotations") + compile project(":arrow-core") + kapt project(":arrow-meta") + testRuntime "org.junit.vintage:junit-vintage-engine:$JUNIT_VINTAGE_VERSION" + testCompile "io.kotlintest:kotlintest-runner-junit5:$KOTLIN_TEST_VERSION", excludeArrow + testCompile project(":arrow-core-test") +} diff --git a/arrow-continuations/gradle.properties b/arrow-continuations/gradle.properties new file mode 100644 index 000000000..05db45fd0 --- /dev/null +++ b/arrow-continuations/gradle.properties @@ -0,0 +1,6 @@ +# Maven publishing configuration +POM_NAME=Arrow Continuations +POM_ARTIFACT_ID=arrow-continuations +POM_PACKAGING=jar +# Build configuration +kapt.incremental.apt=false diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt new file mode 100644 index 000000000..6448f3a5b --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt @@ -0,0 +1,214 @@ +package arrow.continuations.contxx + +import arrow.core.ShortCircuit +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import java.util.* +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.RestrictsSuspension +import kotlin.coroutines.intrinsics.* + +class DelimitedContinuation(val prompt: Prompt, val f: suspend () -> A) : Continuation { + + override val context: CoroutineContext = EmptyCoroutineContext + + fun ShortCircuit.recover(): A = throw this + + override fun resumeWith(result: Result) { + _decision.loop { decision -> + when (decision) { + UNDECIDED -> { + val r: A? = when { + result.isFailure -> { + val e = result.exceptionOrNull() + if (e is ShortCircuit) e.recover() else null + } + result.isSuccess -> result.getOrNull() + else -> throw TODO("Impossible") + } + + when { + r == null -> { + throw result.exceptionOrNull()!! + //resumeWithException(result.exceptionOrNull()!!) + return + } + _decision.compareAndSet(UNDECIDED, Completed(r)) -> return + else -> Unit // loop again + } + } + else -> { // If not `UNDECIDED` then we need to pass result to `parent` + val res: Result = result.fold({ Result.success(it) }, { t -> + if (t is ShortCircuit) Result.success(t.recover()) + else Result.failure(t) + }) + _decision.getAndSet(Completed(res.getOrThrow())) + return + } + } + } + } + + private val _decision = atomic(UNDECIDED) + + fun isDone(): Boolean = + _decision.value is Completed<*> + + fun run(): Unit { + f.startCoroutineUninterceptedOrReturn(this) + _decision.loop { decision -> + when (decision) { + UNDECIDED -> if (this._decision.compareAndSet(UNDECIDED, prompt)) Unit //loop again + else -> return@run + } + } + } + + companion object { + suspend fun yield(p: Prompt) { + suspendCoroutineUninterceptedOrReturn { + it.resumeWith(Result.success(p)) + COROUTINE_SUSPENDED + } + } + } +} + +suspend fun reset(prompt: Prompt, f: suspend () -> A): A { + val k: DelimitedContinuation<*> = DelimitedContinuation(prompt) { + DelimCC.result = f() + DelimCC.result + } + return DelimCC.runCont(k) +} + +suspend fun shift(prompt: Prompt, body: CPS): A { + DelimCC.body = body + DelimitedContinuation.yield(prompt) + return DelimCC.arg as A +} + +// multiprompt delimited continuations in terms of the current API +// this implementation has Felleisen classification -F+ +object DelimCC { + internal var result: Any? = null + internal var arg: Any? = null + internal var body: CPS<*, *>? = null + + internal suspend fun runCont(k: DelimitedContinuation<*>): A { + k.run() + val frames = Stack>() + while (!k.isDone()) { + + // IDEA: + // 1) Push a separate (one-time) prompt on `shift`. + // 2) On resume, capture the continuation on a heap allocated + // stack of `DelimitedContinuation`s + // 3) Trampoline those continuations at the position of the original + // `reset`. + // + // This only works since continuations are one-shot. Otherwise the + // captured frames would contain references to the continuation and + // would be evaluated out of scope. + val bodyPrompt: Prompt = Prompt() + val bodyCont: DelimitedContinuation<*> = + DelimitedContinuation(bodyPrompt) { + result = (body as CPS).invoke(Cont { value -> + // yield and wait until the subcontinuation has been + // evaluated. + arg = value + // yielding here returns control to the outer continuation + DelimitedContinuation.yield(bodyPrompt) + result + }) + body = null + body + } + bodyCont.run() // start it + + // continuation was called within body + if (!bodyCont.isDone()) { + frames.push(bodyCont) + k.run() + + // continuation was discarded or escaped the dynamic scope of + // bodyCont. + } else { + break + } + } + while (!frames.isEmpty()) { + val frame = frames.pop() + if (!frame.isDone()) { + frame.run() + } + } + return result as A + } +} + +interface DelimitedContinuationScope +class Prompt : DelimitedContinuationScope +class Completed(val value: A) : DelimitedContinuationScope +object UNDECIDED : DelimitedContinuationScope + +class Cont(val f: suspend (A) -> R) { + suspend operator fun invoke(p1: A): R = f(p1) +} + +class CPS(val f: suspend (Cont) -> R) { + suspend operator fun invoke(p1: Cont): R = f(p1) +} + +suspend fun test1() { + val p1 = Prompt() + val p2 = Prompt() + val res1 = reset(p1) { 5 - shift(p1, CPS { k: Cont -> k(2) * 7 }) } + val res2 = reset(p1) { + (1 + shift(p1, CPS { k: Cont -> k(2) }) + + shift(p1, CPS { k: Cont -> k(3) })) + } + val res3 = reset(p1) { + (1 + shift(p1, CPS { k: Cont -> k(2) }) + + reset(p2) { + (2 + shift(p2, CPS { k: Cont -> k(3) * 3 }) + + shift(p1, CPS { k: Cont -> k(4) * 2 })) + }) + } + val res4 = reset(p1) { + (1 + shift(p1, CPS { k: Cont -> k(2) }) + + reset(p2) { + (2 + shift(p2, CPS { k: Cont -> k(3) * 3 }) + + shift(p1, CPS { k: Cont -> 42 })) + }) + } + println(res1) // 21 + println(res2) // 6 + println(res3) // 60 + println(res4) // 42 +} + +suspend fun test2() { + val p1 = Prompt() + val res = reset(p1) { + var n = 10000 + var r = 0 + while (n > 0) { + r += shift(p1, CPS { k: Cont -> k(1) }) + n-- + } + r + } + println(res) +} + +fun main() { + suspend { + DelimCC.run { + test1() + test2() + } + }.startCoroutineUninterceptedOrReturn(DelimitedContinuation(Prompt()){}) +} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited.kt new file mode 100644 index 000000000..1251639ed --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited.kt @@ -0,0 +1,180 @@ +package arrow.continuations.conts + +import arrow.core.ShortCircuit +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resumeWithException + +fun reset(prompt: DelimitedScope): A = + ContinuationState.reset(prompt) { + val result = prompt.body(prompt) + result as A + } + +interface DelimitedContinuation : Continuation { + fun invokeWith(value: Result): A + fun isDone(): Boolean + fun run(): Unit + suspend fun yield(): Unit +} + +interface DelimitedScope : DelimitedContinuation { + fun ShortCircuit.recover(): A = throw this + suspend fun shift(block: suspend (DelimitedScope) -> A): B = + ContinuationState.shift(this, block) + + fun startCoroutineUninterceptedOrReturn(): Any? + fun startCoroutineUninterceptedOrReturn(body: suspend DelimitedScope.() -> A): Any? + + val body: suspend DelimitedScope.() -> A +} + +open class DelimitedScopeImpl(open val prompt: Continuation, override val body: suspend DelimitedScope.() -> A) : DelimitedScope { + + /** + * State is either + * 0 - UNDECIDED + * 1 - SUSPENDED + * Any? (3) `resumeWith` always stores it upon UNDECIDED, and `getResult` can atomically get it. + */ + private val _decision = atomic(UNDECIDED) + + override val context: CoroutineContext = EmptyCoroutineContext + + override fun resumeWith(result: Result) { + _decision.loop { decision -> + when (decision) { + UNDECIDED -> { + val r: A? = when { + result.isFailure -> { + val e = result.exceptionOrNull() + if (e is ShortCircuit) e.recover() else null + } + result.isSuccess -> result.getOrNull() + else -> TODO("Impossible bug") + } + + when { + r == null -> { + prompt.resumeWithException(result.exceptionOrNull()!!) + return + } + _decision.compareAndSet(UNDECIDED, r) -> return + else -> Unit // loop again + } + } + else -> { // If not `UNDECIDED` then we need to pass result to `parent` + val res: Result = result.fold({ Result.success(it) }, { t -> + if (t is ShortCircuit) Result.success(t.recover()) + else Result.failure(t) + }) + prompt.resumeWith(res) + return + } + } + } + } + + override fun invokeWith(value: Result): A = + value.getOrThrow() as A + + override fun isDone(): Boolean = + _decision.loop { decision -> + return when (decision) { + UNDECIDED -> false + SUSPENDED -> false + COROUTINE_SUSPENDED -> false + else -> true + } + } + + override fun startCoroutineUninterceptedOrReturn(body: suspend DelimitedScope.() -> A): Any? = + try { + body.startCoroutineUninterceptedOrReturn(this, this)?.let { + if (it == COROUTINE_SUSPENDED) getResult() + else it + } + } catch (e: Throwable) { + if (e is ShortCircuit) e.recover() + else throw e + } + + override fun startCoroutineUninterceptedOrReturn(): Any? = + startCoroutineUninterceptedOrReturn(body) + + override suspend fun yield() { + suspendCoroutineUninterceptedOrReturn { + if (isDone()) getResult() + else COROUTINE_SUSPENDED + } + } + + override fun run(): Unit { + val result = try { + body.startCoroutineUninterceptedOrReturn(this, this)?.let { + if (it == COROUTINE_SUSPENDED) getResult() + else it + } + } catch (e: Throwable) { + if (e is ShortCircuit) e.recover() + else throw e + } + _decision.getAndSet(result) + } + + @PublishedApi // return the result + internal fun getResult(): Any? = + _decision.loop { decision -> + when (decision) { + UNDECIDED -> if (this._decision.compareAndSet(UNDECIDED, SUSPENDED)) return COROUTINE_SUSPENDED + else -> return decision + } + } + + companion object { + internal const val UNDECIDED = 0 + internal const val SUSPENDED = 1 + } +} + +class ListComputation( + continuation: Continuation>, + f: suspend ListComputation<*>.() -> A +) : DelimitedScopeImpl>(continuation, { + val result = f(this as ListComputation>) + listOf(result) +}) { + + var result: List = emptyList() + + suspend operator fun List.invoke(): B = + shift { cont -> + result = flatMap { + cont.invokeWith(Result.success(it)) + reset(this@ListComputation) + } + result + } + +} + +suspend fun list(f: suspend ListComputation<*>.() -> A): List = + suspendCoroutineUninterceptedOrReturn { + reset(ListComputation(it, f)) + } + + +suspend fun main() { + val result = list { + val a = listOf(1, 2, 3)() + val b = listOf(1f, 2f, 3f)() + a + b + } + println(result) +} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited2.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited2.kt new file mode 100644 index 000000000..e84a8f09b --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited2.kt @@ -0,0 +1,112 @@ +package arrow.continuations.conts + +import java.util.* +import kotlin.coroutines.Continuation + +typealias Prompt = DelimitedScope + +typealias CPS = suspend DelimitedScope.() -> R + +interface Cont { + fun resume(value: A): B +} + + +// multiprompt delimited continuations in terms of the current API +// this implementation has Felleisen classification -F+ +object ContinuationState { + + private var result: Any? = null + private var arg: Any? = null + private var body: CPS<*, *>? = null + + fun reset(prompt: Prompt, f: suspend () -> A): A { + val k = DelimitedScopeImpl(prompt) { + val a: Any? = f() + result = a + a as A + } + return runCont(k) + } + + suspend fun shift(prompt: Prompt, body: CPS): A { + this.body = body as CPS<*, *> + prompt.yield() + //Continuation.yield(prompt) + return arg as A + } + + private fun runCont(k: DelimitedContinuation<*, *>): A { + k.run() + + val frames: Stack> = Stack() + + while (!k.isDone()) { + + // IDEA: + // 1) Push a separate (one-time) prompt on `shift`. + // 2) On resume, capture the continuation on a heap allocated + // stack of `Continuation`s + // 3) Trampoline those continuations at the position of the original + // `reset`. + // + // This only works since continuations are one-shot. Otherwise the + // captured frames would contain references to the continuation and + // would be evaluated out of scope. + val bodyPrompt = object : DelimitedScopeImpl(k as Continuation, { + + /** + final var bodyPrompt = new ContinuationScope() {}; + final var bodyCont = new Continuation(bodyPrompt, () -> { + result = ((CPS) body).apply(value -> { + // yield and wait until the subcontinuation has been + // evaluated. + arg = value; + // yielding here returns control to the outer continuation + Continuation.yield(bodyPrompt); + return (A) result; + }); + body = null; + }); + */ + }) {} // TODO what prompt goes here on an empty block? + val bodyCont = DelimitedScopeImpl(bodyPrompt) { + result = DelimitedScopeImpl(this) { + // yield and wait until the subcontinuation has been + // evaluated. + arg = this@ContinuationState.body?.invoke(this) + + // yielding here returns control to the outer continuation + //Continuation.yield(bodyPrompt) + bodyPrompt.yield() + result as A + } + result + } + body = null + + bodyCont.run() // start it + + // continuation was called within body + if (!bodyCont.isDone()) { + frames.push(bodyCont) + k.run() + + // continuation was discarded or escaped the dynamic scope of + // bodyCont. + } else { + break + } + } + + while (!frames.isEmpty()) { + var frame = frames.pop() + + if (!frame.isDone()) { + frame.run() + } + } + + return result as A + } +} diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/Cont.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/Cont.kt deleted file mode 100644 index bd876782f..000000000 --- a/arrow-core-data/src/main/kotlin/arrow/continuations/Cont.kt +++ /dev/null @@ -1,106 +0,0 @@ -package arrow.continuations - -import arrow.core.ArrowCoreInternalException -import arrow.core.Either -import arrow.core.ShortCircuit -import arrow.core.left -import arrow.core.right -import kotlinx.atomicfu.atomic -import kotlinx.atomicfu.loop -import me.eugeniomarletti.kotlin.metadata.shadow.utils.addToStdlib.cast -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn -import kotlin.coroutines.resumeWithException -import kotlin.experimental.ExperimentalTypeInference - -interface Computation - -internal const val UNDECIDED = 0 -internal const val SUSPENDED = 1 - -sealed class ContState(val parent: Continuation<*>) : Continuation, Computation { - - @Suppress("UNCHECKED_CAST") - abstract fun ShortCircuit.recover(): F - - override fun resumeWith(result: Result) { - _decision.loop { decision -> - when (decision) { - UNDECIDED -> { - val r: Any? = when { - result.isFailure -> { - val e = result.exceptionOrNull() - if (e is ShortCircuit) e.recover() else null - } - result.isSuccess -> result.getOrNull() - else -> throw ArrowCoreInternalException - } - - when { - r == null -> { - parent.resumeWithException(result.exceptionOrNull()!!) - return - } - _decision.compareAndSet(UNDECIDED, r) -> return - else -> Unit // loop again - } - } - else -> { // If not `UNDECIDED` then we need to pass result to `parent` - val res: Result = result.fold({ Result.success(it) }, { t -> - if (t is ShortCircuit) Result.success(t.recover()) - else Result.failure(t) - }) - parent.resumeWith(res.cast()) - return - } - } - } - } - - internal fun getResult(): Any? = - _decision.loop { decision -> - when (decision) { - UNDECIDED -> if (this._decision.compareAndSet(UNDECIDED, SUSPENDED)) return COROUTINE_SUSPENDED - else -> return decision - } - } - - private val _decision = atomic(UNDECIDED) - - override val context: CoroutineContext = EmptyCoroutineContext -} - -@UseExperimental(ExperimentalTypeInference::class) -@Suppress("UNCHECKED_CAST") -sealed class Cont(parent: Continuation<*>) : ContState(parent) { - - abstract suspend fun A.just(): F - - abstract class Strict(parent: Continuation<*>) : Cont(parent) { - @UseExperimental(ExperimentalTypeInference::class) - @BuilderInference - @Suppress("UNCHECKED_CAST") - open fun > strict(computation: suspend C.() -> A): Any? = - start { computation(this as C).just() } - } - abstract class Suspend(parent: Continuation<*>) : Cont(parent) - abstract class NonDeterministic(parent: Continuation<*>) : Cont(parent) - abstract class Interleaved(parent: Continuation<*>, val ctxs: Iterable>) : Cont(parent) - - @UseExperimental(ExperimentalTypeInference::class) - @BuilderInference - fun start(@BuilderInference f: suspend () -> F): Any? = - try { - f.startCoroutineUninterceptedOrReturn(this)?.let { - if (it === COROUTINE_SUSPENDED) getResult() - else it - } - } catch (e: Throwable) { - if (e is ShortCircuit) e.recover() - else throw e - } -} - diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/either.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/either.kt deleted file mode 100644 index edcc5b303..000000000 --- a/arrow-core-data/src/main/kotlin/arrow/continuations/either.kt +++ /dev/null @@ -1,28 +0,0 @@ -package arrow.continuations - -import arrow.core.Either -import arrow.core.ShortCircuit -import arrow.core.identity -import arrow.core.left -import arrow.core.right -import kotlin.coroutines.Continuation -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.experimental.ExperimentalTypeInference - -class EitherBuilder(parent: Continuation<*>) : Cont.Strict, Any?>(parent) { - operator fun Either<*, A>.invoke(): A = - fold({ e -> throw ShortCircuit(e) }, ::identity) - - override suspend fun A.just(): Either = - right() - - override fun ShortCircuit.recover(): Either = - resolve().left() -} - -@UseExperimental(ExperimentalTypeInference::class) -@BuilderInference -suspend fun either(@BuilderInference f: suspend EitherBuilder.() -> A): Either = - suspendCoroutineUninterceptedOrReturn { - EitherBuilder(it).strict(f) - } diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/examples/either.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/examples/either.kt deleted file mode 100644 index 17a1dc1fc..000000000 --- a/arrow-core-data/src/main/kotlin/arrow/continuations/examples/either.kt +++ /dev/null @@ -1,32 +0,0 @@ -package arrow.continuations.examples - -import arrow.continuations.either -import arrow.core.Either -import arrow.core.left -import arrow.core.right - -suspend fun main() { - - val fa: Either = 1.right() - val fb: Either = 2f.right() - val fc: Either = "not an int".left() - - val success: Either = - either { - val a: Int = fa() - val b: Float = fb() - a + b - } - - val error: Either = - either { - val a: Int = fa() - val b: Float = fb() - val c: Int = fc() - a + b + c - } - println(success) // Right(3.0) - println(error) // Left(not an int) - -} - diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/examples/nullable.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/examples/nullable.kt deleted file mode 100644 index 69e7aec02..000000000 --- a/arrow-core-data/src/main/kotlin/arrow/continuations/examples/nullable.kt +++ /dev/null @@ -1,29 +0,0 @@ -package arrow.continuations.examples - -import arrow.continuations.nullable - -suspend fun main() { - - val fa: Int? = 1 - val fb: Float? = 2f - val fc: Int? = null - - val success: Float? = - nullable { - val a: Int = fa() - val b: Float = fb() - a + b - } - - val error: Float? = - nullable { - val a: Int = fa() - val b: Float = fb() - val c: Int = fc() - a + b + c - } - println(success) // 3.0 - println(error) // null - -} - diff --git a/arrow-core-data/src/main/kotlin/arrow/continuations/nullable.kt b/arrow-core-data/src/main/kotlin/arrow/continuations/nullable.kt deleted file mode 100644 index 76465be8e..000000000 --- a/arrow-core-data/src/main/kotlin/arrow/continuations/nullable.kt +++ /dev/null @@ -1,26 +0,0 @@ -package arrow.continuations - -import arrow.core.Either -import arrow.core.ShortCircuit -import arrow.core.identity -import arrow.core.left -import arrow.core.right -import kotlin.coroutines.Continuation -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.experimental.ExperimentalTypeInference - -class NullableBuilder(parent: Continuation<*>) : Cont.Strict(parent) { - - operator fun A?.invoke(): A = this ?: throw ShortCircuit(null) - - override suspend fun A.just(): A? = this - - override fun ShortCircuit.recover(): Any? = null -} - -@UseExperimental(ExperimentalTypeInference::class) -@BuilderInference -suspend fun nullable(@BuilderInference f: suspend NullableBuilder.() -> A): A? = - suspendCoroutineUninterceptedOrReturn { - NullableBuilder(it).strict(f) - } diff --git a/settings.gradle b/settings.gradle index 5da190a93..9e35ccf50 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,3 +7,4 @@ include 'arrow-syntax' include 'arrow-core' include 'arrow-core-data' include 'arrow-core-test' +include 'arrow-continuations' From c6dc26cbbd0d51a58683bcdecacc6a80d450010d Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Mon, 20 Jul 2020 14:17:05 +0200 Subject: [PATCH 04/49] multiprompt progress --- .../arrow/continuations/contt/DelimCC.kt | 32 ++-- .../arrow/continuations/reflect/Coroutine.kt | 156 ++++++++++++++++++ .../arrow/continuations/reflect/program.kt | 20 +++ .../arrow/continuations/reflect/reify.kt | 90 ++++++++++ 4 files changed, 283 insertions(+), 15 deletions(-) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt index 6448f3a5b..0af5aed58 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt @@ -9,8 +9,12 @@ import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.RestrictsSuspension import kotlin.coroutines.intrinsics.* +import kotlin.coroutines.resume -class DelimitedContinuation(val prompt: Prompt, val f: suspend () -> A) : Continuation { +class DelimitedContinuation( + val prompt: Prompt, + val f: suspend DelimitedContinuation.() -> A +) : Continuation { override val context: CoroutineContext = EmptyCoroutineContext @@ -57,7 +61,7 @@ class DelimitedContinuation(val prompt: Prompt, val f: suspend () -> A) : _decision.value is Completed<*> fun run(): Unit { - f.startCoroutineUninterceptedOrReturn(this) + f.startCoroutineUninterceptedOrReturn(this, this) _decision.loop { decision -> when (decision) { UNDECIDED -> if (this._decision.compareAndSet(UNDECIDED, prompt)) Unit //loop again @@ -67,12 +71,10 @@ class DelimitedContinuation(val prompt: Prompt, val f: suspend () -> A) : } companion object { - suspend fun yield(p: Prompt) { - suspendCoroutineUninterceptedOrReturn { - it.resumeWith(Result.success(p)) - COROUTINE_SUSPENDED - } - } + suspend fun yield(prompt: Prompt) = + DelimCC.runCont(DelimitedContinuation(prompt) { + resume(prompt) + }) } } @@ -115,7 +117,7 @@ object DelimCC { val bodyPrompt: Prompt = Prompt() val bodyCont: DelimitedContinuation<*> = DelimitedContinuation(bodyPrompt) { - result = (body as CPS).invoke(Cont { value -> + result = (body as CPS)(Cont { value -> // yield and wait until the subcontinuation has been // evaluated. arg = value @@ -205,10 +207,10 @@ suspend fun test2() { } fun main() { - suspend { - DelimCC.run { - test1() - test2() - } - }.startCoroutineUninterceptedOrReturn(DelimitedContinuation(Prompt()){}) + DelimitedContinuation(Prompt()) { + DelimCC.run { + test1() + test2() + } + }.run() } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt new file mode 100644 index 000000000..51f547721 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt @@ -0,0 +1,156 @@ +package arrow.continuations.reflect + +import arrow.core.ShortCircuit +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.RestrictsSuspension +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume + +interface Prompt { + suspend fun suspend(value: A): B +} + +internal const val UNDECIDED = 0 +internal const val SUSPENDED = 1 + +class Coroutine(prog: suspend (Prompt) -> C) { + + private val _decision = atomic(UNDECIDED) + + fun isDone() = continuation.isDone() + + fun value(): A { + assert(!isDone()); return receive() + } + + fun result(): C { + assert(isDone()); return receive() + } + + fun yield(v: B) { + assert(!isDone()) + send(v) + continuation.run() + } + + private var channel: Any? = null + private fun send(v: Any?) { + channel = v + } + + private fun receive(): A { + val v = channel + return v as A + } + + private open inner class InnerPrompt : Prompt /*ContinuationScope("cats-reflect")*/ { + override suspend fun suspend(value: A): B { + return suspendCoroutineUninterceptedOrReturn { + send(value) + val res = continuation.getResult() + if (res == COROUTINE_SUSPENDED) COROUTINE_SUSPENDED + else { + it.resumeWith(Result.success(res) as Result) + COROUTINE_SUSPENDED + } + } + } + } + + private val prompt = InnerPrompt() + + @RestrictsSuspension + inner class Continuation( + val f: suspend () -> Unit + ) : kotlin.coroutines.Continuation { + + override val context: CoroutineContext = EmptyCoroutineContext + + fun run(): Unit { + continuation.startCoroutineUninterceptedOrReturn { + f() + suspendCoroutineUninterceptedOrReturn { + while (!isDone()) { + } + getResult() + } + } + } + + override fun resumeWith(result: Result) { + _decision.loop { decision -> + when (decision) { + UNDECIDED -> { + val r: C? = when { + result.isFailure -> { + val e = result.exceptionOrNull() + if (e is ShortCircuit) throw e else null + } + result.isSuccess -> result.getOrNull() + else -> TODO("Bug?") + } + + when { + r == null -> { + throw result.exceptionOrNull()!! +// parent.resumeWithException(result.exceptionOrNull()!!) +// return + } + _decision.compareAndSet(UNDECIDED, r) -> return + else -> Unit // loop again + } + } + else -> { // If not `UNDECIDED` then we need to pass result to `parent` + val res: Result = result.fold({ Result.success(it) }, { t -> + if (t is ShortCircuit) throw t //Result.success(t.recover()) + else Result.failure(t) + }) + send(res.getOrThrow()) + //parent.resumeWith(res) + return + } + } + } + } + + @PublishedApi // return the result + internal fun getResult(): Any? = + _decision.loop { decision -> + when (decision) { + UNDECIDED -> if (_decision.compareAndSet(UNDECIDED, SUSPENDED)) return COROUTINE_SUSPENDED + else -> return decision + } + } + + fun startCoroutineUninterceptedOrReturn(f: suspend () -> C): Any? = + try { + f.startCoroutineUninterceptedOrReturn(this)?.let { + if (it == COROUTINE_SUSPENDED) getResult() + else it + } + } catch (e: Throwable) { + if (e is ShortCircuit) throw e //e.recover() + else throw e + } + + fun isDone(): Boolean = + _decision.loop { + return when (it) { + UNDECIDED -> false + SUSPENDED -> false + else -> true + } + } + } + + val continuation = Continuation { send(prog(prompt)) } + + init { + continuation.run() + } +} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt new file mode 100644 index 000000000..a701e8685 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt @@ -0,0 +1,20 @@ +package arrow.continuations.reflect + +import arrow.core.ForListK +import arrow.core.ListK +import arrow.core.extensions.listk.monad.monad +import arrow.core.k + +fun main() { + val result = list { + val a: Int = listOf(1, 2, 3).k()() + val b: String = listOf("a", "b", "c").k()() + "$a$b" + } + println(result) +} + +fun list(program: suspend Reflect>.() -> A): List = + listOf(reify()(ListK.monad()) { + program(it as Reflect>) + }) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt new file mode 100644 index 000000000..4885be5ee --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt @@ -0,0 +1,90 @@ +package arrow.continuations.reflect + +import arrow.Kind +import arrow.core.Either +import arrow.core.Left +import arrow.core.Right +import arrow.typeclasses.Monad + +typealias In = suspend (Reflect) -> A + +sealed class Reflect { + abstract suspend operator fun F.invoke(): A +} + +class ReflectM(val prompt: Prompt) : Reflect() { + override suspend fun A.invoke(): B = + prompt.suspend(this) as B + // since we know the receiver of this suspend is the + // call to flatMap, the casts are safe +} + +/** + * for partially applying type arguments and better type inference + * + * reify [F] in { BLOCK } + * + * @usecase def reify[M[_]: Monad] in[R](prog: => R): M[R] + */ +fun reify(): ReifyBuilder = ReifyBuilder() + +class ReifyBuilder { + operator fun invoke( + MM: Monad, + prog: In + ): A = reifyImpl(MM) { prog(it) } +} + + +// this method is private since overloading and partially applying +// type parameters conflicts and results in non-helpful error messages. +// +// tradeoff of using `reify[M] in BLOCK` syntax over this function: +// + type inference on R +// - no type inference on M +// The latter might be a good thing since we want to make explicit +// which monad we are reifying. +private fun reifyImpl( + MM: Monad, + prog: In +): A { + + // The coroutine keeps sending monadic values until it completes + // with a monadic value + val coroutine = Coroutine { prompt -> + // capability to reflect M + val reflect = ReflectM(prompt) + MM.just(prog(reflect)) as A + } + + fun step(x: A): Either { + coroutine.continuation.resumeWith(Result.success(x)) + return if (coroutine.isDone()) + Right(coroutine.result()) + else + Left(coroutine.value()) + } + + fun run(): A = + if (coroutine.isDone()) + coroutine.result() + else { + MM.run { + MM.tailRecM(coroutine.value()) { f -> + when (f) { + is Kind<*, *> -> { + f as Kind + f.flatMap { f.map { step(it) } } + } + else -> { + val c = step(f as A) + just(step(f as A)) + } + } + } + //.flatten>() + } as A + } + + return run() +} From 59f0ace200cca7a499e3af50b58fa5b58084a928 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Mon, 20 Jul 2020 14:18:45 +0200 Subject: [PATCH 05/49] multiprompt progress --- .../arrow/continuations/contt/DelimCCX.kt | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt new file mode 100644 index 000000000..54d3c13b8 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt @@ -0,0 +1,108 @@ +package arrow.continuations.conttxxxx + +import arrow.core.ShortCircuit +import java.util.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* + +fun reset(body: suspend DelimitedScope.() -> T): T = + DelimitedScopeImpl().also { impl -> + body.startCoroutine(impl, impl) + }.runReset() + +interface DelimitedContinuation + +@RestrictsSuspension +abstract class DelimitedScope { + abstract suspend fun shift(block: suspend DelimitedScope.(DelimitedContinuation) -> T): R + abstract suspend operator fun DelimitedContinuation.invoke(value: R): T +} + +private typealias ShiftedFun = (DelimitedScope, DelimitedContinuation, Continuation) -> Any? + +@Suppress("UNCHECKED_CAST") +private class DelimitedScopeImpl : DelimitedScope(), Continuation, DelimitedContinuation { + private var shifted: ShiftedFun? = null + private var shiftCont: Continuation? = null + private var invokeCont: Continuation? = null + private var invokeValue: Any? = null + private var result: Result? = null + + override val context: CoroutineContext + get() = EmptyCoroutineContext + + override fun resumeWith(result: Result) { + this.result = result + } + + override suspend fun shift(block: suspend DelimitedScope.(DelimitedContinuation) -> T): R = + suspendCoroutineUninterceptedOrReturn { + this.shifted = block as ShiftedFun + this.shiftCont = it as Continuation + COROUTINE_SUSPENDED + } + + override suspend fun DelimitedContinuation.invoke(value: R): T = + suspendCoroutineUninterceptedOrReturn sc@{ + check(invokeCont == null) + invokeCont = it + invokeValue = value + COROUTINE_SUSPENDED + } + + fun runReset(): T { + // This is the stack of continuation in the `shift { ... }` after call to delimited continuation + var currentCont: Continuation = this + // Trampoline loop to avoid call stack usage + loop@while (true) { + // Call shift { ... } body or break if there are no more shift calls + val shifted = takeShifted() ?: break + // If shift does not call any continuation, then its value becomes the result -- break out of the loop + try { + val value = shifted.invoke(this, this, currentCont) + if (value !== COROUTINE_SUSPENDED) { + result = Result.success(value as T) + break + } + } catch (e: Throwable) { + result = Result.failure(e) + break + } + // Shift has suspended - check if shift { ... } body had invoked continuation + currentCont = takeInvokeCont() ?: continue@loop + val shiftCont = takeShiftCont() + ?: error("Delimited continuation is single-shot and cannot be invoked twice") + shiftCont.resume(invokeValue) + } + // Propagate the result to all pending continuations in shift { ... } bodies + val res = when (val r = result) { + null -> TODO("Impossible result is null") + else -> r + } + currentCont.resumeWith(res) + // Return the final result + return res.getOrThrow() + } + + private fun takeShifted() = shifted?.also { shifted = null } + private fun takeShiftCont() = shiftCont?.also { shiftCont = null } + private fun takeInvokeCont() = invokeCont?.also { invokeCont = null } +} + +suspend fun DelimitedScope>.bind(list: List): B = + shift { cb -> + list.fold(emptyList()) { acc, b -> + acc + cb(b) + } + } + + +fun main() { + val result: List = reset { + val a = bind(listOf(1, 2, 3)) + val b = bind(listOf("a", "b", "c")) + reset { listOf("$a$b ") } + } + println(result) +} + From 3e9635e2692c7ae9bc066bf387b33276fa485c34 Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Mon, 20 Jul 2020 20:05:31 +0200 Subject: [PATCH 06/49] Follow suspend value into prompt --- .../arrow/continuations/reflect/Coroutine.kt | 14 ++++++---- .../arrow/continuations/reflect/program.kt | 28 ++++++++++++------- .../arrow/continuations/reflect/reify.kt | 13 ++++++--- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt index 51f547721..505e9c02f 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt @@ -50,12 +50,15 @@ class Coroutine(prog: suspend (Prompt) -> C) { private open inner class InnerPrompt : Prompt /*ContinuationScope("cats-reflect")*/ { override suspend fun suspend(value: A): B { - return suspendCoroutineUninterceptedOrReturn { + return suspendCoroutineUninterceptedOrReturn { cont -> send(value) + println("Put value in channel: $value in $channel") + val res = continuation.getResult() + if (res == COROUTINE_SUSPENDED) COROUTINE_SUSPENDED else { - it.resumeWith(Result.success(res) as Result) + cont.resumeWith(Result.success(res) as Result) COROUTINE_SUSPENDED } } @@ -97,6 +100,7 @@ class Coroutine(prog: suspend (Prompt) -> C) { when { r == null -> { + println("resumeWith: result = $result") throw result.exceptionOrNull()!! // parent.resumeWithException(result.exceptionOrNull()!!) // return @@ -107,11 +111,11 @@ class Coroutine(prog: suspend (Prompt) -> C) { } else -> { // If not `UNDECIDED` then we need to pass result to `parent` val res: Result = result.fold({ Result.success(it) }, { t -> - if (t is ShortCircuit) throw t //Result.success(t.recover()) + if (t is ShortCircuit) throw t // Result.success(t.recover()) else Result.failure(t) }) send(res.getOrThrow()) - //parent.resumeWith(res) + // parent.resumeWith(res) return } } @@ -134,7 +138,7 @@ class Coroutine(prog: suspend (Prompt) -> C) { else it } } catch (e: Throwable) { - if (e is ShortCircuit) throw e //e.recover() + if (e is ShortCircuit) throw e // e.recover() else throw e } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt index a701e8685..cd98ec937 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt @@ -1,20 +1,28 @@ package arrow.continuations.reflect -import arrow.core.ForListK import arrow.core.ListK import arrow.core.extensions.listk.monad.monad import arrow.core.k fun main() { - val result = list { - val a: Int = listOf(1, 2, 3).k()() - val b: String = listOf("a", "b", "c").k()() - "$a$b" - } - println(result) +// val result = list { +// val a: Int = listOf(1, 2, 3).k()() +// val b: String = listOf("a", "b", "c").k()() +// "$a$b" +// } +// println(result) + list { + val a: Int = listOf(1).k().invoke() + println("I came here $a") + "$a" + }.let(::println) } -fun list(program: suspend Reflect>.() -> A): List = - listOf(reify()(ListK.monad()) { +fun list(program: suspend Reflect>.() -> A): List { + val a = reify(ListK.monad()) { + println("Starting with: $it") program(it as Reflect>) - }) + } + + return listOf(a) +} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt index 4885be5ee..9365354cf 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt @@ -13,8 +13,10 @@ sealed class Reflect { } class ReflectM(val prompt: Prompt) : Reflect() { - override suspend fun A.invoke(): B = - prompt.suspend(this) as B + override suspend fun A.invoke(): B { + println("invokeSuspend: $prompt") + return prompt.suspend(this) as B + } // since we know the receiver of this suspend is the // call to flatMap, the casts are safe } @@ -28,6 +30,9 @@ class ReflectM(val prompt: Prompt) : Reflect() { */ fun reify(): ReifyBuilder = ReifyBuilder() +fun reify(MM: Monad, prog: In): A = + reifyImpl(MM) { prog(it) } + class ReifyBuilder { operator fun invoke( MM: Monad, @@ -35,7 +40,6 @@ class ReifyBuilder { ): A = reifyImpl(MM) { prog(it) } } - // this method is private since overloading and partially applying // type parameters conflicts and results in non-helpful error messages. // @@ -58,6 +62,7 @@ private fun reifyImpl( } fun step(x: A): Either { + println("Step : $x") coroutine.continuation.resumeWith(Result.success(x)) return if (coroutine.isDone()) Right(coroutine.result()) @@ -82,7 +87,7 @@ private fun reifyImpl( } } } - //.flatten>() + // .flatten>() } as A } From f65cfaa88baaac0d76ab3906baaa3cfe58b00476 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Tue, 21 Jul 2020 14:26:57 +0200 Subject: [PATCH 07/49] progress better typing reflect example and advancing --- .../arrow/continuations/reflect/Coroutine.kt | 24 +++---- .../arrow/continuations/reflect/program.kt | 17 ++--- .../arrow/continuations/reflect/reify.kt | 62 +++++++++---------- 3 files changed, 52 insertions(+), 51 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt index 505e9c02f..a759c22ad 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt @@ -48,24 +48,19 @@ class Coroutine(prog: suspend (Prompt) -> C) { return v as A } - private open inner class InnerPrompt : Prompt /*ContinuationScope("cats-reflect")*/ { + open inner class InnerPrompt : Prompt /*ContinuationScope("cats-reflect")*/ { override suspend fun suspend(value: A): B { + send(value) return suspendCoroutineUninterceptedOrReturn { cont -> - send(value) println("Put value in channel: $value in $channel") - - val res = continuation.getResult() - - if (res == COROUTINE_SUSPENDED) COROUTINE_SUSPENDED - else { - cont.resumeWith(Result.success(res) as Result) - COROUTINE_SUSPENDED - } + val res = receive() + if (isDone()) cont.resumeWith(Result.success(res)) + COROUTINE_SUSPENDED } } } - private val prompt = InnerPrompt() + val prompt = InnerPrompt() @RestrictsSuspension inner class Continuation( @@ -74,9 +69,14 @@ class Coroutine(prog: suspend (Prompt) -> C) { override val context: CoroutineContext = EmptyCoroutineContext + var started = false + fun run(): Unit { continuation.startCoroutineUninterceptedOrReturn { - f() + if (!started) { + f() + started = true + } suspendCoroutineUninterceptedOrReturn { while (!isDone()) { } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt index cd98ec937..0d2d7da16 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt @@ -1,7 +1,9 @@ package arrow.continuations.reflect +import arrow.core.ForListK import arrow.core.ListK import arrow.core.extensions.listk.monad.monad +import arrow.core.fix import arrow.core.k fun main() { @@ -12,17 +14,18 @@ fun main() { // } // println(result) list { - val a: Int = listOf(1).k().invoke() - println("I came here $a") - "$a" + val a: Int = listOf(1, 2, 3).k()() + val b: Int = listOf(1, 3, 4).k()() + println("I came here $a$b") + a + b }.let(::println) } -fun list(program: suspend Reflect>.() -> A): List { - val a = reify(ListK.monad()) { +inline fun list(crossinline program: suspend Reflect.() -> Int): List { + val a = reify(ListK.monad()) { println("Starting with: $it") - program(it as Reflect>) + program(it) } - return listOf(a) + return a.fix() } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt index 9365354cf..4e3c01bfc 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt @@ -4,18 +4,20 @@ import arrow.Kind import arrow.core.Either import arrow.core.Left import arrow.core.Right +import arrow.core.identity import arrow.typeclasses.Monad -typealias In = suspend (Reflect) -> A +typealias In = suspend (Reflect) -> A -sealed class Reflect { - abstract suspend operator fun F.invoke(): A +sealed class Reflect { + abstract suspend operator fun Kind.invoke(): A } -class ReflectM(val prompt: Prompt) : Reflect() { - override suspend fun A.invoke(): B { +class ReflectM(val prompt: Prompt, *>) : Reflect() { + override suspend fun Kind.invoke(): A { println("invokeSuspend: $prompt") - return prompt.suspend(this) as B + val result = prompt.suspend(this) + return result as A } // since we know the receiver of this suspend is the // call to flatMap, the casts are safe @@ -30,14 +32,14 @@ class ReflectM(val prompt: Prompt) : Reflect() { */ fun reify(): ReifyBuilder = ReifyBuilder() -fun reify(MM: Monad, prog: In): A = +fun reify(MM: Monad, prog: In): Kind = reifyImpl(MM) { prog(it) } class ReifyBuilder { operator fun invoke( MM: Monad, prog: In - ): A = reifyImpl(MM) { prog(it) } + ): Kind = reifyImpl(MM) { prog(it) } } // this method is private since overloading and partially applying @@ -51,45 +53,41 @@ class ReifyBuilder { private fun reifyImpl( MM: Monad, prog: In -): A { +): Kind { + + var currentPrompt: Prompt, Any?>? = null // The coroutine keeps sending monadic values until it completes // with a monadic value - val coroutine = Coroutine { prompt -> + val coroutine = Coroutine, Any?, A> { prompt -> + currentPrompt = prompt // capability to reflect M val reflect = ReflectM(prompt) - MM.just(prog(reflect)) as A + prog(reflect) } - fun step(x: A): Either { + fun step(x: A): Either, A> { println("Step : $x") - coroutine.continuation.resumeWith(Result.success(x)) return if (coroutine.isDone()) Right(coroutine.result()) - else + else { + coroutine.continuation.resumeWith(Result.success(x)) Left(coroutine.value()) + } } - fun run(): A = - if (coroutine.isDone()) - coroutine.result() - else { - MM.run { - MM.tailRecM(coroutine.value()) { f -> - when (f) { - is Kind<*, *> -> { - f as Kind - f.flatMap { f.map { step(it) } } - } - else -> { - val c = step(f as A) - just(step(f as A)) - } - } - } + fun run(): Kind = + MM.run { + if (coroutine.isDone()) + coroutine.result().just() + else { + MM.tailRecM(coroutine.value()) { + it.map(::step) + }.flatMap { just(it) } // .flatten>() - } as A + } } + return run() } From 550e5990d4f97d45c117b8300d8fa837f9180c2c Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Thu, 23 Jul 2020 12:30:50 +0200 Subject: [PATCH 08/49] cont adt --- .../kotlin/arrow/continuations/adt/cont.kt | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt new file mode 100644 index 000000000..134f4e207 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt @@ -0,0 +1,105 @@ +package arrow.continuations.adt + +import java.util.* +import kotlin.coroutines.Continuation +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.startCoroutine +import kotlin.coroutines.suspendCoroutine + +val frames: Stack> = Stack() + +sealed class Cont { + + class Reset( + val f: suspend Reset.() -> A + ) : Cont() { + + val shifts: Stack> = Stack() + + inner class Shift( + val f: suspend Shift.((B) -> B) -> A + ) : Cont() { + init { + shifts.push(this) + } + val parent: Reset = this@Reset + } + + } +} + +suspend inline fun reset( + noinline f: suspend Cont.Reset<*>.() -> A +): A = Cont.Reset(f).yield() + +suspend inline fun Cont.Reset<*>.shift( + noinline f: suspend Cont.Reset.Shift.((B) -> B) -> A +): B { + this as Cont.Reset + return Shift(f).yield() +} + +tailrec suspend fun Cont.yield(): A = + when (this) { + is Cont.Reset -> { + if (shifts.isNotEmpty()) shifts.pop().yield() as A + else suspendCoroutine { ca -> + val body = suspend { + if (frames.isNotEmpty()) frames.pop().yield() as A + else null + } + var res: A? = null + body.startCoroutine(object : Continuation { + override val context: CoroutineContext = EmptyCoroutineContext + override fun resumeWith(result: Result) { + println("Resume shift: ${result}") + res = result.getOrNull() + } + }) + res?.let { ca.resumeWith(Result.success(it)) } + } + } + is Cont.Reset<*>.Shift -> { + frames as Stack> + this as Cont.Reset.Shift + val body: suspend () -> A = suspend { + f { a -> // each bound element when a shift is called within the body + println("push $a") + frames.push(Cont.Reset { a }) + a + } + } + var res: A? = null + body.startCoroutine(object : Continuation { + override val context: CoroutineContext = EmptyCoroutineContext + override fun resumeWith(result: Result) { + println("Resume shift: ${result}") + res = result.getOrNull() + } + }) + res!! + } + } + +suspend inline operator fun Cont.Reset<*>.times(fa: List): A = + shift, A> { cb -> + fa.flatMap { + listOf(cb(it)) + } + } + + +suspend fun main() { + val result = + reset { + val a: Int = this * listOf(1, 2, 3) + val b: String = this * listOf("a", "b", "c") + listOf("$a$b") + } + println(result) +} + + + + From 727948f64e057671e9bcbe1fcae3e541d838213a Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Thu, 23 Jul 2020 20:00:14 +0200 Subject: [PATCH 09/49] Reflect based on List and tailRecM --- .../arrow/continuations/reflect2/Coroutine.kt | 77 ++++++++++++++++ .../continuations/reflect2/UnsafePromise.kt | 69 ++++++++++++++ .../arrow/continuations/reflect2/program.kt | 22 +++++ .../arrow/continuations/reflect2/reify.kt | 90 +++++++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/UnsafePromise.kt create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/program.kt create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/reify.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt new file mode 100644 index 000000000..fe81135b1 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt @@ -0,0 +1,77 @@ +package arrow.continuations.reflect2 + +package arrow.continuations + +import arrow.fx.coroutines.UnsafePromise +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn + +interface Prompt { + suspend fun suspend(value: S): R +} + +class Coroutine(prog: suspend (Prompt) -> T) { + + fun isDone() = co.isDone() + + fun value(): S { + assert(!isDone()); + return receive() + } + + fun result(): T { + assert(isDone()) + return receive() + } + + fun resume(v: R): Unit { + assert(!isDone()) + send(v) + co.run() + } + + private var channel: Any? = null + private fun send(v: Any?) { + channel = v + } + + private fun receive(): A { + val v = channel + return v as A + } + + val yielder = UnsafePromise() + val prompt = object : Prompt { + override suspend fun suspend(value: S): R { + send(value) + yielder.join() // Continuation yield prompt + return receive() + } + } + + private val co = Continuation(prompt, yielder) { send(prog(prompt)) } + + init { + co.run() + } +} + +class Continuation( + val prompt: Prompt<*, *>, + val yielder: UnsafePromise, + val f: suspend () -> Unit +) { + + fun isDone(): Boolean = + yielder.isNotEmpty() + + // The run method returns true when the continuation terminates, and false if it suspends. + fun run(): Boolean { + val a = f.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext, yielder::complete)) + return a != COROUTINE_SUSPENDED + } +} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/UnsafePromise.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/UnsafePromise.kt new file mode 100644 index 000000000..e770ebef4 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/UnsafePromise.kt @@ -0,0 +1,69 @@ +package arrow.fx.coroutines + +import kotlinx.atomicfu.AtomicRef +import kotlinx.atomicfu.atomic +import kotlin.coroutines.suspendCoroutine + +/** + * An eager Promise implementation to bridge results across processes internally. + * @see ForkAndForget + */ +class UnsafePromise { + + private sealed class State { + object Empty : State() + data class Waiting(val joiners: List<(Result) -> Unit>) : State() + + @Suppress("RESULT_CLASS_IN_RETURN_TYPE") + data class Full(val a: Result) : State() + } + + private val state: AtomicRef> = atomic(State.Empty) + + fun isNotEmpty(): Boolean = + when (state.value) { + is State.Full -> true + else -> false + } + + @Suppress("RESULT_CLASS_IN_RETURN_TYPE") + fun tryGet(): Result? = + when (val curr = state.value) { + is State.Full -> curr.a + else -> null + } + + fun get(cb: (Result) -> Unit) { + tailrec fun go(): Unit = when (val oldState = state.value) { + State.Empty -> if (state.compareAndSet(oldState, State.Waiting(listOf(cb)))) Unit else go() + is State.Waiting -> if (state.compareAndSet(oldState, State.Waiting(oldState.joiners + cb))) Unit else go() + is State.Full -> cb(oldState.a) + } + + go() + } + + suspend fun join(): A = + suspendCoroutine { cb -> + get(cb::resumeWith) + } + + fun complete(value: Result) { + tailrec fun go(): Unit = when (val oldState = state.value) { + State.Empty -> if (state.compareAndSet(oldState, State.Full(value))) Unit else go() + is State.Waiting -> { + if (state.compareAndSet(oldState, State.Full(value))) oldState.joiners.forEach { it(value) } + else go() + } + is State.Full -> throw RuntimeException("Boom!") + } + + go() + } + + fun remove(cb: (Result) -> Unit) = when (val oldState = state.value) { + State.Empty -> Unit + is State.Waiting -> state.value = State.Waiting(oldState.joiners - cb) + is State.Full -> Unit + } +} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/program.kt new file mode 100644 index 000000000..18313dd5f --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/program.kt @@ -0,0 +1,22 @@ +package arrow.continuations.reflect2 + +import arrow.core.ForListK +import arrow.core.ListK +import arrow.core.extensions.listk.monad.monad +import arrow.core.fix +import arrow.core.k + +fun main() { + list { + val a: Int = listOf(1, 2, 3).invoke() +// val b: Int = listOf(1, 3, 4).invoke() + println("I came here $a") + a + }.let(::println) +} + +inline fun list(crossinline program: suspend Reflect/**/.() -> Int): List = + reify { + println("Starting with: $it") + listOf(program(it)) + } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/reify.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/reify.kt new file mode 100644 index 000000000..81b003ffc --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/reify.kt @@ -0,0 +1,90 @@ +package arrow.continuations.reflect2 + +import arrow.Kind +import arrow.core.* +import arrow.core.extensions.either.applicative.applicative +import arrow.core.extensions.either.applicative.map +import arrow.core.extensions.either.apply.apEval +import arrow.core.extensions.list.traverse.traverse +import arrow.core.extensions.listk.monad.flatMap +import arrow.typeclasses.Applicative + +sealed class Reflect { + abstract suspend operator fun List.invoke(): Int +} + +class ReflectM(val prompt: Prompt, Any?>) : Reflect() { + override suspend fun List.invoke(): Int { +// println("invokeSuspend: $prompt") + val result = prompt.suspend(this) + println("Result: $result") + return result as Int + } + // since we know the receiver of this suspend is the + // call to flatMap, the casts are safe +} + +@PublishedApi +internal fun reify(prog: suspend (Reflect) -> List): List { + +// var currentPrompt: Prompt? = null + + // The coroutine keeps sending monadic values until it completes + // with a monadic value + val coroutine = Coroutine, Any?, List> { prompt -> +// currentPrompt = prompt + // capability to reflect M + val reflect = ReflectM(prompt) + prog(reflect) + } + + fun step(x: Int): Either, List> { + println("Step : $x") + coroutine.resume(x) + return if (coroutine.isDone()) Right(coroutine.result()) + else Left(coroutine.value()) + } + + fun run(): List = + if (coroutine.isDone()) coroutine.result() + else tailRecM(coroutine.value()) { values -> + val r = values.map(::step) + println("run#tailRecM: $r") + r + }.flatten() + + return run() +} + +@Suppress("UNCHECKED_CAST") +private tailrec fun go( + buf: ArrayList, + f: (A) -> List>, + v: List> +) { + if (v.isNotEmpty()) { + val head: Either = v.first() + println("head: $head") + when (head) { + is Either.Right -> { + println("Right?") + buf += head.b + go(buf, f, v.drop(1).k()) + } + is Either.Left -> { + val head: A = head.a + val newHead = f(head) + println("Left: $head, newHead: $newHead") + val newRes = (newHead + v.drop(1)) + println("Head: $head, newHead: $newHead, newRes: $newRes") + go(buf, f, newRes) + } + } + } +} + +fun tailRecM(a: A, f: (A) -> List>): List { + val buf = ArrayList() + go(buf, f, f(a)) + return ListK(buf) +} From b462a1e6451504eb1f15ebb71aa4f42e0a6ff3ba Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Fri, 24 Jul 2020 01:26:16 +0200 Subject: [PATCH 10/49] progress advancing first iteration until suspended reset with stacks for all states --- .../arrow/continuations/contt/DelimCCX.kt | 134 +++++++++++------- 1 file changed, 82 insertions(+), 52 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt index 54d3c13b8..6d4c04b29 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt @@ -1,18 +1,19 @@ package arrow.continuations.conttxxxx -import arrow.core.ShortCircuit import java.util.* import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* -fun reset(body: suspend DelimitedScope.() -> T): T = +suspend fun reset(body: suspend DelimitedScope.() -> T): T = DelimitedScopeImpl().also { impl -> body.startCoroutine(impl, impl) }.runReset() -interface DelimitedContinuation +interface DelimitedContinuation { + fun addResult(result: Result): Unit +} -@RestrictsSuspension +//@RestrictsSuspension abstract class DelimitedScope { abstract suspend fun shift(block: suspend DelimitedScope.(DelimitedContinuation) -> T): R abstract suspend operator fun DelimitedContinuation.invoke(value: R): T @@ -22,86 +23,115 @@ private typealias ShiftedFun = (DelimitedScope, DelimitedContinuation : DelimitedScope(), Continuation, DelimitedContinuation { - private var shifted: ShiftedFun? = null - private var shiftCont: Continuation? = null - private var invokeCont: Continuation? = null - private var invokeValue: Any? = null - private var result: Result? = null + private val shiftedBody: Stack> = Stack() + private var shiftCont: Stack> = Stack() + private var invokeCont: Stack> = Stack() + private var invokeValue: Stack = Stack() + private var completions: Stack> = Stack() override val context: CoroutineContext get() = EmptyCoroutineContext override fun resumeWith(result: Result) { - this.result = result + completions.push(result) + } + + override fun addResult(result: Result) { + completions.push(result) } override suspend fun shift(block: suspend DelimitedScope.(DelimitedContinuation) -> T): R = suspendCoroutineUninterceptedOrReturn { - this.shifted = block as ShiftedFun - this.shiftCont = it as Continuation + this.shiftedBody.push(block as ShiftedFun) + this.shiftCont.push(it as Continuation) COROUTINE_SUSPENDED } override suspend fun DelimitedContinuation.invoke(value: R): T = suspendCoroutineUninterceptedOrReturn sc@{ - check(invokeCont == null) - invokeCont = it - invokeValue = value + //check(invokeCont == null) + invokeCont.push(it) + invokeValue.push(value) COROUTINE_SUSPENDED } - fun runReset(): T { - // This is the stack of continuation in the `shift { ... }` after call to delimited continuation - var currentCont: Continuation = this - // Trampoline loop to avoid call stack usage - loop@while (true) { - // Call shift { ... } body or break if there are no more shift calls - val shifted = takeShifted() ?: break - // If shift does not call any continuation, then its value becomes the result -- break out of the loop - try { - val value = shifted.invoke(this, this, currentCont) - if (value !== COROUTINE_SUSPENDED) { - result = Result.success(value as T) - break + suspend fun runReset(): T = + suspendCoroutineUninterceptedOrReturn { resetCont -> + println("starts runReset") + // This is the stack of continuation in the `shift { ... }` after call to delimited continuation + var currentCont: Continuation = this + //var result: Result? = null + // Trampoline loop to avoid call stack usage + loop@ while (true) { + println("start looping while true") + try { + // Call shift { ... } body or break if there are no more shift calls + // If shift does not call any continuation, then its value is pushed and break out of the loop + val shifted = takeShifted() ?: break + val value = shifted.invoke(this, this, currentCont) + println("value from shift: $value") + if (value !== COROUTINE_SUSPENDED) { + println("completion: $value") + completions.push(Result.success(value as T)) + //continue@loop + //break //TODO or loop again? + } + } catch (e: Throwable) { + println("completion: $e") + completions.push(Result.failure(e)) + //continue@loop + //break //TODO or loop again? + } + // Shift has suspended - check if shift { ... } body had invoked continuation + while (shiftCont.isNotEmpty()) { + println("starts shift loop") + currentCont = takeInvokeCont() ?: continue@loop + val shift = takeShiftCont() + ?: error("Delimited continuation is single-shot and cannot be invoked twice") + val invokeVal = invokeValue.pop() + println("invoke Value: $invokeVal") + shift.resumeWith(Result.success(invokeVal as T)) + println("after shift resume with $invokeVal") + println("end shift loop") + continue@loop } - } catch (e: Throwable) { - result = Result.failure(e) - break } - // Shift has suspended - check if shift { ... } body had invoked continuation - currentCont = takeInvokeCont() ?: continue@loop - val shiftCont = takeShiftCont() - ?: error("Delimited continuation is single-shot and cannot be invoked twice") - shiftCont.resume(invokeValue) - } - // Propagate the result to all pending continuations in shift { ... } bodies - val res = when (val r = result) { - null -> TODO("Impossible result is null") - else -> r + // Propagate the result to all pending continuations in shift { ... } bodies + if (completions.isNotEmpty()) { + when (val r = completions.pop()) { + null -> TODO("Impossible result is null") + else -> { + suspend { + completions.push(r) + currentCont.resumeWith(r) + runReset() + }.startCoroutine(this) + // Return the final result + //resetCont.resumeWith(r) + } + } + } + println("SUSPENDING RESET") + COROUTINE_SUSPENDED } - currentCont.resumeWith(res) - // Return the final result - return res.getOrThrow() - } - private fun takeShifted() = shifted?.also { shifted = null } - private fun takeShiftCont() = shiftCont?.also { shiftCont = null } - private fun takeInvokeCont() = invokeCont?.also { invokeCont = null } + private fun takeShifted() = if (shiftedBody.isNotEmpty()) shiftedBody.pop() else null + private fun takeShiftCont() = if (shiftCont.isNotEmpty()) shiftCont.pop() else null + private fun takeInvokeCont() = if (invokeCont.isNotEmpty()) invokeCont.pop() else null } suspend fun DelimitedScope>.bind(list: List): B = shift { cb -> - list.fold(emptyList()) { acc, b -> + list.fold(emptyList()) { acc, b -> acc + cb(b) } } - -fun main() { +suspend fun main() { val result: List = reset { val a = bind(listOf(1, 2, 3)) val b = bind(listOf("a", "b", "c")) - reset { listOf("$a$b ") } + listOf("$a$b ") } println(result) } From 027e55ac662e3059ab01e8192672951c165e5f19 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sat, 25 Jul 2020 04:43:42 +0200 Subject: [PATCH 11/49] moar progress advancing states --- .../arrow/continuations/contt/DelimCCX.kt | 138 ++++++++++-------- 1 file changed, 81 insertions(+), 57 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt index 6d4c04b29..d0ffcde33 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt @@ -5,13 +5,12 @@ import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* suspend fun reset(body: suspend DelimitedScope.() -> T): T = - DelimitedScopeImpl().also { impl -> - body.startCoroutine(impl, impl) - }.runReset() + DelimitedScopeImpl().run { + body.startCoroutine(this, this) + runReset() + } -interface DelimitedContinuation { - fun addResult(result: Result): Unit -} +interface DelimitedContinuation //@RestrictsSuspension abstract class DelimitedScope { @@ -29,17 +28,12 @@ private class DelimitedScopeImpl : DelimitedScope(), Continuation, Deli private var invokeValue: Stack = Stack() private var completions: Stack> = Stack() - override val context: CoroutineContext - get() = EmptyCoroutineContext + override val context: CoroutineContext = EmptyCoroutineContext override fun resumeWith(result: Result) { completions.push(result) } - override fun addResult(result: Result) { - completions.push(result) - } - override suspend fun shift(block: suspend DelimitedScope.(DelimitedContinuation) -> T): R = suspendCoroutineUninterceptedOrReturn { this.shiftedBody.push(block as ShiftedFun) @@ -55,65 +49,95 @@ private class DelimitedScopeImpl : DelimitedScope(), Continuation, Deli COROUTINE_SUSPENDED } + // This is the stack of continuation in the `shift { ... }` after call to delimited continuation + var currentCont: Continuation = this + suspend fun runReset(): T = - suspendCoroutineUninterceptedOrReturn { resetCont -> - println("starts runReset") - // This is the stack of continuation in the `shift { ... }` after call to delimited continuation - var currentCont: Continuation = this + suspendCoroutineUninterceptedOrReturn { parent -> + println("[SUSPENDED] runReset()") //var result: Result? = null // Trampoline loop to avoid call stack usage - loop@ while (true) { - println("start looping while true") - try { - // Call shift { ... } body or break if there are no more shift calls - // If shift does not call any continuation, then its value is pushed and break out of the loop - val shifted = takeShifted() ?: break - val value = shifted.invoke(this, this, currentCont) - println("value from shift: $value") - if (value !== COROUTINE_SUSPENDED) { - println("completion: $value") - completions.push(Result.success(value as T)) - //continue@loop - //break //TODO or loop again? - } - } catch (e: Throwable) { - println("completion: $e") - completions.push(Result.failure(e)) - //continue@loop - //break //TODO or loop again? - } + resetLoop@ while (true) { + println("\t-> resetLoop") + if (pushCompletion(currentCont)) break // Shift has suspended - check if shift { ... } body had invoked continuation - while (shiftCont.isNotEmpty()) { - println("starts shift loop") - currentCont = takeInvokeCont() ?: continue@loop + shiftLoop@ while (shiftCont.isNotEmpty()) { + println("\t\t-> shiftLoop") + currentCont = takeInvokeCont() ?: continue@resetLoop val shift = takeShiftCont() ?: error("Delimited continuation is single-shot and cannot be invoked twice") val invokeVal = invokeValue.pop() - println("invoke Value: $invokeVal") + println("\t\t!! resumeWith: $invokeVal") shift.resumeWith(Result.success(invokeVal as T)) - println("after shift resume with $invokeVal") - println("end shift loop") - continue@loop + println("\t\t<- shift loop") } + // Propagate the result to all pending continuations in shift { ... } bodies + if (propagateCompletions(currentCont as Continuation)) continue@resetLoop + println("\t<- resetLoop") } - // Propagate the result to all pending continuations in shift { ... } bodies - if (completions.isNotEmpty()) { - when (val r = completions.pop()) { - null -> TODO("Impossible result is null") - else -> { - suspend { + COROUTINE_SUSPENDED + } + + private fun propagateCompletions(currentCont: Continuation?): Boolean { + if (completions.isNotEmpty()) { + when (val r = completions.pop()) { +// null -> TODO("Impossible result is null") + else -> { + println("\t<- propagateCompletion: $r") + //completions.push(r) + //resume first shot if invoked + + val block: suspend DelimitedScope.(cont: DelimitedContinuation) -> T = { + suspendCoroutineUninterceptedOrReturn { + println("\t\t!! [shifted yield on]: $r") + //check(invokeCont == null) + //invokeCont.pop().resumeWith(r) + COROUTINE_SUSPENDED + } + } + + val prompt = suspend { + println("\t*> New Prompt starts for `$r`: $this") completions.push(r) - currentCont.resumeWith(r) + invokeCont.push(currentCont) + shiftedBody.push(block as ShiftedFun) + shiftCont.push(currentCont as Continuation) runReset() - }.startCoroutine(this) - // Return the final result - //resetCont.resumeWith(r) } + prompt.startCoroutine(this) + + //proceed to next value if invoked + //invokeValue.push(r.getOrThrow()) + + return true + // Return the final result + //resetCont.resumeWith(r) } } - println("SUSPENDING RESET") - COROUTINE_SUSPENDED } + return false + } + + private fun pushCompletion(currentCont: Continuation): Boolean { + try { + // Call shift { ... } body or break if there are no more shift calls + // If shift does not call any continuation, then its value is pushed and break out of the loop + val shifted = takeShifted() ?: return true + val value = shifted.invoke(this, this, currentCont) + if (value !== COROUTINE_SUSPENDED) { + println("-> pushCompletion: $value") + completions.push(Result.success(value as T)) + //continue@loop + //break //TODO or loop again? + } + } catch (e: Throwable) { + println("-> pushCompletion: $e") + completions.push(Result.failure(e)) + //continue@loop + //break //TODO or loop again? + } + return false + } private fun takeShifted() = if (shiftedBody.isNotEmpty()) shiftedBody.pop() else null private fun takeShiftCont() = if (shiftCont.isNotEmpty()) shiftCont.pop() else null @@ -122,8 +146,8 @@ private class DelimitedScopeImpl : DelimitedScope(), Continuation, Deli suspend fun DelimitedScope>.bind(list: List): B = shift { cb -> - list.fold(emptyList()) { acc, b -> - acc + cb(b) + list.flatMap { + cb(it) } } From 0a412e5b868c9539fc37d8a34d6e71688eb4d958 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sat, 25 Jul 2020 04:56:32 +0200 Subject: [PATCH 12/49] fix compilation --- .../src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt index fe81135b1..02d5bdce5 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt @@ -1,7 +1,5 @@ package arrow.continuations.reflect2 -package arrow.continuations - import arrow.fx.coroutines.UnsafePromise import kotlinx.atomicfu.atomic import kotlinx.atomicfu.loop From ee85ff9181b051be8e2640f09edc4a8c753d4f40 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sat, 25 Jul 2020 15:17:35 +0200 Subject: [PATCH 13/49] logging states --- .../arrow/continuations/contt/DelimCCX.kt | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt index d0ffcde33..0bfac1566 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt @@ -28,6 +28,17 @@ private class DelimitedScopeImpl : DelimitedScope(), Continuation, Deli private var invokeValue: Stack = Stack() private var completions: Stack> = Stack() + private fun stateHeader(): String = + "shiftedBody\tshiftCont\tinvokeCont\tinvokeValue\tcompletions" + + private fun stateLog(): String = + "${shiftedBody.size}\t\t\t${shiftCont.size}\t\t\t${invokeCont.size}\t\t\t${invokeValue.size}\t\t\t${completions.size}" + + + private fun log(value: String): Unit { + println("${stateLog()}\t\t\t$value") + } + override val context: CoroutineContext = EmptyCoroutineContext override fun resumeWith(result: Result) { @@ -54,26 +65,27 @@ private class DelimitedScopeImpl : DelimitedScope(), Continuation, Deli suspend fun runReset(): T = suspendCoroutineUninterceptedOrReturn { parent -> - println("[SUSPENDED] runReset()") + println(stateHeader()) + log("[SUSPENDED] runReset()") //var result: Result? = null // Trampoline loop to avoid call stack usage resetLoop@ while (true) { - println("\t-> resetLoop") + log("\t-> resetLoop") if (pushCompletion(currentCont)) break // Shift has suspended - check if shift { ... } body had invoked continuation shiftLoop@ while (shiftCont.isNotEmpty()) { - println("\t\t-> shiftLoop") + log("\t\t-> shiftLoop") currentCont = takeInvokeCont() ?: continue@resetLoop val shift = takeShiftCont() ?: error("Delimited continuation is single-shot and cannot be invoked twice") val invokeVal = invokeValue.pop() - println("\t\t!! resumeWith: $invokeVal") + log("\t\t!! resumeWith: $invokeVal") shift.resumeWith(Result.success(invokeVal as T)) - println("\t\t<- shift loop") + log("\t\t<- shift loop") } // Propagate the result to all pending continuations in shift { ... } bodies if (propagateCompletions(currentCont as Continuation)) continue@resetLoop - println("\t<- resetLoop") + log("\t<- resetLoop") } COROUTINE_SUSPENDED } @@ -83,33 +95,23 @@ private class DelimitedScopeImpl : DelimitedScope(), Continuation, Deli when (val r = completions.pop()) { // null -> TODO("Impossible result is null") else -> { - println("\t<- propagateCompletion: $r") + log("\t<- propagateCompletion: $r") //completions.push(r) //resume first shot if invoked val block: suspend DelimitedScope.(cont: DelimitedContinuation) -> T = { - suspendCoroutineUninterceptedOrReturn { - println("\t\t!! [shifted yield on]: $r") - //check(invokeCont == null) - //invokeCont.pop().resumeWith(r) + suspendCoroutineUninterceptedOrReturn { + //shiftedBody.push(block as ShiftedFun) + shiftCont.push(it as Continuation) COROUTINE_SUSPENDED } } - - val prompt = suspend { - println("\t*> New Prompt starts for `$r`: $this") - completions.push(r) - invokeCont.push(currentCont) - shiftedBody.push(block as ShiftedFun) - shiftCont.push(currentCont as Continuation) - runReset() - } - prompt.startCoroutine(this) + suspend { block(this) }.startCoroutine(this) //proceed to next value if invoked //invokeValue.push(r.getOrThrow()) - return true + return false // Return the final result //resetCont.resumeWith(r) } @@ -125,13 +127,13 @@ private class DelimitedScopeImpl : DelimitedScope(), Continuation, Deli val shifted = takeShifted() ?: return true val value = shifted.invoke(this, this, currentCont) if (value !== COROUTINE_SUSPENDED) { - println("-> pushCompletion: $value") + log("-> pushCompletion: $value") completions.push(Result.success(value as T)) //continue@loop //break //TODO or loop again? } } catch (e: Throwable) { - println("-> pushCompletion: $e") + log("-> pushCompletion: $e") completions.push(Result.failure(e)) //continue@loop //break //TODO or loop again? From fd5067b9acad5813b332ed8c102e06d0294e1650 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sun, 26 Jul 2020 05:02:56 +0200 Subject: [PATCH 14/49] Initial attempt at well typed ADT with full reification of callbacks and invokations --- .../kotlin/arrow/continuations/adt/cont.kt | 132 ++++++------------ .../arrow/continuations/contt/DelimCCX.kt | 43 +++--- 2 files changed, 72 insertions(+), 103 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt index 134f4e207..58bcf796f 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt @@ -1,105 +1,65 @@ package arrow.continuations.adt -import java.util.* -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.startCoroutine import kotlin.coroutines.suspendCoroutine -val frames: Stack> = Stack() - -sealed class Cont { - - class Reset( - val f: suspend Reset.() -> A - ) : Cont() { - - val shifts: Stack> = Stack() - - inner class Shift( - val f: suspend Shift.((B) -> B) -> A - ) : Cont() { - init { - shifts.push(this) - } - val parent: Reset = this@Reset - } - +typealias Reset = Continuation.Reset +typealias Scope = Continuation.Scope +typealias Shift = Continuation.Scope.Shift +typealias Invoke = Continuation.Invoke +typealias Intercepted = Continuation.Intercepted +typealias KotlinContinuation = kotlin.coroutines.Continuation + +sealed class Continuation { + data class Reset(val body: suspend Scope.() -> A) : Continuation() + data class Intercepted(val continuation: KotlinContinuation, val prompt: Continuation<*, *>) : Continuation() + inner class Invoke(value: A) : Continuation() + abstract class Scope { + inner class Shift(block: suspend Scope.(Continuation) -> A) : Continuation() } } -suspend inline fun reset( - noinline f: suspend Cont.Reset<*>.() -> A -): A = Cont.Reset(f).yield() - -suspend inline fun Cont.Reset<*>.shift( - noinline f: suspend Cont.Reset.Shift.((B) -> B) -> A -): B { - this as Cont.Reset - return Shift(f).yield() -} +suspend fun reset(body: suspend Scope.() -> A): A = + suspendCoroutine { + Intercepted(it, Reset(body)).compile() + } -tailrec suspend fun Cont.yield(): A = - when (this) { - is Cont.Reset -> { - if (shifts.isNotEmpty()) shifts.pop().yield() as A - else suspendCoroutine { ca -> - val body = suspend { - if (frames.isNotEmpty()) frames.pop().yield() as A - else null - } - var res: A? = null - body.startCoroutine(object : Continuation { - override val context: CoroutineContext = EmptyCoroutineContext - override fun resumeWith(result: Result) { - println("Resume shift: ${result}") - res = result.getOrNull() - } - }) - res?.let { ca.resumeWith(Result.success(it)) } - } - } - is Cont.Reset<*>.Shift -> { - frames as Stack> - this as Cont.Reset.Shift - val body: suspend () -> A = suspend { - f { a -> // each bound element when a shift is called within the body - println("push $a") - frames.push(Cont.Reset { a }) - a - } - } - var res: A? = null - body.startCoroutine(object : Continuation { - override val context: CoroutineContext = EmptyCoroutineContext - override fun resumeWith(result: Result) { - println("Resume shift: ${result}") - res = result.getOrNull() - } - }) - res!! - } +suspend fun Scope.shift(block: suspend Scope.(Continuation) -> A): B = + suspendCoroutine { + Intercepted(it, Shift(block)).compile() } -suspend inline operator fun Cont.Reset<*>.times(fa: List): A = - shift, A> { cb -> - fa.flatMap { - listOf(cb(it)) - } +suspend operator fun Continuation.invoke(value: A): B = + suspendCoroutine { + Intercepted(it, Invoke(value)).compile() } +fun Continuation.compile(): A = + when (this) { + is Reset -> TODO() + is Shift -> TODO() + is Invoke -> TODO() + is Intercepted -> TODO() + } -suspend fun main() { - val result = - reset { - val a: Int = this * listOf(1, 2, 3) - val b: String = this * listOf("a", "b", "c") - listOf("$a$b") +class ListScope : Scope>() { + suspend inline operator fun List.invoke(): B = + shift { cb -> + this@invoke.flatMap { + cb(it) + } } - println(result) } +inline fun list(block: ListScope<*>.() -> A): List = + listOf(block(ListScope())) +suspend fun main() { + val result = list { + val a = listOf(1, 2, 3)() + val b = listOf("a", "b", "c")() + "$a$b " + } + println(result) +} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt index 0bfac1566..4cf5b6648 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt @@ -1,11 +1,12 @@ package arrow.continuations.conttxxxx import java.util.* +import kotlin.collections.ArrayList import kotlin.coroutines.* import kotlin.coroutines.intrinsics.* suspend fun reset(body: suspend DelimitedScope.() -> T): T = - DelimitedScopeImpl().run { + DelimitedScopeImpl(body).run { body.startCoroutine(this, this) runReset() } @@ -21,12 +22,13 @@ abstract class DelimitedScope { private typealias ShiftedFun = (DelimitedScope, DelimitedContinuation, Continuation) -> Any? @Suppress("UNCHECKED_CAST") -private class DelimitedScopeImpl : DelimitedScope(), Continuation, DelimitedContinuation { +private class DelimitedScopeImpl(val body: suspend DelimitedScope.() -> T) : DelimitedScope(), Continuation, DelimitedContinuation { private val shiftedBody: Stack> = Stack() private var shiftCont: Stack> = Stack() private var invokeCont: Stack> = Stack() private var invokeValue: Stack = Stack() private var completions: Stack> = Stack() + private var lastShiftedBody: ShiftedFun? = null private fun stateHeader(): String = "shiftedBody\tshiftCont\tinvokeCont\tinvokeValue\tcompletions" @@ -47,6 +49,7 @@ private class DelimitedScopeImpl : DelimitedScope(), Continuation, Deli override suspend fun shift(block: suspend DelimitedScope.(DelimitedContinuation) -> T): R = suspendCoroutineUninterceptedOrReturn { + this.lastShiftedBody = block as ShiftedFun this.shiftedBody.push(block as ShiftedFun) this.shiftCont.push(it as Continuation) COROUTINE_SUSPENDED @@ -84,13 +87,13 @@ private class DelimitedScopeImpl : DelimitedScope(), Continuation, Deli log("\t\t<- shift loop") } // Propagate the result to all pending continuations in shift { ... } bodies - if (propagateCompletions(currentCont as Continuation)) continue@resetLoop + if (propagateCompletions()) continue@resetLoop log("\t<- resetLoop") } COROUTINE_SUSPENDED } - private fun propagateCompletions(currentCont: Continuation?): Boolean { + private fun propagateCompletions(): Boolean { if (completions.isNotEmpty()) { when (val r = completions.pop()) { // null -> TODO("Impossible result is null") @@ -99,25 +102,24 @@ private class DelimitedScopeImpl : DelimitedScope(), Continuation, Deli //completions.push(r) //resume first shot if invoked - val block: suspend DelimitedScope.(cont: DelimitedContinuation) -> T = { - suspendCoroutineUninterceptedOrReturn { - //shiftedBody.push(block as ShiftedFun) - shiftCont.push(it as Continuation) - COROUTINE_SUSPENDED + suspend { + suspendCoroutine { + //shiftedBody.push(lastShiftedBody) + //invokeValue.push(r.getOrThrow()) + it.resumeWith(r) } - } - suspend { block(this) }.startCoroutine(this) + }.createCoroutine(this).resumeWith(Result.success(Unit)) //proceed to next value if invoked //invokeValue.push(r.getOrThrow()) - return false + return true // Return the final result //resetCont.resumeWith(r) } } } - return false + return true } private fun pushCompletion(currentCont: Continuation): Boolean { @@ -146,12 +148,19 @@ private class DelimitedScopeImpl : DelimitedScope(), Continuation, Deli private fun takeInvokeCont() = if (invokeCont.isNotEmpty()) invokeCont.pop() else null } -suspend fun DelimitedScope>.bind(list: List): B = - shift { cb -> - list.flatMap { - cb(it) +suspend fun DelimitedScope>.bind(list: List): B { + val result: ArrayList = arrayListOf() + return shift { cb -> + for (el in list) { + reset { + result.addAll( + shift { cb(el) } + ) + } } + result } +} suspend fun main() { val result: List = reset { From a2af57b231af03f8989d75eb176501b9f61b2812 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sun, 26 Jul 2020 05:09:13 +0200 Subject: [PATCH 15/49] Remove Reset from ADT as it's implicit in the created scope --- .../src/main/kotlin/arrow/continuations/adt/cont.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt index 58bcf796f..57471e728 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt @@ -2,7 +2,6 @@ package arrow.continuations.adt import kotlin.coroutines.suspendCoroutine -typealias Reset = Continuation.Reset typealias Scope = Continuation.Scope typealias Shift = Continuation.Scope.Shift typealias Invoke = Continuation.Invoke @@ -10,7 +9,6 @@ typealias Intercepted = Continuation.Intercepted typealias KotlinContinuation = kotlin.coroutines.Continuation sealed class Continuation { - data class Reset(val body: suspend Scope.() -> A) : Continuation() data class Intercepted(val continuation: KotlinContinuation, val prompt: Continuation<*, *>) : Continuation() inner class Invoke(value: A) : Continuation() abstract class Scope { @@ -18,11 +16,6 @@ sealed class Continuation { } } -suspend fun reset(body: suspend Scope.() -> A): A = - suspendCoroutine { - Intercepted(it, Reset(body)).compile() - } - suspend fun Scope.shift(block: suspend Scope.(Continuation) -> A): B = suspendCoroutine { Intercepted(it, Shift(block)).compile() @@ -35,7 +28,6 @@ suspend operator fun Continuation.invoke(value: A): B = fun Continuation.compile(): A = when (this) { - is Reset -> TODO() is Shift -> TODO() is Invoke -> TODO() is Intercepted -> TODO() From 55db1b40e2dc1495907ef7455617ba330f0d592d Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sun, 26 Jul 2020 05:14:35 +0200 Subject: [PATCH 16/49] Include scope in interception --- .../src/main/kotlin/arrow/continuations/adt/cont.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt index 57471e728..916c1899e 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt @@ -9,21 +9,25 @@ typealias Intercepted = Continuation.Intercepted typealias KotlinContinuation = kotlin.coroutines.Continuation sealed class Continuation { - data class Intercepted(val continuation: KotlinContinuation, val prompt: Continuation<*, *>) : Continuation() + data class Intercepted( + val parent: Continuation<*, *>, + val continuation: KotlinContinuation, + val prompt: Continuation<*, *> + ) : Continuation() inner class Invoke(value: A) : Continuation() - abstract class Scope { + abstract class Scope: Continuation() { inner class Shift(block: suspend Scope.(Continuation) -> A) : Continuation() } } suspend fun Scope.shift(block: suspend Scope.(Continuation) -> A): B = suspendCoroutine { - Intercepted(it, Shift(block)).compile() + Intercepted(this, it, Shift(block)).compile() } suspend operator fun Continuation.invoke(value: A): B = suspendCoroutine { - Intercepted(it, Invoke(value)).compile() + Intercepted(this, it, Invoke(value)).compile() } fun Continuation.compile(): A = @@ -31,6 +35,7 @@ fun Continuation.compile(): A = is Shift -> TODO() is Invoke -> TODO() is Intercepted -> TODO() + is Scope -> TODO() } class ListScope : Scope>() { From d302bacfb9c319e8692dfa6a7894fec1b73ed336 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sun, 26 Jul 2020 05:44:57 +0200 Subject: [PATCH 17/49] getting ready for the loop --- .../src/main/kotlin/arrow/continuations/adt/cont.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt index 916c1899e..5a207bc1f 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt @@ -14,9 +14,11 @@ sealed class Continuation { val continuation: KotlinContinuation, val prompt: Continuation<*, *> ) : Continuation() - inner class Invoke(value: A) : Continuation() + inner class Invoke(val value: A) : Continuation() abstract class Scope: Continuation() { - inner class Shift(block: suspend Scope.(Continuation) -> A) : Continuation() + inner class Shift(val block: suspend Scope.(Continuation) -> A) : Continuation() { + val scope: Scope = this@Scope + } } } @@ -27,6 +29,7 @@ suspend fun Scope.shift(block: suspend Scope.(Continuation) - suspend operator fun Continuation.invoke(value: A): B = suspendCoroutine { + it.context Intercepted(this, it, Invoke(value)).compile() } From 3b59819bd853496885ab66008ac450f9c3a60b04 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sun, 26 Jul 2020 05:48:00 +0200 Subject: [PATCH 18/49] concrete scope as objects --- .../src/main/kotlin/arrow/continuations/adt/cont.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt index 5a207bc1f..c37df6162 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt @@ -41,7 +41,7 @@ fun Continuation.compile(): A = is Scope -> TODO() } -class ListScope : Scope>() { +object ListScope : Scope>() { suspend inline operator fun List.invoke(): B = shift { cb -> this@invoke.flatMap { @@ -50,8 +50,8 @@ class ListScope : Scope>() { } } -inline fun list(block: ListScope<*>.() -> A): List = - listOf(block(ListScope())) +inline fun list(block: ListScope.() -> A): List = + listOf(block(ListScope)) suspend fun main() { From 23266ab83d6f9acc4ff2f8d1a9a576e2aa4bfb6e Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sun, 26 Jul 2020 06:07:20 +0200 Subject: [PATCH 19/49] Exposing available data in the compiler --- .../kotlin/arrow/continuations/adt/cont.kt | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt index c37df6162..3d8bbd46e 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt @@ -14,8 +14,11 @@ sealed class Continuation { val continuation: KotlinContinuation, val prompt: Continuation<*, *> ) : Continuation() - inner class Invoke(val value: A) : Continuation() + inner class Invoke(val value: A) : Continuation() { + val parent: Continuation = this@Continuation + } abstract class Scope: Continuation() { + abstract val result: A inner class Shift(val block: suspend Scope.(Continuation) -> A) : Continuation() { val scope: Scope = this@Scope } @@ -29,23 +32,40 @@ suspend fun Scope.shift(block: suspend Scope.(Continuation) - suspend operator fun Continuation.invoke(value: A): B = suspendCoroutine { - it.context Intercepted(this, it, Invoke(value)).compile() } fun Continuation.compile(): A = when (this) { - is Shift -> TODO() - is Invoke -> TODO() - is Intercepted -> TODO() - is Scope -> TODO() + is Shift -> { + val block: suspend (Continuation.Scope, Continuation) -> B = block + val scope: Continuation.Scope = scope + TODO() + } + is Invoke -> { + val value: B = value + val parent: Continuation = parent + TODO() + } + is Intercepted -> { + val parent: Continuation<*, *> = parent + val continuation: KotlinContinuation = continuation + val prompt: Continuation<*, *> = prompt + TODO() + } + is Scope -> { + val result: A = result + TODO() + } } object ListScope : Scope>() { + override val result: ArrayList = arrayListOf() suspend inline operator fun List.invoke(): B = shift { cb -> this@invoke.flatMap { - cb(it) + this@ListScope.result.addAll(cb(it)) + this@ListScope.result } } } From ddfbef8f9caf1e235a94bcbe1cd97950ff487e21 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sun, 26 Jul 2020 06:18:51 +0200 Subject: [PATCH 20/49] Shortcircuit case --- .../src/main/kotlin/arrow/continuations/adt/cont.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt index 3d8bbd46e..a55bb8776 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt @@ -5,6 +5,7 @@ import kotlin.coroutines.suspendCoroutine typealias Scope = Continuation.Scope typealias Shift = Continuation.Scope.Shift typealias Invoke = Continuation.Invoke +typealias ShortCircuit = Continuation.ShortCircuit typealias Intercepted = Continuation.Intercepted typealias KotlinContinuation = kotlin.coroutines.Continuation @@ -14,6 +15,7 @@ sealed class Continuation { val continuation: KotlinContinuation, val prompt: Continuation<*, *> ) : Continuation() + inner class ShortCircuit(val value: A) : Continuation() inner class Invoke(val value: A) : Continuation() { val parent: Continuation = this@Continuation } @@ -53,15 +55,15 @@ fun Continuation.compile(): A = val prompt: Continuation<*, *> = prompt TODO() } - is Scope -> { - val result: A = result - TODO() - } + is Scope -> result + is ShortCircuit -> value } + + object ListScope : Scope>() { override val result: ArrayList = arrayListOf() - suspend inline operator fun List.invoke(): B = + suspend operator fun List.invoke(): B = shift { cb -> this@invoke.flatMap { this@ListScope.result.addAll(cb(it)) From e3a74ef5bd54220b365c5ffe1a5a55eaa8756e9c Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sun, 26 Jul 2020 06:26:34 +0200 Subject: [PATCH 21/49] Better typing for scoping callbacks --- .../kotlin/arrow/continuations/adt/cont.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt index a55bb8776..99fcd255f 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt @@ -4,7 +4,7 @@ import kotlin.coroutines.suspendCoroutine typealias Scope = Continuation.Scope typealias Shift = Continuation.Scope.Shift -typealias Invoke = Continuation.Invoke +typealias Invoke = Continuation.Scope.Invoke typealias ShortCircuit = Continuation.ShortCircuit typealias Intercepted = Continuation.Intercepted typealias KotlinContinuation = kotlin.coroutines.Continuation @@ -16,23 +16,23 @@ sealed class Continuation { val prompt: Continuation<*, *> ) : Continuation() inner class ShortCircuit(val value: A) : Continuation() - inner class Invoke(val value: A) : Continuation() { - val parent: Continuation = this@Continuation - } abstract class Scope: Continuation() { abstract val result: A - inner class Shift(val block: suspend Scope.(Continuation) -> A) : Continuation() { + inner class Shift(val block: suspend Scope.(Scope) -> A) : Continuation() { + val scope: Scope = this@Scope + } + inner class Invoke(val value: A) : Continuation() { val scope: Scope = this@Scope } } } -suspend fun Scope.shift(block: suspend Scope.(Continuation) -> A): B = +suspend fun Scope.shift(block: suspend Scope.(Scope) -> A): B = suspendCoroutine { Intercepted(this, it, Shift(block)).compile() } -suspend operator fun Continuation.invoke(value: A): B = +suspend operator fun Scope.invoke(value: A): B = suspendCoroutine { Intercepted(this, it, Invoke(value)).compile() } @@ -40,13 +40,13 @@ suspend operator fun Continuation.invoke(value: A): B = fun Continuation.compile(): A = when (this) { is Shift -> { - val block: suspend (Continuation.Scope, Continuation) -> B = block + val block: suspend (Continuation.Scope, Continuation.Scope) -> B = block val scope: Continuation.Scope = scope TODO() } is Invoke -> { - val value: B = value - val parent: Continuation = parent + val value: A = value + val scope: Continuation.Scope = scope TODO() } is Intercepted -> { From e638c1a98edc30854efe7d1c195edecac46b46f1 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Sun, 26 Jul 2020 06:29:02 +0200 Subject: [PATCH 22/49] prototype the List scope to avoid thread safety since the functions is inline --- .../src/main/kotlin/arrow/continuations/adt/cont.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt index 99fcd255f..7778f69f8 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt @@ -61,7 +61,7 @@ fun Continuation.compile(): A = -object ListScope : Scope>() { +class ListScope : Scope>() { override val result: ArrayList = arrayListOf() suspend operator fun List.invoke(): B = shift { cb -> @@ -73,7 +73,7 @@ object ListScope : Scope>() { } inline fun list(block: ListScope.() -> A): List = - listOf(block(ListScope)) + listOf(block(ListScope())) suspend fun main() { From 68f17dd80afa7ad2797dc613edc0059284740145 Mon Sep 17 00:00:00 2001 From: Jannis Date: Mon, 27 Jul 2020 04:08:26 +0200 Subject: [PATCH 23/49] Multishot cont with effect stack. Examples are in the "test" class and are based on algebraic effect handlers --- .../continuations/effectStack/program.kt | 108 ++++++++++++++++++ .../src/test/kotlin/effectStack/Run.kt | 81 +++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt create mode 100644 arrow-continuations/src/test/kotlin/effectStack/Run.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt new file mode 100644 index 000000000..cecbd9e61 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt @@ -0,0 +1,108 @@ +package arrow.continuations.effectStack + +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +interface DelimitedCont { + suspend operator fun invoke(a: A): R +} + +interface Delimited { + suspend fun control(func: suspend (DelimitedCont) -> R): A +} + +fun prompt(f: suspend Delimited.() -> A): A = DelimitedScope("Prompt", f).run() + +/** + * Idea we have two paths: + * One path is the normal coroutine. It fills an effect stack everytime it's continuation is resumed with a value. + * Then if a continuation is run more than once we restart the entire computation [f] and use the effect stack for as long as possible + * When the effect stack runs out of values we resume normal coroutine behaviour. + * + * This can be used to implement nondeterminism together with any other effect and so long as the "pure" code in a function + * is fast this won't be a problem, but if it isn't this will result in terrible performance (but only if multishot is actually used) + */ +open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.() -> R) : Delimited { + + val ref = atomic(null) + val currF = atomic<(suspend () -> R)?>(null) + open val stack: MutableList = mutableListOf() + val cbs = mutableListOf>() + + override suspend fun control(func: suspend (DelimitedCont) -> R): A { + return suspendCoroutine { k -> + // println("Suspending for control: $label") + // println("Stack: $stack") + val o = object : DelimitedCont { + val state = atomic?>(k) + val snapshot = stack.toList() + override suspend fun invoke(a: A): R { + // println("Invoke cont with state is null: ${state.value == null} && arg $a") + val cont = state.getAndSet(null) + // Reexecute f but this time on control we resume the continuation directly with a + return if (cont == null) startMultiShot(snapshot + a) + else suspendCoroutineUninterceptedOrReturn { + // push stuff to the stack + stack.add(a) + // run cont + cont.resume(a) + cbs.add(it) + COROUTINE_SUSPENDED + } + } + } + currF.value = { func(o) } + } + } + + fun startMultiShot(stack: List): R = MultiShotDelimScope(stack, f).run() + + fun run(): R { + // println("Running $label") + val r = f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { + // println("Put value ${(it.getOrThrow() as Sequence).toList()}") + ref.value = it.getOrThrow() + }).let { res -> + if (res == COROUTINE_SUSPENDED) { + // println("Running suspended $label") + ref.loop { + // control called a continuation which now finished + if (it != null) return@let + else + currF.getAndSet(null)!!.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { res -> + // println("Resumption with ${(res.getOrThrow() as Sequence).toList()}") + ref.value = res.getOrThrow() + }).let { + // early return control did not call it's continuation + if (it != COROUTINE_SUSPENDED) ref.value = it as R + } + } // control has not been called + } else return@run res as R + } + // control has been called, call the continuations in reverse order + cbs.asReversed().forEach { it.resume(ref.value!!) } + return ref.value!! + } +} + +class MultiShotDelimScope( + localStack: List, + f: suspend Delimited.() -> R +) : DelimitedScope("Multishot", f) { + var depth = 0 + override val stack: MutableList = localStack.toMutableList() + override suspend fun control(func: suspend (DelimitedCont) -> R): A = + if (stack.size > depth) stack[depth++] as A + else { + // println("EmptyStack") + depth++ + super.control(func) + } +} diff --git a/arrow-continuations/src/test/kotlin/effectStack/Run.kt b/arrow-continuations/src/test/kotlin/effectStack/Run.kt new file mode 100644 index 000000000..1463b1b04 --- /dev/null +++ b/arrow-continuations/src/test/kotlin/effectStack/Run.kt @@ -0,0 +1,81 @@ +package effectStack + +import arrow.Kind +import arrow.continuations.effectStack.prompt +import arrow.core.Either +import arrow.core.Left +import arrow.core.Right +import arrow.core.Tuple4 +import arrow.core.flatMap +import arrow.core.test.UnitSpec +import kotlin.coroutines.RestrictsSuspension +import kotlin.random.Random + +@RestrictsSuspension +interface Error { + suspend fun raise(e: E): A + suspend fun catch(handle: suspend Error.(E) -> A, f: suspend Error.() -> A): A +} + +fun error(f: suspend Error.() -> A): Either = prompt { + val p = object : Error { + override suspend fun raise(e: E): A = control { Left(e) } + override suspend fun catch(handle: suspend Error.(E) -> B, f: suspend Error.() -> B): B = + control { k -> + error { f() }.fold({ e -> error { handle(e) }.flatMap { k(it) } }, { b -> k(b) }) + } + } + Right(f(p)) +} + +@RestrictsSuspension +interface NonDet { + suspend fun effect(f: suspend () -> B): B + suspend fun empty(): A + suspend fun choose(): Boolean +} + +inline fun nonDet(crossinline f: suspend NonDet.() -> A): Sequence = prompt { + val p = object : NonDet { + override suspend fun effect(f: suspend () -> B): B = control { it(f()) } + override suspend fun choose(): Boolean = control { k -> k(true) + k(false) } + override suspend fun empty(): A = control { emptySequence() } + } + sequenceOf(f(p)) +} + +// I couldn't use a main method because intellij kept complaining about not finding the main file unless I rebuild the entire repo... +// Running tests works fine though, hence I moved it here. +class Test : UnitSpec() { + init { + "testNondet" { + nonDet { + var sum = 0 + val b = choose() + effect { println("PROGRAM: Here $b") } + // stacksafe? + for (i in 0..1000) { + sum += effect { Random.nextInt(100) } + } + val i = effect { Random.nextInt() } + effect { println("PROGRAM: Rand $i") } + val b2 = if (b.not()) choose() + else empty() + effect { println("PROGRAM: Here2 $b2") } + Tuple4(i, b, b2, sum) + }.also { println("PROGRAM: Result ${it.toList()}") } + } + "testError" { + error { + catch({ e -> + println("PROGRAM: Got error: $e") + raise(e) + }) { + val rand = 10 + if (rand.rem(2) == 0) raise("No equal numbers") + else rand + } + }.also { println("PROGRAM: Result $it") } + } + } +} From c1f87da472e0c309c7bd6793f3e84907eb0403ba Mon Sep 17 00:00:00 2001 From: Jannis Date: Mon, 27 Jul 2020 04:21:29 +0200 Subject: [PATCH 24/49] Add monadic bind example --- .../src/test/kotlin/effectStack/Run.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/arrow-continuations/src/test/kotlin/effectStack/Run.kt b/arrow-continuations/src/test/kotlin/effectStack/Run.kt index 1463b1b04..826bc6b51 100644 --- a/arrow-continuations/src/test/kotlin/effectStack/Run.kt +++ b/arrow-continuations/src/test/kotlin/effectStack/Run.kt @@ -3,14 +3,30 @@ package effectStack import arrow.Kind import arrow.continuations.effectStack.prompt import arrow.core.Either +import arrow.core.EitherPartialOf import arrow.core.Left import arrow.core.Right import arrow.core.Tuple4 +import arrow.core.extensions.either.monad.flatten +import arrow.core.fix import arrow.core.flatMap import arrow.core.test.UnitSpec +import arrow.typeclasses.Monad import kotlin.coroutines.RestrictsSuspension import kotlin.random.Random +@RestrictsSuspension +interface Monadic { + suspend operator fun Kind.invoke(): A +} + +fun either(f: suspend Monadic>.() -> A): Kind, A> = prompt { + val m = object : Monadic> { + override suspend fun Kind, A>.invoke(): A = control { k -> fix().flatMap { k(it).fix() } } + } + Either.Right(f(m)) +} + @RestrictsSuspension interface Error { suspend fun raise(e: E): A @@ -77,5 +93,12 @@ class Test : UnitSpec() { } }.also { println("PROGRAM: Result $it") } } + "either" { + either { + val a = Right(11)() + if (a > 10) Left("Larger than 10")() + else a + }.also { println("PROGRAM: Result $it") } + } } } From 62e2e5e04e903d71cb53ff6d11f46b34845da9ea Mon Sep 17 00:00:00 2001 From: Jannis Date: Mon, 27 Jul 2020 04:30:22 +0200 Subject: [PATCH 25/49] Clarify comments and make variables private/internal --- .../continuations/effectStack/program.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt index cecbd9e61..3e64c2b5d 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt @@ -22,7 +22,7 @@ fun prompt(f: suspend Delimited.() -> A): A = DelimitedScope("Prompt", f) /** * Idea we have two paths: - * One path is the normal coroutine. It fills an effect stack everytime it's continuation is resumed with a value. + * One path is the normal coroutine. It fills an effect stack everytime its continuation is resumed with a value. * Then if a continuation is run more than once we restart the entire computation [f] and use the effect stack for as long as possible * When the effect stack runs out of values we resume normal coroutine behaviour. * @@ -31,10 +31,10 @@ fun prompt(f: suspend Delimited.() -> A): A = DelimitedScope("Prompt", f) */ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.() -> R) : Delimited { - val ref = atomic(null) - val currF = atomic<(suspend () -> R)?>(null) - open val stack: MutableList = mutableListOf() - val cbs = mutableListOf>() + private val ref = atomic(null) + private val currF = atomic<(suspend () -> R)?>(null) + internal open val stack: MutableList = mutableListOf() + private val cbs = mutableListOf>() override suspend fun control(func: suspend (DelimitedCont) -> R): A { return suspendCoroutine { k -> @@ -66,27 +66,27 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( fun run(): R { // println("Running $label") - val r = f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { + f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { // println("Put value ${(it.getOrThrow() as Sequence).toList()}") ref.value = it.getOrThrow() }).let { res -> if (res == COROUTINE_SUSPENDED) { // println("Running suspended $label") ref.loop { - // control called a continuation which now finished + // controls function called a continuation which now finished if (it != null) return@let else currF.getAndSet(null)!!.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { res -> // println("Resumption with ${(res.getOrThrow() as Sequence).toList()}") ref.value = res.getOrThrow() }).let { - // early return control did not call it's continuation + // early return controls function did not call its continuation if (it != COROUTINE_SUSPENDED) ref.value = it as R } } // control has not been called } else return@run res as R } - // control has been called, call the continuations in reverse order + // control has been called and its continuations have been invoked, resume the continuations in reverse order cbs.asReversed().forEach { it.resume(ref.value!!) } return ref.value!! } @@ -96,7 +96,7 @@ class MultiShotDelimScope( localStack: List, f: suspend Delimited.() -> R ) : DelimitedScope("Multishot", f) { - var depth = 0 + private var depth = 0 override val stack: MutableList = localStack.toMutableList() override suspend fun control(func: suspend (DelimitedCont) -> R): A = if (stack.size > depth) stack[depth++] as A From 35d5d6f945bdc446577fdbf49fce856a222881dc Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Mon, 27 Jul 2020 04:41:33 +0200 Subject: [PATCH 26/49] Typed GADT and state for delim continuations with multi-prompt. --- .../continuations/adt/ContinuationState.kt | 51 +++++++ .../kotlin/arrow/continuations/adt/cont.kt | 131 +++++++++++------- 2 files changed, 134 insertions(+), 48 deletions(-) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/adt/ContinuationState.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/ContinuationState.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/ContinuationState.kt new file mode 100644 index 000000000..c484ef7cf --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/ContinuationState.kt @@ -0,0 +1,51 @@ +package arrow.continuations.adt + +import arrow.continuations.contxx.Cont +import java.util.* + +interface ContinuationState { + + fun shift(): Shift? = + takePrompt() as? Shift + + fun invoke(): Invoke? = + takePrompt() as? Invoke + + fun scope(): Scope? = + takePrompt() as? Scope + + fun takePrompt(): Continuation? + fun push(prompt: Continuation): Unit + fun log(value: String): Unit + + operator fun plusAssign(other: ContinuationState): Unit + + companion object { + operator fun invoke(): ContinuationState = + StackContinuationState() + + private class StackContinuationState( + private val prompts: Stack> = Stack() + ) : ContinuationState { + + override fun takePrompt(): Continuation? = + if (prompts.isNotEmpty()) prompts.pop() else null + + override fun push(prompt: Continuation) { + prompts.push(prompt) + } + + private fun stateLog(): String = + "/size: ${prompts.size}/ $prompts" + + + override fun log(value: String): Unit { + println("${stateLog()}\t\t\t$value") + } + + override fun plusAssign(other: ContinuationState): Unit { + while (other.takePrompt()?.also { prompts.push(it) } != null) {} + } + } + } +} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt index 7778f69f8..d5a774218 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt @@ -2,78 +2,113 @@ package arrow.continuations.adt import kotlin.coroutines.suspendCoroutine -typealias Scope = Continuation.Scope -typealias Shift = Continuation.Scope.Shift -typealias Invoke = Continuation.Scope.Invoke +typealias Scope = Continuation.Scope +typealias Shift = Continuation.Scope.Shift +typealias Invoke = Continuation.Scope.Shift.Invoke typealias ShortCircuit = Continuation.ShortCircuit -typealias Intercepted = Continuation.Intercepted typealias KotlinContinuation = kotlin.coroutines.Continuation sealed class Continuation { - data class Intercepted( - val parent: Continuation<*, *>, - val continuation: KotlinContinuation, - val prompt: Continuation<*, *> - ) : Continuation() - inner class ShortCircuit(val value: A) : Continuation() - abstract class Scope: Continuation() { + + abstract val parent: Continuation<*, *> + + abstract val state: ContinuationState<*, *, *> + + inner class ShortCircuit(val value: A) : Continuation() { + override val parent: Continuation = this@Continuation + override val state: ContinuationState<*, *, *> = this@Continuation.state + } + + abstract class Scope( + override val state: ContinuationState = ContinuationState() + ) : Continuation() { + + override val parent: Continuation get() = this + abstract val result: A - inner class Shift(val block: suspend Scope.(Scope) -> A) : Continuation() { - val scope: Scope = this@Scope - } - inner class Invoke(val value: A) : Continuation() { - val scope: Scope = this@Scope + + inner class Shift( + val block: suspend Scope.(Shift) -> A, + val continuation: KotlinContinuation + ) : Continuation(), KotlinContinuation by continuation { + val scope: Scope = this@Scope + override val parent: Continuation = scope + override val state: ContinuationState = scope.state + + inner class Invoke(val continuation: KotlinContinuation, val value: C) : Continuation(), KotlinContinuation by continuation { + val shift: Shift = this@Shift + override val parent: Shift = this@Shift + override val state: ContinuationState = shift.state + } + + private var _result: C? = null + + override fun resumeWith(result: Result) { + this._result = result.getOrThrow() + } } + + } } -suspend fun Scope.shift(block: suspend Scope.(Scope) -> A): B = +suspend fun Scope.shift(block: suspend Continuation.Scope.(Continuation.Scope.Shift) -> A): C = suspendCoroutine { - Intercepted(this, it, Shift(block)).compile() + Shift(block, it).compile(state) } -suspend operator fun Scope.invoke(value: A): B = +suspend operator fun Shift.invoke(value: C): A = suspendCoroutine { - Intercepted(this, it, Invoke(value)).compile() + Invoke(it, value).compile(state) } -fun Continuation.compile(): A = - when (this) { - is Shift -> { - val block: suspend (Continuation.Scope, Continuation.Scope) -> B = block - val scope: Continuation.Scope = scope - TODO() - } - is Invoke -> { - val value: A = value - val scope: Continuation.Scope = scope - TODO() - } - is Intercepted -> { - val parent: Continuation<*, *> = parent - val continuation: KotlinContinuation = continuation - val prompt: Continuation<*, *> = prompt - TODO() - } - is Scope -> result - is ShortCircuit -> value +fun ContinuationState.unfold(): A = + when(val prompt = takePrompt()) { + is ShortCircuit -> prompt.value + is Scope -> prompt.result + null -> TODO() + is Shift<*, *, *> -> TODO() + is Invoke<*, *, *> -> TODO() } +fun Continuation.compile(state: ContinuationState): A = + when (this@compile) { + is Shift<*, *, *> -> { + state.log("Shift: [parent: $parent, scope: $scope, block: $block]") + state.push(this@compile) + state.unfold() + } + is Invoke<*, *, *> -> { + state.log("Invoke: [parent: $parent, value: $value]") + state.push(this@compile) + state.unfold() + } + is Scope -> { + state.log("Scope: [parent: $parent, result: $result]") + state.push(this@compile) + state.unfold() + } + is ShortCircuit -> { + state.log("ShortCircuit: [parent: $parent, value: $value]") + value + } + } -class ListScope : Scope>() { - override val result: ArrayList = arrayListOf() - suspend operator fun List.invoke(): B = +class ListScope : Scope, A>() { + private var _result: List = emptyList() + override val result: List get () = _result + suspend operator fun List.invoke(): C = shift { cb -> - this@invoke.flatMap { - this@ListScope.result.addAll(cb(it)) - this@ListScope.result + _result = flatMap { + cb(it) } + result } } -inline fun list(block: ListScope.() -> A): List = - listOf(block(ListScope())) +inline fun list(block: ListScope<*>.() -> A): List = + listOf(block(ListScope())) suspend fun main() { From a2dc9d02655ef33fabc115e9d1417de25eb92145 Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Mon, 27 Jul 2020 04:58:43 +0200 Subject: [PATCH 27/49] test for list in non-deterministic comprehensions over Jannis impl --- .../src/test/kotlin/effectStack/Run.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/arrow-continuations/src/test/kotlin/effectStack/Run.kt b/arrow-continuations/src/test/kotlin/effectStack/Run.kt index 826bc6b51..66fe4e09f 100644 --- a/arrow-continuations/src/test/kotlin/effectStack/Run.kt +++ b/arrow-continuations/src/test/kotlin/effectStack/Run.kt @@ -12,6 +12,7 @@ import arrow.core.fix import arrow.core.flatMap import arrow.core.test.UnitSpec import arrow.typeclasses.Monad +import io.kotlintest.shouldBe import kotlin.coroutines.RestrictsSuspension import kotlin.random.Random @@ -51,6 +52,25 @@ interface NonDet { suspend fun choose(): Boolean } +@RestrictsSuspension +interface ListComputation { + suspend operator fun List.invoke(): C +} + +inline fun list(crossinline f: suspend ListComputation.() -> A): List = + prompt { + val p = object : ListComputation { + override suspend fun List.invoke(): C = + control { cb -> + flatMap { + cb(it) + } + } + } + listOf(f(p)) + } + + inline fun nonDet(crossinline f: suspend NonDet.() -> A): Sequence = prompt { val p = object : NonDet { override suspend fun effect(f: suspend () -> B): B = control { it(f()) } @@ -64,6 +84,13 @@ inline fun nonDet(crossinline f: suspend NonDet.() -> A): Sequence = prom // Running tests works fine though, hence I moved it here. class Test : UnitSpec() { init { + "list" { + list { + val a = listOf(1, 2, 3)() + val b = listOf("a", "b", "c")() + "$a$b " + } shouldBe listOf("1a ", "1b ", "1c ", "2a ", "2b ", "2c ", "3a ", "3b ", "3c ") + } "testNondet" { nonDet { var sum = 0 From 4940437c64fa65188d36c2f975b3cfe213242cd5 Mon Sep 17 00:00:00 2001 From: Jannis Date: Mon, 27 Jul 2020 14:41:44 +0200 Subject: [PATCH 28/49] Handle prompt in prompt case correctly --- .../continuations/effectStack/program.kt | 76 ++++++++++++++----- .../src/test/kotlin/effectStack/Run.kt | 55 ++++++++++++-- 2 files changed, 105 insertions(+), 26 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt index 3e64c2b5d..71c00585f 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt @@ -16,9 +16,10 @@ interface DelimitedCont { interface Delimited { suspend fun control(func: suspend (DelimitedCont) -> R): A + suspend fun prompt(f: suspend Delimited.() -> A): A } -fun prompt(f: suspend Delimited.() -> A): A = DelimitedScope("Prompt", f).run() +suspend fun prompt(f: suspend Delimited.() -> A): A = DelimitedScope("Prompt", f).run() /** * Idea we have two paths: @@ -51,9 +52,9 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( else suspendCoroutineUninterceptedOrReturn { // push stuff to the stack stack.add(a) + cbs.add(it) // run cont cont.resume(a) - cbs.add(it) COROUTINE_SUSPENDED } } @@ -62,30 +63,69 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( } } - fun startMultiShot(stack: List): R = MultiShotDelimScope(stack, f).run() + suspend fun startMultiShot(stack: List): R = MultiShotDelimScope(stack, f).run() + + override suspend fun prompt(f: suspend Delimited.() -> A): A = + DelimitedScope("inner", f).let { scope -> + scope::run.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { + TODO("Is this ever resumed?") + }).let fst@{ + if (it == COROUTINE_SUSPENDED) { + /** + * Simply suspend again. This is only ever called if we suspend to the parent scope and if we actually call + * the continuation it'll lead to an infinite loop anyway. Why? Let's have a look at this example: + * prompt fst@{ + * val a: Int = control { it(5) + it(3) } + * a + prompt snd@{ + * val i = this@fst.control { it(2) } + * i + 1 + * } + * } + * This will first execute `control { it(5) }` which then runs the inner prompt. The inner prompt yields back to + * the outer prompt because to return to i it needs the result of the outer prompt. The only sensible way of getting + * such a result is to rerun it with it's previous stack. However this means the state upon reaching + * the inner prompt again is deterministic and always the same, which is why it'll loop. + */ + suspendCoroutine {} + } + else it as A + } + } - fun run(): R { - // println("Running $label") + private fun getValue(): R? = + // println("Running suspended $label") + ref.loop { + // controls function called a continuation which now finished + if (it != null) return@getValue it + else { + val res = currF.getAndSet(null) + if (res != null) + res.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { res -> + // println("Resumption with ${(res.getOrThrow() as Sequence).toList()}") + ref.value = res.getOrThrow() + }).let { + // early return controls function did not call its continuation + if (it != COROUTINE_SUSPENDED) ref.value = it as R + } + // short since we run out of conts to call + else return@getValue null + } + } + + open suspend fun run(): R { + // println("Running $dbgLabel") f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { // println("Put value ${(it.getOrThrow() as Sequence).toList()}") ref.value = it.getOrThrow() }).let { res -> if (res == COROUTINE_SUSPENDED) { - // println("Running suspended $label") - ref.loop { - // controls function called a continuation which now finished - if (it != null) return@let - else - currF.getAndSet(null)!!.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { res -> - // println("Resumption with ${(res.getOrThrow() as Sequence).toList()}") - ref.value = res.getOrThrow() - }).let { - // early return controls function did not call its continuation - if (it != COROUTINE_SUSPENDED) ref.value = it as R - } - } // control has not been called + // if it is null we are done calling through our control fns and we need a value from a parent scope now + // this will block indefinitely if there is no parent scope, but a program like that should not typecheck + // at least not when using control + getValue() ?: return@run suspendCoroutine {} } else return@run res as R } + // control has been called and its continuations have been invoked, resume the continuations in reverse order cbs.asReversed().forEach { it.resume(ref.value!!) } return ref.value!! diff --git a/arrow-continuations/src/test/kotlin/effectStack/Run.kt b/arrow-continuations/src/test/kotlin/effectStack/Run.kt index 66fe4e09f..a7694815c 100644 --- a/arrow-continuations/src/test/kotlin/effectStack/Run.kt +++ b/arrow-continuations/src/test/kotlin/effectStack/Run.kt @@ -1,19 +1,23 @@ package effectStack import arrow.Kind +import arrow.continuations.effectStack.Delimited import arrow.continuations.effectStack.prompt import arrow.core.Either import arrow.core.EitherPartialOf +import arrow.core.ForListK import arrow.core.Left import arrow.core.Right import arrow.core.Tuple4 import arrow.core.extensions.either.monad.flatten import arrow.core.fix import arrow.core.flatMap +import arrow.core.k import arrow.core.test.UnitSpec import arrow.typeclasses.Monad import io.kotlintest.shouldBe import kotlin.coroutines.RestrictsSuspension +import kotlin.coroutines.suspendCoroutine import kotlin.random.Random @RestrictsSuspension @@ -21,7 +25,7 @@ interface Monadic { suspend operator fun Kind.invoke(): A } -fun either(f: suspend Monadic>.() -> A): Kind, A> = prompt { +suspend fun either(f: suspend Monadic>.() -> A): Kind, A> = prompt { val m = object : Monadic> { override suspend fun Kind, A>.invoke(): A = control { k -> fix().flatMap { k(it).fix() } } } @@ -34,7 +38,7 @@ interface Error { suspend fun catch(handle: suspend Error.(E) -> A, f: suspend Error.() -> A): A } -fun error(f: suspend Error.() -> A): Either = prompt { +suspend fun error(f: suspend Error.() -> A): Either = prompt { val p = object : Error { override suspend fun raise(e: E): A = control { Left(e) } override suspend fun catch(handle: suspend Error.(E) -> B, f: suspend Error.() -> B): B = @@ -57,8 +61,8 @@ interface ListComputation { suspend operator fun List.invoke(): C } -inline fun list(crossinline f: suspend ListComputation.() -> A): List = - prompt { +suspend inline fun list(crossinline f: suspend ListComputation.() -> A): List = + prompt {this val p = object : ListComputation { override suspend fun List.invoke(): C = control { cb -> @@ -71,7 +75,7 @@ inline fun list(crossinline f: suspend ListComputation.() -> A): List = } -inline fun nonDet(crossinline f: suspend NonDet.() -> A): Sequence = prompt { +suspend inline fun nonDet(crossinline f: suspend NonDet.() -> A): Sequence = prompt { val p = object : NonDet { override suspend fun effect(f: suspend () -> B): B = control { it(f()) } override suspend fun choose(): Boolean = control { k -> k(true) + k(false) } @@ -84,6 +88,38 @@ inline fun nonDet(crossinline f: suspend NonDet.() -> A): Sequence = prom // Running tests works fine though, hence I moved it here. class Test : UnitSpec() { init { + "yield building a list" { + println("PROGRAM: Run yield building a list") + prompt> { + suspend fun Delimited>.yield(a: A): Unit = control { listOf(a) + it(Unit) } + + yield(1) + yield(2) + yield(10) + + emptyList() + }.also { println("PROGRAM: Result $it") } + } + "test" { + println("PROGRAM: Run Test") + val res = 10 + prompt { + 2 + control { it(it(3)) + 100 } + } + println("PROGRAM: Result $res") + } + "multi" { + println("PROGRAM: multi") + prompt> fst@{ + val ctx = this + val i: Int = control { it(2) } + Right(i * 2 + prompt snd@{ + val k: Int = this@snd.control { it(1) + it(2) } + val j: Int = if (i == 5) ctx.control { Left("Not today") } + else this@snd.control { it(4) } + j + k + }) + }.also { println("PROGRAM: Result $it") } + } "list" { list { val a = listOf(1, 2, 3)() @@ -91,7 +127,8 @@ class Test : UnitSpec() { "$a$b " } shouldBe listOf("1a ", "1b ", "1c ", "2a ", "2b ", "2c ", "3a ", "3b ", "3c ") } - "testNondet" { + "nonDet" { + println("PROGRAM: Run nonDet") nonDet { var sum = 0 val b = choose() @@ -108,7 +145,8 @@ class Test : UnitSpec() { Tuple4(i, b, b2, sum) }.also { println("PROGRAM: Result ${it.toList()}") } } - "testError" { + "error" { + println("PROGRAM: Run error") error { catch({ e -> println("PROGRAM: Got error: $e") @@ -121,8 +159,9 @@ class Test : UnitSpec() { }.also { println("PROGRAM: Result $it") } } "either" { + println("PROGRAM: Run either") either { - val a = Right(11)() + val a = Right(5)() if (a > 10) Left("Larger than 10")() else a }.also { println("PROGRAM: Result $it") } From 3e03473ac1de1858c71fda5c4a822b492bb186b5 Mon Sep 17 00:00:00 2001 From: Simon Vergauwen Date: Mon, 27 Jul 2020 16:56:36 +0200 Subject: [PATCH 29/49] Add test suite + try to make suspend capable --- .../continuations/effectStack/program.kt | 3 +- .../kotlin/effectStack/EffectStackTest.kt | 107 +++++++++++++++ .../src/test/kotlin/effectStack/Run.kt | 16 +-- .../src/test/kotlin/effectStack/predef.kt | 129 ++++++++++++++++++ 4 files changed, 246 insertions(+), 9 deletions(-) create mode 100644 arrow-continuations/src/test/kotlin/effectStack/EffectStackTest.kt create mode 100644 arrow-continuations/src/test/kotlin/effectStack/predef.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt index 3e64c2b5d..34ee93b48 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt @@ -18,7 +18,8 @@ interface Delimited { suspend fun control(func: suspend (DelimitedCont) -> R): A } -fun prompt(f: suspend Delimited.() -> A): A = DelimitedScope("Prompt", f).run() +fun prompt(f: suspend Delimited.() -> A): A = + DelimitedScope("Prompt", f).run() /** * Idea we have two paths: diff --git a/arrow-continuations/src/test/kotlin/effectStack/EffectStackTest.kt b/arrow-continuations/src/test/kotlin/effectStack/EffectStackTest.kt new file mode 100644 index 000000000..d4a5df2a3 --- /dev/null +++ b/arrow-continuations/src/test/kotlin/effectStack/EffectStackTest.kt @@ -0,0 +1,107 @@ +package effectStack + +import arrow.core.Either +import arrow.core.identity +import arrow.core.test.generators.either +import arrow.core.test.generators.nonEmptyList +import arrow.core.test.generators.throwable +import io.kotlintest.fail +import io.kotlintest.properties.Gen +import io.kotlintest.properties.PropertyContext +import io.kotlintest.properties.assertAll +import io.kotlintest.properties.forAll +import io.kotlintest.shouldBe +import io.kotlintest.shouldThrow +import io.kotlintest.specs.StringSpec +import kotlinx.coroutines.runBlocking + +class EffectStackTest : StringSpec() { + init { + fun forAll2(gena: Gen, fn: suspend PropertyContext.(a: A) -> Boolean) { + assertAll(gena) { a -> + unsafeRunSync { fn(a) shouldBe true } + } + } + + fun forAll2(gena: Gen, genb: Gen, fn: suspend PropertyContext.(a: A, b: B) -> Boolean) { + assertAll(gena, genb) { a, b -> + runBlocking { fn(a, b) shouldBe true } + } + } + + fun forAll2(gena: Gen, genb: Gen, genc: Gen, fn: suspend PropertyContext.(a: A, b: B, c: C) -> Boolean) { + assertAll(gena, genb, genc) { a, b, c -> + unsafeRunSync { fn(a, b, c) shouldBe true } + } + } + + + "monadic can bind values" { + forAll2(Gen.either(Gen.string(), Gen.int())) { value -> + effectStack.either { + value.suspend().invoke() + } == value + } + } + + "monadic rethrows exceptions" { + forAll2(Gen.int(), Gen.throwable()) { value, e -> + shouldThrow { + val r = effectStack.either { + Either.Right(value).suspend().invoke() + e.suspend() + } + + fail("$e expected, but found $r") + } == e + } + } + + "error can bind values" { + forAll2(Gen.either(Gen.string(), Gen.int())) { value -> + error { + value.fold( + { s -> raise(s.suspend()) }, + ::identity + ) + } == value + } + } + + "error can rethrow exceptions" { + forAll2(Gen.throwable()) { e -> + shouldThrow { + val r = error { + e.suspend() + } + fail("$e expected, but found $r") + } == e + } + } + + "list" { + forAll2(Gen.list(Gen.int()), Gen.list(Gen.int())) { iis, sss -> + list { + val a = iis.suspend().invoke() + val b = sss.suspend().invoke() + "$a$b ".suspend() + } == iis.flatMap { a -> sss.map { b -> "$a$b " } } + } + } + + "list can rethrow exceptions" { + forAll2(Gen.nonEmptyList(Gen.int()), Gen.nonEmptyList(Gen.int()), Gen.throwable()) { iis, sss, e -> + shouldThrow { + val r = list { + val a = iis.all.suspend().invoke() + val b = sss.all.suspend().invoke() + e.suspend() + "$a$b " + } + + fail("$e expected, but found $r") + } == e + } + } + } +} diff --git a/arrow-continuations/src/test/kotlin/effectStack/Run.kt b/arrow-continuations/src/test/kotlin/effectStack/Run.kt index 66fe4e09f..529210be9 100644 --- a/arrow-continuations/src/test/kotlin/effectStack/Run.kt +++ b/arrow-continuations/src/test/kotlin/effectStack/Run.kt @@ -16,25 +16,24 @@ import io.kotlintest.shouldBe import kotlin.coroutines.RestrictsSuspension import kotlin.random.Random -@RestrictsSuspension interface Monadic { suspend operator fun Kind.invoke(): A } -fun either(f: suspend Monadic>.() -> A): Kind, A> = prompt { +suspend fun either(f: suspend Monadic>.() -> A): Kind, A> = prompt { val m = object : Monadic> { override suspend fun Kind, A>.invoke(): A = control { k -> fix().flatMap { k(it).fix() } } } + Either.Right(f(m)) } -@RestrictsSuspension interface Error { suspend fun raise(e: E): A suspend fun catch(handle: suspend Error.(E) -> A, f: suspend Error.() -> A): A } -fun error(f: suspend Error.() -> A): Either = prompt { +suspend fun error(f: suspend Error.() -> A): Either = prompt { val p = object : Error { override suspend fun raise(e: E): A = control { Left(e) } override suspend fun catch(handle: suspend Error.(E) -> B, f: suspend Error.() -> B): B = @@ -45,19 +44,17 @@ fun error(f: suspend Error.() -> A): Either = prompt { Right(f(p)) } -@RestrictsSuspension interface NonDet { suspend fun effect(f: suspend () -> B): B suspend fun empty(): A suspend fun choose(): Boolean } -@RestrictsSuspension interface ListComputation { suspend operator fun List.invoke(): C } -inline fun list(crossinline f: suspend ListComputation.() -> A): List = +suspend fun list(f: suspend ListComputation.() -> A): List = prompt { val p = object : ListComputation { override suspend fun List.invoke(): C = @@ -67,11 +64,12 @@ inline fun list(crossinline f: suspend ListComputation.() -> A): List = } } } + listOf(f(p)) } -inline fun nonDet(crossinline f: suspend NonDet.() -> A): Sequence = prompt { +suspend fun nonDet(f: suspend NonDet.() -> A): Sequence = prompt { val p = object : NonDet { override suspend fun effect(f: suspend () -> B): B = control { it(f()) } override suspend fun choose(): Boolean = control { k -> k(true) + k(false) } @@ -108,6 +106,7 @@ class Test : UnitSpec() { Tuple4(i, b, b2, sum) }.also { println("PROGRAM: Result ${it.toList()}") } } + "testError" { error { catch({ e -> @@ -120,6 +119,7 @@ class Test : UnitSpec() { } }.also { println("PROGRAM: Result $it") } } + "either" { either { val a = Right(11)() diff --git a/arrow-continuations/src/test/kotlin/effectStack/predef.kt b/arrow-continuations/src/test/kotlin/effectStack/predef.kt new file mode 100644 index 000000000..baf67d6f7 --- /dev/null +++ b/arrow-continuations/src/test/kotlin/effectStack/predef.kt @@ -0,0 +1,129 @@ +package effectStack + +import arrow.core.Either +import io.kotlintest.properties.Gen +import io.kotlintest.properties.shrinking.Shrinker +import kotlinx.coroutines.Dispatchers +import java.util.concurrent.locks.AbstractQueuedSynchronizer +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.intercepted +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.startCoroutine +import kotlin.random.Random + +internal fun unsafeRunSync(f: suspend () -> A): A { + val latch = OneShotLatch() + var ref: Either? = null + f.startCoroutine(Continuation(EmptyCoroutineContext) { a -> + ref = a.fold({ aa -> Either.Right(aa) }, { t -> Either.Left(t) }) + latch.releaseShared(1) + }) + + latch.acquireSharedInterruptibly(1) + + return when (val either = ref) { + is Either.Left -> throw either.a + is Either.Right -> either.b + null -> throw RuntimeException("Suspend execution should yield a valid result") + } +} + + +private class OneShotLatch : AbstractQueuedSynchronizer() { + override fun tryAcquireShared(ignored: Int): Int = + if (state != 0) { + 1 + } else { + -1 + } + + override fun tryReleaseShared(ignore: Int): Boolean { + state = 1 + return true + } +} + +internal suspend fun Throwable.suspend(): Nothing = + suspendCoroutineUninterceptedOrReturn { cont -> + Gen.int().orNull() + suspend { throw this }.startCoroutine(Continuation(EmptyCoroutineContext) { + cont.intercepted().resumeWith(it) + }) + + COROUTINE_SUSPENDED + } + +internal suspend fun A.suspend(): A = + suspendCoroutineUninterceptedOrReturn { cont -> + suspend { this }.startCoroutine(Continuation(EmptyCoroutineContext) { + cont.intercepted().resumeWith(it) + }) + + COROUTINE_SUSPENDED + } + +typealias Suspended = suspend () -> A + +@JvmName("suspendedErrors") +fun Gen.suspended(): Gen> = + suspended { e -> suspend { throw e } } as Gen> + +fun Gen.suspended(): Gen> = + suspended { a -> suspend { a } } + +private fun Gen.suspended(liftK: (A) -> (suspend () -> A)): Gen> { + val outer = this + return object : Gen> { + override fun constants(): Iterable> = + outer.constants().flatMap { value -> + exhaustiveOptions.map { (a, b, c) -> liftK(value).asSuspended(a, b, c) } + } + + override fun random(): Sequence> = + outer.random().map { + liftK(it).asSuspended(Random.nextBoolean(), Random.nextBoolean(), Random.nextBoolean()) + } + + override fun shrinker(): Shrinker>? { + val s = outer.shrinker() + return if (s == null) null else object : Shrinker> { + override fun shrink(failure: Suspended): List> { + val failed = unsafeRunSync { failure.invoke() } + return s.shrink(failed).map { + liftK(it).asSuspended(Random.nextBoolean(), Random.nextBoolean(), Random.nextBoolean()) + } + } + } + } + } +} + +private val exhaustiveOptions = + listOf( + Triple(false, false, false), + Triple(false, false, true), + Triple(false, true, false), + Triple(false, true, true), + Triple(true, false, false), + Triple(true, false, true), + Triple(true, true, false), + Triple(true, true, true) + ) + +internal fun (suspend () -> A).asSuspended( + suspends: Boolean, + emptyOrNot: Boolean, + intercepts: Boolean +): Suspended = suspend { + if (!suspends) this.invoke() + else suspendCoroutineUninterceptedOrReturn { cont -> + val ctx = if (emptyOrNot) EmptyCoroutineContext else Dispatchers.Default + suspend { this.invoke() }.startCoroutine(Continuation(ctx) { + if (intercepts) cont.resumeWith(it) else cont.intercepted().resumeWith(it) + }) + + COROUTINE_SUSPENDED + } +} From b11bd06ea610af6c25bf8157af078850ae743c6d Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Mon, 27 Jul 2020 22:21:34 +0200 Subject: [PATCH 30/49] minor name refactor --- .../continuations/effectStack/program.kt | 54 +++++++++---------- .../src/test/kotlin/effectStack/Run.kt | 50 ++++++++--------- 2 files changed, 49 insertions(+), 55 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt index 530496b5c..b2fb615f4 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt @@ -10,16 +10,16 @@ import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -interface DelimitedCont { - suspend operator fun invoke(a: A): R +interface DelimitedCont { + suspend operator fun invoke(a: A): B } -interface Delimited { - suspend fun control(func: suspend (DelimitedCont) -> R): A - suspend fun prompt(f: suspend Delimited.() -> A): A +interface Delimited { + suspend fun shift(func: suspend (DelimitedCont) -> A): B + suspend fun reset(f: suspend Delimited.() -> B): B } -suspend fun prompt(f: suspend Delimited.() -> A): A = +suspend fun reset(f: suspend Delimited.() -> A): A = DelimitedScope("Prompt", f).run() /** @@ -31,21 +31,21 @@ suspend fun prompt(f: suspend Delimited.() -> A): A = * This can be used to implement nondeterminism together with any other effect and so long as the "pure" code in a function * is fast this won't be a problem, but if it isn't this will result in terrible performance (but only if multishot is actually used) */ -open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.() -> R) : Delimited { +open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.() -> A) : Delimited { - private val ref = atomic(null) - private val currF = atomic<(suspend () -> R)?>(null) + private val ref = atomic(null) + private val currF = atomic<(suspend () -> A)?>(null) internal open val stack: MutableList = mutableListOf() - private val cbs = mutableListOf>() + private val cbs = mutableListOf>() - override suspend fun control(func: suspend (DelimitedCont) -> R): A { + override suspend fun shift(func: suspend (DelimitedCont) -> A): B { return suspendCoroutine { k -> // println("Suspending for control: $label") // println("Stack: $stack") - val o = object : DelimitedCont { - val state = atomic?>(k) + val o = object : DelimitedCont { + val state = atomic?>(k) val snapshot = stack.toList() - override suspend fun invoke(a: A): R { + override suspend fun invoke(a: B): A { // println("Invoke cont with state is null: ${state.value == null} && arg $a") val cont = state.getAndSet(null) // Reexecute f but this time on control we resume the continuation directly with a @@ -64,9 +64,9 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( } } - suspend fun startMultiShot(stack: List): R = MultiShotDelimScope(stack, f).run() + suspend fun startMultiShot(stack: List): A = MultiShotDelimScope(stack, f).run() - override suspend fun prompt(f: suspend Delimited.() -> A): A = + override suspend fun reset(f: suspend Delimited.() -> B): B = DelimitedScope("inner", f).let { scope -> scope::run.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { TODO("Is this ever resumed?") @@ -89,11 +89,11 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( */ suspendCoroutine {} } - else it as A + else it as B } } - private fun getValue(): R? = + private fun getValue(): A? = // println("Running suspended $label") ref.loop { // controls function called a continuation which now finished @@ -106,14 +106,14 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( ref.value = res.getOrThrow() }).let { // early return controls function did not call its continuation - if (it != COROUTINE_SUSPENDED) ref.value = it as R + if (it != COROUTINE_SUSPENDED) ref.value = it as A } // short since we run out of conts to call else return@getValue null } } - open suspend fun run(): R { + open suspend fun run(): A { // println("Running $dbgLabel") f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { // println("Put value ${(it.getOrThrow() as Sequence).toList()}") @@ -124,7 +124,7 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( // this will block indefinitely if there is no parent scope, but a program like that should not typecheck // at least not when using control getValue() ?: return@run suspendCoroutine {} - } else return@run res as R + } else return@run res as A } // control has been called and its continuations have been invoked, resume the continuations in reverse order @@ -133,17 +133,17 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( } } -class MultiShotDelimScope( +class MultiShotDelimScope( localStack: List, - f: suspend Delimited.() -> R -) : DelimitedScope("Multishot", f) { + f: suspend Delimited.() -> A +) : DelimitedScope("Multishot", f) { private var depth = 0 override val stack: MutableList = localStack.toMutableList() - override suspend fun control(func: suspend (DelimitedCont) -> R): A = - if (stack.size > depth) stack[depth++] as A + override suspend fun shift(func: suspend (DelimitedCont) -> A): B = + if (stack.size > depth) stack[depth++] as B else { // println("EmptyStack") depth++ - super.control(func) + super.shift(func) } } diff --git a/arrow-continuations/src/test/kotlin/effectStack/Run.kt b/arrow-continuations/src/test/kotlin/effectStack/Run.kt index 679e37864..3aeb0ec33 100644 --- a/arrow-continuations/src/test/kotlin/effectStack/Run.kt +++ b/arrow-continuations/src/test/kotlin/effectStack/Run.kt @@ -2,31 +2,25 @@ package effectStack import arrow.Kind import arrow.continuations.effectStack.Delimited -import arrow.continuations.effectStack.prompt +import arrow.continuations.effectStack.reset import arrow.core.Either import arrow.core.EitherPartialOf -import arrow.core.ForListK import arrow.core.Left import arrow.core.Right import arrow.core.Tuple4 -import arrow.core.extensions.either.monad.flatten import arrow.core.fix import arrow.core.flatMap -import arrow.core.k import arrow.core.test.UnitSpec -import arrow.typeclasses.Monad import io.kotlintest.shouldBe -import kotlin.coroutines.RestrictsSuspension -import kotlin.coroutines.suspendCoroutine import kotlin.random.Random interface Monadic { suspend operator fun Kind.invoke(): A } -suspend fun either(f: suspend Monadic>.() -> A): Kind, A> = prompt { +suspend fun either(f: suspend Monadic>.() -> A): Kind, A> = reset { val m = object : Monadic> { - override suspend fun Kind, A>.invoke(): A = control { k -> fix().flatMap { k(it).fix() } } + override suspend fun Kind, A>.invoke(): A = shift { k -> fix().flatMap { k(it).fix() } } } Either.Right(f(m)) @@ -37,11 +31,11 @@ interface Error { suspend fun catch(handle: suspend Error.(E) -> A, f: suspend Error.() -> A): A } -suspend fun error(f: suspend Error.() -> A): Either = prompt { +suspend fun error(f: suspend Error.() -> A): Either = reset { val p = object : Error { - override suspend fun raise(e: E): A = control { Left(e) } + override suspend fun raise(e: E): A = shift { Left(e) } override suspend fun catch(handle: suspend Error.(E) -> B, f: suspend Error.() -> B): B = - control { k -> + shift { k -> error { f() }.fold({ e -> error { handle(e) }.flatMap { k(it) } }, { b -> k(b) }) } } @@ -59,10 +53,10 @@ interface ListComputation { } suspend inline fun list(crossinline f: suspend ListComputation.() -> A): List = - prompt {this + reset {this val p = object : ListComputation { override suspend fun List.invoke(): C = - control { cb -> + shift { cb -> flatMap { cb(it) } @@ -73,11 +67,11 @@ suspend inline fun list(crossinline f: suspend ListComputation.() -> A): Lis } -suspend inline fun nonDet(crossinline f: suspend NonDet.() -> A): Sequence = prompt { +suspend inline fun nonDet(crossinline f: suspend NonDet.() -> A): Sequence = reset { val p = object : NonDet { - override suspend fun effect(f: suspend () -> B): B = control { it(f()) } - override suspend fun choose(): Boolean = control { k -> k(true) + k(false) } - override suspend fun empty(): A = control { emptySequence() } + override suspend fun effect(f: suspend () -> B): B = shift { it(f()) } + override suspend fun choose(): Boolean = shift { k -> k(true) + k(false) } + override suspend fun empty(): A = shift { emptySequence() } } sequenceOf(f(p)) } @@ -88,8 +82,8 @@ class Test : UnitSpec() { init { "yield building a list" { println("PROGRAM: Run yield building a list") - prompt> { - suspend fun Delimited>.yield(a: A): Unit = control { listOf(a) + it(Unit) } + reset> { + suspend fun Delimited>.yield(a: A): Unit = shift { listOf(a) + it(Unit) } yield(1) yield(2) @@ -100,20 +94,20 @@ class Test : UnitSpec() { } "test" { println("PROGRAM: Run Test") - val res = 10 + prompt { - 2 + control { it(it(3)) + 100 } + val res = 10 + reset { + 2 + shift { it(it(3)) + 100 } } println("PROGRAM: Result $res") } "multi" { println("PROGRAM: multi") - prompt> fst@{ + reset> fst@{ val ctx = this - val i: Int = control { it(2) } - Right(i * 2 + prompt snd@{ - val k: Int = this@snd.control { it(1) + it(2) } - val j: Int = if (i == 5) ctx.control { Left("Not today") } - else this@snd.control { it(4) } + val i: Int = shift { it(2) } + Right(i * 2 + reset snd@{ + val k: Int = shift { it(1) + it(2) } + val j: Int = if (i == 5) ctx.shift { Left("Not today") } + else shift { it(4) } j + k }) }.also { println("PROGRAM: Result $it") } From 4533b753e9091871f177627d2cfa87f8d51ee39e Mon Sep 17 00:00:00 2001 From: Jannis Date: Tue, 28 Jul 2020 00:55:08 +0200 Subject: [PATCH 31/49] Added a bunch of comments + slightly better names --- .../continuations/effectStack/program.kt | 75 +++++++++++++------ 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt index b2fb615f4..695281295 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt @@ -33,34 +33,48 @@ suspend fun reset(f: suspend Delimited.() -> A): A = */ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.() -> A) : Delimited { - private val ref = atomic(null) - private val currF = atomic<(suspend () -> A)?>(null) + private val ret = atomic(null) + // TODO More descriptive name + private val currShiftFn = atomic<(suspend () -> A)?>(null) + // TODO more efficient data structures. O(1) append + O(1) pop would be best internal open val stack: MutableList = mutableListOf() + // TODO for this we could use a datastructure that can O(1) append and has O(1) popLast() private val cbs = mutableListOf>() override suspend fun shift(func: suspend (DelimitedCont) -> A): B { + // suspend f since we first need a result from DelimitedCont.invoke return suspendCoroutine { k -> - // println("Suspending for control: $label") + // println("Suspending for shift: $label") // println("Stack: $stack") + // create a continuation which supports invoking either the suspended f or restarting it with a sliced stack val o = object : DelimitedCont { - val state = atomic?>(k) + // The "live" continuation for f which is currently suspended. Can only be called once + val liveContinuation = atomic?>(k) + // TODO better datastructure + // A snapshot of f's effect-stack up to this shift's function invocation val snapshot = stack.toList() override suspend fun invoke(a: B): A { // println("Invoke cont with state is null: ${state.value == null} && arg $a") - val cont = state.getAndSet(null) - // Reexecute f but this time on control we resume the continuation directly with a + val cont = liveContinuation.getAndSet(null) + // Re-execute f, but in a new scope which contains the stack slice + a and will use that to fill in the first + // calls to shift return if (cont == null) startMultiShot(snapshot + a) + // we have a "live" continuation to resume to so we suspend the shift block and do exactly that else suspendCoroutineUninterceptedOrReturn { - // push stuff to the stack + // a is the result of an effect, push it onto the stack. Note this refers to the outer stack, not + // the slice captured here, which is now immutable stack.add(a) + // invoke needs to return A at some point so we need to append the Continuation so that it will be called when this + // scope's run method is done cbs.add(it) - // run cont + // resume f with value a cont.resume(a) COROUTINE_SUSPENDED } } } - currF.value = { func(o) } + // the shift function is the next fn to execute + currShiftFn.value = { func(o) } } } @@ -86,6 +100,13 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( * the outer prompt because to return to i it needs the result of the outer prompt. The only sensible way of getting * such a result is to rerun it with it's previous stack. However this means the state upon reaching * the inner prompt again is deterministic and always the same, which is why it'll loop. + * + * TODO: Is this actually true? We can consider this@fst.control to capture up to the next control/reset. This means + * it could indeed restart the outer continuation with a stack where the top element has been replaced by whatever we invoked with + * If there is nothing on the stack or the topmost item is (reference?-)equal to our a we will infinite loop and we + * should just crash here to not leave the user wondering wtf is happening. It might also be that a user does side-effects + * outside of control which we cannot capture and thus it produces a dirty rerun which might not loop. Idk if that should + * be considered valid behaviour */ suspendCoroutine {} } @@ -95,20 +116,24 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( private fun getValue(): A? = // println("Running suspended $label") - ref.loop { - // controls function called a continuation which now finished + ret.loop { + // shift function called f's continuation which now finished if (it != null) return@getValue it + // we are not done yet else { - val res = currF.getAndSet(null) + val res = currShiftFn.getAndSet(null) if (res != null) res.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { res -> // println("Resumption with ${(res.getOrThrow() as Sequence).toList()}") - ref.value = res.getOrThrow() + // a shift block finished processing. This is now our intermediate return value + ret.value = res.getOrThrow() }).let { - // early return controls function did not call its continuation - if (it != COROUTINE_SUSPENDED) ref.value = it as A + // the shift function did not call its continuation which means we short-circuit + if (it != COROUTINE_SUSPENDED) ret.value = it as A + // if we did suspend we have either hit a shift function from the parent scope or another shift function + // in both cases we just loop } - // short since we run out of conts to call + // short since we run out of shift functions to call else return@getValue null } } @@ -117,19 +142,23 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( // println("Running $dbgLabel") f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { // println("Put value ${(it.getOrThrow() as Sequence).toList()}") - ref.value = it.getOrThrow() + // f finished after being resumed. Save the value to resume the shift blocks later + ret.value = it.getOrThrow() }).let { res -> if (res == COROUTINE_SUSPENDED) { - // if it is null we are done calling through our control fns and we need a value from a parent scope now + // if it is null we are done calling through our shift fns and we need a value from a parent scope now // this will block indefinitely if there is no parent scope, but a program like that should not typecheck - // at least not when using control + // at least not when using shift getValue() ?: return@run suspendCoroutine {} - } else return@run res as A + } // we finished without ever suspending. This means there is no shift block and we can short circuit run + else return@run res as A } - // control has been called and its continuations have been invoked, resume the continuations in reverse order - cbs.asReversed().forEach { it.resume(ref.value!!) } - return ref.value!! + // 1..n shift blocks were called and now need to be resumed with the result. This will sort of bubble up because each + // resumed shift block can alter the returned value. + cbs.asReversed().forEach { it.resume(ret.value!!) } + // return the final value after all shift blocks finished processing the result + return ret.value!! } } From ef56c8847760372afc3cd0dc5be430872ae61f26 Mon Sep 17 00:00:00 2001 From: Jannis Date: Wed, 29 Jul 2020 00:05:39 +0200 Subject: [PATCH 32/49] Broken but informative version of nested effectstack improvements --- .../effectStackUnpackedResets/program.kt | 271 ++++++++++++++++++ .../kotlin/effectStackUnpackedResets/Run2.kt | 32 +++ 2 files changed, 303 insertions(+) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt create mode 100644 arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt new file mode 100644 index 000000000..78a506564 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt @@ -0,0 +1,271 @@ +package arrow.continuations.effectStackUnpackedResets + +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn +import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import kotlin.reflect.KProperty + +interface DelimitedCont { + suspend operator fun invoke(a: A): B +} + +interface Delimited { + suspend fun shift(func: suspend (DelimitedCont) -> A): B + suspend fun reset(f: suspend Delimited.() -> B): B +} + +suspend fun reset(f: suspend Delimited.() -> A): A = + DelimitedScope("Prompt", f).run {} + +/** + * Idea we have two paths: + * One path is the normal coroutine. It fills an effect stack everytime its continuation is resumed with a value. + * Then if a continuation is run more than once we restart the entire computation [f] and use the effect stack for as long as possible + * When the effect stack runs out of values we resume normal coroutine behaviour. + * + * This can be used to implement nondeterminism together with any other effect and so long as the "pure" code in a function + * is fast this won't be a problem, but if it isn't this will result in terrible performance (but only if multishot is actually used) + */ +open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.() -> A) : Delimited { + + open val parentScope: DelimitedScope<*>? = null + internal val childScope = atomic?>(null) + internal fun setChildScope(s: DelimitedScope<*>?): Unit { + childScope.value = s + } + + internal fun getChildScope(): DelimitedScope<*>? = childScope.value + + internal val ret = atomic(null) + + // TODO More descriptive name + private val currShiftFn = atomic<(suspend () -> A)?>(null) + + // TODO more efficient data structures. O(1) append + O(1) pop would be best + internal open val stack: MutableList = mutableListOf() + + // TODO for this we could use a datastructure that can O(1) append and has O(1) popLast() + private val cbs = mutableListOf>() + + internal val childCb = atomic?>(null) + internal val multiChildCb = atomic?>(null) + + internal fun trace(msg: String): Unit = println("$dbgLabel: $msg") + + internal fun setMultiChildCb(c: Continuation): Unit { + trace("Set multi child cb") + multiChildCb.value = c + } + + internal fun setChildCB(c: Continuation): Unit { + trace("Set child cb") + childCb.value = c + } + + override suspend fun shift(func: suspend (DelimitedCont) -> A): B { + when (val cs = childScope.value) { + is MultiShotDelimScope -> if (cs.done.not() && cs.depth < cs.stack.size) { + trace("Early return from stack ${cs.stack[cs.depth]}") + return cs.stack[cs.depth++] as B + } else Unit + else -> Unit + } + // suspend f since we first need a result from DelimitedCont.invoke + return suspendCoroutine { k -> + // println("Suspending for shift: $label") + // println("Stack: $stack") + // create a continuation which supports invoking either the suspended f or restarting it with a sliced stack + val o = object : DelimitedCont { + // The "live" continuation for f which is currently suspended. Can only be called once + val liveContinuation = atomic?>(k) + + // TODO better datastructure + // A snapshot of f's effect-stack up to this shift's function invocation + val offset: Int = stack.size + override suspend fun invoke(a: B): A { + // println("Invoke cont with state is null: ${state.value == null} && arg $a") + val cont = liveContinuation.getAndSet(null) + // Re-execute f, but in a new scope which contains the stack slice + a and will use that to fill in the first + // calls to shift + return if (cont == null) startMultiShot(offset, a) + // we have a "live" continuation to resume to so we suspend the shift block and do exactly that + else suspendCoroutineUninterceptedOrReturn { + trace("Invoke $a") + // a is the result of an effect, push it onto the stack. Note this refers to the outer stack, not + // the slice captured here, which is now immutable + stack.add(a) + // invoke needs to return A at some point so we need to append the Continuation so that it will be called when this + // scope's run method is done + cbs.add(it) + // resume f with value a + cont.resume(a) + COROUTINE_SUSPENDED + } + } + } + // the shift function is the next fn to execute + currShiftFn.value = { func(o) } + } + } + + open suspend fun startMultiShot(end: Int, b: Any?): A = startMultiShot(0, end, b) + + suspend fun startMultiShot(start: Int, end: Int, b: Any?): A = + MultiShotDelimScope(this@DelimitedScope, stack.subList(start, end).toList() + b, f).let { scope -> + // Tell the parent we are running multishot + parentScope?.setChildScope(scope) + scope.run { + trace("Multi cb") + parentScope?.setMultiChildCb(it) + }.also { parentScope?.setChildScope(scope.parentScope) } + } + + override suspend fun reset(f: suspend Delimited.() -> B): B = + ChildScope(this@DelimitedScope, f).run { + trace("Child cb") + setChildCB(it) + } + + internal fun getValue(): A? = + // println("Running suspended $label") + ret.loop { + trace("Get loop start") + // shift function called f's continuation which now finished + if (it != null) return@getValue it + // we are not done yet + else { + trace("Get loop: no result") + val res = currShiftFn.getAndSet(null) + if (res != null) + res.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { res -> + // println("Resumption with ${(res.getOrThrow() as Sequence).toList()}") + // a shift block finished processing. This is now our intermediate return value + ret.value = res.getOrThrow() + }).let { + // the shift function did not call its continuation which means we short-circuit + if (it != COROUTINE_SUSPENDED) ret.value = it as A + // if we did suspend we have either hit a shift function from the parent scope or another shift function + // in both cases we just loop + } + // short since we run out of shift functions to call + else { + trace("Get loop: no work") + multiChildCb.getAndSet(null)?.also { trace("Resuming multishot child") }?.resume(Unit) + childCb.getAndSet(null)?.also { trace("Resuming child") }?.resume(Unit) ?: return@getValue null + } + } + } + + fun hasWork(): Boolean = (currShiftFn.value != null || multiChildCb.value != null) + || (parentScope != null && parentScope!!.hasWork()) + + open suspend fun run(handleSuspend: (Continuation) -> Unit): A { + trace("Started runloop") + f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { + // println("Put value ${(it.getOrThrow() as Sequence).toList()}") + // f finished after being resumed. Save the value to resume the shift blocks later + trace("Resumed with $it") + ret.value = it.getOrThrow() + }).let { res -> + if (res == COROUTINE_SUSPENDED) { + trace("Starting suspend loop") + while (true) { + val a = getValue() + if (a == null) { + trace("Yielding to parent"); suspendCoroutine(handleSuspend) + } else break + } + } // we finished without ever suspending. This means there is no shift block and we can short circuit run + else { + trace("no suspension. Returning $res"); return@run res as A + } + } + + trace("Done. Number of cbs: ${cbs.size}") + cbs.asReversed().forEach { + it.resume(ret.value!!) + // This may have spawned additional work for a parent + trace("Parent has work: ${parentScope != null && parentScope!!.hasWork()}") + if (parentScope != null && parentScope!!.hasWork()) { + trace("Yielding to parent"); suspendCoroutine(handleSuspend) + } + + trace("Value is ${ret.value}") + } + trace("Returned with value ${ret.value}") + // return the final value after all shift blocks finished processing the result + return ret.value!! + } +} + +open class ChildScope( + final override val parentScope: DelimitedScope<*>, + f: suspend Delimited.() -> A +) : DelimitedScope("Child-" + unique++, f) { + override val stack: MutableList = parentScope.stack + private val offset: Int = parentScope.stack.size + override suspend fun startMultiShot(end: Int, b: Any?): A = + startMultiShot(offset, end, b) +} + +// TODO I can avoid list copying if I also work with indexes better here! +open class MultiShotDelimScope( + final override val parentScope: DelimitedScope<*>, + localStack: List, + func: suspend Delimited.() -> A, + var startingOffset: Int = 0 +) : DelimitedScope("Multishot-" + unique++, func) { + internal var depth by ParentDepth(0, parentScope, startingOffset) + internal var done = false + override val stack: MutableList = localStack.toMutableList().also { trace("Running multishot with $localStack") } + + override suspend fun shift(func: suspend (DelimitedCont) -> A): B = + if (stack.size > depth && done.not()) { + trace("Using the stack for shift: ${stack[depth]}") + trace("Stack: $stack") + stack[depth++] as B + } else { + done = true + trace("Done by shift") + super.shift(func) + } + + override suspend fun reset(p: suspend Delimited.() -> B): B = + if (stack.size > depth && done.not()) { + trace("Reset from multishot with stack left. Depth $depth") + // there are still elements on the stack, so run f as Multishot in child mode + MultiShotDelimScope(this@MultiShotDelimScope, stack, p, depth) + .let { + setChildScope(it) + it.run { + trace("Suspended multi shot in multi shot") + setChildCB(it) + } + } + .also { done = true }.also { trace("Done by reset1: $it. Stack $stack") } + } else { + done = true + trace("Done by reset2") + super.reset(p) + } + + override suspend fun startMultiShot(end: Int, b: Any?): A { + trace("Multishot from multishot $startingOffset $end") + return super.startMultiShot(startingOffset, end, b) + } +} + +class ParentDepth(var def: Int, val parentScope: DelimitedScope<*>?, val offset: Int) { + operator fun getValue(thisRef: Any?, property: KProperty<*>): Int = + (if (parentScope is MultiShotDelimScope) parentScope.depth else def) + offset + operator fun setValue(thisRef: Any?, property: KProperty<*>, i: Int): Unit = + if (parentScope is MultiShotDelimScope) parentScope.depth = i - offset else def = i - offset +} + +var unique = 0 diff --git a/arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt b/arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt new file mode 100644 index 000000000..5fcc5f9f6 --- /dev/null +++ b/arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt @@ -0,0 +1,32 @@ +package effectStackUnpackedResets + +import arrow.continuations.effectStackUnpackedResets.reset +import arrow.core.Either +import arrow.core.Left +import arrow.core.Right +import arrow.core.flatMap +import arrow.core.test.UnitSpec + +class Run2 : UnitSpec() { + init { + "multi" { + println("PROGRAM: multi") + reset> fst@{ + val ctx = this + val i: Int = shift { it(5) } + // println("HERE") + val r = i * 2 + reset snd@{ + val k: Int = shift { it(3) } + // println("There") + val j: Int = if (i == 5) ctx.shift { it(10).flatMap { i -> it(10).map { i + it } } } + else shift { it(4) } + val o: Int = shift { it(1) + it(2) } + // println("Those") + j + k + o + } + val l: Int = shift { it(8) } + Right(r + l) + }.also { println("PROGRAM: Result $it") } + } + } +} From 02edd777d318a4a40a505b488e5725b5ffef3656 Mon Sep 17 00:00:00 2001 From: Jannis Date: Wed, 29 Jul 2020 01:01:33 +0200 Subject: [PATCH 33/49] Better nested multishot support Still behaves slightly weird, but for now it works --- .../effectStackUnpackedResets/program.kt | 27 +++++++++++++++---- .../kotlin/effectStackUnpackedResets/Run2.kt | 17 +++++++++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt index 78a506564..7d3bf9c06 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt @@ -93,7 +93,10 @@ open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.( val cont = liveContinuation.getAndSet(null) // Re-execute f, but in a new scope which contains the stack slice + a and will use that to fill in the first // calls to shift - return if (cont == null) startMultiShot(offset, a) + return if (cont == null) { + trace("Starting multishot with $a and current total stack $stack") + startMultiShot(offset, a) + } // we have a "live" continuation to resume to so we suspend the shift block and do exactly that else suspendCoroutineUninterceptedOrReturn { trace("Invoke $a") @@ -219,9 +222,10 @@ open class MultiShotDelimScope( final override val parentScope: DelimitedScope<*>, localStack: List, func: suspend Delimited.() -> A, - var startingOffset: Int = 0 + offsetFromStack: Int = 0, + private val startingOffset: Int = 0 ) : DelimitedScope("Multishot-" + unique++, func) { - internal var depth by ParentDepth(0, parentScope, startingOffset) + internal var depth by ParentDepth(0, parentScope, offsetFromStack) internal var done = false override val stack: MutableList = localStack.toMutableList().also { trace("Running multishot with $localStack") } @@ -240,7 +244,7 @@ open class MultiShotDelimScope( if (stack.size > depth && done.not()) { trace("Reset from multishot with stack left. Depth $depth") // there are still elements on the stack, so run f as Multishot in child mode - MultiShotDelimScope(this@MultiShotDelimScope, stack, p, depth) + MultiShotDelimScope(this@MultiShotDelimScope, stack.subList(depth, stack.size), p, -depth, depth) .let { setChildScope(it) it.run { @@ -249,6 +253,7 @@ open class MultiShotDelimScope( } } .also { done = true }.also { trace("Done by reset1: $it. Stack $stack") } + .also { setChildScope(null) } } else { done = true trace("Done by reset2") @@ -257,7 +262,19 @@ open class MultiShotDelimScope( override suspend fun startMultiShot(end: Int, b: Any?): A { trace("Multishot from multishot $startingOffset $end") - return super.startMultiShot(startingOffset, end, b) + return MultiShotDelimScope( + this@MultiShotDelimScope, + stack.subList(0, end).toList() + b, + f, + -depth + ).let { scope -> + // Tell the parent we are running multishot + parentScope.setChildScope(scope) + scope.run { + trace("Multi cb") + parentScope.setMultiChildCb(it) + }.also { parentScope.setChildScope(scope.parentScope) } + } } } diff --git a/arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt b/arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt index 5fcc5f9f6..405085484 100644 --- a/arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt +++ b/arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt @@ -1,5 +1,6 @@ package effectStackUnpackedResets +import arrow.continuations.effectStackUnpackedResets.Delimited import arrow.continuations.effectStackUnpackedResets.reset import arrow.core.Either import arrow.core.Left @@ -9,6 +10,16 @@ import arrow.core.test.UnitSpec class Run2 : UnitSpec() { init { + suspend fun Delimited>.yield(a: A): Unit = shift { k -> listOf(a) + k(Unit) + k(Unit) } + "yield" { + println("PROGRAM: yield") + reset> { + yield(1) + yield(2) + yield(5) + emptyList() + }.also { println("Result: $it") } // should be 1,2,5, 2,5, 5 + } "multi" { println("PROGRAM: multi") reset> fst@{ @@ -16,11 +27,11 @@ class Run2 : UnitSpec() { val i: Int = shift { it(5) } // println("HERE") val r = i * 2 + reset snd@{ - val k: Int = shift { it(3) } + val k: Int = shift { it(3) + it(2) } // println("There") - val j: Int = if (i == 5) ctx.shift { it(10).flatMap { i -> it(10).map { i + it } } } + val j: Int = if (i == 5) ctx.shift { it(10).flatMap { i -> it(5).map { i + it } } } else shift { it(4) } - val o: Int = shift { it(1) + it(2) } + val o: Int = shift { it(1) } // println("Those") j + k + o } From 1964cf552213db20345d8bb16320d1b841e20e1b Mon Sep 17 00:00:00 2001 From: Raul Raja Date: Wed, 29 Jul 2020 17:47:18 +0200 Subject: [PATCH 34/49] interleaving draft --- .../src/test/kotlin/effectStack/Run.kt | 16 +++++++- .../effectStack/interleave/Interleave.kt | 41 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 arrow-continuations/src/test/kotlin/effectStack/interleave/Interleave.kt diff --git a/arrow-continuations/src/test/kotlin/effectStack/Run.kt b/arrow-continuations/src/test/kotlin/effectStack/Run.kt index 3aeb0ec33..487a3bedf 100644 --- a/arrow-continuations/src/test/kotlin/effectStack/Run.kt +++ b/arrow-continuations/src/test/kotlin/effectStack/Run.kt @@ -7,10 +7,14 @@ import arrow.core.Either import arrow.core.EitherPartialOf import arrow.core.Left import arrow.core.Right +import arrow.core.Some import arrow.core.Tuple4 import arrow.core.fix import arrow.core.flatMap +import arrow.core.some import arrow.core.test.UnitSpec +import effectStack.interleave.Interleave +import effectStack.interleave.lists import io.kotlintest.shouldBe import kotlin.random.Random @@ -53,7 +57,7 @@ interface ListComputation { } suspend inline fun list(crossinline f: suspend ListComputation.() -> A): List = - reset {this + reset { val p = object : ListComputation { override suspend fun List.invoke(): C = shift { cb -> @@ -160,5 +164,15 @@ class Test : UnitSpec() { else a }.also { println("PROGRAM: Result $it") } } + "lists interleave" { + lists { program() } shouldBe listOf(2, 4, 6) + } } } + +suspend inline fun Interleave<*>.program(): Int { + val a : Int = listOf(1, 2, 3)() + val b: Int? = a.some()() + val c: Int = b() + return c + c +} diff --git a/arrow-continuations/src/test/kotlin/effectStack/interleave/Interleave.kt b/arrow-continuations/src/test/kotlin/effectStack/interleave/Interleave.kt new file mode 100644 index 000000000..13f6964fa --- /dev/null +++ b/arrow-continuations/src/test/kotlin/effectStack/interleave/Interleave.kt @@ -0,0 +1,41 @@ +package effectStack.interleave + +import arrow.continuations.effectStack.Delimited +import arrow.continuations.effectStack.DelimitedCont +import arrow.continuations.effectStack.DelimitedScope +import arrow.continuations.effectStack.MultiShotDelimScope +import arrow.continuations.effectStack.reset +import arrow.core.Option +import arrow.core.identity +import effectStack.list + +suspend inline fun lists(crossinline f: suspend Interleave<*>.() -> A): List = + reset { + listOf(f(object : Interleave> , Delimited> by this { + override val f: suspend Interleave<*>.() -> List = { listOf(f(this)) } + override fun shortCircuit(): List = emptyList() + override fun just(b: Any?): List = listOf(b) as List + override suspend fun List.invoke(): C = + shift { cb -> + flatMap { cb(it) } + } + })) + } + +interface Interleave : Delimited { + val f: suspend Interleave<*>.() -> A + fun shortCircuit(): A + fun just(b: Any?): A + suspend operator fun Option.invoke(): C = + fold({ shift { shortCircuit() } }, ::identity) + + suspend operator fun C?.invoke(): C = + this ?: shift { shortCircuit() } + + suspend operator fun List.invoke(): C + +} + + + + From e3a3d976a9d793ecc171653ca9657b47b5e9cf30 Mon Sep 17 00:00:00 2001 From: Jannis Date: Thu, 30 Jul 2020 14:26:03 +0200 Subject: [PATCH 35/49] Implement basic delimited continuations with and without multishot --- .../continuations/generic/DelimitedCont.kt | 21 ++++ .../generic/MultiShotDelimCont.kt | 84 +++++++++++++++ .../generic/SingleShotDelimCont.kt | 67 ++++++++++++ .../src/test/kotlin/generic/TestSuite.kt | 100 ++++++++++++++++++ 4 files changed, 272 insertions(+) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/generic/SingleShotDelimCont.kt create mode 100644 arrow-continuations/src/test/kotlin/generic/TestSuite.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt new file mode 100644 index 000000000..ed06e61e0 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt @@ -0,0 +1,21 @@ +package arrow.continuations.generic + +/** + * Base interface for a continuation + */ +interface DelimitedContinuation { + suspend operator fun invoke(a: A): R +} + +/** + * Base interface for our scope. + */ +// TODO This should be @RestrictSuspension but that breaks because a superclass is not considered to be correct scope +// @RestrictsSuspension +interface DelimitedScope { + suspend fun shift(f: suspend (DelimitedContinuation) -> R): A +} + +interface RunnableDelimitedScope : DelimitedScope { + operator fun invoke(): R +} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt new file mode 100644 index 000000000..3a03829a6 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt @@ -0,0 +1,84 @@ +package arrow.continuations.generic + +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) : RunnableDelimitedScope { + + private val resultVar = atomic(null) + private val nextShift = atomic<(suspend () -> R)?>(null) + // TODO This can be append only and needs fast reversed access + private val shiftFnContinuations = mutableListOf>() + // TODO This can be append only and needs fast random access and slicing + internal open val stack = mutableListOf() + + class MultiShotCont( + liveContinuation: Continuation, + private val f: suspend DelimitedScope.() -> R, + private val stack: MutableList, + private val shiftFnContinuations: MutableList> + ) : DelimitedContinuation { + private val liveContinuation = atomic?>(liveContinuation) + private val stackOffset = stack.size + + override suspend fun invoke(a: A): R = + when (val cont = liveContinuation.getAndSet(null)) { + null -> PrefilledDelimContScope((stack.subList(0, stackOffset).toList() + a).toMutableList(), f).invoke() + else -> suspendCoroutine { resumeShift -> + shiftFnContinuations.add(resumeShift) + stack.add(a) + cont.resume(a) + } + } + } + + override suspend fun shift(func: suspend (DelimitedContinuation) -> R): A = suspendCoroutine { continueMain -> + val c = MultiShotCont(continueMain, f, stack, shiftFnContinuations) + assert(nextShift.compareAndSet(null, suspend { func(c) })) + } + + override fun invoke(): R { + f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result -> + resultVar.value = result.getOrThrow() + }).let { + if (it == COROUTINE_SUSPENDED) { + resultVar.loop { mRes -> + if (mRes == null) { + val nextShiftFn = nextShift.getAndSet(null) + ?: throw IllegalStateException("No further work to do but also no result!") + nextShiftFn.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { result -> + resultVar.value = result.getOrThrow() + }).let { + if (it != COROUTINE_SUSPENDED) resultVar.value = it as R + } + } else return@let + } + } + else return@invoke it as R + } + assert(resultVar.value != null) + for (c in shiftFnContinuations.asReversed()) c.resume(resultVar.value!!) + return resultVar.value!! + } + + companion object { + fun reset(f: suspend DelimitedScope.() -> R): R = MultiShotDelimContScope(f).invoke() + } +} + +class PrefilledDelimContScope( + override val stack: MutableList, + f: suspend DelimitedScope.() -> R +): MultiShotDelimContScope(f) { + var depth = 0 + + override suspend fun shift(func: suspend (DelimitedContinuation) -> R): A = + if (stack.size > depth) stack[depth++] as A + else super.shift(func).also { depth++ } +} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/SingleShotDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/SingleShotDelimCont.kt new file mode 100644 index 000000000..5003dabe0 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/SingleShotDelimCont.kt @@ -0,0 +1,67 @@ +package arrow.continuations.generic + +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +/** + * Implements delimited continuations with with single shot mode. + * + * This is basically only there to show how the run loop works + */ +class SingleShotDelimContScope(val f: suspend DelimitedScope.() -> R): RunnableDelimitedScope { + + private val resultVar = atomic(null) + private val nextShift = atomic<(suspend () -> R)?>(null) + // TODO This can be append only, but needs fast reversed access + private val shiftFnContinuations = mutableListOf>() + + data class SingleShotCont( + private val continuation: Continuation, + private val shiftFnContinuations: MutableList> + ) : DelimitedContinuation { + override suspend fun invoke(a: A): R = suspendCoroutine { resumeShift -> + shiftFnContinuations.add(resumeShift) + continuation.resume(a) + } + } + + override suspend fun shift(func: suspend (DelimitedContinuation) -> R): A = + suspendCoroutine { continueMain -> + val delCont = SingleShotCont(continueMain, shiftFnContinuations) + assert(nextShift.compareAndSet(null, suspend { func(delCont) })) + } + + override fun invoke(): R { + f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result -> + resultVar.value = result.getOrThrow() + }).let { + if (it == COROUTINE_SUSPENDED) { + resultVar.loop { mRes -> + if (mRes == null) { + val nextShiftFn = nextShift.getAndSet(null) + ?: throw IllegalStateException("No further work to do but also no result!") + nextShiftFn.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { result -> + resultVar.value = result.getOrThrow() + }).let { + if (it != COROUTINE_SUSPENDED) resultVar.value = it as R + } + } else return@let + } + } + else return@invoke it as R + } + assert(resultVar.value != null) + for (c in shiftFnContinuations.asReversed()) c.resume(resultVar.value!!) + return resultVar.value!! + } + + companion object { + fun reset(f: suspend DelimitedScope.() -> R): R = SingleShotDelimContScope(f).invoke() + } +} diff --git a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt new file mode 100644 index 000000000..1dfac01a9 --- /dev/null +++ b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt @@ -0,0 +1,100 @@ +package generic + +import arrow.continuations.generic.DelimitedScope +import arrow.continuations.generic.MultiShotDelimContScope +import arrow.continuations.generic.SingleShotDelimContScope +import arrow.core.Either +import arrow.core.Left +import arrow.core.Tuple2 +import arrow.core.Tuple3 +import arrow.core.test.UnitSpec +import arrow.core.toT +import io.kotlintest.shouldBe + +abstract class ContTestSuite: UnitSpec() { + abstract fun reset(func: (suspend DelimitedScope.() -> A)): A + + abstract fun capabilities(): Set + + init { + "yield a list (also verifies stacksafety)" { + reset> { + suspend fun DelimitedScope>.yield(a: A): Unit = shift { k -> listOf(a) + k(Unit) } + for (i in 0..10_000) yield(i) + emptyList() + } shouldBe (0..10_000).toList() + } + "short circuit" { + reset> { + val no: Int = shift { Left("No thank you") } + throw IllegalStateException("This should not be executed") + } shouldBe Left("No thank you") + } + if (capabilities().contains(ScopeCapabilities.MultiShot)) { + "multshot nondet" { + reset>> { + val i: Int = shift { k -> k(10) + k(20) } + val j: Int = shift { k -> k(15) + k(25) } + listOf(i toT j) + } shouldBe listOf(10 toT 15, 10 toT 25, 20 toT 15, 20 toT 25) + } + "multishot more than twice" { + reset>> { + val i: Int = shift { k -> k(10) + k(20) } + val j: Int = shift { k -> k(15) + k(25) } + val k: Int = shift { k -> k(17) + k(27) } + listOf(Tuple3(i, j, k)) + } shouldBe listOf(10, 20).flatMap { i -> listOf(15, 25).flatMap { j -> listOf(17, 27).map { k -> Tuple3(i, j, k) } } } + } + "multishot more than twice and with more multishot invocations" { + reset>> { + val i: Int = shift { k -> k(10) + k(20) + k(30) + k(40) + k(50) } + val j: Int = shift { k -> k(15) + k(25) + k(35) + k(45) + k(55) } + val k: Int = shift { k -> k(17) + k(27) + k(37) + k(47) + k(57) } + listOf(Tuple3(i, j, k)) + } shouldBe + listOf(10, 20, 30, 40, 50) + .flatMap { i -> listOf(15, 25, 35, 45, 55) + .flatMap { j -> listOf(17, 27, 37, 47, 57) + .map { k -> Tuple3(i, j, k) } + } + } + } + "multishot is stacksafe regardless of stack size" { + reset { + // bring 10k elements on the stack + var sum = 0 + for (i0 in 1..10_000) sum += shift { it(i0) } + + // run the continuation from here 10k times and sum the results + // This is about as bad as a scenario as it gets :) + val j: Int = shift { + var sum2 = 0 + for (i0 in 1..10_000) sum2 += it(i0) + sum2 + } + + sum + j + } + } + } + } +} + +sealed class ScopeCapabilities { + object MultiShot : ScopeCapabilities() +} + +class SingleShotContTestSuite : ContTestSuite() { + override fun reset(func: (suspend DelimitedScope.() -> A)): A = + SingleShotDelimContScope.reset(func) + + override fun capabilities(): Set = emptySet() +} + +class MultiShotContTestSuite : ContTestSuite() { + override fun reset(func: (suspend DelimitedScope.() -> A)): A = + MultiShotDelimContScope.reset(func) + + override fun capabilities(): Set = setOf(ScopeCapabilities.MultiShot) +} From 8f3e66c2c4346c0cc44d294992fc7f35de1dd740 Mon Sep 17 00:00:00 2001 From: Jannis Date: Thu, 30 Jul 2020 16:46:38 +0200 Subject: [PATCH 36/49] Add shiftCPS and reset. Also removed broken nested reset implementation --- .../effectStackUnpackedResets/program.kt | 288 ------------------ .../{SingleShotDelimCont.kt => DelimCont.kt} | 22 +- .../continuations/generic/DelimitedCont.kt | 6 + .../generic/MultiShotDelimCont.kt | 15 + .../kotlin/effectStackUnpackedResets/Run2.kt | 43 --- .../src/test/kotlin/generic/TestSuite.kt | 35 ++- 6 files changed, 62 insertions(+), 347 deletions(-) delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt rename arrow-continuations/src/main/kotlin/arrow/continuations/generic/{SingleShotDelimCont.kt => DelimCont.kt} (72%) delete mode 100644 arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt deleted file mode 100644 index 7d3bf9c06..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStackUnpackedResets/program.kt +++ /dev/null @@ -1,288 +0,0 @@ -package arrow.continuations.effectStackUnpackedResets - -import kotlinx.atomicfu.atomic -import kotlinx.atomicfu.loop -import kotlin.coroutines.Continuation -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine -import kotlin.reflect.KProperty - -interface DelimitedCont { - suspend operator fun invoke(a: A): B -} - -interface Delimited { - suspend fun shift(func: suspend (DelimitedCont) -> A): B - suspend fun reset(f: suspend Delimited.() -> B): B -} - -suspend fun reset(f: suspend Delimited.() -> A): A = - DelimitedScope("Prompt", f).run {} - -/** - * Idea we have two paths: - * One path is the normal coroutine. It fills an effect stack everytime its continuation is resumed with a value. - * Then if a continuation is run more than once we restart the entire computation [f] and use the effect stack for as long as possible - * When the effect stack runs out of values we resume normal coroutine behaviour. - * - * This can be used to implement nondeterminism together with any other effect and so long as the "pure" code in a function - * is fast this won't be a problem, but if it isn't this will result in terrible performance (but only if multishot is actually used) - */ -open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.() -> A) : Delimited { - - open val parentScope: DelimitedScope<*>? = null - internal val childScope = atomic?>(null) - internal fun setChildScope(s: DelimitedScope<*>?): Unit { - childScope.value = s - } - - internal fun getChildScope(): DelimitedScope<*>? = childScope.value - - internal val ret = atomic(null) - - // TODO More descriptive name - private val currShiftFn = atomic<(suspend () -> A)?>(null) - - // TODO more efficient data structures. O(1) append + O(1) pop would be best - internal open val stack: MutableList = mutableListOf() - - // TODO for this we could use a datastructure that can O(1) append and has O(1) popLast() - private val cbs = mutableListOf>() - - internal val childCb = atomic?>(null) - internal val multiChildCb = atomic?>(null) - - internal fun trace(msg: String): Unit = println("$dbgLabel: $msg") - - internal fun setMultiChildCb(c: Continuation): Unit { - trace("Set multi child cb") - multiChildCb.value = c - } - - internal fun setChildCB(c: Continuation): Unit { - trace("Set child cb") - childCb.value = c - } - - override suspend fun shift(func: suspend (DelimitedCont) -> A): B { - when (val cs = childScope.value) { - is MultiShotDelimScope -> if (cs.done.not() && cs.depth < cs.stack.size) { - trace("Early return from stack ${cs.stack[cs.depth]}") - return cs.stack[cs.depth++] as B - } else Unit - else -> Unit - } - // suspend f since we first need a result from DelimitedCont.invoke - return suspendCoroutine { k -> - // println("Suspending for shift: $label") - // println("Stack: $stack") - // create a continuation which supports invoking either the suspended f or restarting it with a sliced stack - val o = object : DelimitedCont { - // The "live" continuation for f which is currently suspended. Can only be called once - val liveContinuation = atomic?>(k) - - // TODO better datastructure - // A snapshot of f's effect-stack up to this shift's function invocation - val offset: Int = stack.size - override suspend fun invoke(a: B): A { - // println("Invoke cont with state is null: ${state.value == null} && arg $a") - val cont = liveContinuation.getAndSet(null) - // Re-execute f, but in a new scope which contains the stack slice + a and will use that to fill in the first - // calls to shift - return if (cont == null) { - trace("Starting multishot with $a and current total stack $stack") - startMultiShot(offset, a) - } - // we have a "live" continuation to resume to so we suspend the shift block and do exactly that - else suspendCoroutineUninterceptedOrReturn { - trace("Invoke $a") - // a is the result of an effect, push it onto the stack. Note this refers to the outer stack, not - // the slice captured here, which is now immutable - stack.add(a) - // invoke needs to return A at some point so we need to append the Continuation so that it will be called when this - // scope's run method is done - cbs.add(it) - // resume f with value a - cont.resume(a) - COROUTINE_SUSPENDED - } - } - } - // the shift function is the next fn to execute - currShiftFn.value = { func(o) } - } - } - - open suspend fun startMultiShot(end: Int, b: Any?): A = startMultiShot(0, end, b) - - suspend fun startMultiShot(start: Int, end: Int, b: Any?): A = - MultiShotDelimScope(this@DelimitedScope, stack.subList(start, end).toList() + b, f).let { scope -> - // Tell the parent we are running multishot - parentScope?.setChildScope(scope) - scope.run { - trace("Multi cb") - parentScope?.setMultiChildCb(it) - }.also { parentScope?.setChildScope(scope.parentScope) } - } - - override suspend fun reset(f: suspend Delimited.() -> B): B = - ChildScope(this@DelimitedScope, f).run { - trace("Child cb") - setChildCB(it) - } - - internal fun getValue(): A? = - // println("Running suspended $label") - ret.loop { - trace("Get loop start") - // shift function called f's continuation which now finished - if (it != null) return@getValue it - // we are not done yet - else { - trace("Get loop: no result") - val res = currShiftFn.getAndSet(null) - if (res != null) - res.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { res -> - // println("Resumption with ${(res.getOrThrow() as Sequence).toList()}") - // a shift block finished processing. This is now our intermediate return value - ret.value = res.getOrThrow() - }).let { - // the shift function did not call its continuation which means we short-circuit - if (it != COROUTINE_SUSPENDED) ret.value = it as A - // if we did suspend we have either hit a shift function from the parent scope or another shift function - // in both cases we just loop - } - // short since we run out of shift functions to call - else { - trace("Get loop: no work") - multiChildCb.getAndSet(null)?.also { trace("Resuming multishot child") }?.resume(Unit) - childCb.getAndSet(null)?.also { trace("Resuming child") }?.resume(Unit) ?: return@getValue null - } - } - } - - fun hasWork(): Boolean = (currShiftFn.value != null || multiChildCb.value != null) - || (parentScope != null && parentScope!!.hasWork()) - - open suspend fun run(handleSuspend: (Continuation) -> Unit): A { - trace("Started runloop") - f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { - // println("Put value ${(it.getOrThrow() as Sequence).toList()}") - // f finished after being resumed. Save the value to resume the shift blocks later - trace("Resumed with $it") - ret.value = it.getOrThrow() - }).let { res -> - if (res == COROUTINE_SUSPENDED) { - trace("Starting suspend loop") - while (true) { - val a = getValue() - if (a == null) { - trace("Yielding to parent"); suspendCoroutine(handleSuspend) - } else break - } - } // we finished without ever suspending. This means there is no shift block and we can short circuit run - else { - trace("no suspension. Returning $res"); return@run res as A - } - } - - trace("Done. Number of cbs: ${cbs.size}") - cbs.asReversed().forEach { - it.resume(ret.value!!) - // This may have spawned additional work for a parent - trace("Parent has work: ${parentScope != null && parentScope!!.hasWork()}") - if (parentScope != null && parentScope!!.hasWork()) { - trace("Yielding to parent"); suspendCoroutine(handleSuspend) - } - - trace("Value is ${ret.value}") - } - trace("Returned with value ${ret.value}") - // return the final value after all shift blocks finished processing the result - return ret.value!! - } -} - -open class ChildScope( - final override val parentScope: DelimitedScope<*>, - f: suspend Delimited.() -> A -) : DelimitedScope("Child-" + unique++, f) { - override val stack: MutableList = parentScope.stack - private val offset: Int = parentScope.stack.size - override suspend fun startMultiShot(end: Int, b: Any?): A = - startMultiShot(offset, end, b) -} - -// TODO I can avoid list copying if I also work with indexes better here! -open class MultiShotDelimScope( - final override val parentScope: DelimitedScope<*>, - localStack: List, - func: suspend Delimited.() -> A, - offsetFromStack: Int = 0, - private val startingOffset: Int = 0 -) : DelimitedScope("Multishot-" + unique++, func) { - internal var depth by ParentDepth(0, parentScope, offsetFromStack) - internal var done = false - override val stack: MutableList = localStack.toMutableList().also { trace("Running multishot with $localStack") } - - override suspend fun shift(func: suspend (DelimitedCont) -> A): B = - if (stack.size > depth && done.not()) { - trace("Using the stack for shift: ${stack[depth]}") - trace("Stack: $stack") - stack[depth++] as B - } else { - done = true - trace("Done by shift") - super.shift(func) - } - - override suspend fun reset(p: suspend Delimited.() -> B): B = - if (stack.size > depth && done.not()) { - trace("Reset from multishot with stack left. Depth $depth") - // there are still elements on the stack, so run f as Multishot in child mode - MultiShotDelimScope(this@MultiShotDelimScope, stack.subList(depth, stack.size), p, -depth, depth) - .let { - setChildScope(it) - it.run { - trace("Suspended multi shot in multi shot") - setChildCB(it) - } - } - .also { done = true }.also { trace("Done by reset1: $it. Stack $stack") } - .also { setChildScope(null) } - } else { - done = true - trace("Done by reset2") - super.reset(p) - } - - override suspend fun startMultiShot(end: Int, b: Any?): A { - trace("Multishot from multishot $startingOffset $end") - return MultiShotDelimScope( - this@MultiShotDelimScope, - stack.subList(0, end).toList() + b, - f, - -depth - ).let { scope -> - // Tell the parent we are running multishot - parentScope.setChildScope(scope) - scope.run { - trace("Multi cb") - parentScope.setMultiChildCb(it) - }.also { parentScope.setChildScope(scope.parentScope) } - } - } -} - -class ParentDepth(var def: Int, val parentScope: DelimitedScope<*>?, val offset: Int) { - operator fun getValue(thisRef: Any?, property: KProperty<*>): Int = - (if (parentScope is MultiShotDelimScope) parentScope.depth else def) + offset - operator fun setValue(thisRef: Any?, property: KProperty<*>, i: Int): Unit = - if (parentScope is MultiShotDelimScope) parentScope.depth = i - offset else def = i - offset -} - -var unique = 0 diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/SingleShotDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt similarity index 72% rename from arrow-continuations/src/main/kotlin/arrow/continuations/generic/SingleShotDelimCont.kt rename to arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt index 5003dabe0..9f448473c 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/SingleShotDelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt @@ -10,11 +10,9 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine /** - * Implements delimited continuations with with single shot mode. - * - * This is basically only there to show how the run loop works + * Implements delimited continuations with with no multi shot support (apart from shiftCPS which trivially supports it). */ -class SingleShotDelimContScope(val f: suspend DelimitedScope.() -> R): RunnableDelimitedScope { +class DelimContScope(val f: suspend DelimitedScope.() -> R): RunnableDelimitedScope { private val resultVar = atomic(null) private val nextShift = atomic<(suspend () -> R)?>(null) @@ -31,12 +29,26 @@ class SingleShotDelimContScope(val f: suspend DelimitedScope.() -> R): Run } } + data class CPSCont( + private val runFunc: suspend DelimitedScope.(A) -> R + ): DelimitedContinuation { + override suspend fun invoke(a: A): R = DelimContScope { runFunc(a) }.invoke() + } + override suspend fun shift(func: suspend (DelimitedContinuation) -> R): A = suspendCoroutine { continueMain -> val delCont = SingleShotCont(continueMain, shiftFnContinuations) assert(nextShift.compareAndSet(null, suspend { func(delCont) })) } + override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> R): Nothing = + suspendCoroutine { + assert(nextShift.compareAndSet(null, suspend { func(CPSCont(c)) })) + } + + override fun reset(f: suspend DelimitedScope.() -> A): A = + DelimContScope(f).invoke() + override fun invoke(): R { f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result -> resultVar.value = result.getOrThrow() @@ -62,6 +74,6 @@ class SingleShotDelimContScope(val f: suspend DelimitedScope.() -> R): Run } companion object { - fun reset(f: suspend DelimitedScope.() -> R): R = SingleShotDelimContScope(f).invoke() + fun reset(f: suspend DelimitedScope.() -> R): R = DelimContScope(f).invoke() } } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt index ed06e61e0..9c1908453 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt @@ -13,7 +13,13 @@ interface DelimitedContinuation { // TODO This should be @RestrictSuspension but that breaks because a superclass is not considered to be correct scope // @RestrictsSuspension interface DelimitedScope { + // shift and place an implicit boundary. See shiftCPS for a more accurate definition of what this means suspend fun shift(f: suspend (DelimitedContinuation) -> R): A + // shiftCPS passes the arguments with which the continuation is invoked to the supplied continuation/function c. + // This means it is trivially multishot because c has the stack in its closure. To enforce that this is the last + // statement of a reset block we return Nothing here. + suspend fun shiftCPS(f: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> R): Nothing + fun reset(f: suspend DelimitedScope.() -> A): A } interface RunnableDelimitedScope : DelimitedScope { diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt index 3a03829a6..c78152a94 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt @@ -38,11 +38,26 @@ open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) } } + data class CPSCont( + private val runFunc: suspend DelimitedScope.(A) -> R + ): DelimitedContinuation { + override suspend fun invoke(a: A): R = DelimContScope { runFunc(a) }.invoke() + } + override suspend fun shift(func: suspend (DelimitedContinuation) -> R): A = suspendCoroutine { continueMain -> val c = MultiShotCont(continueMain, f, stack, shiftFnContinuations) assert(nextShift.compareAndSet(null, suspend { func(c) })) } + override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> R): Nothing = + suspendCoroutine { + assert(nextShift.compareAndSet(null, suspend { func(CPSCont(c)) })) + } + + // This assumes RestrictSuspension or at least assumes the user to never reference the parent scope in f. + override fun reset(f: suspend DelimitedScope.() -> A): A = + MultiShotDelimContScope(f).invoke() + override fun invoke(): R { f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result -> resultVar.value = result.getOrThrow() diff --git a/arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt b/arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt deleted file mode 100644 index 405085484..000000000 --- a/arrow-continuations/src/test/kotlin/effectStackUnpackedResets/Run2.kt +++ /dev/null @@ -1,43 +0,0 @@ -package effectStackUnpackedResets - -import arrow.continuations.effectStackUnpackedResets.Delimited -import arrow.continuations.effectStackUnpackedResets.reset -import arrow.core.Either -import arrow.core.Left -import arrow.core.Right -import arrow.core.flatMap -import arrow.core.test.UnitSpec - -class Run2 : UnitSpec() { - init { - suspend fun Delimited>.yield(a: A): Unit = shift { k -> listOf(a) + k(Unit) + k(Unit) } - "yield" { - println("PROGRAM: yield") - reset> { - yield(1) - yield(2) - yield(5) - emptyList() - }.also { println("Result: $it") } // should be 1,2,5, 2,5, 5 - } - "multi" { - println("PROGRAM: multi") - reset> fst@{ - val ctx = this - val i: Int = shift { it(5) } - // println("HERE") - val r = i * 2 + reset snd@{ - val k: Int = shift { it(3) + it(2) } - // println("There") - val j: Int = if (i == 5) ctx.shift { it(10).flatMap { i -> it(5).map { i + it } } } - else shift { it(4) } - val o: Int = shift { it(1) } - // println("Those") - j + k + o - } - val l: Int = shift { it(8) } - Right(r + l) - }.also { println("PROGRAM: Result $it") } - } - } -} diff --git a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt index 1dfac01a9..e7030d741 100644 --- a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt +++ b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt @@ -2,7 +2,7 @@ package generic import arrow.continuations.generic.DelimitedScope import arrow.continuations.generic.MultiShotDelimContScope -import arrow.continuations.generic.SingleShotDelimContScope +import arrow.continuations.generic.DelimContScope import arrow.core.Either import arrow.core.Left import arrow.core.Tuple2 @@ -12,34 +12,47 @@ import arrow.core.toT import io.kotlintest.shouldBe abstract class ContTestSuite: UnitSpec() { - abstract fun reset(func: (suspend DelimitedScope.() -> A)): A + abstract fun runScope(func: (suspend DelimitedScope.() -> A)): A abstract fun capabilities(): Set init { "yield a list (also verifies stacksafety)" { - reset> { + runScope> { suspend fun DelimitedScope>.yield(a: A): Unit = shift { k -> listOf(a) + k(Unit) } for (i in 0..10_000) yield(i) emptyList() } shouldBe (0..10_000).toList() } "short circuit" { - reset> { + runScope> { val no: Int = shift { Left("No thank you") } throw IllegalStateException("This should not be executed") } shouldBe Left("No thank you") } + "shiftCPS supports multishot regardless of scope" { + runScope { + shiftCPS({ it(1) + it(2) }) { i -> i } + throw IllegalStateException("This is unreachable") + } shouldBe 3 + } + "reset" { + runScope { + reset { + shift { it(1) } + } + } shouldBe 1 + } if (capabilities().contains(ScopeCapabilities.MultiShot)) { "multshot nondet" { - reset>> { + runScope>> { val i: Int = shift { k -> k(10) + k(20) } val j: Int = shift { k -> k(15) + k(25) } listOf(i toT j) } shouldBe listOf(10 toT 15, 10 toT 25, 20 toT 15, 20 toT 25) } "multishot more than twice" { - reset>> { + runScope>> { val i: Int = shift { k -> k(10) + k(20) } val j: Int = shift { k -> k(15) + k(25) } val k: Int = shift { k -> k(17) + k(27) } @@ -47,7 +60,7 @@ abstract class ContTestSuite: UnitSpec() { } shouldBe listOf(10, 20).flatMap { i -> listOf(15, 25).flatMap { j -> listOf(17, 27).map { k -> Tuple3(i, j, k) } } } } "multishot more than twice and with more multishot invocations" { - reset>> { + runScope>> { val i: Int = shift { k -> k(10) + k(20) + k(30) + k(40) + k(50) } val j: Int = shift { k -> k(15) + k(25) + k(35) + k(45) + k(55) } val k: Int = shift { k -> k(17) + k(27) + k(37) + k(47) + k(57) } @@ -61,7 +74,7 @@ abstract class ContTestSuite: UnitSpec() { } } "multishot is stacksafe regardless of stack size" { - reset { + runScope { // bring 10k elements on the stack var sum = 0 for (i0 in 1..10_000) sum += shift { it(i0) } @@ -86,14 +99,14 @@ sealed class ScopeCapabilities { } class SingleShotContTestSuite : ContTestSuite() { - override fun reset(func: (suspend DelimitedScope.() -> A)): A = - SingleShotDelimContScope.reset(func) + override fun runScope(func: (suspend DelimitedScope.() -> A)): A = + DelimContScope.reset(func) override fun capabilities(): Set = emptySet() } class MultiShotContTestSuite : ContTestSuite() { - override fun reset(func: (suspend DelimitedScope.() -> A)): A = + override fun runScope(func: (suspend DelimitedScope.() -> A)): A = MultiShotDelimContScope.reset(func) override fun capabilities(): Set = setOf(ScopeCapabilities.MultiShot) From 9208e4e031f7ff81f57e35a2a810e49ad8d0882c Mon Sep 17 00:00:00 2001 From: Jannis Date: Sat, 1 Aug 2020 17:42:26 +0200 Subject: [PATCH 37/49] Push effect interface and a sub-optimal handler implementation --- .../arrow/continuations/generic/DelimCont.kt | 6 +- .../continuations/generic/DelimitedCont.kt | 2 +- .../generic/MultiShotDelimCont.kt | 22 +++--- .../generic/effect/EffectCombinedHandlers.kt | 78 +++++++++++++++++++ .../continuations/generic/effect/Effects.kt | 16 ++++ .../generic/EffectCombinedHandlersTest.kt | 34 ++++++++ .../src/test/kotlin/generic/TestSuite.kt | 29 +++++++ 7 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt create mode 100644 arrow-continuations/src/test/kotlin/generic/EffectCombinedHandlersTest.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt index 9f448473c..66f905190 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt @@ -35,10 +35,12 @@ class DelimContScope(val f: suspend DelimitedScope.() -> R): RunnableDelim override suspend fun invoke(a: A): R = DelimContScope { runFunc(a) }.invoke() } - override suspend fun shift(func: suspend (DelimitedContinuation) -> R): A = + // TODO I wrote this in the middle of the night, double check + // Note we don't wrap the function [func] in an explicit reset because that is already implicit in our scope + override suspend fun shift(func: suspend DelimitedScope.(DelimitedContinuation) -> R): A = suspendCoroutine { continueMain -> val delCont = SingleShotCont(continueMain, shiftFnContinuations) - assert(nextShift.compareAndSet(null, suspend { func(delCont) })) + assert(nextShift.compareAndSet(null, suspend { this.func(delCont) })) } override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> R): Nothing = diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt index 9c1908453..7c66d3efe 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt @@ -14,7 +14,7 @@ interface DelimitedContinuation { // @RestrictsSuspension interface DelimitedScope { // shift and place an implicit boundary. See shiftCPS for a more accurate definition of what this means - suspend fun shift(f: suspend (DelimitedContinuation) -> R): A + suspend fun shift(f: suspend DelimitedScope.(DelimitedContinuation) -> R): A // shiftCPS passes the arguments with which the continuation is invoked to the supplied continuation/function c. // This means it is trivially multishot because c has the stack in its closure. To enforce that this is the last // statement of a reset block we return Nothing here. diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt index c78152a94..571b82e22 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt @@ -13,8 +13,10 @@ open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) private val resultVar = atomic(null) private val nextShift = atomic<(suspend () -> R)?>(null) + // TODO This can be append only and needs fast reversed access private val shiftFnContinuations = mutableListOf>() + // TODO This can be append only and needs fast random access and slicing internal open val stack = mutableListOf() @@ -40,14 +42,17 @@ open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) data class CPSCont( private val runFunc: suspend DelimitedScope.(A) -> R - ): DelimitedContinuation { + ) : DelimitedContinuation { override suspend fun invoke(a: A): R = DelimContScope { runFunc(a) }.invoke() } - override suspend fun shift(func: suspend (DelimitedContinuation) -> R): A = suspendCoroutine { continueMain -> - val c = MultiShotCont(continueMain, f, stack, shiftFnContinuations) - assert(nextShift.compareAndSet(null, suspend { func(c) })) - } + // TODO I wrote this in the middle of the night, double check + // Note we don't wrap the function [func] in an explicit reset because that is already implicit in our scope + override suspend fun shift(func: suspend DelimitedScope.(DelimitedContinuation) -> R): A = + suspendCoroutine { continueMain -> + val c = MultiShotCont(continueMain, f, stack, shiftFnContinuations) + assert(nextShift.compareAndSet(null, suspend { this.func(c) })) + } override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> R): Nothing = suspendCoroutine { @@ -74,8 +79,7 @@ open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) } } else return@let } - } - else return@invoke it as R + } else return@invoke it as R } assert(resultVar.value != null) for (c in shiftFnContinuations.asReversed()) c.resume(resultVar.value!!) @@ -90,10 +94,10 @@ open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) class PrefilledDelimContScope( override val stack: MutableList, f: suspend DelimitedScope.() -> R -): MultiShotDelimContScope(f) { +) : MultiShotDelimContScope(f) { var depth = 0 - override suspend fun shift(func: suspend (DelimitedContinuation) -> R): A = + override suspend fun shift(func: suspend DelimitedScope.(DelimitedContinuation) -> R): A = if (stack.size > depth) stack[depth++] as A else super.shift(func).also { depth++ } } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt new file mode 100644 index 000000000..543be539e --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt @@ -0,0 +1,78 @@ +package arrow.continuations.generic.effect + +import arrow.Kind +import arrow.continuations.generic.DelimitedScope +import arrow.core.Either +import arrow.core.EitherPartialOf +import arrow.core.ForListK +import arrow.core.ListK +import arrow.core.extensions.either.applicative.applicative +import arrow.core.extensions.listk.applicative.applicative +import arrow.core.fix +import arrow.core.k +import arrow.typeclasses.Applicative + +fun Either.Companion.errorHandler( + ap: Applicative, + delimitedScope: DelimitedScope, A>>> +): Error = + object : Error { + override suspend fun , A> Eff.catch(f: suspend Eff.() -> A, hdl: suspend Eff.(E) -> A): A = + TODO("I need more powers over F to do this") + + override suspend fun raise(e: E): Nothing = + delimitedScope.shift { ap.just(left(e)) } + } + +fun nullableHandler(delimitedScope: DelimitedScope): NonDet = + object : NonDet { + override suspend fun choose(): Boolean = + delimitedScope.shift { it(true) ?: it(false) } + + override suspend fun empty(): Nothing = delimitedScope.shift { null } + } + +fun listHandler( + ap: Applicative, + delimitedScope: DelimitedScope>> +): NonDet = + object : NonDet { + override suspend fun choose(): Boolean = + delimitedScope.shift { ap.run { (it(true).map2(it(false)) { (a, b) -> (a.fix() + b.fix()).k() }) } } + + override suspend fun empty(): Nothing = delimitedScope.shift { ap.just(emptyList().k()) } + } + +interface NonDetAndError : NonDet, Error +fun listEitherHandler( + ap: Applicative, + delimitedScope: DelimitedScope, A>>>> +): NonDetAndError = + object : NonDetAndError, + Error by Either.errorHandler(ComposedApplicative(ap, ListK.applicative()), delimitedScope as DelimitedScope, Kind, A>>>), + NonDet by listHandler(ap, delimitedScope) + {} + +fun eitherListHandler( + ap: Applicative, + delimitedScope: DelimitedScope, Kind>>> +): NonDetAndError = + object : NonDetAndError, + Error by Either.errorHandler(ap, delimitedScope), + NonDet by listHandler(ComposedApplicative(ap, Either.applicative()), delimitedScope as DelimitedScope>, Kind>>) + {} + +interface Nested + +fun Kind>.nest(): Kind, A> = this as Kind, A> +fun Kind, A>.unnest(): Kind> = this as Kind> + +fun ComposedApplicative(apF: Applicative, apG: Applicative): Applicative> = object : Applicative> { + override fun Kind, A>.ap(ff: Kind, (A) -> B>): Kind, B> = + apF.run { unnest().ap(ff.unnest().map { gf -> { ga: Kind -> apG.run { ga.ap(gf) } } }).nest() } + + override fun just(a: A): Kind, A> = apF.just(apG.just(a)).nest() +} + +// ######################### +suspend fun E.myFun(): Int where E : NonDet, E : Error = if (choose()) raise("Better luck next time") else 42 diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt new file mode 100644 index 000000000..868dbb7ba --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt @@ -0,0 +1,16 @@ +package arrow.continuations.generic.effect + +interface Error { + suspend fun raise(e: E): Nothing + suspend fun , A> Eff.catch(f: suspend Eff.() -> A, hdl: suspend Eff.(E) -> A): A +} + +interface Empty { + suspend fun empty(): Nothing +} + +interface Choose { + suspend fun choose(): Boolean +} + +interface NonDet : Choose, Empty diff --git a/arrow-continuations/src/test/kotlin/generic/EffectCombinedHandlersTest.kt b/arrow-continuations/src/test/kotlin/generic/EffectCombinedHandlersTest.kt new file mode 100644 index 000000000..447ee4076 --- /dev/null +++ b/arrow-continuations/src/test/kotlin/generic/EffectCombinedHandlersTest.kt @@ -0,0 +1,34 @@ +package generic + +import arrow.Kind +import arrow.continuations.generic.DelimitedScope +import arrow.continuations.generic.MultiShotDelimContScope +import arrow.continuations.generic.effect.eitherListHandler +import arrow.continuations.generic.effect.listEitherHandler +import arrow.continuations.generic.effect.myFun +import arrow.core.Either +import arrow.core.EitherPartialOf +import arrow.core.ForId +import arrow.core.ForListK +import arrow.core.Id +import arrow.core.ListK +import arrow.core.extensions.id.applicative.applicative +import arrow.core.k +import arrow.core.value +import io.kotlintest.shouldBe +import io.kotlintest.specs.StringSpec + +class T : StringSpec({ + "run" { + MultiShotDelimContScope.reset>>> { + val hdl = listEitherHandler(Id.applicative(), this as DelimitedScope, Int>>>>) + Id(listOf(Either.Right(hdl.myFun())).k()) + }.value() shouldBe listOf(Either.Left("Better luck next time"), Either.Right(42)) + } + "run2" { + MultiShotDelimContScope.reset>>> { + val hdl = eitherListHandler(Id.applicative(), this as DelimitedScope, Kind>>>) + Id(Either.Right(listOf(hdl.myFun()).k())) + }.value() shouldBe Either.Left("Better luck next time") + } +}) diff --git a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt index e7030d741..47654c22e 100644 --- a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt +++ b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt @@ -44,6 +44,35 @@ abstract class ContTestSuite: UnitSpec() { } shouldBe 1 } if (capabilities().contains(ScopeCapabilities.MultiShot)) { + // This comes from http://homes.sice.indiana.edu/ccshan/recur/recur.pdf and shows how reset/shift should behave + "multishot reset/shift" { + runScope> { + listOf('a') + reset> { + listOf('b') + shift> { f -> listOf('1') + f(f(listOf('c'))) } + } + } shouldBe listOf('a', '1', 'b', 'b', 'c') + runScope> { + listOf('a') + reset> { + shiftCPS({ f -> listOf('1') + f(f(listOf('c'))) }) { xs: List -> + listOf('b') + xs + } + } + } shouldBe listOf('a', '1', 'b', 'b', 'c') + } + // This also comes from http://homes.sice.indiana.edu/ccshan/recur/recur.pdf and shows that shift surrounds the + // captured continuation and the function receiving it with reset. This is done implicitly in our implementation + "shift and control distinction" { + runScope { + reset { + suspend fun y() = shift { f -> "a" + f("") } + shift { y() } + } + } shouldBe "a" + // TODO this is not very accurate, probably not correct either + runScope { + shiftCPS({ it("") }, { s: String -> shift { f -> "a" + f("") } }) + } shouldBe "a" + } "multshot nondet" { runScope>> { val i: Int = shift { k -> k(10) + k(20) } From 53efcf748ff2e87fd44a8c4d745f4169212df751 Mon Sep 17 00:00:00 2001 From: Jannis Date: Sat, 1 Aug 2020 23:27:36 +0200 Subject: [PATCH 38/49] Add parser implementation based on effects --- .../generic/effect/EffectCombinedHandlers.kt | 4 +- .../continuations/generic/effect/Effects.kt | 4 +- .../continuations/generic/effect/Parser.kt | 190 ++++++++++++++++++ .../src/test/kotlin/generic/ParserTest.kt | 29 +++ 4 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt create mode 100644 arrow-continuations/src/test/kotlin/generic/ParserTest.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt index 543be539e..5e957b0be 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt @@ -17,8 +17,8 @@ fun Either.Companion.errorHandler( delimitedScope: DelimitedScope, A>>> ): Error = object : Error { - override suspend fun , A> Eff.catch(f: suspend Eff.() -> A, hdl: suspend Eff.(E) -> A): A = - TODO("I need more powers over F to do this") + override suspend fun catch(f: suspend () -> B, hdl: suspend (E) -> B): B = + TODO("This requires proper nested scope support to be sensible") override suspend fun raise(e: E): Nothing = delimitedScope.shift { ap.just(left(e)) } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt index 868dbb7ba..43ed56a57 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt @@ -2,7 +2,7 @@ package arrow.continuations.generic.effect interface Error { suspend fun raise(e: E): Nothing - suspend fun , A> Eff.catch(f: suspend Eff.() -> A, hdl: suspend Eff.(E) -> A): A + suspend fun catch(f: suspend () -> A, hdl: suspend (E) -> A): A } interface Empty { @@ -13,4 +13,6 @@ interface Choose { suspend fun choose(): Boolean } +suspend inline fun Choose.choice(f: () -> A, g: () -> A): A = if (choose()) f() else g() + interface NonDet : Choose, Empty diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt new file mode 100644 index 000000000..dc57cb478 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt @@ -0,0 +1,190 @@ +package arrow.continuations.generic.effect + +import arrow.continuations.generic.DelimitedScope +import arrow.continuations.generic.MultiShotDelimContScope +import arrow.core.Either +import arrow.core.Nel +import arrow.core.NonEmptyList +import arrow.core.Tuple2 +import arrow.core.identity +import arrow.core.left +import arrow.core.toT + +typealias Parser = suspend ParserCtx.() -> A + +interface ParserCtx : NonDet, Error { + suspend fun take1(): Char + suspend fun take(n: Int): String + suspend fun takeWhile(matcher: (Char) -> Boolean): String + suspend fun takeAtLeastOneWhile(matcher: (Char) -> Boolean): String + + fun getOffset(): Int + + suspend fun token(expected: Set = emptySet(), matcher: (Char) -> B?): B + suspend fun string(str: String): String + suspend fun eof(): Unit + suspend fun lookAhead(p: Parser): A + suspend fun notFollowedBy(p: Parser): Unit + suspend fun optional(p: Parser): A? + + suspend fun satisfy(expected: Set = emptySet(), f: (Char) -> Boolean): Char = token(expected) { it.takeIf(f) } + suspend fun char(c: Char): Char = satisfy(setOf(ErrorItem.fromChar(c))) { it == c } + + fun attempt(p: Parser): Either +} + +data class ParseError( + val offset: Int, + val unexpectedToken: ErrorItem? = null, + val expectedTokens: Set = emptySet() +) { + operator fun plus(other: ParseError): ParseError = + when { + offset < other.offset -> other + offset > other.offset -> this + else -> ParseError( + offset, + unexpectedToken ?: other.unexpectedToken, + expectedTokens.union(other.expectedTokens) + ) + } + + // This could be a lot better, looking at megaparsec here! + override fun toString(): String = + "ParseError at offset $offset. Got \"$unexpectedToken\" but expected: \"${showExpected()}\"" + + private fun showExpected(): String = + expectedTokens.fold("") { acc, errorItem -> "$acc,$errorItem" }.drop(1) +} + +sealed class ErrorItem { + companion object { + fun unsafeFromString(str: String): ErrorItem = + Tokens(Nel.fromListUnsafe(str.toList())) + + fun fromChar(c: Char): ErrorItem = + Tokens(Nel.of(c)) + } + + override fun toString(): String = when (this) { + is Tokens -> ts.all.joinToString("") + is EndOfInput -> "End of input" + } +} + +class Tokens(val ts: NonEmptyList) : ErrorItem() +object EndOfInput : ErrorItem() + +data class ParseResult(val remaining: String, val result: Either) + +//impl +class ParserCtxImpl(val input: String, val scope: DelimitedScope>) : ParserCtx, DelimitedScope> by scope { + private var offset = 0 + override fun getOffset(): Int = offset + + private fun take1_(): Char? = input.takeIf { it.length > offset }?.get(offset) + private fun take_(n: Int): String? = input.takeIf { it.length >= offset + n }?.substring(offset, offset + n) + fun takeRemaining(): String = input.takeIf { it.length > offset }?.substring(offset) ?: "" + + override suspend fun take1(): Char = take1_()?.also { offset++ } ?: raise(ParseError(offset, EndOfInput)) + override suspend fun take(n: Int): String = take_(n)?.also { offset += n } ?: raise(ParseError(offset, EndOfInput)) + override suspend fun takeWhile(matcher: (Char) -> Boolean): String = + input.takeWhile(matcher).also { offset += it.length } + + override suspend fun takeAtLeastOneWhile(matcher: (Char) -> Boolean): String = + input.takeWhile(matcher).takeIf { it.isNotEmpty() } + ?: raise( + ParseError( + offset, + input + .takeIf { it.length > offset } + ?.get(offset)?.let { ErrorItem.fromChar(it) } ?: EndOfInput + ) + ) + + override suspend fun string(str: String): String = + (take_(str.length) ?: raise(ParseError(offset, EndOfInput, setOf(ErrorItem.unsafeFromString(str))))) + .let { + it.takeIf { it == str } + ?.also { offset += str.length } + ?: raise(ParseError(offset, ErrorItem.unsafeFromString(it), setOf(ErrorItem.unsafeFromString(str)))) + } + + override suspend fun token(expected: Set, matcher: (Char) -> B?): B = + (take1_() ?: raise(ParseError(offset, EndOfInput, expected))) + .let { it.let(matcher)?.also { offset++ } ?: raise(ParseError(offset, ErrorItem.fromChar(it), expected)) } + + override suspend fun eof() = take1_().let { c -> + if (c != null) raise(ParseError(offset, ErrorItem.fromChar(c), setOf(EndOfInput))) + } + + override suspend fun lookAhead(p: Parser): A = + p.runParser(takeRemaining()).result.fold( + { e -> raise(e) }, + ::identity + ) + + override suspend fun notFollowedBy(p: Parser) = + p.runParser(takeRemaining()).result.fold( + {}, + { + raise( + ParseError(offset, input + .takeIf { it.length > offset } + ?.get(offset) + ?.let { c -> ErrorItem.fromChar(c) } + ?: EndOfInput) + ) + } + ) + + override suspend fun optional(p: Parser): A? = + p.runParser(takeRemaining()).result.fold({ null }, ::identity) + + override suspend fun raise(e: ParseError): Nothing = + shift { ParseResult(takeRemaining(), e.left()) } + + /* + This won't work without nesting support because f may call into parent scopes. + Ideally we'd have f: ParserCtx.() -> A here to avoid this by giving an explicit context + override suspend fun catch(f: suspend () -> A, hdl: suspend (ParseError) -> A): A { + val p: Parser> = { f() toT getOffset() } + return p.runParser(takeRemaining()).result.fold({ e -> + hdl(e) + }, { (result, off) -> + offset = off + result + }) + } + */ + override suspend fun catch(f: suspend () -> A, hdl: suspend (ParseError) -> A): A = TODO("Not working. Use attempt for now. See note in code on commented part above") + + override fun attempt(p: Parser): Either = + p.runParser(takeRemaining()).result + + override suspend fun choose(): Boolean { + return shift { k -> + val res = k(true) + when (val lResult = res.result) { + is Either.Left -> { + val res2 = k(false) + val nRem = if (res.remaining.length > res2.remaining.length) res2.remaining else res.remaining + when (val rResult = res2.result) { + is Either.Left -> ParseResult(nRem, Either.Left(lResult.a + rResult.a)) + is Either.Right -> res2 + } + } + is Either.Right -> res + } + } + } + + override suspend fun empty(): Nothing = raise(ParseError(offset, null, emptySet())) +} + +fun Parser.runParser(str: String): ParseResult = + MultiShotDelimContScope.reset { + val ctx = ParserCtxImpl(str, this) + val res = this@runParser.invoke(ctx) + ParseResult(ctx.takeRemaining(), Either.right(res)) + } diff --git a/arrow-continuations/src/test/kotlin/generic/ParserTest.kt b/arrow-continuations/src/test/kotlin/generic/ParserTest.kt new file mode 100644 index 000000000..82e9e4356 --- /dev/null +++ b/arrow-continuations/src/test/kotlin/generic/ParserTest.kt @@ -0,0 +1,29 @@ +package generic + +import arrow.continuations.generic.effect.Parser +import arrow.continuations.generic.effect.choice +import arrow.continuations.generic.effect.runParser +import arrow.core.identity +import io.kotlintest.specs.StringSpec + +sealed class Language +object German : Language() +object English : Language() + +// example +val parser: Parser = { + attempt { choice({ string("Wooo") }, { string("Wee") }) }.fold({ string("Error") }, ::identity) + val lang = optional { + choice({ string("Hello"); English }, { string("Hallo"); German }) + .also { eof() } + } + lang ?: English +} + +class ParserTest : StringSpec({ + "can parse" { + parser + .runParser("WooHalloWeird") + .also(::println) + } +}) From 210e8d3eb3eda2ed7eaf8b3dcfb3090d48cc4dcb Mon Sep 17 00:00:00 2001 From: Jannis Date: Sun, 2 Aug 2020 15:00:48 +0200 Subject: [PATCH 39/49] Split Error into Catch and Raise --- .../generic/effect/EffectCombinedHandlers.kt | 2 +- .../arrow/continuations/generic/effect/Effects.kt | 11 +++++++++-- .../arrow/continuations/generic/effect/Parser.kt | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt index 5e957b0be..0f2057a99 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt @@ -17,7 +17,7 @@ fun Either.Companion.errorHandler( delimitedScope: DelimitedScope, A>>> ): Error = object : Error { - override suspend fun catch(f: suspend () -> B, hdl: suspend (E) -> B): B = + override suspend fun catch(f: suspend Raise.() -> B, hdl: suspend (E) -> B): B = TODO("This requires proper nested scope support to be sensible") override suspend fun raise(e: E): Nothing = diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt index 43ed56a57..f8535909a 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt @@ -1,10 +1,17 @@ package arrow.continuations.generic.effect -interface Error { +import arrow.core.Either + +interface Raise { suspend fun raise(e: E): Nothing - suspend fun catch(f: suspend () -> A, hdl: suspend (E) -> A): A } +interface Catch { + suspend fun catch(f: suspend Raise.() -> A, hdl: suspend (E) -> A): A +} + +interface Error : Raise, Catch + interface Empty { suspend fun empty(): Nothing } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt index dc57cb478..4778396c1 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt @@ -157,7 +157,7 @@ class ParserCtxImpl(val input: String, val scope: DelimitedScope catch(f: suspend () -> A, hdl: suspend (ParseError) -> A): A = TODO("Not working. Use attempt for now. See note in code on commented part above") + override suspend fun catch(f: suspend Raise.() -> A, hdl: suspend (ParseError) -> A): A = TODO("Not working. Use attempt for now. See note in code on commented part above") override fun attempt(p: Parser): Either = p.runParser(takeRemaining()).result From ba81146f393f1bdaa9209c2279fbd5cac838faf4 Mon Sep 17 00:00:00 2001 From: Jannis Date: Mon, 3 Aug 2020 12:33:38 +0200 Subject: [PATCH 40/49] Added nested reset support but without multishot for now --- .../arrow/continuations/generic/DelimCont.kt | 4 +- .../continuations/generic/DelimitedCont.kt | 35 ++++- .../generic/MultiShotDelimCont.kt | 2 +- .../continuations/generic/NestedDelimCont.kt | 137 ++++++++++++++++++ .../continuations/generic/effect/Effects.kt | 9 ++ .../src/test/kotlin/generic/TestSuite.kt | 74 ++++++++++ 6 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt index 66f905190..aa0313566 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt @@ -35,7 +35,7 @@ class DelimContScope(val f: suspend DelimitedScope.() -> R): RunnableDelim override suspend fun invoke(a: A): R = DelimContScope { runFunc(a) }.invoke() } - // TODO I wrote this in the middle of the night, double check + // TODO I wrote this comment in the middle of the night, double check // Note we don't wrap the function [func] in an explicit reset because that is already implicit in our scope override suspend fun shift(func: suspend DelimitedScope.(DelimitedContinuation) -> R): A = suspendCoroutine { continueMain -> @@ -48,7 +48,7 @@ class DelimContScope(val f: suspend DelimitedScope.() -> R): RunnableDelim assert(nextShift.compareAndSet(null, suspend { func(CPSCont(c)) })) } - override fun reset(f: suspend DelimitedScope.() -> A): A = + override suspend fun reset(f: suspend DelimitedScope.() -> A): A = DelimContScope(f).invoke() override fun invoke(): R { diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt index 7c66d3efe..266b65d3e 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt @@ -19,9 +19,42 @@ interface DelimitedScope { // This means it is trivially multishot because c has the stack in its closure. To enforce that this is the last // statement of a reset block we return Nothing here. suspend fun shiftCPS(f: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> R): Nothing - fun reset(f: suspend DelimitedScope.() -> A): A + suspend fun reset(f: suspend DelimitedScope.() -> A): A } interface RunnableDelimitedScope : DelimitedScope { operator fun invoke(): R } + +/** + * Problems of nested reset: + * Scopes cannot be functor, we can not change the return type of a scope through mapping or anything else other than isomorphisms. + * This means the inner scope needs its own scope and thus its own runloop. + * A few interesting scenarios come up now: + * - The function in the inner scope may refer to *any* outer scope and may also call suspend functions on the outer scope + * This means the inner scopes runloop may stop with no further work before finishing because the work has been queued + * on a parent. There may be 1-n parents and do not know which parent got the work, which leads to n checks if this happens. + * When a parent has no further work and is not done it also may have done work for a child. + * This means it has to check it's 0..n childs to see which one needs to be resumed. + * This means every time we invoke an effect on an outer scope we need to check our 1..n parents for work and if they don't + * have work we need to check our 0..m child scopes if we have completed their work. + * One rather extreme measure that I haven't tried is collapsing all scopes to one runloop. This could be possible + * because the part that the runloop executes is a function to a local result. + * The completion still has to be handled by each scope though. + * This is the doable but annoying part of the problem btw. + * - Another fun bit on the runloop is that within shift we can reference parent scopes as well. Thus when suspending to a parent + * we need to somehow tell that scope to call us back before the lowermost child + * - This brings us to problem number 2: Multishot + * Normally when we run multishot we mark effect stack offsets when shift is called and then we can slice the stack from until offset + * when invoke for the continuation is called the second time. + * Because multishot may now rerun inner resets we now have the following changes: + * - if we call shift in our inner scope we have to store both the global offset on the stack as our current offset and + * the local offset from the start of our scope. + * - When we rerun we have to notify our parents of this as well so that they can depending on the multishot's current offset + * also access the stack rather than running the shift function. + * - When we rerun an inner scope from some point and it later calls a parent scope which is outside of our offset we can't use + * the stack. This now means that we have a local stack slice that is different from the global stack, we execute the + * outer level shift and on invoke put the arguments on the stack. This makes multishot from this point on hard: + * - + * + */ diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt index 571b82e22..a00782a01 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt @@ -60,7 +60,7 @@ open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) } // This assumes RestrictSuspension or at least assumes the user to never reference the parent scope in f. - override fun reset(f: suspend DelimitedScope.() -> A): A = + override suspend fun reset(f: suspend DelimitedScope.() -> A): A = MultiShotDelimContScope(f).invoke() override fun invoke(): R { diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt new file mode 100644 index 000000000..de13ff0e7 --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt @@ -0,0 +1,137 @@ +package arrow.continuations.generic + +import kotlinx.atomicfu.atomic +import kotlin.coroutines.Continuation +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED +import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +open class NestedDelimContScope(val f: suspend DelimitedScope.() -> R) : RunnableDelimitedScope { + + private val resultVar = atomic(null) + internal fun getResult(): R? = resultVar.value + internal fun setResult(r: R): Unit { + resultVar.value = r + } + + internal inline fun loopNoResult(f: () -> Unit): Unit { + while (true) { + if (getResult() == null) f() + else return + } + } + + internal val nextShift = atomic<(suspend () -> R)?>(null) + + // TODO This can be append only and needs fast reversed access + internal val shiftFnContinuations = mutableListOf>() + + data class SingleShotCont( + private val continuation: Continuation, + private val shiftFnContinuations: MutableList> + ) : DelimitedContinuation { + override suspend fun invoke(a: A): R = suspendCoroutine { resumeShift -> + shiftFnContinuations.add(resumeShift) + continuation.resume(a) + } + } + + data class CPSCont( + private val runFunc: suspend DelimitedScope.(A) -> R + ) : DelimitedContinuation { + override suspend fun invoke(a: A): R = DelimContScope { runFunc(a) }.invoke() + } + + override suspend fun shift(func: suspend DelimitedScope.(DelimitedContinuation) -> R): A = + suspendCoroutine { continueMain -> + val delCont = SingleShotCont(continueMain, shiftFnContinuations) + assert(nextShift.compareAndSet(null, suspend { this.func(delCont) })) + } + + override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> R): Nothing = + suspendCoroutine { + assert(nextShift.compareAndSet(null, suspend { func(CPSCont(c)) })) + } + + override suspend fun reset(f: suspend DelimitedScope.() -> A): A = + ChildDelimContScope(this, f) + .invokeNested() + + internal inline fun step(hdlMisingWork: () -> Unit): Unit { + val nextShiftFn = nextShift.getAndSet(null) + ?: return hdlMisingWork() + nextShiftFn.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { result -> + resultVar.value = result.getOrThrow() + }).let { + if (it != COROUTINE_SUSPENDED) resultVar.value = it as R + } + } + + override fun invoke(): R { + f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result -> + resultVar.value = result.getOrThrow() + }).let { + if (it == COROUTINE_SUSPENDED) { + loopNoResult { + step { throw IllegalStateException("Suspended parent scope, but found no further work") } + } + } else return@invoke it as R + } + + assert(resultVar.value != null) + for (c in shiftFnContinuations.asReversed()) c.resume(resultVar.value!!) + return resultVar.value!! + } + + open fun getActiveParent(): NestedDelimContScope<*>? = this.takeIf { nextShift.value != null } + + companion object { + fun reset(f: suspend DelimitedScope.() -> R): R = NestedDelimContScope(f).invoke() + } +} + +class ChildDelimContScope( + val parent: NestedDelimContScope<*>, + f: suspend DelimitedScope.() -> R +) : NestedDelimContScope(f) { + override fun getActiveParent(): NestedDelimContScope<*>? = + super.getActiveParent() ?: parent.getActiveParent() + + private suspend fun performParentWorkIfNeeded(): Unit { + while (true) { + parent.getActiveParent()?.let { scope -> + // No need to do anything in steps cb because we handle this case from down here + scope.step { } + // parent short circuited + if (scope.getResult() != null) suspendCoroutine { } + } ?: break + } + } + + suspend fun invokeNested(): R { + f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result -> + setResult(result.getOrThrow()) + }).let { + if (it == COROUTINE_SUSPENDED) { + loopNoResult { + step { performParentWorkIfNeeded() } + } + } else return@invokeNested it as R + } + + assert(getResult() != null) + for (c in shiftFnContinuations.asReversed()) c.resume(getResult()!!) + return getResult()!! + } + + override fun invoke(): R { + println(""" + Using invoke() for child scope. + This will break on nested calls to reset and using shift from different scopes inside those. + Use invokeNested instead. + """) + return super.invoke() + } +} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt index f8535909a..41dee638c 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt @@ -7,6 +7,15 @@ interface Raise { } interface Catch { + /** + * This is not easy to implement. Best attempt is probably going: + * reset> { + * val raiseEff = eitherRaise(this@reset) // define handler for raise here + * f(raiseEff) + * }.fold({ e -> hdl() }, ::identity) + * This runs into another problem though: + * reset as of now is not supporting nested scopes in terms of its runloop or multishot. + */ suspend fun catch(f: suspend Raise.() -> A, hdl: suspend (E) -> A): A } diff --git a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt index 47654c22e..00bb26279 100644 --- a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt +++ b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt @@ -1,8 +1,10 @@ package generic +import arrow.continuations.effectStack.reset import arrow.continuations.generic.DelimitedScope import arrow.continuations.generic.MultiShotDelimContScope import arrow.continuations.generic.DelimContScope +import arrow.continuations.generic.NestedDelimContScope import arrow.core.Either import arrow.core.Left import arrow.core.Tuple2 @@ -120,11 +122,76 @@ abstract class ContTestSuite: UnitSpec() { } } } + if (capabilities().contains(ScopeCapabilities.NestedScopes)) { + "nested reset calling between scopes" { + runScope { + val a: Int = shift { it(5) } + a + reset fst@{ + val i: Int = shift { it(10) } + reset snd@{ + val j: Int = shift { it(20) } + val k: Int = this@fst.shift { it(30) } + i + j + k + } + } shouldBe 65 + } + } + "nested reset calling between a lot of scopes" { + runScope fst@{ + val a: Int = shift { it(5) } + a + reset snd@{ + val i: Int = shift { it(10) } + reset third@{ + val j: Int = shift { it(20) } + val k: Int = this@fst.shift { it(30) } + this@snd.shift { it(40) } + reset fourth@{ + val p: Int = shift { it(20) } + val k2: Int = this@fst.shift { it(30) } + this@snd.shift { it(40) } + val t: Int = this@third.shift { it(5) } + i + j + k + p + k2 + t + } + } + } shouldBe 200 + } + } + "nested reset calling between scopes with short circuit" { + runScope { + val a: Int = shift { it(5) } + a + reset fst@{ + val i: Int = shift { it(10) } + reset snd@{ + val j: Int = shift { it(20) } + val k: Int = this@fst.shift { 5 } + i + j + k + } + } shouldBe 10 + } + } + "nested reset calling between a lot of scopes and short circuit" { + runScope fst@{ + val a: Int = shift { it(5) } + a + reset snd@{ + val i: Int = shift { it(10) } + reset third@{ + val j: Int = shift { it(20) } + val k: Int = this@fst.shift { it(30) } + this@snd.shift { it(40) } + reset fourth@{ + val p: Int = shift { it(20) } + val k2: Int = this@fst.shift { it(30) } + this@snd.shift { it(40) } + val t: Int = this@third.shift { 5 } + i + j + k + p + k2 + t + } + } + } shouldBe 10 + } + } + } } } sealed class ScopeCapabilities { object MultiShot : ScopeCapabilities() + object NestedScopes : ScopeCapabilities() } class SingleShotContTestSuite : ContTestSuite() { @@ -140,3 +207,10 @@ class MultiShotContTestSuite : ContTestSuite() { override fun capabilities(): Set = setOf(ScopeCapabilities.MultiShot) } + +class NestedContTestSuite : ContTestSuite() { + override fun runScope(func: suspend DelimitedScope.() -> A): A = + NestedDelimContScope.reset(func) + + override fun capabilities(): Set = setOf(ScopeCapabilities.NestedScopes) +} From bb76e78ad87afe5478cf38caf5512df2398905e4 Mon Sep 17 00:00:00 2001 From: Jannis Date: Tue, 4 Aug 2020 19:12:48 +0200 Subject: [PATCH 41/49] A better type for shiftCPS + another delimited cont implementation A recent paper on effect systems with haskell included an implementation for delimited continuations^^ --- .../kotlin/arrow/continuations/eveff/Ctl.kt | 91 +++++++++++++++++++ .../arrow/continuations/generic/DelimCont.kt | 2 +- .../continuations/generic/DelimitedCont.kt | 2 +- .../generic/MultiShotDelimCont.kt | 2 +- .../continuations/generic/NestedDelimCont.kt | 2 +- .../src/test/kotlin/eveff/CtlTest.kt | 16 ++++ .../src/test/kotlin/generic/TestSuite.kt | 8 +- 7 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/eveff/Ctl.kt create mode 100644 arrow-continuations/src/test/kotlin/eveff/CtlTest.kt diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/eveff/Ctl.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/eveff/Ctl.kt new file mode 100644 index 000000000..6ad049d8b --- /dev/null +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/eveff/Ctl.kt @@ -0,0 +1,91 @@ +package arrow.continuations.eveff + +// By "Effect Handlers in Haskell, Evidently" by Xie and Leijen +/** + * Implemented for clarity on how kotlin suspend and the other delimited cont impl work. + * + * mark is something we don't have in our implementations for simple reasons: + * we can refer to any scope simply by referring to its this-object and thus can jump + * straight to it when executing. + * op is the prompts handle function, what we call shift, it expects a continuation and provides an answer + * which is the final result of our prompt. This is equal to shift in function because shift implicitly + * multiprompts. + * cont is a partially applied continuation, which is to say it is our function which we execute in our context. + * We don't need this type explicitly because suspend functions are already basically this. + * + * All in all this is very close to my current encoding of delimited continuations just with explicit cps and + * thus with multishot support. Sadly this cannot easily be added to suspend. + */ + +sealed class Ctl { + fun flatMap(f: (A) -> Ctl): Ctl = when (this) { + is Pure -> f(res) + is Yield<*, *, *> -> + Yield( + mark as Marker, + op as (((Any?) -> Ctl) -> Ctl), + f.kcompose(cont as ((Any?) -> Ctl)) + ) + } + fun map(f: (A) -> B): Ctl = flatMap { just(f(it)) } + + companion object { + fun just(a: A): Ctl = Pure(a) + } +} +data class Pure(val res: A): Ctl() +data class Yield( + val mark: Marker, + val op: ((B) -> Ctl) -> Ctl, + val cont: (B) -> Ctl +): Ctl() + +tailrec fun Ctl.runCtl(): A = when (this) { + is Pure -> res + is Yield<*, *, *> -> (op(cont as (Any?) -> Nothing) as Ctl).runCtl() +} + +class Marker + +fun yield(mk: Marker, op: ((B) -> Ctl) -> Ctl): Ctl = + Yield(mk, op, { Pure(it) }) + +inline fun ((B) -> Ctl).kcompose(crossinline f: (A) -> Ctl): (A) -> Ctl = { a -> + f(a).flatMap(this) +} + +inline fun prompt(crossinline f: (Marker) -> Ctl): Ctl = + freshMarker { m: Marker -> mPrompt(m, f(m)) } + +fun freshMarker(f: (Marker) -> A): A = f(Marker()) + +fun mPrompt(m: Marker, c: Ctl): Ctl = when (c) { + is Pure -> c + is Yield<*, *, *> -> { + val nCont = { x: Any? -> + mPrompt( + m, + (c.cont as (Any?) -> Ctl).invoke(x) + ) + } + if (m === c.mark) (c.op as ((Any?) -> Ctl) -> Ctl)(nCont) + else Yield( + c.mark as Marker, + c.op as ((Any?) -> Ctl) -> Ctl, + nCont + ) + } +} + +// Eff +data class Eff(val runEff: (Context) -> Ctl) { + fun flatMap(f: (A) -> Eff): Eff = + Eff { ctx -> runEff(ctx).flatMap { a -> f(a).runEff(ctx) } } + fun map(f: (A) -> B): Eff = flatMap { just(f(it)) } + + companion object { + fun just(a: A): Eff = Eff { Ctl.just(a) } + } +} + +class Context diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt index aa0313566..37139aba8 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt @@ -43,7 +43,7 @@ class DelimContScope(val f: suspend DelimitedScope.() -> R): RunnableDelim assert(nextShift.compareAndSet(null, suspend { this.func(delCont) })) } - override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> R): Nothing = + override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> B): Nothing = suspendCoroutine { assert(nextShift.compareAndSet(null, suspend { func(CPSCont(c)) })) } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt index 266b65d3e..7effbb279 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt @@ -18,7 +18,7 @@ interface DelimitedScope { // shiftCPS passes the arguments with which the continuation is invoked to the supplied continuation/function c. // This means it is trivially multishot because c has the stack in its closure. To enforce that this is the last // statement of a reset block we return Nothing here. - suspend fun shiftCPS(f: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> R): Nothing + suspend fun shiftCPS(f: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> B): Nothing suspend fun reset(f: suspend DelimitedScope.() -> A): A } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt index a00782a01..9eed31e11 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt @@ -54,7 +54,7 @@ open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) assert(nextShift.compareAndSet(null, suspend { this.func(c) })) } - override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> R): Nothing = + override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> B): Nothing = suspendCoroutine { assert(nextShift.compareAndSet(null, suspend { func(CPSCont(c)) })) } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt index de13ff0e7..ce9759674 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt @@ -50,7 +50,7 @@ open class NestedDelimContScope(val f: suspend DelimitedScope.() -> R) : R assert(nextShift.compareAndSet(null, suspend { this.func(delCont) })) } - override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> R): Nothing = + override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> B): Nothing = suspendCoroutine { assert(nextShift.compareAndSet(null, suspend { func(CPSCont(c)) })) } diff --git a/arrow-continuations/src/test/kotlin/eveff/CtlTest.kt b/arrow-continuations/src/test/kotlin/eveff/CtlTest.kt new file mode 100644 index 000000000..428887e5c --- /dev/null +++ b/arrow-continuations/src/test/kotlin/eveff/CtlTest.kt @@ -0,0 +1,16 @@ +package eveff + +import arrow.continuations.eveff.Pure +import arrow.continuations.eveff.prompt +import arrow.continuations.eveff.runCtl +import arrow.continuations.eveff.yield +import io.kotlintest.specs.StringSpec + +class CtlTest : StringSpec({ + "test" { + prompt { + yield(it) { k -> k(10).flatMap(k) } + .map { i -> i * 2 } + }.runCtl().also { println(it) } + } +}) diff --git a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt index 00bb26279..b2795a123 100644 --- a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt +++ b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt @@ -34,9 +34,9 @@ abstract class ContTestSuite: UnitSpec() { } "shiftCPS supports multishot regardless of scope" { runScope { - shiftCPS({ it(1) + it(2) }) { i -> i } + shiftCPS({ it(1) + it(2) }) { i -> i + 1 } throw IllegalStateException("This is unreachable") - } shouldBe 3 + } shouldBe 5 } "reset" { runScope { @@ -55,7 +55,7 @@ abstract class ContTestSuite: UnitSpec() { } shouldBe listOf('a', '1', 'b', 'b', 'c') runScope> { listOf('a') + reset> { - shiftCPS({ f -> listOf('1') + f(f(listOf('c'))) }) { xs: List -> + shiftCPS, List>({ f -> listOf('1') + f(f(listOf('c'))) }) { xs: List -> listOf('b') + xs } } @@ -72,7 +72,7 @@ abstract class ContTestSuite: UnitSpec() { } shouldBe "a" // TODO this is not very accurate, probably not correct either runScope { - shiftCPS({ it("") }, { s: String -> shift { f -> "a" + f("") } }) + shiftCPS({ it("") }, { s: String -> shift { f -> "a" + f("") } }) } shouldBe "a" } "multshot nondet" { From 21bafb17104f0bef4c075630354d00aa69afac74 Mon Sep 17 00:00:00 2001 From: "Rachel M. Carmena" Date: Wed, 12 Aug 2020 21:43:42 +0200 Subject: [PATCH 42/49] Fix task --- .github/workflows/build_arrow-core.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_arrow-core.yml b/.github/workflows/build_arrow-core.yml index a0045be58..ad9c90421 100644 --- a/.github/workflows/build_arrow-core.yml +++ b/.github/workflows/build_arrow-core.yml @@ -21,4 +21,4 @@ jobs: cd $BASEDIR git clone https://github.com/arrow-kt/arrow.git - name: Build with Gradle - run: $BASEDIR/arrow/scripts/project-build.sh $ARROW_LIB + run: ./gradlew jar # build From 3fb948f60c96f85f4319cc1875bb1233a3f3f91f Mon Sep 17 00:00:00 2001 From: Jannis Date: Wed, 26 Aug 2020 19:03:06 +0200 Subject: [PATCH 43/49] Clean out branch to keep only the generic implementations --- arrow-continuations/build.gradle | 5 +- .../continuations/adt/ContinuationState.kt | 51 ----- .../kotlin/arrow/continuations/adt/cont.kt | 122 ---------- .../arrow/continuations/contt/DelimCC.kt | 216 ------------------ .../arrow/continuations/contt/DelimCCX.kt | 173 -------------- .../arrow/continuations/contt/Delimited.kt | 180 --------------- .../arrow/continuations/contt/Delimited2.kt | 112 --------- .../continuations/effectStack/program.kt | 178 --------------- .../kotlin/arrow/continuations/eveff/Ctl.kt | 91 -------- .../arrow/continuations/generic/DelimCont.kt | 6 +- .../continuations/generic/DelimitedCont.kt | 47 +--- .../generic/MultiShotDelimCont.kt | 4 +- .../continuations/generic/NestedDelimCont.kt | 8 +- .../generic/effect/EffectCombinedHandlers.kt | 78 ------- .../continuations/generic/effect/Effects.kt | 34 --- .../continuations/generic/effect/Parser.kt | 190 --------------- .../arrow/continuations/reflect/Coroutine.kt | 160 ------------- .../arrow/continuations/reflect/program.kt | 31 --- .../arrow/continuations/reflect/reify.kt | 93 -------- .../arrow/continuations/reflect2/Coroutine.kt | 75 ------ .../continuations/reflect2/UnsafePromise.kt | 69 ------ .../arrow/continuations/reflect2/program.kt | 22 -- .../arrow/continuations/reflect2/reify.kt | 90 -------- .../kotlin/effectStack/EffectStackTest.kt | 107 --------- .../src/test/kotlin/effectStack/Run.kt | 178 --------------- .../effectStack/interleave/Interleave.kt | 41 ---- .../src/test/kotlin/effectStack/predef.kt | 129 ----------- .../src/test/kotlin/eveff/CtlTest.kt | 16 -- .../generic/EffectCombinedHandlersTest.kt | 34 --- .../src/test/kotlin/generic/ParserTest.kt | 29 --- .../src/test/kotlin/generic/TestSuite.kt | 1 - 31 files changed, 18 insertions(+), 2552 deletions(-) delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/adt/ContinuationState.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited2.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/eveff/Ctl.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/UnsafePromise.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/program.kt delete mode 100644 arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/reify.kt delete mode 100644 arrow-continuations/src/test/kotlin/effectStack/EffectStackTest.kt delete mode 100644 arrow-continuations/src/test/kotlin/effectStack/Run.kt delete mode 100644 arrow-continuations/src/test/kotlin/effectStack/interleave/Interleave.kt delete mode 100644 arrow-continuations/src/test/kotlin/effectStack/predef.kt delete mode 100644 arrow-continuations/src/test/kotlin/eveff/CtlTest.kt delete mode 100644 arrow-continuations/src/test/kotlin/generic/EffectCombinedHandlersTest.kt delete mode 100644 arrow-continuations/src/test/kotlin/generic/ParserTest.kt diff --git a/arrow-continuations/build.gradle b/arrow-continuations/build.gradle index a3fb977a2..c02a7468b 100644 --- a/arrow-continuations/build.gradle +++ b/arrow-continuations/build.gradle @@ -8,9 +8,8 @@ plugins { id "ru.vyarus.animalsniffer" } -apply from: "$SUBPROJECT_CONF" -apply from: "$DOC_CONF" -apply from: "$PUBLISH_CONF" +apply from: "$SUB_PROJECT" +apply from: "$DOC_CREATION" apply plugin: 'kotlinx-atomicfu' dependencies { diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/ContinuationState.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/ContinuationState.kt deleted file mode 100644 index c484ef7cf..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/ContinuationState.kt +++ /dev/null @@ -1,51 +0,0 @@ -package arrow.continuations.adt - -import arrow.continuations.contxx.Cont -import java.util.* - -interface ContinuationState { - - fun shift(): Shift? = - takePrompt() as? Shift - - fun invoke(): Invoke? = - takePrompt() as? Invoke - - fun scope(): Scope? = - takePrompt() as? Scope - - fun takePrompt(): Continuation? - fun push(prompt: Continuation): Unit - fun log(value: String): Unit - - operator fun plusAssign(other: ContinuationState): Unit - - companion object { - operator fun invoke(): ContinuationState = - StackContinuationState() - - private class StackContinuationState( - private val prompts: Stack> = Stack() - ) : ContinuationState { - - override fun takePrompt(): Continuation? = - if (prompts.isNotEmpty()) prompts.pop() else null - - override fun push(prompt: Continuation) { - prompts.push(prompt) - } - - private fun stateLog(): String = - "/size: ${prompts.size}/ $prompts" - - - override fun log(value: String): Unit { - println("${stateLog()}\t\t\t$value") - } - - override fun plusAssign(other: ContinuationState): Unit { - while (other.takePrompt()?.also { prompts.push(it) } != null) {} - } - } - } -} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt deleted file mode 100644 index d5a774218..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/adt/cont.kt +++ /dev/null @@ -1,122 +0,0 @@ -package arrow.continuations.adt - -import kotlin.coroutines.suspendCoroutine - -typealias Scope = Continuation.Scope -typealias Shift = Continuation.Scope.Shift -typealias Invoke = Continuation.Scope.Shift.Invoke -typealias ShortCircuit = Continuation.ShortCircuit -typealias KotlinContinuation = kotlin.coroutines.Continuation - -sealed class Continuation { - - abstract val parent: Continuation<*, *> - - abstract val state: ContinuationState<*, *, *> - - inner class ShortCircuit(val value: A) : Continuation() { - override val parent: Continuation = this@Continuation - override val state: ContinuationState<*, *, *> = this@Continuation.state - } - - abstract class Scope( - override val state: ContinuationState = ContinuationState() - ) : Continuation() { - - override val parent: Continuation get() = this - - abstract val result: A - - inner class Shift( - val block: suspend Scope.(Shift) -> A, - val continuation: KotlinContinuation - ) : Continuation(), KotlinContinuation by continuation { - val scope: Scope = this@Scope - override val parent: Continuation = scope - override val state: ContinuationState = scope.state - - inner class Invoke(val continuation: KotlinContinuation, val value: C) : Continuation(), KotlinContinuation by continuation { - val shift: Shift = this@Shift - override val parent: Shift = this@Shift - override val state: ContinuationState = shift.state - } - - private var _result: C? = null - - override fun resumeWith(result: Result) { - this._result = result.getOrThrow() - } - } - - - } -} - -suspend fun Scope.shift(block: suspend Continuation.Scope.(Continuation.Scope.Shift) -> A): C = - suspendCoroutine { - Shift(block, it).compile(state) - } - -suspend operator fun Shift.invoke(value: C): A = - suspendCoroutine { - Invoke(it, value).compile(state) - } - -fun ContinuationState.unfold(): A = - when(val prompt = takePrompt()) { - is ShortCircuit -> prompt.value - is Scope -> prompt.result - null -> TODO() - is Shift<*, *, *> -> TODO() - is Invoke<*, *, *> -> TODO() - } - - -fun Continuation.compile(state: ContinuationState): A = - when (this@compile) { - is Shift<*, *, *> -> { - state.log("Shift: [parent: $parent, scope: $scope, block: $block]") - state.push(this@compile) - state.unfold() - } - is Invoke<*, *, *> -> { - state.log("Invoke: [parent: $parent, value: $value]") - state.push(this@compile) - state.unfold() - } - is Scope -> { - state.log("Scope: [parent: $parent, result: $result]") - state.push(this@compile) - state.unfold() - } - is ShortCircuit -> { - state.log("ShortCircuit: [parent: $parent, value: $value]") - value - } - } - -class ListScope : Scope, A>() { - private var _result: List = emptyList() - override val result: List get () = _result - suspend operator fun List.invoke(): C = - shift { cb -> - _result = flatMap { - cb(it) - } - result - } -} - -inline fun list(block: ListScope<*>.() -> A): List = - listOf(block(ListScope())) - - -suspend fun main() { - val result = list { - val a = listOf(1, 2, 3)() - val b = listOf("a", "b", "c")() - "$a$b " - } - println(result) -} - diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt deleted file mode 100644 index 0af5aed58..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCC.kt +++ /dev/null @@ -1,216 +0,0 @@ -package arrow.continuations.contxx - -import arrow.core.ShortCircuit -import kotlinx.atomicfu.atomic -import kotlinx.atomicfu.loop -import java.util.* -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.RestrictsSuspension -import kotlin.coroutines.intrinsics.* -import kotlin.coroutines.resume - -class DelimitedContinuation( - val prompt: Prompt, - val f: suspend DelimitedContinuation.() -> A -) : Continuation { - - override val context: CoroutineContext = EmptyCoroutineContext - - fun ShortCircuit.recover(): A = throw this - - override fun resumeWith(result: Result) { - _decision.loop { decision -> - when (decision) { - UNDECIDED -> { - val r: A? = when { - result.isFailure -> { - val e = result.exceptionOrNull() - if (e is ShortCircuit) e.recover() else null - } - result.isSuccess -> result.getOrNull() - else -> throw TODO("Impossible") - } - - when { - r == null -> { - throw result.exceptionOrNull()!! - //resumeWithException(result.exceptionOrNull()!!) - return - } - _decision.compareAndSet(UNDECIDED, Completed(r)) -> return - else -> Unit // loop again - } - } - else -> { // If not `UNDECIDED` then we need to pass result to `parent` - val res: Result = result.fold({ Result.success(it) }, { t -> - if (t is ShortCircuit) Result.success(t.recover()) - else Result.failure(t) - }) - _decision.getAndSet(Completed(res.getOrThrow())) - return - } - } - } - } - - private val _decision = atomic(UNDECIDED) - - fun isDone(): Boolean = - _decision.value is Completed<*> - - fun run(): Unit { - f.startCoroutineUninterceptedOrReturn(this, this) - _decision.loop { decision -> - when (decision) { - UNDECIDED -> if (this._decision.compareAndSet(UNDECIDED, prompt)) Unit //loop again - else -> return@run - } - } - } - - companion object { - suspend fun yield(prompt: Prompt) = - DelimCC.runCont(DelimitedContinuation(prompt) { - resume(prompt) - }) - } -} - -suspend fun reset(prompt: Prompt, f: suspend () -> A): A { - val k: DelimitedContinuation<*> = DelimitedContinuation(prompt) { - DelimCC.result = f() - DelimCC.result - } - return DelimCC.runCont(k) -} - -suspend fun shift(prompt: Prompt, body: CPS): A { - DelimCC.body = body - DelimitedContinuation.yield(prompt) - return DelimCC.arg as A -} - -// multiprompt delimited continuations in terms of the current API -// this implementation has Felleisen classification -F+ -object DelimCC { - internal var result: Any? = null - internal var arg: Any? = null - internal var body: CPS<*, *>? = null - - internal suspend fun runCont(k: DelimitedContinuation<*>): A { - k.run() - val frames = Stack>() - while (!k.isDone()) { - - // IDEA: - // 1) Push a separate (one-time) prompt on `shift`. - // 2) On resume, capture the continuation on a heap allocated - // stack of `DelimitedContinuation`s - // 3) Trampoline those continuations at the position of the original - // `reset`. - // - // This only works since continuations are one-shot. Otherwise the - // captured frames would contain references to the continuation and - // would be evaluated out of scope. - val bodyPrompt: Prompt = Prompt() - val bodyCont: DelimitedContinuation<*> = - DelimitedContinuation(bodyPrompt) { - result = (body as CPS)(Cont { value -> - // yield and wait until the subcontinuation has been - // evaluated. - arg = value - // yielding here returns control to the outer continuation - DelimitedContinuation.yield(bodyPrompt) - result - }) - body = null - body - } - bodyCont.run() // start it - - // continuation was called within body - if (!bodyCont.isDone()) { - frames.push(bodyCont) - k.run() - - // continuation was discarded or escaped the dynamic scope of - // bodyCont. - } else { - break - } - } - while (!frames.isEmpty()) { - val frame = frames.pop() - if (!frame.isDone()) { - frame.run() - } - } - return result as A - } -} - -interface DelimitedContinuationScope -class Prompt : DelimitedContinuationScope -class Completed(val value: A) : DelimitedContinuationScope -object UNDECIDED : DelimitedContinuationScope - -class Cont(val f: suspend (A) -> R) { - suspend operator fun invoke(p1: A): R = f(p1) -} - -class CPS(val f: suspend (Cont) -> R) { - suspend operator fun invoke(p1: Cont): R = f(p1) -} - -suspend fun test1() { - val p1 = Prompt() - val p2 = Prompt() - val res1 = reset(p1) { 5 - shift(p1, CPS { k: Cont -> k(2) * 7 }) } - val res2 = reset(p1) { - (1 + shift(p1, CPS { k: Cont -> k(2) }) - + shift(p1, CPS { k: Cont -> k(3) })) - } - val res3 = reset(p1) { - (1 + shift(p1, CPS { k: Cont -> k(2) }) - + reset(p2) { - (2 + shift(p2, CPS { k: Cont -> k(3) * 3 }) - + shift(p1, CPS { k: Cont -> k(4) * 2 })) - }) - } - val res4 = reset(p1) { - (1 + shift(p1, CPS { k: Cont -> k(2) }) - + reset(p2) { - (2 + shift(p2, CPS { k: Cont -> k(3) * 3 }) - + shift(p1, CPS { k: Cont -> 42 })) - }) - } - println(res1) // 21 - println(res2) // 6 - println(res3) // 60 - println(res4) // 42 -} - -suspend fun test2() { - val p1 = Prompt() - val res = reset(p1) { - var n = 10000 - var r = 0 - while (n > 0) { - r += shift(p1, CPS { k: Cont -> k(1) }) - n-- - } - r - } - println(res) -} - -fun main() { - DelimitedContinuation(Prompt()) { - DelimCC.run { - test1() - test2() - } - }.run() -} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt deleted file mode 100644 index 4cf5b6648..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/DelimCCX.kt +++ /dev/null @@ -1,173 +0,0 @@ -package arrow.continuations.conttxxxx - -import java.util.* -import kotlin.collections.ArrayList -import kotlin.coroutines.* -import kotlin.coroutines.intrinsics.* - -suspend fun reset(body: suspend DelimitedScope.() -> T): T = - DelimitedScopeImpl(body).run { - body.startCoroutine(this, this) - runReset() - } - -interface DelimitedContinuation - -//@RestrictsSuspension -abstract class DelimitedScope { - abstract suspend fun shift(block: suspend DelimitedScope.(DelimitedContinuation) -> T): R - abstract suspend operator fun DelimitedContinuation.invoke(value: R): T -} - -private typealias ShiftedFun = (DelimitedScope, DelimitedContinuation, Continuation) -> Any? - -@Suppress("UNCHECKED_CAST") -private class DelimitedScopeImpl(val body: suspend DelimitedScope.() -> T) : DelimitedScope(), Continuation, DelimitedContinuation { - private val shiftedBody: Stack> = Stack() - private var shiftCont: Stack> = Stack() - private var invokeCont: Stack> = Stack() - private var invokeValue: Stack = Stack() - private var completions: Stack> = Stack() - private var lastShiftedBody: ShiftedFun? = null - - private fun stateHeader(): String = - "shiftedBody\tshiftCont\tinvokeCont\tinvokeValue\tcompletions" - - private fun stateLog(): String = - "${shiftedBody.size}\t\t\t${shiftCont.size}\t\t\t${invokeCont.size}\t\t\t${invokeValue.size}\t\t\t${completions.size}" - - - private fun log(value: String): Unit { - println("${stateLog()}\t\t\t$value") - } - - override val context: CoroutineContext = EmptyCoroutineContext - - override fun resumeWith(result: Result) { - completions.push(result) - } - - override suspend fun shift(block: suspend DelimitedScope.(DelimitedContinuation) -> T): R = - suspendCoroutineUninterceptedOrReturn { - this.lastShiftedBody = block as ShiftedFun - this.shiftedBody.push(block as ShiftedFun) - this.shiftCont.push(it as Continuation) - COROUTINE_SUSPENDED - } - - override suspend fun DelimitedContinuation.invoke(value: R): T = - suspendCoroutineUninterceptedOrReturn sc@{ - //check(invokeCont == null) - invokeCont.push(it) - invokeValue.push(value) - COROUTINE_SUSPENDED - } - - // This is the stack of continuation in the `shift { ... }` after call to delimited continuation - var currentCont: Continuation = this - - suspend fun runReset(): T = - suspendCoroutineUninterceptedOrReturn { parent -> - println(stateHeader()) - log("[SUSPENDED] runReset()") - //var result: Result? = null - // Trampoline loop to avoid call stack usage - resetLoop@ while (true) { - log("\t-> resetLoop") - if (pushCompletion(currentCont)) break - // Shift has suspended - check if shift { ... } body had invoked continuation - shiftLoop@ while (shiftCont.isNotEmpty()) { - log("\t\t-> shiftLoop") - currentCont = takeInvokeCont() ?: continue@resetLoop - val shift = takeShiftCont() - ?: error("Delimited continuation is single-shot and cannot be invoked twice") - val invokeVal = invokeValue.pop() - log("\t\t!! resumeWith: $invokeVal") - shift.resumeWith(Result.success(invokeVal as T)) - log("\t\t<- shift loop") - } - // Propagate the result to all pending continuations in shift { ... } bodies - if (propagateCompletions()) continue@resetLoop - log("\t<- resetLoop") - } - COROUTINE_SUSPENDED - } - - private fun propagateCompletions(): Boolean { - if (completions.isNotEmpty()) { - when (val r = completions.pop()) { -// null -> TODO("Impossible result is null") - else -> { - log("\t<- propagateCompletion: $r") - //completions.push(r) - //resume first shot if invoked - - suspend { - suspendCoroutine { - //shiftedBody.push(lastShiftedBody) - //invokeValue.push(r.getOrThrow()) - it.resumeWith(r) - } - }.createCoroutine(this).resumeWith(Result.success(Unit)) - - //proceed to next value if invoked - //invokeValue.push(r.getOrThrow()) - - return true - // Return the final result - //resetCont.resumeWith(r) - } - } - } - return true - } - - private fun pushCompletion(currentCont: Continuation): Boolean { - try { - // Call shift { ... } body or break if there are no more shift calls - // If shift does not call any continuation, then its value is pushed and break out of the loop - val shifted = takeShifted() ?: return true - val value = shifted.invoke(this, this, currentCont) - if (value !== COROUTINE_SUSPENDED) { - log("-> pushCompletion: $value") - completions.push(Result.success(value as T)) - //continue@loop - //break //TODO or loop again? - } - } catch (e: Throwable) { - log("-> pushCompletion: $e") - completions.push(Result.failure(e)) - //continue@loop - //break //TODO or loop again? - } - return false - } - - private fun takeShifted() = if (shiftedBody.isNotEmpty()) shiftedBody.pop() else null - private fun takeShiftCont() = if (shiftCont.isNotEmpty()) shiftCont.pop() else null - private fun takeInvokeCont() = if (invokeCont.isNotEmpty()) invokeCont.pop() else null -} - -suspend fun DelimitedScope>.bind(list: List): B { - val result: ArrayList = arrayListOf() - return shift { cb -> - for (el in list) { - reset { - result.addAll( - shift { cb(el) } - ) - } - } - result - } -} - -suspend fun main() { - val result: List = reset { - val a = bind(listOf(1, 2, 3)) - val b = bind(listOf("a", "b", "c")) - listOf("$a$b ") - } - println(result) -} - diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited.kt deleted file mode 100644 index 1251639ed..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited.kt +++ /dev/null @@ -1,180 +0,0 @@ -package arrow.continuations.conts - -import arrow.core.ShortCircuit -import kotlinx.atomicfu.atomic -import kotlinx.atomicfu.loop -import kotlin.coroutines.Continuation -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.coroutines.resumeWithException - -fun reset(prompt: DelimitedScope): A = - ContinuationState.reset(prompt) { - val result = prompt.body(prompt) - result as A - } - -interface DelimitedContinuation : Continuation { - fun invokeWith(value: Result): A - fun isDone(): Boolean - fun run(): Unit - suspend fun yield(): Unit -} - -interface DelimitedScope : DelimitedContinuation { - fun ShortCircuit.recover(): A = throw this - suspend fun shift(block: suspend (DelimitedScope) -> A): B = - ContinuationState.shift(this, block) - - fun startCoroutineUninterceptedOrReturn(): Any? - fun startCoroutineUninterceptedOrReturn(body: suspend DelimitedScope.() -> A): Any? - - val body: suspend DelimitedScope.() -> A -} - -open class DelimitedScopeImpl(open val prompt: Continuation, override val body: suspend DelimitedScope.() -> A) : DelimitedScope { - - /** - * State is either - * 0 - UNDECIDED - * 1 - SUSPENDED - * Any? (3) `resumeWith` always stores it upon UNDECIDED, and `getResult` can atomically get it. - */ - private val _decision = atomic(UNDECIDED) - - override val context: CoroutineContext = EmptyCoroutineContext - - override fun resumeWith(result: Result) { - _decision.loop { decision -> - when (decision) { - UNDECIDED -> { - val r: A? = when { - result.isFailure -> { - val e = result.exceptionOrNull() - if (e is ShortCircuit) e.recover() else null - } - result.isSuccess -> result.getOrNull() - else -> TODO("Impossible bug") - } - - when { - r == null -> { - prompt.resumeWithException(result.exceptionOrNull()!!) - return - } - _decision.compareAndSet(UNDECIDED, r) -> return - else -> Unit // loop again - } - } - else -> { // If not `UNDECIDED` then we need to pass result to `parent` - val res: Result = result.fold({ Result.success(it) }, { t -> - if (t is ShortCircuit) Result.success(t.recover()) - else Result.failure(t) - }) - prompt.resumeWith(res) - return - } - } - } - } - - override fun invokeWith(value: Result): A = - value.getOrThrow() as A - - override fun isDone(): Boolean = - _decision.loop { decision -> - return when (decision) { - UNDECIDED -> false - SUSPENDED -> false - COROUTINE_SUSPENDED -> false - else -> true - } - } - - override fun startCoroutineUninterceptedOrReturn(body: suspend DelimitedScope.() -> A): Any? = - try { - body.startCoroutineUninterceptedOrReturn(this, this)?.let { - if (it == COROUTINE_SUSPENDED) getResult() - else it - } - } catch (e: Throwable) { - if (e is ShortCircuit) e.recover() - else throw e - } - - override fun startCoroutineUninterceptedOrReturn(): Any? = - startCoroutineUninterceptedOrReturn(body) - - override suspend fun yield() { - suspendCoroutineUninterceptedOrReturn { - if (isDone()) getResult() - else COROUTINE_SUSPENDED - } - } - - override fun run(): Unit { - val result = try { - body.startCoroutineUninterceptedOrReturn(this, this)?.let { - if (it == COROUTINE_SUSPENDED) getResult() - else it - } - } catch (e: Throwable) { - if (e is ShortCircuit) e.recover() - else throw e - } - _decision.getAndSet(result) - } - - @PublishedApi // return the result - internal fun getResult(): Any? = - _decision.loop { decision -> - when (decision) { - UNDECIDED -> if (this._decision.compareAndSet(UNDECIDED, SUSPENDED)) return COROUTINE_SUSPENDED - else -> return decision - } - } - - companion object { - internal const val UNDECIDED = 0 - internal const val SUSPENDED = 1 - } -} - -class ListComputation( - continuation: Continuation>, - f: suspend ListComputation<*>.() -> A -) : DelimitedScopeImpl>(continuation, { - val result = f(this as ListComputation>) - listOf(result) -}) { - - var result: List = emptyList() - - suspend operator fun List.invoke(): B = - shift { cont -> - result = flatMap { - cont.invokeWith(Result.success(it)) - reset(this@ListComputation) - } - result - } - -} - -suspend fun list(f: suspend ListComputation<*>.() -> A): List = - suspendCoroutineUninterceptedOrReturn { - reset(ListComputation(it, f)) - } - - -suspend fun main() { - val result = list { - val a = listOf(1, 2, 3)() - val b = listOf(1f, 2f, 3f)() - a + b - } - println(result) -} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited2.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited2.kt deleted file mode 100644 index e84a8f09b..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/contt/Delimited2.kt +++ /dev/null @@ -1,112 +0,0 @@ -package arrow.continuations.conts - -import java.util.* -import kotlin.coroutines.Continuation - -typealias Prompt = DelimitedScope - -typealias CPS = suspend DelimitedScope.() -> R - -interface Cont { - fun resume(value: A): B -} - - -// multiprompt delimited continuations in terms of the current API -// this implementation has Felleisen classification -F+ -object ContinuationState { - - private var result: Any? = null - private var arg: Any? = null - private var body: CPS<*, *>? = null - - fun reset(prompt: Prompt, f: suspend () -> A): A { - val k = DelimitedScopeImpl(prompt) { - val a: Any? = f() - result = a - a as A - } - return runCont(k) - } - - suspend fun shift(prompt: Prompt, body: CPS): A { - this.body = body as CPS<*, *> - prompt.yield() - //Continuation.yield(prompt) - return arg as A - } - - private fun runCont(k: DelimitedContinuation<*, *>): A { - k.run() - - val frames: Stack> = Stack() - - while (!k.isDone()) { - - // IDEA: - // 1) Push a separate (one-time) prompt on `shift`. - // 2) On resume, capture the continuation on a heap allocated - // stack of `Continuation`s - // 3) Trampoline those continuations at the position of the original - // `reset`. - // - // This only works since continuations are one-shot. Otherwise the - // captured frames would contain references to the continuation and - // would be evaluated out of scope. - val bodyPrompt = object : DelimitedScopeImpl(k as Continuation, { - - /** - final var bodyPrompt = new ContinuationScope() {}; - final var bodyCont = new Continuation(bodyPrompt, () -> { - result = ((CPS) body).apply(value -> { - // yield and wait until the subcontinuation has been - // evaluated. - arg = value; - // yielding here returns control to the outer continuation - Continuation.yield(bodyPrompt); - return (A) result; - }); - body = null; - }); - */ - }) {} // TODO what prompt goes here on an empty block? - val bodyCont = DelimitedScopeImpl(bodyPrompt) { - result = DelimitedScopeImpl(this) { - // yield and wait until the subcontinuation has been - // evaluated. - arg = this@ContinuationState.body?.invoke(this) - - // yielding here returns control to the outer continuation - //Continuation.yield(bodyPrompt) - bodyPrompt.yield() - result as A - } - result - } - body = null - - bodyCont.run() // start it - - // continuation was called within body - if (!bodyCont.isDone()) { - frames.push(bodyCont) - k.run() - - // continuation was discarded or escaped the dynamic scope of - // bodyCont. - } else { - break - } - } - - while (!frames.isEmpty()) { - var frame = frames.pop() - - if (!frame.isDone()) { - frame.run() - } - } - - return result as A - } -} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt deleted file mode 100644 index 695281295..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/effectStack/program.kt +++ /dev/null @@ -1,178 +0,0 @@ -package arrow.continuations.effectStack - -import kotlinx.atomicfu.atomic -import kotlinx.atomicfu.loop -import kotlin.coroutines.Continuation -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -interface DelimitedCont { - suspend operator fun invoke(a: A): B -} - -interface Delimited { - suspend fun shift(func: suspend (DelimitedCont) -> A): B - suspend fun reset(f: suspend Delimited.() -> B): B -} - -suspend fun reset(f: suspend Delimited.() -> A): A = - DelimitedScope("Prompt", f).run() - -/** - * Idea we have two paths: - * One path is the normal coroutine. It fills an effect stack everytime its continuation is resumed with a value. - * Then if a continuation is run more than once we restart the entire computation [f] and use the effect stack for as long as possible - * When the effect stack runs out of values we resume normal coroutine behaviour. - * - * This can be used to implement nondeterminism together with any other effect and so long as the "pure" code in a function - * is fast this won't be a problem, but if it isn't this will result in terrible performance (but only if multishot is actually used) - */ -open class DelimitedScope(val dbgLabel: String, val f: suspend Delimited.() -> A) : Delimited { - - private val ret = atomic(null) - // TODO More descriptive name - private val currShiftFn = atomic<(suspend () -> A)?>(null) - // TODO more efficient data structures. O(1) append + O(1) pop would be best - internal open val stack: MutableList = mutableListOf() - // TODO for this we could use a datastructure that can O(1) append and has O(1) popLast() - private val cbs = mutableListOf>() - - override suspend fun shift(func: suspend (DelimitedCont) -> A): B { - // suspend f since we first need a result from DelimitedCont.invoke - return suspendCoroutine { k -> - // println("Suspending for shift: $label") - // println("Stack: $stack") - // create a continuation which supports invoking either the suspended f or restarting it with a sliced stack - val o = object : DelimitedCont { - // The "live" continuation for f which is currently suspended. Can only be called once - val liveContinuation = atomic?>(k) - // TODO better datastructure - // A snapshot of f's effect-stack up to this shift's function invocation - val snapshot = stack.toList() - override suspend fun invoke(a: B): A { - // println("Invoke cont with state is null: ${state.value == null} && arg $a") - val cont = liveContinuation.getAndSet(null) - // Re-execute f, but in a new scope which contains the stack slice + a and will use that to fill in the first - // calls to shift - return if (cont == null) startMultiShot(snapshot + a) - // we have a "live" continuation to resume to so we suspend the shift block and do exactly that - else suspendCoroutineUninterceptedOrReturn { - // a is the result of an effect, push it onto the stack. Note this refers to the outer stack, not - // the slice captured here, which is now immutable - stack.add(a) - // invoke needs to return A at some point so we need to append the Continuation so that it will be called when this - // scope's run method is done - cbs.add(it) - // resume f with value a - cont.resume(a) - COROUTINE_SUSPENDED - } - } - } - // the shift function is the next fn to execute - currShiftFn.value = { func(o) } - } - } - - suspend fun startMultiShot(stack: List): A = MultiShotDelimScope(stack, f).run() - - override suspend fun reset(f: suspend Delimited.() -> B): B = - DelimitedScope("inner", f).let { scope -> - scope::run.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { - TODO("Is this ever resumed?") - }).let fst@{ - if (it == COROUTINE_SUSPENDED) { - /** - * Simply suspend again. This is only ever called if we suspend to the parent scope and if we actually call - * the continuation it'll lead to an infinite loop anyway. Why? Let's have a look at this example: - * prompt fst@{ - * val a: Int = control { it(5) + it(3) } - * a + prompt snd@{ - * val i = this@fst.control { it(2) } - * i + 1 - * } - * } - * This will first execute `control { it(5) }` which then runs the inner prompt. The inner prompt yields back to - * the outer prompt because to return to i it needs the result of the outer prompt. The only sensible way of getting - * such a result is to rerun it with it's previous stack. However this means the state upon reaching - * the inner prompt again is deterministic and always the same, which is why it'll loop. - * - * TODO: Is this actually true? We can consider this@fst.control to capture up to the next control/reset. This means - * it could indeed restart the outer continuation with a stack where the top element has been replaced by whatever we invoked with - * If there is nothing on the stack or the topmost item is (reference?-)equal to our a we will infinite loop and we - * should just crash here to not leave the user wondering wtf is happening. It might also be that a user does side-effects - * outside of control which we cannot capture and thus it produces a dirty rerun which might not loop. Idk if that should - * be considered valid behaviour - */ - suspendCoroutine {} - } - else it as B - } - } - - private fun getValue(): A? = - // println("Running suspended $label") - ret.loop { - // shift function called f's continuation which now finished - if (it != null) return@getValue it - // we are not done yet - else { - val res = currShiftFn.getAndSet(null) - if (res != null) - res.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { res -> - // println("Resumption with ${(res.getOrThrow() as Sequence).toList()}") - // a shift block finished processing. This is now our intermediate return value - ret.value = res.getOrThrow() - }).let { - // the shift function did not call its continuation which means we short-circuit - if (it != COROUTINE_SUSPENDED) ret.value = it as A - // if we did suspend we have either hit a shift function from the parent scope or another shift function - // in both cases we just loop - } - // short since we run out of shift functions to call - else return@getValue null - } - } - - open suspend fun run(): A { - // println("Running $dbgLabel") - f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { - // println("Put value ${(it.getOrThrow() as Sequence).toList()}") - // f finished after being resumed. Save the value to resume the shift blocks later - ret.value = it.getOrThrow() - }).let { res -> - if (res == COROUTINE_SUSPENDED) { - // if it is null we are done calling through our shift fns and we need a value from a parent scope now - // this will block indefinitely if there is no parent scope, but a program like that should not typecheck - // at least not when using shift - getValue() ?: return@run suspendCoroutine {} - } // we finished without ever suspending. This means there is no shift block and we can short circuit run - else return@run res as A - } - - // 1..n shift blocks were called and now need to be resumed with the result. This will sort of bubble up because each - // resumed shift block can alter the returned value. - cbs.asReversed().forEach { it.resume(ret.value!!) } - // return the final value after all shift blocks finished processing the result - return ret.value!! - } -} - -class MultiShotDelimScope( - localStack: List, - f: suspend Delimited.() -> A -) : DelimitedScope("Multishot", f) { - private var depth = 0 - override val stack: MutableList = localStack.toMutableList() - override suspend fun shift(func: suspend (DelimitedCont) -> A): B = - if (stack.size > depth) stack[depth++] as B - else { - // println("EmptyStack") - depth++ - super.shift(func) - } -} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/eveff/Ctl.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/eveff/Ctl.kt deleted file mode 100644 index 6ad049d8b..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/eveff/Ctl.kt +++ /dev/null @@ -1,91 +0,0 @@ -package arrow.continuations.eveff - -// By "Effect Handlers in Haskell, Evidently" by Xie and Leijen -/** - * Implemented for clarity on how kotlin suspend and the other delimited cont impl work. - * - * mark is something we don't have in our implementations for simple reasons: - * we can refer to any scope simply by referring to its this-object and thus can jump - * straight to it when executing. - * op is the prompts handle function, what we call shift, it expects a continuation and provides an answer - * which is the final result of our prompt. This is equal to shift in function because shift implicitly - * multiprompts. - * cont is a partially applied continuation, which is to say it is our function which we execute in our context. - * We don't need this type explicitly because suspend functions are already basically this. - * - * All in all this is very close to my current encoding of delimited continuations just with explicit cps and - * thus with multishot support. Sadly this cannot easily be added to suspend. - */ - -sealed class Ctl { - fun flatMap(f: (A) -> Ctl): Ctl = when (this) { - is Pure -> f(res) - is Yield<*, *, *> -> - Yield( - mark as Marker, - op as (((Any?) -> Ctl) -> Ctl), - f.kcompose(cont as ((Any?) -> Ctl)) - ) - } - fun map(f: (A) -> B): Ctl = flatMap { just(f(it)) } - - companion object { - fun just(a: A): Ctl = Pure(a) - } -} -data class Pure(val res: A): Ctl() -data class Yield( - val mark: Marker, - val op: ((B) -> Ctl) -> Ctl, - val cont: (B) -> Ctl -): Ctl() - -tailrec fun Ctl.runCtl(): A = when (this) { - is Pure -> res - is Yield<*, *, *> -> (op(cont as (Any?) -> Nothing) as Ctl).runCtl() -} - -class Marker - -fun yield(mk: Marker, op: ((B) -> Ctl) -> Ctl): Ctl = - Yield(mk, op, { Pure(it) }) - -inline fun ((B) -> Ctl).kcompose(crossinline f: (A) -> Ctl): (A) -> Ctl = { a -> - f(a).flatMap(this) -} - -inline fun prompt(crossinline f: (Marker) -> Ctl): Ctl = - freshMarker { m: Marker -> mPrompt(m, f(m)) } - -fun freshMarker(f: (Marker) -> A): A = f(Marker()) - -fun mPrompt(m: Marker, c: Ctl): Ctl = when (c) { - is Pure -> c - is Yield<*, *, *> -> { - val nCont = { x: Any? -> - mPrompt( - m, - (c.cont as (Any?) -> Ctl).invoke(x) - ) - } - if (m === c.mark) (c.op as ((Any?) -> Ctl) -> Ctl)(nCont) - else Yield( - c.mark as Marker, - c.op as ((Any?) -> Ctl) -> Ctl, - nCont - ) - } -} - -// Eff -data class Eff(val runEff: (Context) -> Ctl) { - fun flatMap(f: (A) -> Eff): Eff = - Eff { ctx -> runEff(ctx).flatMap { a -> f(a).runEff(ctx) } } - fun map(f: (A) -> B): Eff = flatMap { just(f(it)) } - - companion object { - fun just(a: A): Eff = Eff { Ctl.just(a) } - } -} - -class Context diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt index 37139aba8..92ba92575 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt @@ -12,7 +12,7 @@ import kotlin.coroutines.suspendCoroutine /** * Implements delimited continuations with with no multi shot support (apart from shiftCPS which trivially supports it). */ -class DelimContScope(val f: suspend DelimitedScope.() -> R): RunnableDelimitedScope { +class DelimContScope(val f: suspend DelimitedScope.() -> R): DelimitedScope { private val resultVar = atomic(null) private val nextShift = atomic<(suspend () -> R)?>(null) @@ -35,8 +35,6 @@ class DelimContScope(val f: suspend DelimitedScope.() -> R): RunnableDelim override suspend fun invoke(a: A): R = DelimContScope { runFunc(a) }.invoke() } - // TODO I wrote this comment in the middle of the night, double check - // Note we don't wrap the function [func] in an explicit reset because that is already implicit in our scope override suspend fun shift(func: suspend DelimitedScope.(DelimitedContinuation) -> R): A = suspendCoroutine { continueMain -> val delCont = SingleShotCont(continueMain, shiftFnContinuations) @@ -51,7 +49,7 @@ class DelimContScope(val f: suspend DelimitedScope.() -> R): RunnableDelim override suspend fun reset(f: suspend DelimitedScope.() -> A): A = DelimContScope(f).invoke() - override fun invoke(): R { + fun invoke(): R { f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result -> resultVar.value = result.getOrThrow() }).let { diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt index 7effbb279..2c49e2884 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt @@ -13,48 +13,13 @@ interface DelimitedContinuation { // TODO This should be @RestrictSuspension but that breaks because a superclass is not considered to be correct scope // @RestrictsSuspension interface DelimitedScope { - // shift and place an implicit boundary. See shiftCPS for a more accurate definition of what this means suspend fun shift(f: suspend DelimitedScope.(DelimitedContinuation) -> R): A - // shiftCPS passes the arguments with which the continuation is invoked to the supplied continuation/function c. - // This means it is trivially multishot because c has the stack in its closure. To enforce that this is the last - // statement of a reset block we return Nothing here. + + /** + * Manually cps transformed shift. This can be used to gain multishot without hacks, but it's not the nicest for a few reasons: + * - It does not inherit the scope, this means it will be hard to effects offering non-det to offer the same scope again... + * - it is manually cps transformed which means every helper between this and invoking the continuation also needs to be transformed. + */ suspend fun shiftCPS(f: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> B): Nothing suspend fun reset(f: suspend DelimitedScope.() -> A): A } - -interface RunnableDelimitedScope : DelimitedScope { - operator fun invoke(): R -} - -/** - * Problems of nested reset: - * Scopes cannot be functor, we can not change the return type of a scope through mapping or anything else other than isomorphisms. - * This means the inner scope needs its own scope and thus its own runloop. - * A few interesting scenarios come up now: - * - The function in the inner scope may refer to *any* outer scope and may also call suspend functions on the outer scope - * This means the inner scopes runloop may stop with no further work before finishing because the work has been queued - * on a parent. There may be 1-n parents and do not know which parent got the work, which leads to n checks if this happens. - * When a parent has no further work and is not done it also may have done work for a child. - * This means it has to check it's 0..n childs to see which one needs to be resumed. - * This means every time we invoke an effect on an outer scope we need to check our 1..n parents for work and if they don't - * have work we need to check our 0..m child scopes if we have completed their work. - * One rather extreme measure that I haven't tried is collapsing all scopes to one runloop. This could be possible - * because the part that the runloop executes is a function to a local result. - * The completion still has to be handled by each scope though. - * This is the doable but annoying part of the problem btw. - * - Another fun bit on the runloop is that within shift we can reference parent scopes as well. Thus when suspending to a parent - * we need to somehow tell that scope to call us back before the lowermost child - * - This brings us to problem number 2: Multishot - * Normally when we run multishot we mark effect stack offsets when shift is called and then we can slice the stack from until offset - * when invoke for the continuation is called the second time. - * Because multishot may now rerun inner resets we now have the following changes: - * - if we call shift in our inner scope we have to store both the global offset on the stack as our current offset and - * the local offset from the start of our scope. - * - When we rerun we have to notify our parents of this as well so that they can depending on the multishot's current offset - * also access the stack rather than running the shift function. - * - When we rerun an inner scope from some point and it later calls a parent scope which is outside of our offset we can't use - * the stack. This now means that we have a local stack slice that is different from the global stack, we execute the - * outer level shift and on invoke put the arguments on the stack. This makes multishot from this point on hard: - * - - * - */ diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt index 9eed31e11..bb54a2c10 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt @@ -9,7 +9,7 @@ import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) : RunnableDelimitedScope { +open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) : DelimitedScope { private val resultVar = atomic(null) private val nextShift = atomic<(suspend () -> R)?>(null) @@ -63,7 +63,7 @@ open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) override suspend fun reset(f: suspend DelimitedScope.() -> A): A = MultiShotDelimContScope(f).invoke() - override fun invoke(): R { + fun invoke(): R { f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result -> resultVar.value = result.getOrThrow() }).let { diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt index ce9759674..c6f99648c 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt @@ -8,7 +8,11 @@ import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -open class NestedDelimContScope(val f: suspend DelimitedScope.() -> R) : RunnableDelimitedScope { +/** + * Delimited control version which allows `reset { ... reset { ... } }` to function correctly. + * [DelimContScope] fails at this if you call shift on the parent scope inside the inner reset. + */ +open class NestedDelimContScope(val f: suspend DelimitedScope.() -> R) : DelimitedScope { private val resultVar = atomic(null) internal fun getResult(): R? = resultVar.value @@ -69,7 +73,7 @@ open class NestedDelimContScope(val f: suspend DelimitedScope.() -> R) : R } } - override fun invoke(): R { + open fun invoke(): R { f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result -> resultVar.value = result.getOrThrow() }).let { diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt deleted file mode 100644 index 0f2057a99..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/EffectCombinedHandlers.kt +++ /dev/null @@ -1,78 +0,0 @@ -package arrow.continuations.generic.effect - -import arrow.Kind -import arrow.continuations.generic.DelimitedScope -import arrow.core.Either -import arrow.core.EitherPartialOf -import arrow.core.ForListK -import arrow.core.ListK -import arrow.core.extensions.either.applicative.applicative -import arrow.core.extensions.listk.applicative.applicative -import arrow.core.fix -import arrow.core.k -import arrow.typeclasses.Applicative - -fun Either.Companion.errorHandler( - ap: Applicative, - delimitedScope: DelimitedScope, A>>> -): Error = - object : Error { - override suspend fun catch(f: suspend Raise.() -> B, hdl: suspend (E) -> B): B = - TODO("This requires proper nested scope support to be sensible") - - override suspend fun raise(e: E): Nothing = - delimitedScope.shift { ap.just(left(e)) } - } - -fun nullableHandler(delimitedScope: DelimitedScope): NonDet = - object : NonDet { - override suspend fun choose(): Boolean = - delimitedScope.shift { it(true) ?: it(false) } - - override suspend fun empty(): Nothing = delimitedScope.shift { null } - } - -fun listHandler( - ap: Applicative, - delimitedScope: DelimitedScope>> -): NonDet = - object : NonDet { - override suspend fun choose(): Boolean = - delimitedScope.shift { ap.run { (it(true).map2(it(false)) { (a, b) -> (a.fix() + b.fix()).k() }) } } - - override suspend fun empty(): Nothing = delimitedScope.shift { ap.just(emptyList().k()) } - } - -interface NonDetAndError : NonDet, Error -fun listEitherHandler( - ap: Applicative, - delimitedScope: DelimitedScope, A>>>> -): NonDetAndError = - object : NonDetAndError, - Error by Either.errorHandler(ComposedApplicative(ap, ListK.applicative()), delimitedScope as DelimitedScope, Kind, A>>>), - NonDet by listHandler(ap, delimitedScope) - {} - -fun eitherListHandler( - ap: Applicative, - delimitedScope: DelimitedScope, Kind>>> -): NonDetAndError = - object : NonDetAndError, - Error by Either.errorHandler(ap, delimitedScope), - NonDet by listHandler(ComposedApplicative(ap, Either.applicative()), delimitedScope as DelimitedScope>, Kind>>) - {} - -interface Nested - -fun Kind>.nest(): Kind, A> = this as Kind, A> -fun Kind, A>.unnest(): Kind> = this as Kind> - -fun ComposedApplicative(apF: Applicative, apG: Applicative): Applicative> = object : Applicative> { - override fun Kind, A>.ap(ff: Kind, (A) -> B>): Kind, B> = - apF.run { unnest().ap(ff.unnest().map { gf -> { ga: Kind -> apG.run { ga.ap(gf) } } }).nest() } - - override fun just(a: A): Kind, A> = apF.just(apG.just(a)).nest() -} - -// ######################### -suspend fun E.myFun(): Int where E : NonDet, E : Error = if (choose()) raise("Better luck next time") else 42 diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt deleted file mode 100644 index 41dee638c..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Effects.kt +++ /dev/null @@ -1,34 +0,0 @@ -package arrow.continuations.generic.effect - -import arrow.core.Either - -interface Raise { - suspend fun raise(e: E): Nothing -} - -interface Catch { - /** - * This is not easy to implement. Best attempt is probably going: - * reset> { - * val raiseEff = eitherRaise(this@reset) // define handler for raise here - * f(raiseEff) - * }.fold({ e -> hdl() }, ::identity) - * This runs into another problem though: - * reset as of now is not supporting nested scopes in terms of its runloop or multishot. - */ - suspend fun catch(f: suspend Raise.() -> A, hdl: suspend (E) -> A): A -} - -interface Error : Raise, Catch - -interface Empty { - suspend fun empty(): Nothing -} - -interface Choose { - suspend fun choose(): Boolean -} - -suspend inline fun Choose.choice(f: () -> A, g: () -> A): A = if (choose()) f() else g() - -interface NonDet : Choose, Empty diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt deleted file mode 100644 index 4778396c1..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/effect/Parser.kt +++ /dev/null @@ -1,190 +0,0 @@ -package arrow.continuations.generic.effect - -import arrow.continuations.generic.DelimitedScope -import arrow.continuations.generic.MultiShotDelimContScope -import arrow.core.Either -import arrow.core.Nel -import arrow.core.NonEmptyList -import arrow.core.Tuple2 -import arrow.core.identity -import arrow.core.left -import arrow.core.toT - -typealias Parser = suspend ParserCtx.() -> A - -interface ParserCtx : NonDet, Error { - suspend fun take1(): Char - suspend fun take(n: Int): String - suspend fun takeWhile(matcher: (Char) -> Boolean): String - suspend fun takeAtLeastOneWhile(matcher: (Char) -> Boolean): String - - fun getOffset(): Int - - suspend fun token(expected: Set = emptySet(), matcher: (Char) -> B?): B - suspend fun string(str: String): String - suspend fun eof(): Unit - suspend fun lookAhead(p: Parser): A - suspend fun notFollowedBy(p: Parser): Unit - suspend fun optional(p: Parser): A? - - suspend fun satisfy(expected: Set = emptySet(), f: (Char) -> Boolean): Char = token(expected) { it.takeIf(f) } - suspend fun char(c: Char): Char = satisfy(setOf(ErrorItem.fromChar(c))) { it == c } - - fun attempt(p: Parser): Either -} - -data class ParseError( - val offset: Int, - val unexpectedToken: ErrorItem? = null, - val expectedTokens: Set = emptySet() -) { - operator fun plus(other: ParseError): ParseError = - when { - offset < other.offset -> other - offset > other.offset -> this - else -> ParseError( - offset, - unexpectedToken ?: other.unexpectedToken, - expectedTokens.union(other.expectedTokens) - ) - } - - // This could be a lot better, looking at megaparsec here! - override fun toString(): String = - "ParseError at offset $offset. Got \"$unexpectedToken\" but expected: \"${showExpected()}\"" - - private fun showExpected(): String = - expectedTokens.fold("") { acc, errorItem -> "$acc,$errorItem" }.drop(1) -} - -sealed class ErrorItem { - companion object { - fun unsafeFromString(str: String): ErrorItem = - Tokens(Nel.fromListUnsafe(str.toList())) - - fun fromChar(c: Char): ErrorItem = - Tokens(Nel.of(c)) - } - - override fun toString(): String = when (this) { - is Tokens -> ts.all.joinToString("") - is EndOfInput -> "End of input" - } -} - -class Tokens(val ts: NonEmptyList) : ErrorItem() -object EndOfInput : ErrorItem() - -data class ParseResult(val remaining: String, val result: Either) - -//impl -class ParserCtxImpl(val input: String, val scope: DelimitedScope>) : ParserCtx, DelimitedScope> by scope { - private var offset = 0 - override fun getOffset(): Int = offset - - private fun take1_(): Char? = input.takeIf { it.length > offset }?.get(offset) - private fun take_(n: Int): String? = input.takeIf { it.length >= offset + n }?.substring(offset, offset + n) - fun takeRemaining(): String = input.takeIf { it.length > offset }?.substring(offset) ?: "" - - override suspend fun take1(): Char = take1_()?.also { offset++ } ?: raise(ParseError(offset, EndOfInput)) - override suspend fun take(n: Int): String = take_(n)?.also { offset += n } ?: raise(ParseError(offset, EndOfInput)) - override suspend fun takeWhile(matcher: (Char) -> Boolean): String = - input.takeWhile(matcher).also { offset += it.length } - - override suspend fun takeAtLeastOneWhile(matcher: (Char) -> Boolean): String = - input.takeWhile(matcher).takeIf { it.isNotEmpty() } - ?: raise( - ParseError( - offset, - input - .takeIf { it.length > offset } - ?.get(offset)?.let { ErrorItem.fromChar(it) } ?: EndOfInput - ) - ) - - override suspend fun string(str: String): String = - (take_(str.length) ?: raise(ParseError(offset, EndOfInput, setOf(ErrorItem.unsafeFromString(str))))) - .let { - it.takeIf { it == str } - ?.also { offset += str.length } - ?: raise(ParseError(offset, ErrorItem.unsafeFromString(it), setOf(ErrorItem.unsafeFromString(str)))) - } - - override suspend fun token(expected: Set, matcher: (Char) -> B?): B = - (take1_() ?: raise(ParseError(offset, EndOfInput, expected))) - .let { it.let(matcher)?.also { offset++ } ?: raise(ParseError(offset, ErrorItem.fromChar(it), expected)) } - - override suspend fun eof() = take1_().let { c -> - if (c != null) raise(ParseError(offset, ErrorItem.fromChar(c), setOf(EndOfInput))) - } - - override suspend fun lookAhead(p: Parser): A = - p.runParser(takeRemaining()).result.fold( - { e -> raise(e) }, - ::identity - ) - - override suspend fun notFollowedBy(p: Parser) = - p.runParser(takeRemaining()).result.fold( - {}, - { - raise( - ParseError(offset, input - .takeIf { it.length > offset } - ?.get(offset) - ?.let { c -> ErrorItem.fromChar(c) } - ?: EndOfInput) - ) - } - ) - - override suspend fun optional(p: Parser): A? = - p.runParser(takeRemaining()).result.fold({ null }, ::identity) - - override suspend fun raise(e: ParseError): Nothing = - shift { ParseResult(takeRemaining(), e.left()) } - - /* - This won't work without nesting support because f may call into parent scopes. - Ideally we'd have f: ParserCtx.() -> A here to avoid this by giving an explicit context - override suspend fun catch(f: suspend () -> A, hdl: suspend (ParseError) -> A): A { - val p: Parser> = { f() toT getOffset() } - return p.runParser(takeRemaining()).result.fold({ e -> - hdl(e) - }, { (result, off) -> - offset = off - result - }) - } - */ - override suspend fun catch(f: suspend Raise.() -> A, hdl: suspend (ParseError) -> A): A = TODO("Not working. Use attempt for now. See note in code on commented part above") - - override fun attempt(p: Parser): Either = - p.runParser(takeRemaining()).result - - override suspend fun choose(): Boolean { - return shift { k -> - val res = k(true) - when (val lResult = res.result) { - is Either.Left -> { - val res2 = k(false) - val nRem = if (res.remaining.length > res2.remaining.length) res2.remaining else res.remaining - when (val rResult = res2.result) { - is Either.Left -> ParseResult(nRem, Either.Left(lResult.a + rResult.a)) - is Either.Right -> res2 - } - } - is Either.Right -> res - } - } - } - - override suspend fun empty(): Nothing = raise(ParseError(offset, null, emptySet())) -} - -fun Parser.runParser(str: String): ParseResult = - MultiShotDelimContScope.reset { - val ctx = ParserCtxImpl(str, this) - val res = this@runParser.invoke(ctx) - ParseResult(ctx.takeRemaining(), Either.right(res)) - } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt deleted file mode 100644 index a759c22ad..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/Coroutine.kt +++ /dev/null @@ -1,160 +0,0 @@ -package arrow.continuations.reflect - -import arrow.core.ShortCircuit -import kotlinx.atomicfu.atomic -import kotlinx.atomicfu.loop -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.RestrictsSuspension -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.coroutines.resume - -interface Prompt { - suspend fun suspend(value: A): B -} - -internal const val UNDECIDED = 0 -internal const val SUSPENDED = 1 - -class Coroutine(prog: suspend (Prompt) -> C) { - - private val _decision = atomic(UNDECIDED) - - fun isDone() = continuation.isDone() - - fun value(): A { - assert(!isDone()); return receive() - } - - fun result(): C { - assert(isDone()); return receive() - } - - fun yield(v: B) { - assert(!isDone()) - send(v) - continuation.run() - } - - private var channel: Any? = null - private fun send(v: Any?) { - channel = v - } - - private fun receive(): A { - val v = channel - return v as A - } - - open inner class InnerPrompt : Prompt /*ContinuationScope("cats-reflect")*/ { - override suspend fun suspend(value: A): B { - send(value) - return suspendCoroutineUninterceptedOrReturn { cont -> - println("Put value in channel: $value in $channel") - val res = receive() - if (isDone()) cont.resumeWith(Result.success(res)) - COROUTINE_SUSPENDED - } - } - } - - val prompt = InnerPrompt() - - @RestrictsSuspension - inner class Continuation( - val f: suspend () -> Unit - ) : kotlin.coroutines.Continuation { - - override val context: CoroutineContext = EmptyCoroutineContext - - var started = false - - fun run(): Unit { - continuation.startCoroutineUninterceptedOrReturn { - if (!started) { - f() - started = true - } - suspendCoroutineUninterceptedOrReturn { - while (!isDone()) { - } - getResult() - } - } - } - - override fun resumeWith(result: Result) { - _decision.loop { decision -> - when (decision) { - UNDECIDED -> { - val r: C? = when { - result.isFailure -> { - val e = result.exceptionOrNull() - if (e is ShortCircuit) throw e else null - } - result.isSuccess -> result.getOrNull() - else -> TODO("Bug?") - } - - when { - r == null -> { - println("resumeWith: result = $result") - throw result.exceptionOrNull()!! -// parent.resumeWithException(result.exceptionOrNull()!!) -// return - } - _decision.compareAndSet(UNDECIDED, r) -> return - else -> Unit // loop again - } - } - else -> { // If not `UNDECIDED` then we need to pass result to `parent` - val res: Result = result.fold({ Result.success(it) }, { t -> - if (t is ShortCircuit) throw t // Result.success(t.recover()) - else Result.failure(t) - }) - send(res.getOrThrow()) - // parent.resumeWith(res) - return - } - } - } - } - - @PublishedApi // return the result - internal fun getResult(): Any? = - _decision.loop { decision -> - when (decision) { - UNDECIDED -> if (_decision.compareAndSet(UNDECIDED, SUSPENDED)) return COROUTINE_SUSPENDED - else -> return decision - } - } - - fun startCoroutineUninterceptedOrReturn(f: suspend () -> C): Any? = - try { - f.startCoroutineUninterceptedOrReturn(this)?.let { - if (it == COROUTINE_SUSPENDED) getResult() - else it - } - } catch (e: Throwable) { - if (e is ShortCircuit) throw e // e.recover() - else throw e - } - - fun isDone(): Boolean = - _decision.loop { - return when (it) { - UNDECIDED -> false - SUSPENDED -> false - else -> true - } - } - } - - val continuation = Continuation { send(prog(prompt)) } - - init { - continuation.run() - } -} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt deleted file mode 100644 index 0d2d7da16..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/program.kt +++ /dev/null @@ -1,31 +0,0 @@ -package arrow.continuations.reflect - -import arrow.core.ForListK -import arrow.core.ListK -import arrow.core.extensions.listk.monad.monad -import arrow.core.fix -import arrow.core.k - -fun main() { -// val result = list { -// val a: Int = listOf(1, 2, 3).k()() -// val b: String = listOf("a", "b", "c").k()() -// "$a$b" -// } -// println(result) - list { - val a: Int = listOf(1, 2, 3).k()() - val b: Int = listOf(1, 3, 4).k()() - println("I came here $a$b") - a + b - }.let(::println) -} - -inline fun list(crossinline program: suspend Reflect.() -> Int): List { - val a = reify(ListK.monad()) { - println("Starting with: $it") - program(it) - } - - return a.fix() -} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt deleted file mode 100644 index 4e3c01bfc..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect/reify.kt +++ /dev/null @@ -1,93 +0,0 @@ -package arrow.continuations.reflect - -import arrow.Kind -import arrow.core.Either -import arrow.core.Left -import arrow.core.Right -import arrow.core.identity -import arrow.typeclasses.Monad - -typealias In = suspend (Reflect) -> A - -sealed class Reflect { - abstract suspend operator fun Kind.invoke(): A -} - -class ReflectM(val prompt: Prompt, *>) : Reflect() { - override suspend fun Kind.invoke(): A { - println("invokeSuspend: $prompt") - val result = prompt.suspend(this) - return result as A - } - // since we know the receiver of this suspend is the - // call to flatMap, the casts are safe -} - -/** - * for partially applying type arguments and better type inference - * - * reify [F] in { BLOCK } - * - * @usecase def reify[M[_]: Monad] in[R](prog: => R): M[R] - */ -fun reify(): ReifyBuilder = ReifyBuilder() - -fun reify(MM: Monad, prog: In): Kind = - reifyImpl(MM) { prog(it) } - -class ReifyBuilder { - operator fun invoke( - MM: Monad, - prog: In - ): Kind = reifyImpl(MM) { prog(it) } -} - -// this method is private since overloading and partially applying -// type parameters conflicts and results in non-helpful error messages. -// -// tradeoff of using `reify[M] in BLOCK` syntax over this function: -// + type inference on R -// - no type inference on M -// The latter might be a good thing since we want to make explicit -// which monad we are reifying. -private fun reifyImpl( - MM: Monad, - prog: In -): Kind { - - var currentPrompt: Prompt, Any?>? = null - - // The coroutine keeps sending monadic values until it completes - // with a monadic value - val coroutine = Coroutine, Any?, A> { prompt -> - currentPrompt = prompt - // capability to reflect M - val reflect = ReflectM(prompt) - prog(reflect) - } - - fun step(x: A): Either, A> { - println("Step : $x") - return if (coroutine.isDone()) - Right(coroutine.result()) - else { - coroutine.continuation.resumeWith(Result.success(x)) - Left(coroutine.value()) - } - } - - fun run(): Kind = - MM.run { - if (coroutine.isDone()) - coroutine.result().just() - else { - MM.tailRecM(coroutine.value()) { - it.map(::step) - }.flatMap { just(it) } - // .flatten>() - } - } - - - return run() -} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt deleted file mode 100644 index 02d5bdce5..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/Coroutine.kt +++ /dev/null @@ -1,75 +0,0 @@ -package arrow.continuations.reflect2 - -import arrow.fx.coroutines.UnsafePromise -import kotlinx.atomicfu.atomic -import kotlinx.atomicfu.loop -import kotlin.coroutines.* -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn - -interface Prompt { - suspend fun suspend(value: S): R -} - -class Coroutine(prog: suspend (Prompt) -> T) { - - fun isDone() = co.isDone() - - fun value(): S { - assert(!isDone()); - return receive() - } - - fun result(): T { - assert(isDone()) - return receive() - } - - fun resume(v: R): Unit { - assert(!isDone()) - send(v) - co.run() - } - - private var channel: Any? = null - private fun send(v: Any?) { - channel = v - } - - private fun receive(): A { - val v = channel - return v as A - } - - val yielder = UnsafePromise() - val prompt = object : Prompt { - override suspend fun suspend(value: S): R { - send(value) - yielder.join() // Continuation yield prompt - return receive() - } - } - - private val co = Continuation(prompt, yielder) { send(prog(prompt)) } - - init { - co.run() - } -} - -class Continuation( - val prompt: Prompt<*, *>, - val yielder: UnsafePromise, - val f: suspend () -> Unit -) { - - fun isDone(): Boolean = - yielder.isNotEmpty() - - // The run method returns true when the continuation terminates, and false if it suspends. - fun run(): Boolean { - val a = f.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext, yielder::complete)) - return a != COROUTINE_SUSPENDED - } -} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/UnsafePromise.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/UnsafePromise.kt deleted file mode 100644 index e770ebef4..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/UnsafePromise.kt +++ /dev/null @@ -1,69 +0,0 @@ -package arrow.fx.coroutines - -import kotlinx.atomicfu.AtomicRef -import kotlinx.atomicfu.atomic -import kotlin.coroutines.suspendCoroutine - -/** - * An eager Promise implementation to bridge results across processes internally. - * @see ForkAndForget - */ -class UnsafePromise { - - private sealed class State { - object Empty : State() - data class Waiting(val joiners: List<(Result) -> Unit>) : State() - - @Suppress("RESULT_CLASS_IN_RETURN_TYPE") - data class Full(val a: Result) : State() - } - - private val state: AtomicRef> = atomic(State.Empty) - - fun isNotEmpty(): Boolean = - when (state.value) { - is State.Full -> true - else -> false - } - - @Suppress("RESULT_CLASS_IN_RETURN_TYPE") - fun tryGet(): Result? = - when (val curr = state.value) { - is State.Full -> curr.a - else -> null - } - - fun get(cb: (Result) -> Unit) { - tailrec fun go(): Unit = when (val oldState = state.value) { - State.Empty -> if (state.compareAndSet(oldState, State.Waiting(listOf(cb)))) Unit else go() - is State.Waiting -> if (state.compareAndSet(oldState, State.Waiting(oldState.joiners + cb))) Unit else go() - is State.Full -> cb(oldState.a) - } - - go() - } - - suspend fun join(): A = - suspendCoroutine { cb -> - get(cb::resumeWith) - } - - fun complete(value: Result) { - tailrec fun go(): Unit = when (val oldState = state.value) { - State.Empty -> if (state.compareAndSet(oldState, State.Full(value))) Unit else go() - is State.Waiting -> { - if (state.compareAndSet(oldState, State.Full(value))) oldState.joiners.forEach { it(value) } - else go() - } - is State.Full -> throw RuntimeException("Boom!") - } - - go() - } - - fun remove(cb: (Result) -> Unit) = when (val oldState = state.value) { - State.Empty -> Unit - is State.Waiting -> state.value = State.Waiting(oldState.joiners - cb) - is State.Full -> Unit - } -} diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/program.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/program.kt deleted file mode 100644 index 18313dd5f..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/program.kt +++ /dev/null @@ -1,22 +0,0 @@ -package arrow.continuations.reflect2 - -import arrow.core.ForListK -import arrow.core.ListK -import arrow.core.extensions.listk.monad.monad -import arrow.core.fix -import arrow.core.k - -fun main() { - list { - val a: Int = listOf(1, 2, 3).invoke() -// val b: Int = listOf(1, 3, 4).invoke() - println("I came here $a") - a - }.let(::println) -} - -inline fun list(crossinline program: suspend Reflect/**/.() -> Int): List = - reify { - println("Starting with: $it") - listOf(program(it)) - } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/reify.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/reify.kt deleted file mode 100644 index 81b003ffc..000000000 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/reflect2/reify.kt +++ /dev/null @@ -1,90 +0,0 @@ -package arrow.continuations.reflect2 - -import arrow.Kind -import arrow.core.* -import arrow.core.extensions.either.applicative.applicative -import arrow.core.extensions.either.applicative.map -import arrow.core.extensions.either.apply.apEval -import arrow.core.extensions.list.traverse.traverse -import arrow.core.extensions.listk.monad.flatMap -import arrow.typeclasses.Applicative - -sealed class Reflect { - abstract suspend operator fun List.invoke(): Int -} - -class ReflectM(val prompt: Prompt, Any?>) : Reflect() { - override suspend fun List.invoke(): Int { -// println("invokeSuspend: $prompt") - val result = prompt.suspend(this) - println("Result: $result") - return result as Int - } - // since we know the receiver of this suspend is the - // call to flatMap, the casts are safe -} - -@PublishedApi -internal fun reify(prog: suspend (Reflect) -> List): List { - -// var currentPrompt: Prompt? = null - - // The coroutine keeps sending monadic values until it completes - // with a monadic value - val coroutine = Coroutine, Any?, List> { prompt -> -// currentPrompt = prompt - // capability to reflect M - val reflect = ReflectM(prompt) - prog(reflect) - } - - fun step(x: Int): Either, List> { - println("Step : $x") - coroutine.resume(x) - return if (coroutine.isDone()) Right(coroutine.result()) - else Left(coroutine.value()) - } - - fun run(): List = - if (coroutine.isDone()) coroutine.result() - else tailRecM(coroutine.value()) { values -> - val r = values.map(::step) - println("run#tailRecM: $r") - r - }.flatten() - - return run() -} - -@Suppress("UNCHECKED_CAST") -private tailrec fun go( - buf: ArrayList, - f: (A) -> List>, - v: List> -) { - if (v.isNotEmpty()) { - val head: Either = v.first() - println("head: $head") - when (head) { - is Either.Right -> { - println("Right?") - buf += head.b - go(buf, f, v.drop(1).k()) - } - is Either.Left -> { - val head: A = head.a - val newHead = f(head) - println("Left: $head, newHead: $newHead") - val newRes = (newHead + v.drop(1)) - println("Head: $head, newHead: $newHead, newRes: $newRes") - go(buf, f, newRes) - } - } - } -} - -fun tailRecM(a: A, f: (A) -> List>): List { - val buf = ArrayList() - go(buf, f, f(a)) - return ListK(buf) -} diff --git a/arrow-continuations/src/test/kotlin/effectStack/EffectStackTest.kt b/arrow-continuations/src/test/kotlin/effectStack/EffectStackTest.kt deleted file mode 100644 index d4a5df2a3..000000000 --- a/arrow-continuations/src/test/kotlin/effectStack/EffectStackTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -package effectStack - -import arrow.core.Either -import arrow.core.identity -import arrow.core.test.generators.either -import arrow.core.test.generators.nonEmptyList -import arrow.core.test.generators.throwable -import io.kotlintest.fail -import io.kotlintest.properties.Gen -import io.kotlintest.properties.PropertyContext -import io.kotlintest.properties.assertAll -import io.kotlintest.properties.forAll -import io.kotlintest.shouldBe -import io.kotlintest.shouldThrow -import io.kotlintest.specs.StringSpec -import kotlinx.coroutines.runBlocking - -class EffectStackTest : StringSpec() { - init { - fun forAll2(gena: Gen, fn: suspend PropertyContext.(a: A) -> Boolean) { - assertAll(gena) { a -> - unsafeRunSync { fn(a) shouldBe true } - } - } - - fun forAll2(gena: Gen, genb: Gen, fn: suspend PropertyContext.(a: A, b: B) -> Boolean) { - assertAll(gena, genb) { a, b -> - runBlocking { fn(a, b) shouldBe true } - } - } - - fun forAll2(gena: Gen, genb: Gen, genc: Gen, fn: suspend PropertyContext.(a: A, b: B, c: C) -> Boolean) { - assertAll(gena, genb, genc) { a, b, c -> - unsafeRunSync { fn(a, b, c) shouldBe true } - } - } - - - "monadic can bind values" { - forAll2(Gen.either(Gen.string(), Gen.int())) { value -> - effectStack.either { - value.suspend().invoke() - } == value - } - } - - "monadic rethrows exceptions" { - forAll2(Gen.int(), Gen.throwable()) { value, e -> - shouldThrow { - val r = effectStack.either { - Either.Right(value).suspend().invoke() - e.suspend() - } - - fail("$e expected, but found $r") - } == e - } - } - - "error can bind values" { - forAll2(Gen.either(Gen.string(), Gen.int())) { value -> - error { - value.fold( - { s -> raise(s.suspend()) }, - ::identity - ) - } == value - } - } - - "error can rethrow exceptions" { - forAll2(Gen.throwable()) { e -> - shouldThrow { - val r = error { - e.suspend() - } - fail("$e expected, but found $r") - } == e - } - } - - "list" { - forAll2(Gen.list(Gen.int()), Gen.list(Gen.int())) { iis, sss -> - list { - val a = iis.suspend().invoke() - val b = sss.suspend().invoke() - "$a$b ".suspend() - } == iis.flatMap { a -> sss.map { b -> "$a$b " } } - } - } - - "list can rethrow exceptions" { - forAll2(Gen.nonEmptyList(Gen.int()), Gen.nonEmptyList(Gen.int()), Gen.throwable()) { iis, sss, e -> - shouldThrow { - val r = list { - val a = iis.all.suspend().invoke() - val b = sss.all.suspend().invoke() - e.suspend() - "$a$b " - } - - fail("$e expected, but found $r") - } == e - } - } - } -} diff --git a/arrow-continuations/src/test/kotlin/effectStack/Run.kt b/arrow-continuations/src/test/kotlin/effectStack/Run.kt deleted file mode 100644 index 487a3bedf..000000000 --- a/arrow-continuations/src/test/kotlin/effectStack/Run.kt +++ /dev/null @@ -1,178 +0,0 @@ -package effectStack - -import arrow.Kind -import arrow.continuations.effectStack.Delimited -import arrow.continuations.effectStack.reset -import arrow.core.Either -import arrow.core.EitherPartialOf -import arrow.core.Left -import arrow.core.Right -import arrow.core.Some -import arrow.core.Tuple4 -import arrow.core.fix -import arrow.core.flatMap -import arrow.core.some -import arrow.core.test.UnitSpec -import effectStack.interleave.Interleave -import effectStack.interleave.lists -import io.kotlintest.shouldBe -import kotlin.random.Random - -interface Monadic { - suspend operator fun Kind.invoke(): A -} - -suspend fun either(f: suspend Monadic>.() -> A): Kind, A> = reset { - val m = object : Monadic> { - override suspend fun Kind, A>.invoke(): A = shift { k -> fix().flatMap { k(it).fix() } } - } - - Either.Right(f(m)) -} - -interface Error { - suspend fun raise(e: E): A - suspend fun catch(handle: suspend Error.(E) -> A, f: suspend Error.() -> A): A -} - -suspend fun error(f: suspend Error.() -> A): Either = reset { - val p = object : Error { - override suspend fun raise(e: E): A = shift { Left(e) } - override suspend fun catch(handle: suspend Error.(E) -> B, f: suspend Error.() -> B): B = - shift { k -> - error { f() }.fold({ e -> error { handle(e) }.flatMap { k(it) } }, { b -> k(b) }) - } - } - Right(f(p)) -} - -interface NonDet { - suspend fun effect(f: suspend () -> B): B - suspend fun empty(): A - suspend fun choose(): Boolean -} - -interface ListComputation { - suspend operator fun List.invoke(): C -} - -suspend inline fun list(crossinline f: suspend ListComputation.() -> A): List = - reset { - val p = object : ListComputation { - override suspend fun List.invoke(): C = - shift { cb -> - flatMap { - cb(it) - } - } - } - - listOf(f(p)) - } - - -suspend inline fun nonDet(crossinline f: suspend NonDet.() -> A): Sequence = reset { - val p = object : NonDet { - override suspend fun effect(f: suspend () -> B): B = shift { it(f()) } - override suspend fun choose(): Boolean = shift { k -> k(true) + k(false) } - override suspend fun empty(): A = shift { emptySequence() } - } - sequenceOf(f(p)) -} - -// I couldn't use a main method because intellij kept complaining about not finding the main file unless I rebuild the entire repo... -// Running tests works fine though, hence I moved it here. -class Test : UnitSpec() { - init { - "yield building a list" { - println("PROGRAM: Run yield building a list") - reset> { - suspend fun Delimited>.yield(a: A): Unit = shift { listOf(a) + it(Unit) } - - yield(1) - yield(2) - yield(10) - - emptyList() - }.also { println("PROGRAM: Result $it") } - } - "test" { - println("PROGRAM: Run Test") - val res = 10 + reset { - 2 + shift { it(it(3)) + 100 } - } - println("PROGRAM: Result $res") - } - "multi" { - println("PROGRAM: multi") - reset> fst@{ - val ctx = this - val i: Int = shift { it(2) } - Right(i * 2 + reset snd@{ - val k: Int = shift { it(1) + it(2) } - val j: Int = if (i == 5) ctx.shift { Left("Not today") } - else shift { it(4) } - j + k - }) - }.also { println("PROGRAM: Result $it") } - } - "list" { - list { - val a = listOf(1, 2, 3)() - val b = listOf("a", "b", "c")() - "$a$b " - } shouldBe listOf("1a ", "1b ", "1c ", "2a ", "2b ", "2c ", "3a ", "3b ", "3c ") - } - "nonDet" { - println("PROGRAM: Run nonDet") - nonDet { - var sum = 0 - val b = choose() - effect { println("PROGRAM: Here $b") } - // stacksafe? - for (i in 0..1000) { - sum += effect { Random.nextInt(100) } - } - val i = effect { Random.nextInt() } - effect { println("PROGRAM: Rand $i") } - val b2 = if (b.not()) choose() - else empty() - effect { println("PROGRAM: Here2 $b2") } - Tuple4(i, b, b2, sum) - }.also { println("PROGRAM: Result ${it.toList()}") } - } - - "error" { - println("PROGRAM: Run error") - error { - catch({ e -> - println("PROGRAM: Got error: $e") - raise(e) - }) { - val rand = 10 - if (rand.rem(2) == 0) raise("No equal numbers") - else rand - } - }.also { println("PROGRAM: Result $it") } - } - - "either" { - println("PROGRAM: Run either") - either { - val a = Right(5)() - if (a > 10) Left("Larger than 10")() - else a - }.also { println("PROGRAM: Result $it") } - } - "lists interleave" { - lists { program() } shouldBe listOf(2, 4, 6) - } - } -} - -suspend inline fun Interleave<*>.program(): Int { - val a : Int = listOf(1, 2, 3)() - val b: Int? = a.some()() - val c: Int = b() - return c + c -} diff --git a/arrow-continuations/src/test/kotlin/effectStack/interleave/Interleave.kt b/arrow-continuations/src/test/kotlin/effectStack/interleave/Interleave.kt deleted file mode 100644 index 13f6964fa..000000000 --- a/arrow-continuations/src/test/kotlin/effectStack/interleave/Interleave.kt +++ /dev/null @@ -1,41 +0,0 @@ -package effectStack.interleave - -import arrow.continuations.effectStack.Delimited -import arrow.continuations.effectStack.DelimitedCont -import arrow.continuations.effectStack.DelimitedScope -import arrow.continuations.effectStack.MultiShotDelimScope -import arrow.continuations.effectStack.reset -import arrow.core.Option -import arrow.core.identity -import effectStack.list - -suspend inline fun lists(crossinline f: suspend Interleave<*>.() -> A): List = - reset { - listOf(f(object : Interleave> , Delimited> by this { - override val f: suspend Interleave<*>.() -> List = { listOf(f(this)) } - override fun shortCircuit(): List = emptyList() - override fun just(b: Any?): List = listOf(b) as List - override suspend fun List.invoke(): C = - shift { cb -> - flatMap { cb(it) } - } - })) - } - -interface Interleave : Delimited { - val f: suspend Interleave<*>.() -> A - fun shortCircuit(): A - fun just(b: Any?): A - suspend operator fun Option.invoke(): C = - fold({ shift { shortCircuit() } }, ::identity) - - suspend operator fun C?.invoke(): C = - this ?: shift { shortCircuit() } - - suspend operator fun List.invoke(): C - -} - - - - diff --git a/arrow-continuations/src/test/kotlin/effectStack/predef.kt b/arrow-continuations/src/test/kotlin/effectStack/predef.kt deleted file mode 100644 index baf67d6f7..000000000 --- a/arrow-continuations/src/test/kotlin/effectStack/predef.kt +++ /dev/null @@ -1,129 +0,0 @@ -package effectStack - -import arrow.core.Either -import io.kotlintest.properties.Gen -import io.kotlintest.properties.shrinking.Shrinker -import kotlinx.coroutines.Dispatchers -import java.util.concurrent.locks.AbstractQueuedSynchronizer -import kotlin.coroutines.Continuation -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED -import kotlin.coroutines.intrinsics.intercepted -import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn -import kotlin.coroutines.startCoroutine -import kotlin.random.Random - -internal fun unsafeRunSync(f: suspend () -> A): A { - val latch = OneShotLatch() - var ref: Either? = null - f.startCoroutine(Continuation(EmptyCoroutineContext) { a -> - ref = a.fold({ aa -> Either.Right(aa) }, { t -> Either.Left(t) }) - latch.releaseShared(1) - }) - - latch.acquireSharedInterruptibly(1) - - return when (val either = ref) { - is Either.Left -> throw either.a - is Either.Right -> either.b - null -> throw RuntimeException("Suspend execution should yield a valid result") - } -} - - -private class OneShotLatch : AbstractQueuedSynchronizer() { - override fun tryAcquireShared(ignored: Int): Int = - if (state != 0) { - 1 - } else { - -1 - } - - override fun tryReleaseShared(ignore: Int): Boolean { - state = 1 - return true - } -} - -internal suspend fun Throwable.suspend(): Nothing = - suspendCoroutineUninterceptedOrReturn { cont -> - Gen.int().orNull() - suspend { throw this }.startCoroutine(Continuation(EmptyCoroutineContext) { - cont.intercepted().resumeWith(it) - }) - - COROUTINE_SUSPENDED - } - -internal suspend fun A.suspend(): A = - suspendCoroutineUninterceptedOrReturn { cont -> - suspend { this }.startCoroutine(Continuation(EmptyCoroutineContext) { - cont.intercepted().resumeWith(it) - }) - - COROUTINE_SUSPENDED - } - -typealias Suspended = suspend () -> A - -@JvmName("suspendedErrors") -fun Gen.suspended(): Gen> = - suspended { e -> suspend { throw e } } as Gen> - -fun Gen.suspended(): Gen> = - suspended { a -> suspend { a } } - -private fun Gen.suspended(liftK: (A) -> (suspend () -> A)): Gen> { - val outer = this - return object : Gen> { - override fun constants(): Iterable> = - outer.constants().flatMap { value -> - exhaustiveOptions.map { (a, b, c) -> liftK(value).asSuspended(a, b, c) } - } - - override fun random(): Sequence> = - outer.random().map { - liftK(it).asSuspended(Random.nextBoolean(), Random.nextBoolean(), Random.nextBoolean()) - } - - override fun shrinker(): Shrinker>? { - val s = outer.shrinker() - return if (s == null) null else object : Shrinker> { - override fun shrink(failure: Suspended): List> { - val failed = unsafeRunSync { failure.invoke() } - return s.shrink(failed).map { - liftK(it).asSuspended(Random.nextBoolean(), Random.nextBoolean(), Random.nextBoolean()) - } - } - } - } - } -} - -private val exhaustiveOptions = - listOf( - Triple(false, false, false), - Triple(false, false, true), - Triple(false, true, false), - Triple(false, true, true), - Triple(true, false, false), - Triple(true, false, true), - Triple(true, true, false), - Triple(true, true, true) - ) - -internal fun (suspend () -> A).asSuspended( - suspends: Boolean, - emptyOrNot: Boolean, - intercepts: Boolean -): Suspended = suspend { - if (!suspends) this.invoke() - else suspendCoroutineUninterceptedOrReturn { cont -> - val ctx = if (emptyOrNot) EmptyCoroutineContext else Dispatchers.Default - suspend { this.invoke() }.startCoroutine(Continuation(ctx) { - if (intercepts) cont.resumeWith(it) else cont.intercepted().resumeWith(it) - }) - - COROUTINE_SUSPENDED - } -} diff --git a/arrow-continuations/src/test/kotlin/eveff/CtlTest.kt b/arrow-continuations/src/test/kotlin/eveff/CtlTest.kt deleted file mode 100644 index 428887e5c..000000000 --- a/arrow-continuations/src/test/kotlin/eveff/CtlTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package eveff - -import arrow.continuations.eveff.Pure -import arrow.continuations.eveff.prompt -import arrow.continuations.eveff.runCtl -import arrow.continuations.eveff.yield -import io.kotlintest.specs.StringSpec - -class CtlTest : StringSpec({ - "test" { - prompt { - yield(it) { k -> k(10).flatMap(k) } - .map { i -> i * 2 } - }.runCtl().also { println(it) } - } -}) diff --git a/arrow-continuations/src/test/kotlin/generic/EffectCombinedHandlersTest.kt b/arrow-continuations/src/test/kotlin/generic/EffectCombinedHandlersTest.kt deleted file mode 100644 index 447ee4076..000000000 --- a/arrow-continuations/src/test/kotlin/generic/EffectCombinedHandlersTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package generic - -import arrow.Kind -import arrow.continuations.generic.DelimitedScope -import arrow.continuations.generic.MultiShotDelimContScope -import arrow.continuations.generic.effect.eitherListHandler -import arrow.continuations.generic.effect.listEitherHandler -import arrow.continuations.generic.effect.myFun -import arrow.core.Either -import arrow.core.EitherPartialOf -import arrow.core.ForId -import arrow.core.ForListK -import arrow.core.Id -import arrow.core.ListK -import arrow.core.extensions.id.applicative.applicative -import arrow.core.k -import arrow.core.value -import io.kotlintest.shouldBe -import io.kotlintest.specs.StringSpec - -class T : StringSpec({ - "run" { - MultiShotDelimContScope.reset>>> { - val hdl = listEitherHandler(Id.applicative(), this as DelimitedScope, Int>>>>) - Id(listOf(Either.Right(hdl.myFun())).k()) - }.value() shouldBe listOf(Either.Left("Better luck next time"), Either.Right(42)) - } - "run2" { - MultiShotDelimContScope.reset>>> { - val hdl = eitherListHandler(Id.applicative(), this as DelimitedScope, Kind>>>) - Id(Either.Right(listOf(hdl.myFun()).k())) - }.value() shouldBe Either.Left("Better luck next time") - } -}) diff --git a/arrow-continuations/src/test/kotlin/generic/ParserTest.kt b/arrow-continuations/src/test/kotlin/generic/ParserTest.kt deleted file mode 100644 index 82e9e4356..000000000 --- a/arrow-continuations/src/test/kotlin/generic/ParserTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package generic - -import arrow.continuations.generic.effect.Parser -import arrow.continuations.generic.effect.choice -import arrow.continuations.generic.effect.runParser -import arrow.core.identity -import io.kotlintest.specs.StringSpec - -sealed class Language -object German : Language() -object English : Language() - -// example -val parser: Parser = { - attempt { choice({ string("Wooo") }, { string("Wee") }) }.fold({ string("Error") }, ::identity) - val lang = optional { - choice({ string("Hello"); English }, { string("Hallo"); German }) - .also { eof() } - } - lang ?: English -} - -class ParserTest : StringSpec({ - "can parse" { - parser - .runParser("WooHalloWeird") - .also(::println) - } -}) diff --git a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt index b2795a123..3615b2b74 100644 --- a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt +++ b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt @@ -1,6 +1,5 @@ package generic -import arrow.continuations.effectStack.reset import arrow.continuations.generic.DelimitedScope import arrow.continuations.generic.MultiShotDelimContScope import arrow.continuations.generic.DelimContScope From a157276a5a0fa02057a50372770b7b9b3e2baa16 Mon Sep 17 00:00:00 2001 From: Jannis Date: Wed, 26 Aug 2020 21:00:45 +0200 Subject: [PATCH 44/49] Update arrow-continuations/build.gradle Co-authored-by: Rachel M. Carmena --- arrow-continuations/build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/arrow-continuations/build.gradle b/arrow-continuations/build.gradle index c02a7468b..319cac377 100644 --- a/arrow-continuations/build.gradle +++ b/arrow-continuations/build.gradle @@ -1,11 +1,7 @@ plugins { - id "maven-publish" - id "base" id "org.jetbrains.kotlin.jvm" id "org.jetbrains.kotlin.kapt" - id "org.jetbrains.dokka" id "org.jlleitschuh.gradle.ktlint" - id "ru.vyarus.animalsniffer" } apply from: "$SUB_PROJECT" From 1022af8e3877397006df12a03ac8b150d1ecc939 Mon Sep 17 00:00:00 2001 From: Jannis Date: Wed, 26 Aug 2020 21:00:57 +0200 Subject: [PATCH 45/49] Update arrow-continuations/build.gradle Co-authored-by: Rachel M. Carmena --- arrow-continuations/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow-continuations/build.gradle b/arrow-continuations/build.gradle index 319cac377..c4a230456 100644 --- a/arrow-continuations/build.gradle +++ b/arrow-continuations/build.gradle @@ -9,7 +9,7 @@ apply from: "$DOC_CREATION" apply plugin: 'kotlinx-atomicfu' dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" + compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" compile project(":arrow-annotations") compile project(":arrow-core") kapt project(":arrow-meta") From 41847f13678c21bb1e5c4cc96a100f8d844fe59d Mon Sep 17 00:00:00 2001 From: Jannis Date: Wed, 26 Aug 2020 21:01:16 +0200 Subject: [PATCH 46/49] Update arrow-continuations/build.gradle Co-authored-by: Rachel M. Carmena --- arrow-continuations/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arrow-continuations/build.gradle b/arrow-continuations/build.gradle index c4a230456..353eb2abc 100644 --- a/arrow-continuations/build.gradle +++ b/arrow-continuations/build.gradle @@ -13,7 +13,7 @@ dependencies { compile project(":arrow-annotations") compile project(":arrow-core") kapt project(":arrow-meta") - testRuntime "org.junit.vintage:junit-vintage-engine:$JUNIT_VINTAGE_VERSION" - testCompile "io.kotlintest:kotlintest-runner-junit5:$KOTLIN_TEST_VERSION", excludeArrow - testCompile project(":arrow-core-test") + testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$JUNIT_VINTAGE_VERSION" + testCompileOnly "io.kotlintest:kotlintest-runner-junit5:$KOTLIN_TEST_VERSION", excludeArrow + testCompileOnly project(":arrow-core-test") } From 528bd1edbfaa720cc3685a6c2c51d42d791340a9 Mon Sep 17 00:00:00 2001 From: Jannis Date: Thu, 27 Aug 2020 15:18:14 +0200 Subject: [PATCH 47/49] Add a few comments and explanations --- .../arrow/continuations/generic/DelimCont.kt | 42 +++++++++++++++++++ .../continuations/generic/DelimitedCont.kt | 9 ++++ .../generic/MultiShotDelimCont.kt | 29 ++++++++++++- .../continuations/generic/NestedDelimCont.kt | 33 +++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt index 92ba92575..7b78a7099 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt @@ -11,14 +11,36 @@ import kotlin.coroutines.suspendCoroutine /** * Implements delimited continuations with with no multi shot support (apart from shiftCPS which trivially supports it). + * + * For a version that simulates multishot (albeit with drawbacks) see [MultiShotDelimContScope]. + * For a version that allows nesting [reset] and calling parent scopes inside inner scopes see [NestedDelimContScope]. + * + * The basic concept here is appending callbacks and polling for a result. + * Every shift is evaluated until it either finishes (short-circuit) or suspends (called continuation). When it suspends its + * continuation is appended to a list waiting to be invoked with the final result of the block. + * When running a function we jump back and forth between the main function and every function inside shift via their continuations. */ class DelimContScope(val f: suspend DelimitedScope.() -> R): DelimitedScope { + /** + * Variable used for polling the result after suspension happened. + */ private val resultVar = atomic(null) + + /** + * Variable for the next shift block to (partially) run, if it is empty that usually means we are done. + */ private val nextShift = atomic<(suspend () -> R)?>(null) + + /** + * "Callbacks"/partially evaluated shift blocks which now wait for the final result + */ // TODO This can be append only, but needs fast reversed access private val shiftFnContinuations = mutableListOf>() + /** + * Small wrapper that handles invoking the correct continuations and appending continuations from shift blocks + */ data class SingleShotCont( private val continuation: Continuation, private val shiftFnContinuations: MutableList> @@ -29,23 +51,35 @@ class DelimContScope(val f: suspend DelimitedScope.() -> R): DelimitedScop } } + /** + * Wrapper that handles invoking manually cps transformed continuations + */ data class CPSCont( private val runFunc: suspend DelimitedScope.(A) -> R ): DelimitedContinuation { override suspend fun invoke(a: A): R = DelimContScope { runFunc(a) }.invoke() } + /** + * Captures the continuation and set [func] with the continuation to be executed next by the runloop. + */ override suspend fun shift(func: suspend DelimitedScope.(DelimitedContinuation) -> R): A = suspendCoroutine { continueMain -> val delCont = SingleShotCont(continueMain, shiftFnContinuations) assert(nextShift.compareAndSet(null, suspend { this.func(delCont) })) } + /** + * Same as [shift] except we never resume execution because we only continue in [c]. + */ override suspend fun shiftCPS(func: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> B): Nothing = suspendCoroutine { assert(nextShift.compareAndSet(null, suspend { func(CPSCont(c)) })) } + /** + * Unsafe if [f] calls [shift] on this scope! Use [NestedDelimContScope] instead if this is a problem. + */ override suspend fun reset(f: suspend DelimitedScope.() -> A): A = DelimContScope(f).invoke() @@ -54,6 +88,7 @@ class DelimContScope(val f: suspend DelimitedScope.() -> R): DelimitedScop resultVar.value = result.getOrThrow() }).let { if (it == COROUTINE_SUSPENDED) { + // we have a call to shift so we must start execution the blocks there resultVar.loop { mRes -> if (mRes == null) { val nextShiftFn = nextShift.getAndSet(null) @@ -61,15 +96,22 @@ class DelimContScope(val f: suspend DelimitedScope.() -> R): DelimitedScop nextShiftFn.startCoroutineUninterceptedOrReturn(Continuation(EmptyCoroutineContext) { result -> resultVar.value = result.getOrThrow() }).let { + // If we suspended here we can just continue to loop because we should now have a new function to run + // If we did not suspend we short-circuited and are thus done with looping if (it != COROUTINE_SUSPENDED) resultVar.value = it as R } + // Break out of the infinite loop if we have a result } else return@let } } + // we can return directly if we never suspended/called shift else return@invoke it as R } assert(resultVar.value != null) + // We need to finish the partially evaluated shift blocks by passing them our result. + // This will update the result via the continuations that now finish up for (c in shiftFnContinuations.asReversed()) c.resume(resultVar.value!!) + // Return the final result return resultVar.value!! } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt index 2c49e2884..11dc5117f 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimitedCont.kt @@ -13,6 +13,9 @@ interface DelimitedContinuation { // TODO This should be @RestrictSuspension but that breaks because a superclass is not considered to be correct scope // @RestrictsSuspension interface DelimitedScope { + /** + * Capture the continuation and pass it to [f]. + */ suspend fun shift(f: suspend DelimitedScope.(DelimitedContinuation) -> R): A /** @@ -21,5 +24,11 @@ interface DelimitedScope { * - it is manually cps transformed which means every helper between this and invoking the continuation also needs to be transformed. */ suspend fun shiftCPS(f: suspend (DelimitedContinuation) -> R, c: suspend DelimitedScope.(A) -> B): Nothing + + /** + * Nest another scope inside the current one. + * + * It is important to use this over creating an unrelated scope because + */ suspend fun reset(f: suspend DelimitedScope.() -> A): A } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt index bb54a2c10..e2afa5255 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/MultiShotDelimCont.kt @@ -9,6 +9,19 @@ import kotlin.coroutines.intrinsics.startCoroutineUninterceptedOrReturn import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +/** + * (Simulated) Multishot capable delimited control scope + * + * This has several drawbacks: + * - f will rerun completely on multishot and only the results of [shift] are cached so any sideeffects outside of + * [shift] will rerun! + * - This accumulates all results of [shift] (every argument passed when invoking the continuation) so on long running computations + * this may keep quite a bit of memory + * - If the pure part before a multishot is expensive the multishot itself will have to rerun that, which makes it somewhat slow + * - This is terribly hard to implement properly with nested scopes (which this one does not support) + * + * As per usual understanding of [DelimContScope] is required as I will only be commenting differences for now. + */ open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) : DelimitedScope { private val resultVar = atomic(null) @@ -17,21 +30,33 @@ open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) // TODO This can be append only and needs fast reversed access private val shiftFnContinuations = mutableListOf>() + /** + * Keep the arguments passed to [DelimitedContinuation.invoke] to be able to replay the scope if necessary + */ // TODO This can be append only and needs fast random access and slicing internal open val stack = mutableListOf() + /** + * Our continuation now includes the function [f] to rerun on multishot, the current live (single-shot) continuation, + * the current stack and the offset from that stack when this is created which is used to know when to resume normal + * execution again on a replay. + */ class MultiShotCont( liveContinuation: Continuation, private val f: suspend DelimitedScope.() -> R, private val stack: MutableList, private val shiftFnContinuations: MutableList> ) : DelimitedContinuation { + // To make sure the continuation is only invoked once we put it in a nullable atomic and only access it through getAndSet private val liveContinuation = atomic?>(liveContinuation) private val stackOffset = stack.size override suspend fun invoke(a: A): R = when (val cont = liveContinuation.getAndSet(null)) { + // On multishot we replay with a prefilled stack from start to the point at which this object was created + // (when the shift block this runs in was first called) null -> PrefilledDelimContScope((stack.subList(0, stackOffset).toList() + a).toMutableList(), f).invoke() + // on the first pass we operate like a normal delimited scope but we also save the argument to the stack before resuming else -> suspendCoroutine { resumeShift -> shiftFnContinuations.add(resumeShift) stack.add(a) @@ -46,8 +71,6 @@ open class MultiShotDelimContScope(val f: suspend DelimitedScope.() -> R) override suspend fun invoke(a: A): R = DelimContScope { runFunc(a) }.invoke() } - // TODO I wrote this in the middle of the night, double check - // Note we don't wrap the function [func] in an explicit reset because that is already implicit in our scope override suspend fun shift(func: suspend DelimitedScope.(DelimitedContinuation) -> R): A = suspendCoroutine { continueMain -> val c = MultiShotCont(continueMain, f, stack, shiftFnContinuations) @@ -97,6 +120,8 @@ class PrefilledDelimContScope( ) : MultiShotDelimContScope(f) { var depth = 0 + // Here we first check if we still have values in our local stack and if so we use those first + // if not we delegate to the normal delimited control implementation override suspend fun shift(func: suspend DelimitedScope.(DelimitedContinuation) -> R): A = if (stack.size > depth) stack[depth++] as A else super.shift(func).also { depth++ } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt index c6f99648c..e39c22eff 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt @@ -10,16 +10,31 @@ import kotlin.coroutines.suspendCoroutine /** * Delimited control version which allows `reset { ... reset { ... } }` to function correctly. + * * [DelimContScope] fails at this if you call shift on the parent scope inside the inner reset. + * For a version that allows simulated multishot (with drawbacks) see [MultiShotDelimContScope]. + * + * The implementation is basically the same as [DelimContScope] except that we now need to respect calls through different runloops. + * + * Comments in here only describe what differs from [DelimContScope] and why it differs. + * Make sure you understand [DelimContScope] before reading this. + * (When this is somewhat more stable we can copy comments over until then it would be annoying to maintain) + * + * > It would be possible to collapse to one runloop however that comes with drawbacks: + * > - no typesafety because each nested block may result in different types + * > - slightly more complex implementation because each nest must when it finishes know what continuations to resume */ open class NestedDelimContScope(val f: suspend DelimitedScope.() -> R) : DelimitedScope { private val resultVar = atomic(null) + + // We now need a way for nested scopes to access this variable and the atomic plugin prevents direct access internal fun getResult(): R? = resultVar.value internal fun setResult(r: R): Unit { resultVar.value = r } + // Short hand for AtomicRef.loop which the atomic plugin prevents from use from outside of this class afaik internal inline fun loopNoResult(f: () -> Unit): Unit { while (true) { if (getResult() == null) f() @@ -59,10 +74,15 @@ open class NestedDelimContScope(val f: suspend DelimitedScope.() -> R) : D assert(nextShift.compareAndSet(null, suspend { func(CPSCont(c)) })) } + // Here we create a new scope and pass this scope as a parent override suspend fun reset(f: suspend DelimitedScope.() -> A): A = ChildDelimContScope(this, f) .invokeNested() + // helper to execute one single shift function. + // hdlMissingWork is used to handle the case where we suspended but there are no shift functions to execute + // in our scope. This means one of two things: In the top level scope this is an error, in a child this means that a parent + // now has a shift function to execute and we need to yield to the parent. internal inline fun step(hdlMisingWork: () -> Unit): Unit { val nextShiftFn = nextShift.getAndSet(null) ?: return hdlMisingWork() @@ -73,12 +93,14 @@ open class NestedDelimContScope(val f: suspend DelimitedScope.() -> R) : D } } + // This is basically the same as as in DelimContScope but split across a few helpers to ease reuse in nested scopes open fun invoke(): R { f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result -> resultVar.value = result.getOrThrow() }).let { if (it == COROUTINE_SUSPENDED) { loopNoResult { + // At the top level not having a shift function to continue and not having a result is an error step { throw IllegalStateException("Suspended parent scope, but found no further work") } } } else return@invoke it as R @@ -89,6 +111,8 @@ open class NestedDelimContScope(val f: suspend DelimitedScope.() -> R) : D return resultVar.value!! } + // on the top level this checks if we have work to do and returns this if that is the case + // on nested scopes this searches all parents as well open fun getActiveParent(): NestedDelimContScope<*>? = this.takeIf { nextShift.value != null } companion object { @@ -100,9 +124,15 @@ class ChildDelimContScope( val parent: NestedDelimContScope<*>, f: suspend DelimitedScope.() -> R ) : NestedDelimContScope(f) { + // search all parents to find the one that has work to do override fun getActiveParent(): NestedDelimContScope<*>? = super.getActiveParent() ?: parent.getActiveParent() + // Instead of yielding to a parents runloop (which complicates reentering our own runloop a bit) we instead perform our parents + // work from the child scope which is no problem because we just need to run its queued suspend functions and poll for a result + // after each invocation of step we also check if the parent finished (which means short-circuit) or the parent suspended which + // which means work has been queued again (could be anywhere which is why we search again in the loop). + // If we are the ones that need to resume we break the loop and invokeNested will resume work private suspend fun performParentWorkIfNeeded(): Unit { while (true) { parent.getActiveParent()?.let { scope -> @@ -120,6 +150,7 @@ class ChildDelimContScope( }).let { if (it == COROUTINE_SUSPENDED) { loopNoResult { + // if we run out of work we need to check and run work for one of our parent scopes step { performParentWorkIfNeeded() } } } else return@invokeNested it as R @@ -130,6 +161,8 @@ class ChildDelimContScope( return getResult()!! } + // This is now unsupported because it does not allow suspension which is key in yielding to the parent if the parent short-circuited + // TODO if invoke ever becomes suspended invokeNested should be removed override fun invoke(): R { println(""" Using invoke() for child scope. From 3ecfb225e472b2f05438ce933fee4f15b00e083b Mon Sep 17 00:00:00 2001 From: Jannis Date: Thu, 27 Aug 2020 15:25:38 +0200 Subject: [PATCH 48/49] Linter and remove unecessary deps --- arrow-continuations/build.gradle | 3 --- .../continuations/generic/{DelimCont.kt => DelimContScope.kt} | 4 ++-- .../kotlin/arrow/continuations/generic/NestedDelimCont.kt | 4 ++-- arrow-continuations/src/test/kotlin/generic/TestSuite.kt | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) rename arrow-continuations/src/main/kotlin/arrow/continuations/generic/{DelimCont.kt => DelimContScope.kt} (98%) diff --git a/arrow-continuations/build.gradle b/arrow-continuations/build.gradle index 353eb2abc..b81fa3679 100644 --- a/arrow-continuations/build.gradle +++ b/arrow-continuations/build.gradle @@ -10,9 +10,6 @@ apply plugin: 'kotlinx-atomicfu' dependencies { compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" - compile project(":arrow-annotations") - compile project(":arrow-core") - kapt project(":arrow-meta") testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$JUNIT_VINTAGE_VERSION" testCompileOnly "io.kotlintest:kotlintest-runner-junit5:$KOTLIN_TEST_VERSION", excludeArrow testCompileOnly project(":arrow-core-test") diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimContScope.kt similarity index 98% rename from arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt rename to arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimContScope.kt index 7b78a7099..9502c8459 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/DelimContScope.kt @@ -20,7 +20,7 @@ import kotlin.coroutines.suspendCoroutine * continuation is appended to a list waiting to be invoked with the final result of the block. * When running a function we jump back and forth between the main function and every function inside shift via their continuations. */ -class DelimContScope(val f: suspend DelimitedScope.() -> R): DelimitedScope { +class DelimContScope(val f: suspend DelimitedScope.() -> R) : DelimitedScope { /** * Variable used for polling the result after suspension happened. @@ -56,7 +56,7 @@ class DelimContScope(val f: suspend DelimitedScope.() -> R): DelimitedScop */ data class CPSCont( private val runFunc: suspend DelimitedScope.(A) -> R - ): DelimitedContinuation { + ) : DelimitedContinuation { override suspend fun invoke(a: A): R = DelimContScope { runFunc(a) }.invoke() } diff --git a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt index e39c22eff..5abf59800 100644 --- a/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt +++ b/arrow-continuations/src/main/kotlin/arrow/continuations/generic/NestedDelimCont.kt @@ -137,9 +137,9 @@ class ChildDelimContScope( while (true) { parent.getActiveParent()?.let { scope -> // No need to do anything in steps cb because we handle this case from down here - scope.step { } + scope.step {} // parent short circuited - if (scope.getResult() != null) suspendCoroutine { } + if (scope.getResult() != null) suspendCoroutine {} } ?: break } } diff --git a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt index 3615b2b74..d698f5256 100644 --- a/arrow-continuations/src/test/kotlin/generic/TestSuite.kt +++ b/arrow-continuations/src/test/kotlin/generic/TestSuite.kt @@ -12,7 +12,7 @@ import arrow.core.test.UnitSpec import arrow.core.toT import io.kotlintest.shouldBe -abstract class ContTestSuite: UnitSpec() { +abstract class ContTestSuite : UnitSpec() { abstract fun runScope(func: (suspend DelimitedScope.() -> A)): A abstract fun capabilities(): Set From 869c9c75a32887c92bd2292666d6cd3200c7f28d Mon Sep 17 00:00:00 2001 From: "Rachel M. Carmena" Date: Thu, 27 Aug 2020 22:35:48 +0200 Subject: [PATCH 49/49] Fix: UnitSpec is not found during test runtime --- arrow-continuations/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arrow-continuations/build.gradle b/arrow-continuations/build.gradle index b81fa3679..d7ce856b9 100644 --- a/arrow-continuations/build.gradle +++ b/arrow-continuations/build.gradle @@ -12,5 +12,5 @@ dependencies { compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" testRuntimeOnly "org.junit.vintage:junit-vintage-engine:$JUNIT_VINTAGE_VERSION" testCompileOnly "io.kotlintest:kotlintest-runner-junit5:$KOTLIN_TEST_VERSION", excludeArrow - testCompileOnly project(":arrow-core-test") + testImplementation project(":arrow-core-test") }