Writing Custom FIRRTL Passes

Hello all! I’m a hardware designer familiar with Chisel and the Scala FIRRTL Compiler (SFC). My company uses Chipyard for SoC development, and we’re excited to learn more about CIRCT. As an initial experiment, I’m interested in porting some of our custom FIRRTL passes from the SFC into the CIRCT framework, but I’m not sure where to get started.

  • Is there a process for adding custom passes/annotations to CIRCT/firtool? Should I just be forking the CIRCT repo and adding new files to the “lib/Dialect/FIRRTL/Transforms” directory, or is there a way I can use CIRCT as more of a library? I’m curious about how this compares to the Scala version of this, where I can pull FIRRTL in as a dependency to an sbt project, write my FIRRTL passes/annotations in that project, and then inject them into the compiler passes with the DependencyAPIMigration trait.
  • Is there any documentation (or example files to read through) for familiarizing myself with some of the data structures available while writing CIRCT passes? I’m thinking of analogs to the SFC DiGraph, InstanceKeyGraph, and ConnectionGraph classes, for example. Looking at lib/Dialect/FIRRTL/Transforms/CheckCombCycles.cpp, I can see that you pull in a number of both FIRRTL and LLVM data structures. I see the LLVM Programmer’s Manual, and that will probably help me familiarize myself with LLVM data structures, but should I just read through code and GitHub PRs to learn about the FIRRTL/CIRCT data structures?

Finally, thank you for all of the work on this! I’m hoping to be able to contribute soon.

-Tynan

Hi @tynan-nb! Thanks for checking out CIRCT.

We don’t have a good mechanism to do this, yet, but it’s not an intentional limitation. What we need is to add some hooks to firtool that enable users to add passes that get pulled in from shared libraries, likely at the following places: (1) immediately after parsing/at the start of the FIRRTL pipeline, (2) after expand whens (which is after lower types in firtool).

This would cover the main use case of a user with custom annotations / transforms. They would inject a pass after parsing that does any special handling of their custom annotations (“scatter” them into the circuit) and then have a pass that runs after expand whens to consume the annotations and make circuit modifications.

We just need to add some infrastructure to enable this use case.

If your pass is generally useful and you’re interested in open sourcing it, then a direct contribution to upstream CIRCT would be a good idea.

The LLVM programmers manual that you mention is a great place to start. The main thing is to grok that SFC works as a sequence of immutable updates to an AST representing FIRRTL IR while MLIR/LLVM is more standard compiler infrastructure where the IR is stored as a linked list of operations and operands are pointers (I think I have this right!).

This has implications for how you think about passes. E.g., in SFC if you wanted to change the name of a node you would:

  1. Replace the node with a copy of the node that had a different name.
  2. Scan the circuit to find all Ref/WRef that had the old name and change each to the new name.

In CIRCT, all you need to do is mutably update the name of the node because CIRCT doesn’t rely on string references to define usage. If you are creating a new operation in place of the original node, you would:

  1. Create a new node.
  2. Use replaceAllUsesWith (RAUW) to change all uses of the old node to the new node.
  3. Erase the old node.

DiGraph is mostly replicated with internal LLVM datastructures. You can take a look at hw::InstanceGraphBase to see how this works. InstanceKeyGraph is firrtl::InstanceGraph. ConnectionGraph is mostly unnecessary because LLVM/MLIR will let you walk operations. I.e., the underlying LLVM/MLIR datastructures already have ConnectionGraph properties. See: getDefiningOp or getUses. This is not always perfect for a query like “get driver”, but gives you better infrastructure to do this than SFC without ConnectionGraph.

Finally, the terminology is somewhat different. When SFC talks about “IR Nodes”, the LLVM/MLIR equivalent is “operation”. SFC “References” are LLVM/MLIR “operands”.

Fantastic, thanks for all the information @seldridge! This gives me plenty to start digging in, hopefully I’ll be able to help out with developing a custom annotation/transform system.