From 4088a90578ccd86e2a5bebae626390b731da0c06 Mon Sep 17 00:00:00 2001 From: John Demme Date: Sat, 18 Jan 2025 02:54:38 +0000 Subject: [PATCH] [ESI] Snoop op The 'snoop' operation allows something to spy on a channel. It simply exposes the internal signals combinationally. --- include/circt/Dialect/ESI/ESIChannels.td | 46 +++++++++++++++++++ lib/Dialect/ESI/ESIOps.cpp | 30 ++++++++++-- lib/Dialect/ESI/ESITypes.cpp | 43 +++++++++++++++++ lib/Dialect/ESI/Passes/ESILowerToHW.cpp | 20 ++++++-- .../ESI/Passes/ESIVerifyConnections.cpp | 12 ++--- test/Dialect/ESI/connectivity.mlir | 8 +++- test/Dialect/ESI/errors.mlir | 6 ++- test/Dialect/ESI/lowering.mlir | 23 ++++++---- 8 files changed, 159 insertions(+), 29 deletions(-) diff --git a/include/circt/Dialect/ESI/ESIChannels.td b/include/circt/Dialect/ESI/ESIChannels.td index 3bef93462b2d..8b96a1b8efa5 100644 --- a/include/circt/Dialect/ESI/ESIChannels.td +++ b/include/circt/Dialect/ESI/ESIChannels.td @@ -82,6 +82,52 @@ def ChannelTypeImpl : ESI_Type<"Channel"> { ::circt::esi::ChannelSignaling::ValidReady, 0); }]>, ]; + + let extraClassDeclaration = [{ + /// Consumers are ones which actually absorb tokens. Non-consumer ops + /// include any snooping operations. + static SmallVector, 4> getConsumers( + mlir::TypedValue); + static bool hasOneConsumer(mlir::TypedValue); + static bool hasNoConsumers(mlir::TypedValue); + static LogicalResult verifyChannel(mlir::TypedValue); + + /// Get the single consumer of a channel. Returns nullptr if there are zero + /// or more than one. + static OpOperand* getSingleConsumer(mlir::TypedValue); + }]; +} + +//===----------------------------------------------------------------------===// +// Snoop operations reveal the internal signals of a channel. +//===----------------------------------------------------------------------===// + +def SnoopValidReadyOp : ESI_Physical_Op<"snoop.vr", [InferTypeOpInterface]> { + let summary = "Get the valid, ready, and data signals from a channel"; + let description = [{ + A snoop allows one to combinationally observe a channel's internal signals. + It does not count as another user of the channel. Useful for constructing + control logic which can be combinationally driven. Also potentially useful + for debugging. + }]; + + let arguments = (ins ChannelType:$input); + let results = (outs I1:$valid, I1:$ready, AnyType:$data); + let hasVerifier = 1; + let assemblyFormat = [{ + $input attr-dict `:` qualified(type($input)) + }]; + + let extraClassDeclaration = [{ + /// Infer the return types of this operation. + static LogicalResult inferReturnTypes(MLIRContext *context, + std::optional loc, + ValueRange operands, + DictionaryAttr attrs, + mlir::OpaqueProperties properties, + mlir::RegionRange regions, + SmallVectorImpl &results); + }]; } //===----------------------------------------------------------------------===// diff --git a/lib/Dialect/ESI/ESIOps.cpp b/lib/Dialect/ESI/ESIOps.cpp index f0f361431546..6e54eb33b3dd 100644 --- a/lib/Dialect/ESI/ESIOps.cpp +++ b/lib/Dialect/ESI/ESIOps.cpp @@ -72,6 +72,30 @@ LogicalResult ChannelBufferOp::verify() { return success(); } +//===----------------------------------------------------------------------===// +// Snoop operation functions. +//===----------------------------------------------------------------------===// + +LogicalResult SnoopValidReadyOp::verify() { + ChannelType type = getInput().getType(); + if (type.getSignaling() != ChannelSignaling::ValidReady) + return emitOpError("only supports valid-ready signaling"); + if (type.getInner() != getData().getType()) + return emitOpError("input and output types must match"); + return success(); +} + +LogicalResult SnoopValidReadyOp::inferReturnTypes( + MLIRContext *context, std::optional loc, ValueRange operands, + DictionaryAttr attrs, mlir::OpaqueProperties properties, + mlir::RegionRange regions, SmallVectorImpl &results) { + auto i1 = IntegerType::get(context, 1); + results.push_back(i1); + results.push_back(i1); + results.push_back(cast(operands[0].getType()).getInner()); + return success(); +} + //===----------------------------------------------------------------------===// // FIFO functions. //===----------------------------------------------------------------------===// @@ -159,10 +183,8 @@ LogicalResult WrapValidReadyOp::verify() { mlir::TypedValue chanOut = getChanOutput(); if (chanOut.getType().getSignaling() != ChannelSignaling::ValidReady) return emitOpError("only supports valid-ready signaling"); - if (!chanOut.hasOneUse() && !chanOut.getUses().empty()) { - llvm::errs() << "chanOut: " << chanOut.getLoc() << "\n"; - return emitOpError("only supports zero or one use"); - } + if (failed(ChannelType::verifyChannel(chanOut))) + return failure(); return success(); } diff --git a/lib/Dialect/ESI/ESITypes.cpp b/lib/Dialect/ESI/ESITypes.cpp index 3a378b828c55..78445c75b9d2 100644 --- a/lib/Dialect/ESI/ESITypes.cpp +++ b/lib/Dialect/ESI/ESITypes.cpp @@ -12,6 +12,7 @@ //===----------------------------------------------------------------------===// #include "circt/Dialect/ESI/ESITypes.h" +#include "circt/Dialect/ESI/ESIOps.h" #include "circt/Dialect/HW/HWTypes.h" #include "mlir/IR/Attributes.h" #include "mlir/IR/DialectImplementation.h" @@ -24,6 +25,48 @@ using namespace circt::esi; AnyType AnyType::get(MLIRContext *context) { return Base::get(context); } +/// Get the list of users with snoops filtered out. Returns a filtered range +/// which is lazily constructed. +static auto getChannelConsumers(mlir::TypedValue chan) { + return llvm::make_filter_range(chan.getUses(), [](auto &use) { + return !isa(use.getOwner()); + }); +} +SmallVector, 4> +ChannelType::getConsumers(mlir::TypedValue chan) { + return SmallVector, 4>( + getChannelConsumers(chan)); +} +bool ChannelType::hasOneConsumer(mlir::TypedValue chan) { + auto consumers = getChannelConsumers(chan); + if (consumers.empty()) + return false; + return ++consumers.begin() == consumers.end(); +} +bool ChannelType::hasNoConsumers(mlir::TypedValue chan) { + return getChannelConsumers(chan).empty(); +} +OpOperand *ChannelType::getSingleConsumer(mlir::TypedValue chan) { + auto consumers = getChannelConsumers(chan); + auto iter = consumers.begin(); + if (iter == consumers.end()) + return nullptr; + OpOperand *result = &*iter; + if (++iter != consumers.end()) + return nullptr; + return result; +} +LogicalResult ChannelType::verifyChannel(mlir::TypedValue chan) { + auto consumers = getChannelConsumers(chan); + if (consumers.empty() || ++consumers.begin() == consumers.end()) + return success(); + auto err = chan.getDefiningOp()->emitOpError( + "channels must have at most one consumer"); + for (auto &consumer : consumers) + err.attachNote(consumer.getOwner()->getLoc()) << "channel used here"; + return err; +} + LogicalResult WindowType::verify(llvm::function_ref emitError, StringAttr name, Type into, diff --git a/lib/Dialect/ESI/Passes/ESILowerToHW.cpp b/lib/Dialect/ESI/Passes/ESILowerToHW.cpp index ba6791b19437..7ce91d50d827 100644 --- a/lib/Dialect/ESI/Passes/ESILowerToHW.cpp +++ b/lib/Dialect/ESI/Passes/ESILowerToHW.cpp @@ -159,19 +159,26 @@ struct RemoveWrapUnwrap : public ConversionPattern { WrapValidReadyOp wrap = dyn_cast(op); UnwrapValidReadyOp unwrap = dyn_cast(op); if (wrap) { - if (wrap.getChanOutput().getUsers().empty()) { + // Lower away snoop ops. + for (auto user : wrap.getChanOutput().getUsers()) + if (auto snoop = dyn_cast(user)) + rewriter.replaceOp( + snoop, {wrap.getValid(), wrap.getReady(), wrap.getRawInput()}); + + if (ChannelType::hasNoConsumers(wrap.getChanOutput())) { auto c1 = rewriter.create(wrap.getLoc(), rewriter.getI1Type(), 1); rewriter.replaceOp(wrap, {nullptr, c1}); return success(); } - if (!wrap.getChanOutput().hasOneUse()) + if (!ChannelType::hasOneConsumer(wrap.getChanOutput())) return rewriter.notifyMatchFailure( wrap, "This conversion only supports wrap-unwrap back-to-back. " "Wrap didn't have exactly one use."); if (!(unwrap = dyn_cast( - wrap.getChanOutput().use_begin()->getOwner()))) + ChannelType::getSingleConsumer(wrap.getChanOutput()) + ->getOwner()))) return rewriter.notifyMatchFailure( wrap, "This conversion only supports wrap-unwrap back-to-back. " "Could not find 'unwrap'."); @@ -189,11 +196,16 @@ struct RemoveWrapUnwrap : public ConversionPattern { valid = wrap.getValid(); data = wrap.getRawInput(); ready = operands[1]; + + // Lower away snoop ops. + for (auto user : operands[0].getUsers()) + if (auto snoop = dyn_cast(user)) + rewriter.replaceOp(snoop, {valid, ready, data}); } else { return failure(); } - if (!wrap.getChanOutput().hasOneUse()) + if (!ChannelType::hasOneConsumer(wrap.getChanOutput())) return rewriter.notifyMatchFailure(wrap, [](Diagnostic &d) { d << "This conversion only supports wrap-unwrap back-to-back. " "Wrap didn't have exactly one use."; diff --git a/lib/Dialect/ESI/Passes/ESIVerifyConnections.cpp b/lib/Dialect/ESI/Passes/ESIVerifyConnections.cpp index c304d4385278..1e34ed92d5ae 100644 --- a/lib/Dialect/ESI/Passes/ESIVerifyConnections.cpp +++ b/lib/Dialect/ESI/Passes/ESIVerifyConnections.cpp @@ -6,6 +6,7 @@ // //===----------------------------------------------------------------------===// +#include "circt/Dialect/ESI/ESIOps.h" #include "circt/Dialect/ESI/ESIPasses.h" #include "circt/Dialect/ESI/ESITypes.h" @@ -42,14 +43,9 @@ void ESIVerifyConnectionsPass::runOnOperation() { error.attachNote(user->getLoc()) << "bundle used here"; signalPassFailure(); - } else if (isa(v.getType())) { - if (std::distance(v.getUses().begin(), v.getUses().end()) <= 1) - continue; - mlir::InFlightDiagnostic error = - op->emitError("channels must have at most one use"); - for (Operation *user : v.getUsers()) - error.attachNote(user->getLoc()) << "channel used here"; - signalPassFailure(); + } else if (auto cv = dyn_cast>(v)) { + if (failed(ChannelType::verifyChannel(cv))) + signalPassFailure(); } }); } diff --git a/test/Dialect/ESI/connectivity.mlir b/test/Dialect/ESI/connectivity.mlir index de2619ab88f5..22483f895997 100644 --- a/test/Dialect/ESI/connectivity.mlir +++ b/test/Dialect/ESI/connectivity.mlir @@ -1,4 +1,5 @@ // RUN: circt-opt %s -verify-diagnostics | circt-opt -verify-diagnostics | FileCheck %s +// RUN: circt-opt %s --verify-esi-connections hw.module @Sender(out x: !esi.channel) { %0 = arith.constant 0 : i1 @@ -38,8 +39,11 @@ hw.module @test(in %clk: !seq.clock, in %rst: i1) { hw.instance "recv" @Reciever (a: %bufferedChan2: !esi.channel) -> () // CHECK-NEXT: %sender.x_0 = hw.instance "sender" @Sender() -> (x: !esi.channel) - // CHECK-NEXT: %1 = esi.buffer %clk, %rst, %sender.x_0 {stages = 4 : i64} : i1 - // CHECK-NEXT: hw.instance "recv" @Reciever(a: %1: !esi.channel) -> () + // CHECK-NEXT: [[R1:%.+]] = esi.buffer %clk, %rst, %sender.x_0 {stages = 4 : i64} : i1 + // CHECK-NEXT: hw.instance "recv" @Reciever(a: [[R1]]: !esi.channel) -> () + + %valid, %ready, %data = esi.snoop.vr %bufferedChan2 : !esi.channel + // CHECK-NEXT: %valid, %ready, %data = esi.snoop.vr [[R1]] : !esi.channel %nullBit = esi.null : !esi.channel hw.instance "nullRcvr" @Reciever(a: %nullBit: !esi.channel) -> () diff --git a/test/Dialect/ESI/errors.mlir b/test/Dialect/ESI/errors.mlir index a98368265f57..26f41bf74f28 100644 --- a/test/Dialect/ESI/errors.mlir +++ b/test/Dialect/ESI/errors.mlir @@ -164,7 +164,7 @@ hw.module.extern @Source(out a: !esi.channel) hw.module.extern @Sink(in %a: !esi.channel) hw.module @Top() { - // expected-error @+1 {{channels must have at most one use}} + // expected-error @+1 {{channels must have at most one consumer}} %a = hw.instance "src" @Source() -> (a: !esi.channel) // expected-note @+1 {{channel used here}} hw.instance "sink1" @Sink(a: %a: !esi.channel) -> () @@ -203,10 +203,12 @@ hw.module @Top() { // ----- hw.module @wrap_multi_unwrap(in %a_data: i8, in %a_valid: i1, out a_ready: i1) { - // expected-error @+1 {{'esi.wrap.vr' op only supports zero or one use}} + // expected-error @+1 {{'esi.wrap.vr' op channels must have at most one consumer}} %a_chan, %a_ready = esi.wrap.vr %a_data, %a_valid : i8 %true = hw.constant true + // expected-note @+1 {{channel used here}} %ap_data, %ap_valid = esi.unwrap.vr %a_chan, %true : i8 + // expected-note @+1 {{channel used here}} %ab_data, %ab_valid = esi.unwrap.vr %a_chan, %true : i8 hw.output %a_ready : i1 } diff --git a/test/Dialect/ESI/lowering.mlir b/test/Dialect/ESI/lowering.mlir index 93338506ad95..de91cc9972b7 100644 --- a/test/Dialect/ESI/lowering.mlir +++ b/test/Dialect/ESI/lowering.mlir @@ -1,6 +1,6 @@ // RUN: circt-opt %s --lower-esi-to-physical -verify-diagnostics | circt-opt -verify-diagnostics | FileCheck %s // RUN: circt-opt %s --lower-esi-ports -verify-diagnostics | circt-opt -verify-diagnostics | FileCheck --check-prefix=IFACE %s -// 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 +// RUN: circt-opt %s --lower-esi-to-physical --lower-esi-ports --hw-flatten-io --lower-esi-to-hw | FileCheck --check-prefix=HW %s hw.module.extern @Sender(in %clk: !seq.clock, out x: !esi.channel, out y: i8) attributes {esi.bundle} hw.module.extern @ArrSender(out x: !esi.channel>) attributes {esi.bundle} @@ -53,6 +53,7 @@ hw.module @test(in %clk: !seq.clock, in %rst:i1) { // IFACE-NEXT: %[[#modport4:]] = sv.modport.get %i4ToRecv2 @source : !sv.interface<@IValidReady_i4> -> !sv.modport<@IValidReady_i4::@source> // IFACE-NEXT: hw.instance "recv2" @Reciever(a: %[[#modport4:]]: !sv.modport<@IValidReady_i4::@source>, clk: %clk: !seq.clock) -> () + // HW-LABEL: hw.module @test(in %clk : !seq.clock, in %rst : i1) // After all 3 ESI lowering passes, there shouldn't be any ESI constructs! // HW-NOT: esi } @@ -76,6 +77,9 @@ hw.module @InternRcvr(in %in: !esi.channel>) {} hw.module @test2(in %clk: !seq.clock, in %rst:i1) { %ints, %c4 = hw.instance "adder" @add11(clk: %clk: !seq.clock, ints: %ints: !esi.channel) -> (mutatedInts: !esi.channel, c4: i4) + %valid, %ready, %data = esi.snoop.vr %ints: !esi.channel + %xact = comb.and %valid, %ready : i1 + %nullBit = esi.null : !esi.channel hw.instance "nullRcvr" @Reciever(a: %nullBit: !esi.channel, clk: %clk: !seq.clock) -> () @@ -83,14 +87,15 @@ hw.module @test2(in %clk: !seq.clock, in %rst:i1) { hw.instance "nullInternRcvr" @InternRcvr(in: %nullArray: !esi.channel>) -> () } // HW-LABEL: hw.module @test2(in %clk : !seq.clock, in %rst : i1) { -// 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) -// HW: [[ZERO:%.+]] = hw.bitcast %c0_i4 : (i4) -> i4 -// HW: sv.interface.signal.assign %i4ToNullRcvr(@IValidReady_i4::@data) = [[ZERO]] : i4 -// HW: [[ZM:%.+]] = sv.modport.get %{{.+}} @source : !sv.interface<@IValidReady_i4> -> !sv.modport<@IValidReady_i4::@source> -// HW: hw.instance "nullRcvr" @Reciever(a: [[ZM]]: !sv.modport<@IValidReady_i4::@source>, clk: %clk: !seq.clock) -> () -// HW: %c0_i32 = hw.constant 0 : i32 -// HW: [[ZA:%.+]] = hw.bitcast %c0_i32 : (i32) -> !hw.array<4xi8> -// HW: %nullInternRcvr.in_ready = hw.instance "nullInternRcvr" @InternRcvr(in: [[ZA]]: !hw.array<4xi8>, in_valid: %false_0: i1) -> (in_ready: i1) +// 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) +// HW-NEXT: [[XACT:%.+]] = comb.and %adder.mutatedInts_valid, %adder.ints_ready : i1 +// HW: [[ZERO:%.+]] = hw.bitcast %c0_i4 : (i4) -> i4 +// HW: sv.interface.signal.assign %i4ToNullRcvr(@IValidReady_i4::@data) = [[ZERO]] : i4 +// HW: [[ZM:%.+]] = sv.modport.get %{{.+}} @source : !sv.interface<@IValidReady_i4> -> !sv.modport<@IValidReady_i4::@source> +// HW: hw.instance "nullRcvr" @Reciever(a: [[ZM]]: !sv.modport<@IValidReady_i4::@source>, clk: %clk: !seq.clock) -> () +// HW: %c0_i32 = hw.constant 0 : i32 +// HW: [[ZA:%.+]] = hw.bitcast %c0_i32 : (i32) -> !hw.array<4xi8> +// HW: %nullInternRcvr.in_ready = hw.instance "nullInternRcvr" @InternRcvr(in: [[ZA]]: !hw.array<4xi8>, in_valid: %false_0: i1) -> (in_ready: i1) hw.module @twoChannelArgs(in %clk: !seq.clock, in %ints: !esi.channel, in %foo: !esi.channel) { %rdy = hw.constant 1 : i1