diff options
26 files changed, 2307 insertions, 78 deletions
@@ -235,7 +235,8 @@ lazy val docs = project // new documentation project scalacOptions += "-language:reflectiveCalls", mdocIn := file("docs/src"), mdocOut := file("docs/generated"), - mdocExtraArguments := Seq("--cwd", "docs"), + // None of our links are hygienic because they're primarily used on the website with .html + mdocExtraArguments := Seq("--cwd", "docs", "--no-link-hygiene"), mdocVariables := Map( "BUILD_DIR" -> "docs-target" // build dir for mdoc programs to dump temp files ) diff --git a/core/src/main/scala-2.12/scala/collection/immutable/package.scala b/core/src/main/scala-2.12/scala/collection/immutable/package.scala index 7ae87d9b..c06935d0 100644 --- a/core/src/main/scala-2.12/scala/collection/immutable/package.scala +++ b/core/src/main/scala-2.12/scala/collection/immutable/package.scala @@ -10,4 +10,7 @@ package object immutable { val VectorMap = ListMap type VectorMap[K, +V] = ListMap[K, V] + + val LazyList = Stream + type LazyList[+A] = Stream[A] } diff --git a/core/src/main/scala/chisel3/Aggregate.scala b/core/src/main/scala/chisel3/Aggregate.scala index 3a766628..58bc5ccb 100644 --- a/core/src/main/scala/chisel3/Aggregate.scala +++ b/core/src/main/scala/chisel3/Aggregate.scala @@ -3,6 +3,7 @@ package chisel3 import chisel3.experimental.VecLiterals.AddVecLiteralConstructor +import chisel3.experimental.dataview.{InvalidViewException, isView} import scala.collection.immutable.{SeqMap, VectorMap} import scala.collection.mutable.{HashSet, LinkedHashMap} @@ -88,44 +89,6 @@ sealed abstract class Aggregate extends Data { } } - // Returns pairs of all fields, element-level and containers, in a Record and their path names - private[chisel3] def getRecursiveFields(data: Data, path: String): Seq[(Data, String)] = data match { - case data: Record => - data.elements.map { case (fieldName, fieldData) => - getRecursiveFields(fieldData, s"$path.$fieldName") - }.fold(Seq(data -> path)) { - _ ++ _ - } - case data: Vec[_] => - data.getElements.zipWithIndex.map { case (fieldData, fieldIndex) => - getRecursiveFields(fieldData, path = s"$path($fieldIndex)") - }.fold(Seq(data -> path)) { - _ ++ _ - } - case data => Seq(data -> path) // we don't support or recurse into other Aggregate types here - } - - - // Returns pairs of corresponding fields between two Records of the same type - private[chisel3] def getMatchedFields(x: Data, y: Data): Seq[(Data, Data)] = (x, y) match { - case (x: Element, y: Element) => - require(x typeEquivalent y) - Seq(x -> y) - case (x: Record, y: Record) => - (x.elements zip y.elements).map { case ((xName, xElt), (yName, yElt)) => - require(xName == yName) // assume fields returned in same, deterministic order - getMatchedFields(xElt, yElt) - }.fold(Seq(x -> y)) { - _ ++ _ - } - case (x: Vec[_], y: Vec[_]) => - (x.getElements zip y.getElements).map { case (xElt, yElt) => - getMatchedFields(xElt, yElt) - }.fold(Seq(x -> y)) { - _ ++ _ - } - } - override def do_asUInt(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): UInt = { SeqUtils.do_asUInt(flatten.map(_.asUInt())) } @@ -297,6 +260,9 @@ sealed class Vec[T <: Data] private[chisel3] (gen: => T, val length: Int) def do_apply(p: UInt)(implicit compileOptions: CompileOptions): T = { requireIsHardware(this, "vec") requireIsHardware(p, "vec index") + if (isView(this)) { + throw InvalidViewException("Dynamic indexing of Views is not yet supported") + } val port = gen // Reconstruct the resolvedDirection (in Aggregate.bind), since it's not stored. diff --git a/core/src/main/scala/chisel3/Data.scala b/core/src/main/scala/chisel3/Data.scala index 5513035b..32d83008 100644 --- a/core/src/main/scala/chisel3/Data.scala +++ b/core/src/main/scala/chisel3/Data.scala @@ -2,13 +2,16 @@ package chisel3 +import chisel3.experimental.dataview.reify + import scala.language.experimental.macros -import chisel3.experimental.{Analog, DataMirror, FixedPoint, Interval} +import chisel3.experimental.{Analog, BaseModule, DataMirror, FixedPoint, Interval} import chisel3.internal.Builder.pushCommand import chisel3.internal._ import chisel3.internal.firrtl._ import chisel3.internal.sourceinfo.{DeprecatedSourceInfo, SourceInfo, SourceInfoTransform, UnlocatableSourceInfo} +import scala.collection.immutable.LazyList // Needed for 2.12 alias import scala.util.Try /** User-specified directions. @@ -236,6 +239,62 @@ private[chisel3] object cloneSupertype { } } +// Returns pairs of all fields, element-level and containers, in a Record and their path names +private[chisel3] object getRecursiveFields { + def apply(data: Data, path: String): Seq[(Data, String)] = data match { + case data: Record => + data.elements.map { case (fieldName, fieldData) => + getRecursiveFields(fieldData, s"$path.$fieldName") + }.fold(Seq(data -> path)) { + _ ++ _ + } + case data: Vec[_] => + data.getElements.zipWithIndex.map { case (fieldData, fieldIndex) => + getRecursiveFields(fieldData, path = s"$path($fieldIndex)") + }.fold(Seq(data -> path)) { + _ ++ _ + } + case data: Element => Seq(data -> path) + } + + def lazily(data: Data, path: String): Seq[(Data, String)] = data match { + case data: Record => + LazyList(data -> path) ++ + data.elements.view.flatMap { case (fieldName, fieldData) => + getRecursiveFields(fieldData, s"$path.$fieldName") + } + case data: Vec[_] => + LazyList(data -> path) ++ + data.getElements.view.zipWithIndex.flatMap { case (fieldData, fieldIndex) => + getRecursiveFields(fieldData, path = s"$path($fieldIndex)") + } + case data: Element => LazyList(data -> path) + } +} + +// Returns pairs of corresponding fields between two Records of the same type +// TODO it seems wrong that Elements are checked for typeEquivalence in Bundle and Vec lit creation +private[chisel3] object getMatchedFields { + def apply(x: Data, y: Data): Seq[(Data, Data)] = (x, y) match { + case (x: Element, y: Element) => + require(x typeEquivalent y) + Seq(x -> y) + case (x: Record, y: Record) => + (x.elements zip y.elements).map { case ((xName, xElt), (yName, yElt)) => + require(xName == yName) // assume fields returned in same, deterministic order + getMatchedFields(xElt, yElt) + }.fold(Seq(x -> y)) { + _ ++ _ + } + case (x: Vec[_], y: Vec[_]) => + (x.getElements zip y.getElements).map { case (xElt, yElt) => + getMatchedFields(xElt, yElt) + }.fold(Seq(x -> y)) { + _ ++ _ + } + } +} + /** Returns the chisel type of a hardware object, allowing other hardware to be constructed from it. */ object chiselTypeOf { @@ -488,6 +547,14 @@ abstract class Data extends HasId with NamedComponent with SourceInfoDoc { } requireIsHardware(this) topBindingOpt match { + // DataView + case Some(ViewBinding(target)) => reify(target).ref + case Some(AggregateViewBinding(viewMap, _)) => + viewMap.get(this) match { + case None => materializeWire() // FIXME FIRRTL doesn't have Aggregate Init expressions + // This should not be possible because Element does the lookup in .topBindingOpt + case x: Some[_] => throwException(s"Internal Error: In .ref for $this got '$topBindingOpt' and '$x'") + } // Literals case Some(ElementLitBinding(litArg)) => litArg case Some(BundleLitBinding(litMap)) => @@ -514,6 +581,20 @@ abstract class Data extends HasId with NamedComponent with SourceInfoDoc { } } + + // Recursively set the parent of the start Data and any children (eg. in an Aggregate) + private[chisel3] def setAllParents(parent: Option[BaseModule]): Unit = { + def rec(data: Data): Unit = { + data._parent = parent + data match { + case _: Element => + case agg: Aggregate => + agg.getElements.foreach(rec) + } + } + rec(this) + } + private[chisel3] def width: Width private[chisel3] def legacyConnect(that: Data)(implicit sourceInfo: SourceInfo): Unit diff --git a/core/src/main/scala/chisel3/Element.scala b/core/src/main/scala/chisel3/Element.scala index 40291b12..bc006922 100644 --- a/core/src/main/scala/chisel3/Element.scala +++ b/core/src/main/scala/chisel3/Element.scala @@ -34,6 +34,10 @@ abstract class Element extends Data { case Some(litArg) => Some(ElementLitBinding(litArg)) case _ => Some(DontCareBinding()) } + case Some(b @ AggregateViewBinding(viewMap, _)) => viewMap.get(this) match { + case Some(elt) => Some(ViewBinding(elt)) + case _ => throwException(s"Internal Error! $this missing from topBinding $b") + } case topBindingOpt => topBindingOpt } diff --git a/core/src/main/scala/chisel3/Module.scala b/core/src/main/scala/chisel3/Module.scala index 41fe4554..64065ec9 100644 --- a/core/src/main/scala/chisel3/Module.scala +++ b/core/src/main/scala/chisel3/Module.scala @@ -184,7 +184,7 @@ package internal { object BaseModule { // Private internal class to serve as a _parent for Data in cloned ports - private[chisel3] class ModuleClone(_proto: BaseModule) extends BaseModule { + private[chisel3] class ModuleClone(_proto: BaseModule) extends PseudoModule { // ClonePorts that hold the bound ports for this module // Used for setting the refs of both this module and the Record private[BaseModule] var _portsRecord: Record = _ @@ -195,7 +195,7 @@ package internal { _component = _proto._component None } - // This module doesn't acutally exist in the FIRRTL so no initialization to do + // This module doesn't actually exist in the FIRRTL so no initialization to do private[chisel3] def initializeInParent(parentCompileOptions: CompileOptions): Unit = () override def desiredName: String = _proto.name @@ -226,19 +226,6 @@ package internal { override def cloneType = (new ClonePorts(elts: _*)).asInstanceOf[this.type] } - // Recursively set the parent of the start Data and any children (eg. in an Aggregate) - private def setAllParents(start: Data, parent: Option[BaseModule]): Unit = { - def rec(data: Data): Unit = { - data._parent = parent - data match { - case _: Element => - case agg: Aggregate => - agg.getElements.foreach(rec) - } - } - rec(start) - } - private[chisel3] def cloneIORecord(proto: BaseModule)(implicit sourceInfo: SourceInfo, compileOptions: CompileOptions): ClonePorts = { require(proto.isClosed, "Can't clone a module before module close") // Fake Module to serve as the _parent of the cloned ports @@ -249,7 +236,7 @@ package internal { // currentModule (and not clonePorts) val clonePorts = new ClonePorts(proto.getModulePorts: _*) clonePorts.bind(PortBinding(cloneParent)) - setAllParents(clonePorts, Some(cloneParent)) + clonePorts.setAllParents(Some(cloneParent)) cloneParent._portsRecord = clonePorts // Normally handled during Module construction but ClonePorts really lives in its parent's parent if (!compileOptions.explicitInvalidate) { @@ -303,7 +290,15 @@ package experimental { } } - protected def getIds = { + // Returns the last id contained within a Module + private[chisel3] def _lastId: Long = _ids.last match { + case mod: BaseModule => mod._lastId + case _ => + // Ideally we could just take last._id, but Records store and thus bind their Data in reverse order + _ids.maxBy(_._id)._id + } + + private[chisel3] def getIds = { require(_closed, "Can't get ids before module close") _ids.toSeq } @@ -368,10 +363,10 @@ package experimental { /** Legalized name of this module. */ final lazy val name = try { - // ModuleAspects and ModuleClones are not "true modules" and thus should share + // PseudoModules are not "true modules" and thus should share // their original modules names without uniquification this match { - case (_: ModuleAspect | _: internal.BaseModule.ModuleClone) => desiredName + case _: PseudoModule => desiredName case _ => Builder.globalNamespace.name(desiredName) } } catch { @@ -399,7 +394,14 @@ package experimental { final def toAbsoluteTarget: IsModule = { _parent match { case Some(parent) => parent.toAbsoluteTarget.instOf(this.instanceName, toTarget.module) - case None => toTarget + case None => + // FIXME Special handling for Views - evidence of "weirdness" of .toAbsoluteTarget + // In theory, .toAbsoluteTarget should not be necessary, .toTarget combined with the + // target disambiguation in FIRRTL's deduplication transform should ensure that .toTarget + // is always unambigous. However, legacy workarounds for Chisel's lack of an instance API + // have lead some to use .toAbsoluteTarget as a workaround. A proper instance API will make + // it possible to deprecate and remove .toAbsoluteTarget + if (this == ViewParent) ViewParent.absoluteTarget else toTarget } } diff --git a/core/src/main/scala/chisel3/ModuleAspect.scala b/core/src/main/scala/chisel3/ModuleAspect.scala index 4e37ab35..a528fa72 100644 --- a/core/src/main/scala/chisel3/ModuleAspect.scala +++ b/core/src/main/scala/chisel3/ModuleAspect.scala @@ -2,7 +2,7 @@ package chisel3 -import chisel3.internal.Builder +import chisel3.internal.{Builder, PseudoModule} /** Used by Chisel Aspects to inject Chisel code into modules, after they have been elaborated. * This is an internal API - don't use! @@ -13,7 +13,7 @@ import chisel3.internal.Builder * @param moduleCompileOptions */ abstract class ModuleAspect private[chisel3] (module: RawModule) - (implicit moduleCompileOptions: CompileOptions) extends RawModule { + (implicit moduleCompileOptions: CompileOptions) extends RawModule with PseudoModule { Builder.addAspect(module, this) diff --git a/core/src/main/scala/chisel3/RawModule.scala b/core/src/main/scala/chisel3/RawModule.scala index fadb8dae..74e9db6c 100644 --- a/core/src/main/scala/chisel3/RawModule.scala +++ b/core/src/main/scala/chisel3/RawModule.scala @@ -11,6 +11,7 @@ import chisel3.internal.BaseModule.ModuleClone import chisel3.internal.Builder._ import chisel3.internal.firrtl._ import chisel3.internal.sourceinfo.UnlocatableSourceInfo +import _root_.firrtl.annotations.{IsModule, ModuleTarget} /** Abstract base class for Modules that contain Chisel RTL. * This abstract base class is a user-defined module which does not include implicit clock and reset and supports @@ -157,6 +158,9 @@ trait RequireSyncReset extends Module { package object internal { + /** Marker trait for modules that are not true modules */ + private[chisel3] trait PseudoModule extends BaseModule + // Private reflective version of "val io" to maintain Chisel.Module semantics without having // io as a virtual method. See https://github.com/freechipsproject/chisel3/pull/1550 for more // information about the removal of "val io" @@ -264,4 +268,31 @@ package object internal { } } } + + /** Internal API for [[ViewParent]] */ + sealed private[chisel3] class ViewParentAPI extends RawModule()(ExplicitCompileOptions.Strict) with PseudoModule { + // We must provide `absoluteTarget` but not `toTarget` because otherwise they would be exactly + // the same and we'd have no way to distinguish the kind of target when renaming view targets in + // the Converter + // Note that this is not overriding .toAbsoluteTarget, that is a final def in BaseModule that delegates + // to this method + private[chisel3] val absoluteTarget: IsModule = ModuleTarget(this.circuitName, "_$$AbsoluteView$$_") + + // This module is not instantiable + override private[chisel3] def generateComponent(): Option[Component] = None + override private[chisel3] def initializeInParent(parentCompileOptions: CompileOptions): Unit = () + // This module is not really part of the circuit + _parent = None + + // Sigil to mark views, starts with '_' to make it a legal FIRRTL target + override def desiredName = "_$$View$$_" + + private[chisel3] val fakeComponent: Component = DefModule(this, desiredName, Nil, Nil) + } + + /** Special internal object representing the parent of all views + * + * @note this is a val instead of an object because of the need to wrap in Module(...) + */ + private[chisel3] val ViewParent = Module.do_apply(new ViewParentAPI)(UnlocatableSourceInfo, ExplicitCompileOptions.Strict) } diff --git a/core/src/main/scala/chisel3/experimental/dataview/DataProduct.scala b/core/src/main/scala/chisel3/experimental/dataview/DataProduct.scala new file mode 100644 index 00000000..55dd8505 --- /dev/null +++ b/core/src/main/scala/chisel3/experimental/dataview/DataProduct.scala @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 + +package chisel3.experimental.dataview + +import chisel3.experimental.BaseModule +import chisel3.{Data, getRecursiveFields} + +import scala.annotation.implicitNotFound + +/** Typeclass interface for getting elements of type [[Data]] + * + * This is needed for validating [[DataView]]s targeting type `A`. + * Can be thought of as "can be the Target of a DataView". + * + * Chisel provides some implementations in [[DataProduct$ object DataProduct]] that are available + * by default in the implicit scope. + * + * @tparam A Type that has elements of type [[Data]] + * @see [[https://www.chisel-lang.org/chisel3/docs/explanations/dataview#dataproduct Detailed Documentation]] + */ +@implicitNotFound("Could not find implicit value for DataProduct[${A}].\n" + + "Please see https://www.chisel-lang.org/chisel3/docs/explanations/dataview#dataproduct") +trait DataProduct[-A] { + /** Provides [[Data]] elements within some containing object + * + * @param a Containing object + * @param path Hierarchical path to current signal (for error reporting) + * @return Data elements and associated String paths (Strings for error reporting only!) + */ + def dataIterator(a: A, path: String): Iterator[(Data, String)] + + /** Returns a checker to test if the containing object contains a `Data` object + * @note Implementers may want to override if iterating on all `Data` is expensive for `A` and `A` + * will primarily be used in `PartialDataViews` + * @note The returned value is a function, not a true Set, but is describing the functionality of + * Set containment + * @param a Containing object + * @return A checker that itself returns True if a given `Data` is contained in `a` + * as determined by an `==` test + */ + def dataSet(a: A): Data => Boolean = dataIterator(a, "").map(_._1).toSet +} + +/** Encapsulating object for automatically provided implementations of [[DataProduct]] + * + * @note DataProduct implementations provided in this object are available in the implicit scope + */ +object DataProduct { + /** [[DataProduct]] implementation for [[Data]] */ + implicit val dataDataProduct: DataProduct[Data] = new DataProduct[Data] { + def dataIterator(a: Data, path: String): Iterator[(Data, String)] = + getRecursiveFields.lazily(a, path).iterator + } + + /** [[DataProduct]] implementation for [[BaseModule]] */ + implicit val userModuleDataProduct: DataProduct[BaseModule] = new DataProduct[BaseModule] { + def dataIterator(a: BaseModule, path: String): Iterator[(Data, String)] = { + a.getIds.iterator.flatMap { + case d: Data if d.getOptionRef.isDefined => // Using ref to decide if it's truly hardware in the module + Seq(d -> s"${path}.${d.instanceName}") + case b: BaseModule => dataIterator(b, s"$path.${b.instanceName}") + case _ => Seq.empty + } + } + // Overridden for performance + override def dataSet(a: BaseModule): Data => Boolean = { + val lastId = a._lastId // Not cheap to compute + // Return a function + e => e._id > a._id && e._id <= lastId + } + } +} diff --git a/core/src/main/scala/chisel3/experimental/dataview/DataView.scala b/core/src/main/scala/chisel3/experimental/dataview/DataView.scala new file mode 100644 index 00000000..caf004c2 --- /dev/null +++ b/core/src/main/scala/chisel3/experimental/dataview/DataView.scala @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 + +package chisel3.experimental.dataview + +import chisel3._ +import chisel3.internal.sourceinfo.SourceInfo +import scala.reflect.runtime.universe.WeakTypeTag + +import annotation.implicitNotFound + + +/** Mapping between a target type `T` and a view type `V` + * + * Enables calling `.viewAs[T]` on instances of the target type. + * + * ==Detailed documentation== + * - [[https://www.chisel-lang.org/chisel3/docs/explanations/dataview Explanation]] + * - [[https://www.chisel-lang.org/chisel3/docs/cookbooks/dataview Cookbook]] + * + * @example {{{ + * class Foo(val w: Int) extends Bundle { + * val a = UInt(w.W) + * } + * class Bar(val w: Int) extends Bundle { + * val b = UInt(w.W) + * } + * // DataViews are created using factory methods in the companion object + * implicit val view = DataView[Foo, Bar]( + * // The first argument is a function constructing a Foo from a Bar + * foo => new Bar(foo.w) + * // The remaining arguments are a variable number of field pairings + * _.a -> _.b + * ) + * }}} + * + * @tparam T Target type (must have an implementation of [[DataProduct]]) + * @tparam V View type + * @see [[DataView$ object DataView]] for factory methods + * @see [[PartialDataView object PartialDataView]] for defining non-total `DataViews` + */ +@implicitNotFound("Could not find implicit value for DataView[${T}, ${V}].\n" + + "Please see https://www.chisel-lang.org/chisel3/docs/explanations/dataview") +sealed class DataView[T : DataProduct, V <: Data] private[chisel3] ( + /** Function constructing an object of the View type from an object of the Target type */ + private[chisel3] val mkView: T => V, + /** Function that returns corresponding fields of the target and view */ + private[chisel3] val mapping: (T, V) => Iterable[(Data, Data)], + // Aliasing this with a def below to make the ScalaDoc show up for the field + _total: Boolean +)( + implicit private[chisel3] val sourceInfo: SourceInfo +) { + /** Indicates if the mapping contains every field of the target */ + def total: Boolean = _total + + override def toString: String = { + val base = sourceInfo.makeMessage(x => x) + val loc = if (base.nonEmpty) base else "@unknown" + val name = if (total) "DataView" else "PartialDataView" + s"$name(defined $loc)" + } + + /** Compose two `DataViews` together to construct a view from the target of this `DataView` to the + * view type of the second `DataView` + * + * @param g a DataView from `V` to new view-type `V2` + * @tparam V2 View type of `DataView` `g` + * @return a new `DataView` from the original `T` to new view-type `V2` + */ + def andThen[V2 <: Data](g: DataView[V, V2])(implicit sourceInfo: SourceInfo): DataView[T, V2] = { + val self = this + // We have to pass the DataProducts and DataViews manually to .viewAs below + val tdp = implicitly[DataProduct[T]] + val vdp = implicitly[DataProduct[V]] + new DataView[T, V2]( + t => g.mkView(mkView(t)), + { case (t, v2) => List(t.viewAs[V](tdp, self).viewAs[V2](vdp, g) -> v2) }, + this.total && g.total + ) { + override def toString: String = s"$self andThen $g" + } + } +} + +/** Factory methods for constructing [[DataView]]s, see class for example use */ +object DataView { + + /** Default factory method, alias for [[pairs]] */ + def apply[T : DataProduct, V <: Data](mkView: T => V, pairs: ((T, V) => (Data, Data))*)(implicit sourceInfo: SourceInfo): DataView[T, V] = + DataView.pairs(mkView, pairs: _*) + + /** Construct [[DataView]]s with pairs of functions from the target and view to corresponding fields */ + def pairs[T : DataProduct, V <: Data](mkView: T => V, pairs: ((T, V) => (Data, Data))*)(implicit sourceInfo: SourceInfo): DataView[T, V] = + mapping(mkView: T => V, swizzle(pairs)) + + /** More general factory method for complex mappings */ + def mapping[T : DataProduct, V <: Data](mkView: T => V, mapping: (T, V) => Iterable[(Data, Data)])(implicit sourceInfo: SourceInfo): DataView[T, V] = + new DataView[T, V](mkView, mapping, _total = true) + + /** Provides `invert` for invertible [[DataView]]s + * + * This must be done as an extension method because it applies an addition constraint on the `Target` + * type parameter, namely that it must be a subtype of [[Data]]. + * + * @note [[PartialDataView]]s are **not** invertible and will result in an elaboration time exception + */ + implicit class InvertibleDataView[T <: Data : WeakTypeTag, V <: Data : WeakTypeTag](view: DataView[T, V]) { + def invert(mkView: V => T): DataView[V, T] = { + // It would've been nice to make this a compiler error, but it's unclear how to make that work. + // We tried having separate TotalDataView and PartialDataView and only defining inversion for + // TotalDataView. For some reason, implicit resolution wouldn't invert TotalDataViews. This is + // probably because it was looking for the super-type DataView and since invertDataView was + // only defined on TotalDataView, it wasn't included in implicit resolution. Thus we end up + // with a runtime check. + if (!view.total) { + val tt = implicitly[WeakTypeTag[T]].tpe + val vv = implicitly[WeakTypeTag[V]].tpe + val msg = s"Cannot invert '$view' as it is non-total.\n Try providing a DataView[$vv, $tt]." + + s"\n Please see https://www.chisel-lang.org/chisel3/docs/explanations/dataview." + throw InvalidViewException(msg) + } + implicit val sourceInfo = view.sourceInfo + new DataView[V, T](mkView, swapArgs(view.mapping), view.total) + } + } + + private[dataview] def swizzle[A, B, C, D](fs: Iterable[(A, B) => (C, D)]): (A, B) => Iterable[(C, D)] = { + case (a, b) => fs.map(f => f(a, b)) + } + + private def swapArgs[A, B, C, D](f: (A, B) => Iterable[(C, D)]): (B, A) => Iterable[(D, C)] = { + case (b, a) => f(a, b).map(_.swap) + } + + /** All Chisel Data are viewable as their own type */ + implicit def identityView[A <: Data](implicit sourceInfo: SourceInfo): DataView[A, A] = + DataView[A, A](chiselTypeOf.apply, { case (x, y) => (x, y) }) +} + +/** Factory methods for constructing non-total [[DataView]]s */ +object PartialDataView { + + /** Default factory method, alias for [[pairs]] */ + def apply[T: DataProduct, V <: Data](mkView: T => V, pairs: ((T, V) => (Data, Data))*)(implicit sourceInfo: SourceInfo): DataView[T, V] = + PartialDataView.pairs(mkView, pairs: _*) + + /** Construct [[DataView]]s with pairs of functions from the target and view to corresponding fields */ + def pairs[T: DataProduct, V <: Data](mkView: T => V, pairs: ((T, V) => (Data, Data))*)(implicit sourceInfo: SourceInfo): DataView[T, V] = + mapping(mkView, DataView.swizzle(pairs)) + + /** More general factory method for complex mappings */ + def mapping[T: DataProduct, V <: Data](mkView: T => V, mapping: (T, V) => Iterable[(Data, Data)])(implicit sourceInfo: SourceInfo): DataView[T, V] = + new DataView[T, V](mkView, mapping, _total = false) +} diff --git a/core/src/main/scala/chisel3/experimental/dataview/package.scala b/core/src/main/scala/chisel3/experimental/dataview/package.scala new file mode 100644 index 00000000..1acf43e1 --- /dev/null +++ b/core/src/main/scala/chisel3/experimental/dataview/package.scala @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: Apache-2.0 + +package chisel3.experimental + +import chisel3._ +import chisel3.internal._ +import chisel3.internal.sourceinfo.SourceInfo + +import scala.annotation.{implicitNotFound, tailrec} +import scala.collection.mutable +import scala.collection.immutable.LazyList // Needed for 2.12 alias + +package object dataview { + case class InvalidViewException(message: String) extends ChiselException(message) + + /** Provides `viewAs` for types that have an implementation of [[DataProduct]] + * + * Calling `viewAs` also requires an implementation of [[DataView]] for the target type + */ + implicit class DataViewable[T](target: T) { + def viewAs[V <: Data](implicit dataproduct: DataProduct[T], dataView: DataView[T, V]): V = { + // TODO put a try catch here for ExpectedHardwareException and perhaps others + // It's likely users will accidentally use chiselTypeOf or something that may error, + // The right thing to use is DataMirror...chiselTypeClone because of composition with DataView.andThen + // Another option is that .andThen could give a fake binding making chiselTypeOfs in the user code safe + val result: V = dataView.mkView(target) + requireIsChiselType(result, "viewAs") + + doBind(target, result, dataView) + + // Setting the parent marks these Data as Views + result.setAllParents(Some(ViewParent)) + // The names of views do not matter except for when a view is annotated. For Views that correspond + // To a single Data, we just forward the name of the Target. For Views that correspond to more + // than one Data, we return this assigned name but rename it in the Convert stage + result.forceName(None, "view", Builder.viewNamespace) + result + } + } + + // This private type alias lets us provide a custom error message for misuing the .viewAs for upcasting Bundles + @implicitNotFound("${A} is not a subtype of ${B}! Did you mean .viewAs[${B}]? " + + "Please see https://www.chisel-lang.org/chisel3/docs/cookbooks/dataview") + private type SubTypeOf[A, B] = A <:< B + + /** Provides `viewAsSupertype` for subclasses of [[Bundle]] */ + implicit class BundleUpcastable[T <: Bundle](target: T) { + /** View a [[Bundle]] or [[Record]] as a parent type (upcast) */ + def viewAsSupertype[V <: Bundle](proto: V)(implicit ev: SubTypeOf[T, V], sourceInfo: SourceInfo): V = { + implicit val dataView = PartialDataView.mapping[T, V](_ => proto, { + case (a, b) => + val aElts = a.elements + val bElts = b.elements + val bKeys = bElts.keySet + val keys = aElts.keysIterator.filter(bKeys.contains) + keys.map(k => aElts(k) -> bElts(k)).toSeq + }) + target.viewAs[V] + } + } + + private def nonTotalViewException(dataView: DataView[_, _], target: Any, view: Data, targetFields: Seq[String], viewFields: Seq[String]) = { + def missingMsg(name: String, fields: Seq[String]): Option[String] = { + val str = fields.mkString(", ") + fields.size match { + case 0 => None + case 1 => Some(s"$name field '$str' is missing") + case _ => Some(s"$name fields '$str' are missing") + } + } + val vs = missingMsg("view", viewFields) + val ts = missingMsg("target", targetFields) + val reasons = (ts ++ vs).mkString(" and ").capitalize + val suggestion = if (ts.nonEmpty) "\n If the view *should* be non-total, try a 'PartialDataView'." else "" + val msg = s"Viewing $target as $view is non-Total!\n $reasons.\n DataView used is $dataView.$suggestion" + throw InvalidViewException(msg) + } + + // TODO should this be moved to class Aggregate / can it be unified with Aggregate.bind? + private def doBind[T : DataProduct, V <: Data](target: T, view: V, dataView: DataView[T, V]): Unit = { + val mapping = dataView.mapping(target, view) + val total = dataView.total + // Lookups to check the mapping results + val viewFieldLookup: Map[Data, String] = getRecursiveFields(view, "_").toMap + val targetContains: Data => Boolean = implicitly[DataProduct[T]].dataSet(target) + + // Resulting bindings for each Element of the View + val childBindings = + new mutable.HashMap[Data, mutable.ListBuffer[Element]] ++ + viewFieldLookup.view + .collect { case (elt: Element, _) => elt } + .map(_ -> new mutable.ListBuffer[Element]) + + def viewFieldName(d: Data): String = + viewFieldLookup.get(d).map(_ + " ").getOrElse("") + d.toString + + // Helper for recording the binding of each + def onElt(te: Element, ve: Element): Unit = { + // TODO can/should we aggregate these errors? + def err(name: String, arg: Data) = + throw InvalidViewException(s"View mapping must only contain Elements within the $name, got $arg") + + // The elements may themselves be views, look through the potential chain of views for the Elements + // that are actually members of the target or view + val tex = unfoldView(te).find(targetContains).getOrElse(err("Target", te)) + val vex = unfoldView(ve).find(viewFieldLookup.contains).getOrElse(err("View", ve)) + + if (tex.getClass != vex.getClass) { + val fieldName = viewFieldName(vex) + throw InvalidViewException(s"Field $fieldName specified as view of non-type-equivalent value $tex") + } + // View width must be unknown or match target width + if (vex.widthKnown && vex.width != tex.width) { + def widthAsString(x: Element) = x.widthOption.map("<" + _ + ">").getOrElse("<unknown>") + val fieldName = viewFieldName(vex) + val vwidth = widthAsString(vex) + val twidth = widthAsString(tex) + throw InvalidViewException(s"View field $fieldName has width ${vwidth} that is incompatible with target value $tex's width ${twidth}") + } + childBindings(vex) += tex + } + + mapping.foreach { + // Special cased because getMatchedFields checks typeEquivalence on Elements (and is used in Aggregate path) + // Also saves object allocations on common case of Elements + case (ae: Element, be: Element) => onElt(ae, be) + + case (aa: Aggregate, ba: Aggregate) => + if (!ba.typeEquivalent(aa)) { + val fieldName = viewFieldLookup(ba) + throw InvalidViewException(s"field $fieldName specified as view of non-type-equivalent value $aa") + } + getMatchedFields(aa, ba).foreach { + case (aelt: Element, belt: Element) => onElt(aelt, belt) + case _ => // Ignore matching of Aggregates + } + } + + // Errors in totality of the View, use var List to keep fast path cheap (no allocation) + var viewNonTotalErrors: List[Data] = Nil + var targetNonTotalErrors: List[String] = Nil + + val targetSeen: Option[mutable.Set[Data]] = if (total) Some(mutable.Set.empty[Data]) else None + + val resultBindings = childBindings.map { case (data, targets) => + val targetsx = targets match { + case collection.Seq(target: Element) => target + case collection.Seq() => + viewNonTotalErrors = data :: viewNonTotalErrors + data.asInstanceOf[Element] // Return the Data itself, will error after this map, cast is safe + case x => + throw InvalidViewException(s"Got $x, expected Seq(_: Direct)") + } + // TODO record and report aliasing errors + targetSeen.foreach(_ += targetsx) + data -> targetsx + }.toMap + + // Check for totality of Target + targetSeen.foreach { seen => + val lookup = implicitly[DataProduct[T]].dataIterator(target, "_") + for (missed <- lookup.collect { case (d: Element, name) if !seen(d) => name }) { + targetNonTotalErrors = missed :: targetNonTotalErrors + } + } + if (viewNonTotalErrors != Nil || targetNonTotalErrors != Nil) { + val viewErrors = viewNonTotalErrors.map(f => viewFieldLookup.getOrElse(f, f.toString)) + nonTotalViewException(dataView, target, view, targetNonTotalErrors, viewErrors) + } + + view match { + case elt: Element => view.bind(ViewBinding(resultBindings(elt))) + case agg: Aggregate => + // We record total Data mappings to provide a better .toTarget + val topt = target match { + case d: Data if total => Some(d) + case _ => + // Record views that don't have the simpler .toTarget for later renaming + Builder.unnamedViews += view + None + } + // TODO We must also record children as unnamed, some could be namable but this requires changes to the Binding + getRecursiveFields.lazily(view, "_").foreach { + case (agg: Aggregate, _) if agg != view => + Builder.unnamedViews += agg + case _ => // Do nothing + } + agg.bind(AggregateViewBinding(resultBindings, topt)) + } + } + + // Traces an Element that may (or may not) be a view until it no longer maps + // Inclusive of the argument + private def unfoldView(elt: Element): LazyList[Element] = { + def rec(e: Element): LazyList[Element] = e.topBindingOpt match { + case Some(ViewBinding(target)) => target #:: rec(target) + case Some(AggregateViewBinding(mapping, _)) => + val target = mapping(e) + target #:: rec(target) + case Some(_) | None => LazyList.empty + } + elt #:: rec(elt) + } + + // Safe for all Data + private[chisel3] def isView(d: Data): Boolean = d._parent.contains(ViewParent) + + /** Turn any [[Element]] that could be a View into a concrete Element + * + * This is the fundamental "unwrapping" or "tracing" primitive operation for handling Views within + * Chisel. + */ + private[chisel3] def reify(elt: Element): Element = + reify(elt, elt.topBinding) + + /** Turn any [[Element]] that could be a View into a concrete Element + * + * This is the fundamental "unwrapping" or "tracing" primitive operation for handling Views within + * Chisel. + */ + @tailrec private[chisel3] def reify(elt: Element, topBinding: TopBinding): Element = + topBinding match { + case ViewBinding(target) => reify(target, elt.topBinding) + case _ => elt + } + + /** Determine the target of a View if it is a single Target + * + * @note An Aggregate may be a view of unrelated [[Data]] (eg. like a Seq or tuple) and thus this + * there is no single Data representing the Target and this function will return None + * @return The single Data target of this view or None if a single Data doesn't exist + */ + private[chisel3] def reifySingleData(data: Data): Option[Data] = { + val candidate: Option[Data] = + data.binding.collect { // First check if this is a total mapping of an Aggregate + case AggregateViewBinding(_, Some(t)) => t + }.orElse { // Otherwise look via top binding + data.topBindingOpt match { + case None => None + case Some(ViewBinding(target)) => Some(target) + case Some(AggregateViewBinding(lookup, _)) => lookup.get(data) + case Some(_) => None + } + } + candidate.flatMap { d => + // Candidate may itself be a view, keep tracing in those cases + if (isView(d)) reifySingleData(d) else Some(d) + } + } + +} diff --git a/core/src/main/scala/chisel3/internal/BiConnect.scala b/core/src/main/scala/chisel3/internal/BiConnect.scala index aa7d7ac3..4a9bb4f5 100644 --- a/core/src/main/scala/chisel3/internal/BiConnect.scala +++ b/core/src/main/scala/chisel3/internal/BiConnect.scala @@ -3,9 +3,11 @@ package chisel3.internal import chisel3._ +import chisel3.experimental.dataview.reify import chisel3.experimental.{Analog, BaseModule, attach} import chisel3.internal.Builder.pushCommand import chisel3.internal.firrtl.{Connect, DefInvalid} + import scala.language.experimental.macros import chisel3.internal.sourceinfo._ @@ -225,8 +227,10 @@ private[chisel3] object BiConnect { // This function checks if element-level connection operation allowed. // Then it either issues it or throws the appropriate exception. - def elemConnect(implicit sourceInfo: SourceInfo, connectCompileOptions: CompileOptions, left: Element, right: Element, context_mod: RawModule): Unit = { + def elemConnect(implicit sourceInfo: SourceInfo, connectCompileOptions: CompileOptions, _left: Element, _right: Element, context_mod: RawModule): Unit = { import BindingDirection.{Internal, Input, Output} // Using extensively so import these + val left = reify(_left) + val right = reify(_right) // If left or right have no location, assume in context module // This can occur if one of them is a literal, unbound will error previously val left_mod: BaseModule = left.topBinding.location.getOrElse(context_mod) diff --git a/core/src/main/scala/chisel3/internal/Binding.scala b/core/src/main/scala/chisel3/internal/Binding.scala index 300803ce..6f4ab4b0 100644 --- a/core/src/main/scala/chisel3/internal/Binding.scala +++ b/core/src/main/scala/chisel3/internal/Binding.scala @@ -120,6 +120,15 @@ case class MemTypeBinding[T <: Data](parent: MemBase[T]) extends Binding { // It is a source (RHS). It may only be connected/applied to sinks. case class DontCareBinding() extends UnconstrainedBinding +// Views currently only support 1:1 Element-level mappings +private[chisel3] case class ViewBinding(target: Element) extends UnconstrainedBinding +/** Binding for Aggregate Views + * @param childMap Mapping from children of this view to each child's target + * @param target Optional Data this Aggregate views if the view is total and the target is a Data + */ +private[chisel3] case class AggregateViewBinding(childMap: Map[Data, Element], target: Option[Data]) extends UnconstrainedBinding + + sealed trait LitBinding extends UnconstrainedBinding with ReadOnlyBinding // Literal binding attached to a element that is not part of a Bundle. case class ElementLitBinding(litArg: LitArg) extends LitBinding diff --git a/core/src/main/scala/chisel3/internal/Builder.scala b/core/src/main/scala/chisel3/internal/Builder.scala index fef12093..ddaedd2e 100644 --- a/core/src/main/scala/chisel3/internal/Builder.scala +++ b/core/src/main/scala/chisel3/internal/Builder.scala @@ -10,7 +10,8 @@ import chisel3.internal.firrtl._ import chisel3.internal.naming._ import _root_.firrtl.annotations.{CircuitName, ComponentName, IsMember, ModuleName, Named, ReferenceTarget} import _root_.firrtl.annotations.AnnotationUtils.validComponentName -import _root_.firrtl.AnnotationSeq +import _root_.firrtl.{AnnotationSeq, RenameMap} +import chisel3.experimental.dataview.{reify, reifySingleData} import chisel3.internal.Builder.Prefix import logger.LazyLogging @@ -215,32 +216,48 @@ private[chisel3] trait HasId extends InstanceId { private[chisel3] def getRef: Arg = _ref.get private[chisel3] def getOptionRef: Option[Arg] = _ref + private def localName(c: Component): String = _ref match { + case Some(arg) => arg fullName c + case None => computeName(None, None).get + } + + // Helper for reifying views if they map to a single Target + private[chisel3] def reifyTarget: Option[Data] = this match { + case d: Data => reifySingleData(d) // Only Data can be views + case bad => throwException(s"This shouldn't be possible - got $bad with ${_parent}") + } + + // Helper for reifying the parent of a view if the view maps to a single Target + private[chisel3] def reifyParent: BaseModule = reifyTarget.flatMap(_._parent).getOrElse(ViewParent) + // Implementation of public methods. def instanceName: String = _parent match { + case Some(ViewParent) => reifyTarget.map(_.instanceName).getOrElse(this.localName(ViewParent.fakeComponent)) case Some(p) => p._component match { - case Some(c) => _ref match { - case Some(arg) => arg fullName c - case None => computeName(None, None).get - } + case Some(c) => localName(c) case None => throwException("signalName/pathName should be called after circuit elaboration") } case None => throwException("this cannot happen") } def pathName: String = _parent match { case None => instanceName + case Some(ViewParent) => s"${reifyParent.pathName}.$instanceName" case Some(p) => s"${p.pathName}.$instanceName" } def parentPathName: String = _parent match { + case Some(ViewParent) => reifyParent.pathName case Some(p) => p.pathName case None => throwException(s"$instanceName doesn't have a parent") } def parentModName: String = _parent match { + case Some(ViewParent) => reifyParent.name case Some(p) => p.name case None => throwException(s"$instanceName doesn't have a parent") } // TODO Should this be public? protected def circuitName: String = _parent match { case None => instanceName + case Some(ViewParent) => reifyParent.circuitName case Some(p) => p.circuitName } @@ -288,8 +305,10 @@ private[chisel3] trait NamedComponent extends HasId { final def toAbsoluteTarget: ReferenceTarget = { val localTarget = toTarget + def makeTarget(p: BaseModule) = p.toAbsoluteTarget.ref(localTarget.ref).copy(component = localTarget.component) _parent match { - case Some(parent) => parent.toAbsoluteTarget.ref(localTarget.ref).copy(component = localTarget.component) + case Some(ViewParent) => makeTarget(reifyParent) + case Some(parent) => makeTarget(parent) case None => localTarget } } @@ -304,6 +323,11 @@ private[chisel3] class ChiselContext() { // Records the different prefixes which have been scoped at this point in time var prefixStack: Prefix = Nil + + // Views belong to a separate namespace (for renaming) + // The namespace outside of Builder context is useless, but it ensures that views can still be created + // and the resulting .toTarget is very clearly useless (_$$View$$_...) + val viewNamespace = Namespace.empty } private[chisel3] class DynamicContext(val annotationSeq: AnnotationSeq) { @@ -319,6 +343,9 @@ private[chisel3] class DynamicContext(val annotationSeq: AnnotationSeq) { */ val aspectModule: mutable.HashMap[BaseModule, BaseModule] = mutable.HashMap.empty[BaseModule, BaseModule] + // Views that do not correspond to a single ReferenceTarget and thus require renaming + val unnamedViews: ArrayBuffer[Data] = ArrayBuffer.empty + // Set by object Module.apply before calling class Module constructor // Used to distinguish between no Module() wrapping, multiple wrappings, and rewrapping var readyForModuleConstr: Boolean = false @@ -370,6 +397,9 @@ private[chisel3] object Builder extends LazyLogging { def annotationSeq: AnnotationSeq = dynamicContext.annotationSeq def namingStack: NamingStack = dynamicContext.namingStack + def unnamedViews: ArrayBuffer[Data] = dynamicContext.unnamedViews + def viewNamespace: Namespace = chiselContext.get.viewNamespace + // Puts a prefix string onto the prefix stack def pushPrefix(d: String): Unit = { val context = chiselContext.get() @@ -403,6 +433,7 @@ private[chisel3] object Builder extends LazyLogging { case PortBinding(mod) if Builder.currentModule.contains(mod) => data.seedOpt case PortBinding(mod) => map2(mod.seedOpt, data.seedOpt)(_ + "_" + _) case (_: LitBinding | _: DontCareBinding) => None + case _ => Some("view_") // TODO implement } id match { case d: Data => recData(d) @@ -646,8 +677,29 @@ private[chisel3] object Builder extends LazyLogging { } } + // Builds a RenameMap for all Views that do not correspond to a single Data + // These Data give a fake ReferenceTarget for .toTarget and .toReferenceTarget that the returned + // RenameMap can split into the constituent parts + private[chisel3] def makeViewRenameMap: RenameMap = { + val renames = RenameMap() + for (view <- unnamedViews) { + val localTarget = view.toTarget + val absTarget = view.toAbsoluteTarget + val elts = getRecursiveFields.lazily(view, "") + .collect { case (elt: Element, _) => elt } + for (elt <- elts) { + val targetOfView = reify(elt) + renames.record(localTarget, targetOfView.toTarget) + renames.record(absTarget, targetOfView.toAbsoluteTarget) + } + } + renames + } + private [chisel3] def build[T <: RawModule](f: => T, dynamicContext: DynamicContext): (Circuit, T) = { dynamicContextVar.withValue(Some(dynamicContext)) { + ViewParent // Must initialize the singleton in a Builder context or weird things can happen + // in tiny designs/testcases that never access anything in chisel3.internal checkScalaVersion() logger.warn("Elaborating design...") val mod = f @@ -655,7 +707,7 @@ private[chisel3] object Builder extends LazyLogging { errors.checkpoint(logger) logger.warn("Done elaborating.") - (Circuit(components.last.name, components.toSeq, annotations.toSeq), mod) + (Circuit(components.last.name, components.toSeq, annotations.toSeq, makeViewRenameMap), mod) } } initializeSingletons() diff --git a/core/src/main/scala/chisel3/internal/MonoConnect.scala b/core/src/main/scala/chisel3/internal/MonoConnect.scala index b979ebae..5cbab329 100644 --- a/core/src/main/scala/chisel3/internal/MonoConnect.scala +++ b/core/src/main/scala/chisel3/internal/MonoConnect.scala @@ -5,7 +5,9 @@ package chisel3.internal import chisel3._ import chisel3.experimental.{Analog, BaseModule, EnumType, FixedPoint, Interval, UnsafeEnum} import chisel3.internal.Builder.pushCommand +import chisel3.experimental.dataview.reify import chisel3.internal.firrtl.{Connect, DefInvalid} + import scala.language.experimental.macros import chisel3.internal.sourceinfo.SourceInfo @@ -188,8 +190,10 @@ private[chisel3] object MonoConnect { // This function checks if element-level connection operation allowed. // Then it either issues it or throws the appropriate exception. - def elemConnect(implicit sourceInfo: SourceInfo, connectCompileOptions: CompileOptions, sink: Element, source: Element, context_mod: RawModule): Unit = { + def elemConnect(implicit sourceInfo: SourceInfo, connectCompileOptions: CompileOptions, _sink: Element, _source: Element, context_mod: RawModule): Unit = { import BindingDirection.{Internal, Input, Output} // Using extensively so import these + val sink = reify(_sink) + val source = reify(_source) // If source has no location, assume in context module // This can occur if is a literal, unbound will error previously val sink_mod: BaseModule = sink.topBinding.location.getOrElse(throw UnwritableSinkException) diff --git a/core/src/main/scala/chisel3/internal/firrtl/IR.scala b/core/src/main/scala/chisel3/internal/firrtl/IR.scala index 5796522c..f8a3cf7f 100644 --- a/core/src/main/scala/chisel3/internal/firrtl/IR.scala +++ b/core/src/main/scala/chisel3/internal/firrtl/IR.scala @@ -8,7 +8,8 @@ import chisel3.internal._ import chisel3.internal.sourceinfo.SourceInfo import chisel3.experimental._ import _root_.firrtl.{ir => firrtlir} -import _root_.firrtl.PrimOps +import _root_.firrtl.{PrimOps, RenameMap} +import _root_.firrtl.annotations.Annotation import scala.collection.immutable.NumericRange import scala.math.BigDecimal.RoundingMode @@ -789,4 +790,6 @@ abstract class Component extends Arg { case class DefModule(id: RawModule, name: String, ports: Seq[Port], commands: Seq[Command]) extends Component case class DefBlackBox(id: BaseBlackBox, name: String, ports: Seq[Port], topDir: SpecifiedDirection, params: Map[String, Param]) extends Component -case class Circuit(name: String, components: Seq[Component], annotations: Seq[ChiselAnnotation] = Seq.empty) +case class Circuit(name: String, components: Seq[Component], annotations: Seq[ChiselAnnotation], renames: RenameMap) { + def firrtlAnnotations: Iterable[Annotation] = annotations.flatMap(_.toFirrtl.update(renames)) +} diff --git a/docs/src/cookbooks/cookbook.md b/docs/src/cookbooks/cookbook.md index 1b47ad14..f033c285 100644 --- a/docs/src/cookbooks/cookbook.md +++ b/docs/src/cookbooks/cookbook.md @@ -9,12 +9,13 @@ section: "chisel3" Please note that these examples make use of [Chisel's scala-style printing](../explanations/printing#scala-style). -* Converting Chisel Types to/from UInt +* Type Conversions * [How do I create a UInt from an instance of a Bundle?](#how-do-i-create-a-uint-from-an-instance-of-a-bundle) * [How do I create a Bundle from a UInt?](#how-do-i-create-a-bundle-from-a-uint) * [How can I tieoff a Bundle/Vec to 0?](#how-can-i-tieoff-a-bundlevec-to-0) * [How do I create a Vec of Bools from a UInt?](#how-do-i-create-a-vec-of-bools-from-a-uint) * [How do I create a UInt from a Vec of Bool?](#how-do-i-create-a-uint-from-a-vec-of-bool) + * [How do I connect a subset of Bundle fields?](#how-do-i-connect-a-subset-of-bundle-fields) * Vectors and Registers * [How do I create a Vector of Registers?](#how-do-i-create-a-vector-of-registers) * [How do I create a Reg of type Vec?](#how-do-i-create-a-reg-of-type-vec) @@ -27,7 +28,7 @@ Please note that these examples make use of [Chisel's scala-style printing](../e * [How do I get Chisel to name the results of vector reads properly?](#how-do-i-get-chisel-to-name-the-results-of-vector-reads-properly) * [How can I dynamically set/parametrize the name of a module?](#how-can-i-dynamically-setparametrize-the-name-of-a-module) -## Converting Chisel Types to/from UInt +## Type Conversions ### How do I create a UInt from an instance of a Bundle? @@ -148,6 +149,10 @@ class Foo extends RawModule { } ``` +### How do I connect a subset of Bundle fields? + +See the [DataView cookbook](dataview#how-do-i-connect-a-subset-of-bundle-fields). + ## Vectors and Registers ### How do I create a Vector of Registers? diff --git a/docs/src/cookbooks/dataview.md b/docs/src/cookbooks/dataview.md new file mode 100644 index 00000000..ed969ca1 --- /dev/null +++ b/docs/src/cookbooks/dataview.md @@ -0,0 +1,179 @@ +--- +layout: docs +title: "DataView Cookbook" +section: "chisel3" +--- + +# DataView Cookbook + +* [How do I view a Data as a UInt or vice versa?](#how-do-i-view-a-data-as-a-uint-or-vice-versa) +* [How do I create a DataView for a Bundle has a type parameter?](#how-do-i-create-a-dataview-for-a-bundle-has-a-type-parameter) +* [How do I create a DataView for a Bundle with optional fields?](#how-do-i-create-a-dataview-for-a-bundle-with-optional-fields) +* [How do I connect a subset of Bundle fields?](#how-do-i-connect-a-subset-of-bundle-fields) + * [How do I view a Bundle as a parent type (superclass)?](#how-do-i-view-a-bundle-as-a-parent-type-superclass) + * [How do I view a Bundle as a parent type when the parent type is abstract (like a trait)?](#how-do-i-view-a-bundle-as-a-parent-type-when-the-parent-type-is-abstract-like-a-trait) + +## How do I view a Data as a UInt or vice versa? + +Subword viewing (using concatenations or bit extractions in `DataViews`) is not yet supported. +We intend to implement this in the future, but for the time being, use regular casts +(`.asUInt` and `.asTypeOf`). + +## How do I create a DataView for a Bundle has a type parameter? + +Instead of using a `val`, use a `def` which can have type parameters: + +```scala mdoc:silent:reset +import chisel3._ +import chisel3.experimental.dataview._ + +class Foo[T <: Data](val foo: T) extends Bundle +class Bar[T <: Data](val bar: T) extends Bundle + +object Foo { + implicit def view[T <: Data]: DataView[Foo[T], Bar[T]] = { + DataView(f => new Bar(f.foo.cloneType), _.foo -> _.bar) + // .cloneType is necessary because the f passed to this function will be bound hardware + } +} +``` + +```scala mdoc:invisible +// Make sure this works during elaboration, not part of doc +class MyModule extends RawModule { + val in = IO(Input(new Foo(UInt(8.W)))) + val out = IO(Output(new Bar(UInt(8.W)))) + out := in.viewAs[Bar[UInt]] +} +chisel3.stage.ChiselStage.emitVerilog(new MyModule) +``` +If you think about type parameterized classes as really being a family of different classes +(one for each type parameter), you can think about the `implicit def` as a generator of `DataViews` +for each type parameter. + +## How do I create a DataView for a Bundle with optional fields? + +Instead of using the default `DataView` apply method, use `DataView.mapping`: + +```scala mdoc:silent:reset +import chisel3._ +import chisel3.experimental.dataview._ + +class Foo(val w: Option[Int]) extends Bundle { + val foo = UInt(8.W) + val opt = w.map(x => UInt(x.W)) +} +class Bar(val w: Option[Int]) extends Bundle { + val bar = UInt(8.W) + val opt = w.map(x => UInt(x.W)) +} + +object Foo { + implicit val view: DataView[Foo, Bar] = + DataView.mapping( + // First argument is always the function to make the view from the target + f => new Bar(f.w), + // Now instead of a varargs of tuples of individual mappings, we have a single function that + // takes a target and a view and returns an Iterable of tuple + (f, b) => List(f.foo -> b.bar) ++ f.opt.map(_ -> b.opt.get) + // ^ Note that we can append options since they are Iterable! + + ) +} +``` + +```scala mdoc:invisible +// Make sure this works during elaboration, not part of doc +class MyModule extends RawModule { + val in = IO(Input(new Foo(Some(8)))) + val out = IO(Output(new Bar(Some(8)))) + out := in.viewAs[Bar] +} +chisel3.stage.ChiselStage.emitVerilog(new MyModule) +``` + +## How do I connect a subset of Bundle fields? + +Chisel 3 requires types to match exactly for connections. +DataView provides a mechanism for "viewing" one `Bundle` object as if it were the type of another, +which allows them to be connected. + +### How do I view a Bundle as a parent type (superclass)? + +For viewing `Bundles` as the type of the parent, it is as simple as using `viewAsSupertype` and providing a +template object of the parent type: + +```scala mdoc:silent:reset +import chisel3._ +import chisel3.experimental.dataview._ + +class Foo extends Bundle { + val foo = UInt(8.W) +} +class Bar extends Foo { + val bar = UInt(8.W) +} +class MyModule extends Module { + val foo = IO(Input(new Foo)) + val bar = IO(Output(new Bar)) + bar.viewAsSupertype(new Foo) := foo // bar.foo := foo.foo + bar.bar := 123.U // all fields need to be connected +} +``` +```scala mdoc:verilog +chisel3.stage.ChiselStage.emitVerilog(new MyModule) +``` + +### How do I view a Bundle as a parent type when the parent type is abstract (like a trait)? + +Given the following `Bundles` that share a common `trait`: + +```scala mdoc:silent:reset +import chisel3._ +import chisel3.experimental.dataview._ + +trait Super extends Bundle { + def bitwidth: Int + val a = UInt(bitwidth.W) +} +class Foo(val bitwidth: Int) extends Super { + val foo = UInt(8.W) +} +class Bar(val bitwidth: Int) extends Super { + val bar = UInt(8.W) +} +``` + +`Foo` and `Bar` cannot be connected directly, but they could be connected by viewing them both as if +they were instances of their common supertype, `Super`. +A straightforward approach might run into an issue like the following: + +```scala mdoc:fail +class MyModule extends Module { + val foo = IO(Input(new Foo(8))) + val bar = IO(Output(new Bar(8))) + bar.viewAsSupertype(new Super) := foo.viewAsSupertype(new Super) +} +``` + +The problem is that `viewAs` requires an object to use as a type template (so that it can be cloned), +but `traits` are abstract and cannot be instantiated. +The solution is to create an instance of an _anonymous class_ and use that object as the argument to `viewAs`. +We can do this like so: + +```scala mdoc:silent +class MyModule extends Module { + val foo = IO(Input(new Foo(8))) + val bar = IO(Output(new Bar(8))) + val tpe = new Super { // Adding curly braces creates an anonymous class + def bitwidth = 8 // We must implement any abstract methods + } + bar.viewAsSupertype(tpe) := foo.viewAsSupertype(tpe) +} +``` +By adding curly braces after the name of the trait, we're telling Scala to create a new concrete +subclass of the trait, and create an instance of it. +As indicated in the comment, abstract methods must still be implemented. +This is the same that happens when one writes `new Bundle {}`, +the curly braces create a new concrete subclass; however, because `Bundle` has no abstract methods, +the contents of the body can be empty. diff --git a/docs/src/explanations/dataview.md b/docs/src/explanations/dataview.md new file mode 100644 index 00000000..2f229bfc --- /dev/null +++ b/docs/src/explanations/dataview.md @@ -0,0 +1,520 @@ +--- +layout: docs +title: "DataView" +section: "chisel3" +--- + +# DataView + +_New in Chisel 3.5_ + +```scala mdoc:invisible +import chisel3._ +import chisel3.stage.ChiselStage.emitVerilog +``` + +## Introduction + +DataView is a mechanism for "viewing" Scala objects as a subtype of `chisel3.Data`. +Often, this is useful for viewing one subtype of `chisel3.Data`, as another. +One can think about a `DataView` as a mapping from a _Target_ type `T` to a _View_ type `V`. +This is similar to a cast (eg. `.asTypeOf`) with a few differences: +1. Views are _connectable_—connections to the view will occur on the target +2. Whereas casts are _structural_ (a reinterpretation of the underlying bits), a DataView is a customizable mapping +3. Views can be _partial_—not every field in the target must be included in the mapping + +## A Motivating Example (AXI4) + +[AXI4](https://en.wikipedia.org/wiki/Advanced_eXtensible_Interface) is a common interface in digital +design. +A typical Verilog peripheral using AXI4 will define a write channel as something like: +```verilog +module my_module( + // Write Channel + input AXI_AWVALID, + output AXI_AWREADY, + input [3:0] AXI_AWID, + input [19:0] AXI_AWADDR, + input [1:0] AXI_AWLEN, + input [1:0] AXI_AWSIZE, + // ... +); +``` + +This would correspond to the following Chisel Bundle: + +```scala mdoc +class VerilogAXIBundle(val addrWidth: Int) extends Bundle { + val AWVALID = Output(Bool()) + val AWREADY = Input(Bool()) + val AWID = Output(UInt(4.W)) + val AWADDR = Output(UInt(addrWidth.W)) + val AWLEN = Output(UInt(2.W)) + val AWSIZE = Output(UInt(2.W)) + // The rest of AW and other AXI channels here +} + +// Instantiated as +class my_module extends RawModule { + val AXI = IO(new VerilogAXIBundle(20)) +} +``` + +Expressing something that matches a standard Verilog interface is important when instantiating Verilog +modules in a Chisel design as `BlackBoxes`. +Generally though, Chisel developers prefer to use composition via utilities like `Decoupled` rather +than a flat handling of `ready` and `valid` as in the above. +A more "Chisel-y" implementation of this interface might look like: + +```scala mdoc +// Note that both the AW and AR channels look similar and could use the same Bundle definition +class AXIAddressChannel(val addrWidth: Int) extends Bundle { + val id = UInt(4.W) + val addr = UInt(addrWidth.W) + val len = UInt(2.W) + val size = UInt(2.W) + // ... +} +import chisel3.util.Decoupled +// We can compose the various AXI channels together +class AXIBundle(val addrWidth: Int) extends Bundle { + val aw = Decoupled(new AXIAddressChannel(addrWidth)) + // val ar = new AXIAddressChannel + // ... Other channels here ... +} +// Instantiated as +class MyModule extends RawModule { + val axi = IO(new AXIBundle(20)) +} +``` + +Of course, this would result in very different looking Verilog: + +```scala mdoc:verilog +emitVerilog(new MyModule { + override def desiredName = "MyModule" + axi := DontCare // Just to generate Verilog in this stub +}) +``` + +So how can we use our more structured types while maintaining expected Verilog interfaces? +Meet DataView: + +```scala mdoc +import chisel3.experimental.dataview._ + +// We recommend putting DataViews in a companion object of one of the involved types +object AXIBundle { + // Don't be afraid of the use of implicits, we will discuss this pattern in more detail later + implicit val axiView = DataView[VerilogAXIBundle, AXIBundle]( + // The first argument is a function constructing an object of View type (AXIBundle) + // from an object of the Target type (VerilogAXIBundle) + vab => new AXIBundle(vab.addrWidth), + // The remaining arguments are a mapping of the corresponding fields of the two types + _.AWVALID -> _.aw.valid, + _.AWREADY -> _.aw.ready, + _.AWID -> _.aw.bits.id, + _.AWADDR -> _.aw.bits.addr, + _.AWLEN -> _.aw.bits.len, + _.AWSIZE -> _.aw.bits.size, + // ... + ) +} +``` + +This `DataView` is a mapping between our flat, Verilog-style AXI Bundle to our more compositional, +Chisel-style AXI Bundle. +It allows us to define our ports to match the expected Verilog interface, while manipulating it as if +it were the more structured type: + +```scala mdoc +class AXIStub extends RawModule { + val AXI = IO(new VerilogAXIBundle(20)) + val view = AXI.viewAs[AXIBundle] + + // We can now manipulate `AXI` via `view` + view.aw.bits := 0.U.asTypeOf(new AXIAddressChannel(20)) // zero everything out by default + view.aw.valid := true.B + when (view.aw.ready) { + view.aw.bits.id := 5.U + view.aw.bits.addr := 1234.U + // We can still manipulate AXI as well + AXI.AWLEN := 1.U + } +} +``` + +This will generate Verilog that matches the standard naming convention: + +```scala mdoc:verilog +emitVerilog(new AXIStub) +``` + +Note that if both the _Target_ and the _View_ types are subtypes of `Data` (as they are in this example), +the `DataView` is _invertible_. +This means that we can easily create a `DataView[AXIBundle, VerilogAXIBundle]` from our existing +`DataView[VerilogAXIBundle, AXIBundle]`, all we need to do is provide a function to construct +a `VerilogAXIBundle` from an instance of an `AXIBundle`: + +```scala mdoc:silent +// Note that typically you should define these together (eg. inside object AXIBundle) +implicit val axiView2 = AXIBundle.axiView.invert(ab => new VerilogAXIBundle(ab.addrWidth)) +``` + +The following example shows this and illustrates another use case of `DataView`—connecting unrelated +types: + +```scala mdoc +class ConnectionExample extends RawModule { + val in = IO(new AXIBundle(20)) + val out = IO(Flipped(new VerilogAXIBundle(20))) + out.viewAs[AXIBundle] <> in +} +``` + +This results in the corresponding fields being connected in the emitted Verilog: + +```scala mdoc:verilog +emitVerilog(new ConnectionExample) +``` + +## Other Use Cases + +While the ability to map between `Bundle` types as in the AXI4 example is pretty compelling, +DataView has many other applications. +Importantly, because the _Target_ of the `DataView` need not be a `Data`, it provides a way to use +`non-Data` objects with APIs that require `Data`. + +### Tuples + +Perhaps the most helpful use of `DataView` for a non-`Data` type is viewing Scala tuples as `Bundles`. +For example, in Chisel prior to the introduction of `DataView`, one might try to `Mux` tuples and +see an error like the following: + +<!-- Todo will need to ensure built-in code for Tuples is suppressed once added to stdlib --> + +```scala mdoc:fail +class TupleExample extends RawModule { + val a, b, c, d = IO(Input(UInt(8.W))) + val cond = IO(Input(Bool())) + val x, y = IO(Output(UInt(8.W))) + (x, y) := Mux(cond, (a, b), (c, d)) +} +``` + +The issue, is that Chisel primitives like `Mux` and `:=` only operate on subtypes of `Data` and +Tuples (as members of the Scala standard library), are not subclasses of `Data`. +`DataView` provides a mechanism to _view_ a `Tuple` as if it were a `Data`: + +<!-- TODO replace this with stdlib import --> + +```scala mdoc:invisible +// ProductDataProduct +implicit val productDataProduct: DataProduct[Product] = new DataProduct[Product] { + def dataIterator(a: Product, path: String): Iterator[(Data, String)] = { + a.productIterator.zipWithIndex.collect { case (d: Data, i) => d -> s"$path._$i" } + } +} +``` + +```scala mdoc +// We need a type to represent the Tuple +class HWTuple2[A <: Data, B <: Data](val _1: A, val _2: B) extends Bundle + +// Provide DataView between Tuple and HWTuple +implicit def view[A <: Data, B <: Data]: DataView[(A, B), HWTuple2[A, B]] = + DataView(tup => new HWTuple2(tup._1.cloneType, tup._2.cloneType), + _._1 -> _._1, _._2 -> _._2) +``` + +Now, we can use `.viewAs` to view Tuples as if they were subtypes of `Data`: + +```scala mdoc +class TupleVerboseExample extends RawModule { + val a, b, c, d = IO(Input(UInt(8.W))) + val cond = IO(Input(Bool())) + val x, y = IO(Output(UInt(8.W))) + (x, y).viewAs[HWTuple2[UInt, UInt]] := Mux(cond, (a, b).viewAs[HWTuple2[UInt, UInt]], (c, d).viewAs[HWTuple2[UInt, UInt]]) +} +``` + +This is much more verbose than the original idea of just using the Tuples directly as if they were `Data`. +We can make this better by providing an implicit conversion that views a `Tuple` as a `HWTuple2`: + +```scala mdoc +implicit def tuple2hwtuple[A <: Data, B <: Data](tup: (A, B)): HWTuple2[A, B] = + tup.viewAs[HWTuple2[A, B]] +``` + +Now, the original code just works! + +```scala mdoc +class TupleExample extends RawModule { + val a, b, c, d = IO(Input(UInt(8.W))) + val cond = IO(Input(Bool())) + val x, y = IO(Output(UInt(8.W))) + (x, y) := Mux(cond, (a, b), (c, d)) +} +``` + +```scala mdoc:invisible +// Always emit Verilog to make sure it actually works +emitVerilog(new TupleExample) +``` + +Note that this example ignored `DataProduct` which is another required piece (see [the documentation +about it below](#dataproduct)). + +All of this is slated to be included the Chisel standard library. + +## Totality and PartialDataView + +A `DataView` is _total_ if all fields of the _Target_ type and all fields of the _View_ type are +included in the mapping. +Chisel will error if a field is accidentally left out from a `DataView`. +For example: + +```scala mdoc +class BundleA extends Bundle { + val foo = UInt(8.W) + val bar = UInt(8.W) +} +class BundleB extends Bundle { + val fizz = UInt(8.W) +} +``` + +```scala mdoc:crash +{ // Using an extra scope here to avoid a bug in mdoc (documentation generation) +// We forgot BundleA.foo in the mapping! +implicit val myView = DataView[BundleA, BundleB](_ => new BundleB, _.bar -> _.fizz) +class BadMapping extends Module { + val in = IO(Input(new BundleA)) + val out = IO(Output(new BundleB)) + out := in.viewAs[BundleB] +} +// We must run Chisel to see the error +emitVerilog(new BadMapping) +} +``` + +As that error suggests, if we *want* the view to be non-total, we can use a `PartialDataView`: + +```scala mdoc +// A PartialDataView does not have to be total for the Target +implicit val myView = PartialDataView[BundleA, BundleB](_ => new BundleB, _.bar -> _.fizz) +class PartialDataViewModule extends Module { + val in = IO(Input(new BundleA)) + val out = IO(Output(new BundleB)) + out := in.viewAs[BundleB] +} +``` + +```scala mdoc:verilog +emitVerilog(new PartialDataViewModule) +``` + +While `PartialDataViews` need not be total for the _Target_, both `PartialDataViews` and `DataViews` +must always be total for the _View_. +This has the consequence that `PartialDataViews` are **not** invertible in the same way as `DataViews`. + +For example: + +```scala mdoc:crash +{ // Using an extra scope here to avoid a bug in mdoc (documentation generation) +implicit val myView2 = myView.invert(_ => new BundleA) +class PartialDataViewModule2 extends Module { + val in = IO(Input(new BundleA)) + val out = IO(Output(new BundleB)) + // Using the inverted version of the mapping + out.viewAs[BundleA] := in +} +// We must run Chisel to see the error +emitVerilog(new PartialDataViewModule2) +} +``` + +As noted, the mapping must **always** be total for the `View`. + +## Advanced Details + +`DataView` takes advantage of features of Scala that may be new to many users of Chisel—in particular +[Type Classes](#type-classes). + +### Type Classes + +[Type classes](https://en.wikipedia.org/wiki/Type_class) are powerful language feature for writing +polymorphic code. +They are a common feature in "modern programming languages" like +Scala, +Swift (see [protocols](https://docs.swift.org/swift-book/LanguageGuide/Protocols.html)), +and Rust (see [traits](https://doc.rust-lang.org/book/ch10-02-traits.html)). +Type classes may appear similar to inheritance in object-oriented programming but there are some +important differences: + +1. You can provide a type class for a type you don't own (eg. one defined in a 3rd party library, + the Scala standard library, or Chisel itself) +2. You can write a single type class for many types that do not have a sub-typing relationship +3. You can provide multiple different type classes for the same type + +For `DataView`, (1) is crucial because we want to be able to implement `DataViews` of built-in Scala +types like tuples and `Seqs`. Furthermore, `DataView` has two type parameters (the _Target_ and the +_View_ types) so inheritance does not really make sense—which type would `extend` `DataView`? + +In Scala 2, type classes are not a built-in language feature, but rather are implemented using implicits. +There are great resources out there for interested readers: +* [Basic Tutorial](https://scalac.io/blog/typeclasses-in-scala/) +* [Fantastic Explanation on StackOverflow](https://stackoverflow.com/a/5598107/2483329) + +Note that Scala 3 has added built-in syntax for type classes that does not apply to Chisel 3 which +currently only supports Scala 2. + +### Implicit Resolution + +Given that `DataView` is implemented using implicits, it is important to understand implicit +resolution. +Whenever the compiler sees an implicit argument is required, it first looks in _current scope_ +before looking in the _implicit scope_. + +1. Current scope + * Values defined in the current scope + * Explicit imports + * Wildcard imports +2. Implicit scope + * Companion object of a type + * Implicit scope of an argument's type + * Implicit scope of type parameters + +If at either stage, multiple implicits are found, then the static overloading rule is used to resolve +it. +Put simply, if one implicit applies to a more-specific type than the other, the more-specific one +will be selected. +If multiple implicits apply within a given stage, then the compiler throws an ambiguous implicit +resolution error. + + +This section draws heavily from [[1]](https://stackoverflow.com/a/5598107/2483329) and +[[2]](https://stackoverflow.com/a/5598107/2483329). +In particular, see [1] for examples. + +#### Implicit Resolution Example + +To help clarify a bit, let us consider how implicit resolution works for `DataView`. +Consider the definition of `viewAs`: + +```scala +def viewAs[V <: Data](implicit dataView: DataView[T, V]): V +``` + +Armed with the knowledge from the previous section, we know that whenever we call `.viewAs`, the +Scala compiler will first look for a `DataView[T, V]` in the current scope (defined in, or imported), +then it will look in the companion objects of `DataView`, `T`, and `V`. +This enables a fairly powerful pattern, namely that default or typical implementations of a `DataView` +should be defined in the companion object for one of the two types. +We can think about `DataViews` defined in this way as "low priority defaults". +They can then be overruled by a specific import if a given user ever wants different behavior. +For example: + +Given the following types: + +```scala mdoc +class Foo extends Bundle { + val a = UInt(8.W) + val b = UInt(8.W) +} +class Bar extends Bundle { + val c = UInt(8.W) + val d = UInt(8.W) +} +object Foo { + implicit val f2b = DataView[Foo, Bar](_ => new Bar, _.a -> _.c, _.b -> _.d) + implicit val b2f = f2b.invert(_ => new Foo) +} +``` + +This provides an implementation of `DataView` in the _implicit scope_ as a "default" mapping between +`Foo` and `Bar` (and it doesn't even require an import!): + +```scala mdoc +class FooToBar extends Module { + val foo = IO(Input(new Foo)) + val bar = IO(Output(new Bar)) + bar := foo.viewAs[Bar] +} +``` + +```scala mdoc:verilog +emitVerilog(new FooToBar) +``` + +However, it's possible that some user of `Foo` and `Bar` wants different behavior, +perhaps they would prefer more of "swizzling" behavior rather than a direct mapping: + +```scala mdoc +object Swizzle { + implicit val swizzle = DataView[Foo, Bar](_ => new Bar, _.a -> _.d, _.b -> _.c) +} +// Current scope always wins over implicit scope +import Swizzle._ +class FooToBarSwizzled extends Module { + val foo = IO(Input(new Foo)) + val bar = IO(Output(new Bar)) + bar := foo.viewAs[Bar] +} +``` + +```scala mdoc:verilog +emitVerilog(new FooToBarSwizzled) +``` + +### DataProduct + +`DataProduct` is a type class used by `DataView` to validate the correctness of a user-provided mapping. +In order for a type to be "viewable" (ie. the `Target` type of a `DataView`), it must have an +implementation of `DataProduct`. + +For example, say we have some non-Bundle type: +```scala mdoc +// Loosely based on chisel3.util.Counter +class MyCounter(val width: Int) { + /** Indicates if the Counter is incrementing this cycle */ + val active = WireDefault(false.B) + val value = RegInit(0.U(width.W)) + def inc(): Unit = { + active := true.B + value := value + 1.U + } + def reset(): Unit = { + value := 0.U + } +} +``` + +Say we want to view `MyCounter` as a `Valid[UInt]`: + +```scala mdoc:fail +import chisel3.util.Valid +implicit val counterView = DataView[MyCounter, Valid[UInt]](c => Valid(UInt(c.width.W)), _.value -> _.bits, _.active -> _.valid) +``` + +As you can see, this fails Scala compliation. +We need to provide an implementation of `DataProduct[MyCounter]` which provides Chisel a way to access +the objects of type `Data` within `MyCounter`: + +```scala mdoc:silent +import chisel3.util.Valid +implicit val counterProduct = new DataProduct[MyCounter] { + // The String part of the tuple is a String path to the object to help in debugging + def dataIterator(a: MyCounter, path: String): Iterator[(Data, String)] = + List(a.value -> s"$path.value", a.active -> s"$path.active").iterator +} +// Now this works +implicit val counterView = DataView[MyCounter, Valid[UInt]](c => Valid(UInt(c.width.W)), _.value -> _.bits, _.active -> _.valid) +``` + +Why is this useful? +This is how Chisel is able to check for totality as [described above](#totality-and-partialdataview). +In addition to checking if a user has left a field out of the mapping, it also allows Chisel to check +if the user has included a `Data` in the mapping that isn't actually a part of the _target_ nor the +_view_. + diff --git a/src/main/scala/chisel3/stage/phases/Convert.scala b/src/main/scala/chisel3/stage/phases/Convert.scala index bf42b58a..b5b01b8d 100644 --- a/src/main/scala/chisel3/stage/phases/Convert.scala +++ b/src/main/scala/chisel3/stage/phases/Convert.scala @@ -29,8 +29,7 @@ class Convert extends Phase { /* Convert all Chisel Annotations to FIRRTL Annotations */ a .circuit - .annotations - .map(_.toFirrtl) ++ + .firrtlAnnotations ++ a .circuit .annotations diff --git a/src/test/scala/chiselTests/ChiselSpec.scala b/src/test/scala/chiselTests/ChiselSpec.scala index e513189e..8e35273d 100644 --- a/src/test/scala/chiselTests/ChiselSpec.scala +++ b/src/test/scala/chiselTests/ChiselSpec.scala @@ -7,9 +7,11 @@ import chisel3.aop.Aspect import chisel3.stage.{ChiselGeneratorAnnotation, ChiselStage, NoRunFirrtlCompilerAnnotation, PrintFullStackTraceAnnotation} import chisel3.testers._ import firrtl.annotations.Annotation +import firrtl.ir.Circuit import firrtl.util.BackendCompilationUtilities import firrtl.{AnnotationSeq, EmittedVerilogCircuitAnnotation} import _root_.logger.Logger +import firrtl.stage.FirrtlCircuitAnnotation import org.scalacheck._ import org.scalatest._ import org.scalatest.flatspec.AnyFlatSpec @@ -87,6 +89,33 @@ trait ChiselRunners extends Assertions with BackendCompilationUtilities { case EmittedVerilogCircuitAnnotation(a) => a.value }.getOrElse(fail("No Verilog circuit was emitted by the FIRRTL compiler!")) } + + def elaborateAndGetModule[A <: RawModule](t: => A): A = { + var res: Any = null + ChiselStage.elaborate { + res = t + res.asInstanceOf[A] + } + res.asInstanceOf[A] + } + + /** Compiles a Chisel Module to FIRRTL + * NOTE: This uses the "test_run_dir" as the default directory for generated code. + * @param t the generator for the module + * @return The FIRRTL Circuit and Annotations _before_ FIRRTL compilation + */ + def getFirrtlAndAnnos(t: => RawModule): (Circuit, Seq[Annotation]) = { + val args = Array( + "--target-dir", + createTestDirectory(this.getClass.getSimpleName).toString, + "--no-run-firrtl" + ) + val annos = (new ChiselStage).execute(args, Seq(ChiselGeneratorAnnotation(() => t))) + val circuit = annos.collectFirst { + case FirrtlCircuitAnnotation(c) => c + }.getOrElse(fail("No FIRRTL Circuit found!!")) + (circuit, annos) + } } /** Spec base class for BDD-style testers. */ diff --git a/src/test/scala/chiselTests/experimental/DataView.scala b/src/test/scala/chiselTests/experimental/DataView.scala new file mode 100644 index 00000000..381cfeb5 --- /dev/null +++ b/src/test/scala/chiselTests/experimental/DataView.scala @@ -0,0 +1,542 @@ +// See LICENSE for license details. + +package chiselTests.experimental + +import chiselTests.ChiselFlatSpec +import chisel3._ +import chisel3.experimental.dataview._ +import chisel3.experimental.DataMirror.internal.chiselTypeClone +import chisel3.stage.ChiselStage +import chisel3.util.{Decoupled, DecoupledIO} + +object SimpleBundleDataView { + class BundleA(val w: Int) extends Bundle { + val foo = UInt(w.W) + } + class BundleB(val w: Int) extends Bundle { + val bar = UInt(w.W) + } + implicit val v1 = DataView[BundleA, BundleB](a => new BundleB(a.w), _.foo -> _.bar) + implicit val v2 = v1.invert(b => new BundleA(b.w)) +} + +object VecBundleDataView { + class MyBundle extends Bundle { + val foo = UInt(8.W) + val bar = UInt(8.W) + } + implicit val v1: DataView[MyBundle, Vec[UInt]] = DataView(_ => Vec(2, UInt(8.W)), _.foo -> _(1), _.bar -> _(0)) + implicit val v2 = v1.invert(_ => new MyBundle) +} + +// This should become part of Chisel in a later PR +object Tuple2DataProduct { + implicit def tuple2DataProduct[A : DataProduct, B : DataProduct] = new DataProduct[(A, B)] { + def dataIterator(tup: (A, B), path: String): Iterator[(Data, String)] = { + val dpa = implicitly[DataProduct[A]] + val dpb = implicitly[DataProduct[B]] + val (a, b) = tup + dpa.dataIterator(a, s"$path._1") ++ dpb.dataIterator(b, s"$path._2") + } + } +} + +// This should become part of Chisel in a later PR +object HWTuple { + import Tuple2DataProduct._ + + class HWTuple2[A <: Data, B <: Data](val _1: A, val _2: B) extends Bundle + + implicit def view[T1 : DataProduct, T2 : DataProduct, V1 <: Data, V2 <: Data]( + implicit v1: DataView[T1, V1], v2: DataView[T2, V2] + ): DataView[(T1, T2), HWTuple2[V1, V2]] = + DataView.mapping( + { case (a, b) => new HWTuple2(a.viewAs[V1].cloneType, b.viewAs[V2].cloneType)}, + { case ((a, b), hwt) => + Seq(a.viewAs[V1] -> hwt._1, + b.viewAs[V2] -> hwt._2) + } + ) + + implicit def tuple2hwtuple[T1 : DataProduct, T2 : DataProduct, V1 <: Data, V2 <: Data]( + tup: (T1, T2))(implicit v1: DataView[T1, V1], v2: DataView[T2, V2] + ): HWTuple2[V1, V2] = tup.viewAs[HWTuple2[V1, V2]] +} + +// This should become part of Chisel in a later PR +object SeqDataProduct { + // Should we special case Seq[Data]? + implicit def seqDataProduct[A : DataProduct]: DataProduct[Seq[A]] = new DataProduct[Seq[A]] { + def dataIterator(a: Seq[A], path: String): Iterator[(Data, String)] = { + val dpa = implicitly[DataProduct[A]] + a.iterator + .zipWithIndex + .flatMap { case (elt, idx) => + dpa.dataIterator(elt, s"$path[$idx]") + } + } + } +} + +object SeqToVec { + import SeqDataProduct._ + + // TODO this would need a better way to determine the prototype for the Vec + implicit def seqVec[A : DataProduct, B <: Data](implicit dv: DataView[A, B]): DataView[Seq[A], Vec[B]] = + DataView.mapping[Seq[A], Vec[B]]( + xs => Vec(xs.size, chiselTypeClone(xs.head.viewAs[B])), // xs.head is not correct in general + { case (s, v) => s.zip(v).map { case (a, b) => a.viewAs[B] -> b } } + ) + + implicit def seq2Vec[A : DataProduct, B <: Data](xs: Seq[A])(implicit dv: DataView[A, B]): Vec[B] = + xs.viewAs[Vec[B]] +} + +class DataViewSpec extends ChiselFlatSpec { + + behavior of "DataView" + + it should "support simple Bundle viewing" in { + import SimpleBundleDataView._ + class MyModule extends Module { + val in = IO(Input(new BundleA(8))) + val out = IO(Output(new BundleB(8))) + out := in.viewAs[BundleB] + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + chirrtl should include("out.bar <= in.foo") + } + + it should "be a bidirectional mapping" in { + import SimpleBundleDataView._ + class MyModule extends Module { + val in = IO(Input(new BundleA(8))) + val out = IO(Output(new BundleB(8))) + out.viewAs[BundleA] := in + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + chirrtl should include("out.bar <= in.foo") + } + + it should "handle viewing UInts as UInts" in { + class MyModule extends Module { + val in = IO(Input(UInt(8.W))) + val foo = IO(Output(UInt(8.W))) + val bar = IO(Output(UInt(8.W))) + foo := in.viewAs[UInt] + bar.viewAs[UInt] := in + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + chirrtl should include("foo <= in") + chirrtl should include("bar <= in") + } + + it should "handle viewing Bundles as their same concrete type" in { + class MyBundle extends Bundle { + val foo = UInt(8.W) + } + class MyModule extends Module { + val in = IO(Input(new MyBundle)) + val fizz = IO(Output(new MyBundle)) + val buzz = IO(Output(new MyBundle)) + fizz := in.viewAs[MyBundle] + buzz.viewAs[MyBundle] := in + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + chirrtl should include("fizz.foo <= in.foo") + chirrtl should include("buzz.foo <= in.foo") + } + + it should "handle viewing Vecs as their same concrete type" in { + class MyModule extends Module { + val in = IO(Input(Vec(1, UInt(8.W)))) + val fizz = IO(Output(Vec(1, UInt(8.W)))) + val buzz = IO(Output(Vec(1, UInt(8.W)))) + fizz := in.viewAs[Vec[UInt]] + buzz.viewAs[Vec[UInt]] := in + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + chirrtl should include("fizz[0] <= in[0]") + chirrtl should include("buzz[0] <= in[0]") + } + + it should "handle viewing Vecs as Bundles and vice versa" in { + import VecBundleDataView._ + class MyModule extends Module { + val in = IO(Input(new MyBundle)) + val out = IO(Output(Vec(2, UInt(8.W)))) + val out2 = IO(Output(Vec(2, UInt(8.W)))) + out := in.viewAs[Vec[UInt]] + out2.viewAs[MyBundle] := in + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + chirrtl should include("out[0] <= in.bar") + chirrtl should include("out[1] <= in.foo") + chirrtl should include("out2[0] <= in.bar") + chirrtl should include("out2[1] <= in.foo") + } + + it should "work with bidirectional connections for nested types" in { + class FizzBuzz extends Bundle { + val fizz = UInt(8.W) + val buzz = UInt(8.W) + } + class FlatDecoupled extends Bundle { + val valid = Output(Bool()) + val ready = Input(Bool()) + val fizz = Output(UInt(8.W)) + val buzz = Output(UInt(8.W)) + } + implicit val view = DataView[FlatDecoupled, DecoupledIO[FizzBuzz]]( + _ => Decoupled(new FizzBuzz), + _.valid -> _.valid, + _.ready -> _.ready, + _.fizz -> _.bits.fizz, + _.buzz -> _.bits.buzz + ) + implicit val view2 = view.invert(_ => new FlatDecoupled) + class MyModule extends Module { + val enq = IO(Flipped(Decoupled(new FizzBuzz))) + val deq = IO(new FlatDecoupled) + val deq2 = IO(new FlatDecoupled) + deq <> enq.viewAs[FlatDecoupled] + deq2.viewAs[DecoupledIO[FizzBuzz]] <> enq + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + chirrtl should include("deq.valid <= enq.valid") + chirrtl should include("enq.ready <= deq.ready") + chirrtl should include("deq.fizz <= enq.bits.fizz") + chirrtl should include("deq.buzz <= enq.bits.buzz") + chirrtl should include("deq2.valid <= enq.valid") + chirrtl should include("enq.ready <= deq2.ready") + chirrtl should include("deq2.fizz <= enq.bits.fizz") + chirrtl should include("deq2.buzz <= enq.bits.buzz") + } + + it should "support viewing a Bundle as a Parent Bundle type" in { + class Foo extends Bundle { + val foo = UInt(8.W) + } + class Bar extends Foo { + val bar = UInt(8.W) + } + class MyModule extends Module { + val fooIn = IO(Input(new Foo)) + val barOut = IO(Output(new Bar)) + barOut.viewAsSupertype(new Foo) := fooIn + + val barIn = IO(Input(new Bar)) + val fooOut = IO(Output(new Foo)) + fooOut := barIn.viewAsSupertype(new Foo) + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + chirrtl should include("barOut.foo <= fooIn.foo") + chirrtl should include("fooOut.foo <= barIn.foo") + } + + it should "error if viewing a parent Bundle as a child Bundle type" in { + assertTypeError(""" + class Foo extends Bundle { + val foo = UInt(8.W) + } + class Bar extends Foo { + val bar = UInt(8.W) + } + class MyModule extends Module { + val barIn = IO(Input(new Bar)) + val fooOut = IO(Output(new Foo)) + fooOut.viewAs(new Bar) := barIn + } + """) + } + + it should "work in UInt operations" in { + class MyBundle extends Bundle { + val value = UInt(8.W) + } + class MyModule extends Module { + val a = IO(Input(UInt(8.W))) + val b = IO(Input(new MyBundle)) + val cond = IO(Input(Bool())) + val and, mux, bitsCat = IO(Output(UInt(8.W))) + // Chisel unconditionally emits a node, so name it at least + val x = a.viewAs[UInt] & b.viewAs[MyBundle].value + and.viewAs[UInt] := x + + val y = Mux(cond.viewAs[Bool], a.viewAs[UInt], b.value.viewAs[UInt]) + mux.viewAs[UInt] := y + + // TODO should we have a macro so that we don't need .apply? + val aBits = a.viewAs[UInt].apply(3, 0) + val bBits = b.viewAs[MyBundle].value(3, 0) + val abCat = aBits.viewAs[UInt] ## bBits.viewAs[UInt] + bitsCat := abCat + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + val expected = List( + "node x = and(a, b.value)", + "and <= x", + "node y = mux(cond, a, b.value)", + "mux <= y", + "node aBits = bits(a, 3, 0)", + "node bBits = bits(b.value, 3, 0)", + "node abCat = cat(aBits, bBits)", + "bitsCat <= abCat" + ) + for (line <- expected) { + chirrtl should include(line) + } + } + + it should "support .asUInt of Views" in { + import VecBundleDataView._ + class MyModule extends Module { + val barIn = IO(Input(new MyBundle)) + val fooOut = IO(Output(UInt())) + val cat = barIn.viewAs[Vec[UInt]].asUInt + fooOut := cat + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + chirrtl should include ("node cat = cat(barIn.foo, barIn.bar)") + chirrtl should include ("fooOut <= cat") + } + + it should "be composable" in { + // Given DataView[A, B] and DataView[B, C], derive DataView[A, C] + class Foo(val foo: UInt) extends Bundle + class Bar(val bar: UInt) extends Bundle + class Fizz(val fizz: UInt) extends Bundle + + implicit val foo2bar = DataView[Foo, Bar](f => new Bar(chiselTypeClone(f.foo)), _.foo -> _.bar) + implicit val bar2fizz = DataView[Bar, Fizz](b => new Fizz(chiselTypeClone(b.bar)), _.bar -> _.fizz) + + implicit val foo2fizz: DataView[Foo, Fizz] = foo2bar.andThen(bar2fizz) + + class MyModule extends Module { + val a, b = IO(Input(new Foo(UInt(8.W)))) + val y, z = IO(Output(new Fizz(UInt(8.W)))) + y := a.viewAs[Fizz] + z := b.viewAs[Bar].viewAs[Fizz] + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + chirrtl should include ("y.fizz <= a.foo") + chirrtl should include ("z.fizz <= b.foo") + } + + // This example should be turned into a built-in feature + it should "enable implementing \"HardwareTuple\"" in { + import HWTuple._ + + class MyModule extends Module { + val a, b, c, d = IO(Input(UInt(8.W))) + val sel = IO(Input(Bool())) + val y, z = IO(Output(UInt(8.W))) + (y, z) := Mux(sel, (a, b), (c, d)) + } + // Verilog instead of CHIRRTL because the optimizations make it much prettier + val verilog = ChiselStage.emitVerilog(new MyModule) + verilog should include ("assign y = sel ? a : c;") + verilog should include ("assign z = sel ? b : d;") + } + + it should "support nesting of tuples" in { + import Tuple2DataProduct._ + import HWTuple._ + + class MyModule extends Module { + val a, b, c, d = IO(Input(UInt(8.W))) + val w, x, y, z = IO(Output(UInt(8.W))) + ((w, x), (y, z)) := ((a, b), (c, d)) + } + val chirrtl = ChiselStage.emitChirrtl(new MyModule) + chirrtl should include ("w <= a") + chirrtl should include ("x <= b") + chirrtl should include ("y <= c") + chirrtl should include ("z <= d") + } + + // This example should be turned into a built-in feature + it should "enable viewing Seqs as Vecs" in { + import SeqToVec._ + + class MyModule extends Module { + val a, b, c = IO(Input(UInt(8.W))) + val x, y, z = IO(Output(UInt(8.W))) + Seq(x, y, z) := VecInit(a, b, c) + } + // Verilog instead of CHIRRTL because the optimizations make it much prettier + val verilog = ChiselStage.emitVerilog(new MyModule) + verilog should include ("assign x = a;") + verilog should include ("assign y = b;") + verilog should include ("assign z = c;") + } + + it should "support recursive composition of views" in { + import Tuple2DataProduct._ + import SeqDataProduct._ + import SeqToVec._ + import HWTuple._ + + class MyModule extends Module { + val a, b, c, d = IO(Input(UInt(8.W))) + val w, x, y, z = IO(Output(UInt(8.W))) + + // A little annoying that we need the type annotation on VecInit to get the implicit conversion to work + // Note that one can just use the Seq on the RHS so there is an alternative (may lack discoverability) + // We could also overload `VecInit` instead of relying on the implicit conversion + Seq((w, x), (y, z)) := VecInit[HWTuple2[UInt, UInt]]((a, b), (c, d)) + } + val verilog = ChiselStage.emitVerilog(new MyModule) + verilog should include ("assign w = a;") + verilog should include ("assign x = b;") + verilog should include ("assign y = c;") + verilog should include ("assign z = d;") + } + + it should "error if you try to dynamically index a Vec view" in { + import SeqDataProduct._ + import SeqToVec._ + import Tuple2DataProduct._ + import HWTuple._ + + class MyModule extends Module { + val inA, inB = IO(Input(UInt(8.W))) + val outA, outB = IO(Output(UInt(8.W))) + val idx = IO(Input(UInt(1.W))) + + val a, b, c, d = RegInit(0.U) + + // Dynamic indexing is more of a "generator" in Chisel3 than an individual node + val selected = Seq((a, b), (c, d)).apply(idx) + selected := (inA, inB) + (outA, outB) := selected + } + (the [InvalidViewException] thrownBy { + ChiselStage.emitChirrtl(new MyModule) + }).getMessage should include ("Dynamic indexing of Views is not yet supported") + } + + it should "error if the mapping is non-total in the view" in { + class MyBundle(val foo: UInt, val bar: UInt) extends Bundle + implicit val dv = DataView[UInt, MyBundle](_ => new MyBundle(UInt(), UInt()), _ -> _.bar) + class MyModule extends Module { + val tpe = new MyBundle(UInt(8.W), UInt(8.W)) + val in = IO(Input(UInt(8.W))) + val out = IO(Output(tpe)) + out := in.viewAs[MyBundle] + } + val err = the [InvalidViewException] thrownBy (ChiselStage.emitVerilog(new MyModule)) + err.toString should include ("View field '_.foo' is missing") + } + + it should "error if the mapping is non-total in the target" in { + import Tuple2DataProduct._ + implicit val dv = DataView[(UInt, UInt), UInt](_ => UInt(), _._1 -> _) + class MyModule extends Module { + val a, b = IO(Input(UInt(8.W))) + val out = IO(Output(UInt(8.W))) + out := (a, b).viewAs[UInt] + } + val err = the [InvalidViewException] thrownBy (ChiselStage.emitVerilog(new MyModule)) + err.toString should include ("Target field '_._2' is missing") + } + + it should "error if the mapping contains Data that are not part of the Target" in { + class BundleA extends Bundle { + val foo = UInt(8.W) + } + class BundleB extends Bundle { + val fizz = UInt(8.W) + val buzz = UInt(8.W) + } + implicit val dv = DataView[BundleA, BundleB](_ => new BundleB, _.foo -> _.fizz, (_, b) => (3.U, b.buzz)) + class MyModule extends Module { + val in = IO(Input(new BundleA)) + val out = IO(Output(new BundleB)) + out := in.viewAs[BundleB] + } + val err = the [InvalidViewException] thrownBy (ChiselStage.emitVerilog(new MyModule)) + err.toString should include ("View mapping must only contain Elements within the Target") + } + + it should "error if the mapping contains Data that are not part of the View" in { + class BundleA extends Bundle { + val foo = UInt(8.W) + } + class BundleB extends Bundle { + val fizz = UInt(8.W) + val buzz = UInt(8.W) + } + implicit val dv = DataView[BundleA, BundleB](_ => new BundleB, _.foo -> _.fizz, (_, b) => (3.U, b.buzz)) + implicit val dv2 = dv.invert(_ => new BundleA) + class MyModule extends Module { + val in = IO(Input(new BundleA)) + val out = IO(Output(new BundleB)) + out.viewAs[BundleA] := in + } + val err = the [InvalidViewException] thrownBy (ChiselStage.emitVerilog(new MyModule)) + err.toString should include ("View mapping must only contain Elements within the View") + } + + it should "error if a view has a width that does not match the target" in { + class BundleA extends Bundle { + val foo = UInt(8.W) + } + class BundleB extends Bundle { + val bar = UInt(4.W) + } + implicit val dv = DataView[BundleA, BundleB](_ => new BundleB, _.foo -> _.bar) + class MyModule extends Module { + val in = IO(Input(new BundleA)) + val out = IO(Output(new BundleB)) + out := in.viewAs[BundleB] + } + val err = the [InvalidViewException] thrownBy ChiselStage.emitChirrtl(new MyModule) + val expected = """View field _\.bar UInt<4> has width <4> that is incompatible with target value .+'s width <8>""".r + err.getMessage should fullyMatch regex expected + } + + it should "error if a view has a known width when the target width is unknown" in { + class BundleA extends Bundle { + val foo = UInt() + } + class BundleB extends Bundle { + val bar = UInt(4.W) + } + implicit val dv = DataView[BundleA, BundleB](_ => new BundleB, _.foo -> _.bar) + class MyModule extends Module { + val in = IO(Input(new BundleA)) + val out = IO(Output(new BundleB)) + out := in.viewAs[BundleB] + } + val err = the [InvalidViewException] thrownBy ChiselStage.emitChirrtl(new MyModule) + val expected = """View field _\.bar UInt<4> has width <4> that is incompatible with target value .+'s width <unknown>""".r + err.getMessage should fullyMatch regex expected + } + + behavior of "PartialDataView" + + it should "still error if the mapping is non-total in the view" in { + class MyBundle(val foo: UInt, val bar: UInt) extends Bundle + implicit val dv = PartialDataView[UInt, MyBundle](_ => new MyBundle(UInt(), UInt()), _ -> _.bar) + class MyModule extends Module { + val in = IO(Input(UInt(8.W))) + val out = IO(Output(new MyBundle(UInt(8.W), UInt(8.W)))) + out := in.viewAs[MyBundle] + } + val err = the [InvalidViewException] thrownBy (ChiselStage.emitVerilog(new MyModule)) + err.toString should include ("View field '_.foo' is missing") + } + + it should "NOT error if the mapping is non-total in the target" in { + import Tuple2DataProduct._ + implicit val dv = PartialDataView[(UInt, UInt), UInt](_ => UInt(), _._2 -> _) + class MyModule extends Module { + val a, b = IO(Input(UInt(8.W))) + val out = IO(Output(UInt(8.W))) + out := (a, b).viewAs[UInt] + } + val verilog = ChiselStage.emitVerilog(new MyModule) + verilog should include ("assign out = b;") + } +} diff --git a/src/test/scala/chiselTests/experimental/DataViewIntegrationSpec.scala b/src/test/scala/chiselTests/experimental/DataViewIntegrationSpec.scala new file mode 100644 index 00000000..3f149f75 --- /dev/null +++ b/src/test/scala/chiselTests/experimental/DataViewIntegrationSpec.scala @@ -0,0 +1,57 @@ +// See LICENSE for license details. + +package chiselTests.experimental + +import chisel3._ +import chisel3.experimental.{BaseModule, ExtModule} +import chisel3.experimental.dataview._ +import chisel3.util.{Decoupled, DecoupledIO, Queue, QueueIO, log2Ceil} +import chiselTests.ChiselFlatSpec +import firrtl.transforms.DontTouchAnnotation + +// Let's put it all together! +object DataViewIntegrationSpec { + + class QueueIntf[T <: Data](gen: T, entries: Int) extends Bundle { + val ports = new QueueIO(gen, entries) + // Let's grab a reference to something internal too + // Output because can't have directioned and undirectioned stuff + val enq_ptr = Output(UInt(log2Ceil(entries).W)) + } + + // It's not clear if a view of a Module ever _can_ be total since internal nodes are part of the Module + implicit def queueView[T <: Data] = PartialDataView[Queue[T], QueueIntf[T]]( + q => new QueueIntf(q.gen, q.entries), + _.io -> _.ports, + // Some token internal signal + _.enq_ptr.value -> _.enq_ptr + ) + + object MyQueue { + def apply[T <: Data](enq: DecoupledIO[T], n: Int): QueueIntf[T] = { + val queue = Module(new Queue[T](enq.bits.cloneType, n)) + val view = queue.viewAs[QueueIntf[T]] + view.ports.enq <> enq + view + } + } + + class MyModule extends Module { + val enq = IO(Flipped(Decoupled(UInt(8.W)))) + val deq = IO(Decoupled(UInt(8.W))) + + val queue = MyQueue(enq, 4) + deq <> queue.ports.deq + dontTouch(queue.enq_ptr) + } +} + +class DataViewIntegrationSpec extends ChiselFlatSpec { + import DataViewIntegrationSpec.MyModule + + "Users" should "be able to view and annotate Modules" in { + val (_, annos) = getFirrtlAndAnnos(new MyModule) + val ts = annos.collect { case DontTouchAnnotation(t) => t.serialize } + ts should equal (Seq("~MyModule|Queue>enq_ptr_value")) + } +} diff --git a/src/test/scala/chiselTests/experimental/DataViewTargetSpec.scala b/src/test/scala/chiselTests/experimental/DataViewTargetSpec.scala new file mode 100644 index 00000000..41636da7 --- /dev/null +++ b/src/test/scala/chiselTests/experimental/DataViewTargetSpec.scala @@ -0,0 +1,169 @@ +// See LICENSE for license details. + +package chiselTests.experimental + +import chisel3._ +import chisel3.experimental.dataview._ +import chisel3.experimental.{ChiselAnnotation, annotate} +import chisel3.stage.ChiselStage +import chiselTests.ChiselFlatSpec + +object DataViewTargetSpec { + import firrtl.annotations._ + private case class DummyAnno(target: ReferenceTarget, id: Int) extends SingleTargetAnnotation[ReferenceTarget] { + override def duplicate(n: ReferenceTarget) = this.copy(target = n) + } + private def mark(d: Data, id: Int) = annotate(new ChiselAnnotation { + override def toFirrtl: Annotation = DummyAnno(d.toTarget, id) + }) + private def markAbs(d: Data, id: Int) = annotate(new ChiselAnnotation { + override def toFirrtl: Annotation = DummyAnno(d.toAbsoluteTarget, id) + }) +} + +class DataViewTargetSpec extends ChiselFlatSpec { + import DataViewTargetSpec._ + private val checks: Seq[Data => String] = Seq( + _.toTarget.toString, + _.toAbsoluteTarget.toString, + _.instanceName, + _.pathName, + _.parentPathName, + _.parentModName, + ) + + // Check helpers + private def checkAll(impl: Data, refs: String*): Unit = { + refs.size should be (checks.size) + for ((check, value) <- checks.zip(refs)) { + check(impl) should be (value) + } + } + private def checkSameAs(impl: Data, refs: Data*): Unit = + for (ref <- refs) { + checkAll(impl, checks.map(_(ref)):_*) + } + + behavior of "DataView Naming" + + it should "support views of Elements" in { + class MyChild extends Module { + val out = IO(Output(UInt(8.W))) + val insideView = out.viewAs[UInt] + out := 0.U + } + class MyParent extends Module { + val out = IO(Output(UInt(8.W))) + val inst = Module(new MyChild) + out := inst.out + } + val m = elaborateAndGetModule(new MyParent) + val outsideView = m.inst.out.viewAs[UInt] + checkSameAs(m.inst.out, m.inst.insideView, outsideView) + } + + it should "support 1:1 mappings of Aggregates and their children" in { + class MyBundle extends Bundle { + val foo = UInt(8.W) + val bars = Vec(2, UInt(8.W)) + } + implicit val dv = DataView[MyBundle, Vec[UInt]](_ => Vec(3, UInt(8.W)), _.foo -> _(0), _.bars(0) -> _(1), _.bars(1) -> _(2)) + class MyChild extends Module { + val out = IO(Output(new MyBundle)) + val outView = out.viewAs[Vec[UInt]] // Note different type + val outFooView = out.foo.viewAs[UInt] + val outBarsView = out.bars.viewAs[Vec[UInt]] + val outBars0View = out.bars(0).viewAs[UInt] + out := 0.U.asTypeOf(new MyBundle) + } + class MyParent extends Module { + val out = IO(Output(new MyBundle)) + val inst = Module(new MyChild) + out := inst.out + } + val m = elaborateAndGetModule(new MyParent) + val outView = m.inst.out.viewAs[Vec[UInt]]// Note different type + val outFooView = m.inst.out.foo.viewAs[UInt] + val outBarsView = m.inst.out.bars.viewAs[Vec[UInt]] + val outBars0View = m.inst.out.bars(0).viewAs[UInt] + + checkSameAs(m.inst.out, m.inst.outView, outView) + checkSameAs(m.inst.out.foo, m.inst.outFooView, m.inst.outView(0), outFooView, outView(0)) + checkSameAs(m.inst.out.bars, m.inst.outBarsView, outBarsView) + checkSameAs(m.inst.out.bars(0), m.inst.outBars0View, outBars0View, m.inst.outView(1), outView(1), + m.inst.outBarsView(0), outBarsView(0)) + } + + // Ideally this would work 1:1 but that requires changing the binding + it should "support annotation renaming of Aggregate children of Aggregate views" in { + class MyBundle extends Bundle { + val foo = Vec(2, UInt(8.W)) + } + class MyChild extends Module { + val out = IO(Output(new MyBundle)) + val outView = out.viewAs[MyBundle] + mark(out.foo, 0) + mark(outView.foo, 1) + markAbs(out.foo, 2) + markAbs(outView, 3) + out := 0.U.asTypeOf(new MyBundle) + } + class MyParent extends Module { + val out = IO(Output(new MyBundle)) + val inst = Module(new MyChild) + out := inst.out + } + val (_, annos) = getFirrtlAndAnnos(new MyParent) + val pairs = annos.collect { case DummyAnno(t, idx) => (idx, t.toString) }.sortBy(_._1) + val expected = Seq( + 0 -> "~MyParent|MyChild>out.foo", + // The child of the view that was itself an Aggregate got split because 1:1 is lacking here + 1 -> "~MyParent|MyChild>out.foo[0]", + 1 -> "~MyParent|MyChild>out.foo[1]", + 2 -> "~MyParent|MyParent/inst:MyChild>out.foo", + 3 -> "~MyParent|MyParent/inst:MyChild>out" + ) + pairs should equal (expected) + } + + it should "support annotating views that cannot be mapped to a single ReferenceTarget" in { + import HWTuple._ + class MyBundle extends Bundle { + val a, b = Input(UInt(8.W)) + val c, d = Output(UInt(8.W)) + } + // Note that each use of a Tuple as Data causes an implicit conversion creating a View + class MyChild extends Module { + val io = IO(new MyBundle) + (io.c, io.d) := (io.a, io.b) + // The type annotations create the views via the implicit conversion + val view1: Data = (io.a, io.b) + val view2: Data = (io.c, io.d) + mark(view1, 0) + mark(view2, 1) + markAbs(view1, 2) + markAbs(view2, 3) + mark((io.b, io.d), 4) // Mix it up for fun + } + class MyParent extends Module { + val io = IO(new MyBundle) + val inst = Module(new MyChild) + io <> inst.io + } + val (_, annos) = getFirrtlAndAnnos(new MyParent) + val pairs = annos.collect { case DummyAnno(t, idx) => (idx, t.toString) }.sorted + val expected = Seq( + 0 -> "~MyParent|MyChild>io.a", + 0 -> "~MyParent|MyChild>io.b", + 1 -> "~MyParent|MyChild>io.c", + 1 -> "~MyParent|MyChild>io.d", + 2 -> "~MyParent|MyParent/inst:MyChild>io.a", + 2 -> "~MyParent|MyParent/inst:MyChild>io.b", + 3 -> "~MyParent|MyParent/inst:MyChild>io.c", + 3 -> "~MyParent|MyParent/inst:MyChild>io.d", + 4 -> "~MyParent|MyChild>io.b", + 4 -> "~MyParent|MyChild>io.d", + ) + pairs should equal (expected) + } +} diff --git a/src/test/scala/chiselTests/experimental/ModuleDataProductSpec.scala b/src/test/scala/chiselTests/experimental/ModuleDataProductSpec.scala new file mode 100644 index 00000000..78986517 --- /dev/null +++ b/src/test/scala/chiselTests/experimental/ModuleDataProductSpec.scala @@ -0,0 +1,91 @@ +// See LICENSE for license details. + +package chiselTests.experimental + +import chisel3._ +import chisel3.experimental.{BaseModule, ExtModule} +import chisel3.experimental.dataview.DataProduct +import chiselTests.ChiselFlatSpec + +object ModuleDataProductSpec { + class MyBundle extends Bundle { + val foo = UInt(8.W) + val bar = UInt(8.W) + } + trait MyIntf extends BaseModule { + val in = IO(Input(new MyBundle)) + val out = IO(Output(new MyBundle)) + } + class Passthrough extends RawModule { + val in = IO(Input(UInt(8.W))) + val out = IO(Output(UInt(8.W))) + out := in + } + class MyUserModule extends Module with MyIntf { + val inst = Module(new Passthrough) + inst.in := in.foo + val r = RegNext(in) + out := r + } + + class MyExtModule extends ExtModule with MyIntf + class MyExtModuleWrapper extends RawModule with MyIntf { + val inst = Module(new MyExtModule) + inst.in := in + out := inst.out + } +} + +class ModuleDataProductSpec extends ChiselFlatSpec { + import ModuleDataProductSpec._ + + behavior of "DataProduct" + + it should "work for UserModules (recursively)" in { + val m = elaborateAndGetModule(new MyUserModule) + val expected = Seq( + m.clock -> "m.clock", + m.reset -> "m.reset", + m.in -> "m.in", + m.in.foo -> "m.in.foo", + m.in.bar -> "m.in.bar", + m.out -> "m.out", + m.out.foo -> "m.out.foo", + m.out.bar -> "m.out.bar", + m.r -> "m.r", + m.r.foo -> "m.r.foo", + m.r.bar -> "m.r.bar", + m.inst.in -> "m.inst.in", + m.inst.out -> "m.inst.out" + ) + + val impl = implicitly[DataProduct[MyUserModule]] + val set = impl.dataSet(m) + for ((d, _) <- expected) { + set(d) should be (true) + } + val it = impl.dataIterator(m, "m") + it.toList should contain theSameElementsAs (expected) + } + + it should "work for (wrapped) ExtModules" in { + val m = elaborateAndGetModule(new MyExtModuleWrapper).inst + val expected = Seq( + m.in -> "m.in", + m.in.foo -> "m.in.foo", + m.in.bar -> "m.in.bar", + m.out -> "m.out", + m.out.foo -> "m.out.foo", + m.out.bar -> "m.out.bar" + ) + + val impl = implicitly[DataProduct[MyExtModule]] + val set = impl.dataSet(m) + for ((d, _) <- expected) { + set(d) should be (true) + } + val it = impl.dataIterator(m, "m") + it.toList should contain theSameElementsAs (expected) + } + +} diff --git a/src/test/scala/chiselTests/stage/ChiselOptionsViewSpec.scala b/src/test/scala/chiselTests/stage/ChiselOptionsViewSpec.scala index 35e354a6..99c0f7c0 100644 --- a/src/test/scala/chiselTests/stage/ChiselOptionsViewSpec.scala +++ b/src/test/scala/chiselTests/stage/ChiselOptionsViewSpec.scala @@ -4,6 +4,7 @@ package chiselTests.stage import firrtl.options.Viewer.view +import firrtl.RenameMap import chisel3.stage._ import chisel3.internal.firrtl.Circuit @@ -15,7 +16,7 @@ class ChiselOptionsViewSpec extends AnyFlatSpec with Matchers { behavior of ChiselOptionsView.getClass.getName it should "construct a view from an AnnotationSeq" in { - val bar = Circuit("bar", Seq.empty, Seq.empty) + val bar = Circuit("bar", Seq.empty, Seq.empty, RenameMap()) val annotations = Seq( NoRunFirrtlCompilerAnnotation, PrintFullStackTraceAnnotation, |
