[HandshakeToHW] Initial commit for handshake-to-hw (#2680)

This initial commit handles building of the top-level hw.module when converting a handshake.func operation. All handshake modules are created as external hw modules. By doing so, follow-up commits may gradually implement each of the handshake operations using RTL dialect logic.

To transition from FIRRTL bundles with explicit ready/valid/data bundles, this commit relies on ESI channels for handshake module I/O as well as the top-level I/O.

Some changes to the flow are that:
- We're no longer inlining the handshake operations into the hw module. Instead, we maintain a value mapping from SSA values in the handshake.func to those in the hw.module, and maintain this using BackedgeBuilder operations. In general, an approach is taken of maintaining state outside the IR instead of inside the IR.

This is just the first commit in the process of transitioning away from FIRRTL for lowering Handshake operations; transitioning to be ESI based should hopefully also create a good basis for composing Handshake circuits with other parts of future HLS and CIRCT infrastructure.
This commit is contained in:
Morten Borup Petersen 2022-02-26 11:26:50 +01:00 committed by GitHub
parent 106815aea4
commit c7c0765933
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 744 additions and 1 deletions

View File

@ -0,0 +1,27 @@
//===- HandshakeToHW.h ------------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file declares passes which together will lower the Handshake dialect to
// CIRCT RTL dialects.
//
//===----------------------------------------------------------------------===//
#ifndef CIRCT_CONVERSION_HANDSHAKETOHW_H
#define CIRCT_CONVERSION_HANDSHAKETOHW_H
#include <memory>
namespace mlir {
class Pass;
} // namespace mlir
namespace circt {
std::unique_ptr<mlir::Pass> createHandshakeToHWPass();
} // namespace circt
#endif // CIRCT_CONVERSION_HANDSHAKETOHW_H

View File

@ -19,6 +19,7 @@
#include "circt/Conversion/FIRRTLToHW.h"
#include "circt/Conversion/HWToLLHD.h"
#include "circt/Conversion/HandshakeToFIRRTL.h"
#include "circt/Conversion/HandshakeToHW.h"
#include "circt/Conversion/LLHDToLLVM.h"
#include "circt/Conversion/MooreToCore.h"
#include "circt/Conversion/SCFToCalyx.h"

View File

@ -128,6 +128,19 @@ def LowerFIRRTLToHW : Pass<"lower-firrtl-to-hw", "mlir::ModuleOp"> {
];
}
//===----------------------------------------------------------------------===//
// HandshakeToHW
//===----------------------------------------------------------------------===//
def HandshakeToHW : Pass<"lower-handshake-to-hw", "mlir::ModuleOp"> {
let summary = "Lower Handshake to ESI/HW/Comb/Seq";
let description = [{
Lower Handshake to ESI/HW/Comb/Seq.
}];
let constructor = "circt::createHandshakeToHWPass()";
let dependentDialects = ["hw::HWDialect", "esi::ESIDialect"];
}
//===----------------------------------------------------------------------===//
// HandshakeToFIRRTL
//===----------------------------------------------------------------------===//

View File

