From e0951fef346c4e3e2d19a57b396012e9800b69b6 Mon Sep 17 00:00:00 2001 From: Schuyler Eldridge Date: Wed, 22 Aug 2018 15:22:20 -0400 Subject: Add firrtl.options This adds a new package, "firrtl.options", that provides a framework for working with options inside and outside FIRRTL. Small changes: - Make TerminateOnExit return the correct exit code - Deprecate mutable TerminateOnExit - Add immutable DoNotTermianteOnExit Signed-off-by: Schuyler Eldridge --- src/main/scala/firrtl/options/OptionParser.scala | 39 +++++++++++++++ src/main/scala/firrtl/options/OptionsView.scala | 27 ++++++++++ src/main/scala/firrtl/options/Registration.scala | 36 ++++++++++++++ src/main/scala/firrtl/options/Shell.scala | 63 ++++++++++++++++++++++++ src/main/scala/firrtl/options/Stage.scala | 61 +++++++++++++++++++++++ 5 files changed, 226 insertions(+) create mode 100644 src/main/scala/firrtl/options/OptionParser.scala create mode 100644 src/main/scala/firrtl/options/OptionsView.scala create mode 100644 src/main/scala/firrtl/options/Registration.scala create mode 100644 src/main/scala/firrtl/options/Shell.scala create mode 100644 src/main/scala/firrtl/options/Stage.scala (limited to 'src') 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) +} -- cgit v1.2.3 From 27c1b366ce58e93434e77e964365474f5e7aa8d7 Mon Sep 17 00:00:00 2001 From: Schuyler Eldridge Date: Fri, 28 Sep 2018 12:39:01 -0400 Subject: Add firrtl.options tests Signed-off-by: Schuyler Eldridge --- .../services/firrtl.options.RegisteredLibrary | 1 + .../services/firrtl.options.RegisteredTransform | 1 + .../firrtlTests/options/OptionParserSpec.scala | 92 ++++++++++++++++++++++ .../firrtlTests/options/OptionsViewSpec.scala | 75 ++++++++++++++++++ .../firrtlTests/options/RegistrationSpec.scala | 54 +++++++++++++ src/test/scala/firrtlTests/options/ShellSpec.scala | 22 ++++++ 6 files changed, 245 insertions(+) create mode 100644 src/test/resources/META-INF/services/firrtl.options.RegisteredLibrary create mode 100644 src/test/resources/META-INF/services/firrtl.options.RegisteredTransform create mode 100644 src/test/scala/firrtlTests/options/OptionParserSpec.scala create mode 100644 src/test/scala/firrtlTests/options/OptionsViewSpec.scala create mode 100644 src/test/scala/firrtlTests/options/RegistrationSpec.scala create mode 100644 src/test/scala/firrtlTests/options/ShellSpec.scala (limited to 'src') 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") + } +} -- cgit v1.2.3