FIRRTL CatPrimOp semantics

Hello! I have a question about the semantics of concatenation operation. From the FIRRTL spec: cat (e1,e2). The result of the concatenate operation is the bits of e1 concatenated to the most significant end of the bits of e2. How should one interpret it? Should it be like that: e1[we1 - 1]e1[…]e1[0]e2[we2 - 1]e2[…]e2[0] or like that: e2[we2 - 1]e2[…]e2[0]e1[we1 - 1]e1[…]e1[0]? As far as I understand it should be the first option.

I’m asking this question because if the first option is true then it is likely that FIRRTL lowering logic contains a bug.

Yes, it should be the first option. Do you have an example that is showing a miscompilation?

This looks right to me:

FIRRTL version 4.0.0
circuit Foo:
  public module Foo:
    input e1: UInt<1>
    input e2: UInt<1>
    output out: UInt<2>

    connect out, cat(e1, e2)

This produces:

// Generated by CIRCT firtool-1.70.0-24-g2b482b219
module Foo(
  input        e1,
               e2,
  output [1:0] out
);

  assign out = {e1, e2};
endmodule

In the Verilog, this means that out[1] is e1 and out[0] is e2.

Thank you for your reply.
The whole example is very big. I can send it to you, but maybe I can describe you the part, where the strange behavior occurs (in my opinion). It is possible that I’m just misunderstanding something.
FIRRTL lowering produces the following structure:

...
%_GEN__317 = firrtl.wire : !firrtl.uint<32>
...
%_GEN__3609 = firrtl.wire : !firrtl.uint<1>
...
%_GEN__4214 = firrtl.wire : !firrtl.uint<32>
...
%1 = firrtl.cat %0, %_GEN__3609 : (!firrtl.uint<31>, !firrtl.uint<1>) -> !firrtl.uint<32>
firrtl.strictconnect %_GEN__4214, %1 : !firrtl.uint<32>
...
%23 = firrtl.constCast %c0_ui1_0 : (!firrtl.const.uint<1>) -> !firrtl.uint<1>
...
firrtl.strictconnect %_GEN__3609, %23 : !firrtl.uint<1>
...
firrtl.strictconnect %_GEN__317, %_GEN__4214 : !firrtl.uint<32>
...
%69 = firrtl.bits %_GEN__317 31 to 1 : (!firrtl.uint<32>) -> !firrtl.uint<31>
...

As far as I understand %1 concatenates the result of the %23 with %0:
%0[31…1]%23[0]. And after that %69 removes the result of the above concatenation which does not make any sense. Judging by firrtl.shr operation the enumeration of the bits is right. Maybe I am wrong somewhere?

Your interpretation seems correct to me.

Here is your example again with some of the holes filled in so the MLIR is legal:

firrtl.circuit "Test" {
  firrtl.module @Test(in %0: !firrtl.uint<31>, out %out: !firrtl.uint<31>) {
    %c0_ui1_0 = firrtl.constant 0 : !firrtl.const.uint<1>
    %_GEN__317 = firrtl.wire : !firrtl.uint<32>
    %_GEN__3609 = firrtl.wire : !firrtl.uint<1>
    %_GEN__4214 = firrtl.wire : !firrtl.uint<32>
    %1 = firrtl.cat %0, %_GEN__3609 : (!firrtl.uint<31>, !firrtl.uint<1>) -> !firrtl.uint<32>
    firrtl.strictconnect %_GEN__4214, %1 : !firrtl.uint<32>
    %23 = firrtl.constCast %c0_ui1_0 : (!firrtl.const.uint<1>) -> !firrtl.uint<1>
    firrtl.strictconnect %_GEN__3609, %23 : !firrtl.uint<1>
    firrtl.strictconnect %_GEN__317, %_GEN__4214 : !firrtl.uint<32>
    %69 = firrtl.bits %_GEN__317 31 to 1 : (!firrtl.uint<32>) -> !firrtl.uint<31>
    firrtl.strictconnect %out, %69 : !firrtl.uint<31>
  }
}

And then later in lowering, the HW level canonicalizers see the pointless ops and remove them. I just fed the example in to firtool.

// -----// IR Dump Before Canonicalizer (canonicalize) //----- //
hw.module @Test(in %0 "" : i31, out out : i31) {
  %false = hw.constant false
  %_GEN__317 = hw.wire %_GEN__4214  : i32
  %_GEN__4214 = hw.wire %1  : i32
  %1 = comb.concat %0, %false : i31, i1
  %2 = comb.extract %_GEN__317 from 1 : (i32) -> i31
  hw.output %2 : i31
}

// -----// IR Dump After Canonicalizer (canonicalize) //----- //
hw.module @Test(in %0 "" : i31, out out : i31) {
  hw.output %0 : i31
}

Producing this verilog:

// Generated by CIRCT unknown git version
module Test(    // ./test.mlir:2:3
  input  [30:0] _GEN,   // ./test.mlir:2:26
  output [30:0] out     // ./test.mlir:2:52
);

  assign out = _GEN;    // ./test.mlir:2:3
