CIRCT 24.0.0git
Loading...
Searching...
No Matches
FIRRTLReductions.cpp
Go to the documentation of this file.
1//===- FIRRTLReductions.cpp - Reduction patterns for the FIRRTL dialect ---===//
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
25#include "mlir/Analysis/TopologicalSortUtils.h"
26#include "mlir/IR/Dominance.h"
27#include "mlir/IR/ImplicitLocOpBuilder.h"
28#include "mlir/IR/Matchers.h"
29#include "llvm/ADT/APSInt.h"
30#include "llvm/ADT/DenseMap.h"
31#include "llvm/ADT/SmallSet.h"
32#include "llvm/Support/Debug.h"
33
34#define DEBUG_TYPE "firrtl-reductions"
35
36using namespace mlir;
37using namespace circt;
38using namespace firrtl;
39using llvm::SmallDenseSet;
41
42//===----------------------------------------------------------------------===//
43// Utilities
44//===----------------------------------------------------------------------===//
45
46namespace detail {
47/// A utility doing lazy construction of `SymbolTable`s and `SymbolUserMap`s,
48/// which is handy for reductions that need to look up a lot of symbols.
50 SymbolCache() : tables(std::make_unique<SymbolTableCollection>()) {}
51
52 SymbolTable &getSymbolTable(Operation *op) {
53 return tables->getSymbolTable(op);
54 }
55 SymbolTable &getNearestSymbolTable(Operation *op) {
56 return getSymbolTable(SymbolTable::getNearestSymbolTable(op));
57 }
58
59 SymbolUserMap &getSymbolUserMap(Operation *op) {
60 auto it = userMaps.find(op);
61 if (it != userMaps.end())
62 return it->second;
63 return userMaps.insert({op, SymbolUserMap(*tables, op)}).first->second;
64 }
65 SymbolUserMap &getNearestSymbolUserMap(Operation *op) {
66 return getSymbolUserMap(SymbolTable::getNearestSymbolTable(op));
67 }
68
69 void clear() {
70 tables = std::make_unique<SymbolTableCollection>();
71 userMaps.clear();
72 }
73
74private:
75 std::unique_ptr<SymbolTableCollection> tables;
77};
78} // namespace detail
79
80/// Utility to easily get the instantiated firrtl::FModuleOp or an empty
81/// optional in case another type of module is instantiated.
82static std::optional<firrtl::FModuleOp>
83findInstantiatedModule(firrtl::InstanceOp instOp,
84 ::detail::SymbolCache &symbols) {
85 auto *tableOp = SymbolTable::getNearestSymbolTable(instOp);
86 auto moduleOp = dyn_cast<firrtl::FModuleOp>(
87 instOp.getReferencedOperation(symbols.getSymbolTable(tableOp)));
88 return moduleOp ? std::optional(moduleOp) : std::nullopt;
89}
90
91/// Utility to track the transitive size of modules.
93 void clear() { moduleSizes.clear(); }
94
95 uint64_t getModuleSize(Operation *module, ::detail::SymbolCache &symbols) {
96 if (auto it = moduleSizes.find(module); it != moduleSizes.end())
97 return it->second;
98 uint64_t size = 1;
99 module->walk([&](Operation *op) {
100 size += 1;
101 if (auto instOp = dyn_cast<firrtl::InstanceOp>(op))
102 if (auto instModule = findInstantiatedModule(instOp, symbols))
103 size += getModuleSize(*instModule, symbols);
104 });
105 moduleSizes.insert({module, size});
106 return size;
107 }
108
109private:
110 llvm::DenseMap<Operation *, uint64_t> moduleSizes;
111};
112
113/// Check that all connections to a value are invalids.
114static bool onlyInvalidated(Value arg) {
115 return llvm::all_of(arg.getUses(), [](OpOperand &use) {
116 auto *op = use.getOwner();
117 if (!isa<firrtl::ConnectOp, firrtl::MatchingConnectOp>(op))
118 return false;
119 if (use.getOperandNumber() != 0)
120 return false;
121 if (!op->getOperand(1).getDefiningOp<firrtl::InvalidValueOp>())
122 return false;
123 return true;
124 });
125}
126
127/// A tracker for track NLAs affected by a reduction. Performs the necessary
128/// cleanup steps in order to maintain IR validity after the reduction has
129/// applied. For example, removing an instance that forms part of an NLA path
130/// requires that NLA to be removed as well.
132 /// Clear the set of marked NLAs. Call this before attempting a reduction.
133 void clear() { nlasToRemove.clear(); }
134
135 /// Remove all marked annotations. Call this after applying a reduction in
136 /// order to validate the IR.
137 void remove(mlir::ModuleOp module) {
138 unsigned numRemoved = 0;
139 (void)numRemoved;
140 SymbolTableCollection symbolTables;
141 for (Operation &rootOp : *module.getBody()) {
142 if (!isa<firrtl::CircuitOp>(&rootOp))
143 continue;
144 SymbolUserMap symbolUserMap(symbolTables, &rootOp);
145 auto &symbolTable = symbolTables.getSymbolTable(&rootOp);
146 for (auto sym : nlasToRemove) {
147 if (auto *op = symbolTable.lookup(sym)) {
148 if (symbolUserMap.useEmpty(op)) {
149 ++numRemoved;
150 op->erase();
151 }
152 }
153 }
154 }
155 LLVM_DEBUG({
156 unsigned numLost = nlasToRemove.size() - numRemoved;
157 if (numRemoved > 0 || numLost > 0) {
158 llvm::dbgs() << "Removed " << numRemoved << " NLAs";
159 if (numLost > 0)
160 llvm::dbgs() << " (" << numLost << " no longer there)";
161 llvm::dbgs() << "\n";
162 }
163 });
164 }
165
166 /// Mark all NLAs referenced in the given annotation as to be removed. This
167 /// can be an entire array or dictionary of annotations, and the function will
168 /// descend into child annotations appropriately.
169 void markNLAsInAnnotation(Attribute anno) {
170 if (auto dict = dyn_cast<DictionaryAttr>(anno)) {
171 if (auto field = dict.getAs<FlatSymbolRefAttr>("circt.nonlocal"))
172 nlasToRemove.insert(field.getAttr());
173 for (auto namedAttr : dict)
174 markNLAsInAnnotation(namedAttr.getValue());
175 } else if (auto array = dyn_cast<ArrayAttr>(anno)) {
176 for (auto attr : array)
178 }
179 }
180
181 /// Mark all NLAs referenced in an operation. Also traverses all nested
182 /// operations. Call this before removing an operation, to mark any associated
183 /// NLAs as to be removed as well.
184 void markNLAsInOperation(Operation *op) {
185 op->walk([&](Operation *op) {
186 if (auto annos = op->getAttrOfType<ArrayAttr>("annotations"))
188 });
189 }
190
191 /// The set of NLAs to remove, identified by their symbol.
192 llvm::DenseSet<StringAttr> nlasToRemove;
193};
194
195//===----------------------------------------------------------------------===//
196// Reduction patterns
197//===----------------------------------------------------------------------===//
198
199namespace {
200
201/// A sample reduction pattern that maps `firrtl.module` to `firrtl.extmodule`.
202struct FIRRTLModuleExternalizer : public OpReduction<FModuleOp> {
203 void beforeReduction(mlir::ModuleOp op) override {
204 nlaRemover.clear();
205 symbols.clear();
206 moduleSizes.clear();
207 innerSymUses = reduce::InnerSymbolUses(op);
208 }
209 void afterReduction(mlir::ModuleOp op) override { nlaRemover.remove(op); }
210
211 uint64_t match(FModuleOp module) override {
212 if (innerSymUses.hasInnerRef(module))
213 return 0;
214 return moduleSizes.getModuleSize(module, symbols);
215 }
216
217 LogicalResult rewrite(FModuleOp module) override {
218 // Hack up a list of known layers.
219 LayerSet layers;
220 layers.insert_range(module.getLayersAttr().getAsRange<SymbolRefAttr>());
221 for (auto attr : module.getPortTypes()) {
222 auto type = cast<TypeAttr>(attr).getValue();
223 if (auto refType = type_dyn_cast<RefType>(type))
224 if (auto layer = refType.getLayer())
225 layers.insert(layer);
226 }
227 SmallVector<Attribute, 4> layersArray;
228 layersArray.reserve(layers.size());
229 for (auto layer : layers)
230 layersArray.push_back(layer);
231
232 nlaRemover.markNLAsInOperation(module);
233 OpBuilder builder(module);
234 auto extmodule =
235 FExtModuleOp::create(builder, module->getLoc(), module.getSymNameAttr(),
236 module.getConventionAttr(), module.getPorts(),
237 builder.getArrayAttr(layersArray), StringRef(),
238 module.getAnnotationsAttr());
239 SymbolTable::setSymbolVisibility(extmodule,
240 SymbolTable::getSymbolVisibility(module));
241 module->erase();
242 return success();
243 }
244
245 std::string getName() const override { return "firrtl-module-externalizer"; }
246
247 ::detail::SymbolCache symbols;
248 NLARemover nlaRemover;
249 reduce::InnerSymbolUses innerSymUses;
250 ModuleSizeCache moduleSizes;
251};
252
253/// Invalidate all the leaf fields of a value with a given flippedness by
254/// connecting an invalid value to them. This function handles different FIRRTL
255/// types appropriately:
256/// - Ref types (probes): Creates wire infrastructure with ref.send/ref.define
257/// and invalidates the underlying wire.
258/// - Bundle/Vector types: Recursively descends into elements.
259/// - Base types: Creates InvalidValueOp and connects it.
260/// - Property types: Creates UnknownValueOp and assigns it.
261///
262/// This is useful for ensuring that all output ports of an instance or memory
263/// (including those nested in bundles) are properly invalidated.
264static void invalidateOutputs(ImplicitLocOpBuilder &builder, Value value,
265 TieOffCache &tieOffCache, bool flip = false) {
266 auto type = type_dyn_cast<FIRRTLType>(value.getType());
267 if (!type)
268 return;
269
270 // Handle ref types (probes) by creating wires and defining them properly.
271 if (auto refType = type_dyn_cast<RefType>(type)) {
272 // Input probes are illegal in FIRRTL.
273 assert(!flip && "input probes are not allowed");
274
275 auto underlyingType = refType.getType();
276
277 if (!refType.getForceable()) {
278 // For probe types: create underlying wire, ref.send, ref.define, and
279 // invalidate.
280 auto targetWire = WireOp::create(builder, underlyingType);
281 auto refSend = builder.create<RefSendOp>(targetWire.getResult());
282 builder.create<RefDefineOp>(value, refSend.getResult());
283
284 // Invalidate the underlying wire.
285 auto invalid = InvalidValueOp::create(builder, underlyingType);
286 MatchingConnectOp::create(builder, targetWire.getResult(), invalid);
287 return;
288 }
289
290 // For rwprobe types: create forceable wire, ref.define, and invalidate.
291 auto forceableWire =
292 WireOp::create(builder, underlyingType,
293 /*name=*/"", NameKindEnum::DroppableName,
294 /*annotations=*/ArrayRef<Attribute>{},
295 /*innerSym=*/StringAttr{},
296 /*forceable=*/true);
297
298 // The forceable wire returns both the wire and the rwprobe.
299 auto targetWire = forceableWire.getResult();
300 auto forceableRef = forceableWire.getDataRef();
301
302 builder.create<RefDefineOp>(value, forceableRef);
303
304 // Invalidate the underlying wire.
305 auto invalid = InvalidValueOp::create(builder, underlyingType);
306 MatchingConnectOp::create(builder, targetWire, invalid);
307 return;
308 }
309
310 // Descend into bundles by creating subfield ops.
311 if (auto bundleType = type_dyn_cast<BundleType>(type)) {
312 for (auto element : llvm::enumerate(bundleType.getElements())) {
313 auto subfield = builder.createOrFold<SubfieldOp>(value, element.index());
314 invalidateOutputs(builder, subfield, tieOffCache,
315 flip ^ element.value().isFlip);
316 if (subfield.use_empty())
317 subfield.getDefiningOp()->erase();
318 }
319 return;
320 }
321
322 // Descend into vectors by creating subindex ops.
323 if (auto vectorType = type_dyn_cast<FVectorType>(type)) {
324 for (unsigned i = 0, e = vectorType.getNumElements(); i != e; ++i) {
325 auto subindex = builder.createOrFold<SubindexOp>(value, i);
326 invalidateOutputs(builder, subindex, tieOffCache, flip);
327 if (subindex.use_empty())
328 subindex.getDefiningOp()->erase();
329 }
330 return;
331 }
332
333 // Only drive outputs.
334 if (flip)
335 return;
336
337 // Create InvalidValueOp for FIRRTLBaseType.
338 if (auto baseType = type_dyn_cast<FIRRTLBaseType>(type)) {
339 auto invalid = InvalidValueOp::create(builder, baseType);
340 ConnectOp::create(builder, value, invalid);
341 return;
342 }
343
344 // For property types, use UnknownValueOp to tie off the connection.
345 if (auto propType = type_dyn_cast<PropertyType>(type)) {
346 auto unknown = tieOffCache.getUnknown(propType);
347 builder.create<PropAssignOp>(value, unknown);
348 }
349}
350
351/// Connect a value to every leave of a destination value.
352static void connectToLeafs(ImplicitLocOpBuilder &builder, Value dest,
353 Value value) {
354 auto type = dyn_cast<firrtl::FIRRTLBaseType>(dest.getType());
355 if (!type)
356 return;
357 if (auto bundleType = dyn_cast<firrtl::BundleType>(type)) {
358 for (auto element : llvm::enumerate(bundleType.getElements()))
359 connectToLeafs(builder,
360 firrtl::SubfieldOp::create(builder, dest, element.index()),
361 value);
362 return;
363 }
364 if (auto vectorType = dyn_cast<firrtl::FVectorType>(type)) {
365 for (unsigned i = 0, e = vectorType.getNumElements(); i != e; ++i)
366 connectToLeafs(builder, firrtl::SubindexOp::create(builder, dest, i),
367 value);
368 return;
369 }
370 auto valueType = dyn_cast<firrtl::FIRRTLBaseType>(value.getType());
371 if (!valueType)
372 return;
373 auto destWidth = type.getBitWidthOrSentinel();
374 auto valueWidth = valueType ? valueType.getBitWidthOrSentinel() : -1;
375 if (destWidth >= 0 && valueWidth >= 0 && destWidth < valueWidth)
376 value = firrtl::HeadPrimOp::create(builder, value, destWidth);
377 if (!isa<firrtl::UIntType>(type)) {
378 if (isa<firrtl::SIntType>(type))
379 value = firrtl::AsSIntPrimOp::create(builder, value);
380 else
381 return;
382 }
383 firrtl::ConnectOp::create(builder, dest, value);
384}
385
386/// Reduce all leaf fields of a value through an XOR tree.
387static void reduceXor(ImplicitLocOpBuilder &builder, Value &into, Value value) {
388 auto type = dyn_cast<firrtl::FIRRTLType>(value.getType());
389 if (!type)
390 return;
391 if (auto bundleType = dyn_cast<firrtl::BundleType>(type)) {
392 for (auto element : llvm::enumerate(bundleType.getElements()))
393 reduceXor(
394 builder, into,
395 builder.createOrFold<firrtl::SubfieldOp>(value, element.index()));
396 return;
397 }
398 if (auto vectorType = dyn_cast<firrtl::FVectorType>(type)) {
399 for (unsigned i = 0, e = vectorType.getNumElements(); i != e; ++i)
400 reduceXor(builder, into,
401 builder.createOrFold<firrtl::SubindexOp>(value, i));
402 return;
403 }
404 if (!isa<firrtl::UIntType>(type)) {
405 if (isa<firrtl::SIntType>(type))
406 value = firrtl::AsUIntPrimOp::create(builder, value);
407 else
408 return;
409 }
410 into = into ? builder.createOrFold<firrtl::XorPrimOp>(into, value) : value;
411}
412
413/// A sample reduction pattern that maps `firrtl.instance` to a set of
414/// invalidated wires. This often shortcuts a long iterative process of connect
415/// invalidation, module externalization, and wire stripping
416struct InstanceStubber : public OpReduction<firrtl::InstanceOp> {
417 void beforeReduction(mlir::ModuleOp op) override {
418 erasedInsts.clear();
419 erasedModules.clear();
420 symbols.clear();
421 nlaRemover.clear();
422 moduleSizes.clear();
423 }
424 void afterReduction(mlir::ModuleOp op) override {
425 // Look into deleted modules to find additional instances that are no longer
426 // instantiated anywhere.
427 SmallVector<Operation *> worklist;
428 auto deadInsts = erasedInsts;
429 for (auto *op : erasedModules)
430 worklist.push_back(op);
431 while (!worklist.empty()) {
432 auto *op = worklist.pop_back_val();
433 auto *tableOp = SymbolTable::getNearestSymbolTable(op);
434 op->walk([&](firrtl::InstanceOp instOp) {
435 auto moduleOp = cast<firrtl::FModuleLike>(
436 instOp.getReferencedOperation(symbols.getSymbolTable(tableOp)));
437 deadInsts.insert(instOp);
438 if (llvm::all_of(
439 symbols.getSymbolUserMap(tableOp).getUsers(moduleOp),
440 [&](Operation *user) { return deadInsts.contains(user); })) {
441 LLVM_DEBUG(llvm::dbgs() << "- Removing transitively unused module `"
442 << moduleOp.getModuleName() << "`\n");
443 erasedModules.insert(moduleOp);
444 worklist.push_back(moduleOp);
445 }
446 });
447 }
448
449 for (auto *op : erasedInsts)
450 op->erase();
451 for (auto *op : erasedModules)
452 op->erase();
453 nlaRemover.remove(op);
454 }
455
456 uint64_t match(firrtl::InstanceOp instOp) override {
457 if (auto fmoduleOp = findInstantiatedModule(instOp, symbols))
458 return moduleSizes.getModuleSize(*fmoduleOp, symbols);
459 return 0;
460 }
461
462 LogicalResult rewrite(firrtl::InstanceOp instOp) override {
463 LLVM_DEBUG(llvm::dbgs()
464 << "Stubbing instance `" << instOp.getName() << "`\n");
465 ImplicitLocOpBuilder builder(instOp.getLoc(), instOp);
466 TieOffCache tieOffCache(builder);
467 for (unsigned i = 0, e = instOp.getNumResults(); i != e; ++i) {
468 auto result = instOp.getResult(i);
469 auto name = builder.getStringAttr(Twine(instOp.getName()) + "_" +
470 instOp.getPortName(i));
471 auto wire =
472 firrtl::WireOp::create(builder, result.getType(), name,
473 firrtl::NameKindEnum::DroppableName,
474 instOp.getPortAnnotation(i), StringAttr{})
475 .getResult();
476 invalidateOutputs(builder, wire, tieOffCache,
477 instOp.getPortDirection(i) == firrtl::Direction::In);
478 result.replaceAllUsesWith(wire);
479 }
480 auto *tableOp = SymbolTable::getNearestSymbolTable(instOp);
481 auto moduleOp = cast<firrtl::FModuleLike>(
482 instOp.getReferencedOperation(symbols.getSymbolTable(tableOp)));
483 nlaRemover.markNLAsInOperation(instOp);
484 erasedInsts.insert(instOp);
485 if (llvm::all_of(
486 symbols.getSymbolUserMap(tableOp).getUsers(moduleOp),
487 [&](Operation *user) { return erasedInsts.contains(user); })) {
488 LLVM_DEBUG(llvm::dbgs() << "- Removing now unused module `"
489 << moduleOp.getModuleName() << "`\n");
490 erasedModules.insert(moduleOp);
491 }
492 return success();
493 }
494
495 std::string getName() const override { return "instance-stubber"; }
496 bool acceptSizeIncrease() const override { return true; }
497
498 ::detail::SymbolCache symbols;
499 NLARemover nlaRemover;
500 llvm::DenseSet<Operation *> erasedInsts;
501 llvm::DenseSet<Operation *> erasedModules;
502 ModuleSizeCache moduleSizes;
503};
504
505/// A sample reduction pattern that maps `firrtl.mem` to a set of invalidated
506/// wires.
507struct MemoryStubber : public OpReduction<firrtl::MemOp> {
508 void beforeReduction(mlir::ModuleOp op) override { nlaRemover.clear(); }
509 void afterReduction(mlir::ModuleOp op) override { nlaRemover.remove(op); }
510 LogicalResult rewrite(firrtl::MemOp memOp) override {
511 LLVM_DEBUG(llvm::dbgs() << "Stubbing memory `" << memOp.getName() << "`\n");
512 ImplicitLocOpBuilder builder(memOp.getLoc(), memOp);
513 TieOffCache tieOffCache(builder);
514 Value xorInputs;
515 SmallVector<Value> outputs;
516 for (unsigned i = 0, e = memOp.getNumResults(); i != e; ++i) {
517 auto result = memOp.getResult(i);
518 auto name = builder.getStringAttr(Twine(memOp.getName()) + "_" +
519 memOp.getPortName(i));
520 auto wire =
521 firrtl::WireOp::create(builder, result.getType(), name,
522 firrtl::NameKindEnum::DroppableName,
523 memOp.getPortAnnotation(i), StringAttr{})
524 .getResult();
525 invalidateOutputs(builder, wire, tieOffCache, true);
526 result.replaceAllUsesWith(wire);
527
528 // Isolate the input and output data fields of the port.
529 Value input, output;
530 switch (memOp.getPortKind(i)) {
531 case firrtl::MemOp::PortKind::Read:
532 output = builder.createOrFold<firrtl::SubfieldOp>(wire, 3);
533 break;
534 case firrtl::MemOp::PortKind::Write:
535 input = builder.createOrFold<firrtl::SubfieldOp>(wire, 3);
536 break;
537 case firrtl::MemOp::PortKind::ReadWrite:
538 input = builder.createOrFold<firrtl::SubfieldOp>(wire, 5);
539 output = builder.createOrFold<firrtl::SubfieldOp>(wire, 3);
540 break;
541 case firrtl::MemOp::PortKind::Debug:
542 output = wire;
543 break;
544 }
545
546 if (!isa<firrtl::RefType>(result.getType())) {
547 // Reduce all input ports to a single one through an XOR tree.
548 unsigned numFields =
549 cast<firrtl::BundleType>(wire.getType()).getNumElements();
550 for (unsigned i = 0; i != numFields; ++i) {
551 if (i != 2 && i != 3 && i != 5)
552 reduceXor(builder, xorInputs,
553 builder.createOrFold<firrtl::SubfieldOp>(wire, i));
554 }
555 if (input)
556 reduceXor(builder, xorInputs, input);
557 }
558
559 // Track the output port to hook it up to the XORd input later.
560 if (output)
561 outputs.push_back(output);
562 }
563
564 // Hook up the outputs.
565 for (auto output : outputs)
566 connectToLeafs(builder, output, xorInputs);
567
568 nlaRemover.markNLAsInOperation(memOp);
569 memOp->erase();
570 return success();
571 }
572 std::string getName() const override { return "memory-stubber"; }
573 bool acceptSizeIncrease() const override { return true; }
574 NLARemover nlaRemover;
575};
576
577/// Check whether an operation interacts with flows in any way, which can make
578/// replacement and operand forwarding harder in some cases.
579static bool isFlowSensitiveOp(Operation *op) {
580 return isa<WireOp, RegOp, RegResetOp, InstanceOp, SubfieldOp, SubindexOp,
581 SubaccessOp, ObjectSubfieldOp>(op);
582}
583
584/// A sample reduction pattern that replaces all uses of an operation with one
585/// of its operands. This can help pruning large parts of the expression tree
586/// rapidly.
587template <unsigned OpNum>
588struct FIRRTLOperandForwarder : public Reduction {
589 uint64_t match(Operation *op) override {
590 if (op->getNumResults() != 1 || OpNum >= op->getNumOperands())
591 return 0;
592 if (isFlowSensitiveOp(op))
593 return 0;
594 auto resultTy =
595 dyn_cast<firrtl::FIRRTLBaseType>(op->getResult(0).getType());
596 auto opTy =
597 dyn_cast<firrtl::FIRRTLBaseType>(op->getOperand(OpNum).getType());
598 return resultTy && opTy &&
599 resultTy.getWidthlessType() == opTy.getWidthlessType() &&
600 (resultTy.getBitWidthOrSentinel() == -1) ==
601 (opTy.getBitWidthOrSentinel() == -1) &&
602 isa<firrtl::UIntType, firrtl::SIntType>(resultTy);
603 }
604 LogicalResult rewrite(Operation *op) override {
605 assert(match(op));
606 ImplicitLocOpBuilder builder(op->getLoc(), op);
607 auto result = op->getResult(0);
608 auto operand = op->getOperand(OpNum);
609 auto resultTy = cast<firrtl::FIRRTLBaseType>(result.getType());
610 auto operandTy = cast<firrtl::FIRRTLBaseType>(operand.getType());
611 auto resultWidth = resultTy.getBitWidthOrSentinel();
612 auto operandWidth = operandTy.getBitWidthOrSentinel();
613 Value newOp;
614 if (resultWidth < operandWidth)
615 newOp =
616 builder.createOrFold<firrtl::BitsPrimOp>(operand, resultWidth - 1, 0);
617 else if (resultWidth > operandWidth)
618 newOp = builder.createOrFold<firrtl::PadPrimOp>(operand, resultWidth);
619 else
620 newOp = operand;
621 LLVM_DEBUG(llvm::dbgs() << "Forwarding " << newOp << " in " << *op << "\n");
622 result.replaceAllUsesWith(newOp);
623 reduce::pruneUnusedOps(op, *this);
624 return success();
625 }
626 std::string getName() const override {
627 return ("firrtl-operand" + Twine(OpNum) + "-forwarder").str();
628 }
629};
630
631/// A sample reduction pattern that replaces FIRRTL operations with a constant
632/// zero of their type.
633struct Constantifier : public Reduction {
634 void beforeReduction(mlir::ModuleOp op) override {
635 symbols.clear();
636
637 // Find valid dummy classes that we can use for anyref casts.
638 anyrefCastDummy.clear();
639 op.walk<WalkOrder::PreOrder>([&](CircuitOp circuitOp) {
640 for (auto classOp : circuitOp.getOps<ClassOp>()) {
641 if (classOp.getArguments().empty() && classOp.getBodyBlock()->empty()) {
642 anyrefCastDummy.insert({circuitOp, classOp});
643 anyrefCastDummyNames[circuitOp].insert(classOp.getNameAttr());
644 }
645 }
646 return WalkResult::skip();
647 });
648 }
649
650 uint64_t match(Operation *op) override {
651 if (op->hasTrait<OpTrait::ConstantLike>()) {
652 Attribute attr;
653 if (!matchPattern(op, m_Constant(&attr)))
654 return 0;
655 if (auto intAttr = dyn_cast<IntegerAttr>(attr))
656 if (intAttr.getValue().isZero())
657 return 0;
658 if (auto strAttr = dyn_cast<StringAttr>(attr))
659 if (strAttr.empty())
660 return 0;
661 if (auto floatAttr = dyn_cast<FloatAttr>(attr))
662 if (floatAttr.getValue().isZero())
663 return 0;
664 }
665 if (auto listOp = dyn_cast<ListCreateOp>(op))
666 if (listOp.getElements().empty())
667 return 0;
668 if (auto pathOp = dyn_cast<UnresolvedPathOp>(op))
669 if (pathOp.getTarget().empty())
670 return 0;
671
672 // Don't replace anyref casts that already target a dummy class.
673 if (auto anyrefCastOp = dyn_cast<ObjectAnyRefCastOp>(op)) {
674 auto circuitOp = anyrefCastOp->getParentOfType<CircuitOp>();
675 auto className =
676 anyrefCastOp.getInput().getType().getNameAttr().getAttr();
677 if (anyrefCastDummyNames[circuitOp].contains(className))
678 return 0;
679 }
680
681 if (op->getNumResults() != 1)
682 return 0;
683 if (op->hasAttr("inner_sym"))
684 return 0;
685 if (isFlowSensitiveOp(op))
686 return 0;
687 return isa<UIntType, SIntType, StringType, FIntegerType, BoolType,
688 DoubleType, ListType, PathType, AnyRefType>(
689 op->getResult(0).getType());
690 }
691
692 LogicalResult rewrite(Operation *op) override {
693 OpBuilder builder(op);
694 auto type = op->getResult(0).getType();
695
696 // Handle UInt/SInt types.
697 if (isa<UIntType, SIntType>(type)) {
698 auto width = cast<FIRRTLBaseType>(type).getBitWidthOrSentinel();
699 if (width == -1)
700 width = 64;
701 auto newOp = ConstantOp::create(builder, op->getLoc(), type,
702 APSInt(width, isa<UIntType>(type)));
703 op->replaceAllUsesWith(newOp);
704 reduce::pruneUnusedOps(op, *this);
705 return success();
706 }
707
708 // Handle property string types.
709 if (isa<StringType>(type)) {
710 auto attr = builder.getStringAttr("");
711 auto newOp = StringConstantOp::create(builder, op->getLoc(), attr);
712 op->replaceAllUsesWith(newOp);
713 reduce::pruneUnusedOps(op, *this);
714 return success();
715 }
716
717 // Handle property integer types.
718 if (isa<FIntegerType>(type)) {
719 auto attr = builder.getIntegerAttr(builder.getIntegerType(64, true), 0);
720 auto newOp = FIntegerConstantOp::create(builder, op->getLoc(), attr);
721 op->replaceAllUsesWith(newOp);
722 reduce::pruneUnusedOps(op, *this);
723 return success();
724 }
725
726 // Handle property boolean types.
727 if (isa<BoolType>(type)) {
728 auto attr = builder.getBoolAttr(false);
729 auto newOp = BoolConstantOp::create(builder, op->getLoc(), attr);
730 op->replaceAllUsesWith(newOp);
731 reduce::pruneUnusedOps(op, *this);
732 return success();
733 }
734
735 // Handle property double types.
736 if (isa<DoubleType>(type)) {
737 auto attr = builder.getFloatAttr(builder.getF64Type(), 0.0);
738 auto newOp = DoubleConstantOp::create(builder, op->getLoc(), attr);
739 op->replaceAllUsesWith(newOp);
740 reduce::pruneUnusedOps(op, *this);
741 return success();
742 }
743
744 // Handle property list types.
745 if (isa<ListType>(type)) {
746 auto newOp =
747 ListCreateOp::create(builder, op->getLoc(), type, ValueRange{});
748 op->replaceAllUsesWith(newOp);
749 reduce::pruneUnusedOps(op, *this);
750 return success();
751 }
752
753 // Handle property path types.
754 if (isa<PathType>(type)) {
755 auto newOp = UnresolvedPathOp::create(builder, op->getLoc(), "");
756 op->replaceAllUsesWith(newOp);
757 reduce::pruneUnusedOps(op, *this);
758 return success();
759 }
760
761 // Handle anyref types.
762 if (isa<AnyRefType>(type)) {
763 auto circuitOp = op->getParentOfType<CircuitOp>();
764 auto &dummy = anyrefCastDummy[circuitOp];
765 if (!dummy) {
766 OpBuilder::InsertionGuard guard(builder);
767 builder.setInsertionPointToStart(circuitOp.getBodyBlock());
768 auto &symbolTable = symbols.getNearestSymbolTable(op);
769 dummy = ClassOp::create(builder, op->getLoc(), "Dummy", {}, {});
770 symbolTable.insert(dummy);
771 anyrefCastDummyNames[circuitOp].insert(dummy.getNameAttr());
772 }
773 auto objectOp = ObjectOp::create(builder, op->getLoc(), dummy, "dummy");
774 auto anyrefOp =
775 ObjectAnyRefCastOp::create(builder, op->getLoc(), objectOp);
776 op->replaceAllUsesWith(anyrefOp);
777 reduce::pruneUnusedOps(op, *this);
778 return success();
779 }
780
781 return failure();
782 }
783
784 std::string getName() const override { return "firrtl-constantifier"; }
785 bool acceptSizeIncrease() const override { return true; }
786
787 ::detail::SymbolCache symbols;
788 SmallDenseMap<CircuitOp, ClassOp, 2> anyrefCastDummy;
789 SmallDenseMap<CircuitOp, DenseSet<StringAttr>, 2> anyrefCastDummyNames;
790};
791
792/// A sample reduction pattern that replaces the right-hand-side of
793/// `firrtl.connect` and `firrtl.matchingconnect` operations with a
794/// `firrtl.invalidvalue`. This removes uses from the fanin cone to these
795/// connects and creates opportunities for reduction in DCE/CSE.
796struct ConnectInvalidator : public Reduction {
797 uint64_t match(Operation *op) override {
798 if (!isa<FConnectLike>(op))
799 return 0;
800 if (auto *srcOp = op->getOperand(1).getDefiningOp())
801 if (srcOp->hasTrait<OpTrait::ConstantLike>() ||
802 isa<InvalidValueOp>(srcOp))
803 return 0;
804 auto type = dyn_cast<FIRRTLBaseType>(op->getOperand(1).getType());
805 return type && type.isPassive();
806 }
807 LogicalResult rewrite(Operation *op) override {
808 assert(match(op));
809 auto rhs = op->getOperand(1);
810 OpBuilder builder(op);
811 auto invOp = InvalidValueOp::create(builder, rhs.getLoc(), rhs.getType());
812 auto *rhsOp = rhs.getDefiningOp();
813 op->setOperand(1, invOp);
814 if (rhsOp)
815 reduce::pruneUnusedOps(rhsOp, *this);
816 return success();
817 }
818 std::string getName() const override { return "connect-invalidator"; }
819 bool acceptSizeIncrease() const override { return true; }
820};
821
822/// A reduction pattern that removes FIRRTL annotations from ports and
823/// operations. This generates one match per annotation and port annotation,
824/// allowing selective removal of individual annotations.
825struct AnnotationRemover : public Reduction {
826 void beforeReduction(mlir::ModuleOp op) override { nlaRemover.clear(); }
827 void afterReduction(mlir::ModuleOp op) override { nlaRemover.remove(op); }
828
829 void matches(Operation *op,
830 llvm::function_ref<void(uint64_t, uint64_t)> addMatch) override {
831 uint64_t matchId = 0;
832
833 // Generate matches for regular annotations
834 if (auto annos = op->getAttrOfType<ArrayAttr>("annotations"))
835 for (unsigned i = 0; i < annos.size(); ++i)
836 addMatch(1, matchId++);
837
838 // Generate matches for port annotations
839 if (auto portAnnos = op->getAttrOfType<ArrayAttr>("portAnnotations"))
840 for (auto portAnnoArray : portAnnos)
841 if (auto portAnnoArrayAttr = dyn_cast<ArrayAttr>(portAnnoArray))
842 for (unsigned i = 0; i < portAnnoArrayAttr.size(); ++i)
843 addMatch(1, matchId++);
844 }
845
846 LogicalResult rewriteMatches(Operation *op,
847 ArrayRef<uint64_t> matches) override {
848 // Convert matches to a set for fast lookup
849 llvm::SmallDenseSet<uint64_t, 4> matchesSet(matches.begin(), matches.end());
850
851 // Lambda to process annotations and filter out matched ones
852 uint64_t matchId = 0;
853 auto processAnnotations =
854 [&](ArrayRef<Attribute> annotations) -> ArrayAttr {
855 SmallVector<Attribute> newAnnotations;
856 for (auto anno : annotations) {
857 if (!matchesSet.contains(matchId)) {
858 newAnnotations.push_back(anno);
859 } else {
860 // Mark NLAs in the removed annotation for cleanup
861 nlaRemover.markNLAsInAnnotation(anno);
862 }
863 matchId++;
864 }
865 return ArrayAttr::get(op->getContext(), newAnnotations);
866 };
867
868 // Remove regular annotations
869 if (auto annos = op->getAttrOfType<ArrayAttr>("annotations")) {
870 op->setAttr("annotations", processAnnotations(annos.getValue()));
871 }
872
873 // Remove port annotations
874 if (auto portAnnos = op->getAttrOfType<ArrayAttr>("portAnnotations")) {
875 SmallVector<Attribute> newPortAnnos;
876 for (auto portAnnoArrayAttr : portAnnos.getAsRange<ArrayAttr>()) {
877 newPortAnnos.push_back(
878 processAnnotations(portAnnoArrayAttr.getValue()));
879 }
880 op->setAttr("portAnnotations",
881 ArrayAttr::get(op->getContext(), newPortAnnos));
882 }
883
884 return success();
885 }
886
887 std::string getName() const override { return "annotation-remover"; }
888 NLARemover nlaRemover;
889};
890
891/// A reduction pattern that replaces ResetType with UInt<1> across an entire
892/// circuit. This walks all operations in the circuit and replaces ResetType in
893/// results, block arguments, and attributes.
894struct SimplifyResets : public OpReduction<CircuitOp> {
895 uint64_t match(CircuitOp circuit) override {
896 uint64_t numResets = 0;
897 AttrTypeWalker walker;
898 walker.addWalk([&](ResetType type) { ++numResets; });
899
900 circuit.walk([&](Operation *op) {
901 for (auto result : op->getResults())
902 walker.walk(result.getType());
903
904 for (auto &region : op->getRegions())
905 for (auto &block : region)
906 for (auto arg : block.getArguments())
907 walker.walk(arg.getType());
908
909 walker.walk(op->getAttrDictionary());
910 });
911
912 return numResets;
913 }
914
915 LogicalResult rewrite(CircuitOp circuit) override {
916 auto uint1Type = UIntType::get(circuit->getContext(), 1, false);
917 auto constUint1Type = UIntType::get(circuit->getContext(), 1, true);
918
919 AttrTypeReplacer replacer;
920 replacer.addReplacement([&](ResetType type) {
921 return type.isConst() ? constUint1Type : uint1Type;
922 });
923 replacer.recursivelyReplaceElementsIn(circuit, /*replaceAttrs=*/true,
924 /*replaceLocs=*/false,
925 /*replaceTypes=*/true);
926
927 // Remove annotations related to InferResets pass
928 circuit.walk([&](Operation *op) {
929 // Remove operation annotations
930 AnnotationSet::removeAnnotations(op, [&](Annotation anno) {
931 return anno.isClass(fullResetAnnoClass, excludeFromFullResetAnnoClass,
932 fullAsyncResetAnnoClass,
933 ignoreFullAsyncResetAnnoClass);
934 });
935
936 // Remove port annotations for module-like operations
937 if (auto module = dyn_cast<FModuleLike>(op)) {
938 AnnotationSet::removePortAnnotations(module, [&](unsigned portIdx,
939 Annotation anno) {
940 return anno.isClass(fullResetAnnoClass, excludeFromFullResetAnnoClass,
941 fullAsyncResetAnnoClass,
942 ignoreFullAsyncResetAnnoClass);
943 });
944 }
945 });
946
947 return success();
948 }
949
950 std::string getName() const override { return "firrtl-simplify-resets"; }
951 bool acceptSizeIncrease() const override { return true; }
952};
953
954/// A sample reduction pattern that removes ports from the root `firrtl.module`
955/// if the port is not used or just invalidated.
956struct RootPortPruner : public OpReduction<firrtl::FModuleOp> {
957 void matches(firrtl::FModuleOp module,
958 llvm::function_ref<void(uint64_t, uint64_t)> addMatch) override {
959 auto circuit = module->getParentOfType<firrtl::CircuitOp>();
960 if (!circuit || circuit.getNameAttr() != module.getNameAttr())
961 return;
962
963 // Generate one match per port that can be removed
964 size_t numPorts = module.getNumPorts();
965 for (unsigned i = 0; i != numPorts; ++i) {
966 if (onlyInvalidated(module.getArgument(i)))
967 addMatch(1, i);
968 }
969 }
970
971 LogicalResult rewriteMatches(firrtl::FModuleOp module,
972 ArrayRef<uint64_t> matches) override {
973 // Build a BitVector of ports to remove
974 llvm::BitVector dropPorts(module.getNumPorts());
975 for (auto portIdx : matches)
976 dropPorts.set(portIdx);
977
978 // Erase users of the ports being removed
979 for (auto portIdx : matches) {
980 for (auto *user :
981 llvm::make_early_inc_range(module.getArgument(portIdx).getUsers()))
982 user->erase();
983 }
984
985 // Remove the ports from the module
986 module.erasePorts(dropPorts);
987 return success();
988 }
989
990 std::string getName() const override { return "root-port-pruner"; }
991};
992
993/// A reduction pattern that removes all ports from the root `firrtl.extmodule`.
994/// Since extmodules have no body, all ports can be safely removed for reduction
995/// purposes.
996struct RootExtmodulePortPruner : public OpReduction<firrtl::FExtModuleOp> {
997 void matches(firrtl::FExtModuleOp module,
998 llvm::function_ref<void(uint64_t, uint64_t)> addMatch) override {
999 auto circuit = module->getParentOfType<firrtl::CircuitOp>();
1000 if (!circuit || circuit.getNameAttr() != module.getNameAttr())
1001 return;
1002
1003 // Generate one match per port (all ports can be removed from root
1004 // extmodule)
1005 size_t numPorts = module.getNumPorts();
1006 for (unsigned i = 0; i != numPorts; ++i)
1007 addMatch(1, i);
1008 }
1009
1010 LogicalResult rewriteMatches(firrtl::FExtModuleOp module,
1011 ArrayRef<uint64_t> matches) override {
1012 if (matches.empty())
1013 return failure();
1014
1015 // Build a BitVector of ports to remove
1016 llvm::BitVector dropPorts(module.getNumPorts());
1017 for (auto portIdx : matches)
1018 dropPorts.set(portIdx);
1019
1020 // Remove the ports from the module
1021 module.erasePorts(dropPorts);
1022 return success();
1023 }
1024
1025 std::string getName() const override { return "root-extmodule-port-pruner"; }
1026};
1027
1028/// A sample reduction pattern that replaces instances of `firrtl.extmodule`
1029/// with wires.
1030struct ExtmoduleInstanceRemover : public OpReduction<firrtl::InstanceOp> {
1031 void beforeReduction(mlir::ModuleOp op) override {
1032 symbols.clear();
1033 nlaRemover.clear();
1034 }
1035 void afterReduction(mlir::ModuleOp op) override { nlaRemover.remove(op); }
1036
1037 uint64_t match(firrtl::InstanceOp instOp) override {
1038 return isa<firrtl::FExtModuleOp>(
1039 instOp.getReferencedOperation(symbols.getNearestSymbolTable(instOp)));
1040 }
1041 LogicalResult rewrite(firrtl::InstanceOp instOp) override {
1042 auto portInfo =
1043 cast<firrtl::FModuleLike>(instOp.getReferencedOperation(
1044 symbols.getNearestSymbolTable(instOp)))
1045 .getPorts();
1046 ImplicitLocOpBuilder builder(instOp.getLoc(), instOp);
1047 TieOffCache tieOffCache(builder);
1048 SmallVector<Value> replacementWires;
1049 for (firrtl::PortInfo info : portInfo) {
1050 auto wire = firrtl::WireOp::create(
1051 builder, info.type,
1052 (Twine(instOp.getName()) + "_" + info.getName()).str())
1053 .getResult();
1054 if (info.isOutput()) {
1055 // Tie off output ports using TieOffCache.
1056 if (auto baseType = dyn_cast<firrtl::FIRRTLBaseType>(info.type)) {
1057 auto inv = InvalidValueOp::create(builder, baseType);
1058 firrtl::ConnectOp::create(builder, wire, inv);
1059 } else if (auto propType = dyn_cast<firrtl::PropertyType>(info.type)) {
1060 auto unknown = tieOffCache.getUnknown(propType);
1061 builder.create<firrtl::PropAssignOp>(wire, unknown);
1062 }
1063 }
1064 replacementWires.push_back(wire);
1065 }
1066 nlaRemover.markNLAsInOperation(instOp);
1067 instOp.replaceAllUsesWith(std::move(replacementWires));
1068 instOp->erase();
1069 return success();
1070 }
1071 std::string getName() const override { return "extmodule-instance-remover"; }
1072 bool acceptSizeIncrease() const override { return true; }
1073
1074 ::detail::SymbolCache symbols;
1075 NLARemover nlaRemover;
1076};
1077
1078/// A reduction pattern that removes unused ports from extmodules and regular
1079/// modules. This is particularly useful for reducing test cases with many probe
1080/// ports or other unused ports.
1081///
1082/// Shared helper functions for port pruning reductions.
1083struct PortPrunerHelpers {
1084 /// Compute which ports are unused across all instances of a module.
1085 template <typename ModuleOpType>
1086 static void computeUnusedInstancePorts(ModuleOpType module,
1087 ArrayRef<Operation *> users,
1088 llvm::BitVector &portsToRemove) {
1089 auto ports = module.getPorts();
1090 for (size_t portIdx = 0; portIdx < ports.size(); ++portIdx) {
1091 bool portUsed = false;
1092 for (auto *user : users) {
1093 if (auto instOp = dyn_cast<firrtl::InstanceOp>(user)) {
1094 auto result = instOp.getResult(portIdx);
1095 if (!result.use_empty()) {
1096 portUsed = true;
1097 break;
1098 }
1099 }
1100 }
1101 if (!portUsed)
1102 portsToRemove.set(portIdx);
1103 }
1104 }
1105
1106 /// Update all instances of a module to remove the specified ports.
1107 static void
1108 updateInstancesAndErasePorts(Operation *module, ArrayRef<Operation *> users,
1109 const llvm::BitVector &portsToRemove) {
1110 // Update all instances to remove the corresponding results
1111 SmallVector<firrtl::InstanceOp> instancesToUpdate;
1112 for (auto *user : users) {
1113 if (auto instOp = dyn_cast<firrtl::InstanceOp>(user))
1114 instancesToUpdate.push_back(instOp);
1115 }
1116
1117 for (auto instOp : instancesToUpdate) {
1118 auto newInst = instOp.cloneWithErasedPorts(portsToRemove);
1119
1120 // Manually replace uses, skipping erased ports
1121 size_t newResultIdx = 0;
1122 for (size_t oldResultIdx = 0; oldResultIdx < instOp.getNumResults();
1123 ++oldResultIdx) {
1124 if (portsToRemove[oldResultIdx]) {
1125 // This port is being removed, assert it has no uses
1126 assert(instOp.getResult(oldResultIdx).use_empty() &&
1127 "removing port with uses");
1128 } else {
1129 // Replace uses of the old result with the new result
1130 instOp.getResult(oldResultIdx)
1131 .replaceAllUsesWith(newInst->getResult(newResultIdx));
1132 ++newResultIdx;
1133 }
1134 }
1135
1136 instOp->erase();
1137 }
1138 }
1139};
1140
1141/// Reduction to remove unused ports from regular modules.
1142struct ModulePortPruner : public OpReduction<firrtl::FModuleOp> {
1143 void beforeReduction(mlir::ModuleOp op) override {
1144 symbols.clear();
1145 nlaRemover.clear();
1146 }
1147 void afterReduction(mlir::ModuleOp op) override { nlaRemover.remove(op); }
1148
1149 void matches(firrtl::FModuleOp module,
1150 llvm::function_ref<void(uint64_t, uint64_t)> addMatch) override {
1151 auto *tableOp = SymbolTable::getNearestSymbolTable(module);
1152 auto &userMap = symbols.getSymbolUserMap(tableOp);
1153 auto ports = module.getPorts();
1154 auto users = userMap.getUsers(module);
1155
1156 // Compute which ports can be removed. A port can only be removed if it
1157 // is unused in both the module body and across all instances.
1158 llvm::BitVector portsToRemove(ports.size());
1159
1160 // Check if ports are unused across all instances.
1161 if (!users.empty())
1162 PortPrunerHelpers::computeUnusedInstancePorts(module, users,
1163 portsToRemove);
1164 else
1165 // If there are no instances, all ports are candidates for removal.
1166 portsToRemove.set();
1167
1168 // Additionally check if ports are unused within the module body itself.
1169 // A port must be unused in both instances and the module body to be
1170 // removable.
1171 for (size_t portIdx = 0; portIdx < ports.size(); ++portIdx) {
1172 if (!portsToRemove[portIdx])
1173 continue;
1174 if (!module.getArgument(portIdx).use_empty())
1175 portsToRemove.reset(portIdx);
1176 }
1177
1178 // Generate one match per removable port.
1179 for (size_t portIdx = 0; portIdx < ports.size(); ++portIdx)
1180 if (portsToRemove[portIdx])
1181 addMatch(1, portIdx);
1182 }
1183
1184 LogicalResult rewriteMatches(firrtl::FModuleOp module,
1185 ArrayRef<uint64_t> matches) override {
1186 if (matches.empty())
1187 return failure();
1188
1189 // Build a BitVector of ports to remove
1190 llvm::BitVector portsToRemove(module.getNumPorts());
1191 for (auto portIdx : matches)
1192 portsToRemove.set(portIdx);
1193
1194 // Get users for updating instances
1195 auto *tableOp = SymbolTable::getNearestSymbolTable(module);
1196 auto &userMap = symbols.getSymbolUserMap(tableOp);
1197 auto users = userMap.getUsers(module);
1198
1199 // Update all instances
1200 PortPrunerHelpers::updateInstancesAndErasePorts(module, users,
1201 portsToRemove);
1202
1203 // Remove the ports from the module. We don't need to erase users because
1204 // matches() already ensured that these ports have no users.
1205 module.erasePorts(portsToRemove);
1206
1207 return success();
1208 }
1209
1210 std::string getName() const override { return "module-port-pruner"; }
1211
1212 ::detail::SymbolCache symbols;
1213 NLARemover nlaRemover;
1214};
1215
1216/// Reduction to remove unused ports from extmodules.
1217struct ExtmodulePortPruner : public OpReduction<firrtl::FExtModuleOp> {
1218 void beforeReduction(mlir::ModuleOp op) override {
1219 symbols.clear();
1220 nlaRemover.clear();
1221 }
1222 void afterReduction(mlir::ModuleOp op) override { nlaRemover.remove(op); }
1223
1224 void matches(firrtl::FExtModuleOp module,
1225 llvm::function_ref<void(uint64_t, uint64_t)> addMatch) override {
1226 auto *tableOp = SymbolTable::getNearestSymbolTable(module);
1227 auto &userMap = symbols.getSymbolUserMap(tableOp);
1228 auto ports = module.getPorts();
1229 auto users = userMap.getUsers(module);
1230
1231 // Compute which ports can be removed
1232 llvm::BitVector portsToRemove(ports.size());
1233
1234 if (users.empty()) {
1235 // If the extmodule has no instances, aggressively remove all ports
1236 portsToRemove.set();
1237 } else {
1238 // For extmodules with instances, check if ports are unused across all
1239 // instances
1240 PortPrunerHelpers::computeUnusedInstancePorts(module, users,
1241 portsToRemove);
1242 }
1243
1244 // Generate one match per removable port
1245 for (size_t portIdx = 0; portIdx < ports.size(); ++portIdx)
1246 if (portsToRemove[portIdx])
1247 addMatch(1, portIdx);
1248 }
1249
1250 LogicalResult rewriteMatches(firrtl::FExtModuleOp module,
1251 ArrayRef<uint64_t> matches) override {
1252 if (matches.empty())
1253 return failure();
1254
1255 // Build a BitVector of ports to remove
1256 llvm::BitVector portsToRemove(module.getNumPorts());
1257 for (auto portIdx : matches)
1258 portsToRemove.set(portIdx);
1259
1260 // Get users for updating instances
1261 auto *tableOp = SymbolTable::getNearestSymbolTable(module);
1262 auto &userMap = symbols.getSymbolUserMap(tableOp);
1263 auto users = userMap.getUsers(module);
1264
1265 // Update all instances.
1266 PortPrunerHelpers::updateInstancesAndErasePorts(module, users,
1267 portsToRemove);
1268
1269 // Remove the ports from the module (no body to clean up for extmodules).
1270 module.erasePorts(portsToRemove);
1271
1272 return success();
1273 }
1274
1275 std::string getName() const override { return "extmodule-port-pruner"; }
1276
1277 ::detail::SymbolCache symbols;
1278 NLARemover nlaRemover;
1279};
1280
1281/// A sample reduction pattern that pushes connected values through wires.
1282struct ConnectForwarder : public Reduction {
1283 void beforeReduction(mlir::ModuleOp op) override {
1284 domInfo = std::make_unique<DominanceInfo>(op);
1285 }
1286
1287 uint64_t match(Operation *op) override {
1288 if (!isa<firrtl::FConnectLike>(op))
1289 return 0;
1290 auto dest = op->getOperand(0);
1291 auto src = op->getOperand(1);
1292 auto *destOp = dest.getDefiningOp();
1293 auto *srcOp = src.getDefiningOp();
1294 if (dest == src)
1295 return 0;
1296
1297 // Ensure that the destination is something we should be able to forward
1298 // through.
1299 if (!isa_and_nonnull<firrtl::WireOp, firrtl::RegOp, firrtl::RegResetOp>(
1300 destOp))
1301 return 0;
1302
1303 // Ensure that the destination is connected to only once, and all uses of
1304 // the connection occur after the definition of the source.
1305 unsigned numConnects = 0;
1306 for (auto &use : dest.getUses()) {
1307 auto *op = use.getOwner();
1308 if (use.getOperandNumber() == 0 && isa<firrtl::FConnectLike>(op)) {
1309 if (++numConnects > 1)
1310 return 0;
1311 continue;
1312 }
1313 // Check if srcOp properly dominates op, but op is not enclosed in srcOp.
1314 // This handles cross-block cases (e.g., layerblocks).
1315 if (srcOp &&
1316 !domInfo->properlyDominates(srcOp, op, /*enclosingOpOk=*/false))
1317 return 0;
1318 }
1319
1320 return 1;
1321 }
1322
1323 LogicalResult rewrite(Operation *op) override {
1324 auto dst = op->getOperand(0);
1325 auto src = op->getOperand(1);
1326 dst.replaceAllUsesExcept(src, op);
1327 op->erase();
1328 SmallVector<Operation *> worklist(
1329 {dst.getDefiningOp(), src.getDefiningOp()});
1330 reduce::pruneUnusedOps(worklist, *this);
1331 return success();
1332 }
1333
1334 std::string getName() const override { return "connect-forwarder"; }
1335
1336private:
1337 std::unique_ptr<DominanceInfo> domInfo;
1338};
1339
1340/// A sample reduction pattern that replaces a single-use wire and register with
1341/// an operand of the source value of the connection.
1342template <unsigned OpNum>
1343struct ConnectSourceOperandForwarder : public Reduction {
1344 uint64_t match(Operation *op) override {
1345 if (!isa<firrtl::ConnectOp, firrtl::MatchingConnectOp>(op))
1346 return 0;
1347 auto dest = op->getOperand(0);
1348 auto *destOp = dest.getDefiningOp();
1349
1350 // Ensure that the destination is used only once.
1351 if (!destOp || !destOp->hasOneUse() ||
1352 !isa<firrtl::WireOp, firrtl::RegOp, firrtl::RegResetOp>(destOp))
1353 return 0;
1354
1355 auto *srcOp = op->getOperand(1).getDefiningOp();
1356 if (!srcOp || OpNum >= srcOp->getNumOperands())
1357 return 0;
1358
1359 auto resultTy = dyn_cast<firrtl::FIRRTLBaseType>(dest.getType());
1360 auto opTy =
1361 dyn_cast<firrtl::FIRRTLBaseType>(srcOp->getOperand(OpNum).getType());
1362
1363 return resultTy && opTy &&
1364 resultTy.getWidthlessType() == opTy.getWidthlessType() &&
1365 ((resultTy.getBitWidthOrSentinel() == -1) ==
1366 (opTy.getBitWidthOrSentinel() == -1)) &&
1367 isa<firrtl::UIntType, firrtl::SIntType>(resultTy);
1368 }
1369
1370 LogicalResult rewrite(Operation *op) override {
1371 auto *destOp = op->getOperand(0).getDefiningOp();
1372 auto *srcOp = op->getOperand(1).getDefiningOp();
1373 auto forwardedOperand = srcOp->getOperand(OpNum);
1374 ImplicitLocOpBuilder builder(destOp->getLoc(), destOp);
1375 Value newDest;
1376 if (auto wire = dyn_cast<firrtl::WireOp>(destOp))
1377 newDest = firrtl::WireOp::create(builder, forwardedOperand.getType(),
1378 wire.getName())
1379 .getResult();
1380 else {
1381 auto regName = destOp->getAttrOfType<StringAttr>("name");
1382 // We can promote the register into a wire but we wouldn't do here because
1383 // the error might be caused by the register.
1384 auto clock = destOp->getOperand(0);
1385 newDest = firrtl::RegOp::create(builder, forwardedOperand.getType(),
1386 clock, regName ? regName.str() : "")
1387 .getResult();
1388 }
1389
1390 // Create new connection between a new wire and the forwarded operand.
1391 builder.setInsertionPointAfter(op);
1392 if (isa<firrtl::ConnectOp>(op))
1393 firrtl::ConnectOp::create(builder, newDest, forwardedOperand);
1394 else
1395 firrtl::MatchingConnectOp::create(builder, newDest, forwardedOperand);
1396
1397 // Remove the old connection and destination. We don't have to replace them
1398 // because destination has only one use.
1399 op->erase();
1400 destOp->erase();
1401 reduce::pruneUnusedOps(srcOp, *this);
1402
1403 return success();
1404 }
1405 std::string getName() const override {
1406 return ("connect-source-operand-" + Twine(OpNum) + "-forwarder").str();
1407 }
1408};
1409
1410/// A sample reduction pattern that tries to remove aggregate wires by replacing
1411/// all subaccesses with new independent wires. This can disentangle large
1412/// unused wires that are otherwise difficult to collect due to the subaccesses.
1413struct DetachSubaccesses : public Reduction {
1414 void beforeReduction(mlir::ModuleOp op) override { opsToErase.clear(); }
1415 void afterReduction(mlir::ModuleOp op) override {
1416 for (auto *op : opsToErase)
1417 op->dropAllReferences();
1418 for (auto *op : opsToErase)
1419 op->erase();
1420 }
1421 uint64_t match(Operation *op) override {
1422 // Only applies to wires and registers that are purely used in subaccess
1423 // operations.
1424 return isa<firrtl::WireOp, firrtl::RegOp, firrtl::RegResetOp>(op) &&
1425 llvm::all_of(op->getUses(), [](auto &use) {
1426 return use.getOperandNumber() == 0 &&
1427 isa<firrtl::SubfieldOp, firrtl::SubindexOp,
1428 firrtl::SubaccessOp>(use.getOwner());
1429 });
1430 }
1431 LogicalResult rewrite(Operation *op) override {
1432 assert(match(op));
1433 OpBuilder builder(op);
1434 bool isWire = isa<firrtl::WireOp>(op);
1435 Value invalidClock;
1436 if (!isWire)
1437 invalidClock = firrtl::InvalidValueOp::create(
1438 builder, op->getLoc(), firrtl::ClockType::get(op->getContext()));
1439 for (Operation *user : llvm::make_early_inc_range(op->getUsers())) {
1440 builder.setInsertionPoint(user);
1441 auto type = user->getResult(0).getType();
1442 Operation *replOp;
1443 if (isWire)
1444 replOp = firrtl::WireOp::create(builder, user->getLoc(), type);
1445 else
1446 replOp =
1447 firrtl::RegOp::create(builder, user->getLoc(), type, invalidClock);
1448 user->replaceAllUsesWith(replOp);
1449 opsToErase.insert(user);
1450 }
1451 opsToErase.insert(op);
1452 return success();
1453 }
1454 std::string getName() const override { return "detach-subaccesses"; }
1455 llvm::DenseSet<Operation *> opsToErase;
1456};
1457
1458/// This reduction removes inner symbols on ops. Name preservation creates a lot
1459/// of node ops with symbols to keep name information but it also prevents
1460/// normal canonicalizations.
1461struct NodeSymbolRemover : public Reduction {
1462 void beforeReduction(mlir::ModuleOp op) override {
1463 innerSymUses = reduce::InnerSymbolUses(op);
1464 }
1465
1466 uint64_t match(Operation *op) override {
1467 // Only match ops with an inner symbol.
1468 auto sym = op->getAttrOfType<hw::InnerSymAttr>("inner_sym");
1469 if (!sym || sym.empty())
1470 return 0;
1471
1472 // Only match ops that have no references to their inner symbol.
1473 if (innerSymUses.hasInnerRef(op))
1474 return 0;
1475 return 1;
1476 }
1477
1478 LogicalResult rewrite(Operation *op) override {
1479 op->removeAttr("inner_sym");
1480 return success();
1481 }
1482
1483 std::string getName() const override { return "node-symbol-remover"; }
1484 bool acceptSizeIncrease() const override { return true; }
1485
1486 reduce::InnerSymbolUses innerSymUses;
1487};
1488
1489/// Check if inlining the referenced operation into the parent operation would
1490/// cause inner symbol collisions.
1491static bool
1492hasInnerSymbolCollision(Operation *referencedOp, Operation *parentOp,
1493 hw::InnerSymbolTableCollection &innerSymTables) {
1494 // Get the inner symbol tables for both operations
1495 auto &targetTable = innerSymTables.getInnerSymbolTable(referencedOp);
1496 auto &parentTable = innerSymTables.getInnerSymbolTable(parentOp);
1497
1498 // Check if any inner symbol name in the target operation already exists
1499 // in the parent operation. Return failure() if a collision is found to stop
1500 // the walk early.
1501 LogicalResult walkResult = targetTable.walkSymbols(
1502 [&](StringAttr name, const hw::InnerSymTarget &target) -> LogicalResult {
1503 // Check if this symbol name exists in the parent operation
1504 if (parentTable.lookup(name)) {
1505 // Collision found, return failure to stop the walk
1506 return failure();
1507 }
1508 return success();
1509 });
1510
1511 // If the walk failed, it means we found a collision
1512 return failed(walkResult);
1513}
1514
1515/// A sample reduction pattern that eagerly inlines instances.
1516struct EagerInliner : public OpReduction<InstanceOp> {
1517 void beforeReduction(mlir::ModuleOp op) override {
1518 symbols.clear();
1519 nlaRemover.clear();
1520 nlaTables.clear();
1521 for (auto circuitOp : op.getOps<CircuitOp>())
1522 nlaTables.insert({circuitOp, std::make_unique<NLATable>(circuitOp)});
1523 innerSymTables = std::make_unique<hw::InnerSymbolTableCollection>();
1524 }
1525 void afterReduction(mlir::ModuleOp op) override {
1526 nlaRemover.remove(op);
1527 nlaTables.clear();
1528 innerSymTables.reset();
1529 }
1530
1531 uint64_t match(InstanceOp instOp) override {
1532 auto *tableOp = SymbolTable::getNearestSymbolTable(instOp);
1533 auto *moduleOp =
1534 instOp.getReferencedOperation(symbols.getSymbolTable(tableOp));
1535
1536 // Only inline FModuleOp instances
1537 if (!isa<FModuleOp>(moduleOp))
1538 return 0;
1539
1540 // Skip instances that participate in any NLAs
1541 auto circuitOp = instOp->getParentOfType<CircuitOp>();
1542 if (!circuitOp)
1543 return 0;
1544 auto it = nlaTables.find(circuitOp);
1545 if (it == nlaTables.end() || !it->second)
1546 return 0;
1547 DenseSet<hw::HierPathOp> nlas;
1548 it->second->getInstanceNLAs(instOp, nlas);
1549 if (!nlas.empty())
1550 return 0;
1551
1552 // Check for inner symbol collisions between the referenced module and the
1553 // instance's parent module
1554 auto parentOp = instOp->getParentOfType<FModuleLike>();
1555 if (hasInnerSymbolCollision(moduleOp, parentOp, *innerSymTables))
1556 return 0;
1557
1558 return 1;
1559 }
1560
1561 LogicalResult rewrite(InstanceOp instOp) override {
1562 auto *tableOp = SymbolTable::getNearestSymbolTable(instOp);
1563 auto moduleOp = cast<FModuleOp>(
1564 instOp.getReferencedOperation(symbols.getSymbolTable(tableOp)));
1565 bool isLastUse =
1566 (symbols.getSymbolUserMap(tableOp).getUsers(moduleOp).size() == 1);
1567 auto clonedModuleOp = isLastUse ? moduleOp : moduleOp.clone();
1568
1569 // Create wires to replace the instance results.
1570 IRRewriter rewriter(instOp);
1571 SmallVector<Value> argWires;
1572 for (unsigned i = 0, e = instOp.getNumResults(); i != e; ++i) {
1573 auto result = instOp.getResult(i);
1574 auto name = rewriter.getStringAttr(Twine(instOp.getName()) + "_" +
1575 instOp.getPortName(i));
1576 auto wire = WireOp::create(rewriter, instOp.getLoc(), result.getType(),
1577 name, NameKindEnum::DroppableName,
1578 instOp.getPortAnnotation(i), StringAttr{})
1579 .getResult();
1580 result.replaceAllUsesWith(wire);
1581 argWires.push_back(wire);
1582 }
1583
1584 // Splice in the cloned module body.
1585 rewriter.inlineBlockBefore(clonedModuleOp.getBodyBlock(), instOp, argWires);
1586
1587 // Make sure we remove any NLAs that go through this instance, and the
1588 // module if we're about the delete the module.
1589 nlaRemover.markNLAsInOperation(instOp);
1590 if (isLastUse)
1591 nlaRemover.markNLAsInOperation(moduleOp);
1592
1593 instOp.erase();
1594 clonedModuleOp.erase();
1595 return success();
1596 }
1597
1598 std::string getName() const override { return "firrtl-eager-inliner"; }
1599 bool acceptSizeIncrease() const override { return true; }
1600
1601 ::detail::SymbolCache symbols;
1602 NLARemover nlaRemover;
1603 DenseMap<CircuitOp, std::unique_ptr<NLATable>> nlaTables;
1604 std::unique_ptr<hw::InnerSymbolTableCollection> innerSymTables;
1605};
1606
1607/// A reduction pattern that eagerly inlines `ObjectOp`s.
1608struct ObjectInliner : public OpReduction<ObjectOp> {
1609 void beforeReduction(mlir::ModuleOp op) override {
1610 blocksToSort.clear();
1611 symbols.clear();
1612 nlaRemover.clear();
1613 innerSymTables = std::make_unique<hw::InnerSymbolTableCollection>();
1614 }
1615 void afterReduction(mlir::ModuleOp op) override {
1616 for (auto *block : blocksToSort)
1617 mlir::sortTopologically(block);
1618 blocksToSort.clear();
1619 nlaRemover.remove(op);
1620 innerSymTables.reset();
1621 }
1622
1623 uint64_t match(ObjectOp objOp) override {
1624 auto *tableOp = SymbolTable::getNearestSymbolTable(objOp);
1625 auto *classOp =
1626 objOp.getReferencedOperation(symbols.getSymbolTable(tableOp));
1627
1628 // Only inline `ClassOp`s.
1629 if (!isa<ClassOp>(classOp))
1630 return 0;
1631
1632 // Check for inner symbol collisions between the referenced class and the
1633 // object's parent module.
1634 auto parentOp = objOp->getParentOfType<FModuleLike>();
1635 if (hasInnerSymbolCollision(classOp, parentOp, *innerSymTables))
1636 return 0;
1637
1638 // Verify all uses are ObjectSubfieldOp.
1639 for (auto *user : objOp.getResult().getUsers())
1640 if (!isa<ObjectSubfieldOp>(user))
1641 return 0;
1642
1643 return 1;
1644 }
1645
1646 LogicalResult rewrite(ObjectOp objOp) override {
1647 auto *tableOp = SymbolTable::getNearestSymbolTable(objOp);
1648 auto classOp = cast<ClassOp>(
1649 objOp.getReferencedOperation(symbols.getSymbolTable(tableOp)));
1650 auto clonedClassOp = classOp.clone();
1651
1652 // Create wires to replace the ObjectSubfieldOp results.
1653 IRRewriter rewriter(objOp);
1654 SmallVector<Value> portWires;
1655 auto classType = objOp.getType();
1656
1657 // Create a wire for each port in the class
1658 for (unsigned i = 0, e = classType.getNumElements(); i != e; ++i) {
1659 auto element = classType.getElement(i);
1660 auto name = rewriter.getStringAttr(Twine(objOp.getName()) + "_" +
1661 element.name.getValue());
1662 auto wire = WireOp::create(rewriter, objOp.getLoc(), element.type, name,
1663 NameKindEnum::DroppableName,
1664 rewriter.getArrayAttr({}), StringAttr{})
1665 .getResult();
1666 portWires.push_back(wire);
1667 }
1668
1669 // Replace all ObjectSubfieldOp uses with corresponding wires
1670 SmallVector<ObjectSubfieldOp> subfieldOps;
1671 for (auto *user : objOp.getResult().getUsers()) {
1672 auto subfieldOp = cast<ObjectSubfieldOp>(user);
1673 subfieldOps.push_back(subfieldOp);
1674 auto index = subfieldOp.getIndex();
1675 subfieldOp.getResult().replaceAllUsesWith(portWires[index]);
1676 }
1677
1678 // Splice in the cloned class body.
1679 rewriter.inlineBlockBefore(clonedClassOp.getBodyBlock(), objOp, portWires);
1680
1681 // After inlining the class body, we need to eliminate `WireOps` since
1682 // `ClassOps` cannot contain wires. For each port wire, find its single
1683 // connect, remove it, and replace all uses of the wire with the assigned
1684 // value.
1685 SmallVector<FConnectLike> connectsToErase;
1686 for (auto portWire : portWires) {
1687 // Find a single value to replace the wire with, and collect all connects
1688 // to the wire such that we can erase them later.
1689 Value value;
1690 for (auto *user : portWire.getUsers()) {
1691 if (auto connect = dyn_cast<FConnectLike>(user)) {
1692 if (connect.getDest() == portWire) {
1693 value = connect.getSrc();
1694 connectsToErase.push_back(connect);
1695 }
1696 }
1697 }
1698
1699 // Be very conservative about deleting these wires. Other reductions may
1700 // leave class ports unconnected, which means that there isn't always a
1701 // clean replacement available here. Better to just leave the wires in the
1702 // IR and let the verifier fail later.
1703 if (value)
1704 portWire.replaceAllUsesWith(value);
1705 for (auto connect : connectsToErase)
1706 connect.erase();
1707 if (portWire.use_empty())
1708 portWire.getDefiningOp()->erase();
1709 connectsToErase.clear();
1710 }
1711
1712 // Make sure we remove any NLAs that go through this object.
1713 nlaRemover.markNLAsInOperation(objOp);
1714
1715 // Since the above forwarding of SSA values through wires can create
1716 // dominance issues, mark the region containing the object to be sorted
1717 // topologically.
1718 blocksToSort.insert(objOp->getBlock());
1719
1720 // Erase the object and cloned class.
1721 for (auto subfieldOp : subfieldOps)
1722 subfieldOp.erase();
1723 objOp.erase();
1724 clonedClassOp.erase();
1725 return success();
1726 }
1727
1728 std::string getName() const override { return "firrtl-object-inliner"; }
1729 bool acceptSizeIncrease() const override { return true; }
1730
1731 SetVector<Block *> blocksToSort;
1732 ::detail::SymbolCache symbols;
1733 NLARemover nlaRemover;
1734 std::unique_ptr<hw::InnerSymbolTableCollection> innerSymTables;
1735};
1736
1737/// Reduction that converts `regreset` to `reg` by dropping reset and init
1738/// value.
1739struct ResetDisconnector : public OpReduction<RegResetOp> {
1740 uint64_t match(RegResetOp op) override { return 1; }
1741
1742 LogicalResult rewrite(RegResetOp regResetOp) override {
1743 ImplicitLocOpBuilder builder(regResetOp.getLoc(), regResetOp);
1744 auto regOp = RegOp::create(
1745 builder, regResetOp.getResult().getType(), regResetOp.getClockVal(),
1746 regResetOp.getNameAttr(), regResetOp.getNameKindAttr(),
1747 regResetOp.getAnnotationsAttr(), regResetOp.getInnerSymAttr(),
1748 regResetOp.getForceableAttr(), regResetOp.getInitialAttr());
1749
1750 regResetOp.getResult().replaceAllUsesWith(regOp.getResult());
1751 if (regResetOp.getForceable())
1752 regResetOp.getRef().replaceAllUsesWith(regOp.getRef());
1753 regResetOp.erase();
1754
1755 return success();
1756 }
1757
1758 std::string getName() const override { return "reset-disconnector"; }
1759};
1760
1761/// Psuedo-reduction that sanitizes the names of things inside modules. This is
1762/// not an actual reduction, but often removes extraneous information that has
1763/// no bearing on the actual reduction (and would likely be removed before
1764/// sharing the reduction). This makes the following changes:
1765///
1766/// - All wires are renamed to "wire"
1767/// - All registers are renamed to "reg"
1768/// - All nodes are renamed to "node"
1769/// - All memories are renamed to "mem"
1770/// - All verification messages and labels are dropped
1771///
1772struct ModuleInternalNameSanitizer : public Reduction {
1773 uint64_t match(Operation *op) override {
1774 // Only match operations with names.
1775 return isa<firrtl::WireOp, firrtl::RegOp, firrtl::RegResetOp,
1776 firrtl::NodeOp, firrtl::MemOp, chirrtl::CombMemOp,
1777 chirrtl::SeqMemOp, firrtl::AssertOp, firrtl::AssumeOp,
1778 firrtl::CoverOp>(op);
1779 }
1780 LogicalResult rewrite(Operation *op) override {
1781 TypeSwitch<Operation *, void>(op)
1782 .Case<firrtl::WireOp>([](auto op) { op.setName("wire"); })
1783 .Case<firrtl::RegOp, firrtl::RegResetOp>(
1784 [](auto op) { op.setName("reg"); })
1785 .Case<firrtl::NodeOp>([](auto op) { op.setName("node"); })
1786 .Case<firrtl::MemOp, chirrtl::CombMemOp, chirrtl::SeqMemOp>(
1787 [](auto op) { op.setName("mem"); })
1788 .Case<firrtl::AssertOp, firrtl::AssumeOp, firrtl::CoverOp>([](auto op) {
1789 op->setAttr("message", StringAttr::get(op.getContext(), ""));
1790 op->setAttr("name", StringAttr::get(op.getContext(), ""));
1791 });
1792 return success();
1793 }
1794
1795 std::string getName() const override {
1796 return "module-internal-name-sanitizer";
1797 }
1798
1799 bool acceptSizeIncrease() const override { return true; }
1800
1801 bool isOneShot() const override { return true; }
1802};
1803
1804/// Psuedo-reduction that sanitizes module, instance, and port names. This
1805/// makes the following changes:
1806///
1807/// - All modules are given metasyntactic names ("Foo", "Bar", etc.)
1808/// - All instances are renamed to match the new module name
1809/// - All module ports are renamed in the following way:
1810/// - All clocks are reanemd to "clk"
1811/// - All resets are renamed to "rst"
1812/// - All references are renamed to "ref"
1813/// - Anything else is renamed to "port"
1814///
1815struct ModuleNameSanitizer : OpReduction<firrtl::CircuitOp> {
1816
1817 reduce::MetasyntacticNameGenerator nameGenerator;
1818 size_t portNameIndex = 0;
1819
1820 char getPortName() {
1821 if (portNameIndex >= 26)
1822 portNameIndex = 0;
1823 return 'a' + portNameIndex++;
1824 }
1825
1826 void beforeReduction(mlir::ModuleOp op) override { nameGenerator.reset(); }
1827
1828 LogicalResult rewrite(firrtl::CircuitOp circuitOp) override {
1829
1830 // Analyses used to aid the rewrite.
1831 firrtl::InstanceGraph iGraph(circuitOp);
1832 NLATable nlaTable(circuitOp);
1833 SymbolTable symTable(circuitOp);
1834 CircuitNamespace ns(circuitOp);
1835
1836 // Rename symbols and NLAs.
1837 auto renameModule = [&](firrtl::FModuleLike mod,
1838 StringAttr newName) -> LogicalResult {
1839 StringAttr oldName = mod.getModuleNameAttr();
1840 if (failed(symTable.rename(mod, newName)))
1841 return failure();
1842 nlaTable.renameModule(oldName, newName);
1843 return success();
1844 };
1845
1846 // Set the top-modulefirst so that the circuit gets the first metasyntactic
1847 // name, i.e., "Foo".
1848 auto topModule = iGraph.getTopLevelModule();
1849 auto *ctx = circuitOp.getContext();
1851 topModule.getModuleName())) {
1852 auto newTopName = StringAttr::get(ctx, nameGenerator.getNextName(ns));
1853 if (failed(renameModule(topModule, newTopName)))
1854 return failure();
1855 circuitOp.setName(newTopName.getValue());
1856 }
1857
1858 for (auto *node : iGraph) {
1859 auto module = node->getModule<firrtl::FModuleLike>();
1860
1861 bool shouldReplacePorts = false;
1862 SmallVector<Attribute> newPortNames;
1863 if (auto fmodule = dyn_cast<firrtl::FModuleOp>(*module)) {
1864 portNameIndex = 0;
1865 // TODO: The namespace should be unnecessary. However, some FIRRTL
1866 // passes expect that port names are unique.
1867 circt::Namespace ns;
1868 auto oldPorts = fmodule.getPorts();
1869 shouldReplacePorts = !oldPorts.empty();
1870 for (unsigned i = 0, e = fmodule.getNumPorts(); i != e; ++i) {
1871 auto port = oldPorts[i];
1872 auto newName = firrtl::FIRRTLTypeSwitch<Type, StringRef>(port.type)
1873 .Case<firrtl::ClockType>(
1874 [&](auto a) { return ns.newName("clk"); })
1875 .Case<firrtl::ResetType, firrtl::AsyncResetType>(
1876 [&](auto a) { return ns.newName("rst"); })
1877 .Case<firrtl::RefType>(
1878 [&](auto a) { return ns.newName("ref"); })
1879 .Default([&](auto a) {
1880 return ns.newName(Twine(getPortName()));
1881 });
1882 newPortNames.push_back(StringAttr::get(ctx, newName));
1883 }
1884 fmodule->setAttr("portNames",
1885 ArrayAttr::get(fmodule.getContext(), newPortNames));
1886 }
1887
1888 if (module == iGraph.getTopLevelModule())
1889 continue;
1890 // Skip renaming if the module already has a metasyntactic name.
1892 module.getModuleName()))
1893 continue;
1894 auto newName = StringAttr::get(ctx, nameGenerator.getNextName(ns));
1895 if (failed(renameModule(module, newName)))
1896 return failure();
1897 for (auto *use : node->uses()) {
1898 auto useOp = use->getInstance();
1899 if (auto instanceOp = dyn_cast<firrtl::InstanceOp>(*useOp)) {
1900 // SymbolTable::rename already updated the moduleName
1901 // FlatSymbolRefAttr on all InstanceOps. Only the debug instance name
1902 // and port names need manual fixup here.
1903 instanceOp.setName(newName);
1904 if (shouldReplacePorts)
1905 instanceOp.setPortNamesAttr(ArrayAttr::get(ctx, newPortNames));
1906 } else if (auto instanceChoiceOp =
1907 dyn_cast<firrtl::InstanceChoiceOp>(*useOp)) {
1908 if (instanceChoiceOp.getDefaultTargetAttr().getAttr() == newName)
1909 instanceChoiceOp.setName(newName);
1910 if (shouldReplacePorts)
1911 instanceChoiceOp.setPortNamesAttr(
1912 ArrayAttr::get(ctx, newPortNames));
1913 } else if (auto objectOp = dyn_cast<firrtl::ObjectOp>(*useOp)) {
1914 // ObjectOp stores the class name in its result type. Result types
1915 // are not updated by SymbolTable::rename (AttrTypeReplacer is called
1916 // with replaceTypes=false), so we must patch the ClassType manually.
1917 auto oldClassType = objectOp.getType();
1918 auto newClassType = firrtl::ClassType::get(
1919 ctx, FlatSymbolRefAttr::get(newName), oldClassType.getElements());
1920 objectOp.getResult().setType(newClassType);
1921 objectOp.setName(newName);
1922 }
1923 }
1924 }
1925
1926 return success();
1927 }
1928
1929 std::string getName() const override { return "module-name-sanitizer"; }
1930
1931 bool acceptSizeIncrease() const override { return true; }
1932
1933 bool isOneShot() const override { return true; }
1934};
1935
1936/// A reduction pattern that groups modules by their port signature (types and
1937/// directions) and replaces instances with the smallest module in each group.
1938/// This helps reduce the IR by consolidating functionally equivalent modules
1939/// based on their interface.
1940///
1941/// The pattern works by:
1942/// 1. Grouping all modules by their port signature (port types and directions)
1943/// 2. For each group with multiple modules, finding the smallest module using
1944/// the module size cache
1945/// 3. Replacing all instances of larger modules with instances of the smallest
1946/// module in the same group
1947/// 4. Removing the larger modules from the circuit
1948///
1949/// This reduction is useful for reducing circuits where multiple modules have
1950/// the same interface but different implementations, allowing the reducer to
1951/// try the smallest implementation first.
1952struct ModuleSwapper : public OpReduction<InstanceOp> {
1953 // Per-circuit state containing all the information needed for module swapping
1954 using PortSignature = SmallVector<std::pair<Type, Direction>>;
1955 struct CircuitState {
1956 DenseMap<PortSignature, SmallVector<FModuleLike, 4>> moduleTypeGroups;
1957 DenseMap<StringAttr, FModuleLike> instanceToCanonicalModule;
1958 std::unique_ptr<NLATable> nlaTable;
1959 };
1960
1961 void beforeReduction(mlir::ModuleOp op) override {
1962 symbols.clear();
1963 nlaRemover.clear();
1964 moduleSizes.clear();
1965 circuitStates.clear();
1966
1967 // Collect module type groups and NLA tables for all circuits up front
1968 op.walk<WalkOrder::PreOrder>([&](CircuitOp circuitOp) {
1969 auto &state = circuitStates[circuitOp];
1970 state.nlaTable = std::make_unique<NLATable>(circuitOp);
1971 buildModuleTypeGroups(circuitOp, state);
1972 return WalkResult::skip();
1973 });
1974 }
1975 void afterReduction(mlir::ModuleOp op) override { nlaRemover.remove(op); }
1976
1977 /// Create a vector of port type-direction pairs for the given FIRRTL module.
1978 /// This ignores port names, allowing modules with the same port types and
1979 /// directions but different port names to be considered equivalent for
1980 /// swapping.
1981 PortSignature getModulePortSignature(FModuleLike module) {
1982 PortSignature signature;
1983 signature.reserve(module.getNumPorts());
1984 for (unsigned i = 0, e = module.getNumPorts(); i < e; ++i)
1985 signature.emplace_back(module.getPortType(i), module.getPortDirection(i));
1986 return signature;
1987 }
1988
1989 /// Group modules by their port signature and find the smallest in each group.
1990 void buildModuleTypeGroups(CircuitOp circuitOp, CircuitState &state) {
1991 // Group modules by their port signature
1992 for (auto module : circuitOp.getBodyBlock()->getOps<FModuleLike>()) {
1993 auto signature = getModulePortSignature(module);
1994 state.moduleTypeGroups[signature].push_back(module);
1995 }
1996
1997 // For each group, find the smallest module
1998 for (auto &[signature, modules] : state.moduleTypeGroups) {
1999 if (modules.size() <= 1)
2000 continue;
2001
2002 FModuleLike smallestModule = nullptr;
2003 uint64_t smallestSize = std::numeric_limits<uint64_t>::max();
2004
2005 for (auto module : modules) {
2006 uint64_t size = moduleSizes.getModuleSize(module, symbols);
2007 if (size < smallestSize) {
2008 smallestSize = size;
2009 smallestModule = module;
2010 }
2011 }
2012
2013 // Map all modules in this group to the smallest one
2014 for (auto module : modules) {
2015 if (module != smallestModule) {
2016 state.instanceToCanonicalModule[module.getModuleNameAttr()] =
2017 smallestModule;
2018 }
2019 }
2020 }
2021 }
2022
2023 uint64_t match(InstanceOp instOp) override {
2024 // Get the circuit this instance belongs to
2025 auto circuitOp = instOp->getParentOfType<CircuitOp>();
2026 assert(circuitOp);
2027 const auto &state = circuitStates.at(circuitOp);
2028
2029 // Skip instances that participate in any NLAs
2030 DenseSet<hw::HierPathOp> nlas;
2031 state.nlaTable->getInstanceNLAs(instOp, nlas);
2032 if (!nlas.empty())
2033 return 0;
2034
2035 // Check if this instance can be redirected to a smaller module
2036 auto moduleName = instOp.getModuleNameAttr().getAttr();
2037 auto canonicalModule = state.instanceToCanonicalModule.lookup(moduleName);
2038 if (!canonicalModule)
2039 return 0;
2040
2041 // Benefit is the size difference
2042 auto currentModule = cast<FModuleLike>(
2043 instOp.getReferencedOperation(symbols.getNearestSymbolTable(instOp)));
2044 uint64_t currentSize = moduleSizes.getModuleSize(currentModule, symbols);
2045 uint64_t canonicalSize =
2046 moduleSizes.getModuleSize(canonicalModule, symbols);
2047 return currentSize > canonicalSize ? currentSize - canonicalSize : 1;
2048 }
2049
2050 LogicalResult rewrite(InstanceOp instOp) override {
2051 // Get the circuit this instance belongs to
2052 auto circuitOp = instOp->getParentOfType<CircuitOp>();
2053 assert(circuitOp);
2054 const auto &state = circuitStates.at(circuitOp);
2055
2056 // Replace the instantiated module with the canonical module.
2057 auto canonicalModule = state.instanceToCanonicalModule.at(
2058 instOp.getModuleNameAttr().getAttr());
2059 auto canonicalName = canonicalModule.getModuleNameAttr();
2060 instOp.setModuleNameAttr(FlatSymbolRefAttr::get(canonicalName));
2061
2062 // Update port names to match the canonical module
2063 instOp.setPortNamesAttr(canonicalModule.getPortNamesAttr());
2064
2065 return success();
2066 }
2067
2068 std::string getName() const override { return "firrtl-module-swapper"; }
2069 bool acceptSizeIncrease() const override { return true; }
2070
2071private:
2072 ::detail::SymbolCache symbols;
2073 NLARemover nlaRemover;
2074 ModuleSizeCache moduleSizes;
2075
2076 // Per-circuit state containing all module swapping information
2077 DenseMap<CircuitOp, CircuitState> circuitStates;
2078};
2079
2080/// A reduction pattern that handles MustDedup annotations by replacing all
2081/// module names in a dedup group with a single module name. This helps reduce
2082/// the IR by consolidating module references that are required to be identical.
2083///
2084/// The pattern works by:
2085/// 1. Finding all MustDeduplicateAnnotation annotations on the circuit
2086/// 2. For each dedup group, using the first module as the canonical name
2087/// 3. Replacing all instance references to other modules in the group with
2088/// references to the canonical module
2089/// 4. Removing the non-canonical modules from the circuit
2090/// 5. Removing the processed MustDedup annotation
2091///
2092/// This reduction is particularly useful for reducing large circuits where
2093/// multiple modules are known to be identical but haven't been deduplicated
2094/// yet.
2095struct ForceDedup : public OpReduction<CircuitOp> {
2096 void beforeReduction(mlir::ModuleOp op) override {
2097 symbols.clear();
2098 nlaRemover.clear();
2099 modulesToErase.clear();
2100 moduleSizes.clear();
2101 }
2102 void afterReduction(mlir::ModuleOp op) override {
2103 nlaRemover.remove(op);
2104 for (auto mod : modulesToErase)
2105 mod->erase();
2106 }
2107
2108 /// Collect all MustDedup annotations and create matches for each dedup group.
2109 void matches(CircuitOp circuitOp,
2110 llvm::function_ref<void(uint64_t, uint64_t)> addMatch) override {
2111 auto &symbolTable = symbols.getNearestSymbolTable(circuitOp);
2112 auto annotations = AnnotationSet(circuitOp);
2113 for (auto [annoIdx, anno] : llvm::enumerate(annotations)) {
2114 if (!anno.isClass(mustDeduplicateAnnoClass))
2115 continue;
2116
2117 auto modulesAttr = anno.getMember<ArrayAttr>("modules");
2118 if (!modulesAttr || modulesAttr.size() < 2)
2119 continue;
2120
2121 // Check that all modules have the same port signature. Malformed inputs
2122 // may have modules listed in a MustDedup annotation that have distinct
2123 // port types.
2124 uint64_t totalSize = 0;
2125 ArrayAttr portTypes;
2126 DenseBoolArrayAttr portDirections;
2127 bool allSame = true;
2128 for (auto moduleName : modulesAttr.getAsRange<StringAttr>()) {
2129 auto target = tokenizePath(moduleName);
2130 if (!target) {
2131 allSame = false;
2132 break;
2133 }
2134 auto mod = symbolTable.lookup<FModuleLike>(target->module);
2135 if (!mod) {
2136 allSame = false;
2137 break;
2138 }
2139 totalSize += moduleSizes.getModuleSize(mod, symbols);
2140 if (!portTypes) {
2141 portTypes = mod.getPortTypesAttr();
2142 portDirections = mod.getPortDirectionsAttr();
2143 } else if (portTypes != mod.getPortTypesAttr() ||
2144 portDirections != mod.getPortDirectionsAttr()) {
2145 allSame = false;
2146 break;
2147 }
2148 }
2149 if (!allSame)
2150 continue;
2151
2152 // Each dedup group gets its own match with benefit proportional to group
2153 // size.
2154 addMatch(totalSize, annoIdx);
2155 }
2156 }
2157
2158 LogicalResult rewriteMatches(CircuitOp circuitOp,
2159 ArrayRef<uint64_t> matches) override {
2160 auto *context = circuitOp->getContext();
2161 NLATable nlaTable(circuitOp);
2162 hw::InnerSymbolTableCollection innerSymTables;
2163 auto annotations = AnnotationSet(circuitOp);
2164 SmallVector<Annotation> newAnnotations;
2165
2166 for (auto [annoIdx, anno] : llvm::enumerate(annotations)) {
2167 // Check if this annotation was selected.
2168 if (!llvm::is_contained(matches, annoIdx)) {
2169 newAnnotations.push_back(anno);
2170 continue;
2171 }
2172 auto modulesAttr = anno.getMember<ArrayAttr>("modules");
2173 assert(anno.isClass(mustDeduplicateAnnoClass) && modulesAttr &&
2174 modulesAttr.size() >= 2);
2175
2176 // Extract module names from the dedup group.
2177 SmallVector<StringAttr> moduleNames;
2178 for (auto moduleRef : modulesAttr.getAsRange<StringAttr>()) {
2179 // Parse "~CircuitName|ModuleName" format.
2180 auto refStr = moduleRef.getValue();
2181 auto pipePos = refStr.find('|');
2182 if (pipePos != StringRef::npos && pipePos + 1 < refStr.size()) {
2183 auto moduleName = refStr.substr(pipePos + 1);
2184 moduleNames.push_back(StringAttr::get(context, moduleName));
2185 }
2186 }
2187
2188 // Simply drop the annotation if there's only one module.
2189 if (moduleNames.size() < 2)
2190 continue;
2191
2192 // Replace all instances and references to other modules with the
2193 // first module.
2194 replaceModuleReferences(circuitOp, moduleNames, nlaTable, innerSymTables);
2195 nlaRemover.markNLAsInAnnotation(anno.getAttr());
2196 }
2197 if (newAnnotations.size() == annotations.size())
2198 return failure();
2199
2200 // Update circuit annotations.
2201 AnnotationSet newAnnoSet(newAnnotations, context);
2202 newAnnoSet.applyToOperation(circuitOp);
2203 return success();
2204 }
2205
2206 std::string getName() const override { return "firrtl-force-dedup"; }
2207 bool acceptSizeIncrease() const override { return true; }
2208
2209private:
2210 /// Replace all references to modules in the dedup group with the canonical
2211 /// module name
2212 void replaceModuleReferences(CircuitOp circuitOp,
2213 ArrayRef<StringAttr> moduleNames,
2214 NLATable &nlaTable,
2215 hw::InnerSymbolTableCollection &innerSymTables) {
2216 auto *tableOp = SymbolTable::getNearestSymbolTable(circuitOp);
2217 auto &symbolTable = symbols.getSymbolTable(tableOp);
2218 auto &symbolUserMap = symbols.getSymbolUserMap(tableOp);
2219 auto *context = circuitOp->getContext();
2220 auto innerRefs = hw::InnerRefNamespace{symbolTable, innerSymTables};
2221
2222 // Collect the modules.
2223 FModuleLike canonicalModule;
2224 SmallVector<FModuleLike> modulesToReplace;
2225 for (auto name : moduleNames) {
2226 if (auto mod = symbolTable.lookup<FModuleLike>(name)) {
2227 if (!canonicalModule)
2228 canonicalModule = mod;
2229 else
2230 modulesToReplace.push_back(mod);
2231 }
2232 }
2233 if (modulesToReplace.empty())
2234 return;
2235
2236 // Replace all instance references.
2237 auto canonicalName = canonicalModule.getModuleNameAttr();
2238 auto canonicalRef = FlatSymbolRefAttr::get(canonicalName);
2239 for (auto moduleName : moduleNames) {
2240 if (moduleName == canonicalName)
2241 continue;
2242 auto *symbolOp = symbolTable.lookup(moduleName);
2243 if (!symbolOp)
2244 continue;
2245 for (auto *user : symbolUserMap.getUsers(symbolOp)) {
2246 auto instOp = dyn_cast<InstanceOp>(user);
2247 if (!instOp || instOp.getModuleNameAttr().getAttr() != moduleName)
2248 continue;
2249 instOp.setModuleNameAttr(canonicalRef);
2250 instOp.setPortNamesAttr(canonicalModule.getPortNamesAttr());
2251 }
2252 }
2253
2254 // Update NLAs to reference the canonical module instead of modules being
2255 // removed using NLATable for better performance.
2256 for (auto oldMod : modulesToReplace) {
2257 SmallVector<hw::HierPathOp> nlaOps(
2258 nlaTable.lookup(oldMod.getModuleNameAttr()));
2259 for (auto nlaOp : nlaOps) {
2260 nlaTable.erase(nlaOp);
2261 StringAttr oldModName = oldMod.getModuleNameAttr();
2262 StringAttr newModName = canonicalName;
2263 SmallVector<Attribute, 4> newPath;
2264 for (auto nameRef : nlaOp.getNamepath()) {
2265 if (auto ref = dyn_cast<hw::InnerRefAttr>(nameRef)) {
2266 if (ref.getModule() == oldModName) {
2267 auto oldInst = innerRefs.lookupOp<FInstanceLike>(ref);
2268 ref = hw::InnerRefAttr::get(newModName, ref.getName());
2269 auto newInst = innerRefs.lookupOp<FInstanceLike>(ref);
2270 if (oldInst && newInst) {
2271 // Get the first module name from the list (for
2272 // InstanceOp/ObjectOp, there's only one)
2273 auto oldModNames = oldInst.getReferencedModuleNamesAttr();
2274 auto newModNames = newInst.getReferencedModuleNamesAttr();
2275 if (!oldModNames.empty() && !newModNames.empty()) {
2276 oldModName = cast<StringAttr>(oldModNames[0]);
2277 newModName = cast<StringAttr>(newModNames[0]);
2278 }
2279 }
2280 }
2281 newPath.push_back(ref);
2282 } else if (cast<FlatSymbolRefAttr>(nameRef).getAttr() == oldModName) {
2283 newPath.push_back(FlatSymbolRefAttr::get(newModName));
2284 } else {
2285 newPath.push_back(nameRef);
2286 }
2287 }
2288 nlaOp.setNamepathAttr(ArrayAttr::get(context, newPath));
2289 nlaTable.addNLA(nlaOp);
2290 }
2291 }
2292
2293 // Mark NLAs in modules to be removed.
2294 for (auto module : modulesToReplace) {
2295 nlaRemover.markNLAsInOperation(module);
2296 modulesToErase.insert(module);
2297 }
2298 }
2299
2300 ::detail::SymbolCache symbols;
2301 NLARemover nlaRemover;
2302 SetVector<FModuleLike> modulesToErase;
2303 ModuleSizeCache moduleSizes;
2304};
2305
2306/// A reduction pattern that moves `MustDedup` annotations from a module onto
2307/// its child modules. This pattern iterates over all MustDedup annotations,
2308/// collects all `FInstanceLike` ops in each module of the dedup group, and
2309/// creates new MustDedup annotations for corresponding instances across the
2310/// modules. Each set of corresponding instances becomes a separate match of the
2311/// reduction. The reduction also removes the original MustDedup annotation on
2312/// the parent module.
2313///
2314/// The pattern works by:
2315/// 1. Finding all MustDeduplicateAnnotation annotations on the circuit
2316/// 2. For each dedup group, collecting all FInstanceLike operations in each
2317/// module
2318/// 3. Grouping corresponding instances across modules by their position/name
2319/// 4. Creating new MustDedup annotations for each group of corresponding
2320/// instances
2321/// 5. Removing the original MustDedup annotation from the circuit
2322struct MustDedupChildren : public OpReduction<CircuitOp> {
2323 void beforeReduction(mlir::ModuleOp op) override {
2324 symbols.clear();
2325 nlaRemover.clear();
2326 }
2327 void afterReduction(mlir::ModuleOp op) override { nlaRemover.remove(op); }
2328
2329 /// Collect all MustDedup annotations and create matches for each instance
2330 /// group.
2331 void matches(CircuitOp circuitOp,
2332 llvm::function_ref<void(uint64_t, uint64_t)> addMatch) override {
2333 auto annotations = AnnotationSet(circuitOp);
2334 uint64_t matchId = 0;
2335
2336 DenseSet<StringRef> modulesAlreadyInMustDedup;
2337 for (auto [annoIdx, anno] : llvm::enumerate(annotations))
2338 if (anno.isClass(mustDeduplicateAnnoClass))
2339 if (auto modulesAttr = anno.getMember<ArrayAttr>("modules"))
2340 for (auto moduleRef : modulesAttr.getAsRange<StringAttr>())
2341 if (auto target = tokenizePath(moduleRef))
2342 modulesAlreadyInMustDedup.insert(target->module);
2343
2344 for (auto [annoIdx, anno] : llvm::enumerate(annotations)) {
2345 if (!anno.isClass(mustDeduplicateAnnoClass))
2346 continue;
2347
2348 auto modulesAttr = anno.getMember<ArrayAttr>("modules");
2349 if (!modulesAttr || modulesAttr.size() < 2)
2350 continue;
2351
2352 // Process each group of corresponding instances
2353 processInstanceGroups(
2354 circuitOp, modulesAttr, [&](ArrayRef<FInstanceLike> instanceGroup) {
2355 matchId++;
2356
2357 // Make sure there are at least two distinct modules.
2358 SmallDenseSet<StringAttr, 4> moduleTargets;
2359 for (auto instOp : instanceGroup) {
2360 auto moduleNames = instOp.getReferencedModuleNamesAttr();
2361 for (auto moduleName : moduleNames)
2362 moduleTargets.insert(cast<StringAttr>(moduleName));
2363 }
2364 if (moduleTargets.size() < 2)
2365 return;
2366
2367 // Make sure none of the modules are not yet in a must dedup
2368 // annotation.
2369 if (llvm::any_of(instanceGroup, [&](FInstanceLike inst) {
2370 auto moduleNames = inst.getReferencedModuleNames();
2371 return llvm::any_of(moduleNames, [&](StringRef moduleName) {
2372 return modulesAlreadyInMustDedup.contains(moduleName);
2373 });
2374 }))
2375 return;
2376
2377 addMatch(1, matchId - 1);
2378 });
2379 }
2380 }
2381
2382 LogicalResult rewriteMatches(CircuitOp circuitOp,
2383 ArrayRef<uint64_t> matches) override {
2384 auto *context = circuitOp->getContext();
2385 auto annotations = AnnotationSet(circuitOp);
2386 SmallVector<Annotation> newAnnotations;
2387 uint64_t matchId = 0;
2388
2389 for (auto [annoIdx, anno] : llvm::enumerate(annotations)) {
2390 if (!anno.isClass(mustDeduplicateAnnoClass)) {
2391 newAnnotations.push_back(anno);
2392 continue;
2393 }
2394
2395 auto modulesAttr = anno.getMember<ArrayAttr>("modules");
2396 if (!modulesAttr || modulesAttr.size() < 2) {
2397 newAnnotations.push_back(anno);
2398 continue;
2399 }
2400
2401 processInstanceGroups(
2402 circuitOp, modulesAttr, [&](ArrayRef<FInstanceLike> instanceGroup) {
2403 // Check if this instance group was selected
2404 if (!llvm::is_contained(matches, matchId++))
2405 return;
2406
2407 // Create the list of modules to put into this new annotation.
2408 SmallSetVector<StringAttr, 4> moduleTargets;
2409 for (auto instOp : instanceGroup) {
2410 auto moduleNames = instOp.getReferencedModuleNames();
2411 for (auto moduleName : moduleNames) {
2412 auto target = TokenAnnoTarget();
2413 target.circuit = circuitOp.getName();
2414 target.module = moduleName;
2415 moduleTargets.insert(target.toStringAttr(context));
2416 }
2417 }
2418
2419 // Create a new MustDedup annotation for this list of modules.
2420 SmallVector<NamedAttribute> newAnnoAttrs;
2421 newAnnoAttrs.emplace_back(
2422 StringAttr::get(context, "class"),
2423 StringAttr::get(context, mustDeduplicateAnnoClass));
2424 newAnnoAttrs.emplace_back(
2425 StringAttr::get(context, "modules"),
2426 ArrayAttr::get(context,
2427 SmallVector<Attribute>(moduleTargets.begin(),
2428 moduleTargets.end())));
2429
2430 auto newAnnoDict = DictionaryAttr::get(context, newAnnoAttrs);
2431 newAnnotations.emplace_back(newAnnoDict);
2432 });
2433
2434 // Keep the original annotation around.
2435 newAnnotations.push_back(anno);
2436 }
2437
2438 // Update circuit annotations
2439 AnnotationSet newAnnoSet(newAnnotations, context);
2440 newAnnoSet.applyToOperation(circuitOp);
2441 return success();
2442 }
2443
2444 std::string getName() const override { return "must-dedup-children"; }
2445 bool acceptSizeIncrease() const override { return true; }
2446
2447private:
2448 /// Helper function to process groups of corresponding instances from a
2449 /// MustDedup annotation. Calls the provided lambda for each group of
2450 /// corresponding instances across the modules. Only calls the lambda if there
2451 /// are at least 2 modules.
2452 void processInstanceGroups(
2453 CircuitOp circuitOp, ArrayAttr modulesAttr,
2454 llvm::function_ref<void(ArrayRef<FInstanceLike>)> callback) {
2455 auto &symbolTable = symbols.getSymbolTable(circuitOp);
2456
2457 // Extract module names and get the actual modules
2458 SmallVector<FModuleLike> modules;
2459 for (auto moduleRef : modulesAttr.getAsRange<StringAttr>())
2460 if (auto target = tokenizePath(moduleRef))
2461 if (auto mod = symbolTable.lookup<FModuleLike>(target->module))
2462 modules.push_back(mod);
2463
2464 // Need at least 2 modules for deduplication
2465 if (modules.size() < 2)
2466 return;
2467
2468 // Collect all FInstanceLike operations from each module and group them by
2469 // name. Instance names are a good key for matching instances across
2470 // modules. But they may not be unique, so we need to be careful to only
2471 // match up instances that are uniquely named within every module.
2472 struct InstanceGroup {
2473 SmallVector<FInstanceLike> instances;
2474 bool nameIsUnique = true;
2475 };
2476 MapVector<StringAttr, InstanceGroup> instanceGroups;
2477 for (auto module : modules) {
2478 SmallDenseMap<StringAttr, unsigned> nameCounts;
2479 module.walk([&](FInstanceLike instOp) {
2480 if (isa<ObjectOp>(instOp.getOperation()))
2481 return;
2482 auto name = instOp.getInstanceNameAttr();
2483 auto &group = instanceGroups[name];
2484 if (nameCounts[name]++ > 1)
2485 group.nameIsUnique = false;
2486 group.instances.push_back(instOp);
2487 });
2488 }
2489
2490 // Call the callback for each group of instances that are uniquely named and
2491 // consist of at least 2 instances.
2492 for (auto &[name, group] : instanceGroups)
2493 if (group.nameIsUnique && group.instances.size() >= 2)
2494 callback(group.instances);
2495 }
2496
2497 ::detail::SymbolCache symbols;
2498 NLARemover nlaRemover;
2499};
2500
2501struct LayerDisable : public OpReduction<CircuitOp> {
2502 LayerDisable(MLIRContext *context) {
2503 pm = std::make_unique<mlir::PassManager>(
2504 context, "builtin.module", mlir::OpPassManager::Nesting::Explicit);
2505 pm->nest<firrtl::CircuitOp>().addPass(firrtl::createSpecializeLayers());
2506 };
2507
2508 void beforeReduction(mlir::ModuleOp op) override { symbolRefAttrMap.clear(); }
2509
2510 void afterReduction(mlir::ModuleOp op) override { (void)pm->run(op); };
2511
2512 void matches(CircuitOp circuitOp,
2513 llvm::function_ref<void(uint64_t, uint64_t)> addMatch) override {
2514 uint64_t matchId = 0;
2515
2516 SmallVector<FlatSymbolRefAttr> nestedRefs;
2517 std::function<void(StringAttr, LayerOp)> addLayer = [&](StringAttr rootRef,
2518 LayerOp layerOp) {
2519 if (!rootRef)
2520 rootRef = layerOp.getSymNameAttr();
2521 else
2522 nestedRefs.push_back(FlatSymbolRefAttr::get(layerOp));
2523
2524 symbolRefAttrMap[matchId] = SymbolRefAttr::get(rootRef, nestedRefs);
2525 addMatch(1, matchId++);
2526
2527 for (auto nestedLayerOp : layerOp.getOps<LayerOp>())
2528 addLayer(rootRef, nestedLayerOp);
2529
2530 if (!nestedRefs.empty())
2531 nestedRefs.pop_back();
2532 };
2533
2534 for (auto layerOp : circuitOp.getOps<LayerOp>())
2535 addLayer({}, layerOp);
2536 }
2537
2538 LogicalResult rewriteMatches(CircuitOp circuitOp,
2539 ArrayRef<uint64_t> matches) override {
2540 SmallVector<Attribute> disableLayers;
2541 if (auto existingDisables = circuitOp.getDisableLayersAttr()) {
2542 auto disableRange = existingDisables.getAsRange<Attribute>();
2543 disableLayers.append(disableRange.begin(), disableRange.end());
2544 }
2545 for (auto match : matches)
2546 disableLayers.push_back(symbolRefAttrMap.at(match));
2547
2548 circuitOp.setDisableLayersAttr(
2549 ArrayAttr::get(circuitOp.getContext(), disableLayers));
2550
2551 return success();
2552 }
2553
2554 std::string getName() const override { return "firrtl-layer-disable"; }
2555
2556 std::unique_ptr<mlir::PassManager> pm;
2557 DenseMap<uint64_t, SymbolRefAttr> symbolRefAttrMap;
2558};
2559
2560/// A reduction pattern that removes initialized layer entries
2561/// from the FIRRTL module operations.
2562struct LayerEnableRemover : public OpReduction<FModuleOp> {
2563 void matches(FModuleOp moduleOp,
2564 llvm::function_ref<void(uint64_t, uint64_t)> addMatch) override {
2565
2566 auto layers = moduleOp.getLayersAttr();
2567 for (size_t i = 0, e = layers.size(); i != e; ++i)
2568 addMatch(1, i);
2569 }
2570
2571 LogicalResult rewriteMatches(FModuleOp moduleOp,
2572 ArrayRef<uint64_t> matches) override {
2573
2574 llvm::SmallDenseSet<uint64_t, 4> removed(matches.begin(), matches.end());
2575 SmallVector<Attribute> newLayers;
2576 auto layers = moduleOp.getLayersAttr();
2577 for (size_t i = 0, e = layers.size(); i != e; ++i)
2578 if (!removed.contains(i))
2579 newLayers.push_back(layers[i]);
2580
2581 moduleOp.setLayersAttr(ArrayAttr::get(moduleOp.getContext(), newLayers));
2582 return success();
2583 }
2584
2585 std::string getName() const override { return "firrtl-layer-enable-remover"; }
2586};
2587
2588} // namespace
2589
2590/// A reduction pattern that removes elements from FIRRTL list create
2591/// operations. This generates one match per element in each list, allowing
2592/// selective removal of individual elements.
2593struct ListCreateElementRemover : public OpReduction<ListCreateOp> {
2594 void matches(ListCreateOp listOp,
2595 llvm::function_ref<void(uint64_t, uint64_t)> addMatch) override {
2596 // Create one match for each element in the list
2597 auto elements = listOp.getElements();
2598 for (size_t i = 0; i < elements.size(); ++i)
2599 addMatch(1, i);
2600 }
2601
2602 LogicalResult rewriteMatches(ListCreateOp listOp,
2603 ArrayRef<uint64_t> matches) override {
2604 // Convert matches to a set for fast lookup
2605 llvm::SmallDenseSet<uint64_t, 4> matchesSet(matches.begin(), matches.end());
2606
2607 // Collect elements that should be kept (not in matches)
2608 SmallVector<Value> newElements;
2609 auto elements = listOp.getElements();
2610 for (size_t i = 0; i < elements.size(); ++i) {
2611 if (!matchesSet.contains(i))
2612 newElements.push_back(elements[i]);
2613 }
2614
2615 // Create a new list with the remaining elements
2616 OpBuilder builder(listOp);
2617 auto newListOp = ListCreateOp::create(builder, listOp.getLoc(),
2618 listOp.getType(), newElements);
2619 listOp.getResult().replaceAllUsesWith(newListOp.getResult());
2620 listOp.erase();
2621
2622 return success();
2623 }
2624
2625 std::string getName() const override {
2626 return "firrtl-list-create-element-remover";
2627 }
2628};
2629
2630/// Reduction that removes the `convention` attribute from regular modules.
2631struct ModuleConventionRemover : public OpReduction<FModuleOp> {
2632 uint64_t match(FModuleOp module) override {
2633 return module.getConvention() != Convention::Internal;
2634 }
2635
2636 LogicalResult rewrite(FModuleOp module) override {
2637 module.setConvention(Convention::Internal);
2638 return success();
2639 }
2640
2641 std::string getName() const override { return "module-convention-remover"; }
2642 bool acceptSizeIncrease() const override { return true; }
2643 bool isOneShot() const override { return true; }
2644};
2645
2646/// Reduction that removes the `convention` attribute from external modules.
2647struct ExtmoduleConventionRemover : public OpReduction<FExtModuleOp> {
2648 uint64_t match(FExtModuleOp extmodule) override {
2649 return extmodule.getConvention() != Convention::Internal;
2650 }
2651
2652 LogicalResult rewrite(FExtModuleOp extmodule) override {
2653 extmodule.setConvention(Convention::Internal);
2654 return success();
2655 }
2656
2657 std::string getName() const override {
2658 return "extmodule-convention-remover";
2659 }
2660 bool acceptSizeIncrease() const override { return true; }
2661 bool isOneShot() const override { return true; }
2662};
2663
2666 : PassReduction(
2667 context, firrtl::createIMDeadCodeElim({/*removePortsOnly=*/true})) {
2668 }
2669 std::string getName() const override {
2670 return "firrtl-imdeadcodeelim-remove-ports";
2671 }
2672};
2673
2674//===----------------------------------------------------------------------===//
2675// Reduction Registration
2676//===----------------------------------------------------------------------===//
2677
2680 // Gather a list of reduction patterns that we should try. Ideally these are
2681 // assigned reasonable benefit indicators (higher benefit patterns are
2682 // prioritized). For example, things that can knock out entire modules while
2683 // being cheap should be tried first (and thus have higher benefit), before
2684 // trying to tweak operands of individual arithmetic ops.
2685 patterns.add<SimplifyResets, 35>();
2686 patterns.add<ForceDedup, 34>();
2687 patterns.add<MustDedupChildren, 33>();
2688 patterns.add<AnnotationRemover, 32>();
2689 patterns.add<ModuleSwapper, 31>();
2690 patterns.add<LayerDisable, 30>(getContext());
2691 patterns.add<LayerEnableRemover, 30>();
2692 patterns.add<PassReduction, 29>(
2693 getContext(),
2694 firrtl::createDropName({/*preserveMode=*/PreserveValues::None}), false,
2695 true);
2696 patterns.add<PassReduction, 28>(getContext(),
2697 firrtl::createLowerCHIRRTLPass(), true, true);
2698 patterns.add<PassReduction, 27>(getContext(), firrtl::createInferWidths(),
2699 true, true);
2700 patterns.add<PassReduction, 26>(getContext(), firrtl::createInferResets(),
2701 true, true);
2702 patterns.add<FIRRTLModuleExternalizer, 25>();
2703 patterns.add<InstanceStubber, 24>();
2704 patterns.add<MemoryStubber, 23>();
2705 patterns.add<EagerInliner, 22>();
2706 patterns.add<ObjectInliner, 22>();
2707 patterns.add<PassReduction, 21>(getContext(),
2708 firrtl::createLowerFIRRTLTypes(), true, true);
2709 patterns.add<PassReduction, 20>(getContext(), firrtl::createExpandWhens(),
2710 true, true);
2711 patterns.add<PassReduction, 19>(getContext(), firrtl::createInliner());
2712 patterns.add<PassReduction, 18>(getContext(), firrtl::createIMConstProp());
2713 patterns.add<IMDCEPortReduction, 17>(getContext());
2714 patterns.add<NodeSymbolRemover, 16>();
2715 patterns.add<PassReduction, 15>(getContext(), firrtl::createIMDeadCodeElim());
2716 patterns.add<ConnectForwarder, 14>();
2717 patterns.add<ConnectInvalidator, 13>();
2718 patterns.add<Constantifier, 12>();
2719 patterns.add<FIRRTLOperandForwarder<0>, 11>();
2720 patterns.add<FIRRTLOperandForwarder<1>, 10>();
2721 patterns.add<FIRRTLOperandForwarder<2>, 9>();
2723 patterns.add<ResetDisconnector, 8>();
2724 patterns.add<DetachSubaccesses, 7>();
2725 patterns.add<ModulePortPruner, 7>();
2726 patterns.add<ExtmodulePortPruner, 6>();
2727 patterns.add<RootPortPruner, 5>();
2728 patterns.add<RootExtmodulePortPruner, 5>();
2729 patterns.add<ExtmoduleInstanceRemover, 4>();
2730 patterns.add<ConnectSourceOperandForwarder<0>, 3>();
2731 patterns.add<ConnectSourceOperandForwarder<1>, 2>();
2732 patterns.add<ConnectSourceOperandForwarder<2>, 1>();
2734 patterns.add<ModuleNameSanitizer, 0>();
2737}
2738
2740 mlir::DialectRegistry &registry) {
2741 registry.addExtension(+[](MLIRContext *ctx, FIRRTLDialect *dialect) {
2742 dialect->addInterfaces<FIRRTLReducePatternDialectInterface>();
2743 });
2744}
assert(baseType &&"element must be base type")
static std::unique_ptr< Context > context
static bool onlyInvalidated(Value arg)
Check that all connections to a value are invalids.
static std::optional< firrtl::FModuleOp > findInstantiatedModule(firrtl::InstanceOp instOp, ::detail::SymbolCache &symbols)
Utility to easily get the instantiated firrtl::FModuleOp or an empty optional in case another type of...
false return failure()
Parse an optional keyword or string and set instance into 'result'.
if(parseOptionalKeywordOrOptionalString(parser, result.rawName, found)) return failure()
return success()
StringRef newName(const Twine &name)
Return a unique name, derived from the input name, and add the new name to the internal namespace.
Definition Namespace.h:86
bool removeAnnotations(llvm::function_ref< bool(Annotation)> predicate)
Remove all annotations from this annotation set for which predicate returns true.
static bool removePortAnnotations(Operation *module, llvm::function_ref< bool(unsigned, Annotation)> predicate)
Remove all port annotations from a module or extmodule for which predicate returns true.
Attribute getAttr() const
Get the underlying attribute.
AttrClass getMember(StringAttr name) const
Return a member of the annotation.
bool isClass(Args... names) const
Return true if this annotation matches any of the specified class names.
ArrayRef< hw::HierPathOp > lookup(Operation *op)
Lookup all NLAs an operation participates in.
Definition NLATable.cpp:41
void addNLA(hw::HierPathOp nla)
Insert a new NLA.
Definition NLATable.cpp:57
void erase(hw::HierPathOp nlaOp, SymbolTable *symbolTable=nullptr)
Remove the NLA from the analysis.
Definition NLATable.cpp:67
Helper class to cache tie-off values for different FIRRTL types.
Definition FIRRTLUtils.h:75
Value getUnknown(PropertyType type)
Get or create an UnknownValueOp for the given property type.
The target of an inner symbol, the entity the symbol is a handle for.
This class represents a collection of InnerSymbolTable's.
InnerSymbolTable & getInnerSymbolTable(Operation *op)
Get or create the InnerSymbolTable for the specified operation.
static RetTy walkSymbols(Operation *op, FuncTy &&callback)
Walk the given IST operation and invoke the callback for all encountered inner symbols.
static bool isMetasyntacticName(StringRef name)
Return true if name already has a metasyntactic prefix, i.e.
connect(destination, source)
Definition support.py:39
@ None
Don't explicitly preserve any named values.
Definition Passes.h:58
void registerReducePatternDialectInterface(mlir::DialectRegistry &registry)
Register the FIRRTL Reduction pattern dialect interface to the given registry.
SmallSet< SymbolRefAttr, 4, LayerSetCompare > LayerSet
Definition LayerSet.h:43
BaseTy type_dyn_cast(Type type)
std::optional< TokenAnnoTarget > tokenizePath(StringRef origTarget)
Parse a FIRRTL annotation path into its constituent parts.
StringAttr getName(ArrayAttr names, size_t idx)
Return the name at the specified index of the ArrayAttr or null if it cannot be determined.
ModulePort::Direction flip(ModulePort::Direction direction)
Flip a port direction.
Definition HWOps.cpp:36
void info(Twine message)
Definition LSPUtils.cpp:20
void pruneUnusedOps(SmallVectorImpl< Operation * > &worklist, Reduction &reduction)
Starting from an initial worklist of operations, traverse through it and its operands and erase opera...
The InstanceGraph op interface, see InstanceGraphInterface.td for more details.
Reduction that removes the convention attribute from external modules.
bool isOneShot() const override
Return true if the tool should not try to reapply this reduction after it has been successful.
uint64_t match(FExtModuleOp extmodule) override
std::string getName() const override
Return a human-readable name for this reduction pattern.
LogicalResult rewrite(FExtModuleOp extmodule) override
bool acceptSizeIncrease() const override
Return true if the tool should accept the transformation this reduction performs on the module even i...
IMDCEPortReduction(MLIRContext *context)
std::string getName() const override
Return a human-readable name for this reduction pattern.
A reduction pattern that removes elements from FIRRTL list create operations.
LogicalResult rewriteMatches(ListCreateOp listOp, ArrayRef< uint64_t > matches) override
void matches(ListCreateOp listOp, llvm::function_ref< void(uint64_t, uint64_t)> addMatch) override
std::string getName() const override
Return a human-readable name for this reduction pattern.
Reduction that removes the convention attribute from regular modules.
uint64_t match(FModuleOp module) override
std::string getName() const override
Return a human-readable name for this reduction pattern.
bool acceptSizeIncrease() const override
Return true if the tool should accept the transformation this reduction performs on the module even i...
LogicalResult rewrite(FModuleOp module) override
bool isOneShot() const override
Return true if the tool should not try to reapply this reduction after it has been successful.
Pseudo-reduction that sanitizes the names of operations inside modules.
bool acceptSizeIncrease() const override
Return true if the tool should accept the transformation this reduction performs on the module even i...
uint64_t match(Operation *op) override
Check if the reduction can apply to a specific operation.
LogicalResult rewrite(Operation *op) override
Apply the reduction to a specific operation.
std::string getName() const override
Return a human-readable name for this reduction pattern.
bool isOneShot() const override
Return true if the tool should not try to reapply this reduction after it has been successful.
Pseudo-reduction that sanitizes module and port names.
std::string getName() const override
Return a human-readable name for this reduction pattern.
bool isOneShot() const override
Return true if the tool should not try to reapply this reduction after it has been successful.
bool acceptSizeIncrease() const override
Return true if the tool should accept the transformation this reduction performs on the module even i...
LogicalResult rewrite(mlir::ModuleOp moduleOp) override
Utility to track the transitive size of modules.
llvm::DenseMap< Operation *, uint64_t > moduleSizes
uint64_t getModuleSize(Operation *module, ::detail::SymbolCache &symbols)
A tracker for track NLAs affected by a reduction.
void remove(mlir::ModuleOp module)
Remove all marked annotations.
void clear()
Clear the set of marked NLAs. Call this before attempting a reduction.
llvm::DenseSet< StringAttr > nlasToRemove
The set of NLAs to remove, identified by their symbol.
void markNLAsInAnnotation(Attribute anno)
Mark all NLAs referenced in the given annotation as to be removed.
void markNLAsInOperation(Operation *op)
Mark all NLAs referenced in an operation.
A reduction pattern for a specific operation.
Definition Reduction.h:112
A reduction pattern that applies an mlir::Pass.
Definition Reduction.h:142
MLIRContext *const context
Definition Reduction.h:152
PassReduction(MLIRContext *context, std::unique_ptr< Pass > pass, bool canIncreaseSize=false, bool oneShot=false)
Definition Reduction.cpp:33
An abstract reduction pattern.
Definition Reduction.h:24
virtual void beforeReduction(mlir::ModuleOp)
Called before the reduction is applied to a new subset of operations.
Definition Reduction.h:30
A dialect interface to provide reduction patterns to a reducer tool.
void populateReducePatterns(circt::ReducePatternSet &patterns) const override
A helper struct that scans a root operation and all its nested operations for InnerRefAttrs.
A utility doing lazy construction of SymbolTables and SymbolUserMaps, which is handy for reductions t...
std::unique_ptr< SymbolTableCollection > tables
SymbolUserMap & getSymbolUserMap(Operation *op)
SymbolUserMap & getNearestSymbolUserMap(Operation *op)
SymbolTable & getNearestSymbolTable(Operation *op)
SmallDenseMap< Operation *, SymbolUserMap, 2 > userMaps
SymbolTable & getSymbolTable(Operation *op)