diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 55cb1b011a..37d8e01cdf 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -51,13 +51,14 @@ jobs: gzip libreadline-dev \ libboost-all-dev pkg-config python3.12 \ python3.12-venv python3.12-dev \ - ghdl verilator + ghdl verilator \ + flex bison tcl-dev - name: Verify Python 3.12 run: python3.12 --version - name: build - run: ./build.sh --release --use-prebuilt-llvm --enable-abc --enable-cbc --force + run: ./build.sh --release --use-prebuilt-llvm --enable-abc --enable-cbc --enable-yosys --force - name: check-dynamatic if: steps.build.outputs.exit_code == 0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 719ab86e7b..75f3283980 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -268,11 +268,46 @@ if(DYNAMATIC_ENABLE_ABC) FetchContent_MakeAvailable(abc) add_compile_definitions(DYNAMATIC_ENABLE_ABC) + add_compile_definitions("DYNAMATIC_ABC_EXECUTABLE=\"${CMAKE_BINARY_DIR}/_deps/abc-build/abc\"") else() message(FATAL_ERROR "ABC integration is not tested on the OS you use!") endif() endif() +#------------------------------------------------------------------------------- +# Yosys setup +#------------------------------------------------------------------------------- + +if(DYNAMATIC_ENABLE_YOSYS) + if (UNIX) + + FetchContent_Declare( + yosys + GIT_REPOSITORY https://github.com/ETHZ-DYNAMO/yosys.git + GIT_TAG v1.0.0 + GIT_SUBMODULES "" + ) + add_custom_target(yosys-submodules + COMMAND git submodule update --init --recursive + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/_deps/yosys-src + ) + + add_custom_target(build-yosys ALL + COMMAND make -j + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/_deps/yosys-src + ) + + add_dependencies(build-yosys yosys-submodules) + + FetchContent_MakeAvailable(yosys) + add_compile_definitions(DYNAMATIC_ENABLE_YOSYS) + add_compile_definitions("DYNAMATIC_YOSYS_EXECUTABLE=\"${CMAKE_BINARY_DIR}/_deps/yosys-src/yosys\"") + else() + message(FATAL_ERROR "Yosys integration is not tested on the OS you use!") + endif() +endif() + + #------------------------------------------------------------------------------- # Python (Virtual Environment) #------------------------------------------------------------------------------- diff --git a/build.sh b/build.sh index f290d4ed67..c043ef7129 100755 --- a/build.sh +++ b/build.sh @@ -31,6 +31,7 @@ List of options: --use-prebuilt-llvm : download and use the prebuilt LLVM --enable-cbc : enable the CBC milp solver --enable-abc : enable the ABC logic synthesis tool + --enable-yosys : enable the Yosys logic synthesis tool --build-legacy-lsq : build the legacy chisel-based lsq --check | -c : run tests during build --help | -h : display this help message @@ -145,6 +146,7 @@ BUILD_CHIESEL_LSQ=0 ENABLE_CBC=0 CMAKE_DYNAMATIC_ENABLE_CBC="" CMAKE_DYNAMATIC_ENABLE_ABC="" +CMAKE_DYNAMATIC_ENABLE_YOSYS="" LLVM_DIR="$PWD/llvm-project/build" # Loop over command line arguments and update script variables @@ -211,6 +213,9 @@ do "--enable-abc") CMAKE_DYNAMATIC_ENABLE_ABC="-DDYNAMATIC_ENABLE_ABC=ON" ;; + "--enable-yosys") + CMAKE_DYNAMATIC_ENABLE_YOSYS="-DDYNAMATIC_ENABLE_YOSYS=ON" + ;; "--build-legacy-lsq") BUILD_CHIESEL_LSQ=1 ;; @@ -365,6 +370,7 @@ if should_run_cmake ; then $CMAKE_DYNAMATIC_ENABLE_XLS \ $CMAKE_DYNAMATIC_ENABLE_CBC \ $CMAKE_DYNAMATIC_ENABLE_ABC \ + $CMAKE_DYNAMATIC_ENABLE_YOSYS \ $CMAKE_DYNAMATIC_ENABLE_LEQ_BINARIES LLVM_DIR="../build/llvm-project" @@ -382,6 +388,7 @@ if should_run_cmake ; then $CMAKE_DYNAMATIC_ENABLE_XLS \ $CMAKE_DYNAMATIC_ENABLE_CBC \ $CMAKE_DYNAMATIC_ENABLE_ABC \ + $CMAKE_DYNAMATIC_ENABLE_YOSYS \ $CMAKE_DYNAMATIC_ENABLE_LEQ_BINARIES fi exit_on_fail "Failed to cmake dynamatic" diff --git a/docs/DeveloperGuide/DesignDecisionProposals/BackendGenerator.md b/docs/DeveloperGuide/DesignDecisionProposals/BackendGenerator.md new file mode 100644 index 0000000000..7fbab0e49f --- /dev/null +++ b/docs/DeveloperGuide/DesignDecisionProposals/BackendGenerator.md @@ -0,0 +1,147 @@ +# BackendGenerator + +## Overview + +`BackendGenerator` is a C++ class responsible for generating backend representations of Handshake operations. It currently supports two output formats: + +- **Verilog** +- **BLIF** + +The high-level overview of the code is the following: +1. It queryies operation parameters through the Handshake RTL interface. +2. It selects the output directory depending on the operation. +3. It calls the generator for each backend. +4. It records the produced outputs. + +--- + +## Supported Backends + +### Verilog Backend + +- Generates RTL using a generator command defined in the JSON config. +- Produces one or more `.v` files. +- No synthesis or optimization is performed. + +### BLIF Backend + +- Calls on the Verilog backend. +- Synthesizes the generated Verilog using **Yosys**. +- Optimizes the result using **ABC**. +- Produces a final `.blif` file. + +--- + +## Inputs + +`BackendGenerator` requires the following inputs: + +### Operation + +- An `mlir::Operation*` implementing `handshake::RTLAttrInterface` +- The operation must provide its parameters via `getRTLParameters()` + +### Backend Selection + +- A `Backend` enum specifying the target format: + - `Backend::Verilog` + - `Backend::BLIF` + +### Backend Parameters + +- `rtlConfigPath`: Path to the JSON RTL configuration file +- `dynamaticRoot`: Root path of the Dynamatic repository (used in substitutions) +- `outputBaseDir`: Base directory for generated files + + +--- + +## Generation Flow + +### Preparation Steps + +For all backends: + +1. **Parameter Extraction** + Parameters are retrieved from the operation via + `handshake::RTLAttrInterface::getRTLParameters()`. + +2. **Output Directory Construction** + The output directory structure is the following: + `////...` where `baseDir` is the base directory specified as input to the backend generator, `operation` is the name of the operation, `param1`,`param2` and so on are the values of the parameter for the operation. + +3. **JSON Config Lookup** +The file specified by `rtlConfigPath` is parsed to locate a matching entry: +- Match is based on `"name"` == operation name +- Optional `"parameters"` constraints must also match + +4. **Generator Command Execution** +The `"generator"` field is expanded using parameter substitution: +- `$PARAM_NAME` -> actual value +- Special variables that are set by the backend generator code are the following: + - `MODULE_NAME` + - `OUTPUT_DIR` + - `DYNAMATIC` + - `BITWIDTH` + - `EXTRA_SIGNALS` + +--- + +### Verilog Backend Pipeline + +1. Execute the generator command. +2. Expect output: + `/.v` +3. Collect: +- Generated Verilog file +- Additional dependency files from JSON (`generic` entries) + +--- + +### BLIF Backend Pipeline + +The BLIF backend reuses the Verilog backend, then applies synthesis: + +1. **Verilog Generation** +Internally invokes the Verilog backend. + +2. **Yosys Synthesis** +A script `run_yosys.sh` is generated and executed. It performs: +- Verilog parsing +- Hierarchy resolution +- Lowering and optimization +- BLIF emission + +3. **ABC Optimization** +A script `run_abc.sh` is generated and executed. It applies: +``` +strash +6x (rewrite -> balance -> refactor -> balance) +``` + +For the BLIF backend, the directory additionally contains: +- Intermediate BLIF file from Yosys +- Final optimized BLIF file +- Generated Yosys and ABC scripts + +--- + +## Parameter Handling + +Parameters are extracted from the operation using the MLIR function `handshake::RTLAttrInterface::getRTLParameters()`. and stored in a map: +`std::map` + +--- + +## External Dependencies + +### Verilog Backend +- No external tools required beyond the generator command. + +### BLIF Backend +- **Yosys** +- **ABC** + +Both these tools are automatically installed when using the flags `--enable-abc` and `--enable-yosys` when building Dynamatic with the `build.sh` script. If there is a specific choice of the versions of both tools, please include the binaries of both tools in the `PATH` variable. + +Paths to these tools must be provided through the backend parameters. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index cc8f3f056e..bd098c57d8 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -52,6 +52,7 @@ - [Design Decision Proposals]() - [Add/Remove/Promote Extra Signals](DeveloperGuide/DesignDecisionProposals/AddRemovePromoteExtraSignals.md) + - [Backend Generator](DeveloperGuide/DesignDecisionProposals/BackendGenerator.md) - [Circuit Interface](DeveloperGuide/DesignDecisionProposals/CircuitInterface.md) - [Type System](DeveloperGuide/DesignDecisionProposals/TypeSystem.md) - [Wait Synchronization](DeveloperGuide/DesignDecisionProposals/WaitSynchronization.md) diff --git a/include/dynamatic/Support/BLIFFileManager.h b/include/dynamatic/Support/BLIFFileManager.h index 30c2f28880..f215651be3 100644 --- a/include/dynamatic/Support/BLIFFileManager.h +++ b/include/dynamatic/Support/BLIFFileManager.h @@ -1,5 +1,4 @@ -//===- BLIFFileManager.h - Support functions for BLIF importer -*- C++ -//-*-===// +//===- BLIFFileManager.h - Support functions for BLIF importer -*- C++ -*-===// // // Dynamatic is under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. @@ -7,42 +6,45 @@ // //===----------------------------------------------------------------------===// // -// This file contains the function to retrieve the path of the blif file for a -// given handshake operation. The path is created by combining the base path -// contining all the blif files and the operation name and parameters. The file -// is expected to be named in the format ___..._.blif. -// If the file does not exist, an error is emitted. +// Retrieves the BLIF file path for a given Handshake operation by combining +// the base BLIF directory, the operation name, and its RTL parameter values. +// If the file does not exist and Yosys+ABC are configured, it is generated +// on demand via BackendGenerator. // //===----------------------------------------------------------------------===// -#include "dynamatic/Analysis/NameAnalysis.h" -#include "dynamatic/Dialect/Handshake/HandshakeInterfaces.h" -#include "dynamatic/Dialect/Handshake/HandshakeOps.h" +#ifndef DYNAMATIC_SUPPORT_BLIFFILEMANAGER_H +#define DYNAMATIC_SUPPORT_BLIFFILEMANAGER_H + #include "dynamatic/Support/LLVM.h" -#include "llvm/ADT/TypeSwitch.h" -#include -#include +#include "llvm/ADT/SmallVector.h" +#include +#include + +namespace mlir { +class Operation; +} // namespace mlir namespace dynamatic { class BLIFFileManager { public: - // Constructor for the BLIFFileManager class - BLIFFileManager(std::string blifDirPath) : blifDirPath(blifDirPath) {} + BLIFFileManager(std::string blifDirPath, std::string dynamaticRootPath, + std::string rtlJsonFile) + : blifDirPath(std::move(blifDirPath)), + dynamaticRootPath(std::move(dynamaticRootPath)), + rtlJsonFile(std::move(rtlJsonFile)) {} - // Function to combine parameter values, module type and blif directory path - // to create the blif file path - std::string combineBlifFilePath(std::string moduleType, - std::vector paramValues, - std::string extraSuffix = ""); + std::string combineBlifFilePath(const std::string &moduleType, + const std::vector ¶mValues, + const std::string &extraSuffix = ""); - // Function to retrieve the path of the blif file for a given handshake - // operation std::string getBlifFilePathForHandshakeOp(mlir::Operation *op); private: - // String containing the base path of the blif files std::string blifDirPath; + std::string dynamaticRootPath; + std::string rtlJsonFile; }; // Formats a bit-indexed port name: "sig[bit]". @@ -59,15 +61,17 @@ std::string legalizeControlPortName(const std::string &name, // Converts a (root, index) pair into the canonical "root[index]" form. // If root already contains "[N]", the old index is linearised with -// arrayWidth before adding index. +// arrayWidth before adding index. // Example: formatArrayName("data[2]", 3, 4) becomes "data[11]" (2*4 + 3) std::string formatArrayName(const std::string &root, unsigned index, unsigned arrayWidth = 0); // Legalizes a list of handshake port names by converting the "root_N" index // pattern (used internally by NamedIOInterface) into the "root[N]" array -// notation expected by the BLIF importer +// notation expected by the BLIF importer. // Example: ["data_0", "data_1", "valid"] -> ["data[0]", "data[1]", "valid"] -void legalizeBlifPortNames(mlir::SmallVector &names); +void legalizeBlifPortNames(llvm::SmallVector &names); + +} // namespace dynamatic -} // namespace dynamatic \ No newline at end of file +#endif // DYNAMATIC_SUPPORT_BLIFFILEMANAGER_H diff --git a/include/dynamatic/Support/BackendGenerator.h b/include/dynamatic/Support/BackendGenerator.h new file mode 100644 index 0000000000..16441c9637 --- /dev/null +++ b/include/dynamatic/Support/BackendGenerator.h @@ -0,0 +1,85 @@ +//===- BackendGenerator.h - RTL and BLIF backend generator ------*- 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 +// +//===----------------------------------------------------------------------===// +// +// Generates RTL (Verilog) or BLIF output for a given Handshake operation. +// Parameters are extracted from the operation via RTLAttrInterface and matched +// against a JSON config to locate and execute the appropriate generator +// command. +// +// For the Verilog backend the generator command produces .v files directly. +// For the BLIF backend the generator first produces Verilog, then synthesizes +// it to BLIF via Yosys and optimizes the result with ABC. +// +//===----------------------------------------------------------------------===// + +#ifndef DYNAMATIC_SUPPORT_BACKENDGENERATOR_H +#define DYNAMATIC_SUPPORT_BACKENDGENERATOR_H + +#include "mlir/IR/Operation.h" +#include +#include +#include +#include + +namespace dynamatic { + +class BackendGenerator { +public: + enum class Backend { Verilog, BLIF }; + + /// Parameters for Backend generation. + struct BackendParams { + std::string rtlConfigPath; /// Absolute path to the JSON RTL config. + std::string dynamaticRoot; /// Project root for $DYNAMATIC substitution. + std::string outputBaseDir; /// Base directory for per-module output. + }; + + BackendGenerator(Backend backend, BackendParams params); + + /// Run Verilog generation for op and, for the BLIF backend, Yosys+ABC + /// synthesis. On success, results are available via the getters below. + bool generate(mlir::Operation *op); + + const std::string &getModuleName() const { return moduleName; } + /// Verilog backend: returns the generated .v files. + /// BLIF backend: returns a single-element vector with the .blif file path. + const std::vector &getOutputFiles() const { return outputFiles; } + +private: + Backend backend; /// The target backend to generate for (Verilog or BLIF). + BackendParams params; /// Parameters for generation. + + std::string moduleName; /// The name of the generated module, derived from the + /// operation name. + std::vector + outputFiles; /// Paths of the generated files (.v for Verilog backend, + /// .blif for BLIF backend). + + /// Get the directory where the backend generator will place output files for + /// operation op. + std::filesystem::path getOutputDirForOp(mlir::Operation *op, + const std::string &baseDir); + + /// Verilog backend: generate RTL and store .v files in outputFiles. + bool generateVerilog(mlir::Operation *op); + + /// BLIF backend: generate RTL then run Yosys+ABC, store .blif in outputFiles. + bool generateBLIF(mlir::Operation *op); + + /// Run Yosys to synthesize the generated Verilog files into BLIF. + bool runYosys(const std::string &topModule, const std::string &outputDir, + const std::vector &verilogFiles, + const std::string &outputName) const; + /// Run ABC to optimize the BLIF file generated by Yosys. + bool runAbc(const std::string &inputFile, const std::string &outputDir, + const std::string &outputName) const; +}; + +} // namespace dynamatic + +#endif // DYNAMATIC_SUPPORT_BACKENDGENERATOR_H diff --git a/include/dynamatic/Transforms/Passes.td b/include/dynamatic/Transforms/Passes.td index c70840c65a..e7ff9fcd0e 100644 --- a/include/dynamatic/Transforms/Passes.td +++ b/include/dynamatic/Transforms/Passes.td @@ -142,7 +142,7 @@ def HandshakeHoistExtInstances : DynamaticPass<"handshake-hoist-ext-instances"> Replaces instances (`handshake::InstanceOp`) of external Handshake functions by extra arguments/results in their containing (internal) Handshake functions. This is the equivalent of moving RTL modules from inside the - Handshake function to outside of it. + Handshake function to outside of it. }]; } @@ -350,7 +350,13 @@ def HandshakeMarkBLIFImpl : Pass<"handshake-mark-blif-impl"> { let options = [ Option<"blifDirPath", "blif-dir-path", "std::string", "", "Path to directory containing BLIF files for AIG implementations of " - "Handshake operations."> + "Handshake operations.">, + Option<"dynamaticRoot", "dynamatic-root", "std::string", "", + "Path to the root directory of the Dynamatic repository. This is used " + "in the RTL generation process if needed.">, + Option<"RTLJSONFile", "rtl-json-file", "std::string", "", + "Path to JSON-formatted RTL information file. This is used in the RTL " + "generation process if needed."> ]; } diff --git a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp index 96798bc987..9e9a378df8 100644 --- a/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp +++ b/lib/Conversion/HandshakeToSynth/HandshakeToSynth.cpp @@ -26,6 +26,7 @@ #include "dynamatic/Conversion/Passes.h" #include "dynamatic/Dialect/HW/HWOps.h" #include "dynamatic/Dialect/Synth/SynthOps.h" +#include "llvm/Support/raw_ostream.h" namespace dynamatic { #define GEN_PASS_DEF_HANDSHAKETOSYNTH #include "dynamatic/Conversion/Passes.h.inc" @@ -52,6 +53,7 @@ namespace dynamatic { #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" +#include "llvm/ADT/StringSet.h" #include "llvm/Support/ErrorHandling.h" #include #include @@ -795,10 +797,22 @@ LogicalResult HandshakeUnbundler::convertHandshakeFunc() { return failure(); } - // Collect the unbundled operands for the terminator + // Collect the unbundled operands for the terminator. + // Order must match the output port order declared by unbundlePorts for a + // funcOp: first the ready signals for every input argument, then the + // data+valid signals for every result (EndOp operand). SmallVector hwTermOperands; + + // 1. Ready signals for every function input argument. + for (auto [i, arg] : llvm::enumerate(topFunction.getArguments())) { + ReadyPortInfo portInfo{"", hw::ModulePort::Direction::Input, arg}; + auto unbundledValues = + findOrCreateUnbundledValues(1, &portInfo, endOp.getLoc()); + hwTermOperands.append(unbundledValues.begin(), unbundledValues.end()); + } + + // 2. Data + valid signals for every EndOp operand (function results). for (Value operand : endOp.getOperands()) { - // Get the data bitwidth of the operand unsigned dataBitwidth = 0; if (auto channelType = operand.getType().dyn_cast()) dataBitwidth = channelType.getDataType().cast().getWidth(); @@ -807,31 +821,20 @@ LogicalResult HandshakeUnbundler::convertHandshakeFunc() { else if (auto intType = operand.getType().dyn_cast()) dataBitwidth = intType.getWidth(); else - dataBitwidth = - 0; // For control types and other types, we treat them as 0 bits + dataBitwidth = 0; SmallVector unbundledValues; if (dataBitwidth > 0) { - // Add data signals DataPortInfo portInfo{"", hw::ModulePort::Direction::Output, operand, 0, dataBitwidth}; unbundledValues = findOrCreateUnbundledValues(dataBitwidth, &portInfo, endOp.getLoc()); hwTermOperands.append(unbundledValues.begin(), unbundledValues.end()); } - // Add valid signals ValidPortInfo validPortInfo{"", hw::ModulePort::Direction::Output, operand}; unbundledValues = findOrCreateUnbundledValues(1, &validPortInfo, endOp.getLoc()); hwTermOperands.append(unbundledValues.begin(), unbundledValues.end()); } - // Check also the inputs of the topFuncOp whose ready signals are connected - // to the terminator - for (auto [i, arg] : llvm::enumerate(topFunction.getArguments())) { - ReadyPortInfo portInfo{"", hw::ModulePort::Direction::Input, arg}; - auto unbundledValues = - findOrCreateUnbundledValues(1, &portInfo, endOp.getLoc()); - hwTermOperands.append(unbundledValues.begin(), unbundledValues.end()); - } topHWModule.getBodyBlock()->getTerminator()->setOperands(hwTermOperands); // Check there are no more placeholders left, which would indicate some @@ -856,9 +859,9 @@ LogicalResult HandshakeUnbundler::convertHandshakeFunc() { // Function to populate a single hw module by importing the corresponding BLIF // netlist and swapping the module body. -mlir::LogicalResult -populateHWModule(mlir::ModuleOp modOp, hw::InstanceOp inst, - llvm::DenseSet &populatedModules) { +mlir::LogicalResult populateHWModule(mlir::ModuleOp modOp, hw::InstanceOp inst, + llvm::StringSet<> &populatedModules) { + mlir::SymbolTable symTable(modOp); // Look up the hw module definition referenced by this instance. hw::HWModuleOp hwModule = @@ -935,7 +938,7 @@ mlir::LogicalResult populateAllHWModules(mlir::ModuleOp modOp, topMod.walk([&](hw::InstanceOp inst) { instances.push_back(inst); }); // Collect all already populated modules to avoid duplicate work - llvm::DenseSet populatedModules; + llvm::StringSet<> populatedModules; for (hw::InstanceOp inst : instances) if (mlir::failed(populateHWModule(modOp, inst, populatedModules))) return mlir::failure(); @@ -1006,9 +1009,14 @@ class HandshakeToSynthPass } std::string blifFilePath = blifImplInterface.getBLIFImpl().str(); if (blifFilePath.empty()) { + // If the operation is an EndOp, it is expected to not have a BLIF file + // path since it will be converted to the terminator of the top module, + // so we can skip the warning in this case + if (isa(op)) + return; // Write out warning - llvm::errs() << "Warning: Handshake operation " << getUniqueName(op) - << " has an empty BLIF file path\n"; + llvm ::errs() << "Warning: Handshake operation " << getUniqueName(op) + << " has an empty BLIF file path\n"; } else { hasBlifImpl = true; } diff --git a/lib/Support/BLIFFileManager.cpp b/lib/Support/BLIFFileManager.cpp index 4bbfedef04..61672ddc91 100644 --- a/lib/Support/BLIFFileManager.cpp +++ b/lib/Support/BLIFFileManager.cpp @@ -9,13 +9,22 @@ // // This file contains the function to retrieve the path of the blif file for a // given handshake operation. The path is created by combining the base path -// contining all the blif files and the operation name and parameters. The file -// is expected to be named in the format ___..._.blif. -// If the file does not exist, an error is emitted. +// containing all the blif files, the operation name, and the parameter values +// returned by getRTLParameters() via the RTLAttrInterface. +// If the file does not exist, an attempt is made to generate it using +// BLIFGenerator (requires Yosys and ABC to be configured at build time). // //===----------------------------------------------------------------------===// #include "dynamatic/Support/BLIFFileManager.h" +#include "dynamatic/Analysis/NameAnalysis.h" +#include "dynamatic/Dialect/Handshake/HandshakeInterfaces.h" +#include "dynamatic/Dialect/Handshake/HandshakeOps.h" +#include "dynamatic/Support/BackendGenerator.h" +#include "mlir/IR/Operation.h" +#include "llvm/Support/raw_ostream.h" +#include +#include using namespace mlir; using namespace dynamatic; @@ -23,12 +32,24 @@ using namespace dynamatic::handshake; namespace dynamatic { +// Converts a single NamedAttribute value from getRTLParameters() to a string. +// TypeAttr(HandshakeType) -> data bitwidth; IntegerAttr -> integer; StringAttr +// -> string. +static std::string rtlParamToString(mlir::Attribute attr) { + if (auto ta = mlir::dyn_cast(attr)) + return std::to_string(handshake::getHandshakeTypeBitWidth(ta.getValue())); + if (auto ia = mlir::dyn_cast(attr)) + return std::to_string(ia.getValue().getZExtValue()); + if (auto sa = mlir::dyn_cast(attr)) + return sa.str(); + return ""; +} + // Function to combine parameter values, module type and blif directory path // to create the blif file path -std::string -BLIFFileManager::combineBlifFilePath(std::string moduleType, - std::vector paramValues, - std::string extraSuffix) { +std::string BLIFFileManager::combineBlifFilePath( + const std::string &moduleType, const std::vector ¶mValues, + const std::string &extraSuffix) { std::string blifFilePath = blifDirPath + "/" + moduleType + extraSuffix; for (const auto ¶mValue : paramValues) { blifFilePath += "/" + paramValue; @@ -40,142 +61,43 @@ BLIFFileManager::combineBlifFilePath(std::string moduleType, // Function to retrieve the path of the blif file for a given handshake // operation std::string BLIFFileManager::getBlifFilePathForHandshakeOp(Operation *op) { - // 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 + // EndOp has no hardware implementation. + if (mlir::isa(op)) + return ""; + + // Get op name, strip dialect prefix (e.g. "handshake.addi" -> "addi"). 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(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(moduleType, {std::to_string(dataWidth)}); - }) - .Case([&](auto) { - unsigned dataWidth = - handshake::getHandshakeTypeBitWidth(op->getOperand(0).getType()); - if (dataWidth == 0) { - blifFileName = combineBlifFilePath(moduleType, {}, "_dataless"); - } else { - blifFileName = - combineBlifFilePath(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(bufferType, {}, "_dataless"); - } else { - blifFileName = - combineBlifFilePath(bufferType, {std::to_string(dataWidth)}); - } - }) - .Case( - [&](handshake::ConditionalBranchOp cbrOp) { - unsigned dataWidth = handshake::getHandshakeTypeBitWidth( - cbrOp.getDataOperand().getType()); - if (dataWidth == 0) { - blifFileName = combineBlifFilePath(moduleType, {}, "_dataless"); - } else { - blifFileName = - combineBlifFilePath(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( - 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(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(moduleType, {std::to_string(size)}, - "_dataless"); - } else { - blifFileName = combineBlifFilePath( - 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( - 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( - 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( - moduleType, {std::to_string(dataWidth), std::to_string(addrType)}); - }) - .Case( - [&](auto) { blifFileName = combineBlifFilePath(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 - << ". Check the file path specified for this operation is " - "correct. If it is not present, you should generate it " - "using the blif script generator.\n"; - assert(false && "Check the file path specified for this operation is " - "built correctly"); + + // Retrieve all RTL parameters from the op via the interface. + auto iface = mlir::cast(op); + auto paramsOrErr = iface.getRTLParameters(); + if (mlir::failed(paramsOrErr)) { + llvm::errs() << "BLIFFileManager: getRTLParameters() failed for `" + << moduleType << "`\n"; + return ""; + } + + // Convert all parameter values to strings for path construction. + std::vector paramStrs; + paramStrs.reserve(paramsOrErr->size()); + for (const auto &p : *paramsOrErr) + paramStrs.push_back(rtlParamToString(p.getValue())); + + std::string blifFileName = combineBlifFilePath(moduleType, paramStrs); + + // Check if the file exists; if not, attempt to generate it. + if (!std::filesystem::exists(blifFileName)) { + llvm::errs() << "BLIF file missing, generating: " << blifFileName << "\n"; + BackendGenerator gen(BackendGenerator::Backend::BLIF, + BackendGenerator::BackendParams{ + rtlJsonFile, dynamaticRootPath, blifDirPath}); + bool ok = gen.generate(op); + if (!ok || !std::filesystem::exists(blifFileName)) { + llvm::errs() << "Failed to generate BLIF file for operation `" + << getUniqueName(op) << "`: " << blifFileName + << ". Ensure that Yosys and ABC are correctly configured.\n"; + assert(false && "BLIF generation failed"); } } return blifFileName; @@ -188,7 +110,6 @@ std::string legalizeDataPortName(StringRef baseName, unsigned bit, unsigned width) { if (width == 1) return baseName.str(); - // Reuse the formatArrayName() helper from Step 1 / Layer 2. return formatArrayName(baseName.str(), bit, width); } @@ -243,8 +164,6 @@ void legalizeBlifPortNames(SmallVector &names) { result.push_back(root); } else { if (idx == 1) { - // Back-patch the root entry (index 0) to have the "[0]" suffix, - // since it was originally stored without an index. auto *it = std::find(result.begin(), result.end(), root); assert(it != result.end() && "index-0 port not found for back-patch"); *it = formatArrayName(root, 0); @@ -255,4 +174,4 @@ void legalizeBlifPortNames(SmallVector &names) { names = std::move(result); } -} // namespace dynamatic \ No newline at end of file +} // namespace dynamatic diff --git a/lib/Support/BackendGenerator.cpp b/lib/Support/BackendGenerator.cpp new file mode 100644 index 0000000000..233c63a84e --- /dev/null +++ b/lib/Support/BackendGenerator.cpp @@ -0,0 +1,471 @@ +//===- BackendGenerator.cpp - RTL and BLIF backend generator ----*- 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 +// +//===----------------------------------------------------------------------===// +// +// Generates RTL (Verilog) or BLIF output for a given Handshake operation by +// looking up its entry in a JSON RTL config, substituting parameters, and +// running the generator command. For the BLIF backend, Yosys synthesis and +// ABC optimization are also performed. +// +//===----------------------------------------------------------------------===// + +#include "dynamatic/Support/BackendGenerator.h" +#include "dynamatic/Dialect/Handshake/HandshakeInterfaces.h" +#include "dynamatic/Dialect/Handshake/HandshakeTypes.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/Operation.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/raw_ostream.h" +#include +#include +#include +#include + +using namespace dynamatic; + +// Helper functions +namespace { + +// Extracts a string value from a JSON object for a given key, or returns +// std::nullopt if the key is not found or the value is not a string. +static std::optional jsonGetString(const llvm::json::Object &obj, + llvm::StringRef key) { + const llvm::json::Value *v = obj.get(key); + if (!v) + return std::nullopt; + auto s = v->getAsString(); + if (!s) + return std::nullopt; + return s->str(); +} + +// Extracts an array value from a JSON object for a given key, or returns +// nullptr if the key is not found or the value is not an array. +static const llvm::json::Array *jsonGetArray(const llvm::json::Object &obj, + llvm::StringRef key) { + const llvm::json::Value *v = obj.get(key); + if (!v) + return nullptr; + return v->getAsArray(); +} + +// Converts a single NamedAttribute value from getRTLParameters() to a string. +static std::string rtlParamToString(mlir::Attribute attr) { + if (auto ta = mlir::dyn_cast(attr)) + return std::to_string( + dynamatic::handshake::getHandshakeTypeBitWidth(ta.getValue())); + if (auto ia = mlir::dyn_cast(attr)) + return std::to_string(ia.getValue().getZExtValue()); + if (auto sa = mlir::dyn_cast(attr)) + return sa.str(); + return ""; +} + +// Replaces all occurrences of `from` in `str` with `to`. +static void replaceAll(std::string &str, const std::string &from, + const std::string &to) { + size_t pos = 0; + while ((pos = str.find(from, pos)) != std::string::npos) { + str.replace(pos, from.size(), to); + pos += to.size(); + } +} + +// Checks if a JSON config entry matches the given parameter map. An entry +// matches if all parameters specified in the entry are present in the map with +// the same value. +static bool +entryMatchesParams(const llvm::json::Object &entry, + const std::map ¶mMap) { + const auto *params = jsonGetArray(entry, "parameters"); + if (!params) + return true; + for (const auto &pval : *params) { + const auto *pobj = pval.getAsObject(); + if (!pobj) + continue; + auto nameOpt = jsonGetString(*pobj, "name"); + auto eqOpt = jsonGetString(*pobj, "eq"); + if (!nameOpt || !eqOpt) + continue; + auto it = paramMap.find(*nameOpt); + if (it == paramMap.end() || it->second != *eqOpt) + return false; + } + return true; +} + +// Function to extract the parameters of an operation into a map. +bool getParamMap(mlir::Operation *op, + std::map ¶mMap) { + // Retrieve all RTL parameters from the op via the interface, and construct + // the module name and output directory path based on the operation name and + // parameter values. + auto iface = mlir::cast(op); + auto paramsOrErr = iface.getRTLParameters(); + if (mlir::failed(paramsOrErr)) { + llvm::errs() << "BackendGenerator: getRTLParameters() failed for `" + << op->getName().getStringRef() << "`\n"; + return false; + } + + paramMap.clear(); + for (const auto &p : *paramsOrErr) { + std::string name = p.getName().str(); + std::string value = rtlParamToString(p.getValue()); + paramMap[name] = value; + } + return true; +} + +} // namespace + +BackendGenerator::BackendGenerator(Backend backend, BackendParams params) + : backend(backend), params(std::move(params)) {} + +// Main entry point for generating backend output for a given operation. +bool BackendGenerator::generate(mlir::Operation *op) { + switch (backend) { + case Backend::Verilog: + return generateVerilog(op); + case Backend::BLIF: + return generateBLIF(op); + } + assert(false && "Only Verilog and BLIF backends are supported for now"); + return false; +} + +// Function to get the directory path of backend files for a given operation. +std::filesystem::path +BackendGenerator::getOutputDirForOp(mlir::Operation *op, + const std::string &baseDir) { + std::string opName = op->getName().getStringRef().str(); + std::string opShortName = opName.substr(opName.find('.') + 1); + + auto iface = mlir::cast(op); + auto paramsOrErr = iface.getRTLParameters(); + if (mlir::failed(paramsOrErr)) { + llvm::errs() << "BackendGenerator: getRTLParameters() failed for `" + << opName << "`\n"; + return ""; + } + + std::string dir = std::filesystem::path(baseDir) / opShortName; + for (const auto &p : *paramsOrErr) + dir = std::filesystem::path(dir) / rtlParamToString(p.getValue()); + return dir; +} + +// Function to generate a BLIF file for a given operation by running the RTL +// generator, Yosys and ABC. +bool BackendGenerator::generateBLIF(mlir::Operation *op) { + namespace fs = std::filesystem; + + const std::string &rtlConfigPath = params.rtlConfigPath; + const std::string &dynamaticRoot = params.dynamaticRoot; + const std::string &outputBaseDir = params.outputBaseDir; + + // Assert parameters are not empty or NULL + assert(!rtlConfigPath.empty() && "rtlConfigPath cannot be empty"); + assert(!dynamaticRoot.empty() && "dynamaticRoot cannot be empty"); + assert(!outputBaseDir.empty() && "outputBaseDir cannot be empty"); + + BackendGenerator verilogGen(Backend::Verilog, params); + if (!verilogGen.generate(op)) { + llvm::errs() << "BackendGenerator: Verilog generation failed for `" + << op->getName().getStringRef() << "`\n"; + return false; + } + std::vector verilogFiles = verilogGen.getOutputFiles(); + if (verilogFiles.empty()) { + llvm::errs() << "BackendGenerator: no Verilog files generated for `" + << op->getName().getStringRef() << "`\n"; + return false; + } + // Get module name + moduleName = verilogGen.getModuleName(); + assert(!moduleName.empty() && "Module name cannot be empty"); + + // Construct the expected BLIF file path based on the operation name and + std::string opShortName = op->getName().getStringRef().str(); + opShortName = opShortName.substr(opShortName.find('.') + 1); + + // Get output directory for the operation and create it if it doesn't exist. + std::string outDirPath = getOutputDirForOp(op, outputBaseDir); + fs::create_directories(outDirPath); + + // Run Yosys to synthesize the Verilog into BLIF. + std::string yosysOutName = moduleName + "_yosys.blif"; + if (!runYosys(moduleName, outDirPath, verilogFiles, yosysOutName)) { + llvm::errs() << "BackendGenerator: Yosys failed for `" + << op->getName().getStringRef() << "`\n"; + return false; + } + + // Run ABC to optimize the BLIF file generated by Yosys. + std::string yosysOutPath = (fs::path(outDirPath) / yosysOutName).string(); + std::string abcOutName = opShortName + ".blif"; + if (!runAbc(yosysOutPath, outDirPath, abcOutName)) { + llvm::errs() << "BackendGenerator: ABC failed for `" + << op->getName().getStringRef() << "`\n"; + return false; + } + + // Check that the final BLIF file exists and store its path in outputFiles. + fs::path blifPath = fs::path(outDirPath) / abcOutName; + if (!fs::exists(blifPath)) + return false; + outputFiles = {blifPath.string()}; + return true; +} + +bool BackendGenerator::generateVerilog(mlir::Operation *op) { + + const std::string &rtlConfigPath = params.rtlConfigPath; + const std::string &dynamaticRoot = params.dynamaticRoot; + const std::string &outputBaseDir = params.outputBaseDir; + + // Assert parameters are not empty or NULL + assert(!rtlConfigPath.empty() && "rtlConfigPath cannot be empty"); + assert(!dynamaticRoot.empty() && "dynamaticRoot cannot be empty"); + assert(!outputBaseDir.empty() && "outputBaseDir cannot be empty"); + + std::vector verilogFiles; + namespace fs = std::filesystem; + + // Generate the Verilog files using the RTL generator command from the JSON + // config for the operation. + std::string opName = op->getName().getStringRef().str(); + std::string opShortName = opName.substr(opName.find('.') + 1); + + // Get the operation parameters into a map for easy lookup when substituting + // into the generator command. + std::map rtlParamMap; + getParamMap(op, rtlParamMap); + + moduleName = opShortName; + + // Get output directory for the operation and create it if it doesn't exist. + fs::path outDirPath = getOutputDirForOp(op, outputBaseDir); + fs::create_directories(outDirPath); + + // Add common parameters for generator command substitution. + rtlParamMap["MODULE_NAME"] = moduleName; + rtlParamMap["OUTPUT_DIR"] = outDirPath; + rtlParamMap["DYNAMATIC"] = dynamaticRoot; + rtlParamMap["EXTRA_SIGNALS"] = "0"; + if (rtlParamMap.find("BITWIDTH") == rtlParamMap.end()) + rtlParamMap["BITWIDTH"] = "0"; + + // Read and parse the JSON config file to find the generator command for this + // operation. + std::ifstream configFile(rtlConfigPath); + if (!configFile.is_open()) { + llvm::errs() << "BackendGenerator: cannot open config: " << rtlConfigPath + << "\n"; + return false; + } + std::string jsonStr, line; + while (std::getline(configFile, line)) + jsonStr += line; + + auto parsedOrErr = llvm::json::parse(jsonStr); + if (!parsedOrErr) { + llvm::errs() << "BackendGenerator: failed to parse JSON config\n"; + return false; + } + const llvm::json::Array *allEntries = parsedOrErr->getAsArray(); + if (!allEntries) { + llvm::errs() << "BackendGenerator: JSON root is not an array\n"; + return false; + } + + const llvm::json::Object *matchedEntry = nullptr; + for (const auto &entryVal : *allEntries) { + const auto *obj = entryVal.getAsObject(); + if (!obj) + continue; + auto nameOpt = jsonGetString(*obj, "name"); + if (!nameOpt || *nameOpt != opName) + continue; + if (entryMatchesParams(*obj, rtlParamMap)) { + matchedEntry = obj; + break; + } + } + if (!matchedEntry) { + llvm::errs() << "BackendGenerator: no config entry for `" << opName + << "`\n"; + return false; + } + + auto genCmdOpt = jsonGetString(*matchedEntry, "generator"); + if (!genCmdOpt) { + llvm::errs() << "BackendGenerator: no 'generator' field for `" << opName + << "`\n"; + return false; + } + std::string cmd = *genCmdOpt; + + // Substitute longer names first to avoid partial matches. + std::vector> sortedParams( + rtlParamMap.begin(), rtlParamMap.end()); + std::sort(sortedParams.begin(), sortedParams.end(), + [](const auto &a, const auto &b) { + return a.first.size() > b.first.size(); + }); + + // Substitute parameters into the generator command. + for (const auto &[name, value] : sortedParams) + replaceAll(cmd, "$" + name, value); + + // Check that all variables have been substituted (no '$' should remain) and + // run the command. + if (cmd.find('$') != std::string::npos) { + llvm::errs() << "BackendGenerator: unresolved variables in generator " + "command for `" + << opName << "`: " << cmd << "\n"; + return false; + } + if (system(cmd.c_str()) != 0) { + llvm::errs() << "BackendGenerator: generator command failed for `" << opName + << "`\n"; + return false; + } + + // Collect the generated Verilog files. The generator command is expected to + // produce a main file named ".v" in the output directory. + std::string genVerilog = (outDirPath / (moduleName + ".v")).string(); + if (fs::exists(genVerilog)) + verilogFiles.push_back(genVerilog); + // Copy any additional .v files that might be used by the Yosys script (e.g. + // for submodules). + for (const auto &entryVal : *allEntries) { + const auto *obj = entryVal.getAsObject(); + if (!obj || obj->get("name")) + continue; + auto genOpt = jsonGetString(*obj, "generic"); + if (!genOpt) + continue; + std::string path = *genOpt; + replaceAll(path, "$DYNAMATIC", dynamaticRoot); + if (fs::exists(path) && std::find(verilogFiles.begin(), verilogFiles.end(), + path) == verilogFiles.end()) + verilogFiles.push_back(path); + } + // Check that the list of Verilog files is not empty. + if (verilogFiles.empty()) { + llvm::errs() << "Generator command did not produce expected Verilog file: " + << genVerilog << "\n"; + return false; + } + outputFiles = verilogFiles; + return true; +} + +bool BackendGenerator::runYosys(const std::string &topModule, + const std::string &outputDir, + const std::vector &verilogFiles, + const std::string &outputName) const { + + std::string yosysExecutable; + // Check if Yosys executable is configured. +#if !defined(DYNAMATIC_YOSYS_EXECUTABLE) + // If not configured, check that there is a "yosys" executable in the system + // PATH. + if (system("which yosys > /dev/null 2>&1") != 0) { + llvm::errs() << "Yosys executable not found. Please install Yosys or " + "re-build Dynamatic with Yosys enabled.\n"; + return false; + } + yosysExecutable = "yosys"; +#else + yosysExecutable = DYNAMATIC_YOSYS_EXECUTABLE; + if (!std::filesystem::exists(yosysExecutable)) { + llvm::errs() << "Yosys executable not found at configured path: " + << yosysExecutable << "\n"; + return false; + } +#endif + + std::string scriptPath = outputDir + "/run_yosys.sh"; + std::ofstream script(scriptPath); + if (!script.is_open()) + return false; + + script << "#!/bin/bash\n"; + script << yosysExecutable << " -p \""; + for (const auto &f : verilogFiles) + script << "read_verilog -defer " << f << ";\n "; + script << "hierarchy -top " << topModule << ";\n"; + script << " proc;\n"; + script << " opt -nodffe -nosdff;\n"; + script << " memory -nomap;\n"; + script << " techmap;\n"; + script << " flatten;\n"; + script << " clean;\n"; + script << " write_blif " << outputDir << "/" << outputName + << "\" > /dev/null 2>&1\n"; + script.close(); + + std::filesystem::permissions(scriptPath, + std::filesystem::perms::owner_exec | + std::filesystem::perms::owner_read | + std::filesystem::perms::owner_write, + std::filesystem::perm_options::add); + return system(("bash " + scriptPath).c_str()) == 0; +} + +bool BackendGenerator::runAbc(const std::string &inputFile, + const std::string &outputDir, + const std::string &outputName) const { + + std::string abcExecutable; + // Check if ABC executable is configured. +#if !defined(DYNAMATIC_ABC_EXECUTABLE) + // If not configured, check that there is an "abc" executable in the system + // PATH. + if (system("which abc > /dev/null 2>&1") != 0) { + llvm::errs() << "ABC executable not found. Please install ABC or " + "re-build Dynamatic with ABC enabled.\n"; + return false; + } + abcExecutable = "abc"; +#else + abcExecutable = DYNAMATIC_ABC_EXECUTABLE; + if (!std::filesystem::exists(abcExecutable)) { + llvm::errs() << "ABC executable not found at configured path: " + << abcExecutable << "\n"; + return false; + } +#endif + + std::string scriptPath = outputDir + "/run_abc.sh"; + std::ofstream script(scriptPath); + if (!script.is_open()) + return false; + + script << "#!/bin/bash\n"; + script << abcExecutable << " -c \"read_blif " << inputFile << ";\n"; + script << " strash;\n"; + for (int i = 0; i < 6; ++i) { + script << " rewrite;\n"; + script << " b;\n"; + script << " refactor;\n"; + script << " b;\n"; + } + script << " write_blif " << outputDir << "/" << outputName + << "\" > /dev/null 2>&1\n"; + script.close(); + + std::filesystem::permissions(scriptPath, + std::filesystem::perms::owner_exec | + std::filesystem::perms::owner_read | + std::filesystem::perms::owner_write, + std::filesystem::perm_options::add); + return system(("bash " + scriptPath).c_str()) == 0; +} diff --git a/lib/Support/CMakeLists.txt b/lib/Support/CMakeLists.txt index 985d1734ba..ff9338a7f0 100644 --- a/lib/Support/CMakeLists.txt +++ b/lib/Support/CMakeLists.txt @@ -1,6 +1,7 @@ add_dynamatic_library(DynamaticSupport Attribute.cpp Backedge.cpp + BackendGenerator.cpp BLIFFileManager.cpp CFG.cpp DOT.cpp diff --git a/lib/Transforms/HWFlatten.cpp b/lib/Transforms/HWFlatten.cpp index 2a5ebcc11c..2051302bcb 100644 --- a/lib/Transforms/HWFlatten.cpp +++ b/lib/Transforms/HWFlatten.cpp @@ -67,8 +67,30 @@ struct FlattenModulesPass } // namespace void FlattenModulesPass::runOnOperation() { + MLIRContext *ctx = &getContext(); Operation *top = getOperation(); + // Pre-pass: annotate the op that drives each output port of every hw module + // with "hw.module_name" and "hw.port_name" attributes before inlining. + // Because inlineRegion clones the body (shouldClone=true), these attributes + // are preserved in the flattened parent. + top->walk([&](hw::HWModuleOp module) { + auto *outputOp = module.getBodyBlock()->getTerminator(); + unsigned outIdx = 0; + for (auto &port : module.getPortList()) { + if (port.isInput()) + continue; + Value outputVal = outputOp->getOperand(outIdx); + StringAttr modName = StringAttr::get(ctx, module.getName()); + StringAttr portName = port.name; + if (Operation *defOp = outputVal.getDefiningOp()) { + defOp->setAttr("hw.module_name", modName); + defOp->setAttr("hw.port_name", portName); + } + ++outIdx; + } + }); + // Record which modules are instantiated before we start inlining, so we // know which ones to erase afterwards. DenseSet instantiated; diff --git a/lib/Transforms/HandshakeMarkBLIFImpl.cpp b/lib/Transforms/HandshakeMarkBLIFImpl.cpp index 2fcb21e1b5..a31a663e8e 100644 --- a/lib/Transforms/HandshakeMarkBLIFImpl.cpp +++ b/lib/Transforms/HandshakeMarkBLIFImpl.cpp @@ -47,8 +47,16 @@ struct HandshakeMarkBLIFImplPass llvm::errs() << "BLIF directory path is empty\n"; return signalPassFailure(); } + if (dynamaticRoot.empty()) { + llvm::errs() << "Dynamatic root path is empty\n"; + return signalPassFailure(); + } + if (RTLJSONFile.empty()) { + llvm::errs() << "RTL JSON file path is empty\n"; + return signalPassFailure(); + } // Generate blif manager with the provided directory path - BLIFFileManager blifFileManager(blifDirPath); + BLIFFileManager blifFileManager(blifDirPath, dynamaticRoot, RTLJSONFile); // Get the module op mlir::ModuleOp moduleOp = cast(getOperation()); // Walk through all handshake operations in the function diff --git a/unittests/Support/BLIFFileManager/BLIFFileManagerTest.cpp b/unittests/Support/BLIFFileManager/BLIFFileManagerTest.cpp new file mode 100644 index 0000000000..8eeb137f02 --- /dev/null +++ b/unittests/Support/BLIFFileManager/BLIFFileManagerTest.cpp @@ -0,0 +1,156 @@ +//===- BLIFFileManagerTest.cpp - Unit tests for BLIFFileManager -*- 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 +// +//===----------------------------------------------------------------------===// + +#include "dynamatic/Support/BLIFFileManager.h" +#include "dynamatic/Dialect/Handshake/HandshakeOps.h" +#include "dynamatic/Dialect/Handshake/HandshakeTypes.h" +#include "dynamatic/InitAllDialects.h" +#include "mlir/IR/Builders.h" +#include "mlir/IR/BuiltinAttributes.h" +#include "mlir/IR/BuiltinTypes.h" +#include "mlir/IR/MLIRContext.h" +#include "llvm/ADT/APInt.h" +#include +#include +#include + +#ifndef BLIF_TEST_DIR +#error \ + "BLIF_TEST_DIR must be defined as a compiler definition pointing to data/aig" +#endif +#ifndef DYNAMATIC_ROOT +#error \ + "DYNAMATIC_ROOT must be defined as a compiler definition pointing to the Dynamatic root directory" +#endif +#ifndef RTL_JSON_FILE +#error \ + "RTL_JSON_FILE must be defined as a compiler definition pointing to the RTL JSON configuration file" +#endif + +using namespace dynamatic; +using namespace dynamatic::handshake; +namespace fs = std::filesystem; + +class BLIFFileManagerTest : public ::testing::Test { +protected: + mlir::MLIRContext ctx; + BLIFFileManager manager; + + BLIFFileManagerTest() + : manager(BLIF_TEST_DIR, DYNAMATIC_ROOT, RTL_JSON_FILE) {} + + void SetUp() override { + mlir::DialectRegistry registry; + dynamatic::registerAllDialects(registry); + ctx.appendDialectRegistry(registry); + ctx.loadAllAvailableDialects(); + } + + // Helper function to create a control argument at a block and return it. + static mlir::Value addCtrlArg(mlir::Block &b, mlir::MLIRContext &ctx) { + return b.addArgument(ControlType::get(&ctx), mlir::UnknownLoc::get(&ctx)); + } + // Helper function to create a channel argument at a block and return it. + static mlir::Value addChannelArg(mlir::Block &b, mlir::MLIRContext &ctx, + unsigned width) { + return b.addArgument(ChannelType::get(mlir::IntegerType::get(&ctx, width)), + mlir::UnknownLoc::get(&ctx)); + } + // Function to run the BLIFFileManager on an operation + void checkOp(mlir::Operation *op, const char *expectedSubstr) { + // Run the BLIFFileManager to get the BLIF file path for the operation, and + // check that it contains the expected substring (derived from the operation + // name and parameters). + std::string path = manager.getBlifFilePathForHandshakeOp(op); + EXPECT_NE(path.find(expectedSubstr), std::string::npos) + << "[" << expectedSubstr << "] unexpected path: " << path; + // Check that the file exists at the generated path. + EXPECT_TRUE(fs::exists(path)) + << "[" << expectedSubstr << "] BLIF file not found: " << path; + } + + // Checks all two-input arith integer ops with no additional attribute + template + void checkBinaryIntOp(const char *expectedSubstr, unsigned width = 32) { + // Create a block and add two channel arguments of the specified width. + mlir::Block block; + mlir::OpBuilder b(&ctx); + auto lhs = addChannelArg(block, ctx, width), + rhs = addChannelArg(block, ctx, width); + b.setInsertionPointToEnd(&block); + checkOp(b.create(b.getUnknownLoc(), lhs, rhs), expectedSubstr); + } +}; + +// Path convention (driven by getRTLParameters() order): +// addi/subi/andi/ori/xori/shli/shrsi/shrui -> /BITWIDTH/.blif +// muli -> muli/BITWIDTH/LATENCY/muli.blif +// cmpi -> cmpi/PREDICATE/BITWIDTH/cmpi.blif +// constant -> constant/VALUE/BITWIDTH/constant.blif +// select -> select/BITWIDTH/select.blif + +TEST_F(BLIFFileManagerTest, AllSupportedOpsPathAndFileExist) { + // Test with bitwidth 32 + checkBinaryIntOp("addi/32"); + checkBinaryIntOp("subi/32"); + checkBinaryIntOp("andi/32"); + checkBinaryIntOp("ori/32"); + checkBinaryIntOp("xori/32"); + checkBinaryIntOp("shli/32"); + checkBinaryIntOp("shrsi/32"); + checkBinaryIntOp("shrui/32"); + + // muli test with latency 4 and bitwidth 32 + { + mlir::Block block; + mlir::OpBuilder b(&ctx); + auto lhs = addChannelArg(block, ctx, 32), + rhs = addChannelArg(block, ctx, 32); + b.setInsertionPointToEnd(&block); + auto op = b.create(b.getUnknownLoc(), lhs, rhs); + op->setAttr("latency", + mlir::IntegerAttr::get(mlir::IntegerType::get(&ctx, 64), 4)); + checkOp(op, "muli/32/4"); + } + + // cmpi test with predicate "slt" and bitwidth 32 + { + mlir::Block block; + mlir::OpBuilder b(&ctx); + auto lhs = addChannelArg(block, ctx, 32), + rhs = addChannelArg(block, ctx, 32); + b.setInsertionPointToEnd(&block); + checkOp(b.create(b.getUnknownLoc(), CmpIPredicate::slt, + lhs, rhs), + "cmpi/slt/32"); + } + + // constant test with value -25 (1111100111 in binary) and bitwidth 10 + { + mlir::Block block; + mlir::OpBuilder b(&ctx); + auto ctrl = addCtrlArg(block, ctx); + b.setInsertionPointToEnd(&block); + auto valAttr = mlir::IntegerAttr::get(mlir::IntegerType::get(&ctx, 10), + llvm::APInt(10, -25, true)); + checkOp(b.create(b.getUnknownLoc(), valAttr, ctrl), + "constant/1111100111/10"); + } + + // select test with bitwidth 32 + { + mlir::Block block; + mlir::OpBuilder b(&ctx); + auto cond = addChannelArg(block, ctx, 1); + auto tval = addChannelArg(block, ctx, 32); + auto fval = addChannelArg(block, ctx, 32); + b.setInsertionPointToEnd(&block); + checkOp(b.create(b.getUnknownLoc(), cond, tval, fval), + "select/32"); + } +} diff --git a/unittests/Support/BLIFFileManager/CMakeLists.txt b/unittests/Support/BLIFFileManager/CMakeLists.txt new file mode 100644 index 0000000000..99e9ea1c5b --- /dev/null +++ b/unittests/Support/BLIFFileManager/CMakeLists.txt @@ -0,0 +1,45 @@ +add_executable( + blif-file-manager-unit-tests + BLIFFileManagerTest.cpp +) + +target_link_libraries( + blif-file-manager-unit-tests + PRIVATE + DynamaticSupport + DynamaticAnalysis + DynamaticSynth + MLIRControlFlowDialect + MLIRMathDialect + MLIRSCFDialect + GTest::gtest_main +) + +target_compile_definitions( + blif-file-manager-unit-tests + PRIVATE + BLIF_TEST_DIR="${CMAKE_SOURCE_DIR}/aig_folder/" + DYNAMATIC_ROOT="${CMAKE_SOURCE_DIR}/" + RTL_JSON_FILE="${CMAKE_SOURCE_DIR}/data/rtl-config-verilog-beta.json" +) + +enable_testing() + +include(GoogleTest) + +gtest_discover_tests( + blif-file-manager-unit-tests + XML_OUTPUT_DIR ${CMAKE_BINARY_DIR}/unittests/Support/BLIFFileManager/results +) + +add_custom_target( + run-blif-file-manager-tests + COMMAND ${CMAKE_CTEST_COMMAND} -C $ --timeout 1500 --output-on-failure + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Run BLIFFileManager unit tests." + VERBATIM + USES_TERMINAL + DEPENDS blif-file-manager-unit-tests +) + +add_to_unit_testing(check-dynamatic run-blif-file-manager-tests) diff --git a/unittests/Support/CMakeLists.txt b/unittests/Support/CMakeLists.txt index ab2aec8803..d0b196a33c 100644 --- a/unittests/Support/CMakeLists.txt +++ b/unittests/Support/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(ConstraintProgramming) +add_subdirectory(BLIFFileManager)