Annotations

Annotations are metadata containers associated with zero or more “things” in a FIRRTL circuit. Commonly, Annotations are used to communicate information from Chisel to a specific, custom FIRRTL Transform. In this way Annotations can be viewed as the “arguments” that a specific Transform consumes.

This article focuses on the approach to building a basic library that contains Annotations and Transforms.

Imports

We need a few basic imports to reference the components we need.

import chisel3._
import chisel3.experimental.{annotate, ChiselAnnotation, RunFirrtlTransform}
import chisel3.internal.InstanceId

import firrtl._
import firrtl.annotations.{Annotation, SingleTargetAnnotation}
import firrtl.annotations.{CircuitTarget, ModuleTarget, InstanceTarget, ReferenceTarget, Target}
import scala.annotation.nowarn

Define an Annotation and a Transform

First, define an Annotation that contains a string associated with a Target thing in the Chisel circuit. This InfoAnnotation extends SingleTargetAnnotation, an Annotation associated with one thing in a FIRRTL circuit:

/** An annotation that contains some string information */
case class InfoAnnotation(target: Target, info: String) extends SingleTargetAnnotation[Target] {
  def duplicate(newTarget: Target) = this.copy(target = newTarget)
}

Second, define a Transform that consumes this InfoAnnotation. This InfoTransform simply reads all annotations, prints any InfoAnnotations it finds, and removes them.

/** A transform that reads InfoAnnotations and prints information about them */
class InfoTransform() extends Transform with DependencyAPIMigration {

  override def prerequisites = firrtl.stage.Forms.HighForm

  override def execute(state: CircuitState): CircuitState = {
    println("Starting transform 'IdentityTransform'")

    val annotationsx = state.annotations.flatMap{
      case InfoAnnotation(a: CircuitTarget, info) =>
        println(s"  - Circuit '${a.serialize}' annotated with '$info'")
        None
      case InfoAnnotation(a: ModuleTarget, info) =>
        println(s"  - Module '${a.serialize}' annotated with '$info'")
        None
      case InfoAnnotation(a: InstanceTarget, info) =>
        println(s"  - Instance '${a.serialize}' annotated with '$info'")
        None
      case InfoAnnotation(a: ReferenceTarget, info) =>
        println(s"  - Component '${a.serialize} annotated with '$info''")
        None
      case a =>
        Some(a)
    }

    state.copy(annotations = annotationsx)
  }
}

Note: inputForm and outputForm will be deprecated in favor of a new dependency API that allows transforms to specify their dependencies more specifically than with circuit forms. Full backwards compatibility for inputForm and outputForm will be maintained, however.

Create a Chisel API/Annotator

Now, define a Chisel API to annotate Chisel things with this InfoAnnotation. This is commonly referred to as an “annotator”.

Here, define an object, InfoAnnotator with a method info that generates InfoAnnotations. This uses the chisel3.experimental.annotate passed an anonymous ChiselAnnotation object. The need for this ChiselAnnotation (which is different from an actual FIRRTL Annotation) is that no FIRRTL circuit exists at the time the info method is called. This is delaying the generation of the InfoAnnotation until the full circuit is available.

This annotator also mixes in the RunFirrtlTransform trait (abstract in the transformClass method) because this annotator, whenever used, should result in the FIRRTL compiler running the custom InfoTransform.

@nowarn("msg=InstanceId")
object InfoAnnotator {
  def info(component: InstanceId, info: String): Unit = {
    annotate(new ChiselAnnotation with RunFirrtlTransform {
      def toFirrtl: Annotation = InfoAnnotation(component.toTarget, info)
      def transformClass = classOf[InfoTransform]
    })
  }
}

Note: there are a number of different approaches to writing an annotator. You could use a trait that you mix into a Module, an object (like is done above), or any other software approach. The specific choice of how you implement this is up to you!

Using the Chisel API

Now, we can use the method InfoAnnotation.info to create annotations that associate strings with specific things in a FIRRTL circuit. Below is a Chisel Module, ModC, where both the actual module is annotated as well as an output.

class ModC(widthC: Int) extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(widthC.W))
    val out = Output(UInt(widthC.W))
  })
  io.out := io.in

  InfoAnnotator.info(this, s"ModC($widthC)")

  InfoAnnotator.info(io.out, s"ModC(ignore param)")
}

Running the Compilation

Compiling this circuit to Verilog will then result in the InfoTransform running and the added printlns showing information about the components annotated.

import chisel3.stage.ChiselGeneratorAnnotation
import circt.stage.ChiselStage

(new ChiselStage).execute(Array.empty, Seq(ChiselGeneratorAnnotation(() => new ModC(4))))