Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new annotation for Chisel Circuit serialization #1580

Merged
merged 1 commit into from
Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions src/main/scala/chisel3/stage/ChiselAnnotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,53 @@ object ChiselGeneratorAnnotation extends HasShellOptions {
case class ChiselCircuitAnnotation(circuit: Circuit)
extends NoTargetAnnotation
with ChiselOption
with CustomFileEmission {
with Unserializable {
/* Caching the hashCode for a large circuit is necessary due to repeated queries.
* Not caching the hashCode will cause severe performance degredations for large [[Circuit]]s.
*/
override lazy val hashCode: Int = circuit.hashCode
}

protected def baseFileName(annotations: AnnotationSeq): String = {
view[ChiselOptions](annotations).outputFile.getOrElse(circuit.name)
object CircuitSerializationAnnotation {
sealed trait Format {
def extension: String
}
case object FirrtlFileFormat extends Format {
def extension = ".fir"
}
case object ProtoBufFileFormat extends Format {
def extension = ".pb"
}
}

protected def suffix: Option[String] = Some(".fir")
import CircuitSerializationAnnotation._

override def getBytes: Iterable[Byte] = OldEmitter.emit(circuit).getBytes
/** Wraps a [[Circuit]] for serialization via [[CustomFileEmission]]
* @param circuit a Chisel Circuit
* @param filename name of destination file (excludes file extension)
* @param format serialization file format (sets file extension)
*/
case class CircuitSerializationAnnotation(circuit: Circuit, filename: String, format: Format)
extends NoTargetAnnotation
with CustomFileEmission {
/* Caching the hashCode for a large circuit is necessary due to repeated queries.
* Not caching the hashCode will cause severe performance degredations for large [[Circuit]]s.
*/
override lazy val hashCode: Int = circuit.hashCode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch.


protected def baseFileName(annotations: AnnotationSeq): String = filename

protected def suffix: Option[String] = Some(format.extension)

// TODO Use lazy Iterables so that we don't have to materialize full intermediate data structures
override def getBytes: Iterable[Byte] = format match {
case FirrtlFileFormat => OldEmitter.emit(circuit).getBytes
case ProtoBufFileFormat =>
val ostream = new java.io.ByteArrayOutputStream
val modules = circuit.components.map(m => () => chisel3.internal.firrtl.Converter.convert(m))
firrtl.proto.ToProto.writeToStreamFast(ostream, firrtl.ir.NoInfo, modules, circuit.name)
ostream.toByteArray
}
}

case class ChiselOutputFileAnnotation(file: String) extends NoTargetAnnotation with ChiselOption with Unserializable
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/chisel3/stage/ChiselCli.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ trait ChiselCli { this: Shell =>
parser.note("Chisel Front End Options")
Seq( NoRunFirrtlCompilerAnnotation,
PrintFullStackTraceAnnotation,
ChiselOutputFileAnnotation,
ChiselGeneratorAnnotation )
.foreach(_.addOptions(parser))
}
1 change: 1 addition & 0 deletions src/main/scala/chisel3/stage/ChiselPhase.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ private[chisel3] object ChiselPhase {
Dependency[chisel3.stage.phases.AddImplicitOutputFile],
Dependency[chisel3.stage.phases.AddImplicitOutputAnnotationFile],
Dependency[chisel3.stage.phases.MaybeAspectPhase],
Dependency[chisel3.stage.phases.AddSerializationAnnotations],
Dependency[chisel3.stage.phases.Convert],
Dependency[chisel3.stage.phases.MaybeFirrtlStage] )

Expand Down
3 changes: 2 additions & 1 deletion src/main/scala/chisel3/stage/ChiselStage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import firrtl.options.Viewer.view

import chisel3.{ChiselException, RawModule}
import chisel3.internal.{firrtl => cir, ErrorLog}
import chisel3.stage.CircuitSerializationAnnotation.FirrtlFileFormat

import java.io.{StringWriter, PrintWriter}

Expand Down Expand Up @@ -72,7 +73,7 @@ class ChiselStage extends Stage {

annos
.collectFirst {
case a: ChiselCircuitAnnotation => a.getBytes
case a: ChiselCircuitAnnotation => CircuitSerializationAnnotation(a.circuit, "", FirrtlFileFormat).getBytes
}
.get
.map(_.toChar)
Expand Down
8 changes: 6 additions & 2 deletions src/main/scala/chisel3/stage/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import firrtl._
import firrtl.options.OptionsView

import chisel3.internal.firrtl.{Circuit => ChiselCircuit}
import chisel3.stage.CircuitSerializationAnnotation.FirrtlFileFormat

package object stage {

Expand All @@ -31,9 +32,12 @@ package object stage {
var chirrtlCircuit: Option[String] = None

options.foreach {
case a@ ChiselCircuitAnnotation(b) =>
case a @ ChiselCircuitAnnotation(b) =>
chiselCircuit = Some(b)
chirrtlCircuit = Some(a.getBytes.map(_.toChar).mkString)
chirrtlCircuit = {
val anno = CircuitSerializationAnnotation(a.circuit, "", FirrtlFileFormat)
Some(anno.getBytes.map(_.toChar).mkString)
}
case _ =>
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// See LICENSE for license details.

package chisel3.stage.phases

import firrtl.AnnotationSeq
import firrtl.options.{Dependency, Phase}
import firrtl.options.Viewer.view

import chisel3.stage._
import chisel3.stage.CircuitSerializationAnnotation._
import chisel3.internal.ChiselException

/** Adds [[stage.CircuitSerializationAnnotation]]s based on [[ChiselOutputFileAnnotation]]
*/
class AddSerializationAnnotations extends Phase {

override def prerequisites = Seq(Dependency[Elaborate], Dependency[AddImplicitOutputFile])
override def optionalPrerequisites = Seq.empty
override def optionalPrerequisiteOf = Seq.empty
override def invalidates(a: Phase) = false

def transform(annotations: AnnotationSeq): AnnotationSeq = {
val chiselOptions = view[ChiselOptions](annotations)
val circuit = chiselOptions.chiselCircuit.getOrElse {
throw new ChiselException(s"Unable to locate the elaborated circuit, did ${classOf[Elaborate].getName} run correctly")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new ChiselException(s"Unable to locate the elaborated circuit, did ${classOf[Elaborate].getName} run correctly")
throw new ChiselException(s"Unable to locate the elaborated circuit, did ${classOf[Elaborate].getName} run correctly?")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm of two minds on whether or not this should throw an error. Some phases, like Elaborate, are extremely simple and only do work if a sensitive annotation exists. Any user input checking is then pushed to a "checks" phase which runs before all work starts. (Note: that chisel3.stage.phases.Checks does not current assert that a ChiselGeneratorAnnotation exists... it probably should.)

By this logic, the error message here is a bit odd as it isn't that Elaborate ran, but that there may have been user error where a circuit was never specified.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify: this is fine as it stands. I would just like some eventual unification in how we handle this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I see your point. I wasn't sure what to do here so I just decided to throw an error, but I'm happy to change this behavior

}
val baseFilename = chiselOptions.outputFile.getOrElse(circuit.name)

val (filename, format) =
if (baseFilename.endsWith(".pb")) (baseFilename.stripSuffix(".pb"), ProtoBufFileFormat)
else (baseFilename.stripSuffix(".fir"), FirrtlFileFormat)
CircuitSerializationAnnotation(circuit, filename, format) +: annotations
}
}
36 changes: 34 additions & 2 deletions src/test/scala/chiselTests/stage/ChiselMainSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import org.scalatest.GivenWhenThen
import org.scalatest.featurespec.AnyFeatureSpec
import org.scalatest.matchers.should.Matchers

import scala.io.Source
import firrtl.Parser

object ChiselMainSpec {

/** A module that connects two different types together resulting in an elaboration error */
Expand Down Expand Up @@ -52,7 +55,7 @@ class ChiselMainSpec extends AnyFeatureSpec with GivenWhenThen with Matchers wit
}

class TargetDirectoryFixture(dirName: String) {
val dir = new File(s"test_run_dir/FirrtlStageSpec/$dirName")
val dir = new File(s"test_run_dir/ChiselStageSpec/$dirName")
val buildDir = new File(dir + "/build")
dir.mkdirs()
}
Expand All @@ -63,7 +66,8 @@ class ChiselMainSpec extends AnyFeatureSpec with GivenWhenThen with Matchers wit
files: Seq[String] = Seq.empty,
stdout: Option[String] = None,
stderr: Option[String] = None,
result: Int = 0) {
result: Int = 0,
fileChecks: Map[String, File => Unit] = Map.empty) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice addition.

def testName: String = "args" + args.mkString("_")
def argsString: String = args.mkString(" ")
}
Expand Down Expand Up @@ -117,6 +121,7 @@ class ChiselMainSpec extends AnyFeatureSpec with GivenWhenThen with Matchers wit
And(s"file '$f' should be emitted in the target directory")
val out = new File(td.buildDir + s"/$f")
out should (exist)
p.fileChecks.get(f).map(_(out))
}
}
}
Expand Down Expand Up @@ -148,6 +153,33 @@ class ChiselMainSpec extends AnyFeatureSpec with GivenWhenThen with Matchers wit
).foreach(runStageExpectFiles)
}

