diff options
12 files changed, 301 insertions, 75 deletions
diff --git a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala index 559a55bc..732bf8fc 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Aggregate.scala @@ -335,50 +335,93 @@ trait VecLike[T <: Data] extends collection.IndexedSeq[T] with HasId { SeqUtils.oneHotMux(indexWhereHelper(p)) } -/** Base class for data types defined as a bundle of other data types. +/** Base class for Aggregates based on key values pairs of String and Data * - * Usage: extend this class (either as an anonymous or named class) and define - * members variables of [[Data]] subtypes to be elements in the Bundle. + * Record should only be extended by libraries and fairly sophisticated generators. + * RTL writers should use [[Bundle]]. */ -class Bundle extends Aggregate { - private val _namespace = Builder.globalNamespace.child +abstract class Record extends Aggregate { - // TODO: replace with better defined FIRRTL weak-connect operator - /** Connect elements in this Bundle to elements in `that` on a best-effort - * (weak) basis, matching by type, orientation, and name. - * - * @note unconnected elements will NOT generate errors or warnings + /** The collection of [[Data]] * - * @example + * This underlying datastructure is a ListMap because the elements must + * remain ordered for serialization/deserialization. Elements added later + * are higher order when serialized (this is similar to [[Vec]]). For example: * {{{ - * // Pass through wires in this module's io to those mySubModule's io, - * // matching by type, orientation, and name, and ignoring extra wires. - * mySubModule.io <> io + * // Assume we have some type MyRecord that creates a Record from the ListMap + * val record = MyRecord(ListMap("fizz" -> UInt(16.W), "buzz" -> UInt(16.W))) + * // "buzz" is higher order because it was added later than "fizz" + * record("fizz") := "hdead".U + * record("buzz") := "hbeef".U + * val uint = record.asUInt + * assert(uint === "hbeefdead".U) // This will pass * }}} */ + val elements: ListMap[String, Data] - lazy val elements: ListMap[String, Data] = ListMap(namedElts:_*) + /** Name for Pretty Printing */ + def className: String = this.getClass.getSimpleName - /** Returns a best guess at whether a field in this Bundle is a user-defined - * Bundle element without looking at type signatures. - */ - private def isBundleField(m: java.lang.reflect.Method) = - m.getParameterTypes.isEmpty && - !java.lang.reflect.Modifier.isStatic(m.getModifiers) && - !(Bundle.keywords contains m.getName) && !(m.getName contains '$') + private[chisel3] def toType = { + def eltPort(elt: Data): String = { + val flipStr: String = if(Data.isFirrtlFlipped(elt)) "flip " else "" + s"${flipStr}${elt.getRef.name} : ${elt.toType}" + } + elements.toIndexedSeq.reverse.map(e => eltPort(e._2)).mkString("{", ", ", "}") + } - /** Returns a field's contained user-defined Bundle element if it appears to - * be one, otherwise returns None. - */ - private def getBundleField(m: java.lang.reflect.Method): Option[Data] = m.invoke(this) match { - case d: Data => Some(d) - case Some(d: Data) => Some(d) - case _ => None + private[chisel3] lazy val flatten = elements.toIndexedSeq.flatMap(_._2.flatten) + + // NOTE: This sets up dependent references, it can be done before closing the Module + private[chisel3] override def _onModuleClose: Unit = { // scalastyle:ignore method.name + val _namespace = Builder.globalNamespace.child + for ((name, elt) <- elements) { elt.setRef(this, _namespace.name(name)) } } - /** Returns a list of elements in this Bundle. + private[chisel3] final def allElements: Seq[Element] = elements.toIndexedSeq.flatMap(_._2.allElements) + + // Helper because Bundle elements are reversed before printing + private[chisel3] def toPrintableHelper(elts: Seq[(String, Data)]): Printable = { + val xs = + if (elts.isEmpty) List.empty[Printable] // special case because of dropRight below + else elts flatMap { case (name, data) => + List(PString(s"$name -> "), data.toPrintable, PString(", ")) + } dropRight 1 // Remove trailing ", " + PString(s"$className(") + Printables(xs) + PString(")") + } + /** Default "pretty-print" implementation + * Analogous to printing a Map + * Results in "$className(elt0.name -> elt0.value, ...)" + */ + def toPrintable: Printable = toPrintableHelper(elements.toList) +} + +/** Base class for data types defined as a bundle of other data types. + * + * Usage: extend this class (either as an anonymous or named class) and define + * members variables of [[Data]] subtypes to be elements in the Bundle. + */ +class Bundle extends Record { + override def className = "Bundle" + + /** The collection of [[Data]] + * + * Elements defined earlier in the Bundle are higher order upon + * serialization. For example: + * {{{ + * class MyBundle extends Bundle { + * val foo = UInt(16.W) + * val bar = UInt(16.W) + * } + * // Note that foo is higher order because its defined earlier in the Bundle + * val bundle = Wire(new MyBundle) + * bundle.foo := 0x1234.U + * bundle.bar := 0x5678.U + * val uint = bundle.asUInt + * assert(uint === "h12345678".U) // This will pass + * }}} */ - private[core] lazy val namedElts = { + final lazy val elements: ListMap[String, Data] = { val nameMap = LinkedHashMap[String, Data]() val seen = HashSet[Data]() for (m <- getPublicFields(classOf[Bundle])) { @@ -391,20 +434,17 @@ class Bundle extends Aggregate { } } } - ArrayBuffer(nameMap.toSeq:_*) sortWith {case ((an, a), (bn, b)) => (a._id > b._id) || ((a eq b) && (an > bn))} - } - private[chisel3] def toType = { - def eltPort(elt: Data): String = { - val flipStr: String = if(Data.isFirrtlFlipped(elt)) "flip " else "" - s"${flipStr}${elt.getRef.name} : ${elt.toType}" - } - s"{${namedElts.reverse.map(e => eltPort(e._2)).mkString(", ")}}" + ListMap(nameMap.toSeq sortWith { case ((an, a), (bn, b)) => (a._id > b._id) || ((a eq b) && (an > bn)) }: _*) } - private[chisel3] lazy val flatten = namedElts.flatMap(_._2.flatten) - private[chisel3] override def _onModuleClose: Unit = // scalastyle:ignore method.name - for ((name, elt) <- namedElts) { elt.setRef(this, _namespace.name(name)) } - private[chisel3] final def allElements: Seq[Element] = namedElts.flatMap(_._2.allElements) + /** Returns a field's contained user-defined Bundle element if it appears to + * be one, otherwise returns None. + */ + private def getBundleField(m: java.lang.reflect.Method): Option[Data] = m.invoke(this) match { + case d: Data => Some(d) + case Some(d: Data) => Some(d) + case _ => None + } override def cloneType : this.type = { // If the user did not provide a cloneType method, try invoking one of @@ -436,20 +476,14 @@ class Bundle extends Aggregate { /** Default "pretty-print" implementation * Analogous to printing a Map * Results in "Bundle(elt0.name -> elt0.value, ...)" + * @note The order is reversed from the order of elements in order to print + * the fields in the order they were defined */ - def toPrintable: Printable = { - val elts = - if (elements.isEmpty) List.empty[Printable] - else { - elements.toList.reverse flatMap { case (name, data) => - List(PString(s"$name -> "), data.toPrintable, PString(", ")) - } dropRight 1 // Remove trailing ", " - } - PString("Bundle(") + Printables(elts) + PString(")") - } + override def toPrintable: Printable = toPrintableHelper(elements.toList.reverse) } private[core] object Bundle { val keywords = List("flip", "asInput", "asOutput", "cloneType", "chiselCloneType", "toBits", "widthOption", "signalName", "signalPathName", "signalParent", "signalComponent") } + diff --git a/chiselFrontend/src/main/scala/chisel3/core/BiConnect.scala b/chiselFrontend/src/main/scala/chisel3/core/BiConnect.scala index 2599a20a..b17239e7 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/BiConnect.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/BiConnect.scala @@ -37,9 +37,9 @@ object BiConnect { def MismatchedVecException = BiConnectException(": Left and Right are different length Vecs.") def MissingLeftFieldException(field: String) = - BiConnectException(s".$field: Left Bundle missing field ($field).") + BiConnectException(s".$field: Left Record missing field ($field).") def MissingRightFieldException(field: String) = - BiConnectException(s": Right Bundle missing field ($field).") + BiConnectException(s": Right Record missing field ($field).") def MismatchedException(left: String, right: String) = BiConnectException(s": Left ($left) and Right ($right) have different types.") @@ -67,20 +67,20 @@ object BiConnect { } } } - // Handle Bundle case - case (left_b: Bundle, right_b: Bundle) => { + // Handle Record case + case (left_r: Record, right_r: Record) => { // Verify right has no extra fields that left doesn't have - for((field, right_sub) <- right_b.elements) { - if(!left_b.elements.isDefinedAt(field)) { + for((field, right_sub) <- right_r.elements) { + if(!left_r.elements.isDefinedAt(field)) { if (connectCompileOptions.connectFieldsMustMatch) { throw MissingLeftFieldException(field) } } } // For each field in left, descend with right - for((field, left_sub) <- left_b.elements) { + for((field, left_sub) <- left_r.elements) { try { - right_b.elements.get(field) match { + right_r.elements.get(field) match { case Some(right_sub) => connect(sourceInfo, connectCompileOptions, left_sub, right_sub, context_mod) case None => { if (connectCompileOptions.connectFieldsMustMatch) { diff --git a/chiselFrontend/src/main/scala/chisel3/core/Binding.scala b/chiselFrontend/src/main/scala/chisel3/core/Binding.scala index 3dfde7c2..71c441a7 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Binding.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Binding.scala @@ -33,7 +33,7 @@ import chisel3.internal.Builder.{forcedModule} * Aggregate is considered bound via its elements. May be appropriate to allow * Aggregates to be bound along with the Elements. However, certain literal and * port direction information doesn't quite make sense in aggregates. This would - * elegantly handle the empty Vec or Bundle problem though. + * elegantly handle the empty Vec or Record problem though. * * TODO(twigg): Binding is currently done via allElements. It may be more * elegant if this was instead done as a more explicit tree walk as that allows @@ -73,8 +73,8 @@ object Binding { } } } - case (bundle: Bundle) => { - for((field, subelem) <- bundle.elements) { + case (record: Record) => { + for((field, subelem) <- record.elements) { try walkToBinding(subelem, checker) catch { case BindingException(message) => throw BindingException(s".$field$message") diff --git a/chiselFrontend/src/main/scala/chisel3/core/CompileOptions.scala b/chiselFrontend/src/main/scala/chisel3/core/CompileOptions.scala index 4dea39b5..4aa3ad33 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/CompileOptions.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/CompileOptions.scala @@ -5,7 +5,7 @@ package chisel3.core import scala.language.experimental.macros trait CompileOptions { - // Should Bundle connections require a strict match of fields. + // Should Record connections require a strict match of fields. // If true and the same fields aren't present in both source and sink, a MissingFieldException, // MissingLeftFieldException, or MissingRightFieldException will be thrown. val connectFieldsMustMatch: Boolean diff --git a/chiselFrontend/src/main/scala/chisel3/core/Data.scala b/chiselFrontend/src/main/scala/chisel3/core/Data.scala index 6e80f045..4f3e2268 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Data.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Data.scala @@ -66,7 +66,7 @@ object Data { * Note that the current scheme only applies Flip to Elements or Vec chains of * Elements. * - * A Bundle is never marked flip, instead preferring its root fields to be marked + * A Record is never marked flip, instead preferring its root fields to be marked * * The Vec check is due to the fact that flip must be factored out of the vec, ie: * must have flip field: Vec(UInt) instead of field: Vec(flip UInt) @@ -74,7 +74,7 @@ object Data { private[chisel3] def isFlipped(target: Data): Boolean = target match { case (element: Element) => element.binding.direction == Some(Direction.Input) case (vec: Vec[Data @unchecked]) => isFlipped(vec.sample_element) - case (bundle: Bundle) => false + case (record: Record) => false } /** This function returns the "firrtl" flipped-ness for the specified object. diff --git a/chiselFrontend/src/main/scala/chisel3/core/Module.scala b/chiselFrontend/src/main/scala/chisel3/core/Module.scala index 76a3b240..609f2ccf 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/Module.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/Module.scala @@ -141,7 +141,7 @@ extends HasId { /** IO for this Module. At the Scala level (pre-FIRRTL transformations), * connections in and out of a Module may only go through `io` elements. */ - def io: Bundle + def io: Record val clock = Port(Input(Clock())) val reset = Port(Input(Bool())) diff --git a/chiselFrontend/src/main/scala/chisel3/core/MonoConnect.scala b/chiselFrontend/src/main/scala/chisel3/core/MonoConnect.scala index fcb14e6f..8b264801 100644 --- a/chiselFrontend/src/main/scala/chisel3/core/MonoConnect.scala +++ b/chiselFrontend/src/main/scala/chisel3/core/MonoConnect.scala @@ -15,7 +15,7 @@ import chisel3.internal.sourceinfo.{DeprecatedSourceInfo, SourceInfo, SourceInfo * * The connect operation will recurse down the left Data (with the right Data). * An exception will be thrown if a movement through the left cannot be matched -* in the right. The right side is allowed to have extra Bundle fields. +* in the right. The right side is allowed to have extra Record fields. * Vecs must still be exactly the same size. * * See elemConnect for details on how the root connections are issued. @@ -45,7 +45,7 @@ object MonoConnect { def MismatchedVecException = MonoConnectException(": Sink and Source are different length Vecs.") def MissingFieldException(field: String) = - MonoConnectException(s": Source Bundle missing field ($field).") + MonoConnectException(s": Source Record missing field ($field).") def MismatchedException(sink: String, source: String) = MonoConnectException(s": Sink ($sink) and Source ($source) have different types.") @@ -73,12 +73,12 @@ object MonoConnect { } } } - // Handle Bundle case - case (sink_b: Bundle, source_b: Bundle) => { + // Handle Record case + case (sink_r: Record, source_r: Record) => { // For each field, descend with right - for((field, sink_sub) <- sink_b.elements) { + for((field, sink_sub) <- sink_r.elements) { try { - source_b.elements.get(field) match { + source_r.elements.get(field) match { case Some(source_sub) => connect(sourceInfo, connectCompileOptions, sink_sub, source_sub, context_mod) case None => { if (connectCompileOptions.connectFieldsMustMatch) { diff --git a/chiselFrontend/src/main/scala/chisel3/internal/Builder.scala b/chiselFrontend/src/main/scala/chisel3/internal/Builder.scala index 6e463311..c93dbfc7 100644 --- a/chiselFrontend/src/main/scala/chisel3/internal/Builder.scala +++ b/chiselFrontend/src/main/scala/chisel3/internal/Builder.scala @@ -92,7 +92,7 @@ private[chisel3] trait HasId extends InstanceId { // Uses a namespace to convert suggestion into a true name // Will not do any naming if the reference already assigned. - // (e.g. tried to suggest a name to part of a Bundle) + // (e.g. tried to suggest a name to part of a Record) private[chisel3] def forceName(default: =>String, namespace: Namespace): Unit = if(_ref.isEmpty) { val candidate_name = suggested_name.getOrElse(default) diff --git a/src/main/scala/chisel3/compatibility.scala b/src/main/scala/chisel3/compatibility.scala index 613385af..abbe8ffe 100644 --- a/src/main/scala/chisel3/compatibility.scala +++ b/src/main/scala/chisel3/compatibility.scala @@ -36,6 +36,7 @@ package object Chisel { // scalastyle:ignore package.object.name val Vec = chisel3.core.Vec type Vec[T <: Data] = chisel3.core.Vec[T] type VecLike[T <: Data] = chisel3.core.VecLike[T] + type Record = chisel3.core.Record type Bundle = chisel3.core.Bundle val assert = chisel3.core.assert diff --git a/src/main/scala/chisel3/package.scala b/src/main/scala/chisel3/package.scala index 29095243..cba8dffe 100644 --- a/src/main/scala/chisel3/package.scala +++ b/src/main/scala/chisel3/package.scala @@ -26,6 +26,7 @@ package object chisel3 { // scalastyle:ignore package.object.name type Vec[T <: Data] = chisel3.core.Vec[T] type VecLike[T <: Data] = chisel3.core.VecLike[T] type Bundle = chisel3.core.Bundle + type Record = chisel3.core.Record val assert = chisel3.core.assert diff --git a/src/test/scala/chiselTests/BundleSpec.scala b/src/test/scala/chiselTests/BundleSpec.scala new file mode 100644 index 00000000..0a6866d3 --- /dev/null +++ b/src/test/scala/chiselTests/BundleSpec.scala @@ -0,0 +1,71 @@ +// See LICENSE for license details. + +package chiselTests + +import chisel3._ +import chisel3.testers.BasicTester + +trait BundleSpecUtils { + class BundleFooBar extends Bundle { + val foo = UInt(16.W) + val bar = UInt(16.W) + override def cloneType = (new BundleFooBar).asInstanceOf[this.type] + } + class BundleBarFoo extends Bundle { + val bar = UInt(16.W) + val foo = UInt(16.W) + override def cloneType = (new BundleBarFoo).asInstanceOf[this.type] + } + class BundleFoo extends Bundle { + val foo = UInt(16.W) + override def cloneType = (new BundleFoo).asInstanceOf[this.type] + } + class BundleBar extends Bundle { + val bar = UInt(16.W) + override def cloneType = (new BundleBar).asInstanceOf[this.type] + } + + class MyModule(output: Bundle, input: Bundle) extends Module { + val io = IO(new Bundle { + val in = Input(input) + val out = Output(output) + }) + io.out <> io.in + } + + class BundleSerializationTest extends BasicTester { + // Note that foo is higher order because its defined earlier in the Bundle + val bundle = Wire(new BundleFooBar) + bundle.foo := 0x1234.U + bundle.bar := 0x5678.U + // To UInt + val uint = bundle.asUInt + assert(uint.getWidth == 32) // elaboration time + assert(uint === "h12345678".asUInt(32.W)) + // Back to Bundle + val bundle2 = (new BundleFooBar).fromBits(uint) + assert(0x1234.U === bundle2.foo) + assert(0x5678.U === bundle2.bar) + stop() + } +} + +class BundleSpec extends ChiselFlatSpec with BundleSpecUtils { + "Bundles with the same fields but in different orders" should "bulk connect" in { + elaborate { new MyModule(new BundleFooBar, new BundleBarFoo) } + } + + "Bundles" should "follow UInt serialization/deserialization API" in { + assertTesterPasses { new BundleSerializationTest } + } + + "Bulk connect on Bundles" should "check that the fields match" in { + (the [ChiselException] thrownBy { + elaborate { new MyModule(new BundleFooBar, new BundleFoo) } + }).getMessage should include ("Right Record missing field") + + (the [ChiselException] thrownBy { + elaborate { new MyModule(new BundleFoo, new BundleFooBar) } + }).getMessage should include ("Left Record missing field") + } +} diff --git a/src/test/scala/chiselTests/RecordSpec.scala b/src/test/scala/chiselTests/RecordSpec.scala new file mode 100644 index 00000000..c65693ed --- /dev/null +++ b/src/test/scala/chiselTests/RecordSpec.scala @@ -0,0 +1,119 @@ +// See LICENSE for license details. + +package chiselTests + +import chisel3._ +import chisel3.testers.BasicTester +import chisel3.util.{Counter, Queue} +import scala.collection.immutable.ListMap + +trait RecordSpecUtils { + // An example of how Record might be extended + // In this case, CustomBundle is a Record constructed from a Tuple of (String, Data) + // it is a possible implementation of a programmatic "Bundle" + // (and can by connected to MyBundle below) + final class CustomBundle(elts: (String, Data)*) extends Record { + val elements = ListMap(elts map { case (field, elt) => field -> elt.chiselCloneType }: _*) + def apply(elt: String): Data = elements(elt) + override def cloneType = (new CustomBundle(elements.toList: _*)).asInstanceOf[this.type] + } + class MyBundle extends Bundle { + val foo = UInt(32.W) + val bar = UInt(32.W) + override def cloneType = (new MyBundle).asInstanceOf[this.type] + } + // Useful for constructing types from CustomBundle + val fooBarType = new CustomBundle("foo" -> UInt(32.W), "bar" -> UInt(32.W)) + + class MyModule(output: => Record, input: => Record) extends Module { + val io = IO(new Bundle { + val in = Input(input) + val out = Output(output) + }) + io.out <> io.in + } + + class RecordSerializationTest extends BasicTester { + val recordType = new CustomBundle("fizz" -> UInt(16.W), "buzz" -> UInt(16.W)) + val record = Wire(recordType) + // Note that "buzz" was added later than "fizz" and is therefore higher order + record("fizz") := "hdead".U + record("buzz") := "hbeef".U + // To UInt + val uint = record.asUInt + assert(uint.getWidth == 32) // elaboration time + assert(uint === "hbeefdead".U) + // Back to Record + val record2 = recordType.fromBits(uint) + assert("hdead".U === record2("fizz").asUInt) + assert("hbeef".U === record2("buzz").asUInt) + stop() + } + + class RecordQueueTester extends BasicTester { + val queue = Module(new Queue(fooBarType, 4)) + queue.io.enq.valid := false.B + val (cycle, done) = Counter(true.B, 4) + + when (cycle === 0.U) { + queue.io.enq.bits("foo") := 1234.U + queue.io.enq.bits("bar") := 5678.U + queue.io.enq.valid := true.B + } + when (cycle === 1.U) { + queue.io.deq.ready := true.B + assert(queue.io.deq.valid === true.B) + assert(queue.io.deq.bits("foo").asUInt === 1234.U) + assert(queue.io.deq.bits("bar").asUInt === 5678.U) + } + when (done) { + stop() + } + } + + class RecordIOModule extends Module { + val io = IO(new CustomBundle("in" -> Input(UInt(32.W)), "out" -> Output(UInt(32.W)))) + io("out") := io("in") + } + + class RecordIOTester extends BasicTester { + val mod = Module(new RecordIOModule) + mod.io("in") := 1234.U + assert(mod.io("out").asUInt === 1234.U) + stop() + } +} + +class RecordSpec extends ChiselFlatSpec with RecordSpecUtils { + behavior of "Records" + + they should "bulk connect similarly to Bundles" in { + elaborate { new MyModule(fooBarType, fooBarType) } + } + + they should "bulk connect to Bundles" in { + elaborate { new MyModule(new MyBundle, fooBarType) } + } + + they should "follow UInt serialization/deserialization API" in { + assertTesterPasses { new RecordSerializationTest } + } + + they should "work as the type of a Queue" in { + assertTesterPasses { new RecordQueueTester } + } + + they should "work as the type of a Module's io" in { + assertTesterPasses { new RecordIOTester } + } + + "Bulk connect on Record" should "check that the fields match" in { + (the [ChiselException] thrownBy { + elaborate { new MyModule(fooBarType, new CustomBundle("bar" -> UInt(32.W))) } + }).getMessage should include ("Right Record missing field") + + (the [ChiselException] thrownBy { + elaborate { new MyModule(new CustomBundle("bar" -> UInt(32.W)), fooBarType) } + }).getMessage should include ("Left Record missing field") + } +} |
