From 5c415217b7ea302db8554bbe1661ce506cb341c0 Mon Sep 17 00:00:00 2001 From: Prithayan Barua Date: Wed, 8 Jul 2026 22:34:52 -0700 Subject: [PATCH] [FIRRTL] Add RWProbe force/release synthesis to ProbesToSignals --- .../FIRRTL/Transforms/ProbesToSignals.cpp | 1004 ++++++++++++++++- .../FIRRTL/probes-to-signals-errors.mlir | 12 - .../probes-to-signals-force-release.mlir | 528 +++++++++ test/Dialect/FIRRTL/probes-to-signals.mlir | 20 +- 4 files changed, 1512 insertions(+), 52 deletions(-) create mode 100644 test/Dialect/FIRRTL/probes-to-signals-force-release.mlir diff --git a/lib/Dialect/FIRRTL/Transforms/ProbesToSignals.cpp b/lib/Dialect/FIRRTL/Transforms/ProbesToSignals.cpp index 1e0c75387cc4..b023567fde50 100644 --- a/lib/Dialect/FIRRTL/Transforms/ProbesToSignals.cpp +++ b/lib/Dialect/FIRRTL/Transforms/ProbesToSignals.cpp @@ -11,6 +11,19 @@ // behavior-changing transformation that may break ABI compatibility anywhere // probes are used relevant to ABI. // +// Read-only probes lower to plain wire connections. Force/release on RWProbes +// is synthesized into a per-probe state machine: a `forced` flag register and +// a `forcedValue` register encode the effect of the priority-ordered accesses, +// and an override mux is injected at the target's connect point. +// +// Gated-clock force/release: when the access's clock or the target register's +// clock comes from a `firrtl.int.clock_gate`, the gate enable is folded into +// the synchronous predicate so the synthesized state runs on the free-running +// base clock. Cross-module gates are handled by GatedClockConversion, which +// traces each access's clock across module boundaries and plumbs paired +// `_pts_baseClock_*` / `_pts_gateEnable_*` ports along the path so the access +// can reference them as local SSA values. +// // Pre-requisites for complete conversion: // * LowerOpenAggs // - Simplifies this pass, Probes are always separate. @@ -36,6 +49,7 @@ #include "circt/Dialect/FIRRTL/FIRRTLTypes.h" #include "circt/Dialect/FIRRTL/FIRRTLUtils.h" #include "circt/Dialect/FIRRTL/FIRRTLVisitors.h" +#include "circt/Dialect/FIRRTL/GatedClockConversion.h" #include "circt/Dialect/FIRRTL/Passes.h" #include "circt/Support/Debug.h" #include "mlir/IR/Threading.h" @@ -60,6 +74,27 @@ using namespace firrtl; namespace { +/// Return the parent FModuleOp of a given value: for a BlockArgument the module +/// owning its block, otherwise the module containing its defining op. +static FModuleOp getParentModule(Value value) { + if (isa(value)) + return cast(value.getParentBlock()->getParentOp()); + return value.getDefiningOp()->getParentOfType(); +} + +/// One force or release access targeting a single RWProbe. `forceValue` +/// distinguishes the two: `nullopt` means release. `clock` is null for +/// `force_initial` / `release_initial` — those have no synchronous component +/// and only ride along on a sibling clocked access's state machine. +struct ForceReleaseAccess { + Operation *op; + Value predicate; + std::optional forceValue; + Value clock; + + bool isForce() const { return forceValue.has_value(); } +}; + class ProbeVisitor : public FIRRTLVisitor { public: ProbeVisitor(hw::InnerRefNamespace &irn) : irn(irn) {} @@ -67,6 +102,70 @@ class ProbeVisitor : public FIRRTLVisitor { /// Entrypoint. LogicalResult visit(FModuleLike mod); + /// Reduced local force/release control for a probe. `clk` is null when every + /// contributing access is an `_initial` variant. + struct LocalCtrl { + Value forceActive; + Value releaseActive; + Value forcedValue; + Value clk; + }; + + /// A forceable RWProbe exported through a module port. The port insertion + /// and inbound driving are deferred to the sequential post-pass because a + /// module's port count and its instances' port counts must be mutated + /// together to stay in lockstep, which is unsafe under the parallel walk. + struct ExportEntry { + unsigned portIdx; ///< exported port index (== instance result idx) + StringAttr portName; ///< original port name (-> "_force_ctrl") + Location portLoc; ///< location for the new port + FIRRTLBaseType probedType; ///< probed (hardware) type + WireOp controlWire; ///< SM control-bundle wire data in this module + LocalCtrl local; ///< reduced local control; all-null iff no local + ///< force (treated as constant 0 in the merge) + }; + + /// A forceable RWProbe consumed *from* an instance in this (parent) module. + /// The instance's freshly-inserted `_force_ctrl` input port is + /// connected to the parent's forwarding control wire by the sequential + /// post-pass. + struct InstancePlumb { + Operation *inst; ///< type-converted instance (live into the post-pass) + unsigned resultIdx; ///< forceable probe result index on the instance + Value forwardingWire; ///< control-bundle wire in the parent driving it + }; + + /// How a forceable target's synthesized state machine gets its control. + enum class ForceCategory { + /// Purely local: registers driven directly from reduced SSA (no wire). + Local, + /// Exported through a module port: control merged with an inbound + /// `_force_ctrl` bundle by the sequential post-pass. + Exported, + /// Consumed from a child instance: the real SM lives in the child; drive + /// the parent's forwarding control-bundle wire from reduced local control. + InstanceForwarded, + }; + + /// Per-target force/release plan: records *what* to materialize (category + + /// the reduced control + the target metadata) but emits no hardware itself. + struct ForcePlan { + ForceCategory category; + Value target; ///< canonical hardware value being forced + FIRRTLBaseType probedType; ///< probed (hardware) type of `target` + /// Reduced local control (all-null iff no local force, e.g. a probe forced + /// only from outside this module). + LocalCtrl local; + /// Instance-forwarded only: the parent's forwarding control-bundle wire. + WireOp forwardingWire; + }; + + /// Exported forceable probes / instance force-control wires discovered while + /// visiting this module, read by the pass after `visit` returns and replayed + /// in the sequential post-pass. + SmallVector exportWork; + SmallVector instancePlumb; + using FIRRTLVisitor::visitDecl; using FIRRTLVisitor::visitExpr; using FIRRTLVisitor::visitStmt; @@ -146,7 +245,7 @@ class ProbeVisitor : public FIRRTLVisitor { LogicalResult visitDecl(WireOp op); LogicalResult visitActiveForceableDecl(Forceable fop); - LogicalResult visitInstanceLike(Operation *op); + LogicalResult visitInstanceLike(FInstanceLike oldInst); LogicalResult visitDecl(InstanceOp op) { return visitInstanceLike(op); } LogicalResult visitDecl(InstanceChoiceOp op) { return visitInstanceLike(op); } @@ -160,23 +259,16 @@ class ProbeVisitor : public FIRRTLVisitor { LogicalResult visitStmt(RefDefineOp op); - // Force and release operations: reject as unsupported. - LogicalResult visitStmt(RefForceOp op) { - return op.emitError("force not supported"); - } - LogicalResult visitStmt(RefForceInitialOp op) { - return op.emitError("force_initial not supported"); - } - LogicalResult visitStmt(RefReleaseOp op) { - return op.emitError("release not supported"); - } - LogicalResult visitStmt(RefReleaseInitialOp op) { - return op.emitError("release_initial not supported"); - } + // Force and release operations: collect for later synthesis. + LogicalResult visitStmt(RefForceOp op); + LogicalResult visitStmt(RefForceInitialOp op); + LogicalResult visitStmt(RefReleaseOp op); + LogicalResult visitStmt(RefReleaseInitialOp op); private: /// Map from probe-typed Value's to their non-probe equivalent. DenseMap probeToHWMap; + DenseMap targetToForceCtrlWire; /// Forceable operations to demote. SmallVector forceables; @@ -186,6 +278,120 @@ class ProbeVisitor : public FIRRTLVisitor { /// Read-only copy of inner-ref namespace for resolving inner refs. hw::InnerRefNamespace &irn; + + /// Map from RWProbe values to all force/release accesses targeting them. + MapVector> forceReleaseMap; + + /// Reduced local control per probe (see `LocalCtrl`). + DenseMap localCtrlMap; + + /// Per-target force/release plans produced by `analyzeForcePlans`, ordered + /// deterministically (`forceReleaseMap` order). + SmallVector forcePlans; + + /// Probes that are exported through a forceable port. Their control wires + /// are driven by the sequential post-pass (merging local + inbound), so + /// `materializeForcePlans` must skip them to avoid a second driver. + SmallPtrSet exportedProbes; + + /// Cached types for creating force control bundles + UIntType u1Type; + ClockType clkType; + + /// Cache for bundle types to avoid creating duplicates + DenseMap bundleTypeCache; + + /// Reduce the priority-ordered force/release accesses for one target into a + /// single `LocalCtrl`. Builds the `forceWins` arbitration and the + /// OR-reductions of the force/release predicates. `builder`'s insertion + /// point is used for the constants and the per-access + /// `forceWins`/`forcedValue` muxes (inserted after each access op); the final + /// AND/OR reductions are emitted at `builder`'s current point on return. + /// `clk` is null when every contributing access is an `_initial` variant. + LocalCtrl reduceAccesses(ImplicitLocOpBuilder &builder, + FIRRTLBaseType probedType, + ArrayRef accesses); + + /// Reduce every target's priority-ordered accesses (`forceReleaseMap`) into a + /// `LocalCtrl` stored in `localCtrlMap`. Emits the arbitration + /// muxes/OR-reductions but makes no materialization decisions (registers, + /// wires, ports). + LogicalResult reduceForceReleaseControl(FModuleLike mod); + + /// Classify every forced target into a `ForcePlan` (`forcePlans`) recording + /// its `ForceCategory` and reduced control, without emitting any hardware. + /// Runs after the export loop has populated `exportedProbes` / + /// `targetToForceCtrlWire`, so each target's category is known. + void analyzeForcePlans(); + + /// Realize each `ForcePlan`: + /// * Local -> `buildStateMachineRegisters` (no control wire); + /// * InstanceForwarded -> drive the parent's forwarding bundle wire; + /// * Exported -> skipped here (the sequential post-pass merges + /// local + inbound so each field keeps one driver). + LogicalResult materializeForcePlans(); + + /// Synthesize the force/release state machine for a single forceable + /// declaration. Creates the control bundle wire, registers for forced flag + /// and forced value, and inserts the appropriate mux logic. Returns the + /// force control wire, or a null WireOp (with a diagnostic emitted) on error. + WireOp synthesizeForceableStateMachine(FIRRTLBaseType probedType, Value data); + + /// Reduced force/release control fed into the state-machine registers. `clk` + /// is null when every contributing access is an `_initial` variant (which is + /// diagnosed). + struct StateMachineInputs { + Value forceActive; + Value releaseActive; + Value forcedValue; + Value clk; + }; + + /// Emit the two state-machine registers (`forced`, `forcedValue`) and the + /// override mux at `data`'s connect point, driving the registers directly + /// from the supplied control SSA values. Used for purely-local forceable + /// targets, which need no control-bundle wire at all. Returns failure (with + /// a diagnostic emitted) if `data` is read-only or `clk` is null. + LogicalResult buildStateMachineRegisters(FIRRTLBaseType probedType, + Value data, + const StateMachineInputs &in); + + /// Return the control-bundle wire for a forceable target, synthesizing the + /// state machine on first use. Creation can be triggered by a local force, + /// an exported forceable port, or an instance-result forwarding wire, so this + /// single accessor keeps the "did we create it yet?" logic in one place. + /// Returns a null WireOp (with a diagnostic already emitted) on error. + WireOp getOrCreateStateMachine(FIRRTLBaseType probedType, Value hwVal) { + auto it = targetToForceCtrlWire.find(hwVal); + if (it != targetToForceCtrlWire.end()) + return it->second; + auto wire = synthesizeForceableStateMachine(probedType, hwVal); + if (!wire) + return {}; + targetToForceCtrlWire[hwVal] = wire; + return wire; + } + + /// Create the force control bundle type with {forceActive, releaseActive, + /// forcedValue, clk} fields. Uses cached u1Type and clkType, and caches + /// the resulting bundle type to avoid creating duplicates. + BundleType createForceCtrlBundleType(FIRRTLBaseType probedType) { + auto it = bundleTypeCache.find(probedType); + if (it != bundleTypeCache.end()) + return it->second; + + auto *ctx = u1Type.getContext(); + SmallVector elements = { + {StringAttr::get(ctx, "forceActive"), /*isFlip=*/false, u1Type}, + {StringAttr::get(ctx, "releaseActive"), /*isFlip=*/false, u1Type}, + {StringAttr::get(ctx, "forcedValue"), /*isFlip=*/false, probedType}, + {StringAttr::get(ctx, "clk"), /*isFlip=*/false, clkType}, + }; + auto bundleType = BundleType::get(ctx, elements); + + bundleTypeCache[probedType] = bundleType; + return bundleType; + } }; } // end namespace @@ -204,6 +410,9 @@ static Block *getBodyBlock(FModuleLike mod) { /// Visit a module, converting its ports and internals to use hardware signals /// instead of probes. LogicalResult ProbeVisitor::visit(FModuleLike mod) { + auto *ctx = mod->getContext(); + u1Type = UIntType::get(ctx, 1); + clkType = ClockType::get(ctx); // Ports -> new ports without probe-ness. // For all probe ports, insert non-probe duplex values to use // as their replacement while rewriting. Only if has body. @@ -211,8 +420,13 @@ LogicalResult ProbeVisitor::visit(FModuleLike mod) { auto portTypes = mod.getPortTypes(); auto portLocs = mod.getPortLocationsAttr().getAsRange(); + auto portNames = mod.getPortNamesAttr(); SmallVector newPortTypes; + // Collect RWProbe ports: for each port whose original type is an RWProbe, + // record the port index so we can add a control bundle port. + SmallVector rwProbePorts; + wires.reserve(portTypes.size()); newPortTypes.reserve(portTypes.size()); auto *block = getBodyBlock(mod); @@ -232,6 +446,14 @@ LogicalResult ProbeVisitor::visit(FModuleLike mod) { wires.emplace_back(idx, WireOp::create(builder, loc, newType)); probeToHWMap[block->getArgument(idx)] = wires.back().second.getData(); } + // If the original port was an RWProbe, record it so we can add an input + // bundle port (_force_ctrl) carrying {forceActive, releaseActive, + // forcedValue} for driving the remote wire/register target. + if (auto refType = dyn_cast(type.getValue()); + refType.getForceable()) { + rwProbePorts.push_back(idx); + } + } else newPortTypes.push_back(type); } @@ -244,6 +466,10 @@ LogicalResult ProbeVisitor::visit(FModuleLike mod) { .wasInterrupted()) return failure(); + // Reduce each target's accesses to a LocalCtrl before modifying signatures. + if (failed(reduceForceReleaseControl(mod))) + return failure(); + // Update signature and argument types. if (portsToChange) { mod.setPortTypesAttr(ArrayAttr::get(mod->getContext(), newPortTypes)); @@ -263,11 +489,78 @@ LogicalResult ProbeVisitor::visit(FModuleLike mod) { } } - // Delete operations that were converted. + // For each forceable RWProbe port exported through a `ref.define`, gather the + // work needed to plumb an inbound `_force_ctrl` control port. The + // state machine is synthesized here (module-local, so parallel-safe), but the + // port insertion and inbound driving are deferred to a sequential post-pass + // (`runOnOperation`): a module's port count and its instances' port counts + // must be mutated together to stay in lockstep, which is unsafe under the + // parallel module walk. See `ExportEntry`. + if (block && !rwProbePorts.empty()) { + for (unsigned portIdx : rwProbePorts) { + auto rwProbe = block->getArgument(portIdx); + // Find the ref.define that exports the local target out of this port. + RefDefineOp refDef; + for (auto *o : rwProbe.getUsers()) + if (auto rd = dyn_cast(o)) { + refDef = rd; + break; + } + + if (!refDef) + // The port is consumed in a way we cannot route force control through + // (e.g. only via ref.sub). Diagnose rather than assert. + return mod->emitError( + "forceable probe port cannot be lowered: no ref.define " + "exporting a local target for port ") + << cast(portNames[portIdx]).getValue(); + + auto outSrc = refDef.getSrc(); + auto probedType = type_cast( + cast(cast(portTypes[portIdx]).getValue()) + .getType()); + + // localCtrlMap is keyed by hardware values; translate the probe-typed + // outSrc through probeToHWMap to get the canonical key. + auto hwSrcIt = probeToHWMap.find(outSrc); + assert(hwSrcIt != probeToHWMap.end() && + "exported forceable target has no hardware value"); + Value hwSrc = hwSrcIt->second; + + // Ensure the exported target has a state machine: a probe that is only + // forced from outside (never locally) has no entry yet. + if (!getOrCreateStateMachine(probedType, hwSrc)) + return failure(); + + // Record the deferred port/instance plumbing for the sequential pass, + // which is this probe's single driver (merging local + inbound). The + // local control is optional — when absent, an all-null LocalCtrl is + // stored and treated as constant 0 in the uniform merge. + exportedProbes.insert(hwSrc); + auto localIt = localCtrlMap.find(hwSrc); + exportWork.push_back( + {portIdx, cast(portNames[portIdx]), + mod.getPortLocation(portIdx), probedType, + targetToForceCtrlWire[hwSrc], + localIt != localCtrlMap.end() ? localIt->second : LocalCtrl{}}); + } + } + + // Now that the export loop has classified which targets are exported / + // instance-forwarded, build a per-target `ForcePlan`. + analyzeForcePlans(); + + // Realize each plan (local SM registers, or drive an instance forwarding + // wire; exported plans are handled by the sequential post-pass). + if (failed(materializeForcePlans())) + return failure(); + for (auto *op : llvm::reverse(toDelete)) op->erase(); - // Demote forceable's. + // Demote forceable declarations: their force/release effect is now carried by + // the synthesized state machine and override mux, so the rwprobe result type + // must not leak into the output. for (auto fop : forceables) firrtl::detail::replaceWithNewForceability(fop, false); @@ -412,6 +705,40 @@ LogicalResult ProbeVisitor::visitDecl(WireOp op) { toDelete.push_back(op); return success(); } +/// Subfield-extract one named element from a bundle-typed value. +static Value getBundleField(ImplicitLocOpBuilder &builder, Value bundle, + StringRef fieldName) { + auto bundleType = type_cast(bundle.getType()); + auto idx = bundleType.getElementIndex(fieldName); + assert(idx && "field not found in bundle"); + return SubfieldOp::create(builder, bundle, *idx); +} + +/// The four fields of a `_force_ctrl` control bundle, read or to-be-connected +/// as SSA values. Centralizes the repeated field access for the cross-module +/// control path. +struct ForceCtrlFields { + Value forceActive; + Value releaseActive; + Value forcedValue; + Value clk; +}; + +/// Read all four fields of a `_force_ctrl` control bundle into a +/// `ForceCtrlFields`. +static ForceCtrlFields readForceCtrlFields(ImplicitLocOpBuilder &builder, + Value bundle) { + return {getBundleField(builder, bundle, "forceActive"), + getBundleField(builder, bundle, "releaseActive"), + getBundleField(builder, bundle, "forcedValue"), + getBundleField(builder, bundle, "clk")}; +} + +/// Create a 1-bit UInt constant (0 or 1). +static Value createU1Const(ImplicitLocOpBuilder &builder, bool value) { + return builder.createOrFold( + APSInt(APInt(1, value ? 1 : 0, /*isSigned=*/false), /*isUnsigned=*/true)); +} LogicalResult ProbeVisitor::visitActiveForceableDecl(Forceable fop) { assert(fop.isForceable() && "must be called on active forceables"); @@ -432,23 +759,183 @@ LogicalResult ProbeVisitor::visitActiveForceableDecl(Forceable fop) { data = wire.getData(); } probeToHWMap[fop.getDataRef()] = data; + + // The force/release state machine is synthesized lazily, only if this target + // is actually forced/released (locally via forceReleaseMap, or from outside + // via a forceable port). + return success(); +} + +WireOp ProbeVisitor::synthesizeForceableStateMachine(FIRRTLBaseType probedType, + Value data) { + Location loc = data.getLoc(); + + ImplicitLocOpBuilder builder(loc, data.getContext()); + + auto bundleType = createForceCtrlBundleType(probedType); + + auto fModule = getParentModule(data); + assert(fModule && "Expected to find parent FModuleOp"); + + builder.setInsertionPointToStart(fModule.getBodyBlock()); + auto forceCntrlWire = WireOp::create(builder, bundleType); + // The state-machine registers read their control inputs from the bundle wire + // fields; the wire itself is driven later (once) by the sequential post-pass. + // Cross-module targets (exported ports / instance forwarding) need this wire; + // purely-local targets use buildStateMachineRegisters directly and create no + // wire. + StateMachineInputs in; + auto fields = readForceCtrlFields(builder, forceCntrlWire.getData()); + in.forceActive = fields.forceActive; + in.releaseActive = fields.releaseActive; + in.forcedValue = fields.forcedValue; + in.clk = fields.clk; + + if (failed(buildStateMachineRegisters(probedType, data, in))) + return {}; + + return forceCntrlWire; +} + +LogicalResult +ProbeVisitor::buildStateMachineRegisters(FIRRTLBaseType probedType, Value data, + const StateMachineInputs &in) { + Location loc = data.getLoc(); + ImplicitLocOpBuilder builder(loc, data.getContext()); + + auto fModule = getParentModule(data); + assert(fModule && "Expected to find parent FModuleOp"); + + Value forceActive = in.forceActive; + Value releaseActive = in.releaseActive; + Value forcedValue = in.forcedValue; + Value fClk = in.clk; + if (!fClk) + return mlir::emitError(loc, "cannot synthesize force/release: no clock " + "available (all accesses are `_initial`)"); + + /* + when (forceActive) { + forced := true.B + forcedValue := forceValue + }.elsewhen(releaseActive) { + forced := false.B + } + + when (forced) { + probe := forcedValue + } + */ + if (auto *defOp = data.getDefiningOp()) { + builder.setInsertionPointAfter(defOp); + } else { + auto blockArg = cast(data); + builder.setInsertionPointToStart(blockArg.getOwner()); + } + + // Look for a reset port so the synthesized registers start at 0 rather than + // X in simulation. Only ground types with a known bit-width can produce a + // typed reset-value constant; fall back to no-reset otherwise. + // RegResetOp accepts AnyResetType directly (sync or async), so no cast is + // needed — the FIRRTL type encodes the reset flavour. + Value resetSig; + int64_t probedWidth = probedType.getBitWidthOrSentinel(); + for (auto [i, portAttr] : llvm::enumerate(fModule.getPortNamesAttr())) { + if (cast(portAttr).getValue() == "reset") { + resetSig = fModule.getBodyBlock()->getArgument(i); + break; + } + } + + // Create the FIRRTL registers first so their results can be referenced + // directly in the next-state muxes below. FIRRTL registers are driven by + // connects rather than SSA next-state operands, so self-reference is legal. + Value forcedReg, forcedValueReg; + if (resetSig && probedWidth >= 0) { + Value resetValI1 = createU1Const(builder, false); + Value resetValData = builder.createOrFold( + APSInt(APInt(probedWidth, 0, /*isSigned=*/false), /*isUnsigned=*/true)); + forcedReg = RegResetOp::create(builder, u1Type, fClk, resetSig, resetValI1, + "forced") + .getResult(); + forcedValueReg = RegResetOp::create(builder, probedType, fClk, resetSig, + resetValData, "forcedValue") + .getResult(); + } else { + forcedReg = RegOp::create(builder, u1Type, fClk, "forced").getResult(); + forcedValueReg = + RegOp::create(builder, probedType, fClk, "forcedValue").getResult(); + } + + // Build next-state muxes referencing the register results directly. The + // reduced control values (forceActive/releaseActive/forcedValue) for a + // purely-local target are computed at the end of the block, so build the + // next-state muxes and their feedback connects there to keep SSA dominance. + builder.setInsertionPointToEnd(fModule.getBodyBlock()); + Value cZero = createU1Const(builder, false); + Value cOne = createU1Const(builder, true); + Value forcedNext = builder.createOrFold( + forceActive, cOne, + builder.createOrFold(releaseActive, cZero, forcedReg)); + Value forcedValueNext = + builder.createOrFold(forceActive, forcedValue, forcedValueReg); + + // Close the feedback loop: connect next-state muxes back into the registers. + builder.create(forcedReg, forcedNext); + builder.create(forcedValueReg, forcedValueNext); + + Value defaultSrc; + Operation *existingConnect = nullptr; + + auto target = data; + if (foldFlow(target) == Flow::Source) { + mlir::emitError(loc, "cannot synthesize force/release: target is read-only " + "(source flow) and cannot be driven"); + return failure(); + } + + for (auto &use : target.getUses()) { + auto fconn = dyn_cast(use.getOwner()); + if (fconn && fconn.getDest() == target) { + existingConnect = fconn; + defaultSrc = fconn.getSrc(); + break; + } + } + + if (!defaultSrc) + defaultSrc = builder.createOrFold(probedType); + if (existingConnect) + existingConnect->erase(); + + builder.setInsertionPointToEnd(fModule.getBodyBlock()); + MatchingConnectOp::create( + builder, data, + builder.createOrFold(forcedReg, forcedValueReg, defaultSrc)); + return success(); } -LogicalResult ProbeVisitor::visitInstanceLike(Operation *op) { +LogicalResult ProbeVisitor::visitInstanceLike(FInstanceLike oldInst) { SmallVector newTypes; - auto needsConv = mapRange(op->getResultTypes(), op->getLoc(), newTypes); + auto needsConv = + mapRange(oldInst->getResultTypes(), oldInst->getLoc(), newTypes); if (failed(needsConv)) return failure(); + if (!*needsConv) return success(); - // New instance with converted types. - // Move users of unconverted results to the new operation. - ImplicitLocOpBuilder builder(op->getLoc(), op); - auto *newInst = builder.clone(*op); - for (auto [oldResult, newResult, newType] : - llvm::zip_equal(op->getOpResults(), newInst->getOpResults(), newTypes)) { + // Rebuild the instance with the probe result types converted in place. This + // is purely module-local and parallel-safe. Adding the `_force_ctrl` ports + // for forceable probe ports is handled by a sequential post-pass in + // `runOnOperation` (so a module's port count and its instances' port counts + // are mutated together and stay in lockstep). + ImplicitLocOpBuilder builder(oldInst->getLoc(), oldInst); + auto *newInst = builder.clone(*oldInst); + for (auto [idx, oldNewType] : llvm::enumerate(llvm::zip_equal( + oldInst->getOpResults(), newInst->getOpResults(), newTypes))) { + auto [oldResult, newResult, newType] = oldNewType; if (newType == oldResult.getType()) { oldResult.replaceAllUsesWith(newResult); continue; @@ -456,9 +943,73 @@ LogicalResult ProbeVisitor::visitInstanceLike(Operation *op) { newResult.setType(newType); probeToHWMap[oldResult] = newResult; + + // For a forceable probe result, create a module-local forwarding control + // wire now (so the parent's force/release reduction can target it like any + // other control wire). Defer connecting the instance's `_force_ctrl` input + // port to it until the post-pass inserts that port. + auto refType = dyn_cast(oldResult.getType()); + if (refType && refType.getForceable()) { + auto probedType = type_cast(refType.getType()); + auto bundleType = createForceCtrlBundleType(probedType); + OpBuilder::InsertionGuard guard(builder); + builder.setInsertionPointToStart( + oldInst->getParentOfType().getBodyBlock()); + auto forwardingWire = WireOp::create(builder, bundleType); + // Key by the converted hw result (newResult), consistent with all other + // probeToHWMap entries which use the hardware value as the key. + targetToForceCtrlWire[newResult] = forwardingWire; + instancePlumb.push_back( + {newInst, (unsigned)idx, forwardingWire.getData()}); + + // Check if oldResult is used for forcing: either through an output port + // (via RefDefineOp to a BlockArgument output) or by force/release ops. + bool usedForForcing = false; + for (auto *user : oldResult.getUsers()) { + // Check for force/release operations + if (isa(user)) { + usedForForcing = true; + break; + } + // Check for RefDefineOp that connects to an output port + if (auto refDefine = dyn_cast(user)) { + auto dest = refDefine.getDest(); + if (auto blockArg = dyn_cast(dest)) { + // Check if this is an output port + auto parentModule = + dyn_cast(blockArg.getOwner()->getParentOp()); + if (parentModule && + parentModule.getPortDirection(blockArg.getArgNumber()) == + Direction::Out) { + usedForForcing = true; + break; + } + } + } + } + + // If not used for forcing, initialize the control wire with default + // values: forceActive = false, releaseActive = false, forcedValue = + // invalid + if (!usedForForcing) { + ImplicitLocOpBuilder ctrlBuilder(forwardingWire.getLoc(), + forwardingWire); + ctrlBuilder.setInsertionPointAfter(forwardingWire); + auto fields = + readForceCtrlFields(ctrlBuilder, forwardingWire.getData()); + Value falseVal = createU1Const(ctrlBuilder, false); + Value invalidVal = ctrlBuilder.createOrFold(probedType); + // Note: clock field doesn't need initialization as it will be driven + // by the post-pass if needed + ctrlBuilder.create(fields.forceActive, falseVal); + ctrlBuilder.create(fields.releaseActive, falseVal); + ctrlBuilder.create(fields.forcedValue, invalidVal); + } + } } - toDelete.push_back(op); + toDelete.push_back(oldInst); return success(); } @@ -470,7 +1021,6 @@ LogicalResult ProbeVisitor::visitStmt(RefDefineOp op) { // ref.define x, y -> connect map(x), map(y) // Be mindful of connect semantics when considering // placement. - auto newDest = probeToHWMap.at(op.getDest()); auto newSrc = probeToHWMap.at(op.getSrc()); @@ -491,6 +1041,7 @@ LogicalResult ProbeVisitor::visitStmt(RefDefineOp op) { auto builder = ImplicitLocOpBuilder::atBlockEnd(op.getLoc(), destBlock); emitConnect(builder, newDest, newSrc); toDelete.push_back(op); + return success(); } @@ -518,6 +1069,10 @@ LogicalResult ProbeVisitor::visitExpr(RWProbeOp op) { data = wire.getData(); } probeToHWMap[op.getResult()] = data; + + // The force/release state machine for a forceable target is synthesized + // lazily (only when the probe is actually forced/released); here we only + // record the hardware value. return success(); } @@ -589,6 +1144,262 @@ LogicalResult ProbeVisitor::visitExpr(RefSubOp op) { return success(); } +//===----------------------------------------------------------------------===// +// Visitor: Force/Release Synthesis +//===----------------------------------------------------------------------===// + +/// Combine a local-reduced (lF, lR, lV) tuple with an inbound `_force_ctrl` +/// bundle (iF, iR, iV) into a single (oF, oR, oV) tuple. Local force wins +/// over inbound when both fire simultaneously. A purely-inbound target (no +/// local force/release) passes null local values; those are treated as +/// constant 0 / invalid, so the OR/mux fold back to the inbound fields — the +/// same one code path serves both the local+inbound and inbound-only cases. +static void +combineWithInboundCtrl(ImplicitLocOpBuilder &builder, Value localForceActive, + Value localReleaseActive, Value localForceValue, + FIRRTLBaseType probedType, Value inboundBundle, + Value &outForceActive, Value &outReleaseActive, + Value &outForceValue) { + if (!localForceActive) + localForceActive = createU1Const(builder, false); + if (!localReleaseActive) + localReleaseActive = createU1Const(builder, false); + if (!localForceValue) + localForceValue = builder.createOrFold(probedType); + Value iF = getBundleField(builder, inboundBundle, "forceActive"); + Value iR = getBundleField(builder, inboundBundle, "releaseActive"); + Value iV = getBundleField(builder, inboundBundle, "forcedValue"); + outForceActive = builder.createOrFold(localForceActive, iF); + outReleaseActive = builder.createOrFold(localReleaseActive, iR); + outForceValue = + builder.createOrFold(localForceActive, localForceValue, iV); +} + +/// Connect the four fields of a `_force_ctrl` control-bundle wire exactly once +/// from already-computed field values. Emits a diagnostic and fails if `clk` +/// is null (every contributing access was an `_initial` variant, so the +/// synthesized registers would have no clock to run on). +static LogicalResult connectControlWireFields(ImplicitLocOpBuilder &builder, + WireOp controlWire, + Value forceActive, + Value releaseActive, + Value forcedValue, Value clk) { + + if (!clk) + return mlir::emitError(controlWire.getLoc(), + "cannot synthesize force/release: no clock " + "available (all accesses are `_initial`)"); + auto dst = readForceCtrlFields(builder, controlWire.getData()); + builder.createOrFold(dst.forceActive, forceActive); + builder.createOrFold(dst.releaseActive, releaseActive); + builder.createOrFold(dst.forcedValue, forcedValue); + builder.createOrFold(dst.clk, clk); + return success(); +} + +/// Reduce the priority-ordered force/release accesses for one target into a +/// single `LocalCtrl`. See the declaration for insertion-point contract. +ProbeVisitor::LocalCtrl +ProbeVisitor::reduceAccesses(ImplicitLocOpBuilder &builder, + FIRRTLBaseType probedType, + ArrayRef accesses) { + Value cZero = createU1Const(builder, false); + Value cOne = createU1Const(builder, true); + + Value forceWins = cZero; + Value forceValue = builder.createOrFold(probedType); + SmallVector forces, releases; + Value freeRunningClock = {}; + for (auto &access : accesses) { + Value isForceVal = access.isForce() ? cOne : cZero; + builder.setInsertionPointAfter(access.op); + forceWins = builder.createOrFold(access.predicate, isForceVal, + forceWins); + if (!freeRunningClock && access.clock) + freeRunningClock = access.clock; + if (access.isForce()) { + forceValue = builder.createOrFold( + access.predicate, access.forceValue.value(), forceValue); + forces.push_back(access); + } else + releases.push_back(access); + } + builder.setInsertionPointToEnd(builder.getInsertionBlock()); + + // Starting from the first predicate (rather than a cZero seed) let the + // single-predicate case fold to just the predicate value. + auto orReduce = [&](ArrayRef set) -> Value { + if (set.empty()) + return cZero; + Value v = set.front().predicate; + for (auto &a : set.drop_front()) + v = builder.createOrFold(v, a.predicate); + return v; + }; + + // Gate force predicates with forceWins to avoid overwriting forcedValue on + // a cycle where a concurrent higher-priority release fires. + Value forceActive = + forces.empty() + ? Value(cZero) + : builder.createOrFold(orReduce(forces), forceWins); + + // Only clear the forced flag when no higher-priority force fires + // simultaneously. + Value releaseActive = + releases.empty() + ? Value(cZero) + : builder.createOrFold( + orReduce(releases), builder.createOrFold(forceWins)); + + return {forceActive, releaseActive, forceValue, freeRunningClock}; +} + +/// Reduce every target's accesses to a `LocalCtrl`. Makes no materialization +/// decision. +LogicalResult ProbeVisitor::reduceForceReleaseControl(FModuleLike mod) { + auto *block = getBodyBlock(mod); + if (!block || forceReleaseMap.empty()) + return success(); + + for (auto &[hwVal, accesses] : forceReleaseMap) { + if (accesses.empty()) + continue; + + // The forceReleaseMap is keyed by the canonical hardware value (not a + // probe SSA value), so both force and release ops targeting the same wire + // land in the same entry regardless of which firrtl.ref.rwprobe SSA value + // they used as their dest operand. + auto probedType = type_cast(hwVal.getType()); + + Location loc = accesses[0].op->getLoc(); + ImplicitLocOpBuilder builder(loc, mod); + builder.setInsertionPointToStart(block); + + // Materialization (registers / wires / muxes) is deferred to + // `materializeForcePlans`, which runs after `analyzeForcePlans` has + // classified each target. Here we only compute and stash the reduced + // control. + localCtrlMap[hwVal] = reduceAccesses(builder, probedType, accesses); + } + + return success(); +} + +/// Classify each forced target into a `ForcePlan`. +void ProbeVisitor::analyzeForcePlans() { + // Iterate in forceReleaseMap (MapVector) order for determinism. + for (auto &kv : forceReleaseMap) { + Value target = kv.first; + auto localIt = localCtrlMap.find(target); + if (localIt == localCtrlMap.end()) + continue; + + ForcePlan plan; + plan.target = target; + plan.probedType = type_cast(target.getType()); + plan.local = localIt->second; + + if (exportedProbes.contains(target)) { + // Driven by the sequential post-pass (merges local + inbound); recorded + // here only for completeness / determinism. + plan.category = ForceCategory::Exported; + } else if (auto wireIt = targetToForceCtrlWire.find(target); + wireIt != targetToForceCtrlWire.end()) { + // The real SM lives in a child instance; drive the parent's forwarding + // control-bundle wire. + plan.category = ForceCategory::InstanceForwarded; + plan.forwardingWire = wireIt->second; + } else { + // Purely local: registers driven directly from the reduced SSA values. + plan.category = ForceCategory::Local; + } + + forcePlans.push_back(plan); + } +} + +/// Realize each `ForcePlan` with a single dispatch on its category. +LogicalResult ProbeVisitor::materializeForcePlans() { + for (auto &plan : forcePlans) { + auto &l = plan.local; + switch (plan.category) { + case ForceCategory::Exported: + // Driven by the sequential post-pass (merging local + inbound) so each + // control-wire field keeps exactly one driver. + continue; + + case ForceCategory::InstanceForwarded: { + WireOp forceCntrlWire = plan.forwardingWire; + auto *block = forceCntrlWire->getBlock(); + ImplicitLocOpBuilder builder(forceCntrlWire.getLoc(), block, + block->end()); + if (failed(connectControlWireFields(builder, forceCntrlWire, + l.forceActive, l.releaseActive, + l.forcedValue, l.clk))) + return failure(); + continue; + } + + case ForceCategory::Local: + if (failed(buildStateMachineRegisters( + plan.probedType, plan.target, + {l.forceActive, l.releaseActive, l.forcedValue, l.clk}))) + return failure(); + continue; + } + } + + return success(); +} + +//===----------------------------------------------------------------------===// +// Visitor: Force/Release operations +//===----------------------------------------------------------------------===// + +// Collected access ops are held in `forceReleaseMap` until +// `reduceForceReleaseControl` reduces them to a `LocalCtrl` and +// `materializeForcePlans` builds the per-probe state machine, at which point +// the originals are erased. `_initial` variants carry a null clock — they +// have no synchronous component and ride along on a sibling clocked +// access's state machine. + +LogicalResult ProbeVisitor::visitStmt(RefForceOp op) { + Value hwDest = probeToHWMap.lookup(op.getDest()); + assert(hwDest && "forced probe has no hardware mapping"); + forceReleaseMap[hwDest].push_back( + {op, op.getPredicate(), op.getSrc(), op.getClock()}); + toDelete.push_back(op); + return success(); +} + +LogicalResult ProbeVisitor::visitStmt(RefForceInitialOp op) { + Value hwDest = probeToHWMap.lookup(op.getDest()); + assert(hwDest && "forced probe has no hardware mapping"); + forceReleaseMap[hwDest].push_back( + {op, op.getPredicate(), op.getSrc(), /*clock=*/Value()}); + toDelete.push_back(op); + return success(); +} + +LogicalResult ProbeVisitor::visitStmt(RefReleaseOp op) { + Value hwDest = probeToHWMap.lookup(op.getDest()); + assert(hwDest && "released probe has no hardware mapping"); + forceReleaseMap[hwDest].push_back( + {op, op.getPredicate(), std::nullopt, op.getClock()}); + toDelete.push_back(op); + return success(); +} + +LogicalResult ProbeVisitor::visitStmt(RefReleaseInitialOp op) { + Value hwDest = probeToHWMap.lookup(op.getDest()); + assert(hwDest && "released probe has no hardware mapping"); + forceReleaseMap[hwDest].push_back( + {op, op.getPredicate(), std::nullopt, /*clock=*/Value()}); + toDelete.push_back(op); + return success(); +} + //===----------------------------------------------------------------------===// // Pass Infrastructure //===----------------------------------------------------------------------===// @@ -604,16 +1415,145 @@ struct ProbesToSignalsPass void ProbesToSignalsPass::runOnOperation() { CIRCT_DEBUG_SCOPED_PASS_LOGGER(this); - SmallVector ops(getOperation().getOps()); + // Gated-clock conversion is demand-driven: roots are added by the + // per-module visitor, then `tracer.run()` is invoked at the end of each + // module's visit to plumb (base, enable) port pairs across module + // boundaries. Sequential / not thread-safe. + auto &instanceGraph = getAnalysis(); + GatedClockConversion tracer(instanceGraph); + + // `_initial` force/release variants carry no clock, so they are not + // gated-clock roots; they ride along on a sibling clocked access's state + // machine (or are diagnosed if no clocked access exists). + getOperation()->walk([&](Operation *op) { + if (isa(op)) { + if (failed(tracer.addRoot(op))) + return signalPassFailure(); + } else if (auto regOp = dyn_cast(op)) { + if (regOp.isForceable()) + if (failed(tracer.addRoot(op))) + return signalPassFailure(); + } else if (auto regResetOp = dyn_cast(op)) { + if (regResetOp.isForceable()) + if (failed(tracer.addRoot(op))) + return signalPassFailure(); + } + }); + if (failed(tracer.run())) + return signalPassFailure(); hw::InnerRefNamespace irn{getAnalysis(), getAnalysis()}; - auto result = failableParallelForEach(&getContext(), ops, [&](Operation *op) { - ProbeVisitor visitor(irn); - return visitor.visit(cast(op)); - }); + // Per-module deferred force-control plumbing, collected during the parallel + // phase and replayed sequentially below. Each slot is written by exactly one + // thread (its own index), so this is race-free. + struct ModuleWork { + FModuleLike mod; + SmallVector exportWork; + SmallVector instancePlumb; + ModuleWork(FModuleLike mod) : mod(mod) {} + }; + + SmallVector deferred = llvm::to_vector( + llvm::map_range(getOperation().getOps(), + [&](FModuleLike mod) { return ModuleWork(mod); })); + + auto result = + failableParallelForEach(&getContext(), deferred, [&](ModuleWork &w) { + ProbeVisitor visitor(irn); + auto mod = w.mod; + if (failed(visitor.visit(mod))) + return failure(); + w.exportWork = std::move(visitor.exportWork); + w.instancePlumb = std::move(visitor.instancePlumb); + return success(); + }); if (result.failed()) - signalPassFailure(); + return signalPassFailure(); + + // Sequential post-pass: mutate module signatures by inserting the inbound + // `_force_ctrl` ports. + + // (live instance op, forceable result idx) -> parent forwarding control wire. + DenseMap, Value> instForwarding; + for (auto &w : deferred) + for (auto &p : w.instancePlumb) + instForwarding[{p.inst, p.resultIdx}] = p.forwardingWire; + + // Fresh instance graph: the parallel phase rebuilt instances, so the cached + // analysis is stale. + InstanceGraph postIG(getOperation()); + + SmallVector instToErase; + for (auto &w : deferred) { + if (w.exportWork.empty()) + continue; + auto mod = w.mod; + auto *ctx = mod->getContext(); + + // Build the new input ports, all appended at the end. + unsigned appendAt = mod.getNumPorts(); + SmallVector> newPorts; + newPorts.reserve(w.exportWork.size()); + for (auto &e : w.exportWork) { + auto ctrlName = + StringAttr::get(ctx, e.portName.getValue().str() + "_force_ctrl"); + newPorts.emplace_back( + appendAt, + PortInfo(ctrlName, e.controlWire.getData().getType(), Direction::In, + /*symName=*/StringAttr{}, e.portLoc)); + } + mod.insertPorts(newPorts); + + // Drive each exported probe's control wire once. Local control is treated + // uniformly as an OR-input to the inbound bundle: an all-null LocalCtrl (no + // local force) folds back to the plain inbound fields, so there is a single + // code path for both the local+inbound and inbound-only cases. + if (auto fmod = dyn_cast(*mod)) { + auto *block = fmod.getBodyBlock(); + for (auto [j, e] : llvm::enumerate(w.exportWork)) { + Value inbound = block->getArgument(appendAt + j); + ImplicitLocOpBuilder builder(e.portLoc, block, block->end()); + Value fa, ra, fv; + combineWithInboundCtrl(builder, e.local.forceActive, + e.local.releaseActive, e.local.forcedValue, + e.probedType, inbound, fa, ra, fv); + // The clock comes from local control when present, otherwise from the + // inbound bundle (an exported wire target has no local clock). + Value clk = + e.local.clk ? e.local.clk : getBundleField(builder, inbound, "clk"); + if (failed(connectControlWireFields(builder, e.controlWire, fa, ra, fv, + clk))) + return signalPassFailure(); + } + } + + // Insert matching ports on every instance of `mod` and wire each new + // `_force_ctrl` input to the parent's forwarding control wire. + auto *node = postIG.lookup(mod); + SmallVector oldInsts; + for (auto *use : node->uses()) + oldInsts.push_back(use->getInstance()); + for (auto oldInst : oldInsts) { + unsigned origCount = oldInst->getNumResults(); + auto newInst = oldInst.cloneWithInsertedPortsAndReplaceUses(newPorts); + postIG.replaceInstance(oldInst, newInst); + + ImplicitLocOpBuilder builder(newInst->getLoc(), newInst); + builder.setInsertionPointAfter(newInst); + for (auto [j, e] : llvm::enumerate(w.exportWork)) { + auto it = instForwarding.find({oldInst.getOperation(), e.portIdx}); + if (it == instForwarding.end()) + continue; + MatchingConnectOp::create(builder, newInst->getResult(origCount + j), + it->second); + } + instToErase.push_back(oldInst); + } + } + + for (auto *op : instToErase) + op->erase(); } diff --git a/test/Dialect/FIRRTL/probes-to-signals-errors.mlir b/test/Dialect/FIRRTL/probes-to-signals-errors.mlir index c10ed8358be5..7225aa2ccb3a 100644 --- a/test/Dialect/FIRRTL/probes-to-signals-errors.mlir +++ b/test/Dialect/FIRRTL/probes-to-signals-errors.mlir @@ -18,18 +18,6 @@ firrtl.circuit "RefProducer" { // ----- -firrtl.circuit "RejectForce" { - firrtl.module @RejectForce(in %clock: !firrtl.clock, in %val : !firrtl.uint<2>, out %p : !firrtl.rwprobe>) { - %w = firrtl.wire sym @sym : !firrtl.uint<2> - %rwprobe = firrtl.ref.rwprobe <@RejectForce::@sym> : !firrtl.rwprobe> - %c1_ui1 = firrtl.constant 1 : !firrtl.const.uint<1> - // expected-error @below {{force not supported}} - firrtl.ref.force %clock, %c1_ui1, %rwprobe, %val : !firrtl.clock, !firrtl.const.uint<1>, !firrtl.rwprobe>, !firrtl.uint<2> - } -} - -// ----- - firrtl.circuit "ExtOpenAgg" { firrtl.extmodule @ExtOpenAgg( // expected-error @below {{open aggregates not supported, cannot convert type}} diff --git a/test/Dialect/FIRRTL/probes-to-signals-force-release.mlir b/test/Dialect/FIRRTL/probes-to-signals-force-release.mlir new file mode 100644 index 000000000000..abdb5b8525a7 --- /dev/null +++ b/test/Dialect/FIRRTL/probes-to-signals-force-release.mlir @@ -0,0 +1,528 @@ +// RUN: circt-opt --firrtl-probes-to-signals --cse --split-input-file %s | FileCheck %s + +// This test file covers force/release synthesis for the ProbesToSignals pass. +// The pass synthesizes force/release operations into a per-probe state machine: +// - a `forced` register (UInt<1>) tracking whether the target is forced, +// - a `forcedValue` register holding the value to drive when forced, +// - an override mux injected at the target's connect point, and +// - a control bundle wire {forceActive, releaseActive, forcedValue, clk} whose +// fields are driven exactly once by the priority reduction. +// +// Test scenarios covered: +// 1. Force/release of a register in the same module. +// 2. Multiple force/release operations to the same target (priority ordering). +// 3. Mix of clocked force and force_initial (the initial access rides along on +// the clocked access's clock). +// 4. Force/release targeting the same wire via DIFFERENT rwprobe SSA values +// (post-ExpandWhens shape) collapse into one state machine. +// 5. Same split-rwprobe scenario on a register. +// 6. Force + release sharing the SAME rwprobe SSA value (non-bug path). +// 7. A probe exported by a module instantiated more than once (lockstep ports). +// 8. Reset values of the state-machine registers are typed zeros. +// 9. Three-level hierarchy: forces from leaf, middle, and top compose. +// 10. Force + release on a plain (no-reset) register. +// 11. Multiple releases reduced against a single force. +// 12. Un-forced RWProbe. + +// ----- +// TEST 1: Force + release of a register in the same module. + +// CHECK-LABEL: firrtl.module @SameModuleRegisterForceRelease +firrtl.circuit "SameModuleRegisterForceRelease" { + firrtl.module @SameModuleRegisterForceRelease(in %clock: !firrtl.clock,in %enable: !firrtl.uint<1>, in %release: !firrtl.uint<1>, in %value: !firrtl.uint<8>, in %reset: !firrtl.uint<1>) { + %c0 = firrtl.constant 0 : !firrtl.uint<8> + %next = firrtl.constant 1 : !firrtl.uint<8> + // CHECK: %r = firrtl.regreset + %r, %r_ref = firrtl.regreset %clock, %reset, %c0 forceable : !firrtl.clock, !firrtl.uint<1>, !firrtl.uint<8>, !firrtl.uint<8>, !firrtl.rwprobe> + firrtl.matchingconnect %r, %next : !firrtl.uint<8> + + // Force, then release. + firrtl.ref.force %clock, %enable, %r_ref, %value : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + firrtl.ref.release %clock, %release, %r_ref : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe> + + // The module has a `reset` port, so the state-machine registers are created + // as `regreset` with a synchronous reset to 0 (prevents X-initialization). + // CHECK: %forced = firrtl.regreset {{.+}} : !firrtl.clock, !firrtl.uint<1>, !firrtl.uint<1>, !firrtl.uint<1> + // CHECK: %forcedValue = firrtl.regreset {{.+}} : !firrtl.clock, !firrtl.uint<1>, !firrtl.uint<8>, !firrtl.uint<8> + // forceActive = enable AND forceWins. + // CHECK-DAG: firrtl.and %enable, %{{.+}} + // releaseActive is gated by !forceWins so a concurrent force suppresses it. + // CHECK-DAG: %[[NFW:.+]] = firrtl.not %{{.+}} + // CHECK-DAG: firrtl.and %release, %[[NFW]] + // The register's next-value driver is replaced by the override mux output. + // CHECK: firrtl.mux(%forced, %forcedValue, %{{.+}}) + // CHECK: firrtl.matchingconnect %r, %{{.+}} + } +} + +// ----- +// TEST 2: Multiple force/release to the same wire (priority order). + +// CHECK-LABEL: firrtl.module @MultipleForceReleaseSameWire +firrtl.circuit "MultipleForceReleaseSameWire" { + firrtl.module @MultipleForceReleaseSameWire(in %clock: !firrtl.clock, in %en1: !firrtl.uint<1>, in %en2: !firrtl.uint<1>, in %val1: !firrtl.uint<8>, in %val2: !firrtl.uint<8>) { + // CHECK: %w = firrtl.wire : !firrtl.uint<8> + %w, %w_ref = firrtl.wire forceable : !firrtl.uint<8>, !firrtl.rwprobe> + %c0 = firrtl.constant 0 : !firrtl.uint<8> + firrtl.matchingconnect %w, %c0 : !firrtl.uint<8> + + // First force (lowest priority - earliest in PriorityMux chain). + firrtl.ref.force %clock, %en1, %w_ref, %val1 : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + // Second force. + firrtl.ref.force %clock, %en2, %w_ref, %val2 : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + // Release (highest priority - last in chain). + firrtl.ref.release %clock, %en2, %w_ref : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe> + + // No `reset` port on this module, so the state-machine registers are plain + // `firrtl.reg` (no reset value). + // CHECK: %forced = firrtl.reg {{.+}} : !firrtl.clock, !firrtl.uint<1> + // CHECK: %forcedValue = firrtl.reg {{.+}} : !firrtl.clock, !firrtl.uint<8> + // forceActive ORs the two force predicates before gating with forceWins. + // CHECK-DAG: firrtl.or %en1, %en2 + // Override mux drives the target wire from the original driver (%c0) when unforced. + // CHECK: firrtl.mux(%forced, %forcedValue, %{{.+}}) + // CHECK: firrtl.matchingconnect %w, %{{.+}} + } +} + +// ----- +// TEST 3: Mix of clocked force and force_initial. The initial access has no +// clock and rides along on the clocked access's clock (it is not a gated-clock +// root and does not error). + +// CHECK-LABEL: firrtl.module @MixedClockedAndInitial +firrtl.circuit "MixedClockedAndInitial" { + firrtl.module @MixedClockedAndInitial(in %clock: !firrtl.clock, in %en_clocked: !firrtl.uint<1>, in %en_initial: !firrtl.uint<1>, in %val1: !firrtl.uint<8>, in %val2: !firrtl.uint<8>) { + // CHECK: %w = firrtl.wire : !firrtl.uint<8> + %w, %w_ref = firrtl.wire forceable : !firrtl.uint<8>, !firrtl.rwprobe> + %c0 = firrtl.constant 0 : !firrtl.uint<8> + firrtl.matchingconnect %w, %c0 : !firrtl.uint<8> + + // Clocked force. + firrtl.ref.force %clock, %en_clocked, %w_ref, %val1 : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + // Initial force (no clock). + firrtl.ref.force_initial %en_initial, %w_ref, %val2 : !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + + // CHECK: %forced = firrtl.reg %clock : !firrtl.clock, !firrtl.uint<1> + // CHECK: %forcedValue = firrtl.reg %clock : !firrtl.clock, !firrtl.uint<8> + // Both forces (clocked + initial) OR their predicates into forceActive. + // CHECK-DAG: firrtl.or %en_clocked, %en_initial + // The initial access carries no clock, so the state-machine registers run + // on the clocked access's clock (%clock), used directly (no bundle wire). + } +} + +// ----- +// TEST 4: Force and release target the same wire via DIFFERENT rwprobe SSA +// values (post-ExpandWhens shape). The pass must produce one state machine +// with both forceActive and releaseActive wired correctly. +// +// %w_ref comes from firrtl.wire forceable (visited and mapped in probeToHWMap). +// %w_ref2 comes from an explicit firrtl.ref.rwprobe on the same inner sym +// (also visited and mapped to the same hw value in probeToHWMap). +// +// Expected output after fix: +// - Exactly ONE forced register, ONE forcedValue register. +// - forceActive driven by %force_en (not constant 0). +// - releaseActive driven by a non-constant expression involving %release_en. + +// CHECK-LABEL: firrtl.module @ForceReleaseSplitRWProbes +firrtl.circuit "ForceReleaseSplitRWProbes" { + firrtl.module @ForceReleaseSplitRWProbes( + in %clock: !firrtl.clock, + in %reset: !firrtl.uint<1>, + in %force_en: !firrtl.uint<1>, + in %release_en: !firrtl.uint<1>, + in %val: !firrtl.uint<8>) { + %w, %w_ref = firrtl.wire sym @w_sym forceable : + !firrtl.uint<8>, !firrtl.rwprobe> + %c0 = firrtl.constant 0 : !firrtl.uint<8> + firrtl.matchingconnect %w, %c0 : !firrtl.uint<8> + + // A second rwprobe value for the SAME inner symbol — this is exactly the + // shape firrtl-expand-whens produces when force/release are in separate + // when branches. + %w_ref2 = firrtl.ref.rwprobe <@ForceReleaseSplitRWProbes::@w_sym> : + !firrtl.rwprobe> + + // Force uses %w_ref; release uses %w_ref2. Both target @w_sym. + firrtl.ref.force %clock, %force_en, %w_ref, %val : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + firrtl.ref.release %clock, %release_en, %w_ref2 : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe> + + // Correct output assertions: + + // Exactly ONE forced register and ONE forcedValue register. + // The bug produces two (forced + forced_0); a duplicate would be renamed + // with a numeric suffix, so assert no such second register appears. + // CHECK: %forced = firrtl.regreset {{.+}} : !firrtl.clock, !firrtl.uint<1>, !firrtl.uint<1>, !firrtl.uint<1> + // CHECK-NOT: %forced_{{[0-9]+}} = firrtl.regreset + // CHECK: %forcedValue = firrtl.regreset {{.+}} : !firrtl.clock, !firrtl.uint<1>, !firrtl.uint<8>, !firrtl.uint<8> + // CHECK-NOT: %forcedValue_{{[0-9]+}} = firrtl.regreset + + // forceActive must use %force_en in an AND gate (priority scheme, not constant 0). + // CHECK-DAG: firrtl.and %force_en, %{{.+}} + + // releaseActive must be gated by NOT(forceWins) — both ops must appear. + // CHECK-DAG: firrtl.not %{{.+}} + // CHECK-DAG: firrtl.and %release_en, %{{.+}} + + // Exactly ONE override mux and ONE connect to %w, emitted after the + // control-logic reduction (at the end of the block). + // CHECK: firrtl.mux(%{{.+}}, %{{.+}}, %{{.+}}) + // CHECK: firrtl.matchingconnect %w, %{{.+}} + } +} + +// ----- +// TEST 5: ForceRelease via split rwprobes on a REGISTER (regreset). +// Same bug scenario as Test 4 but targeting a regreset instead of a wire. +// +// Expected: one forced/forcedValue pair, forceActive driven by %force_en, +// releaseActive driven by expression containing %release_en. + +// CHECK-LABEL: firrtl.module @ForceReleaseSplitRWProbesReg +firrtl.circuit "ForceReleaseSplitRWProbesReg" { + firrtl.module @ForceReleaseSplitRWProbesReg( + in %clock: !firrtl.clock, + in %reset: !firrtl.uint<1>, + in %next: !firrtl.uint<8>, + in %force_en: !firrtl.uint<1>, + in %release_en: !firrtl.uint<1>, + in %val: !firrtl.uint<8>) { + %c0 = firrtl.constant 0 : !firrtl.uint<8> + %r, %r_ref = firrtl.regreset sym @r_sym %clock, %reset, %c0 forceable : + !firrtl.clock, !firrtl.uint<1>, !firrtl.uint<8>, + !firrtl.uint<8>, !firrtl.rwprobe> + firrtl.matchingconnect %r, %next : !firrtl.uint<8> + + // Second rwprobe for the same inner sym — different SSA value. + %r_ref2 = firrtl.ref.rwprobe <@ForceReleaseSplitRWProbesReg::@r_sym> : + !firrtl.rwprobe> + + firrtl.ref.force %clock, %force_en, %r_ref, %val : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + firrtl.ref.release %clock, %release_en, %r_ref2 : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe> + + // One state machine. + // CHECK: %forced = firrtl.regreset {{.+}} : !firrtl.clock, !firrtl.uint<1>, !firrtl.uint<1>, !firrtl.uint<1> + // CHECK-NOT: %forced_{{[0-9]+}} = firrtl.regreset + // forceActive uses %force_en in AND (priority scheme). + // CHECK-DAG: firrtl.and %force_en, %{{.+}} + // releaseActive has %release_en gated by NOT(forceWins). + // CHECK-DAG: firrtl.and %release_en, %{{.+}} + // Override mux replaces the existing next-value connect (appears after the + // control-logic reduction, at the end of the block). + // CHECK: firrtl.mux(%{{.+}}, %{{.+}}, %next) + // CHECK: firrtl.matchingconnect %r, %{{.+}} + } +} + +// ----- +// TEST 6: Sanity check — force AND release sharing the SAME %w_ref (the +// non-bug case) must continue to work and produce exactly one state machine. +// This tests that the fix does not break the common path. + +// CHECK-LABEL: firrtl.module @ForceReleaseSameRWProbe +firrtl.circuit "ForceReleaseSameRWProbe" { + firrtl.module @ForceReleaseSameRWProbe( + in %clock: !firrtl.clock, + in %force_en: !firrtl.uint<1>, + in %release_en: !firrtl.uint<1>, + in %val: !firrtl.uint<8>) { + %w, %w_ref = firrtl.wire forceable : + !firrtl.uint<8>, !firrtl.rwprobe> + %c0 = firrtl.constant 0 : !firrtl.uint<8> + firrtl.matchingconnect %w, %c0 : !firrtl.uint<8> + + // Both ops use the same %w_ref — the normal (non-bug) path. + firrtl.ref.force %clock, %force_en, %w_ref, %val : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + firrtl.ref.release %clock, %release_en, %w_ref : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe> + + // CHECK: %forced = firrtl.reg {{.+}} : !firrtl.clock, !firrtl.uint<1> + // CHECK-NOT: %forced_{{[0-9]+}} = firrtl.reg + // CHECK: %forcedValue = firrtl.reg {{.+}} : !firrtl.clock, !firrtl.uint<8> + // CHECK-DAG: firrtl.and %force_en, %{{.+}} + // CHECK-DAG: firrtl.not %{{.+}} + // CHECK-DAG: firrtl.and %release_en, %{{.+}} + // CHECK-DAG: firrtl.matchingconnect %w, %{{.+}} + } +} + +// ----- + +// TEST 7: A forceable RWProbe exported by a module that is instantiated more +// than once. +// The `_force_ctrl` port is added to the module and to *every* instance; the +// module's port count and its instances' port counts must stay in lockstep. +// This is done in a sequential post-pass (the per-module conversion runs in +// parallel), so a module's signature and all of its instances are mutated +// together. + +// CHECK-LABEL: firrtl.module @Child +// The child gets exactly one inbound control port. +// CHECK-SAME: in %probe_out_force_ctrl: !firrtl.bundle, releaseActive: uint<1>, forcedValue: uint<8>, clk: clock> + +firrtl.circuit "MultiInst" { + firrtl.module @Child(out %probe_out: !firrtl.rwprobe>) { + %w, %w_ref = firrtl.wire forceable : !firrtl.uint<8>, !firrtl.rwprobe> + firrtl.ref.define %probe_out, %w_ref : !firrtl.rwprobe> + } + + // CHECK-LABEL: firrtl.module @MultiInst + firrtl.module @MultiInst(in %clock: !firrtl.clock, in %en: !firrtl.uint<1>, in %v: !firrtl.uint<8>) { + // Both instances must carry the matching `_force_ctrl` input port and each + // is driven by its own forwarding control wire. + // CHECK: firrtl.instance a @Child(out probe_out: !firrtl.uint<8>, in probe_out_force_ctrl: !firrtl.bundle<{{.*}}>) + // CHECK: firrtl.matchingconnect %a_probe_out_force_ctrl, %{{.+}} + // CHECK: firrtl.instance b @Child(out probe_out: !firrtl.uint<8>, in probe_out_force_ctrl: !firrtl.bundle<{{.*}}>) + // CHECK: firrtl.matchingconnect %b_probe_out_force_ctrl, %{{.+}} + %a_probe = firrtl.instance a @Child(out probe_out: !firrtl.rwprobe>) + %b_probe = firrtl.instance b @Child(out probe_out: !firrtl.rwprobe>) + firrtl.ref.force %clock, %en, %a_probe, %v : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + firrtl.ref.force %clock, %en, %b_probe, %v : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + } +} + + +// ----- +// TEST 8: Verify reset VALUE is 0 for `forced` (uint<1>) and a typed zero for +// `forcedValue` (uint<16>, non-trivial width to catch width bugs). +// +// CHECK-LABEL: firrtl.module @ForceResetValueIsZero +firrtl.circuit "ForceResetValueIsZero" { + firrtl.module @ForceResetValueIsZero( + in %clock: !firrtl.clock, + in %reset: !firrtl.uint<1>, + in %enable: !firrtl.uint<1>, + in %val: !firrtl.uint<16>) { + %w, %w_ref = firrtl.wire forceable : !firrtl.uint<16>, !firrtl.rwprobe> + %c0 = firrtl.constant 0 : !firrtl.uint<16> + firrtl.matchingconnect %w, %c0 : !firrtl.uint<16> + + firrtl.ref.force %clock, %enable, %w_ref, %val : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<16> + + // The reset values must be zero FIRRTL constants. + // CHECK-DAG: firrtl.constant 0 : !firrtl.uint<1> + // CHECK-DAG: firrtl.constant 0 : !firrtl.uint<16> + // `forced` resets to uint<1> value 0. + // CHECK: %forced = firrtl.regreset {{.+}} : !firrtl.clock, !firrtl.uint<1>, !firrtl.uint<1>, !firrtl.uint<1> + // `forcedValue` resets to uint<16> value 0. + // CHECK: %forcedValue = firrtl.regreset {{.+}} : !firrtl.clock, !firrtl.uint<1>, !firrtl.uint<16>, !firrtl.uint<16> + } +} + + +// ----- +// TEST 9: Three-level hierarchy with a register at the leaf and forces from +// Leaf owns the forceable register and forces it locally; Middle and Top each +// force the same probe through the instance chain. Each module in the chain +// gets a `_force_ctrl` port and merges its local force with the inbound one. + +// CHECK-LABEL: firrtl.circuit "Middle" +firrtl.circuit "Middle" { + // Leaf module with forceable register + // CHECK: firrtl.module @Leaf(out %reg_probe: !firrtl.uint<8> + // The leaf gains an inbound control port for the exported probe. + // CHECK-SAME: in %reg_probe_force_ctrl: !firrtl.bundle, releaseActive: uint<1>, forcedValue: uint<8>, clk: clock> + firrtl.module @Leaf(out %reg_probe: !firrtl.rwprobe>, in %clock: !firrtl.clock, in %data_in: !firrtl.uint<8>, in %enable: !firrtl.uint<1>) { + %reg, %reg_ref = firrtl.reg %clock forceable : !firrtl.clock, !firrtl.uint<8>, !firrtl.rwprobe> + firrtl.matchingconnect %reg, %data_in : !firrtl.uint<8> + firrtl.ref.define %reg_probe, %reg_ref : !firrtl.rwprobe> + firrtl.ref.force %clock, %enable, %reg_ref, %data_in : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + // Leaf builds a state machine for its local register and overrides its + // next-value connect with the override mux. + // CHECK: %forced = firrtl.reg {{.+}} : !firrtl.clock, !firrtl.uint<1> + // CHECK: %forcedValue = firrtl.reg {{.+}} : !firrtl.clock, !firrtl.uint<8> + // CHECK: firrtl.mux(%forced, %forcedValue, %{{.+}}) + // CHECK: firrtl.matchingconnect %reg, %{{.+}} + // The local force (%enable) is merged with the inbound control port + // (local force wins), so forceActive ORs the local predicate with inbound. + // CHECK: firrtl.or %enable, %{{.+}} + // CHECK: firrtl.mux(%enable, %{{.+}}, %{{.+}}) + } + + // Middle level module that instantiates Leaf and can force the register + // CHECK-LABEL: firrtl.module @Middle + // Middle gains its own inbound control port and drives the Leaf instance's + // control port from its forwarding wire. + // CHECK-SAME: in %reg_probe_out_force_ctrl: !firrtl.bundle + // CHECK: firrtl.instance leaf @Leaf( + // CHECK-SAME: in reg_probe_force_ctrl: !firrtl.bundle + // CHECK: firrtl.matchingconnect %leaf_reg_probe_force_ctrl, %{{.+}} + // Middle's local force (%enable_middle) is merged with its inbound port. + // CHECK: firrtl.or %enable_middle, %{{.+}} + firrtl.module @Middle(out %reg_probe_out: !firrtl.rwprobe>, in %clock: !firrtl.clock, in %data_in: !firrtl.uint<8>, in %enable_middle: !firrtl.uint<1>, in %value_middle: !firrtl.uint<8>) { + %leaf_probe, %leaf_clock, %leaf_data, %leaf_enable = firrtl.instance leaf @Leaf(out reg_probe: !firrtl.rwprobe>, in clock: !firrtl.clock, in data_in: !firrtl.uint<8>, in enable: !firrtl.uint<1>) + firrtl.matchingconnect %leaf_clock, %clock : !firrtl.clock + firrtl.matchingconnect %leaf_data, %data_in : !firrtl.uint<8> + %c1 = firrtl.constant 1 : !firrtl.uint<1> + firrtl.matchingconnect %leaf_enable, %c1 : !firrtl.uint<1> + + // // Force from middle level + firrtl.ref.force %clock, %enable_middle, %leaf_probe, %value_middle : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + + // // Pass probe up to parent + firrtl.ref.define %reg_probe_out, %leaf_probe : !firrtl.rwprobe> + } + + // Top level module that instantiates Middle and can also force the register + // CHECK-LABEL: firrtl.module @ThreeLevelHierarchy + firrtl.module @ThreeLevelHierarchy(in %clock: !firrtl.clock, in %data_in: !firrtl.uint<8>, in %enable_middle: !firrtl.uint<1>, in %value_middle: !firrtl.uint<8>, in %enable_top: !firrtl.uint<1>, in %value_top: !firrtl.uint<8>) { + %middle_probe, %middle_clock, %middle_data, %middle_enable, %middle_value = firrtl.instance middle @Middle(out reg_probe_out: !firrtl.rwprobe>, in clock: !firrtl.clock, in data_in: !firrtl.uint<8>, in enable_middle: !firrtl.uint<1>, in value_middle: !firrtl.uint<8>) + firrtl.matchingconnect %middle_clock, %clock : !firrtl.clock + firrtl.matchingconnect %middle_data, %data_in : !firrtl.uint<8> + firrtl.matchingconnect %middle_enable, %enable_middle : !firrtl.uint<1> + firrtl.matchingconnect %middle_value, %value_middle : !firrtl.uint<8> + + // Force from top level - same register probe that middle level forces + firrtl.ref.force %clock, %enable_top, %middle_probe, %value_top : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + // The top-level force drives the Middle instance's inbound control port via + // a forwarding wire; Top itself is not exported so it needs no control port. + // CHECK: firrtl.instance middle @Middle( + // CHECK-SAME: in reg_probe_out_force_ctrl: !firrtl.bundle + // CHECK: firrtl.matchingconnect %middle_reg_probe_out_force_ctrl, %{{.+}} + // forceActive on the forwarding wire is driven from %enable_top. + // CHECK: firrtl.matchingconnect %{{.+}}, %enable_top : !firrtl.uint<1> + } +} + + +// ----- +// TEST 10: Force on a RegOp (plain register, no reset) with a release. +// The existing connect on the register (next-value connect) must be found +// and replaced by the override mux; no second driver. + +// CHECK-LABEL: firrtl.circuit "PlainRegForceRelease" +firrtl.circuit "PlainRegForceRelease" { + firrtl.module @PlainRegForceRelease( + in %clock: !firrtl.clock, + in %next: !firrtl.uint<8>, + in %en_force: !firrtl.uint<1>, + in %en_release: !firrtl.uint<1>, + in %val: !firrtl.uint<8>) { + %r, %r_ref = firrtl.reg %clock forceable : + !firrtl.clock, !firrtl.uint<8>, !firrtl.rwprobe> + firrtl.matchingconnect %r, %next : !firrtl.uint<8> + + firrtl.ref.force %clock, %en_force, %r_ref, %val : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + firrtl.ref.release %clock, %en_release, %r_ref : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe> + + // Existing next-value connect is replaced by the override mux. + // CHECK: %r = firrtl.reg %clock + // Release gated by !forceWins. + // CHECK-DAG: %[[NFW:.+]] = firrtl.not %{{.+}} + // CHECK-DAG: firrtl.and %en_release, %[[NFW]] + // Override mux: mux(forced, forcedValue, next), emitted at end of block. + // CHECK: firrtl.mux(%{{.+}}, %{{.+}}, %next) + // Exactly ONE connect to %r (the override mux output). + // CHECK: firrtl.matchingconnect %r, %{{.+}} + } +} + +// ----- +// TEST 11: Multiple releases with a single force. +// releaseActive = OR(all release preds) AND NOT(forceWins). +// forceActive = OR(force preds) AND forceWins. + +// CHECK-LABEL: firrtl.circuit "MultipleReleasesSingleForce" +firrtl.circuit "MultipleReleasesSingleForce" { + firrtl.module @MultipleReleasesSingleForce( + in %clock: !firrtl.clock, + in %en_f: !firrtl.uint<1>, + in %en_r1: !firrtl.uint<1>, + in %en_r2: !firrtl.uint<1>, + in %en_r3: !firrtl.uint<1>, + in %val: !firrtl.uint<8>) { + %w, %w_ref = firrtl.wire forceable : !firrtl.uint<8>, !firrtl.rwprobe> + %c0 = firrtl.constant 0 : !firrtl.uint<8> + firrtl.matchingconnect %w, %c0 : !firrtl.uint<8> + + firrtl.ref.force %clock, %en_f, %w_ref, %val : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + firrtl.ref.release %clock, %en_r1, %w_ref : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe> + firrtl.ref.release %clock, %en_r2, %w_ref : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe> + firrtl.ref.release %clock, %en_r3, %w_ref : + !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe> + + // releaseActive = (en_r1 OR en_r2 OR en_r3) AND NOT(forceWins). + // CHECK-DAG: firrtl.or %en_r1, %en_r2 + // CHECK-DAG: firrtl.or %{{.+}}, %en_r3 + // forceActive = en_f AND forceWins. + // CHECK-DAG: firrtl.and %en_f, %{{.+}} + } +} + +// ----- + +// TEST 12: This test verifies selective force behavior: a parent module +// instantiates a child module twice, forcing the RWProbe from only one instance +// while merely reading from the other instance's probe (no force/release). + +// CHECK-LABEL: firrtl.circuit "SelectiveForce" +firrtl.circuit "SelectiveForce" { + // Child module exports the RWProbe and gains an inbound control port + // CHECK: firrtl.module @Child(out %probe_out: !firrtl.uint<8>, in %probe_out_force_ctrl: !firrtl.bundle, releaseActive: uint<1>, forcedValue: uint<8>, clk: clock>) + firrtl.module @Child(out %probe_out: !firrtl.rwprobe>) { + %target, %target_ref = firrtl.wire forceable : !firrtl.uint<8>, !firrtl.rwprobe> + %c42 = firrtl.constant 42 : !firrtl.uint<8> + firrtl.matchingconnect %target, %c42 : !firrtl.uint<8> + firrtl.ref.define %probe_out, %target_ref : !firrtl.rwprobe> + } + + // CHECK: firrtl.module @SelectiveForce + firrtl.module @SelectiveForce( + in %clock: !firrtl.clock, + in %enable: !firrtl.uint<1>, + in %force_value: !firrtl.uint<8>, + out %read_value: !firrtl.uint<8>) { + + // Instance 'b' is NOT forced (only read), so its control wire is initialized with defaults + // CHECK-NEXT: %[[FALSE:.+]] = firrtl.constant 0 : !firrtl.uint<1> + // CHECK-NEXT: %[[INVALID:.+]] = firrtl.invalidvalue : !firrtl.uint<8> + // CHECK-NEXT: %[[B_CTRL:.+]] = firrtl.wire : !firrtl.bundle, releaseActive: uint<1>, forcedValue: uint<8>, clk: clock> + // CHECK-NEXT: %[[B_FA:.+]] = firrtl.subfield %[[B_CTRL]][forceActive] + // CHECK-NEXT: %[[B_RA:.+]] = firrtl.subfield %[[B_CTRL]][releaseActive] + // CHECK-NEXT: %[[B_FV:.+]] = firrtl.subfield %[[B_CTRL]][forcedValue] + // CHECK-NEXT: %[[INVALID2:.+]] = firrtl.invalidvalue : !firrtl.uint<8> + // CHECK-NEXT: firrtl.matchingconnect %[[B_FA]], %[[FALSE]] + // CHECK-NEXT: firrtl.matchingconnect %[[B_RA]], %[[FALSE]] + // CHECK-NEXT: firrtl.matchingconnect %[[B_FV]], %[[INVALID2]] + // Instance 'a' is forced, so its control wire is driven by the force operation + // CHECK-NEXT: %[[A_CTRL:.+]] = firrtl.wire : !firrtl.bundle, releaseActive: uint<1>, forcedValue: uint<8>, clk: clock> + + // CHECK-NEXT: %a_probe_out, %a_probe_out_force_ctrl = firrtl.instance a @Child(out probe_out: !firrtl.uint<8>, in probe_out_force_ctrl: !firrtl.bundle, releaseActive: uint<1>, forcedValue: uint<8>, clk: clock>) + // CHECK-NEXT: firrtl.matchingconnect %a_probe_out_force_ctrl, %[[A_CTRL]] + %a_probe = firrtl.instance a @Child(out probe_out: !firrtl.rwprobe>) + + // CHECK-NEXT: %b_probe_out, %b_probe_out_force_ctrl = firrtl.instance b @Child(out probe_out: !firrtl.uint<8>, in probe_out_force_ctrl: !firrtl.bundle, releaseActive: uint<1>, forcedValue: uint<8>, clk: clock>) + // CHECK-NEXT: firrtl.matchingconnect %b_probe_out_force_ctrl, %[[B_CTRL]] + %b_probe = firrtl.instance b @Child(out probe_out: !firrtl.rwprobe>) + + firrtl.ref.force %clock, %enable, %a_probe, %force_value : !firrtl.clock, !firrtl.uint<1>, !firrtl.rwprobe>, !firrtl.uint<8> + + // Instance 'b' is only read, not forced + // CHECK: firrtl.matchingconnect %read_value, %b_probe_out + %b_read = firrtl.ref.resolve %b_probe : !firrtl.rwprobe> + firrtl.matchingconnect %read_value, %b_read : !firrtl.uint<8> + + // The force operation drives instance 'a's control wire + // CHECK: %[[A_FA:.+]] = firrtl.subfield %[[A_CTRL]][forceActive] + // CHECK-NEXT: %[[A_RA:.+]] = firrtl.subfield %[[A_CTRL]][releaseActive] + // CHECK-NEXT: %[[A_FV:.+]] = firrtl.subfield %[[A_CTRL]][forcedValue] + // CHECK-NEXT: %[[A_CLK:.+]] = firrtl.subfield %[[A_CTRL]][clk] + // CHECK-NEXT: firrtl.matchingconnect %[[A_FA]], %enable + // CHECK-NEXT: firrtl.matchingconnect %[[A_RA]], %[[FALSE]] + // CHECK-NEXT: firrtl.matchingconnect %[[A_FV]], {{%.+}} + // CHECK-NEXT: firrtl.matchingconnect %[[A_CLK]], %clock + } +} \ No newline at end of file diff --git a/test/Dialect/FIRRTL/probes-to-signals.mlir b/test/Dialect/FIRRTL/probes-to-signals.mlir index f6dd4bbb1f80..76f46bc8d6bd 100644 --- a/test/Dialect/FIRRTL/probes-to-signals.mlir +++ b/test/Dialect/FIRRTL/probes-to-signals.mlir @@ -126,11 +126,13 @@ firrtl.circuit "DbgsMemPort" { // CHECK-LABEL: "ForceableRWProbeExport" firrtl.circuit "ForceableRWProbeExport" { - // CHECK: @ForceableRWProbeExport(out %p: !firrtl.uint<2>) + // CHECK: @ForceableRWProbeExport(out %p: !firrtl.uint<2>, in %p_force_ctrl: !firrtl.bundle, releaseActive: uint<1>, forcedValue: uint<2>, clk: clock>) firrtl.module @ForceableRWProbeExport(out %p : !firrtl.rwprobe>) { - // CHECK-NEXT: %w = firrtl.wire : !firrtl.uint<2> - // CHECK-NEXT: firrtl.matchingconnect %p, %w - // CHECK-NEXT: } + // CHECK-NEXT: %[[CTRL:.+]] = firrtl.wire : !firrtl.bundle, releaseActive: uint<1>, forcedValue: uint<2>, clk: clock> + // CHECK-NEXT: %[[FA:.+]] = firrtl.subfield %[[CTRL]][forceActive] + // CHECK-NEXT: %[[RA:.+]] = firrtl.subfield %[[CTRL]][releaseActive] + // CHECK-NEXT: %[[FV:.+]] = firrtl.subfield %[[CTRL]][forcedValue] + // CHECK-NEXT: %[[CLK:.+]] = firrtl.subfield %[[CTRL]][clk] %w, %w_f = firrtl.wire forceable : !firrtl.uint<2>, !firrtl.rwprobe> firrtl.ref.define %p, %w_f : !firrtl.rwprobe> } @@ -159,11 +161,13 @@ firrtl.circuit "ForceableToRead" { // CHECK-LABEL: "RWProbeOp" firrtl.circuit "RWProbeOp" { - // CHECK: @RWProbeOp(out %p: !firrtl.uint<2>) + // CHECK: @RWProbeOp(out %p: !firrtl.uint<2>, in %p_force_ctrl: !firrtl.bundle, releaseActive: uint<1>, forcedValue: uint<2>, clk: clock>) firrtl.module @RWProbeOp(out %p: !firrtl.rwprobe>) { - // CHECK-NEXT: %w = firrtl.wire sym @sym - // CHECK-NEXT: firrtl.matchingconnect %p, %w - // CHECK-NEXT: } + // CHECK-NEXT: %[[CTRL:.+]] = firrtl.wire : !firrtl.bundle, releaseActive: uint<1>, forcedValue: uint<2>, clk: clock> + // CHECK-NEXT: %[[FA:.+]] = firrtl.subfield %[[CTRL]][forceActive] + // CHECK-NEXT: %[[RA:.+]] = firrtl.subfield %[[CTRL]][releaseActive] + // CHECK-NEXT: %[[FV:.+]] = firrtl.subfield %[[CTRL]][forcedValue] + // CHECK-NEXT: %[[CLK:.+]] = firrtl.subfield %[[CTRL]][clk] %w = firrtl.wire sym @sym : !firrtl.uint<2> %rwprobe = firrtl.ref.rwprobe <@RWProbeOp::@sym> : !firrtl.rwprobe> firrtl.ref.define %p, %rwprobe : !firrtl.rwprobe>