Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/Dialect/FSM/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ add_circt_dialect_library(CIRCTFSM

LINK_LIBS PUBLIC
MLIRIR
CIRCTComb
CIRCTHW
CIRCTSeq
MLIRFuncDialect
Expand Down
72 changes: 72 additions & 0 deletions lib/Dialect/FSM/FSMOps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//===----------------------------------------------------------------------===//

#include "circt/Dialect/FSM/FSMOps.h"
#include "circt/Dialect/Comb/CombOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/IR/BuiltinOps.h"
Expand Down Expand Up @@ -347,7 +348,78 @@ SetVector<StateOp> StateOp::getNextStates() {
return SetVector<StateOp>(nextStates.begin(), nextStates.end());
}

/// Returns the i1 value a transition's guard returns, or null if the transition
/// is unconditional (it has no guard, or its guard returns no value).
static Value getGuardValue(TransitionOp transition) {
if (!transition.hasGuard())
return {};
auto guardReturn = transition.getGuardReturn();
if (guardReturn.getNumOperands() == 0)
return {};
return guardReturn.getOperand();
}

/// Returns true if `value` is a constant i1 `true`, expressed via either
/// hw.constant or arith.constant.
static bool isConstantTrue(Value value) {
if (auto hwConst = value.getDefiningOp<hw::ConstantOp>())
return hwConst.getValue().getBitWidth() == 1 &&
hwConst.getValue().isAllOnes();
if (auto arithConst = value.getDefiningOp<mlir::arith::ConstantOp>())
if (auto boolAttr = dyn_cast<BoolAttr>(arithConst.getValue()))
return boolAttr.getValue();
return false;
}

/// Returns true if `a` is the boolean complement (logical NOT) of `b`, i.e.
/// `a == xor(b, true)`. Handles both the comb and arith spellings of xor as
/// well as operand commutativity.
static bool isComplementOf(Value a, Value b) {
if (!a || !b)
return false;

auto matchXor = [&](Value lhs, Value rhs) {
return (lhs == b && isConstantTrue(rhs)) ||
(rhs == b && isConstantTrue(lhs));
};

if (auto xorOp = a.getDefiningOp<comb::XorOp>())
return xorOp.getNumOperands() == 2 &&
matchXor(xorOp.getOperand(0), xorOp.getOperand(1));
if (auto xorOp = a.getDefiningOp<mlir::arith::XOrIOp>())
return matchXor(xorOp.getLhs(), xorOp.getRhs());
return false;
}

LogicalResult StateOp::canonicalize(StateOp op, PatternRewriter &rewriter) {
// Mutually exclusive transition elimination (llvm/circt#3577): when a state
// has exactly two transitions and the guards are logical complements of each
// other, the second transition is reached only when the first guard failed,
// which already implies the second guard holds. The second guard is therefore
// redundant, so strip it -- turning the transition into an unconditional
// default. This removes the dead complement logic (e.g. a `comb.xor`) that
// would otherwise propagate all the way to emitted SystemVerilog.
auto transitions = llvm::to_vector(op.getTransitions().getOps<TransitionOp>());
if (transitions.size() == 2) {
Value firstGuard = getGuardValue(transitions[0]);
TransitionOp second = transitions[1];
Value secondGuard = getGuardValue(second);
// Either guard may carry the complement (xor); in both cases reaching the
// second transition implies its guard is true, so it can be dropped.
if (firstGuard && secondGuard &&
(isComplementOf(secondGuard, firstGuard) ||
isComplementOf(firstGuard, secondGuard))) {
// Replace the guard's `fsm.return %x` with an operand-less return, making
// the transition unconditional. Mirrors TransitionOp::canonicalize; the
// now-dead complement op is removed by dead-code elimination.
auto guardReturn = second.getGuardReturn();
rewriter.setInsertionPoint(guardReturn);
fsm::ReturnOp::create(rewriter, guardReturn.getLoc());
rewriter.eraseOp(guardReturn);
return success();
}
}

bool hasAlwaysTakenTransition = false;
SmallVector<TransitionOp, 4> transitionsToErase;
// Remove all transitions after an "always-taken" transition.
Expand Down
111 changes: 110 additions & 1 deletion test/Dialect/FSM/canonicalize.mlir
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// RUN: circt-opt --canonicalize %s | FileCheck %s

// No-op variable updates (var <- var) are removed.
// CHECK-LABEL: fsm.machine @foo
// CHECK-NOT: fsm.update

fsm.machine @foo(%arg0: i1) attributes {initialState = "A"} {
%var = fsm.variable "var" {initValue = 0 : i16} : i16
fsm.state @A transitions {
Expand All @@ -10,3 +11,111 @@ fsm.machine @foo(%arg0: i1) attributes {initialState = "A"} {
}
}
}

// Mutually exclusive transition elimination (#3577), comb form: a state with
// exactly two transitions whose second guard is the logical complement of the
// first (`!cond` via comb.xor) has its second guard stripped, dropping the
// redundant complement logic.
// CHECK-LABEL: fsm.machine @mutex_comb
// CHECK: fsm.transition @B guard
// CHECK-NEXT: fsm.return %arg0
// CHECK: fsm.transition @C
// CHECK-NOT: comb.xor
// CHECK-LABEL: fsm.machine @mutex_arith
fsm.machine @mutex_comb(%cond: i1) attributes {initialState = "A"} {
fsm.state @A transitions {
fsm.transition @B guard {
fsm.return %cond
}
fsm.transition @C guard {
%true = hw.constant true
%ncond = comb.xor %cond, %true : i1
fsm.return %ncond
}
}
fsm.state @B transitions {
fsm.transition @A
}
fsm.state @C transitions {
fsm.transition @A
}
}

// Same elimination for the arith spelling of the complement (arith.xori).
// CHECK: fsm.transition @B guard
// CHECK-NEXT: fsm.return %arg0
// CHECK: fsm.transition @C
// CHECK-NOT: arith.xori
// CHECK-LABEL: fsm.machine @not_complementary
fsm.machine @mutex_arith(%cond: i1) attributes {initialState = "A"} {
fsm.state @A transitions {
fsm.transition @B guard {
fsm.return %cond
}
fsm.transition @C guard {
%true = arith.constant true
%ncond = arith.xori %cond, %true : i1
fsm.return %ncond
}
}
fsm.state @B transitions {
fsm.transition @A
}
fsm.state @C transitions {
fsm.transition @A
}
}

// Negative: the second guard (`!b`) is not the complement of the first (`a`),
// so nothing is eliminated and the comb.xor remains.
// CHECK-LABEL: fsm.machine @not_complementary
// CHECK: comb.xor
// CHECK-LABEL: fsm.machine @three_transitions
fsm.machine @not_complementary(%a: i1, %b: i1) attributes {initialState = "A"} {
fsm.state @A transitions {
fsm.transition @B guard {
fsm.return %a
}
fsm.transition @C guard {
%true = hw.constant true
%nb = comb.xor %b, %true : i1
fsm.return %nb
}
}
fsm.state @B transitions {
fsm.transition @A
}
fsm.state @C transitions {
fsm.transition @A
}
}

// Negative: the elimination is conservatively scoped to states with exactly two
// transitions, so a state with three transitions is left untouched even when
// the first two are complementary.
// CHECK-LABEL: fsm.machine @three_transitions
// CHECK: comb.xor
fsm.machine @three_transitions(%cond: i1) attributes {initialState = "A"} {
fsm.state @A transitions {
fsm.transition @B guard {
fsm.return %cond
}
fsm.transition @C guard {
%true = hw.constant true
%ncond = comb.xor %cond, %true : i1
fsm.return %ncond
}
fsm.transition @D guard {
fsm.return %cond
}
}
fsm.state @B transitions {
fsm.transition @A
}
fsm.state @C transitions {
fsm.transition @A
}
fsm.state @D transitions {
fsm.transition @A
}
}