Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 55 additions & 8 deletions core/src/main/scala/chisel3/Data.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1070,14 +1070,61 @@ object Data {
* It is illegal to connect to the return value of this method.
* This Data this method is called on must be a hardware type.
*/
def readOnly(implicit sourceInfo: SourceInfo): T = {
val alreadyReadOnly = self.isLit || self.topBindingOpt.exists(_.isInstanceOf[ReadOnlyBinding])
if (alreadyReadOnly) {
self
} else {
self.viewAsReadOnly(_ => "Cannot connect to read-only value")
}
}
def readOnly(implicit sourceInfo: SourceInfo): T =
viewOrSelf(self)(self.viewAsReadOnly(_ => "Cannot connect to read-only value"))
}

/** Returns `self` unchanged if it is already read-only (a literal or a [[ReadOnlyBinding]]),
* otherwise returns the result of `mk` (the read-only view to build).
*/
private def viewOrSelf[T <: Data](self: T)(mk: => T): T =
if (self.isLit || self.topBindingOpt.exists(_.isInstanceOf[ReadOnlyBinding])) self else mk

implicit class AsProducerConsumer[T <: Data](self: T) {

/** Returns a view of this Data marked as a producer.
*
* Aligned fields (outputs from the producer's perspective) become read-only,
* while flipped fields (inputs to the producer) remain writable.
*
* This should only be used on the producer (RHS) of Connectable operators
* (e.g., `consumer :<>= producer.asProducer`).
*/
def asProducer(implicit sourceInfo: SourceInfo): T =
viewOrSelf(self)(self.viewAsProducer(_ => "Cannot connect to producer's aligned field"))

/** Returns a view of this Data marked as a consumer.
*
* Flipped fields (inputs from the consumer's perspective) become read-only,
* while aligned fields (outputs from the consumer) remain writable.
*
* This should only be used on the consumer (LHS) of Connectable operators
* (e.g., `consumer.asConsumer :<>= producer`).
*/
def asConsumer(implicit sourceInfo: SourceInfo): T =
viewOrSelf(self)(self.viewAsConsumer(_ => "Cannot connect to consumer's flipped field"))

/** Like [[asProducer]] but issues deprecation warnings instead of errors.
*
* Use this to migrate existing code toward [[asProducer]] incrementally.
*/
def asProducerDeprecated(implicit sourceInfo: SourceInfo): T =
viewOrSelf(self)(
self.viewAsProducerDeprecated(info =>
Warning(WarningID.AsProducerDeprecated, "Cannot connect to producer's aligned field")(info)
)
)

/** Like [[asConsumer]] but issues deprecation warnings instead of errors.
*
* Use this to migrate existing code toward [[asConsumer]] incrementally.
*/
def asConsumerDeprecated(implicit sourceInfo: SourceInfo): T =
viewOrSelf(self)(
self.viewAsConsumerDeprecated(info =>
Warning(WarningID.AsConsumerDeprecated, "Cannot connect to consumer's flipped field")(info)
)
)
}
}

Expand Down
103 changes: 87 additions & 16 deletions core/src/main/scala/chisel3/connectable/Connection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
package chisel3.connectable

import chisel3.{Aggregate, BiConnectException, Data, DontCare, InternalErrorException, RawModule}
import chisel3.internal.{BiConnect, Builder}
import chisel3.internal.{BiConnect, Builder, Warning, WarningID}
import chisel3.internal.Builder.pushCommand
import chisel3.internal.binding._
import chisel3.internal.firrtl.ir.DefInvalid
import chisel3.experimental.{prefix, SourceInfo, UnlocatableSourceInfo}
import chisel3.experimental.{attach, Analog}
Expand Down Expand Up @@ -57,20 +58,27 @@ private[chisel3] case object ColonLessGreaterEq extends Connection {
val connectToProducer: Boolean = true
val alwaysConnectToConsumer: Boolean = false
def canFirrtlConnect(consumer: Connectable[Data], producer: Connectable[Data]) = {
val typeEquivalent =
try {
BiConnect.canFirrtlConnectData(
consumer.base,
producer.base,
UnlocatableSourceInfo,
Builder.referenceUserModule
) && consumer.base.typeEquivalent(producer.base)
} catch {
// For some reason, an error is thrown if its a View; since this is purely an optimization, any actual error would get thrown
// when calling DirectionConnection.connect. Hence, we can just default to false to take the non-optimized emission path
case e: Throwable => false
}
(typeEquivalent && consumer.notWaivedOrSqueezedOrExcluded && producer.notWaivedOrSqueezedOrExcluded)
// Role-tagged views must go through the slow path for role enforcement checks
val hasRole =
Connection.roleOf(consumer.base).isDefined ||
Connection.roleOf(producer.base).isDefined
if (hasRole) false
else {
val typeEquivalent =
try {
BiConnect.canFirrtlConnectData(
consumer.base,
producer.base,
UnlocatableSourceInfo,
Builder.referenceUserModule
) && consumer.base.typeEquivalent(producer.base)
} catch {
// For some reason, an error is thrown if its a View; since this is purely an optimization, any actual error would get thrown
// when calling DirectionConnection.connect. Hence, we can just default to false to take the non-optimized emission path
case e: Throwable => false
}
(typeEquivalent && consumer.notWaivedOrSqueezedOrExcluded && producer.notWaivedOrSqueezedOrExcluded)
}
}
}

