New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Compiler support for scala-async, with a state machine transform phase running after erasure [ci: last-only] #8816
Compiler support for scala-async, with a state machine transform phase running after erasure [ci: last-only] #8816
Conversation
b7b826d
to
8d61292
Compare
0dc0333
to
0002b2a
Compare
Can these changes create new shapes of trees using |
@sjrd Yes, I'm using label jumps like def apply(tr: Try[AnyRef]) = {
:while1
while (true) {
state match {
case <i> =>
...
GOTO while1() // continue a new iteration of the dispatch loop
...
GOTO case12() // forward jump to a `caseN` or `matchEnd` label that may
...
case <j> =>
:case12
case _ => throw ...
}
GOTO while1()
} The The I was also experimenting with a novel use of a |
We don't compile to JavaScript, but to our IR. Our IR doesn't have while (true) {
while1[void]: {
val (newState, awaitable) = await[(int, Awaitable)]: {
state match {
case <i> =>
...
return@while1 void
...
return@await (j, future)
case _ => ...
}
return@while1 void
} // end await
state = newState
if (isComplete(awaitable))
tr = awaitable.get
else
{awaitable.onComplete(this); return}
} // end while1
} |
It seems I replied to an earlier version of your comment. For |
8ef4dd9
to
9a7f84e
Compare
7084867
to
a7d22c3
Compare
I'd like to discuss how we can make this testable in Scala.js. At the moment it seems there are essentially two kinds of tests:
Neither kind will be testable in Scala.js, which becomes problematic now that the support of Could we instead use one way to write async tests that is (or can be made) compatible with Scala.js? Off the top of my head, I imagine we could have a single abstract class AsyncTest {
def test: Future[Any]
def main(args: Array[String]): Unit {
Await.result(test, Duration.Inf)
}
} Then, all tests that need running would be written as partests that inherit from this class. In Scala.js, we would override that single class so that it doesn't use |
Some will be made Scala.js compatible in a subsequent commit and moved back to run.
- Remove '_' and line number from state machine classs names. - Rename `state$async` to `state`.
29ac66e
to
db3ea69
Compare
With a few more changes on Scala.js' side, I could make all the tests in edge-cases pass :) I noticed that, with the latest state of this PR, (almost) all generated switch'es have a last state that is empty dead code, and that would loop forever if reached. For example, the very first final class Test$stateMachine$async$1 extends scala.tools.partest.async.OptionStateMachine {
<synthetic> <stable> private[this] var await$1: Object = _;
override def apply(tr$async: Option): Unit = while$(){
try {
Test$stateMachine$async$1.this.state() match {
case 0 => {
val awaitable$async: Some = new Some(scala.Int.box(1));
tr$async = Test$stateMachine$async$1.this.getCompleted(awaitable$async);
Test$stateMachine$async$1.this.state_=(1);
if (null.!=(tr$async))
while$()
else
{
Test$stateMachine$async$1.this.onComplete(awaitable$async);
return ()
}
}
case 1 => {
{
val tryGetResult$async: Object = Test$stateMachine$async$1.this.tryGet(tr$async);
if (Test$stateMachine$async$1.this.==(tryGetResult$async))
Test$stateMachine$async$1.this.await$1 = return ()
else
Test$stateMachine$async$1.this.await$1 = tryGetResult$async.$asInstanceOf[Object]()
};
val awaitable$async: Some = new Some(scala.Int.box(2));
tr$async = Test$stateMachine$async$1.this.getCompleted(awaitable$async);
Test$stateMachine$async$1.this.state_=(2);
if (null.!=(tr$async))
while$()
else
{
Test$stateMachine$async$1.this.onComplete(awaitable$async);
return ()
}
}
case 2 => {
<synthetic> val await$2: Object = {
val tryGetResult$async: Object = Test$stateMachine$async$1.this.tryGet(tr$async);
if (Test$stateMachine$async$1.this.==(tryGetResult$async))
return ()
else
tryGetResult$async.$asInstanceOf[Object]()
};
<synthetic> val x$1: Int = scala.Int.unbox(await$2);
<synthetic> val x$2: Int = scala.Int.unbox(Test$stateMachine$async$1.this.await$1).+(x$1);
Test$stateMachine$async$1.this.completeSuccess(scala.Int.box(x$2));
return ();
Test$stateMachine$async$1.this.await$1 = null
}
case 3 => {
()
}
case _ => throw new IllegalStateException(java.lang.String.valueOf(Test$stateMachine$async$1.this.state()))
}
} catch {
case (throwable$async @ (_: Throwable)) => {
Test$stateMachine$async$1.this.completeFailure(throwable$async);
return ()
}
};
while$()
};
def <init>(): Test$stateMachine$async$1 = {
Test$stateMachine$async$1.super.<init>();
()
}
} Note the This is not wrong/harmful; it's just useless trees. |
Wrap the async expression in `locally { ... }` to force value class boxing. This seems to work better than `{ ... }: Any`, which ends up with `Function` trees typed with `Any` which violoates an assumption in `delambdafy`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few minor comments. I also pushed a commit to remove (hopefully) unused code.
cf16d74
to
ae91a3c
Compare
We can defer the check until the current location of the "fallback" check without sacrificing the specific error messages.
About the risk for regressions that this change may cause:
|
This preliminary work adds an async/await implementation based off the now built-in mechanism in the Scala 2 compiler. The grittiest details of the implementation are borrowed from : * scala/scala#8816 * https://github.com/retronym/monad-ui/tree/master/src/main/scala/monadui * https://github.com/scala/scala-async Due to the reliance on Dispatcher#unsafeRunSync, the implementation currently only works on JVM. Error propagation / cancellation seems to behave as it should. NB : it is worth noting that despite it compiling, using this with OptionT/EitherT/IorT is currently unsafe, for two reasons: * what seems to be a bug in the MonadCancel instance tied to those Error-able types: See https://gitter.im/typelevel/cats-effect-dev?at=60818cf4ae90f3684098c042 * The fact that calling `unsafeRunSync` is `F[A] => A`, which obviously doesn't work for types that have an error channel that isn't accounted for by the CE typeclasses.
scala-async
(SIP-22) is currently implemented as a macro.This pull request incorporates the bulk of the async transform into a fully blown compiler phase of the standard compiler.
scala-async
would remain as a simple macro that expands into the skeleton state machine class including adaptors forscala.concurrent.Future
.Other front end macros could also target the async phase adapting to other awaitable data types. The test cases include an integration with
java.util.concurrent.CompletableFuture
. It should be also be straightforward to integrate withmonix.Task
and the like now, unlike before.Why abandon the macro approach?
uncurry
and easier again aftererasure
. Custom integrations of async that use async pattern extactors and guards are only possible when running afterpatmat
.Integrate Async into the compiler
scala.tools.nsc.transform.async
async(expr)(execCtx)
into (approximately){val temp = execCtx; class stateMachine { def apply(tr: Try) = { expr; () } }; new stateMachine.start()}
. Wrappingexpr
in a class lets intervening compiler phases do their job (e.g. captured enclosing values give rise to outer pointer usage).async
phase proper runs after erasure. As before, it consists of a) a selective ANF transform of the expression; b) a translation of control flow that crosses async boundaries into separate states of a state machine; c) lifting locals that are referred to from multiple states into members of the state machine.c.internal_
.-Xasync
. A new version ofscala-async
(prototype) will be needed to take advantage.test/async
andjunit/test/scala/tools/nsc/transform/async
.Improve the implementation of the ANF transform
Improve the implementation of the State Machine Transform
If
orLabelDef
itself.await
in the operand ofBoolean#{||, &&}
and instead rewrite these toIf
trees.Improve performance
TODO
AsyncStateMachine
@compileTimeOnly
await methodsscala-async
UnsupportedAwaitAnalyzer
if (tree.tpe =:= definitions.NothingTpe) {
branch intransformMatchOrIf
case td@TypeDef(_, _, tparams, rhs) =>
inLifter
(maybe dead code)case Apply(fun, arg1 :: arg2 :: Nil) if isBooleanShortCircuit(fun.symbol) =>
inAsyncTraverser
NoPosition
of the preciding position.