Feature("Specifying a custom output file") {
runStageExpectFiles(ChiselMainTest(
args = Array("--chisel-output-file", "Foo", "--no-run-firrtl"),
generator = Some(classOf[SameTypesModule]),
stdout = Some(""),
files = Seq("Foo.fir"),
fileChecks = Map(
"Foo.fir" -> { file =>
And("It should be valid FIRRTL")
Parser.parse(Source.fromFile(file).mkString)
}
)
))
runStageExpectFiles(ChiselMainTest(
args = Array("--chisel-output-file", "Foo.pb", "--no-run-firrtl"),
generator = Some(classOf[SameTypesModule]),
stdout = Some(""),
files = Seq("Foo.pb"),
fileChecks = Map(
"Foo.pb" -> { file =>
And("It should be valid ProtoBuf")
firrtl.proto.FromProto.fromFile(file.toString)
}
)
))
}

info("As an aspect writer")
info("I write an aspect")
Feature("Running aspects via the command line") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// See LICENSE for license details.

package chiselTests.stage.phases


import chisel3.RawModule
import chisel3.stage.{ChiselGeneratorAnnotation, ChiselOutputFileAnnotation, CircuitSerializationAnnotation}
import chisel3.stage.CircuitSerializationAnnotation._
import chisel3.stage.phases.{AddSerializationAnnotations, AddImplicitOutputFile, Elaborate}