Expand Down Expand Up @@ -108,7 +116,70 @@ private[chisel3] object Connection {
)(
implicit sourceInfo: SourceInfo
): Unit = {
doConnection(cRoot, pRoot, cOp)
if (!checkRoleEnforcement(cRoot, pRoot)) {
doConnection(cRoot, pRoot, cOp)
}
}

/** Check that role-tagged views (.asProducer/.asConsumer) are used on the correct side.
*
* @return true if a hard (non-deprecated) violation was reported, in which case the caller
* should skip the actual connection to avoid piling on per-field writability errors.
*/
private def checkRoleEnforcement[T <: Data](
consumer: Connectable[T],
producer: Connectable[T]
)(
implicit sourceInfo: SourceInfo
): Boolean = {
var hardViolation = false
// .asProducer must not be used on the consumer (LHS)
roleOf(consumer.base).foreach {
case (ConnectableRole.Producer, false) =>
Builder.error(".asProducer cannot be used on the consumer (LHS) of a connection operator")
hardViolation = true
case (ConnectableRole.Producer, true) =>
Builder.warning(
Warning(
WarningID.AsProducerDeprecated,
".asProducer cannot be used on the consumer (LHS) of a connection operator"
)
)
case _ => ()
}
// .asConsumer must not be used on the producer (RHS)
roleOf(producer.base).foreach {
case (ConnectableRole.Consumer, false) =>
Builder.error(".asConsumer cannot be used on the producer (RHS) of a connection operator")
hardViolation = true
case (ConnectableRole.Consumer, true) =>
Builder.warning(
Warning(
WarningID.AsConsumerDeprecated,
".asConsumer cannot be used on the producer (RHS) of a connection operator"
)
)
case _ => ()
}
hardViolation
}

/** Detect the connectable role of a Data from its binding, if it is a role-tagged view.
*
* @return Some((role, isDeprecated)) where isDeprecated is true for the warn-only variants.
*/
private[connectable] def roleOf(data: Data): Option[(ConnectableRole, Boolean)] = {
def extract(wr: ViewWriteability): Option[(ConnectableRole, Boolean)] = wr match {
case ViewWriteability.ReadOnly(_, Some(r)) => Some((r, false))
case ViewWriteability.ReadOnlyDeprecated(_, Some(r)) => Some((r, true))
case _ => None
}
data.topBindingOpt match {
case Some(ViewBinding(_, wr)) => extract(wr)
case Some(avb: AggregateViewBinding) =>
avb.writabilityMap.flatMap(_.values.flatMap(extract).headOption)
case _ => None
}
}