@ -20,7 +20,8 @@ def FuncOp : Op<Handshake_Dialect, "func", [
FunctionOpInterface,
Symbol,
RegionKindInterface,
OpAsmOpInterface
OpAsmOpInterface,
HasClock
]> {
let summary = "Handshake dialect function.";
let description = [{

View File

@ -4,6 +4,7 @@ add_subdirectory(ExportVerilog)
add_subdirectory(FIRRTLToHW)
add_subdirectory(HWToLLHD)
add_subdirectory(HandshakeToFIRRTL)
add_subdirectory(HandshakeToHW)
add_subdirectory(LLHDToLLVM)
add_subdirectory(MooreToCore)
add_subdirectory(SCFToCalyx)

View File

@ -0,0 +1,19 @@
add_circt_library(CIRCTHandshakeToHW
HandshakeToHW.cpp
ADDITIONAL_HEADER_DIRS
${MLIR_MAIN_INCLUDE_DIR}/mlir/Conversion/HandshakeToHW
LINK_LIBS PUBLIC
CIRCTHW
CIRCTESI
CIRCTHandshake
CIRCTHandshakeTransforms
MLIRIR
MLIRPass
MLIRArithmetic
MLIRControlFlow
MLIRStandard
MLIRSupport
MLIRTransforms
)

View File

@ -0,0 +1,603 @@
//===- HandshakeToHW.cpp - Translate Handshake into HW ------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//===----------------------------------------------------------------------===//
//
// This is the main Handshake to HW Conversion Pass Implementation.
//
//===----------------------------------------------------------------------===//
#include "circt/Conversion/HandshakeToHW.h"
#include "../PassDetail.h"
#include "circt/Dialect/ESI/ESIOps.h"
#include "circt/Dialect/HW/HWOps.h"
#include "circt/Dialect/HW/HWTypes.h"
#include "circt/Dialect/Handshake/HandshakeOps.h"
#include "circt/Dialect/Handshake/HandshakePasses.h"
#include "circt/Dialect/Handshake/Visitor.h"
#include "circt/Support/BackedgeBuilder.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/IR/ImplicitLocOpBuilder.h"
#include "mlir/Pass/PassManager.h"
#include "mlir/Transforms/DialectConversion.h"
#include "llvm/ADT/TypeSwitch.h"
#include "llvm/Support/MathExtras.h"
#include <variant>
using namespace mlir;
using namespace circt;
using namespace circt::handshake;
using namespace circt::hw;
using NameUniquer = std::function<std::string(Operation *)>;
/// Returns a submodule name resulting from an operation, without discriminating
/// type information.
static std::string getBareSubModuleName(Operation *oldOp) {
// The dialect name is separated from the operation name by '.', which is not
// valid in SystemVerilog module names. In case this name is used in
// SystemVerilog output, replace '.' with '_'.
std::string subModuleName = oldOp->getName().getStringRef().str();
std::replace(subModuleName.begin(), subModuleName.end(), '.', '_');
return subModuleName;
}
static std::string getCallName(Operation *op) {
auto callOp = dyn_cast<handshake::InstanceOp>(op);
return callOp ? callOp.getModule().str() : getBareSubModuleName(op);
}
/// Extracts the type of the data-carrying type of opType. If opType is an ESI
/// channel, getHandshakeBundleDataType extracts the data-carrying type, else,
/// assume that opType itself is the data-carrying type.
static Type getOperandDataType(Value op) {
auto opType = op.getType();
if (auto channelType = opType.dyn_cast<esi::ChannelPort>())
return channelType.getInner();
return opType;
}
/// Filters NoneType's from the input.
static SmallVector<Type> filterNoneTypes(ArrayRef<Type> input) {
SmallVector<Type> filterRes;
llvm::copy_if(input, std::back_inserter(filterRes),
[](Type type) { return !type.isa<NoneType>(); });
return filterRes;
}
/// Returns a set of types which may uniquely identify the provided op. Return
/// value is <inputTypes, outputTypes>.
using DiscriminatingTypes = std::pair<SmallVector<Type>, SmallVector<Type>>;
static DiscriminatingTypes getHandshakeDiscriminatingTypes(Operation *op) {
return TypeSwitch<Operation *, DiscriminatingTypes>(op)
.Case<MemoryOp>([&](auto memOp) {
return DiscriminatingTypes{{}, {memOp.memRefType().getElementType()}};
})
.Default([&](auto) {
// By default, all in- and output types which is not a control type
// (NoneType) are discriminating types.
std::vector<Type> inTypes, outTypes;
llvm::transform(op->getOperands(), std::back_inserter(inTypes),
getOperandDataType);
llvm::transform(op->getResults(), std::back_inserter(outTypes),
getOperandDataType);
return DiscriminatingTypes{filterNoneTypes(inTypes),
filterNoneTypes(outTypes)};
});
}
// Wraps a type into an ESI ChannelPort type. The inner type is converted to
// ensure comprehensability by the RTL dialects.
static Type esiWrapper(Type t) {
// Translate none- and index types to something HW understands.
if (t.isa<NoneType>())
t = IntegerType::get(t.getContext(), 0);
else if (t.isa<IndexType>())
t = IntegerType::get(t.getContext(), 64);
return esi::ChannelPort::get(t.getContext(), t);
};
/// Get type name. Currently we only support integer or index types.
/// The emitted type aligns with the getFIRRTLType() method. Thus all integers
/// other than signed integers will be emitted as unsigned.
static std::string getTypeName(Location loc, Type type) {
std::string typeName;
// Builtin types
if (type.isIntOrIndex()) {
if (auto indexType = type.dyn_cast<IndexType>())
typeName += "_ui" + std::to_string(indexType.kInternalStorageBitWidth);
else if (type.isSignedInteger())
typeName += "_si" + std::to_string(type.getIntOrFloatBitWidth());
else
typeName += "_ui" + std::to_string(type.getIntOrFloatBitWidth());
} else
emitError(loc) << "unsupported data type '" << type << "'";
return typeName;
}
/// A class to be used with getPortInfoForOp. Provides an opaque interface for
/// generating the port names of an operation; handshake operations generate
/// names by the Handshake NamedIOInterface; and other operations, such as
/// arith ops, are assigned default names.
class HandshakePortNameGenerator {
public:
explicit HandshakePortNameGenerator(Operation *op)
: builder(op->getContext()) {
auto namedOpInterface = dyn_cast<handshake::NamedIOInterface>(op);
if (namedOpInterface)
inferFromNamedOpInterface(namedOpInterface);
else if (auto funcOp = dyn_cast<handshake::FuncOp>(op))
inferFromFuncOp(funcOp);
else
inferDefault(op);
}
StringAttr inputName(unsigned idx) { return inputs[idx]; }
StringAttr outputName(unsigned idx) { return outputs[idx]; }
private:
using IdxToStrF = const std::function<std::string(unsigned)> &;
void infer(Operation *op, IdxToStrF &inF, IdxToStrF &outF) {
llvm::transform(
llvm::enumerate(op->getOperandTypes()), std::back_inserter(inputs),
[&](auto it) { return builder.getStringAttr(inF(it.index())); });
llvm::transform(
llvm::enumerate(op->getResultTypes()), std::back_inserter(outputs),
[&](auto it) { return builder.getStringAttr(outF(it.index())); });
}
void inferDefault(Operation *op) {
infer(
op, [](unsigned idx) { return "in" + std::to_string(idx); },
[](unsigned idx) { return "out" + std::to_string(idx); });
}
void inferFromNamedOpInterface(handshake::NamedIOInterface op) {
infer(
op, [&](unsigned idx) { return op.getOperandName(idx); },
[&](unsigned idx) { return op.getResultName(idx); });
}
void inferFromFuncOp(handshake::FuncOp op) {
auto inF = [&](unsigned idx) { return op.getArgName(idx).str(); };
auto outF = [&](unsigned idx) { return op.getResName(idx).str(); };
llvm::transform(
llvm::enumerate(op.getType().getInputs()), std::back_inserter(inputs),
[&](auto it) { return builder.getStringAttr(inF(it.index())); });
llvm::transform(
llvm::enumerate(op.getType().getResults()), std::back_inserter(outputs),
[&](auto it) { return builder.getStringAttr(outF(it.index())); });
}
Builder builder;
llvm::SmallVector<StringAttr> inputs;
llvm::SmallVector<StringAttr> outputs;
};
/// Construct a name for creating HW sub-module.
static std::string getSubModuleName(Operation *oldOp) {
if (auto instanceOp = dyn_cast<handshake::InstanceOp>(oldOp); instanceOp)
return instanceOp.getModule().str();
std::string subModuleName = getBareSubModuleName(oldOp);
// Add value of the constant operation.
if (auto constOp = dyn_cast<handshake::ConstantOp>(oldOp)) {
if (auto intAttr = constOp.getValue().dyn_cast<IntegerAttr>()) {
auto intType = intAttr.getType();
if (intType.isSignedInteger())
subModuleName += "_c" + std::to_string(intAttr.getSInt());
else if (intType.isUnsignedInteger())
subModuleName += "_c" + std::to_string(intAttr.getUInt());
else
subModuleName += "_c" + std::to_string((uint64_t)intAttr.getInt());
} else
oldOp->emitError("unsupported constant type");
}
// Add discriminating in- and output types.
auto [inTypes, outTypes] = getHandshakeDiscriminatingTypes(oldOp);
if (!inTypes.empty())
subModuleName += "_in";
for (auto inType : inTypes)
subModuleName += getTypeName(oldOp->getLoc(), inType);
if (!outTypes.empty())
subModuleName += "_out";
for (auto outType : outTypes)
subModuleName += getTypeName(oldOp->getLoc(), outType);
// Add memory ID.
if (auto memOp = dyn_cast<handshake::MemoryOp>(oldOp))
subModuleName += "_id" + std::to_string(memOp.id());
// Add compare kind.
if (auto comOp = dyn_cast<mlir::arith::CmpIOp>(oldOp))
subModuleName += "_" + stringifyEnum(comOp.getPredicate()).str();
// Add buffer information.
if (auto bufferOp = dyn_cast<handshake::BufferOp>(oldOp)) {
subModuleName += "_" + std::to_string(bufferOp.getNumSlots()) + "slots";
if (bufferOp.isSequential())
subModuleName += "_seq";
else
subModuleName += "_fifo";
}
// Add control information.
if (auto ctrlInterface = dyn_cast<handshake::ControlInterface>(oldOp);
ctrlInterface && ctrlInterface.isControl()) {
// Add some additional discriminating info for non-typed operations.
subModuleName += "_" + std::to_string(oldOp->getNumOperands()) + "ins_" +
std::to_string(oldOp->getNumResults()) + "outs";
subModuleName += "_ctrl";
} else {
assert(
(!inTypes.empty() || !outTypes.empty()) &&
"Insufficient discriminating type info generated for the operation!");
}
return subModuleName;
}
//===----------------------------------------------------------------------===//
// HW Sub-module Related Functions
//===----------------------------------------------------------------------===//
/// Check whether a submodule with the same name has been created elsewhere in
/// the top level module. Return the matched module operation if true, otherwise
/// return nullptr.
static Operation *checkSubModuleOp(mlir::ModuleOp parentModule,
StringRef modName) {
if (auto mod = parentModule.lookupSymbol<HWModuleOp>(modName))
return mod;
if (auto mod = parentModule.lookupSymbol<HWModuleExternOp>(modName))
return mod;
return nullptr;
}
static Operation *checkSubModuleOp(mlir::ModuleOp parentModule,
Operation *oldOp) {
auto *moduleOp = checkSubModuleOp(parentModule, getSubModuleName(oldOp));
if (isa<handshake::InstanceOp>(oldOp))
assert(moduleOp &&
"handshake.instance target modules should always have been lowered "
"before the modules that reference them!");
return moduleOp;
}
static llvm::SmallVector<PortInfo>
getPortInfoForOp(ConversionPatternRewriter &rewriter, Operation *op,
TypeRange inputs, TypeRange outputs) {
llvm::SmallVector<PortInfo> ports;
HandshakePortNameGenerator portNames(op);
// Add all inputs of funcOp.
unsigned inIdx = 0;
for (auto &arg : llvm::enumerate(inputs)) {
ports.push_back({portNames.inputName(arg.index()), PortDirection::INPUT,
esiWrapper(arg.value()), arg.index(), StringAttr{}});
inIdx++;
}
// Add all outputs of funcOp.
for (auto &res : llvm::enumerate(outputs)) {
ports.push_back({portNames.outputName(res.index()), PortDirection::OUTPUT,
esiWrapper(res.value()), res.index(), StringAttr{}});
}
// Add clock and reset signals.
if (op->hasTrait<mlir::OpTrait::HasClock>()) {
ports.push_back({rewriter.getStringAttr("clock"), PortDirection::INPUT,
rewriter.getI1Type(), inIdx++, StringAttr{}});
ports.push_back({rewriter.getStringAttr("reset"), PortDirection::INPUT,
rewriter.getI1Type(), inIdx, StringAttr{}});
}
return ports;
}
/// Returns a vector of PortInfo's which defines the FIRRTL interface of the
/// to-be-converted op.
static llvm::SmallVector<PortInfo>
getPortInfoForOp(ConversionPatternRewriter &rewriter, Operation *op) {
return getPortInfoForOp(rewriter, op, op->getOperandTypes(),
op->getResultTypes());
}
/// All standard expressions and handshake elastic components will be converted
/// to a HW sub-module and be instantiated in the top-module.
static HWModuleExternOp createSubModuleOp(ModuleOp parentModule,
Operation *oldOp,
ConversionPatternRewriter &rewriter) {
OpBuilder::InsertionGuard g(rewriter);
rewriter.setInsertionPointToStart(parentModule.getBody());
auto ports = getPortInfoForOp(rewriter, oldOp);
// todo: HWModuleOp; for this initial commit we'll leave this as an extern op.
return rewriter.create<HWModuleExternOp>(
parentModule.getLoc(), rewriter.getStringAttr(getSubModuleName(oldOp)),
ports);
}
//===----------------------------------------------------------------------===//
// HW Top-module Related Functions
//===----------------------------------------------------------------------===//
static hw::HWModuleOp createTopModuleOp(handshake::FuncOp funcOp,
ConversionPatternRewriter &rewriter) {
llvm::SmallVector<PortInfo, 8> ports =
getPortInfoForOp(rewriter, funcOp, funcOp.getType().getInputs(),
funcOp.getType().getResults());
// Create a HW module.
auto hwModuleOp = rewriter.create<hw::HWModuleOp>(
funcOp.getLoc(), rewriter.getStringAttr(funcOp.getName()), ports);
// Remove the default created hw_output operation.
auto outputOps = hwModuleOp.getOps<hw::OutputOp>();
assert(std::distance(outputOps.begin(), outputOps.end()) == 1 &&
"Expected exactly 1 default created hw_output operation");
rewriter.eraseOp(*outputOps.begin());
return hwModuleOp;
}
static bool isMemrefType(Type t) { return t.isa<mlir::MemRefType>(); }
static LogicalResult verifyHandshakeFuncOp(handshake::FuncOp &funcOp) {
// @TODO: memory I/O is not yet supported. Figure out how to support memory
// services in ESI.
if (llvm::any_of(funcOp.getArgumentTypes(), isMemrefType) ||
llvm::any_of(funcOp.getResultTypes(), isMemrefType))
return emitError(funcOp.getLoc())
<< "memref ports are not yet supported in handshake-to-hw lowering.";
return success();
}
namespace {
using TypeTransformer = llvm::function_ref<Type(Type)>;
static Type defaultTypeTransformer(Type t) { return t; }
/// The ValueMapping class facilitates the definition and connection of SSA
/// def-use chains between two separate regions - a 'from' region (defining
/// use-def chains) and a 'to' region (where new operations are created based on
/// the 'from' region).´
class ValueMapping {
public:
explicit ValueMapping(BackedgeBuilder &bb) : bb(bb) {}
// Get the mapped value of value 'from'. If no mapping has been registered, a
// new backedge is created. The type of the mapped value may optionally be
// modified through the 'typeTransformer'.
Value get(Value from,
TypeTransformer typeTransformer = defaultTypeTransformer) {
if (mapping.count(from) == 0) {
// Create a backedge which will be resolved at a later time once all
// operands are created.
mapping[from] = bb.get(typeTransformer(from.getType()));
}
auto operandMapping = mapping[from];
Value mappedOperand;
if (auto *v = std::get_if<Value>(&operandMapping))
mappedOperand = *v;
else
mappedOperand = std::get<Backedge>(operandMapping);
return mappedOperand;
}
llvm::SmallVector<Value>
get(ValueRange from,
TypeTransformer typeTransformer = defaultTypeTransformer) {
llvm::SmallVector<Value> to;
for (auto f : from)
to.push_back(get(f, typeTransformer));
return to;
}
// Set the mapped value of 'from' to 'to'. If 'from' is already mapped to a
// backedge, replaces that backedge with 'to'.
void set(Value from, Value to) {
auto it = mapping.find(from);
if (it != mapping.end()) {
if (auto *backedge = std::get_if<Backedge>(&it->second)) {
backedge->setValue(to);
} else {
assert(false && "'from' was already mapped to a final value!");
}
}
// Register the new mapping
mapping[from] = to;
}
void set(ValueRange from, ValueRange to) {
assert(from.size() == to.size() &&
"Expected # of 'from' values and # of 'to' values to be identical.");
for (auto [f, t] : llvm::zip(from, to))
set(f, t);
}
private:
BackedgeBuilder &bb;
DenseMap<Value, std::variant<Value, Backedge>> mapping;
};
// Shared state used by various functions; captured in a struct to reduce the
// number of arguments that we have to pass around.
struct HandshakeLoweringState {
ValueMapping &mapping;
ModuleOp parentModule;
hw::HWModuleOp hwModuleOp;
NameUniquer nameUniquer;
Value clock;
Value reset;
};
static Operation *createOpInHWModule(ConversionPatternRewriter &rewriter,
HandshakeLoweringState &ls,
Operation *op) {
OpBuilder::InsertionGuard g(rewriter);
rewriter.setInsertionPointToEnd(ls.hwModuleOp.getBodyBlock());
// Create a mapping between the operands of 'op' and the replacement operands
// in the target hwModule. For any missing operands, we create a new backedge.
llvm::SmallVector<Value> hwOperands =
ls.mapping.get(op->getOperands(), esiWrapper);
// Add clock and reset if needed.
if (op->hasTrait<mlir::OpTrait::HasClock>())
hwOperands.append({ls.clock, ls.reset});
// Check if a sub-module for the operation already exists.
Operation *subModuleSymOp = checkSubModuleOp(ls.parentModule, op);
if (!subModuleSymOp) {
subModuleSymOp = createSubModuleOp(ls.parentModule, op, rewriter);
// TODO: fill the subModuleSymOp with the meat of the handshake
// operations.
}
// Instantiate the new created sub-module.
rewriter.setInsertionPointToEnd(ls.hwModuleOp.getBodyBlock());
auto submoduleInstanceOp = rewriter.create<hw::InstanceOp>(
op->getLoc(), subModuleSymOp, rewriter.getStringAttr(ls.nameUniquer(op)),
hwOperands);
// Resolve any previously created backedges that referred to the results of
// 'op'.
ls.mapping.set(op->getResults(), submoduleInstanceOp.getResults());
return submoduleInstanceOp;
}
static void convertReturnOp(handshake::ReturnOp op, HandshakeLoweringState &ls,
ConversionPatternRewriter &rewriter) {
OpBuilder::InsertionGuard g(rewriter);
rewriter.setInsertionPointToEnd(ls.hwModuleOp.getBodyBlock());
rewriter.create<hw::OutputOp>(op.getLoc(), ls.mapping.get(op.getOperands()));
}
struct HandshakeFuncOpLowering : public OpConversionPattern<handshake::FuncOp> {
using OpConversionPattern<handshake::FuncOp>::OpConversionPattern;
HandshakeFuncOpLowering(MLIRContext *context)
: OpConversionPattern<handshake::FuncOp>(context) {}
LogicalResult
matchAndRewrite(handshake::FuncOp funcOp, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
if (failed(verifyHandshakeFuncOp(funcOp)))
return failure();
ModuleOp parentModule = funcOp->getParentOfType<ModuleOp>();
rewriter.setInsertionPointToStart(funcOp->getBlock());
HWModuleOp topModuleOp = createTopModuleOp(funcOp, rewriter);
Value clockPort =
topModuleOp.getArgument(topModuleOp.getNumArguments() - 2);
Value resetPort =
topModuleOp.getArgument(topModuleOp.getNumArguments() - 1);
// Create a uniquer function for creating instance names.
NameUniquer instanceUniquer = [&](Operation *op) {
std::string instName = getCallName(op);
if (auto idAttr = op->getAttrOfType<IntegerAttr>("handshake_id");
idAttr) {
// We use a special naming convention for operations which have a
// 'handshake_id' attribute.
instName += "_id" + std::to_string(idAttr.getValue().getZExtValue());
} else {
// Fallback to just prefixing with an integer.
instName += std::to_string(instanceNameCntr[instName]++);
}
return instName;
};
// Initialize the Handshake lowering state.
BackedgeBuilder bb(rewriter, funcOp.getLoc());
ValueMapping valueMapping(bb);
auto ls = HandshakeLoweringState{valueMapping, parentModule, topModuleOp,
instanceUniquer, clockPort, resetPort};
// Extend value mapping with input arguments. Drop the 2 last inputs from
// the HW module (clock and reset).
valueMapping.set(funcOp.getArguments(),
topModuleOp.getArguments().drop_back(2));
// Traverse and convert each operation in funcOp.
for (Operation &op : funcOp.front()) {
if (isa<hw::OutputOp>(op)) {
// Skip the default created HWModule terminator, for now.
continue;
}
if (auto returnOp = dyn_cast<handshake::ReturnOp>(op)) {
convertReturnOp(returnOp, ls, rewriter);
} else {
// Regular operation.
createOpInHWModule(rewriter, ls, &op);
}
}
rewriter.eraseOp(funcOp);
return success();
}
private:
/// Maintain a map from module names to the # of times the module has been
/// instantiated inside this module. This is used to generate unique names for
/// each instance.
mutable std::map<std::string, unsigned> instanceNameCntr;
};
class HandshakeToHWPass : public HandshakeToHWBase<HandshakeToHWPass> {
public:
void runOnOperation() override {
auto op = getOperation();
// Lowering to HW requires that every value is used exactly once. Check
// whether this precondition is met, and if not, exit.
if (llvm::any_of(op.getOps<handshake::FuncOp>(), [](auto f) {
return failed(verifyAllValuesHasOneUse(f));
})) {
signalPassFailure();
return;
}
// Resolve the instance graph to get a top-level module.
std::string topLevel;
handshake::InstanceGraph uses;
SmallVector<std::string> sortedFuncs;
if (resolveInstanceGraph(op, uses, topLevel, sortedFuncs).failed()) {
signalPassFailure();
return;
}
ConversionTarget target(getContext());
target.addLegalDialect<HWDialect>();
target.addIllegalDialect<handshake::HandshakeDialect>();
// Convert the handshake.func operations in post-order wrt. the instance
// graph. This ensures that any referenced submodules (through
// handshake.instance) has already been lowered, and their HW module
// equivalents are available.
for (auto &funcName : llvm::reverse(sortedFuncs)) {
RewritePatternSet patterns(op.getContext());
patterns.insert<HandshakeFuncOpLowering>(op.getContext());
auto *funcOp = op.lookupSymbol(funcName);
assert(funcOp && "Symbol not found in module!");
if (failed(applyPartialConversion(funcOp, target, std::move(patterns)))) {
signalPassFailure();
funcOp->emitOpError() << "error during conversion";
return;
}
}
}
};
} // end anonymous namespace
std::unique_ptr<mlir::Pass> circt::createHandshakeToHWPass() {
return std::make_unique<HandshakeToHWPass>();
}

View File

@ -52,6 +52,10 @@ class HandshakeDialect;
class FuncOp;
} // namespace handshake
namespace esi {
class ESIDialect;
} // namespace esi
namespace moore {
class MooreDialect;
} // namespace moore

View File

@ -0,0 +1,73 @@
// RUN: circt-opt -lower-handshake-to-hw -split-input-file %s | FileCheck %s
// CHECK-LABEL: hw.module.extern @arith_addi_in_ui64_ui64_out_ui64(
// CHECK-SAME: %[[VAL_0:.*]]: !esi.channel<i64>,
// CHECK-SAME: %[[VAL_1:.*]]: !esi.channel<i64>) -> (out0: !esi.channel<i64>)
// CHECK: hw.module.extern @handshake_cond_br_in_ui1_2ins_2outs_ctrl(%[[VAL_2:.*]]: !esi.channel<i1>, %[[VAL_3:.*]]: !esi.channel<i0>) -> (outTrue: !esi.channel<i0>, outFalse: !esi.channel<i0>)
// CHECK: hw.module.extern @handshake_sink_in_ui64(%[[VAL_0]]: !esi.channel<i64>)
// CHECK: hw.module.extern @handshake_cond_br_in_ui1_ui64_out_ui64_ui64(%[[VAL_2]]: !esi.channel<i1>, %[[VAL_3]]: !esi.channel<i64>) -> (outTrue: !esi.channel<i64>, outFalse: !esi.channel<i64>)
// CHECK: hw.module.extern @handshake_fork_in_ui1_out_ui1_ui1_ui1_ui1_ui1(%[[VAL_0]]: !esi.channel<i1>, %[[VAL_4:.*]]: i1, %[[VAL_5:.*]]: i1) -> (out0: !esi.channel<i1>, out1: !esi.channel<i1>, out2: !esi.channel<i1>, out3: !esi.channel<i1>, out4: !esi.channel<i1>)
// CHECK: hw.module.extern @arith_cmpi_in_ui64_ui64_out_ui1_slt(%[[VAL_0]]: !esi.channel<i64>, %[[VAL_1]]: !esi.channel<i64>) -> (out0: !esi.channel<i1>)
// CHECK: hw.module.extern @handshake_fork_in_ui64_out_ui64_ui64(%[[VAL_0]]: !esi.channel<i64>, %[[VAL_4]]: i1, %[[VAL_5]]: i1) -> (out0: !esi.channel<i64>, out1: !esi.channel<i64>)
// CHECK: hw.module.extern @handshake_mux_in_ui1_ui64_ui64_out_ui64(%[[VAL_6:.*]]: !esi.channel<i1>, %[[VAL_0]]: !esi.channel<i64>, %[[VAL_1]]: !esi.channel<i64>) -> (out0: !esi.channel<i64>)
// CHECK: hw.module.extern @handshake_mux_in_ui1_3ins_1outs_ctrl(%[[VAL_6]]: !esi.channel<i1>, %[[VAL_0]]: !esi.channel<i0>, %[[VAL_1]]: !esi.channel<i0>) -> (out0: !esi.channel<i0>)
// CHECK: hw.module.extern @handshake_fork_in_ui1_out_ui1_ui1_ui1_ui1(%[[VAL_0]]: !esi.channel<i1>, %[[VAL_4]]: i1, %[[VAL_5]]: i1) -> (out0: !esi.channel<i1>, out1: !esi.channel<i1>, out2: !esi.channel<i1>, out3: !esi.channel<i1>)
// CHECK: hw.module.extern @handshake_buffer_in_ui1_out_ui1_1slots_seq(%[[VAL_0]]: !esi.channel<i1>, %[[VAL_4]]: i1, %[[VAL_5]]: i1) -> (out0: !esi.channel<i1>)
// CHECK: hw.module.extern @handshake_constant_c42_out_ui64(%[[VAL_7:.*]]: !esi.channel<i0>) -> (out0: !esi.channel<i64>)
// CHECK: hw.module.extern @handshake_constant_c1_out_ui64(%[[VAL_7]]: !esi.channel<i0>) -> (out0: !esi.channel<i64>)
// CHECK: hw.module.extern @handshake_fork_1ins_4outs_ctrl(%[[VAL_0]]: !esi.channel<i0>, %[[VAL_4]]: i1, %[[VAL_5]]: i1) -> (out0: !esi.channel<i0>, out1: !esi.channel<i0>, out2: !esi.channel<i0>, out3: !esi.channel<i0>)
// CHECK-LABEL: hw.module @main(
// CHECK-SAME: %[[VAL_0:.*]]: !esi.channel<i0>,
// CHECK-SAME: %[[VAL_4:.*]]: i1,
// CHECK-SAME: %[[VAL_5:.*]]: i1) -> (out0: !esi.channel<i64>, outCtrl: !esi.channel<i0>) {
// CHECK: %[[VAL_1:.*]], %[[VAL_2:.*]], %[[VAL_3:.*]], %[[VAL_42:.*]] = hw.instance "handshake_fork0" @handshake_fork_1ins_4outs_ctrl(in0: %[[VAL_0]]: !esi.channel<i0>, clock: %[[VAL_4]]: i1, reset: %[[VAL_5]]: i1) -> (out0: !esi.channel<i0>, out1: !esi.channel<i0>, out2: !esi.channel<i0>, out3: !esi.channel<i0>)
// CHECK: %[[VAL_52:.*]] = hw.instance "handshake_constant0" @handshake_constant_c1_out_ui64(ctrl: %[[VAL_3]]: !esi.channel<i0>) -> (out0: !esi.channel<i64>)
// CHECK: %[[VAL_6:.*]] = hw.instance "handshake_constant1" @handshake_constant_c42_out_ui64(ctrl: %[[VAL_2]]: !esi.channel<i0>) -> (out0: !esi.channel<i64>)
// CHECK: %[[VAL_7:.*]] = hw.instance "handshake_constant2" @handshake_constant_c1_out_ui64(ctrl: %[[VAL_1]]: !esi.channel<i0>) -> (out0: !esi.channel<i64>)
// CHECK: %[[VAL_8:.*]] = hw.instance "handshake_buffer0" @handshake_buffer_in_ui1_out_ui1_1slots_seq(in0: %[[VAL_9:.*]]: !esi.channel<i1>, clock: %[[VAL_4]]: i1, reset: %[[VAL_5]]: i1) -> (out0: !esi.channel<i1>)
// CHECK: %[[VAL_10:.*]], %[[VAL_11:.*]], %[[VAL_12:.*]], %[[VAL_13:.*]] = hw.instance "handshake_fork1" @handshake_fork_in_ui1_out_ui1_ui1_ui1_ui1(in0: %[[VAL_8]]: !esi.channel<i1>, clock: %[[VAL_4]]: i1, reset: %[[VAL_5]]: i1) -> (out0: !esi.channel<i1>, out1: !esi.channel<i1>, out2: !esi.channel<i1>, out3: !esi.channel<i1>)
// CHECK: %[[VAL_14:.*]] = hw.instance "handshake_mux0" @handshake_mux_in_ui1_3ins_1outs_ctrl(select: %[[VAL_13]]: !esi.channel<i1>, in0: %[[VAL_42]]: !esi.channel<i0>, in1: %[[VAL_15:.*]]: !esi.channel<i0>) -> (out0: !esi.channel<i0>)
// CHECK: %[[VAL_16:.*]] = hw.instance "handshake_mux1" @handshake_mux_in_ui1_ui64_ui64_out_ui64(select: %[[VAL_12]]: !esi.channel<i1>, in0: %[[VAL_6]]: !esi.channel<i64>, in1: %[[VAL_17:.*]]: !esi.channel<i64>) -> (out0: !esi.channel<i64>)
// CHECK: %[[VAL_18:.*]], %[[VAL_19:.*]] = hw.instance "handshake_fork2" @handshake_fork_in_ui64_out_ui64_ui64(in0: %[[VAL_16]]: !esi.channel<i64>, clock: %[[VAL_4]]: i1, reset: %[[VAL_5]]: i1) -> (out0: !esi.channel<i64>, out1: !esi.channel<i64>)
// CHECK: %[[VAL_20:.*]] = hw.instance "handshake_mux2" @handshake_mux_in_ui1_ui64_ui64_out_ui64(select: %[[VAL_11]]: !esi.channel<i1>, in0: %[[VAL_7]]: !esi.channel<i64>, in1: %[[VAL_21:.*]]: !esi.channel<i64>) -> (out0: !esi.channel<i64>)
// CHECK: %[[VAL_22:.*]] = hw.instance "handshake_mux3" @handshake_mux_in_ui1_ui64_ui64_out_ui64(select: %[[VAL_10]]: !esi.channel<i1>, in0: %[[VAL_52]]: !esi.channel<i64>, in1: %[[VAL_23:.*]]: !esi.channel<i64>) -> (out0: !esi.channel<i64>)
// CHECK: %[[VAL_24:.*]], %[[VAL_25:.*]] = hw.instance "handshake_fork3" @handshake_fork_in_ui64_out_ui64_ui64(in0: %[[VAL_22]]: !esi.channel<i64>, clock: %[[VAL_4]]: i1, reset: %[[VAL_5]]: i1) -> (out0: !esi.channel<i64>, out1: !esi.channel<i64>)
// CHECK: %[[VAL_26:.*]] = hw.instance "arith_cmpi0" @arith_cmpi_in_ui64_ui64_out_ui1_slt(in0: %[[VAL_24]]: !esi.channel<i64>, in1: %[[VAL_18]]: !esi.channel<i64>) -> (out0: !esi.channel<i1>)
// CHECK: %[[VAL_9]], %[[VAL_27:.*]], %[[VAL_28:.*]], %[[VAL_29:.*]], %[[VAL_30:.*]] = hw.instance "handshake_fork4" @handshake_fork_in_ui1_out_ui1_ui1_ui1_ui1_ui1(in0: %[[VAL_26]]: !esi.channel<i1>, clock: %[[VAL_4]]: i1, reset: %[[VAL_5]]: i1) -> (out0: !esi.channel<i1>, out1: !esi.channel<i1>, out2: !esi.channel<i1>, out3: !esi.channel<i1>, out4: !esi.channel<i1>)
// CHECK: %[[VAL_17]], %[[VAL_31:.*]] = hw.instance "handshake_cond_br0" @handshake_cond_br_in_ui1_ui64_out_ui64_ui64(cond: %[[VAL_30]]: !esi.channel<i1>, data: %[[VAL_19]]: !esi.channel<i64>) -> (outTrue: !esi.channel<i64>, outFalse: !esi.channel<i64>)
// CHECK: hw.instance "handshake_sink0" @handshake_sink_in_ui64(in0: %[[VAL_31]]: !esi.channel<i64>) -> ()
// CHECK: %[[VAL_32:.*]], %[[VAL_33:.*]] = hw.instance "handshake_cond_br1" @handshake_cond_br_in_ui1_ui64_out_ui64_ui64(cond: %[[VAL_29]]: !esi.channel<i1>, data: %[[VAL_20]]: !esi.channel<i64>) -> (outTrue: !esi.channel<i64>, outFalse: !esi.channel<i64>)
// CHECK: hw.instance "handshake_sink1" @handshake_sink_in_ui64(in0: %[[VAL_33]]: !esi.channel<i64>) -> ()
// CHECK: %[[VAL_15]], %[[VAL_34:.*]] = hw.instance "handshake_cond_br2" @handshake_cond_br_in_ui1_2ins_2outs_ctrl(cond: %[[VAL_28]]: !esi.channel<i1>, data: %[[VAL_14]]: !esi.channel<i0>) -> (outTrue: !esi.channel<i0>, outFalse: !esi.channel<i0>)
// CHECK: %[[VAL_35:.*]], %[[VAL_36:.*]] = hw.instance "handshake_cond_br3" @handshake_cond_br_in_ui1_ui64_out_ui64_ui64(cond: %[[VAL_27]]: !esi.channel<i1>, data: %[[VAL_25]]: !esi.channel<i64>) -> (outTrue: !esi.channel<i64>, outFalse: !esi.channel<i64>)
// CHECK: %[[VAL_21]], %[[VAL_37:.*]] = hw.instance "handshake_fork5" @handshake_fork_in_ui64_out_ui64_ui64(in0: %[[VAL_32]]: !esi.channel<i64>, clock: %[[VAL_4]]: i1, reset: %[[VAL_5]]: i1) -> (out0: !esi.channel<i64>, out1: !esi.channel<i64>)
// CHECK: %[[VAL_23]] = hw.instance "arith_addi0" @arith_addi_in_ui64_ui64_out_ui64(in0: %[[VAL_35]]: !esi.channel<i64>, in1: %[[VAL_37]]: !esi.channel<i64>) -> (out0: !esi.channel<i64>)
// CHECK: hw.output %[[VAL_36]], %[[VAL_34]] : !esi.channel<i64>, !esi.channel<i0>
// CHECK: }
handshake.func @main(%arg0: none, ...) -> (i64, none) attributes {argNames = ["inCtrl"], resNames = ["out0", "outCtrl"]} {
%0:4 = fork [4] %arg0 : none
%1 = constant %0#2 {value = 1 : i64} : i64
%2 = constant %0#1 {value = 42 : i64} : i64
%3 = constant %0#0 {value = 1 : i64} : i64
%4 = buffer [1] %13#0 {initValues = [0], sequential = true} : i1
%5:4 = fork [4] %4 : i1
%6 = mux %5#3 [%0#3, %trueResult_2] : i1, none
%7 = mux %5#2 [%2, %trueResult] : i1, i64
%8:2 = fork [2] %7 : i64
%9 = mux %5#1 [%3, %14#0] : i1, i64
%10 = mux %5#0 [%1, %15] : i1, i64
%11:2 = fork [2] %10 : i64
%12 = arith.cmpi slt, %11#0, %8#0 : i64
%13:5 = fork [5] %12 : i1
%trueResult, %falseResult = cond_br %13#4, %8#1 : i64
sink %falseResult : i64
%trueResult_0, %falseResult_1 = cond_br %13#3, %9 : i64
sink %falseResult_1 : i64
%trueResult_2, %falseResult_3 = cond_br %13#2, %6 : none
%trueResult_4, %falseResult_5 = cond_br %13#1, %11#1 : i64
%14:2 = fork [2] %trueResult_0 : i64
%15 = arith.addi %trueResult_4, %14#1 : i64
return %falseResult_5, %falseResult_3 : i64, none
}

View File

@ -21,6 +21,7 @@ target_link_libraries(circt-opt
CIRCTFSM
CIRCTHandshake
CIRCTHandshakeToFIRRTL
CIRCTHandshakeToHW
CIRCTHandshakeTransforms
CIRCTLLHD
CIRCTLLHDToLLVM