endmodule

I’m not entirely following the example, but it sounds like you have some code which looks like the following:

FIRRTL version 4.0.0
circuit Foo:
  public module Foo:
    input a: UInt<31>
    input b: UInt<1>
    output c: UInt<31>

    connect c, shr(cat(a, b), 1)

This is initially parsed into:

module {
  firrtl.circuit "Foo" {
    firrtl.module @Foo(in %a: !firrtl.uint<31>, in %b: !firrtl.uint<1>, out %c: !firrtl.uint<31>) attributes {convention = #firrtl<convention scalarized>} {
      %0 = firrtl.cat %a, %b : (!firrtl.uint<31>, !firrtl.uint<1>) -> !firrtl.uint<32>
      %1 = firrtl.shr %0, 1 : (!firrtl.uint<32>) -> !firrtl.uint<31>
      firrtl.strictconnect %c, %1 : !firrtl.uint<31>
    }
  }
}

After FIRRTL canonicalization, this converts the shr to bits:

module {
  firrtl.circuit "Foo" {
    firrtl.module @Foo(in %a: !firrtl.uint<31>, in %b: !firrtl.uint<1>, out %c: !firrtl.uint<31>) attributes {convention = #firrtl<convention scalarized>} {
      %0 = firrtl.cat %a, %b : (!firrtl.uint<31>, !firrtl.uint<1>) -> !firrtl.uint<32>
      %1 = firrtl.bits %0 31 to 1 : (!firrtl.uint<32>) -> !firrtl.uint<31>
      firrtl.strictconnect %c, %1 : !firrtl.uint<31>
    }
  }
}

FIRRTL Dialect doesn’t seem to have any further canonicalization of cat(bits(...))). However, HW Dialect does. When this is converted to HW dialect this becomes:

module {
  hw.module @Foo(in %a : i31, in %b : i1, out c : i31) {
    %0 = comb.concat %a, %b : i31, i1
    %1 = comb.extract %0 from 1 : (i32) -> i31
    hw.output %1 : i31
  }
}

When canonicalized in HW dialect, this becomes:

module {
  hw.module @Foo(in %a : i31, in %b : i1, out c : i31) {
    hw.output %a : i31
  }
}

This is reading between the lines and trying to interpret a lot from your mention of shr. Is this where the confusion is coming from?

Thank you for your reply. I know that later in lowering to hw dialect it removes pointless ops. That’s not the problem. The problem that it produces this pointless chain of ops. What’s more, in the whole example this particular bit remains unconnected. If it was like this %69 = firrtl.bits %_GEN__317 30 to 0 : (!firrtl.uint<32>) -> !firrtl.uint<31> everything would be connected.

Thank you for your reply.
No. firrtl.shr isn’t present in this chain of ops. I mentioned it just to reference where I got bit enumeration. Please forget about it. The structure that I got is exactly like I mentioned before. The problem is that as a result it produces clearly pointless chain of ops. What’s more, when analyzing bit-to-bit connections, this results in this particular bit remains unconnected. I suspect that it should be like this: %69 = firrtl.bits %_GEN__317 30 to 0 : (!firrtl.uint<32>) -> !firrtl.uint<31> instead of %69 = firrtl.bits %_GEN__317 31 to 1 : (!firrtl.uint<32>) -> !firrtl.uint<31>. I will include the source .fir file (as .txt file)
picorv_processor1.txt (514.6 KB)
NOTE: CIRCT hash commit I’m on: 2d822ea

Here is some snippet of that fir file with the relevant pieces:

    wire _GEN__4214 : UInt
    wire _GEN__3609 : UInt<1>
    wire _GEN__3567 : UInt
    wire _GEN__317 : UInt<32>
    _GEN__4214 <= cat(bits(_GEN__3567, 30, 0), bits(_GEN__3609, 0, 0))
    _GEN__3609 <= UInt<1>(0b0)
    _GEN__3567 <= mux(bits(latched_stalu, 0, 0), bits(alu_out_q, 31, 0), bits(reg_out, 31, 0))
    _GEN__3565 <= cat(bits(_GEN__317, 31, 1), UInt<1>(0b0))
    _GEN__3143 <= cat(bits(_GEN__317, 31, 1), bits(_GEN__826, 0, 0))
    _GEN__317 <= _GEN__4214

Looking at that input .fir file I notice two things:

  1. The bottom bit of _GEN__317 is always 0
  2. The bottom bit of _GEN__317 is never used in subsequent calculations

The FIRRTL file seem to have identical logic to the the IR you sent earlier. If there is a bug here, it seems like it would be in the tool that produced this .fir file, or it originates from the picorv32 project itself. Are you building GitHub - YosysHQ/picorv32: PicoRV32 - A Size-Optimized RISC-V CPU? Are you using the yosys FIRRTL backend to translate from System Verilog to FIRRTL?

  1. Yes.
  2. No, it’s ‘homemade’.