From ec4e2d56abd346a2c4189fd458baba4d354e7ab9 Mon Sep 17 00:00:00 2001 From: Jack Koenig Date: Tue, 19 Nov 2024 21:25:10 -0800 Subject: [PATCH] Refactor Lookupable (#4519) Historically, the Lookupable API is able to change the type of fields looked up from Definitions or Instances. This enabled Module fields to be looked up as Instances, as well as user-defined types to opt-in to this same Instance-boxing behavior. This path-dependent type changing behavior is now deprecated. Looking up Modules is also deprecated, instead, the user should cast them Instances (via `.toInstance`). It is also deprecated to mark user-defined types as `@instantiable`. Instead, users should define Lookupable for their types using the new Lookupable.product[1-5] factory methods. See the Chisel website for more details. * Deprecate user-extension of trait Lookupable. * Deprecate Lookupable.lookupModule. Users should use Instances instead of Modules. * Deprecate Lookupable.isInstantiable. User should use new factories to implement Lookupable for their user-defined types instead. * Deprecate Lookupable.SimpleLookupable. * Add Lookupable.isLookupable factory for "simple" Lookupables. * Add Lookupable.product1-5 factories for Lookupables for user-defined types. * Add Lookupable for Tuple3-5 (already existed for Tuple2). * Add private LookupableImpl which is simpler to implement. --- build.sbt | 3 +- build.sc | 6 +- .../hierarchy/core/IsInstantiable.scala | 4 + .../hierarchy/core/Lookupable.scala | 331 +++++++++++++----- .../experimental/hierarchy/package.scala | 2 + docs/src/cookbooks/hierarchy.md | 164 ++++++++- .../hierarchy/DefinitionSpec.scala | 32 +- .../experimental/hierarchy/Examples.scala | 34 ++ .../experimental/hierarchy/InstanceSpec.scala | 33 ++ 9 files changed, 504 insertions(+), 105 deletions(-) diff --git a/build.sbt b/build.sbt index ec10fc45032..8d3f72e5ff7 100644 --- a/build.sbt +++ b/build.sbt @@ -68,7 +68,8 @@ lazy val warningSuppression = Seq( "cat=deprecation&origin=firrtl\\.options\\.internal\\.WriteableCircuitAnnotation:s", "cat=deprecation&origin=chisel3\\.util\\.experimental\\.BoringUtils.*:s", "cat=deprecation&origin=chisel3\\.experimental\\.IntrinsicModule:s", - "cat=deprecation&origin=chisel3\\.ltl.*:s" + "cat=deprecation&origin=chisel3\\.ltl.*:s", + "cat=deprecation&msg=Looking up Modules is deprecated:s", ).mkString(",") ) diff --git a/build.sc b/build.sc index e85cd623260..20480dd3830 100644 --- a/build.sc +++ b/build.sc @@ -73,7 +73,11 @@ object v extends Module { "cat=deprecation&origin=firrtl\\.options\\.internal\\.WriteableCircuitAnnotation:s", "cat=deprecation&origin=chisel3\\.util\\.experimental\\.BoringUtils.*:s", "cat=deprecation&origin=chisel3\\.experimental\\.IntrinsicModule:s", - "cat=deprecation&origin=chisel3\\.ltl.*:s" + "cat=deprecation&origin=chisel3\\.ltl.*:s", + // Deprecated for external users, will eventually be removed. + "cat=deprecation&msg=Looking up Modules is deprecated:s", + // Only for testing of deprecated APIs + "cat=deprecation&msg=Use of @instantiable on user-defined types is deprecated:s" ) // ScalacOptions diff --git a/core/src/main/scala/chisel3/experimental/hierarchy/core/IsInstantiable.scala b/core/src/main/scala/chisel3/experimental/hierarchy/core/IsInstantiable.scala index 298c3279338..7efc5a96d51 100644 --- a/core/src/main/scala/chisel3/experimental/hierarchy/core/IsInstantiable.scala +++ b/core/src/main/scala/chisel3/experimental/hierarchy/core/IsInstantiable.scala @@ -11,6 +11,10 @@ trait IsInstantiable object IsInstantiable { implicit class IsInstantiableExtensions[T <: IsInstantiable](i: T) { + @deprecated( + "Use of @instantiable on user-defined types is deprecated. Implement Lookupable for your type instead.", + "Chisel 7.0.0" + ) def toInstance: Instance[T] = new Instance(Proto(i)) } } diff --git a/core/src/main/scala/chisel3/experimental/hierarchy/core/Lookupable.scala b/core/src/main/scala/chisel3/experimental/hierarchy/core/Lookupable.scala index 29216c04137..d5a25367788 100644 --- a/core/src/main/scala/chisel3/experimental/hierarchy/core/Lookupable.scala +++ b/core/src/main/scala/chisel3/experimental/hierarchy/core/Lookupable.scala @@ -13,13 +13,31 @@ import chisel3.internal.firrtl.ir.{Arg, ILit, Index, LitIndex, ModuleIO, Slot, U import chisel3.internal.{throwException, Builder, ViewParent} import chisel3.internal.binding.{AggregateViewBinding, ChildBinding, CrossModuleBinding, ViewBinding, ViewWriteability} -/** Represents lookup typeclass to determine how a value accessed from an original IsInstantiable - * should be tweaked to return the Instance's version - * Sealed. +/** Typeclass used to recontextualize values from an original Definition to an Instance + * + * Implementations for Chisel types and other common Scala types are provided in the companion object. + * Users can define Lookupable for their own types. + * + * @example {{{ + * case class Foo[T <: Data](name: String, data: T) + * object Foo { + * implicit def lookupable[T <: Data]: Lookupable.Simple[Foo[T]] = + * Lookupable.product2[Foo[T], String, T]( + * x => (x.name, x.data), + * Foo.apply + * ) + * } + * }}} */ @implicitNotFound( - "@public is only legal within a class or trait marked @instantiable, and only on vals of type" + - " Data, BaseModule, MemBase, IsInstantiable, IsLookupable, or Instance[_], or in an Iterable, Option, Either, or Tuple2" + "@public is only legal within a class or trait marked @instantiable, and only on vals of types" + + " that have a Lookupable implementation. Chisel types like Data, BaseModule, and MemBase are supported," + + " as are common Scala types like String, Int, Boolean, Iterable, Option, Either, and Tuples." + + " Please implement Lookupable for ${B}." +) +@deprecatedInheritance( + "Users should use Lookupable factory methods instead (e.g. Lookupable.isLookupable or Lookupable.product)", + "Chisel 7.0.0" ) trait Lookupable[-B] { type C // Return type of the lookup @@ -40,6 +58,24 @@ trait Lookupable[-B] { def definitionLookup[A](that: A => B, definition: Definition[A]): C protected def getProto[A](h: Hierarchy[A]): A = h.proto protected def getUnderlying[A](h: Hierarchy[A]): Underlying[A] = h.underlying + + // Single method that may eventually replace instanceLookup and definitionLookup. + private[chisel3] def hierarchyLookup[A](that: A => B, hierarchy: Hierarchy[A]): C = { + hierarchy match { + case h: Instance[A] => instanceLookup(that, h) + case h: Definition[A] => definitionLookup(that, h) + case h => throw new InternalErrorException(s"Match error: hierarchy=$hierarchy") + } + } +} +// Simplify the implementation of Lookupable by only having 1 virtual method. +// Only the Chisel core types need separate implementations for Instance and Definition. +private trait LookupableImpl[-B] extends Lookupable[B] { + + protected def impl[A](that: A => B, hierarchy: Hierarchy[A]): C + + override def instanceLookup[A](that: A => B, instance: Instance[A]): C = impl(that, instance) + override def definitionLookup[A](that: A => B, definition: Definition[A]): C = impl(that, definition) } object Lookupable { @@ -50,6 +86,101 @@ object Lookupable { /** Type alias for simple Lookupable types */ type Simple[B] = Aux[B, B] + /** Provides a lookupable for a type X that does not need special handling + * + * This can only be used by types that do not contain a [[Data]], [[BaseModule]], or any [[IsInstantiable]]. + */ + def isLookupable[X]: Simple[X] = new LookupableImpl[X] { + type C = X + type B = X + override protected def impl[A](that: A => B, hierarchy: Hierarchy[A]): C = that(hierarchy.proto) + } + + /** Factory method for creating Lookupable for user-defined types + */ + def product1[X, T1: Lookupable]( + in: X => T1, + out: T1 => X + )( + implicit sourceInfo: SourceInfo + ): Simple[X] = new LookupableImpl[X] { + type C = X + override protected def impl[A](that: A => X, hierarchy: Hierarchy[A]): C = { + val t1res = implicitly[Lookupable[T1]].hierarchyLookup[A](a => in(that(a)), hierarchy).asInstanceOf[T1] + out(t1res) + } + } + + /** Factory method for creating Lookupable for user-defined types + */ + def product2[X, T1: Lookupable, T2: Lookupable]( + in: X => (T1, T2), + out: (T1, T2) => X + )( + implicit sourceInfo: SourceInfo + ): Simple[X] = new LookupableImpl[X] { + type C = X + override protected def impl[A](that: A => X, hierarchy: Hierarchy[A]): C = { + val t1res = implicitly[Lookupable[T1]].hierarchyLookup[A](a => in(that(a))._1, hierarchy).asInstanceOf[T1] + val t2res = implicitly[Lookupable[T2]].hierarchyLookup[A](a => in(that(a))._2, hierarchy).asInstanceOf[T2] + out(t1res, t2res) + } + } + + /** Factory method for creating Lookupable for user-defined types + */ + def product3[X, T1: Lookupable, T2: Lookupable, T3: Lookupable]( + in: X => (T1, T2, T3), + out: (T1, T2, T3) => X + )( + implicit sourceInfo: SourceInfo + ): Simple[X] = new LookupableImpl[X] { + type C = X + override protected def impl[A](that: A => X, hierarchy: Hierarchy[A]): C = { + val t1res = implicitly[Lookupable[T1]].hierarchyLookup[A](a => in(that(a))._1, hierarchy).asInstanceOf[T1] + val t2res = implicitly[Lookupable[T2]].hierarchyLookup[A](a => in(that(a))._2, hierarchy).asInstanceOf[T2] + val t3res = implicitly[Lookupable[T3]].hierarchyLookup[A](a => in(that(a))._3, hierarchy).asInstanceOf[T3] + out(t1res, t2res, t3res) + } + } + + /** Factory method for creating Lookupable for user-defined types + */ + def product4[X, T1: Lookupable, T2: Lookupable, T3: Lookupable, T4: Lookupable]( + in: X => (T1, T2, T3, T4), + out: (T1, T2, T3, T4) => X + )( + implicit sourceInfo: SourceInfo + ): Simple[X] = new LookupableImpl[X] { + type C = X + override protected def impl[A](that: A => X, hierarchy: Hierarchy[A]): C = { + val t1res = implicitly[Lookupable[T1]].hierarchyLookup[A](a => in(that(a))._1, hierarchy).asInstanceOf[T1] + val t2res = implicitly[Lookupable[T2]].hierarchyLookup[A](a => in(that(a))._2, hierarchy).asInstanceOf[T2] + val t3res = implicitly[Lookupable[T3]].hierarchyLookup[A](a => in(that(a))._3, hierarchy).asInstanceOf[T3] + val t4res = implicitly[Lookupable[T4]].hierarchyLookup[A](a => in(that(a))._4, hierarchy).asInstanceOf[T4] + out(t1res, t2res, t3res, t4res) + } + } + + /** Factory method for creating Lookupable for user-defined types + */ + def product5[X, T1: Lookupable, T2: Lookupable, T3: Lookupable, T4: Lookupable, T5: Lookupable]( + in: X => (T1, T2, T3, T4, T5), + out: (T1, T2, T3, T4, T5) => X + )( + implicit sourceInfo: SourceInfo + ): Simple[X] = new LookupableImpl[X] { + type C = X + override protected def impl[A](that: A => X, hierarchy: Hierarchy[A]): C = { + val t1res = implicitly[Lookupable[T1]].hierarchyLookup[A](a => in(that(a))._1, hierarchy).asInstanceOf[T1] + val t2res = implicitly[Lookupable[T2]].hierarchyLookup[A](a => in(that(a))._2, hierarchy).asInstanceOf[T2] + val t3res = implicitly[Lookupable[T3]].hierarchyLookup[A](a => in(that(a))._3, hierarchy).asInstanceOf[T3] + val t4res = implicitly[Lookupable[T4]].hierarchyLookup[A](a => in(that(a))._4, hierarchy).asInstanceOf[T4] + val t5res = implicitly[Lookupable[T5]].hierarchyLookup[A](a => in(that(a))._5, hierarchy).asInstanceOf[T5] + out(t1res, t2res, t3res, t4res, t5res) + } + } + /** Clones a data and sets its internal references to its parent module to be in a new context. * * @param data data to be cloned @@ -286,6 +417,7 @@ object Lookupable { } } + @deprecated("Use Lookupable.isLookupable instead of extending this.", "Chisel 7.0.0") class SimpleLookupable[X] extends Lookupable[X] { type B = X type C = X @@ -310,6 +442,7 @@ object Lookupable { } } + @deprecated("Looking up Modules is deprecated, please cast to Instance instead (.toInstance).", "Chisel 7.0.0") implicit def lookupModule[B <: BaseModule](implicit sourceInfo: SourceInfo): Aux[B, Instance[B]] = new Lookupable[B] { type C = Instance[B] @@ -395,13 +528,10 @@ object Lookupable { } implicit def lookupMem[B <: MemBase[_]](implicit sourceInfo: SourceInfo): Simple[B] = - new Lookupable[B] { + new LookupableImpl[B] { type C = B - def definitionLookup[A](that: A => B, definition: Definition[A]): C = { - cloneMemToContext(that(definition.proto), definition.getInnerDataContext.get) - } - def instanceLookup[A](that: A => B, instance: Instance[A]): C = { - cloneMemToContext(that(instance.proto), instance.getInnerDataContext.get) + override protected def impl[A](that: A => B, hierarchy: Hierarchy[A]): C = { + cloneMemToContext(that(hierarchy.proto), hierarchy.getInnerDataContext.get) } } @@ -435,13 +565,10 @@ object Lookupable { } implicit def lookupHasTarget(implicit sourceInfo: SourceInfo): Simple[HasTarget] = - new Lookupable[HasTarget] { + new LookupableImpl[HasTarget] { type C = HasTarget - def definitionLookup[A](that: A => HasTarget, definition: Definition[A]): C = { - cloneHasTargetToContext(that(definition.proto), definition.getInnerDataContext.get) - } - def instanceLookup[A](that: A => HasTarget, instance: Instance[A]): C = { - cloneHasTargetToContext(that(instance.proto), instance.getInnerDataContext.get) + override protected def impl[A](that: A => HasTarget, hierarchy: Hierarchy[A]): C = { + cloneHasTargetToContext(that(hierarchy.proto), hierarchy.getInnerDataContext.get) } } @@ -449,108 +576,136 @@ object Lookupable { implicit def lookupIterable[B, F[_] <: Iterable[_]]( implicit sourceInfo: SourceInfo, lookupable: Lookupable[B] - ): Aux[F[B], F[lookupable.C]] = new Lookupable[F[B]] { + ): Aux[F[B], F[lookupable.C]] = new LookupableImpl[F[B]] { type C = F[lookupable.C] - def definitionLookup[A](that: A => F[B], definition: Definition[A]): C = { - val ret = that(definition.proto).asInstanceOf[Iterable[B]] - ret.map { (x: B) => lookupable.definitionLookup[A](_ => x, definition) }.asInstanceOf[C] - } - def instanceLookup[A](that: A => F[B], instance: Instance[A]): C = { - import instance._ - val ret = that(proto).asInstanceOf[Iterable[B]] - ret.map { (x: B) => lookupable.instanceLookup[A](_ => x, instance) }.asInstanceOf[C] + override protected def impl[A](that: A => F[B], hierarchy: Hierarchy[A]): C = { + val ret = that(hierarchy.proto).asInstanceOf[Iterable[B]] + ret.map { (x: B) => lookupable.hierarchyLookup[A](_ => x, hierarchy) }.asInstanceOf[C] } } implicit def lookupOption[B]( implicit sourceInfo: SourceInfo, lookupable: Lookupable[B] - ): Aux[Option[B], Option[lookupable.C]] = new Lookupable[Option[B]] { + ): Aux[Option[B], Option[lookupable.C]] = new LookupableImpl[Option[B]] { type C = Option[lookupable.C] - def definitionLookup[A](that: A => Option[B], definition: Definition[A]): C = { - val ret = that(definition.proto) - ret.map { (x: B) => lookupable.definitionLookup[A](_ => x, definition) } - } - def instanceLookup[A](that: A => Option[B], instance: Instance[A]): C = { - import instance._ - val ret = that(proto) - ret.map { (x: B) => lookupable.instanceLookup[A](_ => x, instance) } + override protected def impl[A](that: A => Option[B], hierarchy: Hierarchy[A]): C = { + val ret = that(hierarchy.proto) + ret.map { (x: B) => lookupable.hierarchyLookup[A](_ => x, hierarchy) } } } implicit def lookupEither[L, R]( implicit sourceInfo: SourceInfo, lookupableL: Lookupable[L], lookupableR: Lookupable[R] - ): Aux[Either[L, R], Either[lookupableL.C, lookupableR.C]] = new Lookupable[Either[L, R]] { + ): Aux[Either[L, R], Either[lookupableL.C, lookupableR.C]] = new LookupableImpl[Either[L, R]] { type C = Either[lookupableL.C, lookupableR.C] - def definitionLookup[A](that: A => Either[L, R], definition: Definition[A]): C = { - val ret = that(definition.proto) - ret.map { (x: R) => lookupableR.definitionLookup[A](_ => x, definition) }.left.map { (x: L) => - lookupableL.definitionLookup[A](_ => x, definition) - } - } - def instanceLookup[A](that: A => Either[L, R], instance: Instance[A]): C = { - import instance._ - val ret = that(proto) - ret.map { (x: R) => lookupableR.instanceLookup[A](_ => x, instance) }.left.map { (x: L) => - lookupableL.instanceLookup[A](_ => x, instance) + override protected def impl[A](that: A => Either[L, R], hierarchy: Hierarchy[A]): C = { + val ret = that(hierarchy.proto) + ret.map { (x: R) => lookupableR.hierarchyLookup[A](_ => x, hierarchy) }.left.map { (x: L) => + lookupableL.hierarchyLookup[A](_ => x, hierarchy) } } } - implicit def lookupTuple2[X, Y]( + // TODO Once Lookupable return type change is removed, we can just call product factory above. + implicit def lookupTuple2[T1, T2]( implicit sourceInfo: SourceInfo, - lookupableX: Lookupable[X], - lookupableY: Lookupable[Y] - ): Aux[(X, Y), (lookupableX.C, lookupableY.C)] = new Lookupable[(X, Y)] { - type C = (lookupableX.C, lookupableY.C) - def definitionLookup[A](that: A => (X, Y), definition: Definition[A]): C = { - val ret = that(definition.proto) - ( - lookupableX.definitionLookup[A](_ => ret._1, definition), - lookupableY.definitionLookup[A](_ => ret._2, definition) - ) + lookupableT1: Lookupable[T1], + lookupableT2: Lookupable[T2] + ): Aux[(T1, T2), (lookupableT1.C, lookupableT2.C)] = new LookupableImpl[(T1, T2)] { + type C = (lookupableT1.C, lookupableT2.C) + override protected def impl[A](that: A => (T1, T2), hierarchy: Hierarchy[A]): C = { + val t1res = lookupableT1.hierarchyLookup[A](that(_)._1, hierarchy) + val t2res = lookupableT2.hierarchyLookup[A](that(_)._2, hierarchy) + (t1res, t2res) } - def instanceLookup[A](that: A => (X, Y), instance: Instance[A]): C = { - import instance._ - val ret = that(proto) - (lookupableX.instanceLookup[A](_ => ret._1, instance), lookupableY.instanceLookup[A](_ => ret._2, instance)) + } + + // TODO Once Lookupable return type change is removed, we can just call product factory above. + implicit def lookupTuple3[T1, T2, T3]( + implicit sourceInfo: SourceInfo, + lookupableT1: Lookupable[T1], + lookupableT2: Lookupable[T2], + lookupableT3: Lookupable[T3] + ): Aux[(T1, T2, T3), (lookupableT1.C, lookupableT2.C, lookupableT3.C)] = new LookupableImpl[(T1, T2, T3)] { + type C = (lookupableT1.C, lookupableT2.C, lookupableT3.C) + override protected def impl[A](that: A => (T1, T2, T3), hierarchy: Hierarchy[A]): C = { + val t1res = lookupableT1.hierarchyLookup[A](that(_)._1, hierarchy) + val t2res = lookupableT2.hierarchyLookup[A](that(_)._2, hierarchy) + val t3res = lookupableT3.hierarchyLookup[A](that(_)._3, hierarchy) + (t1res, t2res, t3res) } } + // TODO Once Lookupable return type change is removed, we can just call product factory above. + implicit def lookupTuple4[T1, T2, T3, T4]( + implicit sourceInfo: SourceInfo, + lookupableT1: Lookupable[T1], + lookupableT2: Lookupable[T2], + lookupableT3: Lookupable[T3], + lookupableT4: Lookupable[T4] + ): Aux[(T1, T2, T3, T4), (lookupableT1.C, lookupableT2.C, lookupableT3.C, lookupableT4.C)] = + new LookupableImpl[(T1, T2, T3, T4)] { + type C = (lookupableT1.C, lookupableT2.C, lookupableT3.C, lookupableT4.C) + override protected def impl[A](that: A => (T1, T2, T3, T4), hierarchy: Hierarchy[A]): C = { + val t1res = lookupableT1.hierarchyLookup[A](that(_)._1, hierarchy) + val t2res = lookupableT2.hierarchyLookup[A](that(_)._2, hierarchy) + val t3res = lookupableT3.hierarchyLookup[A](that(_)._3, hierarchy) + val t4res = lookupableT4.hierarchyLookup[A](that(_)._4, hierarchy) + (t1res, t2res, t3res, t4res) + } + } + + // TODO Once Lookupable return type change is removed, we can just call product factory above. + implicit def lookupTuple5[T1, T2, T3, T4, T5]( + implicit sourceInfo: SourceInfo, + lookupableT1: Lookupable[T1], + lookupableT2: Lookupable[T2], + lookupableT3: Lookupable[T3], + lookupableT4: Lookupable[T4], + lookupableT5: Lookupable[T5] + ): Aux[(T1, T2, T3, T4, T5), (lookupableT1.C, lookupableT2.C, lookupableT3.C, lookupableT4.C, lookupableT5.C)] = + new LookupableImpl[(T1, T2, T3, T4, T5)] { + type C = (lookupableT1.C, lookupableT2.C, lookupableT3.C, lookupableT4.C, lookupableT5.C) + override protected def impl[A](that: A => (T1, T2, T3, T4, T5), hierarchy: Hierarchy[A]): C = { + val t1res = lookupableT1.hierarchyLookup[A](that(_)._1, hierarchy) + val t2res = lookupableT2.hierarchyLookup[A](that(_)._2, hierarchy) + val t3res = lookupableT3.hierarchyLookup[A](that(_)._3, hierarchy) + val t4res = lookupableT4.hierarchyLookup[A](that(_)._4, hierarchy) + val t5res = lookupableT5.hierarchyLookup[A](that(_)._5, hierarchy) + (t1res, t2res, t3res, t4res, t5res) + } + } + + @deprecated( + "Use of @instantiable on user-defined types is deprecated. Implement Lookupable for your type instead.", + "Chisel 7.0.0" + ) implicit def lookupIsInstantiable[B <: IsInstantiable]( implicit sourceInfo: SourceInfo - ): Aux[B, Instance[B]] = new Lookupable[B] { + ): Aux[B, Instance[B]] = new LookupableImpl[B] { type C = Instance[B] - def definitionLookup[A](that: A => B, definition: Definition[A]): C = { - val ret = that(definition.proto) - val underlying = new InstantiableClone[B] { - val getProto = ret - lazy val _innerContext: Hierarchy[_] = definition - } - new Instance(Clone(underlying)) - } - def instanceLookup[A](that: A => B, instance: Instance[A]): C = { - val ret = that(instance.proto) + override protected def impl[A](that: A => B, hierarchy: Hierarchy[A]): C = { + val ret = that(hierarchy.proto) val underlying = new InstantiableClone[B] { val getProto = ret - lazy val _innerContext: Hierarchy[_] = instance + lazy val _innerContext: Hierarchy[_] = hierarchy } new Instance(Clone(underlying)) } } - implicit def lookupIsLookupable[B <: IsLookupable](implicit sourceInfo: SourceInfo): SimpleLookupable[B] = - new SimpleLookupable[B]() - - implicit val lookupInt: SimpleLookupable[Int] = new SimpleLookupable[Int]() - implicit val lookupByte: SimpleLookupable[Byte] = new SimpleLookupable[Byte]() - implicit val lookupShort: SimpleLookupable[Short] = new SimpleLookupable[Short]() - implicit val lookupLong: SimpleLookupable[Long] = new SimpleLookupable[Long]() - implicit val lookupFloat: SimpleLookupable[Float] = new SimpleLookupable[Float]() - implicit val lookupDouble: SimpleLookupable[Double] = new SimpleLookupable[Double]() - implicit val lookupChar: SimpleLookupable[Char] = new SimpleLookupable[Char]() - implicit val lookupString: SimpleLookupable[String] = new SimpleLookupable[String]() - implicit val lookupBoolean: SimpleLookupable[Boolean] = new SimpleLookupable[Boolean]() - implicit val lookupBigInt: SimpleLookupable[BigInt] = new SimpleLookupable[BigInt]() - implicit val lookupUnit: SimpleLookupable[Unit] = new SimpleLookupable[Unit]() + implicit def lookupIsLookupable[B <: IsLookupable](implicit sourceInfo: SourceInfo): Simple[B] = isLookupable[B] + + implicit val lookupInt: Simple[Int] = isLookupable[Int] + implicit val lookupByte: Simple[Byte] = isLookupable[Byte] + implicit val lookupShort: Simple[Short] = isLookupable[Short] + implicit val lookupLong: Simple[Long] = isLookupable[Long] + implicit val lookupFloat: Simple[Float] = isLookupable[Float] + implicit val lookupDouble: Simple[Double] = isLookupable[Double] + implicit val lookupChar: Simple[Char] = isLookupable[Char] + implicit val lookupString: Simple[String] = isLookupable[String] + implicit val lookupBoolean: Simple[Boolean] = isLookupable[Boolean] + implicit val lookupBigInt: Simple[BigInt] = isLookupable[BigInt] + implicit val lookupUnit: Simple[Unit] = isLookupable[Unit] } diff --git a/core/src/main/scala/chisel3/experimental/hierarchy/package.scala b/core/src/main/scala/chisel3/experimental/hierarchy/package.scala index c9f01706543..838c2328f7b 100644 --- a/core/src/main/scala/chisel3/experimental/hierarchy/package.scala +++ b/core/src/main/scala/chisel3/experimental/hierarchy/package.scala @@ -9,4 +9,6 @@ package object hierarchy { val Hierarchy = core.Hierarchy type IsInstantiable = core.IsInstantiable type IsLookupable = core.IsLookupable + type Lookupable[P] = core.Lookupable[P] + val Lookupable = core.Lookupable } diff --git a/docs/src/cookbooks/hierarchy.md b/docs/src/cookbooks/hierarchy.md index 1a3e264b7df..732916333b4 100644 --- a/docs/src/cookbooks/hierarchy.md +++ b/docs/src/cookbooks/hierarchy.md @@ -85,16 +85,17 @@ chisel3.docs.emitSystemVerilog(new AddTwoInstantiate(16)) ## How do I access internal fields of an instance? -You can mark internal members of a class or trait marked with `@instantiable` with the `@public` annotation. -The requirements are that the field is publicly accessible, is a `val` or `lazy val`, and is a valid type. -The list of valid types are: +You can mark internal members of a Module class or trait marked with `@instantiable` with the `@public` annotation. +The requirements are that the field is publicly accessible, is a `val` or `lazy val`, and must have an implementation of `Lookupable`. -1. `IsInstantiable` -2. `IsLookupable` -3. `Data` -4. `BaseModule` -5. `Iterable`/`Option `containing a type that meets these requirements -6. Basic type like `String`, `Int`, `BigInt` etc. +Types that are supported by default are: + +1. `Data` +2. `BaseModule` +3. `MemBase` +4. `IsLookupable` +5. `Iterable`/`Option`/`Either` containing a type that meets these requirements +6. Basic type like `String`, `Int`, `BigInt`, `Unit`, etc. To mark a superclass's member as `@public`, use the following pattern (shown with `val clock`). @@ -122,13 +123,15 @@ class MyModule extends Module { } ``` -## How do I make my parameters accessible from an instance? +## How do I make my fields accessible from an instance? -If an instance's parameters are simple (e.g. `Int`, `String` etc.) they can be marked directly with `@public`. +If an instance's fields are simple (e.g. `Int`, `String` etc.) they can be marked directly with `@public`. -Often, parameters are more complicated and are contained in case classes. -In such cases, mark the case class with the `IsLookupable` trait. +Often, fields are more complicated (e.g. a user-defined case class). +If a case class is only made up of simple types (i.e. it does *not* contain any `Data`, `BaseModules`, memories, or `Instances`), +it can extend the `IsLookupable` trait. This indicates to Chisel that instances of the `IsLookupable` class may be accessed from within instances. +(If the class *does* contain things like `Data` or modules, [the section below](#how-do-i-make-case-classes-containing-data-or-modules-accessible-from-an-instance).) However, ensure that these parameters are true for **all** instances of a definition. For example, if our parameters contained an id field which was instance-specific but defaulted to zero, @@ -167,7 +170,140 @@ val chiselCircuit = (new chisel3.stage.phases.Elaborate) println("```") ``` -## How do I look up parameters from a Definition, if I don't want to instantiate it? +## How do I make case classes containing Data or Modules accessible from an instance? + +For case classes containing `Data`, `BaseModule`, `MemBase` or `Instance` types, you can provide an implementation of the `Lookupable` typeclass. + +**Note that Lookupable for Modules is deprecated, please cast to Instance instead (with `.toInstance`).** + +Consider the following case class: + +```scala mdoc:reset +import chisel3._ +import chisel3.experimental.hierarchy.{Definition, Instance, instantiable, public} + +@instantiable +class MyModule extends Module { + @public val wire = Wire(UInt(8.W)) +} +case class UserDefinedType(name: String, data: UInt, inst: Instance[MyModule]) +``` + +By default, instances of `UserDefinedType` will not be accessible from instances: + +```scala mdoc:fail +@instantiable +class HasUserDefinedType extends Module { + val inst = Module(new MyModule) + val wire = Wire(UInt(8.W)) + @public val x = UserDefinedType("foo", wire, inst.toInstance) +} +``` + +We can implement the `Lookupable` type class for `UserDefinedType` in order to make it accessible. +This involves defining an implicit val in the companion object for `UserDefinedType`. +Because `UserDefinedType` has three fields, we use the `Lookupable.product3` factory. +It takes 4 type parameters: the type of the case class, and the types of each of its fields. + +**If any fields are `BaseModules`, you must change them to be `Instance[_]` in order to define the `Lookupable` typeclass.** + +For more information about typeclasses, see the [DataView section on Type Classes](https://www.chisel-lang.org/chisel3/docs/explanations/dataview#type-classes). + +```scala mdoc +import chisel3.experimental.hierarchy.Lookupable +object UserDefinedType { + // Use Lookupable.Simple type alias as return type. + implicit val lookupable: Lookupable.Simple[UserDefinedType] = + Lookupable.product3[UserDefinedType, String, UInt, Instance[MyModule]]( + // Provide the recipe for converting the UserDefinedType to a Tuple. + x => (x.name, x.data, x.inst), + // Provide the recipe for converting a Tuple to a user defined type. + // For case classes, you can use the built-in factory method. + UserDefinedType.apply + ) +} +``` + +Now, we can access instances of `UserDefinedType` from instances: + +```scala mdoc +@instantiable +class HasUserDefinedType extends Module { + val inst = Module(new MyModule) + val wire = Wire(UInt(8.W)) + @public val x = UserDefinedType("foo", wire, inst.toInstance) +} +class Top extends Module { + val inst = Instance(Definition(new HasUserDefinedType)) + println(s"Name is: ${inst.x.name}") +} +``` + +## How do I make type parameterized case classes accessible from an instance? + +Consider the following type-parameterized case class: + +```scala mdoc:reset +import chisel3._ +import chisel3.experimental.hierarchy.{Definition, Instance, instantiable, public} + +case class ParameterizedUserDefinedType[A, T <: Data](value: A, data: T) +``` + +Similarly to `HasUserDefinedType` we need to define an implicit to provide the `Lookupable` typeclass. +Unlike the simpler example above, however, we use an `implicit def` to handle the type parameters: + +```scala mdoc +import chisel3.experimental.hierarchy.Lookupable +object ParameterizedUserDefinedType { + // Type class materialization is recursive, so both A and T must have Lookupable instances. + // We required this for A via the context bound `: Lookupable`. + // Data is a Chisel built-in so is known to have a Lookupable instance. + implicit def lookupable[A : Lookupable, T <: Data]: Lookupable.Simple[ParameterizedUserDefinedType[A, T]] = + Lookupable.product2[ParameterizedUserDefinedType[A, T], A, T]( + x => (x.value, x.data), + ParameterizedUserDefinedType.apply + ) +} +``` + +Now, we can access instances of `ParameterizedUserDefinedType` from instances: + +```scala mdoc +class ChildModule extends Module { + @public val wire = Wire(UInt(8.W)) +} +@instantiable +class HasUserDefinedType extends Module { + val wire = Wire(UInt(8.W)) + @public val x = ParameterizedUserDefinedType("foo", wire) + @public val y = ParameterizedUserDefinedType(List(1, 2, 3), wire) +} +class Top extends Module { + val inst = Instance(Definition(new HasUserDefinedType)) + println(s"x.value is: ${inst.x.value}") + println(s"y.value.head is: ${inst.y.value.head}") +} +``` + +## How do I make case classes with lots of fields accessible from an instance? + +Lookupable provides factories for `product1` to `product5`. +If your class has more than 5 fields, you can use nested tuples as "pseduo-fields" in the mapping. + +```scala mdoc +case class LotsOfFields(a: Data, b: Data, c: Data, d: Data, e: Data, f: Data) +object LotsOfFields { + implicit val lookupable: Lookupable.Simple[LotsOfFields] = + Lookupable.product5[LotsOfFields, Data, Data, Data, Data, (Data, Data)]( + x => (x.a, x.b, x.c, x.d, (x.e, x.f)), + // Cannot use factory method directly this time since we have to unpack the tuple. + { case (a, b, c, d, (e, f)) => LotsOfFields(a, b, c, d, e, f) }, + ) +} +``` + +## How do I look up fields from a Definition, if I don't want to instantiate it? Just like `Instance`s, `Definition`'s also contain accessors for `@public` members. As such, you can directly access them: diff --git a/src/test/scala/chiselTests/experimental/hierarchy/DefinitionSpec.scala b/src/test/scala/chiselTests/experimental/hierarchy/DefinitionSpec.scala index 7e08174e1f2..9881edaa82e 100644 --- a/src/test/scala/chiselTests/experimental/hierarchy/DefinitionSpec.scala +++ b/src/test/scala/chiselTests/experimental/hierarchy/DefinitionSpec.scala @@ -226,6 +226,22 @@ class DefinitionSpec extends ChiselFunSpec with Utils { val (_, annos) = getFirrtlAndAnnos(new Top) annos should contain(MarkAnnotation("~Top|AddOneWithAnnotation>innerWire".rt, "innerWire")) } + it("(1.n): should work on user-defined types that provide Lookupable") { + class Top extends Module { + val defn = Definition(new HasUserDefinedType) + defn.simple.name should be("foo") + defn.parameterized.value should be(List(1, 2, 3)) + mark(defn.simple.data, "data") + mark(defn.simple.inst, "inst") + mark(defn.parameterized.inst, "inst2") + } + val (_, annos) = getFirrtlAndAnnos(new Top) + (annos.collect { case c: MarkAnnotation => c } should contain).allOf( + MarkAnnotation("~Top|HasUserDefinedType>wire".rt, "data"), + MarkAnnotation("~Top|HasUserDefinedType/inst0:AddOne".it, "inst"), + MarkAnnotation("~Top|HasUserDefinedType/inst1:AddOne".it, "inst2") + ) + } } describe("(2): Annotations on designs not in the same chisel compilation") { it("(2.a): should work on an innerWire, marked in a different compilation") { @@ -390,7 +406,7 @@ class DefinitionSpec extends ChiselFunSpec with Utils { "Cannot create a memory port in a different module (Top) than where the memory is (HasMems)." ) } - it("(3.o): should work on HasTarget") { + it("(3.p): should work on HasTarget") { class Top() extends Module { val i = Definition(new HasHasTarget) mark(i.x, "x") @@ -400,6 +416,20 @@ class DefinitionSpec extends ChiselFunSpec with Utils { MarkAnnotation("~Top|HasHasTarget>sram_sram".rt, "x") ) } + it("(3.q): should work on Tuple5 with a Module in it") { + class Top() extends Module { + val defn = Definition(new HasTuple5()) + val (3, w: UInt, "hi", inst: Instance[AddOne], l) = defn.tup + l should be(List(1, 2, 3)) + mark(w, "wire") + mark(inst, "inst") + } + val (_, annos) = getFirrtlAndAnnos(new Top) + annos.collect { case c: MarkAnnotation => c } should contain(MarkAnnotation("~Top|HasTuple5>wire".rt, "wire")) + annos.collect { case c: MarkAnnotation => c } should contain( + MarkAnnotation("~Top|HasTuple5/inst:AddOne".it, "inst") + ) + } } describe("(4): toDefinition") { it("(4.a): should work on modules") { diff --git a/src/test/scala/chiselTests/experimental/hierarchy/Examples.scala b/src/test/scala/chiselTests/experimental/hierarchy/Examples.scala index 18db2e9e834..de3d4243346 100644 --- a/src/test/scala/chiselTests/experimental/hierarchy/Examples.scala +++ b/src/test/scala/chiselTests/experimental/hierarchy/Examples.scala @@ -223,6 +223,12 @@ object Examples { @public val xy = (x, y) } @instantiable + class HasTuple5() extends Module { + val wire = Wire(UInt(3.W)) + val inst = Module(new AddOne) + @public val tup = (3, wire, "hi", inst, List(1, 2, 3)) + } + @instantiable class HasHasTarget() extends Module { val sram = SRAM(1024, UInt(8.W), 1, 1, 0) @public val x: HasTarget = sram.underlying.get @@ -376,4 +382,32 @@ object Examples { // Should also work in type-parameterized lookupable things @public val y: (Data, Unit) = (Wire(UInt(3.W)), ()) } + + case class UserDefinedType(name: String, data: UInt, inst: Instance[AddOne]) + object UserDefinedType { + implicit val lookupable: Lookupable.Simple[UserDefinedType] = + Lookupable.product3[UserDefinedType, String, UInt, Instance[AddOne]]( + x => (x.name, x.data, x.inst), + UserDefinedType.apply + ) + } + + case class ParameterizedUserDefinedType[A, M <: BaseModule](value: A, inst: Instance[M]) + object ParameterizedUserDefinedType { + implicit def lookupable[A: Lookupable, M <: BaseModule]: Lookupable.Simple[ParameterizedUserDefinedType[A, M]] = + Lookupable.product2[ParameterizedUserDefinedType[A, M], A, Instance[M]]( + x => (x.value, x.inst), + ParameterizedUserDefinedType.apply + ) + } + + @instantiable + class HasUserDefinedType extends Module { + val defn = Definition(new AddOne) + val inst0: Instance[AddOne] = Instance(defn) + val inst1: Instance[AddOne] = Instance(defn) + val wire = Wire(UInt(8.W)) + @public val simple = UserDefinedType("foo", wire, inst0) + @public val parameterized = ParameterizedUserDefinedType(List(1, 2, 3), inst1) + } } diff --git a/src/test/scala/chiselTests/experimental/hierarchy/InstanceSpec.scala b/src/test/scala/chiselTests/experimental/hierarchy/InstanceSpec.scala index 9bc6e0ef368..0f6ebf00b3b 100644 --- a/src/test/scala/chiselTests/experimental/hierarchy/InstanceSpec.scala +++ b/src/test/scala/chiselTests/experimental/hierarchy/InstanceSpec.scala @@ -248,6 +248,23 @@ class InstanceSpec extends ChiselFunSpec with Utils { ) chirrtl.serialize should include("attach (port, i0.port)") } + it("(1.n): should work on user-defined types that provide Lookupable") { + class Top extends Module { + val definition = Definition(new HasUserDefinedType) + val i0 = Instance(definition) + i0.simple.name should be("foo") + i0.parameterized.value should be(List(1, 2, 3)) + mark(i0.simple.data, "data") + mark(i0.simple.inst, "inst") + mark(i0.parameterized.inst, "inst2") + } + val (_, annos) = getFirrtlAndAnnos(new Top) + (annos.collect { case c: MarkAnnotation => c } should contain).allOf( + MarkAnnotation("~Top|Top/i0:HasUserDefinedType>wire".rt, "data"), + MarkAnnotation("~Top|Top/i0:HasUserDefinedType/inst0:AddOne".it, "inst"), + MarkAnnotation("~Top|Top/i0:HasUserDefinedType/inst1:AddOne".it, "inst2") + ) + } } describe("(2) Annotations on designs not in the same chisel compilation") { it("(2.a): should work on an innerWire, marked in a different compilation") { @@ -486,6 +503,22 @@ class InstanceSpec extends ChiselFunSpec with Utils { MarkAnnotation("~Top|Top/i:HasPublicUnit>y_1".rt, "y_1") ) } + it("(3.t): should work on Tuple5 with a Module in it") { + class Top() extends Module { + val i = Instance(Definition(new HasTuple5())) + val (3, w: UInt, "hi", inst: Instance[AddOne], l) = i.tup + l should be(List(1, 2, 3)) + mark(w, "wire") + mark(inst, "inst") + } + val (_, annos) = getFirrtlAndAnnos(new Top) + annos.collect { case c: MarkAnnotation => c } should contain( + MarkAnnotation("~Top|Top/i:HasTuple5>wire".rt, "wire") + ) + annos.collect { case c: MarkAnnotation => c } should contain( + MarkAnnotation("~Top|Top/i:HasTuple5/inst:AddOne".it, "inst") + ) + } } describe("(4) toInstance") { it("(4.a): should work on modules") {