Longer .sv files from LLVM CIRCT than .v files from Chisel

I took the latest firtool from master for a quick spin.

I see that the .sv files from CIRCT can be quite a bit longer than the .v files from Chisel.

I was wondering if this difference has to do with Chisel optimizing things in during Verilog generation or whether it is LLVM CIRCT that makes this choice.

Since GEN_2719 below doesn’t exist in the .fir file, I’m guessing these are choices done by LLVM CIRCT. The .fir file does the port assignment without temporary values and at a much higher level, with a concept of arrays and bundles.

An example I narrowed down.

Here we can see that in CIRCT .sv file the variable “foo” is never used:

  wire        _GEN_2719;
  wire        foo;
  assign foo = _GEN_2719;
  assign _GEN_2719 = bar[294];
 // module port assignment. 
    .foo                 (_GEN_2719),

Whereas Chisel generates the following code:

  wire  thingy;
  assign thingy = bar[294];
 // module port assignment

My preferred code in this case would be no temporary variable at all:

   // module port assignment. 
    .foo                 (bar[294]),

Thanks for pointing this out, @oharboe.

This is intentional behavior, but you can turn it off with --disable-name-preservation.

There was a hard ask from users to “preserve all val names from Chisel”. This is temporarily implemented as a hack where each “named” thing (a FIRRTL operation that begins with a non-underscore character) will result in a dead wire tap with the same name. The original wire is renamed _<name> and will likely be optimized away. The intent here is to provide a seamless Chisel debug experience in a waveform viewer where users can see signals that have the exact names that they wrote in Chisel. The Scala FIRRTL Compiler does not have this property, but it has far fewer optimizations/canonicalizations so users trained themselves to rely on val preservation.

This is likely way less of a requirement if a user is 100% onboard with ChiselTest, but can still be beneficial for development. There’s discussion around this, and a motivating example using Rocket Chip’s ALU here: [FIRRTL] Preserve All Wires by seldridge · Pull Request #2676 · llvm/circt · GitHub

In the future we will change this behavior to not emit the dead wire and instead make this an option to preserve any named signal. The signal will always exist in the output Verilog, but it may be dead if optimizations make it so. However, it will always exist and users can rely on that behavior (unless they turn it off).

How about that!

The line count of LLVM CIRCT is now 40% less than that of Chisel for our code now. That sounds too good to be true, so I guess I need to actually try it out…

Vector lookup is completely different with LLVM CIRCT, no if statements:

wire [5:0][7:0] _GEN = {{vector_5}, {vector_4}, {vector_3}, {vector_2}, {vector_1}, {vecto
assign out = element > 3’h5 ? vector_0 : _GEN[element];

Hmmm… It appears that unused inputs to modules are not pruned, they are hooked up to wires that are not assigned. That’s a bit unsettling.


The .fir file does the port assignment without temporary values and at a much higher level, with a concept of arrays and bundles.

FYI, I guess creating unnecessary temporary for port assignments is another different problem which should be fixed (tracked by [ExportVerilog] Unnecessary temporary for bind · Issue #2471 · llvm/circt · GitHub).

It appears that unused inputs to modules are not pruned,

Unused inputs should be pruned by default now (implemented in RemoveUnusedPass circt/RemoveUnusedPorts.cpp at main · llvm/circt · GitHub). Could you show us chisel or firrtl program?

I did try with firtool --remove-unused-ports, but unused ports are there still.

I need to see if I can develop a standalone example.

Thank you very much! Chances are that if an unused port has a symbol or annotation, the port will not be eliminated

firtool does inter-module constant propagation, but (afaik) there is no “dead port elimination” to clean up after propagating the constants.

We did eventually add a FIRRTL Dialect RemoveUnusedPorts pass (#2522) which runs after inter-module constant prop (IMCP). However, the “preserve all val names” may work against this in some situations.

A simple example of this all working is:

circuit Foo :
  module Bar :
    input a: UInt<1>
    output b: UInt<1>

    wire w: UInt<1>
    w <= a
    b <= w

  module Foo :
    input a: UInt<1>
    output b: UInt<1>

    inst bar of Bar
    bar.a <= xor(a, a)
    b <= bar.b

firtool Foo.fir produces:

module Bar();	// Foo.fir:2:10
  wire w;	// Foo.fir:6:5

  assign w = 1'h0;	// Foo.fir:2:10, :6:5

module Foo(	// Foo.fir:10:10
  input  a,
  output b);

  Bar bar ();	// Foo.fir:15:5
  assign b = 1'h0;	// Foo.fir:10:10, :16:14

If the ports are made dead after the circuit is out of FIRRTL Dialect, then the port may hang around. (Meaning, we probably need module port elimination inside later dialects.)