Skip to content

Commit c775dcf

Browse files
authored
[ESI] Snoop op (#8096)
The 'snoop' operation allows something to spy on a channel. It simply exposes the internal signals combinationally.
1 parent 77553d7 commit c775dcf

File tree

8 files changed

+159
-29
lines changed

8 files changed

+159
-29
lines changed

include/circt/Dialect/ESI/ESIChannels.td

+46
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,52 @@ def ChannelTypeImpl : ESI_Type<"Channel"> {
8282
::circt::esi::ChannelSignaling::ValidReady, 0);
8383
}]>,
8484
];
85+
86+
let extraClassDeclaration = [{
87+
/// Consumers are ones which actually absorb tokens. Non-consumer ops
88+
/// include any snooping operations.
89+
static SmallVector<std::reference_wrapper<OpOperand>, 4> getConsumers(
90+
mlir::TypedValue<ChannelType>);
91+
static bool hasOneConsumer(mlir::TypedValue<ChannelType>);
92+
static bool hasNoConsumers(mlir::TypedValue<ChannelType>);
93+
static LogicalResult verifyChannel(mlir::TypedValue<ChannelType>);
94+
95+
/// Get the single consumer of a channel. Returns nullptr if there are zero
96+
/// or more than one.
97+
static OpOperand* getSingleConsumer(mlir::TypedValue<ChannelType>);
98+
}];
99+
}
100+
101+
//===----------------------------------------------------------------------===//
102+
// Snoop operations reveal the internal signals of a channel.
103+
//===----------------------------------------------------------------------===//
104+
105+
def SnoopValidReadyOp : ESI_Physical_Op<"snoop.vr", [InferTypeOpInterface]> {
106+
let summary = "Get the valid, ready, and data signals from a channel";
107+
let description = [{
108+
A snoop allows one to combinationally observe a channel's internal signals.
109+
It does not count as another user of the channel. Useful for constructing
110+
control logic which can be combinationally driven. Also potentially useful
111+
for debugging.
112+
}];
113+
114+
let arguments = (ins ChannelType:$input);
115+
let results = (outs I1:$valid, I1:$ready, AnyType:$data);
116+
let hasVerifier = 1;
117+
let assemblyFormat = [{
118+
$input attr-dict `:` qualified(type($input))
119+
}];
120+
121+
let extraClassDeclaration = [{
122+
/// Infer the return types of this operation.
123+
static LogicalResult inferReturnTypes(MLIRContext *context,
124+
std::optional<Location> loc,
125+
ValueRange operands,
126+
DictionaryAttr attrs,
127+
mlir::OpaqueProperties properties,
128+
mlir::RegionRange regions,
129+
SmallVectorImpl<Type> &results);
130+
}];
85131
}
86132

87133
//===----------------------------------------------------------------------===//

lib/Dialect/ESI/ESIOps.cpp

+26-4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,30 @@ LogicalResult ChannelBufferOp::verify() {
7272
return success();
7373
}
7474

75+
//===----------------------------------------------------------------------===//
76+
// Snoop operation functions.
77+
//===----------------------------------------------------------------------===//
78+
79+
LogicalResult SnoopValidReadyOp::verify() {
80+
ChannelType type = getInput().getType();
81+
if (type.getSignaling() != ChannelSignaling::ValidReady)
82+
return emitOpError("only supports valid-ready signaling");
83+
if (type.getInner() != getData().getType())
84+
return emitOpError("input and output types must match");
85+
return success();
86+
}
87+
88+
LogicalResult SnoopValidReadyOp::inferReturnTypes(
89+
MLIRContext *context, std::optional<Location> loc, ValueRange operands,
90+
DictionaryAttr attrs, mlir::OpaqueProperties properties,
91+
mlir::RegionRange regions, SmallVectorImpl<Type> &results) {
92+
auto i1 = IntegerType::get(context, 1);
93+
results.push_back(i1);
94+
results.push_back(i1);
95+
results.push_back(cast<ChannelType>(operands[0].getType()).getInner());
96+
return success();
97+
}
98+
7599
//===----------------------------------------------------------------------===//
76100
// FIFO functions.
77101
//===----------------------------------------------------------------------===//
@@ -159,10 +183,8 @@ LogicalResult WrapValidReadyOp::verify() {
159183
mlir::TypedValue<ChannelType> chanOut = getChanOutput();
160184
if (chanOut.getType().getSignaling() != ChannelSignaling::ValidReady)
161185
return emitOpError("only supports valid-ready signaling");
162-
if (!chanOut.hasOneUse() && !chanOut.getUses().empty()) {
163-
llvm::errs() << "chanOut: " << chanOut.getLoc() << "\n";
164-
return emitOpError("only supports zero or one use");
165-
}
186+
if (failed(ChannelType::verifyChannel(chanOut)))
187+
return failure();
166188
return success();
167189
}
168190

