From 5030a2ad5949f6d57c548e99d035bd825913a13e Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Sat, 20 May 2023 01:18:33 +0100 Subject: [PATCH] Add a new tool: slang-netlist (#757) --- .gitignore | 2 + include/slang/diagnostics/Diagnostics.h | 5 +- {source => include/slang}/text/FormatBuffer.h | 0 source/ast/Bitstream.cpp | 2 +- source/ast/EvalContext.cpp | 3 +- source/ast/Symbol.cpp | 3 +- source/ast/types/TypePrinter.cpp | 3 +- source/diagnostics/TextDiagnosticClient.cpp | 3 +- source/numeric/ConstantValue.cpp | 2 +- source/text/Json.cpp | 2 +- source/util/CommandLine.cpp | 2 +- tools/CMakeLists.txt | 1 + tools/netlist/CMakeLists.txt | 21 + tools/netlist/README.md | 45 ++ tools/netlist/TODO.md | 12 + tools/netlist/include/Config.h | 29 ++ tools/netlist/include/Debug.h | 20 + tools/netlist/include/DepthFirstSearch.h | 82 ++++ tools/netlist/include/DirectedGraph.h | 301 +++++++++++++ tools/netlist/include/Netlist.h | 336 ++++++++++++++ tools/netlist/include/NetlistPath.h | 70 +++ tools/netlist/include/NetlistVisitor.h | 412 ++++++++++++++++++ tools/netlist/include/PathFinder.h | 93 ++++ tools/netlist/include/SplitVariables.h | 139 ++++++ tools/netlist/netlist.cpp | 308 +++++++++++++ tools/netlist/tests/CMakeLists.txt | 22 + tools/netlist/tests/DepthFirstSearchTests.cpp | 113 +++++ tools/netlist/tests/DirectedGraphTests.cpp | 173 ++++++++ tools/netlist/tests/NetlistTests.cpp | 412 ++++++++++++++++++ 29 files changed, 2602 insertions(+), 14 deletions(-) rename {source => include/slang}/text/FormatBuffer.h (100%) create mode 100644 tools/netlist/CMakeLists.txt create mode 100644 tools/netlist/README.md create mode 100644 tools/netlist/TODO.md create mode 100644 tools/netlist/include/Config.h create mode 100644 tools/netlist/include/Debug.h create mode 100644 tools/netlist/include/DepthFirstSearch.h create mode 100644 tools/netlist/include/DirectedGraph.h create mode 100644 tools/netlist/include/Netlist.h create mode 100644 tools/netlist/include/NetlistPath.h create mode 100644 tools/netlist/include/NetlistVisitor.h create mode 100644 tools/netlist/include/PathFinder.h create mode 100644 tools/netlist/include/SplitVariables.h create mode 100644 tools/netlist/netlist.cpp create mode 100644 tools/netlist/tests/CMakeLists.txt create mode 100644 tools/netlist/tests/DepthFirstSearchTests.cpp create mode 100644 tools/netlist/tests/DirectedGraphTests.cpp create mode 100644 tools/netlist/tests/NetlistTests.cpp diff --git a/.gitignore b/.gitignore index 3ff328851..39db3639b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ .vscode/ .venv/ .idea/ +.cache/ +.DS_Store build/ install/ compile_commands.json diff --git a/include/slang/diagnostics/Diagnostics.h b/include/slang/diagnostics/Diagnostics.h index 299b93e7b..51d89acb9 100644 --- a/include/slang/diagnostics/Diagnostics.h +++ b/include/slang/diagnostics/Diagnostics.h @@ -43,8 +43,9 @@ class Symbol; x(SysFuncs) \ x(ConstEval) \ x(Compilation) \ - x(Meta) \ - x(Tidy) + x(Meta) \ + x(Tidy) \ + x(Netlist) SLANG_ENUM_SIZED(DiagSubsystem, uint16_t, DS) #undef DS diff --git a/source/text/FormatBuffer.h b/include/slang/text/FormatBuffer.h similarity index 100% rename from source/text/FormatBuffer.h rename to include/slang/text/FormatBuffer.h diff --git a/source/ast/Bitstream.cpp b/source/ast/Bitstream.cpp index 2a749174b..848ff435c 100644 --- a/source/ast/Bitstream.cpp +++ b/source/ast/Bitstream.cpp @@ -7,7 +7,6 @@ //------------------------------------------------------------------------------ #include "slang/ast/Bitstream.h" -#include "../text/FormatBuffer.h" #include #include "slang/ast/Compilation.h" @@ -18,6 +17,7 @@ #include "slang/diagnostics/ConstEvalDiags.h" #include "slang/diagnostics/ExpressionsDiags.h" #include "slang/diagnostics/NumericDiags.h" +#include "slang/text/FormatBuffer.h" namespace slang::ast { diff --git a/source/ast/EvalContext.cpp b/source/ast/EvalContext.cpp index b205aef79..36dc2f436 100644 --- a/source/ast/EvalContext.cpp +++ b/source/ast/EvalContext.cpp @@ -7,14 +7,13 @@ //------------------------------------------------------------------------------ #include "slang/ast/EvalContext.h" -#include "../text/FormatBuffer.h" - #include "slang/ast/ASTContext.h" #include "slang/ast/Compilation.h" #include "slang/ast/symbols/SubroutineSymbols.h" #include "slang/ast/symbols/VariableSymbols.h" #include "slang/ast/types/Type.h" #include "slang/diagnostics/ConstEvalDiags.h" +#include "slang/text/FormatBuffer.h" namespace slang::ast { diff --git a/source/ast/Symbol.cpp b/source/ast/Symbol.cpp index 5acdd4c0d..f80fdd890 100644 --- a/source/ast/Symbol.cpp +++ b/source/ast/Symbol.cpp @@ -7,8 +7,6 @@ //------------------------------------------------------------------------------ #include "slang/ast/Symbol.h" -#include "../text/FormatBuffer.h" - #include "slang/ast/ASTVisitor.h" #include "slang/ast/Compilation.h" #include "slang/ast/Definition.h" @@ -16,6 +14,7 @@ #include "slang/ast/symbols/MemberSymbols.h" #include "slang/ast/types/Type.h" #include "slang/syntax/AllSyntax.h" +#include "slang/text/FormatBuffer.h" #include "slang/text/SourceManager.h" namespace { diff --git a/source/ast/types/TypePrinter.cpp b/source/ast/types/TypePrinter.cpp index 46327437a..c2222415c 100644 --- a/source/ast/types/TypePrinter.cpp +++ b/source/ast/types/TypePrinter.cpp @@ -7,9 +7,8 @@ //------------------------------------------------------------------------------ #include "slang/ast/types/TypePrinter.h" -#include "../../text/FormatBuffer.h" - #include "slang/ast/ASTVisitor.h" +#include "slang/text/FormatBuffer.h" namespace slang::ast { diff --git a/source/diagnostics/TextDiagnosticClient.cpp b/source/diagnostics/TextDiagnosticClient.cpp index 510fb0fcb..365497dab 100644 --- a/source/diagnostics/TextDiagnosticClient.cpp +++ b/source/diagnostics/TextDiagnosticClient.cpp @@ -7,9 +7,8 @@ //------------------------------------------------------------------------------ #include "slang/diagnostics/TextDiagnosticClient.h" -#include "../text/FormatBuffer.h" - #include "slang/text/CharInfo.h" +#include "slang/text/FormatBuffer.h" #include "slang/text/SourceManager.h" namespace slang { diff --git a/source/numeric/ConstantValue.cpp b/source/numeric/ConstantValue.cpp index 6055ee0f1..cd41673e9 100644 --- a/source/numeric/ConstantValue.cpp +++ b/source/numeric/ConstantValue.cpp @@ -7,9 +7,9 @@ //------------------------------------------------------------------------------ #include "slang/numeric/ConstantValue.h" -#include "../text/FormatBuffer.h" #include +#include "slang/text/FormatBuffer.h" #include "slang/util/Hash.h" namespace slang { diff --git a/source/text/Json.cpp b/source/text/Json.cpp index b37c27be6..19ef56029 100644 --- a/source/text/Json.cpp +++ b/source/text/Json.cpp @@ -7,9 +7,9 @@ //------------------------------------------------------------------------------ #include "slang/text/Json.h" -#include "FormatBuffer.h" #include +#include "slang/text/FormatBuffer.h" #include "slang/util/SmallVector.h" #include "slang/util/String.h" diff --git a/source/util/CommandLine.cpp b/source/util/CommandLine.cpp index 9b5fc2a04..ba392d262 100644 --- a/source/util/CommandLine.cpp +++ b/source/util/CommandLine.cpp @@ -673,7 +673,7 @@ bool CommandLine::Option::expectsValue() const { std::string CommandLine::Option::set(std::string_view name, std::string_view value, bool ignoreDup) { std::string pathMem; - if (isFileName && !value.empty()) { + if (isFileName && !value.empty() && value != "-") { std::error_code ec; fs::path path = fs::weakly_canonical(fs::path(widen(value)), ec); if (!ec) { diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 30fbfde96..c1c0d2a84 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -24,4 +24,5 @@ if(SLANG_INCLUDE_INSTALL) install(TARGETS slang_driver RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() +add_subdirectory(netlist) add_subdirectory(tidy) diff --git a/tools/netlist/CMakeLists.txt b/tools/netlist/CMakeLists.txt new file mode 100644 index 000000000..3cf3ba887 --- /dev/null +++ b/tools/netlist/CMakeLists.txt @@ -0,0 +1,21 @@ +# ~~~ +# SPDX-FileCopyrightText: Michael Popoloski +# SPDX-License-Identifier: MIT +# ~~~ +add_executable(slang_netlist netlist.cpp) +add_executable(slang::netlist ALIAS slang_netlist) + +target_link_libraries( + slang_netlist + PRIVATE slang::slang fmt::fmt + PUBLIC ${SLANG_LIBRARIES}) +target_include_directories(slang_netlist PRIVATE include ../../include) +set_target_properties(slang_netlist PROPERTIES OUTPUT_NAME "slang-netlist") + +if(SLANG_INCLUDE_INSTALL) + install(TARGETS slang_netlist RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif() + +if(SLANG_INCLUDE_TESTS) + add_subdirectory(tests) +endif() diff --git a/tools/netlist/README.md b/tools/netlist/README.md new file mode 100644 index 000000000..6b2f4e397 --- /dev/null +++ b/tools/netlist/README.md @@ -0,0 +1,45 @@ +slang-netlist +============= + +slang-netlist is a library and tool for analysing the source-level static +connectivity of a design. This capability can be useful, for example, to +develop structural checks or to investigate timing paths, rather than having to +use synthesis to obtain a gate-level netlist. + +Using the example of a simple adder: +``` +module adder + #(parameter p_width = 32)( + input logic [p_width-1:0] i_a, + input logic [p_width-1:0] i_b, + output logic [p_width-1:0] o_sum, + output logic o_co + ); + logic [p_width-1:0] sum; + logic co; + assign {co, sum} = i_a + i_b; + assign o_sum = sum; + assign o_co = co; +endmodule +``` + +The slang-netlist command-line tool can be used to trace paths through the +design, such as: +``` +➜ slang-netlist adder.sv --from adder.i_a --to adder.o_sum -q +adder.sv:10:22: note: variable i_a read from + assign {co, sum} = i_a + i_b; + ^~~ + +adder.sv:10:15: note: variable sum assigned to + assign {co, sum} = i_a + i_b; + ^~~ + +adder.sv:11:18: note: variable sum read from + assign o_sum = sum; + ^~~ + +adder.sv:11:10: note: variable o_sum assigned to + assign o_sum = sum; + ^~~~~ +``` diff --git a/tools/netlist/TODO.md b/tools/netlist/TODO.md new file mode 100644 index 000000000..b19086fac --- /dev/null +++ b/tools/netlist/TODO.md @@ -0,0 +1,12 @@ +To dos +====== + +- Reporting of variables in the netlist (by type, matching patterns). +- Infer sequential elements in the netlist (ie non-blocking assignment and + sensitive to a clock edge). +- Constrain paths to start on particular node types (port, register, net etc). +- Support restricting paths to stop at sequential elements. +- Support paths passing through particular nodes. +- Support paths avoiding particular nodes. +- Support reporting of paths fanning into or out of a particular node. +- Provide Python bindings. diff --git a/tools/netlist/include/Config.h b/tools/netlist/include/Config.h new file mode 100644 index 000000000..1d0c4622c --- /dev/null +++ b/tools/netlist/include/Config.h @@ -0,0 +1,29 @@ +//------------------------------------------------------------------------------ +//! @file Config.h +//! @brief Provide singleton configuration class debug printing macro. +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ +#pragma once + +namespace netlist { + +/// A singleton to hold global configuration options. +class Config { +public: + bool debugEnabled{}; + + Config() = default; + + static Config& getInstance() { + static Config instance; + return instance; + } + + // Prevent copies from being made. + Config(Config const&) = delete; + void operator=(Config const&) = delete; +}; + +} // namespace netlist diff --git a/tools/netlist/include/Debug.h b/tools/netlist/include/Debug.h new file mode 100644 index 000000000..fd73b9939 --- /dev/null +++ b/tools/netlist/include/Debug.h @@ -0,0 +1,20 @@ +//------------------------------------------------------------------------------ +//! @file Debug.h +//! @brief Provide a debug printing macro. +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ +#pragma once + +#include "Config.h" +#include + +#ifdef DEBUG +# define DEBUG_PRINT(x) \ + if (netlist::Config::getInstance().debugEnabled) { \ + std::cerr << x; \ + } +#else +# define DEBUG_PRINT(x) +#endif diff --git a/tools/netlist/include/DepthFirstSearch.h b/tools/netlist/include/DepthFirstSearch.h new file mode 100644 index 000000000..a3a22549d --- /dev/null +++ b/tools/netlist/include/DepthFirstSearch.h @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +//! @file DepthFirstSearch.h +//! @brief Implementation of depth-first search on a directed graph. +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ +#pragma once + +#include "DirectedGraph.h" +#include +#include + +namespace netlist { + +struct select_all { + template + bool operator()(const T&) const { + return true; + } +}; + +/// Depth-first search on a directed graph. A visitor class provides visibility +/// to the caller of visits to edges and nodes. An optional edge predicate +/// selects which edges can be included in the traversal. +template +class DepthFirstSearch { +public: + DepthFirstSearch(Visitor& visitor, NodeType& startNode) : visitor(visitor) { + setup(startNode); + run(); + } + + DepthFirstSearch(Visitor& visitor, EdgePredicate edgePredicate, NodeType& startNode) : + visitor(visitor), edgePredicate(edgePredicate) { + setup(startNode); + run(); + } + +private: + using EdgeIteratorType = typename NodeType::iterator; + using VisitStackElement = std::pair; + + /// Setup the traversal. + void setup(NodeType& startNode) { + visitedNodes.insert(&startNode); + visitStack.push_back(VisitStackElement(startNode, startNode.begin())); + visitor.visitNode(startNode); + } + + /// Perform a depth-first traversal, calling the visitor methods on the way. + void run() { + while (!visitStack.empty()) { + auto& node = visitStack.back().first; + auto& nodeIt = visitStack.back().second; + // Visit each child node that hasn't already been visited. + while (nodeIt != node.end()) { + auto* edge = nodeIt->get(); + auto& targetNode = edge->getTargetNode(); + nodeIt++; + if (edgePredicate(*edge) && visitedNodes.count(&targetNode) == 0) { + // Push a new 'current' node onto the stack and mark it as visited. + visitStack.push_back(VisitStackElement(targetNode, targetNode.begin())); + visitedNodes.insert(&targetNode); + visitor.visitEdge(*edge); + visitor.visitNode(targetNode); + return run(); + } + } + // All children of this node have been visited or skipped, so remove from the stack. + visitStack.pop_back(); + } + } + +private: + Visitor& visitor; + EdgePredicate edgePredicate; + std::set visitedNodes; + std::vector visitStack; +}; + +} // namespace netlist diff --git a/tools/netlist/include/DirectedGraph.h b/tools/netlist/include/DirectedGraph.h new file mode 100644 index 000000000..e8548f432 --- /dev/null +++ b/tools/netlist/include/DirectedGraph.h @@ -0,0 +1,301 @@ +//------------------------------------------------------------------------------ +//! @file DirectedGraph.h +//! @brief Directed graph ADT +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ +#pragma once + +#include +#include +#include +#include +#include + +#include "slang/util/Util.h" + +namespace netlist { + +/// A class to represent a directed edge in a graph. +template +class DirectedEdge { +public: + DirectedEdge(NodeType& sourceNode, NodeType& targetNode) : + sourceNode(sourceNode), targetNode(targetNode) {} + + DirectedEdge& operator=(const DirectedEdge& edge) { + sourceNode = edge.sourceNode; + targetNode = edge.targetNode; + return *this; + } + + /// Static polymorphism: delegate implementation (via isEqualTo) to the + /// derived class. Add friend operator to resolve ambiguity between operand + /// ordering with C++20. + friend bool operator==(const EdgeType& A, const EdgeType& B) noexcept { + return A.getDerived().isEqualTo(B); + } + bool operator==(const EdgeType& E) const { return getDerived().isEqualTo(E); } + bool operator!=(const EdgeType& E) const { return !operator==(E); } + + /// Return the source node of this edge. + NodeType& getSourceNode() const { return sourceNode; } + + /// Return the target node of this edge. + NodeType& getTargetNode() const { return targetNode; } + +protected: + // As the default implementation use address comparison for equality. + bool isEqualTo(const EdgeType& edge) const { return this == &edge; } + + // Cast the 'this' pointer to the derived type and return a reference. + EdgeType& getDerived() { return *static_cast(this); } + const EdgeType& getDerived() const { return *static_cast(this); } + + NodeType& sourceNode; + NodeType& targetNode; +}; + +/// A class to represent a node in a directed graph. +template +class Node { +public: + using EdgeListType = std::vector>; + using iterator = typename EdgeListType::iterator; + using const_iterator = typename EdgeListType::const_iterator; + using edge_descriptor = EdgeType*; + + Node() = default; + Node(EdgeType& edge) : edges() { edges.push_back(edge); } + virtual ~Node() = default; + + const_iterator begin() const { return edges.begin(); } + const_iterator end() const { return edges.end(); } + iterator begin() { return edges.begin(); } + iterator end() { return edges.end(); } + + Node& operator=(const Node& node) { + edges = node.edges; + return *this; + } + Node& operator=(Node&& node) noexcept { + edges = std::move(node.Edges); + return *this; + } + + /// Static polymorphism: delegate implementation (via isEqualTo) to the + /// derived class. Add friend operator to resolve ambiguity between operand + /// ordering with C++20. + friend bool operator==(NodeType const& A, NodeType const& B) noexcept { + return A.getDerived().isEqualTo(B); + } + bool operator==(const NodeType& N) const { return getDerived().isEqualTo(N); } + bool operator!=(const NodeType& N) const { return !operator==(N); } + + /// Return an iterator to the edge connecting the target node. + const_iterator findEdgeTo(const NodeType& targetNode) { + return std::find_if(edges.begin(), edges.end(), + [&targetNode](std::unique_ptr& edge) { + return edge->getTargetNode() == targetNode; + }); + } + + /// Add an edge between this node and a target node, only if it does not + /// already exist. Return a pointer to the newly-created edge. + EdgeType& addEdge(NodeType& targetNode) { + auto edgeIt = findEdgeTo(targetNode); + if (edgeIt == edges.end()) { + auto edge = std::make_unique(getDerived(), targetNode); + edges.emplace_back(std::move(edge)); + return *(edges.back().get()); + } + else { + return *((*edgeIt).get()); + } + } + + /// Remove an edge between this node and a target node. + /// Return true if the edge existed and was removed, and false otherwise. + bool removeEdge(NodeType& targetNode) { + auto edgeIt = findEdgeTo(targetNode); + if (edgeIt != edges.end()) { + edges.erase(edgeIt); + return true; + } + else { + return false; + } + } + + /// Populate a result vector of edges from this node to the specified target + /// node. Return true if at least one edge was found. + bool getEdgesTo(const NodeType& targetNode, std::vector& result) { + SLANG_ASSERT(result.empty() && "Expected the results parameter to be empty"); + for (auto& edge : edges) { + if (edge->getTargetNode() == targetNode) { + result.push_back(edge.get()); + } + } + return !result.empty(); + } + + /// Return the list of outgoing edges from this node. + const EdgeListType& getEdges() const { return edges; } + + /// Return the total number of edges outgoing from this node. + size_t outDegree() const { return edges.size(); } + + /// Remove all edges outgoing from this node. + void clearEdges() { edges.clear(); } + +protected: + // As the default implementation use address comparison for equality. + bool isEqualTo(const NodeType& node) const { return this == &node; } + // Cast the 'this' pointer to the derived type and return a reference. + NodeType& getDerived() { return *static_cast(this); } + const NodeType& getDerived() const { return *static_cast(this); } + + EdgeListType edges; +}; + +/// A directed graph. +/// Nodes and edges are stored in an adjacency list data structure, where the +/// DirectedGraph contains a vector of nodes, and each node contains a vector +/// of directed edges to other nodes. Multi-edges are not permitted. +template +class DirectedGraph { +private: + using NodeListType = std::vector>; + using EdgeListType = std::vector>; + +public: + using iterator = typename NodeListType::iterator; + using const_iterator = typename NodeListType::const_iterator; + using node_descriptor = size_t; + using edge_descriptor = EdgeType*; + using DirectedGraphType = DirectedGraph; + const size_t null_node = std::numeric_limits::max(); + + DirectedGraph() = default; + + const_iterator begin() const { return nodes.begin(); } + const_iterator end() const { return nodes.end(); } + iterator begin() { return nodes.begin(); } + iterator end() { return nodes.end(); } + + node_descriptor findNode(const NodeType& nodeToFind) const { + auto it = std::find_if(nodes.begin(), nodes.end(), + [&nodeToFind](const std::unique_ptr& node) { + return const_cast(*node) == nodeToFind; + }); + SLANG_ASSERT(it != nodes.end() && "Could not find node"); + return it - nodes.begin(); + } + + /// Given a node descriptor, return the node by reference. + NodeType& getNode(node_descriptor node) const { + SLANG_ASSERT(node < nodes.size() && "Node does not exist"); + return *nodes[node]; + } + + /// Add a node to the graph and return a reference to it. + NodeType& addNode() { + nodes.push_back(std::make_unique()); + return *(nodes.back().get()); + } + + /// Add an existing node to the graph and return a reference to it. + NodeType& addNode(std::unique_ptr node) { + nodes.push_back(std::move(node)); + return *(nodes.back().get()); + } + + /// Remove the specified node from the graph, including all edges that are + /// incident upon this node, and all edges that are outgoing from this node. + /// Return true if the node exists and was removed and false if it didn't + /// exist. + bool removeNode(NodeType& nodeToRemove) { + auto nodeToRemoveDesc = findNode(nodeToRemove); + if (nodeToRemoveDesc >= nodes.size()) { + // The node is not in the graph. + return false; + } + // Remove incoming edges to node for removal. + std::vector edgesToRemove; + for (auto& node : nodes) { + if (nodeToRemove == *node) { + // Skip the node to remove. + continue; + } + node->getEdgesTo(nodeToRemove, edgesToRemove); + for (auto* edge : edgesToRemove) { + node->removeEdge(nodeToRemove); + } + edgesToRemove.clear(); + } + // Remove the outgoing edges from the node for removal. + nodeToRemove.clearEdges(); + // Remove the node itself. + nodes.erase(std::next(nodes.begin(), nodeToRemoveDesc)); + return true; + } + + /// Add an edge between two existing nodes in the graph. + EdgeType& addEdge(NodeType& sourceNode, NodeType& targetNode) { + SLANG_ASSERT(findNode(sourceNode) < nodes.size() && "Source node does not exist"); + SLANG_ASSERT(findNode(targetNode) < nodes.size() && "Target node does not exist"); + return sourceNode.addEdge(targetNode); + } + + /// Remove an edge between the two specified vertices. Return true if the + /// edge exists and was removed, and false if it didn't exist. + bool removeEdge(NodeType& sourceNode, NodeType& targetNode) { + SLANG_ASSERT(findNode(sourceNode) < nodes.size() && "Source node does not exist"); + SLANG_ASSERT(findNode(targetNode) < nodes.size() && "Target node does not exist"); + return sourceNode.removeEdge(targetNode); + } + + /// Collect all edges that are incident to the specified node. + /// Return true if at least one edge was found and false otherwise. + bool getInEdgesToNode(const NodeType& targetNode, std::vector& results) const { + SLANG_ASSERT(results.empty() && "Expected the results parameter to be empty"); + std::vector tempResults; + for (auto& node : nodes) { + node->getEdgesTo(targetNode, tempResults); + results.insert(results.end(), tempResults.begin(), tempResults.end()); + tempResults.clear(); + } + return !results.empty(); + } + + /// Return the number of edges eminating from the specified node. + size_t outDegree(const NodeType& node) const { + SLANG_ASSERT(findNode(node) < nodes.size() && "Node does not exist"); + return node.outDegree(); + } + + /// Return the number of edges incident to the specified node. + size_t inDegree(const NodeType& node) const { + std::vector results; + getInEdgesToNode(node, results); + return results.size(); + } + + /// Return the size of the graph. + size_t numNodes() const { return nodes.size(); } + + /// Return the number of edges in the graph. + size_t numEdges() const { + size_t count = 0; + for (auto& node : nodes) { + count += node->outDegree(); + } + return count; + } + +protected: + NodeListType nodes; +}; + +} // namespace netlist diff --git a/tools/netlist/include/Netlist.h b/tools/netlist/include/Netlist.h new file mode 100644 index 000000000..570db0bd9 --- /dev/null +++ b/tools/netlist/include/Netlist.h @@ -0,0 +1,336 @@ +//------------------------------------------------------------------------------ +//! @file Netlist.h +//! @brief A class that represents the netlist of an AST. +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ +#pragma once + +#include "Config.h" +#include "Debug.h" +#include "DirectedGraph.h" +#include "fmt/color.h" +#include "fmt/format.h" +#include + +#include "slang/ast/ASTVisitor.h" +#include "slang/ast/symbols/CompilationUnitSymbols.h" +#include "slang/diagnostics/TextDiagnosticClient.h" +#include "slang/syntax/SyntaxTree.h" +#include "slang/syntax/SyntaxVisitor.h" +#include "slang/util/Util.h" + +using namespace slang; + +namespace netlist { + +class NetlistNode; +class NetlistEdge; + +enum class NodeKind { + None = 0, + PortDeclaration, + VariableDeclaration, + VariableReference, + VariableAlias +}; + +enum class VariableSelectorKind { ElementSelect, RangeSelect, MemberAccess }; + +/// Base class representing various selectors that can be applied to references +/// to structured variables (eg vectors, structs, unions). +struct VariableSelectorBase { + VariableSelectorKind kind; + explicit VariableSelectorBase(VariableSelectorKind kind) : kind(kind) {} + virtual ~VariableSelectorBase() = default; + virtual std::string toString() const = 0; + + template + T& as() { + SLANG_ASSERT(T::isKind(kind)); + return *(static_cast(this)); + } + + template + const T& as() const { + SLANG_ASSERT(T::isKind(kind)); + return const_cast(this->as()); + } +}; + +/// A variable selector representing an element selector. +struct VariableElementSelect : public VariableSelectorBase { + ConstantValue index; + + VariableElementSelect(ConstantValue index) : + VariableSelectorBase(VariableSelectorKind::ElementSelect), index(std::move(index)) {} + + static bool isKind(VariableSelectorKind otherKind) { + return otherKind == VariableSelectorKind::ElementSelect; + } + + size_t getIndexInt() const { + auto intValue = index.integer().as(); + SLANG_ASSERT(intValue && "could not convert index to size_t"); + return *intValue; + } + + std::string toString() const override { return fmt::format("[{}]", index.toString()); } +}; + +/// A variable selector representing a range selector. +struct VariableRangeSelect : public VariableSelectorBase { + ConstantValue leftIndex, rightIndex; + + VariableRangeSelect(ConstantValue leftIndex, ConstantValue rightIndex) : + VariableSelectorBase(VariableSelectorKind::RangeSelect), leftIndex(std::move(leftIndex)), + rightIndex(std::move(rightIndex)) {} + + static bool isKind(VariableSelectorKind otherKind) { + return otherKind == VariableSelectorKind::RangeSelect; + } + + size_t getLeftIndexInt() const { + auto intValue = leftIndex.integer().as(); + SLANG_ASSERT(intValue && "could not convert left index to size_t"); + return *intValue; + } + + size_t getRightIndexInt() const { + auto intValue = rightIndex.integer().as(); + SLANG_ASSERT(intValue && "could not convert right index to size_t"); + return *intValue; + } + + std::string toString() const override { + return fmt::format("[{}:{}]", leftIndex.toString(), rightIndex.toString()); + } +}; + +/// A variable selector representing member access of a structure. +struct VariableMemberAccess : public VariableSelectorBase { + std::string_view name; + + VariableMemberAccess(std::string_view name) : + VariableSelectorBase(VariableSelectorKind::MemberAccess), name(name) {} + + static bool isKind(VariableSelectorKind otherKind) { + return otherKind == VariableSelectorKind::MemberAccess; + } + + std::string toString() const override { return fmt::format(".{}", name); } +}; + +/// A class representing a dependency between two variables in the netlist. +class NetlistEdge : public DirectedEdge { +public: + NetlistEdge(NetlistNode& sourceNode, NetlistNode& targetNode) : + DirectedEdge(sourceNode, targetNode) {} + + void disable() { disabled = true; } + +public: + bool disabled{}; +}; + +/// A class representing a node in the netlist, corresponding to the appearance +/// of a variable symbol, with zero or more selectors applied. +class NetlistNode : public Node { +public: + NetlistNode(NodeKind kind, const ast::Symbol& symbol) : + ID(++nextID), kind(kind), symbol(symbol){}; + ~NetlistNode() override = default; + + template + T& as() { + SLANG_ASSERT(T::isKind(kind)); + return *(static_cast(this)); + } + + template + const T& as() const { + SLANG_ASSERT(T::isKind(kind)); + return const_cast(this->as()); + } + + /// Return the out degree of this node, including only enabled edges. + size_t outDegree() { + size_t count = 0; + for (auto& edge : edges) { + if (!edge->disabled) { + count++; + } + } + return count; + } + + std::string_view getName() const { return symbol.name; } + +public: + size_t ID; + NodeKind kind; + const ast::Symbol& symbol; + +private: + static size_t nextID; +}; + +size_t NetlistNode::nextID = 0; + +/// A class representing a port declaration. +class NetlistPortDeclaration : public NetlistNode { +public: + NetlistPortDeclaration(const ast::Symbol& symbol) : + NetlistNode(NodeKind::PortDeclaration, symbol) {} + + static bool isKind(NodeKind otherKind) { return otherKind == NodeKind::PortDeclaration; } + +public: + std::string hierarchicalPath; +}; + +/// A class representing a variable declaration. +class NetlistVariableDeclaration : public NetlistNode { +public: + NetlistVariableDeclaration(const ast::Symbol& symbol) : + NetlistNode(NodeKind::VariableDeclaration, symbol) {} + + static bool isKind(NodeKind otherKind) { return otherKind == NodeKind::VariableDeclaration; } + +public: + std::string hierarchicalPath; +}; + +/// A class representing a variable declaration alias. +class NetlistVariableAlias : public NetlistNode { +public: + NetlistVariableAlias(const ast::Symbol& symbol) : + NetlistNode(NodeKind::VariableAlias, symbol) {} + + static bool isKind(NodeKind otherKind) { return otherKind == NodeKind::VariableAlias; } + +public: + std::string hierarchicalPath; +}; + +/// A class representing a variable reference. +class NetlistVariableReference : public NetlistNode { +public: + using SelectorsListType = std::vector>; + + NetlistVariableReference(const ast::Symbol& symbol, const ast::Expression& expr, + bool leftOperand) : + NetlistNode(NodeKind::VariableReference, symbol), + expression(expr), leftOperand(leftOperand) {} + + void addElementSelect(const ConstantValue& index) { + selectors.emplace_back(std::make_unique(index)); + } + void addRangeSelect(const ConstantValue& leftIndex, const ConstantValue& rightIndex) { + selectors.emplace_back(std::make_unique(leftIndex, rightIndex)); + } + void addMemberAccess(std::string_view name) { + selectors.emplace_back(std::make_unique(name)); + } + + static bool isKind(NodeKind otherKind) { return otherKind == NodeKind::VariableReference; } + + bool isLeftOperand() const { return leftOperand; } + + /// Return a string representation of the selectors applied to this + /// variable reference. + std::string selectorString() const { + std::string buffer; + for (auto& selector : selectors) { + buffer += selector->toString(); + } + return buffer; + } + + /// Return a string representation of this variable reference. + std::string toString() const { return fmt::format("{}{}", getName(), selectorString()); } + +public: + /// The expression containing the variable reference. + const ast::Expression& expression; + /// Whether the variable reference is assignd to (ie appearing on the + /// left-hand side of an assignent), or otherwise read from. + bool leftOperand; + /// Selectors applied to the variable reference. + SelectorsListType selectors; +}; + +/// A class representing the design netlist. +class Netlist : public DirectedGraph { +public: + Netlist() : DirectedGraph() {} + + /// Add a port declaration node to the netlist. + NetlistPortDeclaration& addPortDeclaration(const ast::Symbol& symbol) { + auto nodePtr = std::make_unique(symbol); + auto& node = nodePtr->as(); + symbol.getHierarchicalPath(node.hierarchicalPath); + SLANG_ASSERT(lookupPort(nodePtr->hierarchicalPath) == nullptr && + "Port declaration already exists"); + nodes.push_back(std::move(nodePtr)); + DEBUG_PRINT("Add port decl " << node.hierarchicalPath << "\n"); + return node; + } + + /// Add a variable declaration node to the netlist. + NetlistVariableDeclaration& addVariableDeclaration(const ast::Symbol& symbol) { + auto nodePtr = std::make_unique(symbol); + auto& node = nodePtr->as(); + symbol.getHierarchicalPath(node.hierarchicalPath); + SLANG_ASSERT(lookupVariable(nodePtr->hierarchicalPath) == nullptr && + "Variable declaration already exists"); + nodes.push_back(std::move(nodePtr)); + DEBUG_PRINT("Add var decl " << node.hierarchicalPath << "\n"); + return node; + } + + /// Add a variable declaration alias node to the netlist. + NetlistVariableAlias& addVariableAlias(const ast::Symbol& symbol) { + auto nodePtr = std::make_unique(symbol); + auto& node = nodePtr->as(); + symbol.getHierarchicalPath(node.hierarchicalPath); + nodes.push_back(std::move(nodePtr)); + DEBUG_PRINT("Add var alias " << node.hierarchicalPath << "\n"); + return node; + } + + /// Add a variable reference node to the netlist. + NetlistVariableReference& addVariableReference(const ast::Symbol& symbol, + const ast::Expression& expr, bool leftOperand) { + auto nodePtr = std::make_unique(symbol, expr, leftOperand); + auto& node = nodePtr->as(); + nodes.push_back(std::move(nodePtr)); + DEBUG_PRINT("Add var ref " << symbol.name << "\n"); + return node; + } + + /// Find a variable declaration node in the netlist by hierarchical path. + /// TODO? Optimise this lookup by maintaining a list of declaration nodes. + NetlistNode* lookupVariable(const std::string& hierarchicalPath) { + auto compareNode = [&hierarchicalPath](const std::unique_ptr& node) { + return node->kind == NodeKind::VariableDeclaration && + node->as().hierarchicalPath == hierarchicalPath; + }; + auto it = std::find_if(begin(), end(), compareNode); + return it != end() ? it->get() : nullptr; + } + + /// Find a port declaration node in the netlist by hierarchical path. + /// TODO? Optimise this lookup by maintaining a list of port nodes. + NetlistNode* lookupPort(const std::string& hierarchicalPath) { + auto compareNode = [&hierarchicalPath](const std::unique_ptr& node) { + return node->kind == NodeKind::PortDeclaration && + node->as().hierarchicalPath == hierarchicalPath; + }; + auto it = std::find_if(begin(), end(), compareNode); + return it != end() ? it->get() : nullptr; + } +}; + +} // namespace netlist diff --git a/tools/netlist/include/NetlistPath.h b/tools/netlist/include/NetlistPath.h new file mode 100644 index 000000000..898738405 --- /dev/null +++ b/tools/netlist/include/NetlistPath.h @@ -0,0 +1,70 @@ +#pragma once + +#include "Netlist.h" +#include +#include + +namespace netlist { + +/// A class represening a path traversing nodes in the netlist. +class NetlistPath { +public: + using NodeListType = std::vector; + using iterator = typename NodeListType::iterator; + using const_iterator = typename NodeListType::const_iterator; + + NetlistPath() = default; + + NetlistPath(NodeListType nodes) : nodes(std::move(nodes)){}; + + const_iterator begin() const { return nodes.begin(); } + const_iterator end() const { return nodes.end(); } + iterator begin() { return nodes.begin(); } + iterator end() { return nodes.end(); } + + void add(NetlistNode& node) { nodes.push_back(&node); } + + void add(NetlistNode* node) { nodes.push_back(node); } + + void reverse() { std::reverse(nodes.begin(), nodes.end()); } + + size_t size() const { return nodes.size(); } + + bool empty() const { return nodes.empty(); } + + static std::string getSymbolHierPath(const ast::Symbol& symbol) { + std::string buffer; + symbol.getHierarchicalPath(buffer); + return buffer; + } + + /// Return index within the path if a variable reference matches the + /// specified syntax (ie including the hierarchical reference to the + /// variable name and selectors) and appears on the left-hand side of an + /// assignment (ie a target). + std::optional findVariable(std::string syntax) { + auto match = [this, &syntax](NetlistNode* node) { + if (node->kind == NodeKind::VariableReference) { + auto& varRefNode = node->as(); + auto hierPath = getSymbolHierPath(varRefNode.symbol); + auto selectorString = varRefNode.selectorString(); + return hierPath + selectorString == syntax; + } + else { + return false; + } + }; + auto it = std::find_if(nodes.begin(), nodes.end(), match); + if (it != nodes.end()) { + return std::make_optional(it - nodes.begin()); + } + else { + return std::nullopt; + } + } + +private: + NodeListType nodes; +}; + +} // namespace netlist diff --git a/tools/netlist/include/NetlistVisitor.h b/tools/netlist/include/NetlistVisitor.h new file mode 100644 index 000000000..810b816ce --- /dev/null +++ b/tools/netlist/include/NetlistVisitor.h @@ -0,0 +1,412 @@ +//------------------------------------------------------------------------------ +//! @file NetlistVisitor.h +//! @brief An AST visitor (and sub visitors) to construct a netlist +// representation. +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ +#pragma once + +#include "Config.h" +#include "Debug.h" +#include "Netlist.h" +#include "fmt/color.h" +#include "fmt/format.h" +#include + +#include "slang/ast/ASTVisitor.h" +#include "slang/ast/SemanticFacts.h" +#include "slang/ast/Symbol.h" +#include "slang/ast/symbols/BlockSymbols.h" +#include "slang/ast/symbols/CompilationUnitSymbols.h" +#include "slang/diagnostics/TextDiagnosticClient.h" +#include "slang/syntax/SyntaxTree.h" +#include "slang/syntax/SyntaxVisitor.h" +#include "slang/util/Util.h" + +using namespace slang; + +namespace netlist { + +static std::string getSymbolHierPath(const ast::Symbol& symbol) { + std::string buffer; + symbol.getHierarchicalPath(buffer); + return buffer; +} + +static void connectDeclToVar(Netlist& netlist, NetlistNode& varNode, + const std::string& hierarchicalPath) { + auto* variableNode = netlist.lookupVariable(hierarchicalPath); + netlist.addEdge(*variableNode, varNode); + DEBUG_PRINT( + fmt::format("Edge decl {} to ref {}\n", variableNode->getName(), varNode.getName())); +} + +static void connectVarToDecl(Netlist& netlist, NetlistNode& varNode, + const std::string& hierarchicalPath) { + auto* portNode = netlist.lookupVariable(hierarchicalPath); + netlist.addEdge(varNode, *portNode); + DEBUG_PRINT( + fmt::format("Edge ref {} to port ref {}\n", varNode.getName(), portNode->getName())); +} + +static void connectVarToVar(Netlist& netlist, NetlistNode& sourceVarNode, + NetlistNode& targetVarNode) { + netlist.addEdge(sourceVarNode, targetVarNode); + DEBUG_PRINT( + fmt::format("Edge ref {} to ref {}\n", sourceVarNode.getName(), targetVarNode.getName())); +} + +/// An AST visitor to identify variable references with selectors in +/// expressions, adding them to a visit list during the traversal. +class VariableReferenceVisitor : public ast::ASTVisitor { +public: + explicit VariableReferenceVisitor(Netlist& netlist, std::vector& visitList, + ast::EvalContext& evalCtx, bool leftOperand) : + netlist(netlist), + visitList(visitList), evalCtx(evalCtx), leftOperand(leftOperand) {} + + void handle(const ast::NamedValueExpression& expr) { + auto& node = netlist.addVariableReference(expr.symbol, expr, leftOperand); + visitList.push_back(&node); + for (auto* selector : selectors) { + if (selector->kind == ast::ExpressionKind::ElementSelect) { + auto index = selector->as().selector().eval(evalCtx); + node.addElementSelect(index); + } + else if (selector->kind == ast::ExpressionKind::RangeSelect) { + auto& rangeSelectExpr = selector->as(); + auto leftIndex = rangeSelectExpr.left().eval(evalCtx); + auto rightIndex = rangeSelectExpr.right().eval(evalCtx); + node.addRangeSelect(leftIndex, rightIndex); + } + else if (selector->kind == ast::ExpressionKind::MemberAccess) { + node.addMemberAccess(selector->as().member.name); + } + } + selectors.clear(); + } + + void handle(const ast::ElementSelectExpression& expr) { + selectors.push_back(&expr); + expr.value().visit(*this); + } + + void handle(const ast::RangeSelectExpression& expr) { + selectors.push_back(&expr); + expr.value().visit(*this); + } + + void handle(const ast::MemberAccessExpression& expr) { + selectors.push_back(&expr); + expr.value().visit(*this); + } + +private: + Netlist& netlist; + std::vector& visitList; + ast::EvalContext& evalCtx; + /// Whether this traversal is the target of an assignment or not. + bool leftOperand; + std::vector selectors; +}; + +/// An AST visitor to create dependencies between occurrances of variables +/// appearing on the left and right hand sides of assignment statements. +class AssignmentVisitor : public ast::ASTVisitor { +public: + explicit AssignmentVisitor(Netlist& netlist, ast::EvalContext& evalCtx) : + netlist(netlist), evalCtx(evalCtx) {} + + void handle(const ast::AssignmentExpression& expr) { + // Collect variable references on the left-hand side of the assignment. + std::vector visitListLHS, visitListRHS; + { + VariableReferenceVisitor visitor(netlist, visitListLHS, evalCtx, true); + expr.left().visit(visitor); + } + // Collect variable references on the right-hand side of the assignment. + { + VariableReferenceVisitor visitor(netlist, visitListRHS, evalCtx, false); + expr.right().visit(visitor); + } + // Add edge from LHS variable refrence to variable declaration. + for (auto* leftNode : visitListLHS) { + connectVarToDecl(netlist, *leftNode, getSymbolHierPath(leftNode->symbol)); + } + // Add edge from variable declaration to RHS variable reference. + for (auto* rightNode : visitListRHS) { + connectDeclToVar(netlist, *rightNode, getSymbolHierPath(rightNode->symbol)); + } + // Add edges form RHS expression terms to LHS expression terms. + for (auto* leftNode : visitListLHS) { + for (auto* rightNode : visitListRHS) { + connectVarToVar(netlist, *rightNode, *leftNode); + } + } + } + +private: + Netlist& netlist; + ast::EvalContext& evalCtx; +}; + +/// An AST visitor for proceural blocks that performs loop unrolling. +class ProceduralBlockVisitor : public ast::ASTVisitor { +public: + bool anyErrors = false; + + explicit ProceduralBlockVisitor(ast::Compilation& compilation, Netlist& netlist) : + netlist(netlist), evalCtx(compilation) { + evalCtx.pushEmptyFrame(); + } + + void handle(const ast::ForLoopStatement& loop) { + + // Conditions this loop cannot be unrolled. + if (loop.loopVars.empty() || !loop.stopExpr || loop.steps.empty() || anyErrors) { + loop.body.visit(*this); + return; + } + + // Attempt to unroll the loop. If we are unable to collect constant values + // for all loop variables across all iterations, we won't unroll at all. + auto handleFail = [&] { + for (auto var : loop.loopVars) { + evalCtx.deleteLocal(var); + } + loop.body.visit(*this); + }; + + // Create a list of the initialised loop variables. + SmallVector localPtrs; + for (auto var : loop.loopVars) { + auto init = var->getInitializer(); + if (!init) { + handleFail(); + return; + } + + auto cv = init->eval(evalCtx); + if (!cv) { + handleFail(); + return; + } + + localPtrs.push_back(evalCtx.createLocal(var, std::move(cv))); + } + + // Create a list of all the loop variable values across all iterations. + SmallVector values; + while (true) { + auto cv = step() ? loop.stopExpr->eval(evalCtx) : ConstantValue(); + if (!cv) { + handleFail(); + return; + } + + if (!cv.isTrue()) { + break; + } + + for (auto local : localPtrs) { + values.emplace_back(*local); + } + + for (auto step : loop.steps) { + if (!step->eval(evalCtx)) { + handleFail(); + return; + } + } + } + + // We have all the loop iteration values. Go back through + // and visit the loop body for each iteration. + for (size_t i = 0; i < values.size();) { + for (auto local : localPtrs) { + *local = std::move(values[i++]); + } + + loop.body.visit(*this); + if (anyErrors) + return; + } + } + + void handle(const ast::ConditionalStatement& stmt) { + // Evaluate the condition; if not constant visit both sides, + // otherwise visit only the side that matches the condition. + auto fallback = [&] { + stmt.ifTrue.visit(*this); + if (stmt.ifFalse) + stmt.ifFalse->visit(*this); + }; + + for (auto& cond : stmt.conditions) { + if (cond.pattern || !step()) { + fallback(); + return; + } + + auto result = cond.expr->eval(evalCtx); + if (!result) { + fallback(); + return; + } + + if (!result.isTrue()) { + if (stmt.ifFalse) + stmt.ifFalse->visit(*this); + return; + } + } + + stmt.ifTrue.visit(*this); + } + + void handle(const ast::ExpressionStatement& stmt) { + step(); + AssignmentVisitor visitor(netlist, evalCtx); + stmt.visit(visitor); + } + +private: + bool step() { + if (anyErrors || !evalCtx.step(SourceLocation::NoLocation)) { + anyErrors = true; + return false; + } + return true; + } + + Netlist& netlist; + ast::EvalContext evalCtx; +}; + +/// A visitor that traverses the AST and builds a netlist representation. +class NetlistVisitor : public ast::ASTVisitor { +public: + explicit NetlistVisitor(ast::Compilation& compilation, Netlist& netlist) : + compilation(compilation), netlist(netlist) {} + + /// Connect ports of a module instance to their corresponding variables. + void connectInstancePort(NetlistNode& port) { + if (auto* internalSymbol = port.symbol.as().internalSymbol) { + std::string pathBuffer; + internalSymbol->getHierarchicalPath(pathBuffer); + auto* variableNode = netlist.lookupVariable(pathBuffer); + switch (port.symbol.as().direction) { + case ast::ArgumentDirection::In: + netlist.addEdge(port, *variableNode); + break; + case ast::ArgumentDirection::Out: + netlist.addEdge(*variableNode, port); + break; + case ast::ArgumentDirection::InOut: + netlist.addEdge(port, *variableNode); + netlist.addEdge(*variableNode, port); + break; + case ast::ArgumentDirection::Ref: + break; + } + } + else { + SLANG_UNREACHABLE; + } + } + + /// Variable declaration. + void handle(const ast::VariableSymbol& symbol) {} + + /// Net declaration. + void handle(const ast::NetSymbol& symbol) {} + + /// Port declaration. + void handle(const ast::PortSymbol& symbol) {} + + /// Instance. + void handle(const ast::InstanceSymbol& symbol) { + // Body members. + // Variables first. + for (auto& member : symbol.body.members()) { + if (member.kind == ast::SymbolKind::Variable || member.kind == ast::SymbolKind::Net) { + netlist.addVariableDeclaration(member); + } + } + // Then ports. + for (auto& member : symbol.body.members()) { + if (member.kind == ast::SymbolKind::Port) { + // Create the port declaration netlist node. + auto& portNode = netlist.addPortDeclaration(member); + // Connected to to the corresponding local variable. + connectInstancePort(portNode); + } + } + // Handle connections to the ports of the instance. + for (auto* portConnection : symbol.getPortConnections()) { + // Collect variable references in the port expression. + std::vector exprVisitList; + ast::EvalContext evalCtx(compilation); + auto portDirection = portConnection->port.as().direction; + // The port is effectively the target of an assignment if it is an + // input. + bool isLeftOperand = portDirection == ast::ArgumentDirection::In || + portDirection == ast::ArgumentDirection::InOut; + VariableReferenceVisitor visitor(netlist, exprVisitList, evalCtx, isLeftOperand); + portConnection->getExpression()->visit(visitor); + // Given a port hookup of the form: + // .foo(expr(x, y)) + // Where expr() is an expression involving some variables. + // Then, add the following edges: + // Input port: + // var decl x -> var ref x -> port var ref foo + // Output port: + // var decl y <- var ref y <- port var ref foo + // InOut port: + // var decl x -> var ref x -> port var ref foo + // var decl y <- var ref y <- port var ref foo + for (auto* node : exprVisitList) { + switch (portDirection) { + case ast::ArgumentDirection::In: + connectDeclToVar(netlist, *node, getSymbolHierPath(node->symbol)); + connectVarToDecl(netlist, *node, getSymbolHierPath(portConnection->port)); + break; + case ast::ArgumentDirection::Out: + connectDeclToVar(netlist, *node, getSymbolHierPath(portConnection->port)); + connectVarToDecl(netlist, *node, getSymbolHierPath(node->symbol)); + break; + case ast::ArgumentDirection::InOut: + connectDeclToVar(netlist, *node, getSymbolHierPath(node->symbol)); + connectDeclToVar(netlist, *node, getSymbolHierPath(portConnection->port)); + connectVarToDecl(netlist, *node, getSymbolHierPath(node->symbol)); + connectVarToDecl(netlist, *node, getSymbolHierPath(portConnection->port)); + break; + case ast::ArgumentDirection::Ref: + break; + } + } + } + symbol.body.visit(*this); + } + + /// Procedural block. + void handle(const ast::ProceduralBlockSymbol& symbol) { + ProceduralBlockVisitor visitor(compilation, netlist); + symbol.visit(visitor); + } + + /// Continuous assignment statement. + void handle(const ast::ContinuousAssignSymbol& symbol) { + ast::EvalContext evalCtx(compilation); + AssignmentVisitor visitor(netlist, evalCtx); + symbol.visit(visitor); + } + +private: + ast::Compilation& compilation; + Netlist& netlist; +}; + +} // namespace netlist diff --git a/tools/netlist/include/PathFinder.h b/tools/netlist/include/PathFinder.h new file mode 100644 index 000000000..16a81627a --- /dev/null +++ b/tools/netlist/include/PathFinder.h @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +//! @file PathFinder.h +//! @brief Find paths between two points in the netlist. +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ +#pragma once + +#include "DepthFirstSearch.h" +#include "Netlist.h" +#include "NetlistPath.h" +#include +#include + +#include "slang/util/Util.h" + +namespace netlist { + +/// Find a path between two points in a netlist. +class PathFinder { +private: + /// Depth-first traversal produces a tree sub graph and as such, each node + /// can only have one parent node. This map captures these relationships and + /// is used to determine paths between leaf nodes and the root node of the + /// tree. + using TraversalMap = std::map; + + /// A visitor for the search that constructs the traversal map. + class Visitor { + public: + Visitor(Netlist& netlist, TraversalMap& traversalMap) : + netlist(netlist), traversalMap(traversalMap) {} + void visitNode(NetlistNode& node) {} + void visitEdge(NetlistEdge& edge) { + auto* sourceNode = &edge.getSourceNode(); + auto* targetNode = &edge.getTargetNode(); + SLANG_ASSERT(traversalMap.count(targetNode) == 0 && "node cannot have two parents"); + traversalMap[targetNode] = sourceNode; + } + + private: + Netlist& netlist; + TraversalMap& traversalMap; + }; + + /// A selector for edges that can be traversed in the search. + struct EdgePredicate { + EdgePredicate() = default; + bool operator()(const NetlistEdge& edge) { return !edge.disabled; } + }; + +public: + PathFinder(Netlist& netlist) : netlist(netlist) {} + + NetlistPath buildPath(TraversalMap& traversalMap, NetlistNode& startNode, + NetlistNode& endNode) { + // Empty path. + if (traversalMap.count(&endNode) == 0) { + return NetlistPath(); + } + // Single-node path. + if (startNode == endNode) { + return NetlistPath({&endNode}); + } + // Multi-node path. + NetlistPath path; + auto* nextNode = &endNode; + do { + nextNode = traversalMap[nextNode]; + // Add only the variable references to the path. + if (nextNode->kind == NodeKind::VariableReference) { + path.add(*nextNode); + } + } while (nextNode != &startNode); + path.reverse(); + return path; + } + + /// Find a path between two nodes in the netlist. + /// Return a NetlistPath object that is empty if the path does not exist. + NetlistPath find(NetlistNode& startNode, NetlistNode& endNode) { + TraversalMap traversalMap; + Visitor visitor(netlist, traversalMap); + DepthFirstSearch dfs(visitor, startNode); + return buildPath(traversalMap, startNode, endNode); + } + +private: + Netlist& netlist; +}; + +} // namespace netlist diff --git a/tools/netlist/include/SplitVariables.h b/tools/netlist/include/SplitVariables.h new file mode 100644 index 000000000..611ebb8bf --- /dev/null +++ b/tools/netlist/include/SplitVariables.h @@ -0,0 +1,139 @@ +//------------------------------------------------------------------------------ +//! @file SplitVariables.h +//! @brief Transform netlist variable nodes to reflect connections with +/// structured variables. +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ +#pragma once + +#include "Netlist.h" +#include "fmt/color.h" +#include "fmt/format.h" +#include + +#include "slang/ast/types/Type.h" +#include "slang/util/Util.h" + +namespace netlist { + +/// A class to perform a transformation on the netlist to split variable +/// declaration nodes of structured types into multiple parts based on the +/// types of the incoming and outgoing edges. +class SplitVariables { +public: + SplitVariables(Netlist& netlist) : netlist(netlist) { split(); } + +private: + /// Given two ranges [end1:start1] and [end2:start2], return true if there is + /// any overlap in values between them. + inline bool rangesOverlap(size_t start1, size_t end1, size_t start2, size_t end2) const { + return start1 <= end2 && start2 <= end1; + } + + /// Return true if the selection made by the target node intersects with the + /// selection made by the source node. + bool isIntersectingSelection(NetlistVariableReference& sourceNode, + NetlistVariableReference& targetNode) const { + bool match = true; + size_t selectorDepth = 0; + while (match) { + // Terminate the loop if either variable reference has no further + // selectors. + if (selectorDepth >= sourceNode.selectors.size() || + selectorDepth >= targetNode.selectors.size()) { + break; + } + auto& sourceSelector = sourceNode.selectors[selectorDepth]; + auto& targetSelector = targetNode.selectors[selectorDepth]; + SLANG_ASSERT(sourceSelector->kind == targetSelector->kind && "selectors do not match"); + switch (sourceSelector->kind) { + case VariableSelectorKind::ElementSelect: + // Matching selectors if the index is the same. + match = sourceSelector->as().getIndexInt() == + targetSelector->as().getIndexInt(); + break; + case VariableSelectorKind::RangeSelect: { + // Matching selectors if there is any overlap in the two ranges. + auto sourceRangeSel = sourceSelector->as(); + auto targetRangeSel = targetSelector->as(); + auto srcLeft = sourceRangeSel.getLeftIndexInt(); + auto srcRight = sourceRangeSel.getRightIndexInt(); + auto tgtLeft = targetRangeSel.getLeftIndexInt(); + auto tgtRight = targetRangeSel.getRightIndexInt(); + match = rangesOverlap(srcRight, srcLeft, tgtRight, tgtLeft); + break; + } + case VariableSelectorKind::MemberAccess: + // Matching selectors if the member names match. + match = sourceSelector->as().name == + targetSelector->as().name; + break; + } + selectorDepth++; + } + return match; + } + + void split() { + std::vector> + modifications; + // Find each variable declaration nodes in the graph that has multiple + // outgoing edges. + for (auto& node : netlist) { + if (node->kind == NodeKind::VariableDeclaration && node->outDegree() > 1) { + auto& varDeclNode = node->as(); + auto& varType = varDeclNode.symbol.getDeclaredType()->getType(); + DEBUG_PRINT(fmt::format("Variable {} has type {}\n", varDeclNode.hierarchicalPath, + varType.toString())); + std::vector inEdges; + netlist.getInEdgesToNode(*node, inEdges); + // Find pairs of input and output edges that are attached to variable + // refertence nodes. Eg. + // var ref -> var decl -> var ref + // If the variable references select the same part of a structured + // variable, then transform them into: + // var ref -> var alias -> var ref + // And mark the original edges as disabled. + for (auto* inEdge : inEdges) { + for (auto& outEdge : *node) { + if (inEdge->getSourceNode().kind == NodeKind::VariableReference && + outEdge->getTargetNode().kind == NodeKind::VariableReference) { + auto& sourceVarRef = + inEdge->getSourceNode().as(); + auto& targetVarRef = + outEdge->getTargetNode().as(); + auto match = isIntersectingSelection(sourceVarRef, targetVarRef); + if (match) { + DEBUG_PRINT( + fmt::format("New dependency through variable {} -> {}\n", + sourceVarRef.toString(), targetVarRef.toString())); + modifications.emplace_back(&varDeclNode, inEdge, outEdge.get()); + } + } + } + } + } + } + // Apply the operations to the graph. + for (auto& modification : modifications) { + auto* varDeclNode = std::get<0>(modification); + auto* inEdge = std::get<1>(modification); + auto* outEdge = std::get<2>(modification); + // Disable the existing edges. + inEdge->disable(); + outEdge->disable(); + // Create a new node that aliases the variable declaration. + auto& varAliasNode = netlist.addVariableAlias(varDeclNode->symbol); + // Create edges through the new node. + inEdge->getSourceNode().addEdge(varAliasNode); + varAliasNode.addEdge(outEdge->getTargetNode()); + } + } + +private: + Netlist& netlist; +}; + +} // namespace netlist diff --git a/tools/netlist/netlist.cpp b/tools/netlist/netlist.cpp new file mode 100644 index 000000000..037e8cd13 --- /dev/null +++ b/tools/netlist/netlist.cpp @@ -0,0 +1,308 @@ +//------------------------------------------------------------------------------ +//! @file netlist.cpp +//! @brief The slang netlist tool +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ + +#include "Netlist.h" + +#include "NetlistVisitor.h" +#include "PathFinder.h" +#include "SplitVariables.h" +#include "fmt/color.h" +#include "fmt/format.h" +#include +#include +#include +#include +#include +#include + +#include "slang/ast/ASTSerializer.h" +#include "slang/diagnostics/DiagnosticEngine.h" +#include "slang/diagnostics/Diagnostics.h" +#include "slang/driver/Driver.h" +#include "slang/text/FormatBuffer.h" +#include "slang/text/Json.h" +#include "slang/util/TimeTrace.h" +#include "slang/util/Util.h" +#include "slang/util/Version.h" + +using namespace slang; +using namespace slang::ast; +using namespace slang::driver; +using namespace netlist; + +namespace slang::diag { + +inline constexpr DiagCode VariableReference(DiagSubsystem::Netlist, 0); + +} // namespace slang::diag + +void writeToFile(std::string_view fileName, std::string_view contents); + +void printJson(Compilation& compilation, const std::string& fileName, + const std::vector& scopes) { + JsonWriter writer; + writer.setPrettyPrint(true); + ASTSerializer serializer(compilation, writer); + if (scopes.empty()) { + serializer.serialize(compilation.getRoot()); + } + else { + for (auto& scopeName : scopes) { + auto sym = compilation.getRoot().lookupName(scopeName); + if (sym) { + serializer.serialize(*sym); + } + } + } + writeToFile(fileName, writer.view()); +} + +void printDOT(const Netlist& netlist, const std::string& fileName) { + slang::FormatBuffer buffer; + buffer.append("digraph {\n"); + buffer.append(" node [shape=record];\n"); + for (auto& node : netlist) { + switch (node->kind) { + case NodeKind::PortDeclaration: { + auto& portDecl = node->as(); + buffer.format(" N{} [label=\"Port declaration\\n{}\"]\n", node->ID, + portDecl.hierarchicalPath); + break; + } + case NodeKind::VariableDeclaration: { + auto& varDecl = node->as(); + buffer.format(" N{} [label=\"Variable declaration\\n{}\"]\n", node->ID, + varDecl.hierarchicalPath); + break; + } + case NodeKind::VariableAlias: { + auto& varAlias = node->as(); + buffer.format(" N{} [label=\"Variable alias\\n{}\"]\n", node->ID, + varAlias.hierarchicalPath); + break; + } + case NodeKind::VariableReference: { + auto& varRef = node->as(); + buffer.format(" N{} [label=\"{}\\n{}\"]\n", node->ID, varRef.toString(), + varRef.isLeftOperand() ? "[Assigned to]" : ""); + break; + } + default: + SLANG_UNREACHABLE; + } + } + for (auto& node : netlist) { + for (auto& edge : node->getEdges()) { + if (!edge->disabled) { + buffer.format(" N{} -> N{}\n", node->ID, edge->getTargetNode().ID); + } + } + } + buffer.append("}\n"); + writeToFile(fileName, std::string_view(buffer.data())); +} + +template +void writeToFile(Stream& os, std::string_view fileName, String contents) { + os.write(contents.data(), contents.size()); + os.flush(); + if (!os) { + SLANG_THROW(std::runtime_error(fmt::format("Unable to write to '{}'", fileName))); + } +} + +#if defined(_MSC_VER) + +void writeToFile(std::string_view fileName, std::string_view contents) { + if (fileName == "-") { + writeToFile(std::wcout, "stdout", widen(contents)); + } + else { + std::ofstream file(widen(fileName)); + writeToFile(file, fileName, contents); + } +} + +#else + +void writeToFile(std::string_view fileName, std::string_view contents) { + if (fileName == "-") { + writeToFile(std::cout, "stdout", contents); + } + else { + std::ofstream file{std::string(fileName)}; + writeToFile(file, fileName, contents); + } +} + +#endif + +void reportPath(Compilation& compilation, const NetlistPath& path) { + DiagnosticEngine diagEngine(*compilation.getSourceManager()); + diagEngine.setMessage(diag::VariableReference, "variable {}"); + diagEngine.setSeverity(diag::VariableReference, DiagnosticSeverity::Note); + auto textDiagClient = std::make_shared(); + textDiagClient->showColors(true); + textDiagClient->showLocation(true); + textDiagClient->showSourceLine(true); + textDiagClient->showHierarchyInstance(true); + diagEngine.addClient(textDiagClient); + for (auto* node : path) { + auto* SM = compilation.getSourceManager(); + auto& location = node->symbol.location; + auto bufferID = location.buffer(); + SLANG_ASSERT(node->kind == NodeKind::VariableReference); + const auto& varRefNode = node->as(); + Diagnostic diagnostic(diag::VariableReference, varRefNode.expression.sourceRange.start()); + diagnostic << varRefNode.expression.sourceRange; + if (varRefNode.isLeftOperand()) { + diagnostic << fmt::format("{} assigned to", varRefNode.getName()); + } + else { + diagnostic << fmt::format("{} read from", varRefNode.getName()); + } + diagEngine.issue(diagnostic); + OS::print(fmt::format("{}\n", textDiagClient->getString())); + textDiagClient->clear(); + } +} + +int main(int argc, char** argv) { + + driver::Driver driver; + driver.addStandardArgs(); + + std::optional showHelp; + std::optional showVersion; + std::optional quiet; + std::optional debug; + driver.cmdLine.add("-h,--help", showHelp, "Display available options"); + driver.cmdLine.add("--version", showVersion, "Display version information and exit"); + driver.cmdLine.add("-q,--quiet", quiet, "Suppress non-essential output"); + driver.cmdLine.add("-d,--debug", debug, "Output debugging information"); + + std::optional astJsonFile; + driver.cmdLine.add( + "--ast-json", astJsonFile, + "Dump the compiled AST in JSON format to the specified file, or '-' for stdout", "", + /* isFileName */ true); + + std::vector astJsonScopes; + driver.cmdLine.add("--ast-json-scope", astJsonScopes, + "When dumping AST to JSON, include only the scopes specified by the " + "given hierarchical paths", + ""); + + std::optional netlistDotFile; + driver.cmdLine.add("--netlist-dot", netlistDotFile, + "Dump the netlist in DOT format to the specified file, or '-' for stdout", + "", + /* isFileName */ true); + + std::optional fromPointName; + driver.cmdLine.add("--from", fromPointName, "Specify a start point from which to trace a path", + ""); + + std::optional toPointName; + driver.cmdLine.add("--to", toPointName, "Specify a finish point to trace a path to", ""); + + if (!driver.parseCommandLine(argc, argv)) { + return 1; + } + + if (showHelp == true) { + std::cout << fmt::format( + "{}\n", driver.cmdLine.getHelpText("slang SystemVerilog netlist tool").c_str()); + return 0; + } + + if (showVersion == true) { + std::cout << fmt::format("slang-netlist version {}.{}.{}+{}\n", VersionInfo::getMajor(), + VersionInfo::getMinor(), VersionInfo::getPatch(), + std::string(VersionInfo::getHash()).c_str()); + return 0; + } + + if (!driver.processOptions()) { + return 2; + } + + if (debug) { + Config::getInstance().debugEnabled = true; + } + + SLANG_TRY { + + bool ok = driver.parseAllSources(); + + auto compilation = driver.createCompilation(); + ok &= driver.reportCompilation(*compilation, *quiet); + + if (!ok) { + return ok; + } + + if (astJsonFile) { + printJson(*compilation, *astJsonFile, astJsonScopes); + return 0; + } + + // Create the netlist by traversing the AST. + Netlist netlist; + NetlistVisitor visitor(*compilation, netlist); + compilation->getRoot().visit(visitor); + SplitVariables splitVariables(netlist); + DEBUG_PRINT(fmt::format("Netlist has {} nodes and {} edges\n", netlist.numNodes(), + netlist.numEdges())); + + // Output a DOT file of the netlist. + if (netlistDotFile) { + printDOT(netlist, *netlistDotFile); + return 0; + } + + // Find a point-to-point path in the netlist. + if (fromPointName.has_value() && toPointName.has_value()) { + if (!fromPointName.has_value()) { + SLANG_THROW(std::runtime_error("please specify a start point using --from ")); + } + if (!toPointName.has_value()) { + SLANG_THROW(std::runtime_error("please specify a finish point using --to ")); + } + auto fromPoint = netlist.lookupVariable(*fromPointName); + if (fromPoint == nullptr) { + SLANG_THROW(std::runtime_error( + fmt::format("could not find start point: {}", *fromPointName))); + } + auto toPoint = netlist.lookupVariable(*toPointName); + if (toPoint == nullptr) { + SLANG_THROW(std::runtime_error( + fmt::format("could not find finish point: {}", *toPointName))); + } + PathFinder pathFinder(netlist); + auto path = pathFinder.find(*fromPoint, *toPoint); + if (path.empty()) { + SLANG_THROW(std::runtime_error( + fmt::format("no path between {} and {}", *fromPointName, *toPointName))); + } + // Report the path. + reportPath(*compilation, path); + } + + // No action performed. + return 1; + } + SLANG_CATCH(const std::exception& e) { +#if __cpp_exceptions + OS::printE(fmt::format("{}\n", e.what())); +#endif + return 1; + } + + return 0; +} diff --git a/tools/netlist/tests/CMakeLists.txt b/tools/netlist/tests/CMakeLists.txt new file mode 100644 index 000000000..9204eaca9 --- /dev/null +++ b/tools/netlist/tests/CMakeLists.txt @@ -0,0 +1,22 @@ +# ~~~ +# SPDX-FileCopyrightText: Michael Popoloski +# SPDX-License-Identifier: MIT +# ~~~ + +add_executable( + netlist_unittests + ../../../tests/unittests/main.cpp ../../../tests/unittests/Test.cpp + DepthFirstSearchTests.cpp DirectedGraphTests.cpp NetlistTests.cpp) + +target_link_libraries( + netlist_unittests + PRIVATE slang::slang Catch2::Catch2 fmt::fmt + PUBLIC ${SLANG_LIBRARIES}) + +target_compile_definitions(netlist_unittests PRIVATE UNITTESTS) + +target_include_directories( + netlist_unittests PRIVATE ../include ../../../include + ../../../tests/unittests) + +add_test(NAME netlist_unittests COMMAND netlist_unittests) diff --git a/tools/netlist/tests/DepthFirstSearchTests.cpp b/tools/netlist/tests/DepthFirstSearchTests.cpp new file mode 100644 index 000000000..c31b0738e --- /dev/null +++ b/tools/netlist/tests/DepthFirstSearchTests.cpp @@ -0,0 +1,113 @@ +//------------------------------------------------------------------------------ +//! @file DepthFirstSearchTests.cpp +//! @brief Depth-first search unit tests. +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ + +#include "DepthFirstSearch.h" +#include "DirectedGraph.h" +#include "Test.h" + +using namespace netlist; + +struct TestNode; +struct TestEdge; + +size_t nodeIDs = 0; + +struct TestNode : public Node { + size_t ID; + TestNode() : ID(nodeIDs++){}; +}; + +struct TestEdge : public DirectedEdge { + TestEdge(TestNode& sourceNode, TestNode& targetNode) : DirectedEdge(sourceNode, targetNode) {} +}; + +struct TestVisitor { + std::vector nodes; + std::vector edges; + TestVisitor() = default; + void visitNode(TestNode& node) { nodes.push_back(&node); }; + void visitEdge(TestEdge& edge) { edges.push_back(&edge); }; +}; + +TEST_CASE("Depth-first search on a ring") { + DirectedGraph graph; + auto& n0 = graph.addNode(); + auto& n1 = graph.addNode(); + auto& n2 = graph.addNode(); + auto& n3 = graph.addNode(); + auto& n4 = graph.addNode(); + graph.addEdge(n0, n1); + graph.addEdge(n1, n2); + graph.addEdge(n2, n3); + graph.addEdge(n3, n4); + graph.addEdge(n4, n0); + TestVisitor visitor; + DepthFirstSearch dfs(visitor, n3); + CHECK(visitor.nodes.size() == 5); + CHECK(visitor.edges.size() == 4); + CHECK(*visitor.nodes[0] == n3); + CHECK(*visitor.nodes[1] == n4); + CHECK(*visitor.nodes[2] == n0); + CHECK(*visitor.nodes[3] == n1); + CHECK(*visitor.nodes[4] == n2); +} + +TEST_CASE("Depth-first search on a tree") { + DirectedGraph graph; + auto& n0 = graph.addNode(); + auto& n1 = graph.addNode(); + auto& n2 = graph.addNode(); + auto& n3 = graph.addNode(); + auto& n4 = graph.addNode(); + auto& n5 = graph.addNode(); + auto& n6 = graph.addNode(); + graph.addEdge(n0, n1); + graph.addEdge(n0, n2); + graph.addEdge(n1, n3); + graph.addEdge(n1, n4); + graph.addEdge(n2, n5); + graph.addEdge(n2, n6); + TestVisitor visitor; + DepthFirstSearch dfs(visitor, n0); + CHECK(visitor.nodes.size() == 7); + CHECK(visitor.edges.size() == 6); + CHECK(*visitor.nodes[0] == n0); + CHECK(*visitor.nodes[1] == n1); + CHECK(*visitor.nodes[2] == n3); + CHECK(*visitor.nodes[3] == n4); + CHECK(*visitor.nodes[4] == n2); + CHECK(*visitor.nodes[5] == n5); + CHECK(*visitor.nodes[6] == n6); +} + +// A predicate to select edges that only connect nodes with even IDs. +struct EdgesToOnlyEvenNodes { + EdgesToOnlyEvenNodes() = default; + bool operator()(const TestEdge& edge) { return edge.getTargetNode().ID % 2 == 0; } +}; + +TEST_CASE("Depth-first search with an edge predicate") { + DirectedGraph graph; + auto& n0 = graph.addNode(); + auto& n1 = graph.addNode(); + auto& n2 = graph.addNode(); + auto& n3 = graph.addNode(); + auto& n4 = graph.addNode(); + // Fan out n0 to all other nodes. + graph.addEdge(n0, n1); + graph.addEdge(n0, n2); + graph.addEdge(n0, n3); + graph.addEdge(n0, n4); + TestVisitor visitor; + DepthFirstSearch dfs(visitor, n0); + CHECK(visitor.nodes.size() == 3); + CHECK(visitor.edges.size() == 2); + CHECK(*visitor.nodes[0] == n0); + CHECK(*visitor.nodes[1] == n2); + CHECK(*visitor.nodes[2] == n4); +} diff --git a/tools/netlist/tests/DirectedGraphTests.cpp b/tools/netlist/tests/DirectedGraphTests.cpp new file mode 100644 index 000000000..38f0045c9 --- /dev/null +++ b/tools/netlist/tests/DirectedGraphTests.cpp @@ -0,0 +1,173 @@ +//------------------------------------------------------------------------------ +//! @file DirectedGraphTest.cpp +//! @brief Directed graph ADT unit tests. +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ + +#include "DirectedGraph.h" +#include "Test.h" + +using namespace netlist; + +struct TestNode; +struct TestEdge; + +struct TestNode : public Node { + TestNode() = default; +}; + +struct TestEdge : public DirectedEdge { + TestEdge(TestNode& sourceNode, TestNode& targetNode) : DirectedEdge(sourceNode, targetNode) {} +}; + +TEST_CASE("Test node and edge equality") { + DirectedGraph graph; + auto& n0 = graph.addNode(); + auto& n0Alias = graph.getNode(graph.findNode(n0)); + CHECK(n0 == n0Alias); + auto& n1 = graph.addNode(); + CHECK(n0 != n1); + auto& n2 = graph.addNode(); + auto& e0 = n0.addEdge(n1); + auto& e1 = n1.addEdge(n2); + auto* e0Alias = n0.findEdgeTo(n1)->get(); + CHECK(e0 == *e0Alias); + CHECK(e0 != e1); +} + +TEST_CASE("Test basic connectivity") { + DirectedGraph graph; + auto& n0 = graph.addNode(); + auto& n1 = graph.addNode(); + auto& n2 = graph.addNode(); + auto& n3 = graph.addNode(); + CHECK(graph.numNodes() == 4); + CHECK(graph.numEdges() == 0); + auto& e0 = graph.addEdge(n0, n1); + auto& e1 = graph.addEdge(n0, n2); + auto& e2 = graph.addEdge(n0, n3); + auto& e3 = graph.addEdge(n1, n2); + auto& e4 = graph.addEdge(n1, n3); + auto& e5 = graph.addEdge(n2, n3); + CHECK(graph.numEdges() == 6); + // Edge target nodes. + CHECK(e0.getTargetNode() == n1); + CHECK(e1.getTargetNode() == n2); + CHECK(e2.getTargetNode() == n3); + CHECK(e3.getTargetNode() == n2); + CHECK(e4.getTargetNode() == n3); + CHECK(e5.getTargetNode() == n3); + // Edge source nodes. + CHECK(e0.getSourceNode() == n0); + CHECK(e1.getSourceNode() == n0); + CHECK(e2.getSourceNode() == n0); + CHECK(e3.getSourceNode() == n1); + CHECK(e4.getSourceNode() == n1); + CHECK(e5.getSourceNode() == n2); + // Out degrees. + CHECK(graph.outDegree(n0) == 3); + CHECK(graph.outDegree(n1) == 2); + CHECK(graph.outDegree(n2) == 1); + CHECK(graph.outDegree(n3) == 0); + CHECK(n0.outDegree() == 3); + CHECK(n1.outDegree() == 2); + CHECK(n2.outDegree() == 1); + CHECK(n3.outDegree() == 0); + // In degrees. + CHECK(graph.inDegree(n0) == 0); + CHECK(graph.inDegree(n1) == 1); + CHECK(graph.inDegree(n2) == 2); + CHECK(graph.inDegree(n3) == 3); +} + +TEST_CASE("Test removing nodes") { + DirectedGraph graph; + auto& n0 = graph.addNode(); + auto& n1 = graph.addNode(); + auto& n2 = graph.addNode(); + // Create a ring of n0, n1, n2 + graph.addEdge(n0, n1); + graph.addEdge(n1, n2); + graph.addEdge(n2, n0); + CHECK(graph.numNodes() == 3); + CHECK(graph.numEdges() == 3); + // Remove n0 + CHECK(graph.removeNode(n0)); + CHECK(graph.inDegree(n1) == 0); + CHECK(graph.outDegree(n1) == 1); + CHECK(graph.inDegree(n2) == 1); + CHECK(graph.outDegree(n2) == 0); + // Remoe n1 + CHECK(graph.removeNode(n1)); + CHECK(graph.inDegree(n2) == 0); + CHECK(graph.outDegree(n2) == 0); + // Remove n2 + CHECK(graph.removeNode(n2)); + CHECK(graph.numNodes() == 0); +} + +TEST_CASE("Test removing edges") { + DirectedGraph graph; + auto& n0 = graph.addNode(); + auto& n1 = graph.addNode(); + auto& n2 = graph.addNode(); + // Create a ring of n0 -e0- n1 -e1- n2 -e2- + graph.addEdge(n0, n1); // e0 + graph.addEdge(n1, n2); // e1 + graph.addEdge(n2, n0); // e2 + CHECK(graph.numNodes() == 3); + CHECK(graph.numEdges() == 3); + // Remove e0. + CHECK(graph.removeEdge(n0, n1)); + CHECK(graph.outDegree(n0) == 0); + CHECK(graph.inDegree(n1) == 0); + // Remove e1. + CHECK(graph.removeEdge(n1, n2)); + CHECK(graph.outDegree(n1) == 0); + CHECK(graph.inDegree(n2) == 0); + // Remove e2. + CHECK(graph.removeEdge(n2, n0)); + CHECK(graph.outDegree(n2) == 0); + CHECK(graph.inDegree(n0) == 0); + // Edges no longer exist + CHECK(!graph.removeEdge(n0, n1)); + CHECK(!graph.removeEdge(n1, n2)); + CHECK(!graph.removeEdge(n2, n0)); +} + +TEST_CASE("Test iterating over nodes and node's edges") { + DirectedGraph graph; + auto& n0 = graph.addNode(); + auto& n1 = graph.addNode(); + auto& n2 = graph.addNode(); + auto& n3 = graph.addNode(); + graph.addEdge(n0, n1); + graph.addEdge(n0, n2); + graph.addEdge(n0, n3); + // Nodes. + { + size_t count = 0; + for (auto it = graph.begin(); it != graph.end(); it++) { + count++; + } + CHECK(count == 4); + } + // n0 edges. + { + size_t count = 0; + for (auto it = n0.begin(); it != n0.end(); it++) { + count++; + } + CHECK(count == 3); + } + // n1 edges. + { + size_t count = 0; + for (auto it = n1.begin(); it != n1.end(); it++) { + count++; + } + CHECK(count == 0); + } +} diff --git a/tools/netlist/tests/NetlistTests.cpp b/tools/netlist/tests/NetlistTests.cpp new file mode 100644 index 000000000..798b1d89e --- /dev/null +++ b/tools/netlist/tests/NetlistTests.cpp @@ -0,0 +1,412 @@ +//------------------------------------------------------------------------------ +//! @file NetlistTest.cpp +//! @brief Netlist unit tests. +// +// SPDX-FileCopyrightText: Michael Popoloski +// SPDX-License-Identifier: MIT +//------------------------------------------------------------------------------ + +#include "Netlist.h" +#include "NetlistVisitor.h" +#include "PathFinder.h" +#include "SplitVariables.h" +#include "Test.h" + +using namespace netlist; + +Netlist createNetlist(Compilation& compilation) { + Netlist netlist; + NetlistVisitor visitor(compilation, netlist); + compilation.getRoot().visit(visitor); + SplitVariables splitVariables(netlist); + return netlist; +} + +//===---------------------------------------------------------------------===// +// Basic tests +//===---------------------------------------------------------------------===// + +TEST_CASE("Empty module") { + // Test the simplest path can be traced through a module. + auto tree = SyntaxTree::fromText(R"( +module empty (input logic i_value, output logic o_value); +endmodule +)"); + Compilation compilation; + compilation.addSyntaxTree(tree); + NO_COMPILATION_ERRORS; + auto netlist = createNetlist(compilation); + CHECK(netlist.numNodes() == 4); + CHECK(netlist.numEdges() == 2); + // Lookup the two ports in the netlist. + auto* inPort = netlist.lookupPort("empty.i_value"); + CHECK(inPort != nullptr); + auto* outPort = netlist.lookupPort("empty.o_value"); + CHECK(outPort != nullptr); +} + +TEST_CASE("Pass through a module") { + // Test the simplest path through a module. + auto tree = SyntaxTree::fromText(R"( +module passthrough (input logic i_value, output logic o_value); + assign o_value = i_value; +endmodule +)"); + Compilation compilation; + compilation.addSyntaxTree(tree); + NO_COMPILATION_ERRORS; + auto netlist = createNetlist(compilation); + CHECK(netlist.numNodes() == 6); + CHECK(netlist.numEdges() == 5); + // Lookup the two ports in the netlist. + auto* inPort = netlist.lookupPort("passthrough.i_value"); + CHECK(inPort != nullptr); + auto* outPort = netlist.lookupPort("passthrough.o_value"); + CHECK(outPort != nullptr); + PathFinder pathFinder(netlist); + // Valid i_value -> o_value + CHECK(!pathFinder.find(*inPort, *outPort).empty()); + // Invalid o_value -> i_value + CHECK(pathFinder.find(*outPort, *inPort).empty()); +} + +TEST_CASE("Chain of assignments in a sequence using variables") { + // Test that correct dependencies can be formed from procedural and + // continuous assignments. + auto tree = SyntaxTree::fromText(R"( +module chain_vars (input logic i_value, output logic o_value); + + logic a, b, c, d, e; + + assign a = i_value; + + always_comb begin + b = a; + c = b; + d = c; + end + + assign e = d; + assign o_value = e; + +endmodule +)"); + Compilation compilation; + compilation.addSyntaxTree(tree); + NO_COMPILATION_ERRORS; + auto netlist = createNetlist(compilation); + CHECK(netlist.numNodes() == 21); + CHECK(netlist.numEdges() == 20); + PathFinder pathFinder(netlist); + // Find path i_value -> o_value + auto path = pathFinder.find(*netlist.lookupPort("chain_vars.i_value"), + *netlist.lookupPort("chain_vars.o_value")); + CHECK(path.size() == 12); + CHECK(*path.findVariable("chain_vars.a") == 1); + CHECK(*path.findVariable("chain_vars.b") == 3); + CHECK(*path.findVariable("chain_vars.c") == 5); + CHECK(*path.findVariable("chain_vars.d") == 7); + CHECK(*path.findVariable("chain_vars.e") == 9); +} + +//===---------------------------------------------------------------------===// +// Tests for module instance connectivity. +//===---------------------------------------------------------------------===// + +TEST_CASE("Signal passthrough with a nested module") { + // Test that a nested module is connected correctly. + auto tree = SyntaxTree::fromText(R"( +module passthrough(input logic i_value, output logic o_value); + assign o_value = i_value; +endmodule + +module nested_passthrough(input logic i_value, output logic o_value); + passthrough foo( + .i_value(i_value), + .o_value(o_value)); +endmodule +)"); + Compilation compilation; + compilation.addSyntaxTree(tree); + NO_COMPILATION_ERRORS; + auto netlist = createNetlist(compilation); + PathFinder pathFinder(netlist); + auto path = pathFinder.find(*netlist.lookupPort("nested_passthrough.i_value"), + *netlist.lookupPort("nested_passthrough.o_value")); + CHECK(*path.findVariable("nested_passthrough.foo.i_value") == 1); + CHECK(*path.findVariable("nested_passthrough.foo.o_value") == 2); +} + +TEST_CASE("Signal passthrough with a chain of two nested modules") { + // Test that two nested module are connected correctly. + auto tree = SyntaxTree::fromText(R"( +module passthrough(input logic i_value, output logic o_value); + assign o_value = i_value; +endmodule + +module nested_passthrough(input logic i_value, output logic o_value); + logic value; + passthrough foo_a( + .i_value(i_value), + .o_value(value)); + passthrough foo_b( + .i_value(value), + .o_value(o_value)); +endmodule +)"); + Compilation compilation; + compilation.addSyntaxTree(tree); + NO_COMPILATION_ERRORS; + auto netlist = createNetlist(compilation); + PathFinder pathFinder(netlist); + auto path = pathFinder.find(*netlist.lookupPort("nested_passthrough.i_value"), + *netlist.lookupPort("nested_passthrough.o_value")); + CHECK(path.size() == 8); + CHECK(*path.findVariable("nested_passthrough.foo_a.i_value") == 1); + CHECK(*path.findVariable("nested_passthrough.foo_a.o_value") == 2); + CHECK(*path.findVariable("nested_passthrough.foo_b.i_value") == 5); + CHECK(*path.findVariable("nested_passthrough.foo_b.o_value") == 6); +} + +//===---------------------------------------------------------------------===// +// Tests for variable splitting +//===---------------------------------------------------------------------===// + +TEST_CASE("Chain of assignments in a sequence using a vector") { + // As above but this time using a packed array. + auto tree = SyntaxTree::fromText(R"( +module chain_array (input logic i_value, output logic o_value); + + logic [4:0] x; + + assign x[0] = i_value; + + always_comb begin + x[1] = x[0]; + x[2] = x[1]; + x[3] = x[2]; + end + + assign x[4] = x[3]; + assign o_value = x[4]; + +endmodule +)"); + Compilation compilation; + compilation.addSyntaxTree(tree); + NO_COMPILATION_ERRORS; + auto netlist = createNetlist(compilation); + PathFinder pathFinder(netlist); + auto path = pathFinder.find(*netlist.lookupPort("chain_array.i_value"), + *netlist.lookupPort("chain_array.o_value")); + CHECK(*path.findVariable("chain_array.x[0]") == 1); + CHECK(*path.findVariable("chain_array.x[1]") == 3); + CHECK(*path.findVariable("chain_array.x[2]") == 5); + CHECK(*path.findVariable("chain_array.x[3]") == 7); + CHECK(*path.findVariable("chain_array.x[4]") == 9); +} + +TEST_CASE("Passthrough two signals via a shared structure") { + auto tree = SyntaxTree::fromText(R"( +module passthrough_member_access ( + input logic i_value_a, + input logic i_value_b, + output logic o_value_a, + output logic o_value_b +); + + struct packed { + logic a; + logic b; + } foo; + + assign foo.a = i_value_a; + assign foo.b = i_value_b; + + assign o_value_a = foo.a; + assign o_value_b = foo.b; + +endmodule +)"); + Compilation compilation; + compilation.addSyntaxTree(tree); + NO_COMPILATION_ERRORS; + auto netlist = createNetlist(compilation); + auto* inPortA = netlist.lookupPort("passthrough_member_access.i_value_a"); + auto* inPortB = netlist.lookupPort("passthrough_member_access.i_value_b"); + auto* outPortA = netlist.lookupPort("passthrough_member_access.o_value_a"); + auto* outPortB = netlist.lookupPort("passthrough_member_access.o_value_b"); + PathFinder pathFinder(netlist); + // Valid paths. + CHECK(pathFinder.find(*inPortA, *outPortA).size() == 4); + CHECK(pathFinder.find(*inPortB, *outPortB).size() == 4); + // Invalid paths. + CHECK(pathFinder.find(*inPortA, *outPortB).empty()); + CHECK(pathFinder.find(*inPortB, *outPortA).empty()); +} + +TEST_CASE("Passthrough two signals via ranges in a shared vector") { + auto tree = SyntaxTree::fromText(R"( +module passthrough_ranges ( + input logic [1:0] i_value_a, + input logic [1:0] i_value_b, + output logic [1:0] o_value_a, + output logic [1:0] o_value_b +); + + logic [3:0] foo; + + assign foo[1:0] = i_value_a; + assign foo[3:2] = i_value_b; + + assign o_value_a = foo[1:0]; + assign o_value_b = foo[3:2]; + +endmodule +)"); + Compilation compilation; + compilation.addSyntaxTree(tree); + NO_COMPILATION_ERRORS; + auto netlist = createNetlist(compilation); + auto* inPortA = netlist.lookupPort("passthrough_ranges.i_value_a"); + auto* inPortB = netlist.lookupPort("passthrough_ranges.i_value_b"); + auto* outPortA = netlist.lookupPort("passthrough_ranges.o_value_a"); + auto* outPortB = netlist.lookupPort("passthrough_ranges.o_value_b"); + PathFinder pathFinder(netlist); + // Valid paths. + CHECK(pathFinder.find(*inPortA, *outPortA).size() == 4); + CHECK(pathFinder.find(*inPortB, *outPortB).size() == 4); + // Invalid paths. + CHECK(pathFinder.find(*inPortA, *outPortB).empty()); + CHECK(pathFinder.find(*inPortB, *outPortA).empty()); +} + +//===---------------------------------------------------------------------===// +// Tests for loop unrolling +//===---------------------------------------------------------------------===// + +TEST_CASE("Chain of assignments using a single loop") { + // Test that a loop can be unrolled and variable declarations can be split + // out for elements of an array. + auto tree = SyntaxTree::fromText(R"( +module chain_array #(parameter N=4) ( + input logic i_value, + output logic o_value); + + logic pipeline [N-1:0]; + + assign pipeline[0] = i_value; + + always_comb begin + for (int i=1; i o_value, check it passes through each stage. + CHECK(pathFinder + .find(*netlist.lookupPort("chain_array.i_value"), + *netlist.lookupPort("chain_array.o_value")) + .size() == 10); +} + +TEST_CASE("Chain of assignments using a nested loop") { + // Expand the previous test to check the handling of multiple loop variables. + auto tree = SyntaxTree::fromText(R"( +module chain_nested #(parameter N=3) ( + input logic i_value, + output logic o_value); + + logic [(N*N)-1:0] stages; + + //assign stages[0] = i_value; + assign o_value = stages[(N*N)-1]; + + always_comb begin + for (int i=0; i o_value, check it passes through each stage. + auto path = pathFinder.find(*netlist.lookupPort("chain_nested.i_value"), + *netlist.lookupPort("chain_nested.o_value")); + CHECK(*path.findVariable("chain_nested.stages[0]") == 1); + CHECK(*path.findVariable("chain_nested.stages[1]") == 3); + CHECK(*path.findVariable("chain_nested.stages[2]") == 5); + CHECK(*path.findVariable("chain_nested.stages[3]") == 7); + CHECK(*path.findVariable("chain_nested.stages[4]") == 9); + CHECK(*path.findVariable("chain_nested.stages[5]") == 11); + CHECK(*path.findVariable("chain_nested.stages[6]") == 13); + CHECK(*path.findVariable("chain_nested.stages[7]") == 15); + CHECK(*path.findVariable("chain_nested.stages[8]") == 17); +} + +TEST_CASE("Two chains of assignments using a shared 2D array") { + // Check that assignments corresponding to two distinct paths, using the same + // array variable can be handled. + auto tree = SyntaxTree::fromText(R"( +module chain_loop_dual #(parameter N=4)( + input logic i_value_a, + input logic i_value_b, + output logic o_value_a, + output logic o_value_b +); + + parameter A = 0; + parameter B = 1; + + logic pipeline [1:0][N-1:0]; + + assign pipeline[A][0] = i_value_a; + assign pipeline[B][0] = i_value_b; + + always_comb begin + for (int i=1; i