Skip to content

Commit 9d25120

Browse files
committed
[FIRRTL] Add CheckInstanceChoice pass to validate instance choice configurations
This commit introduces a new analysis and verification pass to ensure that instance choices in FIRRTL circuits follow proper nesting rules. The pass prevents nested instance choices with different options on the same path while allowing the same module to be reached through different options on different paths (disjunction is allowed, conjunction is banned).
1 parent f79553f commit 9d25120

File tree

7 files changed

+487
-0
lines changed

7 files changed

+487
-0
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
//===- InstanceChoiceInfo.h - Instance choice analysis ----------*- 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+
//
9+
// This file defines the InstanceChoiceInfo analysis, which computes dominating
10+
// instance choice options for each (public module, destination module) pair.
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
#ifndef CIRCT_DIALECT_FIRRTL_INSTANCECHOICEINFO_H
15+
#define CIRCT_DIALECT_FIRRTL_INSTANCECHOICEINFO_H
16+
17+
#include "circt/Dialect/FIRRTL/FIRRTLInstanceGraph.h"
18+
#include "circt/Dialect/FIRRTL/FIRRTLOps.h"
19+
#include "circt/Support/LLVM.h"
20+
#include "llvm/ADT/DenseMap.h"
21+
#include "llvm/ADT/DenseSet.h"
22+
#include "llvm/ADT/MapVector.h"
23+
#include "llvm/ADT/SetVector.h"
24+
25+
namespace circt {
26+
namespace firrtl {
27+
28+
/// Represents a choice option and case.
29+
struct ChoiceKey {
30+
/// The option name.
31+
mlir::SymbolRefAttr option;
32+
/// The case name (nullable for default case).
33+
mlir::SymbolRefAttr caseAttr;
34+
35+
bool operator==(const ChoiceKey &other) const {
36+
return option == other.option && caseAttr == other.caseAttr;
37+
}
38+
bool operator!=(const ChoiceKey &other) const { return !(*this == other); }
39+
};
40+
41+
} // namespace firrtl
42+
} // namespace circt
43+
44+
namespace llvm {
45+
template <>
46+
struct DenseMapInfo<circt::firrtl::ChoiceKey, void> {
47+
using ChoiceKey = circt::firrtl::ChoiceKey;
48+
using SymbolRefInfo = DenseMapInfo<mlir::SymbolRefAttr>;
49+
50+
static ChoiceKey getEmptyKey() {
51+
return {SymbolRefInfo::getEmptyKey(), SymbolRefInfo::getEmptyKey()};
52+
}
53+
54+
static ChoiceKey getTombstoneKey() {
55+
return {SymbolRefInfo::getTombstoneKey(), SymbolRefInfo::getTombstoneKey()};
56+
}
57+
58+
static unsigned getHashValue(const ChoiceKey &key) {
59+
return llvm::hash_combine(SymbolRefInfo::getHashValue(key.option),
60+
SymbolRefInfo::getHashValue(key.caseAttr));
61+
}
62+
63+
static bool isEqual(const ChoiceKey &lhs, const ChoiceKey &rhs) {
64+
return lhs == rhs;
65+
}
66+
};
67+
} // namespace llvm
68+
69+
namespace circt {
70+
namespace firrtl {
71+
72+
/// This class checks for nested instance choices with different options
73+
/// and computes the dominating choices for each (public module, destination)
74+
/// pair.
75+
///
76+
/// The restriction is: on any single path from a public module, you cannot
77+
/// go through instance choices with different options (= nested instance is not
78+
/// allowed). However, reaching the same module through different options on
79+
/// different paths is allowed.
80+
class InstanceChoiceInfo {
81+
public:
82+
InstanceChoiceInfo(CircuitOp circuit, InstanceGraph &instanceGraph)
83+
: circuit(circuit), instanceGraph(instanceGraph) {}
84+
85+
/// Run the analysis. Returns failure if there is a nested instance choice.
86+
LogicalResult run();
87+
88+
/// Get choices for a destination module from a public module.
89+
/// Failure is returned if the destination is not reachable from the public
90+
/// module. Empty array is returned if the destination is reachable but there
91+
/// is no instance choice on any path.
92+
FailureOr<ArrayRef<ChoiceKey>> getChoices(FModuleLike publicModule,
93+
FModuleLike destination) const {
94+
assert(publicModule.isPublic() &&
95+
"the first argument must be a public module");
96+
auto publicIt = moduleChoices.find(publicModule);
97+
if (publicIt == moduleChoices.end())
98+
return failure();
99+
auto it = publicIt->second.find(destination);
100+
// It means the destination is not reachable from the public module.
101+
if (it == publicIt->second.end())
102+
return failure();
103+
return it->second.getArrayRef();
104+
}
105+
106+
/// Check if a destination module is always reachable from a public module
107+
/// (i.e., has at least one path with no instance choices).
108+
bool isAlwaysReachable(FModuleLike publicModule,
109+
FModuleLike destination) const {
110+
assert(publicModule.isPublic() &&
111+
"the first argument must be a public module");
112+
auto &it = alwaysReachable.at(publicModule);
113+
return it.contains(destination);
114+
}
115+
116+
void dump(raw_ostream &os) const;
117+
118+
private:
119+
void computeAlwaysReachable(igraph::InstanceGraphNode *node,
120+
FModuleLike publicModule);
121+
122+
LogicalResult processNode(igraph::InstanceGraphNode *node,
123+
FModuleLike publicModule);
124+
125+
CircuitOp circuit;
126+
InstanceGraph &instanceGraph;
127+
128+
/// Map from (public module, destination module) to all choices that can reach
129+
/// it. This tracks ALL choices from ALL paths (union of choices).
130+
/// Uses MapVector to preserve insertion order for deterministic output.
131+
llvm::MapVector<FModuleLike,
132+
llvm::MapVector<FModuleLike, SetVector<ChoiceKey>>>
133+
moduleChoices;
134+
135+
/// Set of (public module, destination module) pairs where the destination
136+
/// is always reachable (has at least one path with no instance choices).
137+
DenseMap<FModuleLike, DenseSet<FModuleLike>> alwaysReachable;
138+
};
139+
140+
} // namespace firrtl
141+
} // namespace circt
142+
143+
#endif // CIRCT_DIALECT_FIRRTL_INSTANCECHOICEINFO_H

