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

467 lines
18 KiB
C++

//===- BlackBoxReader.cpp - Ingest black box sources ------------*- 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
//===----------------------------------------------------------------------===//
//
// Read Verilog source files for black boxes based on corresponding black box
// annotations on the circuit and modules. Primarily based on:
//
// https://github.com/chipsalliance/firrtl/blob/master/src/main/scala/firrtl/
// transforms/BlackBoxSourceHelper.scala
//
//===----------------------------------------------------------------------===//
#include "circt/Dialect/Emit/EmitOps.h"
#include "circt/Dialect/FIRRTL/AnnotationDetails.h"
#include "circt/Dialect/FIRRTL/FIRRTLAnnotations.h"
#include "circt/Dialect/FIRRTL/FIRRTLInstanceGraph.h"
#include "circt/Dialect/FIRRTL/FIRRTLOps.h"
#include "circt/Dialect/FIRRTL/FIRRTLUtils.h"
#include "circt/Dialect/FIRRTL/Namespace.h"
#include "circt/Dialect/FIRRTL/Passes.h"
#include "circt/Dialect/HW/HWAttributes.h"
#include "circt/Support/Debug.h"
#include "circt/Support/Path.h"
#include "mlir/IR/Attributes.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Support/FileUtilities.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/FormatAdapters.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#define DEBUG_TYPE "firrtl-blackbox-reader"
namespace circt {
namespace firrtl {
#define GEN_PASS_DEF_BLACKBOXREADER
#include "circt/Dialect/FIRRTL/Passes.h.inc"
} // namespace firrtl
} // namespace circt
using namespace circt;
using namespace firrtl;
//===----------------------------------------------------------------------===//
// Pass Implementation
//===----------------------------------------------------------------------===//
namespace {
/// Data extracted from BlackBoxInlineAnno or BlackBoxPathAnno.
struct AnnotationInfo {
/// The name of the file that should be created for this BlackBox.
StringAttr name;
/// The output directory information for this extmodule.
hw::OutputFileAttr outputFileAttr;
/// The body of the BlackBox. (This should be Verilog text.)
StringAttr inlineText;
#if !defined(NDEBUG)
/// Pretty print the AnnotationInfo in a YAML-esque format.
void print(raw_ostream &os, unsigned indent = 0) const {
os << llvm::formatv("name: {1}\n"
"{0}outputFile: {2}\n"
"{0}exclude: {3}\n",
llvm::fmt_pad("", indent, 0), name,
outputFileAttr.getFilename(),
outputFileAttr.getExcludeFromFilelist().getValue());
};
#endif
};
struct BlackBoxReaderPass
: public circt::firrtl::impl::BlackBoxReaderBase<BlackBoxReaderPass> {
void runOnOperation() override;
bool runOnAnnotation(Operation *op, Annotation anno, OpBuilder &builder,
bool isCover, AnnotationInfo &annotationInfo);
StringAttr loadFile(Operation *op, StringRef inputPath, OpBuilder &builder);
hw::OutputFileAttr getOutputFile(Operation *origOp, StringAttr fileNameAttr,
bool isCover = false);
// Check if module or any of its parents in the InstanceGraph is a DUT.
bool isDut(Operation *module);
using BlackBoxReaderBase::inputPrefix;
private:
/// A list of all files which will be included in the file list. This is
/// subset of all emitted files.
SmallVector<emit::FileOp> fileListFiles;
/// The target directory to output black boxes into. Can be changed
/// through `firrtl.transforms.BlackBoxTargetDirAnno` annotations.
StringRef targetDir;
/// The target directory for cover statements.
StringRef coverDir;
/// The target directory for testbench files.
StringRef testBenchDir;
/// The design-under-test (DUT) as indicated by the presence of a
/// "sifive.enterprise.firrtl.MarkDUTAnnotation". This will be null if no
/// annotation is present.
FModuleLike dut;
/// The file list file name (sic) for black boxes. If set, generates a file
/// that lists all non-header source files for black boxes. Can be changed
/// through `firrtl.transforms.BlackBoxResourceFileNameAnno` annotations.
StringRef resourceFileName;
/// InstanceGraph to determine modules which are under the DUT.
InstanceGraph *instanceGraph;
/// A cache of the modules which have been marked as DUT or a testbench.
/// This is used to determine the output directory.
DenseMap<Operation *, bool> dutModuleMap;
/// An ordered map of Verilog filenames to the annotation-derived information
/// that will be used to create this file. Due to situations where multiple
/// external modules may not deduplicate (e.g., they have different
/// parameters), multiple annotations may all want to write to the same file.
/// This always tracks the actual annotation that will be used. If a more
/// appropriate annotation is found (e.g., which will cause the file to be
/// written to the DUT directory and not the TestHarness directory), then this
/// will map will be updated.
llvm::MapVector<StringAttr, AnnotationInfo> emittedFileMap;
};
} // end anonymous namespace
/// Emit the annotated source code for black boxes in a circuit.
void BlackBoxReaderPass::runOnOperation() {
LLVM_DEBUG(debugPassHeader(this) << "\n");
CircuitOp circuitOp = getOperation();
CircuitNamespace ns(circuitOp);
instanceGraph = &getAnalysis<InstanceGraph>();
auto context = &getContext();
// If this pass has changed anything.
bool anythingChanged = false;
// Internalize some string attributes for easy reference later.
// Determine the target directory and resource file name from the
// annotations present on the circuit operation.
targetDir = ".";
resourceFileName = "firrtl_black_box_resource_files.f";
// Process black box annotations on the circuit. Some of these annotations
// will affect how the rest of the annotations are resolved.
SmallVector<Attribute, 4> filteredAnnos;
for (auto annot : AnnotationSet(circuitOp)) {
// Handle resource file name annotation.
if (annot.isClass(blackBoxResourceFileNameAnnoClass)) {
if (auto resourceFN = annot.getMember<StringAttr>("resourceFileName")) {
resourceFileName = resourceFN.getValue();
continue;
}
circuitOp->emitError(blackBoxResourceFileNameAnnoClass)
<< " annotation missing \"resourceFileName\" attribute";
signalPassFailure();
continue;
}
filteredAnnos.push_back(annot.getDict());
// Get the testbench and cover directories.
if (annot.isClass(extractCoverageAnnoClass))
if (auto dir = annot.getMember<StringAttr>("directory")) {
coverDir = dir.getValue();
continue;
}
if (annot.isClass(testBenchDirAnnoClass))
if (auto dir = annot.getMember<StringAttr>("dirname")) {
testBenchDir = dir.getValue();
continue;
}
// Handle target dir annotation.
if (annot.isClass(blackBoxTargetDirAnnoClass)) {
if (auto target = annot.getMember<StringAttr>("targetDir")) {
targetDir = target.getValue();
continue;
}
circuitOp->emitError(blackBoxTargetDirAnnoClass)
<< " annotation missing \"targetDir\" attribute";
signalPassFailure();
continue;
}
}
// Apply the filtered annotations to the circuit. If we updated the circuit
// and record that they changed.
anythingChanged |=
AnnotationSet(filteredAnnos, context).applyToOperation(circuitOp);
LLVM_DEBUG(llvm::dbgs() << "Black box target directory: " << targetDir << "\n"
<< "Black box resource file name: "
<< resourceFileName << "\n");
// Newly generated IR will be placed at the end of the circuit.
auto builder = circuitOp.getBodyBuilder();
// Do a shallow walk of the circuit to collect information necessary before we
// do real work.
for (auto &op : *circuitOp.getBodyBlock()) {
FModuleLike module = dyn_cast<FModuleLike>(op);
// Find the DUT if it exists or error if there are multiple DUTs.
if (module)
if (failed(extractDUT(module, dut)))
return signalPassFailure();
}
LLVM_DEBUG(llvm::dbgs() << "Visiting extmodules:\n");
auto bboxAnno =
builder.getDictionaryAttr({{builder.getStringAttr("class"),
builder.getStringAttr(blackBoxAnnoClass)}});
for (auto extmoduleOp : circuitOp.getBodyBlock()->getOps<FExtModuleOp>()) {
LLVM_DEBUG({
llvm::dbgs().indent(2)
<< "- name: " << extmoduleOp.getModuleNameAttr() << "\n";
llvm::dbgs().indent(4) << "annotations:\n";
});
AnnotationSet annotations(extmoduleOp);
bool isCover =
!coverDir.empty() && annotations.hasAnnotation(verifBlackBoxAnnoClass);
bool foundBBoxAnno = false;
annotations.removeAnnotations([&](Annotation anno) {
AnnotationInfo annotationInfo;
if (!runOnAnnotation(extmoduleOp, anno, builder, isCover, annotationInfo))
return false;
LLVM_DEBUG(annotationInfo.print(llvm::dbgs().indent(6) << "- ", 8));
// If we have seen a black box trying to create a blackbox with this
// filename before, then compute the lowest commmon ancestor between the
// two blackbox paths. This is the same logic used in `AssignOutputDirs`.
// However, this needs to incorporate filenames that are only available
// _after_ output directories are assigned.
auto [ptr, inserted] =
emittedFileMap.try_emplace(annotationInfo.name, annotationInfo);
if (inserted) {
emittedFileMap[annotationInfo.name] = annotationInfo;
} else {
auto &fileAttr = ptr->second.outputFileAttr;
SmallString<64> directory(fileAttr.getDirectory());
makeCommonPrefix(directory,
annotationInfo.outputFileAttr.getDirectory());
fileAttr = hw::OutputFileAttr::getFromDirectoryAndFilename(
context, directory, annotationInfo.name.getValue(),
/*excludeFromFileList=*/
fileAttr.getExcludeFromFilelist().getValue());
// TODO: Check that the new text is the _exact same_ as the prior best.
}
foundBBoxAnno = true;
return true;
});
if (foundBBoxAnno) {
annotations.addAnnotations({bboxAnno});
anythingChanged = true;
}
annotations.applyToOperation(extmoduleOp);
}
LLVM_DEBUG(llvm::dbgs() << "emittedFiles:\n");
Location loc = builder.getUnknownLoc();
for (auto &[verilogName, annotationInfo] : emittedFileMap) {
LLVM_DEBUG({
llvm::dbgs().indent(2) << "verilogName: " << verilogName << "\n";
llvm::dbgs().indent(2) << "annotationInfo:\n";
annotationInfo.print(llvm::dbgs().indent(4) << "- ", 6);
});
auto fileName = ns.newName("blackbox_" + verilogName.getValue());
auto fileOp = builder.create<emit::FileOp>(
loc, annotationInfo.outputFileAttr.getFilename(), fileName,
[&, text = annotationInfo.inlineText] {
builder.create<emit::VerbatimOp>(loc, text);
});
if (!annotationInfo.outputFileAttr.getExcludeFromFilelist().getValue())
fileListFiles.push_back(fileOp);
}
// If we have emitted any files, generate a file list operation that
// documents the additional annotation-controlled file listing to be
// created.
if (!fileListFiles.empty()) {
// Output the file list in sorted order.
llvm::sort(fileListFiles.begin(), fileListFiles.end(),
[](emit::FileOp fileA, emit::FileOp fileB) {
return fileA.getFileName() < fileB.getFileName();
});
// Create the file list contents by enumerating the symbols to the files.
SmallVector<Attribute> symbols;
for (emit::FileOp file : fileListFiles)
symbols.push_back(FlatSymbolRefAttr::get(file.getSymNameAttr()));
builder.create<emit::FileListOp>(
loc, builder.getStringAttr(resourceFileName),
builder.getArrayAttr(symbols),
builder.getStringAttr(ns.newName("blackbox_filelist")));
}
// If nothing has changed we can preserve the analysis.
if (!anythingChanged)
markAllAnalysesPreserved();
markAnalysesPreserved<InstanceGraph>();
// Clean up.
emittedFileMap.clear();
fileListFiles.clear();
LLVM_DEBUG(debugFooter() << "\n");
}
/// Run on an operation-annotation pair. The annotation need not be a black box
/// annotation. Returns `true` if the annotation was indeed a black box
/// annotation (even if it was incomplete) and should be removed from the op.
bool BlackBoxReaderPass::runOnAnnotation(Operation *op, Annotation anno,
OpBuilder &builder, bool isCover,
AnnotationInfo &annotationInfo) {
// Handle inline annotation.
if (anno.isClass(blackBoxInlineAnnoClass)) {
auto name = anno.getMember<StringAttr>("name");
auto text = anno.getMember<StringAttr>("text");
if (!name || !text) {
op->emitError(blackBoxInlineAnnoClass)
<< " annotation missing \"name\" or \"text\" attribute";
signalPassFailure();
return true;
}
annotationInfo.outputFileAttr = getOutputFile(op, name, isCover);
annotationInfo.name = name;
annotationInfo.inlineText = text;
return true;
}
// Handle path annotation.
if (anno.isClass(blackBoxPathAnnoClass)) {
auto path = anno.getMember<StringAttr>("path");
if (!path) {
op->emitError(blackBoxPathAnnoClass)
<< " annotation missing \"path\" attribute";
signalPassFailure();
return true;
}
SmallString<128> inputPath(inputPrefix);
appendPossiblyAbsolutePath(inputPath, path.getValue());
auto text = loadFile(op, inputPath, builder);
if (!text) {
op->emitError("Cannot find file ") << inputPath;
signalPassFailure();
return false;
}
auto name = builder.getStringAttr(llvm::sys::path::filename(path));
annotationInfo.outputFileAttr = getOutputFile(op, name, isCover);
annotationInfo.name = name;
annotationInfo.inlineText = text;
return true;
}
// Annotation was not concerned with black boxes.
return false;
}
/// Copies a black box source file to the appropriate location in the target
/// directory.
StringAttr BlackBoxReaderPass::loadFile(Operation *op, StringRef inputPath,
OpBuilder &builder) {
LLVM_DEBUG(llvm::dbgs() << "Add black box source `"
<< llvm::sys::path::filename(inputPath) << "` from `"
<< inputPath << "`\n");
// Open and read the input file.
std::string errorMessage;
auto input = mlir::openInputFile(inputPath, &errorMessage);
if (!input)
return {};
// Return a StringAttr with the buffer contents.
return builder.getStringAttr(input->getBuffer());
}
/// Determine the output file for some operation.
hw::OutputFileAttr BlackBoxReaderPass::getOutputFile(Operation *origOp,
StringAttr fileNameAttr,
bool isCover) {
auto outputFile = origOp->getAttrOfType<hw::OutputFileAttr>("output_file");
if (outputFile && !outputFile.isDirectory()) {
return {outputFile};
}
// Exclude Verilog header files since we expect them to be included
// explicitly by compiler directives in other source files.
auto *context = &getContext();
auto fileName = fileNameAttr.getValue();
auto ext = llvm::sys::path::extension(fileName);
bool exclude = (ext == ".h" || ext == ".vh" || ext == ".svh");
auto outDir = targetDir;
// If the original operation has a specified output file that is not a
// directory, then just use that.
if (outputFile)
outDir = outputFile.getFilename();
// In order to output into the testbench directory, we need to have a
// testbench dir annotation, not have a blackbox target directory annotation
// (or one set to the current directory), have a DUT annotation, and the
// module needs to be in or under the DUT.
else if (!testBenchDir.empty() && targetDir == "." && dut && !isDut(origOp))
outDir = testBenchDir;
else if (isCover)
outDir = coverDir;
// If targetDir is not set explicitly and this is a testbench module, then
// update the targetDir to be the "../testbench".
SmallString<128> outputFilePath(outDir);
llvm::sys::path::append(outputFilePath, fileName);
return hw::OutputFileAttr::getFromFilename(context, outputFilePath, exclude);
}
/// Return true if module is in the DUT hierarchy.
/// NOLINTNEXTLINE(misc-no-recursion)
bool BlackBoxReaderPass::isDut(Operation *module) {
// Check if result already cached.
auto iter = dutModuleMap.find(module);
if (iter != dutModuleMap.end())
return iter->getSecond();
// Any module with the dutAnno, is the DUT.
if (AnnotationSet::hasAnnotation(module, dutAnnoClass)) {
dutModuleMap[module] = true;
return true;
}
auto *node = instanceGraph->lookup(cast<igraph::ModuleOpInterface>(module));
bool anyParentIsDut = false;
if (node)
for (auto *u : node->uses()) {
if (cast<InstanceOp>(u->getInstance().getOperation()).getLowerToBind())
return false;
// Recursively check the parents.
auto dut = isDut(u->getInstance()->getParentOfType<FModuleOp>());
// Cache the result.
dutModuleMap[module] = dut;
anyParentIsDut |= dut;
}
dutModuleMap[module] = anyParentIsDut;
return anyParentIsDut;
}
//===----------------------------------------------------------------------===//
// Pass Creation
//===----------------------------------------------------------------------===//
std::unique_ptr<mlir::Pass>
circt::firrtl::createBlackBoxReaderPass(std::optional<StringRef> inputPrefix) {
auto pass = std::make_unique<BlackBoxReaderPass>();
if (inputPrefix)
pass->inputPrefix = inputPrefix->str();
return pass;
}