Skip to content

[Comb] Add comb.reverse operation with Verilog Export #8758

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from

Conversation

mafeguimaraes
Copy link

This PR introduces a new operation to the Comb dialect: comb.reverse, which performs bitwise reversal (mirroring) of an integer value.
It also adds Verilog export support for this operation using SystemVerilog’s streaming operator {<<{}}.

Bit reversal is a common operation in hardware design, especially in signal processing and communication protocols.
Previously, reversing bits required generating many explicit assign statements.
This new operation makes the IR cleaner and enables direct export to compact Verilog syntax.

Example

Original Verilog:

assign out[0] = in[7];
assign out[1] = in[6];
assign out[2] = in[5];
assign out[3] = in[4];
assign out[4] = in[3];
assign out[5] = in[2];
assign out[6] = in[1];
assign out[7] = in[0];

MLIR
%rev = comb.reverse %in : (i8) -> i8

Verilog (exported)
assign rev = {<<{in}};

Implementation Details

The implementation is divided into three main parts:

  1. Operation Definition (CombOps.td, CombOps.cpp):

    • The Comb_ReverseOp is defined in CombOps.td with Pure and SameOperandsAndResultType traits.
    • The C++ implementation in CombOps.cpp includes:
      • Constant Folding: Handles bit-reversal for constant operands at compile time.
      • Canonicalization Pattern: Simplifies comb.reverse(comb.reverse(x)) to x.
      • Implements verify() for operand/result checks and inferResultType() for result inference.
  2. Verilog Lowering (ExportVerilog.cpp):

    • A dedicated emitter case for comb::ReverseOp was added.
    • Lowers the operation to SystemVerilog’s streaming operator {<<{in}}, generating compact and readable Verilog design.
  3. Testing (reverse.mlir, comb-reverse.mlir):

    • A new test file was created to verify:
      • Constant folding and canonicalization via -canonicalize.
      • Verilog emission via -export-verilog and FileCheck.

@mafeguimaraes mafeguimaraes requested a review from darthscsi as a code owner July 21, 2025 23:17
Copy link
Contributor

@fabianschuiki fabianschuiki left a comment

Choose a reason for hiding this comment

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

Very cool! Just a few nitpicks and ideas for simplifications.


let hasFolder = 1;
let hasCanonicalizer = 1;
let hasVerifier = 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the verifier just returns success(), you can simplify the op by removing this hasVerifier line and removing the verify function implementation 🙂

Comment on lines 2601 to 2608

