CIRCT  20.0.0git
BlackBoxReader.cpp
Go to the documentation of this file.
1 //===- BlackBoxReader.cpp - Ingest black box sources ------------*- C++ -*-===//
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 // Read Verilog source files for black boxes based on corresponding black box
9 // annotations on the circuit and modules. Primarily based on:
10 //
11 // https://github.com/chipsalliance/firrtl/blob/master/src/main/scala/firrtl/
12 // transforms/BlackBoxSourceHelper.scala
13 //
14 //===----------------------------------------------------------------------===//
15 
25 #include "circt/Support/Debug.h"
26 #include "circt/Support/Path.h"
27 #include "mlir/IR/Attributes.h"
28 #include "mlir/Pass/Pass.h"
29 #include "mlir/Support/FileUtilities.h"
30 #include "llvm/Support/Debug.h"
31 #include "llvm/Support/FormatAdapters.h"
32 #include "llvm/Support/FormatVariadic.h"
33 #include "llvm/Support/MemoryBuffer.h"
34 #include "llvm/Support/Path.h"
35 
36 #define DEBUG_TYPE "firrtl-blackbox-reader"
37 
38 namespace circt {
39 namespace firrtl {
40 #define GEN_PASS_DEF_BLACKBOXREADER
41 #include "circt/Dialect/FIRRTL/Passes.h.inc"
42 } // namespace firrtl
43 } // namespace circt
44 
45 using namespace circt;
46 using namespace firrtl;
47 
48 //===----------------------------------------------------------------------===//
49 // Pass Implementation
50 //===----------------------------------------------------------------------===//
51 
52 namespace {
53 
54 /// Data extracted from BlackBoxInlineAnno or BlackBoxPathAnno.
55 struct AnnotationInfo {
56  /// The name of the file that should be created for this BlackBox.
57  StringAttr name;
58  /// The output directory information for this extmodule.
59  hw::OutputFileAttr outputFileAttr;
60  /// The body of the BlackBox. (This should be Verilog text.)
61  StringAttr inlineText;
62 
63 #if !defined(NDEBUG)
64  /// Pretty print the AnnotationInfo in a YAML-esque format.
65  void print(raw_ostream &os, unsigned indent = 0) const {
66  os << llvm::formatv("name: {1}\n"
67  "{0}outputFile: {2}\n"
68  "{0}exclude: {3}\n",
69  llvm::fmt_pad("", indent, 0), name,
70  outputFileAttr.getFilename(),
71  outputFileAttr.getExcludeFromFilelist().getValue());
72  };
73 #endif
74 };
75 
76 struct BlackBoxReaderPass
77  : public circt::firrtl::impl::BlackBoxReaderBase<BlackBoxReaderPass> {
78  void runOnOperation() override;
79  bool runOnAnnotation(Operation *op, Annotation anno, OpBuilder &builder,
80  bool isCover, AnnotationInfo &annotationInfo);
81  StringAttr loadFile(Operation *op, StringRef inputPath, OpBuilder &builder);
82  hw::OutputFileAttr getOutputFile(Operation *origOp, StringAttr fileNameAttr,
83  bool isCover = false);
84  // Check if module or any of its parents in the InstanceGraph is a DUT.
85  bool isDut(Operation *module);
86 
87  using BlackBoxReaderBase::inputPrefix;
88 
89 private:
90  /// A list of all files which will be included in the file list. This is
91  /// subset of all emitted files.
92  SmallVector<emit::FileOp> fileListFiles;
93 
94  /// The target directory to output black boxes into. Can be changed
95  /// through `firrtl.transforms.BlackBoxTargetDirAnno` annotations.
96  StringRef targetDir;
97 
98  /// The target directory for cover statements.
99  StringRef coverDir;
100 
101  /// The target directory for testbench files.
102  StringRef testBenchDir;
103 
104  /// The design-under-test (DUT) as indicated by the presence of a
105  /// "sifive.enterprise.firrtl.MarkDUTAnnotation". This will be null if no
106  /// annotation is present.
107  FModuleLike dut;
108 
109  /// The file list file name (sic) for black boxes. If set, generates a file
110  /// that lists all non-header source files for black boxes. Can be changed
111  /// through `firrtl.transforms.BlackBoxResourceFileNameAnno` annotations.
112  StringRef resourceFileName;
113 
114  /// InstanceGraph to determine modules which are under the DUT.
115  InstanceGraph *instanceGraph;
116 
117  /// A cache of the modules which have been marked as DUT or a testbench.
118  /// This is used to determine the output directory.
119  DenseMap<Operation *, bool> dutModuleMap;
120 
121  /// An ordered map of Verilog filenames to the annotation-derived information
122  /// that will be used to create this file. Due to situations where multiple
123  /// external modules may not deduplicate (e.g., they have different
124  /// parameters), multiple annotations may all want to write to the same file.
125  /// This always tracks the actual annotation that will be used. If a more
126  /// appropriate annotation is found (e.g., which will cause the file to be
127  /// written to the DUT directory and not the TestHarness directory), then this
128  /// will map will be updated.
129  llvm::MapVector<StringAttr, AnnotationInfo> emittedFileMap;
130 };
131 } // end anonymous namespace
132 
133 /// Emit the annotated source code for black boxes in a circuit.
134 void BlackBoxReaderPass::runOnOperation() {
135  LLVM_DEBUG(debugPassHeader(this) << "\n");
136  CircuitOp circuitOp = getOperation();
137  CircuitNamespace ns(circuitOp);
138 
139  instanceGraph = &getAnalysis<InstanceGraph>();
140  auto context = &getContext();
141 
142  // If this pass has changed anything.
143  bool anythingChanged = false;
144 
145  // Internalize some string attributes for easy reference later.
146 
147  // Determine the target directory and resource file name from the
148  // annotations present on the circuit operation.
149  targetDir = ".";
150  resourceFileName = "firrtl_black_box_resource_files.f";
151 
152  // Process black box annotations on the circuit. Some of these annotations
153  // will affect how the rest of the annotations are resolved.
154  SmallVector<Attribute, 4> filteredAnnos;
155  for (auto annot : AnnotationSet(circuitOp)) {
156  // Handle resource file name annotation.
157  if (annot.isClass(blackBoxResourceFileNameAnnoClass)) {
158  if (auto resourceFN = annot.getMember<StringAttr>("resourceFileName")) {
159  resourceFileName = resourceFN.getValue();
160  continue;
161  }
162 
163  circuitOp->emitError(blackBoxResourceFileNameAnnoClass)
164  << " annotation missing \"resourceFileName\" attribute";
165  signalPassFailure();
166  continue;
167  }
168  filteredAnnos.push_back(annot.getDict());
169 
170  // Get the testbench and cover directories.
171  if (annot.isClass(extractCoverageAnnoClass))
172  if (auto dir = annot.getMember<StringAttr>("directory")) {
173  coverDir = dir.getValue();
174  continue;
175  }
176 
177  if (annot.isClass(testBenchDirAnnoClass))
178  if (auto dir = annot.getMember<StringAttr>("dirname")) {
179  testBenchDir = dir.getValue();
180  continue;
181  }
182 
183  // Handle target dir annotation.
184  if (annot.isClass(blackBoxTargetDirAnnoClass)) {
185  if (auto target = annot.getMember<StringAttr>("targetDir")) {
186  targetDir = target.getValue();
187  continue;
188  }
189  circuitOp->emitError(blackBoxTargetDirAnnoClass)
190  << " annotation missing \"targetDir\" attribute";
191  signalPassFailure();
192  continue;
193  }
194  }
195  // Apply the filtered annotations to the circuit. If we updated the circuit
196  // and record that they changed.
197  anythingChanged |=
198  AnnotationSet(filteredAnnos, context).applyToOperation(circuitOp);
199 
200  LLVM_DEBUG(llvm::dbgs() << "Black box target directory: " << targetDir << "\n"
201  << "Black box resource file name: "
202  << resourceFileName << "\n");
203 
204  // Newly generated IR will be placed at the end of the circuit.
205  auto builder = circuitOp.getBodyBuilder();
206 
207  // Do a shallow walk of the circuit to collect information necessary before we
208  // do real work.
209  for (auto &op : *circuitOp.getBodyBlock()) {
210  FModuleLike module = dyn_cast<FModuleLike>(op);
211  // Find the DUT if it exists or error if there are multiple DUTs.
212  if (module)
213  if (failed(extractDUT(module, dut)))
214  return signalPassFailure();
215  }
216 
217  LLVM_DEBUG(llvm::dbgs() << "Visiting extmodules:\n");
218  auto bboxAnno =
219  builder.getDictionaryAttr({{builder.getStringAttr("class"),
220  builder.getStringAttr(blackBoxAnnoClass)}});
221  for (auto extmoduleOp : circuitOp.getBodyBlock()->getOps<FExtModuleOp>()) {
222  LLVM_DEBUG({
223  llvm::dbgs().indent(2)
224  << "- name: " << extmoduleOp.getModuleNameAttr() << "\n";
225  llvm::dbgs().indent(4) << "annotations:\n";
226  });
227  AnnotationSet annotations(extmoduleOp);
228  bool isCover =
229  !coverDir.empty() && annotations.hasAnnotation(verifBlackBoxAnnoClass);
230  bool foundBBoxAnno = false;
231  annotations.removeAnnotations([&](Annotation anno) {
232  AnnotationInfo annotationInfo;
233  if (!runOnAnnotation(extmoduleOp, anno, builder, isCover, annotationInfo))
234  return false;
235 
236  LLVM_DEBUG(annotationInfo.print(llvm::dbgs().indent(6) << "- ", 8));
237 
238  // If we have seen a black box trying to create a blackbox with this
239  // filename before, then compute the lowest commmon ancestor between the
240  // two blackbox paths. This is the same logic used in `AssignOutputDirs`.
241  // However, this needs to incorporate filenames that are only available
242  // _after_ output directories are assigned.
243  auto [ptr, inserted] =
244  emittedFileMap.try_emplace(annotationInfo.name, annotationInfo);
245  if (inserted) {
246  emittedFileMap[annotationInfo.name] = annotationInfo;
247  } else {
248  auto &fileAttr = ptr->second.outputFileAttr;
249  SmallString<64> directory(fileAttr.getDirectory());
250  makeCommonPrefix(directory,
251  annotationInfo.outputFileAttr.getDirectory());
252  fileAttr = hw::OutputFileAttr::getFromDirectoryAndFilename(
253  context, directory, annotationInfo.name.getValue(),
254  /*excludeFromFileList=*/
255  fileAttr.getExcludeFromFilelist().getValue());
256  // TODO: Check that the new text is the _exact same_ as the prior best.
257  }
258 
259  foundBBoxAnno = true;
260  return true;
261  });
262 
263  if (foundBBoxAnno) {
264  annotations.addAnnotations({bboxAnno});
265  anythingChanged = true;
266  }
267  annotations.applyToOperation(extmoduleOp);
268  }
269 
270  LLVM_DEBUG(llvm::dbgs() << "emittedFiles:\n");
271  Location loc = builder.getUnknownLoc();
272  for (auto &[verilogName, annotationInfo] : emittedFileMap) {
273  LLVM_DEBUG({
274  llvm::dbgs().indent(2) << "verilogName: " << verilogName << "\n";
275  llvm::dbgs().indent(2) << "annotationInfo:\n";
276  annotationInfo.print(llvm::dbgs().indent(4) << "- ", 6);
277  });
278 
279  auto fileName = ns.newName("blackbox_" + verilogName.getValue());
280 
281  auto fileOp = builder.create<emit::FileOp>(
282  loc, annotationInfo.outputFileAttr.getFilename(), fileName,
283  [&, text = annotationInfo.inlineText] {
284  builder.create<emit::VerbatimOp>(loc, text);
285  });
286 
287  if (!annotationInfo.outputFileAttr.getExcludeFromFilelist().getValue())
288  fileListFiles.push_back(fileOp);
289  }
290 
291  // If we have emitted any files, generate a file list operation that
292  // documents the additional annotation-controlled file listing to be
293  // created.
294  if (!fileListFiles.empty()) {
295  // Output the file list in sorted order.
296  llvm::sort(fileListFiles.begin(), fileListFiles.end(),
297  [](emit::FileOp fileA, emit::FileOp fileB) {
298  return fileA.getFileName() < fileB.getFileName();
299  });
300 
301  // Create the file list contents by enumerating the symbols to the files.
302  SmallVector<Attribute> symbols;
303  for (emit::FileOp file : fileListFiles)
304  symbols.push_back(FlatSymbolRefAttr::get(file.getSymNameAttr()));
305 
306  builder.create<emit::FileListOp>(
307  loc, builder.getStringAttr(resourceFileName),
308  builder.getArrayAttr(symbols),
309  builder.getStringAttr(ns.newName("blackbox_filelist")));
310  }
311 
312  // If nothing has changed we can preserve the analysis.
313  if (!anythingChanged)
314  markAllAnalysesPreserved();
315  markAnalysesPreserved<InstanceGraph>();
316 
317  // Clean up.
318  emittedFileMap.clear();
319  fileListFiles.clear();
320  LLVM_DEBUG(debugFooter() << "\n");
321 }
322 
323 /// Run on an operation-annotation pair. The annotation need not be a black box
324 /// annotation. Returns `true` if the annotation was indeed a black box
325 /// annotation (even if it was incomplete) and should be removed from the op.
326 bool BlackBoxReaderPass::runOnAnnotation(Operation *op, Annotation anno,
327  OpBuilder &builder, bool isCover,
328  AnnotationInfo &annotationInfo) {
329  // Handle inline annotation.
330  if (anno.isClass(blackBoxInlineAnnoClass)) {
331  auto name = anno.getMember<StringAttr>("name");
332  auto text = anno.getMember<StringAttr>("text");
333  if (!name || !text) {
334  op->emitError(blackBoxInlineAnnoClass)
335  << " annotation missing \"name\" or \"text\" attribute";
336  signalPassFailure();
337  return true;
338  }
339 
340  annotationInfo.outputFileAttr = getOutputFile(op, name, isCover);
341  annotationInfo.name = name;
342  annotationInfo.inlineText = text;
343  return true;
344  }
345 
346  // Handle path annotation.
347  if (anno.isClass(blackBoxPathAnnoClass)) {
348  auto path = anno.getMember<StringAttr>("path");
349  if (!path) {
350  op->emitError(blackBoxPathAnnoClass)
351  << " annotation missing \"path\" attribute";
352  signalPassFailure();
353  return true;
354  }
355  SmallString<128> inputPath(inputPrefix);
356  appendPossiblyAbsolutePath(inputPath, path.getValue());
357  auto text = loadFile(op, inputPath, builder);
358  if (!text) {
359  op->emitError("Cannot find file ") << inputPath;
360  signalPassFailure();
361  return false;
362  }
363  auto name = builder.getStringAttr(llvm::sys::path::filename(path));
364  annotationInfo.outputFileAttr = getOutputFile(op, name, isCover);
365  annotationInfo.name = name;
366  annotationInfo.inlineText = text;
367  return true;
368  }
369 
370  // Annotation was not concerned with black boxes.
371  return false;
372 }
373 
374 /// Copies a black box source file to the appropriate location in the target
375 /// directory.
376 StringAttr BlackBoxReaderPass::loadFile(Operation *op, StringRef inputPath,
377  OpBuilder &builder) {
378  LLVM_DEBUG(llvm::dbgs() << "Add black box source `"
379  << llvm::sys::path::filename(inputPath) << "` from `"
380  << inputPath << "`\n");
381 
382  // Open and read the input file.
383  std::string errorMessage;
384  auto input = mlir::openInputFile(inputPath, &errorMessage);
385  if (!input)
386  return {};
387 
388  // Return a StringAttr with the buffer contents.
389  return builder.getStringAttr(input->getBuffer());
390 }
391 
392 /// Determine the output file for some operation.
393 hw::OutputFileAttr BlackBoxReaderPass::getOutputFile(Operation *origOp,
394  StringAttr fileNameAttr,
395  bool isCover) {
396  auto outputFile = origOp->getAttrOfType<hw::OutputFileAttr>("output_file");
397  if (outputFile && !outputFile.isDirectory()) {
398  return {outputFile};
399  }
400 
401  // Exclude Verilog header files since we expect them to be included
402  // explicitly by compiler directives in other source files.
403  auto *context = &getContext();
404  auto fileName = fileNameAttr.getValue();
405  auto ext = llvm::sys::path::extension(fileName);
406  bool exclude = (ext == ".h" || ext == ".vh" || ext == ".svh");
407  auto outDir = targetDir;
408  // If the original operation has a specified output file that is not a
409  // directory, then just use that.
410  if (outputFile)
411  outDir = outputFile.getFilename();
412  // In order to output into the testbench directory, we need to have a
413  // testbench dir annotation, not have a blackbox target directory annotation
414  // (or one set to the current directory), have a DUT annotation, and the
415  // module needs to be in or under the DUT.
416  else if (!testBenchDir.empty() && targetDir == "." && dut && !isDut(origOp))
417  outDir = testBenchDir;
418  else if (isCover)
419  outDir = coverDir;
420 
421  // If targetDir is not set explicitly and this is a testbench module, then
422  // update the targetDir to be the "../testbench".
423  SmallString<128> outputFilePath(outDir);
424  llvm::sys::path::append(outputFilePath, fileName);
425  return hw::OutputFileAttr::getFromFilename(context, outputFilePath, exclude);
426 }
427 
428 /// Return true if module is in the DUT hierarchy.
429 /// NOLINTNEXTLINE(misc-no-recursion)
430 bool BlackBoxReaderPass::isDut(Operation *module) {
431  // Check if result already cached.
432  auto iter = dutModuleMap.find(module);
433  if (iter != dutModuleMap.end())
434  return iter->getSecond();
435  // Any module with the dutAnno, is the DUT.
437  dutModuleMap[module] = true;
438  return true;
439  }
440  auto *node = instanceGraph->lookup(cast<igraph::ModuleOpInterface>(module));
441  bool anyParentIsDut = false;
442  if (node)
443  for (auto *u : node->uses()) {
444  if (cast<InstanceOp>(u->getInstance().getOperation()).getLowerToBind())
445  return false;
446  // Recursively check the parents.
447  auto dut = isDut(u->getInstance()->getParentOfType<FModuleOp>());
448  // Cache the result.
449  dutModuleMap[module] = dut;
450  anyParentIsDut |= dut;
451  }
452  dutModuleMap[module] = anyParentIsDut;
453  return anyParentIsDut;
454 }
455 
456 //===----------------------------------------------------------------------===//
457 // Pass Creation
458 //===----------------------------------------------------------------------===//
459 
460 std::unique_ptr<mlir::Pass>
461 circt::firrtl::createBlackBoxReaderPass(std::optional<StringRef> inputPrefix) {
462  auto pass = std::make_unique<BlackBoxReaderPass>();
463  if (inputPrefix)
464  pass->inputPrefix = inputPrefix->str();
465  return pass;
466 }
static OutputFileAttr getOutputFile(igraph::ModuleOpInterface op)
static StringAttr append(StringAttr base, const Twine &suffix)
Return a attribute with the specified suffix appended.
This class provides a read-only projection over the MLIR attributes that represent a set of annotatio...
bool applyToOperation(Operation *op) const
Store the annotations in this set in an operation's annotations attribute, overwriting any existing a...
bool hasAnnotation(StringRef className) const
Return true if we have an annotation with the specified class name.
This class provides a read-only projection of an annotation.
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.
This graph tracks modules and where they are instantiated.
Direction get(bool isOutput)
Returns an output direction if isOutput is true, otherwise returns an input direction.
Definition: CalyxOps.cpp:55
constexpr const char * extractCoverageAnnoClass
constexpr const char * blackBoxAnnoClass
constexpr const char * testBenchDirAnnoClass
constexpr const char * dutAnnoClass
std::unique_ptr< mlir::Pass > createBlackBoxReaderPass(std::optional< mlir::StringRef > inputPrefix={})
constexpr const char * verifBlackBoxAnnoClass
constexpr const char * blackBoxPathAnnoClass
LogicalResult extractDUT(FModuleLike mod, FModuleLike &dut)
Utility that searches for a MarkDUTAnnotation on a specific module, mod, and tries to update a design...
constexpr const char * blackBoxTargetDirAnnoClass
constexpr const char * blackBoxInlineAnnoClass
void makeCommonPrefix(SmallString< 64 > &a, StringRef b)
Truncate a to the common prefix of a and b.
constexpr const char * blackBoxResourceFileNameAnnoClass
The InstanceGraph op interface, see InstanceGraphInterface.td for more details.
Definition: DebugAnalysis.h:21
void appendPossiblyAbsolutePath(llvm::SmallVectorImpl< char > &base, const llvm::Twine &suffix)
Append a path to an existing path, replacing it if the other path is absolute.
Definition: Path.cpp:23
llvm::raw_ostream & debugPassHeader(const mlir::Pass *pass, int width=80)
Write a boilerplate header for a pass to the debug stream.
Definition: Debug.cpp:31
llvm::raw_ostream & debugFooter(int width=80)
Write a boilerplate footer to the debug stream to indicate that a pass has ended.
Definition: Debug.cpp:35
The namespace of a CircuitOp, generally inhabited by modules.
Definition: Namespace.h:24