Skip to content
Merged
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
110 changes: 92 additions & 18 deletions include/circt/Dialect/Sim/SimOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,6 @@ def PlusArgsValueOp : SimOp<"plusargs.value", [Pure]> {
let assemblyFormat = "$formatString attr-dict `:` type($result)";
}

def FinishOp : SimOp<"finish"> {
let summary = "Simulation finish condition";

let arguments = (ins ClockType:$clk, I1:$cond);
let results = (outs);

let assemblyFormat = "$clk `,` $cond attr-dict";
}

def FatalOp : SimOp<"fatal"> {
let summary = "Simulation failure condition";

let arguments = (ins ClockType:$clk, I1:$cond);
let results = (outs);

let assemblyFormat = "$clk `,` $cond attr-dict";
}

def DPIFuncOp : SimOp<"func.dpi",
[IsolatedFromAbove, Symbol, OpAsmOpInterface,
FunctionOpInterface]> {
Expand Down Expand Up @@ -379,4 +361,96 @@ def PrintFormattedProcOp : SimOp<"proc.print"> {
let assemblyFormat = "$input attr-dict";
}

//===----------------------------------------------------------------------===//
// Simulation Control
//===----------------------------------------------------------------------===//

def ClockedTerminateOp : SimOp<"clocked_terminate"> {
let summary = "Terminate the simulation";
let description = [{
Implements the semantics of `sim.terminate` if the given condition is true
on the rising edge of the clock operand.
}];
let arguments = (ins
ClockType:$clock,
I1:$condition,
BoolAttr:$success,
BoolAttr:$verbose
);
let assemblyFormat = [{
$clock `,` $condition `,`
custom<KeywordBool>($success, "\"success\"", "\"failure\"") `,`
custom<KeywordBool>($verbose, "\"verbose\"" , "\"quiet\"") attr-dict
}];
}

def ClockedPauseOp : SimOp<"clocked_pause"> {
let summary = "Pause the simulation";
let description = [{
Implements the semantics of `sim.pause` if the given condition is true on
the rising edge of the clock operand.
}];
let arguments = (ins
ClockType:$clock,
I1:$condition,
BoolAttr:$verbose
);
let assemblyFormat = [{
$clock `,` $condition `,`
custom<KeywordBool>($verbose, "\"verbose\"" , "\"quiet\"") attr-dict
}];
}

def TerminateOp : SimOp<"terminate"> {
let summary = "Terminate the simulation";
let description = [{
Terminate the simulation with a success or failure exit code. Depending on
the `verbose` operand, simulators may print additional information about the
current simulation time and hierarchical location of the op.

This op correpsonds to the following SystemVerilog constructs:

| Operation | SystemVerilog |
|----------------------------------|---------------------------|
| `sim.terminate success, quiet` | `$finish(0)` |
| `sim.terminate success, verbose` | `$finish` or `$finish(1)` |
| `sim.terminate failure, quiet` | `$fatal(0)` |
| `sim.terminate failure, verbose` | `$fatal` or `$fatal(1)` |

Note that this op does not match the behavior of the `$exit` system task in
SystemVerilog, which blocks execution of the calling process until all
`program` instances have terminated, and then calls `$finish`.
}];
let arguments = (ins
BoolAttr:$success,
BoolAttr:$verbose
);
let assemblyFormat = [{
custom<KeywordBool>($success, "\"success\"", "\"failure\"") `,`
custom<KeywordBool>($verbose, "\"verbose\"" , "\"quiet\"") attr-dict
}];
}

def PauseOp : SimOp<"pause"> {
let summary = "Pause the simulation";
let description = [{
Interrupt the simulation and give control back to the user in case of an
interactive simulation. Non-interactive simulations may choose to terminate
instead. Depending on the `verbose` operand, simulators may print additional
information about the current simulation time and hierarchical location of
the op.

This op corresponds to the following SystemVerilog constructs:

| Operation | SystemVerilog |
|---------------------|------------------------|
| `sim.pause quiet` | `$stop(0)` |
| `sim.pause verbose` | `$stop` or `$stop(1)` |
}];
let arguments = (ins BoolAttr:$verbose);
let assemblyFormat = [{
custom<KeywordBool>($verbose, "\"verbose\"" , "\"quiet\"") attr-dict
}];
}

#endif // CIRCT_DIALECT_SIM_SIMOPS_TD
11 changes: 5 additions & 6 deletions lib/Conversion/FIRRTLToHW/LowerToHW.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,7 @@ void FIRRTLModuleLowering::lowerFileHeader(CircuitOp op,
// Helper function to emit #ifndef guard.
auto emitGuard = [&](const char *guard, llvm::function_ref<void(void)> body) {
sv::IfDefOp::create(
b, guard, []() {}, body);
b, guard, [] {}, body);
};

if (state.usedFileDescriptorLib) {
Expand Down Expand Up @@ -2895,7 +2895,7 @@ void FIRRTLLowering::addToAlwaysBlock(
// It is weird but intended. Here we want to create an empty sv.if
// with an else block.
insideIfOp = sv::IfOp::create(
builder, reset, []() {}, []() {});
builder, reset, [] {}, [] {});
};
if (resetStyle == sv::ResetType::AsyncReset) {
sv::EventControl events[] = {clockEdge, resetEdge};
Expand Down Expand Up @@ -4973,10 +4973,9 @@ LogicalResult FIRRTLLowering::visitStmt(StopOp op) {
sv::MacroRefExprOp::create(builder, cond.getType(), "STOP_COND_");
Value exitCond = builder.createOrFold<comb::AndOp>(stopCond, cond, true);

if (op.getExitCode())
sim::FatalOp::create(builder, clock, exitCond);
else
sim::FinishOp::create(builder, clock, exitCond);
sim::ClockedTerminateOp::create(builder, clock, exitCond,
/*success=*/op.getExitCode() == 0,
/*verbose=*/true);

return success();
}
Expand Down
165 changes: 136 additions & 29 deletions lib/Conversion/SimToSV/SimToSV.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ namespace circt {
using namespace circt;
using namespace sim;

/// Check whether an op should be placed inside an ifdef guard that prevents it
/// from affecting synthesis runs.
static bool needsIfdefGuard(Operation *op) {
return isa<ClockedTerminateOp, ClockedPauseOp, TerminateOp, PauseOp>(op);
}

/// Check whether an op should be placed inside an always process triggered on a
/// clock, and an if statement checking for a condition.
static std::pair<Value, Value> needsClockAndConditionWrapper(Operation *op) {
return TypeSwitch<Operation *, std::pair<Value, Value>>(op)
.Case<ClockedTerminateOp, ClockedPauseOp>(
[](auto op) -> std::pair<Value, Value> {
return {op.getClock(), op.getCondition()};
})
.Default({});
}

namespace {

struct SimConversionState {
Expand Down Expand Up @@ -146,34 +163,31 @@ class PlusArgsValueLowering : public SimConversionPattern<PlusArgsValueOp> {
}
};

template <typename FromOp, typename ToOp>
class SimulatorStopLowering : public SimConversionPattern<FromOp> {
public:
using SimConversionPattern<FromOp>::SimConversionPattern;

LogicalResult
matchAndRewrite(FromOp op, typename FromOp::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const final {
auto loc = op.getLoc();

Value clockCast = seq::FromClockOp::create(rewriter, loc, adaptor.getClk());
static LogicalResult convert(ClockedTerminateOp op, PatternRewriter &rewriter) {
if (op.getSuccess())
rewriter.replaceOpWithNewOp<sv::FinishOp>(op, op.getVerbose());
else
rewriter.replaceOpWithNewOp<sv::FatalOp>(op, op.getVerbose());
return success();
}

this->state.usedSynthesisMacro = true;
sv::IfDefOp::create(
rewriter, loc, "SYNTHESIS", [&] {},
[&] {
sv::AlwaysOp::create(
rewriter, loc, sv::EventControl::AtPosEdge, clockCast, [&] {
sv::IfOp::create(rewriter, loc, adaptor.getCond(),
[&] { ToOp::create(rewriter, loc); });
});
});
static LogicalResult convert(ClockedPauseOp op, PatternRewriter &rewriter) {
rewriter.replaceOpWithNewOp<sv::StopOp>(op, op.getVerbose());
return success();
}

rewriter.eraseOp(op);
static LogicalResult convert(TerminateOp op, PatternRewriter &rewriter) {
if (op.getSuccess())
rewriter.replaceOpWithNewOp<sv::FinishOp>(op, op.getVerbose());
else
rewriter.replaceOpWithNewOp<sv::FatalOp>(op, op.getVerbose());
return success();
}

return success();
}
};
static LogicalResult convert(PauseOp op, PatternRewriter &rewriter) {
rewriter.replaceOpWithNewOp<sv::StopOp>(op, op.getVerbose());
return success();
}

class DPICallLowering : public SimConversionPattern<DPICallOp> {
public:
Expand Down Expand Up @@ -315,6 +329,96 @@ void LowerDPIFunc::addFragments(hw::HWModuleOp module,
ArrayAttr::get(module.getContext(), fragments.takeVector()));
}

static bool moveOpsIntoIfdefGuardsAndProcesses(Operation *rootOp) {
bool usedSynthesisMacro = false;

rootOp->walk([&](Operation *op) {
auto loc = op->getLoc();

// Move the op into an ifdef guard if needed.
if (needsIfdefGuard(op)) {
// Try to reuse an ifdef guard immediately before the op.
Block *block = nullptr;
if (op->getPrevNode())
block = TypeSwitch<Operation *, Block *>(op->getPrevNode())
.Case<sv::IfDefOp, sv::IfDefProceduralOp>(
[&](auto guardOp) -> Block * {
if (guardOp.getCond().getIdent().getAttr() ==
"SYNTHESIS" &&
guardOp.hasElse())
return guardOp.getElseBlock();
return nullptr;
})
.Default([](auto) { return nullptr; });

// If there was no pre-existing guard, create one.
if (!block) {
OpBuilder builder(op);
if (op->getParentOp()->hasTrait<sv::ProceduralRegion>())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this sufficient at this point? What if the parent op has not been lowered to SV yet, but will become a procedural region? I've been struggling in the past with the lack of a cross-dialect notion of procedural regions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think it is, at least for now. We only ever nest into ifdefs and always processes, which implement the trait. And the verification of SV ops simply checks if the parent has one of these traits.

Maybe we should just switch over to checking if the parent is a graph region or not 🤔, which is universal across dialects, instead of relying on an SV trait.

block = sv::IfDefProceduralOp::create(
builder, loc, "SYNTHESIS", [] {}, [] {})
.getElseBlock();
else
block = sv::IfDefOp::create(
builder, loc, "SYNTHESIS", [] {}, [] {})
.getElseBlock();
usedSynthesisMacro = true;
}

// Move the op into the guard block.
op->moveBefore(block, block->end());
}

// Check if the op requires an clock and condition wrapper.
auto [clock, condition] = needsClockAndConditionWrapper(op);

// Create an enclosing always process.
if (clock) {
// Try to reuse an always process immediately before the op.
Block *block = nullptr;
if (auto alwaysOp = dyn_cast_or_null<sv::AlwaysOp>(op->getPrevNode()))
if (alwaysOp.getNumConditions() == 1 &&
alwaysOp.getCondition(0).event == sv::EventControl::AtPosEdge)
if (auto clockOp = alwaysOp.getCondition(0)
.value.getDefiningOp<seq::FromClockOp>())
if (clockOp.getInput() == clock)
block = alwaysOp.getBodyBlock();

// If there was no pre-existing always process, create one.
if (!block) {
OpBuilder builder(op);
clock = seq::FromClockOp::create(builder, loc, clock);
block = sv::AlwaysOp::create(builder, loc, sv::EventControl::AtPosEdge,
clock, [] {})
.getBodyBlock();
}

// Move the op into the process.
op->moveBefore(block, block->end());
}

// Create an enclosing if condition.
if (condition) {
// Try to reuse an if statement immediately before the op.
Block *block = nullptr;
if (auto ifOp = dyn_cast_or_null<sv::IfOp>(op->getPrevNode()))
if (ifOp.getCond() == condition)
block = ifOp.getThenBlock();

// If there was no pre-existing if statement, create one.
if (!block) {
OpBuilder builder(op);
block = sv::IfOp::create(builder, loc, condition, [] {}).getThenBlock();
}

// Move the op into the if body.
op->moveBefore(block, block->end());
}
});

return usedSynthesisMacro;
}

namespace {
struct SimToSVPass : public circt::impl::LowerSimToSVBase<SimToSVPass> {
void runOnOperation() override {
Expand All @@ -329,6 +433,9 @@ struct SimToSVPass : public circt::impl::LowerSimToSVBase<SimToSVPass> {

std::atomic<bool> usedSynthesisMacro = false;
auto lowerModule = [&](hw::HWModuleOp module) {
if (moveOpsIntoIfdefGuardsAndProcesses(module))
usedSynthesisMacro = true;

SimConversionState state;
ConversionTarget target(*context);
target.addIllegalDialect<SimDialect>();
Expand All @@ -340,10 +447,10 @@ struct SimToSVPass : public circt::impl::LowerSimToSVBase<SimToSVPass> {
RewritePatternSet patterns(context);
patterns.add<PlusArgsTestLowering>(context, state);
patterns.add<PlusArgsValueLowering>(context, state);
patterns.add<SimulatorStopLowering<sim::FinishOp, sv::FinishOp>>(context,
state);
patterns.add<SimulatorStopLowering<sim::FatalOp, sv::FatalOp>>(context,
state);
patterns.add<ClockedTerminateOp>(convert);
patterns.add<ClockedPauseOp>(convert);
patterns.add<TerminateOp>(convert);
patterns.add<PauseOp>(convert);
patterns.add<DPICallLowering>(context, state);
auto result = applyPartialConversion(module, target, std::move(patterns));

Expand Down
1 change: 1 addition & 0 deletions lib/Dialect/Sim/SimOps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "circt/Dialect/Sim/SimOps.h"
#include "circt/Dialect/HW/ModuleImplementation.h"
#include "circt/Dialect/SV/SVOps.h"
#include "circt/Support/CustomDirectiveImpl.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/Interfaces/FunctionImplementation.h"
Expand Down
4 changes: 2 additions & 2 deletions test/Conversion/FIRRTLToHW/lower-to-hw.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -461,12 +461,12 @@ firrtl.circuit "Simple" attributes {annotations = [{class =
firrtl.module private @Stop(in %clock1: !firrtl.clock, in %clock2: !firrtl.clock, in %reset: !firrtl.uint<1>) {
// CHECK-NEXT: [[STOP_COND_1:%.+]] = sv.macro.ref.expr @STOP_COND_
// CHECK-NEXT: [[COND:%.+]] = comb.and bin [[STOP_COND_1]], %reset : i1
// CHECK-NEXT: sim.fatal %clock1, [[COND]]
// CHECK-NEXT: sim.clocked_terminate %clock1, [[COND]], failure
firrtl.stop %clock1, %reset, 42 : !firrtl.clock, !firrtl.uint<1>

// CHECK-NEXT: [[STOP_COND_2:%.+]] = sv.macro.ref.expr @STOP_COND_
// CHECK-NEXT: [[COND:%.+]] = comb.and bin [[STOP_COND_2:%.+]], %reset : i1
// CHECK-NEXT: sim.finish %clock2, [[COND]]
// CHECK-NEXT: sim.clocked_terminate %clock2, [[COND]], success
firrtl.stop %clock2, %reset, 0 : !firrtl.clock, !firrtl.uint<1>
}

Expand Down
Loading