Skip to content

Commit e217d99

Browse files
authored
[SV] Add pass to hide non-synthesizable ops (#10479)
Add `--sv-mask-non-synthesizable`, a late SV pass that hides concurrent and property assert/assume/cover ops from synthesis. Three modes (`delete`, `ifdef`) and a configurable macro name. All modes are idempotent across re-runs. Operates on `mlir::ModuleOp` and processes each `hw.module` in parallel. Wired into the PyCDE final lowering pipeline so its output is acceptable to Quartus and other tools that reject SVA constructs. This is the configurable-pass alternative to the always-on `ifdef SYNTHESIS` guards in `LowerVerifToSV` that were tried in #10387 and reverted. Closes #10378. Assisted-by: vscode:Claude Opus 4.7
1 parent dd78e88 commit e217d99

6 files changed

Lines changed: 462 additions & 0 deletions

File tree

frontends/PyCDE/src/pycde/system.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ def _run_pass_list(self, pass_list, pass_run_name, module=None, debug=False):
473473
"builtin.module(lower-comb)",
474474
"builtin.module(hw.module(lower-verif-to-sv))",
475475
"builtin.module(cse, canonicalize, cse)",
476+
"builtin.module(sv-mask-non-synthesizable)",
476477
"builtin.module(hw.module(prettify-verilog), hw.module(hw-cleanup))",
477478
"builtin.module(msft-export-tcl{{tops={tops} tcl-file={tcl_file}}})"
478479
]

include/circt/Dialect/SV/SVPasses.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818
namespace circt {
1919
namespace sv {
2020

21+
/// Selects how `sv-mask-non-synthesizable` hides matched ops from
22+
/// synthesis-bound SystemVerilog output.
23+
enum class MaskNonSynthesizableMode {
24+
/// Erase the matched ops.
25+
Delete,
26+
/// Wrap the matched ops in `` `ifndef SYNTHESIS `` ... `` `endif ``.
27+
Ifdef,
28+
};
29+
2130
#define GEN_PASS_DECL
2231
#include "circt/Dialect/SV/SVPasses.h.inc"
2332

include/circt/Dialect/SV/SVPasses.td

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,44 @@ def HWEliminateInOutPorts : Pass<"hw-eliminate-inout-ports",
130130
];
131131
}
132132

133+
def SVMaskNonSynthesizable : Pass<"sv-mask-non-synthesizable",
134+
"mlir::ModuleOp"> {
135+
let summary = "Hide non-synthesizable SV ops from synthesis tools";
136+
let description = [{
137+
This pass hides a fixed set of `sv` ops that are not generally
138+
synthesizable (concurrent and property assertions) from the
139+
SystemVerilog output. Two masking modes are supported:
140+
141+
* `delete`: erase the matched ops entirely.
142+
* `ifdef`: wrap each matched op individually in
143+
`` `ifndef <macro> `` ... `` `endif `` (using `sv.ifdef` or
144+
`sv.ifdef.procedural` as appropriate). A matching `sv.macro.decl` is
145+
created at the top of the module if one is not already present. The
146+
macro name is configurable via the `macro` option (default
147+
`SYNTHESIS`).
148+
149+
The pass is intended to run as one of the last passes before `ExportVerilog`
150+
so we only have to know about a smaller number of ops but before the
151+
HWCleanup pass (which will merge the sv.ifdef ops where it can). The set of
152+
masked op names is hard-coded in the pass implementation. Both modes are
153+
idempotent.
154+
}];
155+
156+
let dependentDialects = ["circt::sv::SVDialect"];
157+
let options = [
158+
Option<"mode", "mode", "::circt::sv::MaskNonSynthesizableMode",
159+
"::circt::sv::MaskNonSynthesizableMode::Ifdef",
160+
"How to mask the matched ops",
161+
[{::llvm::cl::values(
162+
clEnumValN(::circt::sv::MaskNonSynthesizableMode::Delete,
163+
"delete", "Erase the matched ops"),
164+
clEnumValN(::circt::sv::MaskNonSynthesizableMode::Ifdef,
165+
"ifdef",
166+
"Wrap matched ops in `ifndef SYNTHESIS")
167+
)}]>,
168+
Option<"macro", "macro", "std::string", "\"SYNTHESIS\"",
169+
"Macro name used by `ifdef` mode">
170+
];
171+
}
172+
133173
#endif // CIRCT_DIALECT_SV_SVPASSES

