aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/scala/firrtl/options/OptionParser.scala39
-rw-r--r--src/main/scala/firrtl/options/OptionsView.scala27
-rw-r--r--src/main/scala/firrtl/options/Registration.scala36
-rw-r--r--src/main/scala/firrtl/options/Shell.scala63
-rw-r--r--src/main/scala/firrtl/options/Stage.scala61
-rw-r--r--src/test/resources/META-INF/services/firrtl.options.RegisteredLibrary1
-rw-r--r--src/test/resources/META-INF/services/firrtl.options.RegisteredTransform1
-rw-r--r--src/test/scala/firrtlTests/options/OptionParserSpec.scala92
-rw-r--r--src/test/scala/firrtlTests/options/OptionsViewSpec.scala75
-rw-r--r--src/test/scala/firrtlTests/options/RegistrationSpec.scala54
-rw-r--r--src/test/scala/firrtlTests/options/ShellSpec.scala22
11 files changed, 471 insertions, 0 deletions
diff --git a/src/main/scala/firrtl/options/OptionParser.scala b/src/main/scala/firrtl/options/OptionParser.scala
new file mode 100644
index 00000000..6d8095c0
--- /dev/null
+++ b/src/main/scala/firrtl/options/OptionParser.scala
@@ -0,0 +1,39 @@
+// See LICENSE for license details.
+
+package firrtl.options
+
+import firrtl.{FIRRTLException, AnnotationSeq}
+
+import scopt.OptionParser
+
+/** Causes an OptionParser to not call exit (call `sys.exit`) if the `--help` option is passed
+ */
+trait DoNotTerminateOnExit { this: OptionParser[_] =>
+ override def terminate(exitState: Either[String, Unit]): Unit = Unit
+}
+
+/** A modified OptionParser with mutable termination and additional checks
+ */
+trait DuplicateHandling extends OptionParser[AnnotationSeq] {
+
+ override def parse(args: Seq[String], init: AnnotationSeq): Option[AnnotationSeq] = {
+
+ /** Message for found duplicate options */
+ def msg(x: String, y: String) = s"""Duplicate $x "$y" (did your custom Transform or OptionsManager add this?)"""
+
+ val longDups = options.map(_.name).groupBy(identity).collect{ case (k, v) if v.size > 1 && k != "" => k }
+ val shortDups = options.map(_.shortOpt).flatten.groupBy(identity).collect{ case (k, v) if v.size > 1 => k }
+
+
+ if (longDups.nonEmpty) {
+ throw new OptionsException(msg("long option", longDups.map("--" + _).mkString(",")), new IllegalArgumentException)
+ }
+
+ if (shortDups.nonEmpty) {
+ throw new OptionsException(msg("short option", shortDups.map("-" + _).mkString(",")), new IllegalArgumentException)
+ }
+
+ super.parse(args, init)
+ }
+
+}
diff --git a/src/main/scala/firrtl/options/OptionsView.scala b/src/main/scala/firrtl/options/OptionsView.scala
new file mode 100644
index 00000000..dade5c56
--- /dev/null
+++ b/src/main/scala/firrtl/options/OptionsView.scala
@@ -0,0 +1,27 @@
+// See LICENSE for license details.
+
+package firrtl.options
+
+import firrtl.AnnotationSeq
+
+/** Type class defining a "view" of an [[AnnotationSeq]]
+ * @tparam T the type to which this viewer converts an [[AnnotationSeq]] to
+ */
+trait OptionsView[T] {
+
+ /** Convert an [[AnnotationSeq]] to some other type
+ * @param options some annotations
+ */
+ def view(options: AnnotationSeq): Option[T]
+}
+
+/** A shim to manage multiple "views" of an [[AnnotationSeq]] */
+object Viewer {
+
+ /** Convert annotations to options using an implicitly provided [[OptionsView]]
+ * @param options some annotations
+ * @param optionsView a converter of options to the requested type
+ * @tparam T the type to which the input [[AnnotationSeq]] should be viewed as
+ */
+ def view[T](options: AnnotationSeq)(implicit optionsView: OptionsView[T]): Option[T] = optionsView.view(options)
+}
diff --git a/src/main/scala/firrtl/options/Registration.scala b/src/main/scala/firrtl/options/Registration.scala
new file mode 100644
index 00000000..481c095b
--- /dev/null
+++ b/src/main/scala/firrtl/options/Registration.scala
@@ -0,0 +1,36 @@
+// See LICENSE for license details.
+
+package firrtl.options
+
+import firrtl.{AnnotationSeq, Transform}
+
+import scopt.OptionParser
+
+/** Indicates that this class/object includes options (but does not add these as a registered class)
+ */
+trait HasScoptOptions {
+
+ /** This method will be called to add options to an OptionParser
+ * @param p an option parser
+ */
+ def addOptions(p: OptionParser[AnnotationSeq]): Unit
+}
+
+/** A [[Transform]] that includes options that should be exposed at the top level.
+ *
+ * @note To complete registration, include an entry in
+ * src/main/resources/META-INF/services/firrtl.options.RegisteredTransform */
+trait RegisteredTransform extends HasScoptOptions { this: Transform => }
+
+/** A class that includes options that should be exposed as a group at the top level.
+ *
+ * @note To complete registration, include an entry in
+ * src/main/resources/META-INF/services/firrtl.options.RegisteredLibrary */
+trait RegisteredLibrary extends HasScoptOptions {
+
+ /** The name of this library.
+ *
+ * This will be used when generating help text.
+ */
+ def name: String
+}
diff --git a/src/main/scala/firrtl/options/Shell.scala b/src/main/scala/firrtl/options/Shell.scala
new file mode 100644
index 00000000..b9278f30
--- /dev/null
+++ b/src/main/scala/firrtl/options/Shell.scala
@@ -0,0 +1,63 @@
+// See LICENSE for license details.
+
+package firrtl.options
+
+import firrtl.AnnotationSeq
+
+import scopt.OptionParser
+
+import java.util.ServiceLoader
+
+/** Indicate an error in [[firrtl.options]]
+ * @param msg a message to print
+ */
+case class OptionsException(msg: String, cause: Throwable = null) extends Exception(msg, cause)
+
+/** A utility for working with command line options
+ * @param applicationName the application associated with these command line options
+ */
+class Shell(val applicationName: String) {
+
+ /** Command line argument parser (OptionParser) with modifications */
+ final val parser = new OptionParser[AnnotationSeq](applicationName) with DoNotTerminateOnExit with DuplicateHandling
+
+ /** Contains all discovered [[RegisteredLibrary]] */
+ lazy val registeredLibraries: Seq[RegisteredLibrary] = {
+ val libraries = scala.collection.mutable.ArrayBuffer[RegisteredLibrary]()
+ val iter = ServiceLoader.load(classOf[RegisteredLibrary]).iterator()
+ while (iter.hasNext) {
+ val lib = iter.next()
+ libraries += lib
+ parser.note(lib.name)
+ lib.addOptions(parser)
+ }
+ libraries
+ }
+
+ /** Contains all discovered [[RegisteredTransform]] */
+ lazy val registeredTransforms: Seq[RegisteredTransform] = {
+ val transforms = scala.collection.mutable.ArrayBuffer[RegisteredTransform]()
+ val iter = ServiceLoader.load(classOf[RegisteredTransform]).iterator()
+ parser.note("FIRRTL Transform Options")
+ while (iter.hasNext) {
+ val tx = iter.next()
+ transforms += tx
+ tx.addOptions(parser)
+ }
+ transforms
+ }
+
+ /** The [[AnnotationSeq]] generated from command line arguments
+ *
+ * This requires lazy evaluation as subclasses will mixin new command
+ * line options via methods of [[Shell.parser]]
+ */
+ def parse(args: Array[String], initAnnos: AnnotationSeq = Seq.empty): AnnotationSeq = {
+ registeredTransforms
+ registeredLibraries
+ parser
+ .parse(args, initAnnos)
+ .getOrElse(throw new OptionsException("Failed to parse command line options", new IllegalArgumentException))
+ }
+
+}
diff --git a/src/main/scala/firrtl/options/Stage.scala b/src/main/scala/firrtl/options/Stage.scala
new file mode 100644
index 00000000..cc651943
--- /dev/null
+++ b/src/main/scala/firrtl/options/Stage.scala
@@ -0,0 +1,61 @@
+// See LICENSE for license details.
+
+package firrtl.options
+
+import firrtl.AnnotationSeq
+
+/** Utilities mixed into something that looks like a [[Stage]] */
+object StageUtils {
+ /** Print a warning message (in yellow)
+ * @param message error message
+ */
+ //scalastyle:off regex
+ def dramaticWarning(message: String): Unit = {
+ println(Console.YELLOW + "-"*78)
+ println(s"Warning: $message")
+ println("-"*78 + Console.RESET)
+ }
+
+ /** Print an error message (in red)
+ * @param message error message
+ * @note This does not stop the Driver.
+ */
+ //scalastyle:off regex
+ def dramaticError(message: String): Unit = {
+ println(Console.RED + "-"*78)
+ println(s"Error: $message")
+ println("-"*78 + Console.RESET)
+ }
+}
+
+/** A [[Stage]] represents one stage in the FIRRTL hardware compiler framework
+ *
+ * The FIRRTL compiler is a stage as well as any frontend or backend that runs before/after FIRRTL. Concretely, Chisel
+ * is a [[Stage]] as is FIRRTL's Verilog emitter. Each stage performs a mathematical transformation on an
+ * [[AnnotationSeq]] where some input annotations are processed to produce different annotations. Command line options
+ * may be pulled in if available.
+ */
+abstract class Stage {
+
+ /** A utility that helps convert command line options to annotations */
+ val shell: Shell
+
+ /** Run this [[Stage]] on some input annotations
+ * @param annotations input annotations
+ * @return output annotations
+ */
+ def execute(annotations: AnnotationSeq): AnnotationSeq
+
+ /** Run this [[Stage]] on on a mix of arguments and annotations
+ * @param args command line arguments
+ * @param initialAnnotations annotation
+ * @return output annotations
+ */
+ def execute(args: Array[String], annotations: AnnotationSeq): AnnotationSeq =
+ execute(shell.parse(args, annotations))
+
+ /** The main function that serves as this [[Stage]]'s command line interface
+ * @param args command line arguments
+ */
+ def main(args: Array[String]): Unit = execute(args, Seq.empty)
+}
diff --git a/src/test/resources/META-INF/services/firrtl.options.RegisteredLibrary b/src/test/resources/META-INF/services/firrtl.options.RegisteredLibrary
new file mode 100644
index 00000000..f28a6850
--- /dev/null
+++ b/src/test/resources/META-INF/services/firrtl.options.RegisteredLibrary
@@ -0,0 +1 @@
+firrtlTests.options.BarLibrary \ No newline at end of file
diff --git a/src/test/resources/META-INF/services/firrtl.options.RegisteredTransform b/src/test/resources/META-INF/services/firrtl.options.RegisteredTransform
new file mode 100644
index 00000000..a9fd3bc5
--- /dev/null
+++ b/src/test/resources/META-INF/services/firrtl.options.RegisteredTransform
@@ -0,0 +1 @@
+firrtlTests.options.FooTransform \ No newline at end of file
diff --git a/src/test/scala/firrtlTests/options/OptionParserSpec.scala b/src/test/scala/firrtlTests/options/OptionParserSpec.scala
new file mode 100644
index 00000000..ae4899d4
--- /dev/null
+++ b/src/test/scala/firrtlTests/options/OptionParserSpec.scala
@@ -0,0 +1,92 @@
+// See LICENSE for license details
+
+package firrtlTests.options
+
+import firrtl.{AnnotationSeq, FIRRTLException}
+import firrtl.annotations.{Annotation, NoTargetAnnotation}
+import firrtl.options.{DoNotTerminateOnExit, DuplicateHandling, OptionsException}
+
+import scopt.OptionParser
+
+import org.scalatest.{FlatSpec, Matchers}
+
+import java.security.Permission
+
+class OptionParserSpec extends FlatSpec with Matchers {
+
+ case class IntAnnotation(x: Int) extends NoTargetAnnotation {
+ def extract: Int = x
+ }
+
+ /* An option parser that prepends to a Seq[Int] */
+ class IntParser extends OptionParser[AnnotationSeq]("Int Parser") {
+ opt[Int]("integer").abbr("n").unbounded.action( (x, c) => IntAnnotation(x) +: c )
+ help("help")
+ }
+
+ trait DuplicateShortOption { this: OptionParser[AnnotationSeq] =>
+ opt[Int]("not-an-integer").abbr("n").unbounded.action( (x, c) => IntAnnotation(x) +: c )
+ }
+
+ trait DuplicateLongOption { this: OptionParser[AnnotationSeq] =>
+ opt[Int]("integer").abbr("m").unbounded.action( (x, c) => IntAnnotation(x) +: c )
+ }
+
+ case class ExitException(status: Option[Int]) extends SecurityException("Found a sys.exit")
+
+ /* Security manager that disallows calls to sys.exit */
+ class ExceptOnExit extends SecurityManager {
+ override def checkPermission(perm: Permission): Unit = {}
+ override def checkPermission(perm: Permission, context: Object): Unit = {}
+ override def checkExit(status: Int): Unit = {
+ super.checkExit(status)
+ throw ExitException(Some(status))
+ }
+ }
+
+ /* Tell a parser to terminate in an environment where sys.exit throws an exception */
+ def catchStatus(parser: OptionParser[_], exitState: Either[String, Unit]): Option[Int] = {
+ System.setSecurityManager(new ExceptOnExit())
+ val status = try {
+ parser.terminate(exitState)
+ throw new ExitException(None)
+ } catch {
+ case ExitException(s) => s
+ }
+ System.setSecurityManager(null)
+ status
+ }
+
+ behavior of "default OptionsParser"
+
+ it should "terminate on exit" in {
+ val parser = new IntParser
+
+ info("By default, exit statuses are reported")
+ catchStatus(parser, Left("some message")) should be (Some(1))
+ catchStatus(parser, Right(Unit)) should be (Some(0))
+ }
+
+ behavior of "DoNotTerminateOnExit"
+
+ it should "disable sys.exit for terminate method" in {
+ val parser = new IntParser with DoNotTerminateOnExit
+ catchStatus(parser, Left("some message")) should be (None)
+ catchStatus(parser, Right(Unit)) should be (None)
+ }
+
+ behavior of "DuplicateHandling"
+
+ it should "detect short duplicates" in {
+ val parser = new IntParser with DuplicateHandling with DuplicateShortOption
+ intercept[OptionsException] { parser.parse(Array[String](), Seq[Annotation]()) }
+ .getMessage should startWith ("Duplicate short option")
+ }
+
+ it should "detect long duplicates" in {
+ val parser = new IntParser with DuplicateHandling with DuplicateLongOption
+ intercept[OptionsException] { parser.parse(Array[String](), Seq[Annotation]()) }
+ .getMessage should startWith ("Duplicate long option")
+ }
+
+}
diff --git a/src/test/scala/firrtlTests/options/OptionsViewSpec.scala b/src/test/scala/firrtlTests/options/OptionsViewSpec.scala
new file mode 100644
index 00000000..dec6a99f
--- /dev/null
+++ b/src/test/scala/firrtlTests/options/OptionsViewSpec.scala
@@ -0,0 +1,75 @@
+// See LICENSE for license details
+
+package firrtlTests.options
+
+import org.scalatest.{FlatSpec, Matchers}
+
+import firrtl.options.OptionsView
+import firrtl.AnnotationSeq
+import firrtl.annotations.{Annotation,NoTargetAnnotation}
+
+class OptionsViewSpec extends FlatSpec with Matchers {
+
+ /* Annotations */
+ case class NameAnnotation(name: String) extends NoTargetAnnotation
+ case class ValueAnnotation(value: Int) extends NoTargetAnnotation
+
+ /* The type we want to view the annotations as */
+ case class Foo(name: Option[String] = None, value: Option[Int] = None)
+ case class Bar(name: String = "bar")
+
+ /* An OptionsView that converts an AnnotationSeq to Option[Foo] */
+ implicit object FooView extends OptionsView[Foo] {
+ private def append(foo: Foo, anno: Annotation): Foo = anno match {
+ case NameAnnotation(n) => foo.copy(name = Some(n))
+ case ValueAnnotation(v) => foo.copy(value = Some(v))
+ case _ => foo
+ }
+
+ def view(options: AnnotationSeq): Option[Foo] = {
+ val annoSeq = options.foldLeft(Foo())(append)
+ Some(annoSeq)
+ }
+ }
+
+ /* An OptionsView that converts an AnnotationSeq to Option[Bar] */
+ implicit object BarView extends OptionsView[Bar] {
+ private def append(bar: Bar, anno: Annotation): Bar = anno match {
+ case NameAnnotation(n) => bar.copy(name = n)
+ case _ => bar
+ }
+
+ def view(options: AnnotationSeq): Option[Bar] = {
+ val annoSeq = options.foldLeft(Bar())(append)
+ Some(annoSeq)
+ }
+ }
+
+ behavior of "OptionsView"
+
+ it should "convert annotations to one of two types" in {
+ /* Some default annotations */
+ val annos = Seq(NameAnnotation("foo"), ValueAnnotation(42))
+
+ info("Foo conversion okay")
+ FooView.view(annos) should be (Some(Foo(Some("foo"), Some(42))))
+
+ info("Bar conversion okay")
+ BarView.view(annos) should be (Some(Bar("foo")))
+ }
+
+ behavior of "Viewer"
+
+ it should "implicitly view annotations as the specified type" in {
+ import firrtl.options.Viewer._
+
+ /* Some empty annotations */
+ val annos = Seq[Annotation]()
+
+ info("Foo view okay")
+ view[Foo](annos) should be (Some(Foo(None, None)))
+
+ info("Bar view okay")
+ view[Bar](annos) should be (Some(Bar()))
+ }
+}
diff --git a/src/test/scala/firrtlTests/options/RegistrationSpec.scala b/src/test/scala/firrtlTests/options/RegistrationSpec.scala
new file mode 100644
index 00000000..c060341d
--- /dev/null
+++ b/src/test/scala/firrtlTests/options/RegistrationSpec.scala
@@ -0,0 +1,54 @@
+// See LICENSE for license details.
+
+package firrtlTests.options
+
+import org.scalatest.{FlatSpec, Matchers}
+import scopt.OptionParser
+import java.util.ServiceLoader
+
+import firrtl.options.{RegisteredTransform, RegisteredLibrary}
+import firrtl.passes.Pass
+import firrtl.ir.Circuit
+import firrtl.annotations.NoTargetAnnotation
+import firrtl.AnnotationSeq
+
+case object HelloAnnotation extends NoTargetAnnotation
+
+class FooTransform extends Pass with RegisteredTransform {
+ def run(c: Circuit): Circuit = c
+ def addOptions(p: OptionParser[AnnotationSeq]): Unit =
+ p.opt[Unit]("hello")
+ .action( (_, c) => HelloAnnotation +: c )
+}
+
+class BarLibrary extends RegisteredLibrary {
+ def name: String = "Bar"
+ def addOptions(p: OptionParser[AnnotationSeq]): Unit =
+ p.opt[Unit]("world")
+ .action( (_, c) => HelloAnnotation +: c )
+}
+
+class RegistrationSpec extends FlatSpec with Matchers {
+
+ behavior of "RegisteredTransform"
+
+ it should "FooTransform should be discovered by Java.util.ServiceLoader" in {
+ val iter = ServiceLoader.load(classOf[RegisteredTransform]).iterator()
+ val transforms = scala.collection.mutable.ArrayBuffer[RegisteredTransform]()
+ while (iter.hasNext) {
+ transforms += iter.next()
+ }
+ transforms.map(_.getClass.getName) should contain ("firrtlTests.options.FooTransform")
+ }
+
+ behavior of "RegisteredLibrary"
+
+ it should "BarLibrary be discovered by Java.util.ServiceLoader" in {
+ val iter = ServiceLoader.load(classOf[RegisteredLibrary]).iterator()
+ val transforms = scala.collection.mutable.ArrayBuffer[RegisteredLibrary]()
+ while (iter.hasNext) {
+ transforms += iter.next()
+ }
+ transforms.map(_.getClass.getName) should contain ("firrtlTests.options.BarLibrary")
+ }
+}
diff --git a/src/test/scala/firrtlTests/options/ShellSpec.scala b/src/test/scala/firrtlTests/options/ShellSpec.scala
new file mode 100644
index 00000000..d87a9a30
--- /dev/null
+++ b/src/test/scala/firrtlTests/options/ShellSpec.scala
@@ -0,0 +1,22 @@
+// See LICENSE for license details.
+
+package firrtlTests.options
+
+import org.scalatest._
+
+import firrtl.options.Shell
+
+class ShellSpec extends FlatSpec with Matchers {
+
+ behavior of "Shell"
+
+ it should "detect all registered libraries and transforms" in {
+ val shell = new Shell("foo")
+
+ info("Found FooTransform")
+ shell.registeredTransforms.map(_.getClass.getName) should contain ("firrtlTests.options.FooTransform")
+
+ info("Found BarLibrary")
+ shell.registeredLibraries.map(_.getClass.getName) should contain ("firrtlTests.options.BarLibrary")
+ }
+}