lib/Dialect/ESI/ESITypes.cpp

+43
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//===----------------------------------------------------------------------===//
1313

1414
#include "circt/Dialect/ESI/ESITypes.h"
15+
#include "circt/Dialect/ESI/ESIOps.h"
1516
#include "circt/Dialect/HW/HWTypes.h"
1617
#include "mlir/IR/Attributes.h"
1718
#include "mlir/IR/DialectImplementation.h"
@@ -24,6 +25,48 @@ using namespace circt::esi;
2425

2526
AnyType AnyType::get(MLIRContext *context) { return Base::get(context); }
2627

28+
/// Get the list of users with snoops filtered out. Returns a filtered range
29+
/// which is lazily constructed.
30+
static auto getChannelConsumers(mlir::TypedValue<ChannelType> chan) {
31+
return llvm::make_filter_range(chan.getUses(), [](auto &use) {
32+
return !isa<SnoopValidReadyOp>(use.getOwner());
33+
});
34+
}
35+
SmallVector<std::reference_wrapper<OpOperand>, 4>
36+
ChannelType::getConsumers(mlir::TypedValue<ChannelType> chan) {
37+
return SmallVector<std::reference_wrapper<OpOperand>, 4>(
38+
getChannelConsumers(chan));
39+
}
40+
bool ChannelType::hasOneConsumer(mlir::TypedValue<ChannelType> chan) {
41+
auto consumers = getChannelConsumers(chan);
42+
if (consumers.empty())
43+
return false;
44+
return ++consumers.begin() == consumers.end();
45+
}
46+
bool ChannelType::hasNoConsumers(mlir::TypedValue<ChannelType> chan) {
47+
return getChannelConsumers(chan).empty();
48+
}
49+
OpOperand *ChannelType::getSingleConsumer(mlir::TypedValue<ChannelType> chan) {
50+
auto consumers = getChannelConsumers(chan);
51+
auto iter = consumers.begin();
52+
if (iter == consumers.end())
53+
return nullptr;
54+
OpOperand *result = &*iter;
55+
if (++iter != consumers.end())
56+
return nullptr;
57+
return result;
58+
}
59+
LogicalResult ChannelType::verifyChannel(mlir::TypedValue<ChannelType> chan) {
60+
auto consumers = getChannelConsumers(chan);
61+
if (consumers.empty() || ++consumers.begin() == consumers.end())
62+
return success();
63+
auto err = chan.getDefiningOp()->emitOpError(
64+
"channels must have at most one consumer");
65+
for (auto &consumer : consumers)
66+
err.attachNote(consumer.getOwner()->getLoc()) << "channel used here";
67+
return err;
68+
}
69+
2770
LogicalResult
2871
WindowType::verify(llvm::function_ref<InFlightDiagnostic()> emitError,
2972
StringAttr name, Type into,

lib/Dialect/ESI/Passes/ESILowerToHW.cpp

+16-4
Original file line numberDiff line numberDiff line change
@@ -159,19 +159,26 @@ struct RemoveWrapUnwrap : public ConversionPattern {
159159
WrapValidReadyOp wrap = dyn_cast<WrapValidReadyOp>(op);
160160
UnwrapValidReadyOp unwrap = dyn_cast<UnwrapValidReadyOp>(op);
161161
if (wrap) {
162-
if (wrap.getChanOutput().getUsers().empty()) {
162+
// Lower away snoop ops.
163+
for (auto user : wrap.getChanOutput().getUsers())
164+
if (auto snoop = dyn_cast<SnoopValidReadyOp>(user))
165+
rewriter.replaceOp(
166+
snoop, {wrap.getValid(), wrap.getReady(), wrap.getRawInput()});
167+
168+
if (ChannelType::hasNoConsumers(wrap.getChanOutput())) {
163169
auto c1 = rewriter.create<hw::ConstantOp>(wrap.getLoc(),
164170
rewriter.getI1Type(), 1);
165171
rewriter.replaceOp(wrap, {nullptr, c1});
166172
return success();
167173
}
168174

169-
if (!wrap.getChanOutput().hasOneUse())
175+
if (!ChannelType::hasOneConsumer(wrap.getChanOutput()))
170176
return rewriter.notifyMatchFailure(
171177
wrap, "This conversion only supports wrap-unwrap back-to-back. "
172178
"Wrap didn't have exactly one use.");
173179
if (!(unwrap = dyn_cast<UnwrapValidReadyOp>(
174-
wrap.getChanOutput().use_begin()->getOwner())))
180+
ChannelType::getSingleConsumer(wrap.getChanOutput())
181+
->getOwner())))
175182
return rewriter.notifyMatchFailure(
176183
wrap, "This conversion only supports wrap-unwrap back-to-back. "
177184
"Could not find 'unwrap'.");
@@ -189,11 +196,16 @@ struct RemoveWrapUnwrap : public ConversionPattern {
189196
valid = wrap.getValid();
190197
data = wrap.getRawInput();
191198
ready = operands[1];
199+
200+
// Lower away snoop ops.
201+
for (auto user : operands[0].getUsers())
202+
if (auto snoop = dyn_cast<SnoopValidReadyOp>(user))
203+
rewriter.replaceOp(snoop, {valid, ready, data});
192204
} else {
193205
return failure();
194206
}
195207

196-
if (!wrap.getChanOutput().hasOneUse())
208+
if (!ChannelType::hasOneConsumer(wrap.getChanOutput()))
197209
return rewriter.notifyMatchFailure(wrap, [](Diagnostic &d) {
198210
d << "This conversion only supports wrap-unwrap back-to-back. "
199211
"Wrap didn't have exactly one use.";

lib/Dialect/ESI/Passes/ESIVerifyConnections.cpp

+4-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77
//===----------------------------------------------------------------------===//
88

9+
#include "circt/Dialect/ESI/ESIOps.h"
910
#include "circt/Dialect/ESI/ESIPasses.h"
1011
#include "circt/Dialect/ESI/ESITypes.h"
1112

@@ -42,14 +43,9 @@ void ESIVerifyConnectionsPass::runOnOperation() {
4243
error.attachNote(user->getLoc()) << "bundle used here";
4344
signalPassFailure();
4445

45-
} else if (isa<ChannelType>(v.getType())) {
46-
if (std::distance(v.getUses().begin(), v.getUses().end()) <= 1)
47-
continue;
48-
mlir::InFlightDiagnostic error =
49-
op->emitError("channels must have at most one use");
50-
for (Operation *user : v.getUsers())
51-
error.attachNote(user->getLoc()) << "channel used here";
52-
signalPassFailure();
46+
} else if (auto cv = dyn_cast<mlir::TypedValue<ChannelType>>(v)) {
47+
if (failed(ChannelType::verifyChannel(cv)))
48+
signalPassFailure();
5349
}
5450
});
5551
}