lib/Dialect/SV/Transforms/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ add_circt_dialect_library(CIRCTSVTransforms
55
HWLegalizeModules.cpp
66
PrettifyVerilog.cpp
77
HWExportModuleHierarchy.cpp
8+
SVMaskNonSynthesizable.cpp
89
SVTraceIVerilog.cpp
910
HWEliminateInOutPorts.cpp
1011

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
//===- SVMaskNonSynthesizable.cpp - Mask non-synthesizable SV ops ---------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
//
9+
// This pass hides a fixed set of `sv` operations that are not generally
10+
// synthesizable (concurrent and property assertions) from the SystemVerilog
11+
// output. It supports two masking modes:
12+
//
13+
// * `delete`: erase the matched ops.
14+
// * `ifdef` : wrap each matched op individually in an
15+
// `sv.ifdef`/`sv.ifdef.procedural` whose else region holds the
16+
// moved op, plus a single `sv.macro.decl @SYNTHESIS` at the top
17+
// of the module if absent.
18+
//
19+
//===----------------------------------------------------------------------===//
20+
21+
#include "circt/Dialect/HW/HWOps.h"
22+
#include "circt/Dialect/SV/SVAttributes.h"
23+
#include "circt/Dialect/SV/SVOps.h"
24+
#include "circt/Dialect/SV/SVPasses.h"
25+
#include "circt/Support/Namespace.h"
26+
#include "circt/Support/ProceduralRegionTrait.h"
27+
#include "circt/Support/Utils.h"
28+
#include "mlir/IR/Builders.h"
29+
#include "mlir/Pass/Pass.h"
30+
#include "llvm/ADT/TypeSwitch.h"
31+
32+
namespace circt {
33+
namespace sv {
34+
#define GEN_PASS_DEF_SVMASKNONSYNTHESIZABLE
35+
#include "circt/Dialect/SV/SVPasses.h.inc"
36+
} // namespace sv
37+
} // namespace circt
38+
39+
using namespace circt;
40+
using namespace circt::sv;
41+
42+
//===----------------------------------------------------------------------===//
43+
// Helpers
44+
//===----------------------------------------------------------------------===//
45+
46+
/// Returns true if `op` is one of the SV ops we mask.
47+
static bool isMaskedOp(Operation *op) {
48+
return isa<sv::AssertConcurrentOp, sv::AssumeConcurrentOp,
49+
sv::CoverConcurrentOp, sv::AssertPropertyOp, sv::AssumePropertyOp,
50+
sv::CoverPropertyOp>(op);
51+
}
52+
53+
/// If `op` is an `sv.ifdef`/`sv.ifdef.procedural` whose macro matches
54+
/// `expectedMacro`, returns its else region; otherwise returns nullptr. Used
55+
/// to identify the region that need not be re-wrapped in `ifdef` mode.
56+
static Region *getMatchingIfDefElseRegion(Operation *op,
57+
StringRef expectedMacro) {
58+
return llvm::TypeSwitch<Operation *, Region *>(op)
59+
.Case<sv::IfDefOp, sv::IfDefProceduralOp>([&](auto ifdef) -> Region * {
60+
if (ifdef.getCond().getName() != expectedMacro)
61+
return nullptr;
62+
return &ifdef.getElseRegion();
63+
})
64+
.Default(static_cast<Region *>(nullptr));
65+
}
66+
67+
/// Resolve the symbol name to use for the `sv.macro.decl` referenced by
68+
/// `ifdef` mode's `sv.ifdef` ops. Returns the existing decl's sym_name if a
69+
/// `sv.macro.decl` whose Verilog identifier matches `verilogName` already
70+
/// exists at top level; otherwise returns a fresh sym_name that does not
71+
/// collide with any existing top-level symbol (which may differ from
72+
/// `verilogName` if a non-`sv.macro.decl` symbol of that name is present).
73+
/// `created` is set to true iff the returned name belongs to a decl that
74+
/// the caller still needs to materialize.
75+
static StringAttr resolveMacroSymName(mlir::ModuleOp moduleOp,
76+
StringRef verilogName, bool &created) {
77+
for (auto decl : moduleOp.getOps<sv::MacroDeclOp>()) {
78+
if (decl.getMacroIdentifier() == verilogName) {
79+
created = false;
80+
return decl.getSymNameAttr();
81+
}
82+
}
83+
Namespace ns;
84+
ns.add(moduleOp);
85+
StringRef name = ns.newName(verilogName);
86+
created = true;
87+
return StringAttr::get(moduleOp.getContext(), name);
88+
}
89+
90+
/// `delete` mode: walk the block and erase every masked op.
91+
static void processBlockDelete(Block &block) {
92+
block.walk([&](Operation *op) {
93+
if (isMaskedOp(op))
94+
op->erase();
95+
});
96+
}
97+
98+
/// Wrap a single masked op in its own `sv.ifdef`/`sv.ifdef.procedural`. Picks
99+
/// the procedural variant based on the enclosing region.
100+
static void maskOpByIfdef(Operation *op, StringRef macro) {
101+
OpBuilder builder(op);
102+
Location loc = op->getLoc();
103+
104+
// Passing a non-null `elseCtor` is what triggers the builder to create an
105+
// else block.
106+
Block *elseBlock;
107+
if (isInProceduralRegion(op))
108+
elseBlock =
109+
sv::IfDefProceduralOp::create(builder, loc, macro,
110+
/*thenCtor=*/{}, /*elseCtor=*/[]() {})
111+
.getElseBlock();
112+
else
113+
elseBlock = sv::IfDefOp::create(builder, loc, macro, /*thenCtor=*/{},
114+
/*elseCtor=*/[]() {})
115+
.getElseBlock();
116+
117+
// Move the op into the else block.
118+
op->moveBefore(elseBlock, elseBlock->end());
119+
}
120+
121+
/// `ifdef` mode: wrap each masked op in its own
122+
/// `sv.ifdef`/`sv.ifdef.procedural`. Recurse into nested regions of non-masked
123+
/// ops, but skip recursion into the else region of a matching
124+
/// `sv.ifdef @<macro>` -- those ops are already guarded.
125+
static bool processBlockIfdef(Block &block, StringRef macro) {
126+
bool changed = false;
127+
// `make_early_inc_range` lets us move the current op out of `block` inside
128+
// the loop body without invalidating the iterator.
129+
for (Operation &op : llvm::make_early_inc_range(block)) {
130+
if (isMaskedOp(&op)) {
131+
maskOpByIfdef(&op, macro);
132+
changed = true;
133+
continue;
134+
}
135+
Region *guardedElseRegion = getMatchingIfDefElseRegion(&op, macro);
136+
for (Region &region : op.getRegions()) {
137+
if (&region == guardedElseRegion)
138+
continue;
139+
for (Block &nested : region)
140+
changed |= processBlockIfdef(nested, macro);
141+
}
142+
}
143+
return changed;
144+
}
145+
146+
//===----------------------------------------------------------------------===//
147+
// Pass
148+
//===----------------------------------------------------------------------===//
149+
150+
namespace {
151+
struct SVMaskNonSynthesizablePass
152+
: public circt::sv::impl::SVMaskNonSynthesizableBase<
153+
SVMaskNonSynthesizablePass> {
154+
using Base::Base;
155+
void runOnOperation() override;
156+
};
157+
} // namespace
158+
159+
void SVMaskNonSynthesizablePass::runOnOperation() {
160+
mlir::ModuleOp moduleOp = getOperation();
161+
162+
// For `ifdef` mode, resolve the symbol name to use for the macro decl
163+
// up front.
164+
StringAttr macroSymName;
165+
bool macroDeclNeedsCreation = false;
166+
if (mode == MaskNonSynthesizableMode::Ifdef)
167+
macroSymName = resolveMacroSymName(moduleOp, macro, macroDeclNeedsCreation);
168+
169+
StringRef passDownMacro =
170+
macroSymName ? macroSymName.getValue() : StringRef();
171+
// Each `hw.module`'s body is independent: `processBlock*` only mutates ops
172+
// inside the module it's called on.
173+
SmallVector<hw::HWModuleOp> hwModules(moduleOp.getOps<hw::HWModuleOp>());
174+
unsigned numChanged =
175+
transformReduce(&getContext(), hwModules, /*init=*/0u, std::plus<>(),
176+
[&](hw::HWModuleOp hwModule) -> unsigned {
177+
Block &body = *hwModule.getBodyBlock();
178+
switch (mode) {
179+
case MaskNonSynthesizableMode::Delete:
180+
processBlockDelete(body);
181+
return 0;
182+
case MaskNonSynthesizableMode::Ifdef:
183+
return processBlockIfdef(body, passDownMacro) ? 1 : 0;
184+
}
185+
llvm_unreachable("all modes handled");
186+
});
187+
188+
if (mode == MaskNonSynthesizableMode::Ifdef && numChanged > 0 &&
189+
macroDeclNeedsCreation) {
190+
auto builder = OpBuilder::atBlockBegin(moduleOp.getBody());
191+
sv::MacroDeclOp::create(builder, moduleOp.getLoc(), macroSymName,
192+
/*args=*/ArrayAttr{}, builder.getStringAttr(macro));
193+
}
194+
}

0 commit comments

Comments
 (0)