circt/lib/Dialect/FIRRTL/Transforms/ExtractInstances.cpp

1076 lines
45 KiB
C++

//===- ExtractInstances.cpp - Move instances up the hierarchy ---*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Moves annotated instances upwards in the module hierarchy. Corresponds to the
// `ExtractBlackBoxes`, `ExtractClockGates`, and `ExtractSeqMems` passes in the
// Scala FIRRTL implementation.
//
//===----------------------------------------------------------------------===//
#include "PassDetails.h"
#include "circt/Dialect/FIRRTL/AnnotationDetails.h"
#include "circt/Dialect/FIRRTL/FIRRTLAnnotations.h"
#include "circt/Dialect/FIRRTL/FIRRTLInstanceGraph.h"
#include "circt/Dialect/FIRRTL/FIRRTLUtils.h"
#include "circt/Dialect/FIRRTL/NLATable.h"
#include "circt/Dialect/FIRRTL/Namespace.h"
#include "circt/Dialect/FIRRTL/Passes.h"
#include "circt/Dialect/HW/HWAttributes.h"
#include "circt/Dialect/HW/HWDialect.h"
#include "circt/Dialect/HW/InnerSymbolNamespace.h"
#include "circt/Dialect/SV/SVOps.h"
#include "circt/Support/Path.h"
#include "mlir/IR/Attributes.h"
#include "mlir/IR/ImplicitLocOpBuilder.h"
#include "mlir/Support/FileUtilities.h"
#include "llvm/ADT/SmallPtrSet.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#define DEBUG_TYPE "firrtl-extract-instances"
using namespace circt;
using namespace firrtl;
using hw::InnerRefAttr;
//===----------------------------------------------------------------------===//
// Pass Implementation
//===----------------------------------------------------------------------===//
namespace {
/// All information necessary to move instances about.
struct ExtractionInfo {
/// A filename into which the performed hierarchy modifications are emitted.
StringRef traceFilename;
/// A prefix to attach to the wiring generated by the extraction.
StringRef prefix;
/// Optional name of the wrapper module that will hold the moved instance.
StringRef wrapperModule;
/// Whether the extraction should stop at the root of the DUT instead of going
/// past that and extracting into the test harness.
bool stopAtDUT;
};
struct ExtractInstancesPass
: public ExtractInstancesBase<ExtractInstancesPass> {
void runOnOperation() override;
void collectAnnos();
void collectAnno(InstanceOp inst, Annotation anno);
void extractInstances();
void groupInstances();
void createTraceFiles();
/// Get the cached namespace for a module.
hw::InnerSymbolNamespace &getModuleNamespace(FModuleLike module) {
return moduleNamespaces.try_emplace(module, module).first->second;
}
/// Obtain an inner reference to an operation, possibly adding an `inner_sym`
/// to that operation.
InnerRefAttr getInnerRefTo(Operation *op) {
return ::getInnerRefTo(op,
[&](FModuleLike mod) -> hw::InnerSymbolNamespace & {
return getModuleNamespace(mod);
});
}
/// Create a clone of a `HierPathOp` with a new uniquified name.
hw::HierPathOp cloneWithNewNameAndPath(hw::HierPathOp pathOp,
ArrayRef<Attribute> newPath) {
OpBuilder builder(pathOp);
auto newPathOp = builder.cloneWithoutRegions(pathOp);
newPathOp.setSymNameAttr(builder.getStringAttr(
circuitNamespace.newName(newPathOp.getSymName())));
newPathOp.setNamepathAttr(builder.getArrayAttr(newPath));
return newPathOp;
}
bool anythingChanged;
bool anyFailures;
InstanceGraph *instanceGraph = nullptr;
/// The modules in the design that are annotated with one or more annotations
/// relevant for instance extraction.
DenseMap<Operation *, SmallVector<Annotation, 1>> annotatedModules;
/// All modules that are marked as DUT themselves. Realistically this is only
/// ever one module in the design.
DenseSet<Operation *> dutRootModules;
/// All modules that are marked as DUT themselves, or that have a DUT parent
/// module.
DenseSet<Operation *> dutModules;
/// All DUT module names.
DenseSet<Attribute> dutModuleNames;
/// The prefix of the DUT module. This is used when creating new modules
/// under the DUT.
StringRef dutPrefix = "";
/// A worklist of instances that need to be moved.
SmallVector<std::pair<InstanceOp, ExtractionInfo>> extractionWorklist;
/// The path along which instances have been extracted. This essentially
/// documents the original location of the instance in reverse. Every push
/// upwards in the hierarchy adds another entry to this path documenting along
/// which instantiation path each instance was extracted.
DenseMap<Operation *, SmallVector<InnerRefAttr>> extractionPaths;
/// A map of the original parent modules of instances before they were
/// extracted. This is used in a corner case during trace file emission.
DenseMap<Operation *, StringAttr> originalInstanceParents;
/// All extracted instances in their position after moving upwards in the
/// hierarchy, but before being grouped into an optional submodule.
SmallVector<std::pair<InstanceOp, ExtractionInfo>> extractedInstances;
// The uniquified wiring prefix for each instance.
DenseMap<Operation *, SmallString<16>> instPrefices;
/// The current circuit namespace valid within the call to `runOnOperation`.
CircuitNamespace circuitNamespace;
/// Cached module namespaces.
DenseMap<Operation *, hw::InnerSymbolNamespace> moduleNamespaces;
};
} // end anonymous namespace
/// Emit the annotated source code for black boxes in a circuit.
void ExtractInstancesPass::runOnOperation() {
CircuitOp circuitOp = getOperation();
anythingChanged = false;
anyFailures = false;
annotatedModules.clear();
dutRootModules.clear();
dutModules.clear();
extractionWorklist.clear();
extractionPaths.clear();
originalInstanceParents.clear();
extractedInstances.clear();
instPrefices.clear();
moduleNamespaces.clear();
circuitNamespace.clear();
circuitNamespace.add(circuitOp);
// Walk the IR and gather all the annotations relevant for extraction that
// appear on instances and the instantiated modules.
instanceGraph = &getAnalysis<InstanceGraph>();
collectAnnos();
if (anyFailures)
return signalPassFailure();
// Actually move instances upwards.
extractInstances();
if (anyFailures)
return signalPassFailure();
// Group instances into submodules, if requested.
groupInstances();
if (anyFailures)
return signalPassFailure();
// Generate the trace files that list where each instance was extracted from.
createTraceFiles();
if (anyFailures)
return signalPassFailure();
// If nothing has changed we can preserve the analysis.
LLVM_DEBUG(llvm::dbgs() << "\n");
if (!anythingChanged)
markAllAnalysesPreserved();
}
static bool isAnnoInteresting(Annotation anno) {
return anno.isClass(extractBlackBoxAnnoClass);
}
/// Gather the modules and instances annotated to be moved by this pass. This
/// populates the corresponding lists and maps of the pass.
void ExtractInstancesPass::collectAnnos() {
CircuitOp circuit = getOperation();
// Grab the clock gate extraction annotation on the circuit.
StringRef clkgateFileName;
StringRef clkgateWrapperModule;
AnnotationSet::removeAnnotations(circuit, [&](Annotation anno) {
if (!anno.isClass(extractClockGatesAnnoClass))
return false;
LLVM_DEBUG(llvm::dbgs()
<< "Clock gate extraction config: " << anno.getDict() << "\n");
auto filenameAttr = anno.getMember<StringAttr>("filename");
auto groupAttr = anno.getMember<StringAttr>("group");
if (!filenameAttr) {
circuit.emitError("missing `filename` attribute in `")
<< anno.getClass() << "` annotation";
anyFailures = true;
return true;
}
if (!clkgateFileName.empty()) {
circuit.emitError("multiple `")
<< anno.getClass() << "` annotations on circuit";
anyFailures = true;
return true;
}
clkgateFileName = filenameAttr.getValue();
if (groupAttr)
clkgateWrapperModule = groupAttr.getValue();
return true;
});
// Grab the memory extraction annotation on the circuit.
StringRef memoryFileName;
StringRef memoryWrapperModule;
AnnotationSet::removeAnnotations(circuit, [&](Annotation anno) {
if (!anno.isClass(extractSeqMemsAnnoClass))
return false;
LLVM_DEBUG(llvm::dbgs()
<< "Memory extraction config: " << anno.getDict() << "\n");
auto filenameAttr = anno.getMember<StringAttr>("filename");
auto groupAttr = anno.getMember<StringAttr>("group");
if (!filenameAttr) {
circuit.emitError("missing `filename` attribute in `")
<< anno.getClass() << "` annotation";
anyFailures = true;
return true;
}
if (!memoryFileName.empty()) {
circuit.emitError("multiple `")
<< anno.getClass() << "` annotations on circuit";
anyFailures = true;
return true;
}
memoryFileName = filenameAttr.getValue();
if (groupAttr)
memoryWrapperModule = groupAttr.getValue();
return true;
});
// Gather the annotations on modules. These complement the later per-instance
// annotations.
for (auto module : circuit.getOps<FModuleLike>()) {
AnnotationSet::removeAnnotations(module, [&](Annotation anno) {
if (anno.isClass(dutAnnoClass)) {
LLVM_DEBUG(llvm::dbgs()
<< "Marking DUT `" << module.getModuleName() << "`\n");
dutRootModules.insert(module);
dutModules.insert(module);
if (auto prefix = anno.getMember<StringAttr>("prefix"))
dutPrefix = prefix;
return false; // other passes may rely on this anno; keep it
}
if (!isAnnoInteresting(anno))
return false;
LLVM_DEBUG(llvm::dbgs() << "Annotated module `" << module.getModuleName()
<< "`:\n " << anno.getDict() << "\n");
annotatedModules[module].push_back(anno);
return true;
});
}
// Gather the annotations on instances to be extracted.
circuit.walk([&](InstanceOp inst) {
SmallVector<Annotation, 1> instAnnos;
Operation *module = inst.getReferencedModule(*instanceGraph);
// Module-level annotations.
auto it = annotatedModules.find(module);
if (it != annotatedModules.end())
instAnnos.append(it->second);
// Instance-level annotations.
AnnotationSet::removeAnnotations(inst, [&](Annotation anno) {
if (!isAnnoInteresting(anno))
return false;
LLVM_DEBUG(llvm::dbgs() << "Annotated instance `" << inst.getName()
<< "`:\n " << anno.getDict() << "\n");
instAnnos.push_back(anno);
return true;
});
// No need to do anything about unannotated instances.
if (instAnnos.empty())
return;
// Ensure there are no conflicting annotations.
if (instAnnos.size() > 1) {
auto d = inst.emitError("multiple extraction annotations on instance `")
<< inst.getName() << "`";
d.attachNote(inst.getLoc()) << "instance has the following annotations, "
"but at most one is allowed:";
for (auto anno : instAnnos)
d.attachNote(inst.getLoc()) << anno.getDict();
anyFailures = true;
return;
}
// Process the annotation.
collectAnno(inst, instAnnos[0]);
});
// Propagate the DUT marking to all arbitrarily nested submodules of the DUT.
LLVM_DEBUG(llvm::dbgs() << "Marking DUT hierarchy\n");
SmallVector<InstanceGraphNode *> worklist;
for (Operation *op : dutModules)
worklist.push_back(
instanceGraph->lookup(cast<igraph::ModuleOpInterface>(op)));
while (!worklist.empty()) {
auto *module = worklist.pop_back_val();
dutModuleNames.insert(module->getModule().getModuleNameAttr());
LLVM_DEBUG(llvm::dbgs()
<< "- " << module->getModule().getModuleName() << "\n");
for (auto *instRecord : *module) {
auto *target = instRecord->getTarget();
if (dutModules.insert(target->getModule()).second)
worklist.push_back(target);
}
}
// If clock gate extraction is requested, find instances of extmodules with
// the corresponding `defname` and mark them as to be extracted.
// TODO: This defname really shouldn't be hardcoded here. Make this at least
// somewhat configurable.
if (!clkgateFileName.empty()) {
auto clkgateDefNameAttr = StringAttr::get(&getContext(), "EICG_wrapper");
for (auto module : circuit.getOps<FExtModuleOp>()) {
if (module.getDefnameAttr() != clkgateDefNameAttr)
continue;
LLVM_DEBUG(llvm::dbgs()
<< "Clock gate `" << module.getModuleName() << "`\n");
if (!dutModules.contains(module)) {
LLVM_DEBUG(llvm::dbgs() << "- Ignored (outside DUT)\n");
continue;
}
ExtractionInfo info;
info.traceFilename = clkgateFileName;
info.prefix = "clock_gate"; // TODO: Don't hardcode this
info.wrapperModule = clkgateWrapperModule;
info.stopAtDUT = !info.wrapperModule.empty();
for (auto *instRecord : instanceGraph->lookup(module)->uses()) {
if (auto inst = dyn_cast<InstanceOp>(*instRecord->getInstance())) {
LLVM_DEBUG(llvm::dbgs()
<< "- Marking `"
<< inst->getParentOfType<FModuleLike>().getModuleName()
<< "." << inst.getName() << "`\n");
extractionWorklist.push_back({inst, info});
}
}
}
}
// If memory extraction is requested, find instances of `FMemModuleOp` and
// mark them as to be extracted.
// somewhat configurable.
if (!memoryFileName.empty()) {
// Create an empty verbatim to guarantee that this file will exist even if
// no memories are found. This is done to align with the SFC implementation
// of this pass where the file is always created. This does introduce an
// additional leading newline in the file.
auto *context = circuit.getContext();
auto builder = ImplicitLocOpBuilder::atBlockEnd(UnknownLoc::get(context),
circuit.getBodyBlock());
builder.create<sv::VerbatimOp>("")->setAttr(
"output_file",
hw::OutputFileAttr::getFromFilename(context, memoryFileName,
/*excludeFromFilelist=*/true));
for (auto module : circuit.getOps<FMemModuleOp>()) {
LLVM_DEBUG(llvm::dbgs() << "Memory `" << module.getModuleName() << "`\n");
if (!dutModules.contains(module)) {
LLVM_DEBUG(llvm::dbgs() << "- Ignored (outside DUT)\n");
continue;
}
ExtractionInfo info;
info.traceFilename = memoryFileName;
info.prefix = "mem_wiring"; // TODO: Don't hardcode this
info.wrapperModule = memoryWrapperModule;
info.stopAtDUT = !info.wrapperModule.empty();
for (auto *instRecord : instanceGraph->lookup(module)->uses()) {
if (auto inst = dyn_cast<InstanceOp>(*instRecord->getInstance())) {
LLVM_DEBUG(llvm::dbgs()
<< "- Marking `"
<< inst->getParentOfType<FModuleLike>().getModuleName()
<< "." << inst.getName() << "`\n");
extractionWorklist.push_back({inst, info});
}
}
}
}
}
/// Process an extraction annotation on an instance into a corresponding
/// `ExtractionInfo` and a spot on the worklist for later moving things around.
void ExtractInstancesPass::collectAnno(InstanceOp inst, Annotation anno) {
LLVM_DEBUG(llvm::dbgs() << "Processing instance `" << inst.getName() << "` "
<< anno.getDict() << "\n");
auto getStringOrError = [&](StringRef member) {
auto attr = anno.getMember<StringAttr>(member);
if (!attr) {
inst.emitError("missing `")
<< member << "` attribute in `" << anno.getClass() << "` annotation";
anyFailures = true;
}
return attr;
};
if (anno.isClass(extractBlackBoxAnnoClass)) {
auto filename = getStringOrError("filename");
auto prefix = getStringOrError("prefix");
auto dest = anno.getMember<StringAttr>("dest"); // optional
if (anyFailures)
return;
ExtractionInfo info;
info.traceFilename = filename;
info.prefix = prefix;
info.wrapperModule = (dest ? dest.getValue() : "");
// CAVEAT: If the instance has a wrapper module configured then extraction
// should stop at the DUT module instead of extracting past the DUT into the
// surrounding test harness. This is all very ugly and hacky.
info.stopAtDUT = !info.wrapperModule.empty();
extractionWorklist.push_back({inst, info});
return;
}
}
/// Find the location in an NLA that corresponds to a given instance (either by
/// mentioning exactly the instance, or the instance's parent module). Returns a
/// position within the NLA's path, or the length of the path if the instances
/// was not found.
static unsigned findInstanceInNLA(InstanceOp inst, hw::HierPathOp nla) {
unsigned nlaLen = nla.getNamepath().size();
auto instName = getInnerSymName(inst);
auto parentName = cast<FModuleOp>(inst->getParentOp()).getModuleNameAttr();
for (unsigned nlaIdx = 0; nlaIdx < nlaLen; ++nlaIdx) {
auto refPart = nla.refPart(nlaIdx);
if (nla.modPart(nlaIdx) == parentName && (!refPart || refPart == instName))
return nlaIdx;
}
return nlaLen;
}
/// Move instances in the extraction worklist upwards in the hierarchy. This
/// iteratively pushes instances up one level of hierarchy until they have
/// arrived in the desired container module.
void ExtractInstancesPass::extractInstances() {
// The list of ports to be added to an instance's parent module. Cleared and
// reused across instances.
SmallVector<std::pair<unsigned, PortInfo>> newPorts;
// The number of instances with the same prefix. Used to uniquify prefices.
DenseMap<StringRef, unsigned> prefixUniqueIDs;
SmallPtrSet<Operation *, 4> nlasToRemove;
auto &nlaTable = getAnalysis<NLATable>();
// Keep track of where the instance was originally.
for (auto &[inst, info] : extractionWorklist)
originalInstanceParents[inst] =
inst->getParentOfType<FModuleLike>().getModuleNameAttr();
while (!extractionWorklist.empty()) {
InstanceOp inst;
ExtractionInfo info;
std::tie(inst, info) = extractionWorklist.pop_back_val();
auto parent = inst->getParentOfType<FModuleOp>();
// Figure out the wiring prefix to use for this instance. If we are supposed
// to use a wiring prefix (`info.prefix` is non-empty), we assemble a
// `<prefix>_<N>` string, where `N` is an unsigned integer used to uniquifiy
// the prefix. This is very close to what the original Scala implementation
// of the pass does, which would group instances to be extracted by prefix
// and then iterate over them with the index in the group being used as `N`.
StringRef prefix;
if (!info.prefix.empty()) {
auto &prefixSlot = instPrefices[inst];
if (prefixSlot.empty()) {
auto idx = prefixUniqueIDs[info.prefix]++;
(Twine(info.prefix) + "_" + Twine(idx)).toVector(prefixSlot);
}
prefix = prefixSlot;
}
// If the instance is already in the right place (outside the DUT or already
// in the root module), there's nothing left for us to do. Otherwise we
// proceed to bubble it up one level in the hierarchy and add the resulting
// instances back to the worklist.
if (!dutModules.contains(parent) ||
instanceGraph->lookup(parent)->noUses() ||
(info.stopAtDUT && dutRootModules.contains(parent))) {
LLVM_DEBUG(llvm::dbgs() << "\nNo need to further move " << inst << "\n");
extractedInstances.push_back({inst, info});
continue;
}
LLVM_DEBUG({
llvm::dbgs() << "\nMoving ";
if (!prefix.empty())
llvm::dbgs() << "`" << prefix << "` ";
llvm::dbgs() << inst << "\n";
});
// Add additional ports to the parent module as a replacement for the
// instance port signals once the instance is extracted.
unsigned numParentPorts = parent.getNumPorts();
unsigned numInstPorts = inst.getNumResults();
for (unsigned portIdx = 0; portIdx < numInstPorts; ++portIdx) {
// Assemble the new port name as "<prefix>_<name>", where the prefix is
// provided by the extraction annotation.
auto name = inst.getPortNameStr(portIdx);
auto nameAttr = StringAttr::get(
&getContext(),
prefix.empty() ? Twine(name) : Twine(prefix) + "_" + name);
PortInfo newPort{nameAttr,
type_cast<FIRRTLType>(inst.getResult(portIdx).getType()),
direction::flip(inst.getPortDirection(portIdx))};
newPort.loc = inst.getResult(portIdx).getLoc();
newPorts.push_back({numParentPorts, newPort});
LLVM_DEBUG(llvm::dbgs()
<< "- Adding port " << newPort.direction << " "
<< newPort.name.getValue() << ": " << newPort.type << "\n");
}
parent.insertPorts(newPorts);
anythingChanged = true;
// Replace all uses of the existing instance ports with the newly-created
// module ports.
for (unsigned portIdx = 0; portIdx < numInstPorts; ++portIdx) {
inst.getResult(portIdx).replaceAllUsesWith(
parent.getArgument(numParentPorts + portIdx));
}
assert(inst.use_empty() && "instance ports should have been detached");
DenseSet<hw::HierPathOp> instanceNLAs;
// Get the NLAs that pass through the InstanceOp `inst`.
// This does not returns NLAs that have the `inst` as the leaf.
nlaTable.getInstanceNLAs(inst, instanceNLAs);
// Map of the NLAs, that are applied to the InstanceOp. That is the NLA
// terminates on the InstanceOp.
DenseMap<hw::HierPathOp, SmallVector<Annotation>> instNonlocalAnnos;
AnnotationSet::removeAnnotations(inst, [&](Annotation anno) {
// Only consider annotations with a `circt.nonlocal` field.
auto nlaName = anno.getMember<FlatSymbolRefAttr>("circt.nonlocal");
if (!nlaName)
return false;
// Track the NLA.
if (hw::HierPathOp nla = nlaTable.getNLA(nlaName.getAttr())) {
instNonlocalAnnos[nla].push_back(anno);
instanceNLAs.insert(nla);
}
return true;
});
// Sort the instance NLAs we've collected by the NLA name to have a
// deterministic output.
SmallVector<hw::HierPathOp> sortedInstanceNLAs(instanceNLAs.begin(),
instanceNLAs.end());
llvm::sort(sortedInstanceNLAs,
[](auto a, auto b) { return a.getSymName() < b.getSymName(); });
// Move the original instance one level up such that it is right next to
// the instances of the parent module, and wire the instance ports up to
// the newly added parent module ports.
auto *instParentNode =
instanceGraph->lookup(cast<igraph::ModuleOpInterface>(*parent));
for (auto *instRecord : instParentNode->uses()) {
auto oldParentInst = cast<InstanceOp>(*instRecord->getInstance());
auto newParent = oldParentInst->getParentOfType<FModuleLike>();
LLVM_DEBUG(llvm::dbgs() << "- Updating " << oldParentInst << "\n");
auto newParentInst = oldParentInst.cloneAndInsertPorts(newPorts);
// Migrate connections to existing ports.
for (unsigned portIdx = 0; portIdx < numParentPorts; ++portIdx)
oldParentInst.getResult(portIdx).replaceAllUsesWith(
newParentInst.getResult(portIdx));
// Clone the existing instance and remove it from its current parent, such
// that we can insert it at its extracted location.
auto newInst = inst.cloneAndInsertPorts({});
newInst->remove();
// Ensure that the `inner_sym` of the instance is unique within the parent
// module we're extracting it to.
if (auto instSym = getInnerSymName(inst)) {
auto newName =
getModuleNamespace(newParent).newName(instSym.getValue());
if (newName != instSym.getValue())
newInst.setInnerSymAttr(
hw::InnerSymAttr::get(StringAttr::get(&getContext(), newName)));
}
// Add the moved instance and hook it up to the added ports.
ImplicitLocOpBuilder builder(inst.getLoc(), newParentInst);
builder.setInsertionPointAfter(newParentInst);
builder.insert(newInst);
for (unsigned portIdx = 0; portIdx < numInstPorts; ++portIdx) {
auto dst = newInst.getResult(portIdx);
auto src = newParentInst.getResult(numParentPorts + portIdx);
if (newPorts[portIdx].second.direction == Direction::In)
std::swap(src, dst);
builder.create<StrictConnectOp>(dst, src);
}
// Move the wiring prefix from the old to the new instance. We just look
// up the prefix for the old instance and if it exists, we remove it and
// assign it to the new instance. This has the effect of making the first
// new instance we create inherit the wiring prefix, and all additional
// new instances (e.g. through multiple instantiation of the parent) will
// pick a new prefix.
auto oldPrefix = instPrefices.find(inst);
if (oldPrefix != instPrefices.end()) {
LLVM_DEBUG(llvm::dbgs()
<< " - Reusing prefix `" << oldPrefix->second << "`\n");
auto newPrefix = std::move(oldPrefix->second);
instPrefices.erase(oldPrefix);
instPrefices.insert({newInst, newPrefix});
}
// Inherit the old instance's extraction path.
extractionPaths.try_emplace(newInst); // (create entry first)
auto &extractionPath = (extractionPaths[newInst] = extractionPaths[inst]);
extractionPath.push_back(getInnerRefTo(newParentInst));
originalInstanceParents.try_emplace(newInst); // (create entry first)
originalInstanceParents[newInst] = originalInstanceParents[inst];
// Record the Nonlocal annotations that need to be applied to the new
// Inst.
SmallVector<Annotation> newInstNonlocalAnnos;
// Update all NLAs that touch the moved instance.
for (auto nla : sortedInstanceNLAs) {
LLVM_DEBUG(llvm::dbgs() << " - Updating " << nla << "\n");
// Find the position of the instance in the NLA path. This is going to
// be the position at which we have to modify the NLA.
SmallVector<Attribute> nlaPath(nla.getNamepath().begin(),
nla.getNamepath().end());
unsigned nlaIdx = findInstanceInNLA(inst, nla);
// Handle the case where the instance no longer shows up in the NLA's
// path. This usually happens if the instance is extracted into multiple
// parents (because the current parent module is multiply instantiated).
// In that case NLAs that were specific to one instance may have been
// moved when we arrive at the second instance, and the NLA is already
// updated.
if (nlaIdx >= nlaPath.size()) {
LLVM_DEBUG(llvm::dbgs() << " - Instance no longer in path\n");
continue;
}
LLVM_DEBUG(llvm::dbgs() << " - Position " << nlaIdx << "\n");
// Handle the case where the NLA's path doesn't go through the
// instance's new parent module, which happens if the current parent
// module is multiply instantiated. In that case, we only move over NLAs
// that actually affect the instance through the new parent module.
if (nlaIdx > 0) {
auto innerRef = dyn_cast<InnerRefAttr>(nlaPath[nlaIdx - 1]);
if (innerRef &&
!(innerRef.getModule() == newParent.getModuleNameAttr() &&
innerRef.getName() == getInnerSymName(newParentInst))) {
LLVM_DEBUG(llvm::dbgs()
<< " - Ignored since NLA parent " << innerRef
<< " does not pass through extraction parent\n");
continue;
}
}
// There are two interesting cases now:
// - If `nlaIdx == 0`, the NLA is rooted at the module the instance was
// located in prior to extraction. This indicates that the NLA applies
// to all instances of that parent module. Since we are extracting
// *out* of that module, we have to create a new NLA rooted at the new
// parent module after extraction.
// - If `nlaIdx > 0`, the NLA is rooted further up in the hierarchy and
// we can simply remove the old parent module from the path.
// Handle the case where we need to come up with a new NLA for this
// instance since we've moved it past the module at which the old NLA
// was rooted at.
if (nlaIdx == 0) {
LLVM_DEBUG(llvm::dbgs() << " - Re-rooting " << nlaPath[0] << "\n");
assert(isa<InnerRefAttr>(nlaPath[0]) &&
"head of hierpath must be an InnerRefAttr");
nlaPath[0] = InnerRefAttr::get(newParent.getModuleNameAttr(),
getInnerSymName(newInst));
if (instParentNode->hasOneUse()) {
// Simply update the existing NLA since our parent is only
// instantiated once, and we therefore are not creating multiple
// instances through the extraction.
nlaTable.erase(nla);
nla.setNamepathAttr(builder.getArrayAttr(nlaPath));
for (auto anno : instNonlocalAnnos.lookup(nla))
newInstNonlocalAnnos.push_back(anno);
nlaTable.addNLA(nla);
LLVM_DEBUG(llvm::dbgs() << " - Modified to " << nla << "\n");
} else {
// Since we are extracting to multiple parent locations, create a
// new NLA for each instantiation site.
auto newNla = cloneWithNewNameAndPath(nla, nlaPath);
for (auto anno : instNonlocalAnnos.lookup(nla)) {
anno.setMember("circt.nonlocal",
FlatSymbolRefAttr::get(newNla.getSymNameAttr()));
newInstNonlocalAnnos.push_back(anno);
}
nlaTable.addNLA(newNla);
LLVM_DEBUG(llvm::dbgs() << " - Created " << newNla << "\n");
// CAVEAT(fschuiki): This results in annotations in the subhierarchy
// below `inst` with the old NLA symbol name, instead of those
// annotations duplicated for each of the newly-created NLAs. This
// shouldn't come up in our current use cases, but is a weakness of
// the current implementation. Instead, we should keep an NLA
// replication table that we fill with mappings from old NLA names
// to lists of new NLA names. A post-pass would then traverse the
// entire subhierarchy and go replicate all annotations with the old
// names.
inst.emitWarning("extraction of instance `")
<< inst.getInstanceName()
<< "` could break non-local annotations rooted at `"
<< parent.getModuleName() << "`";
}
continue;
}
// In the subequent code block we are going to remove one element from
// the NLA path, corresponding to the fact that the extracted instance
// has moved up in the hierarchy by one level. Removing that element may
// leave the NLA in a degenerate state, with only a single element in
// its path. If that is the case we have to convert the NLA into a
// regular local annotation.
if (nlaPath.size() == 2) {
for (auto anno : instNonlocalAnnos.lookup(nla)) {
anno.removeMember("circt.nonlocal");
newInstNonlocalAnnos.push_back(anno);
LLVM_DEBUG(llvm::dbgs() << " - Converted to local "
<< anno.getDict() << "\n");
}
nlaTable.erase(nla);
nlasToRemove.insert(nla);
continue;
}
// At this point the NLA looks like `NewParent::X, OldParent::BB`, and
// the `nlaIdx` points at `OldParent::BB`. To make our lives easier,
// since we know that `nlaIdx` is a `InnerRefAttr`, we'll modify
// `OldParent::BB` to be `NewParent::BB` and delete `NewParent::X`.
StringAttr parentName =
cast<InnerRefAttr>(nlaPath[nlaIdx - 1]).getModule();
Attribute newRef;
if (isa<InnerRefAttr>(nlaPath[nlaIdx]))
newRef = InnerRefAttr::get(parentName, getInnerSymName(newInst));
else
newRef = FlatSymbolRefAttr::get(parentName);
LLVM_DEBUG(llvm::dbgs()
<< " - Replacing " << nlaPath[nlaIdx - 1] << " and "
<< nlaPath[nlaIdx] << " with " << newRef << "\n");
nlaPath[nlaIdx] = newRef;
nlaPath.erase(nlaPath.begin() + nlaIdx - 1);
if (isa<FlatSymbolRefAttr>(newRef)) {
// Since the original NLA ended at the instance's parent module, there
// is no guarantee that the instance is the sole user of the NLA (as
// opposed to the original NLA explicitly naming the instance). Create
// a new NLA.
auto newNla = cloneWithNewNameAndPath(nla, nlaPath);
nlaTable.addNLA(newNla);
LLVM_DEBUG(llvm::dbgs() << " - Created " << newNla << "\n");
for (auto anno : instNonlocalAnnos.lookup(nla)) {
anno.setMember("circt.nonlocal",
FlatSymbolRefAttr::get(newNla.getSymNameAttr()));
newInstNonlocalAnnos.push_back(anno);
}
} else {
nla.setNamepathAttr(builder.getArrayAttr(nlaPath));
LLVM_DEBUG(llvm::dbgs() << " - Modified to " << nla << "\n");
for (auto anno : instNonlocalAnnos.lookup(nla))
newInstNonlocalAnnos.push_back(anno);
}
// No update to NLATable required, since it will be deleted from the
// parent, and it should already exist in the new parent module.
continue;
}
AnnotationSet newInstAnnos(newInst);
newInstAnnos.addAnnotations(newInstNonlocalAnnos);
newInstAnnos.applyToOperation(newInst);
// Add the moved instance to the extraction worklist such that it gets
// bubbled up further if needed.
extractionWorklist.push_back({newInst, info});
LLVM_DEBUG(llvm::dbgs() << " - Updated to " << newInst << "\n");
// Keep instance graph up-to-date.
instanceGraph->replaceInstance(oldParentInst, newParentInst);
oldParentInst.erase();
}
// Remove the obsolete NLAs from the instance of the parent module, since
// the extracted instance no longer resides in that module and any NLAs to
// it no longer go through the parent module.
nlaTable.removeNLAsfromModule(instanceNLAs, parent.getNameAttr());
// Clean up the original instance.
inst.erase();
newPorts.clear();
}
// Remove unused NLAs.
for (Operation *op : nlasToRemove) {
LLVM_DEBUG(llvm::dbgs() << "Removing obsolete " << *op << "\n");
op->erase();
}
}
/// Group instances into submodules after they have been moved upwards. This
/// only occurs for instances that had the corresponding `dest` field of the
/// annotation set.
void ExtractInstancesPass::groupInstances() {
// Group the extracted instances by their wrapper module name and their parent
// module. Note that we cannot group instances that landed in different parent
// modules into the same submodule, so we use that parent module as a grouping
// key.
llvm::MapVector<std::pair<Operation *, StringRef>, SmallVector<InstanceOp>>
instsByWrapper;
for (auto &[inst, info] : extractedInstances) {
if (!info.wrapperModule.empty())
instsByWrapper[{inst->getParentOfType<FModuleOp>(), info.wrapperModule}]
.push_back(inst);
}
if (instsByWrapper.empty())
return;
LLVM_DEBUG(llvm::dbgs() << "\nGrouping instances into wrappers\n");
// Generate the wrappers.
SmallVector<PortInfo> ports;
auto &nlaTable = getAnalysis<NLATable>();
for (auto &[parentAndWrapperName, insts] : instsByWrapper) {
auto [parentOp, wrapperName] = parentAndWrapperName;
auto parent = cast<FModuleOp>(parentOp);
LLVM_DEBUG(llvm::dbgs() << "- Wrapper `" << wrapperName << "` in `"
<< parent.getModuleName() << "` with "
<< insts.size() << " instances\n");
OpBuilder builder(parentOp);
// Uniquify the wrapper name.
auto wrapperModuleName = builder.getStringAttr(
circuitNamespace.newName(dutPrefix + wrapperName));
auto wrapperInstName =
builder.getStringAttr(getModuleNamespace(parent).newName(wrapperName));
// Assemble a list of ports for the wrapper module, which is basically just
// a concatenation of the wrapped instance ports. Also keep track of the
// NLAs that target the grouped instances since these will have to pass
// through the wrapper module.
ports.clear();
for (auto inst : insts) {
// Determine the ports for the wrapper.
StringRef prefix(instPrefices[inst]);
unsigned portNum = inst.getNumResults();
for (unsigned portIdx = 0; portIdx < portNum; ++portIdx) {
auto name = inst.getPortNameStr(portIdx);
auto nameAttr = builder.getStringAttr(
prefix.empty() ? Twine(name) : Twine(prefix) + "_" + name);
PortInfo port{nameAttr,
type_cast<FIRRTLType>(inst.getResult(portIdx).getType()),
inst.getPortDirection(portIdx)};
port.loc = inst.getResult(portIdx).getLoc();
ports.push_back(port);
}
// Set of NLAs that have a reference to this InstanceOp `inst`.
DenseSet<hw::HierPathOp> instNlas;
// Get the NLAs that pass through the `inst`, and not end at it.
nlaTable.getInstanceNLAs(inst, instNlas);
AnnotationSet instAnnos(inst);
// Get the NLAs that end at the InstanceOp, that is the Nonlocal
// annotations that apply to the InstanceOp.
for (auto anno : instAnnos) {
auto nlaName = anno.getMember<FlatSymbolRefAttr>("circt.nonlocal");
if (!nlaName)
continue;
hw::HierPathOp nla = nlaTable.getNLA(nlaName.getAttr());
if (nla)
instNlas.insert(nla);
}
for (auto nla : instNlas) {
LLVM_DEBUG(llvm::dbgs() << " - Updating " << nla << "\n");
// Find the position of the instance in the NLA path. This is going to
// be the position at which we have to modify the NLA.
SmallVector<Attribute> nlaPath(nla.getNamepath().begin(),
nla.getNamepath().end());
unsigned nlaIdx = findInstanceInNLA(inst, nla);
assert(nlaIdx < nlaPath.size() && "instance not found in its own NLA");
LLVM_DEBUG(llvm::dbgs() << " - Position " << nlaIdx << "\n");
// The relevant part of the NLA is of the form `Top::bb`, which we want
// to expand to `Top::wrapperInst` and `Wrapper::bb`.
auto ref1 =
InnerRefAttr::get(parent.getModuleNameAttr(), wrapperInstName);
Attribute ref2;
if (auto innerRef = dyn_cast<InnerRefAttr>(nlaPath[nlaIdx]))
ref2 = InnerRefAttr::get(wrapperModuleName, innerRef.getName());
else
ref2 = FlatSymbolRefAttr::get(wrapperModuleName);
LLVM_DEBUG(llvm::dbgs() << " - Expanding " << nlaPath[nlaIdx]
<< " to (" << ref1 << ", " << ref2 << ")\n");
nlaPath[nlaIdx] = ref1;
nlaPath.insert(nlaPath.begin() + nlaIdx + 1, ref2);
// CAVEAT: This is likely to conflict with additional users of `nla`
// that have nothing to do with this instance. Might need some NLATable
// machinery at some point to allow for these things to be updated.
nla.setNamepathAttr(builder.getArrayAttr(nlaPath));
LLVM_DEBUG(llvm::dbgs() << " - Modified to " << nla << "\n");
// Add the NLA to the wrapper module.
nlaTable.addNLAtoModule(nla, wrapperModuleName);
}
}
// Create the wrapper module.
auto wrapper = builder.create<FModuleOp>(
builder.getUnknownLoc(), wrapperModuleName,
ConventionAttr::get(builder.getContext(), Convention::Internal), ports);
SymbolTable::setSymbolVisibility(wrapper, SymbolTable::Visibility::Private);
// Instantiate the wrapper module in the parent and replace uses of the
// extracted instances' ports with the corresponding wrapper module ports.
// This will essentially disconnect the extracted instances.
builder.setInsertionPointToStart(parent.getBodyBlock());
auto wrapperInst = builder.create<InstanceOp>(
wrapper.getLoc(), wrapper, wrapperName, NameKindEnum::DroppableName,
ArrayRef<Attribute>{},
/*portAnnotations=*/ArrayRef<Attribute>{}, /*lowerToBind=*/false,
hw::InnerSymAttr::get(wrapperInstName));
unsigned portIdx = 0;
for (auto inst : insts)
for (auto result : inst.getResults())
result.replaceAllUsesWith(wrapperInst.getResult(portIdx++));
// Move all instances into the wrapper module and wire them up to the
// wrapper ports.
portIdx = 0;
builder.setInsertionPointToStart(wrapper.getBodyBlock());
for (auto inst : insts) {
inst->remove();
builder.insert(inst);
for (auto result : inst.getResults()) {
Value dst = result;
Value src = wrapper.getArgument(portIdx);
if (ports[portIdx].direction == Direction::Out)
std::swap(dst, src);
builder.create<StrictConnectOp>(result.getLoc(), dst, src);
++portIdx;
}
}
}
}
/// Generate trace files, which are plain text metadata files that list the
/// hierarchical path where each instance was extracted from. The file lists one
/// instance per line in the form `<prefix> -> <original-path>`.
void ExtractInstancesPass::createTraceFiles() {
LLVM_DEBUG(llvm::dbgs() << "\nGenerating trace files\n");
auto builder = OpBuilder::atBlockEnd(getOperation().getBodyBlock());
// Group the extracted instances by their trace file name.
SmallDenseMap<StringRef, SmallVector<InstanceOp>> instsByTraceFile;
for (auto &[inst, info] : extractedInstances)
if (!info.traceFilename.empty())
instsByTraceFile[info.traceFilename].push_back(inst);
// Generate the trace files.
SmallVector<Attribute> symbols;
SmallDenseMap<Attribute, unsigned> symbolIndices;
for (auto &[fileName, insts] : instsByTraceFile) {
LLVM_DEBUG(llvm::dbgs() << "- " << fileName << "\n");
std::string buffer;
llvm::raw_string_ostream os(buffer);
symbols.clear();
symbolIndices.clear();
auto addSymbol = [&](Attribute symbol) {
unsigned id;
auto it = symbolIndices.find(symbol);
if (it != symbolIndices.end()) {
id = it->second;
} else {
id = symbols.size();
symbols.push_back(symbol);
symbolIndices.insert({symbol, id});
}
os << "{{" << id << "}}";
};
for (auto inst : insts) {
StringRef prefix(instPrefices[inst]);
if (prefix.empty()) {
LLVM_DEBUG(llvm::dbgs() << " - Skipping `" << inst.getName()
<< "` since it has no extraction prefix\n");
continue;
}
ArrayRef<InnerRefAttr> path(extractionPaths[inst]);
if (path.empty()) {
LLVM_DEBUG(llvm::dbgs() << " - Skipping `" << inst.getName()
<< "` since it has not been moved\n");
continue;
}
LLVM_DEBUG(llvm::dbgs()
<< " - " << prefix << ": " << inst.getName() << "\n");
os << prefix << " -> ";
// HACK: To match the Scala implementation, we strip all non-DUT modules
// from the path and make the path look like it's rooted at the first DUT
// module (so `TestHarness.dut.foo.bar` becomes `DUTModule.foo.bar`).
while (!path.empty() &&
!dutModuleNames.contains(path.back().getModule())) {
LLVM_DEBUG(llvm::dbgs()
<< " - Dropping non-DUT segment " << path.back() << "\n");
path = path.drop_back();
}
// HACK: This is extremely ugly. In case the instance was just moved by a
// single level, the path may become empty. In that case we simply use the
// instance's original parent before it was moved.
addSymbol(FlatSymbolRefAttr::get(path.empty()
? originalInstanceParents[inst]
: path.back().getModule()));
for (auto sym : llvm::reverse(path)) {
os << ".";
addSymbol(sym);
}
// The final instance name is excluded as this does not provide useful
// additional information and could conflict with a name inside the final
// module.
os << "\n";
}
// Put the information in a verbatim operation.
auto verbatimOp = builder.create<sv::VerbatimOp>(
builder.getUnknownLoc(), buffer, ValueRange{},
builder.getArrayAttr(symbols));
auto fileAttr = hw::OutputFileAttr::getFromFilename(
builder.getContext(), fileName, /*excludeFromFilelist=*/true);
verbatimOp->setAttr("output_file", fileAttr);
}
}
//===----------------------------------------------------------------------===//
// Pass Creation
//===----------------------------------------------------------------------===//
std::unique_ptr<mlir::Pass> circt::firrtl::createExtractInstancesPass() {
return std::make_unique<ExtractInstancesPass>();
}