if (auto reverseOp = dyn_cast_or_null<comb::ReverseOp>(exp.getDefiningOp())) {
ps << "{<<{";
emitSubExpr(reverseOp.getInput(), LowestPrecedence);
ps << "}}";
emittedExprs.insert(reverseOp);
return {Symbol, IsUnsigned};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be fantastic if you could move this emission into a separate ExprEmitter::visitComb function. We already have one such function per Comb operation. That will then properly insert sign casts, parenthesis, and other things around the streaming operator.

Since you are adding a Comb op, you'll also have to add the op to the Comb op visitor. The visitor is what calls the various visitComb functions. You'll need to do this in two places:

Comment on lines 460 to 462
mlir::LogicalResult comb::ReverseOp::verify() {
return mlir::success();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Not needed 🙂

Comment on lines 464 to 469
void comb::ReverseOp::inferResultRanges(
llvm::ArrayRef<mlir::ConstantIntRanges> operandRanges,
llvm::function_ref<void(mlir::Value, const mlir::ConstantIntRanges &)>
setResultRangeFn) {
setResultRangeFn(getResult(), operandRanges.front());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if this implementation of inferResultRanges is correct. As far as I understand it, that interface is supposed to compute upper and lower bounds for the integer value of the result. Since the ReverseOp reverses the bits of a number and therefore changes its value, the upper and lower bounds of the result are not the same as that of the input. The bit reversal probably has the annoying effect of splitting a contiguous range of integer values into potentially multiple ones.

Do any of your passes rely on having the InferIntRangeInterface implemented on this op? If not, I would delete this implementation and remove the DeclareOpInterfaceMethods<InferIntRangeInterface, ["inferResultRanges"]> trait from the op itself.

Comment on lines 6 to 17
// CHECK-LABEL: module test_reverse(
// CHECK-NEXT: input [7:0] in,
// CHECK-NEXT: output [7:0] out
// CHECK-NEXT: );
// CHECK-EMPTY:
// CHECK-NEXT: assign out = {<<{in}};
// CHECK-NEXT: endmodule

hw.module @test_reverse(in %in: i8, out out: i8) {
%reversed = comb.reverse %in : (i8) -> i8
hw.output %reversed : i8
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice! Could you move this into https://github.com/llvm/circt/blob/main/test/Conversion/ExportVerilog/verilog-basic.mlir? That's where the other Comb operations are tested 🙂

Comment on lines 3 to 32
// This test checks two things for comb.reverse:
// 1. Constant folding: reversing a constant should be done at compile time.
// 2. Canonicalization: the pattern reverse(reverse(x)) should be simplified to x.

// CHECK-LABEL: hw.module @test_reverse_canonicalize
// CHECK-SAME: (in %[[IN:.*]] : i8, out out_double_rev : i8, out out_const_rev : i8)

// After canonicalization, there should be NO comb.reverse operations,
// since reverse(reverse(x)) simplifies to x, and reverse(const) should be folded.
// Therefore, we ensure there are no comb.reverse ops in the output:
// CHECK-NOT: comb.reverse

hw.module @test_reverse_canonicalize(in %in : i8, out out_double_rev : i8, out out_const_rev : i8) {

// Apply reverse twice to test canonicalization
%rev1 = comb.reverse %in : (i8) -> i8
%rev2 = comb.reverse %rev1 : (i8) -> i8

// Apply reverse on a constant to test folding
%c13 = hw.constant 13 : i8
%rev_const = comb.reverse %c13 : (i8) -> i8

// Check outputs: after canonicalization,
// out_double_rev should be wired directly to %in,
// and out_const_rev should be wired to the folded constant (which is -80 in i8)
hw.output %rev2, %rev_const : i8, i8
}

// CHECK: %[[CST:.*]] = hw.constant -80 : i8
// CHECK: hw.output %[[IN]], %[[CST]] : i8, i8
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you move this into https://github.com/llvm/circt/blob/main/test/Dialect/Comb/canonicalization.mlir? That's where the Comb dialect canonicalization tests are located 🙂

let hasCanonicalizer = 1;
let hasVerifier = 1;

let assemblyFormat = "$input attr-dict `:` functional-type($input, $result)";
Copy link
Member

Choose a reason for hiding this comment

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

I think functional-type is not necessary as it has SameOperandsAndResultType

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point, you should be able to just use

`:` type($input)

here.

@mafeguimaraes
Copy link
Author

Thank you both, @fabianschuiki and @uenoku, for the helpful review. I've implemented all the suggested changes. Please let me know if there is anything else. 🙂

@fabianschuiki
Copy link
Contributor

Hey @mafeguimaraes, thanks a lot! It looks like there are a few clang-format issues that CI isn't happy about. These are just code formatting guidelines. Check out "clang format patches display" in https://github.com/llvm/circt/actions/runs/16469136872/job/46573783262?pr=8758 for details.

The other CI failure seems to be related to an accidental LLVM downgrade in your branch. Could you undo the LLVM submodule change in your branch such that it lines up again with what's on "main"? That should clear up the CI build.

@mafeguimaraes
Copy link
Author

I've run clang-format to fix the style issues and reset the LLVM submodule to match main. Thank you, @fabianschuiki!

@fabianschuiki
Copy link
Contributor

Hey @mafeguimaraes, it looks like the LLVM submodule is still pointing to a different commit than what's on main, causing CI to break. Try something like the following to change the submodule to point to the same commit as is on main:

git checkout main -- llvm

Copy link
Contributor

@darthscsi darthscsi left a comment

Choose a reason for hiding this comment

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

Thanks! a couple minor nits, but looking good.

@@ -2598,6 +2599,7 @@ SubExprInfo ExprEmitter::emitSubExpr(Value exp,
SubExprSignRequirement signRequirement,
bool isSelfDeterminedUnsignedValue,
bool isAssignmentLikeContext) {

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: stray whitespace change

emitError(op, "SV attributes emission is unimplemented for the op");

ps << "{<<{";
emitSubExpr(op.getInput(), LowestPrecedence);
Copy link
Contributor

Choose a reason for hiding this comment

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

It's been a while since I dealt with this, but is LowestPrecedence needed? The expression is insides brackets. It might be worth checking the spec to avoid unnecessary parens being emitted.

Copy link
Author

Choose a reason for hiding this comment

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

You are right, since the expression is inside the {<<{...}} streaming operator, it's in a new context and will never need to be parenthesized.

In this emitter, passing LowestPrecedence to the recursive emitSubExpr call is the idiomatic way to achieve exactly that and prevent any unnecessary parens from being added.

emitSubExpr(op.getInput(), LowestPrecedence);
ps << "}}";

return {Symbol, IsUnsigned};
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be concatenation precedence?

Copy link
Author

Choose a reason for hiding this comment

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

The streaming operator {<<{...}} should indeed have the same precedence as a regular concatenation.

emitSubExpr(op.getInput(), LowestPrecedence);
ps << "}}";

return {Symbol, IsUnsigned};
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm having a hard time verifying unsigned results from the spec. It seems true, but I can't find it.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, I had the same doubt. From what I found, the SystemVerilog streaming operator ({<<{}}) preserves the bit pattern without applying any sign extension or interpretation, which effectively makes the result unsigned.

Copy link
Contributor

@fabianschuiki fabianschuiki left a comment

Choose a reason for hiding this comment

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

LGTM! Very cool that we now have an op for this 😃

@mafeguimaraes
Copy link
Author

That's great to hear! Thank you so much for the feedback and for guiding me through the process. I'm happy I could contribute. 😃

@fabianschuiki
Copy link
Contributor

Looks like there's a minor breakage in an LLHD pass associated with the CombVisitor. I think you just need to create a visitComb stub for the ReverseOp here: https://github.com/llvm/circt/blob/main/lib/Dialect/LLHD/Transforms/DesequentializationPass.cpp#L155

Interesting that this didn't show up for you in your local build 🤔

@mafeguimaraes
Copy link
Author

Looks like there's a minor breakage in an LLHD pass associated with the CombVisitor. I think you just need to create a visitComb stub for the ReverseOp here: https://github.com/llvm/circt/blob/main/lib/Dialect/LLHD/Transforms/DesequentializationPass.cpp#L155

Interesting that this didn't show up for you in your local build 🤔

Good catch! My local build configuration didn't include the LLHD dialect, so I missed this dependency. I've added the necessary visitComb stub for ReverseOp to the CombInterpreter. Thanks for pointing it out!

@mafeguimaraes mafeguimaraes requested a review from maerhart as a code owner July 23, 2025 18:35
@fabianschuiki
Copy link
Contributor

Looks like clang-format is unhappy about some trailing white space. Try running clang-format locally, or manually fix the whitespace 😃

@fabianschuiki
Copy link
Contributor

@mafeguimaraes You can run the tests locally with ninja check-circt or make check-circt, depending on what build system you are using. That should allow you to debug the CI failures more quickly, since CI basically runs clang-format and ninja check-circt as well 🙂

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

Successfully merging this pull request may close these issues.

4 participants