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

Add explanation for Class and Object, with examples. (backport #3880) #3887

Merged
merged 1 commit into from
Feb 29, 2024
Merged
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
251 changes: 251 additions & 0 deletions docs/src/explanations/properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,254 @@ on integral `Property` typed values.
| `+` | Perform addition as defined by FIRRTL spec section Integer Add Operation |
| `*` | Perform multiplication as defined by FIRRTL spec section Integer Multiply Operation |
| `>>` | Perform shift right as defined by FIRRTL spec section Integer Shift Right Operation |

### Classes and Objects

Classes and Objects are to `Property` types what modules and instances are to
hardware types. That is, they provide a means to declare hierarchies through
which `Property` typed values flow. `Class` declares a hierarchical container
with input and output `Property` ports, and a body that contains `Property`
connections and `Object`s. `Object`s represent the instantiation of a `Class`,
which requires any input `Property` ports to be assigned, and allows any output
`Property` ports to be read.

This allows domain-specific data models to be built using the basic primitives
of an object-oriented programming language, and embedded directly in the
instance graph Chisel is constructing. Intuitively, inputs to a `Class` are like
constructor arguments, which must be supplied to create an `Object`. Similarly,
outputs from a `Class` are like fields, which may be accessed from an `Object`.
This separation allows `Class` declarations to abstract over any `Object`s
created in their body--from the outside, the inputs must be supplied and only
the outputs may be accessed.

The graphs represented by `Class` declarations and `Object` instantiations
coexist within the hardware instance graph. `Object` instances can exist
within hardware modules, providing domain-specific information, but hardware
instances cannot exist within `Class` declarations.

`Object`s can be referenced, and references to `Object`s are a special kind of
`Property[ClassType]` type. This allows the data model captured by `Class`
declarations and `Object` instances to form arbitrary graphs.

To understand how `Object` graphs are represented, and can ultimately be
queried, consider how the hardware instance graph is elaborated. To build the
`Object` graph, we first pick an entrypoint module to start elaboration. The
elaboration process works according to the Verilog spec's definition of
elaboration--instances of modules and `Object`s are instantiated in-memory,
with connections to their inputs and outputs. Inputs are supplied, and outputs
may be read. After elaboration completes, the `Object` graph is exposed in terms
of the output ports, which may contain any `Property` types, including
references to `Object`s.

To illustrate how these pieces come together, consider the following examples:

```scala mdoc:silent
import chisel3.properties.Class
import chisel3.experimental.hierarchy.{instantiable, public, Definition, Instance}

// An abstract description of a CSR, represented as a Class.
@instantiable
class CSRDescription extends Class {
// An output Property indicating the CSR name.
val identifier = IO(Output(Property[String]()))
// An output Property describing the CSR.
val description = IO(Output(Property[String]()))
// An output Property indicating the CSR width.
val width = IO(Output(Property[Int]()))

// Input Properties to be passed to Objects representing instances of the Class.
@public val identifierIn = IO(Input(Property[String]()))
@public val descriptionIn = IO(Input(Property[String]()))
@public val widthIn = IO(Input(Property[Int]()))

// Simply connect the inputs to the outputs to expose the values.
identifier := identifierIn
description := descriptionIn
width := widthIn
}
```

The `CSRDescription` is a `Class` that represents domain-specific information
about a CSR. This uses `@instantiable` and `@public` so the `Class` can work
with the `Definition` and `Instance` APIs.

The readable fields we want to expose on `Object`s of the `CSRDescription` class
are a string identifier, a string description, and an integer bitwidth, so these
are output `Property` type ports on the `Class`.

To capture concrete values at each `Object` instantiation, we have corresponding
input `Property` type ports, which are connected directly to the outputs. This
is how we would represent something like a Scala `case class` using `Class`.

```scala mdoc:silent
// A hardware module representing a CSR and its description.
class CSRModule(
csrDescDef: Definition[CSRDescription],
width: Int,
identifierStr: String,
descriptionStr: String)
extends Module {
override def desiredName = identifierStr

// Create a hardware port for the CSR value.
val value = IO(Output(UInt(width.W)))

// Create a property port for a reference to the CSR description object.
val description = IO(Output(csrDescDef.getPropertyType))

// Instantiate a CSR description object, and connect its input properties.
val csrDescription = Instance(csrDescDef)
csrDescription.identifierIn := Property(identifierStr)
csrDescription.descriptionIn := Property(descriptionStr)
csrDescription.widthIn := Property(width)

// Create a register for the hardware CSR. A real implementation would be more involved.
val csr = RegInit(0.U(width.W))

// Assign the CSR value to the hardware port.
value := csr

// Assign a reference to the CSR description object to the property port.
description := csrDescription.getPropertyReference
}
```

The `CSRModule` is a `Module` that represents the (dummy) hardware for a CSR, as
well as a `CSRDescription`. Using a `Definition` of a `CSRDescription`, an
`Object` is created and its inputs supplied from the `CSRModule` constructor
arguments. Then, a reference to the `Object` is connected to the `CSRModule`
output, so the reference will be exposed to the outside.

```scala mdoc:silent
// The entrypoint module.
class Top extends Module {
// Create a Definition for the CSRDescription Class.
val csrDescDef = Definition(new CSRDescription)

// Get the CSRDescription ClassType.
val csrDescType = csrDescDef.getClassType

// Create a property port to collect all the CSRDescription object references.
val descriptions = IO(Output(Property[Seq[csrDescType.Type]]()))

// Instantiate a couple CSR modules.
val mcycle = Module(new CSRModule(csrDescDef, 64, "mcycle", "Machine cycle counter."))
val minstret = Module(new CSRModule(csrDescDef, 64, "minstret", "Machine instructions-retired counter."))

// Assign references to the CSR description objects to the property port.
descriptions := Property(Seq(mcycle.description.as(csrDescType), minstret.description.as(csrDescType)))
}
```

The `Top` module represents the entrypoint. It creates the `Definition` of the
`CSRDescription`, and creates some `CSRModule`s. It then takes the description
references, collects them into a list, and outputs the list so it will be
exposed to the outside.

While it is not required to use the `Definition` API to define a `Class`, this
is the "safe" API, with support in Chisel for working with `Definition`s and
`Instance`s of a `Class`. There is also an "unsafe" API. See `DynamicObject` for
more information.

To illustrate what this example generates, here is a listing of the FIRRTL:

```
FIRRTL version 4.0.0
circuit Top :
class CSRDescription :
output identifier : String
output description : String
output width : Integer
input identifierIn : String
input descriptionIn : String
input widthIn : Integer

propassign identifier, identifierIn
propassign description, descriptionIn
propassign width, widthIn

module mcycle :
input clock : Clock
input reset : Reset
output value : UInt<64>
output description : Inst<CSRDescription>

object csrDescription of CSRDescription
propassign csrDescription.identifierIn, String("mcycle")
propassign csrDescription.descriptionIn, String("Machine cycle counter.")
propassign csrDescription.widthIn, Integer(64)
regreset csr : UInt<64>, clock, reset, UInt<64>(0h0)
connect value, csr
propassign description, csrDescription

module minstret :
input clock : Clock
input reset : Reset
output value : UInt<64>
output description : Inst<CSRDescription>

object csrDescription of CSRDescription
propassign csrDescription.identifierIn, String("minstret")
propassign csrDescription.descriptionIn, String("Machine instructions-retired counter.")
propassign csrDescription.widthIn, Integer(64)
regreset csr : UInt<64>, clock, reset, UInt<64>(0h0)
connect value, csr
propassign description, csrDescription

public module Top :
input clock : Clock
input reset : UInt<1>
output descriptions : List<Inst<CSRDescription>>

inst mcycle of mcycle
connect mcycle.clock, clock
connect mcycle.reset, reset
inst minstret of minstret
connect minstret.clock, clock
connect minstret.reset, reset
propassign descriptions, List<Inst<CSRDescription>>(mcycle.description, minstret.description)
```

To understand the `Object` graph that is constructed, we will consider an
entrypoint to elaboration, and then show a hypothetical JSON representation of
the `Object` graph. The details of how we go from IR to an `Object` graph are
outside the scope of this document, and implemented by related tools.

If we elaborate `Top`, the `descriptions` output `Property` is our entrypoint to
the `Object` graph. Within it, there are two `Object`s, the `CSRDescription`s of
the `mcycle` and `minstret` modules:

```json mdoc:silent
{
"descriptions": [
{
"identifier": "mcycle",
"description": "Machine cycle counter.",
"width": 64
},
{
"identifier": "minstret",
"description": "Machine instructions-retired counter.",
"width": 64
}
]
}
```

If instead, we elaborate one of the `CSRModule`s, for example, `minstret`, the
`description` output `Property` is our entrypoint to the `Object` graph, which
contains the single `CSRDescription` object:

```json mdoc:silent
{
"description": {
"identifier": "minstret",
"description": "Machine instructions-retired counter.",
"width": 64
}
}
```

In this way, the output `Property` ports, `Object` references, and choice of
elaboration entrypoint allow us to view the `Object` graph representing the
domain-specific data model from different points in the hierarchy.
Loading