include/circt/Dialect/FIRRTL/Passes.td

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,21 @@ def PopulateInstanceChoiceSymbols : Pass<"firrtl-populate-instance-choice-symbol
920920
let dependentDialects = ["sv::SVDialect"];
921921
}
922922

923+
def CheckInstanceChoice : Pass<"firrtl-check-instance-choice", "firrtl::CircuitOp"> {
924+
let summary = "Check for nested instance choices with different options";
925+
let description = [{
926+
This pass checks that no path from a public module contains nested instance
927+
choices with different options (conjunction is banned). On any single path,
928+
you can only go through instance choices of the same option. However,
929+
reaching the same module through different options on different paths is
930+
allowed (disjunction is OK).
931+
}];
932+
let options = [
933+
Option<"dumpInfo", "dump-info", "bool", "false",
934+
"Dump instance choice information for testing">
935+
];
936+
}
937+
923938
def InferDomains : Pass<"firrtl-infer-domains", "firrtl::CircuitOp"> {
924939
let summary = "Infer and type check all firrtl domains";
925940
let description = [{

lib/Dialect/FIRRTL/Transforms/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ add_circt_dialect_library(CIRCTFIRRTLTransforms
55
AssignOutputDirs.cpp
66
BlackBoxReader.cpp
77
CheckCombLoops.cpp
8+
CheckInstanceChoice.cpp
89
CheckLayers.cpp
910
CheckRecursiveInstantiation.cpp
1011
CreateSiFiveMetadata.cpp
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
//===- CheckInstanceChoice.cpp - Check instance choice configurations -----===//
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+
9+
#include "circt/Dialect/FIRRTL/InstanceChoiceInfo.h"
10+
#include "circt/Dialect/FIRRTL/Passes.h"
11+
#include "circt/Support/LLVM.h"
12+
#include "mlir/Pass/Pass.h"
13+
#include "llvm/ADT/DepthFirstIterator.h"
14+
#include "llvm/ADT/PostOrderIterator.h"
15+
16+
namespace circt {
17+
namespace firrtl {
18+
#define GEN_PASS_DEF_CHECKINSTANCECHOICE
19+
#include "circt/Dialect/FIRRTL/Passes.h.inc"
20+
} // namespace firrtl
21+
} // namespace circt
22+
23+
using namespace circt;
24+
using namespace firrtl;
25+
using namespace mlir;
26+
27+
LogicalResult InstanceChoiceInfo::run() {
28+
for (auto &op : *circuit.getBodyBlock()) {
29+
auto module = dyn_cast<FModuleLike>(op);
30+
if (!module || !module.isPublic())
31+
continue;
32+
33+
auto *rootNode = instanceGraph.lookup(module);
34+
if (!rootNode)
35+
continue;
36+
37+
alwaysReachable[module].insert(module);
38+
computeAlwaysReachable(rootNode, module);
39+
40+
moduleChoices[module][module] = {};
41+
42+
DenseSet<igraph::InstanceGraphNode *> visited;
43+
SmallVector<igraph::InstanceGraphNode *> postOrderNodes;
44+
for (auto *node : llvm::post_order_ext(rootNode, visited))
45+
postOrderNodes.push_back(node);
46+
47+
for (auto *node : llvm::reverse(postOrderNodes))
48+
if (failed(processNode(node, module)))
49+
return failure();
50+
}
51+
52+
return success();
53+
}
54+
55+
void InstanceChoiceInfo::computeAlwaysReachable(igraph::InstanceGraphNode *node,
56+
FModuleLike publicModule) {
57+
SmallVector<igraph::InstanceGraphNode *> worklist;
58+
worklist.push_back(node);
59+
auto &reachableSet = alwaysReachable[publicModule];
60+
61+
while (!worklist.empty()) {
62+
auto *currentNode = worklist.pop_back_val();
63+
auto module =
64+
dyn_cast<FModuleLike>(currentNode->getModule().getOperation());
65+
if (!module)
66+
continue;
67+
68+
for (auto *record : *currentNode) {
69+
auto *targetNode = record->getTarget();
70+
if (!targetNode)
71+
continue;
72+
73+
if (!isa<InstanceOp>(record->getInstance().getOperation()))
74+
continue;
75+
76+
auto targetModule =
77+
dyn_cast<FModuleLike>(targetNode->getModule().getOperation());
78+
if (targetModule && reachableSet.insert(targetModule).second)
79+
worklist.push_back(targetNode);
80+
}
81+
}
82+
}
83+
84+
LogicalResult InstanceChoiceInfo::processNode(igraph::InstanceGraphNode *node,
85+
FModuleLike publicModule) {
86+
auto module = dyn_cast<FModuleLike>(node->getModule().getOperation());
87+
if (!module)
88+
return success();
89+
90+
if (isAlwaysReachable(publicModule, module)) {
91+
moduleChoices[publicModule][module] = {};
92+
return success();
93+
}
94+
95+
assert(module != publicModule && "public module should be pre-initialized");
96+
97+
for (auto *use : node->uses()) {
98+
auto *instOp = use->getInstance().getOperation();
99+
auto *parentNode = use->getParent();
100+
if (!parentNode)
101+
continue;
102+
103+
auto parentModule =
104+
dyn_cast<FModuleLike>(parentNode->getModule().getOperation());
105+
if (!parentModule)
106+
continue;
107+
108+
auto parentIt = moduleChoices[publicModule].find(parentModule);
109+
// It means the parent module is not reachable (regardless of instance or
110+
// instance choice) from the public module.
111+
if (parentIt == moduleChoices[publicModule].end())
112+
continue;
113+
114+
const auto &parentChoices = parentIt->second;
115+
116+
if (auto choice = dyn_cast<InstanceChoiceOp>(instOp)) {
117+
SymbolRefAttr optionName =
118+
FlatSymbolRefAttr::get(choice.getOptionNameAttr());
119+
120+
for (const auto &parentChoice : parentChoices) {
121+
if (parentChoice.option != optionName) {
122+
auto diag = choice.emitOpError()
123+
<< "nested instance choice with option '"
124+
<< optionName.getLeafReference().getValue()
125+
<< "' conflicts with option '"
126+
<< parentChoice.option.getLeafReference().getValue()
127+
<< "' already on the path from public module '"
128+
<< publicModule.getModuleName() << "'";
129+
diag.attachNote(publicModule.getLoc()) << "public module here";
130+
return failure();
131+
}
132+
}
133+
134+
auto &choices = moduleChoices[publicModule][module];
135+
choices.insert(parentChoices.begin(), parentChoices.end());
136+
137+
auto checkAndInsert = [&](SymbolRefAttr caseRef) -> LogicalResult {
138+
for (const auto &parentChoice : parentChoices) {
139+
if (parentChoice.option == optionName &&
140+
parentChoice.caseAttr != caseRef) {
141+
auto diag = choice.emitOpError()
142+
<< "nested instance choice with option '"
143+
<< optionName.getLeafReference().getValue() << "'";
144+
if (caseRef)
145+
diag << " and case '" << caseRef << "'";
146+
else
147+
diag << " and default case";
148+
diag << " conflicts with ";
149+
if (parentChoice.caseAttr)
150+
diag << "case '" << parentChoice.caseAttr << "'";
151+
else
152+
diag << "default case";
153+
diag << " already on the path from public module '"
154+
<< publicModule.getModuleName() << "'";
155+
diag.attachNote(publicModule.getLoc()) << "public module here";
156+
return failure();
157+
}
158+
}
159+
choices.insert({optionName, caseRef});
160+
return success();
161+
};
162+
163+
if (choice.getDefaultTargetAttr().getAttr() == module.getModuleNameAttr())
164+
if (failed(checkAndInsert(SymbolRefAttr())))
165+
return failure();
166+
167+
for (auto [caseRef, moduleRef] : choice.getTargetChoices())
168+
if (moduleRef.getAttr() == module.getModuleNameAttr())
169+
if (failed(checkAndInsert(caseRef)))
170+
return failure();
171+
172+
} else if (isa<InstanceOp>(instOp)) {
173+
moduleChoices[publicModule][module].insert(parentChoices.begin(),
174+
parentChoices.end());
175+
}
176+
}
177+
178+
return success();
179+
}
180+
181+
void InstanceChoiceInfo::dump(raw_ostream &os) const {
182+
for (auto [publicModule, destMap] : moduleChoices) {
183+
os << "Public module: " << publicModule.getModuleName() << "\n";
184+
185+
for (auto [destModule, choices] : destMap) {
186+
// Only output modules that have choices
187+
if (choices.empty()) {
188+
os << " -> " << destModule.getModuleName() << ": <always>\n";
189+
continue;
190+
}
191+
192+
os << " -> " << destModule.getModuleName() << ": ";
193+
llvm::interleaveComma(choices, os, [&](const ChoiceKey &key) {
194+
os << key.option.getLeafReference().getValue();
195+
if (key.caseAttr)
196+
os << "=" << key.caseAttr.getLeafReference().getValue();
197+
else
198+
os << "=<default>";
199+
});
200+
os << "\n";
201+
}
202+
}
203+
}
204+
205+
namespace {
206+
class CheckInstanceChoicePass
207+
: public circt::firrtl::impl::CheckInstanceChoiceBase<
208+
CheckInstanceChoicePass> {
209+
public:
210+
using CheckInstanceChoiceBase::CheckInstanceChoiceBase;
211+
212+
void runOnOperation() override {
213+
auto circuit = getOperation();
214+
auto &instanceGraph = getAnalysis<InstanceGraph>();
215+
InstanceChoiceInfo info(circuit, instanceGraph);
216+
if (failed(info.run()))
217+
return signalPassFailure();
218+
219+
if (dumpInfo)
220+
info.dump(llvm::errs());
221+
222+
markAllAnalysesPreserved();
223+
}
224+
};
225+
} // namespace

0 commit comments

Comments
 (0)