Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

VecLiterals First Pass #1793

Closed
wants to merge 15 commits into from
261 changes: 245 additions & 16 deletions core/src/main/scala/chisel3/Aggregate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ package chisel3
import scala.collection.immutable.ListMap
import scala.collection.mutable.{HashSet, LinkedHashMap}
import scala.language.experimental.macros

import chisel3.experimental.BaseModule
import chisel3.experimental.BundleLiteralException
import chisel3.experimental.EnumType
import chisel3.experimental.{BaseModule, BundleLiteralException, ChiselEnum, EnumType, VecLiteralException}
import chisel3.internal._
import chisel3.internal.Builder.pushCommand
import chisel3.internal.firrtl._
import chisel3.internal.sourceinfo._

import scala.collection.mutable

class AliasedAggregateFieldException(message: String) extends ChiselException(message)

/** An abstract class for data types that solely consist of (are an aggregate
Expand Down Expand Up @@ -51,19 +50,32 @@ sealed abstract class Aggregate extends Data {
*/
override def litOption: Option[BigInt] = {
// Shift the accumulated value by our width and add in our component, masked by our width.
def shiftAdd(accumulator: Option[BigInt], elt: Data): Option[BigInt] = (accumulator, elt.litOption()) match {
case (Some(accumulator), Some(eltLit)) =>
val width = elt.width.get
val masked = ((BigInt(1) << width) - 1) & eltLit // also handles the negative case with two's complement
Some((accumulator << width) + masked)
case (None, _) => None
case (_, None) => None
def shiftAdd(accumulator: Option[BigInt], elt: Data): Option[BigInt] = {
(accumulator, elt.litOption()) match {
case (Some(accumulator), Some(eltLit)) =>
val width = elt.width.get
val masked = ((BigInt(1) << width) - 1) & eltLit // also handles the negative case with two's complement
Some((accumulator << width) + masked)
case (None, _) => None
case (_, None) => None
}
}

topBindingOpt match {
case Some(BundleLitBinding(_)) =>
getElements
.reverse
.foldLeft[Option[BigInt]](Some(BigInt(0)))(shiftAdd)
case Some(VecLitBinding(elementsToValue)) =>
if (elementsToValue.size != this.asInstanceOf[Vec[_]].length) {
None
} else {
elementsToValue
.toList
.map(_._1)
.reverse
.foldLeft[Option[BigInt]](Some(BigInt(0)))(shiftAdd)
}
case _ => None
}
}
Expand Down Expand Up @@ -155,9 +167,18 @@ trait VecFactory extends SourceInfoDoc {
*/
sealed class Vec[T <: Data] private[chisel3] (gen: => T, val length: Int)
extends Aggregate with VecLike[T] {

override def toString: String = {
val bindingString = topBindingOpt match {
case Some(VecLitBinding(vecLitBinding)) =>
val contents = vecLitBinding.zipWithIndex.map { case ((data, lit), index) =>
s"$index=$lit"
}.mkString(", ")
s"($contents"
case _ => bindingToString
}
val elementType = sample_element.cloneType
s"$elementType[$length]$bindingToString"
s"$elementType[$length]$bindingString"
}

private[chisel3] override def typeEquivalent(that: Data): Boolean = that match {
Expand Down Expand Up @@ -318,6 +339,190 @@ sealed class Vec[T <: Data] private[chisel3] (gen: => T, val length: Int)
}
curLayer(0)
}

/** Creates a Vec literal of this type with specified values. this must be a chisel type.
*
* @param elementInitializers literal values, specified as a pair of the Vec field to the literal value.
* The Vec field is specified as a function from an object of this type to the field.
* Fields that aren't initialized to DontCare, and assignment to a wire will overwrite any
* existing value with DontCare.
* @return a Vec literal of this type with subelement values specified
*
* @example {{{
* class MyVec extends Vec {
* val a = UInt(8.W)
* val b = Bool()
* }
*
* (mew MyVec).Lit(
* _.a -> 42.U,
* _.b -> true.B
* )
* }}}
*/
private[chisel3] def _makeLit(elementInitializers: (Int, Data)*): this.type = {

// Returns pairs of all fields, element-level and containers, in a Record and their path names
def getRecursiveFields(data: Data, path: String): Seq[(Data, String)] = data match {
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be merged with Record._makeLit? It seems like a lot of helper functions share the same structure, and updates to both would be needed to support like Vecs in Bundles?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is a good idea. But I would not be very comfortable trying this right now. There's pretty much as many differences as there are similarities. Going DRY might break bundle lits in some subtle way, where as keeping it separate should keep any logic flaws limited to Vecs

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 Vecs of the same type and size
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) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

In both the Record and Vec case we might want to check for typeEquivalent and element length equality, since zip will silently truncate

(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)) {
_ ++ _
}
}

def checkLiteralConstruction(): Unit = {
val dupKeys = elementInitializers.map { x => x._1 }.groupBy(x => x).flatMap { case (k, v) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

There are shorter ways to detect duplicates, comparing size against .distinct or .toSet
https://stackoverflow.com/questions/7691971/easiest-way-to-decide-if-list-contains-duplicates

More set operations can probably get the duplicated keys.

You don't get the count, but I'd think the index is the important aspect.

if (v.length > 1) {
Some(k, v.length)
} else {
None
}
}
if (dupKeys.nonEmpty) {
throw new VecLiteralException(
s"VecLiteral: has duplicated indices ${dupKeys.map { case (k, n) => s"$k($n times)" }.mkString(",")}"
)
}

val outOfRangeIndices = elementInitializers.map(_._1).flatMap {
Copy link
Contributor

Choose a reason for hiding this comment

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

consider .collect

case fieldIndex if fieldIndex < 0 || fieldIndex >= length => Some(fieldIndex)
case _ => None
}
if (outOfRangeIndices.nonEmpty) {
throw new VecLiteralException(
s"VecLiteral: The following indices (${outOfRangeIndices.mkString(",")}) " +
s"are less than zero or greater or equal to than Vec length"
)
}

val badLits = elementInitializers.flatMap {
Copy link
Contributor

Choose a reason for hiding this comment

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

.map to transform to widths, followed by .collect to match on widths?

Also, I don't think we do width checking elsewhere in chisel frontend? We don't need to special-case Vecs.

case (index, lit) if (!(gen.getClass == lit.getClass)) => Some(index -> lit)
(sample_element.width, lit.width) match {
case (KnownWidth(m), KnownWidth(n)) =>
if (m < n) Some(index -> lit) else None
case (KnownWidth(_), _) =>
None
case (UnknownWidth(), _) =>
None
case _ =>
Some(index -> lit)
}
case _ => None
}
if (badLits.nonEmpty) {
throw new VecLiteralException(
s"VecLiteral: Vec[${gen}] has the following incorrectly typed or sized initializers: " +
badLits.map { case (a, b) => s"$a -> $b" }.mkString(",")
)
}

}

requireIsChiselType(this, "vec literal constructor model")
checkLiteralConstruction()
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense to just inline the checks here, perhaps with comments for visual delimiting?


val clone = cloneType
val cloneFields = getRecursiveFields(clone, "(vec root)").toMap

// Create the Vec literal binding from litargs of arguments
val vecLitLinkedMap = new mutable.LinkedHashMap[Data, LitArg]()
elementInitializers.sortBy { case (a, _) => a }.foreach { case (fieldIndex, value) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

_._1 ?

val field = apply(fieldIndex)
val clonedField = clone(fieldIndex)

val valueBinding = value.topBindingOpt match {
case Some(litBinding: LitBinding) => litBinding
case _ => throw new VecLiteralException(s"field $fieldIndex specified with non-literal value $value")
}

value match { // Get the litArg(s) for this field
case vecField: Vec[_] =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense to keep the values consistent (value vs. vecField)? Otherwise it seems to be confusing to refer to either, when they're both the same thing?

if (!(vecField typeEquivalent value)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Will this ever happen? vecField is the same object as value, is it not?

throw new VecLiteralException(s"field $fieldIndex $vecField specified with non-type-equivalent value $value")
}
// Copy the source VecLitBinding with vecFields (keys) remapped to the clone
val remap = getMatchedFields(value, clonedField).toMap
value.topBinding.asInstanceOf[VecLitBinding].litMap.map { case (valueField, valueValue) =>
vecLitLinkedMap(remap(valueField)) = valueValue
}
case bitField: Bits =>
val adjustedBitField = if (bitField.isInstanceOf[Bool]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this special case needed?

bitField
} else {
bitField.cloneTypeWidth(field.getWidth.W)
}

val notSameClass = !field.typeEquivalent(adjustedBitField)
if (notSameClass) { // TODO typeEquivalent is too strict because it checks width
throw new VecLiteralException(
s"VecLit: Literal specified at index $fieldIndex ($value) does not match Vec type $sample_element"
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we do these checks in the frontend elsewhere? If so, the code should be unified with that, and if not, I don't think it's a good idea to have mixed semantics

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not completely sure. In general I think errors should be reported as early as possible and at compile time over run time (or firrtl time) also when possible. I think the error messages can be more helpful with information that is possessed at earlier stages

)
} else if (bitField.getWidth > field.getWidth) {
throw new VecLiteralException(
s"VecLit: Literal specified at index $fieldIndex ($value) is too wide for Vec type $sample_element"
)
}
val litArg = valueBinding match {
case ElementLitBinding(litArg) => litArg
case VecLitBinding(litMap) => litMap.getOrElse(value,
throw new VecLiteralException(s"Field $fieldIndex specified with unspecified value"))
}
val adjustedLitArg = litArg.cloneWithWidth(sample_element.width)
vecLitLinkedMap(clonedField) = adjustedLitArg
case recordField: Record =>
if (!(recordField typeEquivalent value)) {
throw new VecLiteralException(s"field $fieldIndex $recordField specified with non-type-equivalent value $value")
}
// Copy the source BundleLitBinding with fields (keys) remapped to the clone
val remap = getMatchedFields(value, recordField).toMap
value.topBinding.asInstanceOf[VecLitBinding].litMap.map { case (valueField, valueValue) =>
vecLitLinkedMap(remap(valueField)) = valueValue
}
case enumField: EnumType => {
if (!(enumField typeEquivalent value)) {
throw new VecLiteralException(s"field $fieldIndex $enumField specified with non-type-equivalent enum value $value")
}
val litArg = valueBinding match {
case ElementLitBinding(litArg) => litArg
}
vecLitLinkedMap(clonedField) = litArg
}
case _ => throw new VecLiteralException(s"unsupported field $fieldIndex of type $field")
}
} // don't convert to a Map yet to preserve duplicate keys

clone.bind(VecLitBinding(ListMap(vecLitLinkedMap.toSeq:_*)))
clone
}
}

