Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions include/circt/Dialect/LLHD/LLHDOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -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
//===----------------------------------------------------------------------===//
Expand Down
26 changes: 23 additions & 3 deletions lib/Conversion/MooreToCore/MooreToCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,25 @@ struct WaitEventOpConversion : public OpConversionPattern<WaitEventOp> {
"mismatched types after clone op");
auto beforeType = cast<IntType>(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) {
Expand All @@ -830,14 +849,15 @@ struct WaitEventOpConversion : public OpConversionPattern<WaitEventOp> {
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,
Expand Down
14 changes: 14 additions & 0 deletions lib/Dialect/Arc/Transforms/LowerCoroutines.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)});
}

Expand Down
10 changes: 10 additions & 0 deletions lib/Dialect/Arc/Transforms/LowerProcesses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
14 changes: 12 additions & 2 deletions lib/Dialect/LLHD/Transforms/Deseq.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResampleOp>())
value = resampleOp.getInput();

// Struct field.
if (auto se = value.getDefiningOp<hw::StructExtractOp>()) {
Value base = se.getInput();
Expand Down Expand Up @@ -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<WaitOp, HaltOp>(op))
if (isa<WaitOp, HaltOp, ResampleOp>(op))
continue;
if (!isMemoryEffectFree(&op)) {
LLVM_DEBUG({
Expand Down
6 changes: 4 additions & 2 deletions lib/Dialect/LLHD/Transforms/HoistSignals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResampleOp>(op))
continue;
else
break;
Expand Down
20 changes: 13 additions & 7 deletions test/Conversion/MooreToCore/basic.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -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 : <i1>
Expand All @@ -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 {
Expand All @@ -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]]
Expand All @@ -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]]
Expand All @@ -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]]
Expand All @@ -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]]
Expand Down
49 changes: 49 additions & 0 deletions test/Dialect/Arc/lower-processes-resample.mlir
Original file line number Diff line number Diff line change
@@ -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
}
51 changes: 51 additions & 0 deletions test/Dialect/LLHD/IR/resample.mlir
Original file line number Diff line number Diff line change
@@ -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
}
29 changes: 29 additions & 0 deletions test/Dialect/LLHD/Transforms/deseq.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading