Repository: llvm/circt
Pull request #8405: [firrtl] Move LowerLayers after LowerXMR
Author: login: seldridge
Created: 2025-04-09T20:07:56Z | Merged: 2025-04-24T15:02:55Z

Move the `LowerLayers` pass after the `LowerXMR` pass.  To do this, all
passes at the end of the CHIRRTL to Low FIRRTL pipeline are moved after
`LowerXMR`.  This is necessary because the `LowerLayers` pass cannot, at
present, be moved after the passes at the end of the pipeline.

This is done to enable forcing out of layers.  By lowering probes to XMRs,
the layers can be lowered trivially to modules/instances and their XMRs
will now (seemingly) magically just work.  By doing the loweirng in this
way, it avoids ever having to represent an input probe in the FIRRTL
dialect.

A consequence of this is that there are now simplifying
assumptions (preconditions) that can be made about the `LowerLayers` pass:

1. It will never see a number of probe ops because the `LowerXMR` pass has
a postcondition that all of these are removed.

2. Input and output ports can never be created on modules created from
layer blocks.

While I am generally always in favor of passes being relocatable anywhere
in the pipeline, this is one pass that really does _not_ make sense to be
relocatable.  In effect, this pass is part of the lowering from FIRRTL to
HW.  There is no point in being able to use it earlier in the pipeline.
That said, it still is tested to work with things which we may one-day
preserve, like `WhenOp`s.

#### Todo

- [x] Handle zero width



Diff:
diff --git a/include/circt-c/Firtool/Firtool.h b/include/circt-c/Firtool/Firtool.h
index beec1f681d25..140adda59b5a 100644
--- a/include/circt-c/Firtool/Firtool.h
+++ b/include/circt-c/Firtool/Firtool.h
@@ -236,11 +236,11 @@ MLIR_CAPI_EXPORTED MlirLogicalResult circtFirtoolPopulatePreprocessTransforms(
     MlirPassManager pm, CirctFirtoolFirtoolOptions options);
 
 MLIR_CAPI_EXPORTED MlirLogicalResult circtFirtoolPopulateCHIRRTLToLowFIRRTL(
-    MlirPassManager pm, CirctFirtoolFirtoolOptions options,
-    MlirStringRef inputFilename);
+    MlirPassManager pm, CirctFirtoolFirtoolOptions options);
 
 MLIR_CAPI_EXPORTED MlirLogicalResult circtFirtoolPopulateLowFIRRTLToHW(
-    MlirPassManager pm, CirctFirtoolFirtoolOptions options);
+    MlirPassManager pm, CirctFirtoolFirtoolOptions options,
+    MlirStringRef inputFilename);
 
 MLIR_CAPI_EXPORTED MlirLogicalResult circtFirtoolPopulateHWToSV(
     MlirPassManager pm, CirctFirtoolFirtoolOptions options);
diff --git a/include/circt/Firtool/Firtool.h b/include/circt/Firtool/Firtool.h
index fb85cbebe4bb..6258fbe0c169 100644
--- a/include/circt/Firtool/Firtool.h
+++ b/include/circt/Firtool/Firtool.h
@@ -433,11 +433,11 @@ LogicalResult populatePreprocessTransforms(mlir::PassManager &pm,
                                            const FirtoolOptions &opt);
 
 LogicalResult populateCHIRRTLToLowFIRRTL(mlir::PassManager &pm,
-                                         const FirtoolOptions &opt,
-                                         StringRef inputFilename);
+                                         const FirtoolOptions &opt);
 
 LogicalResult populateLowFIRRTLToHW(mlir::PassManager &pm,
-                                    const FirtoolOptions &opt);
+                                    const FirtoolOptions &opt,
+                                    StringRef inputFilename);
 
 LogicalResult populateHWToSV(mlir::PassManager &pm, const FirtoolOptions &opt);
 
