From d5f65f90078a3b10120c9372ab120e3543a9d4df Mon Sep 17 00:00:00 2001 From: AmurG Date: Wed, 8 Jul 2026 03:18:55 +0000 Subject: [PATCH] [LLHD] Add llhd.resample: pin event-trigger samples to their check block Consecutive edge-sensitive event waits in one process misfire: with two @(negedge clk) waits in sequence, the second and later waits consume on the FIRST wake after arming, including on posedges. The root cause is visible in the IR of a 20-line reproducer on current main: MooreToCore emits, per wait, a check block whose negedge trigger is `comb.and(%before, comb.xor(%after, true))`, where %after is the live sample -- frequently a value captured from outside the process (a hoisted probe or module port). The polarity term `comb.xor(%after, true)` is pure and IDENTICAL across consecutive negedge check blocks, so CSE merges them into the first check block. The merged term is live across the suspension, gets persisted by the coroutine lowering, and every later wait then tests the FIRST wake's sample: a stale-0 clk sample makes the "new value must be 0" negedge term true at a posedge. A blanket per-resume-block rematerialization would be unsound: a genuine pre-wait local temp `x = f(port); @(clk); if (x)` must keep its arm-time value and is indistinguishable post-CSE from the merged artifact. The distinction only exists at creation time, so materialize it there: - New `llhd.resample` op: an identity carrying a MemRead side effect (same rationale as `llhd.current_time`). MLIR CSE merges read-only ops only within a single block, so samples pinned in different check blocks can never merge, and every derived pure chain stays rooted in a block-local op. No folder on the op by design -- the greedy driver must never erase the barrier. - MooreToCore's computeTrigger materializes the "after" sample first and wraps it in `llhd.resample` for edge-sensitive detects (anychange triggers compare against the per-block "before" operand and cannot merge). The LSB projection (IEEE 1800-2017 9.4.2) now happens at comb level, downstream of the marker, so wide-signal edge chains are covered too. - arc-lower-processes folds the markers to their operand right after the per-argument renaming has bound each block's uses to that block's re-entry values: from that point the freshness is structural (per-block roots differ, CSE cannot re-merge). arc-lower-coroutines folds any remaining markers after threading captured values, so no marker survives to LowerArcToLLVM. - Deseq sees through markers (getValueField unwrap plus the side-effect exemption) and HoistSignals treats them as reads, so promotion and probe hoisting are unaffected. Tests: resample.mlir (round-trip, CSE resistance in-block and cross-block), lower-processes-resample.mlir (fold-after-renaming contract), deseq.mlir and hoist-signals.mlir transparency cases, and updated basic.mlir wait_event checks pinning the marker emission. Co-Authored-By: Claude Fable 5 --- include/circt/Dialect/LLHD/LLHDOps.td | 32 ++++++++++++ lib/Conversion/MooreToCore/MooreToCore.cpp | 26 ++++++++-- .../Arc/Transforms/LowerCoroutines.cpp | 14 +++++ lib/Dialect/Arc/Transforms/LowerProcesses.cpp | 10 ++++ lib/Dialect/LLHD/Transforms/Deseq.cpp | 14 ++++- lib/Dialect/LLHD/Transforms/HoistSignals.cpp | 6 ++- test/Conversion/MooreToCore/basic.mlir | 20 +++++--- .../Dialect/Arc/lower-processes-resample.mlir | 49 ++++++++++++++++++ test/Dialect/LLHD/IR/resample.mlir | 51 +++++++++++++++++++ test/Dialect/LLHD/Transforms/deseq.mlir | 29 +++++++++++ .../LLHD/Transforms/hoist-signals.mlir | 26 ++++++++++ 11 files changed, 263 insertions(+), 14 deletions(-) create mode 100644 test/Dialect/Arc/lower-processes-resample.mlir create mode 100644 test/Dialect/LLHD/IR/resample.mlir diff --git a/include/circt/Dialect/LLHD/LLHDOps.td b/include/circt/Dialect/LLHD/LLHDOps.td index 04f5f7b30324..9f1bfaf6def2 100644 --- a/include/circt/Dialect/LLHD/LLHDOps.td +++ b/include/circt/Dialect/LLHD/LLHDOps.td @@ -126,6 +126,38 @@ def IntToTimeOp : LLHDOp<"int_to_time", [Pure]> { let assemblyFormat = "$input attr-dict"; } +def ResampleOp : LLHDOp<"resample", [ + SameOperandsAndResultType, MemoryEffects<[MemRead]> +]> { + let summary = "Mark a value as a live per-wake sample inside a process"; + let description = [{ + Returns its operand unchanged. The operation marks the use of a value as a + sample taken in the block in which the operation resides, for values that + are captured from outside a process or coroutine and consumed by the + event-trigger checks that follow a suspension point. + + Process and coroutine lowering re-bind captured values at every resume + block, so a direct use of a captured value observes the binding of the + most recent re-entry. Pure operations derived from such a value, however, + are subject to CSE across sibling check blocks: two identical polarity + terms (such as the negedge `comb.xor(%clk, true)`) computed in + consecutive check blocks merge into the first block, where the merged + term is live across the suspension and gets persisted -- later checks + would then test the sample of an earlier wake. `llhd.resample` carries a + memory read side effect (like `llhd.current_time`) precisely so that CSE + and code motion cannot merge or hoist samples across blocks; every + derived chain stays rooted in a block-local operation. + + The marker is an identity for simulation semantics. It is folded away by + the passes that establish the per-resume-block bindings + (`arc-lower-processes` and `arc-lower-coroutines`), at which point + block-local freshness is guaranteed structurally. + }]; + let arguments = (ins AnyType:$input); + let results = (outs AnyType:$result); + let assemblyFormat = "$input attr-dict `:` type($input)"; +} + //===----------------------------------------------------------------------===// // Signal Operations //===----------------------------------------------------------------------===// diff --git a/lib/Conversion/MooreToCore/MooreToCore.cpp b/lib/Conversion/MooreToCore/MooreToCore.cpp index b5a41c72b9d4..3eddbada01b4 100644 --- a/lib/Conversion/MooreToCore/MooreToCore.cpp +++ b/lib/Conversion/MooreToCore/MooreToCore.cpp @@ -822,6 +822,25 @@ struct WaitEventOpConversion : public OpConversionPattern { "mismatched types after clone op"); auto beforeType = cast(before.getType()); + // Materialize the live "after" sample and, for edge-sensitive detects, + // pin it to the containing check block with an `llhd.resample` marker. + // The "after" value is frequently a value captured from outside the + // process (a hoisted probe or a module port); the per-resume-block + // renaming in the process/coroutine lowering re-binds *direct* uses of + // such captures at every wake, but pure terms derived from them (the + // negedge `comb.xor(%after, true)` in particular) are identical across + // consecutive check blocks and CSE merges them into the first one, + // where the merged term is persisted across the suspension -- later + // waits would then test the first wake's sample. The marker's read + // effect keeps each check block's derived chain rooted in a + // block-local op; it folds to an identity once the renaming has bound + // the sample to its resume epoch. + auto afterIntType = rewriter.getIntegerType(beforeType.getWidth()); + after = typeConverter->materializeTargetConversion(rewriter, loc, + afterIntType, after); + if (edge != Edge::AnyChange) + after = llhd::ResampleOp::create(rewriter, loc, after); + // 9.4.2 IEEE 1800-2017: An edge event shall be detected only on the LSB // of the expression if (beforeType.getWidth() != 1 && edge != Edge::AnyChange) { @@ -830,14 +849,15 @@ struct WaitEventOpConversion : public OpConversionPattern { IntType::get(rewriter.getContext(), 1, beforeType.getDomain()); before = moore::ExtractOp::create(rewriter, loc, beforeType, before, LSB); - after = moore::ExtractOp::create(rewriter, loc, beforeType, after, LSB); + after = comb::ExtractOp::create(rewriter, loc, rewriter.getI1Type(), + after, LSB); } auto intType = rewriter.getIntegerType(beforeType.getWidth()); before = typeConverter->materializeTargetConversion(rewriter, loc, intType, before); - after = typeConverter->materializeTargetConversion(rewriter, loc, intType, - after); + assert(before.getType() == after.getType() && + "before/after sample width mismatch"); if (edge == Edge::AnyChange) return comb::ICmpOp::create(rewriter, loc, ICmpPredicate::ne, before, diff --git a/lib/Dialect/Arc/Transforms/LowerCoroutines.cpp b/lib/Dialect/Arc/Transforms/LowerCoroutines.cpp index a059e8dfaae9..5a5a8abf234d 100644 --- a/lib/Dialect/Arc/Transforms/LowerCoroutines.cpp +++ b/lib/Dialect/Arc/Transforms/LowerCoroutines.cpp @@ -57,6 +57,7 @@ #include "circt/Dialect/Arc/ArcPasses.h" #include "circt/Dialect/Comb/CombOps.h" #include "circt/Dialect/HW/HWOps.h" +#include "circt/Dialect/LLHD/LLHDOps.h" #include "mlir/Analysis/Liveness.h" #include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h" #include "mlir/Dialect/Func/IR/FuncOps.h" @@ -529,6 +530,19 @@ void LowerCoroutinesPass::runOnOperation() { for (auto defineOp : defineOps) { if (failed(captureValuesAcrossSuspension(defineOp))) return signalPassFailure(); + + // The `llhd.resample` markers protecting live event-trigger samples from + // cross-check-block CSE have done their job: values live across + // suspension points are now threaded as explicit resume block arguments, + // so every derived chain is rooted in a block-local binding and CSE can + // no longer merge samples across check blocks. Fold the markers to their + // operand. (Process-derived coroutines arrive here already folded by + // arc-lower-processes.) + defineOp.getBody().walk([](llhd::ResampleOp resampleOp) { + resampleOp.getResult().replaceAllUsesWith(resampleOp.getInput()); + resampleOp.erase(); + }); + lowerings.insert({defineOp.getSymNameAttr(), analyzeDefinition(defineOp)}); } diff --git a/lib/Dialect/Arc/Transforms/LowerProcesses.cpp b/lib/Dialect/Arc/Transforms/LowerProcesses.cpp index 14334422115e..496e9c3ff6ce 100644 --- a/lib/Dialect/Arc/Transforms/LowerProcesses.cpp +++ b/lib/Dialect/Arc/Transforms/LowerProcesses.cpp @@ -484,6 +484,16 @@ LogicalResult ProcessLowering::run() { renameCoroutineArgument(argIdx, liveness, dominance); } + // The `llhd.resample` markers protecting live event-trigger samples from + // cross-check-block CSE have done their job: the renaming above bound each + // block's uses of the captured values to that block's re-entry arguments, + // so every derived chain is rooted in a block-local binding. Fold the + // markers to their operand. + coroOp.getBody().walk([](llhd::ResampleOp resampleOp) { + resampleOp.getResult().replaceAllUsesWith(resampleOp.getInput()); + resampleOp.erase(); + }); + buildInstance(); return success(); } diff --git a/lib/Dialect/LLHD/Transforms/Deseq.cpp b/lib/Dialect/LLHD/Transforms/Deseq.cpp index 49209649d2af..e2cd622f2cd1 100644 --- a/lib/Dialect/LLHD/Transforms/Deseq.cpp +++ b/lib/Dialect/LLHD/Transforms/Deseq.cpp @@ -108,6 +108,13 @@ static ValueField getValueField(Value value) { if (!value) return {}; + // `llhd.resample` markers are identities whose read effect only guards the + // event-trigger samples against cross-check-block CSE. For the analysis + // they are fully transparent: unwrap them so trigger atoms and projections + // resolve to the underlying sampled value. + while (auto resampleOp = value.getDefiningOp()) + value = resampleOp.getInput(); + // Struct field. if (auto se = value.getDefiningOp()) { Value base = se.getInput(); @@ -359,10 +366,13 @@ void Deseq::deseq() { /// the wait and drive ops that are relevant. bool Deseq::analyzeProcess() { // We can only desequentialize processes with no side-effecting ops besides - // the `WaitOp` or `HaltOp` terminators. + // the `WaitOp` or `HaltOp` terminators. `llhd.resample` markers are exempt: + // their read effect only guards the event-trigger samples against + // cross-check-block CSE; they compute nothing and the analysis sees through + // them (see `getValueField`). for (auto &block : process.getBody()) { for (auto &op : block) { - if (isa(op)) + if (isa(op)) continue; if (!isMemoryEffectFree(&op)) { LLVM_DEBUG({ diff --git a/lib/Dialect/LLHD/Transforms/HoistSignals.cpp b/lib/Dialect/LLHD/Transforms/HoistSignals.cpp index 4d2940564b65..4cb3e3521cc3 100644 --- a/lib/Dialect/LLHD/Transforms/HoistSignals.cpp +++ b/lib/Dialect/LLHD/Transforms/HoistSignals.cpp @@ -140,9 +140,11 @@ void ProbeHoister::hoistProbes() { // We can only hoist probes that have no side-effecting ops between // themselves and the beginning of a block. If we see a side-effecting op, - // give up on this block. + // give up on this block. `llhd.resample` markers are exempt: their read + // effect only pins an SSA value sample to its block (a CSE barrier); + // as reads they commute with probes and must not block hoisting. if (!probeOp) { - if (isMemoryEffectFree(&op)) + if (isMemoryEffectFree(&op) || isa(op)) continue; else break; diff --git a/test/Conversion/MooreToCore/basic.mlir b/test/Conversion/MooreToCore/basic.mlir index 475a730f7f92..cd60025c0fd9 100644 --- a/test/Conversion/MooreToCore/basic.mlir +++ b/test/Conversion/MooreToCore/basic.mlir @@ -957,9 +957,10 @@ moore.module @WaitEvent() { // CHECK: llhd.wait ([[PRB_A3]] : {{.+}}), ^[[CHECK:.+]] // CHECK: ^[[CHECK]]: // CHECK: [[AFTER:%.+]] = llhd.prb %a + // CHECK: [[LIVE:%.+]] = llhd.resample [[AFTER]] // CHECK: [[TRUE:%.+]] = hw.constant true // CHECK: [[TMP1:%.+]] = comb.xor bin [[BEFORE]], [[TRUE]] - // CHECK: [[TMP2:%.+]] = comb.and bin [[TMP1]], [[AFTER]] + // CHECK: [[TMP2:%.+]] = comb.and bin [[TMP1]], [[LIVE]] // CHECK: cf.cond_br [[TMP2]] moore.wait_event { %0 = moore.read %a : @@ -974,8 +975,9 @@ moore.module @WaitEvent() { // CHECK: llhd.wait ([[PRB_A4]] : {{.+}}), ^[[CHECK:.+]] // CHECK: ^[[CHECK]]: // CHECK: [[AFTER:%.+]] = llhd.prb %a + // CHECK: [[LIVE:%.+]] = llhd.resample [[AFTER]] // CHECK: [[TRUE:%.+]] = hw.constant true - // CHECK: [[TMP1:%.+]] = comb.xor bin [[AFTER]], [[TRUE]] + // CHECK: [[TMP1:%.+]] = comb.xor bin [[LIVE]], [[TRUE]] // CHECK: [[TMP2:%.+]] = comb.and bin [[BEFORE]], [[TMP1]] // CHECK: cf.cond_br [[TMP2]] moore.wait_event { @@ -991,10 +993,11 @@ moore.module @WaitEvent() { // CHECK: llhd.wait ([[PRB_A5]] : {{.+}}), ^[[CHECK:.+]] // CHECK: ^[[CHECK]]: // CHECK: [[AFTER:%.+]] = llhd.prb %a + // CHECK: [[LIVE:%.+]] = llhd.resample [[AFTER]] // CHECK: [[TRUE:%.+]] = hw.constant true // CHECK: [[TMP1:%.+]] = comb.xor bin [[BEFORE]], [[TRUE]] - // CHECK: [[TMP2:%.+]] = comb.and bin [[TMP1]], [[AFTER]] - // CHECK: [[TMP3:%.+]] = comb.xor bin [[AFTER]], [[TRUE]] + // CHECK: [[TMP2:%.+]] = comb.and bin [[TMP1]], [[LIVE]] + // CHECK: [[TMP3:%.+]] = comb.xor bin [[LIVE]], [[TRUE]] // CHECK: [[TMP4:%.+]] = comb.and bin [[BEFORE]], [[TMP3]] // CHECK: [[TMP5:%.+]] = comb.or bin [[TMP2]], [[TMP4]] // CHECK: cf.cond_br [[TMP5]] @@ -1011,8 +1014,9 @@ moore.module @WaitEvent() { // CHECK: llhd.wait ([[PRB_D2]] : {{.+}}), ^[[CHECK:.+]] // CHECK: ^[[CHECK]]: // CHECK: [[AFTER:%.+]] = llhd.prb %d + // CHECK: [[RS:%.+]] = llhd.resample [[AFTER]] // CHECK: [[TMP1:%.+]] = comb.extract [[BEFORE]] from 0 : (i4) -> i1 - // CHECK: [[TMP2:%.+]] = comb.extract [[AFTER]] from 0 : (i4) -> i1 + // CHECK: [[TMP2:%.+]] = comb.extract [[RS]] from 0 : (i4) -> i1 // CHECK: [[TRUE:%.+]] = hw.constant true // CHECK: [[TMP3:%.+]] = comb.xor bin [[TMP1]], [[TRUE]] // CHECK: [[TMP4:%.+]] = comb.and bin [[TMP3]], [[TMP2]] @@ -1030,8 +1034,9 @@ moore.module @WaitEvent() { // CHECK: llhd.wait ([[PRB_D3]] : {{.+}}), ^[[CHECK:.+]] // CHECK: ^[[CHECK]]: // CHECK: [[AFTER:%.+]] = llhd.prb %d + // CHECK: [[RS:%.+]] = llhd.resample [[AFTER]] // CHECK: [[TMP1:%.+]] = comb.extract [[BEFORE]] from 0 : (i4) -> i1 - // CHECK: [[TMP2:%.+]] = comb.extract [[AFTER]] from 0 : (i4) -> i1 + // CHECK: [[TMP2:%.+]] = comb.extract [[RS]] from 0 : (i4) -> i1 // CHECK: [[TRUE:%.+]] = hw.constant true // CHECK: [[TMP3:%.+]] = comb.xor bin [[TMP2]], [[TRUE]] // CHECK: [[TMP4:%.+]] = comb.and bin [[TMP1]], [[TMP3]] @@ -1049,8 +1054,9 @@ moore.module @WaitEvent() { // CHECK: llhd.wait ([[PRB_D4]] : {{.+}}), ^[[CHECK:.+]] // CHECK: ^[[CHECK]]: // CHECK: [[AFTER:%.+]] = llhd.prb %d + // CHECK: [[RS:%.+]] = llhd.resample [[AFTER]] // CHECK: [[TMP1:%.+]] = comb.extract [[BEFORE]] from 0 : (i4) -> i1 - // CHECK: [[TMP2:%.+]] = comb.extract [[AFTER]] from 0 : (i4) -> i1 + // CHECK: [[TMP2:%.+]] = comb.extract [[RS]] from 0 : (i4) -> i1 // CHECK: [[TRUE:%.+]] = hw.constant true // CHECK: [[TMP3:%.+]] = comb.xor bin [[TMP1]], [[TRUE]] // CHECK: [[TMP4:%.+]] = comb.and bin [[TMP3]], [[TMP2]] diff --git a/test/Dialect/Arc/lower-processes-resample.mlir b/test/Dialect/Arc/lower-processes-resample.mlir new file mode 100644 index 000000000000..b0b359a0233a --- /dev/null +++ b/test/Dialect/Arc/lower-processes-resample.mlir @@ -0,0 +1,49 @@ +// RUN: circt-opt --arc-lower-processes %s | FileCheck %s + +// `llhd.resample` markers protect the live "after" samples of consecutive +// edge-sensitive event waits from cross-check-block CSE (the polarity term of +// two negedge waits on the same captured value is otherwise identical and +// merges into the first check block, where it is persisted across the +// suspension). Once this pass has threaded the coroutine arguments through +// the CFG -- binding each resume block's uses to that block's re-entry +// values -- the markers have done their job and must fold to their operand, +// leaving each check block's trigger chain rooted at that block's own +// argument. + +// CHECK-LABEL: arc.coroutine.define @TwoNegedgeWaits.llhd.process +// CHECK-SAME: ([[CLK_ENTRY:%.+]]: i1, [[NOW:%.+]]: i64) +// CHECK-NOT: llhd.resample +hw.module @TwoNegedgeWaits(in %clk: i1) { + %true = hw.constant true + llhd.process { + cf.br ^bb1(%clk : i1) + ^bb1(%before1: i1): + llhd.wait (%clk : i1), ^bb2(%clk : i1) + // First check block: the xor must use this block's own clk binding. + // CHECK: ^[[CHECK1:bb[0-9]+]]([[CLK1:%.+]]: i1, {{%.+}}: i64, [[BEFORE1:%.+]]: i1): + // CHECK: [[X1:%.+]] = comb.xor bin [[CLK1]], %true + // CHECK: [[T1:%.+]] = comb.and bin [[BEFORE1]], [[X1]] + // CHECK: cf.cond_br [[T1]], + ^bb2(%b1: i1): + %0 = llhd.resample %clk : i1 + %1 = comb.xor bin %0, %true : i1 + %2 = comb.and bin %b1, %1 : i1 + cf.cond_br %2, ^bb3, ^bb1(%clk : i1) + ^bb3: + llhd.wait (%clk : i1), ^bb4(%clk : i1) + // Second check block: after folding, the xor must be rebuilt from THIS + // block's clk argument, not reuse the first block's term. + // CHECK: ^[[CHECK2:bb[0-9]+]]([[CLK2:%.+]]: i1, {{%.+}}: i64, [[BEFORE2:%.+]]: i1): + // CHECK: [[X2:%.+]] = comb.xor bin [[CLK2]], %true + // CHECK: [[T2:%.+]] = comb.and bin [[BEFORE2]], [[X2]] + // CHECK: cf.cond_br [[T2]], + ^bb4(%b2: i1): + %3 = llhd.resample %clk : i1 + %4 = comb.xor bin %3, %true : i1 + %5 = comb.and bin %b2, %4 : i1 + cf.cond_br %5, ^bb5, ^bb3 + ^bb5: + llhd.halt + } + hw.output +} diff --git a/test/Dialect/LLHD/IR/resample.mlir b/test/Dialect/LLHD/IR/resample.mlir new file mode 100644 index 000000000000..2f553f5a815a --- /dev/null +++ b/test/Dialect/LLHD/IR/resample.mlir @@ -0,0 +1,51 @@ +// RUN: circt-opt %s | circt-opt | FileCheck %s +// RUN: circt-opt %s --cse | FileCheck %s --check-prefix=CSE + +// `llhd.resample` pins a live sample of a captured value to the block that +// takes it. Its read effect must keep CSE from merging markers (and the pure +// chains rooted at them) across sibling check blocks: merging would let a +// later resume block observe an earlier wake's sample. + +// CHECK-LABEL: hw.module @Roundtrip +hw.module @Roundtrip(in %a : i8) { + // CHECK: llhd.process + llhd.process { + // CHECK: llhd.resample %a : i8 + %0 = llhd.resample %a : i8 + llhd.halt + } + hw.output +} + +// CSE-LABEL: hw.module @KeepAcrossBlocks +hw.module @KeepAcrossBlocks(in %clk : i1) { + %true = hw.constant true + llhd.process { + cf.br ^bb1 + ^bb1: + llhd.wait ^bb2 + ^bb2: + // CSE: ^bb2: + // CSE: [[S1:%.+]] = llhd.resample %clk : i1 + // CSE: [[X1:%.+]] = comb.xor bin [[S1]], %true + // CSE: cf.cond_br [[X1]], + %0 = llhd.resample %clk : i1 + %1 = comb.xor bin %0, %true : i1 + cf.cond_br %1, ^bb3, ^bb1 + ^bb3: + llhd.wait ^bb4 + ^bb4: + // The marker in this block must NOT be merged with ^bb2's, and the xor + // chain must stay rooted at the block-local marker. + // CSE: ^bb4: + // CSE: [[S2:%.+]] = llhd.resample %clk : i1 + // CSE: [[X2:%.+]] = comb.xor bin [[S2]], %true + // CSE: cf.cond_br [[X2]], + %2 = llhd.resample %clk : i1 + %3 = comb.xor bin %2, %true : i1 + cf.cond_br %3, ^bb5, ^bb3 + ^bb5: + llhd.halt + } + hw.output +} diff --git a/test/Dialect/LLHD/Transforms/deseq.mlir b/test/Dialect/LLHD/Transforms/deseq.mlir index 613c1a658d2d..4416806fbe0e 100644 --- a/test/Dialect/LLHD/Transforms/deseq.mlir +++ b/test/Dialect/LLHD/Transforms/deseq.mlir @@ -47,6 +47,35 @@ hw.module @ClockNegEdge(in %clock: i1, in %d: i42) { llhd.drv %3, %1 after %0 if %2 : i42 } +// Same as @ClockNegEdge, but the live clock sample is pinned to the check +// block with an `llhd.resample` marker (the shape MooreToCore emits for +// edge-sensitive waits). The marker is analysis-transparent and must not +// block register inference. +// CHECK-LABEL: @ClockNegEdgeResample( +hw.module @ClockNegEdgeResample(in %clock: i1, in %d: i42) { + %c0_i42 = hw.constant 0 : i42 + %0 = llhd.constant_time <0ns, 1d, 0e> + // CHECK-NOT: llhd.process + // CHECK: [[CLK:%.+]] = seq.to_clock %clock + // CHECK: [[CLK_INV:%.+]] = seq.clock_inv [[CLK]] + // CHECK: [[REG:%.+]] = seq.firreg %d clock [[CLK_INV]] : i42 + %1, %2 = llhd.process -> i42, i1 { + %true = hw.constant true + %false = hw.constant false + cf.br ^bb1(%c0_i42, %false : i42, i1) + ^bb1(%3: i42, %4: i1): + llhd.wait yield (%3, %4 : i42, i1), (%clock : i1), ^bb2(%clock : i1) + ^bb2(%5: i1): + %6 = llhd.resample %clock : i1 + %7 = comb.xor bin %6, %true : i1 + %8 = comb.and bin %5, %7 : i1 // negedge clock + cf.cond_br %8, ^bb1(%d, %true : i42, i1), ^bb1(%c0_i42, %false : i42, i1) + } + %3 = llhd.sig %c0_i42 : i42 + // CHECK: llhd.drv {{%.+}}, [[REG]] after {{%.+}} : + llhd.drv %3, %1 after %0 if %2 : i42 +} + // CHECK-LABEL: @ClockPosEdgeWithActiveLowReset( hw.module @ClockPosEdgeWithActiveLowReset(in %clock: i1, in %reset: i1, in %d: i42) { %c0_i42 = hw.constant 0 : i42 diff --git a/test/Dialect/LLHD/Transforms/hoist-signals.mlir b/test/Dialect/LLHD/Transforms/hoist-signals.mlir index f6ab445dd055..1282093a47c4 100644 --- a/test/Dialect/LLHD/Transforms/hoist-signals.mlir +++ b/test/Dialect/LLHD/Transforms/hoist-signals.mlir @@ -67,6 +67,32 @@ hw.module @DontHoistProbesAcrossSideEffects() { } } +// `llhd.resample` markers only pin an SSA value sample to its block; their +// read effect must not act as a probe-hoisting barrier (reads commute with +// reads). Probes positioned after a marker in a resuming block keep hoisting. +// CHECK-LABEL: @HoistProbesPastResampleMarkers +hw.module @HoistProbesPastResampleMarkers(in %x: i1) { + %c0_i42 = hw.constant 0 : i42 + %a = llhd.sig %c0_i42 : i42 + // CHECK: llhd.sig + // CHECK-NEXT: [[A:%.+]] = llhd.prb %a + // CHECK-NEXT: llhd.process + llhd.process { + cf.br ^bb1 + ^bb1: + llhd.wait ^bb2 + ^bb2: + // CHECK: ^bb2: + // CHECK-NEXT: llhd.resample %x + %0 = llhd.resample %x : i1 + // CHECK-NOT: llhd.prb + %1 = llhd.prb %a : i42 + // CHECK: call @use_i42([[A]]) + func.call @use_i42(%1) : (i42) -> () + llhd.halt + } +} + // CHECK-LABEL: @DontHoistProbesIfLeakingAcrossWait hw.module @DontHoistProbesIfLeakingAcrossWait() { %c0_i42 = hw.constant 0 : i42