From 4cd37091ef173b026cb98a70c46e9e498b65ffc3 Mon Sep 17 00:00:00 2001 From: Fabian Schuiki Date: Thu, 21 Oct 2021 20:26:43 +0200 Subject: [PATCH] [FIRRTL] Add OMIR emission pass (#2010) Add a pass to the FIRRTL dialect that consumes `OMIRAnnotation`s on the circuit, gathers the tracker nodes back up, and serializes the data back into a JSON blob. --- docs/FIRRTLAnnotations.md | 100 ++++ include/circt/Dialect/FIRRTL/Passes.h | 2 + include/circt/Dialect/FIRRTL/Passes.td | 13 + lib/Dialect/FIRRTL/Import/FIRAnnotations.cpp | 4 +- lib/Dialect/FIRRTL/Transforms/CMakeLists.txt | 1 + lib/Dialect/FIRRTL/Transforms/EmitOMIR.cpp | 514 +++++++++++++++++++ test/Dialect/FIRRTL/SFCTests/emit-omir.fir | 84 +++ test/Dialect/FIRRTL/emit-omir.mlir | 163 ++++++ test/Dialect/FIRRTL/omir.fir | 28 +- tools/firtool/firtool.cpp | 7 + 10 files changed, 901 insertions(+), 15 deletions(-) create mode 100644 lib/Dialect/FIRRTL/Transforms/EmitOMIR.cpp create mode 100644 test/Dialect/FIRRTL/SFCTests/emit-omir.fir create mode 100644 test/Dialect/FIRRTL/emit-omir.mlir diff --git a/docs/FIRRTLAnnotations.md b/docs/FIRRTLAnnotations.md index ce12d340031a..91b1d6273219 100644 --- a/docs/FIRRTLAnnotations.md +++ b/docs/FIRRTLAnnotations.md @@ -517,6 +517,106 @@ Example: } ``` +### OMIRFileAnnotation + +| Property | Type | Description | +| ---------- | ------ | ------------- | +| class | string | `freechips.rocketchip.objectmodel.OMIRFileAnnotation` | +| filename | string | Output file to emit OMIR to | + +This annotation defines the output file to write the JSON-serialized OMIR to after compilation. + +Example: +```json +{ + "class": "freechips.rocketchip.objectmodel.OMIRFileAnnotation", + "filename": "path/to/omir.json" +} +``` + +### OMIRAnnotation + +| Property | Type | Description | +| ---------- | ------ | ------------- | +| class | string | `freechips.rocketchip.objectmodel.OMIRAnnotation` | +| nodes | array | A list of OMIR nodes | + +This annotation specifies a piece of Object Model 2.0 IR. The `nodes` field +is an array of individual OMIR nodes (Scala class `OMNode`), which have the +following form: +```json +{ + "info": "@[FileA line:col FileB line:col ...]", + "id": "OMID:42", + "fields": [/*...*/] +} +``` +The `fields` entry is an array of individual OMIR fields (Scala class `OMField`), which have the following form: +```json +{ + "info": "@[FileA line:col FileB line:col ...]", + "name": "foo", + "value": /*...*/ +} +``` +The `value` field can be a JSON array or dictionary (corresponding to the `OMArray` and `OMMap` Scala classes, respectively), or any of the string-encoded OMIR classes: + +- `OMMap:` +- `OMArray:` +- `OMReference:` +- `OMBigInt:` +- `OMInt:` +- `OMLong:` +- `OMString:` +- `OMBoolean:` +- `OMDouble:` +- `OMBigDecimal:` +- `OMFrozenTarget:` +- `OMDeleted` +- `OMConstant:` +- `OMReferenceTarget:` +- `OMMemberReferenceTarget:` +- `OMMemberInstanceTarget:` +- `OMInstanceTarget:` +- `OMDontTouchedReferenceTarget:` + +Example: +```json +{ + "class": "freechips.rocketchip.objectmodel.OMIRAnnotation", + "nodes": [ + { + "info": "", + "id": "OMID:0", + "fields": [ + {"info": "", "name": "a", "value": "OMReference:0"}, + {"info": "", "name": "b", "value": "OMBigInt:42"}, + {"info": "", "name": "c", "value": "OMLong:ff"}, + {"info": "", "name": "d", "value": "OMString:hello"}, + {"info": "", "name": "f", "value": "OMBigDecimal:10.5"}, + {"info": "", "name": "g", "value": "OMDeleted:"}, + {"info": "", "name": "h", "value": "OMConstant:UInt<2>(\"h1\")"}, + {"info": "", "name": "i", "value": 42}, + {"info": "", "name": "j", "value": true}, + {"info": "", "name": "k", "value": 3.14} + ] + }, + { + "info": "", + "id": "OMID:1", + "fields": [ + {"info": "", "name": "a", "value": "OMReferenceTarget:~Foo|Foo"}, + {"info": "", "name": "b", "value": "OMInstanceTarget:~Foo|Foo"}, + {"info": "", "name": "c", "value": "OMMemberReferenceTarget:~Foo|Foo"}, + {"info": "", "name": "d", "value": "OMMemberInstanceTarget:~Foo|Foo"}, + {"info": "", "name": "e", "value": "OMDontTouchedReferenceTarget:~Foo|Foo"}, + {"info": "", "name": "f", "value": "OMReferenceTarget:~Foo|Bar"} + ] + } + ] +} +``` + ### RetimeModuleAnnotation | Property | Type | Description | diff --git a/include/circt/Dialect/FIRRTL/Passes.h b/include/circt/Dialect/FIRRTL/Passes.h index 36e551bd368d..da0afc3f0e03 100644 --- a/include/circt/Dialect/FIRRTL/Passes.h +++ b/include/circt/Dialect/FIRRTL/Passes.h @@ -45,6 +45,8 @@ createCreateSiFiveMetadataPass(bool replSeqMem = false, StringRef replSeqMemCircuit = "", StringRef replSeqMemFile = ""); +std::unique_ptr createEmitOMIRPass(StringRef outputFilename = ""); + std::unique_ptr createExpandWhensPass(); std::unique_ptr createInferWidthsPass(); diff --git a/include/circt/Dialect/FIRRTL/Passes.td b/include/circt/Dialect/FIRRTL/Passes.td index 0bd3fad8be67..12ab322cd632 100644 --- a/include/circt/Dialect/FIRRTL/Passes.td +++ b/include/circt/Dialect/FIRRTL/Passes.td @@ -110,6 +110,19 @@ def CreateSiFiveMetadata : Pass<"firrtl-emit-metadata", "firrtl::CircuitOp"> { ]; } +def EmitOMIR : Pass<"firrtl-emit-omir", "firrtl::CircuitOp"> { + let summary = "Emit OMIR annotations"; + let description = [{ + This pass gathers the `OMIRAnnotation`s in the design, updates the contained + targets with the trackers that were scattered throughout the design upon + reading the OMIR, and serializes the resulting data into a JSON file. + }]; + let constructor = "circt::firrtl::createEmitOMIRPass()"; + let options = [Option<"outputFilename", "file", "std::string", "", + "Output file for the JSON-serialized OMIR data">]; + let dependentDialects = ["sv::SVDialect", "hw::HWDialect"]; +} + def ExpandWhens : Pass<"firrtl-expand-whens", "firrtl::FModuleOp"> { let summary = "Remove all when conditional blocks."; let description = [{ diff --git a/lib/Dialect/FIRRTL/Import/FIRAnnotations.cpp b/lib/Dialect/FIRRTL/Import/FIRAnnotations.cpp index cf5620476422..c16d3790ffdf 100644 --- a/lib/Dialect/FIRRTL/Import/FIRAnnotations.cpp +++ b/lib/Dialect/FIRRTL/Import/FIRAnnotations.cpp @@ -28,6 +28,7 @@ namespace json = llvm::json; using namespace circt; using namespace firrtl; +using mlir::UnitAttr; /// Split a target into a base target (including a reference if one exists) and /// an optional array of subfield/subindex tokens. @@ -891,6 +892,7 @@ scatterOMIR(Attribute original, unsigned &annotationID, NamedAttrList fields; fields.append("id", IntegerAttr::get(IntegerType::get(ctx, 64), annotationID++)); + fields.append("omir.tracker", UnitAttr::get(ctx)); fields.append("type", StringAttr::get(ctx, tpe)); return DictionaryAttr::getWithSorted(ctx, fields); }; @@ -916,9 +918,9 @@ scatterOMIR(Attribute original, unsigned &annotationID, tpe == "OMMemberInstanceTarget" || tpe == "OMInstanceTarget" || tpe == "OMDontTouchedReferenceTarget") { NamedAttrList tracker; + tracker.append("class", StringAttr::get(ctx, omirTrackerAnnoClass)); tracker.append( "id", IntegerAttr::get(IntegerType::get(ctx, 64), annotationID)); - tracker.append("type", StringAttr::get(ctx, tpe)); auto canonTarget = canonicalizeTarget(value); if (!canonTarget) diff --git a/lib/Dialect/FIRRTL/Transforms/CMakeLists.txt b/lib/Dialect/FIRRTL/Transforms/CMakeLists.txt index 5b3756e344c2..faa73d086397 100755 --- a/lib/Dialect/FIRRTL/Transforms/CMakeLists.txt +++ b/lib/Dialect/FIRRTL/Transforms/CMakeLists.txt @@ -3,6 +3,7 @@ add_circt_dialect_library(CIRCTFIRRTLTransforms BlackBoxReader.cpp CheckCombCycles.cpp CreateSiFiveMetadata.cpp + EmitOMIR.cpp ExpandWhens.cpp GrandCentral.cpp GrandCentralTaps.cpp diff --git a/lib/Dialect/FIRRTL/Transforms/EmitOMIR.cpp b/lib/Dialect/FIRRTL/Transforms/EmitOMIR.cpp new file mode 100644 index 000000000000..cd88b69de8f2 --- /dev/null +++ b/lib/Dialect/FIRRTL/Transforms/EmitOMIR.cpp @@ -0,0 +1,514 @@ +//===- EmitOMIR.cpp ---------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// +// +// This file defines the EmitOMIR pass. +// +//===----------------------------------------------------------------------===// + +#include "AnnotationDetails.h" +#include "PassDetails.h" +#include "circt/Dialect/FIRRTL/CircuitNamespace.h" +#include "circt/Dialect/FIRRTL/Passes.h" +#include "circt/Dialect/HW/HWAttributes.h" +#include "circt/Dialect/SV/SVDialect.h" +#include "circt/Dialect/SV/SVOps.h" +#include "mlir/IR/ImplicitLocOpBuilder.h" +#include "llvm/Support/Debug.h" +#include "llvm/Support/JSON.h" + +#define DEBUG_TYPE "omir" + +using namespace circt; +using namespace firrtl; +using mlir::LocationAttr; +using mlir::UnitAttr; + +//===----------------------------------------------------------------------===// +// Utilities +//===----------------------------------------------------------------------===// + +namespace { +/// Information concerning a tracker in the IR. +struct Tracker { + /// The unique ID of this tracker. + IntegerAttr id; + /// The operation onto which this tracker was annotated. + Operation *op; + /// If this tracker is non-local, this is the corresponding anchor. + NonLocalAnchor nla; +}; + +class EmitOMIRPass : public EmitOMIRBase { +public: + using EmitOMIRBase::outputFilename; + +private: + void runOnOperation() override; + + void emitSourceInfo(Location input, SmallString<64> &into); + void emitOMNode(Attribute node, llvm::json::OStream &jsonStream); + void emitOMField(Identifier fieldName, DictionaryAttr field, + llvm::json::OStream &jsonStream); + void emitValue(Attribute node, llvm::json::OStream &jsonStream); + void emitTrackedTarget(DictionaryAttr node, llvm::json::OStream &jsonStream); + + SmallString<8> addSymbol(FlatSymbolRefAttr 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}); + } + SmallString<8> str; + ("{{" + Twine(id) + "}}").toVector(str); + return str; + } + SmallString<8> addSymbol(StringAttr symbolName) { + return addSymbol(FlatSymbolRefAttr::get(symbolName)); + } + SmallString<8> addSymbol(Operation *op) { + return addSymbol(SymbolTable::getSymbolName(op)); + } + + /// Whether any errors have occurred in the current `runOnOperation`. + bool anyFailures; + /// A symbol table for the current operation. + SymbolTable *symtbl; + /// OMIR target trackers gathered in the current operation, by tracker ID. + DenseMap trackers; + /// The list of symbols to be interpolated in the verbatim JSON. This gets + /// populated as the JSON is constructed and module and instance names are + /// collected. + SmallVector symbols; + SmallDenseMap symbolIndices; +}; +} // namespace + +//===----------------------------------------------------------------------===// +// Pass Implementation +//===----------------------------------------------------------------------===// + +void EmitOMIRPass::runOnOperation() { + MLIRContext *context = &getContext(); + anyFailures = false; + symtbl = nullptr; + trackers.clear(); + symbols.clear(); + symbolIndices.clear(); + CircuitOp circuitOp = getOperation(); + + // Gather the relevant annotations from the circuit. On the one hand these are + // all the actual `OMIRAnnotation`s that need processing and emission, as well + // as an optional `OMIRFileAnnotation` that overrides the default OMIR output + // file. + SmallVector> annoNodes; + Optional outputFilename = {}; + + AnnotationSet::removeAnnotations(circuitOp, [&](Annotation anno) { + if (anno.isClass(omirFileAnnoClass)) { + auto pathAttr = anno.getMember("filename"); + if (!pathAttr) { + circuitOp.emitError(omirFileAnnoClass) + << " annotation missing `filename` string attribute"; + anyFailures = true; + return true; + } + LLVM_DEBUG(llvm::dbgs() << "- OMIR path: " << pathAttr << "\n"); + outputFilename = pathAttr.getValue(); + return true; + } + if (anno.isClass(omirAnnoClass)) { + auto nodesAttr = anno.getMember("nodes"); + if (!nodesAttr) { + circuitOp.emitError(omirAnnoClass) + << " annotation missing `nodes` array attribute"; + anyFailures = true; + return true; + } + LLVM_DEBUG(llvm::dbgs() << "- OMIR: " << nodesAttr << "\n"); + annoNodes.push_back(nodesAttr.getValue()); + return true; + } + return false; + }); + if (anyFailures) + return signalPassFailure(); + + // Traverse the IR and collect all tracker annotations that were previously + // scattered into the circuit. + SymbolTable currentSymtbl(circuitOp); + symtbl = ¤tSymtbl; + circuitOp.walk([&](Operation *op) { + AnnotationSet::removeAnnotations(op, [&](Annotation anno) { + if (!anno.isClass(omirTrackerAnnoClass)) + return false; + Tracker tracker; + tracker.op = op; + tracker.id = anno.getMember("id"); + if (!tracker.id) { + op->emitError(omirTrackerAnnoClass) + << " annotation missing `id` integer attribute"; + anyFailures = true; + return true; + } + if (auto nlaSym = anno.getMember("circt.nonlocal")) + tracker.nla = + dyn_cast_or_null(symtbl->lookup(nlaSym.getAttr())); + trackers.insert({tracker.id, tracker}); + return true; + }); + }); + + // If an OMIR output filename has been specified as a pass parameter, override + // whatever the annotations have configured. If neither are specified we just + // bail. + if (!this->outputFilename.empty()) + outputFilename = this->outputFilename; + if (!outputFilename) { + LLVM_DEBUG(llvm::dbgs() << "Not emitting OMIR because no annotation or " + "pass parameter specified an output file\n"); + markAllAnalysesPreserved(); + return; + } + + // Build the output JSON. + std::string jsonBuffer; + llvm::raw_string_ostream jsonOs(jsonBuffer); + llvm::json::OStream json(jsonOs, 2); + json.array([&] { + for (auto nodes : annoNodes) { + for (auto node : nodes) { + emitOMNode(node, json); + if (anyFailures) + return; + } + } + }); + if (anyFailures) + return signalPassFailure(); + + // Emit the OMIR JSON as a verbatim op. + auto builder = OpBuilder(circuitOp); + builder.setInsertionPointAfter(circuitOp); + auto verbatimOp = + builder.create(builder.getUnknownLoc(), jsonBuffer); + auto fileAttr = hw::OutputFileAttr::getFromFilename( + context, *outputFilename, /*excludeFromFilelist=*/true); + verbatimOp->setAttr("output_file", fileAttr); + verbatimOp.symbolsAttr(ArrayAttr::get(context, symbols)); +} + +/// Emit a source locator into a string, for inclusion in the `info` field of +/// `OMNode` and `OMField`. +void EmitOMIRPass::emitSourceInfo(Location input, SmallString<64> &into) { + into.clear(); + input->walk([&](Location loc) { + if (FileLineColLoc fileLoc = loc.dyn_cast()) { + into.append(into.empty() ? "@[" : " "); + auto twine = Twine(fileLoc.getFilename()) + " " + + Twine(fileLoc.getLine()) + ":" + Twine(fileLoc.getColumn()); + twine.toVector(into); + } + return WalkResult::advance(); + }); + if (!into.empty()) + into.append("]"); +} + +/// Emit an entire `OMNode` as JSON. +void EmitOMIRPass::emitOMNode(Attribute node, llvm::json::OStream &jsonStream) { + auto dict = node.dyn_cast(); + if (!dict) { + getOperation() + .emitError("OMNode must be a dictionary") + .attachNote(getOperation().getLoc()) + << node; + anyFailures = true; + return; + } + + // Extract the `info` field and serialize the location. + SmallString<64> info; + if (auto infoAttr = dict.getAs("info")) + emitSourceInfo(infoAttr, info); + if (anyFailures) + return; + + // Extract the `id` field. + auto idAttr = dict.getAs("id"); + if (!idAttr) { + getOperation() + .emitError("OMNode missing `id` string field") + .attachNote(getOperation().getLoc()) + << dict; + anyFailures = true; + return; + } + + // Extract and order the fields of this node. + SmallVector> orderedFields; + if (auto fieldsDict = dict.getAs("fields")) { + for (auto nameAndField : fieldsDict.getValue()) { + auto fieldDict = nameAndField.second.dyn_cast(); + if (!fieldDict) { + getOperation() + .emitError("OMField must be a dictionary") + .attachNote(getOperation().getLoc()) + << nameAndField.second; + anyFailures = true; + return; + } + + unsigned index = 0; + if (auto indexAttr = fieldDict.getAs("index")) + index = indexAttr.getValue().getLimitedValue(); + + orderedFields.push_back({index, nameAndField.first, fieldDict}); + } + llvm::sort(orderedFields, + [](auto a, auto b) { return std::get<0>(a) < std::get<0>(b); }); + } + + jsonStream.object([&] { + jsonStream.attribute("info", info); + jsonStream.attribute("id", idAttr.getValue()); + jsonStream.attributeArray("fields", [&] { + for (auto &orderedField : orderedFields) { + emitOMField(std::get<1>(orderedField), std::get<2>(orderedField), + jsonStream); + if (anyFailures) + return; + } + }); + }); +} + +/// Emit a single `OMField` as JSON. This expects the field's name to be +/// provided from the outside, for example as the field name that this attribute +/// has in the surrounding dictionary. +void EmitOMIRPass::emitOMField(Identifier fieldName, DictionaryAttr field, + llvm::json::OStream &jsonStream) { + // Extract the `info` field and serialize the location. + auto infoAttr = field.getAs("info"); + SmallString<64> info; + if (infoAttr) + emitSourceInfo(infoAttr, info); + if (anyFailures) + return; + + jsonStream.object([&] { + jsonStream.attribute("info", info); + jsonStream.attribute("name", fieldName.strref()); + jsonStream.attributeBegin("value"); + emitValue(field.get("value"), jsonStream); + jsonStream.attributeEnd(); + }); +} + +void EmitOMIRPass::emitValue(Attribute node, llvm::json::OStream &jsonStream) { + // Handle the null case. + if (!node || node.isa()) + return jsonStream.value(nullptr); + + // Handle the trivial cases where the OMIR serialization simply uses the + // builtin JSON types. + if (auto attr = node.dyn_cast()) + return jsonStream.value(attr.getValue()); // OMBoolean + if (auto attr = node.dyn_cast()) { + // CAVEAT: We expect these integers to come from an OMIR file that is + // initially read in from JSON, where they are i32 or i64, so this should + // yield a valid value. However, a user could cook up an arbitrary precision + // integer attr in MLIR input and then subtly break the JSON spec. + SmallString<16> val; + attr.getValue().toStringSigned(val); + return jsonStream.rawValue(val); // OMInt + } + if (auto attr = node.dyn_cast()) { + // CAVEAT: We expect these floats to come from an OMIR file that is + // initially read in from JSON, where they are f32 or f64, so this should + // yield a valid value. However, a user could cook up an arbitrary precision + // float attr in MLIR input and then subtly break the JSON spec. + SmallString<16> val; + attr.getValue().toString(val); + return jsonStream.rawValue(val); // OMDouble + } + + // Handle aggregate types. + if (auto attr = node.dyn_cast()) { + jsonStream.array([&] { + for (auto element : attr.getValue()) { + emitValue(element, jsonStream); + if (anyFailures) + return; + } + }); + return; + } + if (auto attr = node.dyn_cast()) { + // Handle targets that have a corresponding tracker annotation in the IR. + if (attr.getAs("omir.tracker")) + return emitTrackedTarget(attr, jsonStream); + + // Handle regular dictionaries. + jsonStream.object([&] { + for (auto field : attr.getValue()) { + jsonStream.attributeBegin(field.first); + emitValue(field.second, jsonStream); + jsonStream.attributeEnd(); + if (anyFailures) + return; + } + }); + return; + } + + // The remaining types are all simple string-encoded pass-through cases. + if (auto attr = node.dyn_cast()) { + StringRef val = attr.getValue(); + if (isOMIRStringEncodedPassthrough(val.split(":").first)) + return jsonStream.value(val); + } + + // If we get here, we don't know how to serialize the given MLIR attribute as + // a OMIR value. + jsonStream.value(""); + getOperation().emitError("unsupported attribute for OMIR serialization: `") + << node << "`"; + anyFailures = true; +} + +void EmitOMIRPass::emitTrackedTarget(DictionaryAttr node, + llvm::json::OStream &jsonStream) { + // Extract the `id` field. + auto idAttr = node.getAs("id"); + if (!idAttr) { + getOperation() + .emitError("tracked OMIR target missing `id` string field") + .attachNote(getOperation().getLoc()) + << node; + anyFailures = true; + return jsonStream.value(""); + } + + // Extract the `type` field. + auto typeAttr = node.getAs("type"); + if (!typeAttr) { + getOperation() + .emitError("tracked OMIR target missing `type` string field") + .attachNote(getOperation().getLoc()) + << node; + anyFailures = true; + return jsonStream.value(""); + } + StringRef type = typeAttr.getValue(); + + // Find the tracker for this target, and handle the case where the tracker has + // been deleted. + auto trackerIt = trackers.find(idAttr); + if (trackerIt == trackers.end()) { + // Some of the target types indicate removal of the target through an + // `OMDeleted` node. + if (type == "OMReferenceTarget" || type == "OMMemberReferenceTarget" || + type == "OMMemberInstanceTarget") + return jsonStream.value("OMDeleted"); + + // The remaining types produce an error upon removal of the target. + auto diag = getOperation().emitError("tracked OMIR target of type `") + << type << "` was deleted"; + diag.attachNote(getOperation().getLoc()) + << type << " should never be deleted"; + diag.attachNote(getOperation().getLoc()) << node; + anyFailures = true; + return jsonStream.value(""); + } + auto tracker = trackerIt->second; + + // Serialize the target circuit first. + SmallString<64> target(type); + target.append(":~"); + target.append(getOperation().name()); + target.push_back('|'); + + // Serialize the local or non-local module/instance hierarchy path. + if (tracker.nla) { + bool notFirst = false; + StringAttr instName; + for (auto modAndName : llvm::zip(tracker.nla.modpath().getValue(), + tracker.nla.namepath().getValue())) { + auto symAttr = std::get<0>(modAndName).cast(); + auto nameAttr = std::get<1>(modAndName).cast(); + Operation *module = symtbl->lookup(symAttr.getValue()); + assert(module); + if (notFirst) + target.push_back('/'); + notFirst = true; + if (instName) { + // TODO: This should *really* drop a symbol to represent the instance + // name. See below. + target.append(instName.getValue()); + target.push_back(':'); + } + target.append(addSymbol(module)); + instName = nameAttr; + + // Find an instance with the given name in this module. + module->walk([&](InstanceOp instOp) { + if (instOp.nameAttr() == nameAttr) { + LLVM_DEBUG(llvm::dbgs() + << "Marking NLA-participating instance " << nameAttr + << " in module " << symAttr << " as dont-touch\n"); + AnnotationSet::addDontTouch(instOp); + } + }); + } + } else { + FModuleOp module = dyn_cast(tracker.op); + if (!module) + module = tracker.op->getParentOfType(); + assert(module); + target.append(addSymbol(module)); + } + + // Serialize any potential component *inside* the module that this target may + // specifically refer to. + StringRef componentName; + if (isa(tracker.op)) { + AnnotationSet::addDontTouch(tracker.op); + LLVM_DEBUG(llvm::dbgs() + << "Marking OMIR-targeted " << tracker.op << " as dont-touch\n"); + // TODO: This should *really* drop a symbol placeholder into the JSON. But + // we currently don't have any symbols for these FIRRTL ops. May be solved + // through NLAs. + componentName = tracker.op->getAttrOfType("name").getValue(); + } else if (!isa(tracker.op)) { + tracker.op->emitError("invalid target for `") << type << "` OMIR"; + anyFailures = true; + return jsonStream.value(""); + } + if (!componentName.empty()) { + target.push_back('>'); + target.append(componentName); + } + + jsonStream.value(target); +} + +//===----------------------------------------------------------------------===// +// Pass Infrastructure +//===----------------------------------------------------------------------===// + +std::unique_ptr +circt::firrtl::createEmitOMIRPass(StringRef outputFilename) { + auto pass = std::make_unique(); + if (!outputFilename.empty()) + pass->outputFilename = outputFilename.str(); + return pass; +} diff --git a/test/Dialect/FIRRTL/SFCTests/emit-omir.fir b/test/Dialect/FIRRTL/SFCTests/emit-omir.fir new file mode 100644 index 000000000000..a75eed48d068 --- /dev/null +++ b/test/Dialect/FIRRTL/SFCTests/emit-omir.fir @@ -0,0 +1,84 @@ +; RUN: firtool --verify-diagnostics --verilog --emit-omir %s | FileCheck %s + +circuit Foo : %[[ + { + "class": "freechips.rocketchip.objectmodel.OMIRFileAnnotation", + "filename": "omir.json" + }, + { + "class": "freechips.rocketchip.objectmodel.OMIRAnnotation", + "nodes": [ + { + "info": "", + "id": "OMID:0", + "fields": [ + {"info": "", "name": "a", "value": "OMReference:0"}, + {"info": "", "name": "b", "value": "OMBigInt:42"}, + {"info": "", "name": "c", "value": "OMLong:ff"}, + {"info": "", "name": "d", "value": "OMString:hello"}, + {"info": "", "name": "f", "value": "OMBigDecimal:10.5"}, + {"info": "", "name": "g", "value": "OMDeleted"}, + {"info": "", "name": "h", "value": "OMConstant:UInt<2>(\"h1\")"}, + {"info": "", "name": "i", "value": 42}, + {"info": "", "name": "j", "value": true}, + {"info": "", "name": "k", "value": 3.14} + ] + }, + { + "info": "", + "id": "OMID:1", + "fields": [ + {"info": "", "name": "a", "value": "OMReferenceTarget:~Foo|Foo"}, + {"info": "", "name": "b", "value": "OMInstanceTarget:~Foo|Foo"}, + {"info": "", "name": "c", "value": "OMMemberReferenceTarget:~Foo|Foo"}, + {"info": "", "name": "d", "value": "OMMemberInstanceTarget:~Foo|Foo"}, + {"info": "", "name": "e", "value": "OMDontTouchedReferenceTarget:~Foo|Foo"}, + {"info": "", "name": "f", "value": "OMReferenceTarget:~Foo|Bar"} + ] + } + ] + } +]] + module Foo : + inst bar of Bar + skip + module Bar : + skip + +; CHECK-LABEL: FILE "omir.json" + +; CHECK-LABEL: "id": "OMID:0" +; CHECK-LABEL: "name": "a" +; CHECK-NEXT: "value": "OMReference:0" +; CHECK-LABEL: "name": "b" +; CHECK-NEXT: "value": "OMBigInt:42" +; CHECK-LABEL: "name": "c" +; CHECK-NEXT: "value": "OMLong:ff" +; CHECK-LABEL: "name": "d" +; CHECK-NEXT: "value": "OMString:hello" +; CHECK-LABEL: "name": "f" +; CHECK-NEXT: "value": "OMBigDecimal:10.5" +; CHECK-LABEL: "name": "g" +; CHECK-NEXT: "value": "OMDeleted" +; CHECK-LABEL: "name": "h" +; CHECK-NEXT: "value": "OMConstant:UInt<2>(\"h1\")" +; CHECK-LABEL: "name": "i" +; CHECK-NEXT: "value": 42 +; CHECK-LABEL: "name": "j" +; CHECK-NEXT: "value": true +; CHECK-LABEL: "name": "k" +; CHECK-NEXT: "value": 3.14 + +; CHECK-LABEL: "id": "OMID:1" +; CHECK-LABEL: "name": "a" +; CHECK-NEXT: "value": "OMReferenceTarget:~Foo|Foo" +; CHECK-LABEL: "name": "b" +; CHECK-NEXT: "value": "OMInstanceTarget:~Foo|Foo" +; CHECK-LABEL: "name": "c" +; CHECK-NEXT: "value": "OMMemberReferenceTarget:~Foo|Foo" +; CHECK-LABEL: "name": "d" +; CHECK-NEXT: "value": "OMMemberInstanceTarget:~Foo|Foo" +; CHECK-LABEL: "name": "e" +; CHECK-NEXT: "value": "OMDontTouchedReferenceTarget:~Foo|Foo" +; CHECK-LABEL: "name": "f" +; CHECK-NEXT: "value": "OMReferenceTarget:~Foo|Bar" diff --git a/test/Dialect/FIRRTL/emit-omir.mlir b/test/Dialect/FIRRTL/emit-omir.mlir new file mode 100644 index 000000000000..c3d46c8980f7 --- /dev/null +++ b/test/Dialect/FIRRTL/emit-omir.mlir @@ -0,0 +1,163 @@ +// RUN: circt-opt --pass-pipeline='firrtl.circuit(firrtl-emit-omir{file=omir.json})' %s | FileCheck %s + +//===----------------------------------------------------------------------===// +// Absence of any OMIR +//===----------------------------------------------------------------------===// + +firrtl.circuit "NoOMIR" { + firrtl.module @NoOMIR() { + } +} +// CHECK-LABEL: firrtl.circuit "NoOMIR" { +// CHECK-NEXT: firrtl.module @NoOMIR() { +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: sv.verbatim "[]" +// CHECK-SAME: #hw.output_file<"omir.json", excludeFromFileList> + +//===----------------------------------------------------------------------===// +// Empty OMIR data +//===----------------------------------------------------------------------===// + +firrtl.circuit "NoNodes" attributes {annotations = [{class = "freechips.rocketchip.objectmodel.OMIRAnnotation", nodes = []}]} { + firrtl.module @NoNodes() { + } +} +// CHECK-LABEL: firrtl.circuit "NoNodes" { +// CHECK-NEXT: firrtl.module @NoNodes() { +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: sv.verbatim "[]" +// CHECK-SAME: #hw.output_file<"omir.json", excludeFromFileList> + +//===----------------------------------------------------------------------===// +// Empty node +//===----------------------------------------------------------------------===// + +#loc = loc(unknown) +firrtl.circuit "EmptyNode" attributes {annotations = [{class = "freechips.rocketchip.objectmodel.OMIRAnnotation", nodes = [{fields = {}, id = "OMID:0", info = #loc}]}]} { + firrtl.module @EmptyNode() { + } +} + +//===----------------------------------------------------------------------===// +// Source locator serialization +//===----------------------------------------------------------------------===// + +#loc0 = loc("B":2:3) +#loc1 = loc(fused["C":4:5, "D":6:7]) +#loc2 = loc("A":0:1) +firrtl.circuit "SourceLocators" attributes {annotations = [{class = "freechips.rocketchip.objectmodel.OMIRAnnotation", nodes = [{fields = {x = {index = 1 : i64, info = #loc0, value = "OMReference:0"}, y = {index = 0 : i64, info = #loc1, value = "OMReference:0"}}, id = "OMID:0", info = #loc2}]}]} { + firrtl.module @SourceLocators() { + } +} + +//===----------------------------------------------------------------------===// +// Check that all the OMIR types support serialization +//===----------------------------------------------------------------------===// + +firrtl.circuit "AllTypesSupported" attributes {annotations = [{ + class = "freechips.rocketchip.objectmodel.OMIRAnnotation", + nodes = [{info = #loc, id = "OMID:0", fields = { + OMBoolean = {info = #loc, index = 1, value = true}, + OMInt1 = {info = #loc, index = 2, value = 9001 : i32}, + OMInt2 = {info = #loc, index = 3, value = -42 : i32}, + OMDouble = {info = #loc, index = 4, value = 3.14 : f32}, + OMID = {info = #loc, index = 5, value = "OMID:1337"}, + OMReference = {info = #loc, index = 6, value = "OMReference:0"}, + OMBigInt = {info = #loc, index = 7, value = "OMBigInt:42"}, + OMLong = {info = #loc, index = 8, value = "OMLong:ff"}, + OMString = {info = #loc, index = 9, value = "OMString:hello"}, + OMBigDecimal = {info = #loc, index = 10, value = "OMBigDecimal:10.5"}, + OMDeleted = {info = #loc, index = 11, value = "OMDeleted"}, + OMConstant = {info = #loc, index = 12, value = "OMConstant:UInt<2>(\"h1\")"}, + OMArray = {info = #loc, index = 13, value = [true, 9001, "OMString:bar"]}, + OMMap = {info = #loc, index = 14, value = {foo = true, bar = 9001}} + }}] +}]} { + firrtl.module @AllTypesSupported() { + } +} +// CHECK-LABEL: firrtl.circuit "AllTypesSupported" { +// CHECK-NEXT: firrtl.module @AllTypesSupported() { +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: sv.verbatim +// CHECK-SAME: \22name\22: \22OMBoolean\22,\0A \22value\22: true +// CHECK-SAME: \22name\22: \22OMInt1\22,\0A \22value\22: 9001 +// CHECK-SAME: \22name\22: \22OMInt2\22,\0A \22value\22: -42 +// CHECK-SAME: \22name\22: \22OMDouble\22,\0A \22value\22: 3.14 +// CHECK-SAME: \22name\22: \22OMID\22,\0A \22value\22: \22OMID:1337\22 +// CHECK-SAME: \22name\22: \22OMReference\22,\0A \22value\22: \22OMReference:0\22 +// CHECK-SAME: \22name\22: \22OMBigInt\22,\0A \22value\22: \22OMBigInt:42\22 +// CHECK-SAME: \22name\22: \22OMLong\22,\0A \22value\22: \22OMLong:ff\22 +// CHECK-SAME: \22name\22: \22OMString\22,\0A \22value\22: \22OMString:hello\22 +// CHECK-SAME: \22name\22: \22OMBigDecimal\22,\0A \22value\22: \22OMBigDecimal:10.5\22 +// CHECK-SAME: \22name\22: \22OMDeleted\22,\0A \22value\22: \22OMDeleted\22 +// CHECK-SAME: \22name\22: \22OMConstant\22,\0A \22value\22: \22OMConstant:UInt<2>(\\\22h1\\\22)\22 +// CHECK-SAME: \22name\22: \22OMArray\22,\0A \22value\22: [\0A true,\0A 9001,\0A \22OMString:bar\22\0A ] +// CHECK-SAME: \22name\22: \22OMMap\22,\0A \22value\22: {\0A \22bar\22: 9001,\0A \22foo\22: true\0A } +// CHECK-SAME: #hw.output_file<"omir.json", excludeFromFileList> + +//===----------------------------------------------------------------------===// +// Trackers as Local Annotations +//===----------------------------------------------------------------------===// + +firrtl.circuit "LocalTrackers" attributes {annotations = [{ + class = "freechips.rocketchip.objectmodel.OMIRAnnotation", + nodes = [{info = #loc, id = "OMID:0", fields = { + OMReferenceTarget1 = {info = #loc, index = 1, value = {omir.tracker, id = 0, type = "OMReferenceTarget"}}, + OMReferenceTarget2 = {info = #loc, index = 2, value = {omir.tracker, id = 1, type = "OMReferenceTarget"}}, + OMReferenceTarget3 = {info = #loc, index = 3, value = {omir.tracker, id = 2, type = "OMReferenceTarget"}}, + OMReferenceTarget4 = {info = #loc, index = 4, value = {omir.tracker, id = 3, type = "OMReferenceTarget"}} + }}] +}]} { + firrtl.module @A() attributes {annotations = [{class = "freechips.rocketchip.objectmodel.OMIRTracker", id = 0}]} { + %c = firrtl.wire {annotations = [{class = "freechips.rocketchip.objectmodel.OMIRTracker", id = 1}]} : !firrtl.uint<42> + } + firrtl.module @LocalTrackers() { + firrtl.instance a {annotations = [{class = "freechips.rocketchip.objectmodel.OMIRTracker", id = 3}]} @A() + %b = firrtl.wire {annotations = [{class = "freechips.rocketchip.objectmodel.OMIRTracker", id = 2}]} : !firrtl.uint<42> + } +} +// CHECK-LABEL: firrtl.circuit "LocalTrackers" { +// CHECK-NEXT: firrtl.module @A() { +// CHECK-NEXT: %c = firrtl.wire {annotations = [{class = "firrtl.transforms.DontTouchAnnotation"}]} : !firrtl.uint<42> +// CHECK-NEXT: } +// CHECK-NEXT: firrtl.module @LocalTrackers() { +// CHECK-NEXT: firrtl.instance a {annotations = [{class = "firrtl.transforms.DontTouchAnnotation"}]} @A() +// CHECK-NEXT: %b = firrtl.wire {annotations = [{class = "firrtl.transforms.DontTouchAnnotation"}]} : !firrtl.uint<42> +// CHECK-NEXT: } +// CHECK-NEXT: } +// CHECK-NEXT: sv.verbatim +// CHECK-SAME: \22name\22: \22OMReferenceTarget1\22,\0A \22value\22: \22OMReferenceTarget:~LocalTrackers|{{[{][{]0[}][}]}}\22 +// CHECK-SAME: \22name\22: \22OMReferenceTarget2\22,\0A \22value\22: \22OMReferenceTarget:~LocalTrackers|{{[{][{]0[}][}]}}>c\22 +// CHECK-SAME: \22name\22: \22OMReferenceTarget3\22,\0A \22value\22: \22OMReferenceTarget:~LocalTrackers|{{[{][{]1[}][}]}}>b\22 +// CHECK-SAME: \22name\22: \22OMReferenceTarget4\22,\0A \22value\22: \22OMReferenceTarget:~LocalTrackers|{{[{][{]1[}][}]}}>a\22 +// CHECK-SAME: #hw.output_file<"omir.json", excludeFromFileList> +// CHECK-SAME: symbols = [@A, @LocalTrackers] + +//===----------------------------------------------------------------------===// +// Trackers as Non-Local Annotations +//===----------------------------------------------------------------------===// + +firrtl.circuit "NonLocalTrackers" attributes {annotations = [{ + class = "freechips.rocketchip.objectmodel.OMIRAnnotation", + nodes = [{info = #loc, id = "OMID:0", fields = { + OMReferenceTarget1 = {info = #loc, index = 1, id = "OMID:1", value = {omir.tracker, id = 0, type = "OMReferenceTarget"}} + }}] +}]} { + firrtl.nla @nla_0 [@NonLocalTrackers, @B, @A] ["b", "a", "A"] + firrtl.module @A() attributes {annotations = [{circt.nonlocal = @nla_0, class = "freechips.rocketchip.objectmodel.OMIRTracker", id = 0}]} {} + firrtl.module @B() { + firrtl.instance a {annotations = [{circt.nonlocal = @nla_0, class = "circt.nonlocal"}]} @A() + } + firrtl.module @NonLocalTrackers() { + firrtl.instance b {annotations = [{circt.nonlocal = @nla_0, class = "circt.nonlocal"}]} @B() + } +} +// CHECK-LABEL: firrtl.circuit "NonLocalTrackers" { +// CHECK: sv.verbatim +// CHECK-SAME: \22name\22: \22OMReferenceTarget1\22,\0A \22value\22: \22OMReferenceTarget:~NonLocalTrackers|{{[{][{]0[}][}]}}/b:{{[{][{]1[}][}]}}/a:{{[{][{]2[}][}]}}\22 +// CHECK-SAME: #hw.output_file<"omir.json", excludeFromFileList> +// CHECK-SAME: symbols = [@NonLocalTrackers, @B, @A] diff --git a/test/Dialect/FIRRTL/omir.fir b/test/Dialect/FIRRTL/omir.fir index 8909a2f59b43..76518f3b8413 100644 --- a/test/Dialect/FIRRTL/omir.fir +++ b/test/Dialect/FIRRTL/omir.fir @@ -262,27 +262,27 @@ circuit Foo: %[[ inst f of F ; CHECK-LABEL: firrtl.circuit "Foo" - ; CHECK-SAME: a = {{{[^{}]+}} value = {id = [[aID:[0-9]+]] : i64, type = [[aType:"OM[a-zA-Z]+"]] - ; CHECK-SAME: b = {{{[^{}]+}} value = {id = [[bID:[0-9]+]] : i64, type = [[bType:"OM[a-zA-Z]+"]] - ; CHECK-SAME: c = {{{[^{}]+}} value = {id = [[cID:[0-9]+]] : i64, type = [[cType:"OM[a-zA-Z]+"]] - ; CHECK-SAME: d = {{{[^{}]+}} value = {id = [[dID:[0-9]+]] : i64, type = [[dType:"OM[a-zA-Z]+"]] - ; CHECK-SAME: e = {{{[^{}]+}} value = {id = [[eID:[0-9]+]] : i64, type = [[eType:"OM[a-zA-Z]+"]] - ; CHECK-SAME: f = {{{[^{}]+}} value = {id = [[fID:[0-9]+]] : i64, type = [[fType:"OM[a-zA-Z]+"]] - ; CHECK-SAME: g = {{{[^{}]+}} value = {id = [[gID:[0-9]+]] : i64, type = [[gType:"OM[a-zA-Z]+"]] + ; CHECK-SAME: a = {{{[^{}]+}} value = {id = [[aID:[0-9]+]] : i64, omir.tracker, type = {{"OM[a-zA-Z]+"}} + ; CHECK-SAME: b = {{{[^{}]+}} value = {id = [[bID:[0-9]+]] : i64, omir.tracker, type = {{"OM[a-zA-Z]+"}} + ; CHECK-SAME: c = {{{[^{}]+}} value = {id = [[cID:[0-9]+]] : i64, omir.tracker, type = {{"OM[a-zA-Z]+"}} + ; CHECK-SAME: d = {{{[^{}]+}} value = {id = [[dID:[0-9]+]] : i64, omir.tracker, type = {{"OM[a-zA-Z]+"}} + ; CHECK-SAME: e = {{{[^{}]+}} value = {id = [[eID:[0-9]+]] : i64, omir.tracker, type = {{"OM[a-zA-Z]+"}} + ; CHECK-SAME: f = {{{[^{}]+}} value = {id = [[fID:[0-9]+]] : i64, omir.tracker, type = {{"OM[a-zA-Z]+"}} + ; CHECK-SAME: g = {{{[^{}]+}} value = {id = [[gID:[0-9]+]] : i64, omir.tracker, type = {{"OM[a-zA-Z]+"}} ; CHECK-DAG: firrtl.nla @[[Foo_eE_E:nla_[0-9]+]] ; CHECK-DAG: firrtl.nla @[[Foo_cC_C:nla_[0-9]+]] ; CHECK: firrtl.module @C - ; CHECK-SAME: {circt.nonlocal = @[[Foo_cC_C]], id = [[cID]] : i64, type = [[cType]]} + ; CHECK-SAME: {circt.nonlocal = @[[Foo_cC_C]], class = "freechips.rocketchip.objectmodel.OMIRTracker", id = [[cID]] : i64} ; CHECK: firrtl.module @D - ; CHECK-SAME: {id = [[dID]] : i64, type = [[dType]]} + ; CHECK-SAME: {class = "freechips.rocketchip.objectmodel.OMIRTracker", id = [[dID]] : i64} ; CHECK: firrtl.module @E - ; CHECK-SAME: {circt.nonlocal = @[[Foo_eE_E]], id = [[eID]] : i64, type = [[eType]]} + ; CHECK-SAME: {circt.nonlocal = @[[Foo_eE_E]], class = "freechips.rocketchip.objectmodel.OMIRTracker", id = [[eID]] : i64} ; CHECK: firrtl.module @F - ; CHECK-SAME: {id = [[fID]] : i64, type = [[fType]]} + ; CHECK-SAME: {class = "freechips.rocketchip.objectmodel.OMIRTracker", id = [[fID]] : i64} ; CHECK: firrtl.module @Foo ; CHECK-NEXT: %a = firrtl.wire - ; CHECK-SAME: {id = [[aID]] : i64, type = [[aType]]} + ; CHECK-SAME: {class = "freechips.rocketchip.objectmodel.OMIRTracker", id = [[aID]] : i64} ; CHECK-NEXT: %b = firrtl.wire - ; CHECK-SAME: {id = [[bID]] : i64, type = [[bType]]} + ; CHECK-SAME: {class = "freechips.rocketchip.objectmodel.OMIRTracker", id = [[bID]] : i64} ; CHECK-NEXT: %g = firrtl.wire - ; CHECK-SAME: {id = [[gID]] : i64, type = [[gType]]} + ; CHECK-SAME: {class = "freechips.rocketchip.objectmodel.OMIRTracker", id = [[gID]] : i64} diff --git a/tools/firtool/firtool.cpp b/tools/firtool/firtool.cpp index 5e7f2829160a..761a634d724b 100644 --- a/tools/firtool/firtool.cpp +++ b/tools/firtool/firtool.cpp @@ -111,6 +111,10 @@ static cl::opt cl::desc("emit metadata for metadata annotations"), cl::init(true)); +static cl::opt emitOMIR("emit-omir", + cl::desc("emit OMIR annotations to a JSON file"), + cl::init(true)); + static cl::opt replSeqMem( "repl-seq-mem", cl::desc( @@ -391,6 +395,9 @@ processBuffer(MLIRContext &context, TimingScope &ts, llvm::SourceMgr &sourceMgr, pm.nest().addPass(firrtl::createCreateSiFiveMetadataPass( replSeqMem, replSeqMemCircuit, replSeqMemFile)); + if (emitOMIR) + pm.nest().addPass(firrtl::createEmitOMIRPass()); + // Lower if we are going to verilog or if lowering was specifically requested. if (lowerToHW || outputFormat == OutputVerilog || outputFormat == OutputSplitVerilog) {