CIRCT 24.0.0git
Loading...
Searching...
No Matches
ModuleInliner.cpp
Go to the documentation of this file.
1//===- ModuleInliner.cpp - FIRRTL module inlining ---------------*- 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 implements FIRRTL module instance inlining.
10//
11// The challenge here is the robust and efficient computation of the final paths
12// along all inlining contexts, and ensuring the actual inlining can be done
13// directly and reliably.
14//
15// The actual inlining is done top-down, recursing from each module to clone
16// directly everything we've been directed to clone. This transformation can
17// have a number of effects on the annotations on cloned modules and the paths
18// used to encode their behavior.
19//
20// The glossary, phase architecture, and invariants almost exclusively focus
21// around ensuring all the possible outcomes are known and ready before the
22// inlining actually begins, greatly simplifying its task.
23//
24// Read the invariants alongside the phases they correspond to, top to bottom.
25//
26// Glossary:
27//
28// VNLA VirtualNLA. Defined below.
29// context (VNLA) one (source hierpath x surviving copy); the planning unit
30// - origSym the source hw.hierpath's symbol
31// - realizedSym the hierpath symbol a context is emitted under (P4):
32// the primary keeps origSym, forks mint fresh names with
33// canonicalization to reuse equal hierpaths effectively.
34// - wasUsed an annotation referenced this context, used to gate
35// fork-context emission.
36//
37// Context kinds:
38// - fork a non-primary context; emitted under a fresh symbol
39// or folded into a path-equal canonical context
40// - convergent a fork folded onto a canonical from a different origSym
41// - canonical the emitted representative of path-equal contexts
42// - duplicate a context folded onto its canonical context
43// - local a context whose path collapsed to its terminal alone
44// - ghost a context P2 minted whose copy P3 never realizes
45//
46// Other:
47// - activeNLAs the active set of contexts at a given inlining level:
48// narrowed by the recursive walk's path down to the leaf
49// - dangling a reference or handle kept past the point its target
50// is erased or renamed
51// - dead-rooted a hierpath left with no surviving context at all
52// - foreign an inner-ref in a cloned body naming any module other
53// than the child being inlined; no update is defined
54//
55// The pass runs as four phases. Outputs freeze before the next phase reads
56// them; analysis only flows forward.
57//
58// * P1 Classify (InliningFacts):
59// Compute per-module classifications (inline/flatten, liveness, ...),
60// and the parent-first order used for the computation.
61// * P2 Plan (NLAPlanner):
62// A `VirtualNLA` per surviving hierpath context, and the routing table
63// of contexts through each instance. Enumerates contexts
64// approximately with a bottom-up walk, and then top-down precisely
65// refines to the actual root. This and InliningFacts (P1) leave the
66// IR untouched in all cases.
67// * P3 Clone (Inliner):
68// Top-down walk inlining marked bodies into parents: patching
69// inner-symbol users, filling hops' final symbols, recording
70// nonlocal-annotation ownership. Writes no annotation.
71// * P4 Write back:
72// Serially canonicalize contexts (minting symbols), rewrite all
73// annotations in parallel (the single writer), then materialize/erase
74// hierpaths serially.
75//
76// Prerequisites:
77// * Inline/flatten markers are annotations on regular modules.
78// LowerAnnotations attaches them there; hand-written IR is checked in P1.
79// * Hierpath roots must resolve to modules (diagnosed in P2).
80// * The instance graph is required (assumed) to be acyclic. In assert builds
81// this is tracked and diagnosed. This is a compiler-wide contract and not
82// worth production cycles (same reason we don't do it in our verifiers).
83//
84// Diagnosed and rejected:
85// * Inlining an instance sitting under anything but a module or layer block.
86// * Inlining a body with an inner reference to another module's body.
87//
88// Both fire during the clone walk (P3), folding into walks we already perform.
89// The pass fails, but the IR may be left partially inlined.
90// Only a P1/P2 rejection guarantees the input is untouched.
91//
92// The invariants this rests on, referenced by number at their use sites.
93// [asserted]/[diagnosed] marks the ones that break loudly. The rest are
94// structural (upheld by construction, no runtime check).
95//
96// Frozen state and identity (P1/P2):
97// * I1 (frozen-facts)
98// The `ModuleClassification` is frozen after P1.
99// * I2 (pointer-identity)
100// The `VirtualNLA` pool is frozen after P2.
101// P3/P4 identify a context by its raw pointer.
102// * I3 (field-writers)
103// P3 mutates only a hop's `finalSym`.
104// P4 alone writes `realizedSym`, `wasUsed`, and the canonical tables.
105//
106// Planning tables (P2):
107// * I4 (ordered-ids)
108// Ids are creation-ordered, contiguous per source symbol and per root.
109// * I5 (routing-sorted)
110// Routing-table entries are born id-sorted and duplicate-free.
111// [asserted: NLAPlanner::run]
112// * I6 (active-sorted)
113// Every level's `activeNLAs` is id-sorted.
114// * I7 (stable-keys)
115// Routing keys (original instance ops) outlive every query.
116// Cloned ops are never lookup keys, so no stale-address aliasing.
117//
118// Path semantics:
119// * I8 (terminal-survives)
120// Terminal hops never evaporate; only instance hops do.
121// [asserted: VirtualNLA::create]
122// A context keeping an instance hop is non-local; one bottomed out at
123// its terminal alone (incl. any one-element source path) is local.
124// There, the annotation localizes onto the op and the path is dropped.
125// * I9 (ghost-contexts)
126// `underFlatten` ORs over parents (any, not all):
127// P2 may mint contexts P3 never realizes.
128// The upward trace prunes subtrees that can only repeat an
129// already-recorded root, keeping enumeration sized by realized
130// copies; the residue inside flatten-reachable regions is believed
131// empty (no witness constructed) but not asserted.
132// `wasUsed` gates fork emission, not the primary.
133// Retention (I15) prevents a ghost renaming a survivor's symbol.
134//
135// Clone walk (P3):
136// * I10 (mint-once)
137// Only P3 mints inner symbol names; one inner-sym namespace per module.
138// * I11 (relocation-total)
139// `relocatedInnerSyms` is total over the clones' inner-refs.
140// A miss is a foreign reference. [diagnosed]
141// * I12 (parents-first)
142// P3 processes parents strictly before children:
143// bodies clone pristine,
144// and each context's leaf op is cloned exactly once.
145// * I13 (activation-vs-ownership)
146// Activation is route-based and can exceed ownership.
147// Every mutation is also gated on the current inlining destination
148// matching the precomputed final module (`finalMod`).
149// (I14, single-writer; filed under write back).
150//
151// Write back (P4):
152// * I14 (single-writer)
153// P4's parallel rewrite has a single writer per context:
154// the module holding its last hop's `finalMod`.
155// * I15 (retention)
156// Every origSym with a surviving context has exactly one primary
157// claimant (`realizedSym == origSym`),
158// emitted unconditionally. [asserted: writebackHierPaths]
159// Only a context-less (dead-rooted) origSym is ever erased.
160//
161//===----------------------------------------------------------------------===//
162
176#include "circt/Support/Debug.h"
177#include "circt/Support/LLVM.h"
178#include "circt/Support/Utils.h"
179#include "mlir/IR/IRMapping.h"
180#include "mlir/IR/Threading.h"
181#include "mlir/Pass/Pass.h"
182#include "llvm/ADT/DenseMap.h"
183#include "llvm/ADT/MapVector.h"
184#include "llvm/ADT/STLExtras.h"
185#include "llvm/Support/Debug.h"
186#include "llvm/Support/FormatVariadic.h"
187#include "llvm/Support/TrailingObjects.h"
188
189#define DEBUG_TYPE "firrtl-inliner"
190
191namespace circt {
192namespace firrtl {
193#define GEN_PASS_DEF_INLINER
194#include "circt/Dialect/FIRRTL/Passes.h.inc"
195} // namespace firrtl
196} // namespace circt
197
198using namespace circt;
199using namespace firrtl;
200
201using hw::InnerRefAttr;
202using InnerRefToNewNameMap = DenseMap<hw::InnerRefAttr, StringAttr>;
203
204//===----------------------------------------------------------------------===//
205// Module classification (P1)
206//===----------------------------------------------------------------------===//
207
208namespace {
209
210/// Return the instance if the pass can splice a body through this operation.
211/// Today that is exactly a plain InstanceOp.
212///
213/// Non-null answers the operation kind and nothing more.
214/// Whether the instance is inlined also depends on its target being a regular
215/// firrtl.module, and on the inline/flatten marks.
216/// Null is definitive: the pass never splices through anything else.
217///
218/// The operation kind is a conservative stand-in for the precise question,
219/// the meet over the operation's possible targets in the instance graph.
220/// Under that framing an instance_choice naming one module throughout would be
221/// inlinable, and a new instantiation kind slots in by answering that meet.
222static InstanceOp getInlinableInstance(Operation *op) {
223 return dyn_cast_or_null<InstanceOp>(op);
224}
225
226/// Module facts regarding inlining.
227struct ModuleInfo {
228 /// The module carries an inline annotation.
229 bool hasInline : 1;
230
231 /// The module carries a flatten annotation.
232 bool hasFlatten : 1;
233
234 /// Does /any/ instantiation path flatten this module?
235 bool underFlatten : 1;
236
237 /// Does any instantiation path /not/ flatten this module?
238 /// Note: zero-length path counts as well (e.g., public).
239 ///
240 /// Note: Only used during computation, consumers want isLive.
241 bool hasUnflattenedPath : 1;
242
243 /// Will this module exist in the final result?
244 bool isLive : 1;
245
246 /// Derived named predicates over the facts above, used in the walk.
247
248 /// This module's regular children remain instantiated: some path keeps it
249 /// instantiated and it is not flattening them away.
250 bool keepsChildrenInstantiated() const {
251 return hasUnflattenedPath && !hasFlatten;
252 }
253
254 /// Whether this module's body may be flattened.
255 bool mayBeFlattened() const { return underFlatten || hasFlatten; }
256
257 // (No default member initialization of bitfield members until C++20)
258 ModuleInfo()
259 : hasInline(false), hasFlatten(false), underFlatten(false),
260 hasUnflattenedPath(false), isLive(false) {}
261};
262
263class InliningFacts {
264public:
265 using ModuleClassification = DenseMap<Operation *, ModuleInfo>;
266
267private:
268 ModuleClassification classification;
269 SmallVector<FModuleOp, 0> schedule;
270
271 InliningFacts(ModuleClassification &&classification,
272 SmallVector<FModuleOp, 0> &&schedule)
273 : classification(std::move(classification)),
274 schedule(std::move(schedule)) {}
275
276public:
277 /// Analyze circuit and compute per-module inlining facts.
278 ///
279 /// Returns facts on success, on failure emits diagnostics.
280 static FailureOr<InliningFacts> compute(CircuitOp circuit,
281 InstanceGraph &instanceGraph,
282 const mlir::SymbolTable &symbolTable);
283
284 /// Get per-module classification information.
285 const ModuleInfo &getModuleInfo(FModuleLike fmod) const {
286 assert(fmod && "queried with null fmodulelike");
287 auto it = classification.find(fmod);
288 assert(it != classification.end() && "module not found");
289 return it->second;
290 }
291
292 /// Get classification for the specified operation, if known.
293 std::optional<ModuleInfo> getModuleInfoIfPresent(Operation *op) const {
294 auto it = classification.find(op);
295 if (it == classification.end())
296 return std::nullopt;
297 return it->second;
298 }
299
300 /// Convenience helpers, projecting out common fields.
301
302 bool isLive(FModuleLike mod) const { return getModuleInfo(mod).isLive; }
303 bool hasInline(FModuleLike mod) const { return getModuleInfo(mod).hasInline; }
304 bool hasFlatten(FModuleLike mod) const {
305 return getModuleInfo(mod).hasFlatten;
306 }
307
308 // Convenience helper to project out `isLive` if operation is known, or false.
309 bool isKnownLive(Operation *op) const {
310 auto ret = getModuleInfoIfPresent(op);
311 return ret && ret->isLive;
312 }
313
314 /// Regular modules in inverse post-order: parents strictly before children.
315 /// Frozen with the facts, it is the order this analysis was computed over.
316 /// Entries are op handles: they dangle once modules are erased.
317 ArrayRef<FModuleOp> getSchedule() const { return schedule; }
318
319#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
320 void dump() const {
321 auto &os = llvm::dbgs();
322 auto printOne = [&os](StringRef name, ModuleInfo info) {
323 os << "@" << name << ": inline=" << info.hasInline
324 << " flatten=" << info.hasFlatten
325 << " underFlatten=" << info.underFlatten
326 << " unflattenedPath=" << info.hasUnflattenedPath
327 << " live=" << info.isLive << "\n";
328 };
329 // Schedule order for regular modules, then the residue sorted by name.
330 for (auto module : schedule)
331 printOne(module.getModuleName(), classification.lookup(module));
332 SmallVector<std::pair<StringRef, ModuleInfo>> rest;
333 for (auto &[op, info] : classification)
334 if (!isa<FModuleOp>(op))
335 rest.emplace_back(cast<FModuleLike>(op).getModuleName(), info);
336 llvm::sort(rest, llvm::less_first());
337 for (auto &[name, info] : rest)
338 printOne(name, info);
339 }
340#endif
341};
342
343} // namespace
344
345FailureOr<InliningFacts>
346InliningFacts::compute(CircuitOp circuit, InstanceGraph &instanceGraph,
347 const mlir::SymbolTable &symbolTable) {
348 InliningFacts::ModuleClassification classification;
349 SmallVector<FModuleOp, 0> schedule;
350
351 // Cache these attributes.
352 auto *ctx = circuit.getContext();
353 auto inlineAnnoClassAttr = StringAttr::get(ctx, inlineAnnoClass);
354 auto flattenAnnoClassAttr = StringAttr::get(ctx, flattenAnnoClass);
355
356 // Find roots of symbol uses within `op` and mark them live. Root liveness is
357 // all that's promised here (and all likely/able to encounter in practice).
358 // The symbol portion of inner refs are kept alive through this walk too.
359 auto markSymbolUses = [&](Operation &op) -> LogicalResult {
360 // If we can't compute uses, bail.
361 auto symbolUses = SymbolTable::getSymbolUses(&op);
362 if (!symbolUses)
363 return op.emitError("cannot analyze symbol uses of this operation");
364 for (const auto &use : *symbolUses) {
365 auto root = use.getSymbolRef().getRootReference();
366 if (auto moduleLike = symbolTable.lookup<FModuleLike>(root)) {
367 auto &info = classification[moduleLike];
368 info.isLive = true;
369 info.hasUnflattenedPath = true;
370 }
371 }
372 return success();
373 };
374
375 // The circuit's attributes contain symbol references that are expected to
376 // resolve within the circuit, walk those now. Since the circuit is itself a
377 // symbol table this will only walk the attributes. FYI: These attributes
378 // technically are interpreted by MLIR to resolve in the circuit parent's
379 // table, but we use them to name operations within the circuit. None of
380 // these are known to point to modules today, check anyway.
381 if (failed(markSymbolUses(*circuit.getOperation())))
382 return failure();
383
384 for (auto &op : circuit.getOps()) {
385 // Initialize module information. Not order-dependent.
386 if (auto module = dyn_cast<FModuleLike>(op)) {
387 auto &info = classification[module];
388 AnnotationSet anno(module);
389 info.hasInline = anno.hasAnnotation(inlineAnnoClassAttr);
390 info.hasFlatten = anno.hasAnnotation(flattenAnnoClassAttr);
391
392 // Reject inline/flatten on anything but a regular module (FModuleOp).
393 // LowerAnnotations restricts these to FModuleOp; this catches raw IR.
394 if (!isa<FModuleOp>(module) && (info.hasInline || info.hasFlatten))
395 return emitError(module.getLoc()) << "inline/flatten annotations are "
396 "only valid on a 'firrtl.module'";
397
398 // Does anything opaque (see getInlinableInstance) instantiate this?
399 auto instantiators = instanceGraph.lookup(module)->uses();
400 auto opaqueRecIt = llvm::find_if(instantiators, [](InstanceRecord *rec) {
401 return !getInlinableInstance(rec->getInstance());
402 });
403 bool hasOpaqueUse = opaqueRecIt != instantiators.end();
404
405 if (!cast<mlir::SymbolOpInterface>(module.getOperation())
406 .canDiscardOnUseEmpty() ||
407 hasOpaqueUse) {
408 info.isLive = true;
409 info.hasUnflattenedPath = true;
410 }
411 if (info.hasInline && hasOpaqueUse) {
412 auto diag = mlir::emitWarning(module.getLoc())
413 << "module marked inline is also instantiated by an "
414 "operation that cannot be inlined; it is inlined only "
415 "into its 'firrtl.instance' parents and retained";
416 diag.attachNote((*opaqueRecIt)->getInstance()->getLoc())
417 << "instantiated here";
418 }
419 continue;
420 }
421
422 // Symbol use analysis:
423
424 // Ignore symbol uses in NLAs.
425 if (isa<hw::HierPathOp>(op))
426 continue;
427
428 if (failed(markSymbolUses(op)))
429 return failure();
430 }
431
432 // Calculate inlining info top-down.
433 instanceGraph.walkInversePostOrder([&](igraph::InstanceGraphNode &node) {
434 auto *mod = node.getModule().getOperation();
435 assert(isa<FModuleLike>(mod) && "instance graph contains non-fmodulelike");
436
437 // Save IPO over FModuleOp's for later.
438 if (auto fmod = dyn_cast<FModuleOp>(mod))
439 schedule.push_back(fmod);
440 auto &modInfo = classification[mod];
441
442 // Skip if no non-inlined path to this module.
443 if (!modInfo.hasUnflattenedPath && !modInfo.underFlatten)
444 return;
445
446 for (auto *edge : node) {
447 auto *childMod = edge->getTarget()->getModule().getOperation();
448 auto &childInfo = classification[childMod];
449 bool isRegularModule = isa<FModuleOp>(childMod);
450 assert(
451 (isRegularModule || !(childInfo.hasInline || childInfo.hasFlatten)) &&
452 "non-fmoduleop with inline/flatten annotation");
453 if (isRegularModule && modInfo.mayBeFlattened())
454 childInfo.underFlatten = true;
455
456 // A non-regular child is never inlined and so it is always instantiated.
457 // A regular child does iff this module does and isn't flattening it.
458 if (modInfo.keepsChildrenInstantiated() || !isRegularModule)
459 childInfo.hasUnflattenedPath = true;
460
461 // Set liveness.
462 if (childInfo.hasUnflattenedPath &&
463 (!isRegularModule || !childInfo.hasInline))
464 childInfo.isLive = true;
465 }
466 });
467
468 return InliningFacts(std::move(classification), std::move(schedule));
469}
470
471//===----------------------------------------------------------------------===//
472// NLA Planning (P2)
473//===----------------------------------------------------------------------===//
474
475/// Compute the final shape of all NLAs and their routing through the design.
476/// All NLAs in the design are converted into one or more VNLA (VirtualNLA),
477/// which contain surviving portions of the original NLA as well as new context
478/// needed to handle situations where an NLA root is inlined and "forks".
479
480namespace {
481
482/// One hop of a VirtualNLA's surviving path.
483///
484/// Tracks the original and final module/symbol locations for a hierpath hop.
485struct SurvivingHop {
486 /// The original module containing this hop.
487 StringAttr origMod;
488 /// Inner symbol within origMod, when the hop has one.
489 ///
490 /// A terminal hop's sym may name a non-instance, a module-only terminal hop
491 /// has none.
492 StringAttr origSym;
493 /// The module this hop lands in after inlining.
494 StringAttr finalMod;
495 /// I3: the only field P3 mutates, set when the walk realizes the hop.
496 ///
497 /// Everything else on a hop and its VirtualNLA is read-only after P2.
498 StringAttr finalSym;
499};
500
501/// A virtual NLA representing one surviving hierpath context after inlining.
502/// Frozen after P2 (I2); only `finalSym` is mutated by P3 (I3).
503class VirtualNLA final : llvm::TrailingObjects<VirtualNLA, SurvivingHop> {
504 friend TrailingObjects;
505
506 unsigned numHops;
507
508 VirtualNLA(unsigned id, StringAttr origSym, ArrayRef<SurvivingHop> path)
509 : numHops(path.size()), id(id), origSym(origSym) {
510 llvm::uninitialized_copy(path, getTrailingObjects());
511 }
512
513public:
514 /// Unique creation-ordered identifier for this context (I4).
515 unsigned id;
516 /// The original hierpath symbol this context was derived from.
517 StringAttr origSym;
518 /// The symbol that this context's hierpath is emitted under, minted by P4.
519 /// The first canonical claimant of an origSym keeps it. Later ones mint
520 /// fresh names; duplicates and locals get none.
521 StringAttr realizedSym;
522 /// Whether this context was referenced by any annotation (I9). Duplicates
523 /// use canonical context symbols. New hierpaths are emitted iff set.
524 bool wasUsed = false;
525
526 static VirtualNLA *create(llvm::BumpPtrAllocator &alloc, unsigned id,
527 StringAttr origSym, ArrayRef<SurvivingHop> path) {
528 // Every context keeps at least its terminal hop (I8), so the path is never
529 // empty; `back()`/`isLocal()` and the writeback rely on this. Enforce it
530 // once, at the single construction site, not at each use.
531 assert(!path.empty() && "a VNLA always keeps its terminal hop (I8)");
532 size_t size = totalSizeToAlloc<SurvivingHop>(path.size());
533 auto *mem = alloc.Allocate(size, alignof(VirtualNLA));
534 return new (mem) VirtualNLA(id, origSym, path);
535 }
536
537 bool isLocal() const { return numHops <= 1; }
538
539 ArrayRef<SurvivingHop> getPath() const {
540 return {getTrailingObjects(), numHops};
541 }
542
543 MutableArrayRef<SurvivingHop> getPathMutable() {
544 return {getTrailingObjects(), numHops};
545 }
546
547#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
548 LLVM_DUMP_METHOD void dump() const {
549 llvm::dbgs() << llvm::formatv(" VirtualNLA {0}: origSym @{1}", id,
550 origSym);
551 if (isLocal()) {
552 llvm::dbgs() << " -> local\n";
553 } else {
554 llvm::dbgs() << llvm::formatv(", hops: {0}\n", numHops);
555 for (const auto &hop : getPath()) {
556 llvm::dbgs() << llvm::formatv(
557 " - {0}::{1} -> {2}::{3}\n", hop.origMod,
558 (hop.origSym ? hop.origSym.str() : "*"), hop.finalMod,
559 (hop.finalSym ? hop.finalSym.str() : "(TBD)"));
560 }
561 }
562 }
563#endif
564};
565
566static_assert(std::is_trivially_destructible_v<VirtualNLA>,
567 "VirtualNLA is arena-allocated; destructors never run");
568
569} // namespace
570
571/// Context collections are ordered by creation id throughout (I4/I5/I6).
572static bool vnlaIdLess(const VirtualNLA *a, const VirtualNLA *b) {
573 return a->id < b->id;
574}
575
576namespace {
577
578/// One hop of an absolute NLA path.
579///
580/// Hops that are instances have their operation stored in `inst`. If the hop
581/// has an inner symbol, it is stored in `sym` (else null).
582///
583/// Non-terminal hops must be instances, while terminal hops can be module or
584/// innerrefs. A module-only terminal has neither `inst` nor `sym`.
585struct PathHop {
586 StringAttr mod; ///< Containing module.
587 Operation *inst; ///< The instance op, if this hop is an instance.
588 StringAttr sym; ///< Inner symbol, if known.
589 bool operator==(const PathHop &o) const {
590 return mod == o.mod && inst == o.inst && sym == o.sym;
591 }
592};
593
594/// A hop-path view with precomputed hash: the upper-path distinctness key
595/// for the debug check that the pruned climb never repeats a path.
596/// The view must reference storage that outlives the set (`upperPaths`).
597struct TrimmedPathRef {
598 ArrayRef<PathHop> path;
599 llvm::hash_code hash;
600
601 static TrimmedPathRef get(ArrayRef<PathHop> path) {
602 llvm::hash_code h = llvm::hash_value(path.size());
603 for (const PathHop &hop : path)
604 h = llvm::hash_combine(h, hop.mod.getAsOpaquePointer(), hop.inst,
605 hop.sym.getAsOpaquePointer());
606 return {path, h};
607 }
608};
609
610/// P2: Plans VirtualNLA contexts for each hierpath that survives inlining.
611/// Traces up from roots, trims paths, deduplicates, and builds routing tables.
612/// Read-only after run(); a rejected plan leaves IR untouched.
613class NLAPlanner {
614public:
615 NLAPlanner(CircuitOp circuit, SymbolTable &symbolTable,
616 InstanceGraph &instanceGraph, const InliningFacts &facts)
617 : circuit(circuit), symbolTable(symbolTable),
618 instanceGraph(instanceGraph), facts(facts) {}
619
620 LogicalResult run();
621#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
622 LLVM_DUMP_METHOD void dump();
623#endif
624
625 /// Source-path terminal-shape counters, copied into the pass statistics.
626 ///
627 /// The leaf-rename machinery (`updateVirtualNLALeafSymbols` and the scan in
628 /// `rename`) exists to serve paths ending at an inner symbol.
629 struct Statistics {
630 size_t endInnerSym = 0;
631 size_t endModule = 0;
632 } stats;
633
634private:
635 /// Create a VNLA using our allocator; its `id` is its index in `allVNLAs`.
636 VirtualNLA *createVNLA(StringAttr origSym, ArrayRef<SurvivingHop> path);
637
638 /// Trace upward from a root module until reaching surviving modules,
639 /// discovering all contexts where the root appears after inlining.
640 /// Depth-first over the instance graph's uses, with an explicit stack.
641 ///
642 /// Once the path climbed so far contains the module we would trim to
643 /// when NOT under a flatten, climbing through flatten-free parents can only
644 /// rediscover that same context. Those subtrees are skipped entirely
645 /// ("pinned", below). Recorded paths /may/ still be broader than realized
646 /// copies (I9), but the work is sized by those copies, not raw path
647 /// count.
648 LogicalResult
649 traceUpUntilSurviving(StringAttr rootModName, hw::HierPathOp diagAnchor,
650 SmallVectorImpl<SmallVector<PathHop>> &discoveredPaths);
651
652 /// Create a VirtualNLA for one concrete path context.
653 /// The new context lands in `allVNLAs` and in `pathRoutingTable`.
654 /// The result carries only whether planning this context succeeded.
655 LogicalResult
656 processSinglePathContext(StringAttr origSym,
657 const SmallVectorImpl<PathHop> &absPath,
658 hw::HierPathOp diagAnchor);
659
660 /// Return the index of the minimal root within `upperPath`: how many leading
661 /// hops to drop so the context roots at the deepest module still surviving on
662 /// this concrete path.
663 ///
664 /// `rootMod` is the (original) NLA root that the last upper hop climbs into.
665 /// Its own fate decides: real climb above (keep) or spurious (trim).
666 ///
667 /// The pruned climb only produces paths that already trim to themselves, so
668 /// this should always return 0 (asserted in run()).
669 ///
670 /// This is the forward version (top-down) and more intuitive formulation; we
671 /// keep it as the oracle and as the primary "definition".
672 ///
673 /// Additional inlining behaviors need to update this and re-derive the
674 /// upwards tracing logic together.
675 size_t minimalRootIndex(ArrayRef<PathHop> upperPath, StringAttr rootMod);
676
677 /// Resolve an instance named by (`module`, `innerSym`) to its op.
678 ///
679 /// Every non-terminal namepath hop is an instance by definition, and the
680 /// instance graph already holds those ops (no IR walk / IST needed).
681 ///
682 /// Each module's instances are indexed once, on its first hop. Total cost is
683 /// one graph-node sweep per distinct hop module, not per hop.
684 ///
685 /// Returns null when the sym names a non-instance (a terminal wire/port hop)
686 /// or the module is unknown; such hops are not routed through.
687 Operation *resolveInstanceHop(StringAttr module, StringAttr innerSym);
688
689 CircuitOp circuit;
690 SymbolTable &symbolTable;
691 InstanceGraph &instanceGraph;
692 const InliningFacts &facts;
693
694 /// Per-module instance index for `resolveInstanceHop`. Only modules
695 /// hierpaths hop through are indexed.
696 DenseMap<StringAttr, DenseMap<StringAttr, Operation *>> instanceHopIndex;
697
698 /// Stable pool allocation for virtual NLA structures.
699 llvm::BumpPtrAllocator alloc;
700
701 using VirtualNLAHandles = SmallVector<VirtualNLA *>;
702
703public:
704 /// Instance op -> the VNLAs routing through that instance (safe per I7).
705 /// Keyed on the op: one lookup per descent, no (module, sym) probing.
706 DenseMap<Operation *, VirtualNLAHandles> pathRoutingTable;
707 /// NLA sym -> set of its VNLA's: contiguous slices of `allVNLAs` (I4).
708 /// Materialized once the pool freezes; appending after would dangle them.
709 DenseMap<StringAttr, ArrayRef<VirtualNLA *>> origToVNLAs;
710 /// All VNLA's in creation order, contiguous per origSym (I4).
711 SmallVector<VirtualNLA *> allVNLAs;
712
713 /// Source symbol -> its HierPathOp, recorded while bucketing during run().
714 /// Valid pass-wide: hierpath ops are untouched until the final writeback.
715 DenseMap<StringAttr, hw::HierPathOp> hierPathOps;
716};
717
718} // namespace
719
720/// DenseMapInfo specialization for TrimmedPathRef to enable deduplication.
721template <>
722struct llvm::DenseMapInfo<TrimmedPathRef> {
723 static unsigned getHashValue(const TrimmedPathRef &key) {
724 return static_cast<unsigned>(key.hash);
725 }
726 static bool isEqual(const TrimmedPathRef &a, const TrimmedPathRef &b) {
727 return a.hash == b.hash && a.path == b.path;
728 }
729};
730
731LogicalResult NLAPlanner::run() {
732 // Bucket HierPathOps by their root, preserving their encounter order.
733 //
734 // Allows us to reuse `traceUpUntilSurviving` and the trimming/dedup work
735 // across all NLA's sharing a root (bucket).
736 //
737 // VNLA creation is contiguous per origSym (I4), grouped by root.
739 for (auto nla : circuit.getOps<hw::HierPathOp>()) {
740 byRoot[nla.root()].push_back(nla);
741 hierPathOps[nla.getSymNameAttr()] = nla;
742 }
743
744 for (auto &[origRoot, nlas] : byRoot) {
745 // 1. Perform the work shared by all NLA's in this bucket:
746
747 // 1.1 Calculate all the upperPaths by tracing upwards as needed.
748 SmallVector<SmallVector<PathHop>> upperPaths;
749 if (failed(traceUpUntilSurviving(origRoot, nlas.front(), upperPaths)))
750 return failure();
751
752#ifndef NDEBUG
753 // Check paths trim to themselves and there are no duplicates.
754 llvm::SmallDenseSet<TrimmedPathRef, 8> seenPaths;
755 for (auto &upperPath : upperPaths) {
756 assert(minimalRootIndex(upperPath, origRoot) == 0 &&
757 "pruned climb leaked a trimmable path");
758 assert(seenPaths.insert(TrimmedPathRef::get(upperPath)).second &&
759 "pruned climb repeated a path");
760 }
761#endif
762
763 // 2. Process each NLA in the bucket.
764 for (auto nla : nlas) {
765 auto origSym = nla.getSymNameAttr();
766
767 // 2.1 Extract the "hop"s from the NLA.
768 SmallVector<PathHop> nlaHops;
769 for (auto element : nla.getNamepath()) {
770 if (auto ref = dyn_cast<InnerRefAttr>(element)) {
771 nlaHops.push_back({ref.getModule(),
772 resolveInstanceHop(ref.getModule(), ref.getName()),
773 ref.getName()});
774 } else if (auto flat = dyn_cast<FlatSymbolRefAttr>(element))
775 nlaHops.push_back({flat.getAttr(), nullptr, StringAttr()});
776 else
777 llvm_unreachable("NLA element must be innerref or flat symbol");
778 }
779
780 // Track statistics about terminal shape.
781 ++(nlaHops.back().sym ? stats.endInnerSym : stats.endModule);
782
783 // 2.2 Construct each (bucket-common) prefix + NLA path, and hand to
784 // helper to walk the full path and produce final VNLA for each.
785 for (auto &upperPath : upperPaths) {
786 SmallVector<PathHop> absolutePath;
787 llvm::append_range(absolutePath, upperPath);
788 llvm::append_range(absolutePath, nlaHops);
789
790 if (failed(processSinglePathContext(origSym, absolutePath, nla)))
791 return failure();
792 }
793 }
794 }
795
796 // I5: routing entries are born id-sorted and duplicate-free.
797 assert(llvm::all_of(pathRoutingTable,
798 [](const auto &entry) {
799 return llvm::is_sorted(entry.second, vnlaIdLess);
800 }) &&
801 "routing entries must be born id-sorted (I5)");
802
803 // 3. Materialize the per-symbol group views that point to VNLA ranges:
804 // contexts are contiguous per origSym in creation order (I4).
805 //
806 // The pool is frozen from here on (I2).
807 for (size_t i = 0, e = allVNLAs.size(); i < e;) {
808 StringAttr origSym = allVNLAs[i]->origSym;
809 size_t groupStart = i;
810 while (i < e && allVNLAs[i]->origSym == origSym)
811 ++i;
812 origToVNLAs[origSym] =
813 ArrayRef<VirtualNLA *>(&allVNLAs[groupStart], i - groupStart);
814 }
815
816 return success();
817}
818
819VirtualNLA *NLAPlanner::createVNLA(StringAttr origSym,
820 ArrayRef<SurvivingHop> path) {
821 // I4: VNLA id is monotonic and P2 processes one hierpath at a time,
822 // so ids are creation-ordered and contiguous per source symbol.
823 auto id = allVNLAs.size();
824 allVNLAs.push_back(VirtualNLA::create(alloc, id, origSym, path));
825 return allVNLAs.back();
826}
827
828LogicalResult NLAPlanner::traceUpUntilSurviving(
829 StringAttr rootModName, hw::HierPathOp diagAnchor,
830 SmallVectorImpl<SmallVector<PathHop>> &discoveredPaths) {
831 using UseIterator =
832 decltype(std::declval<igraph::InstanceGraphNode>().uses().begin());
833
834 /// Stack frame for iterative upward trace through the instance graph.
835 struct Frame {
836 StringAttr modName;
837 UseIterator currentEdge;
838 UseIterator endEdge;
839 bool isFirstVisit;
840 bool pinned;
841 };
842
843 SmallVector<Frame, 16> stack;
844 SmallVector<PathHop, 8> currentPath;
845
846#ifndef NDEBUG
847 DenseMap<StringAttr, bool> visited;
848#endif
849
850 // Edge-derived names come from the instance graph itself: always resolve.
851 // Only the root, straight from the namepath, can name a missing module.
852 auto pushState = [&](StringAttr name) -> LogicalResult {
853 auto *node = instanceGraph.lookupOrNull(name);
854 if (!node)
855 return diagAnchor.emitOpError()
856 << "names non-existent root module @" << name;
857 auto uses = node->uses();
858 stack.push_back({name, uses.begin(), uses.end(), /*isFirstVisit=*/true,
859 /*pinned=*/false});
860
861#ifndef NDEBUG
862 if (visited[name])
863 return mlir::emitError(node->getModule().getLoc(),
864 "instance graph contains cycle");
865 visited[name] = true;
866#endif
867
868 return success();
869 };
870 auto popState = [&]() {
871#ifndef NDEBUG
872 auto name = stack.back().modName;
873 auto it = visited.find(name);
874 assert(it != visited.end() && "visited map missing module");
875 assert(it->second && "visited not set for module");
876 it->second = false;
877#endif
878 stack.pop_back();
879 };
880
881 if (failed(pushState(rootModName)))
882 return failure();
883
884 while (!stack.empty()) {
885 auto &frame = stack.back();
886 if (frame.isFirstVisit) {
887 frame.isFirstVisit = false;
888
889 auto *currentModNode = instanceGraph.lookup(frame.modName);
890 auto *currentModOp = currentModNode->getModule().getOperation();
891 auto infoIfValid = facts.getModuleInfoIfPresent(currentModOp);
892 // This is expected unreachable, but diagnose for safety.
893 if (!infoIfValid)
894 return mlir::emitError(
895 currentModOp->getLoc(),
896 "hierarchical path traced up through unknown operation")
897 .attachNote(diagAnchor.getLoc())
898 << "encountered tracing up from root of this hierarchical path";
899 auto info = *infoIfValid;
900
901 // Track whether the path climbed so far already contains the module
902 // we would trim to when NOT under a flatten ("pinned").
903 //
904 // Per frame:
905 // - a non-inline module pins (it roots; a deeper root still wins)
906 // - an inline module passes its child's state through
907 // - an inline+flatten module unpins (it cannot root, and its flatten
908 // cuts every deeper root off, so the root must come from above).
909 bool childPinned =
910 stack.size() > 1 ? stack[stack.size() - 2].pinned : false;
911 frame.pinned = !info.hasInline || (!info.hasFlatten && childPinned);
912
913 // Record a path here if this module is live AND actually roots /some/
914 // context.
915 //
916 // Don't emit if a deeper root already emitted the trimmed form for this!
917 // This happened if child frame is pinned (deeper root must be live) as
918 // long as our own flatten doesn't override that.
919 //
920 // A context's original root frame records it before anything above climbs
921 // (stack). Primary selection rests on that ordering (I4).
922 bool deeperRootWins = childPinned && !info.hasFlatten;
923 if (info.isLive && !deeperRootWins)
924 discoveredPaths.push_back(llvm::to_vector(llvm::reverse(currentPath)));
925
926 // If this module is unconditionally live, we're done tracing upwards.
927 if (!info.hasInline && !info.underFlatten) {
928 popState();
929 if (!stack.empty())
930 currentPath.pop_back();
931 continue;
932 }
933 }
934 // If we've exhausted all edges, we're done with this frame.
935 if (frame.currentEdge == frame.endEdge) {
936 popState();
937 if (!stack.empty())
938 currentPath.pop_back();
939 continue;
940 }
941
942 // Trace up the current edge and advance it on the frame.
943 auto *edge = *frame.currentEdge;
944 ++frame.currentEdge;
945
946 auto *instOp = edge->getInstance().getOperation();
947 // Only climb through inlinable instances (see getInlinableInstance).
948 // Opaque instantiations keep their targets alive and are handled above.
949 //
950 // There is no copy in this parent to enumerate -> don't climb!
951 if (!getInlinableInstance(instOp))
952 continue;
953 // Once pinned, the only reason to climb is a flatten above us!
954 //
955 // Parents that neither flatten NOR are (in any context) flattened
956 // themselves will all trim back down, skip those subtrees entirely.
957 if (frame.pinned) {
958 auto *parentOp = edge->getParent()->getModule().getOperation();
959 auto parentInfo = facts.getModuleInfoIfPresent(parentOp);
960 // This is expected unreachable, but diagnose for safety.
961 if (!parentInfo)
962 return mlir::emitError(
963 parentOp->getLoc(),
964 "hierarchical path traced up through unknown operation")
965 .attachNote(diagAnchor.getLoc())
966 << "encountered tracing up from root of this hierarchical path";
967 if (!parentInfo->mayBeFlattened())
968 continue;
969 }
970 auto parentName = edge->getParent()->getModule().getModuleNameAttr();
971 currentPath.push_back({parentName, instOp, getInnerSymName(instOp)});
972 if (failed(pushState(parentName)))
973 return failure();
974 }
975
976 return success();
977}
978
979size_t NLAPlanner::minimalRootIndex(ArrayRef<PathHop> upperPath,
980 StringAttr rootMod) {
981 // The bottom-up trace over-approximates to ensure coverage.
982 // Here we walk top-down to find the minimal root point precisely.
983 //
984 // Returns its index in `upperPath`; `upperPath.size()` roots at `rootMod`.
985 // The namepath itself is the user's spec and is never trimmed.
986 //
987 // This rooting is coupled to inline/flatten knowledge.
988 // A new way for a module to be inlined away or relocated must re-derive it,
989 // or a surviving root is misidentified (misrouted annotation; I9 churn).
990 //
991 // Inline and flatten act differently, deciding how deep to root:
992 // - Inline: never survives as a root but children do. Keep looking.
993 // - Flatten: subtree doesn't survive, this is as deep as we can root.
994 //
995 // The dropped prefix has no flatten, so the kept suffix evaluates the same.
996 bool isTransitiveFlatten = false;
997 // `root` never stays unset: we traced upwards until this was certain.
998 size_t root = 0;
999 for (size_t i = 0, e = upperPath.size(); i <= e; ++i) {
1000 // Flatten means we're done searching, use the deepest we've found.
1001 if (isTransitiveFlatten)
1002 break;
1003 StringAttr mod = i < e ? upperPath[i].mod : rootMod;
1004 const auto &info =
1005 facts.getModuleInfo(symbolTable.lookup<FModuleLike>(mod));
1006 // Inline modules don't survive as a root; can still root below them.
1007 if (!info.hasInline)
1008 root = i;
1009 isTransitiveFlatten |= info.hasFlatten;
1010 }
1011 return root;
1012}
1013
1014Operation *NLAPlanner::resolveInstanceHop(StringAttr module,
1015 StringAttr innerSym) {
1016 auto [entry, inserted] = instanceHopIndex.try_emplace(module);
1017 if (inserted) {
1018 auto *node = instanceGraph.lookupOrNull(module);
1019 if (!node)
1020 return nullptr;
1021 for (auto *record : *node) {
1022 auto *inst = record->getInstance().getOperation();
1023 if (auto sym = getInnerSymName(inst))
1024 entry->second.try_emplace(sym, inst);
1025 }
1026 }
1027 return entry->second.lookup(innerSym);
1028}
1029
1030LogicalResult
1031NLAPlanner::processSinglePathContext(StringAttr origSym,
1032 const SmallVectorImpl<PathHop> &absPath,
1033 hw::HierPathOp diagAnchor) {
1034 SmallVector<SurvivingHop> survivingHops;
1035 assert(!absPath.empty() && "empty absolute path -- empty namepath?");
1036
1037 StringAttr currentDest = absPath.front().mod;
1038 auto destMod = symbolTable.lookup<FModuleLike>(currentDest);
1039 const auto &destInfo = facts.getModuleInfo(destMod);
1040
1041 bool isTransitiveFlatten = destInfo.hasFlatten;
1042 // The module whose flatten most recently set `isTransitiveFlatten`;
1043 // non-null exactly when the flag is set. Names the culprit in diagnostics.
1044 StringAttr flattenCause = isTransitiveFlatten ? currentDest : StringAttr{};
1045 for (auto it = absPath.begin(), end = absPath.end(); it != end; ++it) {
1046 const auto &hop = *it;
1047 bool isTerminal = std::next(it) == end;
1048
1049 // A hop's operation is null, inlinable, or opaque
1050 // (see getInlinableInstance).
1051 auto hopInst = getInlinableInstance(hop.inst);
1052 bool isOpaqueInstanceHop = hop.inst && !hopInst;
1053
1054 // Interior hops read the recorded next module; a terminal names one only
1055 // through a plain instance.
1056 StringAttr nextModName;
1057 if (!isTerminal)
1058 nextModName = std::next(it)->mod;
1059 else if (hopInst)
1060 nextModName = hopInst.getReferencedModuleNameAttr();
1061
1062 // The next module's facts, defaulted for the end of the path.
1063 bool nextHasInline = false;
1064 bool nextHasFlatten = false;
1065 bool nextIsRegular = false;
1066 if (nextModName) {
1067 assert((isTerminal || !hopInst ||
1068 std::next(it)->mod == hopInst.getReferencedModuleNameAttr()) &&
1069 "recorded next module disagrees with the instance");
1070 auto modOp = symbolTable.lookup<FModuleLike>(nextModName);
1071 assert(modOp && "interior namepath module missing -- ran unverified?");
1072 const auto &info = facts.getModuleInfo(modOp);
1073 nextHasInline = info.hasInline;
1074 nextHasFlatten = info.hasFlatten;
1075 nextIsRegular = isa<FModuleOp>(modOp);
1076 }
1077 // - A hop 'evaporates' only when an instance is inlined.
1078 // - Terminal hops never evaporate (I8).
1079 // - Non-regular modules are never inlined; their instances relocate.
1080 // - Neither are opaque instantiations; they relocate with their operation.
1081 bool isEvaporating = nextModName && nextIsRegular && !isOpaqueInstanceHop &&
1082 (isTransitiveFlatten || nextHasInline);
1083
1084 // Diagnose evaporated terminal instance hops, keep I8 accurate and avoid
1085 // bugs. Only old-style annotations have this shape, which is recoverable
1086 // if that is the only user. Other users (e.g., XMR or verbatim) must be
1087 // detected and rejected. This will be handled in follow-on. Expected to
1088 // be unreachable from current frontend + pipeline.
1089 // https://github.com/llvm/circt/issues/10908
1090 if (isEvaporating && isTerminal) {
1091 assert(hop.inst && "expected instance operation");
1092 auto diag = diagAnchor.emitError(
1093 "hierpath points to inlined instance, cannot proceed");
1094 diag.attachNote(hop.inst->getLoc())
1095 << "hierpath targets this inlined instance";
1096 // Name the absorption cause to make the fix actionable.
1097 if (nextHasInline) {
1098 diag.attachNote(symbolTable.lookup(nextModName)->getLoc())
1099 << "target module is marked inline";
1100 } else {
1101 // The walk's own scope state names the culprit: the most recent
1102 // flatten still active here. A flatten above a choice hop is not a
1103 // cause; the choice began a fresh scope.
1104 assert(flattenCause && "flatten-caused absorption without a cause");
1105 diag.attachNote(symbolTable.lookup(flattenCause)->getLoc())
1106 << "flattening this module inlines the instance";
1107 }
1108 return failure();
1109 }
1110
1111 // An opaque instance's target begins a fresh flatten scope: flatten does
1112 // not reach through it (as with an extmodule). Terminal state is never
1113 // carried forward (the loop ends); don't let it redefine the locals.
1114 if (!isTerminal) {
1115 if (isOpaqueInstanceHop) {
1116 isTransitiveFlatten = nextHasFlatten;
1117 flattenCause = nextHasFlatten ? nextModName : StringAttr{};
1118 } else {
1119 isTransitiveFlatten |= nextHasFlatten;
1120 if (nextHasFlatten)
1121 flattenCause = nextModName;
1122 }
1123 }
1124 if (!isEvaporating) {
1125 // `sym` is null only for a non-instance hop (terminal or module-only).
1126 // Namepath InnerRefs always name a sym.
1127 // A climbed symless instance evaporates before reaching here
1128 // (it fronts a regular inline/underFlatten module).
1129 StringAttr sym = hop.sym;
1130 assert((sym || isTerminal || !hop.inst) &&
1131 "surviving instance hop without an inner symbol");
1132 StringAttr finalSym;
1133 if (currentDest == hop.mod || isTerminal /* terminals keep their sym */)
1134 finalSym = sym;
1135 survivingHops.push_back({/*origMod=*/hop.mod, /*origSym=*/sym,
1136 /*finalMod=*/currentDest,
1137 /*finalSym=*/finalSym});
1138 if (!isTerminal && nextModName)
1139 currentDest = nextModName;
1140 }
1141 }
1142
1143 // Create the VNLA using the next id.
1144 auto *vnla = createVNLA(origSym, survivingHops);
1145
1146 // Register this context under each instance it descends through.
1147 // Non-instance hops (module-only, terminal wire/port) are not routed.
1148 // Creation-order appends give I5's sorting; the DAG gives its dup-freedom.
1149 for (const auto &hop : absPath)
1150 if (hop.inst)
1151 pathRoutingTable[hop.inst].push_back(vnla);
1152
1153 return success();
1154}
1155
1156#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
1157LLVM_DUMP_METHOD void NLAPlanner::dump() {
1158 llvm::dbgs() << "\nVirtualNLAs (creation order):\n";
1159 for (auto *vnla : allVNLAs)
1160 vnla->dump();
1161
1162 llvm::dbgs() << "\nPath Routing Table (Instance -> Routed VirtualNLAs):\n";
1163
1164 // Keys are instance ops; sort by (containing module, inner sym) for stable
1165 // output across runs.
1166 auto modOf = [](Operation *op) -> StringAttr {
1167 auto mod = op->getParentOfType<FModuleLike>();
1168 return mod ? mod.getModuleNameAttr() : StringAttr();
1169 };
1170 SmallVector<Operation *> insts;
1171 for (const auto &[inst, _] : pathRoutingTable)
1172 insts.push_back(inst);
1173 llvm::sort(insts, [&](Operation *a, Operation *b) {
1174 auto am = modOf(a), bm = modOf(b);
1175 if (am != bm)
1176 return (am ? am.getValue() : "") < (bm ? bm.getValue() : "");
1177 auto as = getInnerSymName(a), bs = getInnerSymName(b);
1178 return (as ? as.getValue() : "") < (bs ? bs.getValue() : "");
1179 });
1180
1181 for (auto *inst : insts) {
1182 const auto &vnlas = pathRoutingTable.lookup(inst);
1183 llvm::dbgs() << " @" << modOf(inst);
1184 if (auto instSym = getInnerSymName(inst))
1185 llvm::dbgs() << "::" << instSym;
1186 else
1187 llvm::dbgs() << "::<op@" << inst << ">";
1188
1189 llvm::dbgs() << " -> [";
1190 llvm::interleaveComma(vnlas, llvm::dbgs(), [&](VirtualNLA *vnla) {
1191 llvm::dbgs() << "#" << vnla->id;
1192 });
1193 llvm::dbgs() << "]\n";
1194 }
1195
1196 llvm::dbgs() << "\n";
1197}
1198#endif
1199
1200//===----------------------------------------------------------------------===//
1201// Module Inlining Support
1202//===----------------------------------------------------------------------===//
1203
1204/// Map each of the instance's results to its corresponding replacement wire.
1205/// Later clones from the parent block then read the wires.
1206static void mapResultsToWires(IRMapping &mapper, SmallVectorImpl<Value> &wires,
1207 InstanceOp instance) {
1208 for (auto [result, wire] : llvm::zip_equal(instance.getResults(), wires))
1209 mapper.map(result, wire);
1210}
1211
1212/// Process each operation, updating InnerRefAttr's using the specified map,
1213/// with the given name as the containing IST of the mapped-to sym names.
1214///
1215/// Every inner-ref in the cloned ops names the child being inlined, so `map`
1216/// covers them all (I11).
1217///
1218/// A miss is a reference to another module, in an unknown capacity (!).
1219/// There is no correct update, so it is diagnosed.
1220static LogicalResult replaceInnerRefUsers(ArrayRef<Operation *> newOps,
1221 const InnerRefToNewNameMap &map,
1222 StringAttr istName) {
1223 hw::InnerRefAttr foreign;
1224 mlir::AttrTypeReplacer replacer;
1225 replacer.addReplacement([&](hw::InnerRefAttr innerRef) {
1226 auto it = map.find(innerRef);
1227 if (it == map.end()) {
1228 if (!foreign)
1229 foreign = innerRef;
1230 return std::pair{innerRef, WalkResult::skip()};
1231 }
1232 return std::pair{hw::InnerRefAttr::get(istName, it->second),
1233 WalkResult::skip()};
1234 });
1235 for (auto *op : newOps) {
1236 replacer.recursivelyReplaceElementsIn(op);
1237 if (foreign)
1238 return op->emitError("unsupported inner reference ")
1239 << foreign << " found while inlining";
1240 }
1241 return success();
1242}
1243
1244/// Unique each of `old`'s symbols in `ns`; record old-ref -> new-name entries
1245/// in `map` under `istName`.
1246static hw::InnerSymAttr uniqueInNamespace(hw::InnerSymAttr old,
1249 StringAttr istName) {
1250 if (!old || old.empty())
1251 return old;
1252
1253 bool anyChanged = false;
1254
1255 SmallVector<hw::InnerSymPropertiesAttr> newProps;
1256 auto *context = old.getContext();
1257 for (auto &prop : old) {
1258 auto newSym = ns.newName(prop.getName().strref());
1259 if (newSym == prop.getName()) {
1260 newProps.push_back(prop);
1261 continue;
1262 }
1263 auto newSymStrAttr = StringAttr::get(context, newSym);
1264 auto newProp = hw::InnerSymPropertiesAttr::get(
1265 context, newSymStrAttr, prop.getFieldID(), prop.getSymVisibility());
1266 anyChanged = true;
1267 newProps.push_back(newProp);
1268 }
1269
1270 auto newSymAttr = anyChanged ? hw::InnerSymAttr::get(context, newProps) : old;
1271
1272 for (auto [oldProp, newProp] : llvm::zip(old, newSymAttr)) {
1273 assert(oldProp.getFieldID() == newProp.getFieldID() &&
1274 "uniquing must preserve fieldIDs");
1275 // Record every prop, changed or not: the map must be total (I11).
1276 map[hw::InnerRefAttr::get(istName, oldProp.getName())] = newProp.getName();
1277 }
1278
1279 return newSymAttr;
1280}
1281
1282//===----------------------------------------------------------------------===//
1283// Inliner
1284//===----------------------------------------------------------------------===//
1285
1286/// Inlines, flattens, and removes dead modules in a circuit.
1287///
1288/// The inliner works top-down, in parents-before-children order.
1289/// Only live modules (I1) are visited; every marked instance is inlined.
1290/// Each operation clones directly to its final location.
1291/// Dead modules are erased at the end.
1292///
1293/// Every cloned operation with a name gets the instance-name prefix.
1294/// Top-down, the entire prefix is known at clone time, so the name attribute is
1295/// set exactly once (no interned intermediates).
1296namespace {
1297class Inliner {
1298public:
1299 /// Initialize the inliner to run on this circuit.
1300 Inliner(CircuitOp circuit, SymbolTable &symbolTable,
1301 CircuitNamespace &circuitNamespace,
1302 const InliningFacts &inliningFacts, NLAPlanner &nlaPlanner);
1303
1304 /// Run the inliner.
1305 LogicalResult run();
1306
1307 /// Work counters, copied into the pass statistics after run().
1308 struct Statistics {
1309 size_t instancesInlined = 0; ///< Inlined via an inline mark.
1310 size_t instancesFlattened = 0; ///< Inlined via flatten.
1311 size_t deadModules = 0; ///< Modules erased after inlining.
1312 size_t hierPathsUpdated = 0; ///< HierPaths retargeted in place.
1313 size_t hierPathsForked = 0; ///< Fork contexts emitted as new hierpaths.
1314 size_t hierPathsMerged = 0; ///< Contexts folded onto a canonical.
1315 size_t hierPathsErased = 0; ///< Erased: no surviving target
1316 ///< (dead-rooted).
1317 } stats;
1318
1319private:
1320 //===- Inlining contexts ------------------------------------------------===//
1321
1322 /// Inlining context, one per module being inlined into.
1323 struct ModuleInliningContext {
1324 ModuleInliningContext(FModuleOp module)
1325 : module(module), modNamespace(module), b(module.getContext()) {}
1326 FModuleOp module; ///< Top-level module for current inlining task.
1327 /// Inner-symbol namespace for minted names (I10).
1328 /// Every inlining level below this module shares it.
1329 /// Built from the pristine body: I12 defers mutation to this one visit.
1330 hw::InnerSymbolNamespace modNamespace;
1331 OpBuilder b; ///< Builder, insertion point into module.
1332 };
1333
1334 /// One inlining level, created for each instance inlined or flattened.
1335 /// Renamed inner symbols land in relocatedInnerSyms; clones in newOps.
1336 /// `finalize()` fixes the clones up once the level is complete.
1337 struct InliningLevel {
1338 InliningLevel(ModuleInliningContext &mic, FModuleOp childModule)
1339 : mic(mic), childModule(childModule) {}
1340
1341 ModuleInliningContext &mic; ///< Top-level inlining context.
1342 InnerRefToNewNameMap relocatedInnerSyms; ///< Inner-ref rename map.
1343 SmallVector<Operation *> newOps; ///< All cloned operations.
1344 SmallVector<Value> wires; ///< Wires created for ports.
1345 FModuleOp childModule; ///< The module being inlined.
1346 Value debugScope; ///< Debug scope of the instance.
1347 /// VNLAs active at this level, id-sorted (I6; see setActiveNLAsForChild).
1348 SmallVector<VirtualNLA *> activeNLAs;
1349
1350 /// Set the active contexts for this inlining level.
1351 void setActivePaths(ArrayRef<VirtualNLA *> nlas) {
1352 activeNLAs.assign(nlas);
1353 }
1354
1355 /// Retarget the inner references of this level's clones once complete.
1356 /// Called on the creator's success path.
1357 /// A level abandoned to a pass failure skips it.
1358 LogicalResult finalize() {
1359 return replaceInnerRefUsers(newOps, relocatedInnerSyms,
1360 mic.module.getNameAttr());
1361 }
1362 };
1363
1364 //===- P3: clone and rename ---------------------------------------------===//
1365
1366 /// Rename an operation and unique any symbols it has.
1367 /// Returns true iff symbol was changed.
1368 bool rename(StringRef prefix, Operation *op, InliningLevel &il);
1369
1370 /// Rename an instance-like op, uniquing any symbols it has.
1371 /// Requires old and new operations, to update the hierpath hops involved.
1372 bool renameInstance(StringRef prefix, InliningLevel &il, Operation *oldInst,
1373 Operation *newInst);
1374
1375 /// Clone and rename an operation.
1376 /// Insert the operation into the inlining level.
1377 void cloneAndRename(StringRef prefix, InliningLevel &il, IRMapping &mapper,
1378 Operation &op);
1379
1380 /// Record, for a freshly cloned op, which contexts own its nonlocal
1381 /// annotations (`circt.nonlocal` in the annotation payload).
1382 ///
1383 /// For each hierpath named by the clone's annotations, record the owning
1384 /// contexts: those whose route (surviving instance path) passes through this
1385 /// level, gated on ownership (I13). Only the walk knows a clone's position
1386 /// on any route (afterwards clones are indistinguishable), hence the record.
1387 /// P3 records, P4 writes.
1388 ///
1389 /// Every clone with nonlocal annotations still gets an entry, possibly empty,
1390 /// so the writeback never applies the original-op rule to it.
1391 void recordContexts(Operation *newOp, const InliningLevel &il);
1392
1393 /// Record a renamed leaf inner symbol on the active contexts, or the
1394 /// materialized path dangles.
1395 ///
1396 /// Matching is per-field by (origMod, origSym), destination-gated (I12/I13).
1397 /// Needed only to preserve NLA leaf symbols.
1398 void updateVirtualNLALeafSymbols(Inliner::InliningLevel &il,
1399 hw::InnerSymAttr oldSymAttr,
1400 hw::InnerSymAttr newSymAttr);
1401
1402 /// Compute the contexts active inside a child inlining level: the
1403 /// intersection (I5/I6) of the parent's active set with the contexts routed
1404 /// through `instance`.
1405 ///
1406 /// `std::nullopt` = top-level entry, no parent filter.
1407 /// Inputs are id-sorted; the result stays id-sorted.
1408 void setActiveNLAsForChild(std::optional<ArrayRef<VirtualNLA *>> activeNLAs,
1409 InliningLevel &childIL, Operation *instance);
1410
1411 /// Rewrite the ports of a module as wires.
1412 /// This is similar to cloneAndRename, but operating on ports.
1413 /// Wires are added to il.wires.
1414 void mapPortsToWires(StringRef prefix, InliningLevel &il, IRMapping &mapper);
1415
1416 //===- P3: the walk -----------------------------------------------------===//
1417
1418 /// Returns true if the operation is annotated to be flattened.
1419 bool shouldFlatten(FModuleLike mod);
1420
1421 /// Returns true if the operation is annotated to be inlined.
1422 bool shouldInline(FModuleLike mod);
1423
1424 /// Check we're not inlining into anything other than layerblock or module.
1425 /// In the future, could check this per-inlined-operation.
1426 LogicalResult checkInstanceParents(InstanceOp instance);
1427
1428 /// Walk the specified block, invoking `process` forward, pre-order.
1429 ///
1430 /// Handles cloning supported operations with regions, so that `process` is
1431 /// only invoked on regionless operations.
1432 LogicalResult
1433 inliningWalk(OpBuilder &builder, Block *block, IRMapping &mapper,
1434 llvm::function_ref<LogicalResult(Operation *op)> process);
1435
1436 /// Clone a target module's body into the insertion point of the builder,
1437 /// renaming all operations using the prefix, and recurse into the instances
1438 /// the pass inlines.
1439 ///
1440 /// Under `flatten` every regular-module child is inlined,
1441 /// otherwise only the children marked for it.
1442 /// (A flatten-marked child switches its subtree into flatten mode.)
1443 ///
1444 /// Does not trigger inlining on the target itself.
1445 LogicalResult processInto(StringRef prefix, InliningLevel &il,
1446 IRMapping &mapper, bool flatten);
1447
1448 /// Replace with its body every instance in `module` the pass inlines:
1449 /// every regular-module instance when `flatten` is set, otherwise the ones
1450 /// marked for inlining.
1451 LogicalResult processInstances(FModuleOp module, bool flatten);
1452
1453 /// Create a debug scope for an inlined instance at the current insertion
1454 /// point of the `il.mic` builder.
1455 void createDebugScope(InliningLevel &il, InstanceOp instance,
1456 Value parentScope = {});
1457
1458 /// P3: inline/flatten the live modules in parents-before-children order
1459 /// (I12), then drop debug scopes that ended up unused.
1460 LogicalResult inlineModules();
1461
1462 /// Erase the modules the analysis marked dead.
1463 void eraseDeadModules();
1464
1465 //===- P4: write back ---------------------------------------------------===//
1466
1467 /// Append `anno`, rewritten for one context (`matched`) of its source NLA:
1468 /// context went local -> drop the `circt.nonlocal` member
1469 /// context kept -> `circt.nonlocal` = canonical owner's realizedSym
1470 ///
1471 /// `origSym` is the annotation's current nonlocal symbol.
1472 /// Retargeting also flags the context used.
1473 /// P4-only; the context's single writer makes `wasUsed` race-free (I14).
1474 void appendContextAnno(Annotation anno, StringAttr origSym,
1475 VirtualNLA *matched, SmallVectorImpl<Attribute> &out);
1476
1477 /// P4: (serially) canonicalize every non-local context; see `canonicalize`.
1478 /// Also mints each emitted context's `realizedSym`.
1479 void canonicalizeContexts();
1480
1481 /// P4: rewrite all annotations, in parallel across regular modules.
1482 /// The single annotation writer.
1483 void rewriteAnnotations();
1484
1485 /// P4: materialize the surviving hierpaths and erase the rest.
1486 void writebackHierPaths();
1487
1488 /// Build the resolved namepath (an ArrayAttr of inner-refs / flat symbols)
1489 /// from a VNLA's surviving hops.
1490 ///
1491 /// Callable once a VNLA's path is final.
1492 ArrayAttr materializeNamepath(VirtualNLA *vnla);
1493
1494 /// Late-convergence canonicalization:
1495 /// Contexts from different NLAs can realize identical namepaths.
1496 /// A hierpath is defined solely by its namepath, so they share one op.
1497 /// The first to canonicalize (deterministic sweep order) owns it.
1498 ///
1499 /// The mapping lands in `canonicalOf`.
1500 ///
1501 /// Also mints `realizedSym` for canonicals: the primary already claimed
1502 /// origSym at selection, so a canonical fork always mints; duplicates borrow.
1503 ///
1504 /// Serial-sweep only, once every path is final.
1505 void canonicalize(VirtualNLA *vnla);
1506
1507 /// Read-only canonical lookup for the parallel annotation rewrite:
1508 /// every non-local VNLA has been through `canonicalize` by then, so this only
1509 /// reads `canonicalOf`.
1510 ///
1511 /// Returns `vnla` itself when it is canonical or excluded.
1512 VirtualNLA *canonicalOrSelf(VirtualNLA *vnla) const {
1513 return canonicalOf.lookup_or(vnla, vnla);
1514 }
1515
1516 //===- State ------------------------------------------------------------===//
1517
1518 CircuitOp circuit;
1519 MLIRContext *context;
1520
1521 /// A symbol table with references to each module in a circuit.
1522 SymbolTable &symbolTable;
1523
1524 /// Namespace for generating unique circuit-level names.
1525 /// Module-level namespaces live on the MICs (I10).
1526 CircuitNamespace &circuitNamespace;
1527
1528 /// Analysis / planner results (P1 and P2).
1529 const InliningFacts &inliningFacts;
1530 NLAPlanner &nlaPlanner;
1531
1532 /// Late-convergence canonicalization side tables.
1533 /// Inliner-owned so VirtualNLA stays frozen after the prepass (I2).
1534 ///
1535 /// `canonicalByPath` interns each distinct resolved namepath to the VNLA that
1536 /// owns its hierpath; `canonicalOf` maps every non-local VNLA to that owner
1537 /// (itself when canonical).
1538 ///
1539 /// A duplicate is skipped at emission, borrows the owner's realizedSym, and
1540 /// propagates usedness onto it.
1541 DenseMap<ArrayAttr, VirtualNLA *> canonicalByPath;
1542 DenseMap<VirtualNLA *, VirtualNLA *> canonicalOf;
1543
1544 /// Assert-only bookkeeping: origSyms claimed by their group's primary, so
1545 /// `canonicalize` can check the claim order (I15).
1546 struct ClaimedSyms {
1547#ifndef NDEBUG
1548 DenseSet<StringAttr> syms;
1549 void claim(StringAttr sym) { syms.insert(sym); }
1550 bool has(StringAttr sym) const { return syms.contains(sym); }
1551#else
1552 void claim(StringAttr) {}
1553 bool has(StringAttr) const { return true; }
1554#endif
1555 } claimed;
1556
1557 /// For every op the walk cloned that carries `circt.nonlocal` annotations,
1558 /// the contexts that own them: active on the clone's descent (route) and
1559 /// owned by its destination module (I13).
1560 ///
1561 /// Written by P3, read-only in P4.
1562 ///
1563 /// Keys are cloned ops and stay valid through the P4 reads:
1564 /// the pass erases only originals (consumed instances, dead-module bodies)
1565 /// and unused debug scopes, never a clone, so no key is freed and no stale
1566 /// entry can alias a live query.
1567 ///
1568 /// The other side of I7: routing keys are originals that outlive the walk.
1569 /// These keys are clones that do.
1570 DenseMap<Operation *, SmallVector<VirtualNLA *, 2>> clonedAnnoContexts;
1571
1572 /// The debug scopes created for inlined instances.
1573 /// Scopes that are unused after inlining will be deleted again.
1574 SmallVector<debug::ScopeOp> debugScopes;
1575};
1576} // namespace
1577
1578//===- Driver -------------------------------------------------------------===//
1579
1580Inliner::Inliner(CircuitOp circuit, SymbolTable &symbolTable,
1581 CircuitNamespace &circuitNamespace,
1582 const InliningFacts &inliningFacts, NLAPlanner &nlaPlanner)
1583 : circuit(circuit), context(circuit.getContext()), symbolTable(symbolTable),
1584 circuitNamespace(circuitNamespace), inliningFacts(inliningFacts),
1585 nlaPlanner(nlaPlanner) {}
1586
1587LogicalResult Inliner::run() {
1588 if (failed(inlineModules()))
1589 return failure();
1590 eraseDeadModules();
1591
1592 canonicalizeContexts();
1593 rewriteAnnotations();
1594 writebackHierPaths();
1595
1596 return success();
1597}
1598
1599//===- P3: clone and rename -----------------------------------------------===//
1600
1601/// Prefix the op's name and unique its inner symbols in the module namespace.
1602/// Renames land in `relocatedInnerSyms` for the level's inner-ref fixup.
1603bool Inliner::rename(StringRef prefix, Operation *op, InliningLevel &il) {
1604 // Debug operations with implicit module scope now need an explicit scope,
1605 // since inlining has destroyed the module whose scope they implicitly used.
1606 auto updateDebugScope = [&](auto op) {
1607 if (!op.getScope())
1608 op.getScopeMutable().assign(il.debugScope);
1609 };
1610 if (auto varOp = dyn_cast<debug::VariableOp>(op))
1611 return updateDebugScope(varOp), false;
1612 if (auto scopeOp = dyn_cast<debug::ScopeOp>(op))
1613 return updateDebugScope(scopeOp), false;
1614
1615 // Prefix the "name" attribute, when present.
1616 if (auto nameAttr = op->getAttrOfType<StringAttr>("name"))
1617 op->setAttr("name", StringAttr::get(op->getContext(),
1618 (prefix + nameAttr.getValue())));
1619
1620 // Unique any inner symbols; reflect renames on the active contexts' leaves.
1621 auto symOp = dyn_cast<hw::InnerSymbolOpInterface>(op);
1622 if (!symOp)
1623 return false;
1624 auto oldSymAttr = symOp.getInnerSymAttr();
1625 auto newSymAttr =
1626 uniqueInNamespace(oldSymAttr, il.relocatedInnerSyms, il.mic.modNamespace,
1627 il.childModule.getNameAttr());
1628
1629 if (!newSymAttr)
1630 return false;
1631
1632 // TODO: Gate this on whether this participates in NLAs to avoid
1633 // unnecessary scanning.
1634 updateVirtualNLALeafSymbols(il, oldSymAttr, newSymAttr);
1635 symOp.setInnerSymbolAttr(newSymAttr);
1636
1637 return newSymAttr != oldSymAttr;
1638}
1639
1640bool Inliner::renameInstance(StringRef prefix, InliningLevel &il,
1641 Operation *oldInst, Operation *newInst) {
1642 // TODO: No way yet to annotate an explicit parent scope on instances.
1643 // Just emit a note in debug runs until this is resolved.
1644 LLVM_DEBUG({
1645 if (il.debugScope)
1646 llvm::dbgs() << "Discarding parent debug scope for " << *oldInst << "\n";
1647 });
1648
1649 auto oldInstSym = getInnerSymName(oldInst);
1650 auto symbolChanged = rename(prefix, newInst, il);
1651 auto newSymAttr = getInnerSymName(newInst);
1652
1653 // Record the hop even when the symbol is unchanged: a relocated hop's
1654 // `finalSym` starts null and is only filled here, at its clone.
1655 if (oldInstSym) {
1656 assert(newSymAttr && "uniquing dropped an instance sym?");
1657 StringAttr origMod = il.childModule.getModuleNameAttr();
1658 StringAttr destMod = il.mic.module.getModuleNameAttr();
1659 for (auto *nla : il.activeNLAs) {
1660 for (auto &hop : nla->getPathMutable()) {
1661 // The `finalMod` test is the I13 ownership gate: an active context
1662 // belonging to a different copy of this instance must not be updated.
1663 if (hop.origMod == origMod && hop.origSym == oldInstSym &&
1664 hop.finalMod == destMod) {
1665 hop.finalSym = newSymAttr;
1666 }
1667 }
1668 }
1669 }
1670 return symbolChanged;
1671}
1672
1673void Inliner::recordContexts(Operation *newOp, const InliningLevel &il) {
1674 StringAttr destMod = il.mic.module.getModuleNameAttr();
1675
1676 // Intersect a hierpath symbol's context group with the id-sorted active set,
1677 // keeping only those this destination module owns.
1678
1679 // A sym's group spans a gap-free id interval (I4; ids globally unique).
1680 //
1681 // Its intersection with the id-sorted set (I6) forms one contiguous slice:
1682 // two searches at the bounds, no per-member probing, id-ordered result.
1683 auto matchContexts = [&](FlatSymbolRefAttr sym, ArrayRef<VirtualNLA *> active,
1684 SmallVectorImpl<VirtualNLA *> &out) {
1685 auto it = nlaPlanner.origToVNLAs.find(sym.getAttr());
1686 if (it == nlaPlanner.origToVNLAs.end())
1687 return;
1688 ArrayRef<VirtualNLA *> group = it->second;
1689 const auto *lo = llvm::lower_bound(active, group.front(), vnlaIdLess);
1690 const auto *hi =
1691 std::upper_bound(lo, active.end(), group.back(), vnlaIdLess);
1692 for (; lo != hi; ++lo) {
1693 // I13: ownership.
1694 //
1695 // An annotation is written by the module holding the context's leaf: the
1696 // annotated op lives there, and it clones into the destination.
1697 //
1698 // Activation can be broader than ownership: parent-copy contexts route
1699 // through the same original instance ops but belong to the parent's
1700 // clone, not this one.
1701 //
1702 // Nonempty per I8: the terminal hop always survives.
1703 auto path = (*lo)->getPath();
1704 assert(!path.empty() && "terminal hop is expected to always survive");
1705 if (path.back().finalMod != destMod)
1706 continue;
1707 // One op may name a hierpath more than once; record each owned context
1708 // once (the writeback re-associates by origSym).
1709 if (!llvm::is_contained(out, *lo))
1710 out.push_back(*lo);
1711 }
1712 };
1713
1714 // Annotations: record the owning contexts for the writeback.
1715 //
1716 // A clone with nonlocal annotations always gets an entry, possibly empty,
1717 // so the writeback never applies the original-op rule to it.
1718 bool hasNonlocal = false;
1719 SmallVector<VirtualNLA *, 2> annoContexts;
1720 auto visitAnno = [&](Annotation anno) {
1721 auto sym = anno.getMember<FlatSymbolRefAttr>("circt.nonlocal");
1722 if (!sym)
1723 return;
1724 hasNonlocal = true;
1725 matchContexts(sym, il.activeNLAs, annoContexts);
1726 };
1727 if (auto annos = newOp->getAttrOfType<ArrayAttr>("annotations"))
1728 for (Attribute attr : annos)
1729 visitAnno(Annotation(attr));
1730 if (auto portAnnos = newOp->getAttrOfType<ArrayAttr>("portAnnotations"))
1731 for (auto portArray : portAnnos.getAsRange<ArrayAttr>())
1732 for (Attribute attr : portArray)
1733 visitAnno(Annotation(attr));
1734 if (hasNonlocal)
1735 clonedAnnoContexts[newOp] = std::move(annoContexts);
1736}
1737
1738void Inliner::updateVirtualNLALeafSymbols(Inliner::InliningLevel &il,
1739 hw::InnerSymAttr oldSymAttr,
1740 hw::InnerSymAttr newSymAttr) {
1741 // TODO: Record per-target leaf NLAs in recordContexts, then jump to them.
1742 // For now, scan the level's active set.
1743 if (!oldSymAttr || oldSymAttr == newSymAttr)
1744 return;
1745 assert(newSymAttr && "renamed to a null sym?");
1746 StringAttr origMod = il.childModule.getModuleNameAttr();
1747 StringAttr destMod = il.mic.module.getModuleNameAttr();
1748 for (auto *nla : il.activeNLAs) {
1749 // A local context is tracked too: retention can pin it as a one-hop
1750 // primary, whose leaf symbol must then reflect this rename.
1751 //
1752 // `back()` is safe local or not: a VNLA always has its terminal hop (I8,
1753 // asserted at construction).
1754 auto &last = nla->getPathMutable().back();
1755 // `finalMod` is the I13 ownership gate; `origMod` matching is total because
1756 // the leaf is cloned from its original def exactly once (I12).
1757 if (last.origMod == origMod && last.finalMod == destMod) {
1758 for (auto prop : oldSymAttr.getProps()) {
1759 if (last.origSym == prop.getName()) {
1760 last.finalSym = newSymAttr.getSymIfExists(prop.getFieldID());
1761 break;
1762 }
1763 }
1764 }
1765 }
1766}
1767
1768void Inliner::setActiveNLAsForChild(
1769 std::optional<ArrayRef<VirtualNLA *>> activeNLAs, InliningLevel &childIL,
1770 Operation *instance) {
1771 // One lookup by the instance op; the routing entry is born id-sorted and
1772 // duplicate-free (I5, verified once at the end of planning).
1773 ArrayRef<VirtualNLA *> instNLAs;
1774 if (auto it = nlaPlanner.pathRoutingTable.find(instance);
1775 it != nlaPlanner.pathRoutingTable.end())
1776 instNLAs = it->second;
1777
1778 // An empty parent set stays empty; the child default is empty, so leave it.
1779 if (!activeNLAs) {
1780 childIL.setActivePaths(instNLAs);
1781 } else if (!activeNLAs->empty() && !instNLAs.empty()) {
1782 // Both ranges are id-sorted (I5/I6).
1783 //
1784 // A shared instance's routing entry can be fork-count-sized while the
1785 // active set has narrowed, or the reverse.
1786 //
1787 // So: walk the smaller range, binary-search the larger range.
1788 //
1789 // The smaller is walked in id order, so the result stays sorted (I6).
1790 ArrayRef<VirtualNLA *> probe = instNLAs, in = *activeNLAs;
1791 if (probe.size() > in.size())
1792 std::swap(probe, in);
1793 SmallVector<VirtualNLA *> childActiveNLAs;
1794 for (auto *vnla : probe)
1795 if (llvm::binary_search(in, vnla, vnlaIdLess))
1796 childActiveNLAs.push_back(vnla);
1797 childIL.setActivePaths(childActiveNLAs);
1798 }
1799}
1800
1801/// Create a wire per target-module port at the insertion point, mapping each
1802/// port to its wire; the cloned body then reads the wires.
1803void Inliner::mapPortsToWires(StringRef prefix, InliningLevel &il,
1804 IRMapping &mapper) {
1805 auto target = il.childModule;
1806 auto portInfo = target.getPorts();
1807 for (unsigned i = 0, e = target.getNumPorts(); i < e; ++i) {
1808 auto arg = target.getArgument(i);
1809 auto type = type_cast<FIRRTLType>(arg.getType());
1810
1811 auto oldSymAttr = portInfo[i].sym;
1812 auto newSymAttr =
1813 uniqueInNamespace(oldSymAttr, il.relocatedInnerSyms,
1814 il.mic.modNamespace, target.getNameAttr());
1815
1816 // Record the renamed port symbol on the active contexts' leaf hops: a
1817 // renamed port that is an NLA leaf must be reflected even when the port
1818 // itself carries no annotations.
1819 //
1820 // The path may be kept alive by an annotation elsewhere.
1821 updateVirtualNLALeafSymbols(il, oldSymAttr, newSymAttr);
1822
1823 // The wire keeps the port's annotations verbatim; as with cloneAndRename,
1824 // the walk only records which contexts own them and P4 rewrites them.
1825 auto wireOp = WireOp::create(
1826 il.mic.b, target.getLoc(), type,
1827 StringAttr::get(context, (prefix + portInfo[i].getName())),
1828 NameKindEnumAttr::get(context, NameKindEnum::DroppableName),
1829 AnnotationSet::forPort(target, i).getArrayAttr(), newSymAttr,
1830 /*forceable=*/UnitAttr{});
1831 recordContexts(wireOp, il);
1832 Value wire = wireOp.getResult();
1833 il.wires.push_back(wire);
1834 mapper.map(arg, wire);
1835 }
1836}
1837
1838/// Clone `op` at the mic builder's insertion point, rename it, record its
1839/// annotation contexts, and add it to the level.
1840void Inliner::cloneAndRename(StringRef prefix, InliningLevel &il,
1841 IRMapping &mapper, Operation &op) {
1842 // Clone and rename.
1843 //
1844 // Annotations are copied verbatim: the walk only records which contexts own
1845 // them; the final writeback is the single annotation writer (P4), running
1846 // when every context's path is final.
1847 assert(op.getNumRegions() == 0 &&
1848 "operation with regions should not reach cloneAndRename");
1849 auto *newOp = il.mic.b.cloneWithoutRegions(op, mapper);
1850
1851 // Instance renames must also land on the hierpath hops involved.
1852 if (isa<FInstanceLike>(&op))
1853 renameInstance(prefix, il, &op, newOp);
1854 else
1855 rename(prefix, newOp, il);
1856
1857 recordContexts(newOp, il);
1858
1859 il.newOps.push_back(newOp);
1860}
1861
1862//===- P3: the walk -------------------------------------------------------===//
1863
1864bool Inliner::shouldFlatten(FModuleLike mod) {
1865 return inliningFacts.hasFlatten(mod);
1866}
1867
1868bool Inliner::shouldInline(FModuleLike mod) {
1869 return inliningFacts.hasInline(mod);
1870}
1871
1872LogicalResult Inliner::inliningWalk(
1873 OpBuilder &builder, Block *block, IRMapping &mapper,
1874 llvm::function_ref<LogicalResult(Operation *op)> process) {
1875 /// Insertion points: target in the destination, source in the original.
1876 struct IPs {
1877 OpBuilder::InsertPoint target;
1878 Block::iterator source;
1879 };
1880 // Invariant: no Block::iterator == end(), can't getBlock().
1881 SmallVector<IPs> inliningStack;
1882 if (block->empty())
1883 return success();
1884
1885 inliningStack.push_back(IPs{builder.saveInsertionPoint(), block->begin()});
1886 OpBuilder::InsertionGuard guard(builder);
1887
1888 while (!inliningStack.empty()) {
1889 auto target = inliningStack.back().target;
1890 builder.restoreInsertionPoint(target);
1891 Operation *source;
1892 // Take the frame's next op; pop the frame once its block is exhausted.
1893 {
1894 auto &ips = inliningStack.back();
1895 source = &*ips.source;
1896 auto end = source->getBlock()->end();
1897 if (++ips.source == end)
1898 inliningStack.pop_back();
1899 }
1900
1901 if (source->getNumRegions() == 0) {
1902 // `process` must leave the insertion point where it found it.
1903 assert(builder.saveInsertionPoint().getPoint() == target.getPoint());
1904 if (failed(process(source)))
1905 return failure();
1906 assert(builder.saveInsertionPoint().getPoint() == target.getPoint());
1907
1908 continue;
1909 }
1910
1911 // Limited support for region-containing operations.
1912 if (!isa<LayerBlockOp, WhenOp, MatchOp>(source))
1913 return source->emitError("unsupported operation '")
1914 << source->getName() << "' cannot be inlined";
1915
1916 // Not cloneAndRename: nothing to prefix, no annotations, no symbols --
1917 // hence also absent from `newOps` and the level's inner-ref fixup.
1918 auto *newOp = builder.cloneWithoutRegions(*source, mapper);
1919 for (auto [newRegion, oldRegion] : llvm::reverse(
1920 llvm::zip_equal(newOp->getRegions(), source->getRegions()))) {
1921 if (oldRegion.empty()) {
1922 assert(newRegion.empty());
1923 continue;
1924 }
1925 // Single-block regions only, presently.
1926 assert(oldRegion.hasOneBlock());
1927
1928 auto &oldBlock = oldRegion.getBlocks().front();
1929 auto &newBlock = newRegion.emplaceBlock();
1930 mapper.map(&oldBlock, &newBlock);
1931
1932 for (auto arg : oldBlock.getArguments())
1933 mapper.map(arg, newBlock.addArgument(arg.getType(), arg.getLoc()));
1934
1935 if (oldBlock.empty())
1936 continue;
1937
1938 inliningStack.push_back(
1939 IPs{OpBuilder::InsertPoint(&newBlock, newBlock.begin()),
1940 oldBlock.begin()});
1941 }
1942 }
1943 return success();
1944}
1945
1946LogicalResult Inliner::checkInstanceParents(InstanceOp instance) {
1947 auto *parent = instance->getParentOp();
1948 while (!isa<FModuleLike>(parent)) {
1949 if (!isa<LayerBlockOp>(parent))
1950 return instance->emitError("cannot inline instance")
1951 .attachNote(parent->getLoc())
1952 << "containing operation '" << parent->getName()
1953 << "' not safe to inline into";
1954 parent = parent->getParentOp();
1955 }
1956 return success();
1957}
1958
1959// NOLINTNEXTLINE(misc-no-recursion)
1960LogicalResult Inliner::processInto(StringRef prefix, InliningLevel &il,
1961 IRMapping &mapper, bool flatten) {
1962 auto target = il.childModule;
1963
1964 LLVM_DEBUG(llvm::dbgs() << (flatten ? "flattening " : "inlining ")
1965 << target.getModuleName() << " into "
1966 << il.mic.module.getModuleName() << "\n");
1967
1968 auto visit = [&](Operation *op) {
1969 // If the pass can't inline through it, clone it and continue.
1970 auto instance = getInlinableInstance(op);
1971 if (!instance) {
1972 cloneAndRename(prefix, il, mapper, *op);
1973 return success();
1974 }
1975
1976 // Not a regular module: uninlinable; the analysis marked it live.
1977 auto *moduleOp = symbolTable.lookup(instance.getModuleName());
1978 auto childModule = dyn_cast<FModuleOp>(moduleOp);
1979 if (!childModule) {
1980 assert(inliningFacts.isKnownLive(moduleOp) &&
1981 "a kept non-module instance must target a live module");
1982 cloneAndRename(prefix, il, mapper, *op);
1983 return success();
1984 }
1985
1986 // Flatten inlines every child; otherwise only those marked for it.
1987 // A child the pass keeps is cloned as a live instance.
1988 if (!flatten && !shouldInline(childModule)) {
1989 assert(inliningFacts.isLive(childModule) &&
1990 "a kept child module must be live");
1991 cloneAndRename(prefix, il, mapper, *op);
1992 return success();
1993 }
1994
1995 if (failed(checkInstanceParents(instance)))
1996 return failure();
1997
1998 ++(flatten ? stats.instancesFlattened : stats.instancesInlined);
1999
2000 InliningLevel childIL(il.mic, childModule);
2001 setActiveNLAsForChild(il.activeNLAs, childIL, instance);
2002 createDebugScope(childIL, instance, il.debugScope);
2003
2004 // Create the wire mapping for results + ports.
2005 auto nestedPrefix = (prefix + instance.getName() + "_").str();
2006 mapPortsToWires(nestedPrefix, childIL, mapper);
2007 mapResultsToWires(mapper, childIL.wires, instance);
2008
2009 // A flatten-marked child switches its whole subtree into flatten mode.
2010 if (failed(processInto(nestedPrefix, childIL, mapper,
2011 flatten || shouldFlatten(childModule))))
2012 return failure();
2013 return childIL.finalize();
2014 };
2015
2016 return inliningWalk(il.mic.b, target.getBodyBlock(), mapper, visit);
2017}
2018
2019LogicalResult Inliner::processInstances(FModuleOp module, bool flatten) {
2020 ModuleInliningContext mic(module);
2021
2022 LLVM_DEBUG(llvm::dbgs() << "inlining instances within "
2023 << module.getNameAttr() << "...\n");
2024 auto visit = [&](FInstanceLike instanceLike) {
2025 auto instance = getInlinableInstance(instanceLike.getOperation());
2026 if (!instance)
2027 return WalkResult::advance();
2028 // Not a regular module: uninlinable; the analysis marked it live.
2029 auto moduleOp = symbolTable.lookup<FModuleLike>(instance.getModuleName());
2030 assert(moduleOp && "instance target missing -- ran unverified?");
2031 auto target = dyn_cast<FModuleOp>(*moduleOp);
2032 if (!target) {
2033 assert(inliningFacts.isLive(moduleOp) &&
2034 "a kept non-module instance must target a live module");
2035 return WalkResult::advance();
2036 }
2037
2038 // Flatten inlines every child; otherwise only those marked for it.
2039 if (!flatten && !shouldInline(target))
2040 return WalkResult::advance();
2041
2042 if (failed(checkInstanceParents(instance)))
2043 return WalkResult::interrupt();
2044
2045 ++(flatten ? stats.instancesFlattened : stats.instancesInlined);
2046
2047 // Create the wire mapping for results + ports.
2048 // We RAUW the results instead of mapping them.
2049 IRMapping mapper;
2050 mic.b.setInsertionPoint(instance);
2051
2052 InliningLevel childIL(mic, target);
2053 setActiveNLAsForChild(/* Activate all through this instance */ std::nullopt,
2054 childIL, instance);
2055 createDebugScope(childIL, instance);
2056
2057 auto nestedPrefix = (instance.getName() + "_").str();
2058 mapPortsToWires(nestedPrefix, childIL, mapper);
2059 for (unsigned i = 0, e = instance.getNumResults(); i < e; ++i)
2060 instance.getResult(i).replaceAllUsesWith(childIL.wires[i]);
2061
2062 // A flatten-marked child switches its whole subtree into flatten mode.
2063 if (failed(processInto(nestedPrefix, childIL, mapper,
2064 flatten || shouldFlatten(target))) ||
2065 failed(childIL.finalize()))
2066 return WalkResult::interrupt();
2067
2068 instance.erase();
2069 return WalkResult::skip();
2070 };
2071
2072 return failure(module.getBodyBlock()
2073 ->walk<mlir::WalkOrder::PreOrder>(visit)
2074 .wasInterrupted());
2075}
2076
2077void Inliner::createDebugScope(InliningLevel &il, InstanceOp instance,
2078 Value parentScope) {
2079 auto op = debug::ScopeOp::create(
2080 il.mic.b, instance.getLoc(), instance.getInstanceNameAttr(),
2081 instance.getModuleNameAttr().getAttr(), parentScope);
2082 debugScopes.push_back(op);
2083 il.debugScope = op;
2084}
2085
2086LogicalResult Inliner::inlineModules() {
2087 // Process live modules in the analysis's parents-before-children order
2088 // (I12): a parent always clones a child's pristine definition body, since
2089 // a retained child's own body is only mutated by its later self-visit.
2090 //
2091 // Dead modules are skipped here and erased after.
2092 for (auto moduleOp : inliningFacts.getSchedule()) {
2093 const auto &info = inliningFacts.getModuleInfo(moduleOp);
2094 if (!info.isLive)
2095 continue;
2096 // Consume the inline/flatten annotations: InliningFacts is their only
2097 // reader (everything else consults the frozen ModuleClassification, I1).
2098 //
2099 // Every P1/P2 diagnosis has already run, so a run that fails before this
2100 // loop leaves the input untouched; the walk's own diagnoses (instance
2101 // parents, foreign inner refs) fire mid-clone and do not.
2102 if (info.hasFlatten || info.hasInline)
2103 AnnotationSet::removeAnnotations(moduleOp, [](Annotation anno) {
2104 return anno.isClass(flattenAnnoClass, inlineAnnoClass);
2105 });
2106 if (failed(processInstances(moduleOp, info.hasFlatten)))
2107 return failure();
2108 }
2109
2110 // Delete debug scopes that ended up unused.
2111 // Erase in reverse: back scopes may have uses on front scopes.
2112 for (auto scopeOp : llvm::reverse(debugScopes))
2113 if (scopeOp.use_empty())
2114 scopeOp.erase();
2115 debugScopes.clear();
2116
2117 return success();
2118}
2119
2120void Inliner::eraseDeadModules() {
2121 for (auto mod : llvm::make_early_inc_range(circuit.getOps<FModuleLike>())) {
2122 if (inliningFacts.isKnownLive(mod))
2123 continue;
2124 mod.erase();
2125 ++stats.deadModules;
2126 }
2127}
2128
2129//===- P4: write back -----------------------------------------------------===//
2130
2131ArrayAttr Inliner::materializeNamepath(VirtualNLA *vnla) {
2132 SmallVector<Attribute> pathAttrs;
2133 for (auto &hop : vnla->getPath()) {
2134 // Hops that originally had inner symbols must have their final symbols
2135 // assigned by now. These were set during plan (in-place/terminal) or
2136 // filled in during cloning in P3.
2137 //
2138 // This going wrong will corrupt the deduplication mechanisms,
2139 // so be sure to check it.
2140 assert((hop.finalSym || !hop.origSym) &&
2141 "materializing a hop whose final symbol was never filled");
2142 if (hop.finalSym)
2143 pathAttrs.push_back(InnerRefAttr::get(hop.finalMod, hop.finalSym));
2144 else
2145 pathAttrs.push_back(FlatSymbolRefAttr::get(hop.finalMod));
2146 }
2147 return ArrayAttr::get(context, pathAttrs);
2148}
2149
2150void Inliner::canonicalize(VirtualNLA *vnla) {
2151 // A local context never canonicalizes.
2152 //
2153 // The annotation localizes onto the op and the path is dropped.
2154 assert(!vnla->isLocal() && "local VNLAs have no hierpath to canonicalize");
2155 assert(!vnla->realizedSym && "context canonicalized twice");
2156 VirtualNLA *canon =
2157 canonicalByPath.try_emplace(materializeNamepath(vnla), vnla)
2158 .first->second;
2159 canonicalOf[vnla] = canon;
2160 if (canon == vnla) {
2161 // Only forks reach here; the primary claimed origSym before any fork
2162 // canonicalizes, so a canonical fork always mints (I15).
2163 assert(claimed.has(vnla->origSym) &&
2164 "primary claims origSym before any fork canonicalizes (I15)");
2165 vnla->realizedSym = StringAttr::get(
2166 context, circuitNamespace.newName(vnla->origSym.getValue()));
2167 }
2168}
2169
2170void Inliner::appendContextAnno(Annotation anno, StringAttr origSym,
2171 VirtualNLA *matched,
2172 SmallVectorImpl<Attribute> &out) {
2173 if (matched->isLocal()) {
2174 anno.removeMember("circt.nonlocal");
2175 out.push_back(anno.getAttr());
2176 return;
2177 }
2178 matched->wasUsed = true;
2179 StringAttr canonSym = canonicalOrSelf(matched)->realizedSym;
2180 // Keep the annotation as-is if it already names that symbol.
2181 if (canonSym == origSym) {
2182 out.push_back(anno.getAttr());
2183 return;
2184 }
2185 anno.setMember("circt.nonlocal", FlatSymbolRefAttr::get(canonSym));
2186 out.push_back(anno.getAttr());
2187}
2188
2189void Inliner::canonicalizeContexts() {
2190 // Serially, now that all paths are final (the walk is done).
2191 // Leaves `canonicalOf` complete and read-only for the parallel rewrite.
2192 //
2193 // Retention: an original hierpath symbol can be named by users the inliner
2194 // does not rewrite (an sv.xmr.ref target, a circuit-level annotation).
2195 //
2196 // Those are invisible here, so every origSym with a surviving context is kept
2197 // 1:1 rather than dropped when no annotation happens to name it: pinned to a
2198 // `primary` context and emitted unconditionally by writeback.
2199 //
2200 // Only fork symbols stay usedness-gated, since we created them.
2201 // GC of genuinely dead paths is not the job of this pass.
2202 //
2203 // Process one origSym group at a time (contiguous per I4).
2204 for (size_t i = 0, e = nlaPlanner.allVNLAs.size(); i < e;) {
2205 StringAttr origSym = nlaPlanner.allVNLAs[i]->origSym;
2206 size_t groupStart = i;
2207 while (i < e && nlaPlanner.allVNLAs[i]->origSym == origSym)
2208 ++i;
2209 ArrayRef<VirtualNLA *> group(&nlaPlanner.allVNLAs[groupStart],
2210 i - groupStart);
2211
2212 // Pick the primary (I15): the first non-local context, else the front.
2213 // A non-local namepath best matches the source hierpath.
2214 // An all-local group still pins its symbol through its one-hop path.
2215 VirtualNLA *primary = nullptr;
2216 for (auto *v : group)
2217 if (!v->isLocal()) {
2218 primary = v;
2219 break;
2220 }
2221 if (!primary)
2222 primary = group.front();
2223
2224 // The primary keeps origSym unconditionally and is its own canonical,
2225 // even when another origSym's primary already claimed this exact path.
2226
2227 // Each original may have its own external user:
2228 // both must survive, so primaries never merge; only forks do.
2229 primary->realizedSym = origSym;
2230 claimed.claim(origSym);
2231 canonicalOf[primary] = primary;
2232 // Seed `canonicalByPath` so forks can still attach to a primary.
2233 if (!primary->isLocal())
2234 canonicalByPath.try_emplace(materializeNamepath(primary), primary);
2235
2236 // Canonicalize the remaining forks (non-primary): dedup by path and mint
2237 // fresh names (origSym is already claimed).
2238 // A local fork is skipped:
2239 // an annotation on a local context simply drops the path.
2240 for (auto *v : group) {
2241 if (v == primary || v->isLocal())
2242 continue;
2243 canonicalize(v);
2244 }
2245 }
2246}
2247
2248void Inliner::rewriteAnnotations() {
2249 // Cloned ops: owning contexts were recorded at clone time (I13).
2250 // Original ops (`recorded` == null): ownership alone selects them (I14).
2251 auto rewriteAnnos = [&](ArrayAttr annos, StringAttr modName,
2252 const SmallVectorImpl<VirtualNLA *> *recorded,
2253 SmallVectorImpl<Attribute> &newAnnos) {
2254 for (Attribute attr : annos) {
2255 Annotation anno(attr);
2256 auto sym = anno.getMember<FlatSymbolRefAttr>("circt.nonlocal");
2257 if (!sym) {
2258 newAnnos.push_back(anno.getAttr());
2259 continue;
2260 }
2261
2262 if (recorded) {
2263 // Cloned op: write exactly the recorded contexts for this symbol.
2264 // Anything unrecorded belongs to a different copy.
2265 for (auto *matched : *recorded)
2266 if (matched->origSym == sym.getAttr())
2267 appendContextAnno(anno, sym.getAttr(), matched, newAnnos);
2268 continue;
2269 }
2270
2271 // Original op: annotations naming a nonexistent hierpath are dropped.
2272 // Otherwise rewrite per context this module owns.
2273 auto it = nlaPlanner.origToVNLAs.find(sym.getAttr());
2274 if (it == nlaPlanner.origToVNLAs.end())
2275 continue;
2276 for (auto *matched : it->second) {
2277 // Drop the annotation unless this module owns the context's leaf
2278 // (I14/I13); a path is never empty (I8); a local context is one hop,
2279 // not zero.
2280 if (matched->getPath().back().finalMod != modName)
2281 continue;
2282 appendContextAnno(anno, sym.getAttr(), matched, newAnnos);
2283 }
2284 }
2285 };
2286
2287 auto rewriteOpAnnos = [&](Operation *op, StringAttr modName) {
2288 const SmallVectorImpl<VirtualNLA *> *recorded = nullptr;
2289 if (auto it = clonedAnnoContexts.find(op); it != clonedAnnoContexts.end())
2290 recorded = &it->second;
2291
2292 // Update annotations on the op.
2293 // Skip ops without any, to avoid adding an empty annotations attribute.
2294 if (auto annos = getAnnotationsIfPresent(op); annos && !annos.empty()) {
2295 SmallVector<Attribute> newAnnotations;
2296 rewriteAnnos(annos, modName, recorded, newAnnotations);
2297 AnnotationSet(newAnnotations, context).applyToOperation(op);
2298 }
2299
2300 // Update port annotations:
2301 // module ports and the per-port annotations of instances and memories.
2302 if (auto portAnnos = op->getAttrOfType<ArrayAttr>("portAnnotations")) {
2303 SmallVector<Attribute> newPortAnnotations;
2304 SmallVector<Attribute> newAnnotations;
2305 for (auto portArray : portAnnos.getAsRange<ArrayAttr>()) {
2306 newAnnotations.clear();
2307 rewriteAnnos(portArray, modName, recorded, newAnnotations);
2308 newPortAnnotations.push_back(ArrayAttr::get(context, newAnnotations));
2309 }
2310 op->setAttr("portAnnotations",
2311 ArrayAttr::get(context, newPortAnnotations));
2312 }
2313 };
2314 auto rewriteModuleAnnos = [&](FModuleLike fmodule) {
2315 StringAttr modName = fmodule.getModuleNameAttr();
2316 fmodule.walk([&](Operation *op) { rewriteOpAnnos(op, modName); });
2317 };
2318
2319 // Parallel per module: P2 state is read-only here (I2/I3).
2320 // Each context has a single writer (I14).
2321 // Only regular modules are worth a parallel task.
2322 // Other module-likes are each a trivial walk done serially, handled inline
2323 // (the same split as verifyInnerRefNamespace).
2324 SmallVector<FModuleOp> bodyModules;
2325 for (auto fmodule : circuit.getOps<FModuleLike>()) {
2326 if (auto regular = dyn_cast<FModuleOp>(*fmodule))
2327 bodyModules.push_back(regular);
2328 else
2329 rewriteModuleAnnos(fmodule);
2330 }
2331 mlir::parallelForEach(context, bodyModules, [&](FModuleOp fmodule) {
2332 rewriteModuleAnnos(fmodule);
2333 });
2334}
2335
2336void Inliner::writebackHierPaths() {
2337 // Propagate usedness from duplicates onto their canonical, so a converged
2338 // path materializes even when only a duplicate's annotation kept it live.
2339 // Iterating `canonicalOf` unordered is safe: both effects are
2340 // order-independent (a count and a monotonic OR onto the canonical).
2341 for (auto &[dup, canon] : canonicalOf) {
2342 if (dup == canon)
2343 continue;
2344 ++stats.hierPathsMerged;
2345 if (dup->wasUsed)
2346 canon->wasUsed = true;
2347 }
2348
2349#ifndef NDEBUG
2350 // I15: exactly one primary claimant per origSym group, emitted in place
2351 // below, so the symbol always survives for a user the inliner cannot see.
2352 // Supersedes the usedness-uniformity assert: emitted regardless of usedness,
2353 // the churn it guarded against cannot arise.
2354 for (size_t i = 0, e = nlaPlanner.allVNLAs.size(); i < e;) {
2355 StringAttr origSym = nlaPlanner.allVNLAs[i]->origSym;
2356 unsigned claimants = 0;
2357 for (; i < e && nlaPlanner.allVNLAs[i]->origSym == origSym; ++i) {
2358 auto *v = nlaPlanner.allVNLAs[i];
2359 if (v->realizedSym == origSym && canonicalOrSelf(v) == v)
2360 ++claimants;
2361 }
2362 assert(claimants == 1 &&
2363 "retention: each origSym must have exactly one primary claimant");
2364 }
2365#endif
2366
2367 OpBuilder b(context);
2368 // Source symbol -> hierpath op, recorded by the planner.
2369 // (still valid: the ops are only mutated here)
2370 auto &existingPaths = nlaPlanner.hierPathOps;
2371
2372 // Each used canonical context materializes next to its source op:
2373 // realizedSym == origSym -> retarget the original op in place
2374 // realizedSym is fresh -> new private hw.hierpath beside the original
2375 // local/duplicate/unused -> nothing; unreferenced originals are erased
2376 //
2377 // Iterate in creation order for determinism (contiguous per origSym).
2378 //
2379 // At a group boundary, set the insertion point after its hw.hierpath op
2380 // so forks land beside their source, not clumped at the block's end.
2381 StringAttr curGroup;
2382
2383 // Ops kept alive by in-place reuse below.
2384 // A group can hold both a reusing context and later forks.
2385 // The forks still need the original's location, so keep its entry.
2386 DenseSet<StringAttr> retainedPaths;
2387 for (auto *vnla : nlaPlanner.allVNLAs) {
2388 if (vnla->origSym != curGroup) {
2389 curGroup = vnla->origSym;
2390 if (auto it = existingPaths.find(curGroup); it != existingPaths.end())
2391 b.setInsertionPointAfter(it->second);
2392 }
2393
2394 // Duplicates are materialized by their canonical VNLA.
2395 if (canonicalOrSelf(vnla) != vnla)
2396 continue;
2397 // The primary is emitted unconditionally (I15).
2398 // A fork emits only when an annotation referenced it, and a local
2399 // non-primary has neither a minted symbol nor a path.
2400 bool isPrimary = vnla->realizedSym == vnla->origSym;
2401 if (!isPrimary && (vnla->isLocal() || !vnla->wasUsed))
2402 continue;
2403
2404 auto arrayAttr = materializeNamepath(vnla);
2405 // The planner never invents an origSym; its source op is always present.
2406 auto origIt = existingPaths.find(vnla->origSym);
2407 assert(origIt != existingPaths.end() &&
2408 "origSym has no source hw.hierpath");
2409
2410 // Same symbol survives -> mutate the original op in place.
2411 if (vnla->realizedSym == vnla->origSym) {
2412 // Count (and store) only real retargets, so a no-op run reports zero.
2413 if (arrayAttr != origIt->second.getNamepathAttr()) {
2414 origIt->second.setNamepathAttr(arrayAttr);
2415 ++stats.hierPathsUpdated;
2416 }
2417 retainedPaths.insert(vnla->origSym);
2418 continue;
2419 }
2420
2421 // Forked into a fresh symbol: reuse the original op's location so the fork
2422 // keeps its provenance for diagnostics.
2423 auto hp =
2424 hw::HierPathOp::create(b, origIt->second.getLoc(), vnla->realizedSym,
2425 /*sym_visibility=*/{}, arrayAttr);
2426 hp.setPrivate();
2427 ++stats.hierPathsForked;
2428 }
2429 for (auto &[sym, deadPath] : existingPaths) {
2430 if (retainedPaths.contains(sym))
2431 continue;
2432 // Only a context-less (dead-rooted) origSym reaches here (I15).
2433 deadPath.erase();
2434 ++stats.hierPathsErased;
2435 }
2436}
2437
2438//===----------------------------------------------------------------------===//
2439// Pass Infrastructure
2440//===----------------------------------------------------------------------===//
2441
2442namespace {
2443/// The FIRRTL inliner pass.
2444///
2445/// Runs InliningFacts (P1), NLAPlanner (P2), and Inliner (P3/P4) in sequence.
2446class InlinerPass : public circt::firrtl::impl::InlinerBase<InlinerPass> {
2447 using Base::Base;
2448
2449 void runOnOperation() override {
2451 auto circuit = getOperation();
2452 auto &symbolTable = getAnalysis<SymbolTable>();
2453 auto &instanceGraph = getAnalysis<InstanceGraph>();
2454
2455 // Classify modules (P1).
2456 auto facts = InliningFacts::compute(circuit, instanceGraph, symbolTable);
2457 if (failed(facts))
2458 return signalPassFailure();
2459 LLVM_DEBUG({
2460 llvm::dbgs() << "\n";
2461 debugHeader("InliningFacts results", 40) << "\n";
2462 facts->dump();
2463 });
2464
2465 // Run NLA planning (P2).
2466 NLAPlanner nlaPlanner(circuit, symbolTable, instanceGraph, *facts);
2467 if (failed(nlaPlanner.run()))
2468 return signalPassFailure();
2469 LLVM_DEBUG({
2470 llvm::dbgs() << "\n";
2471 debugHeader("NLA planner results", 40) << "\n";
2472 nlaPlanner.dump();
2473 });
2474 numHierPathsEndInnerSym += nlaPlanner.stats.endInnerSym;
2475 numHierPathsEndModule += nlaPlanner.stats.endModule;
2476
2477 // Run Inlining: Clone (P3), and writeback (P4).
2478 CircuitNamespace circuitNamespace(circuit);
2479 Inliner inliner(circuit, symbolTable, circuitNamespace, *facts, nlaPlanner);
2480 if (failed(inliner.run()))
2481 signalPassFailure();
2482
2483 numInstancesInlined += inliner.stats.instancesInlined;
2484 numInstancesFlattened += inliner.stats.instancesFlattened;
2485 numDeadModules += inliner.stats.deadModules;
2486 numHierPathsUpdated += inliner.stats.hierPathsUpdated;
2487 numHierPathsForked += inliner.stats.hierPathsForked;
2488 numHierPathsMerged += inliner.stats.hierPathsMerged;
2489 numHierPathsErased += inliner.stats.hierPathsErased;
2490 }
2491};
2492} // namespace
assert(baseType &&"element must be base type")
static std::unique_ptr< Context > context
static void dump(DIModule &module, raw_indented_ostream &os)
static Location getLoc(DefSlot slot)
Definition Mem2Reg.cpp:222
DenseMap< hw::InnerRefAttr, StringAttr > InnerRefToNewNameMap
static hw::InnerSymAttr uniqueInNamespace(hw::InnerSymAttr old, InnerRefToNewNameMap &map, hw::InnerSymbolNamespace &ns, StringAttr istName)
Unique each of old's symbols in ns; record old-ref -> new-name entries in map under istName.
static bool vnlaIdLess(const VirtualNLA *a, const VirtualNLA *b)
Context collections are ordered by creation id throughout (I4/I5/I6).
static void mapResultsToWires(IRMapping &mapper, SmallVectorImpl< Value > &wires, InstanceOp instance)
Map each of the instance's results to its corresponding replacement wire.
static LogicalResult replaceInnerRefUsers(ArrayRef< Operation * > newOps, const InnerRefToNewNameMap &map, StringAttr istName)
Process each operation, updating InnerRefAttr's using the specified map, with the given name as the c...
#define CIRCT_DEBUG_SCOPED_PASS_LOGGER(PASS)
Definition Debug.h:70
StringRef newName(const Twine &name)
Return a unique name, derived from the input name, and add the new name to the internal namespace.
Definition Namespace.h:86
This class provides a read-only projection over the MLIR attributes that represent a set of annotatio...
bool removeAnnotations(llvm::function_ref< bool(Annotation)> predicate)
Remove all annotations from this annotation set for which predicate returns true.
ArrayAttr getArrayAttr() const
Return this annotation set as an ArrayAttr.
bool applyToOperation(Operation *op) const
Store the annotations in this set in an operation's annotations attribute, overwriting any existing a...
static AnnotationSet forPort(FModuleLike op, size_t portNo)
Get an annotation set for the specified port.
This class provides a read-only projection of an annotation.
Attribute getAttr() const
Get the underlying attribute.
AttrClass getMember(StringAttr name) const
Return a member of the annotation.
void setMember(StringAttr name, Attribute value)
Add or set a member of the annotation to a value.
void removeMember(StringAttr name)
Remove 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.
This is a Node in the InstanceGraph.
llvm::iterator_range< UseIterator > uses()
auto getModule()
Get the module that this node is tracking.
InstanceGraphNode * lookupOrNull(StringAttr name)
Lookup an module by name.
InstanceGraphNode * lookup(ModuleOpInterface op)
Look up an InstanceGraphNode for a module.
decltype(auto) walkInversePostOrder(Fn &&fn)
Perform an inverse-post-order walk across the modules.
This is an edge in the InstanceGraph.
auto getInstance()
Get the instance-like op that this is tracking.
Direction get(bool isOutput)
Returns an output direction if isOutput is true, otherwise returns an input direction.
Definition CalyxOps.cpp:56
ArrayAttr getAnnotationsIfPresent(Operation *op)
StringAttr getInnerSymName(Operation *op)
Return the StringAttr for the inner_sym name, if it exists.
Definition FIRRTLOps.h:108
static bool operator==(const ModulePort &a, const ModulePort &b)
Definition HWTypes.h:63
void info(Twine message)
Definition LSPUtils.cpp:20
The InstanceGraph op interface, see InstanceGraphInterface.td for more details.
llvm::raw_ostream & debugHeader(const llvm::Twine &str, unsigned width=80)
Write a "header"-like string to the debug stream with a certain width.
Definition Debug.cpp:17
size_t hash_combine(size_t h1, size_t h2)
C++'s stdlib doesn't have a hash_combine function. This is a simple one.
Definition Utils.h:36
int run(Type[Generator] generator=CppGenerator, List[str] cmdline_args=sys.argv)
Definition hw.py:1
llvm::hash_code hash_value(const DenseSet< T > &set)
The namespace of a CircuitOp, generally inhabited by modules.
Definition Namespace.h:24
static unsigned getHashValue(const TrimmedPathRef &key)
static bool isEqual(const TrimmedPathRef &a, const TrimmedPathRef &b)