From 298f74ad6b771d7bbdf72e769db70e6c01643de5 Mon Sep 17 00:00:00 2001 From: Carmine Rizzi Date: Thu, 8 Jan 2026 16:51:33 +0100 Subject: [PATCH 01/28] Added description of Synth dialect from the circt project --- .../dynamatic/Dialect/Synth/CMakeLists.txt | 18 + include/dynamatic/Dialect/Synth/Synth.td | 37 ++ .../dynamatic/Dialect/Synth/SynthDialect.h | 28 ++ include/dynamatic/Dialect/Synth/SynthOps.h | 60 +++ include/dynamatic/Dialect/Synth/SynthOps.td | 168 +++++++++ lib/Dialect/Synth/CMakeLists.txt | 31 ++ lib/Dialect/Synth/SynthDialect.cpp | 41 ++ lib/Dialect/Synth/SynthOps.cpp | 355 ++++++++++++++++++ 8 files changed, 738 insertions(+) create mode 100644 include/dynamatic/Dialect/Synth/CMakeLists.txt create mode 100644 include/dynamatic/Dialect/Synth/Synth.td create mode 100644 include/dynamatic/Dialect/Synth/SynthDialect.h create mode 100644 include/dynamatic/Dialect/Synth/SynthOps.h create mode 100644 include/dynamatic/Dialect/Synth/SynthOps.td create mode 100644 lib/Dialect/Synth/CMakeLists.txt create mode 100644 lib/Dialect/Synth/SynthDialect.cpp create mode 100644 lib/Dialect/Synth/SynthOps.cpp diff --git a/include/dynamatic/Dialect/Synth/CMakeLists.txt b/include/dynamatic/Dialect/Synth/CMakeLists.txt new file mode 100644 index 000000000..abb2fb9e8 --- /dev/null +++ b/include/dynamatic/Dialect/Synth/CMakeLists.txt @@ -0,0 +1,18 @@ +##===----------------------------------------------------------------------===// +## All the files related to the description of the Synth dialect +## have been modified from the original version present in the +## circt project at the following link: +## https://github.com/llvm/circt/tree/main/ +##===----------------------------------------------------------------------===// + +##===----------------------------------------------------------------------===// +## +## 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 +## +##===----------------------------------------------------------------------===// + +add_dynamatic_dialect(Synth synth) + +set(LLVM_TARGET_DEFINITIONS Synth.td) \ No newline at end of file diff --git a/include/dynamatic/Dialect/Synth/Synth.td b/include/dynamatic/Dialect/Synth/Synth.td new file mode 100644 index 000000000..b978dd2d0 --- /dev/null +++ b/include/dynamatic/Dialect/Synth/Synth.td @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// All the files related to the description of the Synth dialect +// have been modified from the original version present in the +// circt project at the following link: +// https://github.com/llvm/circt/tree/main/ +//===----------------------------------------------------------------------===// + + +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#ifndef DYNAMATIC_SYNTH_DIALECT_TD +#define DYNAMATIC_SYNTH_DIALECT_TD + +include "mlir/IR/DialectBase.td" + +def Synth_Dialect : Dialect { + let name = "synth"; + let cppNamespace = "::dynamatic::synth"; + let summary = "Synthesis dialect for logic synthesis operations"; + let description = [{ + The Synth dialect provides operations and types for logic synthesis, + including meta operations for synthesis decisions, logic representations + like AIG and MIG, and synthesis pipeline infrastructure. + }]; + + let hasConstantMaterializer = 1; +} + +include "dynamatic/Dialect/Synth/SynthOps.td" + +#endif // DYNAMATIC_SYNTH_DIALECT_TD diff --git a/include/dynamatic/Dialect/Synth/SynthDialect.h b/include/dynamatic/Dialect/Synth/SynthDialect.h new file mode 100644 index 000000000..df3c2add7 --- /dev/null +++ b/include/dynamatic/Dialect/Synth/SynthDialect.h @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// All the files related to the description of the Synth dialect +// have been modified from the original version present in the +// circt project at the following link: +// https://github.com/llvm/circt/tree/main/ +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// 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 defines the Synth dialect. +// +//===----------------------------------------------------------------------===// + +#ifndef DYNAMATIC_DIALECT_SYNTH_SYNTHDIALECT_H +#define DYNAMATIC_DIALECT_SYNTH_SYNTHDIALECT_H + +#include "dynamatic/Support/LLVM.h" +#include "mlir/IR/Dialect.h" + +#include "dynamatic/Dialect/Synth/SynthDialect.h.inc" + +#endif // DYNAMATIC_DIALECT_SYNTH_SYNTHDIALECT_H diff --git a/include/dynamatic/Dialect/Synth/SynthOps.h b/include/dynamatic/Dialect/Synth/SynthOps.h new file mode 100644 index 000000000..31886c161 --- /dev/null +++ b/include/dynamatic/Dialect/Synth/SynthOps.h @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// All the files related to the description of the Synth dialect +// have been modified from the original version present in the +// circt project at the following link: +// https://github.com/llvm/circt/tree/main/ +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// 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 defines the operations of the Synth dialect. +// +//===----------------------------------------------------------------------===// + +#ifndef DYNAMATIC_DIALECT_SYNTH_SYNTHOPS_H +#define DYNAMATIC_DIALECT_SYNTH_SYNTHOPS_H + +#include "dynamatic/Dialect/Synth/SynthDialect.h" +#include "dynamatic/Support/LLVM.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Dialect.h" +#include "mlir/IR/Operation.h" +#include "mlir/Interfaces/InferTypeOpInterface.h" +#include "mlir/Interfaces/SideEffectInterfaces.h" +#include "mlir/Rewrite/PatternApplicator.h" + +#define GET_OP_CLASSES +#include "dynamatic/Dialect/Synth/Synth.h.inc" + +namespace dynamatic { +namespace synth { +struct AndInverterVariadicOpConversion + : mlir::OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + mlir::LogicalResult + matchAndRewrite(aig::AndInverterOp op, + mlir::PatternRewriter &rewriter) const override; +}; + +} // namespace synth +ParseResult parseVariadicInvertibleOperands( + OpAsmParser &parser, + SmallVectorImpl &operands, Type &resultType, + mlir::DenseBoolArrayAttr &inverted, NamedAttrList &attrDict); +void printVariadicInvertibleOperands(OpAsmPrinter &printer, Operation *op, + OperandRange operands, Type resultType, + mlir::DenseBoolArrayAttr inverted, + DictionaryAttr attrDict); + +} // namespace dynamatic + +#endif // DYNAMATIC_DIALECT_SYNTH_SYNTHOPS_H diff --git a/include/dynamatic/Dialect/Synth/SynthOps.td b/include/dynamatic/Dialect/Synth/SynthOps.td new file mode 100644 index 000000000..a6404d082 --- /dev/null +++ b/include/dynamatic/Dialect/Synth/SynthOps.td @@ -0,0 +1,168 @@ +//===----------------------------------------------------------------------===// +// All the files related to the description of the Synth dialect +// have been modified from the original version present in the +// circt project at the following link: +// https://github.com/llvm/circt/tree/main/ +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// 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 defines the operations of the Synth dialect. +// +//===----------------------------------------------------------------------===// + +#ifndef DYNAMATIC_DIALECT_SYNTH_SYNTHOPS_TD +#define DYNAMATIC_DIALECT_SYNTH_SYNTHOPS_TD + +include "dynamatic/Dialect/Synth/Synth.td" +include "mlir/IR/OpBase.td" +include "mlir/Interfaces/ControlFlowInterfaces.td" +include "mlir/Interfaces/InferTypeOpInterface.td" +include "mlir/Interfaces/SideEffectInterfaces.td" + +// Base class for the operations in this dialect. +class SynthOp traits = []> : + Op; + +def MajorityInverterOp : SynthOp<"mig.maj_inv", + [SameOperandsAndResultType, Pure]> { + let summary = "Majority-Inverter operation"; + let description = [{ + The `synth.mig.maj_inv` operation represents a Majority-Inverter in the + Synth dialect. This is used to represent majority inverter graph in + synthesis. This operation computes the majority function of its inputs, + where operands can be inverted respectively. + + The majority function returns 1 when more than half of the inputs are 1, + and 0 otherwise. For three inputs, it's equivalent to: + (a & b) | (a & c) | (b & c). + + Example: + ```mlir + %r1 = synth.mig.maj_inv %a, %b, %c : i1 + %r2 = synth.mig.maj_inv not %a, %b, not %c : i1 + %r3 = synth.mig.maj_inv %a, %b, %c, %d, %e : i3 + ``` + + The number of inputs must be odd to avoid ties. + }]; + let arguments = (ins Variadic:$inputs, + DenseBoolArrayAttr:$inverted); + let results = (outs AnyType:$result); + let hasVerifier = true; + let hasCanonicalizeMethod = true; + let hasFolder = true; + + let assemblyFormat = [{ + custom($inputs, type($result), $inverted, + attr-dict) + }]; + let cppNamespace = "::dynamatic::synth::mig"; + let extraClassDeclaration = [{ + // Evaluate the operation with the given input values. + APInt evaluate(ArrayRef inputs); + + // Check if the input is inverted. + bool isInverted(size_t idx) { + return getInverted()[idx]; + } + }]; + +} + +def AndInverterOp : SynthOp<"aig.and_inv", [SameOperandsAndResultType, Pure]> { + let summary = "AIG dialect AND operation"; + let description = [{ + The `synth.aig.and_inv` operation represents an And-Inverter in the AIG dialect. + Unlike `comb.and`, operands can be inverted respectively. + + Example: + ```mlir + %r1 = synth.aig.and_inv %a, %b: i3 + %r2 = synth.aig.and_inv not %a, %b, not %c : i3 + %r3 = synth.aig.and_inv not %a : i3 + ``` + + Traditionally, an And-Node in AIG has two operands. However, `synth.aig.and_inv` + extends this concept by allowing variadic operands and non-i1 integer types. + Although the final stage of the synthesis pipeline requires lowering + everything to i1-binary operands, it's more efficient to progressively lower + the variadic multibit operations. + + Variadic operands have demonstrated their utility in low-level optimizations + within the `comb` dialect. Furthermore, in synthesis, it's common practice + to re-balance the logic path. Variadic operands enable the compiler to + select more efficient solutions without the need to traverse binary trees + multiple times. + + The ability to represent multibit operations during synthesis is crucial for + scalability. This approach enables a form of vectorization, allowing for + batch processing of logic synthesis when multibit operations are constructed + in a similar manner. + }]; + // TODO: Restrict to HWIntegerType. + let arguments = (ins Variadic:$inputs, DenseBoolArrayAttr:$inverted); + let results = (outs AnyType:$result); + + let assemblyFormat = [{ + custom($inputs, type($result), $inverted, attr-dict) + }]; + + let builders = [OpBuilder<(ins "Value":$input, CArg<"bool", "false">:$invert), + [{ + SmallVector inverted {invert}; + return build($_builder, $_state, {input}, inverted); + }]>, + OpBuilder<(ins "Value":$lhs, "Value":$rhs, + CArg<"bool", "false">:$invertLhs, + CArg<"bool", "false">:$invertRhs), + [{ + SmallVector inverted {invertLhs, invertRhs}; + return build($_builder, $_state, {lhs, rhs}, inverted); + }]>]; + + let extraClassDeclaration = [{ + // Evaluate the operation with the given input values. + APInt evaluate(ArrayRef inputs); + + // Check if the input is inverted. + bool isInverted(size_t idx) { + return getInverted()[idx]; + } + }]; + let hasFolder = 1; + let hasCanonicalizeMethod = 1; + let cppNamespace = "::dynamatic::synth::aig"; +} + +def LatchOp : SynthOp<"latch", [SameOperandsAndResultType, Pure]> { + let summary = "Latch operation"; + let description = [{ + The `synth.latch` operation represents a latch in the Synth dialect. + It models a storage element that holds its value until updated. + }]; + let arguments = (ins AnyType:$input); + let results = (outs AnyType:$output); + + let cppNamespace = "::dynamatic::synth"; +} + +def SubcktOp : SynthOp<"subckt", [Pure]> { + let summary = "Subcircuit operation"; + let description = [{ + The `synth.subckt` operation represents a subcircuit instance in the Synth dialect. + It allows to instantiate a predefined module within the circuit. + }]; + let arguments = (ins Variadic:$inputs, StrAttr:$moduleName); + let results = (outs Variadic:$outputs); + + let cppNamespace = "::dynamatic::synth"; +} + +#endif // DYNAMATIC_DIALECT_SYNTH_SYNTHOPS_TD diff --git a/lib/Dialect/Synth/CMakeLists.txt b/lib/Dialect/Synth/CMakeLists.txt new file mode 100644 index 000000000..8cfe49af4 --- /dev/null +++ b/lib/Dialect/Synth/CMakeLists.txt @@ -0,0 +1,31 @@ +##===----------------------------------------------------------------------===// +## All the files related to the description of the Synth dialect +## have been modified from the original version present in the +## circt project at the following link: +## https://github.com/llvm/circt/tree/main/ +##===----------------------------------------------------------------------===// + +##===----------------------------------------------------------------------===// +## +## Implementation files for the Synth dialect. +## +##===----------------------------------------------------------------------===// + +add_dynamatic_dialect_library(DynamaticSynth + SynthDialect.cpp + SynthOps.cpp + + ADDITIONAL_HEADER_DIRS + ${DYNAMATIC_MAIN_INCLUDE_DIR}/dynamatic/Dialect/Synth + + DEPENDS + MLIRSynthIncGen + + LINK_COMPONENTS + Support + + LINK_LIBS PUBLIC + MLIRIR + DynamaticHW +) + diff --git a/lib/Dialect/Synth/SynthDialect.cpp b/lib/Dialect/Synth/SynthDialect.cpp new file mode 100644 index 000000000..d9da0c44b --- /dev/null +++ b/lib/Dialect/Synth/SynthDialect.cpp @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// All the files related to the description of the Synth dialect +// have been modified from the original version present in the +// circt project at the following link: +// https://github.com/llvm/circt/tree/main/ +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "dynamatic/Dialect/Synth/SynthDialect.h" +#include "dynamatic/Dialect/HW/HWOps.h" +#include "dynamatic/Dialect/Synth/SynthOps.h" + +using namespace dynamatic; +using namespace synth; + +void SynthDialect::initialize() { + addOperations< +#define GET_OP_LIST +#include "dynamatic/Dialect/Synth/Synth.cpp.inc" + >(); +} + +Operation *SynthDialect::materializeConstant(OpBuilder &builder, + Attribute value, Type type, + Location loc) { + // Integer constants. + if (auto intType = dyn_cast(type)) + if (auto attrValue = dyn_cast(value)) + return builder.create(loc, type, attrValue); + // hw::ConstantOp::create(builder, loc, type, attrValue); + return nullptr; +} + +#include "dynamatic/Dialect/Synth/SynthDialect.cpp.inc" diff --git a/lib/Dialect/Synth/SynthOps.cpp b/lib/Dialect/Synth/SynthOps.cpp new file mode 100644 index 000000000..dd0df120f --- /dev/null +++ b/lib/Dialect/Synth/SynthOps.cpp @@ -0,0 +1,355 @@ +//===----------------------------------------------------------------------===// +// All the files related to the description of the Synth dialect +// have been modified from the original version present in the +// circt project at the following link: +// https://github.com/llvm/circt/tree/main/ +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#include "dynamatic/Dialect/Synth/SynthOps.h" +#include "dynamatic/Dialect/HW/HWOps.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/Matchers.h" +#include "mlir/IR/OpDefinition.h" +#include "mlir/IR/PatternMatch.h" +#include "llvm/ADT/APInt.h" +#include "llvm/Support/Casting.h" + +using namespace mlir; +using namespace dynamatic; +using namespace dynamatic::synth::mig; +using namespace dynamatic::synth::aig; + +#define GET_OP_CLASSES +#include "dynamatic/Dialect/Synth/Synth.cpp.inc" + +LogicalResult MajorityInverterOp::verify() { + if (getNumOperands() % 2 != 1) + return emitOpError("requires an odd number of operands"); + + return success(); +} + +llvm::APInt MajorityInverterOp::evaluate(ArrayRef inputs) { + assert(inputs.size() == getNumOperands() && + "Number of inputs must match number of operands"); + + if (inputs.size() == 3) { + auto a = (isInverted(0) ? ~inputs[0] : inputs[0]); + auto b = (isInverted(1) ? ~inputs[1] : inputs[1]); + auto c = (isInverted(2) ? ~inputs[2] : inputs[2]); + return (a & b) | (a & c) | (b & c); + } + + // General case for odd number of inputs != 3 + auto width = inputs[0].getBitWidth(); + APInt result(width, 0); + + for (size_t bit = 0; bit < width; ++bit) { + size_t count = 0; + for (size_t i = 0; i < inputs.size(); ++i) { + // Count the number of 1s, considering inversion. + if (isInverted(i) ^ inputs[i][bit]) + count++; + } + + if (count > inputs.size() / 2) + result.setBit(bit); + } + + return result; +} + +OpFoldResult MajorityInverterOp::fold(FoldAdaptor adaptor) { + // TODO: Implement maj(x, 1, 1) = 1, maj(x, 0, 0) = 0 + + SmallVector inputValues; + for (auto input : adaptor.getInputs()) { + auto attr = llvm::dyn_cast_or_null(input); + if (!attr) + return {}; + inputValues.push_back(attr.getValue()); + } + + auto result = evaluate(inputValues); + return IntegerAttr::get(getType(), result); +} + +LogicalResult MajorityInverterOp::canonicalize(MajorityInverterOp op, + PatternRewriter &rewriter) { + if (op.getNumOperands() == 1) { + if (op.getInverted()[0]) + return failure(); + rewriter.replaceOp(op, op.getOperand(0)); + return success(); + } + + // For now, only support 3 operands. + if (op.getNumOperands() != 3) + return failure(); + + // Return if the idx-th operand is a constant (inverted if necessary), + // otherwise return std::nullopt. + auto getConstant = [&](unsigned index) -> std::optional { + APInt value; + if (mlir::matchPattern(op.getInputs()[index], mlir::m_ConstantInt(&value))) + return op.isInverted(index) ? ~value : value; + return std::nullopt; + }; + + // Replace the op with the idx-th operand (inverted if necessary). + auto replaceWithIndex = [&](int index) { + bool inverted = op.isInverted(index); + if (inverted) + rewriter.replaceOpWithNewOp( + op, op.getType(), op.getOperand(index), true); + else + rewriter.replaceOp(op, op.getOperand(index)); + return success(); + }; + + // Pattern match following cases: + // maj_inv(x, x, y) -> x + // maj_inv(x, y, not y) -> x + for (int i = 0; i < 2; ++i) { + for (int j = i + 1; j < 3; ++j) { + int k = 3 - (i + j); + assert(k >= 0 && k < 3); + // If we have two identical operands, we can fold. + if (op.getOperand(i) == op.getOperand(j)) { + // If they are inverted differently, we can fold to the third. + if (op.isInverted(i) != op.isInverted(j)) + return replaceWithIndex(k); + return replaceWithIndex(i); + } + + // If i and j are constant. + if (auto c1 = getConstant(i)) { + if (auto c2 = getConstant(j)) { + // If both constants are equal, we can fold. + if (*c1 == *c2) { + rewriter.replaceOpWithNewOp( + op, op.getType(), mlir::IntegerAttr::get(op.getType(), *c1)); + return success(); + } + // If constants are complementary, we can fold. + if (*c1 == ~*c2) + return replaceWithIndex(k); + } + } + } + } + return failure(); +} + +//===----------------------------------------------------------------------===// +// AIG Operations +//===----------------------------------------------------------------------===// + +OpFoldResult AndInverterOp::fold(FoldAdaptor adaptor) { + if (getNumOperands() == 1 && !isInverted(0)) + return getOperand(0); + + auto inputs = adaptor.getInputs(); + if (inputs.size() == 2) + if (auto intAttr = dyn_cast_or_null(inputs[1])) { + auto value = intAttr.getValue(); + if (isInverted(1)) + value = ~value; + if (value.isZero()) + return IntegerAttr::get( + IntegerType::get(getContext(), value.getBitWidth()), value); + if (value.isAllOnes()) { + if (isInverted(0)) + return {}; + + return getOperand(0); + } + } + return {}; +} + +LogicalResult AndInverterOp::canonicalize(AndInverterOp op, + PatternRewriter &rewriter) { + SmallDenseMap seen; + SmallVector uniqueValues; + SmallVector uniqueInverts; + + APInt constValue = + APInt::getAllOnes(op.getResult().getType().getIntOrFloatBitWidth()); + + bool invertedConstFound = false; + bool flippedFound = false; + + for (auto [value, inverted] : llvm::zip(op.getInputs(), op.getInverted())) { + bool newInverted = inverted; + if (auto constOp = value.getDefiningOp()) { + if (inverted) { + constValue &= ~constOp.getValue(); + invertedConstFound = true; + } else { + constValue &= constOp.getValue(); + } + continue; + } + + if (auto andInverterOp = value.getDefiningOp()) { + if (andInverterOp.getInputs().size() == 1 && + andInverterOp.isInverted(0)) { + value = andInverterOp.getOperand(0); + newInverted = andInverterOp.isInverted(0) ^ inverted; + flippedFound = true; + } + } + + auto it = seen.find(value); + if (it == seen.end()) { + seen.insert({value, newInverted}); + uniqueValues.push_back(value); + uniqueInverts.push_back(newInverted); + } else if (it->second != newInverted) { + // replace with const 0 + rewriter.replaceOpWithNewOp( + op, APInt::getZero(value.getType().getIntOrFloatBitWidth())); + return success(); + } + } + + // If the constant is zero, we can just replace with zero. + if (constValue.isZero()) { + rewriter.replaceOpWithNewOp(op, constValue); + return success(); + } + + // No change. + if ((uniqueValues.size() == op.getInputs().size() && !flippedFound) || + (!constValue.isAllOnes() && !invertedConstFound && + uniqueValues.size() + 1 == op.getInputs().size())) + return failure(); + + if (!constValue.isAllOnes()) { + auto constOp = rewriter.create(op.getLoc(), constValue); + uniqueInverts.push_back(false); + uniqueValues.push_back(constOp); + } + + // It means the input is reduced to all ones. + if (uniqueValues.empty()) { + rewriter.replaceOpWithNewOp(op, constValue); + return success(); + } + + // build new op with reduced input values + auto name = op->getAttrOfType("sv.namehint"); + auto newOp = rewriter.replaceOpWithNewOp( + op, op.getType(), uniqueValues, uniqueInverts); + if (name && !newOp->hasAttr("sv.namehint")) + newOp->setAttr("sv.namehint", name); + return success(); +} + +APInt AndInverterOp::evaluate(ArrayRef inputs) { + assert(inputs.size() == getNumOperands() && + "Expected as many inputs as operands"); + assert(!inputs.empty() && "Expected non-empty input list"); + APInt result = APInt::getAllOnes(inputs.front().getBitWidth()); + for (auto [idx, input] : llvm::enumerate(inputs)) { + if (isInverted(idx)) + result &= ~input; + else + result &= input; + } + return result; +} + +static Value lowerVariadicAndInverterOp(AndInverterOp op, OperandRange operands, + ArrayRef inverts, + PatternRewriter &rewriter) { + switch (operands.size()) { + case 0: + assert(0 && "cannot be called with empty operand range"); + break; + case 1: + if (inverts[0]) + return rewriter.create(op.getLoc(), + operands[0], true); + else + return operands[0]; + case 2: + return rewriter.create( + op.getLoc(), operands[0], operands[1], inverts[0], inverts[1]); + default: + auto firstHalf = operands.size() / 2; + auto lhs = + lowerVariadicAndInverterOp(op, operands.take_front(firstHalf), + inverts.take_front(firstHalf), rewriter); + auto rhs = + lowerVariadicAndInverterOp(op, operands.drop_front(firstHalf), + inverts.drop_front(firstHalf), rewriter); + return rewriter.create(op.getLoc(), lhs, rhs); + } + return Value(); +} + +LogicalResult +dynamatic::synth::AndInverterVariadicOpConversion::matchAndRewrite( + AndInverterOp op, PatternRewriter &rewriter) const { + if (op.getInputs().size() <= 2) + return failure(); + // TODO: This is a naive implementation that creates a balanced binary tree. + // We can improve by analyzing the dataflow and creating a tree that + // improves the critical path or area. + rewriter.replaceOp(op, lowerVariadicAndInverterOp( + op, op.getOperands(), op.getInverted(), rewriter)); + return success(); +} + +// Parsing function for assembly format of AndInverterOp +ParseResult dynamatic::parseVariadicInvertibleOperands( + OpAsmParser &parser, + SmallVectorImpl &operands, Type &resultType, + mlir::DenseBoolArrayAttr &inverted, NamedAttrList &attrDict) { + SmallVector inverts; + + while (true) { + inverts.push_back(succeeded(parser.parseOptionalKeyword("not"))); + operands.push_back(OpAsmParser::UnresolvedOperand()); + + if (parser.parseOperand(operands.back())) + return failure(); + if (parser.parseOptionalComma()) + break; + } + + if (parser.parseOptionalAttrDict(attrDict) || parser.parseColon() || + parser.parseCustomTypeWithFallback(resultType)) + return failure(); + + inverted = parser.getBuilder().getDenseBoolArrayAttr(inverts); + return success(); +} + +// Printing function for assembly format of AndInverterOp +void dynamatic::printVariadicInvertibleOperands( + OpAsmPrinter &printer, Operation *op, OperandRange operands, + Type resultType, mlir::DenseBoolArrayAttr inverted, + DictionaryAttr attrDict) { + + llvm::interleaveComma(llvm::zip(inverted.asArrayRef(), operands), printer, + [&](auto &&pair) { + auto [invert, input] = pair; + if (invert) + printer << "not "; + printer << input; + }); + printer.printOptionalAttrDict(attrDict.getValue(), {"inverted"}); + printer << " : "; + printer.printType(resultType); +} \ No newline at end of file From e35babb492b89060f36fb0f16e31cf14069876a4 Mon Sep 17 00:00:00 2001 From: Carmine Rizzi Date: Thu, 8 Jan 2026 16:52:43 +0100 Subject: [PATCH 02/28] Added synth in the compilation structure of dynamatic --- include/dynamatic/Dialect/CMakeLists.txt | 1 + lib/Dialect/CMakeLists.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/include/dynamatic/Dialect/CMakeLists.txt b/include/dynamatic/Dialect/CMakeLists.txt index 9fd53996d..3a7207370 100644 --- a/include/dynamatic/Dialect/CMakeLists.txt +++ b/include/dynamatic/Dialect/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(Handshake) add_subdirectory(HW) +add_subdirectory(Synth) diff --git a/lib/Dialect/CMakeLists.txt b/lib/Dialect/CMakeLists.txt index 9fd53996d..3a7207370 100644 --- a/lib/Dialect/CMakeLists.txt +++ b/lib/Dialect/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(Handshake) add_subdirectory(HW) +add_subdirectory(Synth) From 81f3cb7dde49d259fbd4a76c50c75d171c1b262b Mon Sep 17 00:00:00 2001 From: Carmine Rizzi Date: Thu, 8 Jan 2026 16:54:29 +0100 Subject: [PATCH 03/28] Added conversion pass from Handshake to Synth --- .../dynamatic/Conversion/HandshakeToSynth.h | 47 ++ include/dynamatic/Conversion/Passes.h | 1 + include/dynamatic/Conversion/Passes.td | 16 +- lib/Conversion/CMakeLists.txt | 1 + .../HandshakeToSynth/CMakeLists.txt | 19 + .../HandshakeToSynth/HandshakeToSynth.cpp | 693 ++++++++++++++++++ tools/dynamatic-opt/CMakeLists.txt | 1 + 7 files changed, 777 insertions(+), 1 deletion(-) create mode 100644 include/dynamatic/Conversion/HandshakeToSynth.h create mode 100644 lib/Conversion/HandshakeToSynth/CMakeLists.txt create mode 100644 lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp diff --git a/include/dynamatic/Conversion/HandshakeToSynth.h b/include/dynamatic/Conversion/HandshakeToSynth.h new file mode 100644 index 000000000..c629e5d68 --- /dev/null +++ b/include/dynamatic/Conversion/HandshakeToSynth.h @@ -0,0 +1,47 @@ +//===- HandshakeToSynth.h - Convert Handshake to Synth ----------------*- C++ +//-*-===// +// +// Dynamatic is 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 the --lower-handshake-to-synth conversion pass. +// +//===----------------------------------------------------------------------===// + +#ifndef DYNAMATIC_CONVERSION_HANDSHAKETOSYNTH_H +#define DYNAMATIC_CONVERSION_HANDSHAKETOSYNTH_H + +#include "dynamatic/Support/DynamaticPass.h" + +namespace dynamatic { + +namespace synth { +/// Forward declare the Synth dialect which the pass depends on. +class SynthDialect; +} // namespace synth + +namespace hw { +/// Forward declare the HW dialect which the pass depends on. +class HWDialect; +} // namespace hw + +// Keywords for data and control signals when unbundling Handshake types using +// enums. +enum SignalKind { + DATA_SIGNAL = 0, + VALID_SIGNAL = 1, + READY_SIGNAL = 2, +}; + +#define GEN_PASS_DECL_HANDSHAKETOSYNTH +#define GEN_PASS_DEF_HANDSHAKETOSYNTH +#include "dynamatic/Conversion/Passes.h.inc" + +std::unique_ptr createHandshakeToSynthPass(); + +} // namespace dynamatic + +#endif // DYNAMATIC_CONVERSION_HANDSHAKETOSYNTH_H \ No newline at end of file diff --git a/include/dynamatic/Conversion/Passes.h b/include/dynamatic/Conversion/Passes.h index 54b6ec4ee..474775f0c 100644 --- a/include/dynamatic/Conversion/Passes.h +++ b/include/dynamatic/Conversion/Passes.h @@ -16,6 +16,7 @@ #include "dynamatic/Conversion/AffineToScf.h" #include "dynamatic/Conversion/CfToHandshake.h" #include "dynamatic/Conversion/HandshakeToHW.h" +#include "dynamatic/Conversion/HandshakeToSynth.h" #include "dynamatic/Conversion/ScfToCf.h" #include "mlir/IR/DialectRegistry.h" #include "mlir/Pass/Pass.h" diff --git a/include/dynamatic/Conversion/Passes.td b/include/dynamatic/Conversion/Passes.td index c86b98a5e..3a094823f 100644 --- a/include/dynamatic/Conversion/Passes.td +++ b/include/dynamatic/Conversion/Passes.td @@ -81,5 +81,19 @@ def HandshakeToHW }]; let constructor = "dynamatic::createHandshakeToHWPass()"; } - + +//===----------------------------------------------------------------------===// +// HandshakeToSynth +//===----------------------------------------------------------------------===// + +def HandshakeToSynth + : DynamaticPass<"lower-handshake-to-synth", ["dynamatic::synth::SynthDialect", "dynamatic::hw::HWDialect"]> { + let summary = "Lowers Handshake to Synth."; + let description = [{ + Lowers Handshake IR into Synth IR, which is a hardware representation + that focuses on logic synthesis constructs. + }]; + let constructor = "dynamatic::createHandshakeToSynthPass()"; +} + #endif // DYNAMATIC_CONVERSION_PASSES_TD diff --git a/lib/Conversion/CMakeLists.txt b/lib/Conversion/CMakeLists.txt index 3692a26aa..e369e6da2 100644 --- a/lib/Conversion/CMakeLists.txt +++ b/lib/Conversion/CMakeLists.txt @@ -2,3 +2,4 @@ add_subdirectory(AffineToScf) add_subdirectory(CfToHandshake) add_subdirectory(ScfToCf) add_subdirectory(HandshakeToHW) +add_subdirectory(HandshakeToSynth) diff --git a/lib/Conversion/HandshakeToSynth/CMakeLists.txt b/lib/Conversion/HandshakeToSynth/CMakeLists.txt new file mode 100644 index 000000000..b5320676a --- /dev/null +++ b/lib/Conversion/HandshakeToSynth/CMakeLists.txt @@ -0,0 +1,19 @@ +add_dynamatic_library(DynamaticHandshakeToSynth + HandshakeToSynth.cpp + + DEPENDS + DynamaticConversionPassIncGen + + LINK_LIBS PUBLIC + DynamaticSupportRTL + DynamaticHandshake + DynamaticSynth + MLIRIR + MLIRPass + MLIRArithDialect + MLIRControlFlowDialect + MLIRFuncDialect + MLIRSupport + MLIRTransforms + MLIRAffineToStandard + ) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp new file mode 100644 index 000000000..a4f21fd9c --- /dev/null +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -0,0 +1,693 @@ +//===- HandshakeToSynth.cpp - Convert Handshake to Synth --------------*- C++ +//-*-===// +// +// Dynamatic is 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 +// +//===----------------------------------------------------------------------===// +// +// Converts Handshake constructs into equivalent Synth constructs. +// +//===----------------------------------------------------------------------===// + +#include "dynamatic/Conversion/HandshakeToSynth.h" +#include "dynamatic/Analysis/NameAnalysis.h" +#include "dynamatic/Dialect/HW/HWOpInterfaces.h" +#include "dynamatic/Dialect/HW/HWOps.h" +#include "dynamatic/Dialect/HW/HWTypes.h" +#include "dynamatic/Dialect/HW/PortImplementation.h" +#include "dynamatic/Dialect/Handshake/HandshakeDialect.h" +#include "dynamatic/Dialect/Handshake/HandshakeInterfaces.h" +#include "dynamatic/Dialect/Handshake/HandshakeOps.h" +#include "dynamatic/Dialect/Handshake/HandshakeTypes.h" +#include "dynamatic/Dialect/Handshake/MemoryInterfaces.h" +#include "dynamatic/Dialect/Synth/SynthDialect.h" +#include "dynamatic/Dialect/Synth/SynthOps.h" +#include "dynamatic/Support/Attribute.h" +#include "dynamatic/Support/Backedge.h" +#include "dynamatic/Support/Utils/Utils.h" +#include "dynamatic/Transforms/HandshakeMaterialize.h" +#include "mlir/Dialect/MemRef/IR/MemRef.h" +#include "mlir/IR/Attributes.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinOps.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/Diagnostics.h" +#include "mlir/IR/IRMapping.h" +#include "mlir/IR/MLIRContext.h" +#include "mlir/IR/PatternMatch.h" +#include "mlir/IR/TypeRange.h" +#include "mlir/IR/Types.h" +#include "mlir/IR/Value.h" +#include "mlir/Support/LLVM.h" +#include "mlir/Support/LogicalResult.h" +#include "mlir/Transforms/DialectConversion.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/ADT/TypeSwitch.h" +#include "llvm/Support/Casting.h" +#include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace dynamatic; +using namespace dynamatic::handshake; + +#define DEBUG_TYPE "handshake-to-synth" + +// Function to unbundle a single handshake type into its subcomponents +SmallVector> unbundleType(Type type) { + // The reason for unbundling is that handshake channels are bundled + // types that contain data, valid and ready signals, while hw module + // ports are flat, so we need to unbundle the channel into its components + return TypeSwitch>>(type) + // Case for channel type where we unbundle into data, valid, ready + .Case([](handshake::ChannelType chanType) { + return SmallVector>{ + std::make_pair(DATA_SIGNAL, chanType.getDataType()), + std::make_pair(VALID_SIGNAL, + IntegerType::get(chanType.getContext(), 1)), + std::make_pair(READY_SIGNAL, + IntegerType::get(chanType.getContext(), 1))}; + }) + // Case for control type where we unbundle into valid, ready + .Case([](handshake::ControlType ctrlType) { + return SmallVector>{ + std::make_pair(VALID_SIGNAL, + IntegerType::get(ctrlType.getContext(), 1)), + std::make_pair(READY_SIGNAL, + IntegerType::get(ctrlType.getContext(), 1))}; + }) + // Case for memref type where we extract the element type as data, + // valid, ready + .Case([](MemRefType memType) { + return SmallVector>{ + std::make_pair(DATA_SIGNAL, memType.getElementType()), + std::make_pair(VALID_SIGNAL, + IntegerType::get(memType.getContext(), 1)), + std::make_pair(READY_SIGNAL, + IntegerType::get(memType.getContext(), 1))}; + }) + .Default([&](Type t) { + return SmallVector>{ + std::make_pair(DATA_SIGNAL, t)}; + }); +} +// Function to unbundle the ports of an handshake operation into hw module +// Unbundling handshake op ports into hw module ports since handshake ops +// have bundled channel types (composed of data, valid, ready) while hw +// modules have flat ports +void unbundleOpPorts(Operation *op, + SmallVector> &inputPorts, + SmallVector> &outputPorts) { + for (auto input : op->getOperands()) { + // Unbundle the input type depending on its actual type + SmallVector> inputUnbundled = + unbundleType(input.getType()); + inputPorts.append(inputUnbundled.begin(), inputUnbundled.end()); + } + for (auto result : op->getResults()) { + // Unbundle the output type depending on its actual type + SmallVector> outputUnbundled = + unbundleType(result.getType()); + outputPorts.append(outputUnbundled.begin(), outputUnbundled.end()); + } + // If the operation is a handshake function, the ports are extracted + // differently + if (isa(op)) { + handshake::FuncOp funcOp = cast(op); + // Clear existing ports + inputPorts.clear(); + outputPorts.clear(); + // Extract ports from the function type + for (auto arg : funcOp.getArguments()) { + SmallVector> inputUnbundled = + unbundleType(arg.getType()); + inputPorts.append(inputUnbundled.begin(), inputUnbundled.end()); + } + for (auto resultType : funcOp.getResultTypes()) { + SmallVector> outputUnbundled = + unbundleType(resultType); + outputPorts.append(outputUnbundled.begin(), outputUnbundled.end()); + } + } +} + +// Type converter to unbundle Handshake types +class ChannelUnbundlingTypeConverter : public TypeConverter { +public: + ChannelUnbundlingTypeConverter() { + addConversion([](Type type, + SmallVectorImpl &results) -> LogicalResult { + // Unbundle the type into its components + SmallVector unbundledTypes; + SmallVector> unbundled = unbundleType(type); + for (auto &pair : unbundled) { + unbundledTypes.push_back(pair.second); + } + results.append(unbundledTypes.begin(), unbundledTypes.end()); + return success(); + }); + + addTargetMaterialization([&](OpBuilder &builder, Type resultType, + ValueRange inputs, + Location loc) -> std::optional { + if (inputs.size() != 1) + return std::nullopt; + return inputs[0]; + }); + + addSourceMaterialization([&](OpBuilder &builder, Type resultType, + ValueRange inputs, + Location loc) -> std::optional { + if (inputs.size() != 1) + return std::nullopt; + return inputs[0]; + }); + } +}; + +// Function to get the HW Module input and output port info from an handshake +// operation +void getHWModulePortInfo(Operation *op, SmallVector &hwInputPorts, + SmallVector &hwOutputPorts) { + MLIRContext *ctx = op->getContext(); + // Unbundle the operation ports + SmallVector> unbundledInputPorts; + SmallVector> unbundledOutputPorts; + unbundleOpPorts(op, unbundledInputPorts, unbundledOutputPorts); + // Fill in the hw port info for inputs + for (auto [idx, port] : llvm::enumerate(unbundledInputPorts)) { + std::string name; + Type type; + // Unpack the port info as name and type + name = port.first == DATA_SIGNAL ? "in_data_" + std::to_string(idx) + : port.first == VALID_SIGNAL ? "in_valid_" + std::to_string(idx) + : "in_ready_" + std::to_string(idx); + type = port.second; + // Create the hw port info and add it to the list + hwInputPorts.push_back( + hw::PortInfo{hw::ModulePort{StringAttr::get(ctx, name), type, + hw::ModulePort::Direction::Input}, + idx}); + } + // Fill in the hw port info for outputs + for (auto [idx, port] : llvm::enumerate(unbundledOutputPorts)) { + std::string name; + Type type; + // Unpack the port info as name and type + name = port.first == DATA_SIGNAL ? "out_data_" + std::to_string(idx) + : port.first == VALID_SIGNAL ? "out_valid_" + std::to_string(idx) + : "out_ready_" + std::to_string(idx); + type = port.second; + // Create the hw port info and add it to the list + hwOutputPorts.push_back( + hw::PortInfo{hw::ModulePort{StringAttr::get(ctx, name), type, + hw::ModulePort::Direction::Output}, + idx}); + } +} + +// Function to convert a handshake function operation into an hw module +// operation +// Function to convert an handshake operation into an hw module operation +hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, + ConversionPatternRewriter &rewriter) { + MLIRContext *ctx = funcOp.getContext(); + // Obtain the hw module port info by unbundling the function ports + SmallVector hwInputPorts; + SmallVector hwOutputPorts; + getHWModulePortInfo(funcOp, hwInputPorts, hwOutputPorts); + hw::ModulePortInfo portInfo(hwInputPorts, hwOutputPorts); + // Create the hw module operation + auto parentModule = funcOp->getParentOfType(); + SymbolTable symbolTable(parentModule); + StringRef uniqueOpName = getUniqueName(funcOp); + StringAttr moduleName = StringAttr::get(ctx, uniqueOpName); + hw::HWModuleOp hwModule = symbolTable.lookup(moduleName); + + if (!hwModule) { + // IMPORTANT: modules must be created at module scope + rewriter.setInsertionPointToStart(parentModule.getBody()); + + hwModule = + rewriter.create(funcOp.getLoc(), moduleName, portInfo); + + // Move the block from the Handshake function to the new HW module, after + // which the Handshake function becomes empty and can be deleted + Block *funcBlock = funcOp.getBodyBlock(); + Block *modBlock = hwModule.getBodyBlock(); + Operation *termOp = modBlock->getTerminator(); + ValueRange modBlockArgs = modBlock->getArguments(); + // Set insertion point for the casts as inside the module body + rewriter.setInsertionPointToStart(modBlock); + // Create a cast for each argument in modBlockArgs + SmallVector castedArgs; + unsigned modBlockArgIdx = 0; + for (auto arg : funcOp.getArguments()) { + SmallVector> unbundledArgTypes = + unbundleType(arg.getType()); + unsigned numUnbundled = unbundledArgTypes.size(); + SmallVector argComponents; + for (unsigned i = 0; i < numUnbundled; ++i) { + argComponents.push_back(modBlockArgs[modBlockArgIdx + i]); + } + TypeRange unbundledTypeRange; + SmallVector unbundledTypes; + for (auto &pair : unbundledArgTypes) { + unbundledTypes.push_back(pair.second); + } + unbundledTypeRange = TypeRange(unbundledTypes); + auto cast = rewriter.create( + termOp->getLoc(), arg.getType(), argComponents); + castedArgs.push_back(cast->getResult(0)); + modBlockArgIdx += numUnbundled; + } + + // Region &srcRegion = funcOp.getBody(); + // Region &dstRegion = hwModule.getBody(); + // srcRegion.cloneInto(&dstRegion, mapping); + // for (Operation &op : funcBlock->without_terminator()) { + // rewriter.clone(op, mapping); + // } + rewriter.inlineBlockBefore(funcBlock, termOp, castedArgs); + handshake::EndOp endOp; + + for (Operation &op : *hwModule.getBodyBlock()) { + if (auto e = dyn_cast(op)) { + endOp = e; + break; + } + } + + assert(endOp && "Expected handshake.end after inlining"); + + SmallVector hwOutputs; + + for (Value operand : endOp.getOperands()) { + // Unbundle the operand type depending on its actual type + // by creating a cast from the original type to the unbundled types + SmallVector> unbundledTypes = + unbundleType(operand.getType()); + SmallVector unbundledTypeList; + for (auto &pair : unbundledTypes) { + unbundledTypeList.push_back(pair.second); + } + TypeRange unbundledTypeRange(unbundledTypeList); + auto cast = rewriter.create( + endOp.getLoc(), unbundledTypeRange, operand); + hwOutputs.append(cast.getResults().begin(), cast.getResults().end()); + } + rewriter.setInsertionPointToEnd(endOp->getBlock()); + rewriter.replaceOpWithNewOp(endOp, hwOutputs); + Operation *oldOutput = nullptr; + for (Operation &op : *hwModule.getBodyBlock()) { + if (auto out = dyn_cast(op)) { + if (out.getOperands().empty()) { + oldOutput = out; + break; + } + } + } + + if (oldOutput) + rewriter.eraseOp(oldOutput); + + rewriter.eraseOp(funcOp); + return hwModule; + // // Print previous terminator for debugging + // Create terminator for the hw module + SmallVector retOperands; + for (auto operand : funcBlock->getTerminator()->getOperands()) { + SmallVector> unbundledResultTypes = + unbundleType(operand.getType()); + // Create a temporary cast to connect the operand to the unbundled + // module outputs + SmallVector unbundledTypes; + for (auto &pair : unbundledResultTypes) { + unbundledTypes.push_back(pair.second); + } + TypeRange unbundledTypeRange(unbundledTypes); + auto cast = rewriter.create( + termOp->getLoc(), unbundledTypeRange, operand); + + retOperands.append(cast->getResults().begin(), cast->getResults().end()); + } + // rewriter.create(funcOp.getLoc(), retOperands); + rewriter.setInsertionPoint(termOp); + // Add the output operation + rewriter.replaceOpWithNewOp(termOp, retOperands); + // Update terminator pointer + termOp = hwModule.getBodyBlock()->getTerminator(); + // Print termOp for debugging + rewriter.eraseOp(funcOp); + } + return hwModule; +} + +// Function to convert an handshake operation into an hw module operation +hw::HWModuleOp convertOpToHWModule(Operation *op, + ConversionPatternRewriter &rewriter) { + MLIRContext *ctx = op->getContext(); + // Obtain the hw module port info by unbundling the operation ports + SmallVector hwInputPorts; + SmallVector hwOutputPorts; + getHWModulePortInfo(op, hwInputPorts, hwOutputPorts); + hw::ModulePortInfo portInfo(hwInputPorts, hwOutputPorts); + // Create the hw module operation + auto parentModule = op->getParentOfType(); + SymbolTable symbolTable(parentModule); + StringRef uniqueOpName = getUniqueName(op); + StringAttr moduleName = StringAttr::get(ctx, uniqueOpName); + hw::HWModuleOp hwModule = symbolTable.lookup(moduleName); + + if (!hwModule) { + // IMPORTANT: modules must be created at module scope + rewriter.setInsertionPointToStart(parentModule.getBody()); + + hwModule = + rewriter.create(op->getLoc(), moduleName, portInfo); + + // Fill in the body of the hw module a synth subckt instance + rewriter.setInsertionPointToStart(hwModule.getBodyBlock()); + SmallVector synthInputs; + for (auto arg : hwModule.getBodyBlock()->getArguments()) { + synthInputs.push_back(arg); + } + // Outputs types + SmallVector synthOutputTypes; + for (auto &outputPort : hwOutputPorts) { + synthOutputTypes.push_back(outputPort.type); + } + TypeRange synthOutputsTypeRange(synthOutputTypes); + synth::SubcktOp synthInstOp = rewriter.create( + op->getLoc(), synthOutputsTypeRange, synthInputs, "synth_subckt"); + Operation *synthTerminator = hwModule.getBodyBlock()->getTerminator(); + synthTerminator->setOperands(synthInstOp.getResults()); + } + + // Create the hw instance + rewriter.setInsertionPointAfter(op); + SmallVector operandValues; + for (auto operand : op->getOperands()) { + auto definingOp = operand.getDefiningOp(); + // Check if the operation generating the operand is a handshake operation + // or a block argument + bool isBlockArg = isa(operand); + if ((definingOp && + definingOp->getDialect()->getNamespace() == + handshake::HandshakeDialect::getDialectNamespace()) || + isBlockArg) { + // If so, create an unrealized conversion cast to unbundle the operand + // into its components for the hw instance + SmallVector> unbundledTypes = + unbundleType(operand.getType()); + SmallVector unbundledTypeList; + for (auto &pair : unbundledTypes) { + unbundledTypeList.push_back(pair.second); + } + TypeRange unbundledTypeRange(unbundledTypeList); + auto cast = rewriter.create( + op->getLoc(), unbundledTypeRange, operand); + // Append all cast results to the operand values + operandValues.append(cast.getResults().begin(), cast.getResults().end()); + continue; + } + operandValues.push_back(operand); + } + hw::InstanceOp hwInstOp = rewriter.create( + op->getLoc(), hwModule, + StringAttr::get(ctx, uniqueOpName.str() + "_inst"), operandValues); + // Create a cast for each result + SmallVector castsResults; + unsigned resultIdx = 0; + for (unsigned i = 0; i < op->getNumResults(); ++i) { + unsigned numUnbundledTypes = 0; + SmallVector> unbundledTypes = + unbundleType(op->getResult(i).getType()); + numUnbundledTypes = unbundledTypes.size(); + SmallVector hwInstResults = + hwInstOp.getResults().slice(resultIdx, numUnbundledTypes); + auto cast = rewriter.create( + op->getLoc(), TypeRange{op->getResult(i).getType()}, hwInstResults); + castsResults.push_back(cast.getResult(0)); + resultIdx += numUnbundledTypes; + } + + assert(castsResults.size() == op->getNumResults()); + + // Replace all uses of the original operation with the new hw module + rewriter.replaceOp(op, castsResults); + + return hwModule; +} + +// Conversion pattern to convert a generic handshake operation into an hw +// module operation +// This is used only for operation inside the handshake function, since the +// function itself is converted separately +namespace { +template +class ConvertToHWMod : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + using OpAdaptor = typename T::Adaptor; + LogicalResult + matchAndRewrite(T op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; +} // namespace + +template +LogicalResult +ConvertToHWMod::matchAndRewrite(T op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + + // Convert the operation into an hw module operation + hw::HWModuleOp newOp = convertOpToHWModule(op, rewriter); + if (!newOp) + return failure(); + return success(); +} + +// Conversion pattern to convert handshake function operation into an hw +// module operation +namespace { +class ConvertFuncToHWMod : public OpConversionPattern { +public: + using OpConversionPattern::OpConversionPattern; + using OpAdaptor = typename handshake::FuncOp::Adaptor; + LogicalResult + matchAndRewrite(handshake::FuncOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const override; +}; +} // namespace + +LogicalResult +ConvertFuncToHWMod::matchAndRewrite(handshake::FuncOp op, OpAdaptor adaptor, + ConversionPatternRewriter &rewriter) const { + + // Convert the function operation into an hw module operation + hw::HWModuleOp newOp = convertFuncOpToHWModule(op, rewriter); + if (!newOp) + return failure(); + return success(); +} + +namespace { + +class HandshakeToSynthPass + : public dynamatic::impl::HandshakeToSynthBase { +public: + void runDynamaticPass() override { + mlir::ModuleOp modOp = getOperation(); + MLIRContext *ctx = &getContext(); + OpBuilder builder(ctx); + // We only support one function per module + handshake::FuncOp funcOp = nullptr; + // Check this is the case + for (auto op : modOp.getOps()) { + if (op.isExternal()) + continue; + if (funcOp) { + modOp->emitOpError() << "we currently only support one non-external " + "handshake function per module"; + return signalPassFailure(); + } + funcOp = op; + } + // If there is no function, nothing to do + if (!funcOp) + return; + // Apply conversion patterns to convert each handshake operation into + // an hw module operation + RewritePatternSet patterns(ctx); + ChannelUnbundlingTypeConverter typeConverter; + ConversionTarget target(*ctx); + target.addLegalDialect(); + target.addLegalDialect(); + // Add casting as legal + target.addLegalOp(); + target.addIllegalDialect(); + // In the first step, we convert all handshake operations into hw module + // operations, except for the function and end operations which are kept + // for the next step + target.addLegalOp(); + target.addLegalOp(); + patterns.insert< + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + + // Arith operations + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, + + // Speculative operations + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod>(typeConverter, ctx); + if (failed(applyPartialConversion(modOp, target, std::move(patterns)))) + return signalPassFailure(); + + // In the second step, we convert the handshake function operation into + // an hw module operation + RewritePatternSet funcPatterns(ctx); + ConversionTarget funcTarget(*ctx); + funcTarget.addLegalDialect(); + funcTarget.addLegalDialect(); + // Add casting as legal + funcTarget.addLegalOp(); + funcTarget.addIllegalDialect(); + funcPatterns.insert(typeConverter, ctx); + if (failed( + applyPartialConversion(modOp, funcTarget, std::move(funcPatterns)))) + return signalPassFailure(); + + // Third and final step: remove all unrealized conversion casts + // Execute without conversion function but walking the module + // Get the rewriter object + ConversionPatternRewriter rewriter(ctx); + SmallVector castsToErase; + modOp.walk([&](UnrealizedConversionCastOp castOp1) { + // Consider only the following pattern op/arg -> cast (1) -> cast (2) + // -> op where there could be multiple cast2 + // Check if the input of the cast is another unrealized + // conversion cast + auto inputCastOp = + castOp1.getOperand(0).getDefiningOp(); + if (inputCastOp) + return; + // Check that the output/s of the cast are used by only one unrealized + // conversion cast + bool allUsersAreCasts = true; + SmallVector vecCastOps2; + for (auto result : castOp1.getResults()) { + for (auto &use : result.getUses()) { + if (!isa(use.getOwner())) { + allUsersAreCasts = false; + } else { + vecCastOps2.push_back( + cast(use.getOwner())); + } + } + if (!allUsersAreCasts) + break; + } + if (!allUsersAreCasts) { + // This breaks assumptions, so we emit an error for now + castOp1.emitError() + << "unrealized conversion cast removal failed due to complex " + "usage pattern"; + return signalPassFailure(); + } + // Replace all uses of the user cast op results with the original + // cast op inputs + // Assert that the number of inputs and outputs match + for (auto castOp2 : vecCastOps2) { + if (castOp1->getNumOperands() != castOp2->getNumResults()) { + castOp1.emitError() + << "unrealized conversion cast removal failed due to " + "mismatched number of operands and results"; + return signalPassFailure(); + } + for (auto [idx, result] : llvm::enumerate(castOp2.getResults())) { + result.replaceAllUsesWith(castOp1->getOperand(idx)); + } + // Erase both cast operations + castsToErase.push_back(castOp2); + } + castsToErase.push_back(castOp1); + }); + // Erase all collected casts + for (auto castOp : castsToErase) { + castOp.erase(); + } + + // Check that there are no more unrealized conversion casts + bool hasCasts = false; + modOp.walk([&](UnrealizedConversionCastOp castOp) { + hasCasts = true; + llvm::errs() << "Remaining unrealized conversion cast: " << castOp + << "\n"; + }); + + if (hasCasts) { + modOp.emitError() + << "unrealized conversion cast removal failed due to remaining " + "casts"; + return signalPassFailure(); + } + } +}; + +} // namespace + +std::unique_ptr +dynamatic::createHandshakeToSynthPass() { + return std::make_unique(); +} diff --git a/tools/dynamatic-opt/CMakeLists.txt b/tools/dynamatic-opt/CMakeLists.txt index 8516c3524..2be30c01f 100644 --- a/tools/dynamatic-opt/CMakeLists.txt +++ b/tools/dynamatic-opt/CMakeLists.txt @@ -24,6 +24,7 @@ target_link_libraries(dynamatic-opt DynamaticCfToHandshake DynamaticHandshake DynamaticHandshakeToHW + DynamaticHandshakeToSynth DynamaticTestTransforms DynamaticTransforms DynamaticTutorialsCreatingPasses From 660b5642c9fde92d0e345b826f83ed21eeb1336a Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Thu, 8 Jan 2026 18:51:01 +0100 Subject: [PATCH 04/28] Improved comments and restructured code for removing cast operations --- .../HandshakeToSynth/HandshakeToSynth.cpp | 181 ++++++++++-------- 1 file changed, 103 insertions(+), 78 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index a4f21fd9c..d2bc3d8d4 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -505,8 +505,102 @@ ConvertFuncToHWMod::matchAndRewrite(handshake::FuncOp op, OpAdaptor adaptor, return success(); } +// Function to remove unrealized conversion casts after conversion +LogicalResult removeUnrealizedConversionCasts(mlir::ModuleOp modOp) { + SmallVector castsToErase; + // Walk through all unrealized conversion casts in the module + modOp.walk([&](UnrealizedConversionCastOp castOp1) { + // Consider only the following pattern op/arg -> cast (1) -> cast (2) + // -> op where there could be multiple cast2 + // Check if the input of the cast is another unrealized + // conversion cast + auto inputCastOp = + castOp1.getOperand(0).getDefiningOp(); + if (inputCastOp) { + // Skip this cast since it will be handled when processing the + // input cast but first assert that it is not followed by any other cast. + // If this is the case, there is a issue in the code since there are 3 + // casts in chain and this break the assumption of the code + assert(llvm::none_of(castOp1->getUsers(), + [](Operation *user) { + return isa(user); + }) && + "unrealized conversion cast removal failed due to chained casts"); + return; + } + // Check that the output/s of the cast are used by only unrealized + // conversion casts + bool allUsersAreCasts = true; + SmallVector vecCastOps2; + for (auto result : castOp1.getResults()) { + for (auto &use : result.getUses()) { + if (!isa(use.getOwner())) { + allUsersAreCasts = false; + } else { + vecCastOps2.push_back( + cast(use.getOwner())); + } + } + if (!allUsersAreCasts) + break; + } + if (!allUsersAreCasts) { + // This breaks assumption that casts are chained in pairs + castOp1.emitError() + << "unrealized conversion cast removal failed due to complex " + "usage pattern"; + return; + } + // Replace all uses of the user cast op results with the original + // cast op inputs + // Assert that the number of inputs and outputs match + for (auto castOp2 : vecCastOps2) { + if (castOp1->getNumOperands() != castOp2->getNumResults()) { + castOp1.emitError() + << "unrealized conversion cast removal failed due to " + "mismatched number of operands and results"; + return; + } + for (auto [idx, result] : llvm::enumerate(castOp2.getResults())) { + result.replaceAllUsesWith(castOp1->getOperand(idx)); + } + // Add the cast to the list of casts to remove + castsToErase.push_back(castOp2); + } + // Add the cast to the list of casts to remove + castsToErase.push_back(castOp1); + }); + // Erase all collected casts + for (auto castOp : castsToErase) { + castOp.erase(); + } + + // Check that there are no more unrealized conversion casts + bool hasCasts = false; + modOp.walk([&](UnrealizedConversionCastOp castOp) { + hasCasts = true; + llvm::errs() << "Remaining unrealized conversion cast: " << castOp << "\n"; + }); + + if (hasCasts) { + modOp.emitError() + << "unrealized conversion cast removal failed due to remaining " + "casts"; + return failure(); + } + return success(); +} + namespace { +// The following pass converts handshake operations into synth operations +// It executes in multiple steps: +// 1) Convert all handshake operations (except for function and end ops) +// into hw module operations connecting them with unrealized conversion +// casts +// 2) Convert the handshake function operation into an hw module operation +// 3) Remove all unrealized conversion casts by connecting directly the +// inputs and outputs class HandshakeToSynthPass : public dynamatic::impl::HandshakeToSynthBase { public: @@ -530,8 +624,8 @@ class HandshakeToSynthPass // If there is no function, nothing to do if (!funcOp) return; - // Apply conversion patterns to convert each handshake operation into - // an hw module operation + // Step 1: Apply conversion patterns to convert each handshake operation + // into an hw module operation RewritePatternSet patterns(ctx); ChannelUnbundlingTypeConverter typeConverter; ConversionTarget target(*ctx); @@ -541,8 +635,8 @@ class HandshakeToSynthPass target.addLegalOp(); target.addIllegalDialect(); // In the first step, we convert all handshake operations into hw module - // operations, except for the function and end operations which are kept - // for the next step + // operations, except for the function and end operations which are kept to + // convert in the next step target.addLegalOp(); target.addLegalOp(); patterns.insert< @@ -594,8 +688,9 @@ class HandshakeToSynthPass if (failed(applyPartialConversion(modOp, target, std::move(patterns)))) return signalPassFailure(); - // In the second step, we convert the handshake function operation into - // an hw module operation + // Step 2: Convert the handshake function operation into + // an hw module operation and the corresponding terminator into an hw + // terminator RewritePatternSet funcPatterns(ctx); ConversionTarget funcTarget(*ctx); funcTarget.addLegalDialect(); @@ -608,80 +703,10 @@ class HandshakeToSynthPass applyPartialConversion(modOp, funcTarget, std::move(funcPatterns)))) return signalPassFailure(); - // Third and final step: remove all unrealized conversion casts + // Step 3: remove all unrealized conversion casts // Execute without conversion function but walking the module - // Get the rewriter object - ConversionPatternRewriter rewriter(ctx); - SmallVector castsToErase; - modOp.walk([&](UnrealizedConversionCastOp castOp1) { - // Consider only the following pattern op/arg -> cast (1) -> cast (2) - // -> op where there could be multiple cast2 - // Check if the input of the cast is another unrealized - // conversion cast - auto inputCastOp = - castOp1.getOperand(0).getDefiningOp(); - if (inputCastOp) - return; - // Check that the output/s of the cast are used by only one unrealized - // conversion cast - bool allUsersAreCasts = true; - SmallVector vecCastOps2; - for (auto result : castOp1.getResults()) { - for (auto &use : result.getUses()) { - if (!isa(use.getOwner())) { - allUsersAreCasts = false; - } else { - vecCastOps2.push_back( - cast(use.getOwner())); - } - } - if (!allUsersAreCasts) - break; - } - if (!allUsersAreCasts) { - // This breaks assumptions, so we emit an error for now - castOp1.emitError() - << "unrealized conversion cast removal failed due to complex " - "usage pattern"; - return signalPassFailure(); - } - // Replace all uses of the user cast op results with the original - // cast op inputs - // Assert that the number of inputs and outputs match - for (auto castOp2 : vecCastOps2) { - if (castOp1->getNumOperands() != castOp2->getNumResults()) { - castOp1.emitError() - << "unrealized conversion cast removal failed due to " - "mismatched number of operands and results"; - return signalPassFailure(); - } - for (auto [idx, result] : llvm::enumerate(castOp2.getResults())) { - result.replaceAllUsesWith(castOp1->getOperand(idx)); - } - // Erase both cast operations - castsToErase.push_back(castOp2); - } - castsToErase.push_back(castOp1); - }); - // Erase all collected casts - for (auto castOp : castsToErase) { - castOp.erase(); - } - - // Check that there are no more unrealized conversion casts - bool hasCasts = false; - modOp.walk([&](UnrealizedConversionCastOp castOp) { - hasCasts = true; - llvm::errs() << "Remaining unrealized conversion cast: " << castOp - << "\n"; - }); - - if (hasCasts) { - modOp.emitError() - << "unrealized conversion cast removal failed due to remaining " - "casts"; + if (failed(removeUnrealizedConversionCasts(modOp))) return signalPassFailure(); - } } }; From 57cf4584d548d46fd72d11b6308a512dd149dd38 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Thu, 8 Jan 2026 19:02:30 +0100 Subject: [PATCH 05/28] Restructured code to create a cast operation to connect bundled type to unbundled type --- .../HandshakeToSynth/HandshakeToSynth.cpp | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index d2bc3d8d4..0a380c84a 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -36,6 +36,7 @@ #include "mlir/IR/BuiltinTypes.h" #include "mlir/IR/Diagnostics.h" #include "mlir/IR/IRMapping.h" +#include "mlir/IR/Location.h" #include "mlir/IR/MLIRContext.h" #include "mlir/IR/PatternMatch.h" #include "mlir/IR/TypeRange.h" @@ -219,6 +220,23 @@ void getHWModulePortInfo(Operation *op, SmallVector &hwInputPorts, } } +// Function to create a cast operation between an bundled type to a unbundled +// type +UnrealizedConversionCastOp +createCastBundledToUnbundled(Value input, Location loc, + PatternRewriter &rewriter) { + SmallVector> unbundledTypes = + unbundleType(input.getType()); + SmallVector unbundledTypeList; + for (auto &pair : unbundledTypes) { + unbundledTypeList.push_back(pair.second); + } + TypeRange unbundledTypeRange(unbundledTypeList); + auto cast = rewriter.create( + loc, unbundledTypeRange, input); + return cast; +} + // Function to convert a handshake function operation into an hw module // operation // Function to convert an handshake operation into an hw module operation @@ -275,12 +293,6 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, modBlockArgIdx += numUnbundled; } - // Region &srcRegion = funcOp.getBody(); - // Region &dstRegion = hwModule.getBody(); - // srcRegion.cloneInto(&dstRegion, mapping); - // for (Operation &op : funcBlock->without_terminator()) { - // rewriter.clone(op, mapping); - // } rewriter.inlineBlockBefore(funcBlock, termOp, castedArgs); handshake::EndOp endOp; @@ -298,15 +310,8 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, for (Value operand : endOp.getOperands()) { // Unbundle the operand type depending on its actual type // by creating a cast from the original type to the unbundled types - SmallVector> unbundledTypes = - unbundleType(operand.getType()); - SmallVector unbundledTypeList; - for (auto &pair : unbundledTypes) { - unbundledTypeList.push_back(pair.second); - } - TypeRange unbundledTypeRange(unbundledTypeList); - auto cast = rewriter.create( - endOp.getLoc(), unbundledTypeRange, operand); + auto cast = + createCastBundledToUnbundled(operand, endOp->getLoc(), rewriter); hwOutputs.append(cast.getResults().begin(), cast.getResults().end()); } rewriter.setInsertionPointToEnd(endOp->getBlock()); @@ -411,15 +416,7 @@ hw::HWModuleOp convertOpToHWModule(Operation *op, isBlockArg) { // If so, create an unrealized conversion cast to unbundle the operand // into its components for the hw instance - SmallVector> unbundledTypes = - unbundleType(operand.getType()); - SmallVector unbundledTypeList; - for (auto &pair : unbundledTypes) { - unbundledTypeList.push_back(pair.second); - } - TypeRange unbundledTypeRange(unbundledTypeList); - auto cast = rewriter.create( - op->getLoc(), unbundledTypeRange, operand); + auto cast = createCastBundledToUnbundled(operand, op->getLoc(), rewriter); // Append all cast results to the operand values operandValues.append(cast.getResults().begin(), cast.getResults().end()); continue; From cb0720f8474b5178304aaba7363b243fb3a6525b Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Thu, 8 Jan 2026 19:35:11 +0100 Subject: [PATCH 06/28] Restructured creation of cast for operation results --- .../HandshakeToSynth/HandshakeToSynth.cpp | 89 ++++++++++++------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index 0a380c84a..ce6e6d793 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -220,7 +220,7 @@ void getHWModulePortInfo(Operation *op, SmallVector &hwInputPorts, } } -// Function to create a cast operation between an bundled type to a unbundled +// Function to create a cast operation between a bundled type to an unbundled // type UnrealizedConversionCastOp createCastBundledToUnbundled(Value input, Location loc, @@ -237,6 +237,43 @@ createCastBundledToUnbundled(Value input, Location loc, return cast; } +// Function to create a cast operation for each result of an unbundled operation +// when converting an original bundled operation to an unbundled one. This cast +// is essential to connect the unbundled operation back to the rest of the +// circuit which is potentially still bundled +SmallVector +createCastResultsUnbundledOp(TypeRange originalOpResultType, + SmallVector unbundledValues, Location loc, + PatternRewriter &rewriter) { + // Assert the number of results of unbundled op is larger or equal to the + // number of results in original operation + assert(unbundledValues.size() >= originalOpResultType.size() && + "the number of results of the unbundled operation must be larger or " + "equal to the original operation"); + SmallVector castsOps; + unsigned resultIdx = 0; + // Iterate over each result of the original operation + for (unsigned i = 0; i < originalOpResultType.size(); ++i) { + unsigned numUnbundledTypes = 0; + // Identify how many unbundled types correspond to this result + SmallVector> unbundledTypes = + unbundleType(originalOpResultType[i]); + numUnbundledTypes = unbundledTypes.size(); + // Get a slice of the unbundled values corresponding to this result from the + // unbundled operation + SmallVector unbundledValuesSlice(unbundledValues.begin() + resultIdx, + unbundledValues.begin() + + resultIdx + numUnbundledTypes); + // Create the cast from unbundled values to the original bundled type + auto cast = rewriter.create( + loc, TypeRange{originalOpResultType[i]}, unbundledValuesSlice); + // Collect the created cast + castsOps.push_back(cast); + resultIdx += numUnbundledTypes; + } + return castsOps; +} + // Function to convert a handshake function operation into an hw module // operation // Function to convert an handshake operation into an hw module operation @@ -272,25 +309,15 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, rewriter.setInsertionPointToStart(modBlock); // Create a cast for each argument in modBlockArgs SmallVector castedArgs; - unsigned modBlockArgIdx = 0; - for (auto arg : funcOp.getArguments()) { - SmallVector> unbundledArgTypes = - unbundleType(arg.getType()); - unsigned numUnbundled = unbundledArgTypes.size(); - SmallVector argComponents; - for (unsigned i = 0; i < numUnbundled; ++i) { - argComponents.push_back(modBlockArgs[modBlockArgIdx + i]); - } - TypeRange unbundledTypeRange; - SmallVector unbundledTypes; - for (auto &pair : unbundledArgTypes) { - unbundledTypes.push_back(pair.second); - } - unbundledTypeRange = TypeRange(unbundledTypes); - auto cast = rewriter.create( - termOp->getLoc(), arg.getType(), argComponents); - castedArgs.push_back(cast->getResult(0)); - modBlockArgIdx += numUnbundled; + TypeRange funcArgTypes = funcOp.getArgumentTypes(); + SmallVector castsArgs = + createCastResultsUnbundledOp(funcArgTypes, modBlockArgs, + funcOp.getLoc(), rewriter); + for (auto castOp : castsArgs) { + // Assert that each cast has only one result + assert(castOp.getNumResults() == 1 && + "each cast operation must have only one result"); + castedArgs.push_back(castOp.getResult(0)); } rewriter.inlineBlockBefore(funcBlock, termOp, castedArgs); @@ -426,20 +453,16 @@ hw::HWModuleOp convertOpToHWModule(Operation *op, hw::InstanceOp hwInstOp = rewriter.create( op->getLoc(), hwModule, StringAttr::get(ctx, uniqueOpName.str() + "_inst"), operandValues); - // Create a cast for each result + // Create a cast for each group of unbundled results + SmallVector castsOpResults = + createCastResultsUnbundledOp(op->getResultTypes(), hwInstOp->getResults(), + op->getLoc(), rewriter); SmallVector castsResults; - unsigned resultIdx = 0; - for (unsigned i = 0; i < op->getNumResults(); ++i) { - unsigned numUnbundledTypes = 0; - SmallVector> unbundledTypes = - unbundleType(op->getResult(i).getType()); - numUnbundledTypes = unbundledTypes.size(); - SmallVector hwInstResults = - hwInstOp.getResults().slice(resultIdx, numUnbundledTypes); - auto cast = rewriter.create( - op->getLoc(), TypeRange{op->getResult(i).getType()}, hwInstResults); - castsResults.push_back(cast.getResult(0)); - resultIdx += numUnbundledTypes; + for (auto castOp : castsOpResults) { + // Assert that each cast has only one result + assert(castOp.getNumResults() == 1 && + "each cast operation must have only one result"); + castsResults.push_back(castOp.getResult(0)); } assert(castsResults.size() == op->getNumResults()); From 3af418a8d4ab2de36fce927ef97e860690a5b2aa Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Thu, 8 Jan 2026 19:48:15 +0100 Subject: [PATCH 07/28] Re-structured code to instantiate synth operations in hw module representing handshake operation --- .../HandshakeToSynth/HandshakeToSynth.cpp | 65 ++++++++++++------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index ce6e6d793..e41d5211a 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -286,15 +286,15 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, getHWModulePortInfo(funcOp, hwInputPorts, hwOutputPorts); hw::ModulePortInfo portInfo(hwInputPorts, hwOutputPorts); // Create the hw module operation - auto parentModule = funcOp->getParentOfType(); - SymbolTable symbolTable(parentModule); + auto modOp = funcOp->getParentOfType(); + SymbolTable symbolTable(modOp); StringRef uniqueOpName = getUniqueName(funcOp); StringAttr moduleName = StringAttr::get(ctx, uniqueOpName); hw::HWModuleOp hwModule = symbolTable.lookup(moduleName); if (!hwModule) { // IMPORTANT: modules must be created at module scope - rewriter.setInsertionPointToStart(parentModule.getBody()); + rewriter.setInsertionPointToStart(modOp.getBody()); hwModule = rewriter.create(funcOp.getLoc(), moduleName, portInfo); @@ -388,6 +388,35 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, return hwModule; } +// Function to instantiate synth or hw operations inside an hw module describing +// the behavior of the original handshake operation +LogicalResult +instantiateSynthOpInHWModule(Operation *op, hw::HWModuleOp hwModule, + SmallVector &hwInputPorts, + SmallVector &hwOutputPorts, + ConversionPatternRewriter &rewriter) { + // Set insertion point to the start of the hw module body + rewriter.setInsertionPointToStart(hwModule.getBodyBlock()); + // Collect inputs values of the hw module + SmallVector synthInputs; + for (auto arg : hwModule.getBodyBlock()->getArguments()) { + synthInputs.push_back(arg); + } + // Collect the types of the hardware modules outputs + SmallVector synthOutputTypes; + for (auto &outputPort : hwOutputPorts) { + synthOutputTypes.push_back(outputPort.type); + } + TypeRange synthOutputsTypeRange(synthOutputTypes); + // Create the synth subcircuit operation inside the hw module + synth::SubcktOp synthInstOp = rewriter.create( + op->getLoc(), synthOutputsTypeRange, synthInputs, "synth_subckt"); + // Connect the outputs of the synth operation to the outputs of the hw module + Operation *synthTerminator = hwModule.getBodyBlock()->getTerminator(); + synthTerminator->setOperands(synthInstOp.getResults()); + return success(); +} + // Function to convert an handshake operation into an hw module operation hw::HWModuleOp convertOpToHWModule(Operation *op, ConversionPatternRewriter &rewriter) { @@ -397,36 +426,24 @@ hw::HWModuleOp convertOpToHWModule(Operation *op, SmallVector hwOutputPorts; getHWModulePortInfo(op, hwInputPorts, hwOutputPorts); hw::ModulePortInfo portInfo(hwInputPorts, hwOutputPorts); - // Create the hw module operation - auto parentModule = op->getParentOfType(); - SymbolTable symbolTable(parentModule); + // Create the hw module operation if it does not already exist + auto modOp = op->getParentOfType(); + SymbolTable symbolTable(modOp); StringRef uniqueOpName = getUniqueName(op); StringAttr moduleName = StringAttr::get(ctx, uniqueOpName); hw::HWModuleOp hwModule = symbolTable.lookup(moduleName); + // If it does not exist, create it if (!hwModule) { // IMPORTANT: modules must be created at module scope - rewriter.setInsertionPointToStart(parentModule.getBody()); + rewriter.setInsertionPointToStart(modOp.getBody()); hwModule = rewriter.create(op->getLoc(), moduleName, portInfo); - - // Fill in the body of the hw module a synth subckt instance - rewriter.setInsertionPointToStart(hwModule.getBodyBlock()); - SmallVector synthInputs; - for (auto arg : hwModule.getBodyBlock()->getArguments()) { - synthInputs.push_back(arg); - } - // Outputs types - SmallVector synthOutputTypes; - for (auto &outputPort : hwOutputPorts) { - synthOutputTypes.push_back(outputPort.type); - } - TypeRange synthOutputsTypeRange(synthOutputTypes); - synth::SubcktOp synthInstOp = rewriter.create( - op->getLoc(), synthOutputsTypeRange, synthInputs, "synth_subckt"); - Operation *synthTerminator = hwModule.getBodyBlock()->getTerminator(); - synthTerminator->setOperands(synthInstOp.getResults()); + // Instantiate the corresponding synth operation inside the hw module + if (failed(instantiateSynthOpInHWModule(op, hwModule, hwInputPorts, + hwOutputPorts, rewriter))) + return nullptr; } // Create the hw instance From fcf9dc93856f3ecc740025fdb4492b770e39642c Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Thu, 8 Jan 2026 19:54:22 +0100 Subject: [PATCH 08/28] Improved comments --- .../HandshakeToSynth/HandshakeToSynth.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index e41d5211a..ef5acc104 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -418,6 +418,9 @@ instantiateSynthOpInHWModule(Operation *op, hw::HWModuleOp hwModule, } // Function to convert an handshake operation into an hw module operation +// It is divided into two parts: first, create the hw module definition if it +// does not already exist, then instantiate the hw instance of the module and +// replace the original operation with it hw::HWModuleOp convertOpToHWModule(Operation *op, ConversionPatternRewriter &rewriter) { MLIRContext *ctx = op->getContext(); @@ -441,13 +444,15 @@ hw::HWModuleOp convertOpToHWModule(Operation *op, hwModule = rewriter.create(op->getLoc(), moduleName, portInfo); // Instantiate the corresponding synth operation inside the hw module + // definition if (failed(instantiateSynthOpInHWModule(op, hwModule, hwInputPorts, hwOutputPorts, rewriter))) return nullptr; } - // Create the hw instance + // Create the hw instance and replace the original operation with it rewriter.setInsertionPointAfter(op); + // First, collect the operands for the hw instance SmallVector operandValues; for (auto operand : op->getOperands()) { auto definingOp = operand.getDefiningOp(); @@ -459,14 +464,16 @@ hw::HWModuleOp convertOpToHWModule(Operation *op, handshake::HandshakeDialect::getDialectNamespace()) || isBlockArg) { // If so, create an unrealized conversion cast to unbundle the operand - // into its components for the hw instance + // into its components to connect it to the input ports of the hw instance auto cast = createCastBundledToUnbundled(operand, op->getLoc(), rewriter); // Append all cast results to the operand values operandValues.append(cast.getResults().begin(), cast.getResults().end()); continue; } + // If the operand is already unbundled, just use it directly operandValues.push_back(operand); } + // Then, create the hw instance operation hw::InstanceOp hwInstOp = rewriter.create( op->getLoc(), hwModule, StringAttr::get(ctx, uniqueOpName.str() + "_inst"), operandValues); @@ -474,6 +481,7 @@ hw::HWModuleOp convertOpToHWModule(Operation *op, SmallVector castsOpResults = createCastResultsUnbundledOp(op->getResultTypes(), hwInstOp->getResults(), op->getLoc(), rewriter); + // Collect the results of the casts SmallVector castsResults; for (auto castOp : castsOpResults) { // Assert that each cast has only one result @@ -481,7 +489,8 @@ hw::HWModuleOp convertOpToHWModule(Operation *op, "each cast operation must have only one result"); castsResults.push_back(castOp.getResult(0)); } - + // Assert that the number of cast results matches the number of original + // operation results assert(castsResults.size() == op->getNumResults()); // Replace all uses of the original operation with the new hw module From 01b535bcd51f8d1f3c27a1ade43be9da10375bbb Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Thu, 8 Jan 2026 20:08:37 +0100 Subject: [PATCH 09/28] Improved comments and cleaned up unused section of code --- .../HandshakeToSynth/HandshakeToSynth.cpp | 70 ++++++++----------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index ef5acc104..a8d8004db 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -276,7 +276,9 @@ createCastResultsUnbundledOp(TypeRange originalOpResultType, // Function to convert a handshake function operation into an hw module // operation -// Function to convert an handshake operation into an hw module operation +// It is divided into three parts: first, create the hw module definition if it +// does not already exist, then move the function body of the handshake function +// into the hw module, and finally create the output operation for the hw module hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, ConversionPatternRewriter &rewriter) { MLIRContext *ctx = funcOp.getContext(); @@ -285,13 +287,14 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, SmallVector hwOutputPorts; getHWModulePortInfo(funcOp, hwInputPorts, hwOutputPorts); hw::ModulePortInfo portInfo(hwInputPorts, hwOutputPorts); - // Create the hw module operation + // Create the hw module operation if it does not already exist auto modOp = funcOp->getParentOfType(); SymbolTable symbolTable(modOp); StringRef uniqueOpName = getUniqueName(funcOp); StringAttr moduleName = StringAttr::get(ctx, uniqueOpName); hw::HWModuleOp hwModule = symbolTable.lookup(moduleName); + // If it does not exist, create it if (!hwModule) { // IMPORTANT: modules must be created at module scope rewriter.setInsertionPointToStart(modOp.getBody()); @@ -299,30 +302,41 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, hwModule = rewriter.create(funcOp.getLoc(), moduleName, portInfo); - // Move the block from the Handshake function to the new HW module, after - // which the Handshake function becomes empty and can be deleted + // Inline the function body of the handshake function into the hw module + // body Block *funcBlock = funcOp.getBodyBlock(); Block *modBlock = hwModule.getBodyBlock(); Operation *termOp = modBlock->getTerminator(); ValueRange modBlockArgs = modBlock->getArguments(); - // Set insertion point for the casts as inside the module body + // Set insertion point inside the module body rewriter.setInsertionPointToStart(modBlock); - // Create a cast for each argument in modBlockArgs + // There must be a match between the arguments of the hw module and the + // handshake function to be able to inline the function body of the + // handshake function in the hw module. However, the arguments of the + // handshake function are bundled and the ones of the hw module are + // unbundled. For this reason, create a cast for each argument of the hw + // module. The output of the casts will be used to inline the function body + // First create casts for each argument of the hw module SmallVector castedArgs; TypeRange funcArgTypes = funcOp.getArgumentTypes(); SmallVector castsArgs = createCastResultsUnbundledOp(funcArgTypes, modBlockArgs, funcOp.getLoc(), rewriter); + // Collect the results of the casts for (auto castOp : castsArgs) { // Assert that each cast has only one result assert(castOp.getNumResults() == 1 && "each cast operation must have only one result"); castedArgs.push_back(castOp.getResult(0)); } - + // Inline the function body before the terminator of the hw module using the + // casted arguments rewriter.inlineBlockBefore(funcBlock, termOp, castedArgs); - handshake::EndOp endOp; + // Finally, create the output operation for the hw module + // Find the handshake.end operation in the inlined body which is the + // terminator for handshake functions + handshake::EndOp endOp; for (Operation &op : *hwModule.getBodyBlock()) { if (auto e = dyn_cast(op)) { endOp = e; @@ -332,17 +346,21 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, assert(endOp && "Expected handshake.end after inlining"); + // Create a cast for each operand of the end operation to unbundle the types + // which were previously handshake bundled types SmallVector hwOutputs; - for (Value operand : endOp.getOperands()) { - // Unbundle the operand type depending on its actual type - // by creating a cast from the original type to the unbundled types + // Unbundle the operand type depending by creating a cast from the + // original type to the unbundled types auto cast = createCastBundledToUnbundled(operand, endOp->getLoc(), rewriter); hwOutputs.append(cast.getResults().begin(), cast.getResults().end()); } + // Create the output operation for the hw module rewriter.setInsertionPointToEnd(endOp->getBlock()); rewriter.replaceOpWithNewOp(endOp, hwOutputs); + // Remove any old output operation if present (should not be the case after + // replacing the end op) Operation *oldOutput = nullptr; for (Operation &op : *hwModule.getBodyBlock()) { if (auto out = dyn_cast(op)) { @@ -352,37 +370,9 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, } } } - if (oldOutput) rewriter.eraseOp(oldOutput); - - rewriter.eraseOp(funcOp); - return hwModule; - // // Print previous terminator for debugging - // Create terminator for the hw module - SmallVector retOperands; - for (auto operand : funcBlock->getTerminator()->getOperands()) { - SmallVector> unbundledResultTypes = - unbundleType(operand.getType()); - // Create a temporary cast to connect the operand to the unbundled - // module outputs - SmallVector unbundledTypes; - for (auto &pair : unbundledResultTypes) { - unbundledTypes.push_back(pair.second); - } - TypeRange unbundledTypeRange(unbundledTypes); - auto cast = rewriter.create( - termOp->getLoc(), unbundledTypeRange, operand); - - retOperands.append(cast->getResults().begin(), cast->getResults().end()); - } - // rewriter.create(funcOp.getLoc(), retOperands); - rewriter.setInsertionPoint(termOp); - // Add the output operation - rewriter.replaceOpWithNewOp(termOp, retOperands); - // Update terminator pointer - termOp = hwModule.getBodyBlock()->getTerminator(); - // Print termOp for debugging + // Remove the original handshake function operation rewriter.eraseOp(funcOp); } return hwModule; From 3fca47ca9544fd1f6e9b7481fedf18c95372ab43 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Tue, 13 Jan 2026 11:51:36 +0100 Subject: [PATCH 10/28] Added a new class to invert the ready signals of hw modules --- .../HandshakeToSynth/HandshakeToSynth.cpp | 563 ++++++++++++++++-- 1 file changed, 499 insertions(+), 64 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index a8d8004db..10e11ce23 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -39,6 +39,7 @@ #include "mlir/IR/Location.h" #include "mlir/IR/MLIRContext.h" #include "mlir/IR/PatternMatch.h" +#include "mlir/IR/SymbolTable.h" #include "mlir/IR/TypeRange.h" #include "mlir/IR/Types.h" #include "mlir/IR/Value.h" @@ -67,6 +68,56 @@ using namespace dynamatic::handshake; #define DEBUG_TYPE "handshake-to-synth" +class ReadySignalInverter { +public: + // Function to invert the ready signals of an hw module operation + void + invertReadySignalHWModule(hw::HWModuleOp oldMod, ModuleOp parent, + SymbolTable &symTable, + DenseMap &newHWmodules, + DenseMap &oldHWmodules); + + // Function to invert the ready signals of an hw instance operation + void invertReadySignalHWInstance( + hw::InstanceOp oldInst, ModuleOp parent, SymbolTable &symTable, + DenseMap &newHWmodules, + DenseMap &oldHWmodules); + + // Function to get the name of the new hw module with inverted ready signals + mlir::StringAttr getNewModuleName(hw::HWModuleOp oldMod, + mlir::MLIRContext *ctx); + + // Function to get the mapping of an input signal + Value getInputSignalMapping(Value oldInputSignal, OpBuilder builder, + Location loc); + + // Function to update the mapping of an output signal + void updateOutputSignalMapping(Value oldResult, StringRef outputName, + hw::HWModuleOp newMod, hw::InstanceOp newInst); + + // Function to invert all ready signals in a module operation + LogicalResult invertAllReadySignals(mlir::ModuleOp modOp); + +private: + // IMPORTANT: A fundamental assumption for this map values to work is that + // each + // handshake channel connects uniquely one handshake unit to another one. If + // this assumption is broken, the map will not work correctly since one key + // could correspond to multiple values. + // + // Maps to keep track of signal connections during instance rewriting + // Old refers to the original hw instance with wrong ready signal directions + // New refers to the rewritten hw instance with correct ready signal + // directions + DenseMap oldModuleSignalToNewModuleSignalMap; + // Since the hw instances are rewritten in a recursive manner, it might be + // possible that we need the result of an instance that has not been created + // yet. To handle this case, we create temporary hw constant operations + // to hold the place of these values. This map keeps track of these temporary + // values + DenseMap oldModuleSignalToTempValueMap; +}; + // Function to unbundle a single handshake type into its subcomponents SmallVector> unbundleType(Type type) { // The reason for unbundling is that handshake channels are bundled @@ -109,39 +160,38 @@ SmallVector> unbundleType(Type type) { // Unbundling handshake op ports into hw module ports since handshake ops // have bundled channel types (composed of data, valid, ready) while hw // modules have flat ports -void unbundleOpPorts(Operation *op, - SmallVector> &inputPorts, - SmallVector> &outputPorts) { - for (auto input : op->getOperands()) { - // Unbundle the input type depending on its actual type - SmallVector> inputUnbundled = - unbundleType(input.getType()); - inputPorts.append(inputUnbundled.begin(), inputUnbundled.end()); - } - for (auto result : op->getResults()) { - // Unbundle the output type depending on its actual type - SmallVector> outputUnbundled = - unbundleType(result.getType()); - outputPorts.append(outputUnbundled.begin(), outputUnbundled.end()); - } +void unbundleOpPorts( + Operation *op, + SmallVector>> &inputPorts, + SmallVector>> &outputPorts) { // If the operation is a handshake function, the ports are extracted // differently if (isa(op)) { handshake::FuncOp funcOp = cast(op); - // Clear existing ports - inputPorts.clear(); - outputPorts.clear(); // Extract ports from the function type for (auto arg : funcOp.getArguments()) { SmallVector> inputUnbundled = unbundleType(arg.getType()); - inputPorts.append(inputUnbundled.begin(), inputUnbundled.end()); + inputPorts.push_back(inputUnbundled); } for (auto resultType : funcOp.getResultTypes()) { SmallVector> outputUnbundled = unbundleType(resultType); - outputPorts.append(outputUnbundled.begin(), outputUnbundled.end()); + outputPorts.push_back(outputUnbundled); } + return; + } + for (auto input : op->getOperands()) { + // Unbundle the input type depending on its actual type + SmallVector> inputUnbundled = + unbundleType(input.getType()); + inputPorts.push_back(inputUnbundled); + } + for (auto result : op->getResults()) { + // Unbundle the output type depending on its actual type + SmallVector> outputUnbundled = + unbundleType(result.getType()); + outputPorts.push_back(outputUnbundled); } } @@ -185,38 +235,46 @@ void getHWModulePortInfo(Operation *op, SmallVector &hwInputPorts, SmallVector &hwOutputPorts) { MLIRContext *ctx = op->getContext(); // Unbundle the operation ports - SmallVector> unbundledInputPorts; - SmallVector> unbundledOutputPorts; + SmallVector>> unbundledInputPorts; + SmallVector>> unbundledOutputPorts; unbundleOpPorts(op, unbundledInputPorts, unbundledOutputPorts); - // Fill in the hw port info for inputs - for (auto [idx, port] : llvm::enumerate(unbundledInputPorts)) { - std::string name; - Type type; - // Unpack the port info as name and type - name = port.first == DATA_SIGNAL ? "in_data_" + std::to_string(idx) - : port.first == VALID_SIGNAL ? "in_valid_" + std::to_string(idx) - : "in_ready_" + std::to_string(idx); - type = port.second; - // Create the hw port info and add it to the list - hwInputPorts.push_back( - hw::PortInfo{hw::ModulePort{StringAttr::get(ctx, name), type, - hw::ModulePort::Direction::Input}, - idx}); - } - // Fill in the hw port info for outputs - for (auto [idx, port] : llvm::enumerate(unbundledOutputPorts)) { - std::string name; - Type type; - // Unpack the port info as name and type - name = port.first == DATA_SIGNAL ? "out_data_" + std::to_string(idx) - : port.first == VALID_SIGNAL ? "out_valid_" + std::to_string(idx) - : "out_ready_" + std::to_string(idx); - type = port.second; - // Create the hw port info and add it to the list - hwOutputPorts.push_back( - hw::PortInfo{hw::ModulePort{StringAttr::get(ctx, name), type, - hw::ModulePort::Direction::Output}, - idx}); + // Iterate over each set of unbundled input ports and create hw port info from + // them + for (auto [idx, inputPortSet] : llvm::enumerate(unbundledInputPorts)) { + // Fill in the hw port info for inputs + for (auto port : inputPortSet) { + std::string name; + Type type; + // Unpack the port info as name and type + name = port.first == DATA_SIGNAL ? "in_data_" + std::to_string(idx) + : port.first == VALID_SIGNAL ? "in_valid_" + std::to_string(idx) + : "in_ready_" + std::to_string(idx); + type = port.second; + // Create the hw port info and add it to the list + hwInputPorts.push_back( + hw::PortInfo{hw::ModulePort{StringAttr::get(ctx, name), type, + hw::ModulePort::Direction::Input}, + idx}); + } + } + // Iterate over each set of unbundled output ports and create hw port info + // from them + for (auto [idx, outputPortSet] : llvm::enumerate(unbundledOutputPorts)) { + // Fill in the hw port info for outputs + for (auto port : outputPortSet) { + std::string name; + Type type; + // Unpack the port info as name and type + name = port.first == DATA_SIGNAL ? "out_data_" + std::to_string(idx) + : port.first == VALID_SIGNAL ? "out_valid_" + std::to_string(idx) + : "out_ready_" + std::to_string(idx); + type = port.second; + // Create the hw port info and add it to the list + hwOutputPorts.push_back( + hw::PortInfo{hw::ModulePort{StringAttr::get(ctx, name), type, + hw::ModulePort::Direction::Output}, + idx}); + } } } @@ -381,9 +439,7 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, // Function to instantiate synth or hw operations inside an hw module describing // the behavior of the original handshake operation LogicalResult -instantiateSynthOpInHWModule(Operation *op, hw::HWModuleOp hwModule, - SmallVector &hwInputPorts, - SmallVector &hwOutputPorts, +instantiateSynthOpInHWModule(hw::HWModuleOp hwModule, ConversionPatternRewriter &rewriter) { // Set insertion point to the start of the hw module body rewriter.setInsertionPointToStart(hwModule.getBodyBlock()); @@ -394,15 +450,17 @@ instantiateSynthOpInHWModule(Operation *op, hw::HWModuleOp hwModule, } // Collect the types of the hardware modules outputs SmallVector synthOutputTypes; - for (auto &outputPort : hwOutputPorts) { - synthOutputTypes.push_back(outputPort.type); + for (auto &outputPort : hwModule.getPortList()) { + if (outputPort.isOutput()) + synthOutputTypes.push_back(outputPort.type); } TypeRange synthOutputsTypeRange(synthOutputTypes); - // Create the synth subcircuit operation inside the hw module - synth::SubcktOp synthInstOp = rewriter.create( - op->getLoc(), synthOutputsTypeRange, synthInputs, "synth_subckt"); // Connect the outputs of the synth operation to the outputs of the hw module Operation *synthTerminator = hwModule.getBodyBlock()->getTerminator(); + // Create the synth subcircuit operation inside the hw module + synth::SubcktOp synthInstOp = rewriter.create( + synthTerminator->getLoc(), synthOutputsTypeRange, synthInputs, + "synth_subckt"); synthTerminator->setOperands(synthInstOp.getResults()); return success(); } @@ -435,8 +493,7 @@ hw::HWModuleOp convertOpToHWModule(Operation *op, rewriter.create(op->getLoc(), moduleName, portInfo); // Instantiate the corresponding synth operation inside the hw module // definition - if (failed(instantiateSynthOpInHWModule(op, hwModule, hwInputPorts, - hwOutputPorts, rewriter))) + if (failed(instantiateSynthOpInHWModule(hwModule, rewriter))) return nullptr; } @@ -627,6 +684,373 @@ LogicalResult removeUnrealizedConversionCasts(mlir::ModuleOp modOp) { return success(); } +// Function to get a new module name for the rewritten hw module +mlir::StringAttr ReadySignalInverter::getNewModuleName(hw::HWModuleOp oldMod, + mlir::MLIRContext *ctx) { + return mlir::StringAttr::get(ctx, oldMod.getName() + "_rewritten"); +} + +// Function to get the mapping of an input signal from the old module to the +// new module +Value ReadySignalInverter::getInputSignalMapping(Value oldInputSignal, + OpBuilder builder, + Location loc) { + auto it = oldModuleSignalToNewModuleSignalMap.find(oldInputSignal); + if (it != oldModuleSignalToNewModuleSignalMap.end()) { + return it->second; + } + // If there is no mapping, it means that the value is not yet + // available since the instance that produces it has not been + // rewritten yet. In this case, check if we already created a + // temporary value for this ready signal + auto tempIt = oldModuleSignalToTempValueMap.find(oldInputSignal); + if (tempIt != oldModuleSignalToTempValueMap.end()) { + return tempIt->second; + } + // Else, create a new temporary hw constant to hold the connection + auto tempConst = builder.create( + loc, oldInputSignal.getType(), + builder.getIntegerAttr(oldInputSignal.getType(), 0)); + // Store the temporary value in the map + oldModuleSignalToTempValueMap[oldInputSignal] = tempConst; + // Use the result of the constant as the operand + return tempConst->getResult(0); +} + +// Function to update the mapping between old module signals and new module +// signals after getting a new result value +void ReadySignalInverter::updateOutputSignalMapping(Value oldResult, + StringRef outputName, + hw::HWModuleOp newMod, + hw::InstanceOp newInst) { + // Find the corresponding output index of the output in the new module + int outputIdxNewInst = -1; + for (auto &p : newMod.getPortList()) { + if (p.name.getValue() == outputName) { + outputIdxNewInst = p.argNum; + break; + } + } + assert(outputIdxNewInst != -1 && "could not find output port in new module"); + Value newResult = newInst.getResult(outputIdxNewInst); + // Add mapping between old non-ready signal and new non-ready signal + oldModuleSignalToNewModuleSignalMap[oldResult] = newResult; + // Check if any temporary value has been created for the new result + // value by seeing if the old output value is in the map of temporary + // values + auto tempIt = oldModuleSignalToTempValueMap.find(oldResult); + if (tempIt != oldModuleSignalToTempValueMap.end()) { + // Replace all uses of the temporary value with the new result value + tempIt->second.replaceAllUsesWith(newResult); + // Remove the temporary value from the map + oldModuleSignalToTempValueMap.erase(tempIt); + // Remove the operation creating the temporary value + tempIt->second.getDefiningOp()->erase(); + } +} + +// Function to rewrite an hw instance operation to fix ready signal directions +void ReadySignalInverter::invertReadySignalHWInstance( + hw::InstanceOp oldInst, ModuleOp parent, SymbolTable &symTable, + DenseMap &newHWmodules, + DenseMap &oldHWmodules) { + + // This function executes the following steps: + // 1. Check if the instance's module has already been rewritten. If not, + // rewrite it. + // 2. Create a new instance operation with the new module and updated + // operands. + // 3. Update the mapping between old signals and new signals for the outputs + // of the new hw instance. + + // Step 1: Check if the instance's module has already been rewritten + OpBuilder builder(parent); + StringRef moduleName = oldInst.getModuleName(); + // If it has not been processed yet, rewrite it + if (!newHWmodules.count(moduleName)) { + hw::HWModuleOp oldMod = symTable.lookup(moduleName); + if (oldMod) { + invertReadySignalHWModule(oldMod, parent, symTable, newHWmodules, + oldHWmodules); + } + } + + // Step 2: Create a new instance operation with the new module and updated + // operands. + // Get the new hw module after rewriting to be used for the new instance + hw::HWModuleOp newMod = newHWmodules[moduleName]; + assert(newMod && "could not find new hw module for instance"); + + // Get the old hw module to identify the ready signals to change + hw::HWModuleOp oldMod = oldHWmodules[moduleName]; + assert(oldMod && "could not find old hw module for instance"); + + // Get the top-level module of the old instance + hw::HWModuleOp oldInstTopModule = oldInst->getParentOfType(); + StringRef oldInstTopModuleName = oldInstTopModule.getName(); + // Find the corresponding new module in the new hw modules map where to + // insert the new operations + hw::HWModuleOp newTopMod = newHWmodules[oldInstTopModuleName]; + assert(newTopMod && "could not find new top module for instance"); + // Create the new operations within the new hw module + builder.setInsertionPoint(newTopMod.getBodyBlock()->getTerminator()); + Location locNewOps = newTopMod.getBodyBlock()->getTerminator()->getLoc(); + + // Save the list of outputs that are not ready signals to replace uses later + SmallVector> nonReadyOutputs; + // Save the list of the old ready inputs which will be mapped to the new + // ready output values + SmallVector> oldReadyInputs; + // Save the new operands for the new instance + SmallVector newOperands; + // Iterate through the name of the ports to identify the mapping between + // signals of the old hw module and new hw module + for (auto &port : oldMod.getPortList()) { + bool isReady = port.name.getValue().contains("ready"); + if (port.isInput() && isReady) { + // This is an input of the old module and should become an output of the + // new module. Save the old input value to map it later to the + // corresponding new output value + oldReadyInputs.push_back(std::make_pair(port.name.getValue(), + oldInst.getOperand(port.argNum))); + } else if (port.isOutput() && isReady) { + // This is an output of the old module and should become an input of the + // new module. Check if there is a mapping for this ready signal + Value oldReadyOutput = oldInst->getResult(port.argNum); + Value newReadyInput = + getInputSignalMapping(oldReadyOutput, builder, locNewOps); + newOperands.push_back(newReadyInput); + } else if (port.isInput()) { + // Check if there is a mapping for this non-ready signal + Value oldNonReadyInput = oldInst.getOperand(port.argNum); + Value newNonReadyInput = + getInputSignalMapping(oldNonReadyInput, builder, locNewOps); + newOperands.push_back(newNonReadyInput); + } else { + // Collect non-ready outputs to replace uses later + nonReadyOutputs.push_back( + std::make_pair(port.name.getValue(), port.argNum)); + } + } + + // Create the new instance operation within the new hw module + auto newInst = builder.create( + locNewOps, newMod, oldInst.getInstanceNameAttr(), newOperands); + + // Step 3: Update the mapping between old signals and new signals after + // getting the new result values + for (auto [outputName, outputIdxOldInst] : nonReadyOutputs) { + // Find the output port in the old module + Value oldResult = oldInst.getResult(outputIdxOldInst); + updateOutputSignalMapping(oldResult, outputName, newMod, newInst); + } + // Update the mapping between old ready inputs and new ready outputs after + // getting the new result values + for (auto [inputName, oldReadyInput] : oldReadyInputs) { + updateOutputSignalMapping(oldReadyInput, inputName, newMod, newInst); + } +} + +// Function to rewrite an hw module to fix ready signal directions +void ReadySignalInverter::invertReadySignalHWModule( + hw::HWModuleOp oldMod, ModuleOp parent, SymbolTable &symTable, + DenseMap &newHWmodules, + DenseMap &oldHWmodules) { + + // This function executes the following steps: + // 1. Check if the module has already been rewritten. If so, return. + // 2. Create a new hw module with inverted ready signal directions. + // 3. Iterate through the body operations of the old module: + // a. If the operation is an hw instance, rewrite it to fix ready signal + // directions. + // b. If the operation is not an hw instance, check if it is only + // output operations and synth subckt operations. If so, create a + // synth subckt operation to connect the module ports. If not, raise an + // error. + // 4. Finally, connect the hw instances to the terminator operands of the new + // module. + + // Step 1: Check if the module has already been rewritten + if (newHWmodules.count(oldMod.getName())) { + return; + } + + // Step 2: Create a new hw module with inverted ready signal directions + MLIRContext *ctx = parent.getContext(); + OpBuilder builder(parent); + + SmallVector newInputs; + SmallVector newOutputs; + + // Collect the new inputs and outputs with inverted ready signal directions + unsigned inputIdx = 0; + unsigned outputIdx = 0; + // Store mapping from new output idx to old signal + SmallVector> newModuleOutputIdxToOldSignal; + // Store mapping from new input idx to old signal + SmallVector> newModuleInputIdxToOldSignal; + // Iterate over the ports of the old hw module + for (auto &p : oldMod.getPortList()) { + bool isReady = p.name.getValue().contains("ready"); + + if ((p.isInput() && isReady) || (p.isOutput() && !isReady)) { + // If the port is input and ready, it becomes output and ready in the new + // module. If a port is output and not ready, it stays as such. + newOutputs.push_back( + {hw::ModulePort{StringAttr::get(ctx, StringRef{p.name}), p.type, + hw::ModulePort::Direction::Output}, + outputIdx}); + Value oldSignal; + if (isReady) { + // Record mapping from new ready output to old ready input + oldSignal = oldMod.getBodyBlock()->getArgument(p.argNum); + } else { + // Record mapping from new non-ready output to old non-ready output + oldSignal = + oldMod.getBodyBlock()->getTerminator()->getOperand(p.argNum); + } + newModuleOutputIdxToOldSignal.push_back( + std::make_pair(outputIdx, oldSignal)); + outputIdx++; + } else if ((p.isOutput() && isReady) || (p.isInput() && !isReady)) { + // If the port is output and ready, it becomes input and ready in the new + // module. If a port is input and not ready, it stays as such. + newInputs.push_back( + {hw::ModulePort{StringAttr::get(ctx, StringRef{p.name}), p.type, + hw::ModulePort::Direction::Input}, + inputIdx}); + Value oldSignal; + if (isReady) { + // Record mapping from new ready input to old ready output + oldSignal = + oldMod.getBodyBlock()->getTerminator()->getOperand(p.argNum); + } else { + // Record mapping from new non-ready input to old non-ready input + oldSignal = oldMod.getBodyBlock()->getArgument(p.argNum); + } + newModuleInputIdxToOldSignal.push_back( + std::make_pair(inputIdx, oldSignal)); + inputIdx++; + } else { + assert(false && "port is neither input nor output"); + } + } + + hw::ModulePortInfo newPortInfo(newInputs, newOutputs); + + // Create new hw module + builder.setInsertionPointAfter(oldMod); + mlir::StringAttr newModuleName = getNewModuleName(oldMod, ctx); + auto newMod = builder.create(oldMod.getLoc(), newModuleName, + newPortInfo); + // Save it in the list of new module using the same name as the old module as + // key + newHWmodules[oldMod.getName()] = newMod; + + // Add mapping between new inputs to old signals + for (auto [newModuleInputIdx, oldSignal] : newModuleInputIdxToOldSignal) { + Value newReadyInput = newMod.getBodyBlock()->getArgument(newModuleInputIdx); + oldModuleSignalToNewModuleSignalMap[oldSignal] = newReadyInput; + } + + // Step 3: Iterate through the body operations of the old module + bool hasHwInstances = true; + // Iterate through old body operations and invert ready signals in instances + for (auto &op : oldMod.getBody().getOps()) { + // Check if the operation is an hw instance + if (auto instOp = dyn_cast(op)) { + // Step 3a: If the operation is an hw instance, rewrite it to fix ready + // signal directions. + invertReadySignalHWInstance(instOp, parent, symTable, newHWmodules, + oldHWmodules); + } else if (!isa(op)) { + hasHwInstances = false; + } + } + + if (!hasHwInstances) { + // Step 3b: If the operation is not an hw instance, check if it is only + // output operations and synth subckt operations. If so, create a + // synth subckt operation to connect the module ports. If not, raise an + // error. + for (auto &op : oldMod.getBody().getOps()) { + if (!isa(op) && !isa(op)) { + llvm::errs() << "Found non-instance operation in hw module: " << op + << "\n"; + assert(false && "found non-instance operation in hw module"); + } + } + ConversionPatternRewriter rewriter(ctx); + if (failed(instantiateSynthOpInHWModule(newMod, rewriter))) { + assert(false && "synth instantiation in hw module failed"); + } + return; + } + + // Step 4: Finally, connect the hw instances to the terminator operands of the + // new module. + builder.setInsertionPointToEnd(newMod.getBodyBlock()); + SmallVector newTerminatorOperands; + for (auto [newOutputIdx, oldSignal] : newModuleOutputIdxToOldSignal) { + Value newModuleOutput = oldModuleSignalToNewModuleSignalMap[oldSignal]; + assert(newModuleOutput && "could not find mapping for output signal"); + newTerminatorOperands.push_back(newModuleOutput); + } + + // Add operands to existing terminator + Operation *newTerminator = newMod.getBodyBlock()->getTerminator(); + newTerminator->setOperands(newTerminatorOperands); +} + +// Function to invert the direction of ready signals in all hw modules +LogicalResult ReadySignalInverter::invertAllReadySignals(mlir::ModuleOp modOp) { + + // The following function iterates through all hw modules in the module + // and rewrites them to invert the direction of ready signals to follow + // the standard handshake protocol where ready signals go in the opposite + // direction with respect to data and valid signals. It applies recursively + // to all hw modules instantiated within other hw modules. + // It does so by creating new hw modules with the rewritten ready signal + // directions and then connecting them following the same graph structure of + // the old modules. Finally, it removes the old hw modules and renames the new + // hw modules to the original names. + + // Maps to keep track of old and new hw modules + // The old hw modules have the wrong ready signal directions where ready + // signals follows the same direction as data and valid signals + // The new hw modules will contain the rewritten modules with correct ready + // signal directions + DenseMap oldHWModules; + DenseMap newHWModules; + // Get symbol table for hw modules + SymbolTable symTable(modOp); + // Collect all hw modules in the module + modOp.walk([&](hw::HWModuleOp m) { oldHWModules.insert({m.getName(), m}); }); + // Iterate through all hw modules and rewrite them + for (auto [name, hwMod] : oldHWModules) + invertReadySignalHWModule(hwMod, modOp, symTable, newHWModules, + oldHWModules); + // Erase old hw modules + for (auto [modName, oldMod] : oldHWModules) { + oldMod.erase(); + } + // Iterate through all new hw modules and rename them to the original names + for (auto [originalName, newMod] : newHWModules) { + mlir::StringAttr originalNameAttr = + mlir::StringAttr::get(modOp.getContext(), originalName); + StringRef currentModName = newMod.getName(); + // Change the module name of the instances of this module + modOp.walk([&](hw::InstanceOp instOp) { + if (instOp.getModuleName() == currentModName) { + instOp.setModuleName(originalNameAttr); + } + }); + // Rename the new module to the original name + newMod.setName(originalName); + } + return success(); +} + namespace { // The following pass converts handshake operations into synth operations @@ -636,7 +1060,10 @@ namespace { // casts // 2) Convert the handshake function operation into an hw module operation // 3) Remove all unrealized conversion casts by connecting directly the -// inputs and outputs +// inputs and outputs of the hw module instances +// 4) Invert the direction of ready signals in all hw modules and hw instances +// to follow the standard handshake protocol where ready signals go in the +// opposite direction with respect to data and valid signals class HandshakeToSynthPass : public dynamatic::impl::HandshakeToSynthBase { public: @@ -671,8 +1098,8 @@ class HandshakeToSynthPass target.addLegalOp(); target.addIllegalDialect(); // In the first step, we convert all handshake operations into hw module - // operations, except for the function and end operations which are kept to - // convert in the next step + // operations, except for the function and end operations which are kept + // to convert in the next step target.addLegalOp(); target.addLegalOp(); patterns.insert< @@ -743,6 +1170,14 @@ class HandshakeToSynthPass // Execute without conversion function but walking the module if (failed(removeUnrealizedConversionCasts(modOp))) return signalPassFailure(); + + // Step 4: invert the direction of all ready signals in the hw modules + // created from handshake operations + // Create on object of the ReadySignalInverter class to manage the + // inversion + ReadySignalInverter inverter; + if (failed(inverter.invertAllReadySignals(modOp))) + return signalPassFailure(); } }; From f34bb17df63c5f8ddfcd271e3d35451af86758ec Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Tue, 13 Jan 2026 12:07:32 +0100 Subject: [PATCH 11/28] Re-structured main pass function --- .../HandshakeToSynth/HandshakeToSynth.cpp | 194 ++++++++++-------- 1 file changed, 105 insertions(+), 89 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index 10e11ce23..b477b733f 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -684,6 +684,102 @@ LogicalResult removeUnrealizedConversionCasts(mlir::ModuleOp modOp) { return success(); } +// Function to unbundle all handshake type in a handshake function operation +LogicalResult unbundleAllHandshakeTypes(ModuleOp modOp, MLIRContext *ctx) { + + // This function executes the unbundling conversion in three steps: + // 1) Convert all handshake operations (except for function and end ops) + // into hw module operations connecting them with unrealized conversion + // casts + // 2) Convert the handshake function operation into an hw module operation + // 3) Remove all unrealized conversion casts by connecting directly the + // inputs and outputs of the hw module instances + + // Step 1: Apply conversion patterns to convert each handshake operation + // into an hw module operation + RewritePatternSet patterns(ctx); + ChannelUnbundlingTypeConverter typeConverter; + ConversionTarget target(*ctx); + target.addLegalDialect(); + target.addLegalDialect(); + // Add casting as legal + target.addLegalOp(); + target.addIllegalDialect(); + // In the first step, we convert all handshake operations into hw module + // operations, except for the function and end operations which are kept + // to convert in the next step + target.addLegalOp(); + target.addLegalOp(); + patterns.insert< + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + + // Arith operations + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, ConvertToHWMod, + ConvertToHWMod, + + // Speculative operations + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod, + ConvertToHWMod>(typeConverter, ctx); + if (failed(applyPartialConversion(modOp, target, std::move(patterns)))) + return failure(); + + // Step 2: Convert the handshake function operation into + // an hw module operation and the corresponding terminator into an hw + // terminator + RewritePatternSet funcPatterns(ctx); + ConversionTarget funcTarget(*ctx); + funcTarget.addLegalDialect(); + funcTarget.addLegalDialect(); + // Add casting as legal + funcTarget.addLegalOp(); + funcTarget.addIllegalDialect(); + funcPatterns.insert(typeConverter, ctx); + if (failed( + applyPartialConversion(modOp, funcTarget, std::move(funcPatterns)))) + return failure(); + + // Step 3: remove all unrealized conversion casts + // Execute without conversion function but walking the module + if (failed(removeUnrealizedConversionCasts(modOp))) + return failure(); + return success(); +} + +// ------------------------------------------------------------------ +// Definition of functions of ReadySignalInverter class +// ------------------------------------------------------------------ // Function to get a new module name for the rewritten hw module mlir::StringAttr ReadySignalInverter::getNewModuleName(hw::HWModuleOp oldMod, mlir::MLIRContext *ctx) { @@ -1051,17 +1147,16 @@ LogicalResult ReadySignalInverter::invertAllReadySignals(mlir::ModuleOp modOp) { return success(); } +// ------------------------------------------------------------------ +// Main pass definition +// ------------------------------------------------------------------ + namespace { // The following pass converts handshake operations into synth operations // It executes in multiple steps: -// 1) Convert all handshake operations (except for function and end ops) -// into hw module operations connecting them with unrealized conversion -// casts -// 2) Convert the handshake function operation into an hw module operation -// 3) Remove all unrealized conversion casts by connecting directly the -// inputs and outputs of the hw module instances -// 4) Invert the direction of ready signals in all hw modules and hw instances +// 1) Unbundle all handshake types used in the handshake function +// 2) Invert the direction of ready signals in all hw modules and hw instances // to follow the standard handshake protocol where ready signals go in the // opposite direction with respect to data and valid signals class HandshakeToSynthPass @@ -1087,91 +1182,12 @@ class HandshakeToSynthPass // If there is no function, nothing to do if (!funcOp) return; - // Step 1: Apply conversion patterns to convert each handshake operation - // into an hw module operation - RewritePatternSet patterns(ctx); - ChannelUnbundlingTypeConverter typeConverter; - ConversionTarget target(*ctx); - target.addLegalDialect(); - target.addLegalDialect(); - // Add casting as legal - target.addLegalOp(); - target.addIllegalDialect(); - // In the first step, we convert all handshake operations into hw module - // operations, except for the function and end operations which are kept - // to convert in the next step - target.addLegalOp(); - target.addLegalOp(); - patterns.insert< - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, - - // Arith operations - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, ConvertToHWMod, - ConvertToHWMod, - - // Speculative operations - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod, - ConvertToHWMod>(typeConverter, ctx); - if (failed(applyPartialConversion(modOp, target, std::move(patterns)))) - return signalPassFailure(); - - // Step 2: Convert the handshake function operation into - // an hw module operation and the corresponding terminator into an hw - // terminator - RewritePatternSet funcPatterns(ctx); - ConversionTarget funcTarget(*ctx); - funcTarget.addLegalDialect(); - funcTarget.addLegalDialect(); - // Add casting as legal - funcTarget.addLegalOp(); - funcTarget.addIllegalDialect(); - funcPatterns.insert(typeConverter, ctx); - if (failed( - applyPartialConversion(modOp, funcTarget, std::move(funcPatterns)))) - return signalPassFailure(); - // Step 3: remove all unrealized conversion casts - // Execute without conversion function but walking the module - if (failed(removeUnrealizedConversionCasts(modOp))) + // Step 1: unbundle all handshake types in the handshake function + if (failed(unbundleAllHandshakeTypes(modOp, ctx))) return signalPassFailure(); - // Step 4: invert the direction of all ready signals in the hw modules + // Step 2: invert the direction of all ready signals in the hw modules // created from handshake operations // Create on object of the ReadySignalInverter class to manage the // inversion From 08b11b3a8c8e15e03d436547f6bc2622a3176a8d Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Tue, 13 Jan 2026 12:10:21 +0100 Subject: [PATCH 12/28] Added next step in the pass --- lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index b477b733f..da4f2eed9 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -1159,6 +1159,8 @@ namespace { // 2) Invert the direction of ready signals in all hw modules and hw instances // to follow the standard handshake protocol where ready signals go in the // opposite direction with respect to data and valid signals +// 3) (not implemented yet) Convert synth subckt operations into other synth +// operations like registers, combinational logic, etc. if possible class HandshakeToSynthPass : public dynamatic::impl::HandshakeToSynthBase { public: @@ -1194,6 +1196,10 @@ class HandshakeToSynthPass ReadySignalInverter inverter; if (failed(inverter.invertAllReadySignals(modOp))) return signalPassFailure(); + + // Step 3: (not implemented yet) Convert synth subckt operations into other + // synth operations like registers, combinational logic, etc. if possible + // TODO } }; From ab524d44273e3d35a387313be2e5ae3821fee3d3 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Tue, 13 Jan 2026 13:32:46 +0100 Subject: [PATCH 13/28] Added documentation for the conversion pass --- .../ConversionHandshakeToSynth.md | 454 ++++++++++++++++++ 1 file changed, 454 insertions(+) create mode 100644 docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/ConversionHandshakeToSynth.md diff --git a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/ConversionHandshakeToSynth.md b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/ConversionHandshakeToSynth.md new file mode 100644 index 000000000..17646e599 --- /dev/null +++ b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/ConversionHandshakeToSynth.md @@ -0,0 +1,454 @@ +# Handshake to Synth + + +The role of this pass is to transform a Handshake-level representation of a dataflow circuit into a Synth-level representation expressed through HW modules and synth operations. Conceptually, it sits between the Handshake optimization pipeline and a future synthesis backend that will further refine synth operations into a final RTL- or gate-oriented netlist. + +There are three main sections in this document. + +1. Main pass and usage: Overall structure and rationale of the pass. +2. Unbundling conversion: Lowering Handshake channel types to flat HW ports and `synth.subckt`. +3. Ready inversion: Fixing the direction of ready signals to follow the standard handshake protocol. + + +The pass is called [`HandshakeToSynthPass`](HandshakeToSynth.cpp). + +--- + +## Main pass and usage + +At a high level, the *HandshakeToSynth* pass performs the following transformations on a module containing a **single non-external** `handshake.func`: + +- Converts all Handshake-typed values (channels, control, memory) into flat HW-level ports by unbundling them into `{data, valid, ready}` signals. +- Lowers each Handshake operation (including the function itself) to an `hw.module`/`hw.instance` plus an internal `synth.subckt` representing its behavior (except from the top handshake function). +- Rewrites all generated HW modules to enforce the standard handshake convention where ready signals flow in the opposite direction from data and valid, and propagates this convention recursively through module instances. + +The pass operates on: + +- A single non-external `handshake.func` per module (enforced by the pass). +- A graph of Handshake operations inside the function, with bundled channel types. + +and produces: + +- A pure HW/Synth module hierarchy: + - The original `handshake.func` is replaced by a top-level `hw.module`. + - Each Handshake operation becomes an `hw.instance` of an `hw.module` that contains a `synth.subckt`. +- No remaining values of Handshake types and no remaining Handshake operations or functions. + + +The overall pass is implemented as: + +- `class HandshakeToSynthPass : public dynamatic::impl::HandshakeToSynthBase`. + +Its `runDynamaticPass()` method: + +- Retrieves the `mlir::ModuleOp` and `MLIRContext`. +- Ensures that there is at most one non-external `handshake.func` in the module and that if none is found, the pass is a no-op. +- Runs Phase 1 – Unbundling by calling `unbundleAllHandshakeTypes(modOp, ctx)`. +- Runs Phase 2 – Ready inversion by instantiating a `ReadySignalInverter` and calling `invertAllReadySignals(modOp)`. +- Leaves a future Phase 3 – further refinement of `synth.subckt` into other synth operations (registers, combinational logic, etc.) as a TODO. + +In a typical flow, this pass is run after all Handshake-level optimizations and buffer insertion, and before a dedicated synth backend that will interpret or further lower the generated synth operations. + +--- + +## Unbundling conversion + +The first major phase of the pass is *unbundling*, which removes all Handshake-specific bundled types from the IR by converting them to multiple scalar ports using MLIR’s conversion infrastructure. Intuitively, this phase works by temporarily inserting *casts* that bridge between the old, bundled Handshake world and the new, flat HW/Synth world, and then erasing those casts once everything has been rewritten. + +From a top-down perspective, unbundling proceeds in three steps: + +1. Convert all non-function Handshake ops to `hw.module`/`hw.instance` pairs. +2. Convert the `handshake.func` itself to an `hw.module`. +3. Remove all temporary `unrealized_conversion_cast` ops that were used as glue. + +These steps are orchestrated by: + +- `LogicalResult unbundleAllHandshakeTypes(mlir::ModuleOp modOp, MLIRContext *ctx)`. + +This function implements Phase 1 of the pass and is split into the three substeps described below. + +--- + +### Casts: high-level intuition + +Unbundling fundamentally changes the *shape* of values: a single Handshake channel value becomes three scalar signals (data, valid, ready) at the HW level. Directly replacing all uses in one shot would be confusing, so the pass introduces `unrealized_conversion_cast` ops as temporary adapters between the old and new representations. + +Conceptually, the pass maintains the following invariant during conversion: + +- Anywhere a still-bundled value must talk to a newly-unbundled region (or vice versa), an `unrealized_conversion_cast` is inserted to “pack” or “unpack” channels. + +Later, once every Handshake op and the function itself have been converted and no part of the IR *needs* bundled values anymore, these casts are removed by wiring original producers and consumers directly. This lets the pass use MLIR’s pattern-based conversion cleanly without having to perform a fragile global rewrite. + +Two helper utilities implement this logic: + +- `UnrealizedConversionCastOp createCastBundledToUnbundled(Value input, Location loc, PatternRewriter &rewriter)`: + - Given one bundled value, produces a multi-result cast whose results are all unbundled components of that value (e.g., data/valid/ready). +- `SmallVector createCastResultsUnbundledOp(TypeRange originalResultTypes, SmallVector unbundledValues, Location loc, PatternRewriter &rewriter)`: + - Given a flat list of unbundled values (e.g., the results of an `hw.instance`), groups them back into casts that each reconstruct one original bundled result type. + - Asserts that the number of unbundled components for each original result matches `unbundleType(resultType).size()`. + +These casts are introduced systematically when: + +- Wiring HW instances into a still-bundled Handshake region. +- Inlining a Handshake function body into an HW module and reconnecting arguments/results. + +--- + +### Step 1 – Convert Handshake ops to HW modules + +This step converts all Handshake operations except the function and `handshake.end` terminator into `hw.module` and `hw.instance` pairs. It uses MLIR’s dialect conversion to rewrite ops while the cast logic keeps the IR temporarily well-typed across representation boundaries. + +The pass sets up a `RewritePatternSet` with: + +- `ChannelUnbundlingTypeConverter` as the type converter, which expands bundled types into flat signal lists. +- `ConvertToHWMod` patterns instantiated for all supported Handshake operations. + +A `ConversionTarget` marks: + +- `synth::SynthDialect` and `hw::HWDialect` as legal. +- `UnrealizedConversionCastOp` as legal (the glue type). +- The Handshake dialect as illegal, except that `handshake.func` and `handshake.end` remain temporarily legal for Step 2. + +The driver then calls: + +- `applyPartialConversion(modOp, target, std::move(patterns))`. + +If this conversion fails, the unbundling phase aborts. + +Each `ConvertToHWMod` pattern replaces the original Handshake op by: + - Creating (or reusing) an `hw.module` with flat ports that reflect the op’s bundled interface. + - Instantiating a `synth.subckt` inside that module. + - Inserting an `hw.instance` at the call site where: + - Operands from the Handshake region are *unpacked* via bundled → unbundled casts as needed. + - Results from the `hw.instance` are *repacked* into bundled values via unbundled → bundled casts so that surrounding Handshake ops can still see their original types. + +--- + +### Step 2 – Convert the Handshake function + +The second substep lowers the remaining `handshake.func` and its `handshake.end` terminator to an `hw.module` and its `hw.output`. At this stage, the function body mostly contains the `hw.instance`/cast network produced in Step 1. + +The pass constructs a new `RewritePatternSet` containing `ConvertFuncToHWMod` and a `ConversionTarget` that: + +- Marks `synth::SynthDialect` and `hw::HWDialect` as legal. +- Marks `UnrealizedConversionCastOp` as legal. +- Marks the Handshake dialect as fully illegal (including `handshake.func` and `handshake.end`). + +It then calls `applyPartialConversion` again. + +At a high level, `ConvertFuncToHWMod`: + +- Creates (or looks up) a top-level `hw.module` whose ports are the unbundled version of the function’s arguments and results. +- Inserts grouped unbundled → bundled casts for module arguments, so the inlined function body sees the same bundled argument types it had originally. +- Inlines the `handshake.func` body into the HW module, remapping arguments to the cast results. +- Replaces the `handshake.end` with: + - Bundled → unbundled casts for each operand. + - A final `hw.output` using those unbundled values. +- Erases the original `handshake.func`. + +From the cast perspective: + +- Module arguments are *packed* into bundled values before inlining so that the existing body does not need to be rewritten. +- Function results are *unpacked* at the end so that the top `hw.module` exposes flat ports only. + +At the end of Step 2: + +- All Handshake operations and functions have been removed and replaced by HW/Synth operations. +- The only remaining artifacts of this conversion are the `UnrealizedConversionCastOp`s that still separate some bundled/unbundled regions internally. + +--- + +### Step 3 – Cleanup of casts + +The third substep removes all temporary casts by calling: + +- `LogicalResult removeUnrealizedConversionCasts(mlir::ModuleOp modOp)`. + +This cleanup relies on the invariant that casts appear in *simple*, paired chains: + +- `producer → cast1 → cast2 → consumer`. + +It tries to find the following cast pattern: + +operation1 → cast1 → cast2 → operation2 + +in order to remove cast1 and cast2 and directly connect operation1 and operation2. + +The removal algorithm: + +- Walks all `UnrealizedConversionCastOp` in the module. +- For each `cast1`: + - Skips it if its input is another cast (`cast2`) and asserts that there is no third cast in the chain. + - Verifies that all uses of its results are `UnrealizedConversionCastOp`s; otherwise the pattern is considered too complex and fails. +- For each matched pair `(cast1, cast2)`: + - Asserts that `cast1`’s number of operands matches `cast2`’s number of results. + - Replaces every result of `cast2` with the corresponding operand of `cast1`, effectively bypassing the casts. + - Schedules both casts for erasure. +- Erases all scheduled casts and checks that no `UnrealizedConversionCastOp` remain. + +After Step 3, all temporary adapters are gone, and the IR consists purely of HW and Synth operations on flat ports—completing the unbundling phase. + +--- + +### Handshake types and unbundling (details) + +Handshake-level IR uses rich types to represent channels, control, and memory interfaces, whereas HW modules operate on flat ports. To bridge that gap, the pass introduces a dedicated type converter and helper utilities derived from MLIR’s `TypeConverter` so they can be plugged directly into pattern-based conversion. + +#### Type-level unbundling + +The central helper function is: + +- `SmallVector> unbundleType(Type type)`. + +It maps high-level types to lists of `(SignalKind, Type)` pairs: + +- For `handshake::ChannelType`: + - `DATA_SIGNAL` → payload type (e.g., `i32`). + - `VALID_SIGNAL` → `i1`. + - `READY_SIGNAL` → `i1`. +- For `handshake::ControlType`: + - `VALID_SIGNAL` → `i1`. + - `READY_SIGNAL` → `i1`. +- For `MemRefType`: + - `DATA_SIGNAL` → element type. + - `VALID_SIGNAL` → `i1`. + - `READY_SIGNAL` → `i1`. +- For all other types, the default is a single `DATA_SIGNAL`. + +This logic is wrapped into: + +- `ChannelUnbundlingTypeConverter : public TypeConverter`. + +which expands any incoming type to its unbundled component types by calling `unbundleType`. + +#### Operation port unbundling + +At the operation level, unbundling is handled by: + +- `void unbundleOpPorts(Operation *op, SmallVector<...> &inputPorts, SmallVector<...> &outputPorts)`. + +This function: + +- Extracts and unbundles the ports of: + - Handshake functions (`handshake.func`) by visiting the function type’s arguments and results. + - Non-function Handshake operations by visiting operands and results directly. +- Produces, for each original port, a vector of `(SignalKind, Type)` describing the corresponding flat signals. + +These descriptors are then converted into HW module port information via: + +- `void getHWModulePortInfo(Operation *op, SmallVector &inputs, SmallVector &outputs)`. + +This function: + +- Calls `unbundleOpPorts` on the operation. +- Assigns deterministic names based on the original port index and signal kind, e.g.: + - `in_data_0`, `in_valid_0`, `in_ready_0` for input port 0. + - `out_data_0`, `out_valid_0`, `out_ready_0` for output port 0. +- Produces `hw::PortInfo` lists suitable for constructing `hw::HWModuleOp`. + +--- + +### Conversion of Handshake ops to HW modules (details) + +The actual lowering from Handshake operations to HW is driven by a family of conversion patterns and helpers. + +#### Per-operation conversion + +The core helper is: + +- `hw::HWModuleOp convertOpToHWModule(Operation *op, ConversionPatternRewriter &rewriter)`. + +It performs: + +1. **Module interface synthesis.** + - Computes HW input/output ports using `getHWModulePortInfo` on the Handshake operation. +2. **Module creation / lookup.** + - Gets the parent `mlir::ModuleOp` and its `SymbolTable`. + - Computes a unique module name for the Handshake operation using `getUniqueName`. + - Looks up an existing `hw::HWModuleOp` with that name, or creates one at module scope if none exists. +3. **Behavior instantiation.** + - Calls `instantiateSynthOpInHWModule` on the `hw::HWModuleOp` to create a `synth.subckt` representing the converted operation. +4. **Instance insertion.** + - Replaces the original Handshake operation with an `hw.instance` of the module: + - For each operand: + - If the defining operation belongs to the Handshake dialect or is a block argument, a bundled → unbundled cast is created and all its results are used as instance operands. + - Otherwise, the operand is assumed already unbundled and used directly. + - For each group of instance results, an unbundled → bundled cast is created via `createCastResultsUnbundledOp` and its single bundled result replaces the original result. + - Asserts that the number of casted results matches the original operation’s result count. + + +Each generated HW module (representing a dataflow unit) is therefore a wrapper around a `synth.subckt` that describes the original Handshake operation’s behavior. + +#### Generic conversion patterns + +To drive conversion over the whole function, the pass defines: + +- `template class ConvertToHWMod : public OpConversionPattern`. + +with pattern callback: + +- `LogicalResult ConvertToHWMod::matchAndRewrite(T op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const`. + +The pattern calls `convertOpToHWModule(op, rewriter)` and fails if no module could be created. + +The pass instantiates this template for all Handshake operations. All are converted in a uniform way by the same template. + +#### Function-level conversion (details) + +The Handshake function itself (`handshake.func`) is converted by: + +- `hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, ConversionPatternRewriter &rewriter)`. + +It: + +- Computes the HW module interface by unbundling the function’s arguments/results via `getHWModulePortInfo`. +- Ensures a unique top-level `hw::HWModuleOp` exists for the function. +- Creates grouped unbundled → bundled casts for each module argument so that the inlined body still sees the same bundled arguments as before. +- Inlines the original Handshake body into the HW module, remapping arguments to cast results. +- Finds the unique `handshake.end`, inserts bundled → unbundled casts for each operand, and replaces it with an `hw.output` using all unbundled values. +- Deletes any old empty `hw.output` and erases the original `handshake.func`. + +The pattern `class ConvertFuncToHWMod : public OpConversionPattern` simply delegates to this helper. + + +--- + +## Ready signal inversion + +After unbundling, the generated HW modules use a raw mapping of Handshake channels to `{data, valid, ready}` signals. All hw modules represent ready in the same direction as data/valid, while standard handshake protocols require ready to travel against data/valid. The second major phase of the pass fixes this. + +This phase is implemented in the `ReadySignalInverter` helper class and its methods. + +Unlike the previous phase, this one does not rely on MLIR’s pattern-rewrite infrastructure. Because it introduces no type changes and does not modify operations, the full conversion machinery is unnecessary; instead, the phase is implemented using a simpler, ad-hoc approach. + +This phase is also intentionally kept separate from the earlier one. The existing cast logic assumes a clear boundary, with unbundled signals on one side and bundled signals on the other. Extending that logic here would break this assumption: the ready signal flows in the opposite direction of the data and valid signals, eliminating the clean separation and significantly complicating the implementation. + + +The global entry point for ready inversion is: + +- `LogicalResult ReadySignalInverter::invertAllReadySignals(mlir::ModuleOp modOp)`. + +It: + +- Builds a `SymbolTable` for the module. +- Collects all `hw::HWModuleOp`s into `oldHWModules` keyed by name. +- For each `(name, module)` pair in `oldHWModules`, calls `invertReadySignalHWModule` to create the rewritten module and populate `newHWModules`. +- Erases all old modules. +- Renames new modules back to their original names and updates all `hw.instance` operations so that they reference the restored module names rather than the temporary rewritten names. + +At the end of this phase, the HW module hierarchy is structurally the same as after unbundling, but all ready signals follow the standard handshake direction. + + + +### Goals and assumptions + +The ready inversion phase: + +- Iterates over all HW modules. +- Produces new HW modules with ports reorganized so that: + - Data and valid remain in their original directions. + - Ready signals are swapped from input to output or vice versa, to follow the opposite direction convention. +- Rewrites `hw.instance` operations to use the new modules and reconnects all signals consistently. +- Propagates changes recursively through the module hierarchy. + +Two maps track signal equivalences during rewriting: + +- `DenseMap oldModuleSignalToNewModuleSignalMap`: maps original signals to their new counterparts. +- `DenseMap oldModuleSignalToTempValueMap`: maps original signals to temporary placeholder constants when the final signal is not yet available. + +It assumes that each Handshake channel connects exactly one producer to exactly one consumer; otherwise, the signal-mapping logic based on `DenseMap` may not be valid. + +### Per-module rewriting + +The core of the inversion is: + +- `void ReadySignalInverter::invertReadySignalHWModule(hw::HWModuleOp oldMod, ModuleOp parent, SymbolTable &symTable, DenseMap &newHWModules, DenseMap &oldHWModules)`. + +It proceeds in four steps. + +#### Step 1 – Already processed check + +If a module name already exists in `newHWModules`, the function returns immediately. + +#### Step 2 – New module interface and creation + +The function: + +- Scans the old module’s `PortList` to construct new input and output port lists with inverted directions for ready signals. +- For each port `p`: + - If `p` is an input and its name contains `ready`, it becomes a ready output in the new module. + - If `p` is an output and its name contains `ready`, it becomes a ready input in the new module. + - Non-ready inputs and outputs keep their direction but are re-mapped with appropriate index tracking. + +While doing so, it builds: + +- `SmallVector> newModuleOutputIdxToOldSignal`: + - For each new output index, records the old signal (either a module argument or terminator operand) that semantically corresponds to it. +- `SmallVector> newModuleInputIdxToOldSignal`: + - Similarly for new inputs. + +It then: + +- Creates a new `hw::HWModuleOp` after `oldMod` with name `_rewritten` via `getNewModuleName` and ports `newInputs`, `newOutputs`. +- Registers the new module in `newHWModules` under the original name. +- For each new input index, maps its argument to the corresponding old signal in `oldModuleSignalToNewModuleSignalMap`. + +#### Step 3 – Body rewriting + +The body of the old module is processed to handle instances and non-instance modules. + +- The pass first scans the body: + - If it finds any `hw.instance`, `hasHwInstances` is set to true. +- If instances exist: + - Each `hw.instance` is rewritten by calling `invertReadySignalHWInstance`. +- If no instances exist: + - The module is expected to contain only `hw.output` and `synth.subckt` operations. + - Any deviation is treated as an error and causes an assertion. + - In this pure glue module case, a new `synth.subckt` is instantiated in the new module via `instantiateSynthOpInHWModule` to maintain the same connectivity under the new interface. + +#### Step 4 – New terminator wiring + +Finally, the new module’s terminator (`hw.output`) is updated. + +- For each `(newOutputIdx, oldSignal)` pair in `newModuleOutputIdxToOldSignal`, the builder: + - Looks up the mapped new signal from `oldModuleSignalToNewModuleSignalMap`. + - Asserts that the mapping exists. + - Collects the mapped values in order of new output index. +- The terminator’s operands are replaced with this new operand list. + +### Instance rewriting + +Instances are rewritten by: + +- `void ReadySignalInverter::invertReadySignalHWInstance(hw::InstanceOp oldInst, ModuleOp parent, SymbolTable &symTable, DenseMap &newHWModules, DenseMap &oldHWModules)`. + +This function: + +- Ensures the instance’s callee module has been rewritten by checking `newHWModules` and calling `invertReadySignalHWModule` if needed. +- Determines the rewritten callee module (`newMod`), the original callee module (`oldMod`), the top-level module containing the instance, and its corresponding rewritten module (`newTopMod`) where new operations will be inserted. +- Prepares three lists: + - `nonReadyOutputs`: `(outputName, oldResultIdx)` pairs for outputs that are not ready and whose uses must be re-routed. + - `oldReadyInputs`: `(inputName, oldValue)` pairs for ready inputs that will become ready outputs in the new module. + - `newOperands`: operands for the new `hw.instance`. +- Iterates over `oldMod`’s ports and classifies each: + - For ready inputs in the old module: + - The corresponding signal in the new module is an output; the old input value is stored in `oldReadyInputs`. + - For ready outputs in the old module: + - These become inputs in the new module. + - `getInputSignalMappingValue` is called on the old output to find or create the matching new input value, which is added to `newOperands`. + - For non-ready inputs: + - `getInputSignalMappingValue` is called similarly and added to `newOperands`. + - For non-ready outputs: + - They are recorded in `nonReadyOutputs` for later output mapping. +- Creates a new instance of `newMod` in the rewritten top module with the original instance name and `newOperands` as operands. +- Updates signal mappings: + - For each `(outputName, oldResultIdx)` in `nonReadyOutputs`, `updateOutputSignalMappingValue` binds the old non-ready result to the corresponding new result and replaces any temporary placeholders. + - For each `(inputName, oldReadyInput)` in `oldReadyInputs`, `updateOutputSignalMappingValue` maps the old ready input to the new ready output of the rewritten module. + +Helpers: + +- `Value ReadySignalInverter::getInputSignalMappingValue(Value oldInputSignal, OpBuilder &builder, Location loc)`: + - Returns a mapped or temporary value for a given old input, creating a `hw.constant` placeholder if necessary and storing it in `oldModuleSignalToTempValueMap`. +- `void ReadySignalInverter::updateOutputSignalMappingValue(Value oldResult, StringRef outputName, hw::HWModuleOp newMod, hw::InstanceOp newInst)`: + - Finds the output index of `outputName` in `newMod`, obtains the result from `newInst`, updates `oldModuleSignalToNewModuleSignalMap`, replaces any temporary uses, and erases the temporary constant op if present. + From 7a6053d4ff46b23f2f3b6940251e999f6a40df2b Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Tue, 13 Jan 2026 13:39:13 +0100 Subject: [PATCH 14/28] Changed doc structure --- .../{ => Synth}/ConversionHandshakeToSynth.md | 0 .../DynamaticFeaturesAndOptimizations/Synth/Synth.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/{ => Synth}/ConversionHandshakeToSynth.md (100%) create mode 100644 docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/Synth.md diff --git a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/ConversionHandshakeToSynth.md b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md similarity index 100% rename from docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/ConversionHandshakeToSynth.md rename to docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md diff --git a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/Synth.md b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/Synth.md new file mode 100644 index 000000000..e69de29bb From 95c55d5dcdc08f86e13d0be876883b470a5051d3 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Tue, 13 Jan 2026 13:51:18 +0100 Subject: [PATCH 15/28] Added doc for synth dialect --- .../Synth/Synth.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/Synth.md b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/Synth.md index e69de29bb..39544094d 100644 --- a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/Synth.md +++ b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/Synth.md @@ -0,0 +1,60 @@ +# Synth dialect + + +> **NOTE** +> The Synth dialect in Dynamatic is adapted from the original Synth dialect in the CIRCT project. +> As stated in the the header comment in `Synth.td`, “all the files related to the description of the Synth dialect have been modified from the original version present in the CIRCT project at the following link: https://github.com/llvm/circt/tree/main/”. +> This origin explains the focus on AIG/MIG and pipeline infrastructure, which align with broader logic synthesis research and tooling beyond Dynamatic itself. + + +The **Synth** dialect is a low-level synthesis-oriented dialect that provides a common in-MLIR interface for logic synthesis operations and data structures. It is intended as an intermediate representation between higher-level hardware IR (e.g., Handshake or HW) and concrete synthesis backends, capturing both logic networks (such as AIGs and MIGs) and meta-operations that steer synthesis decisions. + +In Dynamatic, the Synth dialect is the target of the [Handshake-to-Synth lowering pass](ConversionHandshakeToSynth.md): each Handshake operation is wrapped in a `synth.subckt` (or similar Synth operation) inside an `hw.module`, and later passes are free to refine these subcircuits into more detailed networks and optimization steps. + +--- + +## Design and role in the flow + +The Synth dialect has three core responsibilities: + +- Provide **operations and types for logic synthesis**, i.e., operations that manipulate logic-level representations and synthesis state. +- Include **meta operations** that encode synthesis decisions (e.g., which implementation, which optimization pass, or which strategy to apply on a given subcircuit). +- Support **logic network representations** such as AIG (And-Inverter Graph) and MIG (Majority-Inverter Graph), as well as the infrastructure to represent synthesis pipelines in MLIR. + +In other words, the Synth dialect is not tied to a particular hardware backend or HDL; instead, it serves as an abstraction layer where logic-level manipulations can be expressed, analyzed, and transformed before being committed to a specific RTL or gate-level format. + +--- + +## Operations and types (conceptual overview) + +This dialect focuses on three aspects: + +- **Logic representations:** + - AIG-style networks, where logic is modeled as AND gates and inverters. + - MIG-style networks, where logic is modeled as majority nodes with optional inversion on edges. +- **Meta operations for synthesis decisions:** + - Operations that encode choices such as: + - Which implementation variant to use for a given functionality. + - Which optimization passes or strategies have been applied or should be applied. + - How to annotate subcircuits for later backend-specific handling. +- **Synthesis pipeline infrastructure:** + - Operations that describe, parameterize, or orchestrate sequences of synthesis steps. + - Potential hooks for external tools or generators that operate on Synth-level IR. + +The Synth dialect is therefore designed as a *synthesis workbench* inside MLIR: it is where logic-level structures and decisions live, separate from higher-level behavioral IR and lower-level RTL or gate-level netlists. + +--- + +## Relationship with Handshake and HW + +In the Dynamatic flow, the Synth dialect is tightly coupled to Handshake and HW through the [Handshake-to-Synth lowering pass](ConversionHandshakeToSynth.md): + +- Handshake operations are first converted to `hw.module`/`hw.instance` structures with flat ports. +- Each module’s behavior is described by synth operations inside the HW module, making Synth describe the logic function of each operation. +- Later passes can: + - Refine `synth.subckt` into more detailed Synth operations (e.g., explicit AIG/MIG nodes). + - Apply logic optimization and technology mapping in terms of Synth operations, with HW only providing the module/instance hierarchy and IO ports. + +This separation of concerns allows Dynamatic to keep Handshake-level reasoning, module hierarchy (HW dialect), and synthesis-specific logic (Synth dialect) cleanly partitioned, while still enabling tight interaction through well-defined IR boundaries. + + From 8236cf7770bd309b6da2685fbd3216d0f2b9619d Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Tue, 13 Jan 2026 13:54:50 +0100 Subject: [PATCH 16/28] Added new docs to summary --- docs/SUMMARY.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 4dabe24af..2aa1e44ec 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -76,6 +76,9 @@ - [Commit Unit Placement Algorithm](DeveloperGuide/DynamaticFeaturesAndOptimizations/Speculation/CommitUnitPlacementAlgorithm.md) - [Integration Tests](DeveloperGuide/DynamaticFeaturesAndOptimizations/Speculation/IntegrationTests.md) - [Save Commit Behavior](DeveloperGuide/DynamaticFeaturesAndOptimizations/Speculation/SaveCommitBehavior.md) + - [Synth]() + - [Synth Dialect](DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth.md) + - [Conversion Pass from Handshake to Synth Dialect](DeveloperGuide/DynamaticFeaturesAndOptimizations/ConversionHandshakeToSynth.md) - [Specs]() - [Floating Point Units](DeveloperGuide/Specs/FloatingPointUnits.md) From 4dc1fa5120f0636647b6788507e76e47dc6a2f10 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Tue, 13 Jan 2026 18:35:26 +0100 Subject: [PATCH 17/28] Modified headers to mention circt in a consistent way with the rest of the project --- .../Synth/Synth.md | 6 ------ include/dynamatic/Dialect/Synth/CMakeLists.txt | 7 ------- include/dynamatic/Dialect/Synth/Synth.td | 17 +++++++++-------- include/dynamatic/Dialect/Synth/SynthDialect.h | 15 ++++++--------- include/dynamatic/Dialect/Synth/SynthOps.h | 14 ++++++-------- include/dynamatic/Dialect/Synth/SynthOps.td | 14 ++++++-------- lib/Dialect/Synth/CMakeLists.txt | 12 +++++------- lib/Dialect/Synth/SynthDialect.cpp | 16 +++++++++------- lib/Dialect/Synth/SynthOps.cpp | 16 +++++++++------- 9 files changed, 50 insertions(+), 67 deletions(-) diff --git a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/Synth.md b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/Synth.md index 39544094d..b20e54774 100644 --- a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/Synth.md +++ b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/Synth.md @@ -1,12 +1,6 @@ # Synth dialect -> **NOTE** -> The Synth dialect in Dynamatic is adapted from the original Synth dialect in the CIRCT project. -> As stated in the the header comment in `Synth.td`, “all the files related to the description of the Synth dialect have been modified from the original version present in the CIRCT project at the following link: https://github.com/llvm/circt/tree/main/”. -> This origin explains the focus on AIG/MIG and pipeline infrastructure, which align with broader logic synthesis research and tooling beyond Dynamatic itself. - - The **Synth** dialect is a low-level synthesis-oriented dialect that provides a common in-MLIR interface for logic synthesis operations and data structures. It is intended as an intermediate representation between higher-level hardware IR (e.g., Handshake or HW) and concrete synthesis backends, capturing both logic networks (such as AIGs and MIGs) and meta-operations that steer synthesis decisions. In Dynamatic, the Synth dialect is the target of the [Handshake-to-Synth lowering pass](ConversionHandshakeToSynth.md): each Handshake operation is wrapped in a `synth.subckt` (or similar Synth operation) inside an `hw.module`, and later passes are free to refine these subcircuits into more detailed networks and optimization steps. diff --git a/include/dynamatic/Dialect/Synth/CMakeLists.txt b/include/dynamatic/Dialect/Synth/CMakeLists.txt index abb2fb9e8..905a87366 100644 --- a/include/dynamatic/Dialect/Synth/CMakeLists.txt +++ b/include/dynamatic/Dialect/Synth/CMakeLists.txt @@ -1,10 +1,3 @@ -##===----------------------------------------------------------------------===// -## All the files related to the description of the Synth dialect -## have been modified from the original version present in the -## circt project at the following link: -## https://github.com/llvm/circt/tree/main/ -##===----------------------------------------------------------------------===// - ##===----------------------------------------------------------------------===// ## ## Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. diff --git a/include/dynamatic/Dialect/Synth/Synth.td b/include/dynamatic/Dialect/Synth/Synth.td index b978dd2d0..3cac67568 100644 --- a/include/dynamatic/Dialect/Synth/Synth.td +++ b/include/dynamatic/Dialect/Synth/Synth.td @@ -1,11 +1,3 @@ -//===----------------------------------------------------------------------===// -// All the files related to the description of the Synth dialect -// have been modified from the original version present in the -// circt project at the following link: -// https://github.com/llvm/circt/tree/main/ -//===----------------------------------------------------------------------===// - - //===----------------------------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. @@ -13,6 +5,15 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// +// +// This file originates from the CIRCT project (https://github.com/llvm/circt). +// It includes modifications made as part of Dynamatic. +// +//===----------------------------------------------------------------------===// +// +// This is the top level file for the Synth dialect. +// +//===----------------------------------------------------------------------===// #ifndef DYNAMATIC_SYNTH_DIALECT_TD #define DYNAMATIC_SYNTH_DIALECT_TD diff --git a/include/dynamatic/Dialect/Synth/SynthDialect.h b/include/dynamatic/Dialect/Synth/SynthDialect.h index df3c2add7..97dfbdef2 100644 --- a/include/dynamatic/Dialect/Synth/SynthDialect.h +++ b/include/dynamatic/Dialect/Synth/SynthDialect.h @@ -1,10 +1,3 @@ -//===----------------------------------------------------------------------===// -// All the files related to the description of the Synth dialect -// have been modified from the original version present in the -// circt project at the following link: -// https://github.com/llvm/circt/tree/main/ -//===----------------------------------------------------------------------===// - //===----------------------------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. @@ -13,10 +6,14 @@ // //===----------------------------------------------------------------------===// // -// This file defines the Synth dialect. +// This file originates from the CIRCT project (https://github.com/llvm/circt). +// It includes modifications made as part of Dynamatic. +// +//===----------------------------------------------------------------------===// +// +// This is the top level file for the Synth dialect. // //===----------------------------------------------------------------------===// - #ifndef DYNAMATIC_DIALECT_SYNTH_SYNTHDIALECT_H #define DYNAMATIC_DIALECT_SYNTH_SYNTHDIALECT_H diff --git a/include/dynamatic/Dialect/Synth/SynthOps.h b/include/dynamatic/Dialect/Synth/SynthOps.h index 31886c161..41cc6c416 100644 --- a/include/dynamatic/Dialect/Synth/SynthOps.h +++ b/include/dynamatic/Dialect/Synth/SynthOps.h @@ -1,10 +1,3 @@ -//===----------------------------------------------------------------------===// -// All the files related to the description of the Synth dialect -// have been modified from the original version present in the -// circt project at the following link: -// https://github.com/llvm/circt/tree/main/ -//===----------------------------------------------------------------------===// - //===----------------------------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. @@ -13,7 +6,12 @@ // //===----------------------------------------------------------------------===// // -// This file defines the operations of the Synth dialect. +// This file originates from the CIRCT project (https://github.com/llvm/circt). +// It includes modifications made as part of Dynamatic. +// +//===----------------------------------------------------------------------===// +// +// This is the top level file for the Synth dialect. // //===----------------------------------------------------------------------===// diff --git a/include/dynamatic/Dialect/Synth/SynthOps.td b/include/dynamatic/Dialect/Synth/SynthOps.td index a6404d082..6c1d2d0f4 100644 --- a/include/dynamatic/Dialect/Synth/SynthOps.td +++ b/include/dynamatic/Dialect/Synth/SynthOps.td @@ -1,10 +1,3 @@ -//===----------------------------------------------------------------------===// -// All the files related to the description of the Synth dialect -// have been modified from the original version present in the -// circt project at the following link: -// https://github.com/llvm/circt/tree/main/ -//===----------------------------------------------------------------------===// - //===----------------------------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. @@ -13,7 +6,12 @@ // //===----------------------------------------------------------------------===// // -// This file defines the operations of the Synth dialect. +// This file originates from the CIRCT project (https://github.com/llvm/circt). +// It includes modifications made as part of Dynamatic. +// +//===----------------------------------------------------------------------===// +// +// This is the top level file for the Synth dialect. // //===----------------------------------------------------------------------===// diff --git a/lib/Dialect/Synth/CMakeLists.txt b/lib/Dialect/Synth/CMakeLists.txt index 8cfe49af4..01cc95db6 100644 --- a/lib/Dialect/Synth/CMakeLists.txt +++ b/lib/Dialect/Synth/CMakeLists.txt @@ -1,15 +1,13 @@ -##===----------------------------------------------------------------------===// -## All the files related to the description of the Synth dialect -## have been modified from the original version present in the -## circt project at the following link: -## https://github.com/llvm/circt/tree/main/ -##===----------------------------------------------------------------------===// - ##===----------------------------------------------------------------------===// ## ## Implementation files for the Synth dialect. ## ##===----------------------------------------------------------------------===// +## +## This file originates from the CIRCT project (https://github.com/llvm/circt). +## It includes modifications made as part of Dynamatic. +## +##===----------------------------------------------------------------------===// add_dynamatic_dialect_library(DynamaticSynth SynthDialect.cpp diff --git a/lib/Dialect/Synth/SynthDialect.cpp b/lib/Dialect/Synth/SynthDialect.cpp index d9da0c44b..5deb35f4b 100644 --- a/lib/Dialect/Synth/SynthDialect.cpp +++ b/lib/Dialect/Synth/SynthDialect.cpp @@ -1,10 +1,3 @@ -//===----------------------------------------------------------------------===// -// All the files related to the description of the Synth dialect -// have been modified from the original version present in the -// circt project at the following link: -// https://github.com/llvm/circt/tree/main/ -//===----------------------------------------------------------------------===// - //===----------------------------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. @@ -12,6 +5,15 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// +// +// This file originates from the CIRCT project (https://github.com/llvm/circt). +// It includes modifications made as part of Dynamatic. +// +//===----------------------------------------------------------------------===// +// +// This is the top level file for the Synth dialect. +// +//===----------------------------------------------------------------------===// #include "dynamatic/Dialect/Synth/SynthDialect.h" #include "dynamatic/Dialect/HW/HWOps.h" diff --git a/lib/Dialect/Synth/SynthOps.cpp b/lib/Dialect/Synth/SynthOps.cpp index dd0df120f..4ba51f5a0 100644 --- a/lib/Dialect/Synth/SynthOps.cpp +++ b/lib/Dialect/Synth/SynthOps.cpp @@ -1,10 +1,3 @@ -//===----------------------------------------------------------------------===// -// All the files related to the description of the Synth dialect -// have been modified from the original version present in the -// circt project at the following link: -// https://github.com/llvm/circt/tree/main/ -//===----------------------------------------------------------------------===// - //===----------------------------------------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. @@ -12,6 +5,15 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// +// +// This file originates from the CIRCT project (https://github.com/llvm/circt). +// It includes modifications made as part of Dynamatic. +// +//===----------------------------------------------------------------------===// +// +// This is the top level file for the Synth dialect. +// +//===----------------------------------------------------------------------===// #include "dynamatic/Dialect/Synth/SynthOps.h" #include "dynamatic/Dialect/HW/HWOps.h" From 849925d6faa0bb8bec019ec9146f78257ee49a23 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Tue, 13 Jan 2026 19:45:54 +0100 Subject: [PATCH 18/28] Added support for splitting data signals in multiple single bits --- .../HandshakeToSynth/HandshakeToSynth.cpp | 186 ++++++++++++------ 1 file changed, 123 insertions(+), 63 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index da4f2eed9..5b52b8242 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -88,8 +88,8 @@ class ReadySignalInverter { mlir::MLIRContext *ctx); // Function to get the mapping of an input signal - Value getInputSignalMapping(Value oldInputSignal, OpBuilder builder, - Location loc); + SmallVector getInputSignalMapping(Value oldInputSignal, + OpBuilder builder, Location loc); // Function to update the mapping of an output signal void updateOutputSignalMapping(Value oldResult, StringRef outputName, @@ -107,15 +107,16 @@ class ReadySignalInverter { // // Maps to keep track of signal connections during instance rewriting // Old refers to the original hw instance with wrong ready signal directions + // and hw types with a potential bitwidth larger than 1. // New refers to the rewritten hw instance with correct ready signal - // directions - DenseMap oldModuleSignalToNewModuleSignalMap; + // directions and with hw types strictly 1 bit wide for each signal. + DenseMap> oldModuleSignalToNewModuleSignalsMap; // Since the hw instances are rewritten in a recursive manner, it might be // possible that we need the result of an instance that has not been created // yet. To handle this case, we create temporary hw constant operations // to hold the place of these values. This map keeps track of these temporary // values - DenseMap oldModuleSignalToTempValueMap; + DenseMap> oldModuleSignalToTempValuesMap; }; // Function to unbundle a single handshake type into its subcomponents @@ -788,29 +789,39 @@ mlir::StringAttr ReadySignalInverter::getNewModuleName(hw::HWModuleOp oldMod, // Function to get the mapping of an input signal from the old module to the // new module -Value ReadySignalInverter::getInputSignalMapping(Value oldInputSignal, - OpBuilder builder, - Location loc) { - auto it = oldModuleSignalToNewModuleSignalMap.find(oldInputSignal); - if (it != oldModuleSignalToNewModuleSignalMap.end()) { +SmallVector +ReadySignalInverter::getInputSignalMapping(Value oldInputSignal, + OpBuilder builder, Location loc) { + auto it = oldModuleSignalToNewModuleSignalsMap.find(oldInputSignal); + if (it != oldModuleSignalToNewModuleSignalsMap.end()) { return it->second; } // If there is no mapping, it means that the value is not yet // available since the instance that produces it has not been // rewritten yet. In this case, check if we already created a // temporary value for this ready signal - auto tempIt = oldModuleSignalToTempValueMap.find(oldInputSignal); - if (tempIt != oldModuleSignalToTempValueMap.end()) { + auto tempIt = oldModuleSignalToTempValuesMap.find(oldInputSignal); + if (tempIt != oldModuleSignalToTempValuesMap.end()) { return tempIt->second; } - // Else, create a new temporary hw constant to hold the connection - auto tempConst = builder.create( - loc, oldInputSignal.getType(), - builder.getIntegerAttr(oldInputSignal.getType(), 0)); + // Else, create a new temporary hw constant to hold the connection for each + // bit + SmallVector tempValues; + Type signalType = oldInputSignal.getType(); + assert(signalType.isa() && + "only integer types are supported for signals for now"); + unsigned bitWidth = signalType.cast().getWidth(); + assert(bitWidth > 0 && "signal must have positive bit width"); + for (unsigned i = 0; i < bitWidth; ++i) { + auto tempConst = builder.create( + loc, oldInputSignal.getType(), + builder.getIntegerAttr(oldInputSignal.getType(), 0)); + tempValues.push_back(tempConst.getResult()); + } // Store the temporary value in the map - oldModuleSignalToTempValueMap[oldInputSignal] = tempConst; + oldModuleSignalToTempValuesMap[oldInputSignal] = tempValues; // Use the result of the constant as the operand - return tempConst->getResult(0); + return tempValues; } // Function to update the mapping between old module signals and new module @@ -820,28 +831,38 @@ void ReadySignalInverter::updateOutputSignalMapping(Value oldResult, hw::HWModuleOp newMod, hw::InstanceOp newInst) { // Find the corresponding output index of the output in the new module - int outputIdxNewInst = -1; + SmallVector outputIdxsNewInst; for (auto &p : newMod.getPortList()) { - if (p.name.getValue() == outputName) { - outputIdxNewInst = p.argNum; - break; + StringRef portName = p.name.getValue(); + if (portName.starts_with(outputName)) { + outputIdxsNewInst.push_back(p.argNum); } } - assert(outputIdxNewInst != -1 && "could not find output port in new module"); - Value newResult = newInst.getResult(outputIdxNewInst); - // Add mapping between old non-ready signal and new non-ready signal - oldModuleSignalToNewModuleSignalMap[oldResult] = newResult; + assert(!outputIdxsNewInst.empty() && + "could not find output port in new module"); + SmallVector newResults; + for (int idx : outputIdxsNewInst) { + newResults.push_back(newInst->getResult(idx)); + } + // Add mapping between old non-ready signal and new non-ready signal + oldModuleSignalToNewModuleSignalsMap[oldResult] = newResults; // Check if any temporary value has been created for the new result // value by seeing if the old output value is in the map of temporary // values - auto tempIt = oldModuleSignalToTempValueMap.find(oldResult); - if (tempIt != oldModuleSignalToTempValueMap.end()) { - // Replace all uses of the temporary value with the new result value - tempIt->second.replaceAllUsesWith(newResult); + auto tempIt = oldModuleSignalToTempValuesMap.find(oldResult); + if (tempIt != oldModuleSignalToTempValuesMap.end()) { + assert(newResults.size() == tempIt->second.size() && + "mismatched number of bits between temporary value and new result"); + for (size_t i = 0; i < newResults.size(); ++i) { + // Replace all uses of the temporary value with the new result value + tempIt->second[i].replaceAllUsesWith(newResults[i]); + } + for (auto tempVal : tempIt->second) { + // Remove the operation creating the temporary value + tempVal.getDefiningOp()->erase(); + } // Remove the temporary value from the map - oldModuleSignalToTempValueMap.erase(tempIt); - // Remove the operation creating the temporary value - tempIt->second.getDefiningOp()->erase(); + oldModuleSignalToTempValuesMap.erase(tempIt); } } @@ -913,15 +934,19 @@ void ReadySignalInverter::invertReadySignalHWInstance( // This is an output of the old module and should become an input of the // new module. Check if there is a mapping for this ready signal Value oldReadyOutput = oldInst->getResult(port.argNum); - Value newReadyInput = + SmallVector newReadyInputs = getInputSignalMapping(oldReadyOutput, builder, locNewOps); + assert(newReadyInputs.size() == 1 && "ready signal should be 1 bit wide"); + Value newReadyInput = newReadyInputs[0]; newOperands.push_back(newReadyInput); } else if (port.isInput()) { // Check if there is a mapping for this non-ready signal Value oldNonReadyInput = oldInst.getOperand(port.argNum); - Value newNonReadyInput = + SmallVector newNonReadyInputs = getInputSignalMapping(oldNonReadyInput, builder, locNewOps); - newOperands.push_back(newNonReadyInput); + // Append each bit separately of the non-ready signal to the new operands + for (auto newNonReadyInput : newNonReadyInputs) + newOperands.push_back(newNonReadyInput); } else { // Collect non-ready outputs to replace uses later nonReadyOutputs.push_back( @@ -981,21 +1006,34 @@ void ReadySignalInverter::invertReadySignalHWModule( // Collect the new inputs and outputs with inverted ready signal directions unsigned inputIdx = 0; unsigned outputIdx = 0; - // Store mapping from new output idx to old signal - SmallVector> newModuleOutputIdxToOldSignal; - // Store mapping from new input idx to old signal - SmallVector> newModuleInputIdxToOldSignal; + // Store mapping from new output idx start and end to old signal + SmallVector, Value>> + newModuleOutputIdxToOldSignal; + // Store mapping from new input idx start and end to old signal + SmallVector, Value>> + newModuleInputIdxToOldSignal; // Iterate over the ports of the old hw module for (auto &p : oldMod.getPortList()) { bool isReady = p.name.getValue().contains("ready"); + unsigned startOutputIdx = outputIdx; + unsigned startInputIdx = inputIdx; if ((p.isInput() && isReady) || (p.isOutput() && !isReady)) { // If the port is input and ready, it becomes output and ready in the new // module. If a port is output and not ready, it stays as such. - newOutputs.push_back( - {hw::ModulePort{StringAttr::get(ctx, StringRef{p.name}), p.type, - hw::ModulePort::Direction::Output}, - outputIdx}); + Type signalType = p.type; + assert(signalType.isa() && + "only integer types are supported for signals for now"); + unsigned bitWidth = signalType.cast().getWidth(); + for (unsigned i = 0; i < bitWidth; ++i) { + newOutputs.push_back( + {hw::ModulePort{StringAttr::get(ctx, p.name.getValue().str() + "_" + + std::to_string(i)), + builder.getIntegerType(1), + hw::ModulePort::Direction::Output}, + outputIdx}); + outputIdx++; + } Value oldSignal; if (isReady) { // Record mapping from new ready output to old ready input @@ -1005,16 +1043,25 @@ void ReadySignalInverter::invertReadySignalHWModule( oldSignal = oldMod.getBodyBlock()->getTerminator()->getOperand(p.argNum); } - newModuleOutputIdxToOldSignal.push_back( - std::make_pair(outputIdx, oldSignal)); - outputIdx++; + unsigned endOutputIdx = outputIdx - 1; + newModuleOutputIdxToOldSignal.push_back(std::make_pair( + std::make_pair(startOutputIdx, endOutputIdx), oldSignal)); } else if ((p.isOutput() && isReady) || (p.isInput() && !isReady)) { // If the port is output and ready, it becomes input and ready in the new // module. If a port is input and not ready, it stays as such. - newInputs.push_back( - {hw::ModulePort{StringAttr::get(ctx, StringRef{p.name}), p.type, - hw::ModulePort::Direction::Input}, - inputIdx}); + Type signalType = p.type; + assert(signalType.isa() && + "only integer types are supported for signals for now"); + unsigned bitWidth = signalType.cast().getWidth(); + for (unsigned i = 0; i < bitWidth; ++i) { + newInputs.push_back( + {hw::ModulePort{ + StringAttr::get(ctx, StringRef{p.name.getValue().str() + "_" + + std::to_string(i)}), + builder.getIntegerType(1), hw::ModulePort::Direction::Input}, + inputIdx}); + inputIdx++; + } Value oldSignal; if (isReady) { // Record mapping from new ready input to old ready output @@ -1024,9 +1071,9 @@ void ReadySignalInverter::invertReadySignalHWModule( // Record mapping from new non-ready input to old non-ready input oldSignal = oldMod.getBodyBlock()->getArgument(p.argNum); } - newModuleInputIdxToOldSignal.push_back( - std::make_pair(inputIdx, oldSignal)); - inputIdx++; + unsigned endInputIdx = inputIdx - 1; + newModuleInputIdxToOldSignal.push_back(std::make_pair( + std::make_pair(startInputIdx, endInputIdx), oldSignal)); } else { assert(false && "port is neither input nor output"); } @@ -1044,9 +1091,17 @@ void ReadySignalInverter::invertReadySignalHWModule( newHWmodules[oldMod.getName()] = newMod; // Add mapping between new inputs to old signals - for (auto [newModuleInputIdx, oldSignal] : newModuleInputIdxToOldSignal) { - Value newReadyInput = newMod.getBodyBlock()->getArgument(newModuleInputIdx); - oldModuleSignalToNewModuleSignalMap[oldSignal] = newReadyInput; + for (auto [newModuleInputIdxs, oldSignal] : newModuleInputIdxToOldSignal) { + unsigned newModuleInputIdxStart = newModuleInputIdxs.first; + unsigned newModuleInputIdxEnd = newModuleInputIdxs.second; + // For each bit of the input signal, get the corresponding argument + SmallVector newReadyInput; + for (unsigned idx = newModuleInputIdxStart; idx <= newModuleInputIdxEnd; + ++idx) { + newReadyInput.push_back(newMod.getBodyBlock()->getArgument(idx)); + } + assert(newReadyInput.size() && "could not find mapping for input signal"); + oldModuleSignalToNewModuleSignalsMap[oldSignal] = newReadyInput; } // Step 3: Iterate through the body operations of the old module @@ -1087,10 +1142,13 @@ void ReadySignalInverter::invertReadySignalHWModule( // new module. builder.setInsertionPointToEnd(newMod.getBodyBlock()); SmallVector newTerminatorOperands; - for (auto [newOutputIdx, oldSignal] : newModuleOutputIdxToOldSignal) { - Value newModuleOutput = oldModuleSignalToNewModuleSignalMap[oldSignal]; - assert(newModuleOutput && "could not find mapping for output signal"); - newTerminatorOperands.push_back(newModuleOutput); + for (auto [newOutputIdxs, oldSignal] : newModuleOutputIdxToOldSignal) { + SmallVector newModuleOutputs = + oldModuleSignalToNewModuleSignalsMap[oldSignal]; + assert(newModuleOutputs.size() && + "could not find mapping for output signal"); + newTerminatorOperands.append(newModuleOutputs.begin(), + newModuleOutputs.end()); } // Add operands to existing terminator @@ -1158,7 +1216,8 @@ namespace { // 1) Unbundle all handshake types used in the handshake function // 2) Invert the direction of ready signals in all hw modules and hw instances // to follow the standard handshake protocol where ready signals go in the -// opposite direction with respect to data and valid signals +// opposite direction with respect to data and valid signals. Additionally, +// data signals are unbundled into single-bit signals. // 3) (not implemented yet) Convert synth subckt operations into other synth // operations like registers, combinational logic, etc. if possible class HandshakeToSynthPass @@ -1190,7 +1249,8 @@ class HandshakeToSynthPass return signalPassFailure(); // Step 2: invert the direction of all ready signals in the hw modules - // created from handshake operations + // created from handshake operations. Additionally, unbundle data signals + // into single-bit signals. // Create on object of the ReadySignalInverter class to manage the // inversion ReadySignalInverter inverter; From cbdefa007bebd4ec1112d8fe54fafd317b488b93 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Tue, 13 Jan 2026 19:48:44 +0100 Subject: [PATCH 19/28] Added small comments related to the new feature in the doc and in the code --- .../Synth/ConversionHandshakeToSynth.md | 4 ++-- lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md index 17646e599..0ff4238b1 100644 --- a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md +++ b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md @@ -7,7 +7,7 @@ There are three main sections in this document. 1. Main pass and usage: Overall structure and rationale of the pass. 2. Unbundling conversion: Lowering Handshake channel types to flat HW ports and `synth.subckt`. -3. Ready inversion: Fixing the direction of ready signals to follow the standard handshake protocol. +3. Ready inversion: Fixing the direction of ready signals to follow the standard handshake protocol and unbundling multi-bit data signals into multiple single bit signals. The pass is called [`HandshakeToSynthPass`](HandshakeToSynth.cpp). @@ -315,7 +315,7 @@ The pattern `class ConvertFuncToHWMod : public OpConversionPattern Date: Tue, 13 Jan 2026 20:02:31 +0100 Subject: [PATCH 20/28] Changed naming convention for ready inverter to be more generically signal rewriter --- .../HandshakeToSynth/HandshakeToSynth.cpp | 133 +++++++++--------- 1 file changed, 69 insertions(+), 64 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index 3992fe877..a154d71a2 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -68,56 +68,57 @@ using namespace dynamatic::handshake; #define DEBUG_TYPE "handshake-to-synth" -// TODO: Potentially change the name of this class since it now also -// performs unbundling of data signals into single-bit signals -class ReadySignalInverter { +class SignalRewriter { public: - // Function to invert the ready signals of an hw module operation - void - invertReadySignalHWModule(hw::HWModuleOp oldMod, ModuleOp parent, - SymbolTable &symTable, - DenseMap &newHWmodules, - DenseMap &oldHWmodules); - - // Function to invert the ready signals of an hw instance operation - void invertReadySignalHWInstance( - hw::InstanceOp oldInst, ModuleOp parent, SymbolTable &symTable, - DenseMap &newHWmodules, - DenseMap &oldHWmodules); - - // Function to get the name of the new hw module with inverted ready signals - mlir::StringAttr getNewModuleName(hw::HWModuleOp oldMod, - mlir::MLIRContext *ctx); - - // Function to get the mapping of an input signal + /// Rewrite a HW module interface and body to apply signal-direction and + /// bit-level rewrites (e.g., ready inversion and bit unbundling). + void rewriteHWModule(hw::HWModuleOp oldMod, ModuleOp parent, + SymbolTable &symTable, + DenseMap &newHWModules, + DenseMap &oldHWModules); + + /// Rewrite a HW instance to use a rewritten module and updated operands. + void rewriteHWInstance(hw::InstanceOp oldInst, ModuleOp parent, + SymbolTable &symTable, + DenseMap &newHWModules, + DenseMap &oldHWModules); + + /// Get the name of the rewritten HW module (e.g., by appending a suffix). + mlir::StringAttr getRewrittenModuleName(hw::HWModuleOp oldMod, + mlir::MLIRContext *ctx); + + /// Get the mapping of an old input signal to its rewritten signals. SmallVector getInputSignalMapping(Value oldInputSignal, OpBuilder builder, Location loc); - // Function to update the mapping of an output signal + /// Update the mapping for an output signal group after a new instance is + /// created. void updateOutputSignalMapping(Value oldResult, StringRef outputName, hw::HWModuleOp newMod, hw::InstanceOp newInst); - // Function to invert all ready signals in a module operation - LogicalResult invertAllReadySignals(mlir::ModuleOp modOp); + /// Rewrite all HW modules in the given MLIR module to apply signal + /// restructuring (direction changes, bit unbundling, etc.). + LogicalResult rewriteAllSignals(mlir::ModuleOp modOp); private: - // IMPORTANT: A fundamental assumption for this map values to work is that - // each + // IMPORTANT: A fundamental assumption for these maps to work is that each // handshake channel connects uniquely one handshake unit to another one. If - // this assumption is broken, the map will not work correctly since one key + // this assumption is broken, the maps will not work correctly since one key // could correspond to multiple values. // - // Maps to keep track of signal connections during instance rewriting - // Old refers to the original hw instance with wrong ready signal directions - // and hw types with a potential bitwidth larger than 1. - // New refers to the rewritten hw instance with correct ready signal - // directions and with hw types strictly 1 bit wide for each signal. + // Maps to keep track of signal connections during instance rewriting. + // + // "Old" refers to the original HW instance, with potentially incorrect + // signal directions and HW types that may have bitwidths larger than 1. + // "New" refers to the rewritten HW instance, with corrected signal + // directions and HW types normalized to 1-bit signals for each component. DenseMap> oldModuleSignalToNewModuleSignalsMap; - // Since the hw instances are rewritten in a recursive manner, it might be - // possible that we need the result of an instance that has not been created - // yet. To handle this case, we create temporary hw constant operations - // to hold the place of these values. This map keeps track of these temporary - // values + + // Since the HW instances are rewritten in a recursive manner, it might be + // possible that we need results of an instance that has not been created + // yet. To handle this case, we create temporary HW constant operations + // to hold places for these values. This map keeps track of these temporary + // values grouped per original signal. DenseMap> oldModuleSignalToTempValuesMap; }; @@ -781,19 +782,20 @@ LogicalResult unbundleAllHandshakeTypes(ModuleOp modOp, MLIRContext *ctx) { } // ------------------------------------------------------------------ -// Definition of functions of ReadySignalInverter class +// Definition of functions of SignalRewriter class // ------------------------------------------------------------------ // Function to get a new module name for the rewritten hw module -mlir::StringAttr ReadySignalInverter::getNewModuleName(hw::HWModuleOp oldMod, - mlir::MLIRContext *ctx) { +mlir::StringAttr +SignalRewriter::getRewrittenModuleName(hw::HWModuleOp oldMod, + mlir::MLIRContext *ctx) { return mlir::StringAttr::get(ctx, oldMod.getName() + "_rewritten"); } // Function to get the mapping of an input signal from the old module to the // new module -SmallVector -ReadySignalInverter::getInputSignalMapping(Value oldInputSignal, - OpBuilder builder, Location loc) { +SmallVector SignalRewriter::getInputSignalMapping(Value oldInputSignal, + OpBuilder builder, + Location loc) { auto it = oldModuleSignalToNewModuleSignalsMap.find(oldInputSignal); if (it != oldModuleSignalToNewModuleSignalsMap.end()) { return it->second; @@ -828,10 +830,10 @@ ReadySignalInverter::getInputSignalMapping(Value oldInputSignal, // Function to update the mapping between old module signals and new module // signals after getting a new result value -void ReadySignalInverter::updateOutputSignalMapping(Value oldResult, - StringRef outputName, - hw::HWModuleOp newMod, - hw::InstanceOp newInst) { +void SignalRewriter::updateOutputSignalMapping(Value oldResult, + StringRef outputName, + hw::HWModuleOp newMod, + hw::InstanceOp newInst) { // Find the corresponding output index of the output in the new module SmallVector outputIdxsNewInst; for (auto &p : newMod.getPortList()) { @@ -868,8 +870,10 @@ void ReadySignalInverter::updateOutputSignalMapping(Value oldResult, } } -// Function to rewrite an hw instance operation to fix ready signal directions -void ReadySignalInverter::invertReadySignalHWInstance( +/// Rewrite an HW instance to use the rewritten module interface and operands. +/// This updates operand connections, reconstructs result groups, and updates +/// internal mapping structures used by the module-level rewriting. +void SignalRewriter::rewriteHWInstance( hw::InstanceOp oldInst, ModuleOp parent, SymbolTable &symTable, DenseMap &newHWmodules, DenseMap &oldHWmodules) { @@ -889,8 +893,7 @@ void ReadySignalInverter::invertReadySignalHWInstance( if (!newHWmodules.count(moduleName)) { hw::HWModuleOp oldMod = symTable.lookup(moduleName); if (oldMod) { - invertReadySignalHWModule(oldMod, parent, symTable, newHWmodules, - oldHWmodules); + rewriteHWModule(oldMod, parent, symTable, newHWmodules, oldHWmodules); } } @@ -974,8 +977,10 @@ void ReadySignalInverter::invertReadySignalHWInstance( } } -// Function to rewrite an hw module to fix ready signal directions -void ReadySignalInverter::invertReadySignalHWModule( +/// Rewrite a HW module to normalize signal directions (e.g., ready going +/// opposite to data/valid) and unbundle multi-bit signals into single-bit +/// signals according to the chosen convention. +void SignalRewriter::rewriteHWModule( hw::HWModuleOp oldMod, ModuleOp parent, SymbolTable &symTable, DenseMap &newHWmodules, DenseMap &oldHWmodules) { @@ -1085,7 +1090,7 @@ void ReadySignalInverter::invertReadySignalHWModule( // Create new hw module builder.setInsertionPointAfter(oldMod); - mlir::StringAttr newModuleName = getNewModuleName(oldMod, ctx); + mlir::StringAttr newModuleName = getRewrittenModuleName(oldMod, ctx); auto newMod = builder.create(oldMod.getLoc(), newModuleName, newPortInfo); // Save it in the list of new module using the same name as the old module as @@ -1114,8 +1119,7 @@ void ReadySignalInverter::invertReadySignalHWModule( if (auto instOp = dyn_cast(op)) { // Step 3a: If the operation is an hw instance, rewrite it to fix ready // signal directions. - invertReadySignalHWInstance(instOp, parent, symTable, newHWmodules, - oldHWmodules); + rewriteHWInstance(instOp, parent, symTable, newHWmodules, oldHWmodules); } else if (!isa(op)) { hasHwInstances = false; } @@ -1158,8 +1162,9 @@ void ReadySignalInverter::invertReadySignalHWModule( newTerminator->setOperands(newTerminatorOperands); } -// Function to invert the direction of ready signals in all hw modules -LogicalResult ReadySignalInverter::invertAllReadySignals(mlir::ModuleOp modOp) { +/// Function to apply signal-direction and bit-level rewrites to all HW +/// modules in the MLIR module. +LogicalResult SignalRewriter::rewriteAllSignals(mlir::ModuleOp modOp) { // The following function iterates through all hw modules in the module // and rewrites them to invert the direction of ready signals to follow @@ -1169,7 +1174,8 @@ LogicalResult ReadySignalInverter::invertAllReadySignals(mlir::ModuleOp modOp) { // It does so by creating new hw modules with the rewritten ready signal // directions and then connecting them following the same graph structure of // the old modules. Finally, it removes the old hw modules and renames the new - // hw modules to the original names. + // hw modules to the original names. Additionally, during the ready signal + // inversion, it also unbundles multi-bit signals into single-bit signals. // Maps to keep track of old and new hw modules // The old hw modules have the wrong ready signal directions where ready @@ -1184,8 +1190,7 @@ LogicalResult ReadySignalInverter::invertAllReadySignals(mlir::ModuleOp modOp) { modOp.walk([&](hw::HWModuleOp m) { oldHWModules.insert({m.getName(), m}); }); // Iterate through all hw modules and rewrite them for (auto [name, hwMod] : oldHWModules) - invertReadySignalHWModule(hwMod, modOp, symTable, newHWModules, - oldHWModules); + rewriteHWModule(hwMod, modOp, symTable, newHWModules, oldHWModules); // Erase old hw modules for (auto [modName, oldMod] : oldHWModules) { oldMod.erase(); @@ -1253,10 +1258,10 @@ class HandshakeToSynthPass // Step 2: invert the direction of all ready signals in the hw modules // created from handshake operations. Additionally, unbundle data signals // into single-bit signals. - // Create on object of the ReadySignalInverter class to manage the + // Create on object of the SignalRewriter class to manage the // inversion - ReadySignalInverter inverter; - if (failed(inverter.invertAllReadySignals(modOp))) + SignalRewriter signalRewriter; + if (failed(signalRewriter.rewriteAllSignals(modOp))) return signalPassFailure(); // Step 3: (not implemented yet) Convert synth subckt operations into other From 16354b0b96244a73060c1df723bd8b3a00d7a018 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Tue, 13 Jan 2026 20:13:08 +0100 Subject: [PATCH 21/28] Changed naming convention for ready inverter to be more generically signal rewriter --- .../Synth/ConversionHandshakeToSynth.md | 240 +++++++++++------- 1 file changed, 149 insertions(+), 91 deletions(-) diff --git a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md index 0ff4238b1..482fda353 100644 --- a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md +++ b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md @@ -7,7 +7,7 @@ There are three main sections in this document. 1. Main pass and usage: Overall structure and rationale of the pass. 2. Unbundling conversion: Lowering Handshake channel types to flat HW ports and `synth.subckt`. -3. Ready inversion: Fixing the direction of ready signals to follow the standard handshake protocol and unbundling multi-bit data signals into multiple single bit signals. +3. Signal rewriting: Inverting the direction of ready signals to follow the standard handshake protocol and unbundling multi-bit data signals into multiple single bit signals. The pass is called [`HandshakeToSynthPass`](HandshakeToSynth.cpp). @@ -44,7 +44,7 @@ Its `runDynamaticPass()` method: - Retrieves the `mlir::ModuleOp` and `MLIRContext`. - Ensures that there is at most one non-external `handshake.func` in the module and that if none is found, the pass is a no-op. - Runs Phase 1 – Unbundling by calling `unbundleAllHandshakeTypes(modOp, ctx)`. -- Runs Phase 2 – Ready inversion by instantiating a `ReadySignalInverter` and calling `invertAllReadySignals(modOp)`. +- Runs Phase 2 – Signal rewriting by instantiating a `SignalRewriter` and calling `rewriteAllSignals(modOp)`. - Leaves a future Phase 3 – further refinement of `synth.subckt` into other synth operations (registers, combinational logic, etc.) as a TODO. In a typical flow, this pass is run after all Handshake-level optimizations and buffer insertion, and before a dedicated synth backend that will interpret or further lower the generated synth operations. @@ -313,58 +313,53 @@ The pattern `class ConvertFuncToHWMod : public OpConversionPattern oldModuleSignalToNewModuleSignalMap`: maps original signals to their new counterparts. -- `DenseMap oldModuleSignalToTempValueMap`: maps original signals to temporary placeholder constants when the final signal is not yet available. +- `DenseMap> oldModuleSignalToNewModuleSignalsMap` + Maps an original signal (from old modules) to the vector of new signals that represent it in the rewritten hierarchy (e.g., a group of single-bit signals after unbundling). +- `DenseMap> oldModuleSignalToTempValuesMap` + Maps an original signal to a vector of temporary placeholder values used when the final rewritten signals are not yet available (e.g., because the producing instance has not been rewritten yet). It assumes that each Handshake channel connects exactly one producer to exactly one consumer; otherwise, the signal-mapping logic based on `DenseMap` may not be valid. ### Per-module rewriting -The core of the inversion is: +- `void SignalRewriter::rewriteHWModule(hw::HWModuleOp oldMod, ModuleOp parent, SymbolTable &symTable, DenseMap &newHWModules, DenseMap &oldHWModules)`. -- `void ReadySignalInverter::invertReadySignalHWModule(hw::HWModuleOp oldMod, ModuleOp parent, SymbolTable &symTable, DenseMap &newHWModules, DenseMap &oldHWModules)`. - -It proceeds in four steps. +This function rewrites one module in four conceptual steps: skip already processed modules, build a rewritten interface, transform the body, and wire the new terminator. #### Step 1 – Already processed check @@ -372,83 +367,146 @@ If a module name already exists in `newHWModules`, the function returns immediat #### Step 2 – New module interface and creation -The function: +The rewriter first determines the interface of the new module. + +- It scans `oldMod.getPortList()` and, for each port `p`, decides: + - Whether the signal should keep its direction (e.g., data/valid). + - Whether the signal must have its direction flipped (e.g., ready-like signals, recognized by name). + - How multi-bit ports should be represented after unbundling (e.g., grouped single-bit ports), if applicable. + - + +While scanning, it builds two association tables: + +- `SmallVector> newModuleOutputIdxToOldSignal` + For each new output index, records the old signal (either a module argument or terminator operand) that it logically corresponds to. + +- `SmallVector> newModuleInputIdxToOldSignal` + For each new input index, records the old signal or group of signals that feed it in the original module. -- Scans the old module’s `PortList` to construct new input and output port lists with inverted directions for ready signals. -- For each port `p`: - - If `p` is an input and its name contains `ready`, it becomes a ready output in the new module. - - If `p` is an output and its name contains `ready`, it becomes a ready input in the new module. - - Non-ready inputs and outputs keep their direction but are re-mapped with appropriate index tracking. -While doing so, it builds: +Using these port lists, `SignalRewriter` then: -- `SmallVector> newModuleOutputIdxToOldSignal`: - - For each new output index, records the old signal (either a module argument or terminator operand) that semantically corresponds to it. -- `SmallVector> newModuleInputIdxToOldSignal`: - - Similarly for new inputs. +- Creates a new `hw::HWModuleOp` immediately after `oldMod`, with name returned by `getRewrittenModuleName(oldMod, ctx)` (e.g., `_rewritten`) and ports `newInputs`, `newOutputs`. +- Inserts this new module into `newHWModules` under the *original* module name, so that lookups by name see the rewritten version. +- For each `(newInputIdx, oldSignal)` in `newModuleInputIdxToOldSignal`: + - Fetches the new module argument at index `newInputIdx`. + - Records a mapping from the old signal to the new argument in `oldModuleSignalToNewModuleSignalsMap`. + +This step defines how the new interface maps back to the original signals, including direction changes and bit-level expansions. -It then: +#### Step 3 – Rewrite the body -- Creates a new `hw::HWModuleOp` after `oldMod` with name `_rewritten` via `getNewModuleName` and ports `newInputs`, `newOutputs`. -- Registers the new module in `newHWModules` under the original name. -- For each new input index, maps its argument to the corresponding old signal in `oldModuleSignalToNewModuleSignalMap`. +The body of the old module is then transformed to match the rewritten interface. -#### Step 3 – Body rewriting +- The rewriter scans all operations in `oldMod`: + - If it encounters any `hw.instance`, it sets `hasHwInstances = true`. -The body of the old module is processed to handle instances and non-instance modules. +If `hasHwInstances` is true: -- The pass first scans the body: - - If it finds any `hw.instance`, `hasHwInstances` is set to true. -- If instances exist: - - Each `hw.instance` is rewritten by calling `invertReadySignalHWInstance`. -- If no instances exist: - - The module is expected to contain only `hw.output` and `synth.subckt` operations. - - Any deviation is treated as an error and causes an assertion. - - In this pure glue module case, a new `synth.subckt` is instantiated in the new module via `instantiateSynthOpInHWModule` to maintain the same connectivity under the new interface. +- Every `hw.instance` in `oldMod` is rewritten using `rewriteHWInstance`, which: + - Ensures the callee module has a rewritten version (recursively calling `rewriteHWModule` if needed). + - Creates a new instance of the rewritten module in the corresponding rewritten top module. + - Updates the mapping structures with the new instance’s results. -#### Step 4 – New terminator wiring +If `hasHwInstances` is false: -Finally, the new module’s terminator (`hw.output`) is updated. +- The module is expected to contain only: + - `hw.output` operations, and + - `synth.subckt` operations. +- Any additional operation is treated as an error because such modules are assumed to be pure “glue” between ports and a `synth.subckt`. +- In this case, the rewriter invokes `instantiateSynthOpInHWModule` on the new module to recreate a `synth.subckt` that reflects the original behavior under the new interface. -- For each `(newOutputIdx, oldSignal)` pair in `newModuleOutputIdxToOldSignal`, the builder: - - Looks up the mapped new signal from `oldModuleSignalToNewModuleSignalMap`. - - Asserts that the mapping exists. - - Collects the mapped values in order of new output index. -- The terminator’s operands are replaced with this new operand list. +This step ensures that the internal logic of each module is re-expressed in terms of the new interface and the new per-bit signal representation. + +#### Step 4 – Wire the new terminator + +Finally, the rewriter wires the terminator of the new module. + +- For each `(newOutputIdx, oldSignal)` in `newModuleOutputIdxToOldSignal`, it: + - Looks up the group of new signals corresponding to `oldSignal` in `oldModuleSignalToNewModuleSignalsMap`, asserting that a mapping exists. + - Accumulates these new signals in order of `newOutputIdx` and per-bit position. +- It then sets the operands of the new module’s `hw.output` terminator to this ordered list of values. + +After this step, the new module is self-consistent: its ports, body, and terminator all use the updated directions and bit-level structure. + +--- ### Instance rewriting -Instances are rewritten by: +Rewriting instances of HW modules is handled by: -- `void ReadySignalInverter::invertReadySignalHWInstance(hw::InstanceOp oldInst, ModuleOp parent, SymbolTable &symTable, DenseMap &newHWModules, DenseMap &oldHWModules)`. +- `void SignalRewriter::rewriteHWInstance(hw::InstanceOp oldInst, ModuleOp parent, SymbolTable &symTable, DenseMap &newHWModules, DenseMap &oldHWModules)`. -This function: +This function replaces an instance of an old module with an instance of the corresponding rewritten module, reconnecting operands and results according to the new interface. + +#### Step 1 – Ensure the callee is rewritten + +From `oldInst.getModuleName()`, the rewriter: + +- Checks whether the rewritten callee already exists in `newHWModules`. +- If not, it looks up the original callee in `oldHWModules` and calls `rewriteHWModule` on it to create the rewritten version. + +This guarantees that each instance always has a rewritten callee to target. + +#### Step 2 – Find the rewritten context and insertion point + +The rewriter identifies: + +- `newMod`: the rewritten callee module from `newHWModules`. +- `oldMod`: the original callee from `oldHWModules`. +- `oldInstTopModule`: the HW module that contains `oldInst`. +- `newTopMod`: the rewritten counterpart of `oldInstTopModule` in `newHWModules` where the new instance will be created. + +It sets the builder insertion point just before the terminator of `newTopMod`, so that new instances are appended at the end of its body. + +#### Step 3 – Classify ports and build operands + +The rewriter then constructs three lists: + +- A list of outputs whose structure/direction remains stable and whose uses will later be redirected (e.g., data/valid outputs). +- A list of old signals whose direction or bit-level representation changes and that must be mapped to new outputs (e.g., ready-like signals or unbundled bits). +- A list of operands to feed the new instance (`newOperands`). + +To do this, it iterates over `oldMod.getPortList()` and, for each port `p`: + +- For ports whose direction must be flipped in the new module (e.g., ready-like ports detected by name): + - Ready-like inputs in the old module become outputs in the new one; their old values are recorded for later mapping. + - Ready-like outputs in the old module become inputs in the new one; the rewriter calls `getInputSignalMapping` on the old result to obtain or create the new input signals, then appends those to `newOperands`. + +- For ports whose direction stays the same but whose bit representation may change: + - Non-ready inputs are handled by calling `getInputSignalMapping` on the old operand and appending the returned signals to `newOperands`. + - Non-ready outputs are recorded as “stable outputs” whose uses must later be redirected to groups of new signals. + +The helper `SmallVector SignalRewriter::getInputSignalMapping(Value oldInputSignal, OpBuilder builder, Location loc)` behaves as follows: + +- If `oldInputSignal` is already in `oldModuleSignalToNewModuleSignalsMap`, it returns the associated vector of new signals. +- If it appears in `oldModuleSignalToTempValuesMap`, it returns the associated temporary vector. +- Otherwise, it: + - Creates one or more `hw.constant` ops of the appropriate type(s), typically producing single-bit zero values. + - Stores these constants in `oldModuleSignalToTempValuesMap[oldInputSignal]`. + - Returns the vector of constant results. + +This mechanism enables instance rewriting even when the producers of some signals have not yet been rewritten. + +#### Step 4 – Create the new instance and update mappings + +Once `newOperands` is complete, the rewriter: + +- Creates a new `hw::InstanceOp` `newInst` in `newTopMod`, using: + - `newMod` as the callee. + - The same instance name attribute as `oldInst`. + - `newOperands` as operands. + +It then updates the mapping structures: + +- For each stable output `(outputName, oldResultIdx)`: + - Let `oldResult = oldInst.getResult(oldResultIdx)`. + - Invoke `updateOutputSignalMapping(oldResult, outputName, newMod, newInst)`, which: + - Finds all indexes in `newMod` that correspond to `outputName` (e.g., multiple bits). + - Gathers the corresponding `newInst` results into a vector. + - Registers `oldModuleSignalToNewModuleSignalsMap[oldResult] = newResults`. + - If `oldResult` appears in `oldModuleSignalToTempValuesMap`, checks that the temporary group has the same size as `newResults`, replaces all uses of each temporary by the corresponding new value, removes the temps’ defining ops, and erases the entry from the temp map. -- Ensures the instance’s callee module has been rewritten by checking `newHWModules` and calling `invertReadySignalHWModule` if needed. -- Determines the rewritten callee module (`newMod`), the original callee module (`oldMod`), the top-level module containing the instance, and its corresponding rewritten module (`newTopMod`) where new operations will be inserted. -- Prepares three lists: - - `nonReadyOutputs`: `(outputName, oldResultIdx)` pairs for outputs that are not ready and whose uses must be re-routed. - - `oldReadyInputs`: `(inputName, oldValue)` pairs for ready inputs that will become ready outputs in the new module. - - `newOperands`: operands for the new `hw.instance`. -- Iterates over `oldMod`’s ports and classifies each: - - For ready inputs in the old module: - - The corresponding signal in the new module is an output; the old input value is stored in `oldReadyInputs`. - - For ready outputs in the old module: - - These become inputs in the new module. - - `getInputSignalMappingValue` is called on the old output to find or create the matching new input value, which is added to `newOperands`. - - For non-ready inputs: - - `getInputSignalMappingValue` is called similarly and added to `newOperands`. - - For non-ready outputs: - - They are recorded in `nonReadyOutputs` for later output mapping. -- Creates a new instance of `newMod` in the rewritten top module with the original instance name and `newOperands` as operands. -- Updates signal mappings: - - For each `(outputName, oldResultIdx)` in `nonReadyOutputs`, `updateOutputSignalMappingValue` binds the old non-ready result to the corresponding new result and replaces any temporary placeholders. - - For each `(inputName, oldReadyInput)` in `oldReadyInputs`, `updateOutputSignalMappingValue` maps the old ready input to the new ready output of the rewritten module. - -Helpers: - -- `Value ReadySignalInverter::getInputSignalMappingValue(Value oldInputSignal, OpBuilder &builder, Location loc)`: - - Returns a mapped or temporary value for a given old input, creating a `hw.constant` placeholder if necessary and storing it in `oldModuleSignalToTempValueMap`. -- `void ReadySignalInverter::updateOutputSignalMappingValue(Value oldResult, StringRef outputName, hw::HWModuleOp newMod, hw::InstanceOp newInst)`: - - Finds the output index of `outputName` in `newMod`, obtains the result from `newInst`, updates `oldModuleSignalToNewModuleSignalMap`, replaces any temporary uses, and erases the temporary constant op if present. +- For each signal whose direction or structure changed (e.g., old ready inputs now mapped to new ready outputs), `updateOutputSignalMapping` is used similarly, but the original value may be an operand instead of a result. +After this step, any user that was wired to the old instance (or to its temporary proxies) can be redirected to the final new signals by consulting `oldModuleSignalToNewModuleSignalsMap`. From a5e54122028b070cf70e68283fd5736d55d1d187 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Wed, 14 Jan 2026 16:56:27 +0100 Subject: [PATCH 22/28] Added support to add the same naming convention as the ports in hw --- .../HandshakeToSynth/HandshakeToSynth.cpp | 274 ++++++++++++++---- 1 file changed, 216 insertions(+), 58 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index a154d71a2..3a76d44df 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -53,12 +53,15 @@ #include "llvm/ADT/TypeSwitch.h" #include "llvm/Support/Casting.h" #include "llvm/Support/ErrorHandling.h" +#include "llvm/Support/JSON.h" #include "llvm/Support/raw_ostream.h" #include #include #include #include +#include #include +#include #include #include @@ -233,11 +236,143 @@ class ChannelUnbundlingTypeConverter : public TypeConverter { } }; +// Utility function to insert a string before the first occurrence of '[' +std::string insertBeforeBracket(const std::string &s, + const std::string &toInsert) { + std::string result = s; + std::size_t pos = result.find('['); + // Find first occurrence of '[' and insert the string before it + if (pos != std::string::npos) { + result.insert(pos, toInsert); + } else { + // If no '[', just append at the end + result += toInsert; + } + return result; +} + +// Utility function to get the string formatted in the desired portname[idx] +// format +std::string formatPortName(const std::string &rootName, + const std::optional &index) { + if (index.has_value()) { + return rootName + "[" + std::to_string(index.value()) + "]"; + } + return rootName; +} + +// Function to get the list of names of hardware ports using the format: +// portname[idx] for each port part of an array or portname for single ports +std::pair, SmallVector> +getHWModulePortNames(Operation *op) { + SmallVector inputPortNames; + SmallVector outputPortNames; + // Check if the operation implements NamedIOInterface to get port names + auto namedIO = dyn_cast(op); + if (!namedIO) { + // If the operation is the handshake function, just name it in a default way + if (auto funcOp = dyn_cast(op)) { + for (auto [idx, _] : llvm::enumerate(funcOp.getArguments())) { + inputPortNames.push_back("in" + std::to_string(idx)); + } + for (auto [idx, _] : llvm::enumerate(funcOp.getResultTypes())) { + outputPortNames.push_back("out" + std::to_string(idx)); + } + return {inputPortNames, outputPortNames}; + } + // It is essential to retrieve the port names to correctly connect signals + // during unbundling. If the operation does not implement this interface, + // we cannot proceed. + llvm::errs() << op->getName() << " does not implement NamedIOInterface\n"; + assert(false && "Operation does not implement NamedIOInterface"); + } + std::regex pattern(R"((\w+)_(\d+))"); + // Elaborate the portname in case it contains the following format + // "portname_index" where index is an integer representing the index-th + // input. However, in order to respect the format of port names in the blif + // files, we have to change this format into "portname[index]". + // However, the pattern "portname_index" might indicate an index with size 0. + // For this reason we need to keep track of the size of each port. + // Iterate over input operands to get their names + for (auto [idx, _] : llvm::enumerate(op->getOperands())) { + std::string portNameStr = namedIO.getOperandName(idx).c_str(); + // We use regex to identify this pattern + std::string elaboratedPortName; + std::smatch matches; + if (std::regex_match(portNameStr, matches, pattern)) { + // If the pattern matches, construct the new port name + std::string rootName = matches[1].str(); + unsigned index = std::stoi(matches[2].str()); + if (index == 0) { + // If index is 0, keep the original port name in case there is only + // one port + elaboratedPortName = rootName; + } else { + elaboratedPortName = formatPortName(rootName, index); + } + // If index == 1, fix the name of index 0 to follow the same format + if (index == 1) { + std::string firstPortName = rootName; + firstPortName = formatPortName(firstPortName, 0); + // Replace the port named only rootName with the new one + int idxToReplace = -1; + for (auto [i, name] : llvm::enumerate(inputPortNames)) { + if (name == rootName) { + idxToReplace = i; + break; + } + } + assert(idxToReplace != -1 && "Could not find the first port to rename"); + inputPortNames[idxToReplace] = firstPortName; + } + } else { + // If the pattern does not match, keep the original port name + elaboratedPortName = portNameStr; + } + inputPortNames.push_back(elaboratedPortName); + } + // Iterate over outputs to replicate the same logic + for (auto [idx, _] : llvm::enumerate(op->getResults())) { + std::string portNameStr = namedIO.getResultName(idx).c_str(); + std::string elaboratedPortName; + std::smatch matches; + if (std::regex_match(portNameStr, matches, pattern)) { + std::string rootName = matches[1].str(); + unsigned index = std::stoi(matches[2].str()); + if (index == 0) { + elaboratedPortName = rootName; + } else { + elaboratedPortName = formatPortName(rootName, index); + } + if (index == 1) { + std::string firstPortName = rootName; + firstPortName = formatPortName(firstPortName, 0); + int idxToReplace = -1; + for (auto [i, name] : llvm::enumerate(outputPortNames)) { + if (name == rootName) { + idxToReplace = i; + break; + } + } + assert(idxToReplace != -1 && "Could not find the first port to rename"); + outputPortNames[idxToReplace] = firstPortName; + } + } else { + elaboratedPortName = portNameStr; + } + outputPortNames.push_back(elaboratedPortName); + } + + return {inputPortNames, outputPortNames}; +} + // Function to get the HW Module input and output port info from an handshake // operation void getHWModulePortInfo(Operation *op, SmallVector &hwInputPorts, SmallVector &hwOutputPorts) { MLIRContext *ctx = op->getContext(); + // Get the port names of the operation + auto [inputPortNames, outputPortNames] = getHWModulePortNames(op); // Unbundle the operation ports SmallVector>> unbundledInputPorts; SmallVector>> unbundledOutputPorts; @@ -245,14 +380,16 @@ void getHWModulePortInfo(Operation *op, SmallVector &hwInputPorts, // Iterate over each set of unbundled input ports and create hw port info from // them for (auto [idx, inputPortSet] : llvm::enumerate(unbundledInputPorts)) { + std::string portName = inputPortNames[idx]; // Fill in the hw port info for inputs for (auto port : inputPortSet) { std::string name; Type type; // Unpack the port info as name and type - name = port.first == DATA_SIGNAL ? "in_data_" + std::to_string(idx) - : port.first == VALID_SIGNAL ? "in_valid_" + std::to_string(idx) - : "in_ready_" + std::to_string(idx); + name = port.first == DATA_SIGNAL ? portName + : port.first == VALID_SIGNAL + ? insertBeforeBracket(portName, "_valid") + : insertBeforeBracket(portName, "_ready"); type = port.second; // Create the hw port info and add it to the list hwInputPorts.push_back( @@ -264,14 +401,16 @@ void getHWModulePortInfo(Operation *op, SmallVector &hwInputPorts, // Iterate over each set of unbundled output ports and create hw port info // from them for (auto [idx, outputPortSet] : llvm::enumerate(unbundledOutputPorts)) { + std::string portName = outputPortNames[idx]; // Fill in the hw port info for outputs for (auto port : outputPortSet) { std::string name; Type type; // Unpack the port info as name and type - name = port.first == DATA_SIGNAL ? "out_data_" + std::to_string(idx) - : port.first == VALID_SIGNAL ? "out_valid_" + std::to_string(idx) - : "out_ready_" + std::to_string(idx); + name = port.first == DATA_SIGNAL ? portName + : port.first == VALID_SIGNAL + ? insertBeforeBracket(portName, "_valid") + : insertBeforeBracket(portName, "_ready"); type = port.second; // Create the hw port info and add it to the list hwOutputPorts.push_back( @@ -299,10 +438,10 @@ createCastBundledToUnbundled(Value input, Location loc, return cast; } -// Function to create a cast operation for each result of an unbundled operation -// when converting an original bundled operation to an unbundled one. This cast -// is essential to connect the unbundled operation back to the rest of the -// circuit which is potentially still bundled +// Function to create a cast operation for each result of an unbundled +// operation when converting an original bundled operation to an unbundled +// one. This cast is essential to connect the unbundled operation back to the +// rest of the circuit which is potentially still bundled SmallVector createCastResultsUnbundledOp(TypeRange originalOpResultType, SmallVector unbundledValues, Location loc, @@ -321,8 +460,8 @@ createCastResultsUnbundledOp(TypeRange originalOpResultType, SmallVector> unbundledTypes = unbundleType(originalOpResultType[i]); numUnbundledTypes = unbundledTypes.size(); - // Get a slice of the unbundled values corresponding to this result from the - // unbundled operation + // Get a slice of the unbundled values corresponding to this result from + // the unbundled operation SmallVector unbundledValuesSlice(unbundledValues.begin() + resultIdx, unbundledValues.begin() + resultIdx + numUnbundledTypes); @@ -338,9 +477,10 @@ createCastResultsUnbundledOp(TypeRange originalOpResultType, // Function to convert a handshake function operation into an hw module // operation -// It is divided into three parts: first, create the hw module definition if it -// does not already exist, then move the function body of the handshake function -// into the hw module, and finally create the output operation for the hw module +// It is divided into three parts: first, create the hw module definition if +// it does not already exist, then move the function body of the handshake +// function into the hw module, and finally create the output operation for +// the hw module hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, ConversionPatternRewriter &rewriter) { MLIRContext *ctx = funcOp.getContext(); @@ -377,8 +517,8 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, // handshake function in the hw module. However, the arguments of the // handshake function are bundled and the ones of the hw module are // unbundled. For this reason, create a cast for each argument of the hw - // module. The output of the casts will be used to inline the function body - // First create casts for each argument of the hw module + // module. The output of the casts will be used to inline the function + // body First create casts for each argument of the hw module SmallVector castedArgs; TypeRange funcArgTypes = funcOp.getArgumentTypes(); SmallVector castsArgs = @@ -391,8 +531,8 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, "each cast operation must have only one result"); castedArgs.push_back(castOp.getResult(0)); } - // Inline the function body before the terminator of the hw module using the - // casted arguments + // Inline the function body before the terminator of the hw module using + // the casted arguments rewriter.inlineBlockBefore(funcBlock, termOp, castedArgs); // Finally, create the output operation for the hw module @@ -408,8 +548,8 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, assert(endOp && "Expected handshake.end after inlining"); - // Create a cast for each operand of the end operation to unbundle the types - // which were previously handshake bundled types + // Create a cast for each operand of the end operation to unbundle the + // types which were previously handshake bundled types SmallVector hwOutputs; for (Value operand : endOp.getOperands()) { // Unbundle the operand type depending by creating a cast from the @@ -421,8 +561,8 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, // Create the output operation for the hw module rewriter.setInsertionPointToEnd(endOp->getBlock()); rewriter.replaceOpWithNewOp(endOp, hwOutputs); - // Remove any old output operation if present (should not be the case after - // replacing the end op) + // Remove any old output operation if present (should not be the case + // after replacing the end op) Operation *oldOutput = nullptr; for (Operation &op : *hwModule.getBodyBlock()) { if (auto out = dyn_cast(op)) { @@ -440,8 +580,8 @@ hw::HWModuleOp convertFuncOpToHWModule(handshake::FuncOp funcOp, return hwModule; } -// Function to instantiate synth or hw operations inside an hw module describing -// the behavior of the original handshake operation +// Function to instantiate synth or hw operations inside an hw module +// describing the behavior of the original handshake operation LogicalResult instantiateSynthOpInHWModule(hw::HWModuleOp hwModule, ConversionPatternRewriter &rewriter) { @@ -459,7 +599,8 @@ instantiateSynthOpInHWModule(hw::HWModuleOp hwModule, synthOutputTypes.push_back(outputPort.type); } TypeRange synthOutputsTypeRange(synthOutputTypes); - // Connect the outputs of the synth operation to the outputs of the hw module + // Connect the outputs of the synth operation to the outputs of the hw + // module Operation *synthTerminator = hwModule.getBodyBlock()->getTerminator(); // Create the synth subcircuit operation inside the hw module synth::SubcktOp synthInstOp = rewriter.create( @@ -515,7 +656,8 @@ hw::HWModuleOp convertOpToHWModule(Operation *op, handshake::HandshakeDialect::getDialectNamespace()) || isBlockArg) { // If so, create an unrealized conversion cast to unbundle the operand - // into its components to connect it to the input ports of the hw instance + // into its components to connect it to the input ports of the hw + // instance auto cast = createCastBundledToUnbundled(operand, op->getLoc(), rewriter); // Append all cast results to the operand values operandValues.append(cast.getResults().begin(), cast.getResults().end()); @@ -615,9 +757,9 @@ LogicalResult removeUnrealizedConversionCasts(mlir::ModuleOp modOp) { castOp1.getOperand(0).getDefiningOp(); if (inputCastOp) { // Skip this cast since it will be handled when processing the - // input cast but first assert that it is not followed by any other cast. - // If this is the case, there is a issue in the code since there are 3 - // casts in chain and this break the assumption of the code + // input cast but first assert that it is not followed by any other + // cast. If this is the case, there is a issue in the code since there + // are 3 casts in chain and this break the assumption of the code assert(llvm::none_of(castOp1->getUsers(), [](Operation *user) { return isa(user); @@ -838,7 +980,12 @@ void SignalRewriter::updateOutputSignalMapping(Value oldResult, SmallVector outputIdxsNewInst; for (auto &p : newMod.getPortList()) { StringRef portName = p.name.getValue(); - if (portName.starts_with(outputName)) { + // Check the beginning of the port name to match the output name but it + // should not be followed by the character '_', to avoid matching + // similarly named ports like output and output_valid + if (portName.starts_with(outputName) && + (portName.size() == outputName.size() || + portName[outputName.size()] != '_')) { outputIdxsNewInst.push_back(p.argNum); } } @@ -949,7 +1096,8 @@ void SignalRewriter::rewriteHWInstance( Value oldNonReadyInput = oldInst.getOperand(port.argNum); SmallVector newNonReadyInputs = getInputSignalMapping(oldNonReadyInput, builder, locNewOps); - // Append each bit separately of the non-ready signal to the new operands + // Append each bit separately of the non-ready signal to the new + // operands for (auto newNonReadyInput : newNonReadyInputs) newOperands.push_back(newNonReadyInput); } else { @@ -993,9 +1141,10 @@ void SignalRewriter::rewriteHWModule( // directions. // b. If the operation is not an hw instance, check if it is only // output operations and synth subckt operations. If so, create a - // synth subckt operation to connect the module ports. If not, raise an - // error. - // 4. Finally, connect the hw instances to the terminator operands of the new + // synth subckt operation to connect the module ports. If not, raise + // an error. + // 4. Finally, connect the hw instances to the terminator operands of the + // new // module. // Step 1: Check if the module has already been rewritten @@ -1026,19 +1175,22 @@ void SignalRewriter::rewriteHWModule( unsigned startOutputIdx = outputIdx; unsigned startInputIdx = inputIdx; if ((p.isInput() && isReady) || (p.isOutput() && !isReady)) { - // If the port is input and ready, it becomes output and ready in the new - // module. If a port is output and not ready, it stays as such. + // If the port is input and ready, it becomes output and ready in the + // new module. If a port is output and not ready, it stays as such. Type signalType = p.type; assert(signalType.isa() && "only integer types are supported for signals for now"); unsigned bitWidth = signalType.cast().getWidth(); for (unsigned i = 0; i < bitWidth; ++i) { - newOutputs.push_back( - {hw::ModulePort{StringAttr::get(ctx, p.name.getValue().str() + "_" + - std::to_string(i)), - builder.getIntegerType(1), - hw::ModulePort::Direction::Output}, - outputIdx}); + // Update portname indexing the specific bit unless it is 1 bit wide + std::string portName = formatPortName(p.name.getValue().str(), i); + if (bitWidth == 1) { + portName = p.name.getValue().str(); + } + newOutputs.push_back({hw::ModulePort{StringAttr::get(ctx, portName), + builder.getIntegerType(1), + hw::ModulePort::Direction::Output}, + outputIdx}); outputIdx++; } Value oldSignal; @@ -1054,18 +1206,22 @@ void SignalRewriter::rewriteHWModule( newModuleOutputIdxToOldSignal.push_back(std::make_pair( std::make_pair(startOutputIdx, endOutputIdx), oldSignal)); } else if ((p.isOutput() && isReady) || (p.isInput() && !isReady)) { - // If the port is output and ready, it becomes input and ready in the new - // module. If a port is input and not ready, it stays as such. + // If the port is output and ready, it becomes input and ready in the + // new module. If a port is input and not ready, it stays as such. Type signalType = p.type; assert(signalType.isa() && "only integer types are supported for signals for now"); unsigned bitWidth = signalType.cast().getWidth(); for (unsigned i = 0; i < bitWidth; ++i) { + // Update portname indexing the specific bit unless it is 1 bit wide + std::string portName = formatPortName(p.name.getValue().str(), i); + if (bitWidth == 1) { + portName = p.name.getValue().str(); + } newInputs.push_back( - {hw::ModulePort{ - StringAttr::get(ctx, StringRef{p.name.getValue().str() + "_" + - std::to_string(i)}), - builder.getIntegerType(1), hw::ModulePort::Direction::Input}, + {hw::ModulePort{StringAttr::get(ctx, StringRef{portName}), + builder.getIntegerType(1), + hw::ModulePort::Direction::Input}, inputIdx}); inputIdx++; } @@ -1093,8 +1249,8 @@ void SignalRewriter::rewriteHWModule( mlir::StringAttr newModuleName = getRewrittenModuleName(oldMod, ctx); auto newMod = builder.create(oldMod.getLoc(), newModuleName, newPortInfo); - // Save it in the list of new module using the same name as the old module as - // key + // Save it in the list of new module using the same name as the old module + // as key newHWmodules[oldMod.getName()] = newMod; // Add mapping between new inputs to old signals @@ -1144,8 +1300,8 @@ void SignalRewriter::rewriteHWModule( return; } - // Step 4: Finally, connect the hw instances to the terminator operands of the - // new module. + // Step 4: Finally, connect the hw instances to the terminator operands of + // the new module. builder.setInsertionPointToEnd(newMod.getBodyBlock()); SmallVector newTerminatorOperands; for (auto [newOutputIdxs, oldSignal] : newModuleOutputIdxToOldSignal) { @@ -1173,9 +1329,10 @@ LogicalResult SignalRewriter::rewriteAllSignals(mlir::ModuleOp modOp) { // to all hw modules instantiated within other hw modules. // It does so by creating new hw modules with the rewritten ready signal // directions and then connecting them following the same graph structure of - // the old modules. Finally, it removes the old hw modules and renames the new - // hw modules to the original names. Additionally, during the ready signal - // inversion, it also unbundles multi-bit signals into single-bit signals. + // the old modules. Finally, it removes the old hw modules and renames the + // new hw modules to the original names. Additionally, during the ready + // signal inversion, it also unbundles multi-bit signals into single-bit + // signals. // Maps to keep track of old and new hw modules // The old hw modules have the wrong ready signal directions where ready @@ -1264,8 +1421,9 @@ class HandshakeToSynthPass if (failed(signalRewriter.rewriteAllSignals(modOp))) return signalPassFailure(); - // Step 3: (not implemented yet) Convert synth subckt operations into other - // synth operations like registers, combinational logic, etc. if possible + // Step 3: (not implemented yet) Convert synth subckt operations into + // other synth operations like registers, combinational logic, etc. if + // possible // TODO } }; From 2cbb07ed83b8682eacb676b1cf677983117bb8a3 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Wed, 14 Jan 2026 18:12:08 +0100 Subject: [PATCH 23/28] Added new option to pass the directory containing blif files and hard-coded switch to retrieve the blif path of each operation --- include/dynamatic/Conversion/Passes.td | 3 + .../HandshakeToSynth/HandshakeToSynth.cpp | 205 +++++++++++++++++- 2 files changed, 207 insertions(+), 1 deletion(-) diff --git a/include/dynamatic/Conversion/Passes.td b/include/dynamatic/Conversion/Passes.td index 3a094823f..c2ee43dd3 100644 --- a/include/dynamatic/Conversion/Passes.td +++ b/include/dynamatic/Conversion/Passes.td @@ -94,6 +94,9 @@ def HandshakeToSynth that focuses on logic synthesis constructs. }]; let constructor = "dynamatic::createHandshakeToSynthPass()"; + let options = + [Option<"blifDirPath", "blif-dir-path", "std::string", "", + "Path to directory containing BLIF files for logic synthesis representations.">]; } #endif // DYNAMATIC_CONVERSION_PASSES_TD diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index 3a76d44df..acd18a322 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -59,6 +59,7 @@ #include #include #include +#include #include #include #include @@ -1369,14 +1370,207 @@ LogicalResult SignalRewriter::rewriteAllSignals(mlir::ModuleOp modOp) { return success(); } +// ------------------------------------------------------------------ +// Functions to mark handshake operations with blif path +// ------------------------------------------------------------------ + +// Function to combine parameter values, module type and blif directory path +// to create the blif file path +std::string combineBlifFilePath(std::string blifDirPath, std::string moduleType, + std::vector paramValues, + std::string extraSuffix = "") { + std::string blifFilePath = blifDirPath + "/" + moduleType + extraSuffix; + for (const auto ¶mValue : paramValues) { + blifFilePath += "/" + paramValue; + } + blifFilePath += "/" + moduleType + extraSuffix + ".blif"; + return blifFilePath; +} + +// Function to retrieve the path of the blif file for a given handshake +// operation +std::string getBlifFilePathForHandshakeOp(Operation *op, + std::string blifDirPath) { + // Depending on the operation type, there is a different number and type of + // parameters. For now, we use a switch hard-coded on the operation type. In + // the future, this should be improved by allowing extraction of parameter + // information from the operation itself + std::string moduleType = op->getName().getStringRef().str(); + // Erase the dialect name from the moduleType + moduleType = moduleType.substr(moduleType.find('.') + 1); + std::string blifFileName; + llvm::TypeSwitch(op) + .Case([&](auto) { + unsigned dataWidth = + handshake::getHandshakeTypeBitWidth(op->getOperand(0).getType()); + blifFileName = combineBlifFilePath(blifDirPath, moduleType, + {std::to_string(dataWidth)}); + }) + .Case([&](handshake::ConstantOp constOp) { + handshake::ChannelType cstType = constOp.getResult().getType(); + // Get the data width of the constant operation + unsigned dataWidth = cstType.getDataBitWidth(); + blifFileName = combineBlifFilePath(blifDirPath, moduleType, + {std::to_string(dataWidth)}); + }) + .Case([&](auto) { + unsigned dataWidth = + handshake::getHandshakeTypeBitWidth(op->getOperand(0).getType()); + if (dataWidth == 0) { + blifFileName = + combineBlifFilePath(blifDirPath, moduleType, {}, "_dataless"); + } else { + blifFileName = combineBlifFilePath(blifDirPath, moduleType, + {std::to_string(dataWidth)}); + } + }) + .Case([&](handshake::BufferOp op) { + std::string bufferType = + handshake::stringifyEnum(op.getBufferType()).str(); + unsigned dataWidth = + handshake::getHandshakeTypeBitWidth(op->getOperand(0).getType()); + if (dataWidth == 0) { + blifFileName = + combineBlifFilePath(blifDirPath, bufferType, {}, "_dataless"); + } else { + blifFileName = combineBlifFilePath(blifDirPath, bufferType, + {std::to_string(dataWidth)}); + } + }) + .Case( + [&](handshake::ConditionalBranchOp cbrOp) { + unsigned dataWidth = handshake::getHandshakeTypeBitWidth( + cbrOp.getDataOperand().getType()); + if (dataWidth == 0) { + blifFileName = + combineBlifFilePath(blifDirPath, moduleType, {}, "_dataless"); + } else { + blifFileName = combineBlifFilePath(blifDirPath, moduleType, + {std::to_string(dataWidth)}); + } + }) + .Case([&](handshake::ControlMergeOp cmergeOp) { + unsigned size = cmergeOp.getDataOperands().size(); + unsigned dataWidth = + handshake::getHandshakeTypeBitWidth(cmergeOp.getResult().getType()); + unsigned indexType = + handshake::getHandshakeTypeBitWidth(cmergeOp.getIndex().getType()); + if (dataWidth == 0) { + blifFileName = combineBlifFilePath( + blifDirPath, moduleType, + {std::to_string(size), std::to_string(indexType)}, "_dataless"); + } else { + assert(false && "ControlMerge with data not supported yet"); + } + }) + .Case([&](auto op) { + unsigned inputWidth = + handshake::getHandshakeTypeBitWidth(op.getOperand().getType()); + unsigned outputWidth = + handshake::getHandshakeTypeBitWidth(op.getResult().getType()); + blifFileName = combineBlifFilePath( + blifDirPath, moduleType, + {std::to_string(inputWidth), std::to_string(outputWidth)}); + }) + .Case([&](auto) { + unsigned size = op->getNumResults(); + unsigned dataWidth = + handshake::getHandshakeTypeBitWidth(op->getOperand(0).getType()); + if (dataWidth == 0) { + blifFileName = combineBlifFilePath( + blifDirPath, moduleType, {std::to_string(size)}, "_dataless"); + } else { + blifFileName = combineBlifFilePath( + blifDirPath, moduleType, + {std::to_string(size), std::to_string(dataWidth)}, "_type"); + } + }) + .Case([&](handshake::MuxOp muxOp) { + unsigned size = muxOp.getDataOperands().size(); + unsigned dataWidth = + handshake::getHandshakeTypeBitWidth(muxOp.getResult().getType()); + unsigned selectType = handshake::getHandshakeTypeBitWidth( + muxOp.getSelectOperand().getType()); + blifFileName = combineBlifFilePath(blifDirPath, moduleType, + {std::to_string(size), + std::to_string(dataWidth), + std::to_string(selectType)}); + }) + .Case([&](handshake::MergeOp mergeOp) { + unsigned size = mergeOp.getDataOperands().size(); + unsigned dataWidth = handshake::getHandshakeTypeBitWidth( + mergeOp.getDataOperands()[0].getType()); + blifFileName = combineBlifFilePath( + blifDirPath, moduleType, + {std::to_string(size), std::to_string(dataWidth)}); + }) + .Case([&](auto op) { + unsigned dataWidth = + handshake::getHandshakeTypeBitWidth(op.getDataInput().getType()); + unsigned addrType = + handshake::getHandshakeTypeBitWidth(op.getAddressInput().getType()); + blifFileName = combineBlifFilePath( + blifDirPath, moduleType, + {std::to_string(dataWidth), std::to_string(addrType)}); + }) + .Case([&](auto) { + blifFileName = combineBlifFilePath(blifDirPath, moduleType, {}); + }) + .Default([&](auto) { blifFileName = ""; }); + // Check blifFileName path exists + if (blifFileName != "") { + // Check if the file exists + if (!std::filesystem::exists(blifFileName)) { + llvm::errs() << "BLIF file for operation " << getUniqueName(op) + << "does not exist: " << blifFileName << "\n"; + assert(false && "Check the file path specified for this operation is " + "built correctly"); + } + } + return blifFileName; +} + +// Function to add blif path attribute to each handshake operation +LogicalResult markHandshakeOpsWithBlifPath(handshake::FuncOp funcOp, + std::string blifDirPath, + OpBuilder &builder) { + // Check blifDirPath is not empty + if (blifDirPath.empty()) { + llvm::errs() << "BLIF directory path is empty\n"; + return failure(); + } + // Walk through all handshake operations in the function + funcOp.walk([&](Operation *op) { + // Skip function operations + if (isa(op)) + return; + // Create the blif path by combining the directory path and the op name + // depending on the operation type + std::string blifFilePath = getBlifFilePathForHandshakeOp(op, blifDirPath); + // Create a string attribute for the blif path + mlir::StringAttr blifPathAttr = + mlir::StringAttr::get(builder.getContext(), blifFilePath); + // Set the blif path attribute on the operation + op->setAttr("blif_path", blifPathAttr); + }); + return success(); +} + // ------------------------------------------------------------------ // Main pass definition // ------------------------------------------------------------------ namespace { - // The following pass converts handshake operations into synth operations // It executes in multiple steps: +// 0) Mark each handshake operation with the path of the blif file where its +// definition is located // 1) Unbundle all handshake types used in the handshake function // 2) Invert the direction of ready signals in all hw modules and hw instances // to follow the standard handshake protocol where ready signals go in the @@ -1387,6 +1581,9 @@ namespace { class HandshakeToSynthPass : public dynamatic::impl::HandshakeToSynthBase { public: + HandshakeToSynthPass(std::string blifDirPath); + using HandshakeToSynthBase::HandshakeToSynthBase; + void runDynamaticPass() override { mlir::ModuleOp modOp = getOperation(); MLIRContext *ctx = &getContext(); @@ -1408,6 +1605,11 @@ class HandshakeToSynthPass if (!funcOp) return; + // Step 0: Mark each handshake operation with the path of the blif file + // where its definition is located + if (failed(markHandshakeOpsWithBlifPath(funcOp, blifDirPath, builder))) + return signalPassFailure(); + // Step 1: unbundle all handshake types in the handshake function if (failed(unbundleAllHandshakeTypes(modOp, ctx))) return signalPassFailure(); @@ -1415,6 +1617,7 @@ class HandshakeToSynthPass // Step 2: invert the direction of all ready signals in the hw modules // created from handshake operations. Additionally, unbundle data signals // into single-bit signals. + // // Create on object of the SignalRewriter class to manage the // inversion SignalRewriter signalRewriter; From 44c6d4f1116543d39707da791affce83dd9862a1 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Wed, 14 Jan 2026 18:17:33 +0100 Subject: [PATCH 24/28] Added code to pass blif_path attribute across the different stages of the flow --- lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index acd18a322..c123704b8 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -641,6 +641,10 @@ hw::HWModuleOp convertOpToHWModule(Operation *op, // definition if (failed(instantiateSynthOpInHWModule(hwModule, rewriter))) return nullptr; + // Copy attribute for blif data path + if (auto blifAttr = op->getAttrOfType(blifPathAttrStr)) { + hwModule->setAttr(blifPathAttrStr, blifAttr); + } } // Create the hw instance and replace the original operation with it @@ -1254,6 +1258,11 @@ void SignalRewriter::rewriteHWModule( // as key newHWmodules[oldMod.getName()] = newMod; + // Copy the blif path attribute + if (oldMod->hasAttr(blifPathAttrStr)) { + newMod->setAttr(blifPathAttrStr, oldMod->getAttr(blifPathAttrStr)); + } + // Add mapping between new inputs to old signals for (auto [newModuleInputIdxs, oldSignal] : newModuleInputIdxToOldSignal) { unsigned newModuleInputIdxStart = newModuleInputIdxs.first; @@ -1557,7 +1566,7 @@ LogicalResult markHandshakeOpsWithBlifPath(handshake::FuncOp funcOp, mlir::StringAttr blifPathAttr = mlir::StringAttr::get(builder.getContext(), blifFilePath); // Set the blif path attribute on the operation - op->setAttr("blif_path", blifPathAttr); + op->setAttr(blifPathAttrStr, blifPathAttr); }); return success(); } From f3918be58b360c3342201d56db1cb004e2944ef7 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Fri, 16 Jan 2026 16:27:38 +0100 Subject: [PATCH 25/28] Added defintion for strings referring to blif path, clk and rst --- include/dynamatic/Conversion/HandshakeToSynth.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/dynamatic/Conversion/HandshakeToSynth.h b/include/dynamatic/Conversion/HandshakeToSynth.h index c629e5d68..4d247c07c 100644 --- a/include/dynamatic/Conversion/HandshakeToSynth.h +++ b/include/dynamatic/Conversion/HandshakeToSynth.h @@ -36,6 +36,13 @@ enum SignalKind { READY_SIGNAL = 2, }; +// String of the blif path attribute +static const std::string blifPathAttrStr = "blif_path"; + +// Strings representing the name of the clock and reset signals +static const std::string clockSignal = "clk"; +static const std::string resetSignal = "rst"; + #define GEN_PASS_DECL_HANDSHAKETOSYNTH #define GEN_PASS_DEF_HANDSHAKETOSYNTH #include "dynamatic/Conversion/Passes.h.inc" From 9883dabc3023ff668f623ae5a6085f8e17fa4ff3 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Fri, 16 Jan 2026 16:29:38 +0100 Subject: [PATCH 26/28] Added support to correctly generate the clk and rst signals and correctly connect them to the top function for each hw instance. Added function to convert the hw instances into AIG. --- .../HandshakeToSynth/HandshakeToSynth.cpp | 639 +++++++++++++++++- 1 file changed, 606 insertions(+), 33 deletions(-) diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index c123704b8..b5beacbc1 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -26,6 +26,7 @@ #include "dynamatic/Dialect/Synth/SynthOps.h" #include "dynamatic/Support/Attribute.h" #include "dynamatic/Support/Backedge.h" +#include "dynamatic/Support/LLVM.h" #include "dynamatic/Support/Utils/Utils.h" #include "dynamatic/Transforms/HandshakeMaterialize.h" #include "mlir/Dialect/MemRef/IR/MemRef.h" @@ -58,6 +59,7 @@ #include #include #include +#include #include #include #include @@ -98,12 +100,28 @@ class SignalRewriter { /// Update the mapping for an output signal group after a new instance is /// created. void updateOutputSignalMapping(Value oldResult, StringRef outputName, + int oldOutputIdx, hw::HWModuleOp oldMod, hw::HWModuleOp newMod, hw::InstanceOp newInst); /// Rewrite all HW modules in the given MLIR module to apply signal /// restructuring (direction changes, bit unbundling, etc.). LogicalResult rewriteAllSignals(mlir::ModuleOp modOp); + // Function to set the name of the top function + void setTopFunctionName(StringRef topFunctionName) { + assert(topFunctionName != "" && + "top function name cannot be set to an empty string"); + topFunction = topFunctionName; + } + + // Function to get the name of the top function + StringRef getTopFunctionName() { + assert( + topFunction != "" && + "top function name should be set before being able to get its value"); + return topFunction; + } + private: // IMPORTANT: A fundamental assumption for these maps to work is that each // handshake channel connects uniquely one handshake unit to another one. If @@ -124,6 +142,18 @@ class SignalRewriter { // to hold places for these values. This map keeps track of these temporary // values grouped per original signal. DenseMap> oldModuleSignalToTempValuesMap; + + // Map to connect the idx of the old output signal to the idxs of the new + // output signals after rewriting. + DenseMap>>> + oldOutputIdxToNewOutputIdxMap; + + // Name of the top function + StringRef topFunction = ""; + // Signal of the top function that refers to the clock signal of the top func + Value clkSignalTop; + // Signal of the top function that refers to the reset signal of the top func + Value rstSignalTop; }; // Function to unbundle a single handshake type into its subcomponents @@ -254,10 +284,30 @@ std::string insertBeforeBracket(const std::string &s, // Utility function to get the string formatted in the desired portname[idx] // format +// If the root name already contains an index, the old index is linearized and +// added to the new one using the width input std::string formatPortName(const std::string &rootName, - const std::optional &index) { + const std::optional &index, + unsigned width = 0) { if (index.has_value()) { - return rootName + "[" + std::to_string(index.value()) + "]"; + unsigned newIndex = index.value(); + std::string newRootName = rootName; + // Check if there is already present an index + std::regex pattern(R"((\w+)\[(\d+)\])"); + // We use regex to identify this pattern + std::smatch matches; + if (std::regex_match(rootName, matches, pattern)) { + // If the pattern matches, assert the width is not 0 + assert(width != 0 && + "the width of the signal cannot be 0 if it is an array"); + // Linearize old index + std::string oldIndex = matches[2].str(); + unsigned oldIndexLinearized = std::stoi(oldIndex) * width; + // Add it to new one + newIndex += oldIndexLinearized; + newRootName = matches[1].str(); + } + return newRootName + "[" + std::to_string(newIndex) + "]"; } return rootName; } @@ -271,7 +321,8 @@ getHWModulePortNames(Operation *op) { // Check if the operation implements NamedIOInterface to get port names auto namedIO = dyn_cast(op); if (!namedIO) { - // If the operation is the handshake function, just name it in a default way + // If the operation is the handshake function, just name it in a default + // way if (auto funcOp = dyn_cast(op)) { for (auto [idx, _] : llvm::enumerate(funcOp.getArguments())) { inputPortNames.push_back("in" + std::to_string(idx)); @@ -292,8 +343,8 @@ getHWModulePortNames(Operation *op) { // "portname_index" where index is an integer representing the index-th // input. However, in order to respect the format of port names in the blif // files, we have to change this format into "portname[index]". - // However, the pattern "portname_index" might indicate an index with size 0. - // For this reason we need to keep track of the size of each port. + // However, the pattern "portname_index" might indicate an index with size + // 0. For this reason we need to keep track of the size of each port. // Iterate over input operands to get their names for (auto [idx, _] : llvm::enumerate(op->getOperands())) { std::string portNameStr = namedIO.getOperandName(idx).c_str(); @@ -378,8 +429,8 @@ void getHWModulePortInfo(Operation *op, SmallVector &hwInputPorts, SmallVector>> unbundledInputPorts; SmallVector>> unbundledOutputPorts; unbundleOpPorts(op, unbundledInputPorts, unbundledOutputPorts); - // Iterate over each set of unbundled input ports and create hw port info from - // them + // Iterate over each set of unbundled input ports and create hw port info + // from them for (auto [idx, inputPortSet] : llvm::enumerate(unbundledInputPorts)) { std::string portName = inputPortNames[idx]; // Fill in the hw port info for inputs @@ -977,27 +1028,40 @@ SmallVector SignalRewriter::getInputSignalMapping(Value oldInputSignal, // Function to update the mapping between old module signals and new module // signals after getting a new result value -void SignalRewriter::updateOutputSignalMapping(Value oldResult, - StringRef outputName, - hw::HWModuleOp newMod, - hw::InstanceOp newInst) { - // Find the corresponding output index of the output in the new module - SmallVector outputIdxsNewInst; - for (auto &p : newMod.getPortList()) { - StringRef portName = p.name.getValue(); - // Check the beginning of the port name to match the output name but it - // should not be followed by the character '_', to avoid matching - // similarly named ports like output and output_valid - if (portName.starts_with(outputName) && - (portName.size() == outputName.size() || - portName[outputName.size()] != '_')) { - outputIdxsNewInst.push_back(p.argNum); +void SignalRewriter::updateOutputSignalMapping( + Value oldResult, StringRef outputName, int oldOutputIdx, + hw::HWModuleOp oldMod, hw::HWModuleOp newMod, hw::InstanceOp newInst) { + // Check if you can find the output idx of the oldResult in the list + // oldOutputIdxToNewOutputIdxMap + OpBuilder builder(oldMod); + StringAttr oldModName = builder.getStringAttr(oldMod.getName()); + SmallVector>> + oldOutIdxToNewOutIdxs = oldOutputIdxToNewOutputIdxMap[oldModName]; + SmallVector outputIdxsNewInst = {}; + for (auto &pair : oldOutIdxToNewOutIdxs) { + if (oldOutputIdx != -1 && + pair.first == static_cast(oldOutputIdx)) { + outputIdxsNewInst = pair.second; + break; + } + } + // If you cannot find it, use the output name to find it + if (outputIdxsNewInst.empty()) { + // The only case in which this is possible is when it is a ready signal + assert(outputName.contains("_ready") && + "could not find output index mapping for non-ready signal"); + // Find the corresponding output index of the output in the new module + for (auto &p : newMod.getPortList()) { + StringRef portName = p.name.getValue(); + if (portName == outputName) { + outputIdxsNewInst.push_back(p.argNum); + } } } assert(!outputIdxsNewInst.empty() && "could not find output port in new module"); SmallVector newResults; - for (int idx : outputIdxsNewInst) { + for (unsigned idx : outputIdxsNewInst) { newResults.push_back(newInst->getResult(idx)); } // Add mapping between old non-ready signal and new non-ready signal @@ -1112,6 +1176,10 @@ void SignalRewriter::rewriteHWInstance( } } + // Add new inputs for clk and rst from the top function + newOperands.push_back(oldModuleSignalToNewModuleSignalsMap[clkSignalTop][0]); + newOperands.push_back(oldModuleSignalToNewModuleSignalsMap[rstSignalTop][0]); + // Create the new instance operation within the new hw module auto newInst = builder.create( locNewOps, newMod, oldInst.getInstanceNameAttr(), newOperands); @@ -1121,12 +1189,14 @@ void SignalRewriter::rewriteHWInstance( for (auto [outputName, outputIdxOldInst] : nonReadyOutputs) { // Find the output port in the old module Value oldResult = oldInst.getResult(outputIdxOldInst); - updateOutputSignalMapping(oldResult, outputName, newMod, newInst); + updateOutputSignalMapping(oldResult, outputName, outputIdxOldInst, oldMod, + newMod, newInst); } // Update the mapping between old ready inputs and new ready outputs after // getting the new result values for (auto [inputName, oldReadyInput] : oldReadyInputs) { - updateOutputSignalMapping(oldReadyInput, inputName, newMod, newInst); + updateOutputSignalMapping(oldReadyInput, inputName, -1, oldMod, newMod, + newInst); } } @@ -1173,6 +1243,10 @@ void SignalRewriter::rewriteHWModule( // Store mapping from new input idx start and end to old signal SmallVector, Value>> newModuleInputIdxToOldSignal; + // Store the mapping between old output signal idx and new output signal idxs. + // This should be applied only to non-ready signals. + SmallVector>> + oldOutputIdxToNewOutputIdxs; // Iterate over the ports of the old hw module for (auto &p : oldMod.getPortList()) { bool isReady = p.name.getValue().contains("ready"); @@ -1188,7 +1262,8 @@ void SignalRewriter::rewriteHWModule( unsigned bitWidth = signalType.cast().getWidth(); for (unsigned i = 0; i < bitWidth; ++i) { // Update portname indexing the specific bit unless it is 1 bit wide - std::string portName = formatPortName(p.name.getValue().str(), i); + std::string portName = + formatPortName(p.name.getValue().str(), i, bitWidth); if (bitWidth == 1) { portName = p.name.getValue().str(); } @@ -1210,6 +1285,16 @@ void SignalRewriter::rewriteHWModule( unsigned endOutputIdx = outputIdx - 1; newModuleOutputIdxToOldSignal.push_back(std::make_pair( std::make_pair(startOutputIdx, endOutputIdx), oldSignal)); + if (!isReady) { + // Store mapping from old output idx to new output idxs for + // non-ready outputs + SmallVector newOutputIdxs; + for (unsigned i = startOutputIdx; i <= endOutputIdx; ++i) { + newOutputIdxs.push_back(i); + } + oldOutputIdxToNewOutputIdxs.push_back( + std::make_pair(p.argNum, newOutputIdxs)); + } } else if ((p.isOutput() && isReady) || (p.isInput() && !isReady)) { // If the port is output and ready, it becomes input and ready in the // new module. If a port is input and not ready, it stays as such. @@ -1219,7 +1304,8 @@ void SignalRewriter::rewriteHWModule( unsigned bitWidth = signalType.cast().getWidth(); for (unsigned i = 0; i < bitWidth; ++i) { // Update portname indexing the specific bit unless it is 1 bit wide - std::string portName = formatPortName(p.name.getValue().str(), i); + std::string portName = + formatPortName(p.name.getValue().str(), i, bitWidth); if (bitWidth == 1) { portName = p.name.getValue().str(); } @@ -1247,6 +1333,24 @@ void SignalRewriter::rewriteHWModule( } } + // Record the old output idx to new output idxs mapping for non-ready outputs + oldOutputIdxToNewOutputIdxMap[builder.getStringAttr(oldMod.getName())] = + oldOutputIdxToNewOutputIdxs; + + // Add clk and rst for all new inputs of hw modules + newInputs.push_back({hw::ModulePort{mlir::StringAttr::get(ctx, clockSignal), + builder.getIntegerType(1), + hw::ModulePort::Direction::Input}, + inputIdx}); + unsigned clkInputIdx = inputIdx; + inputIdx++; + newInputs.push_back({hw::ModulePort{mlir::StringAttr::get(ctx, resetSignal), + builder.getIntegerType(1), + hw::ModulePort::Direction::Input}, + inputIdx}); + unsigned rstInputIdx = inputIdx; + inputIdx++; + hw::ModulePortInfo newPortInfo(newInputs, newOutputs); // Create new hw module @@ -1277,6 +1381,18 @@ void SignalRewriter::rewriteHWModule( oldModuleSignalToNewModuleSignalsMap[oldSignal] = newReadyInput; } + // If the hw module represent the top function + if (oldMod.getName() == getTopFunctionName()) { + // Save the clock and reset signals + assert((clkSignalTop == nullptr && rstSignalTop == nullptr) && + "reset and clock signals should be set only once for the top " + "function"); + clkSignalTop = newMod.getBodyBlock()->getArgument(clkInputIdx); + rstSignalTop = newMod.getBodyBlock()->getArgument(rstInputIdx); + oldModuleSignalToNewModuleSignalsMap[clkSignalTop] = {clkSignalTop}; + oldModuleSignalToNewModuleSignalsMap[rstSignalTop] = {rstSignalTop}; + } + // Step 3: Iterate through the body operations of the old module bool hasHwInstances = true; // Iterate through old body operations and invert ready signals in instances @@ -1571,6 +1687,444 @@ LogicalResult markHandshakeOpsWithBlifPath(handshake::FuncOp funcOp, return success(); } +// ------------------------------------------------------------------ +// Functions to convert hw instances to synth operations +// ------------------------------------------------------------------ + +// Function to get the input mapping of a signal value +Value getInputMappingSynthSignal(Location loc, + DenseMap &nodeValuesMap, + DenseMap &tmpValuesMap, + StringRef nodeName, OpBuilder &builder) { + assert(builder.getInsertionBlock() && "Builder has no insertion block!"); + // Check if the signal is already in the map + if (nodeValuesMap.count(builder.getStringAttr(nodeName))) { + return nodeValuesMap[builder.getStringAttr(nodeName)]; + } + // Check if the signal is in the tmp values map + if (tmpValuesMap.count(builder.getStringAttr(nodeName))) { + return tmpValuesMap[builder.getStringAttr(nodeName)]; + } + // If not, create a temporary input signal (e.g., hw constant) + auto tempInput = builder.create( + loc, builder.getIntegerType(1), + builder.getIntegerAttr(builder.getIntegerType(1), 0)); + + // Add the temporary input signal to the map + tmpValuesMap[builder.getStringAttr(nodeName)] = tempInput; + return tempInput; +} + +// Function to update the output signal mapping after creating a new synth +// operation +void updateOutputSynthSignalMapping(Value newResult, StringRef nodeName, + DenseMap &nodeValuesMap, + DenseMap &tmpValuesMap, + OpBuilder &builder) { + // Check if the old result is in the tmp values map + if (tmpValuesMap.count(builder.getStringAttr(nodeName))) { + // Replace all uses of the temporary value with the new result value + tmpValuesMap[builder.getStringAttr(nodeName)].replaceAllUsesWith(newResult); + auto *constOp = + tmpValuesMap[builder.getStringAttr(nodeName)].getDefiningOp(); + assert(constOp && "temporary value should have a defining operation"); + assert(constOp->use_empty() && "temporary value should have no more uses"); + // Erase the operation + constOp->erase(); + // Remove the temporary value from the map + tmpValuesMap.erase(builder.getStringAttr(nodeName)); + } + // Add the new result value to the node values map + nodeValuesMap[builder.getStringAttr(nodeName)] = newResult; +} + +// Function to create a wire in function of synth logic gate operations based +// on .names definition +void createSynthWire(Location loc, std::vector &ports, + std::string &function, + DenseMap &nodeValuesMap, + DenseMap &tmpValuesMap, + OpBuilder &builder) { + assert(builder.getInsertionBlock() && "Builder has no insertion block!"); + unsigned numInputs = ports.size() - 1; + assert(numInputs == 1 && "wire should have only one input"); + SmallVector inputValues; + // Get input values + for (unsigned i = 0; i < numInputs; ++i) { + StringRef inputNodeName = ports[i]; + Value inputValue = getInputMappingSynthSignal( + loc, nodeValuesMap, tmpValuesMap, inputNodeName, builder); + inputValues.push_back(inputValue); + } + StringRef outputNodeName = ports.back(); + // Elaborate function + assert(function.size() == 3 && + "function string must be of size 3 for a wire"); + char inputValueFunc = function[0]; + char outputValueFunc = function[2]; + assert((inputValueFunc == '0' || inputValueFunc == '1') && + "input value must be 0 or 1"); + assert((outputValueFunc == '0' || outputValueFunc == '1') && + "output value must be 0 or 1"); + int inputBit = (inputValueFunc == '1') ? 1 : 0; + int outputBit = (outputValueFunc == '1') ? 1 : 0; + Value inputValue = inputValues[0]; + // If the input and output bits are identical, you can skip the creation + // of any node since they are identical + if (inputBit == outputBit) { + // We just need to update the output map function so that the output + // points to the same value as input + updateOutputSynthSignalMapping(inputValue, outputNodeName, nodeValuesMap, + tmpValuesMap, builder); + return; + } + // If this is not the case, we have to create a new aig node which inverts + // the value of the input and receives one as another input Create constant + // node 1 + auto constOp = builder.create( + loc, builder.getIntegerType(1), + builder.getIntegerAttr(builder.getIntegerType(1), 1)); + + // Create aig node + auto aigOp = builder.create( + loc, inputValue, constOp.getResult(), + /*invertInput0=*/true, /*invertInput1=*/false); + + updateOutputSynthSignalMapping(aigOp.getResult(), outputNodeName, + nodeValuesMap, tmpValuesMap, builder); +} + +// Function to create synth logic gate operations based on .names definition +void createSynthLogicGate(Location loc, std::vector &ports, + std::string &function, + DenseMap &nodeValuesMap, + DenseMap &tmpValuesMap, + OpBuilder &builder) { + assert(builder.getInsertionBlock() && "Builder has no insertion block!"); + unsigned numInputs = ports.size() - 1; + assert(numInputs > 1 && "logic gate must have at least two inputs"); + assert(numInputs == 2 && "the following pass transforms the logic gates to " + "aig nodes only"); + SmallVector inputValues; + // Get input values + for (unsigned i = 0; i < numInputs; ++i) { + StringRef inputNodeName = ports[i]; + Value inputValue = getInputMappingSynthSignal( + loc, nodeValuesMap, tmpValuesMap, inputNodeName, builder); + inputValues.push_back(inputValue); + } + StringRef outputNodeName = ports.back(); + + // Elaborate function to understand inversion of inputs/outputs + // Get the value of the first and second input and output + assert(function.size() == 4 && + "function string must be of size 4 for 2-input logic gate"); + char firstInputValue = function[0]; + char secondInputValue = function[1]; + char outputValue = function[3]; + assert((firstInputValue == '0' || firstInputValue == '1') && + "first input value must be 0 or 1"); + assert((secondInputValue == '0' || secondInputValue == '1') && + "second input value must be 0 or 1"); + assert((outputValue == '0' || outputValue == '1') && + "output value must be 0 or 1"); + int firstInputBit = (firstInputValue == '1') ? 1 : 0; + int secondInputBit = (secondInputValue == '1') ? 1 : 0; + int outputBit = (outputValue == '1') ? 1 : 0; + // Invert inputs/outputs if output is 0 + if (outputBit == 0) { + firstInputBit = 1 - firstInputBit; + secondInputBit = 1 - secondInputBit; + } + bool invertInput0 = (firstInputBit == 0) ? true : false; + bool invertInput1 = (secondInputBit == 0) ? true : false; + + // Create aig node + auto aigOp = builder.create( + loc, inputValues[0], inputValues[1], + /*invertInput0=*/invertInput0, /*invertInput1=*/invertInput1); + + // Update output mapping + updateOutputSynthSignalMapping(aigOp.getResult(), outputNodeName, + nodeValuesMap, tmpValuesMap, builder); +} + +// Function to read a blif file and generate the synth circuit depending on the +// blif description +LogicalResult generateSynthCircuitFromBlif(StringRef blifFilePath, Location loc, + hw::InstanceOp hwInst, + hw::HWModuleOp hwModule, + OpBuilder &builder) { + builder.setInsertionPoint(hwInst); + // The following function reads the blif file specified by blifFilePath + // and generates the synth circuit defined in it. + + // Map to store the values of the nodes + DenseMap nodeValuesMap; + // Map to store temporary values for nodes not yet defined + DenseMap tmpValuesMap; + // Collect hw instance inputs and the corresponding naming in the hw module + DenseMap hwInstInputs; + + // Check that the instance is fully connected + assert(hwInst.getOperands().size() == hwModule.getNumInputPorts() && + "Cannot use positional operands on partially-connected instance"); + + for (auto &port : hwModule.getPortList()) { + if (port.isInput()) { + // Check the port is not already in the map + assert(!hwInstInputs.count(builder.getStringAttr(port.name.getValue())) && + "port name already exists in the hw instance inputs map"); + // Check bounds of argNum + unsigned argNum = port.argNum; + assert(argNum < hwInst.getNumOperands() && + "Instance operand index out of bounds"); + Value hwInstInput = hwInst.getOperand(argNum); + assert(hwInstInput && "hw instance input value should not be null"); + StringAttr portNameAttr = builder.getStringAttr(port.name.getValue()); + hwInstInputs[portNameAttr] = hwInstInput; + nodeValuesMap[portNameAttr] = hwInstInput; + } + } + + // Collect hw instance outputs + SmallVector hwInstOutputs; + for (auto &port : hwModule.getPortList()) { + if (port.isOutput()) { + hwInstOutputs.push_back(port.name.getValue()); + } + } + + std::ifstream file(blifFilePath.str()); + + if (!file.is_open()) { + llvm::errs() << "The blif file '" << blifFilePath + << "' has not been found or could not be opened." << "\n"; + return failure(); + } + + std::string line; + // Loop over all the lines in .blif file. + while (std::getline(file, line)) { + // If comment or empty line, skip. + if (line.empty() || line.find("#") == 0) { + continue; + } + + // If line ends with '\\', read the next line and append to the current + // line. + while (line.back() == '\\') { + line.pop_back(); + std::string nextLine; + std::getline(file, nextLine); + line += nextLine; + } + + std::istringstream iss(line); + std::string type; + iss >> type; + + // Model name + if (type == ".model") { + std::string moduleName; + iss >> moduleName; + } + + // Input/Output nodes. These are also Dataflow graph channels. + else if ((type == ".inputs") || (type == ".outputs")) { + std::string nodeName; + while (iss >> nodeName) { + if (type == ".inputs") { + // Check if the input node is in the hw instance inputs map + if (!hwInstInputs.count(builder.getStringAttr(nodeName))) { + llvm::errs() << "Input node '" << nodeName + << "' not found in hw instance inputs." << "\n"; + return failure(); + } + } else { + // Check if the output node is in the hw instance outputs + if (std::find(hwInstOutputs.begin(), hwInstOutputs.end(), nodeName) == + hwInstOutputs.end()) { + llvm::errs() << "Output node '" << nodeName + << "' not found in hw instance outputs." << "\n"; + return failure(); + } + } + } + } + + // Latches. + else if (type == ".latch") { + std::string regInput, regOutput; + iss >> regInput >> regOutput; + // Get the input signal for the register + Value inputSignal = getInputMappingSynthSignal( + loc, nodeValuesMap, tmpValuesMap, regInput, builder); + // Create synth register operation + auto regOp = builder.create( + loc, builder.getIntegerType(1), inputSignal); + + // Map the output node to the register output + updateOutputSynthSignalMapping(regOp.getResult(), regOutput, + nodeValuesMap, tmpValuesMap, builder); + + } + + // .names stand for logic gates. + else if (type == ".names") { + std::vector nodeNames; + std::string currentNode; + + // Read node names from current line (e.g., "a b c") + while (iss >> currentNode) { + nodeNames.push_back(currentNode); + } + + // Read logic function from next line (e.g., "11 1") + std::getline(file, line); + std::string function = line; + + if (nodeNames.size() == 1) { + // Create hw constant + auto constOp = builder.create( + loc, builder.getIntegerType(1), + builder.getIntegerAttr(builder.getIntegerType(1), + function == "1" ? 1 : 0)); + + // Map the output node to the constant output + updateOutputSynthSignalMapping(constOp.getResult(), nodeNames[0], + nodeValuesMap, tmpValuesMap, builder); + } else if (nodeNames.size() == 2) { + // Create wire as a function of other existing synth operations + createSynthWire(loc, nodeNames, function, nodeValuesMap, tmpValuesMap, + builder); + } else { + // Create logic gate as a function of other existing synth operations + createSynthLogicGate(loc, nodeNames, function, nodeValuesMap, + tmpValuesMap, builder); + } + } + + // Subcircuits. not used for now. + else if (line.find(".subckt") == 0) { + llvm::errs() << "Subcircuits inside the blif file not supported " << "\n"; + continue; + } + + // Ends the file. + else if (line.find(".end") == 0) { + break; + } + } + + SmallVector newOutputs; + // Map hw instance outputs to the corresponding synth signals + for (const auto &outputNodeName : hwInstOutputs) { + if (!nodeValuesMap.count(builder.getStringAttr(outputNodeName))) { + llvm::errs() << "Output node '" << outputNodeName + << "' not found in synth circuit." << "\n"; + return failure(); + } + Value outputSignal = nodeValuesMap[builder.getStringAttr(outputNodeName)]; + newOutputs.push_back(outputSignal); + } + // Close the file + file.close(); + // Replace hw instance outputs with the corresponding synth signals + for (unsigned i = 0; i < hwInst.getNumResults(); ++i) { + hwInst.getResult(i).replaceAllUsesWith(newOutputs[i]); + } + // Erase the hw instance operation + hwInst.erase(); + return success(); +} + +// Function to replace an instance with synth operations +// It manages the replacement of a single hw instance operation +LogicalResult convertHWInstanceToSynthOps(hw::InstanceOp op, + OpBuilder &builder) { + + builder.setInsertionPoint(op); + // Ensure that the blif path is specified in the hw module of the hw + // instance operation + SymbolTable symTable = SymbolTable(op->getParentOfType()); + hw::HWModuleOp hwModule = symTable.lookup(op.getModuleName()); + if (!hwModule) { + llvm::errs() << "could not find hw module for instance: " << op << "\n"; + return failure(); + } + if (!hwModule->hasAttr(blifPathAttrStr)) { + llvm::errs() << "hw module for instance does not have blif path attribute: " + << hwModule << "\n"; + return failure(); + } + // Get the blif path attribute + mlir::StringAttr blifPathAttr = + hwModule->getAttrOfType(blifPathAttrStr); + StringRef blifFilePath = blifPathAttr.getValue(); + + // Check if blifFilePath is empty + if (blifFilePath == "") { + // If this is the case, there is no blif for this specific module and it + // should be replaced with a subckt operation + // Create the synth subcircuit operation inside the hw module + synth::SubcktOp synthInstOp = builder.create( + op->getLoc(), op.getResultTypes(), op.getOperands(), "synth_subckt"); + // Collect results of the subckt operation + SmallVector synthResults = synthInstOp.getResults(); + // For each output of the hw instance, replace with the corresponding + // output of the synth subckt operation + for (unsigned i = 0; i < op.getNumResults(); ++i) { + op.getResult(i).replaceAllUsesWith(synthResults[i]); + } + // Erase the hw instance operation + op.erase(); + return success(); + } + + // Read the blif path and extract the synth circuit + if (failed(generateSynthCircuitFromBlif(blifFilePath, op.getLoc(), op, + hwModule, builder))) { + return failure(); + } + + return success(); +} + +// Function to convert hw instances into synth operations +LogicalResult convertHWInstancesToSynthOps(mlir::ModuleOp modOp, + StringRef topModuleName, + MLIRContext *ctx) { + // The following function iterates through all the hw instances in the top + // module and convert them into synth operations like registers, + // combinational logic, etc. if possible. The description of the + // implementation is defined in the path specified by the attribute + // blifPathAttrStr on each hw module + + // Get hw module corresponding to the top module + SymbolTable symTable(modOp); + hw::HWModuleOp topHWModule = symTable.lookup(topModuleName); + if (!topHWModule) { + llvm::errs() << "could not find hw module for top module: " << topModuleName + << "\n"; + return failure(); + } + + // Collect all hw instances in the top module + SmallVector hwInstances; + topHWModule.walk([&](hw::InstanceOp op) { hwInstances.push_back(op); }); + OpBuilder builder(ctx); + // Convert each hw instance into synth operations + for (hw::InstanceOp hwInst : hwInstances) { + if (failed(convertHWInstanceToSynthOps(hwInst, builder))) { + llvm::errs() << "Failed to convert hw instance to synth ops: " << hwInst + << "\n"; + return failure(); + } + } + return success(); +} + // ------------------------------------------------------------------ // Main pass definition // ------------------------------------------------------------------ @@ -1585,8 +2139,10 @@ namespace { // to follow the standard handshake protocol where ready signals go in the // opposite direction with respect to data and valid signals. Additionally, // data signals are unbundled into single-bit signals. -// 3) (not implemented yet) Convert synth subckt operations into other synth -// operations like registers, combinational logic, etc. if possible +// 3) Convert hw instances into other synth operations like registers, +// combinational logic, etc. if possible. The description of the +// implementation is defined in the path specified by the attribute +// blifPathAttrStr on each hw module class HandshakeToSynthPass : public dynamatic::impl::HandshakeToSynthBase { public: @@ -1613,6 +2169,8 @@ class HandshakeToSynthPass // If there is no function, nothing to do if (!funcOp) return; + // Save the name of the top module function + StringRef topModuleName = funcOp.getName(); // Step 0: Mark each handshake operation with the path of the blif file // where its definition is located @@ -1630,13 +2188,28 @@ class HandshakeToSynthPass // Create on object of the SignalRewriter class to manage the // inversion SignalRewriter signalRewriter; + // Set the name of the top function + signalRewriter.setTopFunctionName(topModuleName); if (failed(signalRewriter.rewriteAllSignals(modOp))) return signalPassFailure(); - // Step 3: (not implemented yet) Convert synth subckt operations into - // other synth operations like registers, combinational logic, etc. if - // possible - // TODO + // Step 3: Convert hw instances into other synth operations like + // registers, combinational logic, etc. if possible. The description of + // the implementation is defined in the path specified by the attribute + // blifPathAttrStr on each hw module + if (failed(convertHWInstancesToSynthOps(modOp, topModuleName, ctx))) + return signalPassFailure(); + // Remove all hw modules that are different from the top function hw + // module + SmallVector hwModuleToErase; + modOp.walk([&](hw::HWModuleOp op) { + if (op.getName() != topModuleName) { + hwModuleToErase.push_back(op); + } + }); + for (Operation *hwMod : hwModuleToErase) { + hwMod->erase(); + } } }; From 23bc1fae28bc7af31fc15d5a28b2db28e812d53e Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Fri, 16 Jan 2026 16:45:01 +0100 Subject: [PATCH 27/28] Added last section of the documentation --- .../Synth/ConversionHandshakeToSynth.md | 137 +++++++++++++++++- 1 file changed, 133 insertions(+), 4 deletions(-) diff --git a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md index 482fda353..e12c60af8 100644 --- a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md +++ b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md @@ -7,7 +7,8 @@ There are three main sections in this document. 1. Main pass and usage: Overall structure and rationale of the pass. 2. Unbundling conversion: Lowering Handshake channel types to flat HW ports and `synth.subckt`. -3. Signal rewriting: Inverting the direction of ready signals to follow the standard handshake protocol and unbundling multi-bit data signals into multiple single bit signals. +3. Signal rewriting: Inverting the direction of ready signals to follow the standard handshake protocol, unbundling multi-bit data signals into multiple single bit signals and adding reset and clock signal to each module and connecting them to the top function ones. +4. AIG construction: Convert all hw instances into AIG nodes. The pass is called [`HandshakeToSynthPass`](HandshakeToSynth.cpp). @@ -20,7 +21,8 @@ At a high level, the *HandshakeToSynth* pass performs the following transformati - Converts all Handshake-typed values (channels, control, memory) into flat HW-level ports by unbundling them into `{data, valid, ready}` signals. - Lowers each Handshake operation (including the function itself) to an `hw.module`/`hw.instance` plus an internal `synth.subckt` representing its behavior (except from the top handshake function). -- Rewrites all generated HW modules to enforce the standard handshake convention where ready signals flow in the opposite direction from data and valid, and propagates this convention recursively through module instances. +- Rewrites all generated HW modules to enforce the standard handshake convention where ready signals flow in the opposite direction from data and valid, and propagates this convention recursively through module instances. During this step, it also unbundles the multi-bit data signals into multiple single bit signals and adds reset and clock signals to each module. +- Rewrite all generated HW instances into synth operations (AIG nodes mainly) and connect them accordingly. The description of each hw instance is specified in the BLIF library. More information on how to generate this blif library are specified in the doc [BlifGenerator](../Buffering/MapBuf/BlifGenerator.md) The pass operates on: @@ -31,7 +33,7 @@ and produces: - A pure HW/Synth module hierarchy: - The original `handshake.func` is replaced by a top-level `hw.module`. - - Each Handshake operation becomes an `hw.instance` of an `hw.module` that contains a `synth.subckt`. + - Each Handshake operation becomes a set of synth operations. - No remaining values of Handshake types and no remaining Handshake operations or functions. @@ -45,7 +47,7 @@ Its `runDynamaticPass()` method: - Ensures that there is at most one non-external `handshake.func` in the module and that if none is found, the pass is a no-op. - Runs Phase 1 – Unbundling by calling `unbundleAllHandshakeTypes(modOp, ctx)`. - Runs Phase 2 – Signal rewriting by instantiating a `SignalRewriter` and calling `rewriteAllSignals(modOp)`. -- Leaves a future Phase 3 – further refinement of `synth.subckt` into other synth operations (registers, combinational logic, etc.) as a TODO. +- Runs Phase 3 – Rewrite of hw instances into synth operations (registers, combinational logic, etc.). In a typical flow, this pass is run after all Handshake-level optimizations and buffer insertion, and before a dedicated synth backend that will interpret or further lower the generated synth operations. @@ -510,3 +512,130 @@ It then updates the mapping structures: - For each signal whose direction or structure changed (e.g., old ready inputs now mapped to new ready outputs), `updateOutputSignalMapping` is used similarly, but the original value may be an operand instead of a result. After this step, any user that was wired to the old instance (or to its temporary proxies) can be redirected to the final new signals by consulting `oldModuleSignalToNewModuleSignalsMap`. + +## AIG construction + +The final step of the pass refines the HW-level hierarchy into a purely Synth-level representation by replacing each HW instance with a network of Synth operations derived from its BLIF description. Conceptually, this phase interprets each BLIF model as an AIG-style circuit composed of 1-bit registers and AND-with-inverter gates, and re-expresses it directly in the Synth dialect while preserving the instance’s interface. + +### Goals and high-level behavior + +This phase has three main goals: + +- Eliminate HW instances by inlining their behavior as Synth operations, while keeping the top HW module as the structural shell. +- Interpret each BLIF model as a combination of 1-bit latches and AIG-style combinational logic and reconstruct the same structure in the Synth dialect. +- Maintain a one-to-one correspondence between BLIF inputs/outputs and the HW instance’s operands/results, so that external connectivity remains unchanged. + +The top-level driver is: + +- `LogicalResult convertHWInstancesToSynthOps(mlir::ModuleOp modOp, StringRef topModuleName, MLIRContext *ctx)`. + +It: + +- Looks up the top HW module corresponding to `topModuleName` and iterates over all `hw.instance` operations inside it. +- For each instance, calls `convertHWInstanceToSynthOps(instOp, builder)` to build the corresponding Synth circuit. +- After successful conversion of all instances, erases all non-top HW modules, leaving a representation where the top HW module’s body contains only Synth operations (and no nested HW modules). + +If a given HW module has no BLIF path attribute or if conversion is not supported, the phase falls back to replacing the instance with a single `synth.subckt` that preserves its flat interface. + +### Per-instance BLIF-based refinement + +The per-instance conversion is handled by: + +- `LogicalResult convertHWInstanceToSynthOps(hw::InstanceOp instOp, OpBuilder &builder)`. + +Its behavior is as follows: + +- Retrieves the callee `hw::HWModuleOp` via `instOp.getModuleName()` and checks that it carries the `blifPathAttrStr` attribute containing the BLIF file path. +- Parses the BLIF file associated with that module and builds an internal mapping between BLIF node names and Synth `Value`s using two maps: + - `nodeValuesMap` – mapping from BLIF node name to final Synth value. + - `tmpValuesMap` – mapping from BLIF node name to temporary placeholder constants when a node is referenced before being defined. +- Establishes a direct correspondence between BLIF `.inputs` / `.outputs` and HW instance operands/results, so that input node names map to `instOp` operands and output node names determine which Synth values will eventually replace `instOp` results. + +At the end of BLIF parsing, the conversion collects Synth values for all BLIF output nodes that correspond to the instance’s results and replaces `instOp` with these values; the HW instance operation is then erased. + +### Latch construction (`.latch` lines) + +BLIF `.latch` lines describe sequential elements, which the pass lowers to Synth latches: + +- For each `.latch` line, the parser extracts: + - The input node name (register data input). + - The output node name (register output). + - Optional additional fields (e.g., reset values), which are currently either checked or ignored depending on support. +- The helper `getInputMappingSynthSignal(loc, nodeValuesMap, tmpValuesMap, inputName, builder)` is used to obtain the Synth value corresponding to the latch input node: + - If the node was already defined, its value is returned from `nodeValuesMap`. + - If it was only referenced previously, an existing temporary constant is retrieved from `tmpValuesMap`. + - Otherwise, a new 1-bit `hw.constant` zero is created, recorded in `tmpValuesMap`, and returned as a placeholder. +- A `synth::LatchOp` is created with a 1-bit integer type (`i1`) and the chosen input value. +- The result of the latch is registered with the output node name via `updateOutputSynthSignalMapping(latchResult, outputName, nodeValuesMap, tmpValuesMap, builder)`, which: + - Replaces any previous temporary values for that node by the latch’s result. + - Erases the defining ops of those temporaries. + - Records the latch output in `nodeValuesMap` as the final signal for that node. + +This process ensures that every sequential element in the BLIF model is represented by an explicit Synth latch in the reconstructed circuit. + +### Combinational AIG logic (`.names` lines) + +BLIF `.names` lines describe combinational logic using a simple truth-table syntax. The pass distinguishes between three structural cases depending on the number of node names that follow `.names`. + +#### Case 1 – Single-node `.names` (constants) + +If the `.names` line lists only one node name, it represents a constant node: + +- The associated truth table line (e.g., `1` or `0`) determines whether the constant is logical 1 or 0. +- A 1-bit `hw.constant` of the appropriate value is created at the current insertion point. +- `updateOutputSynthSignalMapping` is called to associate this constant with the node name and to remove any temporary values previously created for that node. + +#### Case 2 – Two-node `.names` (wire or inverter) + +If the `.names` line has exactly two node names (one input, one output), the block encodes either a direct wire or an inversion between them, which is implemented via the helper: + +- `LogicalResult createSynthWire(Location loc, ArrayRef ports, StringRef function, DenseMap &nodeValuesMap, DenseMap &tmpValuesMap, OpBuilder &builder)`. + +The behavior is: + +- The single input node is resolved to a Synth value using `getInputMappingSynthSignal`. +- The truth table (`function`) is inspected: + - If it indicates that the output equals the input (no inversion), no new operation is created; `updateOutputSynthSignalMapping` simply records that the output node name maps to the same Synth value as the input. + - If it indicates inversion, the pass creates a `synth::aigAndInverterOp` with: + - The original input as its sole data input. + - An implied AND with logical 1 and an inversion flag that effectively models a NOT gate. +- In both cases, `updateOutputSynthSignalMapping` registers the final output node and replaces any temporaries for that name. + +This guarantees that identity and inversion edges in the BLIF graph are represented explicitly (or by aliasing) in the Synth-level AIG. + +#### Case 3 – Multi-input `.names` (logic gates) + +If the `.names` line has more than two node names, it represents a multi-input combinational gate described by its truth table and is implemented using: + +- `LogicalResult createSynthLogicGate(Location loc, ArrayRef ports, StringRef function, DenseMap &nodeValuesMap, DenseMap &tmpValuesMap, OpBuilder &builder)`. + +This helper: + +- Asserts that there is at least one input and exactly one output node name. +- Resolves all input node names to Synth values using `getInputMappingSynthSignal`. +- Interprets the BLIF truth table as specifying the conditions under which the output is 1, and constructs an AIG network using only `synth::aigAndInverterOp` operations: + - Each product term (row of the BLIF table) is converted into a small tree of AND-with-inverter nodes, where literals may be inverted as required. + - Multiple product terms are combined using additional AND-with-inverter nodes and constant signals to emulate OR behavior in AIG form. +- The final Synth value for the gate output node is registered via `updateOutputSynthSignalMapping`, which also resolves and eliminates any temporary placeholders created earlier for that node. + +By restricting the implementation to `synth::aigAndInverterOp`, the pass ensures that **all combinational logic** is normalized to an AIG representation consisting of AND gates and literal inversion flags only. + +### Temporary signal handling and final wiring + +Throughout BLIF parsing, the helpers `getInputMappingSynthSignal` and `updateOutputSynthSignalMapping` ensure that forward references and late definitions are handled robustly: + +- `getInputMappingSynthSignal`: + - Returns a previously defined Synth value if the node name is in `nodeValuesMap`. + - Returns an existing temporary constant if the node name is in `tmpValuesMap`. + - Otherwise, creates a new 1-bit zero `hw.constant`, records it in `tmpValuesMap`, and returns it as a placeholder. +- `updateOutputSynthSignalMapping`: + - If the node name exists in `tmpValuesMap`, replaces all uses of the temporary constant with the new result value and erases the constant operation. + - Inserts the new result into `nodeValuesMap` as the canonical value for that node. + +After all `.latch` and `.names` sections of the BLIF model have been processed, the converter: + +- Gathers Synth values for all BLIF output node names that correspond to the HW instance’s outputs (using the `hwInstOutputs` mapping built from BLIF `.outputs`). +- Replaces each result of the original `hw.instance` with the corresponding Synth value. +- Erases the `hw.instance` from the IR. + +At this point, the behavior previously encapsulated in the HW instance and its referenced HW module has been fully re-expressed as an inlined AIG of `synth::LatchOp` and `synth::aigAndInverterOp` operations connected directly to the top module’s ports, which completes the last step of the Handshake-to-Synth conversion. From fa4ce3ff2eaf247f742acd81266d7f5f9b3679e4 Mon Sep 17 00:00:00 2001 From: Carmine50 Date: Fri, 16 Jan 2026 16:51:58 +0100 Subject: [PATCH 28/28] Added important information relative to the marking of blif paths for each handshake unit --- .../Synth/ConversionHandshakeToSynth.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md index e12c60af8..e3b628719 100644 --- a/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md +++ b/docs/DeveloperGuide/DynamaticFeaturesAndOptimizations/Synth/ConversionHandshakeToSynth.md @@ -19,6 +19,7 @@ The pass is called [`HandshakeToSynthPass`](HandshakeToSynth.cpp). At a high level, the *HandshakeToSynth* pass performs the following transformations on a module containing a **single non-external** `handshake.func`: +- Mark each handshake operation with the blif path which describes it AIG description. This information is propagated through each step as an attribute (`blif_path`) present in each hw module. This information will be useful in the last step. - Converts all Handshake-typed values (channels, control, memory) into flat HW-level ports by unbundling them into `{data, valid, ready}` signals. - Lowers each Handshake operation (including the function itself) to an `hw.module`/`hw.instance` plus an internal `synth.subckt` representing its behavior (except from the top handshake function). - Rewrites all generated HW modules to enforce the standard handshake convention where ready signals flow in the opposite direction from data and valid, and propagates this convention recursively through module instances. During this step, it also unbundles the multi-bit data signals into multiple single bit signals and adds reset and clock signals to each module. @@ -45,12 +46,15 @@ Its `runDynamaticPass()` method: - Retrieves the `mlir::ModuleOp` and `MLIRContext`. - Ensures that there is at most one non-external `handshake.func` in the module and that if none is found, the pass is a no-op. +- Runs Phase 0 – Mark each handshake unit with the blif path that describes its AIG beaviour using the function `getBlifFilePathForHandshakeOp(op, blifDirPath)` - Runs Phase 1 – Unbundling by calling `unbundleAllHandshakeTypes(modOp, ctx)`. - Runs Phase 2 – Signal rewriting by instantiating a `SignalRewriter` and calling `rewriteAllSignals(modOp)`. - Runs Phase 3 – Rewrite of hw instances into synth operations (registers, combinational logic, etc.). In a typical flow, this pass is run after all Handshake-level optimizations and buffer insertion, and before a dedicated synth backend that will interpret or further lower the generated synth operations. +**IMPORTANT**: For now, the identification of the blif path heavily relies on the values of parameters of each handshake unit. Since there is still no unique database that correlates each handhshake unit with its parameter to instantiate its RTL, these information are hard-coded for each operation type in the `getBlifFilePathForHandshakeOp(op, blifDirPath)` function. + --- ## Unbundling conversion