Skip to content

Commit 490e3e3

Browse files
authored
Merge pull request scala#9179 from retronym/topic/xasync-stability
Make async compiler output deterministic
2 parents 529f089 + 8d913c3 commit 490e3e3

File tree

2 files changed

+101
-3
lines changed

2 files changed

+101
-3
lines changed

src/compiler/scala/tools/nsc/transform/async/LiveVariables.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ trait LiveVariables extends ExprBuilder {
3030
def fieldsToNullOut(asyncStates: List[AsyncState], finalState: AsyncState,
3131
liftables: List[Tree]): mutable.LinkedHashMap[Int, (mutable.LinkedHashSet[Symbol], mutable.LinkedHashSet[Symbol])] = {
3232

33-
val liftedSyms = mutable.HashSet[Symbol]()
33+
val liftedSyms = mutable.LinkedHashSet[Symbol]()
3434

3535
// include only vars
3636
liftedSyms ++= liftables.iterator.collect {
@@ -54,8 +54,8 @@ trait LiveVariables extends ExprBuilder {
5454
*/
5555
def fieldsUsedIn(as: AsyncState): (collection.Set[Symbol], collection.Set[Symbol]) = {
5656
class FindUseTraverser extends AsyncTraverser {
57-
val usedBeforeAssignment = new mutable.HashSet[Symbol]()
58-
val assignedFields = new mutable.HashSet[Symbol]()
57+
val usedBeforeAssignment = new mutable.LinkedHashSet[Symbol]()
58+
val assignedFields = new mutable.LinkedHashSet[Symbol]()
5959
private def capturing[A](body: => A): A = {
6060
val saved = capturing
6161
try {

test/junit/scala/tools/nsc/DeterminismTest.scala

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,27 @@ class DeterminismTest {
300300
test(List(code))
301301
}
302302

303+
@Test def testAsync(): Unit = {
304+
def code = List[SourceFile](
305+
source("a.scala",
306+
"""
307+
| object A {
308+
| import scala.tools.nsc.OptionAwait.{optionally, value}
309+
| def test = optionally {
310+
| if (value(Some(true))) {
311+
| var x = ""
312+
| if (value(Some(false))) {
313+
| value(Some(x)) + value(Some(2))
314+
| }
315+
| }
316+
| }
317+
| }
318+
|
319+
""".stripMargin)
320+
)
321+
test(List(code))
322+
}
323+
303324
def source(name: String, code: String): SourceFile = new BatchSourceFile(name, code)
304325
private def test(groups: List[List[SourceFile]]): Unit = {
305326
val referenceOutput = Files.createTempDirectory("reference")
@@ -309,6 +330,7 @@ class DeterminismTest {
309330
g.settings.usejavacp.value = true
310331
g.settings.classpath.value = output.toAbsolutePath.toString
311332
g.settings.outputDirs.setSingleOutput(output.toString)
333+
g.settings.async.value = true
312334
val storeReporter = new StoreReporter
313335
g.reporter = storeReporter
314336
import g._
@@ -362,3 +384,79 @@ class DeterminismTest {
362384
def permutationsWithSubsets[A](as: List[A]): List[List[A]] =
363385
as.permutations.toList.flatMap(_.inits.filter(_.nonEmpty)).distinct
364386
}
387+
388+
389+
390+
import scala.annotation.compileTimeOnly
391+
import scala.language.experimental.macros
392+
import scala.reflect.macros.blackbox
393+
394+
object OptionAwait {
395+
def optionally[T](body: T): Option[T] = macro impl
396+
@compileTimeOnly("[async] `value` must be enclosed in `optionally`")
397+
def value[T](option: Option[T]): T = ???
398+
def impl(c: blackbox.Context)(body: c.Tree): c.Tree = {
399+
import c.universe._
400+
val awaitSym = typeOf[OptionAwait.type].decl(TermName("value"))
401+
def mark(t: DefDef): Tree = c.internal.markForAsyncTransform(c.internal.enclosingOwner, t, awaitSym, Map.empty)
402+
val name = TypeName("stateMachine$async")
403+
q"""
404+
final class $name extends _root_.scala.tools.nsc.OptionStateMachine {
405+
${mark(q"""override def apply(tr$$async: _root_.scala.Option[_root_.scala.AnyRef]) = ${body}""")}
406+
}
407+
new $name().start().asInstanceOf[${c.macroApplication.tpe}]
408+
"""
409+
}
410+
}
411+
412+
trait AsyncStateMachine[F, R] {
413+
/** Assign `i` to the state variable */
414+
protected def state_=(i: Int): Unit
415+
/** Retrieve the current value of the state variable */
416+
protected def state: Int
417+
/** Complete the state machine with the given failure. */
418+
protected def completeFailure(t: Throwable): Unit
419+
/** Complete the state machine with the given value. */
420+
protected def completeSuccess(value: AnyRef): Unit
421+
/** Register the state machine as a completion callback of the given future. */
422+
protected def onComplete(f: F): Unit
423+
/** Extract the result of the given future if it is complete, or `null` if it is incomplete. */
424+
protected def getCompleted(f: F): R
425+
/**
426+
* Extract the success value of the given future. If the state machine detects a failure it may
427+
* complete the async block and return `this` as a sentinel value to indicate that the caller
428+
* (the state machine dispatch loop) should immediately exit.
429+
*/
430+
protected def tryGet(tr: R): AnyRef
431+
}
432+
433+
434+
abstract class OptionStateMachine extends AsyncStateMachine[Option[AnyRef], Option[AnyRef]] {
435+
var result$async: Option[AnyRef] = _
436+
437+
// FSM translated method
438+
def apply(tr$async: Option[AnyRef]): Unit
439+
440+
// Required methods
441+
private[this] var state$async: Int = 0
442+
protected def state: Int = state$async
443+
protected def state_=(s: Int): Unit = state$async = s
444+
protected def completeFailure(t: Throwable): Unit = throw t
445+
protected def completeSuccess(value: AnyRef): Unit = result$async = Some(value)
446+
protected def onComplete(f: Option[AnyRef]): Unit = ???
447+
protected def getCompleted(f: Option[AnyRef]): Option[AnyRef] = {
448+
f
449+
}
450+
protected def tryGet(tr: Option[AnyRef]): AnyRef = tr match {
451+
case Some(value) =>
452+
value.asInstanceOf[AnyRef]
453+
case None =>
454+
result$async = None
455+
this // sentinel value to indicate the dispatch loop should exit.
456+
}
457+
def start(): Option[AnyRef] = {
458+
apply(None)
459+
result$async
460+
}
461+
}
462+

0 commit comments

Comments
 (0)