import firrtl.AnnotationSeq
import firrtl.options.{Phase, PhaseManager, Dependency, TargetDirAnnotation}
import firrtl.options.Viewer.view
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class AddSerializationAnnotationsSpec extends AnyFlatSpec with Matchers {

class Foo extends RawModule { override val desiredName = "Foo" }

class Fixture {
val phase: Phase = new AddSerializationAnnotations
val manager = new PhaseManager(Dependency[AddSerializationAnnotations] :: Nil)
}

behavior of classOf[AddSerializationAnnotations].toString

it should "default to FirrtlFileFormat" in new Fixture {
val annotations: AnnotationSeq = Seq(
ChiselGeneratorAnnotation(() => new Foo),
ChiselOutputFileAnnotation("Bar") )

manager
.transform(annotations)
.collect { case CircuitSerializationAnnotation(_, filename, format) => (filename, format) }
.toSeq should be (Seq(("Bar", FirrtlFileFormat)))
}

it should "support ProtoBufFileFormat" in new Fixture {
val annotations: AnnotationSeq = Seq(
ChiselGeneratorAnnotation(() => new Foo),
ChiselOutputFileAnnotation("Bar.pb") )

manager
.transform(annotations)
.collect { case CircuitSerializationAnnotation(_, filename, format) => (filename, format) }
.toSeq should be (Seq(("Bar", ProtoBufFileFormat)))
}

it should "support explicitly asking for FirrtlFileFormat" in new Fixture {
val annotations: AnnotationSeq = Seq(
ChiselGeneratorAnnotation(() => new Foo),
ChiselOutputFileAnnotation("Bar.pb.fir") )

manager
.transform(annotations)
.collect { case CircuitSerializationAnnotation(_, filename, format) => (filename, format) }
.toSeq should be (Seq(("Bar.pb", FirrtlFileFormat)))
}

}