Skip to content

Commit

Permalink
Refactor Lookupable (#4519)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jackkoenig authored Nov 20, 2024
1 parent 058f162 commit ec4e2d5
Show file tree
Hide file tree
Showing 9 changed files with 504 additions and 105 deletions.
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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(",")
)

Expand Down
6 changes: 5 additions & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
331 changes: 243 additions & 88 deletions core/src/main/scala/chisel3/experimental/hierarchy/core/Lookupable.scala

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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
}
164 changes: 150 additions & 14 deletions docs/src/cookbooks/hierarchy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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")
Expand All @@ -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") {
Expand Down
34 changes: 34 additions & 0 deletions src/test/scala/chiselTests/experimental/hierarchy/Examples.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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") {
Expand Down

0 comments on commit ec4e2d5

Please sign in to comment.