diff --git a/lib/CAPI/Firtool/Firtool.cpp b/lib/CAPI/Firtool/Firtool.cpp
index 94808e3c603b..118004fa359b 100644
--- a/lib/CAPI/Firtool/Firtool.cpp
+++ b/lib/CAPI/Firtool/Firtool.cpp
@@ -340,16 +340,17 @@ circtFirtoolPopulatePreprocessTransforms(MlirPassManager pm,
 
 MlirLogicalResult
 circtFirtoolPopulateCHIRRTLToLowFIRRTL(MlirPassManager pm,
-                                       CirctFirtoolFirtoolOptions options,
-                                       MlirStringRef inputFilename) {
-  return wrap(firtool::populateCHIRRTLToLowFIRRTL(*unwrap(pm), *unwrap(options),
-                                                  unwrap(inputFilename)));
+                                       CirctFirtoolFirtoolOptions options) {
+  return wrap(
+      firtool::populateCHIRRTLToLowFIRRTL(*unwrap(pm), *unwrap(options)));
 }
 
 MlirLogicalResult
 circtFirtoolPopulateLowFIRRTLToHW(MlirPassManager pm,
-                                  CirctFirtoolFirtoolOptions options) {
-  return wrap(firtool::populateLowFIRRTLToHW(*unwrap(pm), *unwrap(options)));
+                                  CirctFirtoolFirtoolOptions options,
+                                  MlirStringRef inputFilename) {
+  return wrap(firtool::populateLowFIRRTLToHW(*unwrap(pm), *unwrap(options),
+                                             unwrap(inputFilename)));
 }
 
 MlirLogicalResult
diff --git a/lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp b/lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
index 5d10a8c29eb7..cd7eac672ed7 100644
--- a/lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
+++ b/lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
@@ -13,6 +13,7 @@
 #include "circt/Dialect/FIRRTL/FIRRTLUtils.h"
 #include "circt/Dialect/FIRRTL/Namespace.h"
 #include "circt/Dialect/FIRRTL/Passes.h"
+#include "circt/Dialect/HW/HierPathCache.h"
 #include "circt/Dialect/HW/InnerSymbolNamespace.h"
 #include "circt/Dialect/SV/SVOps.h"
 #include "circt/Support/Utils.h"
@@ -52,6 +53,18 @@ struct ConnectInfo {
 /// for modules and files, as well as by convention.
 enum class Delimiter { BindModule = '_', BindFile = '-', InlineMacro = '$' };
 
+/// This struct contains pre-allocated "safe" names that parallel regions can
+/// use to create names in the global namespace.  This is allocated per-layer
+/// block.
+struct LayerBlockGlobals {
+
+  /// If the layer needs to create a module, use this name.
+  StringRef moduleName;
+
+  /// If the layer needs to create a hw::HierPathOp, use this name.
+  StringRef hierPathName;
+};
+
 } // namespace
 
 // A mapping of an old InnerRefAttr to the new inner symbol and module name that
@@ -98,6 +111,16 @@ static SmallString<32> moduleNameForLayer(StringRef moduleName,
   return result;
 }
 
+static SmallString<32> hierPathNameForLayer(StringRef moduleName,
+                                            SymbolRefAttr layerName) {
+  SmallString<32> result("__lowerLayers_path");
+  appendName(moduleName, result, /*toLower=*/false,
+             /*delimiter=*/Delimiter::BindModule);
+  appendName(layerName, result, /*toLower=*/false,
+             /*delimiter=*/Delimiter::BindModule);
+  return result;
+}
+
 /// For a layerblock `@A::@B::@C`,
 /// the generated instance is called `a_b_c`.
 static SmallString<32> instanceNameForLayer(SymbolRefAttr layerName) {
@@ -158,8 +181,7 @@ class LowerLayersPass
 
   /// Safely build a new module with a given namehint.  This handles geting a
   /// lock to modify the top-level circuit.
-  FModuleOp buildNewModule(OpBuilder &builder, LayerBlockOp layerBlock,
-                           SmallVectorImpl<PortInfo> &ports);
+  FModuleOp buildNewModule(OpBuilder &builder, LayerBlockOp layerBlock);
 
   /// Strip layer colors from the module's interface.
   FailureOr<InnerRefMap> runOnModuleLike(FModuleLike moduleLike);
@@ -174,10 +196,6 @@ class LowerLayersPass
   /// Update the value's type to remove any layers from any probe types.
   void removeLayersFromValue(Value value);
 
-  /// Remove any layers from the result of the cast. If the cast becomes a nop,
-  /// remove the cast itself from the IR.
-  void removeLayersFromRefCast(RefCastOp cast);
-
   /// Lower an inline layerblock to an ifdef block.
   void lowerInlineLayerBlock(LayerOp layer, LayerBlockOp layerBlock);
 
@@ -195,14 +213,18 @@ class LowerLayersPass
   /// Indicates exclusive access to modify the circuitNamespace and the circuit.
   llvm::sys::SmartMutex<true> *circuitMutex;
 
-  /// A map of layer blocks to module name that should be created for it.
-  DenseMap<LayerBlockOp, StringRef> moduleNames;
+  /// A map of layer blocks to "safe" global names which are fine to create in
+  /// the circuit namespace.
+  DenseMap<LayerBlockOp, LayerBlockGlobals> layerBlockGlobals;
 
   /// A map from inline layers to their macro names.
   DenseMap<LayerOp, FlatSymbolRefAttr> macroNames;
 
   /// A mapping of symbol name to layer operation.
   DenseMap<SymbolRefAttr, LayerOp> symbolToLayer;
+
+  /// Utility for creating hw::HierPathOp.
+  hw::HierPathCache *hierPathCache;
 };
 
 /// Multi-process safe function to build a module in the circuit and return it.
@@ -210,15 +232,14 @@ class LowerLayersPass
 /// generated if there are conflicts with the namehint in the circuit-level
 /// namespace.
 FModuleOp LowerLayersPass::buildNewModule(OpBuilder &builder,
-                                          LayerBlockOp layerBlock,
-                                          SmallVectorImpl<PortInfo> &ports) {
+                                          LayerBlockOp layerBlock) {
   auto location = layerBlock.getLoc();
-  auto namehint = moduleNames.lookup(layerBlock);
+  auto namehint = layerBlockGlobals.lookup(layerBlock).moduleName;
   llvm::sys::SmartScopedLock<true> instrumentationLock(*circuitMutex);
   FModuleOp newModule = builder.create<FModuleOp>(
       location, builder.getStringAttr(namehint),
-      ConventionAttr::get(builder.getContext(), Convention::Internal), ports,
-      ArrayAttr{});
+      ConventionAttr::get(builder.getContext(), Convention::Internal),
+      ArrayRef<PortInfo>{}, ArrayAttr{});
   if (auto dir = getOutputFile(layerBlock.getLayerNameAttr())) {
     assert(dir.isDirectory());
     newModule->setAttr("output_file", dir);
@@ -234,22 +255,6 @@ void LowerLayersPass::removeLayersFromValue(Value value) {
   value.setType(type.removeLayer());
 }
 
-void LowerLayersPass::removeLayersFromRefCast(RefCastOp cast) {
-  auto result = cast.getResult();
-  auto oldType = result.getType();
-  if (oldType.getLayer()) {
-    auto input = cast.getInput();
-    auto srcType = input.getType();
-    auto newType = oldType.removeLayer();
-    if (newType == srcType) {
-      result.replaceAllUsesWith(input);
-      cast->erase();
-      return;
-    }
-    result.setType(newType);
-  }
-}
-
 void LowerLayersPass::removeLayersFromPorts(FModuleLike moduleLike) {
   auto oldTypeAttrs = moduleLike.getPortTypesAttr();
   SmallVector<Attribute> newTypeAttrs;
@@ -324,47 +329,180 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand, ImplicitLocOpBuilder &builder,
+                               StringRef nameHint) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand,
+                 builder.create<NodeOp>(operand.getLoc(), operand, nameHint)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *user, Value value) -> Value {
+    auto it = replacements.find(value);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(value.getLoc(), &getContext());
+    Value replacement;
+
+    auto layerBlockOp = user->getParentOfType<LayerBlockOp>();
+    localBuilder.setInsertionPointToStart(layerBlockOp.getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(value.getType())) {
+      localBuilder.setInsertionPoint(user);
+      replacement = localBuilder.clone(*value.getDefiningOp())->getResult(0);
+      replacements.insert({value, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = value.getDefiningOp();
+    if (isa_and_present<XMRRefOp>(definingOp)) {
+      replacement = localBuilder.clone(*definingOp)->getResult(0);
+      replacements.insert({value, replacement});
+      return replacement;
+    }
+
+    // Determine the replacement value for the captured operand.  There are
+    // three cases that can occur:
+    //
+    // 1. Capturing something zero-width.  Create a zero-width constant zero.
+    // 2. Capture an expression or instance port.  Drop a node and XMR deref
+    //    that.
+    // 3. Capture something that can handle an inner sym.  XMR deref that.
+    //
+    // Note: (3) can be either an operation or a _module_ port.
+    auto baseType = type_cast<FIRRTLBaseType>(value.getType());
+    if (baseType && baseType.getBitWidthOrSentinel() == 0) {
+      OpBuilder::InsertionGuard guard(localBuilder);
+      auto zeroUIntType = UIntType::get(localBuilder.getContext(), 0);
+      replacement = localBuilder.createOrFold<BitCastOp>(
+          value.getType(), localBuilder.create<ConstantOp>(
+                               zeroUIntType, getIntZerosAttr(zeroUIntType)));
+    } else {
+      auto *definingOp = value.getDefiningOp();
+      hw::InnerRefAttr innerRef;
+      if (definingOp) {
+        // TODO: Always create a node.  This is a trade-off between
+        // optimizations and dead code.  By adding the node, this allows the
+        // original path to be better optimized, but will leave dead code in the
+        // design.  If the node is not created, then the output is less
+        // optimized.  Err on the side of dead code.  This _should_ be able to
+        // be removed eventually [1].  A node should only be necessary for
+        // operations which cannot have a symbol attached to them, i.e.,
+        // expressions or instance ports.
+        //
+        // [1]: https://github.com/llvm/circt/issues/8426
+        definingOp = getOrCreateNodeOp(value, localBuilder, "_layer_probe");
+        innerRef = getInnerRefTo(
+            definingOp, [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      } else {
+        auto portIdx = cast<BlockArgument>(value).getArgNumber();
+        innerRef = getInnerRefTo(
+            cast<FModuleLike>(*moduleOp), portIdx,
+            [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      }
+
+      hw::HierPathOp hierPathOp;
+      {
+        // TODO: Move to before parallel region to avoid the lock.
+        auto insertPoint = OpBuilder::InsertPoint(moduleOp->getBlock(),
+                                                  Block::iterator(moduleOp));
+        llvm::sys::SmartScopedLock<true> circuitLock(*circuitMutex);
+        hierPathOp = hierPathCache->getOrCreatePath(
+            localBuilder.getArrayAttr({innerRef}), localBuilder.getLoc(),
+            insertPoint, layerBlockGlobals.lookup(layerBlockOp).hierPathName);
+        hierPathOp.setVisibility(SymbolTable::Visibility::Private);
+      }
+
+      replacement = localBuilder.create<XMRDerefOp>(
+          value.getType(), hierPathOp.getSymNameAttr());
+    }
+
+    replacements.insert({value, replacement});
+
+    return replacement;
+  };
+
   // A map of instance ops to modules that this pass creates.  This is used to
   // check if this was an instance that we created and to do fast module
   // dereferencing (avoiding a symbol table).
   DenseMap<InstanceOp, FModuleOp> createdInstances;
 
-  // Post-order traversal that expands a layer block into its parent. For each
-  // layer block found do the following:
+  // Check that the preconditions for this pass are met.  Reject any ops which
+  // must have been removed before this runs.
+  auto opPreconditionCheck = [](Operation *op) -> LogicalResult {
+    // LowerXMR op removal postconditions.
+    if (isa<RefCastOp, RefDefineOp, RefResolveOp, RefSendOp, RefSubOp,
+            RWProbeOp>(op))
+      return op->emitOpError()
+             << "cannot be handled by the lower-layers pass.  This should have "
+                "already been removed by the lower-xmr pass.";
+
+    return success();
+  };
+
+  // Post-order traversal that expands a layer block into its parent. Because of
+  // the pass precondition that this runs _after_ `LowerXMR`, not much has to
+  // happen here.  All of the following do happen, though:
+  //
+  // 1. Any layer coloring is stripped.
+  // 2. Layers with Inline convention are converted to SV ifdefs.
+  // 3. Layers with Bind convention are converted to new modules and then
+  //    instantiated at their original location.  Any captured values are either
+  //    moved, cloned, or converted to XMR deref ops.
+  // 4. Move instances created from earlier (3) conversions out of later (3)
+  //    conversions.  This is necessary to avoid a SystemVerilog-illegal
+  //    bind-under-bind.  (See Section 23.11 of 1800-2023.)
+  // 5. Keep track of special ops (ops with inner symbols or verbatims) which
+  //    need to have something updated because of the new instance hierarchy
+  //    being created.
   //
-  // 1. Create and connect one ref-type output port for each value defined in
-  //    this layer block that drives an instance marked lowerToBind and move
-  //    this instance outside the layer block.
-  // 2. Create one input port for each value captured by this layer block.
-  // 3. Create a new module for this layer block and move the (mutated) body of
-  //    this layer block to the new module.
-  // 4. Instantiate the new module outside the layer block and hook it up.
-  // 5. Erase the layer block.
+  // Remember, this is post-order, in-order.  Child layer blocks are visited
+  // before parents.  Any nested regions _within_ the layer block are also
+  // visited before the outer layer block.
   auto result = moduleOp.walk<mlir::WalkOrder::PostOrder>([&](Operation *op) {
+    if (failed(opPreconditionCheck(op)))
+      return WalkResult::interrupt();
+
     // Strip layer requirements from any op that might represent a probe.
     if (auto wire = dyn_cast<WireOp>(op)) {
       removeLayersFromValue(wire.getResult());
       return WalkResult::advance();
     }
-    if (auto sub = dyn_cast<RefSubOp>(op)) {
-      removeLayersFromValue(sub.getResult());
-      return WalkResult::advance();
-    }
     if (auto instance = dyn_cast<InstanceOp>(op)) {
       instance.setLayers({});
       for (auto result : instance.getResults())
         removeLayersFromValue(result);
       return WalkResult::advance();
     }
-    if (auto cast = dyn_cast<RefCastOp>(op)) {
-      removeLayersFromRefCast(cast);
-      return WalkResult::advance();
-    }
 
     auto layerBlock = dyn_cast<LayerBlockOp>(op);
     if (!layerBlock)
       return WalkResult::advance();
 
+    // After this point, we are dealing with a layer block.
     auto layer = symbolToLayer.lookup(layerBlock.getLayerName());
 
     if (layer.getConvention() == LayerConvention::Inline) {
@@ -372,162 +510,40 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
       return WalkResult::advance();
     }
 
+    // After this point, we are dealing with a bind convention layer block.
     assert(layer.getConvention() == LayerConvention::Bind);
-    Block *body = layerBlock.getBody(0);
-    OpBuilder builder(moduleOp);
-
-    // Ports that need to be created for the module derived from this layer
-    // block.
-    SmallVector<PortInfo> ports;
-
-    // Connection that need to be made to the instance of the derived module.
-    SmallVector<ConnectInfo> connectValues;
-
-    // Create an input port for an operand that is captured from outside.
-    auto createInputPort = [&](Value operand, Location loc) {
-      const auto &[portName, _] = getFieldName(FieldRef(operand, 0), true);
-
-      // If the value is a ref, we must resolve the ref inside the parent,
-      // passing the input as a value instead of a ref. Inside the layer, we
-      // convert (ref.send) the value back into a ref.
-      auto type = operand.getType();
-      if (auto refType = dyn_cast<RefType>(type))
-        type = refType.getType();
-
-      ports.push_back({builder.getStringAttr(portName), type, Direction::In,
-                       /*symName=*/{},
-                       /*location=*/loc});
-      // Update the layer block's body with arguments as we will swap this body
-      // into the module when we create it.  If this is a ref type, then add a
-      // refsend to convert from the non-ref type input port.
-      Value replacement = body->addArgument(type, loc);
-      if (isa<RefType>(operand.getType())) {
-        OpBuilder::InsertionGuard guard(builder);
-        builder.setInsertionPointToStart(body);
-        replacement = builder.create<RefSendOp>(loc, replacement);
-      }
-      operand.replaceUsesWithIf(replacement, [&](OpOperand &use) {
-        auto *user = use.getOwner();
-        if (!layerBlock->isAncestor(user))
-          return false;
-        if (auto connectLike = dyn_cast<FConnectLike>(user)) {
-          if (use.getOperandNumber() == 0)
-            return false;
-        }
-        return true;
-      });
-
-      connectValues.push_back({operand, ConnectKind::NonRef});
-    };
-
-    // Set the location intelligently.  Use the location of the capture if this
-    // is a port created for forwarding from a parent layer block to a nested
-    // layer block.  Otherwise, use unknown.
-    auto getPortLoc = [&](Value port) -> Location {
-      Location loc = UnknownLoc::get(port.getContext());
-      if (auto *destOp = port.getDefiningOp())
-        if (auto instOp = dyn_cast<InstanceOp>(destOp)) {
-          auto modOpIt = createdInstances.find(instOp);
-          if (modOpIt != createdInstances.end()) {
-            auto portNum = cast<OpResult>(port).getResultNumber();
-            loc = modOpIt->getSecond().getPortLocation(portNum);
-          }
-        }
-      return loc;
-    };
-
-    // Create an output probe port port and adds a ref.define/ref.send to
-    // drive the port if this was not already capturing a ref type.
-    auto createOutputPort = [&](Value dest, Value src) {
-      auto loc = getPortLoc(dest);
-      auto portNum = ports.size();
-      const auto &[portName, _] = getFieldName(FieldRef(dest, 0), true);
-
-      RefType refType;
-      if (auto oldRef = dyn_cast<RefType>(dest.getType()))
-        refType = oldRef;
-      else
-        refType = RefType::get(
-            type_cast<FIRRTLBaseType>(dest.getType()).getPassiveType(),
-            /*forceable=*/false);
-
-      ports.push_back({builder.getStringAttr(portName), refType, Direction::Out,
-                       /*symName=*/{}, /*location=*/loc});
-      Value replacement = body->addArgument(refType, loc);
-      if (isa<RefType>(dest.getType())) {
-        dest.replaceUsesWithIf(replacement, [&](OpOperand &use) {
-          auto *user = use.getOwner();
-          if (!layerBlock->isAncestor(user))
-            return false;
-          if (auto connectLike = dyn_cast<FConnectLike>(user)) {
-            if (use.getOperandNumber() == 0)
-              return true;
-          }
-          return false;
-        });
-        connectValues.push_back({dest, ConnectKind::Ref});
-        return;
-      }
-      connectValues.push_back({dest, ConnectKind::NonRef});
-      OpBuilder::InsertionGuard guard(builder);
-      builder.setInsertionPointAfterValue(src);
-      builder.create<RefDefineOp>(
-          loc, body->getArgument(portNum),
-          builder.create<RefSendOp>(loc, src)->getResult(0));
-    };
 
+    // Clear the replacements so that none are re-used across layer blocks.
+    replacements.clear();
+    OpBuilder builder(moduleOp);
     SmallVector<hw::InnerSymAttr> innerSyms;
-    SmallVector<RWProbeOp> rwprobes;
     SmallVector<sv::VerbatimOp> verbatims;
+    DenseSet<Operation *> spilledSubOps;
     auto layerBlockWalkResult = layerBlock.walk([&](Operation *op) {
-      // Record any operations inside the layer block which have inner symbols.
-      // Theses may have symbol users which need to be updated.
-      if (auto symOp = dyn_cast<hw::InnerSymbolOpInterface>(op))
-        if (auto innerSym = symOp.getInnerSymAttr())
-          innerSyms.push_back(innerSym);
-
-      // Handle instance ops that were created from nested layer blocks.  These
-      // ops need to be moved outside the layer block to avoid nested binds.
-      // Nested binds are illegal in the SystemVerilog specification (and
-      // checked by FIRRTL verification).
-      //
-      // For each value defined in this layer block which drives a port of one
-      // of these instances, create an output reference type port on the
-      // to-be-created module and drive it with the value.  Move the instance
-      // outside the layer block.  We will hook it up later once we replace the
-      // layer block with an instance.
-      if (auto instOp = dyn_cast<InstanceOp>(op)) {
-        // Ignore instances which this pass did not create.
-        if (!createdInstances.contains(instOp))
-          return WalkResult::advance();
-
-        LLVM_DEBUG({
-          llvm::dbgs()
-              << "      Found instance created from nested layer block:\n"
-              << "        module: " << instOp.getModuleName() << "\n"
-              << "        instance: " << instOp.getName() << "\n";
-        });
-        instOp->moveBefore(layerBlock);
-        return WalkResult::advance();
-      }
+      // Error if pass preconditions are not met.
+      if (failed(opPreconditionCheck(op)))
+        return WalkResult::interrupt();
 
-      // Handle subfields, subindexes, and subaccesses which are indexing into
-      // non-passive values.  If these are kept in the module, then the module
-      // must create bi-directional ports.  This doesn't make sense as the
-      // FIRRTL spec states that no layerblock may write to values outside it.
-      // Fix this for subfield and subindex by moving these ops outside the
+      // Specialized handling of subfields, subindexes, and subaccesses which
+      // need to be spilled and nodes that referred to spilled nodes.  If these
+      // are kept in the module, then the XMR is going to be bidirectional.  Fix
+      // this for subfield and subindex by moving these ops outside the
       // layerblock.  Try to fix this for subaccess and error if the move can't
       // be made because the index is defined inside the layerblock.  (This case
       // is exceedingly rare given that subaccesses are almost always unexepcted
-      // when this pass runs.)
+      // when this pass runs.)  Additionally, if any nodes are seen that are
+      // transparently referencing a spilled op, spill the node, too.  The node
+      // provides an anchor for an inner symbol (which subfield, subindex, and
+      // subaccess do not).
       if (isa<SubfieldOp, SubindexOp>(op)) {
         auto input = op->getOperand(0);
         if (!firrtl::type_cast<FIRRTLBaseType>(input.getType()).isPassive() &&
-            !isAncestorOfValueOwner(layerBlock, input))
+            !isAncestorOfValueOwner(layerBlock, input)) {
           op->moveBefore(layerBlock);
+          spilledSubOps.insert(op);
+        }
         return WalkResult::advance();
       }
-
       if (auto subOp = dyn_cast<SubaccessOp>(op)) {
         auto input = subOp.getInput();
         if (firrtl::type_cast<FIRRTLBaseType>(input.getType()).isPassive())
@@ -536,6 +552,7 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
         if (!isAncestorOfValueOwner(layerBlock, input) &&
             !isAncestorOfValueOwner(layerBlock, subOp.getIndex())) {
           subOp->moveBefore(layerBlock);
+          spilledSubOps.insert(op);
           return WalkResult::advance();
         }
         auto diag = op->emitOpError()
@@ -547,83 +564,61 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
             << "the layerblock is defined here";
         return WalkResult::interrupt();
       }
-
-      if (auto rwprobe = dyn_cast<RWProbeOp>(op)) {
-        rwprobes.push_back(rwprobe);
-        return WalkResult::advance();
-      }
-
-      if (auto connect = dyn_cast<FConnectLike>(op)) {
-        auto src = connect.getSrc();
-        auto dst = connect.getDest();
-        auto srcInLayerBlock = isAncestorOfValueOwner(layerBlock, src);
-        auto dstInLayerBlock = isAncestorOfValueOwner(layerBlock, dst);
-        if (!srcInLayerBlock && !dstInLayerBlock) {
-          connect->moveBefore(layerBlock);
-          return WalkResult::advance();
-        }
-        // Create an input port.
-        if (!srcInLayerBlock) {
-          createInputPort(src, op->getLoc());
+      if (auto nodeOp = dyn_cast<NodeOp>(op)) {
+        auto *definingOp = nodeOp.getInput().getDefiningOp();
+        if (definingOp &&
+            spilledSubOps.contains(nodeOp.getInput().getDefiningOp())) {
+          op->moveBefore(layerBlock);
           return WalkResult::advance();
         }
-        // Create an output port.
-        if (!dstInLayerBlock) {
-          createOutputPort(dst, src);
-          if (!isa<RefType>(dst.getType()))
-            connect.erase();
+      }
+
+      // Record any operations inside the layer block which have inner symbols.
+      // Theses may have symbol users which need to be updated.
+      //
+      // Note: this needs to _not_ index spilled NodeOps above.
+      if (auto symOp = dyn_cast<hw::InnerSymbolOpInterface>(op))
+        if (auto innerSym = symOp.getInnerSymAttr())
+          innerSyms.push_back(innerSym);
+
+      // Handle instance ops that were created from nested layer blocks.  These
+      // ops need to be moved outside the layer block to avoid nested binds.
+      // Nested binds are illegal in the SystemVerilog specification (and
+      // checked by FIRRTL verification).
+      //
+      // For each value defined in this layer block which drives a port of one
+      // of these instances, create an output reference type port on the
+      // to-be-created module and drive it with the value.  Move the instance
+      // outside the layer block.  We will hook it up later once we replace the
+      // layer block with an instance.
+      if (auto instOp = dyn_cast<InstanceOp>(op)) {
+        // Ignore instances which this pass did not create.
+        if (!createdInstances.contains(instOp))
           return WalkResult::advance();
-        }
-        // Source and destination in layer block.  Nothing to do.
+
+        LLVM_DEBUG({
+          llvm::dbgs()
+              << "      Found instance created from nested layer block:\n"
+              << "        module: " << instOp.getModuleName() << "\n"
+              << "        instance: " << instOp.getName() << "\n";
+        });
+        instOp->moveBefore(layerBlock);
         return WalkResult::advance();
       }
 
-      // Pre-emptively de-squiggle connections that we are creating.  This will
-      // later be cleaned up by the de-squiggling pass.  However, there is no
-      // point in creating deeply squiggled connections if we don't have to.
-      //
-      // This pattern matches the following structure.  Move the ref.resolve
-      // outside the layer block.  The matchingconnect will be moved outside in
-      // the next loop iteration:
-      //     %0 = ...
-      //     %1 = ...
-      //     firrtl.layerblock {
-      //       %2 = ref.resolve %0
-      //       firrtl.matchingconnect %1, %2
-      //     }
-      if (auto refResolve = dyn_cast<RefResolveOp>(op))
-        if (refResolve.getResult().hasOneUse() &&
-            refResolve.getRef().getParentBlock() != body)
-          if (auto connect = dyn_cast<MatchingConnectOp>(
-                  *refResolve.getResult().getUsers().begin()))
-            if (connect.getDest().getParentBlock() != body) {
-              refResolve->moveBefore(layerBlock);
-              return WalkResult::advance();
-            }
-
-      // For any other ops, create input ports for any captured operands.
+      // Handle captures.  For any captured operands, convert them to a suitable
+      // replacement value.  The `getReplacement` function will automatically
+      // reuse values whenever possible.
       for (size_t i = 0, e = op->getNumOperands(); i != e; ++i) {
         auto operand = op->getOperand(i);
 
         // If the operand is in this layer block, do nothing.
-        if (isAncestorOfValueOwner(layerBlock, operand))
-          continue;
-
-        // If the operand is "special", e.g., it has no port representation,
-        // then we need to clone it.
         //
-        // TODO: Change this to recursively clone.  This will matter once
-        // FString operations have operands.
-        if (type_isa<FStringType>(operand.getType())) {
-          OpBuilder::InsertionGuard guard(builder);
-          builder.setInsertionPoint(op);
-          op->setOperand(i,
-                         builder.clone(*operand.getDefiningOp())->getResult(0));
+        // Note: This check is what avoids handling ConnectOp destinations.
+        if (isAncestorOfValueOwner(layerBlock, operand))
           continue;
-        }
 
-        // Create a port to capture the operand.
-        createInputPort(operand, op->getLoc());
+        op->setOperand(i, getReplacement(op, operand));
       }
 
       if (auto verbatim = dyn_cast<sv::VerbatimOp>(op))
@@ -636,33 +631,22 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
       return WalkResult::interrupt();
 
     // Create the new module.  This grabs a lock to modify the circuit.
-    FModuleOp newModule = buildNewModule(builder, layerBlock, ports);
+    FModuleOp newModule = buildNewModule(builder, layerBlock);
     SymbolTable::setSymbolVisibility(newModule,
                                      SymbolTable::Visibility::Private);
     newModule.getBody().takeBody(layerBlock.getRegion());
 
     LLVM_DEBUG({
-      llvm::dbgs() << "      New Module: " << moduleNames.lookup(layerBlock)
-                   << "\n";
-      llvm::dbgs() << "        ports:\n";
-      for (size_t i = 0, e = ports.size(); i != e; ++i) {
-        auto port = ports[i];
-        auto value = connectValues[i];
-        llvm::dbgs() << "          - name: " << port.getName() << "\n"
-                     << "            type: " << port.type << "\n"
-                     << "            direction: " << port.direction << "\n"
-                     << "            value: " << value.value << "\n"
-                     << "            kind: "
-                     << (value.kind == ConnectKind::NonRef ? "NonRef" : "Ref")
-                     << "\n";
-      }
+      llvm::dbgs() << "      New Module: "
+                   << layerBlockGlobals.lookup(layerBlock).moduleName << "\n";
     });
 
-    // Replace the original layer block with an instance.  Hook up the instance.
-    // Intentionally create instance with probe ports which do not have an
-    // associated layer.  This is illegal IR that will be made legal by the end
-    // of the pass.  This is done to avoid having to revisit and rewrite each
-    // instance everytime it is moved into a parent layer.
+    // Replace the original layer block with an instance.  Hook up the
+    // instance. Intentionally create instance with probe ports which do
+    // not have an associated layer.  This is illegal IR that will be
+    // made legal by the end of the pass.  This is done to avoid having
+    // to revisit and rewrite each instance everytime it is moved into a
+    // parent layer.
     builder.setInsertionPointAfter(layerBlock);
     auto instanceName = instanceNameForLayer(layerBlock.getLayerName());
     auto instanceOp = builder.create<InstanceOp>(
@@ -695,34 +679,6 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
                               << splice.second << "\n";);
     }
 
-    // Update RWProbe operations.
-    for (auto rwprobe : rwprobes) {
-      auto targetRef = rwprobe.getTarget();
-      auto mapped = innerRefMap.find(targetRef);
-      if (mapped == innerRefMap.end()) {
-        assert(targetRef.getModule() == moduleOp.getNameAttr());
-        auto ist = hw::InnerSymbolTable::get(moduleOp);
-        if (failed(ist))
-          return WalkResult::interrupt();
-        auto target = ist->lookup(targetRef.getName());
-        assert(target);
-        auto fieldref = getFieldRefForTarget(target);
-        rwprobe
-            .emitError(
-                "rwprobe capture not supported with bind convention layer")
-            .attachNote(fieldref.getLoc())
-            .append("rwprobe target outside of bind layer");
-        return WalkResult::interrupt();
-      }
-
-      if (mapped->second.second != newModule.getModuleNameAttr())
-        return rwprobe.emitError("rwprobe target refers to different module"),
-               WalkResult::interrupt();
-
-      rwprobe.setTargetAttr(
-          hw::InnerRefAttr::get(mapped->second.second, targetRef.getName()));
-    }
-
     // Update verbatims that target operations extracted alongside.
     if (!verbatims.empty()) {
       mlir::AttrTypeReplacer replacer;
@@ -737,34 +693,6 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
         replacer.replaceElementsIn(verbatim);
     }
 
-    // Connect instance ports to values.
-    assert(ports.size() == connectValues.size() &&
-           "the number of instance ports and values to connect to them must be "
-           "equal");
-    for (unsigned portNum = 0, e = newModule.getNumPorts(); portNum < e;
-         ++portNum) {
-      OpBuilder::InsertionGuard guard(builder);
-      builder.setInsertionPointAfterValue(instanceOp.getResult(portNum));
-      if (instanceOp.getPortDirection(portNum) == Direction::In) {
-        auto src = connectValues[portNum].value;
-        if (isa<RefType>(src.getType()))
-          src = builder.create<RefResolveOp>(
-              newModule.getPortLocationAttr(portNum), src);
-        builder.create<MatchingConnectOp>(
-            newModule.getPortLocationAttr(portNum),
-            instanceOp.getResult(portNum), src);
-      } else if (isa<RefType>(instanceOp.getResult(portNum).getType()) &&
-                 connectValues[portNum].kind == ConnectKind::Ref)
-        builder.create<RefDefineOp>(getPortLoc(connectValues[portNum].value),
-                                    connectValues[portNum].value,
-                                    instanceOp.getResult(portNum));
-      else
-        builder.create<MatchingConnectOp>(
-            getPortLoc(connectValues[portNum].value),
-            connectValues[portNum].value,
-            builder.create<RefResolveOp>(newModule.getPortLocationAttr(portNum),
-                                         instanceOp.getResult(portNum)));
-    }
     layerBlock.erase();
 
     return WalkResult::advance();
@@ -820,14 +748,22 @@ void LowerLayersPass::runOnOperation() {
   circuitMutex = &mutex;
 
   CircuitNamespace ns(circuitOp);
+  hw::HierPathCache hpc(
+      &ns, OpBuilder::InsertPoint(getOperation().getBodyBlock(),
+                                  getOperation().getBodyBlock()->begin()));
+  hierPathCache = &hpc;
+
   for (auto &op : *circuitOp.getBodyBlock()) {
     // Determine names for all modules that will be created.  Do this serially
     // to avoid non-determinism from creating these in the parallel region.
     if (auto moduleOp = dyn_cast<FModuleOp>(op)) {
       moduleOp->walk([&](LayerBlockOp layerBlockOp) {
-        auto name = moduleNameForLayer(moduleOp.getModuleName(),
-                                       layerBlockOp.getLayerName());
-        moduleNames.insert({layerBlockOp, ns.newName(name)});
+        auto moduleName = moduleNameForLayer(moduleOp.getModuleName(),
+                                             layerBlockOp.getLayerName());
+        auto hierPathName = hierPathNameForLayer(moduleOp.getModuleName(),
+                                                 layerBlockOp.getLayerName());
+        layerBlockGlobals.insert(
+            {layerBlockOp, {ns.newName(moduleName), ns.newName(hierPathName)}});
       });
       continue;
     }
@@ -980,6 +916,9 @@ void LowerLayersPass::runOnOperation() {
   for (auto layerOp :
        llvm::make_early_inc_range(circuitOp.getBodyBlock()->getOps<LayerOp>()))
     layerOp.erase();
+
+  // Cleanup state.
+  hierPathCache = nullptr;
 }
 
 std::unique_ptr<mlir::Pass> circt::firrtl::createLowerLayersPass() {
diff --git a/lib/Firtool/Firtool.cpp b/lib/Firtool/Firtool.cpp
index d661eaa04cc2..8511edd1f343 100644
--- a/lib/Firtool/Firtool.cpp
+++ b/lib/Firtool/Firtool.cpp
@@ -53,8 +53,7 @@ LogicalResult firtool::populatePreprocessTransforms(mlir::PassManager &pm,
 }
 
 LogicalResult firtool::populateCHIRRTLToLowFIRRTL(mlir::PassManager &pm,
-                                                  const FirtoolOptions &opt,
-                                                  StringRef inputFilename) {
+                                                  const FirtoolOptions &opt) {
   // TODO: Ensure instance graph and other passes can handle instance choice
   // then run this pass after all diagnostic passes have run.
   pm.addNestedPass<firrtl::CircuitOp>(firrtl::createSpecializeOptionPass(
@@ -217,6 +216,17 @@ LogicalResult firtool::populateCHIRRTLToLowFIRRTL(mlir::PassManager &pm,
     pm.nest<firrtl::CircuitOp>().nest<firrtl::FModuleOp>().addPass(
         firrtl::createVectorizationPass());
 
+  return success();
+}
+
+LogicalResult firtool::populateLowFIRRTLToHW(mlir::PassManager &pm,
+                                             const FirtoolOptions &opt,
+                                             StringRef inputFilename) {
+  // Lower the ref.resolve and ref.send ops and remove the RefType ports.
+  // LowerToHW cannot handle RefType so, this pass must be run to remove all
+  // RefType ports and ops.
+  pm.nest<firrtl::CircuitOp>().addPass(firrtl::createLowerXMRPass());
+
   // Layer lowering passes.  Move operations into layers when possible and
   // remove layers by converting them to other constructs.  This lowering
   // process can create a few optimization opportunities.
@@ -258,21 +268,12 @@ LogicalResult firtool::populateCHIRRTLToLowFIRRTL(mlir::PassManager &pm,
                                : opt.getBlackBoxRootPath();
   pm.nest<firrtl::CircuitOp>().addPass(
       firrtl::createBlackBoxReaderPass(blackBoxRoot));
-  return success();
-}
 
-LogicalResult firtool::populateLowFIRRTLToHW(mlir::PassManager &pm,
-                                             const FirtoolOptions &opt) {
   // Remove TraceAnnotations and write their updated paths to an output
   // annotation file.
   pm.nest<firrtl::CircuitOp>().addPass(
       firrtl::createResolveTracesPass(opt.getOutputAnnotationFilename()));
 
-  // Lower the ref.resolve and ref.send ops and remove the RefType ports.
-  // LowerToHW cannot handle RefType so, this pass must be run to remove all
-  // RefType ports and ops.
-  pm.nest<firrtl::CircuitOp>().addPass(firrtl::createLowerXMRPass());
-
   pm.nest<firrtl::CircuitOp>().addPass(firrtl::createLowerDPIPass());
   pm.nest<firrtl::CircuitOp>().addPass(firrtl::createLowerClassesPass());
   pm.nest<firrtl::CircuitOp>().addPass(om::createVerifyObjectFieldsPass());
diff --git a/test/CAPI/firtool.c b/test/CAPI/firtool.c
index 7bd47cb5105a..462b55c76936 100644
--- a/test/CAPI/firtool.c
+++ b/test/CAPI/firtool.c
@@ -50,11 +50,11 @@ void exportVerilog(MlirContext ctx, bool disableOptimization) {
       circtFirtoolPopulatePreprocessTransforms(pm, options);
   assert(mlirLogicalResultIsSuccess(result));
 
-  result = circtFirtoolPopulateCHIRRTLToLowFIRRTL(
-      pm, options, mlirStringRefCreateFromCString("-"));
+  result = circtFirtoolPopulateCHIRRTLToLowFIRRTL(pm, options);
   assert(mlirLogicalResultIsSuccess(result));
 
-  result = circtFirtoolPopulateLowFIRRTLToHW(pm, options);
+  result = circtFirtoolPopulateLowFIRRTLToHW(
+      pm, options, mlirStringRefCreateFromCString("-"));
   assert(mlirLogicalResultIsSuccess(result));
 
   result = circtFirtoolPopulateHWToSV(pm, options);
diff --git a/test/Dialect/FIRRTL/lower-layers-errors.mlir b/test/Dialect/FIRRTL/lower-layers-errors.mlir
index 62448de2e607..4df341379220 100644
--- a/test/Dialect/FIRRTL/lower-layers-errors.mlir
+++ b/test/Dialect/FIRRTL/lower-layers-errors.mlir
@@ -10,23 +10,7 @@ firrtl.circuit "NonPassiveSubaccess" {
     firrtl.layerblock @A {
       %n = firrtl.node %b : !firrtl.uint<1>
       // expected-error @below {{'firrtl.subaccess' op has a non-passive operand and captures a value defined outside its enclosing bind-convention layerblock}}
-      %0 = firrtl.subaccess %a[%b] : !firrtl.vector<bundle<a: uint<1>, b flip: uint<1>>, 2>, !firrtl.uint<1>
+      %0 = firrtl.subaccess %a[%n] : !firrtl.vector<bundle<a: uint<1>, b flip: uint<1>>, 2>, !firrtl.uint<1>
     }
   }
 }
-
-// -----
-
-firrtl.circuit "RWProbeCantMove" {
-  firrtl.layer @A bind { }
-  firrtl.module @RWProbeCantMove() attributes {layers = [@A]} {
-    %z = firrtl.constant 0 : !firrtl.uint<5>
-    // expected-note @below {{rwprobe target outside of bind layer}}
-    %w = firrtl.node sym @sym %z : !firrtl.uint<5>
-    firrtl.layerblock @A {
-      // expected-error @below {{rwprobe capture not supported with bind convention layer}}
-      %rw = firrtl.ref.rwprobe <@RWProbeCantMove::@sym> : !firrtl.rwprobe<uint<5>>
-    }
-  }
-}
-
diff --git a/test/Dialect/FIRRTL/lower-layers.mlir b/test/Dialect/FIRRTL/lower-layers.mlir
index 83086c98f1f3..70b9e1acb2c9 100644
--- a/test/Dialect/FIRRTL/lower-layers.mlir
+++ b/test/Dialect/FIRRTL/lower-layers.mlir
@@ -1,5 +1,6 @@
 // RUN: circt-opt -firrtl-lower-layers -split-input-file %s | FileCheck %s
 
+// CHECK-LABEL: firrtl.circuit "Test"
 firrtl.circuit "Test" {
   firrtl.module @Test() {}
 
@@ -32,10 +33,6 @@ firrtl.circuit "Test" {
   firrtl.module @ColoredThings() {
     // CHECK: %0 = firrtl.wire : !firrtl.probe<bundle<f: uint<1>>>
     %0 = firrtl.wire : !firrtl.probe<bundle<f: uint<1>>, @A>
-    // CHECK: %1 = firrtl.ref.sub %0[0] : !firrtl.probe<bundle<f: uint<1>>>
-    %1 = firrtl.ref.sub %0[0] : !firrtl.probe<bundle<f: uint<1>>, @A>
-    // CHECK-NOT: firrtl.ref.cast
-    %2 = firrtl.ref.cast %1 : (!firrtl.probe<uint<1>, @A>) -> !firrtl.probe<uint<1>, @A::@B>
   }
 
     // CHECK-LABEL: @ColoredThingUnderWhen
@@ -44,10 +41,6 @@ firrtl.circuit "Test" {
     firrtl.when %b : !firrtl.uint<1> {
       // CHECK: %0 = firrtl.wire : !firrtl.probe<bundle<f: uint<1>>>
       %0 = firrtl.wire : !firrtl.probe<bundle<f: uint<1>>, @A>
-      // CHECK: %1 = firrtl.ref.sub %0[0] : !firrtl.probe<bundle<f: uint<1>>>
-      %1 = firrtl.ref.sub %0[0] : !firrtl.probe<bundle<f: uint<1>>, @A>
-      // CHECK-NOT: firrtl.ref.cast
-      %2 = firrtl.ref.cast %1 : (!firrtl.probe<uint<1>, @A>) -> !firrtl.probe<uint<1>, @A::@B>
     }
   }
 
@@ -71,7 +64,7 @@ firrtl.circuit "Test" {
   // CHECK-NOT: firrtl.layer @GoodbyeCruelWorld
   firrtl.layer @GoodbyeCruelWorld bind {}
 
-  // CHECK-LABEL @WithLayerBlock
+  // CHECK-LABEL firrtl.module @WithLayerBlock
   firrtl.module @WithLayerBlock() {
     // CHECK-NOT firrtl.layerblock @GoodbyeCruelWorld
     firrtl.layerblock @GoodbyeCruelWorld {
@@ -82,16 +75,20 @@ firrtl.circuit "Test" {
   // Capture
   //===--------------------------------------------------------------------===//
 
-  // CHECK: firrtl.module private @[[A:.+]](in %[[x:.+]]: !firrtl.uint<1>, in %[[y:.+]]: !firrtl.uint<1>)
-  // CHECK:   %0 = firrtl.add %[[x]], %[[y]] : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<2>
-  // CHECK: }
-  // CHECK: firrtl.module @CaptureHardware() {
-  // CHECK:   %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
-  // CHECK:   %c1_ui1 = firrtl.constant 1 : !firrtl.uint<1>
-  // CHECK:   %[[p:.+]], %[[q:.+]] = firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]
-  // CHECK:   firrtl.matchingconnect %[[q]], %c1_ui1 : !firrtl.uint<1>
-  // CHECK:   firrtl.matchingconnect %[[p]], %c0_ui1 : !firrtl.uint<1>
-  // CHECK: }
+  // CHECK:      hw.hierpath private @[[CaptureHardware_c0_ui1_path:.+]] [@CaptureHardware::@[[CaptureHardware_c0_ui1_sym:.+]]]
+  // CHECK-NEXT: hw.hierpath private @[[CaptureHardware_c1_ui1_path:.+]] [@CaptureHardware::@[[CaptureHardware_c1_ui1_sym:.+]]]
+  // CHECK-NEXT: firrtl.module private @CaptureHardware_A() {
+  // CHECK-NEXT:   %[[c1_ui1:.+]] = firrtl.xmr.deref @[[CaptureHardware_c1_ui1_path]] : !firrtl.uint<1>
+  // CHECK-NEXT:   %[[c0_ui1:.+]] = firrtl.xmr.deref @[[CaptureHardware_c0_ui1_path]] : !firrtl.uint<1>
+  // CHECK-NEXT:   firrtl.add %[[c0_ui1]], %[[c1_ui1]] : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<2>
+  // CHECK-NEXT: }
+  // CHECK-NEXT: firrtl.module @CaptureHardware() {
+  // CHECK-NEXT:   %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
+  // CHECK-NEXT:   %[[c0_ui1_node:.+]] = firrtl.node sym @[[CaptureHardware_c0_ui1_sym]] %c0_ui1
+  // CHECK-NEXT:   %c1_ui1 = firrtl.constant 1 : !firrtl.uint<1>
+  // CHECK-NEXT:   %[[c1_ui1_node:.+]] = firrtl.node sym @[[CaptureHardware_c1_ui1_sym]] %c1_ui1
+  // CHECK-NEXT:   firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @CaptureHardware_A()
+  // CHECK-NEXT: }
   firrtl.module @CaptureHardware() {
     %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
     %c1_ui1 = firrtl.constant 1 : !firrtl.uint<1>
@@ -100,12 +97,13 @@ firrtl.circuit "Test" {
     }
   }
 
-  // CHECK: firrtl.module private @[[A:.+]](in %[[p:.+]]: !firrtl.uint<1>) {
-  // CHECK:   %x = firrtl.node %[[p]] : !firrtl.uint<1>
+  // CHECK: hw.hierpath private @[[CapturePort_port_path:.+]] [@CapturePort::@[[CapturePort_port_sym:.+]]]
+  // CHECK: firrtl.module private @CapturePort_A() {
+  // CHECK:   %[[port:.+]] = firrtl.xmr.deref @[[CapturePort_port_path]] : !firrtl.uint<1>
+  // CHECK:   %x = firrtl.node %[[port]] : !firrtl.uint<1>
   // CHECK: }
-  // CHECK: firrtl.module @CapturePort(in %in: !firrtl.uint<1>) {
-  // CHECK:   %[[p:.+]] = firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]
-  // CHECK:   firrtl.matchingconnect %[[p]], %in : !firrtl.uint<1>
+  // CHECK: firrtl.module @CapturePort(in %in: !firrtl.uint<1> sym @[[CapturePort_port_sym]]) {
+  // CHECK:   firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @CapturePort_A
   // CHECK: }
   firrtl.module @CapturePort(in %in: !firrtl.uint<1>){
     firrtl.layerblock @A {
@@ -113,90 +111,82 @@ firrtl.circuit "Test" {
     }
   }
 
-  // CHECK: firrtl.module private @[[A:.+]](in %[[p:.+]]: !firrtl.uint<1>)
-  // CHECK:   %w = firrtl.wire : !firrtl.uint<1>
-  // CHECK:   firrtl.connect %w, %[[p]] : !firrtl.uint<1>
-  // CHECK: }
-  // CHECK: firrtl.module @CaptureHardwareViaConnect() {
-  // CHECK:   %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
-  // CHECK:   %[[p:.+]] = firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]
-  // CHECK:   firrtl.matchingconnect %[[p]], %c0_ui1 : !firrtl.uint<1>
-  // CHECK: }
-  firrtl.module @CaptureHardwareViaConnect() {
-    %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
+  // CHECK:      hw.hierpath private @[[CaptureConnect_a_path:.+]] [@CaptureConnect::@[[CaptureConnect_a_sym:.+]]]
+  // CHECK-NEXT: firrtl.module private @CaptureConnect_A()
+  // CHECK-NEXT:   %[[a:.+]] = firrtl.xmr.deref @[[CaptureConnect_a_path]] : !firrtl.uint<1>
+  // CHECK-NEXT:   %b = firrtl.wire : !firrtl.uint<1>
+  // CHECK-NEXT:   firrtl.connect %b, %[[a]] : !firrtl.uint<1>
+  // CHECK-NEXT: }
+  // CHECK-NEXT: firrtl.module @CaptureConnect() {
+  // CHECK-NEXT:   %a = firrtl.wire : !firrtl.uint<1>
+  // CHECK-NEXT:   %_layer_probe = firrtl.node sym @[[CaptureConnect_a_sym]] %a
+  // CHECK-NEXT:   firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @CaptureConnect_A()
+  // CHECK-NEXT: }
+  firrtl.module @CaptureConnect() {
+    %a = firrtl.wire : !firrtl.uint<1>
     firrtl.layerblock @A {
-      %w = firrtl.wire : !firrtl.uint<1>
-      firrtl.connect %w, %c0_ui1 : !firrtl.uint<1>, !firrtl.uint<1>
+      %b = firrtl.wire : !firrtl.uint<1>
+      firrtl.connect %b, %a : !firrtl.uint<1>, !firrtl.uint<1>
     }
   }
 
-  // CHECK: firrtl.module private @[[A:.+]](in %[[p:.+]]: !firrtl.uint<1>)
-  // CHECK:   %0 = firrtl.ref.send %[[p]] : !firrtl.uint<1>
-  // CHECK:   %1 = firrtl.ref.resolve %0 : !firrtl.probe<uint<1>>
-  // CHECK: }
-  // CHECK: firrtl.module @CaptureProbeSrc() {
-  // CHECK:   %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
-  // CHECK:   %w = firrtl.wire : !firrtl.uint<1>
-  // CHECK:   %0 = firrtl.ref.send %w : !firrtl.uint<1>
-  // CHECK:   %[[p:.+]] = firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]
-  // CHECK:   %1 = firrtl.ref.resolve %0 : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.matchingconnect %[[p]], %1 : !firrtl.uint<1>
-  // CHECK: }
+  // CHECK:      firrtl.module private @CaptureProbeSrc_A()
+  // CHECK-NEXT:   %0 = firrtl.xmr.deref @xmrPath : !firrtl.uint<1>
+  // CHECK-NEXT: }
+  // CHECK-NEXT: firrtl.module @CaptureProbeSrc() {
+  // CHECK-NEXT:   %w = firrtl.wire : !firrtl.uint<1>
+  // CHECK-NEXT:   %w_probe = firrtl.node sym @sym interesting_name %w : !firrtl.uint<1>
+  // CHECK-NEXT:   firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @CaptureProbeSrc_A
+  // CHECK-NEXT: }
   firrtl.module @CaptureProbeSrc() {
-    %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
     %w = firrtl.wire : !firrtl.uint<1>
-    %r = firrtl.ref.send %w : !firrtl.uint<1>
+    %w_probe = firrtl.node sym @sym interesting_name %w : !firrtl.uint<1>
     firrtl.layerblock @A {
-      firrtl.ref.resolve %r : !firrtl.probe<uint<1>>
+      %0 = firrtl.xmr.deref @xmrPath : !firrtl.uint<1>
     }
   }
 
-  // CHECK: firrtl.module private @[[B:.+]](in %[[p:.+]]: !firrtl.uint<1>, in %[[q:.+]]: !firrtl.uint<1>)
-  // CHECK:   %0 = firrtl.add %[[p]], %[[q]] : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<2>
-  // CHECK: }
-  // CHECK: firrtl.module private @[[A:.+]](out %[[p:.+]]: !firrtl.probe<uint<1>>, out %[[q:.+]]: !firrtl.probe<uint<1>>) attributes {
-  // CHECK:   %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
-  // CHECK:   %0 = firrtl.ref.send %c0_ui1 : !firrtl.uint<1>
-  // CHECK:   firrtl.ref.define %[[q]], %0 : !firrtl.probe<uint<1>>
-  // CHECK:   %c0_ui1_1 = firrtl.constant 0 : !firrtl.uint<1>
-  // CHECK:   %1 = firrtl.ref.send %c0_ui1_1 : !firrtl.uint<1>
-  // CHECK:   firrtl.ref.define %[[p]], %1 : !firrtl.probe<uint<1>>
-  // CHECK: }
-  // CHECK: firrtl.module @NestedCaptureHardware() {
-  // CHECK:   %[[b1:.+]], %[[b2:.+]] = firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A-B.sv", excludeFromFileList>} @[[B]]
-  // CHECK:   %[[a1:.+]], %[[a2:.+]] = firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]
-  // CHECK:   %0 = firrtl.ref.resolve %[[a2]] : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.matchingconnect %[[b1]], %0 : !firrtl.uint<1>
-  // CHECK:   %1 = firrtl.ref.resolve %[[a1]] : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.matchingconnect %[[b2]], %1 : !firrtl.uint<1>
-  // CHECK: }
-  firrtl.module @NestedCaptureHardware() {
+  // CHECK:      hw.hierpath private @[[NestedCapture_x_path:.+]] [@NestedCapture::@[[inst:.+]], @NestedCapture_A::@[[NestedCapture_a_sym:.+]]]
+  // CHECK-NEXT: firrtl.module private @NestedCapture_A_B()
+  // CHECK-NEXT:   %0 = firrtl.xmr.deref @[[NestedCapture_x_path]]
+  // CHECK-NEXT:   %1 = firrtl.node %0 : !firrtl.uint<1>
+  // CHECK-NEXT: }
+  // CHECK-NEXT: firrtl.module private @NestedCapture_A() {
+  // CHECK-NEXT:   %x = firrtl.wire : !firrtl.uint<1>
+  // CHECK-NEXT:   %_layer_probe = firrtl.node sym @[[NestedCapture_a_sym]] %x
+  // CHECK-NEXT: }
+  // CHECK-NEXT: firrtl.module @NestedCapture() {
+  // CHECK-NEXT:   firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A-B.sv", excludeFromFileList>} @NestedCapture_A_B()
+  // CHECK-NEXT:   firrtl.instance {{.+}} sym @[[inst]] {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @NestedCapture_A()
+  // CHECK-NEXT: }
+  firrtl.module @NestedCapture() {
     firrtl.layerblock @A {
-      %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
-      %c1_ui1 = firrtl.constant 0 : !firrtl.uint<1>
+      %x = firrtl.wire : !firrtl.uint<1>
       firrtl.layerblock @A::@B {
-        %0 = firrtl.add %c0_ui1, %c1_ui1 : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<2>
+        %0 = firrtl.node %x : !firrtl.uint<1>
       }
     }
   }
 
-  // CHECK: firrtl.module private @[[A:.+]](in %[[p:.+]]: !firrtl.uint<1>)
-  // CHECK:   %c1_ui1 = firrtl.constant 1 : !firrtl.uint<1>
-  // CHECK:   firrtl.when %[[p]] : !firrtl.uint<1> {
-  // CHECK:     %0 = firrtl.add %[[p]], %c1_ui1 : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<2>
-  // CHECK:   }
-  // CHECK: }
-  // CHECK: firrtl.module @WhenUnderLayer() {
-  // CHECK:   %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
-  // CHECK:   %[[p:.+]] = firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]
-  // CHECK:   firrtl.matchingconnect %[[p]], %c0_ui1 : !firrtl.uint<1>
-  // CHECK: }
+  // CHECK:      hw.hierpath private @[[WhenUnderLayer_x_path:.+]] [@WhenUnderLayer::@[[WhenUnderLayer_x_sym:.+]]]
+  // CHECK-NEXT: firrtl.module private @WhenUnderLayer_A()
+  // CHECK-NEXT:   %0 = firrtl.xmr.deref @[[WhenUnderLayer_x_path]] : !firrtl.uint<1>
+  // CHECK-NEXT:   %c1_ui1 = firrtl.constant 1 : !firrtl.uint<1>
+  // CHECK-NEXT:   firrtl.when %0 : !firrtl.uint<1> {
+  // CHECK-NEXT:     %1 = firrtl.add %0, %c1_ui1 : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<2>
+  // CHECK-NEXT:   }
+  // CHECK-NEXT: }
+  // CHECK-NEXT: firrtl.module @WhenUnderLayer() {
+  // CHECK-NEXT:   %x = firrtl.wire : !firrtl.uint<1>
+  // CHECK-NEXT:   %_layer_probe = firrtl.node sym @[[WhenUnderLayer_x_sym]] %x
+  // CHECK-NEXT:   firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @WhenUnderLayer_A
+  // CHECK-NEXT: }
   firrtl.module @WhenUnderLayer() {
-    %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
+    %x = firrtl.wire : !firrtl.uint<1>
     firrtl.layerblock @A {
       %c1_ui1 = firrtl.constant 1 : !firrtl.uint<1>
-      firrtl.when %c0_ui1 : !firrtl.uint<1> {
-        %0 = firrtl.add %c0_ui1, %c1_ui1 : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<2>
+      firrtl.when %x : !firrtl.uint<1> {
+        %0 = firrtl.add %x, %c1_ui1 : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<2>
       }
     }
   }
@@ -204,14 +194,17 @@ firrtl.circuit "Test" {
   // Test that subfield, subindex, and subaccess are moved out of layerblocks to
   // avoid capturing non-passive types.
   //
-  // CHECK:      firrtl.module private @[[SubOpsInLayerBlock_A:[A-Za-z0-9_]+]]
-  // CHECK-SAME:   in %[[port:[A-Za-z0-9_]+]]: !firrtl.uint<1>
-  // CHECK-NEXT:   firrtl.node %[[port]]
+  // CHECK:      hw.hierpath private @[[SubOpsInLayerBlock_sub_path:.+]] [@SubOpsInLayerBlock::@[[SubOpsInLayerBlock_sub_sym:.+]]]
+  // CHECK-NEXT: firrtl.module private @SubOpsInLayerBlock_A() {
+  // CHECK-NEXT:   %0 = firrtl.xmr.deref @[[SubOpsInLayerBlock_sub_path]]
+  // CHECK-NEXT:   firrtl.node %0
   // CHECK-NEXT: }
-  // CHECK:      firrtl.module @SubOpsInLayerBlock
-  // CHECK-NEXT:   firrtl.subaccess
-  // CHECK-NEXT:   firrtl.subindex
-  // CHECK-NEXT:   firrtl.subfield
+  // CHECK:      firrtl.module @SubOpsInLayerBlock(
+  // CHECK-NEXT:   %0 = firrtl.subaccess %a[%b]
+  // CHECK-NEXT:   %1 = firrtl.subindex %0[0]
+  // CHECK-NEXT:   %2 = firrtl.subfield %1[a]
+  // CHECK-NEXT:   %3 = firrtl.node %2 :
+  // CHECK-NEXT:   %_layer_probe = firrtl.node sym @[[SubOpsInLayerBlock_sub_sym]] %3
   firrtl.module @SubOpsInLayerBlock(
     in %a: !firrtl.vector<vector<bundle<a: uint<1>, b flip: uint<2>>, 2>, 2>,
     in %b: !firrtl.uint<1>
@@ -221,19 +214,23 @@ firrtl.circuit "Test" {
       %1 = firrtl.subindex %0[0] : !firrtl.vector<bundle<a: uint<1>, b flip: uint<2>>, 2>
       %2 = firrtl.subfield %1[a] : !firrtl.bundle<a: uint<1>, b flip: uint<2>>
       %3 = firrtl.node %2 : !firrtl.uint<1>
+      %4 = firrtl.node %3 : !firrtl.uint<1>
     }
   }
 
-  // CHECK:      firrtl.module private @CaptureInWhen_A(
-  // CHECK-SAME:   in %a: !firrtl.uint<1>
-  // CHECK-SAME:   in %cond: !firrtl.uint<1>
-  // CHECK-SAME: )
+  // CHECK:      hw.hierpath private @[[CaptureWhen2_a_path:.+]] [@CaptureWhen2::@[[CaptureWhen2_a_sym:.+]]]
+  // CHECK-NEXT: hw.hierpath private @[[CaptureWhen2_cond_path:.+]] [@CaptureWhen2::@[[CaptureWhen2_cond_sym:.+]]]
+  // CHECK-NEXT: firrtl.module private @CaptureWhen2_A() {
+  // CHECK-NEXT:   %0 = firrtl.xmr.deref @[[CaptureWhen2_cond_path]]
+  // CHECK-NEXT:   %1 = firrtl.xmr.deref @[[CaptureWhen2_a_path]]
+  // CHECK-NEXT:   firrtl.when %0 {{.*}} {
+  // CHECK-NEXT:     %b = firrtl.node %1
 
-  // CHECK:      firrtl.module @CaptureInWhen(
-  // CHECK:        %a_a, %a_cond = firrtl.instance a
-  // CHECK-NEXT:   firrtl.matchingconnect %a_cond, %cond :
-  // CHECK-NEXT:   firrtl.matchingconnect %a_a, %a :
-  firrtl.module @CaptureInWhen(in %cond: !firrtl.uint<1>) {
+  // CHECK:      firrtl.module @CaptureWhen2(
+  // CHECK-NEXT:   %a = firrtl.wire
+  // CHECK-NEXT:   %_layer_probe = firrtl.node {{.*}} %a
+  // CHECK-NEXT:   firrtl.instance a
+  firrtl.module @CaptureWhen2(in %cond: !firrtl.uint<1>) {
     %a = firrtl.wire : !firrtl.uint<1>
     firrtl.layerblock @A {
       firrtl.when %cond : !firrtl.uint<1> {
@@ -242,85 +239,57 @@ firrtl.circuit "Test" {
     }
   }
 
-  //===--------------------------------------------------------------------===//
-  // Connecting/Defining Refs
-  //===--------------------------------------------------------------------===//
-
-  // Src and Dst Outside Layerblock.
+  // Capture of a zero-width value creates a local zero-width constant zero.
   //
-  // CHECK: firrtl.module private @[[A:.+]]() {
-  // CHECK: }
-  // CHECK: firrtl.module @SrcDstOutside() {
-  // CHECK:   %0 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   %1 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.ref.define %1, %0 : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A:.+]]()
-  // CHECK: }
-  firrtl.module @SrcDstOutside() {
-    %0 = firrtl.wire : !firrtl.probe<uint<1>, @A>
-    %1 = firrtl.wire : !firrtl.probe<uint<1>, @A>
+  // CHECK:      firrtl.module private @ZeroWidthCapture_A() {
+  // CHECK-NEXT:   %c0_ui0 = firrtl.constant 0 : !firrtl.uint<0>
+  // CHECK-NEXT:   %b = firrtl.node %c0_ui0 : !firrtl.uint<0>
+  // CHECK-NEXT: }
+  // CHECK:      firrtl.module @ZeroWidthCapture() {
+  // CHECK-NEXT:   %a = firrtl.wire : !firrtl.uint<0>
+  // CHECK-NEXT:   firrtl.instance a
+  firrtl.module @ZeroWidthCapture() {
+    %a = firrtl.wire : !firrtl.uint<0>
     firrtl.layerblock @A {
-      firrtl.ref.define %1, %0 : !firrtl.probe<uint<1>, @A>
+      %b = firrtl.node %a : !firrtl.uint<0>
     }
   }
 
-  // Src Outside Layerblock.
+  // Port capture needs to create a node.
   //
-  // CHECK: firrtl.module private @[[A:.+]](in %[[p:.+]]: !firrtl.uint<1>)
-  // CHECK:   %0 = firrtl.ref.send %[[p]] : !firrtl.uint<1>
-  // CHECK:   %1 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.ref.define %1, %0 : !firrtl.probe<uint<1>>
-  // CHECK: }
-  // CHECK: firrtl.module @SrcOutside() {
-  // CHECK:   %0 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   %[[p:.+]] = firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]
-  // CHECK:   %1 = firrtl.ref.resolve %0 : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.matchingconnect %[[p]], %1 : !firrtl.uint<1>
-  // CHECK: }
-  firrtl.module @SrcOutside() {
-    %0 = firrtl.wire : !firrtl.probe<uint<1>, @A>
-    firrtl.layerblock @A {
-      %1 = firrtl.wire : !firrtl.probe<uint<1>, @A>
-      firrtl.ref.define %1, %0 : !firrtl.probe<uint<1>, @A>
-    }
-  }
-
-  // Dst Outside Layerblock.
+  // CHECK:      hw.hierpath private @[[InstancePortCapture_ext_a_path:.+]] [@InstancePortCapture::@[[InstancePortCapture_ext_a_sym:.+]]]
+  // CHECK-NEXT: hw.hierpath private @[[InstancePortCapture_ext_b_path:.+]] [@InstancePortCapture::@[[InstancePortCapture_ext_b_sym:.+]]]
+  // CHECK-NEXT: firrtl.module private @InstancePortCapture_A() {
+  // CHECK-NEXT:   %0 = firrtl.xmr.deref @[[InstancePortCapture_ext_b_path]]
+  // CHECK-NEXT:   %1 = firrtl.xmr.deref @[[InstancePortCapture_ext_a_path]]
+  // CHECK-NEXT:   %a = firrtl.node %1
+  // CHECK-NEXT:   %b = firrtl.node %0
+  // CHECK-NEXT: }
   //
-  // CHECK: firrtl.module private @[[A:.+]](out %[[p:.+]]: !firrtl.probe<uint<1>>)
-  // CHECK:   %0 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.ref.define %[[p]], %0 : !firrtl.probe<uint<1>>
-  // CHECK: }
-  // CHECK: firrtl.module @DestOutside() {
-  // CHECK:   %0 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   %[[p:.+]] = firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]
-  // CHECK:   firrtl.ref.define %0, %[[p]] : !firrtl.probe<uint<1>>
-  // CHECK: }
-  firrtl.module @DestOutside() {
-    %0 = firrtl.wire : !firrtl.probe<uint<1>, @A>
+  // CHECK:      firrtl.module @InstancePortCapture() {
+  // CHECK-NEXT:   %ext_a, %ext_b = firrtl.instance ext @InstancePortCapture_ext
+  // CHECK-NEXT:   %_layer_probe = firrtl.node sym @[[InstancePortCapture_ext_b_sym]] %ext_b
+  // CHECK-NEXT:   %_layer_probe_0 = firrtl.node sym @[[InstancePortCapture_ext_a_sym]] %ext_a
+  // CHECK-NEXT:   firrtl.instance {{.*}}
+  // CHECK-NEXT: }
+  firrtl.extmodule @InstancePortCapture_ext(
+    in a: !firrtl.uint<1>,
+    out b: !firrtl.uint<1>
+  )
+  firrtl.module @InstancePortCapture() {
+    %ext_a, %ext_b = firrtl.instance ext @InstancePortCapture_ext(
+      in a: !firrtl.uint<1>,
+      out b: !firrtl.uint<1>
+    )
     firrtl.layerblock @A {
-      %1 = firrtl.wire : !firrtl.probe<uint<1>, @A>
-      firrtl.ref.define %0, %1 : !firrtl.probe<uint<1>, @A>
+      %a = firrtl.node %ext_a : !firrtl.uint<1>
+      %b = firrtl.node %ext_b : !firrtl.uint<1>
     }
   }
 
-  // Src and Dst Inside Layerblock.
-  //
-  // CHECK: firrtl.module private @[[A:.+]]() {
-  // CHECK:   %0 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   %1 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.ref.define %1, %0 : !firrtl.probe<uint<1>>
-  // CHECK: }
-  // CHECK: firrtl.module @SrcDstInside() {
-  // CHECK:   firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]()
-  // CHECK: }
-  firrtl.module @SrcDstInside() {
-    firrtl.layerblock @A {
-      %0 = firrtl.wire : !firrtl.probe<uint<1>, @A>
-      %1 = firrtl.wire : !firrtl.probe<uint<1>, @A>
-      firrtl.ref.define %1, %0 : !firrtl.probe<uint<1>, @A>
-    }
-  }
+  //===--------------------------------------------------------------------===//
+  // Cloning of special operations
+  //===--------------------------------------------------------------------===//
 
   // An FString operation is outside the layer block.  This needs to be cloned.
   //
@@ -338,76 +307,23 @@ firrtl.circuit "Test" {
     }
   }
 
-  //===--------------------------------------------------------------------===//
-  // Resolving Colored Probes
-  //===--------------------------------------------------------------------===//
-
-  // CHECK: firrtl.module private @[[A:.+]](in %[[p:.+]]: !firrtl.uint<1>) {
-  // CHECK:   %0 = firrtl.ref.send %[[p]] : !firrtl.uint<1>
-  // CHECK:   %1 = firrtl.ref.resolve %0 : !firrtl.probe<uint<1>>
-  // CHECK: }
-  // CHECK: firrtl.module @ResolveColoredRefUnderLayerBlock() {
-  // CHECK:   %w = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   %[[p:.+]] = firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]
-  // CHECK:   %0 = firrtl.ref.resolve %w : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.matchingconnect %[[p]], %0 : !firrtl.uint<1>
-  // CHECK: }
-  firrtl.module @ResolveColoredRefUnderLayerBlock() {
-    %w = firrtl.wire : !firrtl.probe<uint<1>, @A>
-    firrtl.layerblock @A {
-      %0 = firrtl.ref.resolve %w : !firrtl.probe<uint<1>, @A>
-    }
-  }
-
-  // CHECK: firrtl.module @ResolveColoredRefUnderEnabledLayer() {
-  // CHECK:   %w = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   %0 = firrtl.ref.resolve %w : !firrtl.probe<uint<1>>
-  // CHECK: }
-  firrtl.module @ResolveColoredRefUnderEnabledLayer() attributes {layers=[@A]} {
-    %w = firrtl.wire : !firrtl.probe<uint<1>, @A>
-    %0 = firrtl.ref.resolve %w : !firrtl.probe<uint<1>, @A>
-  }
-
-  // CHECK: firrtl.module private @[[A:.+]](in %[[p:.+]]: !firrtl.uint<1>) {
-  // CHECK:   %0 = firrtl.ref.send %[[p]] : !firrtl.uint<1>
-  // CHECK:   %1 = firrtl.ref.resolve %0 : !firrtl.probe<uint<1>>
-  // CHECK: }
-  // CHECK: firrtl.module @ResolveColoredRefPortUnderLayerBlock1() {
-  // CHECK:   %foo_o = firrtl.instance foo @Foo(out o: !firrtl.probe<uint<1>>)
-  // CHECK:   %[[p:.+]] = firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]
-  // CHECK:   %0 = firrtl.ref.resolve %foo_o : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.matchingconnect %[[p]], %0 : !firrtl.uint<1>
-  // CHECK: }
-  firrtl.module @ResolveColoredRefPortUnderLayerBlock1() {
-    %foo_o = firrtl.instance foo @Foo(out o : !firrtl.probe<uint<1>, @A>)
-    firrtl.layerblock @A {
-      %x = firrtl.ref.resolve %foo_o : !firrtl.probe<uint<1>, @A>
-    }
-  }
-
-  // CHECK: firrtl.module private @[[A:.+]]() {
-  // CHECK:   %foo_o = firrtl.instance foo @Foo(out o: !firrtl.probe<uint<1>>)
-  // CHECK:   %0 = firrtl.ref.resolve %foo_o : !firrtl.probe<uint<1>>
-  // CHECK: }
-  // CHECK: firrtl.module @ResolveColoredRefPortUnderLayerBlock2() {
-  // CHECK:   firrtl.instance {{.+}} {lowerToBind, output_file = #hw.output_file<"layers-Test-A.sv", excludeFromFileList>} @[[A]]()
-  // CHECK: }
-  firrtl.module @ResolveColoredRefPortUnderLayerBlock2() {
+  // XMR Ref ops used by force_initial are cloned.
+  //
+  // CHECK:      firrtl.module private @XmrRef_A()
+  // CHECK-NEXT:   %0 = firrtl.xmr.ref @RefXmrRef_path : !firrtl.rwprobe<uint<1>, @A>
+  // CHECK-NEXT:   %a = firrtl.wire
+  // CHECK-NEXT:   %c1_ui1 = firrtl.constant 1
+  // CHECK-NEXT:   firrtl.ref.force_initial %c1_ui1, %0, %c1_ui1
+  hw.hierpath private @XmrRef_path [@XmrRef::@a]
+  firrtl.module @XmrRef() {
+    %0 = firrtl.xmr.ref @RefXmrRef_path : !firrtl.rwprobe<uint<1>, @A>
     firrtl.layerblock @A {
-      %foo_o = firrtl.instance foo @Foo(out o : !firrtl.probe<uint<1>, @A>)
-      %x = firrtl.ref.resolve %foo_o : !firrtl.probe<uint<1>, @A>
+      %a = firrtl.wire sym @a : !firrtl.uint<1>
+      %c1_ui1 = firrtl.constant 1 : !firrtl.const.uint<1>
+      firrtl.ref.force_initial %c1_ui1, %0, %c1_ui1 : !firrtl.const.uint<1>, !firrtl.rwprobe<uint<1>, @A>, !firrtl.const.uint<1>
     }
   }
 
-  // CHECK: firrtl.module @ResolveColoredRefPortUnderEnabledLayer() {
-  // CHECK:   %foo_o = firrtl.instance foo @Foo(out o: !firrtl.probe<uint<1>>)
-  // CHECK:   %0 = firrtl.ref.resolve %foo_o : !firrtl.probe<uint<1>>
-  // CHECK: }
-  firrtl.module @ResolveColoredRefPortUnderEnabledLayer() attributes {layers=[@A]} {
-    %foo_o = firrtl.instance foo @Foo(out o : !firrtl.probe<uint<1>, @A>)
-    %x = firrtl.ref.resolve %foo_o : !firrtl.probe<uint<1>, @A>
-  }
-
   //===--------------------------------------------------------------------===//
   // Inline Layers
   //===--------------------------------------------------------------------===//
@@ -497,55 +413,42 @@ firrtl.circuit "Simple" {
 // CHECK-SAME:   define layers_Simple_A"
 // CHECK-SAME:   output_file = #hw.output_file<"layers-Simple-A.sv", excludeFromFileList>
 //
-// CHECK:      firrtl.module private @Simple_A_B_C(
-// CHECK-NOT:  firrtl.module
-// CHECK-SAME:   in %[[cc_port:[_a-zA-Z0-9]+]]: !firrtl.uint<3>
-// CHECK-NEXT:   %ccc = firrtl.node %[[cc_port]]
+// CHECK: hw.hierpath private @[[Simple_A_B_cc_path:.+]] [@Simple::@a_b, @Simple_A_B::@[[Simple_A_B_cc_sym:.+]]]
+//
+// CHECK:      firrtl.module private @Simple_A_B_C() {
+// CHECK-NEXT:   %0 = firrtl.xmr.deref @[[Simple_A_B_cc_path]]
+// CHECK-NEXT:   %ccc = firrtl.node %0
 // CHECK-NEXT: }
 //
-// CHECK:      firrtl.module private @Simple_A_B(
-// CHECK-NOT:  firrtl.module
-// CHECK-SAME:   in %[[b_port:[_a-zA-Z0-9]+]]: !firrtl.uint<2>
-// CHECK-SAME:   in %[[c_port:[_a-zA-Z0-9]+]]: !firrtl.uint<3>
-// CHECK-SAME:   out %[[cc_port:[_a-zA-Z0-9_]+]]: !firrtl.probe<uint<3>>
-// CHECK-NEXT:   %bb = firrtl.node %[[b_port]]
-// CHECK-NEXT:   %cc = firrtl.node %[[c_port]]
-// CHECK-NEXT:   %0 = firrtl.ref.send %cc
-// CHECK-NEXT:   firrtl.ref.define %[[cc_port]], %0
+// CHECK:      hw.hierpath private @[[Simple_b_path:.+]] [@Simple::@[[Simple_b_sym:.+]]]
+// CHECK-NEXT: hw.hierpath private @[[Simple_A_c_path:.+]] [@Simple::@a, @Simple_A::@[[Simple_A_c_sym:.+]]]
+//
+// CHECK:      firrtl.module private @Simple_A_B() {
+// CHECK-NEXT:   %0 = firrtl.xmr.deref @[[Simple_A_c_path]]
+// CHECK-NEXT:   %1 = firrtl.xmr.deref @[[Simple_b_path]]
+// CHECK-NEXT:   %bb = firrtl.node %1
+// CHECK-NEXT:   %cc = firrtl.node %0
+// CHECK-NEXT:   %_layer_probe = firrtl.node sym @[[Simple_A_B_cc_sym]] %cc
 // CHECK-NEXT: }
 //
-// CHECK:      firrtl.module private @Simple_A(
-// CHECK-NOT:  firrtl.module
-// CHECK-SAME:   in %[[a_port:[_a-zA-Z0-9]+]]: !firrtl.uint<1>
-// CHECK-SAME:   out %[[c_port:[_a-zA-Z0-9_]+]]: !firrtl.probe<uint<3>>
-// CHECK-NEXT:   %aa = firrtl.node %[[a_port]]
-// CHECK:        %[[c_ref:[_a-zA-Z0-9]+]] = firrtl.ref.send %c
-// CHECK-NEXT:   firrtl.ref.define %[[c_port]], %[[c_ref]]
+// CHECK: hw.hierpath private @[[Simple_a_path:.+]] [@Simple::@[[Simple_a_sym:.+]]]
+//
+// CHECK:      firrtl.module private @Simple_A() {
+// CHECK-NEXT:   %0 = firrtl.xmr.deref @[[Simple_a_path]]
+// CHECK-NEXT:   %aa = firrtl.node %0
+// CHECK-NEXT:   %c = firrtl.wire
+// CHECK-NEXT:   %_layer_probe = firrtl.node sym @[[Simple_A_c_sym]] %c
 // CHECK-NEXT: }
 //
 // CHECK:      firrtl.module @Simple() {
-// CHECK-NOT:  firrtl.module
-// CHECK-NOT:    firrtl.layerblock
-// CHECK:        %[[A_B_C_cc:[_a-zA-Z0-9_]+]] = firrtl.instance a_b_c {
-// CHECK-SAME:     lowerToBind
-// CHECK-SAME:     output_file = #hw.output_file<"layers-Simple-A-B-C.sv"
-// CHECK-SAME:     excludeFromFileList
-// CHECK-SAME:     @Simple_A_B_C(
-// CHECK-NEXT:   %[[A_B_b:[_a-zA-Z0-9_]+]], %[[A_B_c:[_a-zA-Z0-9_]+]], %[[A_B_cc:[_a-zA-Z0-9_]+]] = firrtl.instance a_b {
-// CHECK-SAME:     lowerToBind
-// CHECK-SAME:     output_file = #hw.output_file<"layers-Simple-A-B.sv", excludeFromFileList>
-// CHECK-SAME:     @Simple_A_B(
-// CHECK-NEXT:   %[[A_B_cc_resolve:[_a-zA-Z0-9]+]] = firrtl.ref.resolve %[[A_B_cc]]
-// CHECK-NEXT:   firrtl.matchingconnect %[[A_B_C_cc]], %[[A_B_cc_resolve]]
-// CHECK-NEXT:   firrtl.matchingconnect %[[A_B_b]], %b
-// CHECK-NEXT:   %[[A_a:[_a-zA-Z0-9_]+]], %[[A_c:[_a-zA-Z0-9_]+]] = firrtl.instance a {
-// CHECK-SAME:     lowerToBind
-// CHECK-SAME:     output_file = #hw.output_file<"layers-Simple-A.sv", excludeFromFileList>
-// CHECK-SAME:     @Simple_A(
-// CHECK-NEXT:   %[[A_c_resolve:[_a-zA-Z0-9]+]] = firrtl.ref.resolve %[[A_c]]
-// CHECK-NEXT:   firrtl.matchingconnect %[[A_B_c]], %[[A_c_resolve]]
-// CHECK-NEXT:   firrtl.matchingconnect %[[A_a]], %a
-// CHECK:      }
+// CHECK-NEXT:   %a = firrtl.wire
+// CHECK-NEXT:   %_layer_probe = firrtl.node sym @[[Simple_a_sym]] %a
+// CHECK-NEXT:   %b = firrtl.wire
+// CHECK-NEXT:   %_layer_probe_0 = firrtl.node sym @[[Simple_b_sym]] %b
+// CHECK-NEXT:   firrtl.instance a_b_c {{.*}}
+// CHECK-NEXT:   firrtl.instance a_b {{.*}}
+// CHECK-NEXT:   firrtl.instance a {{.*}}
+// CHECK-NEXT: }
 //
 // CHECK-DAG:  sv.verbatim "`endif // layers_Simple_A"
 // CHECK-SAME:   output_file = #hw.output_file<"layers-Simple-A.sv", excludeFromFileList>
@@ -556,7 +459,7 @@ firrtl.circuit "Simple" {
 
 firrtl.circuit "ModuleNameConflict" {
   firrtl.layer @A bind {}
-  firrtl.module private @ModuleNameConflict_A() {}
+  firrtl.module @ModuleNameConflict_A() {}
   firrtl.module @ModuleNameConflict() {
     %a = firrtl.wire : !firrtl.uint<1>
     firrtl.instance foo @ModuleNameConflict_A()
@@ -568,7 +471,8 @@ firrtl.circuit "ModuleNameConflict" {
 
 // CHECK-LABEL: firrtl.circuit "ModuleNameConflict"
 //
-// CHECK:       firrtl.module private @[[groupModule:[_a-zA-Z0-9_]+]](in
+// CHECK:       firrtl.module @ModuleNameConflict_A()
+// CHECK:       firrtl.module private @[[groupModule:[_a-zA-Z0-9_]+]]()
 //
 // CHECK:       firrtl.module @ModuleNameConflict()
 // CHECK-NOT:   firrtl.module
@@ -587,13 +491,16 @@ firrtl.circuit "CaptureHardwareMultipleTimes" {
 
   firrtl.extmodule @CaptureHardwareMultipleTimes ()
 
-  // CHECK: firrtl.module private @[[A:.+]](in %[[p:.+]]: !firrtl.uint<1>)
-  // CHECK:   %0 = firrtl.add %[[p]], %[[p]] : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<2>
+  // CHECK: hw.hierpath private @[[path:.+]] [@CaptureSrcTwice::@[[sym:.+]]]
+  //
+  // CHECK: firrtl.module private @[[A:.+]]()
+  // CHECK:   %0 = firrtl.xmr.deref @[[path]] : !firrtl.uint<1>
+  // CHECK:   %1 = firrtl.add %0, %0 : (!firrtl.uint<1>, !firrtl.uint<1>) -> !firrtl.uint<2>
   // CHECK: }
   // CHECK: firrtl.module @CaptureSrcTwice() {
   // CHECK:   %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
-  // CHECK:   %[[p:.+]] = firrtl.instance {{.+}} @[[A]]
-  // CHECK:   firrtl.matchingconnect %[[p]], %c0_ui1 : !firrtl.uint<1>
+  // CHECK:   %_layer_probe = firrtl.node sym @[[sym]] %c0_ui1 : !firrtl.uint<1>
+  // CHECK:   firrtl.instance {{.+}} @[[A]]
   // CHECK: }
   firrtl.module @CaptureSrcTwice() {
     %c0_ui1 = firrtl.constant 0 : !firrtl.uint<1>
@@ -602,53 +509,6 @@ firrtl.circuit "CaptureHardwareMultipleTimes" {
     }
   }
 
-  // CHECK: firrtl.module private @[[A:.+]](out %[[dst:.+]]: !firrtl.probe<uint<1>>, in %[[src:.+]]: !firrtl.uint<1>)
-  // CHECK:   %0 = firrtl.ref.send %[[src]] : !firrtl.uint<1>
-  // CHECK:   %w2 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.ref.define %[[dst]], %w2 : !firrtl.probe<uint<1>>
-  // CHECK:   %1 = firrtl.ref.resolve %0 : !firrtl.probe<uint<1>>
-  // CHECK: }
-  // CHECK: firrtl.module @CaptureAsDstThenSrc() {
-  // CHECK:   %w1 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   %[[out:.+]], %[[in:.+]] = firrtl.instance {{.+}} @[[A]](out {{.+}}: !firrtl.probe<uint<1>>, in {{.+}}: !firrtl.uint<1>)
-  // CHECK:   %0 = firrtl.ref.resolve %w1 : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.matchingconnect %[[in]], %0 : !firrtl.uint<1>
-  // CHECK:   firrtl.ref.define %w1, %[[out]] : !firrtl.probe<uint<1>>
-  // CHECK: }
-  firrtl.module @CaptureAsDstThenSrc() {
-    %w1 = firrtl.wire : !firrtl.probe<uint<1>, @A>
-    firrtl.layerblock @A {
-      // capture first as a sink.
-      %w2 = firrtl.wire : !firrtl.probe<uint<1>, @A>
-      firrtl.ref.define %w1, %w2 : !firrtl.probe<uint<1>, @A>
-      // Capture again, as a source.
-      %2 = firrtl.ref.resolve %w1 : !firrtl.probe<uint<1>, @A>
-    }
-  }
-
-  // CHECK: firrtl.module private @[[A:.+]](in %[[src:.+]]: !firrtl.uint<1>, out %[[dst:.+]]: !firrtl.probe<uint<1>>)
-  // CHECK:   %0 = firrtl.ref.send %[[src]] : !firrtl.uint<1>
-  // CHECK:   %1 = firrtl.ref.resolve %0 : !firrtl.probe<uint<1>>
-  // CHECK:   %w2 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.ref.define %[[dst]], %w2 : !firrtl.probe<uint<1>>
-  // CHECK: }
-  // CHECK: firrtl.module @CaptureAsSrcThenDst() {
-  // CHECK:   %w1 = firrtl.wire : !firrtl.probe<uint<1>>
-  // CHECK:   %[[in:.+]], %[[out:.+]] = firrtl.instance {{.+}} @[[A]]
-  // CHECK:   firrtl.ref.define %w1, %[[out]] : !firrtl.probe<uint<1>>
-  // CHECK:   %0 = firrtl.ref.resolve %w1 : !firrtl.probe<uint<1>>
-  // CHECK:   firrtl.matchingconnect %[[in]], %0 : !firrtl.uint<1>
-  // CHECK: }
-  firrtl.module @CaptureAsSrcThenDst() {
-    %w1 = firrtl.wire : !firrtl.probe<uint<1>, @A>
-    firrtl.layerblock @A {
-      // capture first as a source.
-      %2 = firrtl.ref.resolve %w1 : !firrtl.probe<uint<1>, @A>
-      // capture again, as a sink.
-      %w2 = firrtl.wire : !firrtl.probe<uint<1>, @A>
-      firrtl.ref.define %w1, %w2 : !firrtl.probe<uint<1>, @A>
-    }
-  }
 }
 
 // -----
@@ -751,37 +611,6 @@ firrtl.circuit "Foo" {
 
 // -----
 
-// Check rwprobe ops are updated.
-// CHECK-LABEL: circuit "RWTH"
-firrtl.circuit "RWTH" {
-  firrtl.layer @T  bind { }
-  firrtl.module @RWTH() attributes {convention = #firrtl<convention scalarized>, layers = [@T]} {
-    %d_p = firrtl.instance d @DUT(out p: !firrtl.rwprobe<uint<1>, @T>)
-    %one = firrtl.constant 1 : !firrtl.uint<1>
-    firrtl.ref.force_initial %one, %d_p, %one: !firrtl.uint<1>, !firrtl.rwprobe<uint<1>, @T>, !firrtl.uint<1>
-  }
-//      CHECK:    firrtl.module private @DUT_T(out %p: !firrtl.rwprobe<uint<1>>) {
-// CHECK-NEXT:      %w = firrtl.wire sym @[[SYM:.+]] : !firrtl.uint<1>
-// CHECK-NEXT:      %0 = firrtl.ref.rwprobe <@DUT_T::@[[SYM]]> : !firrtl.rwprobe<uint<1>>
-// CHECK-NEXT:      firrtl.ref.define %p, %0 : !firrtl.rwprobe<uint<1>>
-// CHECK-NEXT:    }
-// CHECK-NEXT:    firrtl.module @DUT(out %p: !firrtl.rwprobe<uint<1>>) attributes {convention = #firrtl<convention scalarized>} {
-// CHECK-NEXT:      %t_p = firrtl.instance t sym @t {lowerToBind, output_file = #hw.output_file<"layers-RWTH-T.sv", excludeFromFileList>} @DUT_T(out p: !firrtl.rwprobe<uint<1>>)
-// CHECK-NEXT:      firrtl.ref.define %p, %t_p : !firrtl.rwprobe<uint<1>>
-// CHECK-NEXT:    }
-
-  firrtl.module @DUT(out %p: !firrtl.rwprobe<uint<1>, @T>) attributes {convention = #firrtl<convention scalarized>} {
-    firrtl.layerblock @T {
-      %w = firrtl.wire sym @sym : !firrtl.uint<1>
-      %0 = firrtl.ref.rwprobe <@DUT::@sym> : !firrtl.rwprobe<uint<1>>
-      %1 = firrtl.ref.cast %0 : (!firrtl.rwprobe<uint<1>>) -> !firrtl.rwprobe<uint<1>, @T>
-      firrtl.ref.define %p, %1 : !firrtl.rwprobe<uint<1>, @T>
-    }
-  }
-}
-
-// -----
-
 // Check sv.verbatim inner refs are updated, as occurs with views under layers.
 // CHECK-LABEL: circuit "Verbatim"
 firrtl.circuit "Verbatim" {
diff --git a/test/firtool/layer-merge-across-inlined-submodule.fir b/test/firtool/layer-merge-across-inlined-submodule.fir
index 0b6109c4b517..65ab4d4aca55 100644
--- a/test/firtool/layer-merge-across-inlined-submodule.fir
+++ b/test/firtool/layer-merge-across-inlined-submodule.fir
@@ -9,9 +9,9 @@ FIRRTL version 4.0.0
 ; CHECK-NOT: module Child
 
 ; CHECK: module Top_Verification_Assert
-; CHECK:   assert(p) else $error("before child");
-; CHECK:   assert(p) else $error("in child");
-; CHECK:   assert(p) else $error("after child");
+; CHECK:   assert(Top.p) else $error("before child");
+; CHECK:   assert(Top.p) else $error("in child");
+; CHECK:   assert(Top.p) else $error("after child");
 ; CHECK: endmodule
 
 circuit Top: %[[
@@ -29,7 +29,7 @@ circuit Top: %[[
     layerblock Verification:
       layerblock Assert:
         assert(c, p, e, "in child")
-        
+
   public module Top:
     input p : UInt<1>
     input e : UInt<1>
@@ -38,7 +38,7 @@ circuit Top: %[[
     layerblock Verification:
       layerblock Assert:
         assert(c, p, e, "before child")
-    
+
     inst child of Child
     connect child.p, p
     connect child.e, e
diff --git a/test/firtool/layers.fir b/test/firtool/layers.fir
index d4298fc91097..17bb13a7d967 100644
--- a/test/firtool/layers.fir
+++ b/test/firtool/layers.fir
@@ -37,42 +37,29 @@ circuit Foo: %[[
             node z = x
             assert(clock, cond, enable, "Test")
 
-; CHECK-LABEL: module Foo_A_B(
-; CHECK-NEXT:    input x,
-; CHECK-NEXT:          cond,
-; CHECK-NEXT:          enable,
-; CHECK-NEXT:          clock
-; CHECK-NEXT:  );
-; CHECK:         wire y = x;
-; CHECK:         wire z = x;
-; CHECK:         always @(posedge clock) begin
-; CHECK-NEXT:      if (cond & x & enable)
-; CHECK-NEXT:        assert(cond) else $error("Test");
+; CHECK-LABEL: module Foo_A_B();
+; CHECK:         wire y = Foo.a._layer_probe;
+; CHECK:         wire z = Foo.a._layer_probe;
+; CHECK:         always @(posedge Foo.clock) begin
+; CHECK-NEXT:      if (Foo.cond & Foo.a._layer_probe & Foo.enable)
+; CHECK-NEXT:        assert(Foo.cond) else $error("Test");
 ; CHECK-NEXT:    end // always @(posedge)
 ; CHECK-NEXT:  endmodule
 
-; CHECK-LABEL: module Foo_A(
-; CHECK-NEXT:    input in
-; CHECK:         wire x = in;
-; CHECK-NEXT:    wire x_probe = x;
+; CHECK-LABEL: module Foo_A();
+; CHECK:         wire x = Foo.in;
+; CHECK-NEXT:    wire _layer_probe = x;
 ; CHECK-NEXT:  endmodule
 
 ; CHECK-LABEL: FILE "layers-Foo-A-B.sv"
 ; CHECK:       `include "layers-Foo-A.sv"
 ; CHECK-NEXT:  `ifndef layers_Foo_A_B
 ; CHECK-NEXT:  `define layers_Foo_A_B
-; CHECK-NEXT:  bind Foo Foo_A_B a_b (
-; CHECK-NEXT:    .x (Foo.a.x_probe),
-; CHECK-NEXT:    .cond (cond),
-; CHECK-NEXT:    .enable (enable),
-; CHECK-NEXT:    .clock (clock)
-; CHECK-NEXT:  );
+; CHECK-NEXT:  bind Foo Foo_A_B a_b ();
 ; CHECK-NEXT:  `endif // layers_Foo_A_B
 
 ; CHECK-LABEL: FILE "layers-Foo-A.sv"
 ; CHECK:       `ifndef layers_Foo_A
 ; CHECK-NEXT:  `define layers_Foo_A
-; CHECK-NEXT:   bind Foo Foo_A a (
-; CHECK-NEXT:     .in (in)
-; CHECK-NEXT:   );
+; CHECK-NEXT:   bind Foo Foo_A a ();
 ; CHECK-NEXT:  `endif // layers_Foo_A
diff --git a/test/firtool/lower-layers.fir b/test/firtool/lower-layers.fir
index ffac876a7803..e471d79e726d 100644
--- a/test/firtool/lower-layers.fir
+++ b/test/firtool/lower-layers.fir
@@ -1,4 +1,4 @@
-; RUN: firtool %s -disable-all-randomization -split-input-file | FileCheck %s
+; RUN: firtool %s -disable-all-randomization -split-input-file -advanced-layer-sink -lowering-options=emittedLineLength=1024 | FileCheck %s
 
 ; This is an end-to-end example of a test-bench (Foo) enabling verification,
 ; probing into a device-under-test (Bar), and reading from hardware which is
@@ -83,14 +83,11 @@ circuit TestHarness:
 
   layer Verification, bind:
 
-  ; CHECK: module DUT_Verification(
-  ; CHECK:   input        clock,
-  ; CHECK:   input [31:0] a
-  ; CHECK: );
+  ; CHECK: module DUT_Verification();
   ; CHECK:   reg  [31:0] pc_d;
   ; CHECK:   wire [31:0] pc_d_probe = pc_d;
-  ; CHECK:   always @(posedge clock)
-  ; CHECK:     pc_d <= a;
+  ; CHECK:   always @(posedge DUT.clock)
+  ; CHECK:     pc_d <= DUT.a;
   ; CHECK: endmodule
 
   ; CHECK: module DUT(
@@ -124,15 +121,11 @@ circuit TestHarness:
     layerblock Verification:
       define trace = x
 
-  ; CHECK: module TestHarness_Verification(
-  ; CHECK:   input [31:0] dut_trace,
-  ; CHECK:   input        clock,
-  ; CHECK:                reset
-  ; CHECK: );
+  ; CHECK: module TestHarness_Verification()
   ; CHECK:   `ifndef SYNTHESIS
-  ; CHECK:     always @(posedge clock) begin
-  ; CHECK:       if ((`PRINTF_COND_) & reset)
-  ; CHECK:         $fwrite(`PRINTF_FD_, "The last PC was: %x", dut_trace);
+  ; CHECK:     always @(posedge TestHarness.clock) begin
+  ; CHECK:       if ((`PRINTF_COND_) & TestHarness.reset)
+  ; CHECK:         $fwrite(`PRINTF_FD_, "The last PC was: %x", TestHarness.dut.verification.pc_d_probe);
   ; CHECK:     end // always @(posedge)
   ; CHECK:   `endif // not def SYNTHESIS
   ; CHECK: endmodule
@@ -167,13 +160,95 @@ circuit TestHarness:
 ; CHECK: FILE "layers-TestHarness-Verification.sv"
 ; CHECK: `ifndef layers_TestHarness_Verification
 ; CHECK: `define layers_TestHarness_Verification
-; CHECK: bind DUT DUT_Verification verification (
-; CHECK:   .clock (clock),
-; CHECK:   .a     (a)
-; CHECK: );
-; CHECK: bind TestHarness TestHarness_Verification verification (
-; CHECK:   .dut_trace (TestHarness.dut.verification.pc_d_probe),
-; CHECK:   .clock     (clock),
-; CHECK:   .reset     (reset)
-; CHECK: );
+; CHECK: bind DUT DUT_Verification verification ();
+; CHECK: bind TestHarness TestHarness_Verification verification ();
 ; CHECK: `endif // layers_TestHarness_Verification
+
+; // -----
+
+; This example demonstrates forcing _out_ of a layer into the outer module, a
+; parent layer, or into another module.
+
+FIRRTL version 5.1.0
+circuit Foo: %[[
+  {"class": "firrtl.transforms.DontTouchAnnotation", "target": "~|ForceOutOfLayer>a"},
+  {"class": "firrtl.transforms.DontTouchAnnotation", "target": "~|Submodule>root"},
+  {"class": "firrtl.transforms.DontTouchAnnotation", "target": "~|Submodule>a"},
+  {"class": "firrtl.transforms.DontTouchAnnotation", "target": "~|Submodule>b"}
+]]
+
+  layer A, bind:
+  layer B, inline:
+
+  ; Test that forcing out of a layer into the root module works.
+  ;
+  ; CHECK:      module ForceOutOfLayer_A();
+  ; CHECK:        initial
+  ; CHECK-NEXT:     force ForceOutOfLayer.a = 2'h1;
+  ; CHECK:      endmodule
+  ;
+  ; CHECK:      module ForceOutOfLayer();
+  ; CHECK:        initial
+  ; CHECK-NEXT:     force ForceOutOfLayer.a = 2'h2;
+  ; CHECK:      endmodule
+  module ForceOutOfLayer:
+
+    wire a: UInt<2>
+    connect a, UInt<2>(0)
+
+    wire a_probe: RWProbe<UInt<2>>
+    define a_probe = rwprobe(a)
+
+    layerblock A:
+      force_initial(a_probe, UInt<2>(1))
+
+    layerblock B:
+      force_initial(a_probe, UInt<2>(2))
+
+  ; Test that forcing out of a layer into another works.  Test both forcing into
+  ; the other module and forcing into a layer in the other module.
+  ;
+  ; CHECK:      module ForceIntoSubmodule_A();
+  ; CHECK:        initial
+  ; CHECK-NEXT:     force ForceIntoSubmodule.submodule.root = 2'h1;
+  ; CHECK-NEXT:     force ForceIntoSubmodule.submodule.a.a = 2'h1;
+  ; CHECK:      endmodule
+  ;
+  ; CHECK:      module ForceIntoSubmodule();
+  ; CHECK:        initial
+  ; CHECK-NEXT:     force ForceIntoSubmodule.submodule.root = 2'h2;
+  ; CHECK-NEXT:     force ForceIntoSubmodule.submodule.b = 2'h2;
+  ; CHECK:      endmodule
+  module Submodule:
+    output root_probe: RWProbe<UInt<2>>
+    output a_probe: RWProbe<UInt<2>, A>
+    output b_probe: RWProbe<UInt<2>, B>
+
+    wire root: UInt<2>
+    connect root, UInt<2>(0)
+    define root_probe = rwprobe(root)
+
+    layerblock A:
+      wire a: UInt<2>
+      connect a, UInt<2>(0)
+      define a_probe = rwprobe(a)
+
+    layerblock B:
+      wire b: UInt<2>
+      connect b, UInt<2>(0)
+      define b_probe = rwprobe(b)
+
+  module ForceIntoSubmodule:
+    inst submodule of Submodule
+
+    layerblock A:
+      force_initial(submodule.root_probe, UInt<2>(1))
+      force_initial(submodule.a_probe, UInt<2>(1))
+
+    layerblock B:
+      force_initial(submodule.root_probe, UInt<2>(2))
+      force_initial(submodule.b_probe, UInt<2>(2))
+
+  public module Foo:
+    inst forceOutOfLayer of ForceOutOfLayer
+    inst forceIntoSubmodule of ForceIntoSubmodule
diff --git a/tools/firtool/firtool.cpp b/tools/firtool/firtool.cpp
index afbd43fc3681..b5a2d6a88737 100644
--- a/tools/firtool/firtool.cpp
+++ b/tools/firtool/firtool.cpp
@@ -473,8 +473,7 @@ static LogicalResult processBuffer(
     if (failed(parsePassPipeline(StringRef(highFIRRTLPassPlugin), pm)))
       return failure();
 
-  if (failed(firtool::populateCHIRRTLToLowFIRRTL(pm, firtoolOptions,
-                                                 inputFilename)))
+  if (failed(firtool::populateCHIRRTLToLowFIRRTL(pm, firtoolOptions)))
     return failure();
 
   if (!lowFIRRTLPassPlugin.empty())
@@ -484,7 +483,8 @@ static LogicalResult processBuffer(
   // Lower if we are going to verilog or if lowering was specifically
   // requested.
   if (outputFormat != OutputIRFir) {
-    if (failed(firtool::populateLowFIRRTLToHW(pm, firtoolOptions)))
+    if (failed(
+            firtool::populateLowFIRRTLToHW(pm, firtoolOptions, inputFilename)))
       return failure();
     if (!hwPassPlugin.empty())
       if (failed(parsePassPipeline(StringRef(hwPassPlugin), pm)))


Reviews:
user: login: rwy7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T15:42:19Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T20:32:59Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:03:43Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:03:48Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:03:52Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:03:55Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:03:59Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:16:48Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:17:54Z
user: login: rwy7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T14:59:41Z
user: login: rwy7
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T18:28:32Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T22:12:19Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-19T00:01:33Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-19T00:01:53Z
user: login: seldridge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-19T00:02:11Z
user: login: rwy7
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T13:45:58Z

Review comments:
diff_hunk: @@ -22,6 +22,7 @@ HierPathOp HierPathBuilder::getOrCreatePath(ArrayAttr pathArray,
   assert(pathArray && !pathArray.empty());
   // Return an existing HierPathOp if one exists with the same path.
   auto pathIter = pathCache.find(pathArray);
+
path: lib/Dialect/HW/HierPathBuilder.cpp
user: login: rwy7
body: ```suggestion
```
created_at: 2025-04-16T13:28:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
+      // localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*definingOp)->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // Determine the replacement value for the captured operand.  There are
+    // three cases that can occur:
+    //
+    // 1. Capturing something zero-width.  Create a zero-width constant zero.
+    // 2. Capture an expression.  Drop a node and XMR deref that.
+    // 3. Capture something that can handle an inner sym.  XMR deref that.
+    //
+    // Note: (3) can be either an operation or a port.
+    auto baseType = type_cast<FIRRTLBaseType>(operand.getType());
+    if (baseType && baseType.getBitWidthOrSentinel() == 0) {
+      OpBuilder::InsertionGuard guard(localBuilder);
+      auto zeroUIntType = UIntType::get(localBuilder.getContext(), 0);
+      replacement = localBuilder.createOrFold<BitCastOp>(
+          operand.getType(), localBuilder.create<ConstantOp>(
+                                 zeroUIntType, getIntZerosAttr(zeroUIntType)));
+    } else {
+      auto *definingOp = operand.getDefiningOp();
+      hw::InnerRefAttr innerRef;
+      if (definingOp) {
+        // If this is an expression (something which can't have an inner
+        // symbol attached to it, then we need to create a node).
+        if (!isa<hw::InnerSymbolOpInterface>(definingOp))
+          definingOp = getOrCreateNodeOp(operand, localBuilder);
+        innerRef = getInnerRefTo(
+            definingOp, [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      } else {
+        innerRef = getInnerRefTo(
+            cast<FModuleLike>(*moduleOp),
+            cast<BlockArgument>(operand).getArgNumber(),
+            [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      }
+
+      hw::HierPathOp hierPathOp;
+      {
+        // TODO: Move to before parallel region to avoid the lock.
+        llvm::sys::SmartScopedLock<true> instrumentationLock(*circuitMutex);
+        hierPathOp = hierPathBuilder->getOrCreatePath(
+            localBuilder.getArrayAttr({innerRef}), localBuilder);
+        hierPathOp.setVisibility(SymbolTable::Visibility::Private);
+      }
+
+      replacement = localBuilder.create<XMRDerefOp>(
+          op->getLoc(), operand.getType(), hierPathOp.getSymNameAttr());
+    }
+
+    replacements.insert({operand, replacement});
+
+    return replacement;
+  };
+
   // A map of instance ops to modules that this pass creates.  This is used to
   // check if this was an instance that we created and to do fast module
   // dereferencing (avoiding a symbol table).
   DenseMap<InstanceOp, FModuleOp> createdInstances;
 
-  // Post-order traversal that expands a layer block into its parent. For each
-  // layer block found do the following:
+  // Check that the preconditiosn for this pass are met.  Reject any ops which
+  // must have been removed before this runs.
+  auto opPreconditionCheck = [](Operation *op) -> LogicalResult {
+    // LowerXMR op removal postconditions.
+    if (isa<RefCastOp, RefDefineOp, RefResolveOp, RefSendOp, RefSubOp,
+            RWProbeOp>(op))
+      return op->emitOpError()
+             << "cannot be handled by the lower-layers pass.  This should have "
+                "already been removed by the lower-xmr pass.";
+
+    return success();
+  };
+
+  // Post-order traversal that expands a layer block into its parent. Because of
+  // the pass precondition that this runs _after_ `LowerXMR`, not much has to
+  // happen here.  All of the following do happen, though:
+  //
+  // 1. Any layer coloring is stripped.
+  // 2. Layers with Inline convention are converted to SV ifdefs.
+  // 3. Layers with Bind convention are converted to new modules and then
+  //    instantiated at their original location.  Any captured values are either
+  //    moved, cloned, or converted to XMR deref ops.
+  // 4. Move instances created from earlier (3) conversions out of later (3)
+  //    conversions.  This is necessary to avoid a SystemVerilog-illegal
+  //    bind-under-bind.  (See Section 23.11 of 1800-2023.)
+  // 5. Keep track of special ops (ops with inner symbols or verbatims) which
+  //    need to have something updated because of the new instance hierarchy
+  //    being created.
   //
-  // 1. Create and connect one ref-type output port for each value defined in
-  //    this layer block that drives an instance marked lowerToBind and move
-  //    this instance outside the layer block.
-  // 2. Create one input port for each value captured by this layer block.
-  // 3. Create a new module for this layer block and move the (mutated) body of
-  //    this layer block to the new module.
-  // 4. Instantiate the new module outside the layer block and hook it up.
-  // 5. Erase the layer block.
+  // Remember, this is post-order, in-order.  Child layer blocks are visited
+  // before parents.  Any nested regions _within_ the layer block are also
+  // visited before the outer layer block.
   auto result = moduleOp.walk<mlir::WalkOrder::PostOrder>([&](Operation *op) {
+    if (failed(opPreconditionCheck(op)))
+      return WalkResult::interrupt();
+
     // Strip layer requirements from any op that might represent a probe.
     if (auto wire = dyn_cast<WireOp>(op)) {
       removeLayersFromValue(wire.getResult());
       return WalkResult::advance();
     }
-    if (auto sub = dyn_cast<RefSubOp>(op)) {
-      removeLayersFromValue(sub.getResult());
-      return WalkResult::advance();
-    }
     if (auto instance = dyn_cast<InstanceOp>(op)) {
       instance.setLayers({});
       for (auto result : instance.getResults())
         removeLayersFromValue(result);
       return WalkResult::advance();
     }
-    if (auto cast = dyn_cast<RefCastOp>(op)) {
-      removeLayersFromRefCast(cast);
-      return WalkResult::advance();
-    }
 
     auto layerBlock = dyn_cast<LayerBlockOp>(op);
     if (!layerBlock)
       return WalkResult::advance();
 
+    // After this point, we are dealing with a layer block.
     auto layer = symbolToLayer.lookup(layerBlock.getLayerName());
 
     if (layer.getConvention() == LayerConvention::Inline) {
       lowerInlineLayerBlock(layer, layerBlock);
       return WalkResult::advance();
     }
 
+    // After this point, we are dealing with a bind convetion layer block.
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: rwy7
body: ```suggestion
    // After this point, we are dealing with a bind convention layer block.
```
created_at: 2025-04-16T13:35:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
+      // localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*definingOp)->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // Determine the replacement value for the captured operand.  There are
+    // three cases that can occur:
+    //
+    // 1. Capturing something zero-width.  Create a zero-width constant zero.
+    // 2. Capture an expression.  Drop a node and XMR deref that.
+    // 3. Capture something that can handle an inner sym.  XMR deref that.
+    //
+    // Note: (3) can be either an operation or a port.
+    auto baseType = type_cast<FIRRTLBaseType>(operand.getType());
+    if (baseType && baseType.getBitWidthOrSentinel() == 0) {
+      OpBuilder::InsertionGuard guard(localBuilder);
+      auto zeroUIntType = UIntType::get(localBuilder.getContext(), 0);
+      replacement = localBuilder.createOrFold<BitCastOp>(
+          operand.getType(), localBuilder.create<ConstantOp>(
+                                 zeroUIntType, getIntZerosAttr(zeroUIntType)));
+    } else {
+      auto *definingOp = operand.getDefiningOp();
+      hw::InnerRefAttr innerRef;
+      if (definingOp) {
+        // If this is an expression (something which can't have an inner
+        // symbol attached to it, then we need to create a node).
+        if (!isa<hw::InnerSymbolOpInterface>(definingOp))
+          definingOp = getOrCreateNodeOp(operand, localBuilder);
+        innerRef = getInnerRefTo(
+            definingOp, [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      } else {
+        innerRef = getInnerRefTo(
+            cast<FModuleLike>(*moduleOp),
+            cast<BlockArgument>(operand).getArgNumber(),
+            [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      }
+
+      hw::HierPathOp hierPathOp;
+      {
+        // TODO: Move to before parallel region to avoid the lock.
+        llvm::sys::SmartScopedLock<true> instrumentationLock(*circuitMutex);
+        hierPathOp = hierPathBuilder->getOrCreatePath(
+            localBuilder.getArrayAttr({innerRef}), localBuilder);
+        hierPathOp.setVisibility(SymbolTable::Visibility::Private);
+      }
+
+      replacement = localBuilder.create<XMRDerefOp>(
+          op->getLoc(), operand.getType(), hierPathOp.getSymNameAttr());
+    }
+
+    replacements.insert({operand, replacement});
+
+    return replacement;
+  };
+
   // A map of instance ops to modules that this pass creates.  This is used to
   // check if this was an instance that we created and to do fast module
   // dereferencing (avoiding a symbol table).
   DenseMap<InstanceOp, FModuleOp> createdInstances;
 
-  // Post-order traversal that expands a layer block into its parent. For each
-  // layer block found do the following:
+  // Check that the preconditiosn for this pass are met.  Reject any ops which
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: rwy7
body: ```suggestion
  // Check that the preconditions for this pass are met.  Reject any ops which
```
created_at: 2025-04-16T13:40:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
+      // localBuilder.setInsertionPoint(op);
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: rwy7
body: ```suggestion
```
created_at: 2025-04-16T13:49:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: rwy7
body: IMHO it would be clearer if `op` was called `user` and `operand` was called `value`.
created_at: 2025-04-16T13:51:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: rwy7
body: You are replacing `operand` with result 0, when it should match the original result number, which you can only get if you check that `operand` is an `OpResult`. If operand is a block argument then this should fail gracefully. It doesn't seem like there is a reachable bug here, but I think it would be easier to understand if it was written more defensively.
created_at: 2025-04-16T14:26:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: rwy7
body: I think `isa_and_present` is preferred over `isa_and_nonnull`.
created_at: 2025-04-16T14:29:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
+      // localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*definingOp)->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // Determine the replacement value for the captured operand.  There are
+    // three cases that can occur:
+    //
+    // 1. Capturing something zero-width.  Create a zero-width constant zero.
+    // 2. Capture an expression.  Drop a node and XMR deref that.
+    // 3. Capture something that can handle an inner sym.  XMR deref that.
+    //
+    // Note: (3) can be either an operation or a port.
+    auto baseType = type_cast<FIRRTLBaseType>(operand.getType());
+    if (baseType && baseType.getBitWidthOrSentinel() == 0) {
+      OpBuilder::InsertionGuard guard(localBuilder);
+      auto zeroUIntType = UIntType::get(localBuilder.getContext(), 0);
+      replacement = localBuilder.createOrFold<BitCastOp>(
+          operand.getType(), localBuilder.create<ConstantOp>(
+                                 zeroUIntType, getIntZerosAttr(zeroUIntType)));
+    } else {
+      auto *definingOp = operand.getDefiningOp();
+      hw::InnerRefAttr innerRef;
+      if (definingOp) {
+        // If this is an expression (something which can't have an inner
+        // symbol attached to it, then we need to create a node).
+        if (!isa<hw::InnerSymbolOpInterface>(definingOp))
+          definingOp = getOrCreateNodeOp(operand, localBuilder);
+        innerRef = getInnerRefTo(
+            definingOp, [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      } else {
+        innerRef = getInnerRefTo(
+            cast<FModuleLike>(*moduleOp),
+            cast<BlockArgument>(operand).getArgNumber(),
+            [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      }
+
+      hw::HierPathOp hierPathOp;
+      {
+        // TODO: Move to before parallel region to avoid the lock.
+        llvm::sys::SmartScopedLock<true> instrumentationLock(*circuitMutex);
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: rwy7
body: ```suggestion
        llvm::sys::SmartScopedLock<true> circuitLock(*circuitMutex);
```
created_at: 2025-04-16T14:31:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
+      // localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*definingOp)->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // Determine the replacement value for the captured operand.  There are
+    // three cases that can occur:
+    //
+    // 1. Capturing something zero-width.  Create a zero-width constant zero.
+    // 2. Capture an expression.  Drop a node and XMR deref that.
+    // 3. Capture something that can handle an inner sym.  XMR deref that.
+    //
+    // Note: (3) can be either an operation or a port.
+    auto baseType = type_cast<FIRRTLBaseType>(operand.getType());
+    if (baseType && baseType.getBitWidthOrSentinel() == 0) {
+      OpBuilder::InsertionGuard guard(localBuilder);
+      auto zeroUIntType = UIntType::get(localBuilder.getContext(), 0);
+      replacement = localBuilder.createOrFold<BitCastOp>(
+          operand.getType(), localBuilder.create<ConstantOp>(
+                                 zeroUIntType, getIntZerosAttr(zeroUIntType)));
+    } else {
+      auto *definingOp = operand.getDefiningOp();
+      hw::InnerRefAttr innerRef;
+      if (definingOp) {
+        // If this is an expression (something which can't have an inner
+        // symbol attached to it, then we need to create a node).
+        if (!isa<hw::InnerSymbolOpInterface>(definingOp))
+          definingOp = getOrCreateNodeOp(operand, localBuilder);
+        innerRef = getInnerRefTo(
+            definingOp, [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      } else {
+        innerRef = getInnerRefTo(
+            cast<FModuleLike>(*moduleOp),
+            cast<BlockArgument>(operand).getArgNumber(),
+            [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      }
+
+      hw::HierPathOp hierPathOp;
+      {
+        // TODO: Move to before parallel region to avoid the lock.
+        llvm::sys::SmartScopedLock<true> instrumentationLock(*circuitMutex);
+        hierPathOp = hierPathBuilder->getOrCreatePath(
+            localBuilder.getArrayAttr({innerRef}), localBuilder);
+        hierPathOp.setVisibility(SymbolTable::Visibility::Private);
+      }
+
+      replacement = localBuilder.create<XMRDerefOp>(
+          op->getLoc(), operand.getType(), hierPathOp.getSymNameAttr());
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: rwy7
body: I think using the loc of the operand or layerblock makes more sense than op.
created_at: 2025-04-16T14:36:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -22,6 +22,7 @@ HierPathOp HierPathBuilder::getOrCreatePath(ArrayAttr pathArray,
   assert(pathArray && !pathArray.empty());
   // Return an existing HierPathOp if one exists with the same path.
   auto pathIter = pathCache.find(pathArray);
+
path: lib/Dialect/HW/HierPathBuilder.cpp
user: login: seldridge
body: Will be fixed in: a3614613b.
created_at: 2025-04-16T20:32:59Z
author_association: MEMBER
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: seldridge
body: Fixed in: 68aecd1c6.
created_at: 2025-04-16T22:03:43Z
author_association: MEMBER
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: seldridge
body: Changed in 7b89207bd43f6ab49dac958ebab68d3285765ec6.
created_at: 2025-04-16T22:03:48Z
author_association: MEMBER
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
+      // localBuilder.setInsertionPoint(op);
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: seldridge
body: Fixed in 7d0fe8b7b.
created_at: 2025-04-16T22:03:51Z
author_association: MEMBER
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
+      // localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*definingOp)->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // Determine the replacement value for the captured operand.  There are
+    // three cases that can occur:
+    //
+    // 1. Capturing something zero-width.  Create a zero-width constant zero.
+    // 2. Capture an expression.  Drop a node and XMR deref that.
+    // 3. Capture something that can handle an inner sym.  XMR deref that.
+    //
+    // Note: (3) can be either an operation or a port.
+    auto baseType = type_cast<FIRRTLBaseType>(operand.getType());
+    if (baseType && baseType.getBitWidthOrSentinel() == 0) {
+      OpBuilder::InsertionGuard guard(localBuilder);
+      auto zeroUIntType = UIntType::get(localBuilder.getContext(), 0);
+      replacement = localBuilder.createOrFold<BitCastOp>(
+          operand.getType(), localBuilder.create<ConstantOp>(
+                                 zeroUIntType, getIntZerosAttr(zeroUIntType)));
+    } else {
+      auto *definingOp = operand.getDefiningOp();
+      hw::InnerRefAttr innerRef;
+      if (definingOp) {
+        // If this is an expression (something which can't have an inner
+        // symbol attached to it, then we need to create a node).
+        if (!isa<hw::InnerSymbolOpInterface>(definingOp))
+          definingOp = getOrCreateNodeOp(operand, localBuilder);
+        innerRef = getInnerRefTo(
+            definingOp, [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      } else {
+        innerRef = getInnerRefTo(
+            cast<FModuleLike>(*moduleOp),
+            cast<BlockArgument>(operand).getArgNumber(),
+            [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      }
+
+      hw::HierPathOp hierPathOp;
+      {
+        // TODO: Move to before parallel region to avoid the lock.
+        llvm::sys::SmartScopedLock<true> instrumentationLock(*circuitMutex);
+        hierPathOp = hierPathBuilder->getOrCreatePath(
+            localBuilder.getArrayAttr({innerRef}), localBuilder);
+        hierPathOp.setVisibility(SymbolTable::Visibility::Private);
+      }
+
+      replacement = localBuilder.create<XMRDerefOp>(
+          op->getLoc(), operand.getType(), hierPathOp.getSymNameAttr());
+    }
+
+    replacements.insert({operand, replacement});
+
+    return replacement;
+  };
+
   // A map of instance ops to modules that this pass creates.  This is used to
   // check if this was an instance that we created and to do fast module
   // dereferencing (avoiding a symbol table).
   DenseMap<InstanceOp, FModuleOp> createdInstances;
 
-  // Post-order traversal that expands a layer block into its parent. For each
-  // layer block found do the following:
+  // Check that the preconditiosn for this pass are met.  Reject any ops which
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: seldridge
body: Fixed in: b3b024811
created_at: 2025-04-16T22:03:55Z
author_association: MEMBER
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
+      // localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*definingOp)->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // Determine the replacement value for the captured operand.  There are
+    // three cases that can occur:
+    //
+    // 1. Capturing something zero-width.  Create a zero-width constant zero.
+    // 2. Capture an expression.  Drop a node and XMR deref that.
+    // 3. Capture something that can handle an inner sym.  XMR deref that.
+    //
+    // Note: (3) can be either an operation or a port.
+    auto baseType = type_cast<FIRRTLBaseType>(operand.getType());
+    if (baseType && baseType.getBitWidthOrSentinel() == 0) {
+      OpBuilder::InsertionGuard guard(localBuilder);
+      auto zeroUIntType = UIntType::get(localBuilder.getContext(), 0);
+      replacement = localBuilder.createOrFold<BitCastOp>(
+          operand.getType(), localBuilder.create<ConstantOp>(
+                                 zeroUIntType, getIntZerosAttr(zeroUIntType)));
+    } else {
+      auto *definingOp = operand.getDefiningOp();
+      hw::InnerRefAttr innerRef;
+      if (definingOp) {
+        // If this is an expression (something which can't have an inner
+        // symbol attached to it, then we need to create a node).
+        if (!isa<hw::InnerSymbolOpInterface>(definingOp))
+          definingOp = getOrCreateNodeOp(operand, localBuilder);
+        innerRef = getInnerRefTo(
+            definingOp, [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      } else {
+        innerRef = getInnerRefTo(
+            cast<FModuleLike>(*moduleOp),
+            cast<BlockArgument>(operand).getArgNumber(),
+            [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      }
+
+      hw::HierPathOp hierPathOp;
+      {
+        // TODO: Move to before parallel region to avoid the lock.
+        llvm::sys::SmartScopedLock<true> instrumentationLock(*circuitMutex);
+        hierPathOp = hierPathBuilder->getOrCreatePath(
+            localBuilder.getArrayAttr({innerRef}), localBuilder);
+        hierPathOp.setVisibility(SymbolTable::Visibility::Private);
+      }
+
+      replacement = localBuilder.create<XMRDerefOp>(
+          op->getLoc(), operand.getType(), hierPathOp.getSymNameAttr());
+    }
+
+    replacements.insert({operand, replacement});
+
+    return replacement;
+  };
+
   // A map of instance ops to modules that this pass creates.  This is used to
   // check if this was an instance that we created and to do fast module
   // dereferencing (avoiding a symbol table).
   DenseMap<InstanceOp, FModuleOp> createdInstances;
 
-  // Post-order traversal that expands a layer block into its parent. For each
-  // layer block found do the following:
+  // Check that the preconditiosn for this pass are met.  Reject any ops which
+  // must have been removed before this runs.
+  auto opPreconditionCheck = [](Operation *op) -> LogicalResult {
+    // LowerXMR op removal postconditions.
+    if (isa<RefCastOp, RefDefineOp, RefResolveOp, RefSendOp, RefSubOp,
+            RWProbeOp>(op))
+      return op->emitOpError()
+             << "cannot be handled by the lower-layers pass.  This should have "
+                "already been removed by the lower-xmr pass.";
+
+    return success();
+  };
+
+  // Post-order traversal that expands a layer block into its parent. Because of
+  // the pass precondition that this runs _after_ `LowerXMR`, not much has to
+  // happen here.  All of the following do happen, though:
+  //
+  // 1. Any layer coloring is stripped.
+  // 2. Layers with Inline convention are converted to SV ifdefs.
+  // 3. Layers with Bind convention are converted to new modules and then
+  //    instantiated at their original location.  Any captured values are either
+  //    moved, cloned, or converted to XMR deref ops.
+  // 4. Move instances created from earlier (3) conversions out of later (3)
+  //    conversions.  This is necessary to avoid a SystemVerilog-illegal
+  //    bind-under-bind.  (See Section 23.11 of 1800-2023.)
+  // 5. Keep track of special ops (ops with inner symbols or verbatims) which
+  //    need to have something updated because of the new instance hierarchy
+  //    being created.
   //
-  // 1. Create and connect one ref-type output port for each value defined in
-  //    this layer block that drives an instance marked lowerToBind and move
-  //    this instance outside the layer block.
-  // 2. Create one input port for each value captured by this layer block.
-  // 3. Create a new module for this layer block and move the (mutated) body of
-  //    this layer block to the new module.
-  // 4. Instantiate the new module outside the layer block and hook it up.
-  // 5. Erase the layer block.
+  // Remember, this is post-order, in-order.  Child layer blocks are visited
+  // before parents.  Any nested regions _within_ the layer block are also
+  // visited before the outer layer block.
   auto result = moduleOp.walk<mlir::WalkOrder::PostOrder>([&](Operation *op) {
+    if (failed(opPreconditionCheck(op)))
+      return WalkResult::interrupt();
+
     // Strip layer requirements from any op that might represent a probe.
     if (auto wire = dyn_cast<WireOp>(op)) {
       removeLayersFromValue(wire.getResult());
       return WalkResult::advance();
     }
-    if (auto sub = dyn_cast<RefSubOp>(op)) {
-      removeLayersFromValue(sub.getResult());
-      return WalkResult::advance();
-    }
     if (auto instance = dyn_cast<InstanceOp>(op)) {
       instance.setLayers({});
       for (auto result : instance.getResults())
         removeLayersFromValue(result);
       return WalkResult::advance();
     }
-    if (auto cast = dyn_cast<RefCastOp>(op)) {
-      removeLayersFromRefCast(cast);
-      return WalkResult::advance();
-    }
 
     auto layerBlock = dyn_cast<LayerBlockOp>(op);
     if (!layerBlock)
       return WalkResult::advance();
 
+    // After this point, we are dealing with a layer block.
     auto layer = symbolToLayer.lookup(layerBlock.getLayerName());
 
     if (layer.getConvention() == LayerConvention::Inline) {
       lowerInlineLayerBlock(layer, layerBlock);
       return WalkResult::advance();
     }
 
+    // After this point, we are dealing with a bind convetion layer block.
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: seldridge
body: This will be fixed in f1765386cb24f6cc04899fdfc809fcb84671e2f9.
created_at: 2025-04-16T22:03:59Z
author_association: MEMBER
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
+      // localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*definingOp)->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // Determine the replacement value for the captured operand.  There are
+    // three cases that can occur:
+    //
+    // 1. Capturing something zero-width.  Create a zero-width constant zero.
+    // 2. Capture an expression.  Drop a node and XMR deref that.
+    // 3. Capture something that can handle an inner sym.  XMR deref that.
+    //
+    // Note: (3) can be either an operation or a port.
+    auto baseType = type_cast<FIRRTLBaseType>(operand.getType());
+    if (baseType && baseType.getBitWidthOrSentinel() == 0) {
+      OpBuilder::InsertionGuard guard(localBuilder);
+      auto zeroUIntType = UIntType::get(localBuilder.getContext(), 0);
+      replacement = localBuilder.createOrFold<BitCastOp>(
+          operand.getType(), localBuilder.create<ConstantOp>(
+                                 zeroUIntType, getIntZerosAttr(zeroUIntType)));
+    } else {
+      auto *definingOp = operand.getDefiningOp();
+      hw::InnerRefAttr innerRef;
+      if (definingOp) {
+        // If this is an expression (something which can't have an inner
+        // symbol attached to it, then we need to create a node).
+        if (!isa<hw::InnerSymbolOpInterface>(definingOp))
+          definingOp = getOrCreateNodeOp(operand, localBuilder);
+        innerRef = getInnerRefTo(
+            definingOp, [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      } else {
+        innerRef = getInnerRefTo(
+            cast<FModuleLike>(*moduleOp),
+            cast<BlockArgument>(operand).getArgNumber(),
+            [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      }
+
+      hw::HierPathOp hierPathOp;
+      {
+        // TODO: Move to before parallel region to avoid the lock.
+        llvm::sys::SmartScopedLock<true> instrumentationLock(*circuitMutex);
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: seldridge
body: Fixed in: 7d7c6fd41
created_at: 2025-04-16T22:16:48Z
author_association: MEMBER
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: seldridge
body: Note: to avoid an include or using the full namespace, I added `isa_and_present` to LLVM.h in: b0c5b629c
created_at: 2025-04-16T22:17:53Z
author_association: MEMBER
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: rwy7
body: Ah OK sorry I didn't realize we haven't been using `isa_and_present`. Thanks
created_at: 2025-04-17T14:59:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -324,210 +306,214 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *user, Value value) -> Value {
+    auto it = replacements.find(value);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(value.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        user->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(value.getType())) {
+      localBuilder.setInsertionPoint(user);
+      replacement = localBuilder.clone(*value.getDefiningOp())->getResult(0);
+      replacements.insert({value, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = value.getDefiningOp();
+    if (isa_and_present<XMRRefOp>(definingOp)) {
+      replacement = localBuilder.clone(*definingOp)->getResult(0);
+      replacements.insert({value, replacement});
+      return replacement;
+    }
+
+    // Determine the replacement value for the captured operand.  There are
+    // three cases that can occur:
+    //
+    // 1. Capturing something zero-width.  Create a zero-width constant zero.
+    // 2. Capture an expression or instance port.  Drop a node and XMR deref
+    //    that.
+    // 3. Capture something that can handle an inner sym.  XMR deref that.
+    //
+    // Note: (3) can be either an operation or a _module_ port.
+    auto baseType = type_cast<FIRRTLBaseType>(value.getType());
+    if (baseType && baseType.getBitWidthOrSentinel() == 0) {
+      OpBuilder::InsertionGuard guard(localBuilder);
+      auto zeroUIntType = UIntType::get(localBuilder.getContext(), 0);
+      replacement = localBuilder.createOrFold<BitCastOp>(
+          value.getType(), localBuilder.create<ConstantOp>(
+                               zeroUIntType, getIntZerosAttr(zeroUIntType)));
+    } else {
+      auto *definingOp = value.getDefiningOp();
+      hw::InnerRefAttr innerRef;
+      if (definingOp) {
+        // If this is an expression (something which can't have an inner symbol
+        // attached to it) or if this is an instance port, then we need to
+        // create a node.
+        if (!isa<hw::InnerSymbolOpInterface>(definingOp) ||
+            isa<InstanceOp>(definingOp))
+          definingOp = getOrCreateNodeOp(value, localBuilder);
+        innerRef = getInnerRefTo(
+            definingOp, [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      } else {
+        auto portIdx = cast<BlockArgument>(value).getArgNumber();
+        innerRef = getInnerRefTo(
+            cast<FModuleLike>(*moduleOp), portIdx,
+            [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      }
+
+      hw::HierPathOp hierPathOp;
+      {
+        // TODO: Move to before parallel region to avoid the lock.
+        llvm::sys::SmartScopedLock<true> circuitLock(*circuitMutex);
+        hierPathOp = hierPathBuilder->getOrCreatePath(
+            localBuilder.getArrayAttr({innerRef}), localBuilder);
+        hierPathOp.setVisibility(SymbolTable::Visibility::Private);
+      }
+
+      replacement = localBuilder.create<XMRDerefOp>(
+          value.getType(), hierPathOp.getSymNameAttr());
+    }
+
+    replacements.insert({value, replacement});
+
+    return replacement;
+  };
+
   // A map of instance ops to modules that this pass creates.  This is used to
   // check if this was an instance that we created and to do fast module
   // dereferencing (avoiding a symbol table).
   DenseMap<InstanceOp, FModuleOp> createdInstances;
 
-  // Post-order traversal that expands a layer block into its parent. For each
-  // layer block found do the following:
+  // Check that the preconditions for this pass are met.  Reject any ops which
+  // must have been removed before this runs.
+  auto opPreconditionCheck = [](Operation *op) -> LogicalResult {
+    // LowerXMR op removal postconditions.
+    if (isa<RefCastOp, RefDefineOp, RefResolveOp, RefSendOp, RefSubOp,
+            RWProbeOp>(op))
+      return op->emitOpError()
+             << "cannot be handled by the lower-layers pass.  This should have "
+                "already been removed by the lower-xmr pass.";
+
+    return success();
+  };
+
+  // Post-order traversal that expands a layer block into its parent. Because of
+  // the pass precondition that this runs _after_ `LowerXMR`, not much has to
+  // happen here.  All of the following do happen, though:
   //
-  // 1. Create and connect one ref-type output port for each value defined in
-  //    this layer block that drives an instance marked lowerToBind and move
-  //    this instance outside the layer block.
-  // 2. Create one input port for each value captured by this layer block.
-  // 3. Create a new module for this layer block and move the (mutated) body of
-  //    this layer block to the new module.
-  // 4. Instantiate the new module outside the layer block and hook it up.
-  // 5. Erase the layer block.
+  // 1. Any layer coloring is stripped.
+  // 2. Layers with Inline convention are converted to SV ifdefs.
+  // 3. Layers with Bind convention are converted to new modules and then
+  //    instantiated at their original location.  Any captured values are either
+  //    moved, cloned, or converted to XMR deref ops.
+  // 4. Move instances created from earlier (3) conversions out of later (3)
+  //    conversions.  This is necessary to avoid a SystemVerilog-illegal
+  //    bind-under-bind.  (See Section 23.11 of 1800-2023.)
+  // 5. Keep track of special ops (ops with inner symbols or verbatims) which
+  //    need to have something updated because of the new instance hierarchy
+  //    being created.
+  //
+  // Remember, this is post-order, in-order.  Child layer blocks are visited
+  // before parents.  Any nested regions _within_ the layer block are also
+  // visited before the outer layer block.
   auto result = moduleOp.walk<mlir::WalkOrder::PostOrder>([&](Operation *op) {
+    // llvm::errs() << "in module, visiting: " << *op << "\n";
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: rwy7
body: ```suggestion
```
created_at: 2025-04-17T15:14:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,10 @@
-// RUN: circt-opt -firrtl-lower-layers -split-input-file %s | FileCheck %s
+// RUN: circt-opt -firrtl-lower-layers -split-input-file -mlir-disable-threading %s | FileCheck %s
+//
+// TODO: Remove the "-mlir-disable-threading".  This is used so that the order
+// of the `hw.hierpath` ops can be checked.  It would be better to split up all
+// the checks into individual circuits to avoid this.
path: test/Dialect/FIRRTL/lower-layers.mlir
user: login: rwy7
body: We would rather make lower-layers deterministic instead :)
created_at: 2025-04-17T17:02:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,10 @@
-// RUN: circt-opt -firrtl-lower-layers -split-input-file %s | FileCheck %s
+// RUN: circt-opt -firrtl-lower-layers -split-input-file -mlir-disable-threading %s | FileCheck %s
+//
+// TODO: Remove the "-mlir-disable-threading".  This is used so that the order
+// of the `hw.hierpath` ops can be checked.  It would be better to split up all
+// the checks into individual circuits to avoid this.
path: test/Dialect/FIRRTL/lower-layers.mlir
user: login: seldridge
body: 🫠 I'm going to look at doing the pre-allocation in the same way that `moduleNames` are pre-allocated. I may not go as far as true "prefix-uniqueness" as this gets very tedious without a refactor to namespaces to store names in a trie in order to get a cheap query of "are there any users of this prefix. (Though there is an `llvm::TrieRawHashMap`?)
created_at: 2025-04-18T22:12:18Z
author_association: MEMBER
diff_hunk: @@ -1,5 +1,10 @@
-// RUN: circt-opt -firrtl-lower-layers -split-input-file %s | FileCheck %s
+// RUN: circt-opt -firrtl-lower-layers -split-input-file -mlir-disable-threading %s | FileCheck %s
+//
+// TODO: Remove the "-mlir-disable-threading".  This is used so that the order
+// of the `hw.hierpath` ops can be checked.  It would be better to split up all
+// the checks into individual circuits to avoid this.
path: test/Dialect/FIRRTL/lower-layers.mlir
user: login: seldridge
body: The current version should now be stable, modulo the prefix-uniqueness question. This comment, and the `--mlir-disable-threading` is now removed.
created_at: 2025-04-19T00:01:33Z
author_association: MEMBER
diff_hunk: @@ -324,210 +306,214 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *user, Value value) -> Value {
+    auto it = replacements.find(value);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(value.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        user->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(value.getType())) {
+      localBuilder.setInsertionPoint(user);
+      replacement = localBuilder.clone(*value.getDefiningOp())->getResult(0);
+      replacements.insert({value, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = value.getDefiningOp();
+    if (isa_and_present<XMRRefOp>(definingOp)) {
+      replacement = localBuilder.clone(*definingOp)->getResult(0);
+      replacements.insert({value, replacement});
+      return replacement;
+    }
+
+    // Determine the replacement value for the captured operand.  There are
+    // three cases that can occur:
+    //
+    // 1. Capturing something zero-width.  Create a zero-width constant zero.
+    // 2. Capture an expression or instance port.  Drop a node and XMR deref
+    //    that.
+    // 3. Capture something that can handle an inner sym.  XMR deref that.
+    //
+    // Note: (3) can be either an operation or a _module_ port.
+    auto baseType = type_cast<FIRRTLBaseType>(value.getType());
+    if (baseType && baseType.getBitWidthOrSentinel() == 0) {
+      OpBuilder::InsertionGuard guard(localBuilder);
+      auto zeroUIntType = UIntType::get(localBuilder.getContext(), 0);
+      replacement = localBuilder.createOrFold<BitCastOp>(
+          value.getType(), localBuilder.create<ConstantOp>(
+                               zeroUIntType, getIntZerosAttr(zeroUIntType)));
+    } else {
+      auto *definingOp = value.getDefiningOp();
+      hw::InnerRefAttr innerRef;
+      if (definingOp) {
+        // If this is an expression (something which can't have an inner symbol
+        // attached to it) or if this is an instance port, then we need to
+        // create a node.
+        if (!isa<hw::InnerSymbolOpInterface>(definingOp) ||
+            isa<InstanceOp>(definingOp))
+          definingOp = getOrCreateNodeOp(value, localBuilder);
+        innerRef = getInnerRefTo(
+            definingOp, [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      } else {
+        auto portIdx = cast<BlockArgument>(value).getArgNumber();
+        innerRef = getInnerRefTo(
+            cast<FModuleLike>(*moduleOp), portIdx,
+            [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      }
+
+      hw::HierPathOp hierPathOp;
+      {
+        // TODO: Move to before parallel region to avoid the lock.
+        llvm::sys::SmartScopedLock<true> circuitLock(*circuitMutex);
+        hierPathOp = hierPathBuilder->getOrCreatePath(
+            localBuilder.getArrayAttr({innerRef}), localBuilder);
+        hierPathOp.setVisibility(SymbolTable::Visibility::Private);
+      }
+
+      replacement = localBuilder.create<XMRDerefOp>(
+          value.getType(), hierPathOp.getSymNameAttr());
+    }
+
+    replacements.insert({value, replacement});
+
+    return replacement;
+  };
+
   // A map of instance ops to modules that this pass creates.  This is used to
   // check if this was an instance that we created and to do fast module
   // dereferencing (avoiding a symbol table).
   DenseMap<InstanceOp, FModuleOp> createdInstances;
 
-  // Post-order traversal that expands a layer block into its parent. For each
-  // layer block found do the following:
+  // Check that the preconditions for this pass are met.  Reject any ops which
+  // must have been removed before this runs.
+  auto opPreconditionCheck = [](Operation *op) -> LogicalResult {
+    // LowerXMR op removal postconditions.
+    if (isa<RefCastOp, RefDefineOp, RefResolveOp, RefSendOp, RefSubOp,
+            RWProbeOp>(op))
+      return op->emitOpError()
+             << "cannot be handled by the lower-layers pass.  This should have "
+                "already been removed by the lower-xmr pass.";
+
+    return success();
+  };
+
+  // Post-order traversal that expands a layer block into its parent. Because of
+  // the pass precondition that this runs _after_ `LowerXMR`, not much has to
+  // happen here.  All of the following do happen, though:
   //
-  // 1. Create and connect one ref-type output port for each value defined in
-  //    this layer block that drives an instance marked lowerToBind and move
-  //    this instance outside the layer block.
-  // 2. Create one input port for each value captured by this layer block.
-  // 3. Create a new module for this layer block and move the (mutated) body of
-  //    this layer block to the new module.
-  // 4. Instantiate the new module outside the layer block and hook it up.
-  // 5. Erase the layer block.
+  // 1. Any layer coloring is stripped.
+  // 2. Layers with Inline convention are converted to SV ifdefs.
+  // 3. Layers with Bind convention are converted to new modules and then
+  //    instantiated at their original location.  Any captured values are either
+  //    moved, cloned, or converted to XMR deref ops.
+  // 4. Move instances created from earlier (3) conversions out of later (3)
+  //    conversions.  This is necessary to avoid a SystemVerilog-illegal
+  //    bind-under-bind.  (See Section 23.11 of 1800-2023.)
+  // 5. Keep track of special ops (ops with inner symbols or verbatims) which
+  //    need to have something updated because of the new instance hierarchy
+  //    being created.
+  //
+  // Remember, this is post-order, in-order.  Child layer blocks are visited
+  // before parents.  Any nested regions _within_ the layer block are also
+  // visited before the outer layer block.
   auto result = moduleOp.walk<mlir::WalkOrder::PostOrder>([&](Operation *op) {
+    // llvm::errs() << "in module, visiting: " << *op << "\n";
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: seldridge
body: Good catch. Fixed in the latest version.
created_at: 2025-04-19T00:01:53Z
author_association: MEMBER
diff_hunk: @@ -324,162 +306,189 @@ LogicalResult LowerLayersPass::runOnModuleBody(FModuleOp moduleOp,
   StringRef circuitName = circuitOp.getName();
   hw::InnerSymbolNamespace ns(moduleOp);
 
+  // A cache of nodes we've created for values which are captured, but cannot
+  // have a symbol attached to them.  This is done to avoid creating the same
+  // node multiple times.
+  DenseMap<Value, NodeOp> nodeCache;
+
+  // Get or create a node within this module.  This is used when we need to
+  // create an XMRDerefOp to an expression.
+  auto getOrCreateNodeOp = [&](Value operand,
+                               ImplicitLocOpBuilder &builder) -> NodeOp {
+    auto it = nodeCache.find(operand);
+    if (it != nodeCache.end())
+      return it->getSecond();
+    OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointAfter(operand.getDefiningOp());
+    return nodeCache
+        .insert({operand, builder.create<NodeOp>(operand.getLoc(), operand)})
+        .first->getSecond();
+  };
+
+  // Determine the replacement for an operand within the current region.  Keep a
+  // densemap of replacements around to avoid creating the same hardware
+  // multiple times.
+  DenseMap<Value, Value> replacements;
+  auto getReplacement = [&](Operation *op, Value operand) -> Value {
+    auto it = replacements.find(operand);
+    if (it != replacements.end())
+      return it->getSecond();
+
+    ImplicitLocOpBuilder localBuilder(operand.getLoc(), &getContext());
+    Value replacement;
+
+    localBuilder.setInsertionPointToStart(
+        op->getParentOfType<LayerBlockOp>().getBody());
+
+    // If the operand is "special", e.g., it has no XMR representation, then we
+    // need to clone it.
+    //
+    // TODO: Change this to recursively clone.  This will matter once FString
+    // operations have operands.
+    if (type_isa<FStringType>(operand.getType())) {
+      localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*operand.getDefiningOp())->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // If the operand is an XMR ref, then we _have_ to clone it.
+    auto *definingOp = operand.getDefiningOp();
+    if (isa_and_nonnull<XMRRefOp>(definingOp)) {
+      // localBuilder.setInsertionPoint(op);
+      replacement = localBuilder.clone(*definingOp)->getResult(0);
+      replacements.insert({operand, replacement});
+      return replacement;
+    }
+
+    // Determine the replacement value for the captured operand.  There are
+    // three cases that can occur:
+    //
+    // 1. Capturing something zero-width.  Create a zero-width constant zero.
+    // 2. Capture an expression.  Drop a node and XMR deref that.
+    // 3. Capture something that can handle an inner sym.  XMR deref that.
+    //
+    // Note: (3) can be either an operation or a port.
+    auto baseType = type_cast<FIRRTLBaseType>(operand.getType());
+    if (baseType && baseType.getBitWidthOrSentinel() == 0) {
+      OpBuilder::InsertionGuard guard(localBuilder);
+      auto zeroUIntType = UIntType::get(localBuilder.getContext(), 0);
+      replacement = localBuilder.createOrFold<BitCastOp>(
+          operand.getType(), localBuilder.create<ConstantOp>(
+                                 zeroUIntType, getIntZerosAttr(zeroUIntType)));
+    } else {
+      auto *definingOp = operand.getDefiningOp();
+      hw::InnerRefAttr innerRef;
+      if (definingOp) {
+        // If this is an expression (something which can't have an inner
+        // symbol attached to it, then we need to create a node).
+        if (!isa<hw::InnerSymbolOpInterface>(definingOp))
+          definingOp = getOrCreateNodeOp(operand, localBuilder);
+        innerRef = getInnerRefTo(
+            definingOp, [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      } else {
+        innerRef = getInnerRefTo(
+            cast<FModuleLike>(*moduleOp),
+            cast<BlockArgument>(operand).getArgNumber(),
+            [&](auto) -> hw::InnerSymbolNamespace & { return ns; });
+      }
+
+      hw::HierPathOp hierPathOp;
+      {
+        // TODO: Move to before parallel region to avoid the lock.
+        llvm::sys::SmartScopedLock<true> instrumentationLock(*circuitMutex);
+        hierPathOp = hierPathBuilder->getOrCreatePath(
+            localBuilder.getArrayAttr({innerRef}), localBuilder);
+        hierPathOp.setVisibility(SymbolTable::Visibility::Private);
+      }
+
+      replacement = localBuilder.create<XMRDerefOp>(
+          op->getLoc(), operand.getType(), hierPathOp.getSymNameAttr());
path: lib/Dialect/FIRRTL/Transforms/LowerLayers.cpp
user: login: seldridge
body: Agree. This should be fixed in the latest version.
created_at: 2025-04-19T00:02:11Z
author_association: MEMBER

PR comments:
user: login: seldridge
created_at: 2025-04-12T03:47:21Z
body: The changes in this PR introduce a lot of unreachable code in `LowerLayers`. I will clean this up as long as there is consensus that we want to enforce the precondition that `LowerXMR` runs before this always (and thereby certain ops will never be seen). It is possible to continue to support this, but it will not be load bearing in `firtool`.
author_association: MEMBER
user: login: seldridge
created_at: 2025-04-16T17:47:01Z
body: https://github.com/llvm/circt/pull/8405/commits/40a968543d90ff524fef2f9daea6a36a63518fe6 fixes a bug where when capturing an instance port, a node needs to be created. This matches the behavior of LowerXMR. The buggy behavior before this is that this would XMR directly to the instance which is just wrong.
author_association: MEMBER
user: login: seldridge
created_at: 2025-04-22T18:38:12Z
body: Thanks for the review @rwy7. I will merge this after running some internal tests. I'm slating to get this into the 1.115.0 release this week.
author_association: MEMBER
user: login: seldridge
created_at: 2025-04-24T15:02:46Z
body: I have this passing regressions internally, though cherry-picked onto the 1.114.1 tag.
author_association: MEMBER