Skip to content
This repository has been archived by the owner on Feb 24, 2021. It is now read-only.

Delimited continuations #226

Merged
merged 57 commits into from
Aug 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8463380
roots of suspension control
raulraja Jun 27, 2020
6616cd7
Merge remote-tracking branch 'origin/master' into rr-continuation-tricks
raulraja Jul 1, 2020
eba7b60
Computation builders first draft
raulraja Jul 1, 2020
19bd93f
multiprompt progress
raulraja Jul 17, 2020
c6dc26c
multiprompt progress
raulraja Jul 20, 2020
59f0ace
multiprompt progress
raulraja Jul 20, 2020
3e9635e
Follow suspend value into prompt
nomisRev Jul 20, 2020
f65cfaa
progress better typing reflect example and advancing
raulraja Jul 21, 2020
550e599
cont adt
raulraja Jul 23, 2020
727948f
Reflect based on List and tailRecM
nomisRev Jul 23, 2020
b462a1e
progress advancing first iteration until suspended reset with stacks …
raulraja Jul 23, 2020
027e55a
moar progress advancing states
raulraja Jul 25, 2020
0a412e5
fix compilation
raulraja Jul 25, 2020
ee85ff9
logging states
raulraja Jul 25, 2020
fd5067b
Initial attempt at well typed ADT with full reification of callbacks …
raulraja Jul 26, 2020
a2af57b
Remove Reset from ADT as it's implicit in the created scope
raulraja Jul 26, 2020
55db1b4
Include scope in interception
raulraja Jul 26, 2020
d302bac
getting ready for the loop
raulraja Jul 26, 2020
3b59819
concrete scope as objects
raulraja Jul 26, 2020
23266ab
Exposing available data in the compiler
raulraja Jul 26, 2020
ddfbef8
Shortcircuit case
raulraja Jul 26, 2020
e3a74ef
Better typing for scoping callbacks
raulraja Jul 26, 2020
e638c1a
prototype the List scope to avoid thread safety since the functions i…
raulraja Jul 26, 2020
68f17dd
Multishot cont with effect stack.
1Jajen1 Jul 27, 2020
c1f87da
Add monadic bind example
1Jajen1 Jul 27, 2020
62e2e5e
Clarify comments and make variables private/internal
1Jajen1 Jul 27, 2020
35d5d6f
Typed GADT and state for delim continuations with multi-prompt.
raulraja Jul 27, 2020
eb30a02
Merge remote-tracking branch 'origin/rr-continuation-tricks' into rr-…
raulraja Jul 27, 2020
a2dc9d0
test for list in non-deterministic comprehensions over Jannis impl
raulraja Jul 27, 2020
4940437
Handle prompt in prompt case correctly
1Jajen1 Jul 27, 2020
3e03473
Add test suite + try to make suspend capable
nomisRev Jul 27, 2020
0abf7c7
Merge branch 'rr-continuation-tricks' of github.com:arrow-kt/arrow-co…
nomisRev Jul 27, 2020
b11bd06
minor name refactor
raulraja Jul 27, 2020
4533b75
Added a bunch of comments + slightly better names
1Jajen1 Jul 27, 2020
ef56c88
Broken but informative version of nested effectstack improvements
1Jajen1 Jul 28, 2020
02edd77
Better nested multishot support
1Jajen1 Jul 28, 2020
1964cf5
interleaving draft
raulraja Jul 29, 2020
aefc1b0
Merge remote-tracking branch 'origin/rr-continuation-tricks' into rr-…
raulraja Jul 29, 2020
e3a3d97
Implement basic delimited continuations with and without multishot
1Jajen1 Jul 30, 2020
8f3e66c
Add shiftCPS and reset. Also removed broken nested reset implementation
1Jajen1 Jul 30, 2020
9208e4e
Push effect interface and a sub-optimal handler implementation
1Jajen1 Aug 1, 2020
53efcf7
Add parser implementation based on effects
1Jajen1 Aug 1, 2020
210e8d3
Split Error into Catch and Raise
1Jajen1 Aug 2, 2020
ba81146
Added nested reset support but without multishot for now
1Jajen1 Aug 3, 2020
bb76e78
A better type for shiftCPS + another delimited cont implementation
1Jajen1 Aug 4, 2020
21bafb1
Fix task
rachelcarmena Aug 12, 2020
2b610f5
Merge remote-tracking branch 'origin/master' into rr-continuation-tricks
1Jajen1 Aug 26, 2020
3fb948f
Clean out branch to keep only the generic implementations
1Jajen1 Aug 26, 2020
a157276
Update arrow-continuations/build.gradle
1Jajen1 Aug 26, 2020
1022af8
Update arrow-continuations/build.gradle
1Jajen1 Aug 26, 2020
41847f1
Update arrow-continuations/build.gradle
1Jajen1 Aug 26, 2020
528bd1e
Add a few comments and explanations
1Jajen1 Aug 27, 2020
f08de87
Merge remote-tracking branch 'origin/rr-continuation-tricks' into rr-…
1Jajen1 Aug 27, 2020
3ecfb22
Linter and remove unecessary deps
1Jajen1 Aug 27, 2020
edd6880
Merge branch 'master' into rr-continuation-tricks
raulraja Aug 27, 2020
a8d92a0
Merge branch 'master' into rr-continuation-tricks
raulraja Aug 27, 2020
869c9c7
Fix: UnitSpec is not found during test runtime
rachelcarmena Aug 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions arrow-continuations/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
plugins {
id "org.jetbrains.kotlin.jvm"
id "org.jetbrains.kotlin.kapt"
id "org.jlleitschuh.gradle.ktlint"
}