test/Dialect/ESI/connectivity.mlir

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// RUN: circt-opt %s -verify-diagnostics | circt-opt -verify-diagnostics | FileCheck %s
2+
// RUN: circt-opt %s --verify-esi-connections
23

34
hw.module @Sender(out x: !esi.channel<i1>) {
45
%0 = arith.constant 0 : i1
@@ -38,8 +39,11 @@ hw.module @test(in %clk: !seq.clock, in %rst: i1) {
3839
hw.instance "recv" @Reciever (a: %bufferedChan2: !esi.channel<i1>) -> ()
3940

4041
// CHECK-NEXT: %sender.x_0 = hw.instance "sender" @Sender() -> (x: !esi.channel<i1>)
41-
// CHECK-NEXT: %1 = esi.buffer %clk, %rst, %sender.x_0 {stages = 4 : i64} : i1
42-
// CHECK-NEXT: hw.instance "recv" @Reciever(a: %1: !esi.channel<i1>) -> ()
42+
// CHECK-NEXT: [[R1:%.+]] = esi.buffer %clk, %rst, %sender.x_0 {stages = 4 : i64} : i1
43+
// CHECK-NEXT: hw.instance "recv" @Reciever(a: [[R1]]: !esi.channel<i1>) -> ()
44+
45+
%valid, %ready, %data = esi.snoop.vr %bufferedChan2 : !esi.channel<i1>
46+
// CHECK-NEXT: %valid, %ready, %data = esi.snoop.vr [[R1]] : !esi.channel<i1>
4347

4448
%nullBit = esi.null : !esi.channel<i1>
4549
hw.instance "nullRcvr" @Reciever(a: %nullBit: !esi.channel<i1>) -> ()

test/Dialect/ESI/errors.mlir

+4-2
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ hw.module.extern @Source(out a: !esi.channel<i1>)
164164
hw.module.extern @Sink(in %a: !esi.channel<i1>)
165165

166166
hw.module @Top() {
167-
// expected-error @+1 {{channels must have at most one use}}
167+
// expected-error @+1 {{channels must have at most one consumer}}
168168
%a = hw.instance "src" @Source() -> (a: !esi.channel<i1>)
169169
// expected-note @+1 {{channel used here}}
170170
hw.instance "sink1" @Sink(a: %a: !esi.channel<i1>) -> ()
@@ -203,10 +203,12 @@ hw.module @Top() {
203203
// -----
204204

205205
hw.module @wrap_multi_unwrap(in %a_data: i8, in %a_valid: i1, out a_ready: i1) {
206-
// expected-error @+1 {{'esi.wrap.vr' op only supports zero or one use}}
206+
// expected-error @+1 {{'esi.wrap.vr' op channels must have at most one consumer}}
207207
%a_chan, %a_ready = esi.wrap.vr %a_data, %a_valid : i8
208208
%true = hw.constant true
209+
// expected-note @+1 {{channel used here}}
209210
%ap_data, %ap_valid = esi.unwrap.vr %a_chan, %true : i8
211+
// expected-note @+1 {{channel used here}}
210212
%ab_data, %ab_valid = esi.unwrap.vr %a_chan, %true : i8
211213
hw.output %a_ready : i1
212214
}

test/Dialect/ESI/lowering.mlir

+14-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// RUN: circt-opt %s --lower-esi-to-physical -verify-diagnostics | circt-opt -verify-diagnostics | FileCheck %s
22
// RUN: circt-opt %s --lower-esi-ports -verify-diagnostics | circt-opt -verify-diagnostics | FileCheck --check-prefix=IFACE %s
3-
// RUN: circt-opt %s --lower-esi-to-physical --lower-esi-ports --hw-flatten-io --lower-esi-to-hw -verify-diagnostics | circt-opt -verify-diagnostics | FileCheck --check-prefix=HW %s
3+
// RUN: circt-opt %s --lower-esi-to-physical --lower-esi-ports --hw-flatten-io --lower-esi-to-hw | FileCheck --check-prefix=HW %s
44

55
hw.module.extern @Sender(in %clk: !seq.clock, out x: !esi.channel<i4>, out y: i8) attributes {esi.bundle}
66
hw.module.extern @ArrSender(out x: !esi.channel<!hw.array<4xi64>>) attributes {esi.bundle}
@@ -53,6 +53,7 @@ hw.module @test(in %clk: !seq.clock, in %rst:i1) {
5353
// IFACE-NEXT: %[[#modport4:]] = sv.modport.get %i4ToRecv2 @source : !sv.interface<@IValidReady_i4> -> !sv.modport<@IValidReady_i4::@source>
5454
// IFACE-NEXT: hw.instance "recv2" @Reciever(a: %[[#modport4:]]: !sv.modport<@IValidReady_i4::@source>, clk: %clk: !seq.clock) -> ()
5555

56+
// HW-LABEL: hw.module @test(in %clk : !seq.clock, in %rst : i1)
5657
// After all 3 ESI lowering passes, there shouldn't be any ESI constructs!
5758
// HW-NOT: esi
5859
}
@@ -76,21 +77,25 @@ hw.module @InternRcvr(in %in: !esi.channel<!hw.array<4xi8>>) {}
7677
hw.module @test2(in %clk: !seq.clock, in %rst:i1) {
7778
%ints, %c4 = hw.instance "adder" @add11(clk: %clk: !seq.clock, ints: %ints: !esi.channel<i32>) -> (mutatedInts: !esi.channel<i32>, c4: i4)
7879

80+
%valid, %ready, %data = esi.snoop.vr %ints: !esi.channel<i32>
81+
%xact = comb.and %valid, %ready : i1
82+
7983
%nullBit = esi.null : !esi.channel<i4>
8084
hw.instance "nullRcvr" @Reciever(a: %nullBit: !esi.channel<i4>, clk: %clk: !seq.clock) -> ()
8185

8286
%nullArray = esi.null : !esi.channel<!hw.array<4xi8>>
8387
hw.instance "nullInternRcvr" @InternRcvr(in: %nullArray: !esi.channel<!hw.array<4xi8>>) -> ()
8488
}
8589
// HW-LABEL: hw.module @test2(in %clk : !seq.clock, in %rst : i1) {
86-
// HW: %adder.ints_ready, %adder.mutatedInts, %adder.mutatedInts_valid, %adder.c4 = hw.instance "adder" @add11(clk: %clk: !seq.clock, ints: %adder.mutatedInts: i32, ints_valid: %adder.mutatedInts_valid: i1, mutatedInts_ready: %adder.ints_ready: i1) -> (ints_ready: i1, mutatedInts: i32, mutatedInts_valid: i1, c4: i4)
87-
// HW: [[ZERO:%.+]] = hw.bitcast %c0_i4 : (i4) -> i4
88-
// HW: sv.interface.signal.assign %i4ToNullRcvr(@IValidReady_i4::@data) = [[ZERO]] : i4
89-
// HW: [[ZM:%.+]] = sv.modport.get %{{.+}} @source : !sv.interface<@IValidReady_i4> -> !sv.modport<@IValidReady_i4::@source>
90-
// HW: hw.instance "nullRcvr" @Reciever(a: [[ZM]]: !sv.modport<@IValidReady_i4::@source>, clk: %clk: !seq.clock) -> ()
91-
// HW: %c0_i32 = hw.constant 0 : i32
92-
// HW: [[ZA:%.+]] = hw.bitcast %c0_i32 : (i32) -> !hw.array<4xi8>
93-
// HW: %nullInternRcvr.in_ready = hw.instance "nullInternRcvr" @InternRcvr(in: [[ZA]]: !hw.array<4xi8>, in_valid: %false_0: i1) -> (in_ready: i1)
90+
// HW-NEXT: %adder.ints_ready, %adder.mutatedInts, %adder.mutatedInts_valid, %adder.c4 = hw.instance "adder" @add11(clk: %clk: !seq.clock, ints: %adder.mutatedInts: i32, ints_valid: %adder.mutatedInts_valid: i1, mutatedInts_ready: %adder.ints_ready: i1) -> (ints_ready: i1, mutatedInts: i32, mutatedInts_valid: i1, c4: i4)
91+
// HW-NEXT: [[XACT:%.+]] = comb.and %adder.mutatedInts_valid, %adder.ints_ready : i1
92+
// HW: [[ZERO:%.+]] = hw.bitcast %c0_i4 : (i4) -> i4
93+
// HW: sv.interface.signal.assign %i4ToNullRcvr(@IValidReady_i4::@data) = [[ZERO]] : i4
94+
// HW: [[ZM:%.+]] = sv.modport.get %{{.+}} @source : !sv.interface<@IValidReady_i4> -> !sv.modport<@IValidReady_i4::@source>
95+
// HW: hw.instance "nullRcvr" @Reciever(a: [[ZM]]: !sv.modport<@IValidReady_i4::@source>, clk: %clk: !seq.clock) -> ()
96+
// HW: %c0_i32 = hw.constant 0 : i32
97+
// HW: [[ZA:%.+]] = hw.bitcast %c0_i32 : (i32) -> !hw.array<4xi8>
98+
// HW: %nullInternRcvr.in_ready = hw.instance "nullInternRcvr" @InternRcvr(in: [[ZA]]: !hw.array<4xi8>, in_valid: %false_0: i1) -> (in_ready: i1)
9499

95100
hw.module @twoChannelArgs(in %clk: !seq.clock, in %ints: !esi.channel<i32>, in %foo: !esi.channel<i7>) {
96101
%rdy = hw.constant 1 : i1

0 commit comments

Comments
 (0)