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