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)
false return failure()
Parse an optional keyword or string and set instance into 'result'.
return success()
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
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.
static AnnotationSet forPort(FModuleLike op, size_t portNo)
Get an annotation set for the specified port.
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.
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.
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
BaseTy type_cast(Type type)
igraph::InstanceRecord InstanceRecord
ArrayAttr getAnnotationsIfPresent(Operation *op)
StringAttr getInnerSymName(Operation *op)
Return the StringAttr for the inner_sym name, if it exists.
Definition FIRRTLOps.h:108
std::map< std::string, std::set< std::string > > InstanceGraph
Iterates over the handshake::FuncOp's in the program to build an instance graph.
static bool operator==(const ModulePort &a, const ModulePort &b)
Definition HWTypes.h:64
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
int run(Type[Generator] generator=CppGenerator, List[str] cmdline_args=sys.argv)
llvm::hash_code hash_value(const DenseSet< T > &set)
static unsigned getHashValue(const TrimmedPathRef &key)
static bool isEqual(const TrimmedPathRef &a, const TrimmedPathRef &b)