apply from: "$SUB_PROJECT"
apply from: "$DOC_CREATION"
apply plugin: 'kotlinx-atomicfu'

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
testImplementation project(":arrow-core-test")
}
6 changes: 6 additions & 0 deletions arrow-continuations/gradle.properties
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
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 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<R>(val f: suspend DelimitedScope<R>.() -> R) : DelimitedScope<R> {

/**
* Variable used for polling the result after suspension happened.
*/
private val resultVar = atomic<R?>(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<Continuation<R>>()

/**
* Small wrapper that handles invoking the correct continuations and appending continuations from shift blocks
*/
data class SingleShotCont<A, R>(
private val continuation: Continuation<A>,
private val shiftFnContinuations: MutableList<Continuation<R>>
) : DelimitedContinuation<A, R> {
override suspend fun invoke(a: A): R = suspendCoroutine { resumeShift ->
shiftFnContinuations.add(resumeShift)
continuation.resume(a)
}
}

/**
* Wrapper that handles invoking manually cps transformed continuations
*/
data class CPSCont<A, R>(
private val runFunc: suspend DelimitedScope<R>.(A) -> R
) : DelimitedContinuation<A, R> {
override suspend fun invoke(a: A): R = DelimContScope<R> { runFunc(a) }.invoke()
}

/**
* Captures the continuation and set [func] with the continuation to be executed next by the runloop.
*/
override suspend fun <A> shift(func: suspend DelimitedScope<R>.(DelimitedContinuation<A, R>) -> 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 <A, B> shiftCPS(func: suspend (DelimitedContinuation<A, B>) -> R, c: suspend DelimitedScope<B>.(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 <A> reset(f: suspend DelimitedScope<A>.() -> A): A =
DelimContScope(f).invoke()

fun invoke(): R {
f.startCoroutineUninterceptedOrReturn(this, Continuation(EmptyCoroutineContext) { result ->
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)
?: throw IllegalStateException("No further work to do but also no result!")
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!!
}

companion object {
fun <R> reset(f: suspend DelimitedScope<R>.() -> R): R = DelimContScope(f).invoke()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package arrow.continuations.generic

/**
* Base interface for a continuation
*/
interface DelimitedContinuation<A, R> {
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<R> {
/**
* Capture the continuation and pass it to [f].
*/
suspend fun <A> shift(f: suspend DelimitedScope<R>.(DelimitedContinuation<A, R>) -> R): A

/**
* 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 <A, B> shiftCPS(f: suspend (DelimitedContinuation<A, B>) -> R, c: suspend DelimitedScope<B>.(A) -> B): Nothing

/**
* Nest another scope inside the current one.
*
* It is important to use this over creating an unrelated scope because
*/
suspend fun <A> reset(f: suspend DelimitedScope<A>.() -> A): A
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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

/**
* (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<R>(val f: suspend DelimitedScope<R>.() -> R) : DelimitedScope<R> {

private val resultVar = atomic<R?>(null)
private val nextShift = atomic<(suspend () -> R)?>(null)

// TODO This can be append only and needs fast reversed access
private val shiftFnContinuations = mutableListOf<Continuation<R>>()

/**
* 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<Any?>()

/**
* 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<A, R>(
liveContinuation: Continuation<A>,
private val f: suspend DelimitedScope<R>.() -> R,
private val stack: MutableList<Any?>,
private val shiftFnContinuations: MutableList<Continuation<R>>
) : DelimitedContinuation<A, R> {
// 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<Continuation<A>?>(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)
cont.resume(a)
}
}
}

data class CPSCont<A, R>(
private val runFunc: suspend DelimitedScope<R>.(A) -> R
) : DelimitedContinuation<A, R> {
override suspend fun invoke(a: A): R = DelimContScope<R> { runFunc(a) }.invoke()
}

override suspend fun <A> shift(func: suspend DelimitedScope<R>.(DelimitedContinuation<A, R>) -> R): A =
suspendCoroutine { continueMain ->
val c = MultiShotCont(continueMain, f, stack, shiftFnContinuations)
assert(nextShift.compareAndSet(null, suspend { this.func(c) }))
}

override suspend fun <A, B> shiftCPS(func: suspend (DelimitedContinuation<A, B>) -> R, c: suspend DelimitedScope<B>.(A) -> B): 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 suspend fun <A> reset(f: suspend DelimitedScope<A>.() -> A): A =
MultiShotDelimContScope(f).invoke()

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 <R> reset(f: suspend DelimitedScope<R>.() -> R): R = MultiShotDelimContScope(f).invoke()
}
}

class PrefilledDelimContScope<R>(
override val stack: MutableList<Any?>,
f: suspend DelimitedScope<R>.() -> R
) : MultiShotDelimContScope<R>(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 <A> shift(func: suspend DelimitedScope<R>.(DelimitedContinuation<A, R>) -> R): A =
if (stack.size > depth) stack[depth++] as A
else super.shift(func).also { depth++ }
}
Loading