Skip to content

Conversation

fabianschuiki
Copy link
Contributor

Case statements often occur in the following form in real-world Verilog:

module Foo(input logic [1:0] a, output logic [3:0] z);
  always_comb begin
    case (a)
      2'd0: z = 4'b0001;
      2'd1: z = 4'b0010;
      2'd2: z = 4'b0100;
      2'd3: z = 4'b1000;
    endcase
  end
endmodule

The intention being that the case statement exhaustively covers all possible a input values, and assigns a corresponding z output value. This case statement is exhaustive assuming two-state logic. However, the Verilog logic type is four-state, where the bits in a can also have the value X and Z. If a contains X or Z, none of the case statements would execute and z would not be assigned, essentially creating a latch.

In practice, logic synthesizers fold X and Z values to 0 or 1 however they see fit, often in an attempt to minimize logic. For this case statement the obvious assumption is that the input is two-state and the match is exhaustive. This does not work in simulation, though.

This commit makes ImportVerilog detect if a case statement is exhaustive assuming two-state logic. If it is, the last case item is treated as the default, being executed regardless of its case value. As a result, the above example generates a CFG where all possible paths through the case statement assign the output z. This then ensures that the case statement can be lowered to properly in circt-verilog, without generating conditional drives or latches.

The core dialects of CIRCT currently don't represent X and Z well, and most of ImportVerilog and the circt-verilog pipeline assumes all values are two-state. Once we add X and Z handling to the core dialects, we will have to revisit this behavior and make it opt-out.

Fixes #8626.

Case statements often occur in the following form in real-world Verilog:

```
module Foo(input logic [1:0] a, output logic [3:0] z);
  always_comb begin
    case (a)
      2'd0: z = 4'b0001;
      2'd1: z = 4'b0010;
      2'd2: z = 4'b0100;
      2'd3: z = 4'b1000;
    endcase
  end
endmodule
```

The intention being that the case statement exhaustively covers all
possible `a` input values, and assigns a corresponding `z` output value.
This case statement _is_ exhaustive assuming two-state logic. However,
the Verilog `logic` type is four-state, where the bits in `a` can also
have the value X and Z. If `a` contains X or Z, none of the case
statements would execute and `z` would not be assigned, essentially
creating a latch.

In practice, logic synthesizers fold X and Z values to 0 or 1 however
they see fit, often in an attempt to minimize logic. For this case
statement the obvious assumption is that the input is two-state and the
match is exhaustive. This does not work in simulation, though.

This commit makes ImportVerilog detect if a case statement is exhaustive
assuming two-state logic. If it is, the last case item is treated as the
default, being executed regardless of its case value. As a result, the
above example generates a CFG where all possible paths through the case
statement assign the output `z`. This then ensures that the case
statement can be lowered to properly in circt-verilog, without
generating conditional drives or latches.

The core dialects of CIRCT currently don't represent X and Z well, and
most of ImportVerilog and the circt-verilog pipeline assumes all values
are two-state. Once we add X and Z handling to the core dialects, we
will have to revisit this behavior and make it opt-out.

Fixes #8626.
Copy link
Member

@maerhart maerhart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Kinda sad that it is so common place to apply such kind of hacks in Verilog tools/compilers.

auto twoStateExhaustive = false;
if (auto intType = dyn_cast<moore::IntType>(caseExpr.getType());
intType && intType.getWidth() < 32 &&
itemConsts.size() == (1 << intType.getWidth())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is itemConsts guaranteed to be unique?

Copy link
Contributor Author

@fabianschuiki fabianschuiki Jul 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think not necessarily. If I recall correctly, you can have multiple case items match in Verilog, but only the first match as listed in the source text is executed. In that case this change wouldn't treat that case statement as exhaustive and instead just generate the regular default/no-match case in the control flow.

We might want a dedicated moore.switch control flow statement in the long run, especially once we need to handle X/Z bits properly. cf.switch doesn't really work too well there, since it uses plain old integers. There's also this batshit crazy Verilog pattern to express a one-hot match (constant in the case expr, dynamic values in the individual case items):

unique case (1)
  a[0]: doStuff();
  a[1]: doStuff();
  a[2]: doStuff();
  a[3]: doStuff();
endcase

No idea if we'd want to capture this in the IR somehow. Verilog makes me sad.

@fabianschuiki
Copy link
Contributor Author

@maerhart Yeah this makes me sad, too 😞. Especially because Verilog goes out of its way so much just to look and feel like C/C++, without realizing that its variable mutation is definitely not a good model to describe hardware. This case thing really wants to be an expression, like Rust's match.

@fabianschuiki fabianschuiki merged commit e636298 into main Jul 1, 2025
7 checks passed
@fabianschuiki fabianschuiki deleted the fschuiki/fix-exhaustive-case branch July 1, 2025 15:52
TaoBi22 pushed a commit to TaoBi22/circt that referenced this pull request Jul 17, 2025
…llvm#8628)

Case statements often occur in the following form in real-world Verilog:

```
module Foo(input logic [1:0] a, output logic [3:0] z);
  always_comb begin
    case (a)
      2'd0: z = 4'b0001;
      2'd1: z = 4'b0010;
      2'd2: z = 4'b0100;
      2'd3: z = 4'b1000;
    endcase
  end
endmodule
```

The intention being that the case statement exhaustively covers all
possible `a` input values, and assigns a corresponding `z` output value.
This case statement _is_ exhaustive assuming two-state logic. However,
the Verilog `logic` type is four-state, where the bits in `a` can also
have the value X and Z. If `a` contains X or Z, none of the case
statements would execute and `z` would not be assigned, essentially
creating a latch.

In practice, logic synthesizers fold X and Z values to 0 or 1 however
they see fit, often in an attempt to minimize logic. For this case
statement the obvious assumption is that the input is two-state and the
match is exhaustive. This does not work in simulation, though.

This commit makes ImportVerilog detect if a case statement is exhaustive
assuming two-state logic. If it is, the last case item is treated as the
default, being executed regardless of its case value. As a result, the
above example generates a CFG where all possible paths through the case
statement assign the output `z`. This then ensures that the case
statement can be lowered to properly in circt-verilog, without
generating conditional drives or latches.

The core dialects of CIRCT currently don't represent X and Z well, and
most of ImportVerilog and the circt-verilog pipeline assumes all values
are two-state. Once we add X and Z handling to the core dialects, we
will have to revisit this behavior and make it opt-out.

Fixes llvm#8626.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[ImportVerilog] Fixup case statements that look exhaustive but are not
3 participants