From 00f77027e0011c19f60d070397bdeb9701b6bc42 Mon Sep 17 00:00:00 2001 From: Jess smith Date: Fri, 17 Jun 2016 12:08:17 -0400 Subject: [PATCH 01/20] Add coroutines-extra subproject with Async implementation --- .../src/main/scala/AsyncAwait.scala | 51 +++++++++++++ .../src/test/scala/AsyncAwaitTests.scala | 57 +++++++++++++++ dependencies.conf | 2 + project/Build.scala | 73 +++++++++++++++++++ 4 files changed, 183 insertions(+) create mode 100644 coroutines-extra/src/main/scala/AsyncAwait.scala create mode 100644 coroutines-extra/src/test/scala/AsyncAwaitTests.scala diff --git a/coroutines-extra/src/main/scala/AsyncAwait.scala b/coroutines-extra/src/main/scala/AsyncAwait.scala new file mode 100644 index 0000000..72c03b8 --- /dev/null +++ b/coroutines-extra/src/main/scala/AsyncAwait.scala @@ -0,0 +1,51 @@ +package org.coroutines.extra + + + +import org.coroutines._ +import scala.annotation.unchecked.uncheckedVariance +import scala.concurrent._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.util.{ Success, Failure } + + + +object AsyncAwait { + class Cell[+T] { + var x: T @uncheckedVariance = _ + } + + /** The future should be computed after the pair is yielded. The result of + * this future can be used to assign a value to `cell.x`. + * Note that `Cell` is used in order to give users the option to not directly + * return the result of the future. + */ + def await[R]: Future[R] ~~> ((Future[R], Cell[R]), R) = + coroutine { (f: Future[R]) => + val cell = new Cell[R] + yieldval((f, cell)) + cell.x + } + + def async[Y, R](body: ~~~>[(Future[Y], Cell[Y]), R]): Future[R] = { + val c = call(body()) + val p = Promise[R] + def loop() { + if (!c.resume) { + if (c.hasException) { + p.failure(c.$exception) + } else { + p.success(c.result) + } + } else { + val (future, cell) = c.value + for (x <- future) { + cell.x = x + loop() + } + } + } + Future { loop() } + p.future + } +} diff --git a/coroutines-extra/src/test/scala/AsyncAwaitTests.scala b/coroutines-extra/src/test/scala/AsyncAwaitTests.scala new file mode 100644 index 0000000..dc81cc2 --- /dev/null +++ b/coroutines-extra/src/test/scala/AsyncAwaitTests.scala @@ -0,0 +1,57 @@ +package org.coroutines.extra + + + +import org.coroutines._ +import org.scalatest._ +import scala.concurrent.{ Await, Future } +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ + + + +class AsyncAwaitTests extends FunSuite with Matchers { + val errorMessage = "Life ain't no Nintendo game" + + test("simple test defined in Scala Async") { + val future = AsyncAwait.async(coroutine { () => + val f1 = AsyncAwait.async(coroutine { () => + AsyncAwait.await(Future(true)) + }) + val f2 = AsyncAwait.async(coroutine { () => + AsyncAwait.await(Future(42)) + }) + if (AsyncAwait.await(f1)) + AsyncAwait.await(f2) + else + 0 + }) + val result = Await.result(future, 1 seconds) + assert(result == 42) + } + + test("error handling test 1") { + val exception = intercept[RuntimeException] { + val c = coroutine { () => + sys.error(errorMessage) + AsyncAwait.await(Future("dog")) + } + val future = AsyncAwait.async(c) + val result = Await.result(future, 1 seconds) + } + assert(exception.getMessage == errorMessage) + } + + test("error handling test 2") { + val exception = intercept[RuntimeException] { + val c = coroutine { () => + sys.error(errorMessage) + yieldval((Future("god"), new AsyncAwait.Cell[String])) + AsyncAwait.await(Future("dog")) + } + val future = AsyncAwait.async(c) + Await.result(future, 1 seconds) + } + assert(exception.getMessage == errorMessage) + } +} diff --git a/dependencies.conf b/dependencies.conf index 74d5c33..2cd4f0f 100755 --- a/dependencies.conf +++ b/dependencies.conf @@ -8,3 +8,5 @@ coroutines = [ artifact = ["com.storm-enroute", "scalameter", "0.8-SNAPSHOT", "test;bench"] } ] + +coroutines-extra = [] diff --git a/project/Build.scala b/project/Build.scala index 6f82753..6fd25b1 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -159,6 +159,63 @@ object CoroutinesBuild extends MechaRepoBuild { mechaDocsPathKey := "coroutines-common" ) + val coroutinesExtraSettings = + Defaults.defaultSettings ++ MechaRepoPlugin.defaultSettings ++ Seq( + name := "coroutines-extra", + organization := "com.storm-enroute", + version <<= frameworkVersion, + scalaVersion <<= coroutinesScalaVersion, + crossScalaVersions <<= coroutinesCrossScalaVersions, + libraryDependencies <++= (scalaVersion)(sv => extraDependencies(sv)), + scalacOptions ++= Seq( + "-deprecation", + "-unchecked", + "-optimise", + "-Yinline-warnings" + ), + resolvers ++= Seq( + "Sonatype OSS Snapshots" at + "https://oss.sonatype.org/content/repositories/snapshots", + "Sonatype OSS Releases" at + "https://oss.sonatype.org/content/repositories/releases" + ), + ivyLoggingLevel in ThisBuild := UpdateLogging.Quiet, + publishMavenStyle := true, + publishTo <<= version { (v: String) => + val nexus = "https://oss.sonatype.org/" + if (v.trim.endsWith("SNAPSHOT")) + Some("snapshots" at nexus + "content/repositories/snapshots") + else + Some("releases" at nexus + "service/local/staging/deploy/maven2") + }, + publishArtifact in Test := false, + pomIncludeRepository := { _ => false }, + pomExtra := + http://storm-enroute.com/ + + + BSD-style + http://opensource.org/licenses/BSD-3-Clause + repo + + + + git@github.com:storm-enroute/coroutines.git + scm:git:git@github.com:storm-enroute/coroutines.git + + + + axel22 + Aleksandar Prokopec + http://axel22.github.com/ + + , + mechaPublishKey <<= mechaPublishKey.dependsOn(publish), + mechaDocsRepoKey := "git@github.com:storm-enroute/apidocs.git", + mechaDocsBranchKey := "gh-pages", + mechaDocsPathKey := "coroutines-extra" + ) + def commonDependencies(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { case Some((2, major)) if major >= 11 => Seq( @@ -169,6 +226,14 @@ object CoroutinesBuild extends MechaRepoBuild { case _ => Nil } + def extraDependencies(scalaVersion: String) = + CrossVersion.partialVersion(scalaVersion) match { + case Some((2, major)) if major >= 11 => Seq( + "org.scalatest" % "scalatest_2.11" % "2.2.6" % "test" + ) + case _ => Nil + } + lazy val Benchmarks = config("bench") extend (Test) lazy val coroutines: Project = Project( @@ -191,4 +256,12 @@ object CoroutinesBuild extends MechaRepoBuild { settings = coroutinesCommonSettings ) dependsOnSuperRepo + + lazy val coroutinesExtra: Project = Project( + "coroutines-extra", + file("coroutines-extra"), + settings = coroutinesExtraSettings + ) dependsOn( + coroutines % "compile->compile;test->test" + ) dependsOnSuperRepo } From 54c568d5e5c504005998fa0bbb5468543193ec2b Mon Sep 17 00:00:00 2001 From: Jess smith Date: Fri, 17 Jun 2016 17:28:40 -0400 Subject: [PATCH 02/20] Document more methods on Coroutine.Instance --- src/main/scala/org/coroutines/Coroutine.scala | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/src/main/scala/org/coroutines/Coroutine.scala b/src/main/scala/org/coroutines/Coroutine.scala index aea78d0..8335e1d 100644 --- a/src/main/scala/org/coroutines/Coroutine.scala +++ b/src/main/scala/org/coroutines/Coroutine.scala @@ -172,10 +172,26 @@ object Coroutine { $yield } + /** Returns whether or not the coroutine yielded a value. This value can be + * accessed via `getValue`. + * + * @return `true` if the coroutine yielded a value, `false` otherwise. + */ final def hasValue: Boolean = $hasYield + /** Returns an `Option` instance wrapping the current value of the coroutine, + * if any. + * + * @return `Some(value)` if `hasValue`, `None` otherwise. + */ final def getValue: Option[Y] = if (hasValue) Some(value) else None + /** Returns a `Try` instance wrapping either the current value of this coroutine + * or any exceptions thrown when trying to get the value. + * + * @return `Success(value)` if `value` does not throw an exception, or + * a `Failure` instance if it does. + */ final def tryValue: Try[Y] = try { Success(value) } catch { case t: Throwable => Failure(t) } @@ -198,15 +214,36 @@ object Coroutine { $result } + /** Returns whether or not the coroutine completed without an exception. + * + * @return `true` if the coroutine completed without an exception, `false` + * otherwise. + */ final def hasResult: Boolean = isCompleted && $exception == null + /** Returns an `Option` instance wrapping this coroutine's non-exception + * result, if any. + * + * @return `Some(result)` if `hasResult`, `None` otherwise. + */ final def getResult: Option[R] = if (hasResult) Some(result) else None + /** Returns a `Try` object wrapping either the successful result of this + * coroutine or the exception that the coroutine threw. + * + * @return A `Failure` instance if the coroutine has an exception, + * `Try(result)` otherwise. + */ final def tryResult: Try[R] = { if ($exception != null) Failure($exception) else Try(result) } + /** Returns whether or not the coroutine completed with an exception. + * + * @return `true` iff `isCompleted` and the coroutine has a non-null + * exception, `false` otherwise. + */ final def hasException: Boolean = isCompleted && $exception != null /** Returns `false` iff the coroutine instance completed execution. @@ -227,12 +264,19 @@ object Coroutine { */ final def isCompleted: Boolean = !isLive - /** Returns a string representation of the coroutine's state. + /** Returns a string representation of the coroutine's state. Contains less + * information than `debugString`. * * @return A string describing the coroutine state. */ override def toString = s"Coroutine.Instance" + /** Returns a string containing information about the internal state of the + * coroutine. Contains more information than `toString`. + * + * @return A string containing information about the internal state of the + * coroutine. + */ final def debugString: String = { def toStackLength[T](stack: Array[T]) = if (stack != null) "${stack.length}" else "" From bc59993cd97932e0e569a7b8e2ecc1604e62b76b Mon Sep 17 00:00:00 2001 From: Jess smith Date: Mon, 20 Jun 2016 15:15:42 -0400 Subject: [PATCH 03/20] Handle futures' errors in AsyncAwait and add more error handling tests --- .../src/main/scala/AsyncAwait.scala | 16 ++++--- .../src/test/scala/AsyncAwaitTests.scala | 48 ++++++++++++++++++- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/coroutines-extra/src/main/scala/AsyncAwait.scala b/coroutines-extra/src/main/scala/AsyncAwait.scala index 72c03b8..63cf673 100644 --- a/coroutines-extra/src/main/scala/AsyncAwait.scala +++ b/coroutines-extra/src/main/scala/AsyncAwait.scala @@ -32,16 +32,18 @@ object AsyncAwait { val p = Promise[R] def loop() { if (!c.resume) { - if (c.hasException) { - p.failure(c.$exception) - } else { - p.success(c.result) + c.tryResult match { + case Success(result) => p.success(result) + case Failure(exception) => p.failure(exception) } } else { val (future, cell) = c.value - for (x <- future) { - cell.x = x - loop() + future onComplete { + case Success(x) => + cell.x = x + loop() + case Failure(exception) => + p.failure(exception) } } } diff --git a/coroutines-extra/src/test/scala/AsyncAwaitTests.scala b/coroutines-extra/src/test/scala/AsyncAwaitTests.scala index dc81cc2..8683024 100644 --- a/coroutines-extra/src/test/scala/AsyncAwaitTests.scala +++ b/coroutines-extra/src/test/scala/AsyncAwaitTests.scala @@ -11,7 +11,7 @@ import scala.concurrent.duration._ class AsyncAwaitTests extends FunSuite with Matchers { - val errorMessage = "Life ain't no Nintendo game" + val errorMessage = "problem" test("simple test defined in Scala Async") { val future = AsyncAwait.async(coroutine { () => @@ -54,4 +54,50 @@ class AsyncAwaitTests extends FunSuite with Matchers { } assert(exception.getMessage == errorMessage) } + + // Source: https://git.io/vowde + test("uncaught exception within async after await") { + val future = AsyncAwait.async(coroutine { () => + AsyncAwait.await(Future(())) + throw new Exception(errorMessage) + }) + intercept[Exception] { Await.result(future, 1 seconds) } + } + + // Source: https://git.io/vowdk + test("await failing future within async") { + val base = Future[Int] { throw new Exception(errorMessage) } + val future = AsyncAwait.async(coroutine { () => + val x = AsyncAwait.await(base) + x * 2 + }) + intercept[Exception] { Await.result(future, 1 seconds) } + } + + // Source: https://git.io/vowdY + test("await failing future within async after await") { + val base = Future[Any] { "five!".length } + val future = AsyncAwait.async(coroutine { () => + val a = AsyncAwait.await(base.mapTo[Int]) + val b = AsyncAwait.await(Future { (a * 2).toString }.mapTo[Int]) + val c = AsyncAwait.await(Future { (7 * 2).toString }) + b + "-" + c + }) + intercept[ClassCastException] { + Await.result(future, 1 seconds) + } + } + + test("nested failing future within async after await") { + val base = Future[Any] { "five!".length } + val future = AsyncAwait.async(coroutine { () => + val a = AsyncAwait.await(base.mapTo[Int]) + val b = AsyncAwait.await(AsyncAwait.await(Future((Future { (a * 2).toString }).mapTo[Int]))) + val c = AsyncAwait.await(Future { (7 * 2).toString }) + b + "-" + c + }) + intercept[ClassCastException] { + Await.result(future, 1 seconds) + } + } } From 84b206a78d5fe2f6e3fdd5ae43687cdb51f9771e Mon Sep 17 00:00:00 2001 From: Jess smith Date: Mon, 20 Jun 2016 15:25:33 -0400 Subject: [PATCH 04/20] Rework exceptions inside AsyncAwaitTests --- .../src/test/scala/AsyncAwaitTests.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/coroutines-extra/src/test/scala/AsyncAwaitTests.scala b/coroutines-extra/src/test/scala/AsyncAwaitTests.scala index 8683024..fe367b0 100644 --- a/coroutines-extra/src/test/scala/AsyncAwaitTests.scala +++ b/coroutines-extra/src/test/scala/AsyncAwaitTests.scala @@ -11,7 +11,7 @@ import scala.concurrent.duration._ class AsyncAwaitTests extends FunSuite with Matchers { - val errorMessage = "problem" + class TestException extends Throwable test("simple test defined in Scala Async") { val future = AsyncAwait.async(coroutine { () => @@ -31,6 +31,7 @@ class AsyncAwaitTests extends FunSuite with Matchers { } test("error handling test 1") { + val errorMessage = "System error!" val exception = intercept[RuntimeException] { val c = coroutine { () => sys.error(errorMessage) @@ -43,35 +44,34 @@ class AsyncAwaitTests extends FunSuite with Matchers { } test("error handling test 2") { - val exception = intercept[RuntimeException] { + intercept[TestException] { val c = coroutine { () => - sys.error(errorMessage) + throw new TestException yieldval((Future("god"), new AsyncAwait.Cell[String])) AsyncAwait.await(Future("dog")) } val future = AsyncAwait.async(c) Await.result(future, 1 seconds) } - assert(exception.getMessage == errorMessage) } // Source: https://git.io/vowde test("uncaught exception within async after await") { val future = AsyncAwait.async(coroutine { () => AsyncAwait.await(Future(())) - throw new Exception(errorMessage) + throw new TestException }) - intercept[Exception] { Await.result(future, 1 seconds) } + intercept[TestException] { Await.result(future, 1 seconds) } } // Source: https://git.io/vowdk test("await failing future within async") { - val base = Future[Int] { throw new Exception(errorMessage) } + val base = Future[Int] { throw new TestException } val future = AsyncAwait.async(coroutine { () => val x = AsyncAwait.await(base) x * 2 }) - intercept[Exception] { Await.result(future, 1 seconds) } + intercept[TestException] { Await.result(future, 1 seconds) } } // Source: https://git.io/vowdY From a20aa982036e948fe30891439fca7f5148b84e65 Mon Sep 17 00:00:00 2001 From: Jess smith Date: Mon, 20 Jun 2016 19:19:38 -0400 Subject: [PATCH 05/20] Add one async/await example, document others --- .../src/test/scala/AsyncAwaitTests.scala | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/coroutines-extra/src/test/scala/AsyncAwaitTests.scala b/coroutines-extra/src/test/scala/AsyncAwaitTests.scala index fe367b0..c859552 100644 --- a/coroutines-extra/src/test/scala/AsyncAwaitTests.scala +++ b/coroutines-extra/src/test/scala/AsyncAwaitTests.scala @@ -13,21 +13,44 @@ import scala.concurrent.duration._ class AsyncAwaitTests extends FunSuite with Matchers { class TestException extends Throwable - test("simple test defined in Scala Async") { - val future = AsyncAwait.async(coroutine { () => - val f1 = AsyncAwait.async(coroutine { () => - AsyncAwait.await(Future(true)) - }) - val f2 = AsyncAwait.async(coroutine { () => - AsyncAwait.await(Future(42)) - }) - if (AsyncAwait.await(f1)) - AsyncAwait.await(f2) - else + /** Source: https://git.io/vorXv + * The use of Async/Await as opposed to pure futures allows this control flow + * to be written more easily. + * The execution blocks when awaiting for the result of `f1`. `f2` only blocks + * after `AsyncAwait.await(f1)` evaluates to `true`. + */ + test("simple test") { + val future = AsyncAwait.async(coroutine { () => + val f1 = Future(true) + val f2 = Future(42) + if (AsyncAwait.await(f1)) { + AsyncAwait.await(f2) + } else { 0 + } + }) + assert(Await.result(future, 1 seconds) == 42) + } + + /** Asynchronous blocks of code can be defined either outside of or within any + * part of an `async` block. This allows the user to avoid triggering the + * computation of slow futures until it is necessary. + * For instance, computation will not begin on `innerFuture` until + * `await(trueFuture)` evaluates to true. + */ + test("nested async blocks") { + val outerFuture = AsyncAwait.async(coroutine {() => + val trueFuture = Future { true } + if (AsyncAwait.await(trueFuture)) { + val innerFuture = AsyncAwait.async(coroutine { () => + AsyncAwait.await(Future { 100 } ) + }) + AsyncAwait.await(innerFuture) + } else { + 200 + } }) - val result = Await.result(future, 1 seconds) - assert(result == 42) + assert(Await.result(outerFuture, 1 seconds) == 100) } test("error handling test 1") { From 8b03721de98634462b4cc24b18ff44d91bf0bbab Mon Sep 17 00:00:00 2001 From: Jess smith Date: Tue, 21 Jun 2016 16:36:12 -0400 Subject: [PATCH 06/20] Document more tests and add macro-based implementation of async --- .../src/main/scala/AsyncAwait.scala | 17 +++++- .../src/test/scala/AsyncAwaitTests.scala | 55 +++++++++++-------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/coroutines-extra/src/main/scala/AsyncAwait.scala b/coroutines-extra/src/main/scala/AsyncAwait.scala index 63cf673..bc293f1 100644 --- a/coroutines-extra/src/main/scala/AsyncAwait.scala +++ b/coroutines-extra/src/main/scala/AsyncAwait.scala @@ -7,6 +7,8 @@ import scala.annotation.unchecked.uncheckedVariance import scala.concurrent._ import scala.concurrent.ExecutionContext.Implicits.global import scala.util.{ Success, Failure } +import scala.language.experimental.macros +import scala.reflect.macros.whitebox.Context @@ -27,7 +29,7 @@ object AsyncAwait { cell.x } - def async[Y, R](body: ~~~>[(Future[Y], Cell[Y]), R]): Future[R] = { + def asyncCall[Y, R](body: ~~~>[(Future[Y], Cell[Y]), R]): Future[R] = { val c = call(body()) val p = Promise[R] def loop() { @@ -50,4 +52,17 @@ object AsyncAwait { Future { loop() } p.future } + + def async[Y, R](body: =>R): Future[R] = macro asyncMacro[Y, R] + + def asyncMacro[Y, R](c: Context)(body: c.Tree): c.Tree = { + import c.universe._ + + q""" + val c = coroutine { () => + $body + } + _root_.org.coroutines.extra.AsyncAwait.asyncCall(c) + """ + } } diff --git a/coroutines-extra/src/test/scala/AsyncAwaitTests.scala b/coroutines-extra/src/test/scala/AsyncAwaitTests.scala index c859552..23924b1 100644 --- a/coroutines-extra/src/test/scala/AsyncAwaitTests.scala +++ b/coroutines-extra/src/test/scala/AsyncAwaitTests.scala @@ -20,7 +20,7 @@ class AsyncAwaitTests extends FunSuite with Matchers { * after `AsyncAwait.await(f1)` evaluates to `true`. */ test("simple test") { - val future = AsyncAwait.async(coroutine { () => + val future = AsyncAwait.async { val f1 = Future(true) val f2 = Future(42) if (AsyncAwait.await(f1)) { @@ -28,7 +28,7 @@ class AsyncAwaitTests extends FunSuite with Matchers { } else { 0 } - }) + } assert(Await.result(future, 1 seconds) == 42) } @@ -39,28 +39,30 @@ class AsyncAwaitTests extends FunSuite with Matchers { * `await(trueFuture)` evaluates to true. */ test("nested async blocks") { - val outerFuture = AsyncAwait.async(coroutine {() => + val outerFuture = AsyncAwait.async { val trueFuture = Future { true } if (AsyncAwait.await(trueFuture)) { - val innerFuture = AsyncAwait.async(coroutine { () => + val innerFuture = AsyncAwait.async { AsyncAwait.await(Future { 100 } ) - }) + } AsyncAwait.await(innerFuture) } else { 200 } - }) + } assert(Await.result(outerFuture, 1 seconds) == 100) } + /** Uncaught exceptions thrown inside async blocks cause the associated futures + * to fail. + */ test("error handling test 1") { val errorMessage = "System error!" val exception = intercept[RuntimeException] { - val c = coroutine { () => + val future = AsyncAwait.async { sys.error(errorMessage) AsyncAwait.await(Future("dog")) } - val future = AsyncAwait.async(c) val result = Await.result(future, 1 seconds) } assert(exception.getMessage == errorMessage) @@ -68,44 +70,52 @@ class AsyncAwaitTests extends FunSuite with Matchers { test("error handling test 2") { intercept[TestException] { - val c = coroutine { () => + val future = AsyncAwait.async { throw new TestException yieldval((Future("god"), new AsyncAwait.Cell[String])) AsyncAwait.await(Future("dog")) } - val future = AsyncAwait.async(c) Await.result(future, 1 seconds) } } - // Source: https://git.io/vowde + /** Source: https://git.io/vowde + * Without the closing `()`, the compiler complains about expecting return + * type `Future[Unit]` but finding `Future[Nothing]`. + */ test("uncaught exception within async after await") { - val future = AsyncAwait.async(coroutine { () => + val future = AsyncAwait.async { AsyncAwait.await(Future(())) throw new TestException - }) - intercept[TestException] { Await.result(future, 1 seconds) } + () + } + intercept[TestException] { + Await.result(future, 1 seconds) + } } // Source: https://git.io/vowdk test("await failing future within async") { val base = Future[Int] { throw new TestException } - val future = AsyncAwait.async(coroutine { () => + val future = AsyncAwait.async { val x = AsyncAwait.await(base) x * 2 - }) + } intercept[TestException] { Await.result(future, 1 seconds) } } - // Source: https://git.io/vowdY + /** Source: https://git.io/vowdY + * Exceptions thrown inside `await` calls are properly bubbled up. They cause + * the async block's future to fail. + */ test("await failing future within async after await") { val base = Future[Any] { "five!".length } - val future = AsyncAwait.async(coroutine { () => + val future = AsyncAwait.async { val a = AsyncAwait.await(base.mapTo[Int]) val b = AsyncAwait.await(Future { (a * 2).toString }.mapTo[Int]) val c = AsyncAwait.await(Future { (7 * 2).toString }) b + "-" + c - }) + } intercept[ClassCastException] { Await.result(future, 1 seconds) } @@ -113,12 +123,13 @@ class AsyncAwaitTests extends FunSuite with Matchers { test("nested failing future within async after await") { val base = Future[Any] { "five!".length } - val future = AsyncAwait.async(coroutine { () => + val future = AsyncAwait.async { val a = AsyncAwait.await(base.mapTo[Int]) - val b = AsyncAwait.await(AsyncAwait.await(Future((Future { (a * 2).toString }).mapTo[Int]))) + val b = AsyncAwait.await( + AsyncAwait.await(Future((Future { (a * 2).toString }).mapTo[Int]))) val c = AsyncAwait.await(Future { (7 * 2).toString }) b + "-" + c - }) + } intercept[ClassCastException] { Await.result(future, 1 seconds) } From 35fadaff95f963a778f173e6ef64e9e75d8325e9 Mon Sep 17 00:00:00 2001 From: Jess smith Date: Wed, 22 Jun 2016 13:44:05 -0400 Subject: [PATCH 07/20] Document Async/Await methods --- .../src/main/scala/AsyncAwait.scala | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/coroutines-extra/src/main/scala/AsyncAwait.scala b/coroutines-extra/src/main/scala/AsyncAwait.scala index bc293f1..1c982c9 100644 --- a/coroutines-extra/src/main/scala/AsyncAwait.scala +++ b/coroutines-extra/src/main/scala/AsyncAwait.scala @@ -13,14 +13,26 @@ import scala.reflect.macros.whitebox.Context object AsyncAwait { + /** A wrapper class for a variable of type `T`. + * + * Used inside `await` to wrap the results of asynchronous computations. + */ class Cell[+T] { var x: T @uncheckedVariance = _ } - /** The future should be computed after the pair is yielded. The result of - * this future can be used to assign a value to `cell.x`. - * Note that `Cell` is used in order to give users the option to not directly + /** Await the result of a future. + * + * When called inside an `async` body, this function will block until its + * associated future completes. + * + * Note that the usage of `Cell` gives users the option to not directly * return the result of the future. + * + * @return A coroutine that yields a tuple. `async` will assign this tuple's + * second element to hold the completed result of the `Future` passed + * into the coroutine. The coroutine will directly return the + * result of the future. */ def await[R]: Future[R] ~~> ((Future[R], Cell[R]), R) = coroutine { (f: Future[R]) => @@ -29,6 +41,13 @@ object AsyncAwait { cell.x } + /** Calls `body`, blocking on any calls to `await`. + * + * @param body A coroutine to be invoked. + * @return A `Future` wrapping the result of the coroutine. The future fails + * if `body.hasException` or if one of `body`'s yielded tuples + * has a failing future. + */ def asyncCall[Y, R](body: ~~~>[(Future[Y], Cell[Y]), R]): Future[R] = { val c = call(body()) val p = Promise[R] @@ -53,8 +72,22 @@ object AsyncAwait { p.future } + /** Wraps `body` inside a coroutine and asynchronously invokes it using + * `asyncMacro`. + * + * @param body The block of code to wrap inside an asynchronous coroutine. + * @return A `Future` wrapping the result of `body`. + */ def async[Y, R](body: =>R): Future[R] = macro asyncMacro[Y, R] + /** Implements `async`. + * + * Wraps `body` inside a coroutine and calls `asyncCall`. + * + * @param body The function to be wrapped in a coroutine. + * @return A tree that contains an invocation of `asyncCall` on a coroutine + * with `body` as its body. + */ def asyncMacro[Y, R](c: Context)(body: c.Tree): c.Tree = { import c.universe._ From 43fa9fae4a80f1eb8c1ca7740e65890a47b5bcdf Mon Sep 17 00:00:00 2001 From: Jess smith Date: Tue, 28 Jun 2016 15:48:32 -0400 Subject: [PATCH 08/20] Fix spacing inside Coroutine documentation --- src/main/scala/org/coroutines/Coroutine.scala | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/scala/org/coroutines/Coroutine.scala b/src/main/scala/org/coroutines/Coroutine.scala index 8335e1d..4397abb 100644 --- a/src/main/scala/org/coroutines/Coroutine.scala +++ b/src/main/scala/org/coroutines/Coroutine.scala @@ -172,15 +172,16 @@ object Coroutine { $yield } - /** Returns whether or not the coroutine yielded a value. This value can be - * accessed via `getValue`. + /** Returns whether or not the coroutine yielded a value. + * + * This value can be accessed via `getValue`. * * @return `true` if the coroutine yielded a value, `false` otherwise. */ final def hasValue: Boolean = $hasYield - /** Returns an `Option` instance wrapping the current value of the coroutine, - * if any. + /** Returns an `Option` instance wrapping the current value of the coroutine, if + * any. * * @return `Some(value)` if `hasValue`, `None` otherwise. */ @@ -221,8 +222,8 @@ object Coroutine { */ final def hasResult: Boolean = isCompleted && $exception == null - /** Returns an `Option` instance wrapping this coroutine's non-exception - * result, if any. + /** Returns an `Option` instance wrapping this coroutine's non-exception result, if + * any. * * @return `Some(result)` if `hasResult`, `None` otherwise. */ @@ -264,15 +265,18 @@ object Coroutine { */ final def isCompleted: Boolean = !isLive - /** Returns a string representation of the coroutine's state. Contains less - * information than `debugString`. + /** Returns a string representation of the coroutine's state. + * + * Contains less information than `debugString`. * * @return A string describing the coroutine state. */ override def toString = s"Coroutine.Instance" /** Returns a string containing information about the internal state of the - * coroutine. Contains more information than `toString`. + * coroutine. + * + * Contains more information than `toString`. * * @return A string containing information about the internal state of the * coroutine. From 124c3a1d8e475c60c2dcb1489f974d85de4badfb Mon Sep 17 00:00:00 2001 From: Jess smith Date: Tue, 28 Jun 2016 17:11:30 -0400 Subject: [PATCH 09/20] Change async/await implementation and relocate some files --- .../coroutines/extra}/AsyncAwait.scala | 42 ++- .../src/test/scala/AsyncAwaitTests.scala | 137 --------- .../coroutines/extra}/async-await-tests.scala | 283 +++++++++++------- 3 files changed, 191 insertions(+), 271 deletions(-) rename coroutines-extra/src/main/scala/{ => org/coroutines/extra}/AsyncAwait.scala (76%) delete mode 100644 coroutines-extra/src/test/scala/AsyncAwaitTests.scala rename {src/test/scala/org/coroutines => coroutines-extra/src/test/scala/org/coroutines/extra}/async-await-tests.scala (61%) diff --git a/coroutines-extra/src/main/scala/AsyncAwait.scala b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala similarity index 76% rename from coroutines-extra/src/main/scala/AsyncAwait.scala rename to coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala index 1c982c9..075a8bf 100644 --- a/coroutines-extra/src/main/scala/AsyncAwait.scala +++ b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala @@ -4,41 +4,35 @@ package org.coroutines.extra import org.coroutines._ import scala.annotation.unchecked.uncheckedVariance -import scala.concurrent._ import scala.concurrent.ExecutionContext.Implicits.global -import scala.util.{ Success, Failure } +import scala.concurrent._ import scala.language.experimental.macros import scala.reflect.macros.whitebox.Context +import scala.util.{ Success, Failure } object AsyncAwait { - /** A wrapper class for a variable of type `T`. - * - * Used inside `await` to wrap the results of asynchronous computations. - */ - class Cell[+T] { - var x: T @uncheckedVariance = _ - } - /** Await the result of a future. * * When called inside an `async` body, this function will block until its * associated future completes. * - * Note that the usage of `Cell` gives users the option to not directly - * return the result of the future. - * * @return A coroutine that yields a tuple. `async` will assign this tuple's * second element to hold the completed result of the `Future` passed * into the coroutine. The coroutine will directly return the * result of the future. */ - def await[R]: Future[R] ~~> ((Future[R], Cell[R]), R) = - coroutine { (f: Future[R]) => - val cell = new Cell[R] - yieldval((f, cell)) - cell.x + def await[R]: Future[R] ~~> (Future[R], R) = + coroutine { (awaitedFuture: Future[R]) => + yieldval(awaitedFuture) + var result: R = null.asInstanceOf[R] + awaitedFuture.value match { + case Some(Success(x)) => result = x + case Some(Failure(error)) => throw error + case None => sys.error("Future was not completed") + } + result } /** Calls `body`, blocking on any calls to `await`. @@ -48,7 +42,7 @@ object AsyncAwait { * if `body.hasException` or if one of `body`'s yielded tuples * has a failing future. */ - def asyncCall[Y, R](body: ~~~>[(Future[Y], Cell[Y]), R]): Future[R] = { + def asyncCall[Y, R](body: ~~~>[Future[Y], R]): Future[R] = { val c = call(body()) val p = Promise[R] def loop() { @@ -58,10 +52,9 @@ object AsyncAwait { case Failure(exception) => p.failure(exception) } } else { - val (future, cell) = c.value - future onComplete { - case Success(x) => - cell.x = x + val awaitedFuture = c.value + awaitedFuture onComplete { + case Success(result) => loop() case Failure(exception) => p.failure(exception) @@ -72,8 +65,7 @@ object AsyncAwait { p.future } - /** Wraps `body` inside a coroutine and asynchronously invokes it using - * `asyncMacro`. + /** Wraps `body` inside a coroutine and asynchronously invokes it using `asyncMacro`. * * @param body The block of code to wrap inside an asynchronous coroutine. * @return A `Future` wrapping the result of `body`. diff --git a/coroutines-extra/src/test/scala/AsyncAwaitTests.scala b/coroutines-extra/src/test/scala/AsyncAwaitTests.scala deleted file mode 100644 index 23924b1..0000000 --- a/coroutines-extra/src/test/scala/AsyncAwaitTests.scala +++ /dev/null @@ -1,137 +0,0 @@ -package org.coroutines.extra - - - -import org.coroutines._ -import org.scalatest._ -import scala.concurrent.{ Await, Future } -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ - - - -class AsyncAwaitTests extends FunSuite with Matchers { - class TestException extends Throwable - - /** Source: https://git.io/vorXv - * The use of Async/Await as opposed to pure futures allows this control flow - * to be written more easily. - * The execution blocks when awaiting for the result of `f1`. `f2` only blocks - * after `AsyncAwait.await(f1)` evaluates to `true`. - */ - test("simple test") { - val future = AsyncAwait.async { - val f1 = Future(true) - val f2 = Future(42) - if (AsyncAwait.await(f1)) { - AsyncAwait.await(f2) - } else { - 0 - } - } - assert(Await.result(future, 1 seconds) == 42) - } - - /** Asynchronous blocks of code can be defined either outside of or within any - * part of an `async` block. This allows the user to avoid triggering the - * computation of slow futures until it is necessary. - * For instance, computation will not begin on `innerFuture` until - * `await(trueFuture)` evaluates to true. - */ - test("nested async blocks") { - val outerFuture = AsyncAwait.async { - val trueFuture = Future { true } - if (AsyncAwait.await(trueFuture)) { - val innerFuture = AsyncAwait.async { - AsyncAwait.await(Future { 100 } ) - } - AsyncAwait.await(innerFuture) - } else { - 200 - } - } - assert(Await.result(outerFuture, 1 seconds) == 100) - } - - /** Uncaught exceptions thrown inside async blocks cause the associated futures - * to fail. - */ - test("error handling test 1") { - val errorMessage = "System error!" - val exception = intercept[RuntimeException] { - val future = AsyncAwait.async { - sys.error(errorMessage) - AsyncAwait.await(Future("dog")) - } - val result = Await.result(future, 1 seconds) - } - assert(exception.getMessage == errorMessage) - } - - test("error handling test 2") { - intercept[TestException] { - val future = AsyncAwait.async { - throw new TestException - yieldval((Future("god"), new AsyncAwait.Cell[String])) - AsyncAwait.await(Future("dog")) - } - Await.result(future, 1 seconds) - } - } - - /** Source: https://git.io/vowde - * Without the closing `()`, the compiler complains about expecting return - * type `Future[Unit]` but finding `Future[Nothing]`. - */ - test("uncaught exception within async after await") { - val future = AsyncAwait.async { - AsyncAwait.await(Future(())) - throw new TestException - () - } - intercept[TestException] { - Await.result(future, 1 seconds) - } - } - - // Source: https://git.io/vowdk - test("await failing future within async") { - val base = Future[Int] { throw new TestException } - val future = AsyncAwait.async { - val x = AsyncAwait.await(base) - x * 2 - } - intercept[TestException] { Await.result(future, 1 seconds) } - } - - /** Source: https://git.io/vowdY - * Exceptions thrown inside `await` calls are properly bubbled up. They cause - * the async block's future to fail. - */ - test("await failing future within async after await") { - val base = Future[Any] { "five!".length } - val future = AsyncAwait.async { - val a = AsyncAwait.await(base.mapTo[Int]) - val b = AsyncAwait.await(Future { (a * 2).toString }.mapTo[Int]) - val c = AsyncAwait.await(Future { (7 * 2).toString }) - b + "-" + c - } - intercept[ClassCastException] { - Await.result(future, 1 seconds) - } - } - - test("nested failing future within async after await") { - val base = Future[Any] { "five!".length } - val future = AsyncAwait.async { - val a = AsyncAwait.await(base.mapTo[Int]) - val b = AsyncAwait.await( - AsyncAwait.await(Future((Future { (a * 2).toString }).mapTo[Int]))) - val c = AsyncAwait.await(Future { (7 * 2).toString }) - b + "-" + c - } - intercept[ClassCastException] { - Await.result(future, 1 seconds) - } - } -} diff --git a/src/test/scala/org/coroutines/async-await-tests.scala b/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala similarity index 61% rename from src/test/scala/org/coroutines/async-await-tests.scala rename to coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala index b2fb020..2e2a1fc 100644 --- a/src/test/scala/org/coroutines/async-await-tests.scala +++ b/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala @@ -1,56 +1,29 @@ -package org.coroutines +package org.coroutines.extra +import org.coroutines._ import org.scalatest._ import scala.annotation.unchecked.uncheckedVariance +import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent._ import scala.concurrent.duration._ -import scala.concurrent.ExecutionContext.Implicits.global import scala.language.{ reflectiveCalls, postfixOps } import scala.util.Success object AsyncAwaitTest { - class Cell[+T] { - var x: T @uncheckedVariance = _ - } + import AsyncAwait._ object ToughTypeObject { class Inner - def m2 = async(coroutine { () => + def m2 = async { val y = await { Future[List[_]] { Nil } } val z = await { Future[Inner] { new Inner } } (y, z) - }) - } - - // Doubly defined for ToughTypeObject - def await[R]: Future[R] ~~> ((Future[R], Cell[R]), R) = - coroutine { (f: Future[R]) => - val cell = new Cell[R] - yieldval((f, cell)) - cell.x - } - - // Doubly defined for ToughTypeObject - def async[Y, R](body: ~~~>[(Future[Y], Cell[Y]), R]): Future[R] = { - val c = call(body()) - val p = Promise[R] - def loop() { - if (!c.resume) p.success(c.result) - else { - val (future, cell) = c.value - for (x <- future) { - cell.x = x - loop() - } - } } - Future { loop() } - p.future } } @@ -71,54 +44,146 @@ object PrivateWrapper { } +class TestException extends Throwable + + class AsyncAwaitTest extends FunSuite with Matchers { - def await[R]: Future[R] ~~> ((Future[R], AsyncAwaitTest.Cell[R]), R) = - coroutine { (f: Future[R]) => - val cell = new AsyncAwaitTest.Cell[R] - yieldval((f, cell)) - cell.x - } - - def async[Y, R](body: ~~~>[(Future[Y], AsyncAwaitTest.Cell[Y]), R]): Future[R] = { - val c = call(body()) - val p = Promise[R] - def loop() { - if (!c.resume) p.success(c.result) - else { - val (future, cell) = c.value - for (x <- future) { - cell.x = x - loop() + import AsyncAwait._ + + /** Source: https://git.io/vorXv + * The use of Async/Await as opposed to pure futures allows this control flow + * to be written more easily. + * The execution blocks when awaiting for the result of `f1`. `f2` only blocks + * after `AsyncAwait.await(f1)` evaluates to `true`. + */ + test("simple test") { + val future = async { + val f1 = Future(true) + val f2 = Future(42) + if (await(f1)) { + await(f2) + } else { + 0 + } + } + assert(Await.result(future, 1 seconds) == 42) + } + + /** Asynchronous blocks of code can be defined either outside of or within any + * part of an `async` block. This allows the user to avoid triggering the + * computation of slow futures until it is necessary. + * For instance, computation will not begin on `innerFuture` until + * `await(trueFuture)` evaluates to true. + */ + test("nested async blocks") { + val outerFuture = async { + val trueFuture = Future { true } + if (await(trueFuture)) { + val innerFuture = async { + await(Future { 100 } ) } + await(innerFuture) + } else { + 200 + } + } + assert(Await.result(outerFuture, 1 seconds) == 100) + } + + /** Uncaught exceptions thrown inside async blocks cause the associated futures + * to fail. + */ + test("error handling test 1") { + val errorMessage = "System error!" + val exception = intercept[RuntimeException] { + val future = async { + sys.error(errorMessage) + await(Future("dog")) } + val result = Await.result(future, 1 seconds) + } + assert(exception.getMessage == errorMessage) + } + + /** Source: https://git.io/vowde + * Without the closing `()`, the compiler complains about expecting return + * type `Future[Unit]` but finding `Future[Nothing]`. + */ + test("uncaught exception within async after await") { + val future = async { + await(Future(())) + throw new TestException + () + } + intercept[TestException] { + Await.result(future, 1 seconds) + } + } + + // Source: https://git.io/vowdk + test("await failing future within async") { + val base = Future[Int] { throw new TestException } + val future = async { + val x = await(base) + x * 2 + } + intercept[TestException] { Await.result(future, 1 seconds) } + } + + /** Source: https://git.io/vowdY + * Exceptions thrown inside `await` calls are properly bubbled up. They cause + * the async block's future to fail. + */ + test("await failing future within async after await") { + val base = Future[Any] { "five!".length } + val future = async { + val a = await(base.mapTo[Int]) + val b = await(Future { (a * 2).toString }.mapTo[Int]) + val c = await(Future { (7 * 2).toString }) + b + "-" + c + } + intercept[ClassCastException] { + Await.result(future, 1 seconds) + } + } + + test("nested failing future within async after await") { + val base = Future[Any] { "five!".length } + val future = async { + val a = await(base.mapTo[Int]) + val b = await( + await(Future((Future { (a * 2).toString }).mapTo[Int]))) + val c = await(Future { (7 * 2).toString }) + b + "-" + c + } + intercept[ClassCastException] { + Await.result(future, 1 seconds) } - Future { loop() } - p.future } // Source: https://git.io/vrHtj test("propagates tough types") { - val fut = org.coroutines.AsyncAwaitTest.ToughTypeObject.m2 - val result: (List[_], org.coroutines.AsyncAwaitTest.ToughTypeObject.Inner) = + val fut = org.coroutines.extra.AsyncAwaitTest.ToughTypeObject.m2 + val result: (List[_], org.coroutines.extra.AsyncAwaitTest.ToughTypeObject.Inner) = Await.result(fut, 2 seconds) assert(result._1 == Nil) } // Source: https://git.io/vr7H9 test("pattern matching function") { - val c = async(coroutine { () => + val c = async { await(Future(1)) val a = await(Future(1)) val f = { case x => x + a }: Function[Int, Int] await(Future(f(2))) - }) + } val res = Await.result(c, 2 seconds) assert(res == 3) } // Source: https://git.io/vr7HA test("existential bind 1") { - def m(a: Any) = async(coroutine { () => + def m(a: Any) = async { a match { case s: Seq[_] => val x = s.size @@ -126,7 +191,7 @@ class AsyncAwaitTest extends FunSuite with Matchers { ss = s await(Future(x)) } - }) + } val res = Await.result(m(Nil), 2 seconds) assert(res == 0) } @@ -135,29 +200,29 @@ class AsyncAwaitTest extends FunSuite with Matchers { test("existential bind 2") { def conjure[T]: T = null.asInstanceOf[T] - def m1 = AsyncAwaitTest.async(coroutine { () => + def m1 = async { val p: List[Option[_]] = conjure[List[Option[_]]] - AsyncAwaitTest.await(Future(1)) - }) + await(Future(1)) + } - def m2 = AsyncAwaitTest.async(coroutine { () => - AsyncAwaitTest.await(Future[List[_]](Nil)) - }) + def m2 = async { + await(Future[List[_]](Nil)) + } } // Source: https://git.io/vr7Fx test("existential if/else") { trait Container[+A] case class ContainerImpl[A](value: A) extends Container[A] - def foo: Future[Container[_]] = AsyncAwaitTest.async(coroutine { () => + def foo: Future[Container[_]] = async { val a: Any = List(1) if (true) { val buf: Seq[_] = List(1) - val foo = AsyncAwaitTest.await(Future(5)) + val foo = await(Future(5)) val e0 = buf(0) ContainerImpl(e0) } else ??? - }) + } foo } @@ -176,14 +241,14 @@ class AsyncAwaitTest extends FunSuite with Matchers { implicit def `Something to do with List`[W, S, R] (implicit funDep: FunDep[W, S, R]) = new FunDep[W, List[S], W] { - def method(w: W, l: List[S]) = AsyncAwaitTest.async(coroutine { () => + def method(w: W, l: List[S]) = async { val it = l.iterator while (it.hasNext) { - AsyncAwaitTest.await(Future(funDep.method(w, it.next())) + await(Future(funDep.method(w, it.next())) (SomeExecutionContext)) } w - }) + } } } } @@ -192,9 +257,9 @@ class AsyncAwaitTest extends FunSuite with Matchers { test("ticket 66 in scala/async") { val e = new Exception() val f: Future[Nothing] = Future.failed(e) - val f1 = AsyncAwaitTest.async(coroutine { () => - AsyncAwaitTest.await(Future(f)) - }) + val f1 = async { + await(Future(f)) + } try { Await.result(f1, 5.seconds) } catch { @@ -204,10 +269,10 @@ class AsyncAwaitTest extends FunSuite with Matchers { // Source: https://git.io/vr7Nf test("ticket 83 in scala/async-- using value class") { - val f = AsyncAwaitTest.async(coroutine { () => + val f = async { val uid = new IntWrapper("foo") - AsyncAwaitTest.await(Future(Future(uid))) - }) + await(Future(Future(uid))) + } val outer = Await.result(f, 5.seconds) val inner = Await.result(outer, 5 seconds) assert(inner == new IntWrapper("foo")) @@ -217,14 +282,14 @@ class AsyncAwaitTest extends FunSuite with Matchers { test("ticket 86 in scala/async-- using matched value class") { def doAThing(param: IntWrapper) = Future(None) - val fut = AsyncAwaitTest.async(coroutine { () => + val fut = async { Option(new IntWrapper("value!")) match { case Some(valueHolder) => - AsyncAwaitTest.await(Future(doAThing(valueHolder))) + await(Future(doAThing(valueHolder))) case None => None } - }) + } val result = Await.result(fut, 5 seconds) assert(result.asInstanceOf[Future[IntWrapper]].value == Some(Success(None))) @@ -234,14 +299,14 @@ class AsyncAwaitTest extends FunSuite with Matchers { test("ticket 86 in scala/async-- using matched parameterized value class") { def doAThing(param: ParamWrapper[String]) = Future(None) - val fut = AsyncAwaitTest.async(coroutine { () => + val fut = async { Option(new ParamWrapper("value!")) match { case Some(valueHolder) => - AsyncAwaitTest.await(Future(doAThing(valueHolder))) + await(Future(doAThing(valueHolder))) case None => None } - }) + } val result = Await.result(fut, 5 seconds) assert(result.asInstanceOf[Future[ParamWrapper[String]]].value == @@ -252,14 +317,14 @@ class AsyncAwaitTest extends FunSuite with Matchers { test("ticket 86 in scala/async-- using private value class") { def doAThing(param: PrivateWrapper) = Future(None) - val fut = AsyncAwaitTest.async(coroutine { () => + val fut = async { Option(PrivateWrapper.Instance) match { case Some(valueHolder) => - AsyncAwaitTest.await(doAThing(valueHolder)) + await(doAThing(valueHolder)) case None => None } - }) + } val result = Await.result(fut, 5 seconds) assert(result == None) @@ -270,9 +335,9 @@ class AsyncAwaitTest extends FunSuite with Matchers { def combine[A](a1: A, a2: A): A = a1 def combineAsync[A](a1: Future[A], a2: Future[A]) = - async(coroutine { () => + async { combine(await(Future(a1)), await(Future(a2))) - }) + } val fut = combineAsync(Future(1), Future(2)) @@ -283,19 +348,19 @@ class AsyncAwaitTest extends FunSuite with Matchers { // Source: https://git.io/vrFp5 test("match as expression 1") { - val c = AsyncAwaitTest.async(coroutine { () => + val c = async { val x = "" match { - case _ => AsyncAwaitTest.await(Future(1)) + 1 + case _ => await(Future(1)) + 1 } x - }) + } val result = Await.result(c, 5 seconds) assert(result == 2) } // Source: https://git.io/vrFhh test("match as expression 2") { - val c = AsyncAwaitTest.async(coroutine { () => + val c = async { val x = "" match { case "" if false => await(Future(1)) + 1 case _ => 2 + await(Future(1)) @@ -304,26 +369,26 @@ class AsyncAwaitTest extends FunSuite with Matchers { "" match { case _ => await(Future(y)) + 100 } - }) + } val result = Await.result(c, 5 seconds) assert(result == 103) } // Source: https://git.io/vrFj3 test("nested await as bare expression") { - val c = async(coroutine { () => + val c = async { await(Future(await(Future("")).isEmpty)) - }) + } val result = Await.result(c, 5 seconds) assert(result == true) } // Source: https://git.io/vrAnM test("nested await in block") { - val c = async(coroutine { () => + val c = async { () await(Future(await(Future("")).isEmpty)) - }) + } val result = Await.result(c, 5 seconds) assert(result == true) } @@ -336,14 +401,14 @@ class AsyncAwaitTest extends FunSuite with Matchers { i } def foo(a: Int = next(), b: Int = next()) = (a, b) - val c1 = async(coroutine { () => + val c1 = async { foo(b = await(Future(next()))) - }) + } assert(Await.result(c1, 5 seconds) == (2, 1)) i = 0 - val c2 = async(coroutine { () => + val c2 = async { foo(a = await(Future(next()))) - }) + } assert(Await.result(c2, 5 seconds) == (1, 2)) } @@ -352,9 +417,9 @@ class AsyncAwaitTest extends FunSuite with Matchers { var i = 0 def foo(a: Int, b: Int*) = b.toList def id(i: Int) = i - val c = async(coroutine { () => + val c = async { foo(await(Future(0)), id(1), id(2), id(3), await(Future(4))) - }) + } assert(Await.result(c, 5 seconds) == List(1, 2, 3, 4)) } @@ -363,27 +428,27 @@ class AsyncAwaitTest extends FunSuite with Matchers { var i = 0 def foo(a: Int, b: Int*) = b.toList def id(i: Int) = i - val c = async(coroutine { () => + val c = async { foo(await(Future(0)), List(id(1), id(2), id(3)): _*) - }) + } assert(Await.result(c, 5 seconds) == List(1, 2, 3)) } // Source: https://git.io/vrhT0 test("await in typed") { - val c = async(coroutine { () => + val c = async { (("msg: " + await(Future(0))): String).toString - }) + } assert(Await.result(c, 5 seconds) == "msg: 0") } // Source: https://git.io/vrhTz test("await in assign") { - val c = async(coroutine { () => + val c = async { var x = 0 x = await(Future(1)) x - }) + } assert(Await.result(c, 5 seconds) == 1) } @@ -391,12 +456,12 @@ class AsyncAwaitTest extends FunSuite with Matchers { test("case body must be typed as unit") { val Up = 1 val Down = 2 - val sign = async(coroutine { () => + val sign = async { await(Future(1)) match { case Up => 1.0 case Down => -1.0 } - }) + } assert(Await.result(sign, 5 seconds) == 1.0) } } From 5bf908c758cfa5c909854d27b5b791f070712e78 Mon Sep 17 00:00:00 2001 From: Jess smith Date: Fri, 8 Jul 2016 15:20:15 -0400 Subject: [PATCH 10/20] Disallow yields inside Async blocks --- .../org/coroutines/extra/AsyncAwait.scala | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala index 075a8bf..47a8b1c 100644 --- a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala +++ b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala @@ -83,6 +83,40 @@ object AsyncAwait { def asyncMacro[Y, R](c: Context)(body: c.Tree): c.Tree = { import c.universe._ + // This class shares functionality with `NestedContextValidator`. It ensures that + // no values are yielded inside the async block. + class NoYieldsValidator(implicit typer: common.ByTreeTyper[c.type]) + extends Traverser { + // return type is the lub of the function return type and yield argument types + def isCoroutinesPkg(q: Tree) = q match { + case q"org.coroutines.`package`" => true + case q"coroutines.this.`package`" => true + case t => false + } + + override def traverse(tree: Tree): Unit = tree match { + case q"$qual.yieldval[$_]($_)" if isCoroutinesPkg(qual) => + c.abort( + tree.pos, + "The yieldval statement only be invoked directly inside the coroutine. " + + "Nested classes, functions or for-comprehensions, should either use the " + + "call statement or declare another coroutine.") + case q"$qual.yieldto[$_]($_)" if isCoroutinesPkg(qual) => + c.abort( + tree.pos, + "The yieldto statement only be invoked directly inside the coroutine. " + + "Nested classes, functions or for-comprehensions, should either use the " + + "call statement or declare another coroutine.") + case q"$qual.call($co.apply(..$args))" if isCoroutinesPkg(qual) => + // no need to check further, the call macro will validate the coroutine type + case _ => + super.traverse(tree) + } + } + + implicit val typer = new common.ByTreeTyper[c.type](c)(body) + new NoYieldsValidator().traverse(body) + q""" val c = coroutine { () => $body From 4ef308a80523cbcf0bceb421d195583471c82ab1 Mon Sep 17 00:00:00 2001 From: Jess smith Date: Fri, 8 Jul 2016 15:26:11 -0400 Subject: [PATCH 11/20] Update documentation inside Coroutine.scala --- src/main/scala/org/coroutines/Coroutine.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/scala/org/coroutines/Coroutine.scala b/src/main/scala/org/coroutines/Coroutine.scala index 4397abb..a090f7d 100644 --- a/src/main/scala/org/coroutines/Coroutine.scala +++ b/src/main/scala/org/coroutines/Coroutine.scala @@ -187,8 +187,10 @@ object Coroutine { */ final def getValue: Option[Y] = if (hasValue) Some(value) else None - /** Returns a `Try` instance wrapping either the current value of this coroutine - * or any exceptions thrown when trying to get the value. + /** Returns a `Try` instance wrapping this coroutine's value, if any. + * + * The `Try` wraps either the current value of this coroutine or any exceptions + * thrown when trying to get the value. * * @return `Success(value)` if `value` does not throw an exception, or * a `Failure` instance if it does. @@ -273,8 +275,7 @@ object Coroutine { */ override def toString = s"Coroutine.Instance" - /** Returns a string containing information about the internal state of the - * coroutine. + /** Returns a string that describes the internal state of the coroutine. * * Contains more information than `toString`. * From 4ce1fbd525f6125c01647d3191cb2d8a324ec0f2 Mon Sep 17 00:00:00 2001 From: Jess smith Date: Fri, 8 Jul 2016 15:34:00 -0400 Subject: [PATCH 12/20] Optimize Async implementation If the future completed, we don't have to create a callback via `onComplete`. Instead, we can directly invoke `loop`. --- .../scala/org/coroutines/extra/AsyncAwait.scala | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala index 47a8b1c..b96d47d 100644 --- a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala +++ b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala @@ -53,11 +53,15 @@ object AsyncAwait { } } else { val awaitedFuture = c.value - awaitedFuture onComplete { - case Success(result) => - loop() - case Failure(exception) => - p.failure(exception) + if (awaitedFuture.isCompleted) { + loop() + } else { + awaitedFuture onComplete { + case Success(result) => + loop() + case Failure(exception) => + p.failure(exception) + } } } } From ffd73e1af5785184eafc9f25fd95d49a83f136cf Mon Sep 17 00:00:00 2001 From: Jess smith Date: Fri, 8 Jul 2016 15:50:52 -0400 Subject: [PATCH 13/20] Add error-handling semantics to optimized Async --- .../scala/org/coroutines/extra/AsyncAwait.scala | 9 ++++++++- .../org/coroutines/extra/async-await-tests.scala | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala index b96d47d..3d0c65f 100644 --- a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala +++ b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala @@ -54,7 +54,14 @@ object AsyncAwait { } else { val awaitedFuture = c.value if (awaitedFuture.isCompleted) { - loop() + awaitedFuture.value match { + case Some(Success(result)) => + loop() + case Some(Failure(exception)) => + p.failure(exception) + case None => + sys.error("Awaited future completed but had no value") + } } else { awaitedFuture onComplete { case Success(result) => diff --git a/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala b/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala index 2e2a1fc..4d88fd6 100644 --- a/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala +++ b/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala @@ -105,6 +105,20 @@ class AsyncAwaitTest extends FunSuite with Matchers { assert(exception.getMessage == errorMessage) } + test("error handling test 2") { + val errorMessage = "Internal await error" + val exception = intercept[RuntimeException] { + val future = async { + await(Future { + sys.error(errorMessage) + "Here ya go" + }) + } + val result = Await.result(future, 1 seconds) + } + assert(exception.getMessage == errorMessage) + } + /** Source: https://git.io/vowde * Without the closing `()`, the compiler complains about expecting return * type `Future[Unit]` but finding `Future[Nothing]`. From 95876fdb4838d398426d5f478e8dfd2d12aa2143 Mon Sep 17 00:00:00 2001 From: Jess Smith Date: Sat, 9 Jul 2016 13:49:02 -0400 Subject: [PATCH 14/20] Clean up doc comments in AsyncAwait.scala --- .../org/coroutines/extra/AsyncAwait.scala | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala index 3d0c65f..9094dc1 100644 --- a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala +++ b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala @@ -37,10 +37,10 @@ object AsyncAwait { /** Calls `body`, blocking on any calls to `await`. * - * @param body A coroutine to be invoked. - * @return A `Future` wrapping the result of the coroutine. The future fails - * if `body.hasException` or if one of `body`'s yielded tuples - * has a failing future. + * @param body A coroutine to be invoked. + * @return A `Future` wrapping the result of the coroutine. The future fails + * if `body` throws an exception or one of the `await`s takes a failed + * future. */ def asyncCall[Y, R](body: ~~~>[Future[Y], R]): Future[R] = { val c = call(body()) @@ -78,8 +78,8 @@ object AsyncAwait { /** Wraps `body` inside a coroutine and asynchronously invokes it using `asyncMacro`. * - * @param body The block of code to wrap inside an asynchronous coroutine. - * @return A `Future` wrapping the result of `body`. + * @param body The block of code to wrap inside an asynchronous coroutine. + * @return A `Future` wrapping the result of `body`. */ def async[Y, R](body: =>R): Future[R] = macro asyncMacro[Y, R] @@ -87,9 +87,9 @@ object AsyncAwait { * * Wraps `body` inside a coroutine and calls `asyncCall`. * - * @param body The function to be wrapped in a coroutine. - * @return A tree that contains an invocation of `asyncCall` on a coroutine - * with `body` as its body. + * @param body The function to be wrapped in a coroutine. + * @return A tree that contains an invocation of `asyncCall` on a coroutine + * with `body` as its body. */ def asyncMacro[Y, R](c: Context)(body: c.Tree): c.Tree = { import c.universe._ From fd8712c9f2810be784b981af04c4ed89d01ba85d Mon Sep 17 00:00:00 2001 From: Jess Smith Date: Sun, 10 Jul 2016 14:06:40 -0400 Subject: [PATCH 15/20] Add and stylize documentation --- .../main/scala/org/coroutines/extra/AsyncAwait.scala | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala index 9094dc1..8529fc2 100644 --- a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala +++ b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala @@ -94,8 +94,14 @@ object AsyncAwait { def asyncMacro[Y, R](c: Context)(body: c.Tree): c.Tree = { import c.universe._ - // This class shares functionality with `NestedContextValidator`. It ensures that - // no values are yielded inside the async block. + /** This class ensrues that no values are yielded inside the async block. + * + * It is similar to and shares functionality with + * [[org.coroutines.AstCanonicalization.NestedContextValidator]]. + * + * @param typer Holds the typings for the body of the coroutine. Can be generated + * using `org.coroutines.common.ByTreeTyper`. + */ class NoYieldsValidator(implicit typer: common.ByTreeTyper[c.type]) extends Traverser { // return type is the lub of the function return type and yield argument types From be959c9495e70c9146f80630f03e726413d96903 Mon Sep 17 00:00:00 2001 From: Jess Smith Date: Sun, 10 Jul 2016 14:27:39 -0400 Subject: [PATCH 16/20] Stylize more documentation. Note that this change was meant to be included in the previous commit, fd8712c9f2810be784b981af04c4ed89d01ba85d. --- src/main/scala/org/coroutines/Coroutine.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/scala/org/coroutines/Coroutine.scala b/src/main/scala/org/coroutines/Coroutine.scala index a090f7d..15d7a68 100644 --- a/src/main/scala/org/coroutines/Coroutine.scala +++ b/src/main/scala/org/coroutines/Coroutine.scala @@ -224,8 +224,7 @@ object Coroutine { */ final def hasResult: Boolean = isCompleted && $exception == null - /** Returns an `Option` instance wrapping this coroutine's non-exception result, if - * any. + /** Returns an `Option` wrapping this coroutine's non-exception result, if any. * * @return `Some(result)` if `hasResult`, `None` otherwise. */ From 9d2793f9fb3ac8bf49ca20fc02d61d7cb7cce50e Mon Sep 17 00:00:00 2001 From: Jess Smith Date: Sun, 10 Jul 2016 17:38:00 -0400 Subject: [PATCH 17/20] Remove NoYieldsValidator --- .../org/coroutines/extra/AsyncAwait.scala | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala index 8529fc2..0d3a061 100644 --- a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala +++ b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala @@ -94,46 +94,6 @@ object AsyncAwait { def asyncMacro[Y, R](c: Context)(body: c.Tree): c.Tree = { import c.universe._ - /** This class ensrues that no values are yielded inside the async block. - * - * It is similar to and shares functionality with - * [[org.coroutines.AstCanonicalization.NestedContextValidator]]. - * - * @param typer Holds the typings for the body of the coroutine. Can be generated - * using `org.coroutines.common.ByTreeTyper`. - */ - class NoYieldsValidator(implicit typer: common.ByTreeTyper[c.type]) - extends Traverser { - // return type is the lub of the function return type and yield argument types - def isCoroutinesPkg(q: Tree) = q match { - case q"org.coroutines.`package`" => true - case q"coroutines.this.`package`" => true - case t => false - } - - override def traverse(tree: Tree): Unit = tree match { - case q"$qual.yieldval[$_]($_)" if isCoroutinesPkg(qual) => - c.abort( - tree.pos, - "The yieldval statement only be invoked directly inside the coroutine. " + - "Nested classes, functions or for-comprehensions, should either use the " + - "call statement or declare another coroutine.") - case q"$qual.yieldto[$_]($_)" if isCoroutinesPkg(qual) => - c.abort( - tree.pos, - "The yieldto statement only be invoked directly inside the coroutine. " + - "Nested classes, functions or for-comprehensions, should either use the " + - "call statement or declare another coroutine.") - case q"$qual.call($co.apply(..$args))" if isCoroutinesPkg(qual) => - // no need to check further, the call macro will validate the coroutine type - case _ => - super.traverse(tree) - } - } - - implicit val typer = new common.ByTreeTyper[c.type](c)(body) - new NoYieldsValidator().traverse(body) - q""" val c = coroutine { () => $body From 7fa77135d81c355bbee0121ada494bda88babb0f Mon Sep 17 00:00:00 2001 From: Jess Smith Date: Sun, 10 Jul 2016 18:10:13 -0400 Subject: [PATCH 18/20] Separate async/await feature and stability tests --- .../coroutines/extra/async-await-tests.scala | 335 --------------- .../org/coroutines/async-await-tests.scala | 402 ++++++++++++++++++ 2 files changed, 402 insertions(+), 335 deletions(-) create mode 100644 src/test/scala/org/coroutines/async-await-tests.scala diff --git a/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala b/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala index 4d88fd6..dd70bca 100644 --- a/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala +++ b/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala @@ -13,37 +13,6 @@ import scala.util.Success -object AsyncAwaitTest { - import AsyncAwait._ - - object ToughTypeObject { - class Inner - - def m2 = async { - val y = await { Future[List[_]] { Nil } } - val z = await { Future[Inner] { new Inner } } - (y, z) - } - } -} - - -class IntWrapper(val value: String) extends AnyVal { - def plusStr = Future.successful(value + "!") -} - - -class ParamWrapper[T](val value: T) extends AnyVal - - -class PrivateWrapper private (private val value: String) extends AnyVal - - -object PrivateWrapper { - def Instance = new PrivateWrapper("") -} - - class TestException extends Throwable @@ -174,308 +143,4 @@ class AsyncAwaitTest extends FunSuite with Matchers { Await.result(future, 1 seconds) } } - - // Source: https://git.io/vrHtj - test("propagates tough types") { - val fut = org.coroutines.extra.AsyncAwaitTest.ToughTypeObject.m2 - val result: (List[_], org.coroutines.extra.AsyncAwaitTest.ToughTypeObject.Inner) = - Await.result(fut, 2 seconds) - assert(result._1 == Nil) - } - - // Source: https://git.io/vr7H9 - test("pattern matching function") { - val c = async { - await(Future(1)) - val a = await(Future(1)) - val f = { case x => x + a }: Function[Int, Int] - await(Future(f(2))) - } - val res = Await.result(c, 2 seconds) - assert(res == 3) - } - - // Source: https://git.io/vr7HA - test("existential bind 1") { - def m(a: Any) = async { - a match { - case s: Seq[_] => - val x = s.size - var ss = s - ss = s - await(Future(x)) - } - } - val res = Await.result(m(Nil), 2 seconds) - assert(res == 0) - } - - // Source: https://git.io/vr7Qm - test("existential bind 2") { - def conjure[T]: T = null.asInstanceOf[T] - - def m1 = async { - val p: List[Option[_]] = conjure[List[Option[_]]] - await(Future(1)) - } - - def m2 = async { - await(Future[List[_]](Nil)) - } - } - - // Source: https://git.io/vr7Fx - test("existential if/else") { - trait Container[+A] - case class ContainerImpl[A](value: A) extends Container[A] - def foo: Future[Container[_]] = async { - val a: Any = List(1) - if (true) { - val buf: Seq[_] = List(1) - val foo = await(Future(5)) - val e0 = buf(0) - ContainerImpl(e0) - } else ??? - } - foo - } - - // Source: https://git.io/vr7ba - test("ticket 63 in scala/async") { - object SomeExecutionContext extends ExecutionContext { - def reportFailure(t: Throwable): Unit = ??? - def execute(runnable: Runnable): Unit = ??? - } - - trait FunDep[W, S, R] { - def method(w: W, s: S): Future[R] - } - - object FunDep { - implicit def `Something to do with List`[W, S, R] - (implicit funDep: FunDep[W, S, R]) = - new FunDep[W, List[S], W] { - def method(w: W, l: List[S]) = async { - val it = l.iterator - while (it.hasNext) { - await(Future(funDep.method(w, it.next())) - (SomeExecutionContext)) - } - w - } - } - } - } - - // Source: https://git.io/vr7bX - test("ticket 66 in scala/async") { - val e = new Exception() - val f: Future[Nothing] = Future.failed(e) - val f1 = async { - await(Future(f)) - } - try { - Await.result(f1, 5.seconds) - } catch { - case `e` => - } - } - - // Source: https://git.io/vr7Nf - test("ticket 83 in scala/async-- using value class") { - val f = async { - val uid = new IntWrapper("foo") - await(Future(Future(uid))) - } - val outer = Await.result(f, 5.seconds) - val inner = Await.result(outer, 5 seconds) - assert(inner == new IntWrapper("foo")) - } - - // Source: https://git.io/vr7Nk - test("ticket 86 in scala/async-- using matched value class") { - def doAThing(param: IntWrapper) = Future(None) - - val fut = async { - Option(new IntWrapper("value!")) match { - case Some(valueHolder) => - await(Future(doAThing(valueHolder))) - case None => - None - } - } - - val result = Await.result(fut, 5 seconds) - assert(result.asInstanceOf[Future[IntWrapper]].value == Some(Success(None))) - } - - // Source: https://git.io/vr7NZ - test("ticket 86 in scala/async-- using matched parameterized value class") { - def doAThing(param: ParamWrapper[String]) = Future(None) - - val fut = async { - Option(new ParamWrapper("value!")) match { - case Some(valueHolder) => - await(Future(doAThing(valueHolder))) - case None => - None - } - } - - val result = Await.result(fut, 5 seconds) - assert(result.asInstanceOf[Future[ParamWrapper[String]]].value == - Some(Success(None))) - } - - // Source: https://git.io/vr7NW - test("ticket 86 in scala/async-- using private value class") { - def doAThing(param: PrivateWrapper) = Future(None) - - val fut = async { - Option(PrivateWrapper.Instance) match { - case Some(valueHolder) => - await(doAThing(valueHolder)) - case None => - None - } - } - - val result = Await.result(fut, 5 seconds) - assert(result == None) - } - - // Source: https://git.io/vr7N8 - test("await of abstract type") { - def combine[A](a1: A, a2: A): A = a1 - - def combineAsync[A](a1: Future[A], a2: Future[A]) = - async { - combine(await(Future(a1)), await(Future(a2))) - } - - val fut = combineAsync(Future(1), Future(2)) - - val outer = Await.result(fut, 5 seconds) - val inner = Await.result(outer, 5 seconds) - assert(inner == 1) - } - - // Source: https://git.io/vrFp5 - test("match as expression 1") { - val c = async { - val x = "" match { - case _ => await(Future(1)) + 1 - } - x - } - val result = Await.result(c, 5 seconds) - assert(result == 2) - } - - // Source: https://git.io/vrFhh - test("match as expression 2") { - val c = async { - val x = "" match { - case "" if false => await(Future(1)) + 1 - case _ => 2 + await(Future(1)) - } - val y = x - "" match { - case _ => await(Future(y)) + 100 - } - } - val result = Await.result(c, 5 seconds) - assert(result == 103) - } - - // Source: https://git.io/vrFj3 - test("nested await as bare expression") { - val c = async { - await(Future(await(Future("")).isEmpty)) - } - val result = Await.result(c, 5 seconds) - assert(result == true) - } - - // Source: https://git.io/vrAnM - test("nested await in block") { - val c = async { - () - await(Future(await(Future("")).isEmpty)) - } - val result = Await.result(c, 5 seconds) - assert(result == true) - } - - // Source: https://git.io/vrhTe - test("named and default arguments respect evaluation order") { - var i = 0 - def next() = { - i += 1; - i - } - def foo(a: Int = next(), b: Int = next()) = (a, b) - val c1 = async { - foo(b = await(Future(next()))) - } - assert(Await.result(c1, 5 seconds) == (2, 1)) - i = 0 - val c2 = async { - foo(a = await(Future(next()))) - } - assert(Await.result(c2, 5 seconds) == (1, 2)) - } - - // Source: https://git.io/vrhTT - test("repeated params 1") { - var i = 0 - def foo(a: Int, b: Int*) = b.toList - def id(i: Int) = i - val c = async { - foo(await(Future(0)), id(1), id(2), id(3), await(Future(4))) - } - assert(Await.result(c, 5 seconds) == List(1, 2, 3, 4)) - } - - // Source: https://git.io/vrhTY - test("repeated params 2") { - var i = 0 - def foo(a: Int, b: Int*) = b.toList - def id(i: Int) = i - val c = async { - foo(await(Future(0)), List(id(1), id(2), id(3)): _*) - } - assert(Await.result(c, 5 seconds) == List(1, 2, 3)) - } - - // Source: https://git.io/vrhT0 - test("await in typed") { - val c = async { - (("msg: " + await(Future(0))): String).toString - } - assert(Await.result(c, 5 seconds) == "msg: 0") - } - - // Source: https://git.io/vrhTz - test("await in assign") { - val c = async { - var x = 0 - x = await(Future(1)) - x - } - assert(Await.result(c, 5 seconds) == 1) - } - - // Source: https://git.io/vrhTr - test("case body must be typed as unit") { - val Up = 1 - val Down = 2 - val sign = async { - await(Future(1)) match { - case Up => 1.0 - case Down => -1.0 - } - } - assert(Await.result(sign, 5 seconds) == 1.0) - } } diff --git a/src/test/scala/org/coroutines/async-await-tests.scala b/src/test/scala/org/coroutines/async-await-tests.scala new file mode 100644 index 0000000..65b7114 --- /dev/null +++ b/src/test/scala/org/coroutines/async-await-tests.scala @@ -0,0 +1,402 @@ +package org.coroutines + + + +import org.scalatest._ +import scala.annotation.unchecked.uncheckedVariance +import scala.concurrent._ +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.language.{ reflectiveCalls, postfixOps } +import scala.util.Success + + + +object AsyncAwaitTest { + class Cell[+T] { + var x: T @uncheckedVariance = _ + } + + object ToughTypeObject { + class Inner + + def m2 = async(coroutine { () => + val y = await { Future[List[_]] { Nil } } + val z = await { Future[Inner] { new Inner } } + (y, z) + }) + } + + // Doubly defined for ToughTypeObject + def await[R]: Future[R] ~~> ((Future[R], Cell[R]), R) = + coroutine { (f: Future[R]) => + val cell = new Cell[R] + yieldval((f, cell)) + cell.x + } + + // Doubly defined for ToughTypeObject + def async[Y, R](body: ~~~>[(Future[Y], Cell[Y]), R]): Future[R] = { + val c = call(body()) + val p = Promise[R] + def loop() { + if (!c.resume) p.success(c.result) + else { + val (future, cell) = c.value + for (x <- future) { + cell.x = x + loop() + } + } + } + Future { loop() } + p.future + } +} + + +class IntWrapper(val value: String) extends AnyVal { + def plusStr = Future.successful(value + "!") +} + + +class ParamWrapper[T](val value: T) extends AnyVal + + +class PrivateWrapper private (private val value: String) extends AnyVal + + +object PrivateWrapper { + def Instance = new PrivateWrapper("") +} + + +class AsyncAwaitTest extends FunSuite with Matchers { + def await[R]: Future[R] ~~> ((Future[R], AsyncAwaitTest.Cell[R]), R) = + coroutine { (f: Future[R]) => + val cell = new AsyncAwaitTest.Cell[R] + yieldval((f, cell)) + cell.x + } + + def async[Y, R](body: ~~~>[(Future[Y], AsyncAwaitTest.Cell[Y]), R]): Future[R] = { + val c = call(body()) + val p = Promise[R] + def loop() { + if (!c.resume) p.success(c.result) + else { + val (future, cell) = c.value + for (x <- future) { + cell.x = x + loop() + } + } + } + Future { loop() } + p.future + } + + // Source: https://git.io/vrHtj + test("propagates tough types") { + val fut = org.coroutines.AsyncAwaitTest.ToughTypeObject.m2 + val result: (List[_], org.coroutines.AsyncAwaitTest.ToughTypeObject.Inner) = + Await.result(fut, 2 seconds) + assert(result._1 == Nil) + } + + // Source: https://git.io/vr7H9 + test("pattern matching function") { + val c = async(coroutine { () => + await(Future(1)) + val a = await(Future(1)) + val f = { case x => x + a }: Function[Int, Int] + await(Future(f(2))) + }) + val res = Await.result(c, 2 seconds) + assert(res == 3) + } + + // Source: https://git.io/vr7HA + test("existential bind 1") { + def m(a: Any) = async(coroutine { () => + a match { + case s: Seq[_] => + val x = s.size + var ss = s + ss = s + await(Future(x)) + } + }) + val res = Await.result(m(Nil), 2 seconds) + assert(res == 0) + } + + // Source: https://git.io/vr7Qm + test("existential bind 2") { + def conjure[T]: T = null.asInstanceOf[T] + + def m1 = AsyncAwaitTest.async(coroutine { () => + val p: List[Option[_]] = conjure[List[Option[_]]] + AsyncAwaitTest.await(Future(1)) + }) + + def m2 = AsyncAwaitTest.async(coroutine { () => + AsyncAwaitTest.await(Future[List[_]](Nil)) + }) + } + + // Source: https://git.io/vr7Fx + test("existential if/else") { + trait Container[+A] + case class ContainerImpl[A](value: A) extends Container[A] + def foo: Future[Container[_]] = AsyncAwaitTest.async(coroutine { () => + val a: Any = List(1) + if (true) { + val buf: Seq[_] = List(1) + val foo = AsyncAwaitTest.await(Future(5)) + val e0 = buf(0) + ContainerImpl(e0) + } else ??? + }) + foo + } + + // Source: https://git.io/vr7ba + test("ticket 63 in scala/async") { + object SomeExecutionContext extends ExecutionContext { + def reportFailure(t: Throwable): Unit = ??? + def execute(runnable: Runnable): Unit = ??? + } + + trait FunDep[W, S, R] { + def method(w: W, s: S): Future[R] + } + + object FunDep { + implicit def `Something to do with List`[W, S, R] + (implicit funDep: FunDep[W, S, R]) = + new FunDep[W, List[S], W] { + def method(w: W, l: List[S]) = AsyncAwaitTest.async(coroutine { () => + val it = l.iterator + while (it.hasNext) { + AsyncAwaitTest.await(Future(funDep.method(w, it.next())) + (SomeExecutionContext)) + } + w + }) + } + } + } + + // Source: https://git.io/vr7bX + test("ticket 66 in scala/async") { + val e = new Exception() + val f: Future[Nothing] = Future.failed(e) + val f1 = AsyncAwaitTest.async(coroutine { () => + AsyncAwaitTest.await(Future(f)) + }) + try { + Await.result(f1, 5.seconds) + } catch { + case `e` => + } + } + + // Source: https://git.io/vr7Nf + test("ticket 83 in scala/async-- using value class") { + val f = AsyncAwaitTest.async(coroutine { () => + val uid = new IntWrapper("foo") + AsyncAwaitTest.await(Future(Future(uid))) + }) + val outer = Await.result(f, 5.seconds) + val inner = Await.result(outer, 5 seconds) + assert(inner == new IntWrapper("foo")) + } + + // Source: https://git.io/vr7Nk + test("ticket 86 in scala/async-- using matched value class") { + def doAThing(param: IntWrapper) = Future(None) + + val fut = AsyncAwaitTest.async(coroutine { () => + Option(new IntWrapper("value!")) match { + case Some(valueHolder) => + AsyncAwaitTest.await(Future(doAThing(valueHolder))) + case None => + None + } + }) + + val result = Await.result(fut, 5 seconds) + assert(result.asInstanceOf[Future[IntWrapper]].value == Some(Success(None))) + } + + // Source: https://git.io/vr7NZ + test("ticket 86 in scala/async-- using matched parameterized value class") { + def doAThing(param: ParamWrapper[String]) = Future(None) + + val fut = AsyncAwaitTest.async(coroutine { () => + Option(new ParamWrapper("value!")) match { + case Some(valueHolder) => + AsyncAwaitTest.await(Future(doAThing(valueHolder))) + case None => + None + } + }) + + val result = Await.result(fut, 5 seconds) + assert(result.asInstanceOf[Future[ParamWrapper[String]]].value == + Some(Success(None))) + } + + // Source: https://git.io/vr7NW + test("ticket 86 in scala/async-- using private value class") { + def doAThing(param: PrivateWrapper) = Future(None) + + val fut = AsyncAwaitTest.async(coroutine { () => + Option(PrivateWrapper.Instance) match { + case Some(valueHolder) => + AsyncAwaitTest.await(doAThing(valueHolder)) + case None => + None + } + }) + + val result = Await.result(fut, 5 seconds) + assert(result == None) + } + + // Source: https://git.io/vr7N8 + test("await of abstract type") { + def combine[A](a1: A, a2: A): A = a1 + + def combineAsync[A](a1: Future[A], a2: Future[A]) = + async(coroutine { () => + combine(await(Future(a1)), await(Future(a2))) + }) + + val fut = combineAsync(Future(1), Future(2)) + + val outer = Await.result(fut, 5 seconds) + val inner = Await.result(outer, 5 seconds) + assert(inner == 1) + } + + // Source: https://git.io/vrFp5 + test("match as expression 1") { + val c = AsyncAwaitTest.async(coroutine { () => + val x = "" match { + case _ => AsyncAwaitTest.await(Future(1)) + 1 + } + x + }) + val result = Await.result(c, 5 seconds) + assert(result == 2) + } + + // Source: https://git.io/vrFhh + test("match as expression 2") { + val c = AsyncAwaitTest.async(coroutine { () => + val x = "" match { + case "" if false => await(Future(1)) + 1 + case _ => 2 + await(Future(1)) + } + val y = x + "" match { + case _ => await(Future(y)) + 100 + } + }) + val result = Await.result(c, 5 seconds) + assert(result == 103) + } + + // Source: https://git.io/vrFj3 + test("nested await as bare expression") { + val c = async(coroutine { () => + await(Future(await(Future("")).isEmpty)) + }) + val result = Await.result(c, 5 seconds) + assert(result == true) + } + + // Source: https://git.io/vrAnM + test("nested await in block") { + val c = async(coroutine { () => + () + await(Future(await(Future("")).isEmpty)) + }) + val result = Await.result(c, 5 seconds) + assert(result == true) + } + + // Source: https://git.io/vrhTe + test("named and default arguments respect evaluation order") { + var i = 0 + def next() = { + i += 1; + i + } + def foo(a: Int = next(), b: Int = next()) = (a, b) + val c1 = async(coroutine { () => + foo(b = await(Future(next()))) + }) + assert(Await.result(c1, 5 seconds) == (2, 1)) + i = 0 + val c2 = async(coroutine { () => + foo(a = await(Future(next()))) + }) + assert(Await.result(c2, 5 seconds) == (1, 2)) + } + + // Source: https://git.io/vrhTT + test("repeated params 1") { + var i = 0 + def foo(a: Int, b: Int*) = b.toList + def id(i: Int) = i + val c = async(coroutine { () => + foo(await(Future(0)), id(1), id(2), id(3), await(Future(4))) + }) + assert(Await.result(c, 5 seconds) == List(1, 2, 3, 4)) + } + + // Source: https://git.io/vrhTY + test("repeated params 2") { + var i = 0 + def foo(a: Int, b: Int*) = b.toList + def id(i: Int) = i + val c = async(coroutine { () => + foo(await(Future(0)), List(id(1), id(2), id(3)): _*) + }) + assert(Await.result(c, 5 seconds) == List(1, 2, 3)) + } + + // Source: https://git.io/vrhT0 + test("await in typed") { + val c = async(coroutine { () => + (("msg: " + await(Future(0))): String).toString + }) + assert(Await.result(c, 5 seconds) == "msg: 0") + } + + // Source: https://git.io/vrhTz + test("await in assign") { + val c = async(coroutine { () => + var x = 0 + x = await(Future(1)) + x + }) + assert(Await.result(c, 5 seconds) == 1) + } + + // Source: https://git.io/vrhTr + test("case body must be typed as unit") { + val Up = 1 + val Down = 2 + val sign = async(coroutine { () => + await(Future(1)) match { + case Up => 1.0 + case Down => -1.0 + } + }) + assert(Await.result(sign, 5 seconds) == 1.0) + } +} \ No newline at end of file From 06e8224d9b9afc16f00f4eee1263fb5a34b8758c Mon Sep 17 00:00:00 2001 From: Jess Smith Date: Tue, 26 Jul 2016 01:55:39 -0400 Subject: [PATCH 19/20] Add new line at end of file --- src/test/scala/org/coroutines/async-await-tests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/org/coroutines/async-await-tests.scala b/src/test/scala/org/coroutines/async-await-tests.scala index 65b7114..b2fb020 100644 --- a/src/test/scala/org/coroutines/async-await-tests.scala +++ b/src/test/scala/org/coroutines/async-await-tests.scala @@ -399,4 +399,4 @@ class AsyncAwaitTest extends FunSuite with Matchers { }) assert(Await.result(sign, 5 seconds) == 1.0) } -} \ No newline at end of file +} From 2b50dd3364fd656fa6d16be4c91766252ebc895d Mon Sep 17 00:00:00 2001 From: Aleksandar Prokopec Date: Thu, 28 Jul 2016 01:36:10 +0200 Subject: [PATCH 20/20] Fix exception handling in Async/Await implementation. Problem was that the promise was prematurely failed in the `async` coroutine, instead of passing control to user code. This revealed a bug in the initial exception check. The exception check had to be pushed deep down into the deepest `try` block of the entrypoint -- without this, the exception from a direct call was just being rethrown at the beginning of the method, regardless of the error handlers in the body of the method. --- .../org/coroutines/extra/AsyncAwait.scala | 14 +----- .../coroutines/extra/async-await-tests.scala | 39 ++++++++++++++- .../scala/org/coroutines/CfgGenerator.scala | 49 ++++++++++++++----- .../org/coroutines/regression-tests.scala | 21 ++++++++ 4 files changed, 97 insertions(+), 26 deletions(-) diff --git a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala index 0d3a061..d7018e3 100644 --- a/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala +++ b/coroutines-extra/src/main/scala/org/coroutines/extra/AsyncAwait.scala @@ -54,20 +54,10 @@ object AsyncAwait { } else { val awaitedFuture = c.value if (awaitedFuture.isCompleted) { - awaitedFuture.value match { - case Some(Success(result)) => - loop() - case Some(Failure(exception)) => - p.failure(exception) - case None => - sys.error("Awaited future completed but had no value") - } + loop() } else { awaitedFuture onComplete { - case Success(result) => - loop() - case Failure(exception) => - p.failure(exception) + case _ => loop() } } } diff --git a/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala b/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala index dd70bca..ae0477d 100644 --- a/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala +++ b/coroutines-extra/src/test/scala/org/coroutines/extra/async-await-tests.scala @@ -13,7 +13,7 @@ import scala.util.Success -class TestException extends Throwable +class TestException(msg: String = "") extends Throwable(msg) class AsyncAwaitTest extends FunSuite with Matchers { @@ -143,4 +143,41 @@ class AsyncAwaitTest extends FunSuite with Matchers { Await.result(future, 1 seconds) } } + + test("await should bubble up exceptions") { + def thrower() = { + throw new TestException + Future(1) + } + + var exceptionFound = false + val future = async { + try { + await(thrower()) + () + } catch { + case _: TestException => exceptionFound = true + } + } + val r = Await.result(future, 1 seconds) + assert(exceptionFound) + } + + test("await should bubble up exceptions from failed futures") { + def failer(): Future[Int] = { + Future.failed(new TestException("kaboom")) + } + + var exceptionFound = false + val future = async { + try { + await(failer()) + () + } catch { + case _: TestException => exceptionFound = true + } + } + val r = Await.result(future, 1 seconds) + assert(exceptionFound) + } } diff --git a/src/main/scala/org/coroutines/CfgGenerator.scala b/src/main/scala/org/coroutines/CfgGenerator.scala index 618f74f..50df859 100644 --- a/src/main/scala/org/coroutines/CfgGenerator.scala +++ b/src/main/scala/org/coroutines/CfgGenerator.scala @@ -921,14 +921,20 @@ trait CfgGenerator[C <: Context] { def emit(cfg: Cfg)(implicit table: Table): Tree = { val cparam = table.names.coroutineParam - def patch(n: Node, chain: Chain): Node = { + + /** Patches the CFG with declaration nodes and code block nodes to lift the scope. + * Returns the patched node, and whether or not the exception check was added. + */ + def patch(n: Node, chain: Chain, checkthrow: Option[Tree]): (Node, Boolean) = { val head = chain.info.tryuids match { case Some((tryuid, enduid)) => cfg.allnodes(tryuid).copyWithoutSuccessors(chain) case None => Node.CodeBlock(q"()", chain, table.newNodeUid()) } - val decls = for { + + // Compute declarations + var decls: Seq[Node] = for { ((sym, info), idx) <- chain.decls.zipWithIndex if mustLoadVar(sym, chain) } yield { @@ -947,22 +953,33 @@ trait CfgGenerator[C <: Context] { table.newNodeUid()) } } + + // Check if the exception check needs to be inserted at this point. + var checked = false + if (checkthrow.nonEmpty) chain.info.tryuids match { + case Some((tryuid, enduid)) => + checked = true + val ch = Node.DefaultStatement(checkthrow.get, chain, table.newNodeUid()) + decls = ch +: decls + case None => + } + (decls.foldLeft(head: Node) { (previous, current) => previous.successor = Some(current) current }).successor = Some(n) - if (chain.parent == null) head else patch(head, chain.parent) - } - // emit body - val startZipper = Zipper(null, Nil, trees => q"..$trees") - val patchedStart = patch(start, start.chain) - val bodyzipper = patchedStart.emitCode(startZipper, this) - val body = bodyzipper.result + if (chain.parent == null) (head, checked) + else { + val ncheckthrow = if (checked) None else checkthrow + val (n, parentchecked) = patch(head, chain.parent, ncheckthrow) + (n, checked || parentchecked) + } + } - // add exception check - val checkexception = { + // Prepare exception check. + val checkthrow = { val needcheck = cfg.subgraphs.exists { case (_, sub) if sub.exitSubgraphs.exists(_._2 eq this) => sub.exitSubgraphs.find(_._2 eq this).get._1 match { @@ -986,10 +1003,16 @@ trait CfgGenerator[C <: Context] { } } - // wrap inside an exception + // Emit body. Note that the exception check must be pushed to innermost try-block. + val startZipper = Zipper(null, Nil, trees => q"..$trees") + val (patchedStart, checked) = patch(start, start.chain, Some(checkthrow)) + val bodyzipper = patchedStart.emitCode(startZipper, this) + val body = bodyzipper.result + + // Wrap inside an exception. q""" try { - $checkexception + ${if (checked) q"()" else checkthrow} $body } catch { case t: _root_.java.lang.Throwable => diff --git a/src/test/scala/org/coroutines/regression-tests.scala b/src/test/scala/org/coroutines/regression-tests.scala index 19b474d..3ac9b0e 100644 --- a/src/test/scala/org/coroutines/regression-tests.scala +++ b/src/test/scala/org/coroutines/regression-tests.scala @@ -214,4 +214,25 @@ class RegressionTest extends FunSuite with Matchers { } } } + + test("must catch exception passed from a direct call") { + val buggy = coroutine { () => + throw new Exception + } + val catchy = coroutine { () => + var result = "initial value" + try { + buggy() + "not ok..." + } catch { + case e: Exception => + result = "caught!" + } + result + } + + val c = call(catchy()) + assert(!c.resume) + assert(c.result == "caught!") + } }