From 2e2f5a04bbbce72d92f660925e6c4866a8b30803 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 21 Jan 2023 09:40:42 +0100 Subject: [PATCH 01/52] Futures via suspend --- tests/pos/futures.scala | 65 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/pos/futures.scala diff --git a/tests/pos/futures.scala b/tests/pos/futures.scala new file mode 100644 index 000000000000..71103d7cf752 --- /dev/null +++ b/tests/pos/futures.scala @@ -0,0 +1,65 @@ +import collection.mutable.ListBuffer +import scala.util.boundary, boundary.{Label, break} + +/** A hypthetical task scheduler */ +object Scheduler: + def schedule(task: Runnable): Unit = ??? + +/** Contains a delimited contination, which can be invoked with `run`, + * plus some other value that is returned from a `suspend`. + */ +case class Suspension[+T](x: T): + def resume(): Unit = ??? + +/** Returns `Suspension(x)` to the boundary associated with the given label */ +def suspend[T](x: T)(using Label[Suspension[T]]): Unit = + break(Suspension(x)) + +/** A suspension indicating the Future for which it is waiting */ +type Waiting = Suspension[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(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: => T): Future[T] = + val f = new Future[T] + Scheduler.schedule: () => + boundary[Unit | Waiting]: + complete(f, body) + match + case s @ Suspension(blocking) => + blocking.waiting += (() => s.resume()) + case () => + f + +def Test(x1: Future[Int], x2: Future[Int])(using CanWait) = + Future: + x1.await + x2.await + + + + + + + + From 143c75d18291a0a86d79a61705306668a7541585 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 21 Jan 2023 20:39:29 +0100 Subject: [PATCH 02/52] Choice via suspend --- tests/pos/choices.scala | 97 +++++++++++++++++++++++++++++++++++++++++ tests/pos/futures.scala | 8 ++-- 2 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 tests/pos/choices.scala diff --git a/tests/pos/choices.scala b/tests/pos/choices.scala new file mode 100644 index 000000000000..605b9f276ee1 --- /dev/null +++ b/tests/pos/choices.scala @@ -0,0 +1,97 @@ +import collection.mutable.ListBuffer +import compiletime.uninitialized +import scala.util.boundary, boundary.{Label, break} + +/** Contains a delimited contination, which can be invoked with `run`, + * plus some other value that is returned from a `suspend`. + */ +case class Suspension[+T, +R](x: T): + def resume(): R = ??? + +/** Returns `Suspension(x)` to the boundary associated with the given label */ +def suspend[T, R](x: T)(using Label[Suspension[T, R]]): Unit = + break(Suspension(x)) + +def suspend[T, R]()(using Label[Suspension[Unit, R]]): Unit = + suspend(()) + +/** 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[Unit, 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 all[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[Unit, 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[Unit, 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.all + xx + ( + if xx > 0 then y.all.length * x.all + else y.all.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 + */ + + + diff --git a/tests/pos/futures.scala b/tests/pos/futures.scala index 71103d7cf752..80434b2f4ded 100644 --- a/tests/pos/futures.scala +++ b/tests/pos/futures.scala @@ -8,15 +8,15 @@ object Scheduler: /** Contains a delimited contination, which can be invoked with `run`, * plus some other value that is returned from a `suspend`. */ -case class Suspension[+T](x: T): - def resume(): Unit = ??? +case class Suspension[+T, +R](x: T): + def resume(): R = ??? /** Returns `Suspension(x)` to the boundary associated with the given label */ -def suspend[T](x: T)(using Label[Suspension[T]]): Unit = +def suspend[T, R](x: T)(using Label[Suspension[T, R]]): Unit = break(Suspension(x)) /** A suspension indicating the Future for which it is waiting */ -type Waiting = Suspension[Future[?]] +type Waiting = Suspension[Future[?], Unit] /** The capability to suspend while waiting for some other future */ type CanWait = Label[Waiting] From a4de29b740021e7eb79a27ca2914904e968150ad Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 23 Jan 2023 09:59:38 +0100 Subject: [PATCH 03/52] Rename `all` to `each` --- tests/pos/choices.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pos/choices.scala b/tests/pos/choices.scala index 605b9f276ee1..d9fa9adda094 100644 --- a/tests/pos/choices.scala +++ b/tests/pos/choices.scala @@ -41,7 +41,7 @@ class Ref[T](choices: => Choices[T]): * 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 all[R](using CanChoose[R]): T = + def each[R](using CanChoose[R]): T = val cs = choices suspend() cs.next.getOrElse(break(None)) @@ -75,10 +75,10 @@ def choices[T](body: CanChoose[T] ?=> T): Choices[T] = new Choices: val x = Ref(Choices(1, -2, -3)) val y = Ref(Choices("ab", "cde")) choices: - val xx = x.all + val xx = x.each xx + ( - if xx > 0 then y.all.length * x.all - else y.all.length + if xx > 0 then y.each.length * x.each + else y.each.length ) /* Gives the results of the following computations: 1 + 2 * 1 From b8455012a6dec8b806621f07291845a874353bd5 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 23 Jan 2023 12:41:19 +0100 Subject: [PATCH 04/52] Variant: Wrap suspension instead in break result Instead of having an additional value be a part of a suspension, allow to wrap the suspension with a wrapper function passed to `suspend`. This saves one type parameter on `Suspension` and gives more flexibility in what gets returned to a boundary. --- tests/pos/choices.scala | 30 ++++++++++++++++++------------ tests/pos/futures.scala | 27 +++++++++++++++------------ 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/tests/pos/choices.scala b/tests/pos/choices.scala index d9fa9adda094..7e2a47871900 100644 --- a/tests/pos/choices.scala +++ b/tests/pos/choices.scala @@ -2,18 +2,24 @@ import collection.mutable.ListBuffer import compiletime.uninitialized import scala.util.boundary, boundary.{Label, break} -/** Contains a delimited contination, which can be invoked with `run`, - * plus some other value that is returned from a `suspend`. - */ -case class Suspension[+T, +R](x: T): +/** Contains a delimited contination, which can be invoked with `r`esume` */ +class Suspension[+R]: def resume(): R = ??? +object Suspension: + def apply[R](): Suspension[R] = + ??? // magic, can be called only from `suspend` -/** Returns `Suspension(x)` to the boundary associated with the given label */ -def suspend[T, R](x: T)(using Label[Suspension[T, R]]): Unit = - break(Suspension(x)) +/** 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]): Unit = + ??? // break(fn(Suspension())) -def suspend[T, R]()(using Label[Suspension[Unit, R]]): Unit = - suspend(()) +/** 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) /** A single method iterator */ abstract class Choices[+T]: @@ -32,7 +38,7 @@ end Choices * - 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[Unit, Option[T]]] +type CanChoose[T] = Label[None.type | Suspension[Option[T]]] /** A variable representing Choices */ class Ref[T](choices: => Choices[T]): @@ -53,7 +59,7 @@ end Ref * @return all results in a new `Choices` iterator, */ def choices[T](body: CanChoose[T] ?=> T): Choices[T] = new Choices: - var stack: List[Suspension[Unit, Option[T]]] = Nil + var stack: List[Suspension[Option[T]]] = Nil def next: Option[T] = boundary(Some(body)) match @@ -66,7 +72,7 @@ def choices[T](body: CanChoose[T] ?=> T): Choices[T] = new Choices: // last variable's choices exhausted; use next value of previous variable stack = stack.tail stack.head.resume() - case susp: Suspension[Unit, Option[T]] => + case susp: Suspension[Option[T]] => // A new Choices variable was encountered stack = susp :: stack stack.head.resume() diff --git a/tests/pos/futures.scala b/tests/pos/futures.scala index 80434b2f4ded..8ed5d44ae200 100644 --- a/tests/pos/futures.scala +++ b/tests/pos/futures.scala @@ -5,18 +5,21 @@ import scala.util.boundary, boundary.{Label, break} object Scheduler: def schedule(task: Runnable): Unit = ??? -/** Contains a delimited contination, which can be invoked with `run`, - * plus some other value that is returned from a `suspend`. - */ -case class Suspension[+T, +R](x: T): +/** Contains a delimited contination, which can be invoked with `resume` */ +class Suspension[+R]: def resume(): R = ??? +object Suspension: + def apply[R](): Suspension[R] = + ??? // magic, can be called only from `suspend` -/** Returns `Suspension(x)` to the boundary associated with the given label */ -def suspend[T, R](x: T)(using Label[Suspension[T, R]]): Unit = - break(Suspension(x)) +/** 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]): Unit = + break(fn(Suspension())) -/** A suspension indicating the Future for which it is waiting */ -type Waiting = Suspension[Future[?], Unit] +/** 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] @@ -30,7 +33,7 @@ class Future[+T] private (): def await(using CanWait): T = result match case Some(x) => x case None => - suspend(this) + suspend((s: Suspension[Unit]) => (s, this)) await object Future: @@ -47,8 +50,8 @@ object Future: boundary[Unit | Waiting]: complete(f, body) match - case s @ Suspension(blocking) => - blocking.waiting += (() => s.resume()) + case (suspension, blocking) => + blocking.waiting += (() => suspension.resume()) case () => f From 8ac3a887818ce854b9239cff8a0bcf8586403c4b Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 1 Feb 2023 14:49:19 +0100 Subject: [PATCH 05/52] Add alternative futures test --- tests/pos/futuresALT.scala | 65 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/pos/futuresALT.scala diff --git a/tests/pos/futuresALT.scala b/tests/pos/futuresALT.scala new file mode 100644 index 000000000000..b9c42e58fb6e --- /dev/null +++ b/tests/pos/futuresALT.scala @@ -0,0 +1,65 @@ +import scala.collection.mutable.ListBuffer +import scala.util.boundary, boundary.{Label, break} +import scala.annotation.unchecked.uncheckedVariance + +// Alternative version of Futures following the design of @b-studios + +/** A hypthetical task scheduler */ +object Scheduler: + def schedule(task: Runnable): Unit = ??? + +/** Contains a delimited contination, which can be invoked with `resume` */ +class Suspension[-T, +R]: + def resume(arg: T): R = ??? + +def suspend[T, R](body: Suspension[T, R] ?=> R)(using Label[R]): T = ??? + +trait Async: + def await[T](f: Future[T]): T + +class Future[+T] private(): + private var result: Option[T] = None + private var waiting: ListBuffer[T => Unit] @uncheckedVariance = + // Variance can be ignored since `waiting` is only accessed from `Future.complete` + // and `complete` is only called from `Future.apply` with the exact result type + // with which is was created. + ListBuffer() + def await(using a: Async): T = a.await(this) + +object Future: + private def complete[T](f: Future[T], value: T): Unit = + f.result = Some(value) + for wf <- f.waiting do + Scheduler.schedule(() => wf(value)) + f.waiting = ListBuffer() + + // a handler for Async + def async[T](body: Async ?=> Unit): Unit = + boundary [Unit]: + given Async with + def await[T](f: Future[T]): T = f.result match + case Some(x) => x + case None => suspend[T, Unit]: s ?=> + f.waiting += (v => s.resume(v)) + f.await + body + + def apply[T](body: Async ?=> T): Future[T] = + val f = new Future[T] + Scheduler.schedule: () => + async(complete(f, body)) + f + +end Future + +def Test(x: Future[Int], xs: List[Future[Int]])(using Async) = + Future: + x.await + xs.map(_.await).sum + + + + + + + + From cca4eb60d99f693489e5237f680f113ce8fa5e12 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 1 Feb 2023 15:28:28 +0100 Subject: [PATCH 06/52] Fix test --- tests/pos/futuresALT.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/pos/futuresALT.scala b/tests/pos/futuresALT.scala index b9c42e58fb6e..69af3e895b66 100644 --- a/tests/pos/futuresALT.scala +++ b/tests/pos/futuresALT.scala @@ -39,8 +39,9 @@ object Future: given Async with def await[T](f: Future[T]): T = f.result match case Some(x) => x - case None => suspend[T, Unit]: s ?=> - f.waiting += (v => s.resume(v)) + case None => + suspend[T, Unit]: s ?=> + f.waiting += (v => s.resume(v)) f.await body From fddf3b6ad8a71ad5ee586c30a54ce4a6ca63a7f0 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 1 Feb 2023 15:34:09 +0100 Subject: [PATCH 07/52] Use general design of `FuturesALT` but with unit-valued suspend --- tests/pos/futuresALT2.scala | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/pos/futuresALT2.scala diff --git a/tests/pos/futuresALT2.scala b/tests/pos/futuresALT2.scala new file mode 100644 index 000000000000..e16b698e0031 --- /dev/null +++ b/tests/pos/futuresALT2.scala @@ -0,0 +1,61 @@ +import scala.collection.mutable.ListBuffer +import scala.util.boundary, boundary.{Label, break} +import scala.annotation.unchecked.uncheckedVariance + +// Alternative version of Futures following the design of @b-studios + +/** 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 = ??? + +def suspend[R](body: Suspension[R] => R)(using Label[R]): Unit = ??? + +trait Async: + def await[T](f: Future[T]): T + +class Future[+T] private(): + private var result: Option[T] = None + private var waiting: ListBuffer[Runnable] = ListBuffer() + def await(using a: Async): T = a.await(this) + +object Future: + private def complete[T](f: Future[T], value: T): Unit = + f.result = Some(value) + f.waiting.foreach(Scheduler.schedule) + f.waiting = ListBuffer() + + // a handler for Async + def async[T](body: Async ?=> Unit): Unit = + boundary [Unit]: + given Async with + def await[T](f: Future[T]): T = f.result match + case Some(x) => x + case None => + suspend[Unit]: s => + f.waiting += (() => s.resume()) + f.await + body + + def apply[T](body: Async ?=> T): Future[T] = + val f = new Future[T] + Scheduler.schedule: () => + async(complete(f, body)) + f + +end Future + +def Test(x: Future[Int], xs: List[Future[Int]])(using Async) = + Future: + x.await + xs.map(_.await).sum + + + + + + + + From be5876495495d9630b45588ff8670ba1f3535e58 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 1 Feb 2023 15:40:12 +0100 Subject: [PATCH 08/52] Drop redundant parameter --- tests/pos/futuresALT.scala | 2 +- tests/pos/futuresALT2.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pos/futuresALT.scala b/tests/pos/futuresALT.scala index 69af3e895b66..090b73e1420f 100644 --- a/tests/pos/futuresALT.scala +++ b/tests/pos/futuresALT.scala @@ -53,7 +53,7 @@ object Future: end Future -def Test(x: Future[Int], xs: List[Future[Int]])(using Async) = +def Test(x: Future[Int], xs: List[Future[Int]]) = Future: x.await + xs.map(_.await).sum diff --git a/tests/pos/futuresALT2.scala b/tests/pos/futuresALT2.scala index e16b698e0031..21525d325984 100644 --- a/tests/pos/futuresALT2.scala +++ b/tests/pos/futuresALT2.scala @@ -48,7 +48,7 @@ object Future: end Future -def Test(x: Future[Int], xs: List[Future[Int]])(using Async) = +def Test(x: Future[Int], xs: List[Future[Int]]) = Future: x.await + xs.map(_.await).sum From ec94fc677ba3f8572101fc7345c6ac26deb62068 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 1 Feb 2023 16:57:11 +0100 Subject: [PATCH 09/52] Simplified alternative version --- tests/pos/futuresALT.scala | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/pos/futuresALT.scala b/tests/pos/futuresALT.scala index 090b73e1420f..85872b81d46f 100644 --- a/tests/pos/futuresALT.scala +++ b/tests/pos/futuresALT.scala @@ -12,7 +12,7 @@ object Scheduler: class Suspension[-T, +R]: def resume(arg: T): R = ??? -def suspend[T, R](body: Suspension[T, R] ?=> R)(using Label[R]): T = ??? +def suspend[T, R](body: Suspension[T, R] => R)(using Label[R]): T = ??? trait Async: def await[T](f: Future[T]): T @@ -29,26 +29,22 @@ class Future[+T] private(): object Future: private def complete[T](f: Future[T], value: T): Unit = f.result = Some(value) - for wf <- f.waiting do - Scheduler.schedule(() => wf(value)) + for resumption <- f.waiting do + Scheduler.schedule(() => resumption(value)) f.waiting = ListBuffer() // a handler for Async - def async[T](body: Async ?=> Unit): Unit = + def async(body: Async ?=> Unit): Unit = boundary [Unit]: given Async with def await[T](f: Future[T]): T = f.result match case Some(x) => x - case None => - suspend[T, Unit]: s ?=> - f.waiting += (v => s.resume(v)) - f.await + case None => suspend[T, Unit](s => f.waiting += s.resume) body def apply[T](body: Async ?=> T): Future[T] = val f = new Future[T] - Scheduler.schedule: () => - async(complete(f, body)) + Scheduler.schedule(() => async(complete(f, body))) f end Future From 350cb3852e12a41453405513b99e20f35fae61d9 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 2 Feb 2023 12:05:02 +0100 Subject: [PATCH 10/52] Reorganize; add monadic reflection We now have in suspend-strawman-1: futures and choice in the original version suspend-strawman-2: futures, choices, and monadic reflection according to Jonathan's design --- tests/pos/futuresALT2.scala | 61 ------------------- .../{ => suspend-strawman-1}/choices.scala | 21 +------ .../{ => suspend-strawman-1}/futures.scala | 25 ++------ tests/pos/suspend-strawman-1/runtime.scala | 21 +++++++ tests/pos/suspend-strawman-2/choices.scala | 28 +++++++++ .../futures.scala} | 15 +---- .../suspend-strawman-2/monadic-reflect.scala | 57 +++++++++++++++++ tests/pos/suspend-strawman-2/runtime.scala | 12 ++++ 8 files changed, 125 insertions(+), 115 deletions(-) delete mode 100644 tests/pos/futuresALT2.scala rename tests/pos/{ => suspend-strawman-1}/choices.scala (77%) rename tests/pos/{ => suspend-strawman-1}/futures.scala (64%) create mode 100644 tests/pos/suspend-strawman-1/runtime.scala create mode 100644 tests/pos/suspend-strawman-2/choices.scala rename tests/pos/{futuresALT.scala => suspend-strawman-2/futures.scala} (75%) create mode 100644 tests/pos/suspend-strawman-2/monadic-reflect.scala create mode 100644 tests/pos/suspend-strawman-2/runtime.scala diff --git a/tests/pos/futuresALT2.scala b/tests/pos/futuresALT2.scala deleted file mode 100644 index 21525d325984..000000000000 --- a/tests/pos/futuresALT2.scala +++ /dev/null @@ -1,61 +0,0 @@ -import scala.collection.mutable.ListBuffer -import scala.util.boundary, boundary.{Label, break} -import scala.annotation.unchecked.uncheckedVariance - -// Alternative version of Futures following the design of @b-studios - -/** 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 = ??? - -def suspend[R](body: Suspension[R] => R)(using Label[R]): Unit = ??? - -trait Async: - def await[T](f: Future[T]): T - -class Future[+T] private(): - private var result: Option[T] = None - private var waiting: ListBuffer[Runnable] = ListBuffer() - def await(using a: Async): T = a.await(this) - -object Future: - private def complete[T](f: Future[T], value: T): Unit = - f.result = Some(value) - f.waiting.foreach(Scheduler.schedule) - f.waiting = ListBuffer() - - // a handler for Async - def async[T](body: Async ?=> Unit): Unit = - boundary [Unit]: - given Async with - def await[T](f: Future[T]): T = f.result match - case Some(x) => x - case None => - suspend[Unit]: s => - f.waiting += (() => s.resume()) - f.await - body - - def apply[T](body: Async ?=> T): Future[T] = - val f = new Future[T] - Scheduler.schedule: () => - async(complete(f, body)) - f - -end Future - -def Test(x: Future[Int], xs: List[Future[Int]]) = - Future: - x.await + xs.map(_.await).sum - - - - - - - - diff --git a/tests/pos/choices.scala b/tests/pos/suspend-strawman-1/choices.scala similarity index 77% rename from tests/pos/choices.scala rename to tests/pos/suspend-strawman-1/choices.scala index 7e2a47871900..b969a1367116 100644 --- a/tests/pos/choices.scala +++ b/tests/pos/suspend-strawman-1/choices.scala @@ -1,25 +1,6 @@ import collection.mutable.ListBuffer -import compiletime.uninitialized import scala.util.boundary, boundary.{Label, break} - -/** Contains a delimited contination, which can be invoked with `r`esume` */ -class Suspension[+R]: - def resume(): R = ??? -object Suspension: - def apply[R](): Suspension[R] = - ??? // magic, can be called only from `suspend` - -/** 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]): Unit = - ??? // break(fn(Suspension())) - -/** 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) +import runtime.* /** A single method iterator */ abstract class Choices[+T]: diff --git a/tests/pos/futures.scala b/tests/pos/suspend-strawman-1/futures.scala similarity index 64% rename from tests/pos/futures.scala rename to tests/pos/suspend-strawman-1/futures.scala index 8ed5d44ae200..54a569c5cce3 100644 --- a/tests/pos/futures.scala +++ b/tests/pos/suspend-strawman-1/futures.scala @@ -1,22 +1,6 @@ import collection.mutable.ListBuffer import scala.util.boundary, boundary.{Label, break} - -/** 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 = ??? -object Suspension: - def apply[R](): Suspension[R] = - ??? // magic, can be called only from `suspend` - -/** 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]): Unit = - break(fn(Suspension())) +import runtime.* /** A suspension and a value indicating the Future for which the suspension is waiting */ type Waiting = (Suspension[Unit], Future[?]) @@ -44,7 +28,7 @@ object Future: f.waiting = ListBuffer() /** Create future that eventually returns the result of `op` */ - def apply[T](body: => T): Future[T] = + def apply[T](body: CanWait ?=> T): Future[T] = val f = new Future[T] Scheduler.schedule: () => boundary[Unit | Waiting]: @@ -55,10 +39,9 @@ object Future: case () => f -def Test(x1: Future[Int], x2: Future[Int])(using CanWait) = +def Test(x: Future[Int], xs: List[Future[Int]]) = Future: - x1.await + x2.await - + x.await + xs.map(_.await).sum diff --git a/tests/pos/suspend-strawman-1/runtime.scala b/tests/pos/suspend-strawman-1/runtime.scala new file mode 100644 index 000000000000..e021f317de25 --- /dev/null +++ b/tests/pos/suspend-strawman-1/runtime.scala @@ -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) \ No newline at end of file diff --git a/tests/pos/suspend-strawman-2/choices.scala b/tests/pos/suspend-strawman-2/choices.scala new file mode 100644 index 000000000000..7d8ec373b0bb --- /dev/null +++ b/tests/pos/suspend-strawman-2/choices.scala @@ -0,0 +1,28 @@ +import scala.util.boundary, boundary.Label +import runtime.* + +trait Choice: + def choose[A](choices: A*): A + +// the handler +def choices[T](body: Choice ?=> T): Seq[T] = + boundary[Seq[T]]: + given Choice with + def choose[A](choices: A*): A = + suspend[A, Seq[T]](s => choices.flatMap(s.resume)) + Seq(body) + +def choose[A](choices: A*)(using c: Choice): A = c.choose(choices*) + +@main def test: Seq[Int] = + choices: + def x = choose(1, -2, -3) + def y = choose("ab", "cde") + val xx = x; + xx + ( + if xx > 0 then + val z = choose(xx / 2, xx * 2) + y.length * z + else y.length + ) + diff --git a/tests/pos/futuresALT.scala b/tests/pos/suspend-strawman-2/futures.scala similarity index 75% rename from tests/pos/futuresALT.scala rename to tests/pos/suspend-strawman-2/futures.scala index 85872b81d46f..c893f9dd59be 100644 --- a/tests/pos/futuresALT.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -1,18 +1,7 @@ import scala.collection.mutable.ListBuffer -import scala.util.boundary, boundary.{Label, break} +import scala.util.boundary, boundary.Label import scala.annotation.unchecked.uncheckedVariance - -// Alternative version of Futures following the design of @b-studios - -/** A hypthetical task scheduler */ -object Scheduler: - def schedule(task: Runnable): Unit = ??? - -/** Contains a delimited contination, which can be invoked with `resume` */ -class Suspension[-T, +R]: - def resume(arg: T): R = ??? - -def suspend[T, R](body: Suspension[T, R] => R)(using Label[R]): T = ??? +import runtime.* trait Async: def await[T](f: Future[T]): T diff --git a/tests/pos/suspend-strawman-2/monadic-reflect.scala b/tests/pos/suspend-strawman-2/monadic-reflect.scala new file mode 100644 index 000000000000..f2d2a6266cdf --- /dev/null +++ b/tests/pos/suspend-strawman-2/monadic-reflect.scala @@ -0,0 +1,57 @@ +import scala.util.boundary +import runtime.* + +trait Monad[F[_]]: + + /** The unit value for a monad */ + def pure[A](x: A): F[A] + + extension [A](x: F[A]) + /** The fundamental composition operation */ + def flatMap[B](f: A => F[B]): F[B] + + /** The `map` operation can now be defined in terms of `flatMap` */ + def map[B](f: A => B) = x.flatMap(f.andThen(pure)) + +end Monad + +trait CanReflect[M[_], R] { + def reflect(mr: M[R])(using Monad[M]): R +} + +trait Monadic[M[_]: Monad]: + + /** + * Embedding of pure values into the monad M + */ + def pure[A](a: A): M[A] + + /** + * Sequencing of monadic values + * + * Implementations are required to implement sequencing in a stack-safe + * way, that is they either need to implement trampolining on their own + * or implement `sequence` as a tail recursive function. + * + * Actually the type X can be different for every call to f... + * It is a type aligned sequence, but for simplicity we do not enforce this + * here. + */ + def sequence[X, R](init: M[X])(f: X => Either[M[X], M[R]]): M[R] + + /** + * Helper to summon and use an instance of CanReflect[M] + */ + def reflect[R](mr: M[R])(using r: CanReflect[M, R]): R = r.reflect(mr) + + /** + * Reify a computation into a monadic value + */ + def reify[R](prog: CanReflect[M, R] ?=> R): M[R] = + boundary [M[R]]: + given CanReflect[M, R] with + def reflect(mr: M[R])(using Monad[M]): R = + suspend [R, M[R]] (s => mr.flatMap(s.resume)) + pure(prog) + +end Monadic \ No newline at end of file diff --git a/tests/pos/suspend-strawman-2/runtime.scala b/tests/pos/suspend-strawman-2/runtime.scala new file mode 100644 index 000000000000..ff86252ec550 --- /dev/null +++ b/tests/pos/suspend-strawman-2/runtime.scala @@ -0,0 +1,12 @@ +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[-T, +R]: + def resume(arg: T): R = ??? + + def suspend[T, R](body: Suspension[T, R] => R)(using Label[R]): T = ??? From 5845108f7f9d4bf50ed141b6ae87402d61887916 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 3 Feb 2023 09:50:36 +0100 Subject: [PATCH 11/52] Move context bound to enclosing trait --- tests/pos/suspend-strawman-2/monadic-reflect.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/pos/suspend-strawman-2/monadic-reflect.scala b/tests/pos/suspend-strawman-2/monadic-reflect.scala index f2d2a6266cdf..bdd8c1868cca 100644 --- a/tests/pos/suspend-strawman-2/monadic-reflect.scala +++ b/tests/pos/suspend-strawman-2/monadic-reflect.scala @@ -15,9 +15,8 @@ trait Monad[F[_]]: end Monad -trait CanReflect[M[_], R] { - def reflect(mr: M[R])(using Monad[M]): R -} +trait CanReflect[M[_]: Monad, R]: + def reflect(mr: M[R]): R trait Monadic[M[_]: Monad]: @@ -50,7 +49,7 @@ trait Monadic[M[_]: Monad]: def reify[R](prog: CanReflect[M, R] ?=> R): M[R] = boundary [M[R]]: given CanReflect[M, R] with - def reflect(mr: M[R])(using Monad[M]): R = + def reflect(mr: M[R]): R = suspend [R, M[R]] (s => mr.flatMap(s.resume)) pure(prog) From 8ff7966ad86f494ad0627c18efd758489a5b0c93 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 3 Feb 2023 14:16:01 +0100 Subject: [PATCH 12/52] Drop Monad bound on CanReflect --- tests/pos/suspend-strawman-2/monadic-reflect.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/suspend-strawman-2/monadic-reflect.scala b/tests/pos/suspend-strawman-2/monadic-reflect.scala index bdd8c1868cca..4413280e11fa 100644 --- a/tests/pos/suspend-strawman-2/monadic-reflect.scala +++ b/tests/pos/suspend-strawman-2/monadic-reflect.scala @@ -15,7 +15,7 @@ trait Monad[F[_]]: end Monad -trait CanReflect[M[_]: Monad, R]: +trait CanReflect[M[_], R]: def reflect(mr: M[R]): R trait Monadic[M[_]: Monad]: From 3008df558f722de3fa6ee96f3e27d1f4f1c0b25d Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 3 Feb 2023 14:35:29 +0100 Subject: [PATCH 13/52] Make reflect polymorphic --- tests/pos/suspend-strawman-2/monadic-reflect.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/pos/suspend-strawman-2/monadic-reflect.scala b/tests/pos/suspend-strawman-2/monadic-reflect.scala index 4413280e11fa..c74fe779135f 100644 --- a/tests/pos/suspend-strawman-2/monadic-reflect.scala +++ b/tests/pos/suspend-strawman-2/monadic-reflect.scala @@ -15,8 +15,8 @@ trait Monad[F[_]]: end Monad -trait CanReflect[M[_], R]: - def reflect(mr: M[R]): R +trait CanReflect[M[_]]: + def reflect[R](mr: M[R]): R trait Monadic[M[_]: Monad]: @@ -41,16 +41,16 @@ trait Monadic[M[_]: Monad]: /** * Helper to summon and use an instance of CanReflect[M] */ - def reflect[R](mr: M[R])(using r: CanReflect[M, R]): R = r.reflect(mr) + def reflect[R](mr: M[R])(using r: CanReflect[M]): R = r.reflect(mr) /** * Reify a computation into a monadic value */ - def reify[R](prog: CanReflect[M, R] ?=> R): M[R] = + def reify[R](prog: CanReflect[M] ?=> R): M[R] = boundary [M[R]]: - given CanReflect[M, R] with - def reflect(mr: M[R]): R = - suspend [R, M[R]] (s => mr.flatMap(s.resume)) + given CanReflect[M] with + def reflect[R2](mr: M[R2]): R2 = + suspend [R2, M[R]] (s => mr.flatMap(s.resume)) pure(prog) end Monadic \ No newline at end of file From 690fd11dbdc706f2b65be78b1e97ac9c7cc7e7ad Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 4 Feb 2023 18:26:00 +0100 Subject: [PATCH 14/52] Setup enriched futures Original kept in simple-futures.scala. It is changed so that @uncheckedVariance is no longer necessary. futures.scala will be stepwide extended to resemble production futures. --- tests/pos/suspend-strawman-2/futures.scala | 38 +++++++------- tests/pos/suspend-strawman-2/runtime.scala | 18 ++++--- .../suspend-strawman-2/simple-futures.scala | 50 +++++++++++++++++++ 3 files changed, 78 insertions(+), 28 deletions(-) create mode 100644 tests/pos/suspend-strawman-2/simple-futures.scala diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index c893f9dd59be..7ef3b8c923d2 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -1,41 +1,39 @@ +package futures + import scala.collection.mutable.ListBuffer import scala.util.boundary, boundary.Label -import scala.annotation.unchecked.uncheckedVariance import runtime.* trait Async: def await[T](f: Future[T]): T -class Future[+T] private(): +class Future[+T](body: Async ?=> T): private var result: Option[T] = None - private var waiting: ListBuffer[T => Unit] @uncheckedVariance = - // Variance can be ignored since `waiting` is only accessed from `Future.complete` - // and `complete` is only called from `Future.apply` with the exact result type - // with which is was created. - ListBuffer() - def await(using a: Async): T = a.await(this) + private var waiting: ListBuffer[T => Unit] = ListBuffer() + private def addWaiting(k: T => Unit): Unit = waiting += k -object Future: - private def complete[T](f: Future[T], value: T): Unit = - f.result = Some(value) - for resumption <- f.waiting do - Scheduler.schedule(() => resumption(value)) - f.waiting = ListBuffer() + def await(using a: Async): T = a.await(this) // a handler for Async - def async(body: Async ?=> Unit): Unit = + private def async(body: Async ?=> Unit): Unit = boundary [Unit]: given Async with def await[T](f: Future[T]): T = f.result match case Some(x) => x - case None => suspend[T, Unit](s => f.waiting += s.resume) + case None => suspend[T, Unit](s => f.addWaiting(s.resume)) body - def apply[T](body: Async ?=> T): Future[T] = - val f = new Future[T] - Scheduler.schedule(() => async(complete(f, body))) - f + private def complete(): Unit = + async: + val value = body + val result = Some(value) + for k <- waiting do + Scheduler.schedule(() => k(value)) + waiting.clear() + + Scheduler.schedule(() => complete()) +object Future end Future def Test(x: Future[Int], xs: List[Future[Int]]) = diff --git a/tests/pos/suspend-strawman-2/runtime.scala b/tests/pos/suspend-strawman-2/runtime.scala index ff86252ec550..aa9aa7fd26c4 100644 --- a/tests/pos/suspend-strawman-2/runtime.scala +++ b/tests/pos/suspend-strawman-2/runtime.scala @@ -1,12 +1,14 @@ +package runtime import scala.util.boundary, boundary.Label -object runtime: - /** A hypthetical task scheduler */ - object Scheduler: - def schedule(task: Runnable): Unit = ??? +/** A hypothetical task scheduler trait */ +trait Scheduler: + def schedule(task: Runnable): Unit = ??? - /** Contains a delimited contination, which can be invoked with `resume` */ - class Suspension[-T, +R]: - def resume(arg: T): R = ??? +object Scheduler extends Scheduler - def suspend[T, R](body: Suspension[T, R] => R)(using Label[R]): T = ??? +/** Contains a delimited contination, which can be invoked with `resume` */ +class Suspension[-T, +R]: + def resume(arg: T): R = ??? + +def suspend[T, R](body: Suspension[T, R] => R)(using Label[R]): T = ??? diff --git a/tests/pos/suspend-strawman-2/simple-futures.scala b/tests/pos/suspend-strawman-2/simple-futures.scala new file mode 100644 index 000000000000..b0b90db4adec --- /dev/null +++ b/tests/pos/suspend-strawman-2/simple-futures.scala @@ -0,0 +1,50 @@ +package simpleFutures + +import scala.collection.mutable.ListBuffer +import scala.util.boundary, boundary.Label +import runtime.* + +trait Async: + def await[T](f: Future[T]): T + +class Future[+T](body: Async ?=> T): + private var result: Option[T] = None + private var waiting: ListBuffer[T => Unit] = ListBuffer() + private def addWaiting(k: T => Unit): Unit = waiting += k + + def await(using a: Async): T = a.await(this) + + private def complete(): Unit = + Future.async: + val value = body + val result = Some(value) + for k <- waiting do + Scheduler.schedule(() => k(value)) + waiting.clear() + + Scheduler.schedule(() => complete()) + +object Future: + + // a handler for Async + def async(body: Async ?=> Unit): Unit = + boundary [Unit]: + given Async with + def await[T](f: Future[T]): T = f.result match + case Some(x) => x + case None => suspend[T, Unit](s => f.addWaiting(s.resume)) + body + +end Future + +def Test(x: Future[Int], xs: List[Future[Int]]) = + Future: + x.await + xs.map(_.await).sum + + + + + + + + From c5aac2ad983215f46fe978170c1d82e9580755ce Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 4 Feb 2023 18:50:30 +0100 Subject: [PATCH 15/52] Add exception handling Collect thrown exceptions in a Try and rethrow on await --- tests/pos/suspend-strawman-2/futures.scala | 47 ++++++++++++++++------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 7ef3b8c923d2..110f3feab96e 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -2,38 +2,59 @@ package futures import scala.collection.mutable.ListBuffer import scala.util.boundary, boundary.Label +import scala.compiletime.uninitialized +import scala.util.{Try, Success, Failure} import runtime.* trait Async: - def await[T](f: Future[T]): T + + /** Wait for completion of future `f`. + */ + def await[T](f: Future[T]): Try[T] + +end Async class Future[+T](body: Async ?=> T): - private var result: Option[T] = None - private var waiting: ListBuffer[T => Unit] = ListBuffer() - private def addWaiting(k: T => Unit): Unit = waiting += k + import Future.Status, Status.* + + private var status: Status = Started + private var result: Try[T] = uninitialized + private var waiting: ListBuffer[Try[T] => Unit] = ListBuffer() - def await(using a: Async): T = a.await(this) + private def addWaiting(k: Try[T] => Unit): Unit = + waiting += k + + /** Wait for this future to be completed, return its value in case of success, + * or rethrow exception in case of failure. + */ + def await(using a: Async): T = a.await(this).get // a handler for Async private def async(body: Async ?=> Unit): Unit = boundary [Unit]: given Async with - def await[T](f: Future[T]): T = f.result match - case Some(x) => x - case None => suspend[T, Unit](s => f.addWaiting(s.resume)) + def await[T](f: Future[T]): Try[T] = f.status match + case Started => + suspend[Try[T], Unit](s => f.addWaiting(s.resume)) + case Completed => + f.result body private def complete(): Unit = async: - val value = body - val result = Some(value) + val result = + try Success(body) + catch case ex: Exception => Failure(ex) + status = Completed for k <- waiting do - Scheduler.schedule(() => k(value)) + Scheduler.schedule(() => k(result)) waiting.clear() - + Scheduler.schedule(() => complete()) -object Future +object Future: + enum Status: + case Started, Completed end Future def Test(x: Future[Int], xs: List[Future[Int]]) = From 6bb716b46ed9c6d8c6af36571367007d4b539b92 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 4 Feb 2023 18:59:41 +0100 Subject: [PATCH 16/52] Make futures lazy --- tests/pos/suspend-strawman-2/futures.scala | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 110f3feab96e..d088ae859402 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -8,7 +8,9 @@ import runtime.* trait Async: - /** Wait for completion of future `f`. + /** Wait for completion of future `f`. This means: + * - ensure that computing `f` has started + * - wait for the completion and return the completed Try */ def await[T](f: Future[T]): Try[T] @@ -34,6 +36,9 @@ class Future[+T](body: Async ?=> T): boundary [Unit]: given Async with def await[T](f: Future[T]): Try[T] = f.status match + case Initial => + f.start() + await(f) case Started => suspend[Try[T], Unit](s => f.addWaiting(s.resume)) case Completed => @@ -49,17 +54,23 @@ class Future[+T](body: Async ?=> T): for k <- waiting do Scheduler.schedule(() => k(result)) waiting.clear() - - Scheduler.schedule(() => complete()) + + /** Start future's execution */ + def start(): this.type = + if status == Initial then + Scheduler.schedule(() => complete()) + status = Started + this object Future: enum Status: - case Started, Completed + case Initial, Started, Completed end Future def Test(x: Future[Int], xs: List[Future[Int]]) = Future: x.await + xs.map(_.await).sum + .start() From a52c47571a2226111f96391484f43485608794d3 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 4 Feb 2023 19:07:26 +0100 Subject: [PATCH 17/52] Handle possible race conditions --- tests/pos/suspend-strawman-2/futures.scala | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index d088ae859402..4781363fac42 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -19,12 +19,13 @@ end Async class Future[+T](body: Async ?=> T): import Future.Status, Status.* - private var status: Status = Started + @volatile private var status: Status = Initial private var result: Try[T] = uninitialized private var waiting: ListBuffer[Try[T] => Unit] = ListBuffer() - private def addWaiting(k: Try[T] => Unit): Unit = - waiting += k + private def addWaiting(k: Try[T] => Unit): Unit = synchronized: + if status == Completed then k(result) + else waiting += k /** Wait for this future to be completed, return its value in case of success, * or rethrow exception in case of failure. @@ -57,9 +58,10 @@ class Future[+T](body: Async ?=> T): /** Start future's execution */ def start(): this.type = - if status == Initial then - Scheduler.schedule(() => complete()) - status = Started + synchronized: + if status == Initial then + Scheduler.schedule(() => complete()) + status = Started this object Future: From f2db134e472619bc14f27c9aa9ce7236d94a8216 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 4 Feb 2023 19:10:00 +0100 Subject: [PATCH 18/52] Make scheduler a parameter The scheduler to use for a future gets passed when the future is started. --- tests/pos/suspend-strawman-2/futures.scala | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 4781363fac42..2b563e84b012 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -22,6 +22,7 @@ class Future[+T](body: Async ?=> T): @volatile private var status: Status = Initial private var result: Try[T] = uninitialized private var waiting: ListBuffer[Try[T] => Unit] = ListBuffer() + private var scheduler: Scheduler = uninitialized private def addWaiting(k: Try[T] => Unit): Unit = synchronized: if status == Completed then k(result) @@ -38,10 +39,13 @@ class Future[+T](body: Async ?=> T): given Async with def await[T](f: Future[T]): Try[T] = f.status match case Initial => - f.start() + f.start()(using scheduler) await(f) case Started => - suspend[Try[T], Unit](s => f.addWaiting(s.resume)) + suspend[Try[T], Unit]: s => + f.addWaiting: result => + scheduler.schedule: () => + s.resume(result) case Completed => f.result body @@ -52,15 +56,15 @@ class Future[+T](body: Async ?=> T): try Success(body) catch case ex: Exception => Failure(ex) status = Completed - for k <- waiting do - Scheduler.schedule(() => k(result)) + waiting.foreach(_(result)) waiting.clear() /** Start future's execution */ - def start(): this.type = + def start()(using scheduler: Scheduler): this.type = + this.scheduler = scheduler synchronized: if status == Initial then - Scheduler.schedule(() => complete()) + scheduler.schedule(() => complete()) status = Started this @@ -69,7 +73,7 @@ object Future: case Initial, Started, Completed end Future -def Test(x: Future[Int], xs: List[Future[Int]]) = +def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler) = Future: x.await + xs.map(_.await).sum .start() From 4f146c673d48cb351c1d8ef42e36ef73c47caeba Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 5 Feb 2023 10:10:37 +0100 Subject: [PATCH 19/52] Add Future.spawn to create futures that start immediately --- tests/pos/suspend-strawman-2/futures.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 2b563e84b012..3890f4a9dcdc 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -71,12 +71,14 @@ class Future[+T](body: Async ?=> T): object Future: enum Status: case Initial, Started, Completed + + def spawn[T](body: Async ?=> T)(using Scheduler): Future[T] = + Future(body).start() end Future -def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler) = - Future: +def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = + Future.spawn: x.await + xs.map(_.await).sum - .start() From 7969a3c82162d781f68dececc000821837096b7a Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 5 Feb 2023 12:03:37 +0100 Subject: [PATCH 20/52] Add simple cancellation --- tests/pos/suspend-strawman-2/futures.scala | 88 +++++++++++++++------- 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 3890f4a9dcdc..7fe81abe384e 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -17,7 +17,7 @@ trait Async: end Async class Future[+T](body: Async ?=> T): - import Future.Status, Status.* + import Future.{Status, Cancellation}, Status.* @volatile private var status: Status = Initial private var result: Try[T] = uninitialized @@ -28,6 +28,14 @@ class Future[+T](body: Async ?=> T): if status == Completed then k(result) else waiting += k + private def currentWaiting(): List[Try[T] => Unit] = synchronized: + val ws = waiting.toList + waiting.clear() + ws + + private def checkCancellation(): Unit = + if status == Cancelled then throw Cancellation() + /** Wait for this future to be completed, return its value in case of success, * or rethrow exception in case of failure. */ @@ -37,18 +45,33 @@ class Future[+T](body: Async ?=> T): private def async(body: Async ?=> Unit): Unit = boundary [Unit]: given Async with - def await[T](f: Future[T]): Try[T] = f.status match - case Initial => - f.start()(using scheduler) - await(f) - case Started => - suspend[Try[T], Unit]: s => - f.addWaiting: result => - scheduler.schedule: () => - s.resume(result) + + private def resultOption[T](f: Future[T]): Option[Try[T]] = f.status match + case Initial => + f.ensureStarted()(using scheduler) + resultOption(f) + case Started => + None case Completed => - f.result + Some(f.result) + case Cancelled => + Some(Failure(Cancellation())) + + private inline def cancelChecked[T](op: => T): T = + checkCancellation() + val res = op + checkCancellation() + res + + def await[T](f: Future[T]): Try[T] = + cancelChecked: + resultOption(f).getOrElse: + suspend[Try[T], Unit]: s => + f.addWaiting: result => + scheduler.schedule: () => + s.resume(result) body + end async private def complete(): Unit = async: @@ -56,22 +79,41 @@ class Future[+T](body: Async ?=> T): try Success(body) catch case ex: Exception => Failure(ex) status = Completed - waiting.foreach(_(result)) - waiting.clear() + for task <- currentWaiting() do task(result) + + /** Ensure future's execution has started */ + def ensureStarted()(using scheduler: Scheduler): this.type = + synchronized: + if status == Initial then start() + this - /** Start future's execution */ + /** Start future's execution + * @pre future has not yet started + */ def start()(using scheduler: Scheduler): this.type = - this.scheduler = scheduler synchronized: - if status == Initial then - scheduler.schedule(() => complete()) - status = Started + assert(status == Initial) + this.scheduler = scheduler + scheduler.schedule(() => complete()) + status = Started this + def cancel(): Unit = synchronized: + if status != Completed && status != Cancelled then + status = Cancelled + object Future: + + class Cancellation extends Exception + enum Status: - case Initial, Started, Completed + // Transitions always go left to right. + // Cancelled --> Completed with Failure(Cancellation()) result + case Initial, Started, Cancelled, Completed + /** Construct a future and start it so that ion runs in parallel with the + * current thread. + */ def spawn[T](body: Async ?=> T)(using Scheduler): Future[T] = Future(body).start() end Future @@ -79,11 +121,3 @@ end Future def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = Future.spawn: x.await + xs.map(_.await).sum - - - - - - - - From 5950febd221b7bfb9e6c4d10786dc6bc3ce85a3a Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 5 Feb 2023 18:46:20 +0100 Subject: [PATCH 21/52] Structured concurrency 1: Add future linking Allow a future to be linked to a parent. Completing or cancelling the parent entails cancelling all its linked children. --- tests/pos/suspend-strawman-2/futures.scala | 32 +++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 7fe81abe384e..69f513ccb675 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -1,6 +1,6 @@ package futures -import scala.collection.mutable.ListBuffer +import scala.collection.mutable, mutable.ListBuffer import scala.util.boundary, boundary.Label import scala.compiletime.uninitialized import scala.util.{Try, Success, Failure} @@ -14,6 +14,9 @@ trait Async: */ def await[T](f: Future[T]): Try[T] + /** The future computed by this async computation. */ + def client: Future[?] + end Async class Future[+T](body: Async ?=> T): @@ -23,6 +26,7 @@ class Future[+T](body: Async ?=> T): private var result: Try[T] = uninitialized private var waiting: ListBuffer[Try[T] => Unit] = ListBuffer() private var scheduler: Scheduler = uninitialized + private var children: mutable.Set[Future[?]] = mutable.Set() private def addWaiting(k: Try[T] => Unit): Unit = synchronized: if status == Completed then k(result) @@ -33,6 +37,11 @@ class Future[+T](body: Async ?=> T): waiting.clear() ws + private def currentChildren(): List[Future[?]] = synchronized: + val cs = children.toList + children.clear() + cs + private def checkCancellation(): Unit = if status == Cancelled then throw Cancellation() @@ -70,6 +79,10 @@ class Future[+T](body: Async ?=> T): f.addWaiting: result => scheduler.schedule: () => s.resume(result) + + def client = Future.this + end given + body end async @@ -80,6 +93,7 @@ class Future[+T](body: Async ?=> T): catch case ex: Exception => Failure(ex) status = Completed for task <- currentWaiting() do task(result) + cancelChildren() /** Ensure future's execution has started */ def ensureStarted()(using scheduler: Scheduler): this.type = @@ -98,9 +112,25 @@ class Future[+T](body: Async ?=> T): status = Started this + /** Links the future as a child to the current async client. + * This means the future will be cancelled when the async client + * completes. + */ + def linked(using async: Async): this.type = synchronized: + if status != Completed then + async.client.children += this + this + + private def cancelChildren(): Unit = + for f <- currentChildren() do f.cancel() + + /** Eventually stop computation of this future and fail with + * a `Cancellation` exception. Also cancel all linked children. + */ def cancel(): Unit = synchronized: if status != Completed && status != Cancelled then status = Cancelled + cancelChildren() object Future: From 2cf9f788f1f0375228d1f8e0ad3bee9e92a1d316 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 5 Feb 2023 18:51:59 +0100 Subject: [PATCH 22/52] Structured concurrency 2: parallel and alternative composition - Add awaitEither to wait for more than one future at the same time - Add conjunctions and disjunctions of futures. --- tests/pos/suspend-strawman-2/futures.scala | 52 +++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 69f513ccb675..11ef7eabe497 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -4,6 +4,7 @@ import scala.collection.mutable, mutable.ListBuffer import scala.util.boundary, boundary.Label import scala.compiletime.uninitialized import scala.util.{Try, Success, Failure} +import java.util.concurrent.atomic.AtomicBoolean import runtime.* trait Async: @@ -14,6 +15,12 @@ trait Async: */ def await[T](f: Future[T]): Try[T] + /** Wait for completion of the first of the futures `f1`, `f2` + * @return `Left(r1)` if `f1` completed first with `r1` + * `Right(r2)` if `f2` completed first with `r2` + */ + def awaitEither[T1, T2](f1: Future[T1], f2: Future[T2]): Either[Try[T1], Try[T2]] + /** The future computed by this async computation. */ def client: Future[?] @@ -80,9 +87,24 @@ class Future[+T](body: Async ?=> T): scheduler.schedule: () => s.resume(result) + def awaitEither[T1, T2](f1: Future[T1], f2: Future[T2]): Either[Try[T1], Try[T2]] = + cancelChecked: + resultOption(f1).map(Left(_)).getOrElse: + resultOption(f2).map(Right(_)).getOrElse: + suspend[Either[Try[T1], Try[T2]], Unit]: s => + var found = AtomicBoolean() + f1.addWaiting: result => + if !found.getAndSet(true) then + scheduler.schedule: () => + s.resume(Left(result)) + f2.addWaiting: result => + if !found.getAndSet(true) then + scheduler.schedule: () => + s.resume(Right(result)) + def client = Future.this end given - + body end async @@ -146,6 +168,34 @@ object Future: */ def spawn[T](body: Async ?=> T)(using Scheduler): Future[T] = Future(body).start() + + /** The conjuntion of two futures with given bodies `body1` and `body2`. + * If both futures succeed, suceed with their values in a pair. Otherwise, + * fail with the failure that was returned first. + */ + def both[T1, T2](body1: Async ?=> T1, body2: Async ?=> T2): Future[(T1, T2)] = + Future: async ?=> + val f1 = Future(body1).linked + val f2 = Future(body2).linked + async.awaitEither(f1, f2) match + case Left(Success(x1)) => (x1, f2.await) + case Right(Success(x2)) => (f1.await, x2) + case Left(Failure(ex)) => throw ex + case Right(Failure(ex)) => throw ex + + /** The disjuntion of two futures with given bodies `body1` and `body2`. + * If either future succeeds, suceed with the success that was returned first. + * Otherwise, fail with the failure that was returned last. + */ + def either[T](body1: Async ?=> T, body2: Async ?=> T): Future[T] = + Future: async ?=> + val f1 = Future(body1).linked + val f2 = Future(body2).linked + async.awaitEither(f1, f2) match + case Left(Success(x1)) => x1 + case Right(Success(x2)) => x2 + case Left(_: Failure[?]) => f2.await + case Right(_: Failure[?]) => f1.await end Future def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = From 20f08faf808dbd8a4845488935c193e5f9f343f0 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 5 Feb 2023 18:56:29 +0100 Subject: [PATCH 23/52] Intgerate futures with threads Add a `force` operation that blocks a thread until a future is completed. --- tests/pos/suspend-strawman-2/futures.scala | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 11ef7eabe497..42e0bb5bde97 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -116,6 +116,7 @@ class Future[+T](body: Async ?=> T): status = Completed for task <- currentWaiting() do task(result) cancelChildren() + notifyAll() /** Ensure future's execution has started */ def ensureStarted()(using scheduler: Scheduler): this.type = @@ -154,6 +155,13 @@ class Future[+T](body: Async ?=> T): status = Cancelled cancelChildren() + /** Block thread until future is completed and return result + * N.B. This should be parameterized with a timeout. + */ + def force(): T = + while status != Completed do wait() + result.get + object Future: class Cancellation extends Exception @@ -201,3 +209,7 @@ end Future def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = Future.spawn: x.await + xs.map(_.await).sum + +def Main(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Int = + Test(x, xs).force() + From a448793fc095068f3bebef1a79518499a9e98e7b Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 6 Feb 2023 14:53:12 +0100 Subject: [PATCH 24/52] Rename Future#await --> Future#value Avoid confusion with `Async#await`, which returns a different type. --- tests/pos/suspend-strawman-2/futures.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 42e0bb5bde97..a512515191c9 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -55,7 +55,7 @@ class Future[+T](body: Async ?=> T): /** Wait for this future to be completed, return its value in case of success, * or rethrow exception in case of failure. */ - def await(using a: Async): T = a.await(this).get + def value(using async: Async): T = async.await(this).get // a handler for Async private def async(body: Async ?=> Unit): Unit = @@ -186,8 +186,8 @@ object Future: val f1 = Future(body1).linked val f2 = Future(body2).linked async.awaitEither(f1, f2) match - case Left(Success(x1)) => (x1, f2.await) - case Right(Success(x2)) => (f1.await, x2) + case Left(Success(x1)) => (x1, f2.value) + case Right(Success(x2)) => (f1.value, x2) case Left(Failure(ex)) => throw ex case Right(Failure(ex)) => throw ex @@ -202,13 +202,13 @@ object Future: async.awaitEither(f1, f2) match case Left(Success(x1)) => x1 case Right(Success(x2)) => x2 - case Left(_: Failure[?]) => f2.await - case Right(_: Failure[?]) => f1.await + case Left(_: Failure[?]) => f2.value + case Right(_: Failure[?]) => f1.value end Future def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = Future.spawn: - x.await + xs.map(_.await).sum + x.value + xs.map(_.value).sum def Main(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Int = Test(x, xs).force() From 3c058b4ffa48e74817c3006008932550d6aa917b Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 6 Feb 2023 14:56:22 +0100 Subject: [PATCH 25/52] Introduce Task abstraction Tasks are Futures that have not started running yet. A task can be turned into a future by calling its `run` method. Futures are now always eager, so `Future.start()` was dropped. Tasks can be composed with `par` for parallel computations of pairs and `alt` for alternative computations in case of errors. --- tests/pos/suspend-strawman-2/futures.scala | 91 +++++++++------------- 1 file changed, 36 insertions(+), 55 deletions(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index a512515191c9..e5ce2fd2e3fa 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -5,7 +5,15 @@ import scala.util.boundary, boundary.Label import scala.compiletime.uninitialized import scala.util.{Try, Success, Failure} import java.util.concurrent.atomic.AtomicBoolean -import runtime.* +import runtime.suspend + +/** A hypothetical task scheduler trait */ +trait Scheduler: + def schedule(task: Runnable): Unit = ??? + +object Scheduler extends Scheduler: + given fromAsync(using async: Async): Scheduler = async.client.scheduler +end Scheduler trait Async: @@ -24,15 +32,16 @@ trait Async: /** The future computed by this async computation. */ def client: Future[?] +object Async: + inline def current(using async: Async): Async = async end Async -class Future[+T](body: Async ?=> T): +class Future[+T](body: Async ?=> T)(using val scheduler: Scheduler): import Future.{Status, Cancellation}, Status.* - @volatile private var status: Status = Initial + @volatile private var status: Status = Started private var result: Try[T] = uninitialized private var waiting: ListBuffer[Try[T] => Unit] = ListBuffer() - private var scheduler: Scheduler = uninitialized private var children: mutable.Set[Future[?]] = mutable.Set() private def addWaiting(k: Try[T] => Unit): Unit = synchronized: @@ -63,9 +72,6 @@ class Future[+T](body: Async ?=> T): given Async with private def resultOption[T](f: Future[T]): Option[Try[T]] = f.status match - case Initial => - f.ensureStarted()(using scheduler) - resultOption(f) case Started => None case Completed => @@ -110,30 +116,13 @@ class Future[+T](body: Async ?=> T): private def complete(): Unit = async: - val result = + result = try Success(body) catch case ex: Exception => Failure(ex) status = Completed - for task <- currentWaiting() do task(result) - cancelChildren() - notifyAll() - - /** Ensure future's execution has started */ - def ensureStarted()(using scheduler: Scheduler): this.type = - synchronized: - if status == Initial then start() - this - - /** Start future's execution - * @pre future has not yet started - */ - def start()(using scheduler: Scheduler): this.type = - synchronized: - assert(status == Initial) - this.scheduler = scheduler - scheduler.schedule(() => complete()) - status = Started - this + for task <- currentWaiting() do task(result) + cancelChildren() + notifyAll() /** Links the future as a child to the current async client. * This means the future will be cancelled when the async client @@ -162,52 +151,44 @@ class Future[+T](body: Async ?=> T): while status != Completed do wait() result.get -object Future: - - class Cancellation extends Exception + scheduler.schedule(() => complete()) +end Future +object Future: enum Status: // Transitions always go left to right. // Cancelled --> Completed with Failure(Cancellation()) result - case Initial, Started, Cancelled, Completed + case Started, Cancelled, Completed - /** Construct a future and start it so that ion runs in parallel with the - * current thread. - */ - def spawn[T](body: Async ?=> T)(using Scheduler): Future[T] = - Future(body).start() + class Cancellation extends Exception +end Future - /** The conjuntion of two futures with given bodies `body1` and `body2`. - * If both futures succeed, suceed with their values in a pair. Otherwise, - * fail with the failure that was returned first. - */ - def both[T1, T2](body1: Async ?=> T1, body2: Async ?=> T2): Future[(T1, T2)] = - Future: async ?=> - val f1 = Future(body1).linked - val f2 = Future(body2).linked +class Task[+T](val body: Async ?=> T): + def run(using Scheduler): Future[T] = Future(body) + + def par[U](other: Task[U]): Task[(T, U)] = + Task: async ?=> + val f1 = Future(this.body).linked + val f2 = Future(other.body).linked async.awaitEither(f1, f2) match case Left(Success(x1)) => (x1, f2.value) case Right(Success(x2)) => (f1.value, x2) case Left(Failure(ex)) => throw ex case Right(Failure(ex)) => throw ex - /** The disjuntion of two futures with given bodies `body1` and `body2`. - * If either future succeeds, suceed with the success that was returned first. - * Otherwise, fail with the failure that was returned last. - */ - def either[T](body1: Async ?=> T, body2: Async ?=> T): Future[T] = - Future: async ?=> - val f1 = Future(body1).linked - val f2 = Future(body2).linked + def alt[U >: T](other: Task[U]): Task[U] = + Task: async ?=> + val f1 = Future(this.body).linked + val f2 = Future(other.body).linked async.awaitEither(f1, f2) match case Left(Success(x1)) => x1 case Right(Success(x2)) => x2 case Left(_: Failure[?]) => f2.value case Right(_: Failure[?]) => f1.value -end Future +end Task def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = - Future.spawn: + Future: x.value + xs.map(_.value).sum def Main(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Int = From 7b834934d3526140e290a5e7590fe43c50dcce69 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 6 Feb 2023 18:28:50 +0100 Subject: [PATCH 26/52] Add Async.Source, Async.Runner abstractions Async.Source encapsulates the core functionality of allowing to poll or wait for an asynchronous event. Async.Runner encapsulates the core functionality of a scheduled activity that can be canceled. Also adds an unbounded channel that implements Async.Source for demonstration purposes. --- tests/pos/suspend-strawman-2/futures.scala | 200 +++++++++++++-------- 1 file changed, 129 insertions(+), 71 deletions(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index e5ce2fd2e3fa..1e7895b7ea99 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -12,72 +12,133 @@ trait Scheduler: def schedule(task: Runnable): Unit = ??? object Scheduler extends Scheduler: - given fromAsync(using async: Async): Scheduler = async.client.scheduler + given fromAsync(using async: Async): Scheduler = async.runner.scheduler end Scheduler +/** A context that allows one to suspend waiting for asynchronous data sources */ trait Async: - /** Wait for completion of future `f`. This means: - * - ensure that computing `f` has started - * - wait for the completion and return the completed Try - */ - def await[T](f: Future[T]): Try[T] + /** Wait for completion of async source `src` and return the result */ + def await[T](src: Async.Source[T]): T - /** Wait for completion of the first of the futures `f1`, `f2` - * @return `Left(r1)` if `f1` completed first with `r1` - * `Right(r2)` if `f2` completed first with `r2` + /** Wait for completion of the first of the sources `src1`, `src2` + * @return `Left(r1)` if `src1` completed first with `r1` + * `Right(r2)` if `src2` completed first with `r2` */ - def awaitEither[T1, T2](f1: Future[T1], f2: Future[T2]): Either[Try[T1], Try[T2]] + def awaitEither[T1, T2](src1: Async.Source[T1], src2: Async.Source[T2]): Either[T1, T2] - /** The future computed by this async computation. */ - def client: Future[?] + /** The runner underlying this async computation. */ + def runner: Async.Runner object Async: + + /** The currently executing Async context */ inline def current(using async: Async): Async = async + + /** An asynchronous data source. Sources can be persistent or ephemeral. + * A persistent source will always return the same data to calls to `poll` + * and pass the same data to calls of `handle`. An ephemeral source might pass new + * data in every call. An example of a persistent source is `Future`. An + * example of an ephemeral source is `Channel`. + */ + trait Source[+T]: + + /** Poll whether data is available + * @return The data or None in an option. Depending on the nature of the + * source, data might be returned only once in a poll. E.g. if + * the source is a channel, a Some result might skip to the next + * entry. + */ + def poll: Option[T] + + /** When data is available, pass it to function `k`. + */ + def handleWith(k: T => Unit): Unit + + end Source + + /** A thread-like entity that can be cancelled */ + trait Runner: + + /** The scheduler on which this computation is running */ + def scheduler: Scheduler + + /** Cancel computation for this runner and all its children */ + def cancel(): Unit + + /** Add a given child to this runner */ + def addChild(child: Runner): Unit + end Runner + end Async -class Future[+T](body: Async ?=> T)(using val scheduler: Scheduler): + +class Future[+T](body: Async ?=> T)(using val scheduler: Scheduler) +extends Async.Source[Try[T]], Async.Runner: import Future.{Status, Cancellation}, Status.* @volatile private var status: Status = Started private var result: Try[T] = uninitialized private var waiting: ListBuffer[Try[T] => Unit] = ListBuffer() - private var children: mutable.Set[Future[?]] = mutable.Set() - - private def addWaiting(k: Try[T] => Unit): Unit = synchronized: - if status == Completed then k(result) - else waiting += k + private var children: mutable.Set[Async.Runner] = mutable.Set() private def currentWaiting(): List[Try[T] => Unit] = synchronized: val ws = waiting.toList waiting.clear() ws - private def currentChildren(): List[Future[?]] = synchronized: + private def currentChildren(): List[Async.Runner] = synchronized: val cs = children.toList children.clear() cs - private def checkCancellation(): Unit = - if status == Cancelled then throw Cancellation() + def poll: Option[Try[T]] = status match + case Started => None + case Completed => Some(result) + case Cancelled => Some(Failure(Cancellation())) + + def handleWith(k: Try[T] => Unit): Unit = synchronized: + if status == Completed then k(result) + else waiting += k + + /** Eventually stop computation of this future and fail with + * a `Cancellation` exception. Also cancel all linked children. + */ + def cancel(): Unit = synchronized: + if status != Completed && status != Cancelled then + status = Cancelled + for f <- currentChildren() do f.cancel() + + def addChild(child: Async.Runner): Unit = synchronized: + if status == Completed then child.cancel() + else children += this + + /** Links the future as a child to the current async client. + * This means the future will be cancelled when the async client + * completes. + */ + def linked(using async: Async): this.type = synchronized: + if status != Completed then async.runner.addChild(this) + this /** Wait for this future to be completed, return its value in case of success, * or rethrow exception in case of failure. */ def value(using async: Async): T = async.await(this).get + /** Block thread until future is completed and return result + * N.B. This should be parameterized with a timeout. + */ + def force(): T = + while status != Completed do wait() + result.get + // a handler for Async private def async(body: Async ?=> Unit): Unit = boundary [Unit]: given Async with - - private def resultOption[T](f: Future[T]): Option[Try[T]] = f.status match - case Started => - None - case Completed => - Some(f.result) - case Cancelled => - Some(Failure(Cancellation())) + private def checkCancellation(): Unit = + if status == Cancelled then throw Cancellation() private inline def cancelChecked[T](op: => T): T = checkCancellation() @@ -85,30 +146,30 @@ class Future[+T](body: Async ?=> T)(using val scheduler: Scheduler): checkCancellation() res - def await[T](f: Future[T]): Try[T] = + def await[T](src: Async.Source[T]): T = cancelChecked: - resultOption(f).getOrElse: - suspend[Try[T], Unit]: s => - f.addWaiting: result => + src.poll.getOrElse: + suspend[T, Unit]: k => + src.handleWith: result => scheduler.schedule: () => - s.resume(result) + k.resume(result) - def awaitEither[T1, T2](f1: Future[T1], f2: Future[T2]): Either[Try[T1], Try[T2]] = + def awaitEither[T1, T2](src1: Async.Source[T1], src2: Async.Source[T2]): Either[T1, T2] = cancelChecked: - resultOption(f1).map(Left(_)).getOrElse: - resultOption(f2).map(Right(_)).getOrElse: - suspend[Either[Try[T1], Try[T2]], Unit]: s => + src1.poll.map(Left(_)).getOrElse: + src2.poll.map(Right(_)).getOrElse: + suspend[Either[T1, T2], Unit]: k => var found = AtomicBoolean() - f1.addWaiting: result => + src1.handleWith: result => if !found.getAndSet(true) then scheduler.schedule: () => - s.resume(Left(result)) - f2.addWaiting: result => + k.resume(Left(result)) + src2.handleWith: result => if !found.getAndSet(true) then scheduler.schedule: () => - s.resume(Right(result)) + k.resume(Right(result)) - def client = Future.this + def runner = Future.this end given body @@ -121,36 +182,9 @@ class Future[+T](body: Async ?=> T)(using val scheduler: Scheduler): catch case ex: Exception => Failure(ex) status = Completed for task <- currentWaiting() do task(result) - cancelChildren() + cancel() notifyAll() - /** Links the future as a child to the current async client. - * This means the future will be cancelled when the async client - * completes. - */ - def linked(using async: Async): this.type = synchronized: - if status != Completed then - async.client.children += this - this - - private def cancelChildren(): Unit = - for f <- currentChildren() do f.cancel() - - /** Eventually stop computation of this future and fail with - * a `Cancellation` exception. Also cancel all linked children. - */ - def cancel(): Unit = synchronized: - if status != Completed && status != Cancelled then - status = Cancelled - cancelChildren() - - /** Block thread until future is completed and return result - * N.B. This should be parameterized with a timeout. - */ - def force(): T = - while status != Completed do wait() - result.get - scheduler.schedule(() => complete()) end Future @@ -187,6 +221,30 @@ class Task[+T](val body: Async ?=> T): case Right(_: Failure[?]) => f1.value end Task +/** An unbounded channel */ +class Channel[T] extends Async.Source[T]: + private val pending = ListBuffer[T]() + private val waiting = ListBuffer[T => Unit]() + def send(x: T): Unit = synchronized: + if waiting.isEmpty then pending += x + else + val k = waiting.head + waiting.dropInPlace(1) + k(x) + def poll: Option[T] = synchronized: + if pending.isEmpty then None + else + val x = pending.head + pending.dropInPlace(1) + Some(x) + def handleWith(k: T => Unit): Unit = synchronized: + if pending.isEmpty then waiting += k + else + val x = pending.head + pending.dropInPlace(1) + k(x) +end Channel + def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = Future: x.value + xs.map(_.value).sum From 30f33899ca4c2ec3834adb2af80b70b22b17b63f Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 7 Feb 2023 09:32:33 +0100 Subject: [PATCH 27/52] Split concurrency library into separate files --- tests/pos/suspend-strawman-2/Async.scala | 53 +++ .../pos/suspend-strawman-2/Cancellable.scala | 15 + tests/pos/suspend-strawman-2/futures.scala | 367 ++++++++---------- tests/pos/suspend-strawman-2/runtime.scala | 6 - tests/pos/suspend-strawman-2/scheduler.scala | 10 + 5 files changed, 247 insertions(+), 204 deletions(-) create mode 100644 tests/pos/suspend-strawman-2/Async.scala create mode 100644 tests/pos/suspend-strawman-2/Cancellable.scala create mode 100644 tests/pos/suspend-strawman-2/scheduler.scala diff --git a/tests/pos/suspend-strawman-2/Async.scala b/tests/pos/suspend-strawman-2/Async.scala new file mode 100644 index 000000000000..b6a3b3e8bd84 --- /dev/null +++ b/tests/pos/suspend-strawman-2/Async.scala @@ -0,0 +1,53 @@ +package concurrent + +/** A context that allows to suspend waiting for asynchronous data sources */ +trait Async: + + /** Wait for completion of async source `src` and return the result */ + def await[T](src: Async.Source[T]): T + + /** Wait for completion of the first of the sources `src1`, `src2` + * @return `Left(r1)` if `src1` completed first with `r1` + * `Right(r2)` if `src2` completed first with `r2` + */ + def awaitEither[T1, T2](src1: Async.Source[T1], src2: Async.Source[T2]): Either[T1, T2] + + /** The cancellable runner underlying this async computation. */ + def runner: Cancellable + + /** The scheduler for runnables defined in this async computation */ + def scheduler: Scheduler + +object Async: + + /** The currently executing Async context */ + inline def current(using async: Async): Async = async + + /** An asynchronous data source. Sources can be persistent or ephemeral. + * A persistent source will always return the same data to calls to `poll` + * and pass the same data to calls of `handle`. An ephemeral source might pass new + * data in every call. An example of a persistent source is `Future`. An + * example of an ephemeral source is `Channel`. + */ + trait Source[+T]: + thisSource => + + /** Poll whether data is available + * @return The data or None in an option. Depending on the nature of the + * source, data might be returned only once in a poll. E.g. if + * the source is a channel, a Some result might skip to the next + */ + def poll: Option[T] + + /** When data is available, pass it to function `k`. + */ + def handleWith(k: T => Unit): Unit + + def map[U](f: T => U): Source[U] = new Source: + def poll = thisSource.poll.map(f) + def handleWith(k: U => Unit): Unit = thisSource.handleWith(f.andThen(k)) + + end Source + +end Async + diff --git a/tests/pos/suspend-strawman-2/Cancellable.scala b/tests/pos/suspend-strawman-2/Cancellable.scala new file mode 100644 index 000000000000..a6556aa16e25 --- /dev/null +++ b/tests/pos/suspend-strawman-2/Cancellable.scala @@ -0,0 +1,15 @@ +package concurrent + +/** A trait for cancellable entiries that can be grouped */ +trait Cancellable: + + def cancel(): Unit + + /** Add a given child to this Cancellable, so that the child will be cancelled + * when the Cancellable itself is cancelled. + */ + def addChild(child: Cancellable): Unit + + def isCancelled: Boolean + +end Cancellable diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 1e7895b7ea99..56e289b622a8 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -1,205 +1,196 @@ -package futures +package concurrent import scala.collection.mutable, mutable.ListBuffer -import scala.util.boundary, boundary.Label +import scala.util.boundary import scala.compiletime.uninitialized import scala.util.{Try, Success, Failure} +import scala.annotation.unchecked.uncheckedVariance +import java.util.concurrent.CancellationException import java.util.concurrent.atomic.AtomicBoolean import runtime.suspend -/** A hypothetical task scheduler trait */ -trait Scheduler: - def schedule(task: Runnable): Unit = ??? +trait Future[+T] extends Async.Source[Try[T]], Cancellable: -object Scheduler extends Scheduler: - given fromAsync(using async: Async): Scheduler = async.runner.scheduler -end Scheduler - -/** A context that allows one to suspend waiting for asynchronous data sources */ -trait Async: - - /** Wait for completion of async source `src` and return the result */ - def await[T](src: Async.Source[T]): T - - /** Wait for completion of the first of the sources `src1`, `src2` - * @return `Left(r1)` if `src1` completed first with `r1` - * `Right(r2)` if `src2` completed first with `r2` - */ - def awaitEither[T1, T2](src1: Async.Source[T1], src2: Async.Source[T2]): Either[T1, T2] - - /** The runner underlying this async computation. */ - def runner: Async.Runner - -object Async: - - /** The currently executing Async context */ - inline def current(using async: Async): Async = async - - /** An asynchronous data source. Sources can be persistent or ephemeral. - * A persistent source will always return the same data to calls to `poll` - * and pass the same data to calls of `handle`. An ephemeral source might pass new - * data in every call. An example of a persistent source is `Future`. An - * example of an ephemeral source is `Channel`. + /** Wait for this future to be completed, return its value in case of success, + * or rethrow exception in case of failure. */ - trait Source[+T]: - - /** Poll whether data is available - * @return The data or None in an option. Depending on the nature of the - * source, data might be returned only once in a poll. E.g. if - * the source is a channel, a Some result might skip to the next - * entry. - */ - def poll: Option[T] - - /** When data is available, pass it to function `k`. - */ - def handleWith(k: T => Unit): Unit + def value(using async: Async): T - end Source - - /** A thread-like entity that can be cancelled */ - trait Runner: - - /** The scheduler on which this computation is running */ - def scheduler: Scheduler - - /** Cancel computation for this runner and all its children */ - def cancel(): Unit - - /** Add a given child to this runner */ - def addChild(child: Runner): Unit - end Runner - -end Async - - -class Future[+T](body: Async ?=> T)(using val scheduler: Scheduler) -extends Async.Source[Try[T]], Async.Runner: - import Future.{Status, Cancellation}, Status.* - - @volatile private var status: Status = Started - private var result: Try[T] = uninitialized - private var waiting: ListBuffer[Try[T] => Unit] = ListBuffer() - private var children: mutable.Set[Async.Runner] = mutable.Set() - - private def currentWaiting(): List[Try[T] => Unit] = synchronized: - val ws = waiting.toList - waiting.clear() - ws - - private def currentChildren(): List[Async.Runner] = synchronized: - val cs = children.toList - children.clear() - cs - - def poll: Option[Try[T]] = status match - case Started => None - case Completed => Some(result) - case Cancelled => Some(Failure(Cancellation())) + /** Block thread until future is completed and return result + * N.B. This should be parameterized with a timeout. + */ + def force(): T - def handleWith(k: Try[T] => Unit): Unit = synchronized: - if status == Completed then k(result) - else waiting += k + /** Links the future as a child to the current async runner. + * This means the future will be cancelled when the async runner + * completes. + */ + def linked(using async: Async): this.type /** Eventually stop computation of this future and fail with * a `Cancellation` exception. Also cancel all linked children. */ - def cancel(): Unit = synchronized: - if status != Completed && status != Cancelled then - status = Cancelled - for f <- currentChildren() do f.cancel() + def cancel(): Unit - def addChild(child: Async.Runner): Unit = synchronized: - if status == Completed then child.cancel() - else children += this +object Future: - /** Links the future as a child to the current async client. - * This means the future will be cancelled when the async client - * completes. + private enum Status: + // Transitions always go left to right. + // Cancelled --> Completed with Failure(CancellationException()) result + case Started, Cancelled, Completed + import Status.* + + private class CoreFuture[+T] extends Future[T]: + @volatile protected var status: Status = Started + private var result: Try[T] = uninitialized + private var waiting: ListBuffer[Try[T] => Unit] = ListBuffer() + private var children: mutable.Set[Cancellable] = mutable.Set() + + private def currentWaiting(): List[Try[T] => Unit] = synchronized: + val ws = waiting.toList + waiting.clear() + ws + + private def currentChildren(): List[Cancellable] = synchronized: + val cs = children.toList + children.clear() + cs + + def poll: Option[Try[T]] = + if status == Started then None else Some(result) + + def handleWith(k: Try[T] => Unit): Unit = synchronized: + if status == Started then waiting += k else k(result) + + def cancel(): Unit = + val toCancel = synchronized: + if status != Completed && status != Cancelled then + result = Failure(new CancellationException()) + status = Cancelled + currentChildren() + else + Nil + toCancel.foreach(_.cancel()) + + def addChild(child: Cancellable): Unit = synchronized: + if status == Completed then child.cancel() + else children += this + + def isCancelled = status == Cancelled + + def linked(using async: Async): this.type = + if status != Completed then async.runner.addChild(this) + this + + def value(using async: Async): T = async.await(this).get + + def force(): T = + while status != Completed do wait() + result.get + + /** Complete future with result. If future was cancelled in the meantime, + * return a CancellationException failure instead. + * Note: @uncheckedVariance is safe here since `complete` is called from + * only two places: + * - from the initializer of RunnableFuture, where we are sure that `T` + * is exactly the type with which the future was created, and + * - from Promise.complete, where we are sure the type `T` is exactly + * the type with which the future was created since `Promise` is invariant. + */ + private[Future] def complete(result: Try[T] @uncheckedVariance): Unit = + if status == Started then this.result = result + status = Completed + for task <- currentWaiting() do task(result) + notifyAll() + + end CoreFuture + + private class RunnableFuture[+T](body: Async ?=> T)(using scheduler: Scheduler) + extends CoreFuture[T]: + + // a handler for Async + private def async(body: Async ?=> Unit): Unit = + boundary [Unit]: + given Async with + private def checkCancellation(): Unit = + if status == Cancelled then throw new CancellationException() + + private inline def cancelChecked[T](op: => T): T = + checkCancellation() + val res = op + checkCancellation() + res + + def await[T](src: Async.Source[T]): T = + cancelChecked: + src.poll.getOrElse: + suspend[T, Unit]: k => + src.handleWith: result => + scheduler.schedule: () => + k.resume(result) + + def awaitEither[T1, T2](src1: Async.Source[T1], src2: Async.Source[T2]): Either[T1, T2] = + cancelChecked: + src1.poll.map(Left(_)).getOrElse: + src2.poll.map(Right(_)).getOrElse: + suspend[Either[T1, T2], Unit]: k => + var found = AtomicBoolean() + src1.handleWith: result => + if !found.getAndSet(true) then + scheduler.schedule: () => + k.resume(Left(result)) + src2.handleWith: result => + if !found.getAndSet(true) then + scheduler.schedule: () => + k.resume(Right(result)) + + def runner: Cancellable = RunnableFuture.this + def scheduler = RunnableFuture.this.scheduler + end given + + body + end async + + scheduler.schedule: () => + async: + complete( + try Success(body) + catch case ex: Exception => Failure(ex)) + end RunnableFuture + + /** Create a future that asynchronously executes `body` to define + * its result value in a Try or returns failure if an exception was thrown. */ - def linked(using async: Async): this.type = synchronized: - if status != Completed then async.runner.addChild(this) - this + def apply[T](body: Async ?=> T)(using Scheduler): Future[T] = RunnableFuture(body) - /** Wait for this future to be completed, return its value in case of success, - * or rethrow exception in case of failure. + /** A promise defines a future that is be completed via the + * promise's `complete` method. */ - def value(using async: Async): T = async.await(this).get + class Promise[T]: + private val myFuture = CoreFuture[T]() - /** Block thread until future is completed and return result - * N.B. This should be parameterized with a timeout. - */ - def force(): T = - while status != Completed do wait() - result.get - - // a handler for Async - private def async(body: Async ?=> Unit): Unit = - boundary [Unit]: - given Async with - private def checkCancellation(): Unit = - if status == Cancelled then throw Cancellation() - - private inline def cancelChecked[T](op: => T): T = - checkCancellation() - val res = op - checkCancellation() - res - - def await[T](src: Async.Source[T]): T = - cancelChecked: - src.poll.getOrElse: - suspend[T, Unit]: k => - src.handleWith: result => - scheduler.schedule: () => - k.resume(result) - - def awaitEither[T1, T2](src1: Async.Source[T1], src2: Async.Source[T2]): Either[T1, T2] = - cancelChecked: - src1.poll.map(Left(_)).getOrElse: - src2.poll.map(Right(_)).getOrElse: - suspend[Either[T1, T2], Unit]: k => - var found = AtomicBoolean() - src1.handleWith: result => - if !found.getAndSet(true) then - scheduler.schedule: () => - k.resume(Left(result)) - src2.handleWith: result => - if !found.getAndSet(true) then - scheduler.schedule: () => - k.resume(Right(result)) - - def runner = Future.this - end given - - body - end async - - private def complete(): Unit = - async: - result = - try Success(body) - catch case ex: Exception => Failure(ex) - status = Completed - for task <- currentWaiting() do task(result) - cancel() - notifyAll() - - scheduler.schedule(() => complete()) -end Future + /** The future defined by this promise */ + def future: Future[T] = myFuture -object Future: - enum Status: - // Transitions always go left to right. - // Cancelled --> Completed with Failure(Cancellation()) result - case Started, Cancelled, Completed + /** Define the result value of `future`. However, if `future` was + * cancelled in the meantime complete with a `CancellationException` + * failure instead. + */ + def complete(result: Try[T]): Unit = myFuture.complete(result) - class Cancellation extends Exception + end Promise end Future +/** A task is a template that can be turned into a runnable future */ class Task[+T](val body: Async ?=> T): + + /** Start a future computed from the `body` of this task */ def run(using Scheduler): Future[T] = Future(body) + /** Parallel composition of this task with `other` task. + * If both tasks succeed, succeed with their values in a pair. Otherwise, + * fail with the failure that was returned first. + */ def par[U](other: Task[U]): Task[(T, U)] = Task: async ?=> val f1 = Future(this.body).linked @@ -210,6 +201,10 @@ class Task[+T](val body: Async ?=> T): case Left(Failure(ex)) => throw ex case Right(Failure(ex)) => throw ex + /** Alternative parallel composition of this task with `other` task. + * If either task succeeds, succeed with the success that was returned first. + * Otherwise, fail with the failure that was returned last. + */ def alt[U >: T](other: Task[U]): Task[U] = Task: async ?=> val f1 = Future(this.body).linked @@ -221,30 +216,6 @@ class Task[+T](val body: Async ?=> T): case Right(_: Failure[?]) => f1.value end Task -/** An unbounded channel */ -class Channel[T] extends Async.Source[T]: - private val pending = ListBuffer[T]() - private val waiting = ListBuffer[T => Unit]() - def send(x: T): Unit = synchronized: - if waiting.isEmpty then pending += x - else - val k = waiting.head - waiting.dropInPlace(1) - k(x) - def poll: Option[T] = synchronized: - if pending.isEmpty then None - else - val x = pending.head - pending.dropInPlace(1) - Some(x) - def handleWith(k: T => Unit): Unit = synchronized: - if pending.isEmpty then waiting += k - else - val x = pending.head - pending.dropInPlace(1) - k(x) -end Channel - def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = Future: x.value + xs.map(_.value).sum diff --git a/tests/pos/suspend-strawman-2/runtime.scala b/tests/pos/suspend-strawman-2/runtime.scala index aa9aa7fd26c4..fa14fba07f7e 100644 --- a/tests/pos/suspend-strawman-2/runtime.scala +++ b/tests/pos/suspend-strawman-2/runtime.scala @@ -1,12 +1,6 @@ package runtime import scala.util.boundary, boundary.Label -/** A hypothetical task scheduler trait */ -trait Scheduler: - def schedule(task: Runnable): Unit = ??? - -object Scheduler extends Scheduler - /** Contains a delimited contination, which can be invoked with `resume` */ class Suspension[-T, +R]: def resume(arg: T): R = ??? diff --git a/tests/pos/suspend-strawman-2/scheduler.scala b/tests/pos/suspend-strawman-2/scheduler.scala new file mode 100644 index 000000000000..89b3bd3e7ddb --- /dev/null +++ b/tests/pos/suspend-strawman-2/scheduler.scala @@ -0,0 +1,10 @@ +package concurrent + +/** A hypothetical task scheduler trait */ +trait Scheduler: + def schedule(task: Runnable): Unit = ??? + +object Scheduler extends Scheduler: + given fromAsync(using async: Async): Scheduler = async.scheduler +end Scheduler + From 62c2f211b9e130f9e09f25148fdfa6be55fd7fd2 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 7 Feb 2023 10:37:03 +0100 Subject: [PATCH 28/52] Auto-link futures If a future gets created in an Async context, link it automatically to the runner of that context. --- tests/pos/suspend-strawman-2/futures.scala | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 56e289b622a8..bc7756f24a29 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -158,10 +158,16 @@ object Future: catch case ex: Exception => Failure(ex)) end RunnableFuture - /** Create a future that asynchronously executes `body` to define + /** Create a future that asynchronously executes `body` that defines * its result value in a Try or returns failure if an exception was thrown. + * If the future is created in an Async context, it is added to the + * children of that context's runner. */ - def apply[T](body: Async ?=> T)(using Scheduler): Future[T] = RunnableFuture(body) + def apply[T](body: Async ?=> T)( + using scheduler: Scheduler, environment: Async | Null = null): Future[T] = + val f = RunnableFuture(body) + if environment != null then environment.runner.addChild(f) + f /** A promise defines a future that is be completed via the * promise's `complete` method. @@ -185,7 +191,8 @@ end Future class Task[+T](val body: Async ?=> T): /** Start a future computed from the `body` of this task */ - def run(using Scheduler): Future[T] = Future(body) + def run(using scheduler: Scheduler, environment: Async | Null = null): Future[T] = + Future(body) /** Parallel composition of this task with `other` task. * If both tasks succeed, succeed with their values in a pair. Otherwise, @@ -193,8 +200,8 @@ class Task[+T](val body: Async ?=> T): */ def par[U](other: Task[U]): Task[(T, U)] = Task: async ?=> - val f1 = Future(this.body).linked - val f2 = Future(other.body).linked + val f1 = Future(this.body) + val f2 = Future(other.body) async.awaitEither(f1, f2) match case Left(Success(x1)) => (x1, f2.value) case Right(Success(x2)) => (f1.value, x2) @@ -207,8 +214,8 @@ class Task[+T](val body: Async ?=> T): */ def alt[U >: T](other: Task[U]): Task[U] = Task: async ?=> - val f1 = Future(this.body).linked - val f2 = Future(other.body).linked + val f1 = Future(this.body) + val f2 = Future(other.body) async.awaitEither(f1, f2) match case Left(Success(x1)) => x1 case Right(Success(x2)) => x2 From 4cf756c64403577872346e0e9561d9d0e8f2eb97 Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 9 Feb 2023 09:41:52 +0100 Subject: [PATCH 29/52] Add derived sources and channels --- tests/pos/suspend-strawman-2/Async.scala | 192 ++++++++++++++--- .../pos/suspend-strawman-2/Cancellable.scala | 10 +- tests/pos/suspend-strawman-2/channels.scala | 122 +++++++++++ tests/pos/suspend-strawman-2/futures.scala | 200 ++++++++---------- 4 files changed, 379 insertions(+), 145 deletions(-) create mode 100644 tests/pos/suspend-strawman-2/channels.scala diff --git a/tests/pos/suspend-strawman-2/Async.scala b/tests/pos/suspend-strawman-2/Async.scala index b6a3b3e8bd84..a82a928a26f3 100644 --- a/tests/pos/suspend-strawman-2/Async.scala +++ b/tests/pos/suspend-strawman-2/Async.scala @@ -1,53 +1,189 @@ package concurrent +import java.util.concurrent.atomic.AtomicBoolean +import scala.collection.mutable +import runtime.suspend +import scala.util.boundary + +/** The underlying configuration of an async block */ +trait AsyncConfig: + + /** The cancellable async source underlying this async computation */ + def root: Cancellable + + /** The scheduler for runnables defined in this async computation */ + def scheduler: Scheduler + +object AsyncConfig: + + /** A toplevel async group with given scheduler and a synthetic root that + * ignores cancellation requests + */ + given fromScheduler(using s: Scheduler): AsyncConfig with + def root = Cancellable.empty + def scheduler = s + +end AsyncConfig /** A context that allows to suspend waiting for asynchronous data sources */ -trait Async: +trait Async extends AsyncConfig: /** Wait for completion of async source `src` and return the result */ def await[T](src: Async.Source[T]): T - /** Wait for completion of the first of the sources `src1`, `src2` - * @return `Left(r1)` if `src1` completed first with `r1` - * `Right(r2)` if `src2` completed first with `r2` - */ - def awaitEither[T1, T2](src1: Async.Source[T1], src2: Async.Source[T2]): Either[T1, T2] +object Async: - /** The cancellable runner underlying this async computation. */ - def runner: Cancellable + abstract class AsyncImpl(val root: Cancellable, val scheduler: Scheduler) + (using boundary.Label[Unit]) extends Async: - /** The scheduler for runnables defined in this async computation */ - def scheduler: Scheduler + protected def checkCancellation(): Unit -object Async: + private var result: T + + def await[T](src: Async.Source[T]): T = + checkCancellation() + var resultOpt: Option[T] = None + if src.poll: x => + result = x + true + then result + else + try suspend[T, Unit]: k => + src.onComplete: x => + scheduler.schedule: () => + k.resume(x) + true // signals to `src` that result `x` was consumed + finally checkCancellation() + + end AsyncImpl /** The currently executing Async context */ inline def current(using async: Async): Async = async - /** An asynchronous data source. Sources can be persistent or ephemeral. - * A persistent source will always return the same data to calls to `poll` - * and pass the same data to calls of `handle`. An ephemeral source might pass new - * data in every call. An example of a persistent source is `Future`. An - * example of an ephemeral source is `Channel`. + /** Await source result in currently executing Async context */ + inline def await[T](src: Source[T])(using async: Async): T = async.await(src) + + /** A function `T => Boolean` whose lineage is recorded by its implementing + * classes. The Listener function accepts values of type `T` and returns + * `true` iff the value was consumed by an async block. */ - trait Source[+T]: - thisSource => + trait Listener[-T] extends Function[T, Boolean] - /** Poll whether data is available - * @return The data or None in an option. Depending on the nature of the - * source, data might be returned only once in a poll. E.g. if - * the source is a channel, a Some result might skip to the next + /** A listener for values that are processed by the given source `src` and + * that are demanded by the continuation listener `continue`. + */ + abstract case class ForwardingListener[T](src: Source[?], continue: Listener[?]) extends Listener[T] + + /** A listener for values that are processed directly in an async block. + * Closures of type `T => Boolean` can be SAM converted to this type. + */ + abstract case class FinalListener[T]() extends Listener[T] + + /** A source that cannot be mapped, filtered, or raced. In other words, + * an item coming from a direct source must be immediately consumed in + * another async computation; no rejection of this item is possible. + */ + trait DirectSource[+T]: + + /** If data is available at present, pass it to function `k` + * and return the result if this call. + * `k` returns true iff the data was consumed in an async block. + * Calls to `poll` are always synchronous. + */ + def poll(k: Listener[T]): Boolean + + /** Once data is available, pass it to function `k`. + * `k` returns true iff the data was consumed in an async block. + * Calls to `onComplete` are usually asynchronous, meaning that + * the passed continuation `k` is a suspension. */ - def poll: Option[T] + def onComplete(k: Listener[T]): Unit - /** When data is available, pass it to function `k`. + /** Signal that listener `k` is dead (i.e. will always return `false` from now on). + * This permits original, (i.e. non-derived) sources like futures or channels + * to drop the listener from their `waiting` sets. */ - def handleWith(k: T => Unit): Unit + def dropListener(k: Listener[T]): Unit - def map[U](f: T => U): Source[U] = new Source: - def poll = thisSource.poll.map(f) - def handleWith(k: U => Unit): Unit = thisSource.handleWith(f.andThen(k)) + end DirectSource + + /** An asynchronous data source. Sources can be persistent or ephemeral. + * A persistent source will always pass same data to calls of `poll and `onComplete`. + * An ephememral source can pass new data in every call. + * An example of a persistent source is `Future`. + * An example of an ephemeral source is `Channel`. + */ + trait Source[+T] extends DirectSource[T]: + + /** Pass on data transformed by `f` */ + def map[U](f: T => U): Source[U] = + new DerivedSource[T, U](this): + def listen(x: T, k: Listener[U]) = k(f(x)) + + /** Pass on only data matching the predicate `p` */ + def filter(p: T => Boolean): Source[T] = + new DerivedSource[T, T](this): + def listen(x: T, k: Listener[T]) = p(x) && k(x) end Source + /** As source that transforms an original source in some way */ + + abstract class DerivedSource[T, U](src: Source[T]) extends Source[U]: + + /** Handle a value `x` passed to the original source by possibly + * invokiong the continuation for this source. + */ + protected def listen(x: T, k: Listener[U]): Boolean + + private def transform(k: Listener[U]): Listener[T] = + new ForwardingListener[T](this, k): + def apply(x: T): Boolean = listen(x, k) + + def poll(k: Listener[U]): Boolean = + src.poll(transform(k)) + def onComplete(k: Listener[U]): Unit = + src.onComplete(transform(k)) + def dropListener(k: Listener[U]): Unit = + src.dropListener(transform(k)) + end DerivedSource + + /** Pass first result from any of `sources` to the continuation */ + def race[T](sources: Source[T]*): Source[T] = new Source: + + def poll(k: Listener[T]): Boolean = + val it = sources.iterator + var found = false + while it.hasNext && !found do + it.next.poll: x => + found = k(x) + found + found + + def onComplete(k: Listener[T]): Unit = + val listener = new ForwardingListener[T](this, k): + var foundBefore = false + def continueIfFirst(x: T): Boolean = synchronized: + if foundBefore then false else { foundBefore = k(x); foundBefore } + def apply(x: T): Boolean = + val found = continueIfFirst(x) + if found then sources.foreach(_.dropListener(this)) + found + sources.foreach(_.onComplete(listener)) + + def dropListener(k: Listener[T]): Unit = + val listener = new ForwardingListener[T](this, k): + def apply(x: T): Boolean = ??? + // not to be called, we need the listener only for its + // hashcode and equality test. + sources.foreach(_.dropListener(listener)) + + end race + + /** If left (respectively, right) source succeeds with `x`, pass `Left(x)`, + * (respectively, Right(x)) on to the continuation. + */ + def either[T, U](src1: Source[T], src2: Source[U]): Source[Either[T, U]] = + race[Either[T, U]](src1.map(Left(_)), src2.map(Right(_))) + end Async diff --git a/tests/pos/suspend-strawman-2/Cancellable.scala b/tests/pos/suspend-strawman-2/Cancellable.scala index a6556aa16e25..1a99142380dc 100644 --- a/tests/pos/suspend-strawman-2/Cancellable.scala +++ b/tests/pos/suspend-strawman-2/Cancellable.scala @@ -1,8 +1,9 @@ package concurrent -/** A trait for cancellable entiries that can be grouped */ +/** A trait for cancellable entities that can be grouped */ trait Cancellable: + /** Issue a cancel request */ def cancel(): Unit /** Add a given child to this Cancellable, so that the child will be cancelled @@ -10,6 +11,11 @@ trait Cancellable: */ def addChild(child: Cancellable): Unit - def isCancelled: Boolean +object Cancellable: + + /** A cancelled entity that ignores all `cancel` and `addChild` requests */ + object empty extends Cancellable: + def cancel() = () + def addChild(child: Cancellable) = () end Cancellable diff --git a/tests/pos/suspend-strawman-2/channels.scala b/tests/pos/suspend-strawman-2/channels.scala new file mode 100644 index 000000000000..f7a3f3b9cdcb --- /dev/null +++ b/tests/pos/suspend-strawman-2/channels.scala @@ -0,0 +1,122 @@ +package concurrent +import scala.collection.mutable, mutable.ListBuffer +import scala.util.boundary.Label +import runtime.suspend +import Async.{Listener, await} + +/** An unbounded channel */ +class UnboundedChannel[T] extends Async.Source[T]: + private val pending = ListBuffer[T]() + private val waiting = mutable.Set[Listener[T]]() + + private def drainWaiting(x: T): Boolean = + val it = waiting.iterator + var sent = false + while it.hasNext && !sent do + val k = it.next() + sent = k(x) + if sent then waiting -= k + sent + + private def drainPending(k: Listener[T]): Boolean = + val sent = pending.nonEmpty && k(pending.head) + if sent then + while + pending.dropInPlace(1) + pending.nonEmpty && drainWaiting(pending.head) + do () + sent + + def read()(using Async): T = synchronized: + await(this) + + def send(x: T): Unit = synchronized: + val sent = pending.isEmpty && drainWaiting(x) + if !sent then pending += x + + def poll(k: Listener[T]): Boolean = synchronized: + drainPending(k) + + def onComplete(k: Listener[T]): Unit = synchronized: + if !drainPending(k) then waiting += k + + def dropListener(k: Listener[T]): Unit = synchronized: + waiting -= k + +end UnboundedChannel + +class SyncChannel[T]: + + private val pendingReads = mutable.Set[Listener[T]]() + private val pendingSends = mutable.Set[Listener[Listener[T]]]() + + private def collapse[T](k2: Listener[Listener[T]]): Option[T] = + var r: Option[T] = None + if k2 { x => r = Some(x); true } then r else None + + protected def link[T](pending: mutable.Set[T], op: T => Boolean): Boolean = + pending.iterator.find(op) match + case Some(elem) => pending -= elem; true + case None => false + + val canRead = new Async.Source[T]: + def poll(k: Listener[T]): Boolean = + link(pendingSends, sender => collapse(sender).map(k) == Some(true)) + def onComplete(k: Listener[T]): Unit = + if !poll(k) then pendingReads += k + def dropListener(k: Listener[T]): Unit = + pendingReads -= k + + val canSend = new Async.Source[Listener[T]]: + def poll(k: Listener[Listener[T]]): Boolean = + link(pendingReads, k) + def onComplete(k: Listener[Listener[T]]): Unit = + if !poll(k) then pendingSends += k + def dropListener(k: Listener[Listener[T]]): Unit = + pendingSends -= k + + def send(x: T)(using Async): Unit = + await(canSend)(x) + + def read()(using Async): T = + await(canRead) + +end SyncChannel + +class DirectSyncChannel[T]: + + private val pendingReads = mutable.Set[Listener[T]]() + private val pendingSends = mutable.Set[Listener[Listener[T]]]() + + private def collapse[T](k2: Listener[Listener[T]]): Option[T] = + var r: Option[T] = None + if k2 { x => r = Some(x); true } then r else None + + private def link[T](pending: mutable.Set[T], op: T => Unit): Boolean = + pending.headOption match + case Some(elem) => op(elem); true + case None => false + + val canRead = new Async.Source[T]: + def poll(k: Listener[T]): Boolean = + link(pendingSends, sender => collapse(sender).map(k)) + def onComplete(k: Listener[T]): Unit = + if !poll(k) then pendingReads += k + def dropListener(k: Listener[T]): Unit = + pendingReads -= k + + val canSend = new Async.Source[Listener[T]]: + def poll(k: Listener[Listener[T]]): Boolean = + link(pendingReads, k(_)) + def onComplete(k: Listener[Listener[T]]): Unit = + if !poll(k) then pendingSends += k + def dropListener(k: Listener[Listener[T]]): Unit = + pendingSends -= k + + def send(x: T)(using Async): Unit = + await(canSend)(x) + + def read()(using Async): T = + await(canRead) + +end DirectSyncChannel diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index bc7756f24a29..5042d46f978f 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -6,9 +6,10 @@ import scala.compiletime.uninitialized import scala.util.{Try, Success, Failure} import scala.annotation.unchecked.uncheckedVariance import java.util.concurrent.CancellationException -import java.util.concurrent.atomic.AtomicBoolean import runtime.suspend +/** A cancellable future that can suspend waiting for other synchronous sources + */ trait Future[+T] extends Async.Source[Try[T]], Cancellable: /** Wait for this future to be completed, return its value in case of success, @@ -21,8 +22,8 @@ trait Future[+T] extends Async.Source[Try[T]], Cancellable: */ def force(): T - /** Links the future as a child to the current async runner. - * This means the future will be cancelled when the async runner + /** Links the future as a child to the current async root. + * This means the future will be cancelled when the async root * completes. */ def linked(using async: Async): this.type @@ -32,60 +33,65 @@ trait Future[+T] extends Async.Source[Try[T]], Cancellable: */ def cancel(): Unit + /** If this future has not yet completed, add `child` so that it will + * be cancelled together with this future in case the future is cancelled. + */ + def addChild(child: Cancellable): Unit + object Future: private enum Status: - // Transitions always go left to right. - // Cancelled --> Completed with Failure(CancellationException()) result - case Started, Cancelled, Completed + case Initial, Completed import Status.* + /** The core part of a future that is compled explicitly by calling its + * `complete` method. There are two implementations + * + * - RunnableFuture: Completion is done by running a block of code + * - Promise.future: Completion is done by external request. + */ private class CoreFuture[+T] extends Future[T]: - @volatile protected var status: Status = Started - private var result: Try[T] = uninitialized - private var waiting: ListBuffer[Try[T] => Unit] = ListBuffer() - private var children: mutable.Set[Cancellable] = mutable.Set() - private def currentWaiting(): List[Try[T] => Unit] = synchronized: - val ws = waiting.toList - waiting.clear() - ws + @volatile protected var hasCompleted: Boolean = false + private var result: Try[T] = uninitialized // guaranteed to be set if hasCompleted = true + private var waiting: mutable.Set[Try[T] => Boolean] = mutable.Set() + private var children: mutable.Set[Cancellable] = mutable.Set() - private def currentChildren(): List[Cancellable] = synchronized: - val cs = children.toList - children.clear() - cs + private def extract[T](s: mutable.Set[T]): List[T] = synchronized: + val xs = s.toList + s.clear() + xs - def poll: Option[Try[T]] = - if status == Started then None else Some(result) + def poll(k: Async.Listener[Try[T]]): Boolean = + hasCompleted && k(result) - def handleWith(k: Try[T] => Unit): Unit = synchronized: - if status == Started then waiting += k else k(result) + def onComplete(k: Async.Listener[Try[T]]): Unit = synchronized: + if !poll(k) then waiting += k def cancel(): Unit = - val toCancel = synchronized: - if status != Completed && status != Cancelled then - result = Failure(new CancellationException()) - status = Cancelled - currentChildren() + val othersToCancel = synchronized: + if hasCompleted then Nil else - Nil - toCancel.foreach(_.cancel()) + result = Failure(new CancellationException()) + hasCompleted = true + extract(children) + othersToCancel.foreach(_.cancel()) def addChild(child: Cancellable): Unit = synchronized: - if status == Completed then child.cancel() - else children += this - - def isCancelled = status == Cancelled + if !hasCompleted then children += this def linked(using async: Async): this.type = - if status != Completed then async.runner.addChild(this) + if !hasCompleted then async.root.addChild(this) this - def value(using async: Async): T = async.await(this).get + def dropListener(k: Async.Listener[Try[T]]): Unit = + waiting -= k + + def value(using async: Async): T = + async.await(this).get def force(): T = - while status != Completed do wait() + while !hasCompleted do wait() result.get /** Complete future with result. If future was cancelled in the meantime, @@ -98,9 +104,10 @@ object Future: * the type with which the future was created since `Promise` is invariant. */ private[Future] def complete(result: Try[T] @uncheckedVariance): Unit = - if status == Started then this.result = result - status = Completed - for task <- currentWaiting() do task(result) + if !hasCompleted then + this.result = result + hasCompleted = true + for listener <- extract(waiting) do listener(result) notifyAll() end CoreFuture @@ -111,64 +118,53 @@ object Future: // a handler for Async private def async(body: Async ?=> Unit): Unit = boundary [Unit]: - given Async with - private def checkCancellation(): Unit = - if status == Cancelled then throw new CancellationException() - - private inline def cancelChecked[T](op: => T): T = - checkCancellation() - val res = op - checkCancellation() - res - - def await[T](src: Async.Source[T]): T = - cancelChecked: - src.poll.getOrElse: - suspend[T, Unit]: k => - src.handleWith: result => - scheduler.schedule: () => - k.resume(result) - - def awaitEither[T1, T2](src1: Async.Source[T1], src2: Async.Source[T2]): Either[T1, T2] = - cancelChecked: - src1.poll.map(Left(_)).getOrElse: - src2.poll.map(Right(_)).getOrElse: - suspend[Either[T1, T2], Unit]: k => - var found = AtomicBoolean() - src1.handleWith: result => - if !found.getAndSet(true) then - scheduler.schedule: () => - k.resume(Left(result)) - src2.handleWith: result => - if !found.getAndSet(true) then - scheduler.schedule: () => - k.resume(Right(result)) - - def runner: Cancellable = RunnableFuture.this - def scheduler = RunnableFuture.this.scheduler - end given - + given Async = new Async.AsyncImpl(this, scheduler): + def checkCancellation() = + if hasCompleted then throw new CancellationException() body end async scheduler.schedule: () => - async: - complete( - try Success(body) - catch case ex: Exception => Failure(ex)) + async(complete(Try(body))) + end RunnableFuture /** Create a future that asynchronously executes `body` that defines * its result value in a Try or returns failure if an exception was thrown. * If the future is created in an Async context, it is added to the - * children of that context's runner. + * children of that context's root. */ - def apply[T](body: Async ?=> T)( - using scheduler: Scheduler, environment: Async | Null = null): Future[T] = - val f = RunnableFuture(body) - if environment != null then environment.runner.addChild(f) + def apply[T](body: Async ?=> T)(using ac: AsyncConfig): Future[T] = + val f = RunnableFuture(body)(using ac.scheduler) + ac.root.addChild(f) f + extension [T1](f1: Future[T1]) + + /** Parallel composition of two futures. + * If both futures succeed, succeed with their values in a pair. Otherwise, + * fail with the failure that was returned first and cancel the other. + */ + def par[T2](f2: Future[T2])(using AsyncConfig): Future[(T1, T2)] = Future: + Async.await(Async.either(f1, f2)) match + case Left(Success(x1)) => (x1, f2.value) + case Right(Success(x2)) => (f1.value, x2) + case Left(Failure(ex)) => f2.cancel(); throw ex + case Right(Failure(ex)) => f1.cancel(); throw ex + + /** Alternative parallel composition of this task with `other` task. + * If either task succeeds, succeed with the success that was returned first + * and cancel the other. Otherwise, fail with the failure that was returned last. + */ + def alt[T2 >: T1](f2: Future[T2])(using AsyncConfig): Future[T2] = Future: + Async.await(Async.either(f1, f2)) match + case Left(Success(x1)) => f2.cancel(); x1 + case Right(Success(x2)) => f1.cancel(); x2 + case Left(_: Failure[?]) => f2.value + case Right(_: Failure[?]) => f1.value + + end extension + /** A promise defines a future that is be completed via the * promise's `complete` method. */ @@ -176,7 +172,7 @@ object Future: private val myFuture = CoreFuture[T]() /** The future defined by this promise */ - def future: Future[T] = myFuture + val future: Future[T] = myFuture /** Define the result value of `future`. However, if `future` was * cancelled in the meantime complete with a `CancellationException` @@ -191,40 +187,14 @@ end Future class Task[+T](val body: Async ?=> T): /** Start a future computed from the `body` of this task */ - def run(using scheduler: Scheduler, environment: Async | Null = null): Future[T] = - Future(body) + def run(using AsyncConfig) = Future(body) - /** Parallel composition of this task with `other` task. - * If both tasks succeed, succeed with their values in a pair. Otherwise, - * fail with the failure that was returned first. - */ - def par[U](other: Task[U]): Task[(T, U)] = - Task: async ?=> - val f1 = Future(this.body) - val f2 = Future(other.body) - async.awaitEither(f1, f2) match - case Left(Success(x1)) => (x1, f2.value) - case Right(Success(x2)) => (f1.value, x2) - case Left(Failure(ex)) => throw ex - case Right(Failure(ex)) => throw ex - - /** Alternative parallel composition of this task with `other` task. - * If either task succeeds, succeed with the success that was returned first. - * Otherwise, fail with the failure that was returned last. - */ - def alt[U >: T](other: Task[U]): Task[U] = - Task: async ?=> - val f1 = Future(this.body) - val f2 = Future(other.body) - async.awaitEither(f1, f2) match - case Left(Success(x1)) => x1 - case Right(Success(x2)) => x2 - case Left(_: Failure[?]) => f2.value - case Right(_: Failure[?]) => f1.value end Task def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = Future: + val f1 = Future: + x.value * 2 x.value + xs.map(_.value).sum def Main(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Int = From 0ef4ab6ffd19d41ee75fbcdc43241ad566b43d7b Mon Sep 17 00:00:00 2001 From: odersky Date: Thu, 9 Feb 2023 17:13:59 +0100 Subject: [PATCH 30/52] Reorganize channels around Sources vs ComposableSources --- tests/pos/suspend-strawman-2/Async.scala | 104 ++++++++-------- tests/pos/suspend-strawman-2/channels.scala | 127 +++++++++----------- tests/pos/suspend-strawman-2/futures.scala | 8 +- 3 files changed, 112 insertions(+), 127 deletions(-) diff --git a/tests/pos/suspend-strawman-2/Async.scala b/tests/pos/suspend-strawman-2/Async.scala index a82a928a26f3..173b73a58bfc 100644 --- a/tests/pos/suspend-strawman-2/Async.scala +++ b/tests/pos/suspend-strawman-2/Async.scala @@ -32,21 +32,18 @@ trait Async extends AsyncConfig: object Async: - abstract class AsyncImpl(val root: Cancellable, val scheduler: Scheduler) + abstract class Impl(val root: Cancellable, val scheduler: Scheduler) (using boundary.Label[Unit]) extends Async: protected def checkCancellation(): Unit - private var result: T - def await[T](src: Async.Source[T]): T = checkCancellation() var resultOpt: Option[T] = None - if src.poll: x => - result = x + src.poll: x => + resultOpt = Some(x) true - then result - else + resultOpt.getOrElse: try suspend[T, Unit]: k => src.onComplete: x => scheduler.schedule: () => @@ -54,7 +51,7 @@ object Async: true // signals to `src` that result `x` was consumed finally checkCancellation() - end AsyncImpl + end Impl /** The currently executing Async context */ inline def current(using async: Async): Async = async @@ -78,11 +75,13 @@ object Async: */ abstract case class FinalListener[T]() extends Listener[T] - /** A source that cannot be mapped, filtered, or raced. In other words, - * an item coming from a direct source must be immediately consumed in - * another async computation; no rejection of this item is possible. + /** An asynchronous data source. Sources can be persistent or ephemeral. + * A persistent source will always pass same data to calls of `poll and `onComplete`. + * An ephememral source can pass new data in every call. + * An example of a persistent source is `Future`. + * An example of an ephemeral source is `Channel`. */ - trait DirectSource[+T]: + trait Source[+T]: /** If data is available at present, pass it to function `k` * and return the result if this call. @@ -104,31 +103,29 @@ object Async: */ def dropListener(k: Listener[T]): Unit - end DirectSource + end Source - /** An asynchronous data source. Sources can be persistent or ephemeral. - * A persistent source will always pass same data to calls of `poll and `onComplete`. - * An ephememral source can pass new data in every call. - * An example of a persistent source is `Future`. - * An example of an ephemeral source is `Channel`. - */ - trait Source[+T] extends DirectSource[T]: + /** A source that can be mapped, filtered, or raced. Only ComposableSources + * can pass `false` to the `Listener` in `poll` or `onComplete`. They do + * that if the data is rejected by a filter or did not come first in a race. + */ + trait ComposableSource[+T] extends Source[T]: /** Pass on data transformed by `f` */ - def map[U](f: T => U): Source[U] = + def map[U](f: T => U): ComposableSource[U] = new DerivedSource[T, U](this): def listen(x: T, k: Listener[U]) = k(f(x)) /** Pass on only data matching the predicate `p` */ - def filter(p: T => Boolean): Source[T] = + def filter(p: T => Boolean): ComposableSource[T] = new DerivedSource[T, T](this): def listen(x: T, k: Listener[T]) = p(x) && k(x) - end Source + end ComposableSource /** As source that transforms an original source in some way */ - abstract class DerivedSource[T, U](src: Source[T]) extends Source[U]: + abstract class DerivedSource[T, U](src: Source[T]) extends ComposableSource[U]: /** Handle a value `x` passed to the original source by possibly * invokiong the continuation for this source. @@ -148,41 +145,42 @@ object Async: end DerivedSource /** Pass first result from any of `sources` to the continuation */ - def race[T](sources: Source[T]*): Source[T] = new Source: - - def poll(k: Listener[T]): Boolean = - val it = sources.iterator - var found = false - while it.hasNext && !found do - it.next.poll: x => - found = k(x) - found - found - - def onComplete(k: Listener[T]): Unit = - val listener = new ForwardingListener[T](this, k): - var foundBefore = false - def continueIfFirst(x: T): Boolean = synchronized: - if foundBefore then false else { foundBefore = k(x); foundBefore } - def apply(x: T): Boolean = - val found = continueIfFirst(x) - if found then sources.foreach(_.dropListener(this)) - found - sources.foreach(_.onComplete(listener)) - - def dropListener(k: Listener[T]): Unit = - val listener = new ForwardingListener[T](this, k): - def apply(x: T): Boolean = ??? - // not to be called, we need the listener only for its - // hashcode and equality test. - sources.foreach(_.dropListener(listener)) + def race[T](sources: ComposableSource[T]*): ComposableSource[T] = + new ComposableSource: + + def poll(k: Listener[T]): Boolean = + val it = sources.iterator + var found = false + while it.hasNext && !found do + it.next.poll: x => + found = k(x) + found + found + + def onComplete(k: Listener[T]): Unit = + val listener = new ForwardingListener[T](this, k): + var foundBefore = false + def continueIfFirst(x: T): Boolean = synchronized: + if foundBefore then false else { foundBefore = k(x); foundBefore } + def apply(x: T): Boolean = + val found = continueIfFirst(x) + if found then sources.foreach(_.dropListener(this)) + found + sources.foreach(_.onComplete(listener)) + + def dropListener(k: Listener[T]): Unit = + val listener = new ForwardingListener[T](this, k): + def apply(x: T): Boolean = ??? + // not to be called, we need the listener only for its + // hashcode and equality test. + sources.foreach(_.dropListener(listener)) end race /** If left (respectively, right) source succeeds with `x`, pass `Left(x)`, * (respectively, Right(x)) on to the continuation. */ - def either[T, U](src1: Source[T], src2: Source[U]): Source[Either[T, U]] = + def either[T, U](src1: ComposableSource[T], src2: ComposableSource[U]): ComposableSource[Either[T, U]] = race[Either[T, U]](src1.map(Left(_)), src2.map(Right(_))) end Async diff --git a/tests/pos/suspend-strawman-2/channels.scala b/tests/pos/suspend-strawman-2/channels.scala index f7a3f3b9cdcb..2d849e62ec63 100644 --- a/tests/pos/suspend-strawman-2/channels.scala +++ b/tests/pos/suspend-strawman-2/channels.scala @@ -5,7 +5,7 @@ import runtime.suspend import Async.{Listener, await} /** An unbounded channel */ -class UnboundedChannel[T] extends Async.Source[T]: +class UnboundedChannel[T] extends Async.ComposableSource[T]: private val pending = ListBuffer[T]() private val waiting = mutable.Set[Listener[T]]() @@ -45,78 +45,69 @@ class UnboundedChannel[T] extends Async.Source[T]: end UnboundedChannel -class SyncChannel[T]: - - private val pendingReads = mutable.Set[Listener[T]]() - private val pendingSends = mutable.Set[Listener[Listener[T]]]() - - private def collapse[T](k2: Listener[Listener[T]]): Option[T] = - var r: Option[T] = None - if k2 { x => r = Some(x); true } then r else None - - protected def link[T](pending: mutable.Set[T], op: T => Boolean): Boolean = - pending.iterator.find(op) match - case Some(elem) => pending -= elem; true - case None => false - - val canRead = new Async.Source[T]: - def poll(k: Listener[T]): Boolean = - link(pendingSends, sender => collapse(sender).map(k) == Some(true)) - def onComplete(k: Listener[T]): Unit = - if !poll(k) then pendingReads += k - def dropListener(k: Listener[T]): Unit = - pendingReads -= k - - val canSend = new Async.Source[Listener[T]]: - def poll(k: Listener[Listener[T]]): Boolean = - link(pendingReads, k) - def onComplete(k: Listener[Listener[T]]): Unit = - if !poll(k) then pendingSends += k - def dropListener(k: Listener[Listener[T]]): Unit = - pendingSends -= k - - def send(x: T)(using Async): Unit = - await(canSend)(x) - - def read()(using Async): T = - await(canRead) +trait SyncChannel[T]: + def canRead: Async.Source[T] + def canSend: Async.Source[Listener[T]] + + def send(x: T)(using Async): Unit = await(canSend)(x) + + def read()(using Async): T = await(canRead) + +object SyncChannel: + def apply[T](): SyncChannel[T] = new Impl[T]: + val canRead = new ReadSource + val canSend = new SendSource + + abstract class Impl[T] extends SyncChannel[T]: + protected val pendingReads = mutable.Set[Listener[T]]() + protected val pendingSends = mutable.Set[Listener[Listener[T]]]() + + protected def link[T](pending: mutable.Set[T], op: T => Boolean): Boolean = + pending.headOption match + case Some(elem) => op(elem); true + case None => false + + private def collapse[T](k2: Listener[Listener[T]]): Option[T] = + var r: Option[T] = None + if k2 { x => r = Some(x); true } then r else None + + protected class ReadSource extends Async.Source[T]: + def poll(k: Listener[T]): Boolean = + link(pendingSends, sender => collapse(sender).map(k) == Some(true)) + def onComplete(k: Listener[T]): Unit = + if !poll(k) then pendingReads += k + def dropListener(k: Listener[T]): Unit = + pendingReads -= k + + protected class SendSource extends Async.Source[Listener[T]]: + def poll(k: Listener[Listener[T]]): Boolean = + link(pendingReads, k(_)) + def onComplete(k: Listener[Listener[T]]): Unit = + if !poll(k) then pendingSends += k + def dropListener(k: Listener[Listener[T]]): Unit = + pendingSends -= k + end Impl end SyncChannel -class DirectSyncChannel[T]: - - private val pendingReads = mutable.Set[Listener[T]]() - private val pendingSends = mutable.Set[Listener[Listener[T]]]() - - private def collapse[T](k2: Listener[Listener[T]]): Option[T] = - var r: Option[T] = None - if k2 { x => r = Some(x); true } then r else None - - private def link[T](pending: mutable.Set[T], op: T => Unit): Boolean = - pending.headOption match - case Some(elem) => op(elem); true - case None => false +trait ComposableSyncChannel[T] extends SyncChannel[T]: + def canRead: Async.ComposableSource[T] + def canSend: Async.ComposableSource[Listener[T]] - val canRead = new Async.Source[T]: - def poll(k: Listener[T]): Boolean = - link(pendingSends, sender => collapse(sender).map(k)) - def onComplete(k: Listener[T]): Unit = - if !poll(k) then pendingReads += k - def dropListener(k: Listener[T]): Unit = - pendingReads -= k +object ComposableSyncChannel: + def apply[T](): ComposableSyncChannel[T] = new Impl[T]: + val canRead = new ComposableReadSource + val canSend = new ComposableSendSource - val canSend = new Async.Source[Listener[T]]: - def poll(k: Listener[Listener[T]]): Boolean = - link(pendingReads, k(_)) - def onComplete(k: Listener[Listener[T]]): Unit = - if !poll(k) then pendingSends += k - def dropListener(k: Listener[Listener[T]]): Unit = - pendingSends -= k + abstract class Impl[T] extends SyncChannel.Impl[T], ComposableSyncChannel[T]: - def send(x: T)(using Async): Unit = - await(canSend)(x) + override protected def link[T](pending: mutable.Set[T], op: T => Boolean): Boolean = + pending.iterator.find(op) match + case Some(elem) => pending -= elem; true + case None => false - def read()(using Async): T = - await(canRead) + class ComposableReadSource extends ReadSource, Async.ComposableSource[T] + class ComposableSendSource extends SendSource, Async.ComposableSource[Listener[T]] + end Impl -end DirectSyncChannel +end ComposableSyncChannel diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 5042d46f978f..f17fa552b736 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -10,7 +10,7 @@ import runtime.suspend /** A cancellable future that can suspend waiting for other synchronous sources */ -trait Future[+T] extends Async.Source[Try[T]], Cancellable: +trait Future[+T] extends Async.ComposableSource[Try[T]], Cancellable: /** Wait for this future to be completed, return its value in case of success, * or rethrow exception in case of failure. @@ -40,10 +40,6 @@ trait Future[+T] extends Async.Source[Try[T]], Cancellable: object Future: - private enum Status: - case Initial, Completed - import Status.* - /** The core part of a future that is compled explicitly by calling its * `complete` method. There are two implementations * @@ -118,7 +114,7 @@ object Future: // a handler for Async private def async(body: Async ?=> Unit): Unit = boundary [Unit]: - given Async = new Async.AsyncImpl(this, scheduler): + given Async = new Async.Impl(this, scheduler): def checkCancellation() = if hasCompleted then throw new CancellationException() body From f91d369c3b5ff4c86068bd11671c94cbaf2af2f3 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 10 Feb 2023 11:11:16 +0100 Subject: [PATCH 31/52] Use CanFilter type to distinguish FilterableChannels --- tests/pos/suspend-strawman-2/Async.scala | 60 +++++++----- tests/pos/suspend-strawman-2/channels.scala | 103 ++++++++++++++------ tests/pos/suspend-strawman-2/futures.scala | 4 +- 3 files changed, 110 insertions(+), 57 deletions(-) diff --git a/tests/pos/suspend-strawman-2/Async.scala b/tests/pos/suspend-strawman-2/Async.scala index 173b73a58bfc..142086e6986b 100644 --- a/tests/pos/suspend-strawman-2/Async.scala +++ b/tests/pos/suspend-strawman-2/Async.scala @@ -32,6 +32,9 @@ trait Async extends AsyncConfig: object Async: + /** A marker type for Source#CanFilter */ + opaque type Yes = Unit + abstract class Impl(val root: Cancellable, val scheduler: Scheduler) (using boundary.Label[Unit]) extends Async: @@ -83,6 +86,8 @@ object Async: */ trait Source[+T]: + type CanFilter + /** If data is available at present, pass it to function `k` * and return the result if this call. * `k` returns true iff the data was consumed in an async block. @@ -105,27 +110,9 @@ object Async: end Source - /** A source that can be mapped, filtered, or raced. Only ComposableSources - * can pass `false` to the `Listener` in `poll` or `onComplete`. They do - * that if the data is rejected by a filter or did not come first in a race. - */ - trait ComposableSource[+T] extends Source[T]: - - /** Pass on data transformed by `f` */ - def map[U](f: T => U): ComposableSource[U] = - new DerivedSource[T, U](this): - def listen(x: T, k: Listener[U]) = k(f(x)) - - /** Pass on only data matching the predicate `p` */ - def filter(p: T => Boolean): ComposableSource[T] = - new DerivedSource[T, T](this): - def listen(x: T, k: Listener[T]) = p(x) && k(x) - - end ComposableSource - /** As source that transforms an original source in some way */ - abstract class DerivedSource[T, U](src: Source[T]) extends ComposableSource[U]: + abstract class DerivedSource[T, U](val original: Source[T]) extends Source[U]: /** Handle a value `x` passed to the original source by possibly * invokiong the continuation for this source. @@ -137,16 +124,34 @@ object Async: def apply(x: T): Boolean = listen(x, k) def poll(k: Listener[U]): Boolean = - src.poll(transform(k)) + original.poll(transform(k)) def onComplete(k: Listener[U]): Unit = - src.onComplete(transform(k)) + original.onComplete(transform(k)) def dropListener(k: Listener[U]): Unit = - src.dropListener(transform(k)) + original.dropListener(transform(k)) end DerivedSource + extension [T](src: Source[T]) + + /** Pass on data transformed by `f` */ + def map[U](f: T => U): Source[U] { type CanFilter = src.CanFilter } = + new DerivedSource[T, U](src): + type CanFilter = src.CanFilter + def listen(x: T, k: Listener[U]) = k(f(x)) + + extension [T](src: Source[T] { type CanFilter = Yes }) + + /** Pass on only data matching the predicate `p` */ + def filter(p: T => Boolean): Source[T] { type CanFilter = src.CanFilter } = + new DerivedSource[T, T](src): + type CanFilter = src.CanFilter + def listen(x: T, k: Listener[T]) = p(x) && k(x) + + /** Pass first result from any of `sources` to the continuation */ - def race[T](sources: ComposableSource[T]*): ComposableSource[T] = - new ComposableSource: + def race[T, CF](sources: Source[T] { type CanFilter <: CF} *): Source[T] { type CanFilter <: CF } = + new Source[T]: + type CanFilter <: CF def poll(k: Listener[T]): Boolean = val it = sources.iterator @@ -180,8 +185,11 @@ object Async: /** If left (respectively, right) source succeeds with `x`, pass `Left(x)`, * (respectively, Right(x)) on to the continuation. */ - def either[T, U](src1: ComposableSource[T], src2: ComposableSource[U]): ComposableSource[Either[T, U]] = - race[Either[T, U]](src1.map(Left(_)), src2.map(Right(_))) + def either[T, U, CF]( + src1: Source[T] { type CanFilter <: CF }, + src2: Source[U] { type CanFilter <: CF }) + : Source[Either[T, U]] { type CanFilter <: CF } = + race[Either[T, U], CF](src1.map(Left(_)), src2.map(Right(_))) end Async diff --git a/tests/pos/suspend-strawman-2/channels.scala b/tests/pos/suspend-strawman-2/channels.scala index 2d849e62ec63..4ecd3982718b 100644 --- a/tests/pos/suspend-strawman-2/channels.scala +++ b/tests/pos/suspend-strawman-2/channels.scala @@ -2,10 +2,14 @@ package concurrent import scala.collection.mutable, mutable.ListBuffer import scala.util.boundary.Label import runtime.suspend -import Async.{Listener, await} +import Async.{Listener, await, Yes} + +/** An unbounded channel + * Unbounded channels are composable async sources. + */ +class UnboundedChannel[T] extends Async.Source[T]: + type CanFilter = Yes -/** An unbounded channel */ -class UnboundedChannel[T] extends Async.ComposableSource[T]: private val pending = ListBuffer[T]() private val waiting = mutable.Set[Listener[T]]() @@ -45,33 +49,56 @@ class UnboundedChannel[T] extends Async.ComposableSource[T]: end UnboundedChannel +/** An unbuffered, synchronous channel. Senders and readers both block + * until a communication between them happens. + * The channel provides two async sources, one for reading and one for + * sending. The two sources are not composable. This allows a simple + * implementation strategy where at each point either some senders + * are waiting for matching readers, or some readers are waiting for matching + * senders, or the channel is idle, i.e. there are no waiting readers or senders. + * If a send operation encounters some waiting readers, or a read operation + * encounters some waiting sender the data is transmitted directly. Otherwise + * we add the operation to the corresponding waiting pending set. + */ trait SyncChannel[T]: - def canRead: Async.Source[T] - def canSend: Async.Source[Listener[T]] + thisCannel => + + type CanFilter + + val canRead: Async.Source[T] { type CanFilter = thisCannel.CanFilter } + val canSend: Async.Source[Listener[T]] { type CanFilter = thisCannel.CanFilter } def send(x: T)(using Async): Unit = await(canSend)(x) def read()(using Async): T = await(canRead) object SyncChannel: - def apply[T](): SyncChannel[T] = new Impl[T]: - val canRead = new ReadSource - val canSend = new SendSource + def apply[T](): SyncChannel[T] = Impl[T]() + + class Impl[T] extends SyncChannel[T]: - abstract class Impl[T] extends SyncChannel[T]: - protected val pendingReads = mutable.Set[Listener[T]]() - protected val pendingSends = mutable.Set[Listener[Listener[T]]]() + private val pendingReads = mutable.Set[Listener[T]]() + private val pendingSends = mutable.Set[Listener[Listener[T]]]() protected def link[T](pending: mutable.Set[T], op: T => Boolean): Boolean = pending.headOption match - case Some(elem) => op(elem); true + case Some(elem) => + val ok = op(elem) + if !ok then + // Since sources are not filterable, we can be here only if a race + // was lost and the entry was not yet removed. In that case, remove + // it here. + pending -= pending.head + link(pending, op) + ok case None => false private def collapse[T](k2: Listener[Listener[T]]): Option[T] = var r: Option[T] = None if k2 { x => r = Some(x); true } then r else None - protected class ReadSource extends Async.Source[T]: + private class ReadSource extends Async.Source[T]: + type CanFilter = Impl.this.CanFilter def poll(k: Listener[T]): Boolean = link(pendingSends, sender => collapse(sender).map(k) == Some(true)) def onComplete(k: Listener[T]): Unit = @@ -79,35 +106,51 @@ object SyncChannel: def dropListener(k: Listener[T]): Unit = pendingReads -= k - protected class SendSource extends Async.Source[Listener[T]]: + private class SendSource extends Async.Source[Listener[T]]: + type CanFilter = Impl.this.CanFilter def poll(k: Listener[Listener[T]]): Boolean = link(pendingReads, k(_)) def onComplete(k: Listener[Listener[T]]): Unit = if !poll(k) then pendingSends += k def dropListener(k: Listener[Listener[T]]): Unit = pendingSends -= k - end Impl + val canRead = new ReadSource + val canSend = new SendSource + end Impl end SyncChannel -trait ComposableSyncChannel[T] extends SyncChannel[T]: - def canRead: Async.ComposableSource[T] - def canSend: Async.ComposableSource[Listener[T]] - -object ComposableSyncChannel: - def apply[T](): ComposableSyncChannel[T] = new Impl[T]: - val canRead = new ComposableReadSource - val canSend = new ComposableSendSource - - abstract class Impl[T] extends SyncChannel.Impl[T], ComposableSyncChannel[T]: +object FilterableSyncChannel: + def apply[T](): SyncChannel[T] { type CanFilter = Yes } = Impl[T]() + class Impl[T] extends SyncChannel.Impl[T]: + type CanFilter = Yes override protected def link[T](pending: mutable.Set[T], op: T => Boolean): Boolean = + // Since sources are filterable, we have to match all pending readers or writers + // against the incoming request pending.iterator.find(op) match case Some(elem) => pending -= elem; true case None => false - class ComposableReadSource extends ReadSource, Async.ComposableSource[T] - class ComposableSendSource extends SendSource, Async.ComposableSource[Listener[T]] - end Impl - -end ComposableSyncChannel +end FilterableSyncChannel + +def TestRace = + val c1, c2 = FilterableSyncChannel[Int]() + val s = c1.canSend + val c3 = Async.race(c1.canRead, c2.canRead) + val c4 = c3.filter(_ >= 0) + val d0 = SyncChannel[Int]() + val d1 = Async.race(c1.canRead, c2.canRead, d0.canRead) + val d2 = d1.map(_ + 1) + val c5 = Async.either(c1.canRead, c2.canRead) + .map: + case Left(x) => -x + case Right(x) => x + .filter(_ >= 0) + + //val d3bad = d1.filter(_ >= 0) + val d5 = Async.either(c1.canRead, d2) + .map: + case Left(x) => -x + case Right(x) => x + //val d6bad = d5.filter(_ >= 0) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index f17fa552b736..e446a81d47c0 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -10,7 +10,9 @@ import runtime.suspend /** A cancellable future that can suspend waiting for other synchronous sources */ -trait Future[+T] extends Async.ComposableSource[Try[T]], Cancellable: +trait Future[+T] extends Async.Source[Try[T]], Cancellable: + + type CanFilter = Async.Yes /** Wait for this future to be completed, return its value in case of success, * or rethrow exception in case of failure. From a10fbf92c1efdd0bad7dce318d3974f15a3ce7cd Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 10 Feb 2023 11:32:55 +0100 Subject: [PATCH 32/52] Drop distinction between filterable and normal sync channels Filterable sync channels don't require significantly more complexity than normal channels. The only thing a normal channel could do that a filterable channel could not, was to discard a reader and sender immediately if there was no match. If there are no filters on the source, then the only way a source could not match is by losing a race. But then the source will be removed anyway by the `race` method. So the only improvement would be had if there was a data race condition where the removal by race happens after a second channel was triggered. --- tests/pos/suspend-strawman-1/futures.scala | 7 -- tests/pos/suspend-strawman-2/Async.scala | 91 ++++++++++----------- tests/pos/suspend-strawman-2/channels.scala | 86 ++++++------------- tests/pos/suspend-strawman-2/futures.scala | 33 ++++++-- 4 files changed, 91 insertions(+), 126 deletions(-) diff --git a/tests/pos/suspend-strawman-1/futures.scala b/tests/pos/suspend-strawman-1/futures.scala index 54a569c5cce3..d9c1b6870e89 100644 --- a/tests/pos/suspend-strawman-1/futures.scala +++ b/tests/pos/suspend-strawman-1/futures.scala @@ -42,10 +42,3 @@ object Future: def Test(x: Future[Int], xs: List[Future[Int]]) = Future: x.await + xs.map(_.await).sum - - - - - - - diff --git a/tests/pos/suspend-strawman-2/Async.scala b/tests/pos/suspend-strawman-2/Async.scala index 142086e6986b..066a54518fe3 100644 --- a/tests/pos/suspend-strawman-2/Async.scala +++ b/tests/pos/suspend-strawman-2/Async.scala @@ -4,43 +4,52 @@ import scala.collection.mutable import runtime.suspend import scala.util.boundary -/** The underlying configuration of an async block */ -trait AsyncConfig: - - /** The cancellable async source underlying this async computation */ - def root: Cancellable - - /** The scheduler for runnables defined in this async computation */ - def scheduler: Scheduler - -object AsyncConfig: - - /** A toplevel async group with given scheduler and a synthetic root that - * ignores cancellation requests - */ - given fromScheduler(using s: Scheduler): AsyncConfig with - def root = Cancellable.empty - def scheduler = s - -end AsyncConfig - -/** A context that allows to suspend waiting for asynchronous data sources */ -trait Async extends AsyncConfig: +/** A context that allows to suspend waiting for asynchronous data sources + */ +trait Async extends Async.Config: /** Wait for completion of async source `src` and return the result */ def await[T](src: Async.Source[T]): T object Async: - /** A marker type for Source#CanFilter */ - opaque type Yes = Unit + /** The underlying configuration of an async block */ + trait Config: + + /** The cancellable async source underlying this async computation */ + def root: Cancellable + + /** The scheduler for runnables defined in this async computation */ + def scheduler: Scheduler + object Config: + + /** A toplevel async group with given scheduler and a synthetic root that + * ignores cancellation requests + */ + given fromScheduler(using s: Scheduler): AsyncConfig with + def root = Cancellable.empty + def scheduler = s + + end Config + + /** A possible implementation of Async. Defines an `await` method based + * on a method to check for cancellation that needs to be implemented by + * subclasses. + * + * @param root the root of the Async's config + * @param scheduler the scheduler of the Async's config + * @param label the label of the boundary that defines the representedd async block + */ abstract class Impl(val root: Cancellable, val scheduler: Scheduler) - (using boundary.Label[Unit]) extends Async: + (using label: boundary.Label[Unit]) extends Async: protected def checkCancellation(): Unit - def await[T](src: Async.Source[T]): T = + /** Await a source first by polling it, and, if that fails, by suspending + * in a onComplete call. + */ + def await[T](src: Source[T]): T = checkCancellation() var resultOpt: Option[T] = None src.poll: x => @@ -76,7 +85,7 @@ object Async: /** A listener for values that are processed directly in an async block. * Closures of type `T => Boolean` can be SAM converted to this type. */ - abstract case class FinalListener[T]() extends Listener[T] + abstract case class FinalListener[T](apply: T => Boolean) extends Listener[T] /** An asynchronous data source. Sources can be persistent or ephemeral. * A persistent source will always pass same data to calls of `poll and `onComplete`. @@ -86,10 +95,8 @@ object Async: */ trait Source[+T]: - type CanFilter - /** If data is available at present, pass it to function `k` - * and return the result if this call. + * and return the result of this call. * `k` returns true iff the data was consumed in an async block. * Calls to `poll` are always synchronous. */ @@ -104,14 +111,13 @@ object Async: /** Signal that listener `k` is dead (i.e. will always return `false` from now on). * This permits original, (i.e. non-derived) sources like futures or channels - * to drop the listener from their `waiting` sets. + * to drop the listener from their waiting sets. */ def dropListener(k: Listener[T]): Unit end Source - /** As source that transforms an original source in some way */ - + /** A source that transforms an original source in some way */ abstract class DerivedSource[T, U](val original: Source[T]) extends Source[U]: /** Handle a value `x` passed to the original source by possibly @@ -134,24 +140,18 @@ object Async: extension [T](src: Source[T]) /** Pass on data transformed by `f` */ - def map[U](f: T => U): Source[U] { type CanFilter = src.CanFilter } = + def map[U](f: T => U): Source[U] = new DerivedSource[T, U](src): - type CanFilter = src.CanFilter def listen(x: T, k: Listener[U]) = k(f(x)) - extension [T](src: Source[T] { type CanFilter = Yes }) - /** Pass on only data matching the predicate `p` */ - def filter(p: T => Boolean): Source[T] { type CanFilter = src.CanFilter } = + def filter(p: T => Boolean): Source[T] = new DerivedSource[T, T](src): - type CanFilter = src.CanFilter def listen(x: T, k: Listener[T]) = p(x) && k(x) - /** Pass first result from any of `sources` to the continuation */ - def race[T, CF](sources: Source[T] { type CanFilter <: CF} *): Source[T] { type CanFilter <: CF } = + def race[T](sources: Source[T]*): Source[T] = new Source[T]: - type CanFilter <: CF def poll(k: Listener[T]): Boolean = val it = sources.iterator @@ -185,11 +185,8 @@ object Async: /** If left (respectively, right) source succeeds with `x`, pass `Left(x)`, * (respectively, Right(x)) on to the continuation. */ - def either[T, U, CF]( - src1: Source[T] { type CanFilter <: CF }, - src2: Source[U] { type CanFilter <: CF }) - : Source[Either[T, U]] { type CanFilter <: CF } = - race[Either[T, U], CF](src1.map(Left(_)), src2.map(Right(_))) + def either[T, U](src1: Source[T], src2: Source[U]): Source[Either[T, U]] = + race[Either[T, U]](src1.map(Left(_)), src2.map(Right(_))) end Async diff --git a/tests/pos/suspend-strawman-2/channels.scala b/tests/pos/suspend-strawman-2/channels.scala index 4ecd3982718b..0bc5d7201d2e 100644 --- a/tests/pos/suspend-strawman-2/channels.scala +++ b/tests/pos/suspend-strawman-2/channels.scala @@ -2,25 +2,20 @@ package concurrent import scala.collection.mutable, mutable.ListBuffer import scala.util.boundary.Label import runtime.suspend -import Async.{Listener, await, Yes} +import Async.{Listener, await} -/** An unbounded channel - * Unbounded channels are composable async sources. +/** An unbounded asynchronous channel. Senders do not wait for matching + * readers. */ class UnboundedChannel[T] extends Async.Source[T]: - type CanFilter = Yes private val pending = ListBuffer[T]() private val waiting = mutable.Set[Listener[T]]() private def drainWaiting(x: T): Boolean = - val it = waiting.iterator - var sent = false - while it.hasNext && !sent do - val k = it.next() - sent = k(x) - if sent then waiting -= k - sent + waiting.iterator.find(_(x)) match + case Some(k) => waiting -= k; true + case None => false private def drainPending(k: Listener[T]): Boolean = val sent = pending.nonEmpty && k(pending.head) @@ -50,55 +45,40 @@ class UnboundedChannel[T] extends Async.Source[T]: end UnboundedChannel /** An unbuffered, synchronous channel. Senders and readers both block - * until a communication between them happens. - * The channel provides two async sources, one for reading and one for - * sending. The two sources are not composable. This allows a simple - * implementation strategy where at each point either some senders - * are waiting for matching readers, or some readers are waiting for matching - * senders, or the channel is idle, i.e. there are no waiting readers or senders. - * If a send operation encounters some waiting readers, or a read operation - * encounters some waiting sender the data is transmitted directly. Otherwise - * we add the operation to the corresponding waiting pending set. + * until a communication between them happens. The channel provides two + * async sources, one for reading and one for sending. If a send operation + * encounters some waiting readers, or a read operation encounters some + * waiting sender the data is transmitted directly. Otherwise we add + * the operation to the corresponding pending set. */ trait SyncChannel[T]: - thisCannel => - - type CanFilter - val canRead: Async.Source[T] { type CanFilter = thisCannel.CanFilter } - val canSend: Async.Source[Listener[T]] { type CanFilter = thisCannel.CanFilter } + val canRead: Async.Source[T] + val canSend: Async.Source[Listener[T]] def send(x: T)(using Async): Unit = await(canSend)(x) def read()(using Async): T = await(canRead) object SyncChannel: - def apply[T](): SyncChannel[T] = Impl[T]() - class Impl[T] extends SyncChannel[T]: + def apply[T](): SyncChannel[T] = new SyncChannel[T]: private val pendingReads = mutable.Set[Listener[T]]() private val pendingSends = mutable.Set[Listener[Listener[T]]]() - protected def link[T](pending: mutable.Set[T], op: T => Boolean): Boolean = - pending.headOption match - case Some(elem) => - val ok = op(elem) - if !ok then - // Since sources are not filterable, we can be here only if a race - // was lost and the entry was not yet removed. In that case, remove - // it here. - pending -= pending.head - link(pending, op) - ok + private def link[T](pending: mutable.Set[T], op: T => Boolean): Boolean = + // Since sources are filterable, we have to match all pending readers or writers + // against the incoming request + pending.iterator.find(op) match + case Some(elem) => pending -= elem; true case None => false private def collapse[T](k2: Listener[Listener[T]]): Option[T] = var r: Option[T] = None if k2 { x => r = Some(x); true } then r else None - private class ReadSource extends Async.Source[T]: - type CanFilter = Impl.this.CanFilter + val canRead = new Async.Source[T]: def poll(k: Listener[T]): Boolean = link(pendingSends, sender => collapse(sender).map(k) == Some(true)) def onComplete(k: Listener[T]): Unit = @@ -106,8 +86,7 @@ object SyncChannel: def dropListener(k: Listener[T]): Unit = pendingReads -= k - private class SendSource extends Async.Source[Listener[T]]: - type CanFilter = Impl.this.CanFilter + val canSend = new Async.Source[Listener[T]]: def poll(k: Listener[Listener[T]]): Boolean = link(pendingReads, k(_)) def onComplete(k: Listener[Listener[T]]): Unit = @@ -115,27 +94,10 @@ object SyncChannel: def dropListener(k: Listener[Listener[T]]): Unit = pendingSends -= k - val canRead = new ReadSource - val canSend = new SendSource - end Impl end SyncChannel -object FilterableSyncChannel: - def apply[T](): SyncChannel[T] { type CanFilter = Yes } = Impl[T]() - - class Impl[T] extends SyncChannel.Impl[T]: - type CanFilter = Yes - override protected def link[T](pending: mutable.Set[T], op: T => Boolean): Boolean = - // Since sources are filterable, we have to match all pending readers or writers - // against the incoming request - pending.iterator.find(op) match - case Some(elem) => pending -= elem; true - case None => false - -end FilterableSyncChannel - def TestRace = - val c1, c2 = FilterableSyncChannel[Int]() + val c1, c2 = SyncChannel[Int]() val s = c1.canSend val c3 = Async.race(c1.canRead, c2.canRead) val c4 = c3.filter(_ >= 0) @@ -148,9 +110,7 @@ def TestRace = case Right(x) => x .filter(_ >= 0) - //val d3bad = d1.filter(_ >= 0) - val d5 = Async.either(c1.canRead, d2) + val d5 = Async.either(c1.canRead, d2) .map: case Left(x) => -x case Right(x) => x - //val d6bad = d5.filter(_ >= 0) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index e446a81d47c0..36c43aa847f7 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -8,12 +8,10 @@ import scala.annotation.unchecked.uncheckedVariance import java.util.concurrent.CancellationException import runtime.suspend -/** A cancellable future that can suspend waiting for other synchronous sources +/** A cancellable future that can suspend waiting for other asynchronous sources */ trait Future[+T] extends Async.Source[Try[T]], Cancellable: - type CanFilter = Async.Yes - /** Wait for this future to be completed, return its value in case of success, * or rethrow exception in case of failure. */ @@ -25,8 +23,8 @@ trait Future[+T] extends Async.Source[Try[T]], Cancellable: def force(): T /** Links the future as a child to the current async root. - * This means the future will be cancelled when the async root - * completes. + * This means the future will be cancelled if the async root + * is canceled. */ def linked(using async: Async): this.type @@ -42,7 +40,7 @@ trait Future[+T] extends Async.Source[Try[T]], Cancellable: object Future: - /** The core part of a future that is compled explicitly by calling its + /** The core part of a future that is completed explicitly by calling its * `complete` method. There are two implementations * * - RunnableFuture: Completion is done by running a block of code @@ -113,7 +111,7 @@ object Future: private class RunnableFuture[+T](body: Async ?=> T)(using scheduler: Scheduler) extends CoreFuture[T]: - // a handler for Async + /** a handler for Async */ private def async(body: Async ?=> Unit): Unit = boundary [Unit]: given Async = new Async.Impl(this, scheduler): @@ -143,7 +141,7 @@ object Future: * If both futures succeed, succeed with their values in a pair. Otherwise, * fail with the failure that was returned first and cancel the other. */ - def par[T2](f2: Future[T2])(using AsyncConfig): Future[(T1, T2)] = Future: + def zip[T2](f2: Future[T2])(using AsyncConfig): Future[(T1, T2)] = Future: Async.await(Async.either(f1, f2)) match case Left(Success(x1)) => (x1, f2.value) case Right(Success(x2)) => (f1.value, x2) @@ -163,6 +161,8 @@ object Future: end extension + // TODO: efficient n-ary versions of the last two operations + /** A promise defines a future that is be completed via the * promise's `complete` method. */ @@ -181,7 +181,9 @@ object Future: end Promise end Future -/** A task is a template that can be turned into a runnable future */ +/** A task is a template that can be turned into a runnable future + * Composing tasks can be referentially transparent. + */ class Task[+T](val body: Async ?=> T): /** Start a future computed from the `body` of this task */ @@ -190,11 +192,24 @@ class Task[+T](val body: Async ?=> T): end Task def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = + val b = x.zip: + Future: + xs.headOption.toString + + val _: Future[(Int, String)] = b + + val c = x.alt: + Future: + b.value._1 + val _: Future[Int] = c + Future: val f1 = Future: x.value * 2 x.value + xs.map(_.value).sum +end Test + def Main(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Int = Test(x, xs).force() From a81febc32e20c2a19a0cf24212ecb1b5e1629b5c Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 11 Feb 2023 12:49:48 +0100 Subject: [PATCH 33/52] Add CSP-style coroutines that communicate with channels --- tests/pos/suspend-strawman-2/channels.scala | 38 ++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/pos/suspend-strawman-2/channels.scala b/tests/pos/suspend-strawman-2/channels.scala index 0bc5d7201d2e..2e7b3c0903c8 100644 --- a/tests/pos/suspend-strawman-2/channels.scala +++ b/tests/pos/suspend-strawman-2/channels.scala @@ -1,7 +1,8 @@ package concurrent import scala.collection.mutable, mutable.ListBuffer -import scala.util.boundary.Label +import scala.util.boundary, boundary.Label import runtime.suspend +import java.util.concurrent.CancellationException import Async.{Listener, await} /** An unbounded asynchronous channel. Senders do not wait for matching @@ -96,6 +97,40 @@ object SyncChannel: end SyncChannel +/** A simplistic coroutine. Error handling is still missing, */ +class Coroutine(body: Async ?=> Unit)(using scheduler: Scheduler) extends Cancellable: + private var children: mutable.ListBuffer[Cancellable] = mutable.ListBuffer() + @volatile var cancelled = false + + def cancel() = + cancelled = true + synchronized(children).foreach(_.cancel()) + + def addChild(child: Cancellable) = synchronized: + children += child + + boundary [Unit]: + given Async = new Async.Impl(this, scheduler): + def checkCancellation() = + if cancelled then throw new CancellationException() + try body + catch case ex: CancellationException => () +end Coroutine + +def TestChannel(using Scheduler) = + val c = SyncChannel[Option[Int]]() + Coroutine: + for i <- 0 to 100 do + c.send(Some(i)) + c.send(None) + Coroutine: + var sum = 0 + def loop(): Unit = + c.read() match + case Some(x) => sum += x; loop() + case None => println(sum) + loop() + def TestRace = val c1, c2 = SyncChannel[Int]() val s = c1.canSend @@ -114,3 +149,4 @@ def TestRace = .map: case Left(x) => -x case Right(x) => x + From 25efae13c5cacba2ceb389d22d17f4bd543b4163 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 11 Feb 2023 15:44:13 +0100 Subject: [PATCH 34/52] Fix compilation failures --- tests/pos/suspend-strawman-2/Async.scala | 2 +- tests/pos/suspend-strawman-2/futures.scala | 8 ++++---- tests/pos/suspend-strawman-2/simple-futures.scala | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/pos/suspend-strawman-2/Async.scala b/tests/pos/suspend-strawman-2/Async.scala index 066a54518fe3..d283148feda4 100644 --- a/tests/pos/suspend-strawman-2/Async.scala +++ b/tests/pos/suspend-strawman-2/Async.scala @@ -27,7 +27,7 @@ object Async: /** A toplevel async group with given scheduler and a synthetic root that * ignores cancellation requests */ - given fromScheduler(using s: Scheduler): AsyncConfig with + given fromScheduler(using s: Scheduler): Config with def root = Cancellable.empty def scheduler = s diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index 36c43aa847f7..f95f3f6960e4 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -130,7 +130,7 @@ object Future: * If the future is created in an Async context, it is added to the * children of that context's root. */ - def apply[T](body: Async ?=> T)(using ac: AsyncConfig): Future[T] = + def apply[T](body: Async ?=> T)(using ac: Async.Config): Future[T] = val f = RunnableFuture(body)(using ac.scheduler) ac.root.addChild(f) f @@ -141,7 +141,7 @@ object Future: * If both futures succeed, succeed with their values in a pair. Otherwise, * fail with the failure that was returned first and cancel the other. */ - def zip[T2](f2: Future[T2])(using AsyncConfig): Future[(T1, T2)] = Future: + def zip[T2](f2: Future[T2])(using Async.Config): Future[(T1, T2)] = Future: Async.await(Async.either(f1, f2)) match case Left(Success(x1)) => (x1, f2.value) case Right(Success(x2)) => (f1.value, x2) @@ -152,7 +152,7 @@ object Future: * If either task succeeds, succeed with the success that was returned first * and cancel the other. Otherwise, fail with the failure that was returned last. */ - def alt[T2 >: T1](f2: Future[T2])(using AsyncConfig): Future[T2] = Future: + def alt[T2 >: T1](f2: Future[T2])(using Async.Config): Future[T2] = Future: Async.await(Async.either(f1, f2)) match case Left(Success(x1)) => f2.cancel(); x1 case Right(Success(x2)) => f1.cancel(); x2 @@ -187,7 +187,7 @@ end Future class Task[+T](val body: Async ?=> T): /** Start a future computed from the `body` of this task */ - def run(using AsyncConfig) = Future(body) + def run(using Async.Config) = Future(body) end Task diff --git a/tests/pos/suspend-strawman-2/simple-futures.scala b/tests/pos/suspend-strawman-2/simple-futures.scala index b0b90db4adec..60645598c613 100644 --- a/tests/pos/suspend-strawman-2/simple-futures.scala +++ b/tests/pos/suspend-strawman-2/simple-futures.scala @@ -3,6 +3,7 @@ package simpleFutures import scala.collection.mutable.ListBuffer import scala.util.boundary, boundary.Label import runtime.* +import concurrent.Scheduler trait Async: def await[T](f: Future[T]): T From 60847c08146da3e19875a2028cd51012e7a75206 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 11 Feb 2023 15:44:27 +0100 Subject: [PATCH 35/52] Improve boundary doc comment --- library/src/scala/util/boundary.scala | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/src/scala/util/boundary.scala b/library/src/scala/util/boundary.scala index 3c6c6982c7ee..26972a2d4b76 100644 --- a/library/src/scala/util/boundary.scala +++ b/library/src/scala/util/boundary.scala @@ -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] From fffd21b30e0505ebdff37a4d63e797ef67305636 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 14 Feb 2023 16:16:07 +0100 Subject: [PATCH 36/52] Tweaks - Add first-order poll method for convenience - Drop link() method --- tests/pos/suspend-strawman-2/Async.scala | 24 ++++++----- tests/pos/suspend-strawman-2/futures.scala | 45 ++++++++++---------- tests/pos/suspend-strawman-2/scheduler.scala | 2 +- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/tests/pos/suspend-strawman-2/Async.scala b/tests/pos/suspend-strawman-2/Async.scala index d283148feda4..f98c36b620da 100644 --- a/tests/pos/suspend-strawman-2/Async.scala +++ b/tests/pos/suspend-strawman-2/Async.scala @@ -51,16 +51,13 @@ object Async: */ def await[T](src: Source[T]): T = checkCancellation() - var resultOpt: Option[T] = None - src.poll: x => - resultOpt = Some(x) - true - resultOpt.getOrElse: - try suspend[T, Unit]: k => - src.onComplete: x => - scheduler.schedule: () => - k.resume(x) - true // signals to `src` that result `x` was consumed + src.poll().getOrElse: + try + suspend[T, Unit]: k => + src.onComplete: x => + scheduler.schedule: () => + k.resume(x) + true // signals to `src` that result `x` was consumed finally checkCancellation() end Impl @@ -115,6 +112,12 @@ object Async: */ def dropListener(k: Listener[T]): Unit + /** Utililty method for direct polling. */ + def poll(): Option[T] = + var resultOpt: Option[T] = None + poll { x => resultOpt = Some(x); true } + resultOpt + end Source /** A source that transforms an original source in some way */ @@ -135,6 +138,7 @@ object Async: original.onComplete(transform(k)) def dropListener(k: Listener[U]): Unit = original.dropListener(transform(k)) + end DerivedSource extension [T](src: Source[T]) diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/pos/suspend-strawman-2/futures.scala index f95f3f6960e4..18cfdb884b39 100644 --- a/tests/pos/suspend-strawman-2/futures.scala +++ b/tests/pos/suspend-strawman-2/futures.scala @@ -6,7 +6,6 @@ import scala.compiletime.uninitialized import scala.util.{Try, Success, Failure} import scala.annotation.unchecked.uncheckedVariance import java.util.concurrent.CancellationException -import runtime.suspend /** A cancellable future that can suspend waiting for other asynchronous sources */ @@ -22,14 +21,8 @@ trait Future[+T] extends Async.Source[Try[T]], Cancellable: */ def force(): T - /** Links the future as a child to the current async root. - * This means the future will be cancelled if the async root - * is canceled. - */ - def linked(using async: Async): this.type - /** Eventually stop computation of this future and fail with - * a `Cancellation` exception. Also cancel all linked children. + * a `Cancellation` exception. Also cancel all children. */ def cancel(): Unit @@ -40,8 +33,8 @@ trait Future[+T] extends Async.Source[Try[T]], Cancellable: object Future: - /** The core part of a future that is completed explicitly by calling its - * `complete` method. There are two implementations + /** A future that is completed explicitly by calling its + * `complete` method. There are two public implementations * * - RunnableFuture: Completion is done by running a block of code * - Promise.future: Completion is done by external request. @@ -50,20 +43,27 @@ object Future: @volatile protected var hasCompleted: Boolean = false private var result: Try[T] = uninitialized // guaranteed to be set if hasCompleted = true - private var waiting: mutable.Set[Try[T] => Boolean] = mutable.Set() - private var children: mutable.Set[Cancellable] = mutable.Set() + private val waiting: mutable.Set[Try[T] => Boolean] = mutable.Set() + private val children: mutable.Set[Cancellable] = mutable.Set() private def extract[T](s: mutable.Set[T]): List[T] = synchronized: val xs = s.toList s.clear() xs + // Async.Source method implementations + def poll(k: Async.Listener[Try[T]]): Boolean = hasCompleted && k(result) def onComplete(k: Async.Listener[Try[T]]): Unit = synchronized: if !poll(k) then waiting += k + def dropListener(k: Async.Listener[Try[T]]): Unit = + waiting -= k + + // Cancellable method implementations + def cancel(): Unit = val othersToCancel = synchronized: if hasCompleted then Nil @@ -76,17 +76,12 @@ object Future: def addChild(child: Cancellable): Unit = synchronized: if !hasCompleted then children += this - def linked(using async: Async): this.type = - if !hasCompleted then async.root.addChild(this) - this - - def dropListener(k: Async.Listener[Try[T]]): Unit = - waiting -= k + // Future method implementations def value(using async: Async): T = async.await(this).get - def force(): T = + def force(): T = synchronized: while !hasCompleted do wait() result.get @@ -104,10 +99,14 @@ object Future: this.result = result hasCompleted = true for listener <- extract(waiting) do listener(result) - notifyAll() + synchronized: + notifyAll() end CoreFuture + /** A future that is completed by evaluating `body` as a separate + * asynchronous operation in the given `scheduler` + */ private class RunnableFuture[+T](body: Async ?=> T)(using scheduler: Scheduler) extends CoreFuture[T]: @@ -191,7 +190,7 @@ class Task[+T](val body: Async ?=> T): end Task -def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = +def add(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = val b = x.zip: Future: xs.headOption.toString @@ -208,8 +207,8 @@ def Test(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = x.value * 2 x.value + xs.map(_.value).sum -end Test +end add def Main(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Int = - Test(x, xs).force() + add(x, xs).force() diff --git a/tests/pos/suspend-strawman-2/scheduler.scala b/tests/pos/suspend-strawman-2/scheduler.scala index 89b3bd3e7ddb..f6e00638db15 100644 --- a/tests/pos/suspend-strawman-2/scheduler.scala +++ b/tests/pos/suspend-strawman-2/scheduler.scala @@ -5,6 +5,6 @@ trait Scheduler: def schedule(task: Runnable): Unit = ??? object Scheduler extends Scheduler: - given fromAsync(using async: Async): Scheduler = async.scheduler + given fromAsyncConfig(using ac: Async.Config): Scheduler = ac.scheduler end Scheduler From c91fad9435951c7e157955c2d269270ba963884a Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 19 Feb 2023 12:04:57 +0100 Subject: [PATCH 37/52] Move strawman files to tests/run --- tests/{pos => run}/suspend-strawman-2/Async.scala | 2 ++ tests/{pos => run}/suspend-strawman-2/Cancellable.scala | 0 tests/run/suspend-strawman-2/Test.scala | 4 ++++ tests/{pos => run}/suspend-strawman-2/channels.scala | 0 tests/{pos => run}/suspend-strawman-2/choices.scala | 2 +- tests/{pos => run}/suspend-strawman-2/futures.scala | 0 tests/{pos => run}/suspend-strawman-2/monadic-reflect.scala | 0 tests/{pos => run}/suspend-strawman-2/runtime.scala | 0 tests/{pos => run}/suspend-strawman-2/simple-futures.scala | 0 9 files changed, 7 insertions(+), 1 deletion(-) rename tests/{pos => run}/suspend-strawman-2/Async.scala (99%) rename tests/{pos => run}/suspend-strawman-2/Cancellable.scala (100%) create mode 100644 tests/run/suspend-strawman-2/Test.scala rename tests/{pos => run}/suspend-strawman-2/channels.scala (100%) rename tests/{pos => run}/suspend-strawman-2/choices.scala (95%) rename tests/{pos => run}/suspend-strawman-2/futures.scala (100%) rename tests/{pos => run}/suspend-strawman-2/monadic-reflect.scala (100%) rename tests/{pos => run}/suspend-strawman-2/runtime.scala (100%) rename tests/{pos => run}/suspend-strawman-2/simple-futures.scala (100%) diff --git a/tests/pos/suspend-strawman-2/Async.scala b/tests/run/suspend-strawman-2/Async.scala similarity index 99% rename from tests/pos/suspend-strawman-2/Async.scala rename to tests/run/suspend-strawman-2/Async.scala index f98c36b620da..45cee0264c00 100644 --- a/tests/pos/suspend-strawman-2/Async.scala +++ b/tests/run/suspend-strawman-2/Async.scala @@ -68,6 +68,8 @@ object Async: /** Await source result in currently executing Async context */ inline def await[T](src: Source[T])(using async: Async): T = async.await(src) + + /** A function `T => Boolean` whose lineage is recorded by its implementing * classes. The Listener function accepts values of type `T` and returns * `true` iff the value was consumed by an async block. diff --git a/tests/pos/suspend-strawman-2/Cancellable.scala b/tests/run/suspend-strawman-2/Cancellable.scala similarity index 100% rename from tests/pos/suspend-strawman-2/Cancellable.scala rename to tests/run/suspend-strawman-2/Cancellable.scala diff --git a/tests/run/suspend-strawman-2/Test.scala b/tests/run/suspend-strawman-2/Test.scala new file mode 100644 index 000000000000..6bb4743e9095 --- /dev/null +++ b/tests/run/suspend-strawman-2/Test.scala @@ -0,0 +1,4 @@ +import concurrent.* + +@main def Test = () + diff --git a/tests/pos/suspend-strawman-2/channels.scala b/tests/run/suspend-strawman-2/channels.scala similarity index 100% rename from tests/pos/suspend-strawman-2/channels.scala rename to tests/run/suspend-strawman-2/channels.scala diff --git a/tests/pos/suspend-strawman-2/choices.scala b/tests/run/suspend-strawman-2/choices.scala similarity index 95% rename from tests/pos/suspend-strawman-2/choices.scala rename to tests/run/suspend-strawman-2/choices.scala index 7d8ec373b0bb..ff434735a80c 100644 --- a/tests/pos/suspend-strawman-2/choices.scala +++ b/tests/run/suspend-strawman-2/choices.scala @@ -14,7 +14,7 @@ def choices[T](body: Choice ?=> T): Seq[T] = def choose[A](choices: A*)(using c: Choice): A = c.choose(choices*) -@main def test: Seq[Int] = +def TestChoices: Seq[Int] = choices: def x = choose(1, -2, -3) def y = choose("ab", "cde") diff --git a/tests/pos/suspend-strawman-2/futures.scala b/tests/run/suspend-strawman-2/futures.scala similarity index 100% rename from tests/pos/suspend-strawman-2/futures.scala rename to tests/run/suspend-strawman-2/futures.scala diff --git a/tests/pos/suspend-strawman-2/monadic-reflect.scala b/tests/run/suspend-strawman-2/monadic-reflect.scala similarity index 100% rename from tests/pos/suspend-strawman-2/monadic-reflect.scala rename to tests/run/suspend-strawman-2/monadic-reflect.scala diff --git a/tests/pos/suspend-strawman-2/runtime.scala b/tests/run/suspend-strawman-2/runtime.scala similarity index 100% rename from tests/pos/suspend-strawman-2/runtime.scala rename to tests/run/suspend-strawman-2/runtime.scala diff --git a/tests/pos/suspend-strawman-2/simple-futures.scala b/tests/run/suspend-strawman-2/simple-futures.scala similarity index 100% rename from tests/pos/suspend-strawman-2/simple-futures.scala rename to tests/run/suspend-strawman-2/simple-futures.scala From 08a29dd938bbee5b9f5cc64ac4cda9e9889aa687 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 19 Feb 2023 19:14:51 +0100 Subject: [PATCH 38/52] Add OriginalSource abstract class --- tests/run/suspend-strawman-2/Async.scala | 15 ++++++++++++- tests/run/suspend-strawman-2/channels.scala | 22 +++++++++---------- tests/run/suspend-strawman-2/choices.scala | 2 +- tests/run/suspend-strawman-2/futures.scala | 8 +++---- .../suspend-strawman-2/monadic-reflect.scala | 2 +- .../suspend-strawman-2/scheduler.scala | 2 +- .../suspend-strawman-2/simple-futures.scala | 2 +- 7 files changed, 33 insertions(+), 20 deletions(-) rename tests/{pos => run}/suspend-strawman-2/scheduler.scala (80%) diff --git a/tests/run/suspend-strawman-2/Async.scala b/tests/run/suspend-strawman-2/Async.scala index 45cee0264c00..cd311ae0e72d 100644 --- a/tests/run/suspend-strawman-2/Async.scala +++ b/tests/run/suspend-strawman-2/Async.scala @@ -95,7 +95,7 @@ object Async: trait Source[+T]: /** If data is available at present, pass it to function `k` - * and return the result of this call. + * and return the result of this call. Otherwise return false. * `k` returns true iff the data was consumed in an async block. * Calls to `poll` are always synchronous. */ @@ -122,6 +122,19 @@ object Async: end Source + /** An original source has a standard definition of `onCopmplete` in terms + * of `poll` and `addListener`. + */ + abstract class OriginalSource[+T] extends Source[T]: + + /** Add `k` to the listener set of this source */ + protected def addListener(k: Listener[T]): Unit + + def onComplete(k: Listener[T]): Unit = synchronized: + if !poll(k) then addListener(k) + + end OriginalSource + /** A source that transforms an original source in some way */ abstract class DerivedSource[T, U](val original: Source[T]) extends Source[U]: diff --git a/tests/run/suspend-strawman-2/channels.scala b/tests/run/suspend-strawman-2/channels.scala index 2e7b3c0903c8..a3ba508d9866 100644 --- a/tests/run/suspend-strawman-2/channels.scala +++ b/tests/run/suspend-strawman-2/channels.scala @@ -8,7 +8,7 @@ import Async.{Listener, await} /** An unbounded asynchronous channel. Senders do not wait for matching * readers. */ -class UnboundedChannel[T] extends Async.Source[T]: +class UnboundedChannel[T] extends Async.OriginalSource[T]: private val pending = ListBuffer[T]() private val waiting = mutable.Set[Listener[T]]() @@ -37,8 +37,8 @@ class UnboundedChannel[T] extends Async.Source[T]: def poll(k: Listener[T]): Boolean = synchronized: drainPending(k) - def onComplete(k: Listener[T]): Unit = synchronized: - if !drainPending(k) then waiting += k + def addListener(k: Listener[T]): Unit = synchronized: + waiting += k def dropListener(k: Listener[T]): Unit = synchronized: waiting -= k @@ -79,20 +79,20 @@ object SyncChannel: var r: Option[T] = None if k2 { x => r = Some(x); true } then r else None - val canRead = new Async.Source[T]: + val canRead = new Async.OriginalSource[T]: def poll(k: Listener[T]): Boolean = link(pendingSends, sender => collapse(sender).map(k) == Some(true)) - def onComplete(k: Listener[T]): Unit = - if !poll(k) then pendingReads += k - def dropListener(k: Listener[T]): Unit = + def addListener(k: Listener[T]) = synchronized: + pendingReads += k + def dropListener(k: Listener[T]): Unit = synchronized: pendingReads -= k - val canSend = new Async.Source[Listener[T]]: + val canSend = new Async.OriginalSource[Listener[T]]: def poll(k: Listener[Listener[T]]): Boolean = link(pendingReads, k(_)) - def onComplete(k: Listener[Listener[T]]): Unit = - if !poll(k) then pendingSends += k - def dropListener(k: Listener[Listener[T]]): Unit = + def addListener(k: Listener[Listener[T]]) = synchronized: + pendingSends += k + def dropListener(k: Listener[Listener[T]]): Unit = synchronized: pendingSends -= k end SyncChannel diff --git a/tests/run/suspend-strawman-2/choices.scala b/tests/run/suspend-strawman-2/choices.scala index ff434735a80c..968c223d9c0b 100644 --- a/tests/run/suspend-strawman-2/choices.scala +++ b/tests/run/suspend-strawman-2/choices.scala @@ -1,5 +1,5 @@ import scala.util.boundary, boundary.Label -import runtime.* +import runtime.suspend trait Choice: def choose[A](choices: A*): A diff --git a/tests/run/suspend-strawman-2/futures.scala b/tests/run/suspend-strawman-2/futures.scala index 18cfdb884b39..9d9c06505c69 100644 --- a/tests/run/suspend-strawman-2/futures.scala +++ b/tests/run/suspend-strawman-2/futures.scala @@ -9,7 +9,7 @@ import java.util.concurrent.CancellationException /** A cancellable future that can suspend waiting for other asynchronous sources */ -trait Future[+T] extends Async.Source[Try[T]], Cancellable: +trait Future[+T] extends Async.OriginalSource[Try[T]], Cancellable: /** Wait for this future to be completed, return its value in case of success, * or rethrow exception in case of failure. @@ -56,10 +56,10 @@ object Future: def poll(k: Async.Listener[Try[T]]): Boolean = hasCompleted && k(result) - def onComplete(k: Async.Listener[Try[T]]): Unit = synchronized: - if !poll(k) then waiting += k + def addListener(k: Async.Listener[Try[T]]): Unit = synchronized: + waiting += k - def dropListener(k: Async.Listener[Try[T]]): Unit = + def dropListener(k: Async.Listener[Try[T]]): Unit = synchronized: waiting -= k // Cancellable method implementations diff --git a/tests/run/suspend-strawman-2/monadic-reflect.scala b/tests/run/suspend-strawman-2/monadic-reflect.scala index c74fe779135f..394e6ced71c5 100644 --- a/tests/run/suspend-strawman-2/monadic-reflect.scala +++ b/tests/run/suspend-strawman-2/monadic-reflect.scala @@ -1,5 +1,5 @@ import scala.util.boundary -import runtime.* +import runtime.suspend trait Monad[F[_]]: diff --git a/tests/pos/suspend-strawman-2/scheduler.scala b/tests/run/suspend-strawman-2/scheduler.scala similarity index 80% rename from tests/pos/suspend-strawman-2/scheduler.scala rename to tests/run/suspend-strawman-2/scheduler.scala index f6e00638db15..53c14b551d4a 100644 --- a/tests/pos/suspend-strawman-2/scheduler.scala +++ b/tests/run/suspend-strawman-2/scheduler.scala @@ -2,7 +2,7 @@ package concurrent /** A hypothetical task scheduler trait */ trait Scheduler: - def schedule(task: Runnable): Unit = ??? + def schedule(task: Runnable): Unit = task.run() object Scheduler extends Scheduler: given fromAsyncConfig(using ac: Async.Config): Scheduler = ac.scheduler diff --git a/tests/run/suspend-strawman-2/simple-futures.scala b/tests/run/suspend-strawman-2/simple-futures.scala index 60645598c613..19063605dfea 100644 --- a/tests/run/suspend-strawman-2/simple-futures.scala +++ b/tests/run/suspend-strawman-2/simple-futures.scala @@ -2,7 +2,7 @@ package simpleFutures import scala.collection.mutable.ListBuffer import scala.util.boundary, boundary.Label -import runtime.* +import runtime.suspend import concurrent.Scheduler trait Async: From f9675a51e9a5430f3adef395f02a9d24fd14bbee Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 19 Feb 2023 19:16:42 +0100 Subject: [PATCH 39/52] Variant of suspension based on fibers --- tests/run/suspend-strawman-2.check | 3 ++ tests/run/suspend-strawman-2/Async.scala | 35 ++++++++++++++++--- tests/run/suspend-strawman-2/Test.scala | 22 +++++++++++- tests/run/suspend-strawman-2/channels.scala | 4 +-- .../run/suspend-strawman-2/fiberRuntime.scala | 29 +++++++++++++++ tests/run/suspend-strawman-2/futures.scala | 18 +++------- 6 files changed, 89 insertions(+), 22 deletions(-) create mode 100644 tests/run/suspend-strawman-2.check create mode 100644 tests/run/suspend-strawman-2/fiberRuntime.scala diff --git a/tests/run/suspend-strawman-2.check b/tests/run/suspend-strawman-2.check new file mode 100644 index 000000000000..4c66392cd76a --- /dev/null +++ b/tests/run/suspend-strawman-2.check @@ -0,0 +1,3 @@ +test async: +33 +(22,11) diff --git a/tests/run/suspend-strawman-2/Async.scala b/tests/run/suspend-strawman-2/Async.scala index cd311ae0e72d..f2cf9d9dea41 100644 --- a/tests/run/suspend-strawman-2/Async.scala +++ b/tests/run/suspend-strawman-2/Async.scala @@ -1,8 +1,8 @@ package concurrent import java.util.concurrent.atomic.AtomicBoolean import scala.collection.mutable -import runtime.suspend -import scala.util.boundary +import fiberRuntime.suspend +import fiberRuntime.boundary /** A context that allows to suspend waiting for asynchronous data sources */ @@ -53,23 +53,48 @@ object Async: checkCancellation() src.poll().getOrElse: try + var result: Option[T] = None suspend[T, Unit]: k => src.onComplete: x => scheduler.schedule: () => - k.resume(x) + result = Some(x) + k.resume() true // signals to `src` that result `x` was consumed + result.get finally checkCancellation() end Impl + private class Blocking(val scheduler: Scheduler = Scheduler) extends Async: + + def root = Cancellable.empty + + protected def checkCancellation(): Unit = () + + private var hasResumed = false + + def await[T](src: Source[T]): T = synchronized: + src.poll() match + case Some(x) => x + case None => + var result: Option[T] = None + src.onComplete: x => + synchronized: + result = Some(x) + notify() + true + while result.isEmpty do wait() + result.get + + def blocking[T](body: Async ?=> T, scheduler: Scheduler = Scheduler): T = + body(using Blocking()) + /** The currently executing Async context */ inline def current(using async: Async): Async = async /** Await source result in currently executing Async context */ inline def await[T](src: Source[T])(using async: Async): T = async.await(src) - - /** A function `T => Boolean` whose lineage is recorded by its implementing * classes. The Listener function accepts values of type `T` and returns * `true` iff the value was consumed by an async block. diff --git a/tests/run/suspend-strawman-2/Test.scala b/tests/run/suspend-strawman-2/Test.scala index 6bb4743e9095..066a3c38e017 100644 --- a/tests/run/suspend-strawman-2/Test.scala +++ b/tests/run/suspend-strawman-2/Test.scala @@ -1,4 +1,24 @@ import concurrent.* +import fiberRuntime.boundary.setName + +@main def Test = + given Scheduler = Scheduler + val x = Future: + setName("x") + val a = Future{ setName("xa"); 22 } + val b = Future{ setName("xb"); 11 } + val c = Future { setName("xc"); assert(false); 1 } + c.alt(Future{ setName("alt1"); a.value + b.value }).alt(c).value + val y = Future: + setName("y") + val a = Future{ setName("ya"); 22 } + val b = Future{ setName("yb"); 11 } + a.zip(b).value + println("test async:") + Async.blocking: + println(x.value) + println(y.value) + //println("test choices:") + //println(TestChoices) -@main def Test = () diff --git a/tests/run/suspend-strawman-2/channels.scala b/tests/run/suspend-strawman-2/channels.scala index a3ba508d9866..88b5785ecbae 100644 --- a/tests/run/suspend-strawman-2/channels.scala +++ b/tests/run/suspend-strawman-2/channels.scala @@ -1,7 +1,7 @@ package concurrent import scala.collection.mutable, mutable.ListBuffer -import scala.util.boundary, boundary.Label -import runtime.suspend +import fiberRuntime.boundary, boundary.Label +import fiberRuntime.suspend import java.util.concurrent.CancellationException import Async.{Listener, await} diff --git a/tests/run/suspend-strawman-2/fiberRuntime.scala b/tests/run/suspend-strawman-2/fiberRuntime.scala new file mode 100644 index 000000000000..73365e326d77 --- /dev/null +++ b/tests/run/suspend-strawman-2/fiberRuntime.scala @@ -0,0 +1,29 @@ +package fiberRuntime + +/** A delimited contination, which can be invoked with `resume` */ +class Suspension: + private var hasResumed = false + def resume(): Unit = synchronized: + hasResumed = true + notify() + def suspend(): Unit = synchronized: + if !hasResumed then + wait() + +def suspend[T, R](body: Suspension => Unit): Unit = + val susp = Suspension() + body(susp) + susp.suspend() + +object boundary: + final class Label[-T]() + + def setName(name: String) = () + + def apply[T](body: Label[T] ?=> Unit): Unit = + new Thread: + override def run() = + body(using Label[T]()) + .start() + + diff --git a/tests/run/suspend-strawman-2/futures.scala b/tests/run/suspend-strawman-2/futures.scala index 9d9c06505c69..8da6937e4a0e 100644 --- a/tests/run/suspend-strawman-2/futures.scala +++ b/tests/run/suspend-strawman-2/futures.scala @@ -1,7 +1,7 @@ package concurrent import scala.collection.mutable, mutable.ListBuffer -import scala.util.boundary +import fiberRuntime.boundary import scala.compiletime.uninitialized import scala.util.{Try, Success, Failure} import scala.annotation.unchecked.uncheckedVariance @@ -16,11 +16,6 @@ trait Future[+T] extends Async.OriginalSource[Try[T]], Cancellable: */ def value(using async: Async): T - /** Block thread until future is completed and return result - * N.B. This should be parameterized with a timeout. - */ - def force(): T - /** Eventually stop computation of this future and fail with * a `Cancellation` exception. Also cancel all children. */ @@ -81,10 +76,6 @@ object Future: def value(using async: Async): T = async.await(this).get - def force(): T = synchronized: - while !hasCompleted do wait() - result.get - /** Complete future with result. If future was cancelled in the meantime, * return a CancellationException failure instead. * Note: @uncheckedVariance is safe here since `complete` is called from @@ -99,8 +90,6 @@ object Future: this.result = result hasCompleted = true for listener <- extract(waiting) do listener(result) - synchronized: - notifyAll() end CoreFuture @@ -151,7 +140,8 @@ object Future: * If either task succeeds, succeed with the success that was returned first * and cancel the other. Otherwise, fail with the failure that was returned last. */ - def alt[T2 >: T1](f2: Future[T2])(using Async.Config): Future[T2] = Future: + def alt[T2 >: T1](f2: Future[T2], name: String = "alt")(using Async.Config): Future[T2] = Future: + boundary.setName(name) Async.await(Async.either(f1, f2)) match case Left(Success(x1)) => f2.cancel(); x1 case Right(Success(x2)) => f1.cancel(); x2 @@ -210,5 +200,5 @@ def add(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = end add def Main(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Int = - add(x, xs).force() + Async.blocking(add(x, xs).value) From dfb17131b8554a2aaa7a80b1dc779a82cbefa05d Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 19 Feb 2023 21:58:02 +0100 Subject: [PATCH 40/52] Add logging and timing randomization --- tests/run/suspend-strawman-2/Async.scala | 23 +++++++++------- tests/run/suspend-strawman-2/Test.scala | 2 ++ .../run/suspend-strawman-2/fiberRuntime.scala | 27 +++++++++++++++++-- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/tests/run/suspend-strawman-2/Async.scala b/tests/run/suspend-strawman-2/Async.scala index f2cf9d9dea41..b5fb1a9a0c91 100644 --- a/tests/run/suspend-strawman-2/Async.scala +++ b/tests/run/suspend-strawman-2/Async.scala @@ -65,6 +65,7 @@ object Async: end Impl + /** An implementation of Async that blocks the running thread when waiting */ private class Blocking(val scheduler: Scheduler = Scheduler) extends Async: def root = Cancellable.empty @@ -73,19 +74,21 @@ object Async: private var hasResumed = false - def await[T](src: Source[T]): T = synchronized: - src.poll() match - case Some(x) => x - case None => - var result: Option[T] = None - src.onComplete: x => - synchronized: - result = Some(x) - notify() - true + def await[T](src: Source[T]): T = + src.poll().getOrElse: + var result: Option[T] = None + src.onComplete: x => + synchronized: + result = Some(x) + notify() + true + synchronized: while result.isEmpty do wait() result.get + /** Execute asynchronous computation `body` on currently running thread. + * The thread will suspend when the computation waits. + */ def blocking[T](body: Async ?=> T, scheduler: Scheduler = Scheduler): T = body(using Blocking()) diff --git a/tests/run/suspend-strawman-2/Test.scala b/tests/run/suspend-strawman-2/Test.scala index 066a3c38e017..6decfc16b8f1 100644 --- a/tests/run/suspend-strawman-2/Test.scala +++ b/tests/run/suspend-strawman-2/Test.scala @@ -1,3 +1,5 @@ +// scalajs: --skip + import concurrent.* import fiberRuntime.boundary.setName diff --git a/tests/run/suspend-strawman-2/fiberRuntime.scala b/tests/run/suspend-strawman-2/fiberRuntime.scala index 73365e326d77..dbbc6fa66e68 100644 --- a/tests/run/suspend-strawman-2/fiberRuntime.scala +++ b/tests/run/suspend-strawman-2/fiberRuntime.scala @@ -1,5 +1,19 @@ package fiberRuntime +object util: + inline val logging = false + inline def log(inline msg: String) = + if logging then println(msg) + + private val rand = new java.util.Random + + def sleepABit() = + Thread.sleep(rand.nextInt(100)) + + val threadName = new ThreadLocal[String] +end util +import util.* + /** A delimited contination, which can be invoked with `resume` */ class Suspension: private var hasResumed = false @@ -8,22 +22,31 @@ class Suspension: notify() def suspend(): Unit = synchronized: if !hasResumed then + log(s"suspended ${threadName.get()}") wait() def suspend[T, R](body: Suspension => Unit): Unit = + sleepABit() + log(s"suspending ${threadName.get()}") val susp = Suspension() body(susp) + sleepABit() susp.suspend() object boundary: final class Label[-T]() - def setName(name: String) = () + def setName(name: String) = + log(s"started $name, ${Thread.currentThread.getId()}") + sleepABit() + threadName.set(name) def apply[T](body: Label[T] ?=> Unit): Unit = new Thread: override def run() = - body(using Label[T]()) + sleepABit() + try body(using Label[T]()) + finally log(s"finished ${threadName.get()} ${Thread.currentThread.getId()}") .start() From c810bf27fd66feab09c408b9a14f9a469a46001d Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 20 Feb 2023 14:00:13 +0100 Subject: [PATCH 41/52] Polishings --- tests/run/suspend-strawman-2/Async.scala | 21 ++++++++++++--------- tests/run/suspend-strawman-2/Test.scala | 6 ++++++ tests/run/suspend-strawman-2/futures.scala | 6 +++--- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/tests/run/suspend-strawman-2/Async.scala b/tests/run/suspend-strawman-2/Async.scala index b5fb1a9a0c91..d7774f5d2639 100644 --- a/tests/run/suspend-strawman-2/Async.scala +++ b/tests/run/suspend-strawman-2/Async.scala @@ -53,7 +53,7 @@ object Async: checkCancellation() src.poll().getOrElse: try - var result: Option[T] = None + var result: Option[T] = None // Not needed if we have full continuations suspend[T, Unit]: k => src.onComplete: x => scheduler.schedule: () => @@ -61,6 +61,14 @@ object Async: k.resume() true // signals to `src` that result `x` was consumed result.get + /* With full continuations, the try block can be written more simply as follows: + + suspend[T, Unit]: k => + src.onComplete: x => + scheduler.schedule: () => + k.resume(x) + true + */ finally checkCancellation() end Impl @@ -109,11 +117,6 @@ object Async: */ abstract case class ForwardingListener[T](src: Source[?], continue: Listener[?]) extends Listener[T] - /** A listener for values that are processed directly in an async block. - * Closures of type `T => Boolean` can be SAM converted to this type. - */ - abstract case class FinalListener[T](apply: T => Boolean) extends Listener[T] - /** An asynchronous data source. Sources can be persistent or ephemeral. * A persistent source will always pass same data to calls of `poll and `onComplete`. * An ephememral source can pass new data in every call. @@ -158,7 +161,7 @@ object Async: /** Add `k` to the listener set of this source */ protected def addListener(k: Listener[T]): Unit - def onComplete(k: Listener[T]): Unit = synchronized: + def onComplete(k: Listener[T]): Unit = if !poll(k) then addListener(k) end OriginalSource @@ -232,8 +235,8 @@ object Async: /** If left (respectively, right) source succeeds with `x`, pass `Left(x)`, * (respectively, Right(x)) on to the continuation. */ - def either[T, U](src1: Source[T], src2: Source[U]): Source[Either[T, U]] = - race[Either[T, U]](src1.map(Left(_)), src2.map(Right(_))) + def either[T1, T2](src1: Source[T1], src2: Source[T2]): Source[Either[T1, T2]] = + race(src1.map(Left(_)), src2.map(Right(_))) end Async diff --git a/tests/run/suspend-strawman-2/Test.scala b/tests/run/suspend-strawman-2/Test.scala index 6decfc16b8f1..c15f69f81c65 100644 --- a/tests/run/suspend-strawman-2/Test.scala +++ b/tests/run/suspend-strawman-2/Test.scala @@ -16,6 +16,11 @@ import fiberRuntime.boundary.setName val a = Future{ setName("ya"); 22 } val b = Future{ setName("yb"); 11 } a.zip(b).value + val z = Future: + val a = Future{ setName("za"); 22 } + val b = Future{ setName("zb"); true } + a.alt(b).value + val _: Future[Int | Boolean] = z println("test async:") Async.blocking: println(x.value) @@ -24,3 +29,4 @@ import fiberRuntime.boundary.setName //println(TestChoices) + diff --git a/tests/run/suspend-strawman-2/futures.scala b/tests/run/suspend-strawman-2/futures.scala index 8da6937e4a0e..1edce053c583 100644 --- a/tests/run/suspend-strawman-2/futures.scala +++ b/tests/run/suspend-strawman-2/futures.scala @@ -123,13 +123,13 @@ object Future: ac.root.addChild(f) f - extension [T1](f1: Future[T1]) + extension [T](f1: Future[T]) /** Parallel composition of two futures. * If both futures succeed, succeed with their values in a pair. Otherwise, * fail with the failure that was returned first and cancel the other. */ - def zip[T2](f2: Future[T2])(using Async.Config): Future[(T1, T2)] = Future: + def zip[U](f2: Future[U])(using Async.Config): Future[(T, U)] = Future: Async.await(Async.either(f1, f2)) match case Left(Success(x1)) => (x1, f2.value) case Right(Success(x2)) => (f1.value, x2) @@ -140,7 +140,7 @@ object Future: * If either task succeeds, succeed with the success that was returned first * and cancel the other. Otherwise, fail with the failure that was returned last. */ - def alt[T2 >: T1](f2: Future[T2], name: String = "alt")(using Async.Config): Future[T2] = Future: + def alt(f2: Future[T], name: String = "alt")(using Async.Config): Future[T] = Future: boundary.setName(name) Async.await(Async.either(f1, f2)) match case Left(Success(x1)) => f2.cancel(); x1 From 294bf1de9a3fee87296460c395189e3a1b4e9e0b Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 21 Feb 2023 18:18:12 +0100 Subject: [PATCH 42/52] Use Futures instead of Coroutines Even for a coroutine we want to observe normal or anormal termination. In that sense a Coroutine is really nothing but a Future[Unit]. --- tests/run/suspend-strawman-2/channels.scala | 24 ++------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/tests/run/suspend-strawman-2/channels.scala b/tests/run/suspend-strawman-2/channels.scala index 88b5785ecbae..c36be4da0704 100644 --- a/tests/run/suspend-strawman-2/channels.scala +++ b/tests/run/suspend-strawman-2/channels.scala @@ -97,33 +97,13 @@ object SyncChannel: end SyncChannel -/** A simplistic coroutine. Error handling is still missing, */ -class Coroutine(body: Async ?=> Unit)(using scheduler: Scheduler) extends Cancellable: - private var children: mutable.ListBuffer[Cancellable] = mutable.ListBuffer() - @volatile var cancelled = false - - def cancel() = - cancelled = true - synchronized(children).foreach(_.cancel()) - - def addChild(child: Cancellable) = synchronized: - children += child - - boundary [Unit]: - given Async = new Async.Impl(this, scheduler): - def checkCancellation() = - if cancelled then throw new CancellationException() - try body - catch case ex: CancellationException => () -end Coroutine - def TestChannel(using Scheduler) = val c = SyncChannel[Option[Int]]() - Coroutine: + Future: for i <- 0 to 100 do c.send(Some(i)) c.send(None) - Coroutine: + Future: var sum = 0 def loop(): Unit = c.read() match From 7819b61cc282b4fdb564ae832537be505ab40c91 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 21 Feb 2023 20:28:49 +0100 Subject: [PATCH 43/52] Refactoring of Cancellables --- tests/run/suspend-strawman-2/Async.scala | 69 ++++---------- .../run/suspend-strawman-2/Cancellable.scala | 55 ++++++++++-- tests/run/suspend-strawman-2/futures.scala | 90 +++++++++++-------- tests/run/suspend-strawman-2/scheduler.scala | 1 - 4 files changed, 118 insertions(+), 97 deletions(-) diff --git a/tests/run/suspend-strawman-2/Async.scala b/tests/run/suspend-strawman-2/Async.scala index d7774f5d2639..20ad9150c70d 100644 --- a/tests/run/suspend-strawman-2/Async.scala +++ b/tests/run/suspend-strawman-2/Async.scala @@ -1,8 +1,6 @@ package concurrent import java.util.concurrent.atomic.AtomicBoolean import scala.collection.mutable -import fiberRuntime.suspend -import fiberRuntime.boundary /** A context that allows to suspend waiting for asynchronous data sources */ @@ -11,13 +9,16 @@ trait Async extends Async.Config: /** Wait for completion of async source `src` and return the result */ def await[T](src: Async.Source[T]): T + /** An Async of the same kind as this one, with a given cancellation group */ + def withGroup(group: Cancellable.Group): Async + object Async: /** The underlying configuration of an async block */ trait Config: - /** The cancellable async source underlying this async computation */ - def root: Cancellable + /** The group of cancellableup to which nested futures belong */ + def group: Cancellable.Group /** The scheduler for runnables defined in this async computation */ def scheduler: Scheduler @@ -28,59 +29,13 @@ object Async: * ignores cancellation requests */ given fromScheduler(using s: Scheduler): Config with - def root = Cancellable.empty + def group = Cancellable.Unlinked def scheduler = s end Config - /** A possible implementation of Async. Defines an `await` method based - * on a method to check for cancellation that needs to be implemented by - * subclasses. - * - * @param root the root of the Async's config - * @param scheduler the scheduler of the Async's config - * @param label the label of the boundary that defines the representedd async block - */ - abstract class Impl(val root: Cancellable, val scheduler: Scheduler) - (using label: boundary.Label[Unit]) extends Async: - - protected def checkCancellation(): Unit - - /** Await a source first by polling it, and, if that fails, by suspending - * in a onComplete call. - */ - def await[T](src: Source[T]): T = - checkCancellation() - src.poll().getOrElse: - try - var result: Option[T] = None // Not needed if we have full continuations - suspend[T, Unit]: k => - src.onComplete: x => - scheduler.schedule: () => - result = Some(x) - k.resume() - true // signals to `src` that result `x` was consumed - result.get - /* With full continuations, the try block can be written more simply as follows: - - suspend[T, Unit]: k => - src.onComplete: x => - scheduler.schedule: () => - k.resume(x) - true - */ - finally checkCancellation() - - end Impl - /** An implementation of Async that blocks the running thread when waiting */ - private class Blocking(val scheduler: Scheduler = Scheduler) extends Async: - - def root = Cancellable.empty - - protected def checkCancellation(): Unit = () - - private var hasResumed = false + private class Blocking(val scheduler: Scheduler, val group: Cancellable.Group) extends Async: def await[T](src: Source[T]): T = src.poll().getOrElse: @@ -94,11 +49,15 @@ object Async: while result.isEmpty do wait() result.get + def withGroup(group: Cancellable.Group) = Blocking(scheduler, group) + end Blocking + + /** Execute asynchronous computation `body` on currently running thread. * The thread will suspend when the computation waits. */ def blocking[T](body: Async ?=> T, scheduler: Scheduler = Scheduler): T = - body(using Blocking()) + body(using Blocking(scheduler, Cancellable.Unlinked)) /** The currently executing Async context */ inline def current(using async: Async): Async = async @@ -106,6 +65,10 @@ object Async: /** Await source result in currently executing Async context */ inline def await[T](src: Source[T])(using async: Async): T = async.await(src) + def group[T](body: Async ?=> T)(using async: Async): T = + val newGroup = Cancellable.Group().link() + body(using async.withGroup(newGroup)) + /** A function `T => Boolean` whose lineage is recorded by its implementing * classes. The Listener function accepts values of type `T` and returns * `true` iff the value was consumed by an async block. diff --git a/tests/run/suspend-strawman-2/Cancellable.scala b/tests/run/suspend-strawman-2/Cancellable.scala index 1a99142380dc..76c0f65dd031 100644 --- a/tests/run/suspend-strawman-2/Cancellable.scala +++ b/tests/run/suspend-strawman-2/Cancellable.scala @@ -1,21 +1,62 @@ package concurrent +import scala.collection.mutable /** A trait for cancellable entities that can be grouped */ trait Cancellable: + private var group: Cancellable.Group = Cancellable.Unlinked + /** Issue a cancel request */ def cancel(): Unit - /** Add a given child to this Cancellable, so that the child will be cancelled - * when the Cancellable itself is cancelled. + /** Add this cancellable to the given group after removing + * it from the previous group in which it was. + */ + def link(group: Cancellable.Group): this.type = + this.group.drop(this) + this.group = group + this.group.add(this) + this + + /** Link this cancellable to the cancellable group of the + * current async context. */ - def addChild(child: Cancellable): Unit + def link()(using ac: Async): this.type = + link(ac.group) + + /** Unlink this cancellable from its group. */ + def unlink(): this.type = + link(Cancellable.Unlinked) object Cancellable: - /** A cancelled entity that ignores all `cancel` and `addChild` requests */ - object empty extends Cancellable: - def cancel() = () - def addChild(child: Cancellable) = () + /** A group of cancellable members */ + class Group extends Cancellable: + private var members: mutable.Set[Cancellable] = mutable.Set() + + /** Cancel all members and clear the members set */ + def cancel() = + members.toArray.foreach(_.cancel()) + members.clear() + + /** Add given member to the members set */ + def add(member: Cancellable): Unit = synchronized: + members += member + + /** Remove given member from the members set if it is an element */ + def drop(member: Cancellable): Unit = synchronized: + members -= member + end Group + + /** A sentinal group of cancellables that are in fact not linked + * to any real group. `cancel`, `add`, and `drop` do nothing when + * called on this group. + */ + object Unlinked extends Group: + override def cancel() = () + override def add(member: Cancellable): Unit = () + override def drop(member: Cancellable): Unit = () + end Unlinked end Cancellable + diff --git a/tests/run/suspend-strawman-2/futures.scala b/tests/run/suspend-strawman-2/futures.scala index 1edce053c583..5502f3096ed3 100644 --- a/tests/run/suspend-strawman-2/futures.scala +++ b/tests/run/suspend-strawman-2/futures.scala @@ -1,6 +1,7 @@ package concurrent import scala.collection.mutable, mutable.ListBuffer +import fiberRuntime.suspend import fiberRuntime.boundary import scala.compiletime.uninitialized import scala.util.{Try, Success, Failure} @@ -17,15 +18,10 @@ trait Future[+T] extends Async.OriginalSource[Try[T]], Cancellable: def value(using async: Async): T /** Eventually stop computation of this future and fail with - * a `Cancellation` exception. Also cancel all children. + * a `Cancellation` exception. */ def cancel(): Unit - /** If this future has not yet completed, add `child` so that it will - * be cancelled together with this future in case the future is cancelled. - */ - def addChild(child: Cancellable): Unit - object Future: /** A future that is completed explicitly by calling its @@ -37,14 +33,9 @@ object Future: private class CoreFuture[+T] extends Future[T]: @volatile protected var hasCompleted: Boolean = false + protected var cancelRequest = false private var result: Try[T] = uninitialized // guaranteed to be set if hasCompleted = true private val waiting: mutable.Set[Try[T] => Boolean] = mutable.Set() - private val children: mutable.Set[Cancellable] = mutable.Set() - - private def extract[T](s: mutable.Set[T]): List[T] = synchronized: - val xs = s.toList - s.clear() - xs // Async.Source method implementations @@ -60,16 +51,7 @@ object Future: // Cancellable method implementations def cancel(): Unit = - val othersToCancel = synchronized: - if hasCompleted then Nil - else - result = Failure(new CancellationException()) - hasCompleted = true - extract(children) - othersToCancel.foreach(_.cancel()) - - def addChild(child: Cancellable): Unit = synchronized: - if !hasCompleted then children += this + cancelRequest = true // Future method implementations @@ -86,30 +68,68 @@ object Future: * the type with which the future was created since `Promise` is invariant. */ private[Future] def complete(result: Try[T] @uncheckedVariance): Unit = - if !hasCompleted then - this.result = result - hasCompleted = true - for listener <- extract(waiting) do listener(result) + val toNotify = synchronized: + if hasCompleted then Nil + else + this.result = result + hasCompleted = true + val ws = waiting.toList + waiting.clear() + ws + for listener <- toNotify do listener(result) end CoreFuture /** A future that is completed by evaluating `body` as a separate * asynchronous operation in the given `scheduler` */ - private class RunnableFuture[+T](body: Async ?=> T)(using scheduler: Scheduler) + private class RunnableFuture[+T](body: Async ?=> T)(using ac: Async.Config) extends CoreFuture[T]: /** a handler for Async */ private def async(body: Async ?=> Unit): Unit = + class FutureAsync(val scheduler: Scheduler, val group: Cancellable.Group) extends Async: + + def checkCancellation() = + if cancelRequest then throw CancellationException() + + /** Await a source first by polling it, and, if that fails, by suspending + * in a onComplete call. + */ + def await[T](src: Async.Source[T]): T = + checkCancellation() + src.poll().getOrElse: + try + var result: Option[T] = None // Not needed if we have full continuations + suspend[T, Unit]: k => + src.onComplete: x => + scheduler.schedule: () => + result = Some(x) + k.resume() + true // signals to `src` that result `x` was consumed + result.get + /* With full continuations, the try block can be written more simply as follows: + + suspend[T, Unit]: k => + src.onComplete: x => + scheduler.schedule: () => + k.resume(x) + true + */ + finally checkCancellation() + + def withGroup(group: Cancellable.Group) = FutureAsync(scheduler, group) + boundary [Unit]: - given Async = new Async.Impl(this, scheduler): - def checkCancellation() = - if hasCompleted then throw new CancellationException() - body + body(using FutureAsync(ac.scheduler, ac.group)) end async - scheduler.schedule: () => - async(complete(Try(body))) + ac.scheduler.schedule: () => + async: + link() + Async.group: + complete(Try(body)) + unlink() end RunnableFuture @@ -119,9 +139,7 @@ object Future: * children of that context's root. */ def apply[T](body: Async ?=> T)(using ac: Async.Config): Future[T] = - val f = RunnableFuture(body)(using ac.scheduler) - ac.root.addChild(f) - f + RunnableFuture(body) extension [T](f1: Future[T]) diff --git a/tests/run/suspend-strawman-2/scheduler.scala b/tests/run/suspend-strawman-2/scheduler.scala index 53c14b551d4a..56458439f5ba 100644 --- a/tests/run/suspend-strawman-2/scheduler.scala +++ b/tests/run/suspend-strawman-2/scheduler.scala @@ -5,6 +5,5 @@ trait Scheduler: def schedule(task: Runnable): Unit = task.run() object Scheduler extends Scheduler: - given fromAsyncConfig(using ac: Async.Config): Scheduler = ac.scheduler end Scheduler From 518f767db5d09923aa30f4e596d477f4b7504151 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 21 Feb 2023 22:43:47 +0100 Subject: [PATCH 44/52] Make Async.Group a field in Async instead of inheriting from it --- tests/run/suspend-strawman-2/Async.scala | 41 ++++++++++--------- .../run/suspend-strawman-2/Cancellable.scala | 4 +- tests/run/suspend-strawman-2/futures.scala | 8 ++-- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/tests/run/suspend-strawman-2/Async.scala b/tests/run/suspend-strawman-2/Async.scala index 20ad9150c70d..a3aea34bfd9a 100644 --- a/tests/run/suspend-strawman-2/Async.scala +++ b/tests/run/suspend-strawman-2/Async.scala @@ -4,38 +4,40 @@ import scala.collection.mutable /** A context that allows to suspend waiting for asynchronous data sources */ -trait Async extends Async.Config: +trait Async: /** Wait for completion of async source `src` and return the result */ def await[T](src: Async.Source[T]): T - /** An Async of the same kind as this one, with a given cancellation group */ - def withGroup(group: Cancellable.Group): Async + /** The configuration of this Async */ + def config: Async.Config + + /** An Async of the same kind as this one, with a new configuration as given */ + def withConfig(config: Async.Config): Async object Async: /** The underlying configuration of an async block */ - trait Config: - - /** The group of cancellableup to which nested futures belong */ - def group: Cancellable.Group + case class Config(scheduler: Scheduler, group: Cancellable.Group) - /** The scheduler for runnables defined in this async computation */ - def scheduler: Scheduler - - object Config: + trait LowPrioConfig: /** A toplevel async group with given scheduler and a synthetic root that * ignores cancellation requests */ - given fromScheduler(using s: Scheduler): Config with - def group = Cancellable.Unlinked - def scheduler = s + given fromScheduler(using s: Scheduler): Config = Config(s, Cancellable.Unlinked) + + end LowPrioConfig + + object Config extends LowPrioConfig: + + /** The async configuration stored in the given async capabaility */ + given fromAsync(using async: Async): Config = async.config end Config /** An implementation of Async that blocks the running thread when waiting */ - private class Blocking(val scheduler: Scheduler, val group: Cancellable.Group) extends Async: + private class Blocking(using val config: Config) extends Async: def await[T](src: Source[T]): T = src.poll().getOrElse: @@ -49,15 +51,14 @@ object Async: while result.isEmpty do wait() result.get - def withGroup(group: Cancellable.Group) = Blocking(scheduler, group) + def withConfig(config: Config) = Blocking(using config) end Blocking - /** Execute asynchronous computation `body` on currently running thread. * The thread will suspend when the computation waits. */ - def blocking[T](body: Async ?=> T, scheduler: Scheduler = Scheduler): T = - body(using Blocking(scheduler, Cancellable.Unlinked)) + def blocking[T](body: Async ?=> T)(using Scheduler): T = + body(using Blocking()) /** The currently executing Async context */ inline def current(using async: Async): Async = async @@ -67,7 +68,7 @@ object Async: def group[T](body: Async ?=> T)(using async: Async): T = val newGroup = Cancellable.Group().link() - body(using async.withGroup(newGroup)) + body(using async.withConfig(async.config.copy(group = newGroup))) /** A function `T => Boolean` whose lineage is recorded by its implementing * classes. The Listener function accepts values of type `T` and returns diff --git a/tests/run/suspend-strawman-2/Cancellable.scala b/tests/run/suspend-strawman-2/Cancellable.scala index 76c0f65dd031..994c182bac54 100644 --- a/tests/run/suspend-strawman-2/Cancellable.scala +++ b/tests/run/suspend-strawman-2/Cancellable.scala @@ -21,8 +21,8 @@ trait Cancellable: /** Link this cancellable to the cancellable group of the * current async context. */ - def link()(using ac: Async): this.type = - link(ac.group) + def link()(using async: Async): this.type = + link(async.config.group) /** Unlink this cancellable from its group. */ def unlink(): this.type = diff --git a/tests/run/suspend-strawman-2/futures.scala b/tests/run/suspend-strawman-2/futures.scala index 5502f3096ed3..60d68a49de60 100644 --- a/tests/run/suspend-strawman-2/futures.scala +++ b/tests/run/suspend-strawman-2/futures.scala @@ -88,7 +88,7 @@ object Future: /** a handler for Async */ private def async(body: Async ?=> Unit): Unit = - class FutureAsync(val scheduler: Scheduler, val group: Cancellable.Group) extends Async: + class FutureAsync(using val config: Async.Config) extends Async: def checkCancellation() = if cancelRequest then throw CancellationException() @@ -103,7 +103,7 @@ object Future: var result: Option[T] = None // Not needed if we have full continuations suspend[T, Unit]: k => src.onComplete: x => - scheduler.schedule: () => + config.scheduler.schedule: () => result = Some(x) k.resume() true // signals to `src` that result `x` was consumed @@ -118,10 +118,10 @@ object Future: */ finally checkCancellation() - def withGroup(group: Cancellable.Group) = FutureAsync(scheduler, group) + def withConfig(config: Async.Config) = FutureAsync(using config) boundary [Unit]: - body(using FutureAsync(ac.scheduler, ac.group)) + body(using FutureAsync()) end async ac.scheduler.schedule: () => From 503ec8b36e2b7e24c5fad3e1916c5696d09143d5 Mon Sep 17 00:00:00 2001 From: odersky Date: Tue, 21 Feb 2023 23:16:37 +0100 Subject: [PATCH 45/52] Use ExecutionContext instead of Scheduler --- tests/run/suspend-strawman-2/Async.scala | 8 +++++--- tests/run/suspend-strawman-2/Test.scala | 3 ++- tests/run/suspend-strawman-2/channels.scala | 4 ++-- tests/run/suspend-strawman-2/futures.scala | 9 +++++---- tests/run/suspend-strawman-2/scheduler.scala | 9 --------- tests/run/suspend-strawman-2/simple-futures.scala | 4 +++- 6 files changed, 17 insertions(+), 20 deletions(-) delete mode 100644 tests/run/suspend-strawman-2/scheduler.scala diff --git a/tests/run/suspend-strawman-2/Async.scala b/tests/run/suspend-strawman-2/Async.scala index a3aea34bfd9a..197f4903d641 100644 --- a/tests/run/suspend-strawman-2/Async.scala +++ b/tests/run/suspend-strawman-2/Async.scala @@ -1,6 +1,7 @@ package concurrent import java.util.concurrent.atomic.AtomicBoolean import scala.collection.mutable +import scala.concurrent.ExecutionContext /** A context that allows to suspend waiting for asynchronous data sources */ @@ -18,14 +19,15 @@ trait Async: object Async: /** The underlying configuration of an async block */ - case class Config(scheduler: Scheduler, group: Cancellable.Group) + case class Config(scheduler: ExecutionContext, group: Cancellable.Group) trait LowPrioConfig: /** A toplevel async group with given scheduler and a synthetic root that * ignores cancellation requests */ - given fromScheduler(using s: Scheduler): Config = Config(s, Cancellable.Unlinked) + given fromExecutionContext(using scheduler: ExecutionContext): Config = + Config(scheduler, Cancellable.Unlinked) end LowPrioConfig @@ -57,7 +59,7 @@ object Async: /** Execute asynchronous computation `body` on currently running thread. * The thread will suspend when the computation waits. */ - def blocking[T](body: Async ?=> T)(using Scheduler): T = + def blocking[T](body: Async ?=> T)(using ExecutionContext): T = body(using Blocking()) /** The currently executing Async context */ diff --git a/tests/run/suspend-strawman-2/Test.scala b/tests/run/suspend-strawman-2/Test.scala index c15f69f81c65..42588295bc0b 100644 --- a/tests/run/suspend-strawman-2/Test.scala +++ b/tests/run/suspend-strawman-2/Test.scala @@ -2,9 +2,10 @@ import concurrent.* import fiberRuntime.boundary.setName +import scala.concurrent.ExecutionContext @main def Test = - given Scheduler = Scheduler + given ExecutionContext = ExecutionContext.global val x = Future: setName("x") val a = Future{ setName("xa"); 22 } diff --git a/tests/run/suspend-strawman-2/channels.scala b/tests/run/suspend-strawman-2/channels.scala index c36be4da0704..cd81e458c220 100644 --- a/tests/run/suspend-strawman-2/channels.scala +++ b/tests/run/suspend-strawman-2/channels.scala @@ -2,7 +2,7 @@ package concurrent import scala.collection.mutable, mutable.ListBuffer import fiberRuntime.boundary, boundary.Label import fiberRuntime.suspend -import java.util.concurrent.CancellationException +import scala.concurrent.ExecutionContext import Async.{Listener, await} /** An unbounded asynchronous channel. Senders do not wait for matching @@ -97,7 +97,7 @@ object SyncChannel: end SyncChannel -def TestChannel(using Scheduler) = +def TestChannel(using ExecutionContext) = val c = SyncChannel[Option[Int]]() Future: for i <- 0 to 100 do diff --git a/tests/run/suspend-strawman-2/futures.scala b/tests/run/suspend-strawman-2/futures.scala index 60d68a49de60..1649f41ca178 100644 --- a/tests/run/suspend-strawman-2/futures.scala +++ b/tests/run/suspend-strawman-2/futures.scala @@ -7,6 +7,7 @@ import scala.compiletime.uninitialized import scala.util.{Try, Success, Failure} import scala.annotation.unchecked.uncheckedVariance import java.util.concurrent.CancellationException +import scala.concurrent.ExecutionContext /** A cancellable future that can suspend waiting for other asynchronous sources */ @@ -103,7 +104,7 @@ object Future: var result: Option[T] = None // Not needed if we have full continuations suspend[T, Unit]: k => src.onComplete: x => - config.scheduler.schedule: () => + config.scheduler.execute: () => result = Some(x) k.resume() true // signals to `src` that result `x` was consumed @@ -124,7 +125,7 @@ object Future: body(using FutureAsync()) end async - ac.scheduler.schedule: () => + ac.scheduler.execute: () => async: link() Async.group: @@ -198,7 +199,7 @@ class Task[+T](val body: Async ?=> T): end Task -def add(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = +def add(x: Future[Int], xs: List[Future[Int]])(using ExecutionContext): Future[Int] = val b = x.zip: Future: xs.headOption.toString @@ -217,6 +218,6 @@ def add(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Future[Int] = end add -def Main(x: Future[Int], xs: List[Future[Int]])(using Scheduler): Int = +def Main(x: Future[Int], xs: List[Future[Int]])(using ExecutionContext): Int = Async.blocking(add(x, xs).value) diff --git a/tests/run/suspend-strawman-2/scheduler.scala b/tests/run/suspend-strawman-2/scheduler.scala deleted file mode 100644 index 56458439f5ba..000000000000 --- a/tests/run/suspend-strawman-2/scheduler.scala +++ /dev/null @@ -1,9 +0,0 @@ -package concurrent - -/** A hypothetical task scheduler trait */ -trait Scheduler: - def schedule(task: Runnable): Unit = task.run() - -object Scheduler extends Scheduler: -end Scheduler - diff --git a/tests/run/suspend-strawman-2/simple-futures.scala b/tests/run/suspend-strawman-2/simple-futures.scala index 19063605dfea..0a80a74d49dc 100644 --- a/tests/run/suspend-strawman-2/simple-futures.scala +++ b/tests/run/suspend-strawman-2/simple-futures.scala @@ -3,7 +3,9 @@ package simpleFutures import scala.collection.mutable.ListBuffer import scala.util.boundary, boundary.Label import runtime.suspend -import concurrent.Scheduler + +object Scheduler: + def schedule(task: Runnable): Unit = ??? trait Async: def await[T](f: Future[T]): T From 6f5dfc573ae86c063dea0c992efc66bd02f9e388 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 26 Feb 2023 20:25:32 +0100 Subject: [PATCH 46/52] Add explanation document and align implementation with it --- tests/run/suspend-strawman-2/Async.scala | 8 +- .../run/suspend-strawman-2/Cancellable.scala | 40 +- tests/run/suspend-strawman-2/channels.scala | 33 +- tests/run/suspend-strawman-2/foundations.md | 474 ++++++++++++++++++ tests/run/suspend-strawman-2/futures.scala | 65 ++- tests/run/suspend-strawman-2/streams.scala | 15 + 6 files changed, 582 insertions(+), 53 deletions(-) create mode 100644 tests/run/suspend-strawman-2/foundations.md create mode 100644 tests/run/suspend-strawman-2/streams.scala diff --git a/tests/run/suspend-strawman-2/Async.scala b/tests/run/suspend-strawman-2/Async.scala index 197f4903d641..c7abe5fd6e7c 100644 --- a/tests/run/suspend-strawman-2/Async.scala +++ b/tests/run/suspend-strawman-2/Async.scala @@ -19,7 +19,7 @@ trait Async: object Async: /** The underlying configuration of an async block */ - case class Config(scheduler: ExecutionContext, group: Cancellable.Group) + case class Config(scheduler: ExecutionContext, group: CancellationGroup) trait LowPrioConfig: @@ -27,7 +27,7 @@ object Async: * ignores cancellation requests */ given fromExecutionContext(using scheduler: ExecutionContext): Config = - Config(scheduler, Cancellable.Unlinked) + Config(scheduler, CancellationGroup.Unlinked) end LowPrioConfig @@ -69,14 +69,14 @@ object Async: inline def await[T](src: Source[T])(using async: Async): T = async.await(src) def group[T](body: Async ?=> T)(using async: Async): T = - val newGroup = Cancellable.Group().link() + val newGroup = CancellationGroup().link() body(using async.withConfig(async.config.copy(group = newGroup))) /** A function `T => Boolean` whose lineage is recorded by its implementing * classes. The Listener function accepts values of type `T` and returns * `true` iff the value was consumed by an async block. */ - trait Listener[-T] extends Function[T, Boolean] + trait Listener[-T] extends (T => Boolean) /** A listener for values that are processed by the given source `src` and * that are demanded by the continuation listener `continue`. diff --git a/tests/run/suspend-strawman-2/Cancellable.scala b/tests/run/suspend-strawman-2/Cancellable.scala index 994c182bac54..096c2d923162 100644 --- a/tests/run/suspend-strawman-2/Cancellable.scala +++ b/tests/run/suspend-strawman-2/Cancellable.scala @@ -4,7 +4,7 @@ import scala.collection.mutable /** A trait for cancellable entities that can be grouped */ trait Cancellable: - private var group: Cancellable.Group = Cancellable.Unlinked + private var group: CancellationGroup = CancellationGroup.Unlinked /** Issue a cancel request */ def cancel(): Unit @@ -12,7 +12,7 @@ trait Cancellable: /** Add this cancellable to the given group after removing * it from the previous group in which it was. */ - def link(group: Cancellable.Group): this.type = + def link(group: CancellationGroup): this.type = this.group.drop(this) this.group = group this.group.add(this) @@ -26,37 +26,37 @@ trait Cancellable: /** Unlink this cancellable from its group. */ def unlink(): this.type = - link(Cancellable.Unlinked) + link(CancellationGroup.Unlinked) -object Cancellable: +end Cancellable + +class CancellationGroup extends Cancellable: + private var members: mutable.Set[Cancellable] = mutable.Set() - /** A group of cancellable members */ - class Group extends Cancellable: - private var members: mutable.Set[Cancellable] = mutable.Set() + /** Cancel all members and clear the members set */ + def cancel() = + members.toArray.foreach(_.cancel()) + members.clear() - /** Cancel all members and clear the members set */ - def cancel() = - members.toArray.foreach(_.cancel()) - members.clear() + /** Add given member to the members set */ + def add(member: Cancellable): Unit = synchronized: + members += member - /** Add given member to the members set */ - def add(member: Cancellable): Unit = synchronized: - members += member + /** Remove given member from the members set if it is an element */ + def drop(member: Cancellable): Unit = synchronized: + members -= member - /** Remove given member from the members set if it is an element */ - def drop(member: Cancellable): Unit = synchronized: - members -= member - end Group +object CancellationGroup: /** A sentinal group of cancellables that are in fact not linked * to any real group. `cancel`, `add`, and `drop` do nothing when * called on this group. */ - object Unlinked extends Group: + object Unlinked extends CancellationGroup: override def cancel() = () override def add(member: Cancellable): Unit = () override def drop(member: Cancellable): Unit = () end Unlinked -end Cancellable +end CancellationGroup diff --git a/tests/run/suspend-strawman-2/channels.scala b/tests/run/suspend-strawman-2/channels.scala index cd81e458c220..e4f62783a709 100644 --- a/tests/run/suspend-strawman-2/channels.scala +++ b/tests/run/suspend-strawman-2/channels.scala @@ -5,13 +5,25 @@ import fiberRuntime.suspend import scala.concurrent.ExecutionContext import Async.{Listener, await} +/** A common interface for channels */ +trait Channel[T]: + def read()(using Async): T + def send(x: T)(using Async): Unit + def close(): Unit + +class ChannelClosedException extends Exception + /** An unbounded asynchronous channel. Senders do not wait for matching * readers. */ -class UnboundedChannel[T] extends Async.OriginalSource[T]: +class AsyncChannel[T] extends Async.OriginalSource[T], Channel[T]: private val pending = ListBuffer[T]() private val waiting = mutable.Set[Listener[T]]() + private var isClosed = false + + private def ensureOpen() = + if isClosed then throw ChannelClosedException() private def drainWaiting(x: T): Boolean = waiting.iterator.find(_(x)) match @@ -30,11 +42,13 @@ class UnboundedChannel[T] extends Async.OriginalSource[T]: def read()(using Async): T = synchronized: await(this) - def send(x: T): Unit = synchronized: + def send(x: T)(using Async): Unit = synchronized: + ensureOpen() val sent = pending.isEmpty && drainWaiting(x) if !sent then pending += x def poll(k: Listener[T]): Boolean = synchronized: + ensureOpen() drainPending(k) def addListener(k: Listener[T]): Unit = synchronized: @@ -43,7 +57,10 @@ class UnboundedChannel[T] extends Async.OriginalSource[T]: def dropListener(k: Listener[T]): Unit = synchronized: waiting -= k -end UnboundedChannel + def close() = + isClosed = true + +end AsyncChannel /** An unbuffered, synchronous channel. Senders and readers both block * until a communication between them happens. The channel provides two @@ -52,7 +69,7 @@ end UnboundedChannel * waiting sender the data is transmitted directly. Otherwise we add * the operation to the corresponding pending set. */ -trait SyncChannel[T]: +trait SyncChannel[T] extends Channel[T]: val canRead: Async.Source[T] val canSend: Async.Source[Listener[T]] @@ -67,8 +84,13 @@ object SyncChannel: private val pendingReads = mutable.Set[Listener[T]]() private val pendingSends = mutable.Set[Listener[Listener[T]]]() + private var isClosed = false + + private def ensureOpen() = + if isClosed then throw ChannelClosedException() private def link[T](pending: mutable.Set[T], op: T => Boolean): Boolean = + ensureOpen() // Since sources are filterable, we have to match all pending readers or writers // against the incoming request pending.iterator.find(op) match @@ -95,6 +117,9 @@ object SyncChannel: def dropListener(k: Listener[Listener[T]]): Unit = synchronized: pendingSends -= k + def close() = + isClosed = true + end SyncChannel def TestChannel(using ExecutionContext) = diff --git a/tests/run/suspend-strawman-2/foundations.md b/tests/run/suspend-strawman-2/foundations.md new file mode 100644 index 000000000000..bd2c4da0a9fd --- /dev/null +++ b/tests/run/suspend-strawman-2/foundations.md @@ -0,0 +1,474 @@ +# Towards A New Base Library for Asynchronous Computing + +Martin Odersky +16 Feb 2023 + +## Why a New Library? + +We are seeing increasing adoption of continuations, coroutines, or green threads in modern runtimes. Examples are goroutines in golang, coroutines in C++, or virtual threads in project Loom. Complementary to this, we see a maturing of techniques to implement continuations by code generation. Examples range from more local solutions such as async/await in C#, Python, or Scala to more sweeping implementations such as Kotlin coroutines or dotty-cps-async, and +the stack capture techniques pioneered by Krishnamurti et al. and Brachthäuser. This means that we can realistically expect support for continuations or coroutines in most runtimes in the near future. + +This will lead to a fundamental paradigm shift in reactive programming since we can now assume a lightweight and universal `await` construct that can be called anywhere. Previously, most reactive code was required to be cps-transformed into (something resembling) a monad, so that suspension could be implemented in a library. + +As an example, here is some code using new, direct style futures: +```scala + val sum = Future: + val f1 = Future(c1.read) + val f2 = Future(c2.read) + f1.value + f2.value +``` +We set up two futures that each read from a connection (which might take a while). We return the +sum of the read values in a new future. The `value` method returns the result value of a future once it is available, +or throws an exception if the future returns a `Failure`. + +By contrast, with current, monadic style futures, we'd need a composition with `flatMap` to achieve the same effect: +```scala + val sum = + val f1 = Future(c1.read) + val f2 = Future(c2.read) + for + x <- f1 + y <- f2 + yield x + y +``` +The proposed direct style futures also support structured concurrency with cancellation. If the `sum` +future in the direct style is cancelled, the two nested futures reading the connections are cancelled as well. Or, if one of the nested futures +finishes with an exception, the exception is propagated and the other future is cancelled. + +Lightweight blocking thus gives us a fundamentally new tool to design concurrent systems. Paired with the principles of structured concurrency this allows for direct-style systems that are both very lightweight and very expressive. In the following I describe the outline of such a system. I start with the public APIs and then discuss some internal data structures and implementation details. + +## Disclaimer + +The following is an exploration of what might be possible and desirable. It is backed by a complete implementation, but the implementation is neither thoroughly tested nor optimized in any way. +The current implementation only serves as a prototype to explore general feasibility of the presented concepts. + +## Outline + +The library is built around four core abstractions: + + - **Future** Futures are the primary active elements of the framework. A future starts a computation that delivers a result at some point in the future. The result can be a computed value or a failure value that contains an exception. One can wait for the result of a future. Futures can suspend when waiting for other futures to complete and when reading from channels. + + - **Channel** Channels are the primary passive elements of the framework. + A channel provides a way to send data from producers to consumers (which can both be futures). There are several versions of channels. **Rendevouz channels** block both pending receivers and senders until a communication happens. **Buffered channels** allow a sender to continue immediately, buffering the sent data until it is received. + + - **Async Source** Futures and Channels are both described in terms of a new fundamental abstraction of an _asynchronous source_. Async sources can be polled or awaited by suspending a computation. They can be composed by mapping or filtering their results, or by combining several sources in a race where the first arriving result wins. + + - **Async Context** An async context is a capability that allows a computation to suspend while waiting for the result of an async source. This capability is encapsulated in the `Async` trait. Code that has access to a (usually implicit) parameter of type `Async` is said to be in an async context. The bodies of futures are in such a context, so they can suspend. + +The library supports **structured concurrency** with combinators on futures such as `alt`, which returns the first succeeding future and `zip`, which combines all success results or otherwise returns with the first failing future. These combinators are supported by a cancellation mechanism that discards futures whose outcome is no longer relevant. + +**Cancellation** is scoped and hierarchical. Futures created in the scope of some other future are registered as children of that future. If a parent is cancelled, all its children are cancelled as well. + +## Futures + +The `Future` trait is defined as follows: +```scala +trait Future[+T] extends Async.Source[Try[T]], Cancellable: + def result(using async: Async): Try[T] + def value(using async: Async): T = result.get +``` +Futures represent a computation that is completed concurrently. The computation +yields a result value or a exception encapsulated in a `Try` result. The `value` method produces the future's value if it completed successfully or re-throws the +exception contained in the `Failure` alternative of the `Try` otherwise. + +The `result` method can be defined like this: +```scala + def result(using async: Async): T = async.await(this) +``` +Here, `async` is a capability that allows to suspend in an `await` method. The `Async` trait is defined as follows: +```scala +trait Async: + def await[T](src: Async.Source[T]): T + + def config: Async.Config + def withConfig(config: Async.Config): Async +``` +The most important abstraction here is the `await` method. We will get +to configurations handling with the other two methods later. + +Code with the `Async` capability can _await_ an _asynchronous source_ of type `Async.Source`. This implies that the code will suspend if the +result of the async source is not yet ready. Futures are async sources of type `Try[T]`. + +## Async Sources + +We have seen that futures are a particular kind of an async source. We will see other implementations related to channels later. Async sources are the primary means of communication between asynchronous computations and they can be composed in powerful ways. + +In particular, we have two extension methods on async sources of type `Source[T]`: +```scala + def map[U](f: T => U): Source[U] + def filter(p: T => Boolean): Source[T] +``` +`map` transforms elements of a `Source` whereas `filter` only passes on elements satisfying some condition. + +Furthermore, there is a `race` method that passes on the first of several +sources: +```scala + def race[T](sources: Source[T]*): Source[T] +``` + +These methods are building blocks for higher-level operations. For instance, `Async` also defines an `either` combinator over two sources `src1: Source[T1]` and `src2: Source[T2]` that returns an `Either[T1, T2]` with the result of `src1` if it finishes first and with the result of `src2` otherwise. It is defined as follows: +```scala + def either[T1, T2](src1: Source[T1], src2: Source[T2]): Source[Either[T, U]] = + race(src1.map(Left(_)), src2.map(Right(_))) +``` +We distinguish between _original_ async sources such as futures or channels and +_derived_ sources such as the results of `map`, `filter`, or `race`. + +Async sources need to define three abstract methods in trait `Async.Source[T]`: +```scala + trait Source[+T]: + def poll(k: Listener[T]): Boolean + def onComplete(k: Listener[T]): Unit + def dropListener(k: Listener[T]): Unit +``` +All three methods take a `Listener` argument. A `Listener[T]` is a function from `T` to `Boolean`. +```scala + trait Listener[-T] extends (T => Boolean) +``` +The `T` argument is the value obtained from an async source. The boolean result returns `true` if the argument was read by another async computation. It returns `false` if the argument was dropped by a `filter` or lost in a `race`. +Listeners also come with a _lineage_, which tells us what source combinators were used to build a listener. + +The `poll` method of an async source allows to poll whether data is present. If that's the case, the listener `k` is applied to the data. The result of `poll` is the result of the listener if it was applied and `false` otherwise. There is also a first-order variant of `poll` that returns data in an `Option`. It is defined +as follows: +```scala + def poll(): Option[T] = + var resultOpt: Option[T] = None + poll { x => resultOpt = Some(x); true } + resultOpt +``` +The `onComplete` method of an async source calls the listener `k` once data is present. This could either be immediately, in which case the effect is the same as `poll`, or it could be in the future in which case the listener is installed in waiting lists in the original sources on which it depends so that it can be called when the data is ready. Note that there could be several such original sources, since the listener could have been passed to a `race` source, which itself depends on several other sources. + +The `dropListener` method drops the listener `k` from the waiting lists of all original sources on which it depends. This an optimization that is necessary in practice +to support races efficiently. Once a race is decided, all losing listeners will +never pass data (i.e. they always return `false`), so we do not want them to clutter the waiting lists of their original sources anymore. + +A typical way to implement `onComplete` for original sources is to poll first and install a listener only if no data is present. This behavior is encapsulated in the `OriginalSource` abstraction: +```scala + abstract class OriginalSource[+T] extends Source[T]: + + /** Add `k` to the waiting list of this source */ + protected def addListener(k: Listener[T]): Unit + + def onComplete(k: Listener[T]): Unit = + if !poll(k) then addListener(k) +``` +So original sources are defined in terms if `poll`, `addListener`, and `dropListener`. + +## Creating Futures + +A simple future can be created by calling the `apply` method of the `Future` object. We have seen an example in the introduction: + +```scala + val sum = Future: + val f1 = Future(c1.read) + val f2 = Future(c2.read) + f1.value + f2.value +``` +The `Future.apply` method is has the following signature: +```scala + def apply[T](body: Async ?=> T)(using config: Async.Config): Future[T] +``` +`apply` creates an `Async` capability with the given configuration `config` and passes it to its `body` argument. + +Futures also have a set of useful combinators that support what is usually called _structured concurrency_. In particular, there is the `zip` operator, +which takes two futures and if they complete successfully returns their results in a pair. If one or both of the operand futures fail, the first failure is returned as failure result of the zip. Dually, there is the `alt` operator, which returns the result of the first succeeding future and fails only if both operand futures fail. + +`zip` and `alt` can be implemented as extension methods on futures as follows: + +```scala + extension [T](f1: Future[T]) + + def zip[U](f2: Future[U])(using Async.Config): Future[(T, U)] = Future: + Async.await(Async.either(f1, f2)) match + case Left(Success(x1)) => (x1, f2.value) + case Right(Success(x2)) => (f1.value, x2) + case Left(Failure(ex)) => throw ex + case Right(Failure(ex)) => throw ex + + def alt(f2: Future[T])(using Async.Config): Future[T] = Future: + Async.await(Async.race(f1, f2)).get +``` +The `zip` implementation calls `await` over a source which results from an `either`. We have seen that `either` is in turn implemented by a combination of `map` and `race`. It distinguishes four cases reflecting which of the argument futures finished first, and whether that was with a success or a failure. + +The `alt` implementation is a bit simpler. It is implemented directly in terms +of `await`ing the result of a `race`. + +In some cases an operand future is no longer needed for the result of a `zip` or an `alt`. For `zip` this is the case if one of the operands fails, since then the result is always a failure, and for `alt` this is the case if one of the operands succeeds, since then the result is that success value. + +## Cancellation + +Futures that are no longer needed can be cancelled. `Future` extends the `Cancellable` trait, which is defined as follows: +```scala + trait Cancellable: + def cancel(): Unit + def link(group: CancellationGroup): this.type + ... +``` +A cancel request is transmitted via the `cancel` method. It sets the +`cancelRequest` flag of the future to `true`. The flag is tested +before and after each `await` and can also be tested from user code. +If a test returns `true`, a `CancellationException` is thrown, which +usually terminates the running future. + +## Cancellation Groups + +A cancellable object such as a future belongs to a `CancellationGroup`. +Cancellation groups are themselves cancellable objects. Cancelling +a cancellation group means cancelling all its members. +```scala +class CancellationGroup extends Cancellable: + private var members: mutable.Set[Cancellable] = mutable.Set() + + /** Cancel all members and clear the members set */ + def cancel() = + members.toArray.foreach(_.cancel()) + members.clear() + + /** Add given member to the members set */ + def add(member: Cancellable): Unit = synchronized: + members += member + + /** Remove given member from the members set if it is an element */ + def drop(member: Cancellable): Unit = synchronized: + members -= member +``` +One can include a cancellable object in a cancellation group using +the object's `link` method. An object can belong only to one cancellation group, so linking an already linked cancellable object will unlink it from its previous cancellation group. The `link` method is defined +as follows: +```scala +def link(group: CancellationGroup): this.type = + this.group.drop(this) + this.group = group + this.group.add(this) + this +``` +There are also two variants of `link` in `Cancellable`, defined as follows: + +```scala +trait Cancellable: + ... + def link()(using async: Async): this.type = + link(async.config.group) + def unlink(): this.type = + link(CancellationGroup.Unlinked) +``` +The second variant of `link` links a cancellable object to the group of the current `Async` context. The `unlink` method drops a cancellable object from its group. This is achieved by "linking" the object to the +special `Unlinked` cancellation group, which ignores all cancel requests +as well as all add/drop member requests. +```scala +object CancellationGroup + object Unlinked extends CancellationGroup: + override def cancel() = () + override def add(member: Cancellable): Unit = () + override def drop(member: Cancellable): Unit = () + end Unlinked +``` + +## Structured Concurrency + +As we have seen in the `sum` example, futures can be nested. +```scala + val sum = Future: + val f1 = Future(c1.read) + val f2 = Future(c2.read) + f1.value + f2.value +``` +Our library follows the _structured concurrency_ principle which says that +the lifetime of nested computations is contained within the lifetime of enclosing computations. In the previous example, `f1` and `f2` will be guaranteed to terminate when the `sum` future terminates. This is already implied by the program logic if both futures terminate successfully. But what if `f1` fails with an exception? In that case `f2` will be canceled before the `sum` future is completed. + +The mechanism which achieves this is as follows: When defining a future, +the body of the future is run in the scope of an `Async.group` wrapper, which is defined like this: +```scala + def group[T](body: Async ?=> T)(using async: Async): T = + val g = CancellationGroup().link() + try body(using async.withConfig(async.config.copy(group = g))) + finally g.cancel() +``` +The `group` wrapper sets up a new cancellation group, runs the given `body` in an `Async` context with that group, and finally cancels the group once `body` has finished. + +## Channels + +Channels are a means for futures and related asynchronous computations to synchronize and exchange messages. +There are +two broad categories of channels: _asynchronous_ or _synchronous_.Synchronous channels block the sender of a message until it is received, whereas asynchronous channels don't do this as a general rule (but they might still block a sender by some back-pressure mechanism or if a bounded buffer gets full). + +The general interface of a channel is as follows: +```scala +trait Channel[T]: + def read()(using Async): T + def send(x: T)(using Async): Unit + def close(): Unit +``` +Channels provide + + - a `read` method, which might suspend while waiting for a message to arrive, + - a `send` method, which also might suspend in case this is a sync channel or there is some other mechanism that forces a sender to wait, + - a `close` method which closes the channel. Trying to send to a closed channel or to read from it results in a `ChannelClosedException` to be thrown. + +### Async Channels + +An asynchronous channel implements both the `Async.Source` and `Channel` interfaces. This means inputs from an asychronous channel can be mapped, filtered or combined with other sources in races. + +```scala +class AsyncChannel[T] extends Async.OriginalSource[T], Channel[T] +``` + +### Synchronous Channels + +A sync channel pairs a read request with a send request in a _rendezvous_. Readers and/or senders are blocked until a rendezvous between them is established which causes a message to be sent and received. A sync channel provides two separate async sources +for reading a message and sending one. The `canRead` source +provides messages to readers of the channel. The `canSend` source +provides message listeners to writers that send messages to the channel. +```scala +trait SyncChannel[T] extends Channel[T]: + + val canRead: Async.Source[T] + val canSend: Async.Source[Listener[T]] + + def send(x: T)(using Async): Unit = await(canSend)(x) + def read()(using Async): T = await(canRead) +``` + +## Going Further + +The library is expressive enough so that higher-order abstractions over channels can be built with ease. In the following, I outline some of the possible extensions and explain how they could be defined and implemented. + +### Streams + +A stream represents a sequence of values that are computed one-by-one in a separate concurrent computation. Conceptually, streams are simply nested futures, where each future produces one element: +```scala + type Stream[+T] = Future[StreamResult[T]] + + enum StreamResult[+T]: + case More(elem: T, rest: Stream[T]) + case End extends StreamResult[Nothing] +``` +One can see a stream as a static representation of the values that are transmitted over a channel. Indeed, there is an easy way to convert a channel to a stream: +```scala + extension [T](c: Channel[T]) + def toStream(using Async.Config): Stream[T] = Future: + try StreamResult.More(read(), toStream) + catch case ex: ChannelClosedException => StreamResult.End +``` + +### Coroutines or Fibers + +A coroutine or fiber is simply a `Future[Unit]`. This might seem surprising at first. Why should we return something from a coroutine or fiber? Well, we certainly do want to observe that a coroutine has terminated, and we also need to handle any exceptions that are thrown +from it. A result of type `Try[Unit]` has exactly the information we need for this. We typically want to add some supervisor framework that +waits for coroutines to terminate and handles failures. A possible setup would be to send terminated coroutines to a channel that is serviced by a supervisor future. + +### Actors + +Similarly, we can model an actor by a `Future[Unit]` paired with a channel which serves as the actor's inbox. + +## Implementation Details + +## Internals of Async Contexts + +An async context provides two elements: + + - an `await` method that allows a caller to suspend while waiting for the result of an async source to arrive, + - a `config` value that refers to the configuration used in the async + context. + +A configuration of an async context is defined by the following class: +```scala + case class Config(scheduler: ExecutionContext, group: CancellationGroup) +``` +It contains as members a scheduler and a cancellation group. The scheduler +is an `ExecutionContext` that determines when and how suspended tasks are run. The cancellation group determines the default linkage of all cancellable objects that are created in an async context. + +Async contexts and their configurations are usually passed as implicit parameters. Async configurations can be implicitly generated from async contexts by simply pulling out the `config` value of the context. +They can also be implicitly generated from execution contexts, by +combining an execution context with the special `Unlinked` cancellation group. The generation from an async context has higher priority than the generation from an execution context. This schema is implemented in the companion object of class `Config`: +```scala + trait LowPrioConfig: + given fromExecutionContext(using scheduler: ExecutionContext): Config = + Config(scheduler, CancellationGroup.Unlinked) + + object Config extends LowPrioConfig: + given fromAsync(using async: Async): Config = async.config +``` + +## Implementing Await + +The most interesting part of an async context is its implementation of the `await` method. These implementations need to be based on a lower-level mechanism of suspensions or green threads. + +### Using Delimited Continuations + +We first describe +the implementation if support for full delimited continuations is available. We assume in this case a trait +```scala +trait Suspension[-T, +R]: + def resume(arg: T): R = ??? +``` +and a method +```scala +def suspend[T, R](body: Suspension[T, R] => R)(using Label[R]): T = ??? +``` +A call of `suspend(body)` captures the continuation up to an enclosing boundary in a `Suspension` object and passes it to `body`. The continuation can be resumed by calling the suspension's `resume` method. The enclosing boundary +is the one which created the implicit `Label` argument. + +Using this infrastructure, `await` can be implemented like this: +```scala + def await[T](src: Async.Source[T]): T = + checkCancellation() + src.poll().getOrElse: + try + suspend[T, Unit]: k => + src.onComplete: x => + config.scheduler.schedule: () => + k.resume(x) + true // signals to `src` that result `x` was consumed + finally checkCancellation() +``` +Notes: + + - The main body of `await` is enclosed by two `checkCancellation` calls that abort + the computation with a `CancellationException` in case of a cancel request. + - Await first polls the async source and returns the result if one is present. + - If no result is present, it suspends the computation and adds a listener to the source via its `onComplete` method. The listener is generated via a SAM conversion from the closure following `x =>`. + - If the listener is invoked with a result, it resumes the suspension with that result argument in a newly scheduled task. The listener returns `true` to indicate that the result value was consumed. + +An Async context with this version of `await` is used in the following +implementation of `async`, the wrapper for the body of a future: +```scala +private def async(body: Async ?=> Unit): Unit = + class FutureAsync(using val config: Async.Config) extends Async: + def await[T](src: Async.Source[T]): T = ... + ... + + boundary [Unit]: + body(using FutureAsync()) +``` + +### Using Fibers + +On a runtime that only provides fibers (_aka_ green threads), the implementation of `await` is a bit more complicated, since we cannot suspend awaiting an argument value. We can work around this restriction by re-formulating the body of `await` as follows: +```scala + def await[T](src: Async.Source[T]): T = + checkCancellation() + src.poll().getOrElse: + try + var result: Option[T] = None + src.onComplete: x => + synchronized: + result = Some(x) + notify() + true + synchronized: + while result.isEmpty do wait() + result.get + finally checkCancellation() +``` +Only the body of the `try` is different from the previous implementation. Here we now create a variable holding an optional result value. The computation `wait`s until the result value is defined. The variable becomes is set to a defined value when the listener is invoked, followed by a call to `notify()` to wake up the waiting fiber. + +Since the whole fiber suspends, we don't need a `boundary` anymore to delineate the limit of a continuation, so the `async` can be defined as follows: +```scala +private def async(body: Async ?=> Unit): Unit = + class FutureAsync(using val config: Async.Config) extends Async: + def await[T](src: Async.Source[T]): T = ... + ... + + body(using FutureAsync()) +``` diff --git a/tests/run/suspend-strawman-2/futures.scala b/tests/run/suspend-strawman-2/futures.scala index 1649f41ca178..4e221b990991 100644 --- a/tests/run/suspend-strawman-2/futures.scala +++ b/tests/run/suspend-strawman-2/futures.scala @@ -1,7 +1,7 @@ package concurrent import scala.collection.mutable, mutable.ListBuffer -import fiberRuntime.suspend +import fiberRuntime.util.* import fiberRuntime.boundary import scala.compiletime.uninitialized import scala.util.{Try, Success, Failure} @@ -13,10 +13,13 @@ import scala.concurrent.ExecutionContext */ trait Future[+T] extends Async.OriginalSource[Try[T]], Cancellable: + /** Wait for this future to be completed and return its result */ + def result(using async: Async): Try[T] + /** Wait for this future to be completed, return its value in case of success, * or rethrow exception in case of failure. */ - def value(using async: Async): T + def value(using async: Async): T = result.get /** Eventually stop computation of this future and fail with * a `Cancellation` exception. @@ -56,8 +59,7 @@ object Future: // Future method implementations - def value(using async: Async): T = - async.await(this).get + def result(using async: Async): Try[T] = async.await(this) /** Complete future with result. If future was cancelled in the meantime, * return a CancellationException failure instead. @@ -87,13 +89,13 @@ object Future: private class RunnableFuture[+T](body: Async ?=> T)(using ac: Async.Config) extends CoreFuture[T]: + def checkCancellation() = + if cancelRequest then throw CancellationException() + /** a handler for Async */ private def async(body: Async ?=> Unit): Unit = class FutureAsync(using val config: Async.Config) extends Async: - def checkCancellation() = - if cancelRequest then throw CancellationException() - /** Await a source first by polling it, and, if that fails, by suspending * in a onComplete call. */ @@ -101,14 +103,20 @@ object Future: checkCancellation() src.poll().getOrElse: try - var result: Option[T] = None // Not needed if we have full continuations - suspend[T, Unit]: k => + src.poll().getOrElse: + sleepABit() + log(s"suspending ${threadName.get()}") + var result: Option[T] = None src.onComplete: x => - config.scheduler.execute: () => + synchronized: result = Some(x) - k.resume() - true // signals to `src` that result `x` was consumed - result.get + notify() + true + sleepABit() + synchronized: + log(s"suspended ${threadName.get()}") + while result.isEmpty do wait() + result.get /* With full continuations, the try block can be written more simply as follows: suspend[T, Unit]: k => @@ -121,8 +129,14 @@ object Future: def withConfig(config: Async.Config) = FutureAsync(using config) + sleepABit() + try body(using FutureAsync()) + finally log(s"finished ${threadName.get()} ${Thread.currentThread.getId()}") + /** With continuations, this becomes: + boundary [Unit]: body(using FutureAsync()) + */ end async ac.scheduler.execute: () => @@ -142,30 +156,31 @@ object Future: def apply[T](body: Async ?=> T)(using ac: Async.Config): Future[T] = RunnableFuture(body) + /** A future that immediately terminates with the given result */ + def now[T](result: Try[T]): Future[T] = + val f = CoreFuture[T]() + f.complete(result) + f + extension [T](f1: Future[T]) /** Parallel composition of two futures. * If both futures succeed, succeed with their values in a pair. Otherwise, - * fail with the failure that was returned first and cancel the other. + * fail with the failure that was returned first. */ def zip[U](f2: Future[U])(using Async.Config): Future[(T, U)] = Future: Async.await(Async.either(f1, f2)) match case Left(Success(x1)) => (x1, f2.value) case Right(Success(x2)) => (f1.value, x2) - case Left(Failure(ex)) => f2.cancel(); throw ex - case Right(Failure(ex)) => f1.cancel(); throw ex + case Left(Failure(ex)) => throw ex + case Right(Failure(ex)) => throw ex /** Alternative parallel composition of this task with `other` task. - * If either task succeeds, succeed with the success that was returned first - * and cancel the other. Otherwise, fail with the failure that was returned last. + * If either task succeeds, succeed with the success that was returned first. + * Otherwise, fail with the failure that was returned last. */ - def alt(f2: Future[T], name: String = "alt")(using Async.Config): Future[T] = Future: - boundary.setName(name) - Async.await(Async.either(f1, f2)) match - case Left(Success(x1)) => f2.cancel(); x1 - case Right(Success(x2)) => f1.cancel(); x2 - case Left(_: Failure[?]) => f2.value - case Right(_: Failure[?]) => f1.value + def alt(f2: Future[T])(using Async.Config): Future[T] = Future: + Async.await(Async.race(f1, f2)).get end extension diff --git a/tests/run/suspend-strawman-2/streams.scala b/tests/run/suspend-strawman-2/streams.scala new file mode 100644 index 000000000000..94a5ed309486 --- /dev/null +++ b/tests/run/suspend-strawman-2/streams.scala @@ -0,0 +1,15 @@ +package concurrent +import scala.util.{Try, Success, Failure} + +type Stream[+T] = Future[StreamResult[T]] + +enum StreamResult[+T]: + case More(elem: T, rest: Stream[T]) + case End extends StreamResult[Nothing] + +import StreamResult.* + +extension [T](c: Channel[T]) + def toStream(using Async.Config): Stream[T] = Future: + try StreamResult.More(c.read(), toStream) + catch case ex: ChannelClosedException => StreamResult.End From 639bb718dc706e780b27e147857677d45ddf31e5 Mon Sep 17 00:00:00 2001 From: odersky Date: Mon, 27 Feb 2023 10:12:01 +0100 Subject: [PATCH 47/52] Fix alt --- tests/run/suspend-strawman-2/foundations.md | 9 ++++++--- tests/run/suspend-strawman-2/futures.scala | 6 +++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/run/suspend-strawman-2/foundations.md b/tests/run/suspend-strawman-2/foundations.md index bd2c4da0a9fd..47c25ab49a13 100644 --- a/tests/run/suspend-strawman-2/foundations.md +++ b/tests/run/suspend-strawman-2/foundations.md @@ -186,12 +186,15 @@ which takes two futures and if they complete successfully returns their results case Right(Failure(ex)) => throw ex def alt(f2: Future[T])(using Async.Config): Future[T] = Future: - Async.await(Async.race(f1, f2)).get + Async.await(Async.either(f1, f2)) match + case Left(Success(x1)) => x1 + case Right(Success(x2)) => x2 + case Left(_: Failure[?]) => f2.value + case Right(_: Failure[?]) => f1.value ``` The `zip` implementation calls `await` over a source which results from an `either`. We have seen that `either` is in turn implemented by a combination of `map` and `race`. It distinguishes four cases reflecting which of the argument futures finished first, and whether that was with a success or a failure. -The `alt` implementation is a bit simpler. It is implemented directly in terms -of `await`ing the result of a `race`. +The `alt` implementation starts in the same way, calling `await` over `either`. If the first result was a success, it returns it. If not, it waits for the second result. In some cases an operand future is no longer needed for the result of a `zip` or an `alt`. For `zip` this is the case if one of the operands fails, since then the result is always a failure, and for `alt` this is the case if one of the operands succeeds, since then the result is that success value. diff --git a/tests/run/suspend-strawman-2/futures.scala b/tests/run/suspend-strawman-2/futures.scala index 4e221b990991..f6dca15a9292 100644 --- a/tests/run/suspend-strawman-2/futures.scala +++ b/tests/run/suspend-strawman-2/futures.scala @@ -180,7 +180,11 @@ object Future: * Otherwise, fail with the failure that was returned last. */ def alt(f2: Future[T])(using Async.Config): Future[T] = Future: - Async.await(Async.race(f1, f2)).get + Async.await(Async.either(f1, f2)) match + case Left(Success(x1)) => x1 + case Right(Success(x2)) => x2 + case Left(_: Failure[?]) => f2.value + case Right(_: Failure[?]) => f1.value end extension From 3091402e7a64d5fa711511411b60aa49ad147b05 Mon Sep 17 00:00:00 2001 From: odersky Date: Wed, 1 Mar 2023 11:32:25 +0100 Subject: [PATCH 48/52] Describe Tasks and Promises --- tests/run/suspend-strawman-2/channels.scala | 12 ++++ tests/run/suspend-strawman-2/foundations.md | 63 +++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/tests/run/suspend-strawman-2/channels.scala b/tests/run/suspend-strawman-2/channels.scala index e4f62783a709..cbb1588baa2a 100644 --- a/tests/run/suspend-strawman-2/channels.scala +++ b/tests/run/suspend-strawman-2/channels.scala @@ -135,6 +135,18 @@ def TestChannel(using ExecutionContext) = case Some(x) => sum += x; loop() case None => println(sum) loop() + val chan = SyncChannel[Int]() + val allTasks = List( + Task: + println("task1") + chan.read(), + Task: + println("task2") + chan.read() + ) + + def start() = Future: + allTasks.map(_.run.value).sum def TestRace = val c1, c2 = SyncChannel[Int]() diff --git a/tests/run/suspend-strawman-2/foundations.md b/tests/run/suspend-strawman-2/foundations.md index 47c25ab49a13..cec046b47cd6 100644 --- a/tests/run/suspend-strawman-2/foundations.md +++ b/tests/run/suspend-strawman-2/foundations.md @@ -332,6 +332,69 @@ trait SyncChannel[T] extends Channel[T]: def read()(using Async): T = await(canRead) ``` +## Tasks + +One criticism leveled against futures is that they "lack referential transparency". What this means is that a future starts running when it is defined, so passing a reference to a future is not the same as passing the referenced expression itself. Example: +```scala +val x = Future { println("started") } +f(x) +``` +is not the same as +```scala +val x = Future { println("started") } +f(Future { println("started") }) +``` +In the first case the program prints "started" once whereas in the second case it prints "started" twice. In a sense that's exactly what's intended. After all, the whole point of futures is to get parallelism. So a future should start well before its result is requested and the simplest way to achieve that is to start the future when it is defined. _Aside_: I believe the criticism of the existing `scala.concurrent.Future` design in Scala 2.13 is understandable, since +these futures are usually composed monad-style using for expressions, which informally suggests referential transparency. Direct-style futures like the ones presented here don't have that problem. + +On the other hand, the early start of futures _does_ makes it harder to assemble parallel computations as first class values in data structures and to launch them according to user-defined execution rules. Of course one can still achieve all that by working with +functions producing futures instead of futures directly. A function of type `() => Future[T]` will start executing its embedded future only once it is called. + +Tasks make the definition of such delayed futures a bit easier. The `Task` class is defined +as follows: +```scala +class Task[+T](val body: Async ?=> T): + def run(using Async.Config) = Future(body) +``` +A `Task` takes the body of a future as an argument. Its `run` method converts that body to a `Future`, which means starting its execution. + +Example: +```scala + val chan: Channel[Int] + val allTasks = List( + Task: + println("task1") + chan.read(), + Task: + println("task2") + chan.read() + ) + + def start() = Future: + allTasks.map(_.run.value).sum +``` + +Tasks have two advantages over simple lambdas when it comes to delaying futures: + + - The intent is made clear: This is a computation intended to be executed concurrently in a future. + - The `Async` context is implicitly provided, since `Task.apply` takes a context function over `Async` as argument. + +## Promises + +Sometimes we want to define future's value externally instead of executing a specific body of code. This can be done using a promise. +The design and implementation of promises is simply this: +```scala +class Promise[T]: + private val myFuture = CoreFuture[T]() + + val future: Future[T] = myFuture + + def complete(result: Try[T]): Unit = + myFuture.complete(result) +``` +A promise provides a `future` and a way to define the result of that +future in its `complete` method. + ## Going Further The library is expressive enough so that higher-order abstractions over channels can be built with ease. In the following, I outline some of the possible extensions and explain how they could be defined and implemented. From 7c6c41bab3ea2efcf5c58ba05fa6844a9e105f21 Mon Sep 17 00:00:00 2001 From: odersky Date: Fri, 3 Mar 2023 14:16:17 +0100 Subject: [PATCH 49/52] Fix missing finally clause in Async.group Was in the docs but not in the code. --- .../src/dotty/tools/dotc/parsing/Parsers.scala | 16 ++++++++-------- tests/run/suspend-strawman-2/Async.scala | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 479ae1fa9095..661bc502dbad 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -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( @@ -3179,7 +3179,7 @@ object Parsers { * UsingClsTermParamClause::= ‘(’ ‘using’ [‘erased’] (ClsParams | ContextTypes) ‘)’ * ClsParams ::= ClsParam {‘,’ ClsParam} * ClsParam ::= {Annotation} - * + * * TypelessClause ::= DefTermParamClause * | UsingParamClause * @@ -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] */ @@ -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 @@ -3613,7 +3613,7 @@ object Parsers { joinParams(tparams, vparamss) var tpt = fromWithinReturnType { typedOpt() } - + if (migrateTo3) newLineOptWhenFollowedBy(LBRACE) val rhs = if in.token == EQUALS then diff --git a/tests/run/suspend-strawman-2/Async.scala b/tests/run/suspend-strawman-2/Async.scala index c7abe5fd6e7c..b68c4ad62e17 100644 --- a/tests/run/suspend-strawman-2/Async.scala +++ b/tests/run/suspend-strawman-2/Async.scala @@ -70,7 +70,8 @@ object Async: def group[T](body: Async ?=> T)(using async: Async): T = val newGroup = CancellationGroup().link() - body(using async.withConfig(async.config.copy(group = newGroup))) + try body(using async.withConfig(async.config.copy(group = newGroup))) + finally newGroup.cancel() /** A function `T => Boolean` whose lineage is recorded by its implementing * classes. The Listener function accepts values of type `T` and returns From f49eaca4b5c8a0e0f1722bdafc67c2c94dc73b62 Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 4 Mar 2023 19:05:36 +0100 Subject: [PATCH 50/52] Change toStream method - Remove Channel.close since there is no good general implementation. - Make toStream work on Channel[Try[T]] instead. --- tests/run/suspend-strawman-2/channels.scala | 15 ++++++++++++--- tests/run/suspend-strawman-2/foundations.md | 15 +++++++++------ tests/run/suspend-strawman-2/streams.scala | 9 ++++++--- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/tests/run/suspend-strawman-2/channels.scala b/tests/run/suspend-strawman-2/channels.scala index cbb1588baa2a..71c9bd5e3ce4 100644 --- a/tests/run/suspend-strawman-2/channels.scala +++ b/tests/run/suspend-strawman-2/channels.scala @@ -3,13 +3,20 @@ import scala.collection.mutable, mutable.ListBuffer import fiberRuntime.boundary, boundary.Label import fiberRuntime.suspend import scala.concurrent.ExecutionContext +import scala.util.{Try, Failure} import Async.{Listener, await} /** A common interface for channels */ trait Channel[T]: def read()(using Async): T def send(x: T)(using Async): Unit - def close(): Unit + protected def shutDown(finalValue: T): Unit + +object Channel: + + extension [T](c: Channel[Try[T]]) + def close(): Unit = + c.shutDown(Failure(ChannelClosedException())) class ChannelClosedException extends Exception @@ -57,8 +64,9 @@ class AsyncChannel[T] extends Async.OriginalSource[T], Channel[T]: def dropListener(k: Listener[T]): Unit = synchronized: waiting -= k - def close() = + protected def shutDown(finalValue: T) = isClosed = true + waiting.foreach(_(finalValue)) end AsyncChannel @@ -117,8 +125,9 @@ object SyncChannel: def dropListener(k: Listener[Listener[T]]): Unit = synchronized: pendingSends -= k - def close() = + protected def shutDown(finalValue: T) = isClosed = true + pendingReads.foreach(_(finalValue)) end SyncChannel diff --git a/tests/run/suspend-strawman-2/foundations.md b/tests/run/suspend-strawman-2/foundations.md index cec046b47cd6..1a5e8502694d 100644 --- a/tests/run/suspend-strawman-2/foundations.md +++ b/tests/run/suspend-strawman-2/foundations.md @@ -300,13 +300,11 @@ The general interface of a channel is as follows: trait Channel[T]: def read()(using Async): T def send(x: T)(using Async): Unit - def close(): Unit ``` Channels provide - a `read` method, which might suspend while waiting for a message to arrive, - a `send` method, which also might suspend in case this is a sync channel or there is some other mechanism that forces a sender to wait, - - a `close` method which closes the channel. Trying to send to a closed channel or to read from it results in a `ChannelClosedException` to be thrown. ### Async Channels @@ -409,12 +407,17 @@ A stream represents a sequence of values that are computed one-by-one in a separ case More(elem: T, rest: Stream[T]) case End extends StreamResult[Nothing] ``` -One can see a stream as a static representation of the values that are transmitted over a channel. Indeed, there is an easy way to convert a channel to a stream: +One can see a stream as a static representation of the values that are transmitted over a channel. This poses the question of termination -- when do we know that a channel receives no further values, so the stream can be terminated +with an `StreamResult.End` value. The following implementation shows one +possibility: Here we map a channel of `Try` results to a stream, mapping +failures with a special =`ChannelClosedException` to `StreamResult.End`. ```scala - extension [T](c: Channel[T]) + extension [T](c: Channel[Try[T]]) def toStream(using Async.Config): Stream[T] = Future: - try StreamResult.More(read(), toStream) - catch case ex: ChannelClosedException => StreamResult.End + c.read() match + case Success(x) => StreamResult.More(x, toStream) + case Failure(ex: ChannelClosedException) => StreamResult.End + case Failure(ex) => throw ex ``` ### Coroutines or Fibers diff --git a/tests/run/suspend-strawman-2/streams.scala b/tests/run/suspend-strawman-2/streams.scala index 94a5ed309486..f6bc61c73ef1 100644 --- a/tests/run/suspend-strawman-2/streams.scala +++ b/tests/run/suspend-strawman-2/streams.scala @@ -9,7 +9,10 @@ enum StreamResult[+T]: import StreamResult.* -extension [T](c: Channel[T]) +extension [T](c: Channel[Try[T]]) def toStream(using Async.Config): Stream[T] = Future: - try StreamResult.More(c.read(), toStream) - catch case ex: ChannelClosedException => StreamResult.End + c.read() match + case Success(x) => StreamResult.More(x, toStream) + case Failure(ex: ChannelClosedException) => StreamResult.End + case Failure(ex) => throw ex + From 86c09639ba5b07fbd2af59a8b46bd835b654eeda Mon Sep 17 00:00:00 2001 From: odersky Date: Sat, 4 Mar 2023 21:10:26 +0100 Subject: [PATCH 51/52] fix race condition in cancel --- tests/run/suspend-strawman-2/Cancellable.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/run/suspend-strawman-2/Cancellable.scala b/tests/run/suspend-strawman-2/Cancellable.scala index 096c2d923162..91e86abe89ed 100644 --- a/tests/run/suspend-strawman-2/Cancellable.scala +++ b/tests/run/suspend-strawman-2/Cancellable.scala @@ -35,7 +35,7 @@ class CancellationGroup extends Cancellable: /** Cancel all members and clear the members set */ def cancel() = - members.toArray.foreach(_.cancel()) + synchronized(members.toArray).foreach(_.cancel()) members.clear() /** Add given member to the members set */ From d15fe460b875b55c1a28d9e8497a9f1a472e6fe3 Mon Sep 17 00:00:00 2001 From: odersky Date: Sun, 5 Mar 2023 12:58:38 +0100 Subject: [PATCH 52/52] Drop foundations.md The document for describing this has moved to the README.md in the lampepfl/async project. I am leaving the code as tests to have a version that works with normal threads. lampepfl/async requires Loom's virtual threads --- tests/run/suspend-strawman-2/foundations.md | 543 -------------------- 1 file changed, 543 deletions(-) delete mode 100644 tests/run/suspend-strawman-2/foundations.md diff --git a/tests/run/suspend-strawman-2/foundations.md b/tests/run/suspend-strawman-2/foundations.md deleted file mode 100644 index 1a5e8502694d..000000000000 --- a/tests/run/suspend-strawman-2/foundations.md +++ /dev/null @@ -1,543 +0,0 @@ -# Towards A New Base Library for Asynchronous Computing - -Martin Odersky -16 Feb 2023 - -## Why a New Library? - -We are seeing increasing adoption of continuations, coroutines, or green threads in modern runtimes. Examples are goroutines in golang, coroutines in C++, or virtual threads in project Loom. Complementary to this, we see a maturing of techniques to implement continuations by code generation. Examples range from more local solutions such as async/await in C#, Python, or Scala to more sweeping implementations such as Kotlin coroutines or dotty-cps-async, and -the stack capture techniques pioneered by Krishnamurti et al. and Brachthäuser. This means that we can realistically expect support for continuations or coroutines in most runtimes in the near future. - -This will lead to a fundamental paradigm shift in reactive programming since we can now assume a lightweight and universal `await` construct that can be called anywhere. Previously, most reactive code was required to be cps-transformed into (something resembling) a monad, so that suspension could be implemented in a library. - -As an example, here is some code using new, direct style futures: -```scala - val sum = Future: - val f1 = Future(c1.read) - val f2 = Future(c2.read) - f1.value + f2.value -``` -We set up two futures that each read from a connection (which might take a while). We return the -sum of the read values in a new future. The `value` method returns the result value of a future once it is available, -or throws an exception if the future returns a `Failure`. - -By contrast, with current, monadic style futures, we'd need a composition with `flatMap` to achieve the same effect: -```scala - val sum = - val f1 = Future(c1.read) - val f2 = Future(c2.read) - for - x <- f1 - y <- f2 - yield x + y -``` -The proposed direct style futures also support structured concurrency with cancellation. If the `sum` -future in the direct style is cancelled, the two nested futures reading the connections are cancelled as well. Or, if one of the nested futures -finishes with an exception, the exception is propagated and the other future is cancelled. - -Lightweight blocking thus gives us a fundamentally new tool to design concurrent systems. Paired with the principles of structured concurrency this allows for direct-style systems that are both very lightweight and very expressive. In the following I describe the outline of such a system. I start with the public APIs and then discuss some internal data structures and implementation details. - -## Disclaimer - -The following is an exploration of what might be possible and desirable. It is backed by a complete implementation, but the implementation is neither thoroughly tested nor optimized in any way. -The current implementation only serves as a prototype to explore general feasibility of the presented concepts. - -## Outline - -The library is built around four core abstractions: - - - **Future** Futures are the primary active elements of the framework. A future starts a computation that delivers a result at some point in the future. The result can be a computed value or a failure value that contains an exception. One can wait for the result of a future. Futures can suspend when waiting for other futures to complete and when reading from channels. - - - **Channel** Channels are the primary passive elements of the framework. - A channel provides a way to send data from producers to consumers (which can both be futures). There are several versions of channels. **Rendevouz channels** block both pending receivers and senders until a communication happens. **Buffered channels** allow a sender to continue immediately, buffering the sent data until it is received. - - - **Async Source** Futures and Channels are both described in terms of a new fundamental abstraction of an _asynchronous source_. Async sources can be polled or awaited by suspending a computation. They can be composed by mapping or filtering their results, or by combining several sources in a race where the first arriving result wins. - - - **Async Context** An async context is a capability that allows a computation to suspend while waiting for the result of an async source. This capability is encapsulated in the `Async` trait. Code that has access to a (usually implicit) parameter of type `Async` is said to be in an async context. The bodies of futures are in such a context, so they can suspend. - -The library supports **structured concurrency** with combinators on futures such as `alt`, which returns the first succeeding future and `zip`, which combines all success results or otherwise returns with the first failing future. These combinators are supported by a cancellation mechanism that discards futures whose outcome is no longer relevant. - -**Cancellation** is scoped and hierarchical. Futures created in the scope of some other future are registered as children of that future. If a parent is cancelled, all its children are cancelled as well. - -## Futures - -The `Future` trait is defined as follows: -```scala -trait Future[+T] extends Async.Source[Try[T]], Cancellable: - def result(using async: Async): Try[T] - def value(using async: Async): T = result.get -``` -Futures represent a computation that is completed concurrently. The computation -yields a result value or a exception encapsulated in a `Try` result. The `value` method produces the future's value if it completed successfully or re-throws the -exception contained in the `Failure` alternative of the `Try` otherwise. - -The `result` method can be defined like this: -```scala - def result(using async: Async): T = async.await(this) -``` -Here, `async` is a capability that allows to suspend in an `await` method. The `Async` trait is defined as follows: -```scala -trait Async: - def await[T](src: Async.Source[T]): T - - def config: Async.Config - def withConfig(config: Async.Config): Async -``` -The most important abstraction here is the `await` method. We will get -to configurations handling with the other two methods later. - -Code with the `Async` capability can _await_ an _asynchronous source_ of type `Async.Source`. This implies that the code will suspend if the -result of the async source is not yet ready. Futures are async sources of type `Try[T]`. - -## Async Sources - -We have seen that futures are a particular kind of an async source. We will see other implementations related to channels later. Async sources are the primary means of communication between asynchronous computations and they can be composed in powerful ways. - -In particular, we have two extension methods on async sources of type `Source[T]`: -```scala - def map[U](f: T => U): Source[U] - def filter(p: T => Boolean): Source[T] -``` -`map` transforms elements of a `Source` whereas `filter` only passes on elements satisfying some condition. - -Furthermore, there is a `race` method that passes on the first of several -sources: -```scala - def race[T](sources: Source[T]*): Source[T] -``` - -These methods are building blocks for higher-level operations. For instance, `Async` also defines an `either` combinator over two sources `src1: Source[T1]` and `src2: Source[T2]` that returns an `Either[T1, T2]` with the result of `src1` if it finishes first and with the result of `src2` otherwise. It is defined as follows: -```scala - def either[T1, T2](src1: Source[T1], src2: Source[T2]): Source[Either[T, U]] = - race(src1.map(Left(_)), src2.map(Right(_))) -``` -We distinguish between _original_ async sources such as futures or channels and -_derived_ sources such as the results of `map`, `filter`, or `race`. - -Async sources need to define three abstract methods in trait `Async.Source[T]`: -```scala - trait Source[+T]: - def poll(k: Listener[T]): Boolean - def onComplete(k: Listener[T]): Unit - def dropListener(k: Listener[T]): Unit -``` -All three methods take a `Listener` argument. A `Listener[T]` is a function from `T` to `Boolean`. -```scala - trait Listener[-T] extends (T => Boolean) -``` -The `T` argument is the value obtained from an async source. The boolean result returns `true` if the argument was read by another async computation. It returns `false` if the argument was dropped by a `filter` or lost in a `race`. -Listeners also come with a _lineage_, which tells us what source combinators were used to build a listener. - -The `poll` method of an async source allows to poll whether data is present. If that's the case, the listener `k` is applied to the data. The result of `poll` is the result of the listener if it was applied and `false` otherwise. There is also a first-order variant of `poll` that returns data in an `Option`. It is defined -as follows: -```scala - def poll(): Option[T] = - var resultOpt: Option[T] = None - poll { x => resultOpt = Some(x); true } - resultOpt -``` -The `onComplete` method of an async source calls the listener `k` once data is present. This could either be immediately, in which case the effect is the same as `poll`, or it could be in the future in which case the listener is installed in waiting lists in the original sources on which it depends so that it can be called when the data is ready. Note that there could be several such original sources, since the listener could have been passed to a `race` source, which itself depends on several other sources. - -The `dropListener` method drops the listener `k` from the waiting lists of all original sources on which it depends. This an optimization that is necessary in practice -to support races efficiently. Once a race is decided, all losing listeners will -never pass data (i.e. they always return `false`), so we do not want them to clutter the waiting lists of their original sources anymore. - -A typical way to implement `onComplete` for original sources is to poll first and install a listener only if no data is present. This behavior is encapsulated in the `OriginalSource` abstraction: -```scala - abstract class OriginalSource[+T] extends Source[T]: - - /** Add `k` to the waiting list of this source */ - protected def addListener(k: Listener[T]): Unit - - def onComplete(k: Listener[T]): Unit = - if !poll(k) then addListener(k) -``` -So original sources are defined in terms if `poll`, `addListener`, and `dropListener`. - -## Creating Futures - -A simple future can be created by calling the `apply` method of the `Future` object. We have seen an example in the introduction: - -```scala - val sum = Future: - val f1 = Future(c1.read) - val f2 = Future(c2.read) - f1.value + f2.value -``` -The `Future.apply` method is has the following signature: -```scala - def apply[T](body: Async ?=> T)(using config: Async.Config): Future[T] -``` -`apply` creates an `Async` capability with the given configuration `config` and passes it to its `body` argument. - -Futures also have a set of useful combinators that support what is usually called _structured concurrency_. In particular, there is the `zip` operator, -which takes two futures and if they complete successfully returns their results in a pair. If one or both of the operand futures fail, the first failure is returned as failure result of the zip. Dually, there is the `alt` operator, which returns the result of the first succeeding future and fails only if both operand futures fail. - -`zip` and `alt` can be implemented as extension methods on futures as follows: - -```scala - extension [T](f1: Future[T]) - - def zip[U](f2: Future[U])(using Async.Config): Future[(T, U)] = Future: - Async.await(Async.either(f1, f2)) match - case Left(Success(x1)) => (x1, f2.value) - case Right(Success(x2)) => (f1.value, x2) - case Left(Failure(ex)) => throw ex - case Right(Failure(ex)) => throw ex - - def alt(f2: Future[T])(using Async.Config): Future[T] = Future: - Async.await(Async.either(f1, f2)) match - case Left(Success(x1)) => x1 - case Right(Success(x2)) => x2 - case Left(_: Failure[?]) => f2.value - case Right(_: Failure[?]) => f1.value -``` -The `zip` implementation calls `await` over a source which results from an `either`. We have seen that `either` is in turn implemented by a combination of `map` and `race`. It distinguishes four cases reflecting which of the argument futures finished first, and whether that was with a success or a failure. - -The `alt` implementation starts in the same way, calling `await` over `either`. If the first result was a success, it returns it. If not, it waits for the second result. - -In some cases an operand future is no longer needed for the result of a `zip` or an `alt`. For `zip` this is the case if one of the operands fails, since then the result is always a failure, and for `alt` this is the case if one of the operands succeeds, since then the result is that success value. - -## Cancellation - -Futures that are no longer needed can be cancelled. `Future` extends the `Cancellable` trait, which is defined as follows: -```scala - trait Cancellable: - def cancel(): Unit - def link(group: CancellationGroup): this.type - ... -``` -A cancel request is transmitted via the `cancel` method. It sets the -`cancelRequest` flag of the future to `true`. The flag is tested -before and after each `await` and can also be tested from user code. -If a test returns `true`, a `CancellationException` is thrown, which -usually terminates the running future. - -## Cancellation Groups - -A cancellable object such as a future belongs to a `CancellationGroup`. -Cancellation groups are themselves cancellable objects. Cancelling -a cancellation group means cancelling all its members. -```scala -class CancellationGroup extends Cancellable: - private var members: mutable.Set[Cancellable] = mutable.Set() - - /** Cancel all members and clear the members set */ - def cancel() = - members.toArray.foreach(_.cancel()) - members.clear() - - /** Add given member to the members set */ - def add(member: Cancellable): Unit = synchronized: - members += member - - /** Remove given member from the members set if it is an element */ - def drop(member: Cancellable): Unit = synchronized: - members -= member -``` -One can include a cancellable object in a cancellation group using -the object's `link` method. An object can belong only to one cancellation group, so linking an already linked cancellable object will unlink it from its previous cancellation group. The `link` method is defined -as follows: -```scala -def link(group: CancellationGroup): this.type = - this.group.drop(this) - this.group = group - this.group.add(this) - this -``` -There are also two variants of `link` in `Cancellable`, defined as follows: - -```scala -trait Cancellable: - ... - def link()(using async: Async): this.type = - link(async.config.group) - def unlink(): this.type = - link(CancellationGroup.Unlinked) -``` -The second variant of `link` links a cancellable object to the group of the current `Async` context. The `unlink` method drops a cancellable object from its group. This is achieved by "linking" the object to the -special `Unlinked` cancellation group, which ignores all cancel requests -as well as all add/drop member requests. -```scala -object CancellationGroup - object Unlinked extends CancellationGroup: - override def cancel() = () - override def add(member: Cancellable): Unit = () - override def drop(member: Cancellable): Unit = () - end Unlinked -``` - -## Structured Concurrency - -As we have seen in the `sum` example, futures can be nested. -```scala - val sum = Future: - val f1 = Future(c1.read) - val f2 = Future(c2.read) - f1.value + f2.value -``` -Our library follows the _structured concurrency_ principle which says that -the lifetime of nested computations is contained within the lifetime of enclosing computations. In the previous example, `f1` and `f2` will be guaranteed to terminate when the `sum` future terminates. This is already implied by the program logic if both futures terminate successfully. But what if `f1` fails with an exception? In that case `f2` will be canceled before the `sum` future is completed. - -The mechanism which achieves this is as follows: When defining a future, -the body of the future is run in the scope of an `Async.group` wrapper, which is defined like this: -```scala - def group[T](body: Async ?=> T)(using async: Async): T = - val g = CancellationGroup().link() - try body(using async.withConfig(async.config.copy(group = g))) - finally g.cancel() -``` -The `group` wrapper sets up a new cancellation group, runs the given `body` in an `Async` context with that group, and finally cancels the group once `body` has finished. - -## Channels - -Channels are a means for futures and related asynchronous computations to synchronize and exchange messages. -There are -two broad categories of channels: _asynchronous_ or _synchronous_.Synchronous channels block the sender of a message until it is received, whereas asynchronous channels don't do this as a general rule (but they might still block a sender by some back-pressure mechanism or if a bounded buffer gets full). - -The general interface of a channel is as follows: -```scala -trait Channel[T]: - def read()(using Async): T - def send(x: T)(using Async): Unit -``` -Channels provide - - - a `read` method, which might suspend while waiting for a message to arrive, - - a `send` method, which also might suspend in case this is a sync channel or there is some other mechanism that forces a sender to wait, - -### Async Channels - -An asynchronous channel implements both the `Async.Source` and `Channel` interfaces. This means inputs from an asychronous channel can be mapped, filtered or combined with other sources in races. - -```scala -class AsyncChannel[T] extends Async.OriginalSource[T], Channel[T] -``` - -### Synchronous Channels - -A sync channel pairs a read request with a send request in a _rendezvous_. Readers and/or senders are blocked until a rendezvous between them is established which causes a message to be sent and received. A sync channel provides two separate async sources -for reading a message and sending one. The `canRead` source -provides messages to readers of the channel. The `canSend` source -provides message listeners to writers that send messages to the channel. -```scala -trait SyncChannel[T] extends Channel[T]: - - val canRead: Async.Source[T] - val canSend: Async.Source[Listener[T]] - - def send(x: T)(using Async): Unit = await(canSend)(x) - def read()(using Async): T = await(canRead) -``` - -## Tasks - -One criticism leveled against futures is that they "lack referential transparency". What this means is that a future starts running when it is defined, so passing a reference to a future is not the same as passing the referenced expression itself. Example: -```scala -val x = Future { println("started") } -f(x) -``` -is not the same as -```scala -val x = Future { println("started") } -f(Future { println("started") }) -``` -In the first case the program prints "started" once whereas in the second case it prints "started" twice. In a sense that's exactly what's intended. After all, the whole point of futures is to get parallelism. So a future should start well before its result is requested and the simplest way to achieve that is to start the future when it is defined. _Aside_: I believe the criticism of the existing `scala.concurrent.Future` design in Scala 2.13 is understandable, since -these futures are usually composed monad-style using for expressions, which informally suggests referential transparency. Direct-style futures like the ones presented here don't have that problem. - -On the other hand, the early start of futures _does_ makes it harder to assemble parallel computations as first class values in data structures and to launch them according to user-defined execution rules. Of course one can still achieve all that by working with -functions producing futures instead of futures directly. A function of type `() => Future[T]` will start executing its embedded future only once it is called. - -Tasks make the definition of such delayed futures a bit easier. The `Task` class is defined -as follows: -```scala -class Task[+T](val body: Async ?=> T): - def run(using Async.Config) = Future(body) -``` -A `Task` takes the body of a future as an argument. Its `run` method converts that body to a `Future`, which means starting its execution. - -Example: -```scala - val chan: Channel[Int] - val allTasks = List( - Task: - println("task1") - chan.read(), - Task: - println("task2") - chan.read() - ) - - def start() = Future: - allTasks.map(_.run.value).sum -``` - -Tasks have two advantages over simple lambdas when it comes to delaying futures: - - - The intent is made clear: This is a computation intended to be executed concurrently in a future. - - The `Async` context is implicitly provided, since `Task.apply` takes a context function over `Async` as argument. - -## Promises - -Sometimes we want to define future's value externally instead of executing a specific body of code. This can be done using a promise. -The design and implementation of promises is simply this: -```scala -class Promise[T]: - private val myFuture = CoreFuture[T]() - - val future: Future[T] = myFuture - - def complete(result: Try[T]): Unit = - myFuture.complete(result) -``` -A promise provides a `future` and a way to define the result of that -future in its `complete` method. - -## Going Further - -The library is expressive enough so that higher-order abstractions over channels can be built with ease. In the following, I outline some of the possible extensions and explain how they could be defined and implemented. - -### Streams - -A stream represents a sequence of values that are computed one-by-one in a separate concurrent computation. Conceptually, streams are simply nested futures, where each future produces one element: -```scala - type Stream[+T] = Future[StreamResult[T]] - - enum StreamResult[+T]: - case More(elem: T, rest: Stream[T]) - case End extends StreamResult[Nothing] -``` -One can see a stream as a static representation of the values that are transmitted over a channel. This poses the question of termination -- when do we know that a channel receives no further values, so the stream can be terminated -with an `StreamResult.End` value. The following implementation shows one -possibility: Here we map a channel of `Try` results to a stream, mapping -failures with a special =`ChannelClosedException` to `StreamResult.End`. -```scala - extension [T](c: Channel[Try[T]]) - def toStream(using Async.Config): Stream[T] = Future: - c.read() match - case Success(x) => StreamResult.More(x, toStream) - case Failure(ex: ChannelClosedException) => StreamResult.End - case Failure(ex) => throw ex -``` - -### Coroutines or Fibers - -A coroutine or fiber is simply a `Future[Unit]`. This might seem surprising at first. Why should we return something from a coroutine or fiber? Well, we certainly do want to observe that a coroutine has terminated, and we also need to handle any exceptions that are thrown -from it. A result of type `Try[Unit]` has exactly the information we need for this. We typically want to add some supervisor framework that -waits for coroutines to terminate and handles failures. A possible setup would be to send terminated coroutines to a channel that is serviced by a supervisor future. - -### Actors - -Similarly, we can model an actor by a `Future[Unit]` paired with a channel which serves as the actor's inbox. - -## Implementation Details - -## Internals of Async Contexts - -An async context provides two elements: - - - an `await` method that allows a caller to suspend while waiting for the result of an async source to arrive, - - a `config` value that refers to the configuration used in the async - context. - -A configuration of an async context is defined by the following class: -```scala - case class Config(scheduler: ExecutionContext, group: CancellationGroup) -``` -It contains as members a scheduler and a cancellation group. The scheduler -is an `ExecutionContext` that determines when and how suspended tasks are run. The cancellation group determines the default linkage of all cancellable objects that are created in an async context. - -Async contexts and their configurations are usually passed as implicit parameters. Async configurations can be implicitly generated from async contexts by simply pulling out the `config` value of the context. -They can also be implicitly generated from execution contexts, by -combining an execution context with the special `Unlinked` cancellation group. The generation from an async context has higher priority than the generation from an execution context. This schema is implemented in the companion object of class `Config`: -```scala - trait LowPrioConfig: - given fromExecutionContext(using scheduler: ExecutionContext): Config = - Config(scheduler, CancellationGroup.Unlinked) - - object Config extends LowPrioConfig: - given fromAsync(using async: Async): Config = async.config -``` - -## Implementing Await - -The most interesting part of an async context is its implementation of the `await` method. These implementations need to be based on a lower-level mechanism of suspensions or green threads. - -### Using Delimited Continuations - -We first describe -the implementation if support for full delimited continuations is available. We assume in this case a trait -```scala -trait Suspension[-T, +R]: - def resume(arg: T): R = ??? -``` -and a method -```scala -def suspend[T, R](body: Suspension[T, R] => R)(using Label[R]): T = ??? -``` -A call of `suspend(body)` captures the continuation up to an enclosing boundary in a `Suspension` object and passes it to `body`. The continuation can be resumed by calling the suspension's `resume` method. The enclosing boundary -is the one which created the implicit `Label` argument. - -Using this infrastructure, `await` can be implemented like this: -```scala - def await[T](src: Async.Source[T]): T = - checkCancellation() - src.poll().getOrElse: - try - suspend[T, Unit]: k => - src.onComplete: x => - config.scheduler.schedule: () => - k.resume(x) - true // signals to `src` that result `x` was consumed - finally checkCancellation() -``` -Notes: - - - The main body of `await` is enclosed by two `checkCancellation` calls that abort - the computation with a `CancellationException` in case of a cancel request. - - Await first polls the async source and returns the result if one is present. - - If no result is present, it suspends the computation and adds a listener to the source via its `onComplete` method. The listener is generated via a SAM conversion from the closure following `x =>`. - - If the listener is invoked with a result, it resumes the suspension with that result argument in a newly scheduled task. The listener returns `true` to indicate that the result value was consumed. - -An Async context with this version of `await` is used in the following -implementation of `async`, the wrapper for the body of a future: -```scala -private def async(body: Async ?=> Unit): Unit = - class FutureAsync(using val config: Async.Config) extends Async: - def await[T](src: Async.Source[T]): T = ... - ... - - boundary [Unit]: - body(using FutureAsync()) -``` - -### Using Fibers - -On a runtime that only provides fibers (_aka_ green threads), the implementation of `await` is a bit more complicated, since we cannot suspend awaiting an argument value. We can work around this restriction by re-formulating the body of `await` as follows: -```scala - def await[T](src: Async.Source[T]): T = - checkCancellation() - src.poll().getOrElse: - try - var result: Option[T] = None - src.onComplete: x => - synchronized: - result = Some(x) - notify() - true - synchronized: - while result.isEmpty do wait() - result.get - finally checkCancellation() -``` -Only the body of the `try` is different from the previous implementation. Here we now create a variable holding an optional result value. The computation `wait`s until the result value is defined. The variable becomes is set to a defined value when the listener is invoked, followed by a call to `notify()` to wake up the waiting fiber. - -Since the whole fiber suspends, we don't need a `boundary` anymore to delineate the limit of a continuation, so the `async` can be defined as follows: -```scala -private def async(body: Async ?=> Unit): Unit = - class FutureAsync(using val config: Async.Config) extends Async: - def await[T](src: Async.Source[T]): T = ... - ... - - body(using FutureAsync()) -```