1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
|
// See LICENSE for license details.
package firrtl.options
import firrtl.AnnotationSeq
import firrtl.graph.{DiGraph, CyclicException}
import scala.collection.Set
import scala.collection.immutable.{Set => ISet}
import scala.collection.mutable.{ArrayBuffer, HashMap, LinkedHashMap, LinkedHashSet, Queue}
/** An exception arising from an in a [[DependencyManager]] */
case class DependencyManagerException(message: String, cause: Throwable = null) extends RuntimeException(message, cause)
/** A [[firrtl.options.TransformLike TransformLike]] that resolves a linear ordering of dependencies based on
* requirements.
* @tparam A the type over which this transforms
* @tparam B the type of the [[firrtl.options.TransformLike TransformLike]]
*/
trait DependencyManager[A, B <: TransformLike[A] with DependencyAPI[B]] extends TransformLike[A] with DependencyAPI[B] {
/** Requested [[firrtl.options.TransformLike TransformLike]]s that should be run. Internally, this will be converted to
* a set based on the ordering defined here.
*/
def targets: Seq[Class[B]]
private lazy val _targets: LinkedHashSet[Class[B]] = targets
.foldLeft(new LinkedHashSet[Class[B]]()){ case (a, b) => a += b }
/** A sequence of [[firrtl.Transform]]s that have been run. Internally, this will be converted to an ordered set.
*/
def currentState: Seq[Class[B]]
private lazy val _currentState: LinkedHashSet[Class[B]] = currentState
.foldLeft(new LinkedHashSet[Class[B]]()){ case (a, b) => a += b }
/** Existing transform objects that have already been constructed */
def knownObjects: Set[B]
/** A sequence of wrappers to apply to the resulting [[firrtl.options.TransformLike TransformLike]] sequence. This can
* be used to, e.g., add automated pre-processing and post-processing.
*/
def wrappers: Seq[(B) => B] = Seq.empty
/** Store of conversions between classes and objects. Objects that do not exist in the map will be lazily constructed.
*/
protected lazy val classToObject: LinkedHashMap[Class[B], B] = {
val init = LinkedHashMap[Class[B], B](knownObjects.map(x => x.getClass.asInstanceOf[Class[B]] -> x).toSeq: _*)
(_targets ++ _currentState)
.filter(!init.contains(_))
.map(x => init(x) = safeConstruct(x))
init
}
/** A method that will create a copy of this [[firrtl.options.DependencyManager DependencyManager]], but with different
* requirements. This is used to solve sub-problems arising from invalidations.
*/
protected def copy(
targets: Seq[Class[B]],
currentState: Seq[Class[B]],
knownObjects: ISet[B] = classToObject.values.toSet): B
/** Implicit conversion from Class[B] to B */
private implicit def cToO(c: Class[B]): B = classToObject.getOrElseUpdate(c, safeConstruct(c))
/** Implicit conversion from B to Class[B] */
private implicit def oToC(b: B): Class[B] = b.getClass.asInstanceOf[Class[B]]
/** Modified breadth-first search that supports multiple starting nodes and a custom extractor that can be used to
* generate/filter the edges to explore. Additionally, this will include edges to previously discovered nodes.
*/
private def bfs( start: LinkedHashSet[Class[B]],
blacklist: LinkedHashSet[Class[B]],
extractor: B => Set[Class[B]] ): LinkedHashMap[B, LinkedHashSet[B]] = {
val (queue, edges) = {
val a: Queue[Class[B]] = Queue(start.toSeq:_*)
val b: LinkedHashMap[B, LinkedHashSet[B]] = LinkedHashMap[B, LinkedHashSet[B]](
start.map((cToO(_) -> LinkedHashSet[B]())).toSeq:_*)
(a, b)
}
while (queue.nonEmpty) {
val u: Class[B] = queue.dequeue
for (v: Class[B] <- extractor(classToObject(u))) {
if (!blacklist.contains(v) && !edges.contains(v)) {
queue.enqueue(v)
}
if (!edges.contains(v)) {
val obj = cToO(v)
edges(obj) = LinkedHashSet.empty
classToObject += (v -> obj)
}
edges(classToObject(u)) = edges(classToObject(u)) + classToObject(v)
}
}
edges
}
/** Pull in all registered [[firrtl.options.TransformLike TransformLike]] once [[firrtl.options.TransformLike
* TransformLike]] registration is integrated
* @todo implement this
*/
private lazy val registeredTransforms: Set[B] = Set.empty
/** A directed graph consisting of prerequisite edges */
private lazy val prerequisiteGraph: DiGraph[B] = {
val edges = bfs(
start = _targets -- _currentState,
blacklist = _currentState,
extractor = (p: B) => new LinkedHashSet[Class[B]]() ++ p.prerequisites -- _currentState)
DiGraph(edges)
}
/** A directed graph consisting of prerequisites derived from only those transforms which are supposed to run. This
* pulls in dependents for transforms which are not in the target set.
*/
private lazy val dependentsGraph: DiGraph[B] = {
val v = new LinkedHashSet() ++ prerequisiteGraph.getVertices
DiGraph(new LinkedHashMap() ++ v.map(vv => vv -> ((new LinkedHashSet() ++ vv.dependents).map(cToO) & v))).reverse
}
/** A directed graph consisting of prerequisites derived from ALL targets. This is necessary for defining targets for
* [[DependencyManager]] sub-problems.
*/
private lazy val otherDependents: DiGraph[B] = {
val edges = {
val x = new LinkedHashMap ++ _targets
.map(classToObject)
.map{ a => a -> prerequisiteGraph.getVertices.filter(a._dependents(_)) }
x
.values
.reduce(_ ++ _)
.foldLeft(x){ case (xx, y) => if (xx.contains(y)) { xx } else { xx ++ Map(y -> Set.empty[B]) } }
}
DiGraph(edges).reverse
}
/** A directed graph consisting of all prerequisites, including prerequisites derived from dependents */
lazy val dependencyGraph: DiGraph[B] = prerequisiteGraph + dependentsGraph
/** A directed graph consisting of invalidation edges */
lazy val invalidateGraph: DiGraph[B] = {
val v = dependencyGraph.getVertices
DiGraph(
bfs(
start = _targets -- _currentState,
blacklist = _currentState,
extractor = (p: B) => v.filter(p.invalidates).map(_.asClass).toSet))
.reverse
}
/** Wrap a possible [[CyclicException]] thrown by a thunk in a [[DependencyManagerException]] */
private def cyclePossible[A](a: String, diGraph: DiGraph[_])(thunk: => A): A = try { thunk } catch {
case e: CyclicException =>
throw new DependencyManagerException(
s"""|No transform ordering possible due to cyclic dependency in $a with cycles:
|${diGraph.findSCCs.filter(_.size > 1).mkString(" - ", "\n - ", "")}""".stripMargin, e)
}
/** Wrap an [[IllegalAccessException]] due to attempted object construction in a [[DependencyManagerException]] */
private def safeConstruct[A](a: Class[A]): A = try { a.newInstance } catch {
case e: IllegalAccessException => throw new DependencyManagerException(
s"Failed to construct '$a'! (Did you try to construct an object?)", e)
case e: InstantiationException => throw new DependencyManagerException(
s"Failed to construct '$a'! (Did you try to construct an inner class or a class with parameters?)", e)
}
/** An ordering of [[firrtl.options.TransformLike TransformLike]]s that causes the requested [[DependencyManager.targets
* targets]] to be executed starting from the [[DependencyManager.currentState currentState]]. This ordering respects
* prerequisites, dependents, and invalidates of all constituent [[firrtl.options.TransformLike TransformLike]]s.
* This uses an algorithm that attempts to reduce the number of re-lowerings due to invalidations. Re-lowerings are
* implemented as new [[DependencyManager]]s.
* @throws DependencyManagerException if a cycle exists in either the [[DependencyManager.dependencyGraph
* dependencyGraph]] or the [[DependencyManager.invalidateGraph invalidateGraph]].
*/
lazy val transformOrder: Seq[B] = {
/* Topologically sort the dependency graph using the invalidate graph topological sort as a seed. This has the effect of
* reducing (perhaps minimizing?) the number of work re-lowerings.
*/
val sorted = {
val seed = cyclePossible("invalidates", invalidateGraph){ invalidateGraph.linearize }.reverse
cyclePossible("prerequisites/dependents", dependencyGraph) {
dependencyGraph
.seededLinearize(Some(seed))
.reverse
.dropWhile(b => _currentState.contains(b))
}
}
val (state, lowerers) = {
/* [todo] Seq is inefficient here, but Array has ClassTag problems. Use something else? */
val (s, l) = sorted.foldLeft((_currentState, Seq[B]())){ case ((state, out), in) =>
/* The prerequisites are both prerequisites AND dependents. */
val prereqs = new LinkedHashSet() ++ in.prerequisites ++
dependencyGraph.getEdges(in).toSeq.map(oToC) ++
otherDependents.getEdges(in).toSeq.map(oToC)
val missing = (prereqs -- state)
val preprocessing: Option[B] = {
if (missing.nonEmpty) { Some(this.copy(prereqs.toSeq, state.toSeq)) }
else { None }
}
((state ++ missing + in).map(cToO).filterNot(in.invalidates).map(oToC), out ++ preprocessing :+ in)
}
val missing = (_targets -- s)
val postprocessing: Option[B] = {
if (missing.nonEmpty) { Some(this.copy(_targets.toSeq, s.toSeq)) }
else { None }
}
(s ++ missing, l ++ postprocessing)
}
if (!_targets.subsetOf(state)) {
throw new DependencyManagerException(
s"The final state ($state) did not include the requested targets (${targets})!")
}
lowerers
}
/** A version of the [[DependencyManager.transformOrder transformOrder]] that flattens the transforms of any internal
* [[DependencyManager]]s.
*/
lazy val flattenedTransformOrder: Seq[B] = transformOrder.flatMap {
case p: DependencyManager[A, B] => p.flattenedTransformOrder
case p => Some(p)
}
final override def transform(annotations: A): A = {
/* A local store of each wrapper to it's underlying class. */
val wrapperToClass = new HashMap[B, Class[B]]
/* The determined, flat order of transforms is wrapped with surrounding transforms while populating wrapperToClass so
* that each wrapped transform object can be dereferenced to its underlying class. Each wrapped transform is then
* applied while tracking the state of the underlying A. If the state ever disagrees with a prerequisite, then this
* throws an exception.
*/
flattenedTransformOrder
.map{ t =>
val w = wrappers.foldLeft(t){ case (tx, wrapper) => wrapper(tx) }
wrapperToClass += (w -> t)
w
}.foldLeft((annotations, _currentState)){ case ((a, state), t) =>
if (!t.prerequisites.toSet.subsetOf(state)) {
throw new DependencyManagerException(
s"""|Tried to execute '$t' for which run-time prerequisites were not satisfied:
| state: ${state.mkString("\n -", "\n -", "")}
| prerequisites: ${prerequisites.mkString("\n -", "\n -", "")}""".stripMargin)
}
(t.transform(a), ((state + wrapperToClass(t)).map(cToO).filterNot(t.invalidates).map(oToC)))
}._1
}
/** This colormap uses Colorbrewer's 4-class OrRd color scheme */
protected val colormap = Seq("#fef0d9", "#fdcc8a", "#fc8d59", "#d7301f")
/** Get a name of some [[firrtl.options.TransformLike TransformLike]] */
private def transformName(transform: B, suffix: String = ""): String = s""""${transform.name}$suffix""""
/** Convert all prerequisites, dependents, and invalidates to a Graphviz representation.
* @param file the name of the output file
*/
def dependenciesToGraphviz: String = {
def toGraphviz(digraph: DiGraph[B], attributes: String = "", tab: String = " "): Option[String] = {
val edges =
digraph
.getEdgeMap
.collect{ case (v, edges) if edges.nonEmpty => (v -> edges) }
.map{ case (v, edges) =>
s"""${transformName(v)} -> ${edges.map(e => transformName(e)).mkString("{ ", " ", " }")}""" }
if (edges.isEmpty) { None } else {
Some(
s"""| { $attributes
|${edges.mkString(tab, "\n" + tab, "")}
| }""".stripMargin
)
}
}
val connections =
Seq( (prerequisiteGraph, "edge []"),
(dependentsGraph, """edge [color="#de2d26"]"""),
(invalidateGraph, "edge [minlen=2,style=dashed,constraint=false]") )
.flatMap{ case (a, b) => toGraphviz(a, b) }
.mkString("\n")
val nodes =
(prerequisiteGraph + dependentsGraph + invalidateGraph + otherDependents)
.getVertices
.map(v => s"""${transformName(v)} [label="${v.getClass.getSimpleName}"]""")
s"""|digraph DependencyManager {
| graph [rankdir=BT]
| node [fillcolor="${colormap(0)}",style=filled,shape=box]
|${nodes.mkString(" ", "\n" + " ", "")}
|$connections
|}
|""".stripMargin
}
def transformOrderToGraphviz(colormap: Seq[String] = colormap): String = {
def rotate[A](a: Seq[A]): Seq[A] = a match {
case Nil => Nil
case car :: cdr => cdr :+ car
case car => car
}
val sorted = ArrayBuffer.empty[String]
def rec(pm: DependencyManager[A, B], cm: Seq[String], tab: String = "", id: Int = 0): (String, Int) = {
var offset = id
val targets = pm._targets.toSeq.map(_.getSimpleName).mkString(", ")
val state = pm._currentState.toSeq.map(_.getSimpleName).mkString(", ")
val header = s"""|${tab}subgraph cluster_$id {
|$tab label="targets: $targets\\nstate: $state"
|$tab labeljust=l
|$tab node [fillcolor="${cm.head}"]""".stripMargin
val body = pm.transformOrder.map{
case a: DependencyManager[A, B] =>
val (str, d) = rec(a, rotate(cm), tab + " ", offset + 1)
offset = d
str
case a =>
val name = s"""${transformName(a, "_" + id)}"""
sorted += name
s"""$tab $name [label="${a.getClass.getSimpleName}"]"""
}.mkString("\n")
(Seq(header, body, s"$tab}").mkString("\n"), offset)
}
s"""|digraph DependencyManagerTransformOrder {
| graph [rankdir=TB]
| node [style=filled,shape=box]
|${rec(this, colormap, " ")._1}
| ${sorted.mkString(" -> ")}
|}""".stripMargin
}
}
/** A [[Phase]] that will ensure that some other [[Phase]]s and their prerequisites are executed.
*
* This tries to determine a phase ordering such that an [[AnnotationSeq]] ''output'' is produced that has had all of
* the requested [[Phase]] target transforms run without having them be invalidated.
* @param targets the [[Phase]]s you want to run
*/
class PhaseManager(
val targets: Seq[Class[Phase]],
val currentState: Seq[Class[Phase]] = Seq.empty,
val knownObjects: Set[Phase] = Set.empty) extends Phase with DependencyManager[AnnotationSeq, Phase] {
protected def copy(a: Seq[Class[Phase]], b: Seq[Class[Phase]], c: ISet[Phase]) = new PhaseManager(a, b, c)
}
|