Skip to content

Commit 7857e41

Browse files
authored
Merge pull request #203 from retronym/topic/rt
Deterministic output from the async macro
2 parents b662af6 + 2c4ac2f commit 7857e41

File tree

4 files changed

+84
-26
lines changed

4 files changed

+84
-26
lines changed

build.sbt

+46
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,49 @@ pomExtra := (
5858
</developers>
5959
)
6060
OsgiKeys.exportPackage := Seq(s"scala.async.*;version=${version.value}")
61+
62+
commands += testDeterminism
63+
64+
def testDeterminism = Command.command("testDeterminism") { state =>
65+
val extracted = Project.extract(state)
66+
println("Running test:clean")
67+
val (state1, _) = extracted.runTask(clean in Test in LocalRootProject, state)
68+
println("Running test:compile")
69+
val (state2, _) = extracted.runTask(compile in Test in LocalRootProject, state1)
70+
val testClasses = extracted.get(classDirectory in Test)
71+
val baseline: File = testClasses.getParentFile / (testClasses.getName + "-baseline")
72+
baseline.mkdirs()
73+
IO.copyDirectory(testClasses, baseline, overwrite = true)
74+
IO.delete(testClasses)
75+
println("Running test:compile")
76+
val (state3, _) = extracted.runTask(compile in Test in LocalRootProject, state2)
77+
78+
import java.nio.file.FileVisitResult
79+
import java.nio.file.{Files, Path}
80+
import java.nio.file.SimpleFileVisitor
81+
import java.nio.file.attribute.BasicFileAttributes
82+
import java.util
83+
84+
def checkSameFileContents(one: Path, other: Path): Unit = {
85+
Files.walkFileTree(one, new SimpleFileVisitor[Path]() {
86+
override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = {
87+
val result: FileVisitResult = super.visitFile(file, attrs)
88+
// get the relative file name from path "one"
89+
val relativize: Path = one.relativize(file)
90+
// construct the path for the counterpart file in "other"
91+
val fileInOther: Path = other.resolve(relativize)
92+
val otherBytes: Array[Byte] = Files.readAllBytes(fileInOther)
93+
val thisBytes: Array[Byte] = Files.readAllBytes(file)
94+
if (!(util.Arrays.equals(otherBytes, thisBytes))) {
95+
throw new AssertionError(file + " is not equal to " + fileInOther)
96+
}
97+
return result
98+
}
99+
})
100+
}
101+
println("Comparing: " + baseline.toPath + " and " + testClasses.toPath)
102+
checkSameFileContents(baseline.toPath, testClasses.toPath)
103+
checkSameFileContents(testClasses.toPath, baseline.toPath)
104+
105+
state3
106+
}

src/main/scala/scala/async/internal/Lifter.scala

+18-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package scala.async.internal
22