private def connect(
Expand Down
88 changes: 80 additions & 8 deletions core/src/main/scala/chisel3/experimental/dataview/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@ package object dataview {
val result: V = dataView.mkView(target)
requireIsChiselType(result, "viewAs")

doBind(target, result, dataView, writability)
// A role-bearing writability (from .asProducer/.asConsumer) applies read-only per-field based on
// alignment: a Producer view marks aligned fields read-only, a Consumer view marks flipped fields.
val role = writability match {
case ViewWriteability.ReadOnly(_, r) => r
case ViewWriteability.ReadOnlyDeprecated(_, r) => r
case _ => None
}
val wrMap = role.map { r =>
computeAlignmentWritabilityMap(result, writability, markAligned = r == ConnectableRole.Producer)
}
doBind(target, result, dataView, if (wrMap.isDefined) ViewWriteability.Default else writability, wrMap)

// Setting the parent marks these Data as Views
result.setAllParents(Some(ViewParent))
Expand Down Expand Up @@ -66,6 +76,38 @@ package object dataview {
dataView: DataView[T, V],
sourceInfo: SourceInfo
): V = _viewAsImpl(ViewWriteability.ReadOnly(getError))

private[chisel3] def viewAsProducer[V <: Data](
getError: SourceInfo => String
)(
implicit dataproduct: DataProduct[T],
dataView: DataView[T, V],
sourceInfo: SourceInfo
): V = _viewAsImpl(ViewWriteability.ReadOnly(getError, Some(ConnectableRole.Producer)))

private[chisel3] def viewAsConsumer[V <: Data](
getError: SourceInfo => String
)(
implicit dataproduct: DataProduct[T],
dataView: DataView[T, V],
sourceInfo: SourceInfo
): V = _viewAsImpl(ViewWriteability.ReadOnly(getError, Some(ConnectableRole.Consumer)))

private[chisel3] def viewAsProducerDeprecated[V <: Data](
getWarning: SourceInfo => Warning
)(
implicit dataproduct: DataProduct[T],
dataView: DataView[T, V],
sourceInfo: SourceInfo
): V = _viewAsImpl(ViewWriteability.ReadOnlyDeprecated(getWarning, Some(ConnectableRole.Producer)))

private[chisel3] def viewAsConsumerDeprecated[V <: Data](
getWarning: SourceInfo => Warning
)(
implicit dataproduct: DataProduct[T],
dataView: DataView[T, V],
sourceInfo: SourceInfo
): V = _viewAsImpl(ViewWriteability.ReadOnlyDeprecated(getWarning, Some(ConnectableRole.Consumer)))
}

/** Provides `viewAsSupertype` for subclasses of [[Record]] */
Expand Down Expand Up @@ -103,10 +145,11 @@ package object dataview {

// 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],
writability: ViewWriteability
target: T,
view: V,
dataView: DataView[T, V],
writability: ViewWriteability,
writabilityMapOverride: Option[Map[Data, ViewWriteability]] = None
)(
implicit sourceInfo: SourceInfo
): Unit = {
Expand Down Expand Up @@ -233,16 +276,45 @@ package object dataview {
}

view match {
case elt: Element => view.bind(ViewBinding(elementResult(elt), writability))
case elt: Element =>
val w = writabilityMapOverride.flatMap(_.get(elt)).getOrElse(writability)
view.bind(ViewBinding(elementResult(elt), w))
case agg: Aggregate =>
val fullResult = elementResult ++ aggregateMappings
val aggWritability = Option.when(writability.isReadOnly)(
Map((agg: Data) -> writability)
val aggWritability = writabilityMapOverride.orElse(
Option.when(writability.isReadOnly)(
Map((agg: Data) -> writability)
)
)
agg.bind(AggregateViewBinding(fullResult, aggWritability))
}
}

/** Compute a per-field writability map based on alignment.
*
* @param readOnly the ViewWriteability to apply to the marked fields
* @param markAligned if true, aligned fields get readOnly; if false, flipped fields get readOnly
*/
private def computeAlignmentWritabilityMap(
view: Data,
readOnly: ViewWriteability,
markAligned: Boolean
): Map[Data, ViewWriteability] = {
val default = ViewWriteability.Default
// Collect all aligned members (including root, aggregates, and leaves)
val alignedSet: Set[Data] =
DataMirror.collectAlignedDeep(view) { case d => d }.toSet
// Collect all members
val allMembers: Iterable[Data] =
DataMirror.collectMembers(view) { case d => d }
// Build per-field writability: markAligned=true marks aligned fields, false marks flipped
// Note: we emit an entry for EVERY member (including Default ones) because
// AggregateViewBinding.lookupWritability relies on these entries for its parent fallback.
allMembers.map { m =>
m -> (if (alignedSet.contains(m) == markAligned) readOnly else default)
}.toMap
}

// When annotating views that are not identity mappings, we need to record them for renaming
// Technically, this adds any Aggregate that is not an identity mapping,
// but we don't have a cheap way to check for single-target.
Expand Down
16 changes: 12 additions & 4 deletions core/src/main/scala/chisel3/internal/Binding.scala
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ private[chisel3] object binding {
// It is a source (RHS). It may only be connected/applied to sinks.
case class DontCareBinding() extends UnconstrainedBinding

/** The connectable "role" of a view created by `.asProducer` / `.asConsumer`. */
sealed trait ConnectableRole
object ConnectableRole {
case object Producer extends ConnectableRole
case object Consumer extends ConnectableRole
}

/** Views are able to restrict writability of the target */
sealed trait ViewWriteability {

Expand All @@ -142,10 +149,10 @@ private[chisel3] object binding {
*/
final def reportIfReadOnly[A](onPass: => A)(onFail: => A)(implicit info: SourceInfo): A = this match {
case ViewWriteability.Default => onPass
case ViewWriteability.ReadOnlyDeprecated(getWarning) =>
case ViewWriteability.ReadOnlyDeprecated(getWarning, _) =>
Builder.warning(getWarning(info))
onPass // This is just a warning so we propagate the pass value.
case ViewWriteability.ReadOnly(getError) =>
case ViewWriteability.ReadOnly(getError, _) =>
Builder.error(getError(info))
onFail
}
Expand All @@ -164,15 +171,16 @@ private[chisel3] object binding {
}

/** Signals that will eventually become read only */
case class ReadOnlyDeprecated(getWarning: SourceInfo => Warning) extends ViewWriteability {
case class ReadOnlyDeprecated(getWarning: SourceInfo => Warning, role: Option[ConnectableRole] = None)
extends ViewWriteability {
override def combine(that: ViewWriteability): ViewWriteability = that match {
case ro: ReadOnly => ro
case _ => this
}
}

/** Signals that are read only */
case class ReadOnly(getError: SourceInfo => String) extends ViewWriteability {
case class ReadOnly(getError: SourceInfo => String, role: Option[ConnectableRole] = None) extends ViewWriteability {
override def combine(that: ViewWriteability): ViewWriteability = this
}
}
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/scala/chisel3/internal/Warning.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ private[chisel3] object WarningID extends Enumeration {
val ExtractFromVecSizeZero = Value(6)
val BundleLiteralValueTooWide = Value(7)
val AsTypeOfReadOnly = Value(8)
val AsProducerDeprecated = Value(9)
val AsConsumerDeprecated = Value(10)
}
import WarningID.WarningID

Expand Down
Loading
Loading