object VecInit extends SourceInfoDoc {
Expand Down Expand Up @@ -530,9 +735,14 @@ abstract class Record(private[chisel3] implicit val compileOptions: CompileOptio
private[chisel3] def _makeLit(elems: (this.type => (Data, Data))*): this.type = {
// Returns pairs of all fields, element-level and containers, in a Record and their path names
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: 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
}

Expand All @@ -546,6 +756,10 @@ abstract class Record(private[chisel3] implicit val compileOptions: CompileOptio
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)) { _ ++ _ }
}

requireIsChiselType(this, "bundle literal constructor model")
Expand All @@ -572,7 +786,12 @@ abstract class Record(private[chisel3] implicit val compileOptions: CompileOptio
val litArg = valueBinding match {
case ElementLitBinding(litArg) => litArg
case BundleLitBinding(litMap) => litMap.getOrElse(value,
throw new BundleLiteralException(s"Field $fieldName specified with unspecified value"))
throw new BundleLiteralException(s"Field $fieldName specified with unspecified value")
)
case VecLitBinding(litMap) => litMap.getOrElse(value,
throw new VecLiteralException(s"Vec literal $fieldName specified with out literal values")
)

}
Seq(field -> litArg)
case field: Record =>
Expand All @@ -584,6 +803,15 @@ abstract class Record(private[chisel3] implicit val compileOptions: CompileOptio
value.topBinding.asInstanceOf[BundleLitBinding].litMap.map { case (valueField, valueValue) =>
remap(valueField) -> valueValue
}
case vecField: Vec[_] =>
if (!(vecField typeEquivalent value)) {
throw new BundleLiteralException(s"field $fieldName $vecField specified with non-type-equivalent value $value")
}
// Copy the source BundleLitBinding with fields (keys) remapped to the clone
val remap = getMatchedFields(value, vecField).toMap
value.topBinding.asInstanceOf[VecLitBinding].litMap.map { case (valueField, valueValue) =>
remap(valueField) -> valueValue
}
case field: EnumType => {
if (!(field typeEquivalent value)) {
throw new BundleLiteralException(s"field $fieldName $field specified with non-type-equivalent enum value $value")
Expand Down Expand Up @@ -694,6 +922,7 @@ class AutoClonetypeException(message: String) extends ChiselException(message)
package experimental {

class BundleLiteralException(message: String) extends ChiselException(message)
class VecLiteralException(message: String) extends ChiselException(message)

}

Expand Down
6 changes: 6 additions & 0 deletions core/src/main/scala/chisel3/Data.scala
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ abstract class Data extends HasId with NamedComponent with SourceInfoDoc {
case Some(DontCareBinding()) => s"(DontCare)"
case Some(ElementLitBinding(litArg)) => s"(unhandled literal)"
case Some(BundleLitBinding(litMap)) => s"(unhandled bundle literal)"
case Some(VecLitBinding(litMap)) => s"(unhandled vec literal)"
}).getOrElse("")

// Return ALL elements at root of this type.
Expand Down Expand Up @@ -491,6 +492,11 @@ abstract class Data extends HasId with NamedComponent with SourceInfoDoc {
case Some(litArg) => litArg
case _ => materializeWire() // FIXME FIRRTL doesn't have Bundle literal expressions
}
case Some(VecLitBinding(litMap)) =>
litMap.get(this) match {
case Some(litArg) => litArg
case _ => materializeWire() // FIXME FIRRTL doesn't have Vec literal expressions
}
case Some(DontCareBinding()) =>
materializeWire() // FIXME FIRRTL doesn't have a DontCare expression so materialize a Wire
// Non-literals
Expand Down
4 changes: 4 additions & 0 deletions core/src/main/scala/chisel3/Element.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ abstract class Element extends Data {
case Some(litArg) => Some(ElementLitBinding(litArg))
case _ => Some(DontCareBinding())
}
case Some(VecLitBinding(litMap)) => litMap.get(this) match {
case Some(litArg) => Some(ElementLitBinding(litArg))
case _ => Some(DontCareBinding())
}
case topBindingOpt => topBindingOpt
}

Expand Down
16 changes: 16 additions & 0 deletions core/src/main/scala/chisel3/experimental/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,22 @@ package object experimental {
}
}

/** This class provides the `Lit` method needed to define a `Vec` literal
*/
object VecLiterals {
implicit class AddVecLiteralConstructor[T <: Vec[_]](x: T) {
/** Given a generator of a list tuples of the form [Int, Data]
* constructs a Vec literal, parallel concept to `BundleLiteral`
*
* @param elems generates a lit of `(Int, Data)` where the I
* @return
*/
def Lit(elems: (Int, Data)*): T = {
x._makeLit(elems: _*)
}
}
}

// Use to add a prefix to any component generated in input scope
val prefix = chisel3.internal.prefix
// Use to remove prefixes not in provided scope
Expand Down
Loading