3+
import scala.collection.mutable
4+
35
trait Lifter {
46
self: AsyncMacro =>
57
import c.universe._
@@ -37,7 +39,7 @@ trait Lifter {
3739
}
3840

3941

40-
val defs: Map[Tree, Int] = {
42+
val defs: mutable.LinkedHashMap[Tree, Int] = {
4143
/** Collect the DefTrees directly enclosed within `t` that have the same owner */
4244
def collectDirectlyEnclosedDefs(t: Tree): List[DefTree] = t match {
4345
case ld: LabelDef => Nil
@@ -48,33 +50,33 @@ trait Lifter {
4850
companionship.record(childDefs)
4951
childDefs
5052
}
51-
asyncStates.flatMap {
53+
mutable.LinkedHashMap(asyncStates.flatMap {
5254
asyncState =>
5355
val defs = collectDirectlyEnclosedDefs(Block(asyncState.allStats: _*))
5456
defs.map((_, asyncState.state))
55-
}.toMap
57+
}: _*)
5658
}
5759

5860
// In which block are these symbols defined?
59-
val symToDefiningState: Map[Symbol, Int] = defs.map {
61+
val symToDefiningState: mutable.LinkedHashMap[Symbol, Int] = defs.map {
6062
case (k, v) => (k.symbol, v)
6163
}
6264

6365
// The definitions trees
64-
val symToTree: Map[Symbol, Tree] = defs.map {
66+
val symToTree: mutable.LinkedHashMap[Symbol, Tree] = defs.map {
6567
case (k, v) => (k.symbol, k)
6668
}
6769

6870
// The direct references of each definition tree
69-
val defSymToReferenced: Map[Symbol, List[Symbol]] = defs.keys.map {
70-
case tree => (tree.symbol, tree.collect {
71+
val defSymToReferenced: mutable.LinkedHashMap[Symbol, List[Symbol]] = defs.map {
72+
case (tree, _) => (tree.symbol, tree.collect {
7173
case rt: RefTree if symToDefiningState.contains(rt.symbol) => rt.symbol
7274
})
73-
}.toMap
75+
}
7476

7577
// The direct references of each block, excluding references of `DefTree`-s which
7678
// are already accounted for.
77-
val stateIdToDirectlyReferenced: Map[Int, List[Symbol]] = {
79+
val stateIdToDirectlyReferenced: mutable.LinkedHashMap[Int, List[Symbol]] = {
7880
val refs: List[(Int, Symbol)] = asyncStates.flatMap(
7981
asyncState => asyncState.stats.filterNot(t => t.isDef && !isLabel(t.symbol)).flatMap(_.collect {
8082
case rt: RefTree
@@ -84,8 +86,8 @@ trait Lifter {
8486
toMultiMap(refs)
8587
}
8688

87-
def liftableSyms: Set[Symbol] = {
88-
val liftableMutableSet = collection.mutable.Set[Symbol]()
89+
def liftableSyms: mutable.LinkedHashSet[Symbol] = {
90+
val liftableMutableSet = mutable.LinkedHashSet[Symbol]()
8991
def markForLift(sym: Symbol): Unit = {
9092
if (!liftableMutableSet(sym)) {
9193
liftableMutableSet += sym
@@ -97,19 +99,19 @@ trait Lifter {
9799
}
98100
}
99101
// Start things with DefTrees directly referenced from statements from other states...
100-
val liftableStatementRefs: List[Symbol] = stateIdToDirectlyReferenced.toList.flatMap {
102+
val liftableStatementRefs: List[Symbol] = stateIdToDirectlyReferenced.iterator.flatMap {
101103
case (i, syms) => syms.filter(sym => symToDefiningState(sym) != i)
102-
}
104+
}.toList
103105
// .. and likewise for DefTrees directly referenced by other DefTrees from other states
104106
val liftableRefsOfDefTrees = defSymToReferenced.toList.flatMap {
105107
case (referee, referents) => referents.filter(sym => symToDefiningState(sym) != symToDefiningState(referee))
106108
}
107109
// Mark these for lifting, which will follow transitive references.
108110
(liftableStatementRefs ++ liftableRefsOfDefTrees).foreach(markForLift)
109-
liftableMutableSet.toSet
111+
liftableMutableSet
110112
}
111113

112-
val lifted = liftableSyms.map(symToTree).toList.map {
114+
liftableSyms.iterator.map(symToTree).map {
113115
t =>
114116
val sym = t.symbol
115117
val treeLifted = t match {
@@ -147,7 +149,6 @@ trait Lifter {
147149
treeCopy.TypeDef(td, Modifiers(sym.flags), sym.name, tparams, rhs)
148150
}
149151
atPos(t.pos)(treeLifted)
150-
}
151-
lifted
152+
}.toList
152153
}
153154
}

src/main/scala/scala/async/internal/LiveVariables.scala

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package scala.async.internal
22

3+
import scala.collection.mutable
4+
35
import java.util
46
import java.util.function.{IntConsumer, IntPredicate}
57

@@ -19,12 +21,12 @@ trait LiveVariables {
1921
* @return a map mapping a state to the fields that should be nulled out
2022
* upon resuming that state
2123
*/
22-
def fieldsToNullOut(asyncStates: List[AsyncState], liftables: List[Tree]): Map[Int, List[Tree]] = {
24+
def fieldsToNullOut(asyncStates: List[AsyncState], liftables: List[Tree]): mutable.LinkedHashMap[Int, List[Tree]] = {
2325
// live variables analysis:
2426
// the result map indicates in which states a given field should be nulled out
25-
val liveVarsMap: Map[Tree, StateSet] = liveVars(asyncStates, liftables)
27+
val liveVarsMap: mutable.LinkedHashMap[Tree, StateSet] = liveVars(asyncStates, liftables)
2628

27-
var assignsOf = Map[Int, List[Tree]]()
29+
var assignsOf = mutable.LinkedHashMap[Int, List[Tree]]()
2830

2931
for ((fld, where) <- liveVarsMap) {
3032
where.foreach { new IntConsumer { def accept(state: Int): Unit = {
@@ -54,7 +56,7 @@ trait LiveVariables {
5456
* @param liftables the lifted fields
5557
* @return a map which indicates for a given field (the key) the states in which it should be nulled out
5658
*/
57-
def liveVars(asyncStates: List[AsyncState], liftables: List[Tree]): Map[Tree, StateSet] = {
59+
def liveVars(asyncStates: List[AsyncState], liftables: List[Tree]): mutable.LinkedHashMap[Tree, StateSet] = {
5860
val liftedSyms: Set[Symbol] = // include only vars
5961
liftables.iterator.filter {
6062
case ValDef(mods, _, _, _) => mods.hasFlag(MUTABLE)
@@ -262,15 +264,15 @@ trait LiveVariables {
262264
result
263265
}
264266

265-
val lastUsages: Map[Tree, StateSet] =
266-
liftables.iterator.map(fld => fld -> lastUsagesOf(fld, finalState)).toMap
267+
val lastUsages: mutable.LinkedHashMap[Tree, StateSet] =
268+
mutable.LinkedHashMap(liftables.map(fld => fld -> lastUsagesOf(fld, finalState)): _*)
267269

268270
if(AsyncUtils.verbose) {
269271
for ((fld, lastStates) <- lastUsages)
270272
AsyncUtils.vprintln(s"field ${fld.symbol.name} is last used in states ${lastStates.iterator.mkString(", ")}")
271273
}
272274

273-
val nullOutAt: Map[Tree, StateSet] =
275+
val nullOutAt: mutable.LinkedHashMap[Tree, StateSet] =
274276
for ((fld, lastStates) <- lastUsages) yield {
275277
var result = new StateSet
276278
lastStates.foreach(new IntConsumer { def accept(s: Int): Unit = {

src/main/scala/scala/async/internal/TransformUtils.scala

+11-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package scala.async.internal
66
import scala.reflect.macros.Context
77
import reflect.ClassTag
88
import scala.collection.immutable.ListMap
9+
import scala.collection.mutable
10+
import scala.collection.mutable.ListBuffer
911

1012
/**
1113
* Utilities used in both `ExprBuilder` and `AnfTransform`.
@@ -303,8 +305,15 @@ private[async] trait TransformUtils {
303305
})
304306
}
305307

306-
def toMultiMap[A, B](as: Iterable[(A, B)]): Map[A, List[B]] =
307-
as.toList.groupBy(_._1).mapValues(_.map(_._2).toList).toMap
308+
def toMultiMap[A, B](abs: Iterable[(A, B)]): mutable.LinkedHashMap[A, List[B]] = {
309+
// LinkedHashMap for stable order of results.
310+
val result = new mutable.LinkedHashMap[A, ListBuffer[B]]()
311+
for ((a, b) <- abs) {
312+
val buffer = result.getOrElseUpdate(a, new ListBuffer[B])
313+
buffer += b
314+
}
315+
result.map { case (a, b) => (a, b.toList) }
316+
}
308317

309318
// Attributed version of `TreeGen#mkCastPreservingAnnotations`
310319
def mkAttributedCastPreservingAnnotations(tree: Tree, tp: Type): Tree = {

0 commit comments

Comments
 (0)