Skip to content

Strawman: Suspensions for algebraic effects #16739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 52 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
2e2f5a0
Futures via suspend
odersky Jan 21, 2023
143c75d
Choice via suspend
odersky Jan 21, 2023
a4de29b
Rename `all` to `each`
odersky Jan 23, 2023
b845501
Variant: Wrap suspension instead in break result
odersky Jan 23, 2023
8ac3a88
Add alternative futures test
odersky Feb 1, 2023
cca4eb6
Fix test
odersky Feb 1, 2023
fddf3b6
Use general design of `FuturesALT` but with unit-valued suspend
odersky Feb 1, 2023
be58764
Drop redundant parameter
odersky Feb 1, 2023
ec94fc6
Simplified alternative version
odersky Feb 1, 2023
350cb38
Reorganize; add monadic reflection
odersky Feb 2, 2023
5845108
Move context bound to enclosing trait
odersky Feb 3, 2023
8ff7966
Drop Monad bound on CanReflect
odersky Feb 3, 2023
3008df5
Make reflect polymorphic
odersky Feb 3, 2023
690fd11
Setup enriched futures
odersky Feb 4, 2023
c5aac2a
Add exception handling
odersky Feb 4, 2023
6bb716b
Make futures lazy
odersky Feb 4, 2023
a52c475
Handle possible race conditions
odersky Feb 4, 2023
f2db134
Make scheduler a parameter
odersky Feb 4, 2023
4f146c6
Add Future.spawn to create futures that start immediately
odersky Feb 5, 2023
7969a3c
Add simple cancellation
odersky Feb 5, 2023
5950feb
Structured concurrency 1: Add future linking
odersky Feb 5, 2023
2cf9f78
Structured concurrency 2: parallel and alternative composition
odersky Feb 5, 2023
20f08fa
Intgerate futures with threads
odersky Feb 5, 2023
a448793
Rename Future#await --> Future#value
odersky Feb 6, 2023
3c058b4
Introduce Task abstraction
odersky Feb 6, 2023
7b83493
Add Async.Source, Async.Runner abstractions
odersky Feb 6, 2023
30f3389
Split concurrency library into separate files
odersky Feb 7, 2023
62c2f21
Auto-link futures
odersky Feb 7, 2023
4cf756c
Add derived sources and channels
odersky Feb 9, 2023
0ef4ab6
Reorganize channels around Sources vs ComposableSources
odersky Feb 9, 2023
f91d369
Use CanFilter type to distinguish FilterableChannels
odersky Feb 10, 2023
a10fbf9
Drop distinction between filterable and normal sync channels
odersky Feb 10, 2023
a81febc
Add CSP-style coroutines that communicate with channels
odersky Feb 11, 2023
25efae1
Fix compilation failures
odersky Feb 11, 2023
60847c0
Improve boundary doc comment
odersky Feb 11, 2023
fffd21b
Tweaks
odersky Feb 14, 2023
c91fad9
Move strawman files to tests/run
odersky Feb 19, 2023
08a29dd
Add OriginalSource abstract class
odersky Feb 19, 2023
f9675a5
Variant of suspension based on fibers
odersky Feb 19, 2023
dfb1713
Add logging and timing randomization
odersky Feb 19, 2023
c810bf2
Polishings
odersky Feb 20, 2023
294bf1d
Use Futures instead of Coroutines
odersky Feb 21, 2023
7819b61
Refactoring of Cancellables
odersky Feb 21, 2023
518f767
Make Async.Group a field in Async instead of inheriting from it
odersky Feb 21, 2023
503ec8b
Use ExecutionContext instead of Scheduler
odersky Feb 21, 2023
6f5dfc5
Add explanation document and align implementation with it
odersky Feb 26, 2023
639bb71
Fix alt
odersky Feb 27, 2023
3091402
Describe Tasks and Promises
odersky Mar 1, 2023
7c6c41b
Fix missing finally clause in Async.group
odersky Mar 3, 2023
f49eaca
Change toStream method
odersky Mar 4, 2023
86c0963
fix race condition in cancel
odersky Mar 4, 2023
d15fe46
Drop foundations.md
odersky Mar 5, 2023
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: 8 additions & 8 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3080,8 +3080,8 @@ object Parsers {
/* -------- PARAMETERS ------------------------------------------- */

/** DefParamClauses ::= DefParamClause { DefParamClause } -- and two DefTypeParamClause cannot be adjacent
* DefParamClause ::= DefTypeParamClause
* | DefTermParamClause
* DefParamClause ::= DefTypeParamClause
* | DefTermParamClause
* | UsingParamClause
*/
def typeOrTermParamClauses(
Expand Down Expand Up @@ -3179,7 +3179,7 @@ object Parsers {
* UsingClsTermParamClause::= ‘(’ ‘using’ [‘erased’] (ClsParams | ContextTypes) ‘)’
* ClsParams ::= ClsParam {‘,’ ClsParam}
* ClsParam ::= {Annotation}
*
*
* TypelessClause ::= DefTermParamClause
* | UsingParamClause
*
Expand Down Expand Up @@ -3557,13 +3557,13 @@ object Parsers {
}
}



/** DefDef ::= DefSig [‘:’ Type] ‘=’ Expr
* | this TypelessClauses [DefImplicitClause] `=' ConstrExpr
* DefDcl ::= DefSig `:' Type
* DefSig ::= id [DefTypeParamClause] DefTermParamClauses
*
*
* if clauseInterleaving is enabled:
* DefSig ::= id [DefParamClauses] [DefImplicitClause]
*/
Expand Down Expand Up @@ -3602,8 +3602,8 @@ object Parsers {
val mods1 = addFlag(mods, Method)
val ident = termIdent()
var name = ident.name.asTermName
val paramss =
if in.featureEnabled(Feature.clauseInterleaving) then
val paramss =
if in.featureEnabled(Feature.clauseInterleaving) then
// If you are making interleaving stable manually, please refer to the PR introducing it instead, section "How to make non-experimental"
typeOrTermParamClauses(ParamOwner.Def, numLeadParams = numLeadParams)
else
Expand All @@ -3613,7 +3613,7 @@ object Parsers {
joinParams(tparams, vparamss)

var tpt = fromWithinReturnType { typedOpt() }

if (migrateTo3) newLineOptWhenFollowedBy(LBRACE)
val rhs =
if in.token == EQUALS then
Expand Down
5 changes: 5 additions & 0 deletions library/src/scala/util/boundary.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ object boundary:
/*message*/ null, /*cause*/ null, /*enableSuppression=*/ false, /*writableStackTrace*/ false)

/** Labels are targets indicating which boundary will be exited by a `break`.
* A Label is generated and passed to code wrapped in a `boundary` call. Example
*
* boundary[Unit]:
* ...
* summon[Label[Unit]] // will link to the label generated by `boundary`
*/
final class Label[-T]

Expand Down
84 changes: 84 additions & 0 deletions tests/pos/suspend-strawman-1/choices.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import collection.mutable.ListBuffer
import scala.util.boundary, boundary.{Label, break}
import runtime.*

/** A single method iterator */
abstract class Choices[+T]:
def next: Option[T]

object Choices:
def apply[T](elems: T*): Choices[T] =
apply(elems.iterator)

def apply[T](it: Iterator[T]) = new Choices[T]:
def next = if it.hasNext then Some(it.next) else None
end Choices

/** A Label representikng a boundary to which can be returned
* - None, indicating an mepty choice
* - a suspension with Option[T] result, which will be
* iterated over element by element in the boundary's result.
*/
type CanChoose[T] = Label[None.type | Suspension[Option[T]]]

/** A variable representing Choices */
class Ref[T](choices: => Choices[T]):

/** Try all values of this variable. For each value, run the continuation.
* If it yields a Some result take that as the next value of the enclosing
* boundary. If it yields a Non, skip the element.
*/
def each[R](using CanChoose[R]): T =
val cs = choices
suspend()
cs.next.getOrElse(break(None))

end Ref

/** A prompt to iterate a compputation `body` over all choices of
* variables which it references.
* @return all results in a new `Choices` iterator,
*/
def choices[T](body: CanChoose[T] ?=> T): Choices[T] = new Choices:
var stack: List[Suspension[Option[T]]] = Nil

def next: Option[T] =
boundary(Some(body)) match
case s: Some[T] => s
case None =>
if stack.isEmpty then
// no (more) variable choices encountered
None
else
// last variable's choices exhausted; use next value of previous variable
stack = stack.tail
stack.head.resume()
case susp: Suspension[Option[T]] =>
// A new Choices variable was encountered
stack = susp :: stack
stack.head.resume()

@main def test: Choices[Int] =
val x = Ref(Choices(1, -2, -3))
val y = Ref(Choices("ab", "cde"))
choices:
val xx = x.each
xx + (
if xx > 0 then y.each.length * x.each
else y.each.length
)
/* Gives the results of the following computations:
1 + 2 * 1
1 + 2 * -2
1 + 2 * -3
1 + 3 * 1
1 + 3 * -2
1 + 3 * -3
-2 + 2
-2 + 3
-3 + 2
-3 + 3
*/



44 changes: 44 additions & 0 deletions tests/pos/suspend-strawman-1/futures.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import collection.mutable.ListBuffer
import scala.util.boundary, boundary.{Label, break}
import runtime.*

/** A suspension and a value indicating the Future for which the suspension is waiting */
type Waiting = (Suspension[Unit], Future[?])

/** The capability to suspend while waiting for some other future */
type CanWait = Label[Waiting]

class Future[+T] private ():
private var result: Option[T] = None
private var waiting: ListBuffer[Runnable] = ListBuffer()

/** Return future's result, while waiting until it is completed if necessary.
*/
def await(using CanWait): T = result match
case Some(x) => x
case None =>
suspend((s: Suspension[Unit]) => (s, this))
await

object Future:

private def complete[T](f: Future[T], value: T): Unit =
f.result = Some(value)
f.waiting.foreach(Scheduler.schedule)
f.waiting = ListBuffer()

/** Create future that eventually returns the result of `op` */
def apply[T](body: CanWait ?=> T): Future[T] =
val f = new Future[T]
Scheduler.schedule: () =>
boundary[Unit | Waiting]:
complete(f, body)
match
case (suspension, blocking) =>
blocking.waiting += (() => suspension.resume())
case () =>
f

def Test(x: Future[Int], xs: List[Future[Int]]) =
Future:
x.await + xs.map(_.await).sum
21 changes: 21 additions & 0 deletions tests/pos/suspend-strawman-1/runtime.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import scala.util.boundary, boundary.Label
object runtime:

/** A hypthetical task scheduler */
object Scheduler:
def schedule(task: Runnable): Unit = ???

/** Contains a delimited contination, which can be invoked with `resume` */
class Suspension[+R]:
def resume(): R = ???

/** Returns `fn(s)` where `s` is the current suspension to the boundary associated
* with the given label.
*/
def suspend[R, T](fn: Suspension[R] => T)(using Label[T]): T = ???

/** Returns the current suspension to the boundary associated
* with the given label.
*/
def suspend[R]()(using Label[Suspension[R]]): Unit =
suspend[R, Suspension[R]](identity)
3 changes: 3 additions & 0 deletions tests/run/suspend-strawman-2.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test async:
33
(22,11)
Loading