1089 lines
39 KiB
C++
1089 lines
39 KiB
C++
// -*- mode: C++; c-file-style: "cc-mode" -*-
|
|
//*************************************************************************
|
|
// DESCRIPTION: Verilator: Data flow graph (DFG) representation of logic
|
|
//
|
|
// Code available from: https://verilator.org
|
|
//
|
|
//*************************************************************************
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify it
|
|
// under the terms of either the GNU Lesser General Public License Version 3
|
|
// or the Perl Artistic License Version 2.0.
|
|
// SPDX-FileCopyrightText: 2003-2026 Wilson Snyder
|
|
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
|
|
//
|
|
//*************************************************************************
|
|
|
|
#include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT
|
|
|
|
#include "V3Dfg.h"
|
|
|
|
#include "V3Ast.h"
|
|
#include "V3EmitV.h"
|
|
#include "V3File.h"
|
|
|
|
VL_DEFINE_DEBUG_FUNCTIONS;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// DfgGraph
|
|
|
|
DfgGraph::DfgGraph(const string& name)
|
|
: m_name{name} {}
|
|
|
|
DfgGraph::~DfgGraph() {
|
|
forEachVertex([&](DfgVertex& vtx) { vtx.unlinkDelete(*this); });
|
|
}
|
|
|
|
std::unique_ptr<DfgGraph> DfgGraph::clone() const {
|
|
// Create the new graph
|
|
DfgGraph* const clonep = new DfgGraph{name()};
|
|
|
|
// Map from original vertex to clone
|
|
std::unordered_map<const DfgVertex*, DfgVertex*> vtxp2clonep(size() * 2);
|
|
|
|
// Clone constVertices
|
|
for (const DfgConst& vtx : m_constVertices) {
|
|
DfgConst* const cp = new DfgConst{*clonep, vtx.fileline(), vtx.num()};
|
|
vtxp2clonep.emplace(&vtx, cp);
|
|
}
|
|
// Clone variable vertices
|
|
for (const DfgVertexVar& vtx : m_varVertices) {
|
|
const DfgVertexVar* const vp = vtx.as<DfgVertexVar>();
|
|
DfgVertexVar* cp = nullptr;
|
|
|
|
switch (vtx.type()) {
|
|
case VDfgType::VarArray: {
|
|
cp = new DfgVarArray{*clonep, vp->vscp()};
|
|
vtxp2clonep.emplace(&vtx, cp);
|
|
break;
|
|
}
|
|
case VDfgType::VarPacked: {
|
|
cp = new DfgVarPacked{*clonep, vp->vscp()};
|
|
vtxp2clonep.emplace(&vtx, cp);
|
|
break;
|
|
}
|
|
default: {
|
|
vtx.v3fatalSrc("Unhandled variable vertex type: " + vtx.typeName());
|
|
VL_UNREACHABLE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (AstVarScope* const tmpForp = vp->tmpForp()) cp->tmpForp(tmpForp);
|
|
}
|
|
// Clone ast reference vertices
|
|
for (const DfgVertexAst& vtx : m_astVertices) { // LCOV_EXCL_START
|
|
switch (vtx.type()) {
|
|
case VDfgType::AstRd: {
|
|
const DfgAstRd* const vp = vtx.as<DfgAstRd>();
|
|
DfgAstRd* const cp = new DfgAstRd{*clonep, vp->exprp(), vp->inSenItem(), vp->inLoop()};
|
|
vtxp2clonep.emplace(&vtx, cp);
|
|
break;
|
|
}
|
|
default: {
|
|
vtx.v3fatalSrc("Unhandled ast reference vertex type: " + vtx.typeName());
|
|
VL_UNREACHABLE;
|
|
break;
|
|
}
|
|
}
|
|
} // LCOV_EXCL_STOP
|
|
// Clone operation vertices
|
|
for (const DfgVertex& vtx : m_opVertices) {
|
|
switch (vtx.type()) {
|
|
#include "V3Dfg__gen_clone_cases.h" // From ./astgen
|
|
case VDfgType::Sel: {
|
|
DfgSel* const cp = new DfgSel{*clonep, vtx.fileline(), vtx.dtype()};
|
|
cp->lsb(vtx.as<DfgSel>()->lsb());
|
|
vtxp2clonep.emplace(&vtx, cp);
|
|
break;
|
|
}
|
|
case VDfgType::Rep: {
|
|
DfgRep* const cp = new DfgRep{*clonep, vtx.fileline(), vtx.dtype()};
|
|
vtxp2clonep.emplace(&vtx, cp);
|
|
break;
|
|
}
|
|
case VDfgType::UnitArray: {
|
|
DfgUnitArray* const cp = new DfgUnitArray{*clonep, vtx.fileline(), vtx.dtype()};
|
|
vtxp2clonep.emplace(&vtx, cp);
|
|
break;
|
|
}
|
|
case VDfgType::Mux: {
|
|
DfgMux* const cp = new DfgMux{*clonep, vtx.fileline(), vtx.dtype()};
|
|
vtxp2clonep.emplace(&vtx, cp);
|
|
break;
|
|
}
|
|
case VDfgType::SpliceArray: {
|
|
DfgSpliceArray* const cp = new DfgSpliceArray{*clonep, vtx.fileline(), vtx.dtype()};
|
|
vtxp2clonep.emplace(&vtx, cp);
|
|
break;
|
|
}
|
|
case VDfgType::SplicePacked: {
|
|
DfgSplicePacked* const cp = new DfgSplicePacked{*clonep, vtx.fileline(), vtx.dtype()};
|
|
vtxp2clonep.emplace(&vtx, cp);
|
|
break;
|
|
}
|
|
case VDfgType::Logic: {
|
|
vtx.v3fatalSrc("DfgLogic cannot be cloned");
|
|
VL_UNREACHABLE;
|
|
break;
|
|
}
|
|
case VDfgType::Unresolved: {
|
|
vtx.v3fatalSrc("DfgUnresolved cannot be cloned");
|
|
VL_UNREACHABLE;
|
|
break;
|
|
}
|
|
default: {
|
|
vtx.v3fatalSrc("Unhandled operation vertex type: " + vtx.typeName());
|
|
VL_UNREACHABLE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
UASSERT(size() == clonep->size(), "Size of clone should be the same");
|
|
|
|
// Constants have no inputs
|
|
// Hook up inputs of cloned variables
|
|
for (const DfgVertexVar& vtx : m_varVertices) {
|
|
DfgVertexVar* const cp = vtxp2clonep.at(&vtx)->as<DfgVertexVar>();
|
|
if (const DfgVertex* const srcp = vtx.srcp()) cp->srcp(vtxp2clonep.at(srcp));
|
|
if (const DfgVertex* const defp = vtx.defaultp()) cp->defaultp(vtxp2clonep.at(defp));
|
|
}
|
|
// Hook up inputs of cloned ast references
|
|
for (const DfgVertexAst& vtx : m_astVertices) { // LCOV_EXCL_START
|
|
switch (vtx.type()) {
|
|
case VDfgType::AstRd: {
|
|
const DfgAstRd* const vp = vtx.as<DfgAstRd>();
|
|
DfgAstRd* const cp = vtxp2clonep.at(&vtx)->as<DfgAstRd>();
|
|
if (const DfgVertex* const srcp = vp->srcp()) cp->srcp(vtxp2clonep.at(srcp));
|
|
break;
|
|
}
|
|
default: {
|
|
vtx.v3fatalSrc("Unhandled DfgVertexAst sub type: " + vtx.typeName());
|
|
VL_UNREACHABLE;
|
|
break;
|
|
}
|
|
}
|
|
} // LCOV_EXCL_STOP
|
|
// Hook up inputs of cloned operation vertices
|
|
for (const DfgVertex& vtx : m_opVertices) {
|
|
if (vtx.is<DfgVertexVariadic>()) {
|
|
switch (vtx.type()) {
|
|
case VDfgType::SpliceArray:
|
|
case VDfgType::SplicePacked: {
|
|
const DfgVertexSplice* const vp = vtx.as<DfgVertexSplice>();
|
|
DfgVertexSplice* const cp = vtxp2clonep.at(vp)->as<DfgVertexSplice>();
|
|
vp->foreachDriver([&](const DfgVertex& src, uint32_t lo, FileLine* flp) {
|
|
cp->addDriver(vtxp2clonep.at(&src), lo, flp);
|
|
return false;
|
|
});
|
|
break;
|
|
}
|
|
default: {
|
|
vtx.v3fatalSrc("Unhandled DfgVertexVariadic sub type: " + vtx.typeName());
|
|
VL_UNREACHABLE;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
DfgVertex* const cp = vtxp2clonep.at(&vtx);
|
|
for (size_t i = 0; i < vtx.nInputs(); ++i) {
|
|
cp->inputp(i, vtxp2clonep.at(vtx.inputp(i)));
|
|
}
|
|
}
|
|
}
|
|
|
|
return std::unique_ptr<DfgGraph>{clonep};
|
|
}
|
|
|
|
void DfgGraph::mergeGraphs(std::vector<std::unique_ptr<DfgGraph>>&& otherps) {
|
|
if (otherps.empty()) return;
|
|
|
|
// NODE STATE
|
|
// AstVarScope::user2p() -> corresponding DfgVertexVar* in 'this' graph
|
|
const VNUser2InUse user2InUse;
|
|
|
|
// Set up Ast Variable -> DfgVertexVar map for 'this' graph
|
|
for (DfgVertexVar& vtx : m_varVertices) vtx.vscp()->user2p(&vtx);
|
|
|
|
// Merge in each of the other graphs
|
|
for (const std::unique_ptr<DfgGraph>& otherp : otherps) {
|
|
// Process variables
|
|
for (DfgVertexVar* const vtxp : otherp->m_varVertices.unlinkable()) {
|
|
// Variabels that are present in 'this', make them use the DfgVertexVar in 'this'.
|
|
if (DfgVertexVar* const altp = vtxp->vscp()->user2u().to<DfgVertexVar*>()) {
|
|
DfgVertex* const srcp = vtxp->srcp();
|
|
DfgVertex* const defaultp = vtxp->defaultp();
|
|
UASSERT_OBJ(!(srcp || defaultp) || (!altp->srcp() && !altp->defaultp()), vtxp,
|
|
"At most one alias should be driven");
|
|
vtxp->replaceWith(altp);
|
|
if (srcp) altp->srcp(srcp);
|
|
if (defaultp) altp->defaultp(defaultp);
|
|
VL_DO_DANGLING(vtxp->unlinkDelete(*otherp), vtxp);
|
|
continue;
|
|
}
|
|
// Otherwise they will be moved
|
|
vtxp->vscp()->user2p(vtxp);
|
|
vtxp->m_userGeneration = 0;
|
|
#ifdef VL_DEBUG
|
|
vtxp->m_dfgp = this;
|
|
#endif
|
|
}
|
|
m_varVertices.splice(m_varVertices.end(), otherp->m_varVertices);
|
|
// Process Ast references
|
|
for (DfgVertexAst* const vtxp : otherp->m_astVertices.unlinkable()) {
|
|
vtxp->m_userGeneration = 0;
|
|
#ifdef VL_DEBUG
|
|
vtxp->m_dfgp = this;
|
|
#endif
|
|
}
|
|
m_astVertices.splice(m_astVertices.end(), otherp->m_astVertices);
|
|
// Process constants
|
|
for (DfgConst& vtx : otherp->m_constVertices) {
|
|
vtx.m_userGeneration = 0;
|
|
#ifdef VL_DEBUG
|
|
vtx.m_dfgp = this;
|
|
#endif
|
|
}
|
|
m_constVertices.splice(m_constVertices.end(), otherp->m_constVertices);
|
|
// Process operations
|
|
for (DfgVertex& vtx : otherp->m_opVertices) {
|
|
vtx.m_userGeneration = 0;
|
|
#ifdef VL_DEBUG
|
|
vtx.m_dfgp = this;
|
|
#endif
|
|
}
|
|
m_opVertices.splice(m_opVertices.end(), otherp->m_opVertices);
|
|
// Update graph sizes
|
|
m_size += otherp->m_size;
|
|
otherp->m_size = 0;
|
|
}
|
|
}
|
|
|
|
std::string DfgGraph::makeUniqueName(const std::string& prefix, size_t n) {
|
|
// Construct the tmpNameStub if we have not done so yet
|
|
if (m_tmpNameStub.empty()) {
|
|
// Use the hash of the graph name (avoid long names and non-identifiers)
|
|
const std::string hash = V3Hash{m_name}.toString();
|
|
// We need to keep every variable globally unique, and graph hashed
|
|
// names might not be, so keep a static table to track multiplicity
|
|
static std::unordered_map<std::string, uint32_t> s_multiplicity;
|
|
m_tmpNameStub += '_' + hash + '_' + std::to_string(s_multiplicity[hash]++) + '_';
|
|
}
|
|
// Assemble the globally unique name
|
|
return "__Vdfg" + prefix + m_tmpNameStub + std::to_string(n);
|
|
}
|
|
|
|
DfgVertexVar* DfgGraph::makeNewVar(FileLine* flp, const std::string& name,
|
|
const DfgDataType& dtype, AstScope* scopep) {
|
|
// Create AstVar
|
|
AstVar* const varp = new AstVar{flp, VVarType::MODULETEMP, name, dtype.astDtypep()};
|
|
// Add AstVar to the scope's module
|
|
scopep->modp()->addStmtsp(varp);
|
|
// Create AstVarScope
|
|
AstVarScope* const vscp = new AstVarScope{flp, scopep, varp};
|
|
// Add to scope
|
|
scopep->addVarsp(vscp);
|
|
// Create and return the corresponding variable vertex
|
|
if (dtype.isArray()) return new DfgVarArray{*this, vscp};
|
|
return new DfgVarPacked{*this, vscp};
|
|
}
|
|
|
|
static const std::string toDotId(const DfgVertex& vtx) { return '"' + cvtToHex(&vtx) + '"'; }
|
|
|
|
// Dump one DfgVertex in Graphviz format
|
|
static void dumpDotVertex(std::ostream& os, const DfgVertex& vtx) {
|
|
if (const DfgVertexVar* const varVtxp = vtx.cast<DfgVertexVar>()) {
|
|
const AstVarScope* const vscp = varVtxp->vscp();
|
|
os << toDotId(vtx);
|
|
// Begin attributes
|
|
os << " [";
|
|
// Begin 'label'
|
|
os << "label=\"";
|
|
// Name
|
|
os << vscp->prettyName();
|
|
// Address
|
|
os << '\n' << cvtToHex(varVtxp);
|
|
// Original variable, if any
|
|
if (const AstVarScope* const tmpForp = varVtxp->tmpForp()) {
|
|
if (tmpForp != vscp) os << "\ntemporary for: " << tmpForp->prettyName();
|
|
}
|
|
// Type and fanout
|
|
os << '\n';
|
|
varVtxp->dtype().astDtypep()->dumpSmall(os);
|
|
os << " / F" << varVtxp->fanout();
|
|
// Reference flags
|
|
os << " / ";
|
|
static const char* const rwmn[2][2] = {{"_", "W"}, {"R", "M"}};
|
|
os << rwmn[varVtxp->hasExtRdRefs()][varVtxp->hasExtWrRefs()];
|
|
os << rwmn[false][varVtxp->hasModWrRefs()];
|
|
os << (varVtxp->hasDfgRefs() ? "D" : "_");
|
|
// End 'label'
|
|
os << '"';
|
|
// Shape
|
|
if (varVtxp->is<DfgVarPacked>()) {
|
|
os << ", shape=box";
|
|
} else if (varVtxp->is<DfgVarArray>()) {
|
|
os << ", shape=box3d";
|
|
} else {
|
|
varVtxp->v3fatalSrc("Unhandled DfgVertexVar sub-type");
|
|
}
|
|
// Color
|
|
const char* const colorp = varVtxp->hasExtRefs() ? "firebrick2" // Red
|
|
: varVtxp->hasModWrRefs() ? "darkorange1" // Orange
|
|
: varVtxp->hasDfgRefs() ? "gold2" // Yellow
|
|
: varVtxp->tmpForp() ? "gray95" // Gray
|
|
: "white";
|
|
os << ", style=filled";
|
|
os << ", fillcolor=\"" << colorp << "\"";
|
|
// End attributes
|
|
os << "]\n";
|
|
return;
|
|
}
|
|
|
|
if (const DfgConst* const constVtxp = vtx.cast<DfgConst>()) {
|
|
const V3Number& num = constVtxp->num();
|
|
|
|
os << toDotId(vtx);
|
|
os << " [label=\"";
|
|
if (num.width() <= 32 && !num.isSigned()) {
|
|
os << constVtxp->width() << "'d" << num.toUInt() << '\n';
|
|
os << constVtxp->width() << "'h" << std::hex << num.toUInt() << std::dec << '\n';
|
|
} else {
|
|
os << num.ascii() << '\n';
|
|
}
|
|
os << cvtToHex(constVtxp) << '\n';
|
|
os << '"';
|
|
os << ", shape=plain";
|
|
os << "]\n";
|
|
return;
|
|
}
|
|
|
|
if (const DfgSel* const selVtxp = vtx.cast<DfgSel>()) {
|
|
const uint32_t lsb = selVtxp->lsb();
|
|
const uint32_t msb = lsb + selVtxp->width() - 1;
|
|
os << toDotId(vtx);
|
|
os << " [label=\"SEL _[" << msb << ":" << lsb << "]\n";
|
|
os << cvtToHex(selVtxp) << '\n';
|
|
vtx.dtype().astDtypep()->dumpSmall(os);
|
|
os << " / F" << vtx.fanout() << '"';
|
|
if (vtx.hasMultipleSinks()) {
|
|
os << ", shape=doublecircle";
|
|
} else {
|
|
os << ", shape=circle";
|
|
}
|
|
os << "]\n";
|
|
return;
|
|
}
|
|
|
|
if (vtx.is<DfgVertexSplice>() || vtx.is<DfgUnitArray>() || vtx.is<DfgUnresolved>()) {
|
|
os << toDotId(vtx);
|
|
os << " [label=\"" << vtx.typeName() << '\n';
|
|
os << cvtToHex(&vtx) << '\n';
|
|
vtx.dtype().astDtypep()->dumpSmall(os);
|
|
os << " / F" << vtx.fanout() << '"';
|
|
if (vtx.hasMultipleSinks()) {
|
|
os << ", shape=doubleoctagon";
|
|
} else {
|
|
os << ", shape=octagon";
|
|
}
|
|
os << "]\n";
|
|
return;
|
|
}
|
|
|
|
if (const DfgLogic* const logicp = vtx.cast<DfgLogic>()) {
|
|
os << toDotId(vtx);
|
|
std::stringstream ss;
|
|
V3EmitV::debugVerilogForTree(logicp->nodep(), ss);
|
|
std::string str = "AstNode: " + cvtToHex(logicp->nodep()) + "\n" + ss.str();
|
|
str = VString::quoteBackslash(str);
|
|
str = VString::quoteAny(str, '"', '\\');
|
|
str = VString::replaceSubstr(str, "\n", "\\l");
|
|
const char* const colorp = !logicp->selectedForSynthesis() ? "#d0d0ff" // Pale Blue
|
|
: logicp->nonSynthesizable() ? "#ffd0d0" // Pale Red
|
|
: logicp->reverted() ? "#faffd0" // Pale Yellow
|
|
: "#d0ffd0"; // Pale Green
|
|
|
|
os << " [label=\"";
|
|
os << str;
|
|
os << '\n' << vtx.typeName() << '\n' << cvtToHex(&vtx);
|
|
os << "\"\n";
|
|
os << ", shape=box, style=\"rounded,filled\", nojustify=true";
|
|
os << ", fillcolor=\"" << colorp << "\"";
|
|
os << "]\n";
|
|
return;
|
|
}
|
|
|
|
if (const DfgVertexAst* const astVtxp = vtx.cast<DfgVertexAst>()) {
|
|
os << toDotId(vtx);
|
|
std::stringstream ss;
|
|
V3EmitV::debugVerilogForTree(astVtxp->exprp(), ss);
|
|
std::string str = "AstNode: " + cvtToHex(astVtxp->exprp()) + "\n" + ss.str() + "\n";
|
|
str = VString::quoteBackslash(str);
|
|
str = VString::quoteAny(str, '"', '\\');
|
|
str = VString::replaceSubstr(str, "\n", "\\l");
|
|
const DfgAstRd* const astRdVtxp = astVtxp->cast<DfgAstRd>();
|
|
const char* const colorp = astRdVtxp ? "#80ff80" // Green
|
|
: "#ffff80"; // Yellow
|
|
os << " [label=\"";
|
|
os << str;
|
|
os << '\n' << vtx.typeName() << '\n' << cvtToHex(&vtx) << '\n';
|
|
vtx.dtype().astDtypep()->dumpSmall(os);
|
|
os << " / ";
|
|
os << "_S"[astRdVtxp->inSenItem()];
|
|
os << "_L"[astRdVtxp->inLoop()];
|
|
os << "\"\n";
|
|
os << ", shape=box, style=\"rounded,filled\", nojustify=true";
|
|
os << ", fillcolor=\"" << colorp << "\"";
|
|
os << "]\n";
|
|
return;
|
|
}
|
|
|
|
os << toDotId(vtx);
|
|
os << " [label=\"" << vtx.typeName() << '\n';
|
|
os << cvtToHex(&vtx) << '\n';
|
|
vtx.dtype().astDtypep()->dumpSmall(os);
|
|
os << " / F" << vtx.fanout() << '"';
|
|
if (vtx.hasMultipleSinks()) {
|
|
os << ", shape=doublecircle";
|
|
} else {
|
|
os << ", shape=circle";
|
|
}
|
|
os << "]\n";
|
|
}
|
|
|
|
void DfgGraph::dumpDot(std::ostream& os, const std::string& label,
|
|
std::function<bool(const DfgVertex&)> p) const {
|
|
// This generates a graphviz dump, https://www.graphviz.org
|
|
|
|
// Header
|
|
os << "digraph dfg {\n";
|
|
os << "rankdir=LR\n";
|
|
|
|
// If predicate not given, dump everything
|
|
if (!p) p = [](const DfgVertex&) { return true; };
|
|
|
|
std::unordered_set<const DfgVertex*> emitted;
|
|
// Emit all vertices associated with a DfgLogic
|
|
forEachVertex([&](const DfgVertex& vtx) {
|
|
const DfgLogic* const logicp = vtx.cast<DfgLogic>();
|
|
if (!logicp) return;
|
|
if (logicp->synth().empty()) return;
|
|
if (!p(vtx)) return;
|
|
os << "subgraph cluster_" << cvtToHex(logicp) << " {\n";
|
|
dumpDotVertex(os, *logicp);
|
|
emitted.insert(logicp);
|
|
for (DfgVertex* const vtxp : logicp->synth()) {
|
|
if (!p(*vtxp)) continue;
|
|
dumpDotVertex(os, *vtxp);
|
|
emitted.insert(vtxp);
|
|
}
|
|
os << "}\n";
|
|
});
|
|
// Emit all remaining vertices
|
|
forEachVertex([&](const DfgVertex& vtx) {
|
|
if (emitted.count(&vtx)) return;
|
|
if (!p(vtx)) return;
|
|
dumpDotVertex(os, vtx);
|
|
});
|
|
// Emit all edges
|
|
forEachVertex([&](const DfgVertex& vtx) {
|
|
if (!p(vtx)) return;
|
|
for (size_t i = 0; i < vtx.nInputs(); ++i) {
|
|
DfgVertex* const srcp = vtx.inputp(i);
|
|
if (!srcp) continue;
|
|
if (!p(*srcp)) continue;
|
|
os << toDotId(*srcp) << " -> " << toDotId(vtx);
|
|
os << " [headlabel=\"" << vtx.srcName(i) << "\"]";
|
|
os << '\n';
|
|
}
|
|
});
|
|
|
|
// Footer
|
|
os << "label=\"" << name() + (label.empty() ? "" : "-" + label) << "\"\n";
|
|
os << "labelloc=t\n";
|
|
os << "labeljust=l\n";
|
|
os << "}\n";
|
|
}
|
|
|
|
std::string DfgGraph::dumpDotString(const std::string& label,
|
|
std::function<bool(const DfgVertex&)> p) const {
|
|
std::stringstream ss;
|
|
dumpDot(ss, label, p);
|
|
return ss.str();
|
|
}
|
|
|
|
void DfgGraph::dumpDotFile(const std::string& filename, const std::string& label,
|
|
std::function<bool(const DfgVertex&)> p) const {
|
|
const std::unique_ptr<std::ofstream> os{V3File::new_ofstream(filename)};
|
|
if (os->fail()) v3fatal("Can't write file: " << filename);
|
|
dumpDot(*os.get(), label, p);
|
|
os->close();
|
|
}
|
|
|
|
void DfgGraph::dumpDotFilePrefixed(const std::string& label,
|
|
std::function<bool(const DfgVertex&)> p) const {
|
|
std::string filename = name();
|
|
if (!label.empty()) filename += "-" + label;
|
|
dumpDotFile(v3Global.debugFilename(filename) + ".dot", label, p);
|
|
}
|
|
|
|
template <bool T_SinksNotSources>
|
|
static std::unique_ptr<std::unordered_set<const DfgVertex*>>
|
|
dfgGraphCollectCone(const std::vector<const DfgVertex*>& vtxps) {
|
|
// Work queue for traversal starting from all the seed vertices
|
|
std::vector<const DfgVertex*> queue = vtxps;
|
|
// Set of already visited vertices
|
|
std::unique_ptr<std::unordered_set<const DfgVertex*>> resp{
|
|
new std::unordered_set<const DfgVertex*>{}};
|
|
// Depth first traversal
|
|
while (!queue.empty()) {
|
|
// Pop next work item
|
|
const DfgVertex* const vtxp = queue.back();
|
|
queue.pop_back();
|
|
// Mark vertex as visited, move on if already visited
|
|
if (!resp->insert(vtxp).second) continue;
|
|
// Enqueue all siblings of this vertex.
|
|
if VL_CONSTEXPR_CXX17 (T_SinksNotSources) {
|
|
vtxp->foreachSink([&](const DfgVertex& sink) {
|
|
queue.push_back(&sink);
|
|
return false;
|
|
});
|
|
} else {
|
|
vtxp->foreachSource([&](const DfgVertex& src) {
|
|
queue.push_back(&src);
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
// Done
|
|
return resp;
|
|
}
|
|
|
|
std::unique_ptr<std::unordered_set<const DfgVertex*>>
|
|
DfgGraph::sourceCone(const std::vector<const DfgVertex*>& vtxps) const {
|
|
return dfgGraphCollectCone<false>(vtxps);
|
|
}
|
|
|
|
std::unique_ptr<std::unordered_set<const DfgVertex*>>
|
|
DfgGraph::sinkCone(const std::vector<const DfgVertex*>& vtxps) const {
|
|
return dfgGraphCollectCone<true>(vtxps);
|
|
}
|
|
|
|
std::unique_ptr<std::unordered_set<const DfgVertex*>>
|
|
DfgGraph::neighborhood(const std::vector<const DfgVertex*>& vtxps, size_t n) const {
|
|
// Neighborhood
|
|
std::vector<const DfgVertex*> vec = vtxps;
|
|
// Set of already visited vertices
|
|
std::unordered_set<const DfgVertex*> res{vec.begin(), vec.end()};
|
|
// Expand neihborhood by 'n' hops
|
|
size_t begin = 0;
|
|
size_t end = vec.size();
|
|
for (size_t hops = 1; hops <= n; ++hops) {
|
|
for (size_t i = begin; i < end; ++i) {
|
|
const DfgVertex* const vtxp = vec[i];
|
|
vtxp->foreachSink([&](const DfgVertex& dst) {
|
|
if (res.insert(&dst).second) vec.push_back(&dst);
|
|
return false;
|
|
});
|
|
vtxp->foreachSource([&](const DfgVertex& src) {
|
|
if (res.insert(&src).second) vec.push_back(&src);
|
|
return false;
|
|
});
|
|
}
|
|
begin = end;
|
|
end = vec.size();
|
|
}
|
|
// Move out the results
|
|
return std::make_unique<std::unordered_set<const DfgVertex*>>(std::move(res));
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// DfgVertex
|
|
|
|
DfgVertex::DfgVertex(DfgGraph& dfg, VDfgType type, FileLine* flp, const DfgDataType& dt)
|
|
: m_filelinep{flp}
|
|
, m_dtype{dt}
|
|
, m_type{type} {
|
|
dfg.addVertex(*this);
|
|
}
|
|
|
|
void DfgVertex::typeCheck(const DfgGraph& dfg) const {
|
|
|
|
#define CHECK(cond, msg) \
|
|
UASSERT_OBJ(cond, this, \
|
|
"Dfg type error for vertex " << typeName() << " in " << dfg.name() << ": " \
|
|
<< msg);
|
|
switch (type()) {
|
|
case VDfgType::Const: {
|
|
CHECK(isPacked(), "Should be Packed type");
|
|
return;
|
|
}
|
|
case VDfgType::AstRd: {
|
|
const DfgAstRd& v = *as<DfgAstRd>();
|
|
CHECK(v.isPacked() || v.isArray(), "Should be Packed or Array type");
|
|
return;
|
|
}
|
|
case VDfgType::VarArray:
|
|
case VDfgType::VarPacked: {
|
|
const DfgVertexVar& v = *as<DfgVertexVar>();
|
|
CHECK(!v.defaultp() || v.defaultp()->dtype() == v.dtype(), "'defaultp' should match");
|
|
CHECK(!v.srcp() || v.srcp()->dtype() == v.dtype(), "'srcp' should match");
|
|
return;
|
|
}
|
|
case VDfgType::SpliceArray:
|
|
case VDfgType::SplicePacked: {
|
|
const DfgVertexSplice& v = *as<DfgVertexSplice>();
|
|
v.foreachDriver([&](const DfgVertex& src, uint32_t lo) {
|
|
CHECK(src.dtype() == DfgDataType::select(v.dtype(), lo, src.size()), "driver");
|
|
return false;
|
|
});
|
|
return;
|
|
}
|
|
case VDfgType::Logic: {
|
|
CHECK(dtype().isNull(), "Should be Null type");
|
|
return;
|
|
}
|
|
case VDfgType::Unresolved: {
|
|
CHECK(!dtype().isNull(), "Should not be Null type");
|
|
return;
|
|
}
|
|
case VDfgType::UnitArray: {
|
|
const DfgUnitArray& v = *as<DfgUnitArray>();
|
|
CHECK(v.isArray(), "Should be Array type");
|
|
CHECK(v.size() == 1, "Should be one element");
|
|
CHECK(v.srcp()->dtype() == v.dtype().elemDtype(), "Input should be the element type");
|
|
return;
|
|
}
|
|
case VDfgType::Sel: {
|
|
const DfgSel& v = *as<DfgSel>();
|
|
CHECK(v.isPacked(), "Should be Packed type");
|
|
CHECK(v.dtype() == DfgDataType::select(v.srcp()->dtype(), v.lsb(), v.size()), "sel");
|
|
return;
|
|
}
|
|
case VDfgType::Mux: {
|
|
const DfgMux& v = *as<DfgMux>();
|
|
CHECK(v.isPacked(), "Should be Packed type");
|
|
CHECK(v.fromp()->isPacked(), "Source operand should be Packed type");
|
|
CHECK(v.fromp()->size() >= v.size(), "Source operand should not be narrower");
|
|
CHECK(v.lsbp()->isPacked(), "Index should be Packed type");
|
|
return;
|
|
}
|
|
case VDfgType::ArraySel: {
|
|
const DfgArraySel& v = *as<DfgArraySel>();
|
|
CHECK(v.dtype() == v.fromp()->dtype().elemDtype(), "Element type should match");
|
|
CHECK(v.bitp()->isPacked(), "Index should be Packed type");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::Add:
|
|
case VDfgType::And:
|
|
case VDfgType::Div:
|
|
case VDfgType::DivS:
|
|
case VDfgType::ModDiv:
|
|
case VDfgType::ModDivS:
|
|
case VDfgType::Mul:
|
|
case VDfgType::MulS:
|
|
case VDfgType::Or:
|
|
case VDfgType::Sub:
|
|
case VDfgType::Xor: {
|
|
CHECK(isPacked(), "Should be Packed type");
|
|
CHECK(inputp(0)->dtype() == dtype(), "LHS should be same type");
|
|
CHECK(inputp(1)->dtype() == dtype(), "RHS should be same type");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::Negate:
|
|
case VDfgType::Not: {
|
|
CHECK(isPacked(), "Should be Packed type");
|
|
CHECK(inputp(0)->dtype() == dtype(), "Input should be same type");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::ShiftL:
|
|
case VDfgType::ShiftR:
|
|
case VDfgType::ShiftRS: {
|
|
CHECK(isPacked(), "Should be Packed type");
|
|
CHECK(inputp(0)->dtype() == dtype(), "LHS should be same type");
|
|
CHECK(inputp(1)->isPacked(), "RHS should be Packed type");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::Concat: {
|
|
const DfgConcat& v = *as<DfgConcat>();
|
|
CHECK(v.isPacked(), "Should be Packed type");
|
|
CHECK(v.lhsp()->isPacked(), "LHS should be Packed type");
|
|
CHECK(v.rhsp()->isPacked(), "RHS should be Packed type");
|
|
CHECK(v.size() == v.rhsp()->size() + v.lhsp()->size(), "Concat result mismatch");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::Rep: {
|
|
const DfgRep& v = *as<DfgRep>();
|
|
CHECK(v.isPacked(), "Should be Packed type");
|
|
CHECK(v.srcp()->isPacked(), "'srcp' should be same type");
|
|
CHECK(v.size() % v.srcp()->size() == 0, "Not a replicate");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::StreamL:
|
|
case VDfgType::StreamR: {
|
|
// TODO: model these without an explicit slice size which is always constant (?)
|
|
CHECK(isPacked(), "Should be Packed type");
|
|
CHECK(inputp(0)->dtype() == dtype(), "LHS should be same type");
|
|
CHECK(inputp(1)->isPacked(), "Slice size should be Packed type");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::Eq:
|
|
case VDfgType::EqCase:
|
|
case VDfgType::EqWild:
|
|
case VDfgType::Neq:
|
|
case VDfgType::NeqCase:
|
|
case VDfgType::NeqWild:
|
|
case VDfgType::Gt:
|
|
case VDfgType::GtS:
|
|
case VDfgType::Gte:
|
|
case VDfgType::GteS:
|
|
case VDfgType::Lt:
|
|
case VDfgType::LtS:
|
|
case VDfgType::Lte:
|
|
case VDfgType::LteS: {
|
|
CHECK(dtype() == DfgDataType::packed(1), "Should be 1-bit");
|
|
CHECK(inputp(0)->dtype() == inputp(1)->dtype(), "Sides should match");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::Extend:
|
|
case VDfgType::ExtendS: {
|
|
CHECK(isPacked(), "Should be Packed type");
|
|
CHECK(inputp(0)->isPacked(), "Operand should be same type");
|
|
CHECK(inputp(0)->size() < size(), "Operand should be narrower");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::LogAnd:
|
|
case VDfgType::LogEq:
|
|
case VDfgType::LogIf:
|
|
case VDfgType::LogOr: {
|
|
CHECK(dtype() == DfgDataType::packed(1), "Should be 1-bit");
|
|
CHECK(inputp(0)->isPacked(), "LHS should be Packed type");
|
|
CHECK(inputp(1)->isPacked(), "RHS should be Packed type");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::LogNot:
|
|
case VDfgType::RedAnd:
|
|
case VDfgType::RedOr:
|
|
case VDfgType::RedXor: {
|
|
CHECK(dtype() == DfgDataType::packed(1), "Should be 1-bit");
|
|
CHECK(inputp(0)->isPacked(), "Operand should be Packed type");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::Cond: {
|
|
const DfgCond& v = *as<DfgCond>();
|
|
CHECK(v.isPacked(), "Should be Packed type");
|
|
CHECK(v.condp()->isPacked(), "Condition should be Packed type");
|
|
CHECK(v.thenp()->dtype() == v.dtype(), "Then should be same type");
|
|
CHECK(v.elsep()->dtype() == v.dtype(), "Else should be same type");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::CountOnes: {
|
|
const DfgCountOnes& v = *as<DfgCountOnes>();
|
|
CHECK(v.isPacked(), "Should be Packed type");
|
|
CHECK(v.srcp()->isPacked(), "Source should be Packed type");
|
|
CHECK(v.size() == 32U, "Should yield a 32-bit result");
|
|
return;
|
|
}
|
|
|
|
case VDfgType::Pow:
|
|
case VDfgType::PowSS:
|
|
case VDfgType::PowSU:
|
|
case VDfgType::PowUS: {
|
|
CHECK(isPacked(), "Should be Packed type");
|
|
CHECK(inputp(0)->dtype() == dtype(), "LHS should be same type");
|
|
CHECK(inputp(1)->isPacked(), "RHS should be Packed type");
|
|
return;
|
|
}
|
|
}
|
|
#undef CHECK
|
|
}
|
|
|
|
uint32_t DfgVertex::fanout() const {
|
|
uint32_t result = 0;
|
|
foreachSink([&](const DfgVertex&) {
|
|
++result;
|
|
return false;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
DfgVertexVar* DfgVertex::getResultVar() {
|
|
// It's easy if the vertex is already a variable ...
|
|
if (DfgVertexVar* const varp = this->cast<DfgVertexVar>()) return varp;
|
|
|
|
// Inspect existing variables written by this vertex, and choose one
|
|
DfgVertexVar* resp = nullptr;
|
|
// cppcheck-has-bug-suppress constParameter
|
|
this->foreachSink([&resp](DfgVertex& sink) {
|
|
DfgVertexVar* const varp = sink.cast<DfgVertexVar>();
|
|
if (!varp) return false;
|
|
|
|
// Do not use it if value might differ from its drivers
|
|
if (varp->isVolatile()) return false;
|
|
|
|
// First variable found
|
|
if (!resp) {
|
|
resp = varp;
|
|
return false;
|
|
}
|
|
|
|
// Prefer those variables that must be kept anyway
|
|
if (resp->hasExtRdRefs() != varp->hasExtRdRefs()) {
|
|
if (!resp->hasExtRdRefs()) resp = varp;
|
|
return false;
|
|
}
|
|
if (resp->hasDfgRefs() != varp->hasDfgRefs()) {
|
|
if (!resp->hasDfgRefs()) resp = varp;
|
|
return false;
|
|
}
|
|
|
|
// Prefer real variables over temporaries
|
|
const bool resIsTemp = resp->vscp()->varp()->isTemp();
|
|
const bool varIsTemp = varp->vscp()->varp()->isTemp();
|
|
if (resIsTemp != varIsTemp) {
|
|
if (resIsTemp) resp = varp;
|
|
return false;
|
|
}
|
|
if (!resp->tmpForp() != !varp->tmpForp()) {
|
|
if (resp->tmpForp()) resp = varp;
|
|
return false;
|
|
}
|
|
|
|
// Prefer the earlier one in source order
|
|
const FileLine& oldFlp = *(resp->fileline());
|
|
const FileLine& newFlp = *(varp->fileline());
|
|
if (const int cmp = oldFlp.operatorCompare(newFlp)) {
|
|
if (cmp > 0) resp = varp;
|
|
return false;
|
|
}
|
|
// Prefer the one with the lexically smaller name
|
|
if (const int cmp = resp->vscp()->name().compare(varp->vscp()->name())) {
|
|
if (cmp > 0) resp = varp;
|
|
return false;
|
|
}
|
|
// 'resp' and 'varp' are all the same, keep using the existing 'resp'
|
|
return false;
|
|
});
|
|
return resp;
|
|
}
|
|
|
|
AstScope* DfgVertex::scopep(ScopeCache& cache, bool tryResultVar) VL_MT_DISABLED {
|
|
// If this is a variable, we are done
|
|
if (const DfgVertexVar* const varp = this->cast<DfgVertexVar>()) {
|
|
return varp->vscp()->scopep();
|
|
}
|
|
|
|
// Try the result var first if instructed (usully only in the recursive case)
|
|
if (tryResultVar) {
|
|
if (const DfgVertexVar* const varp = this->getResultVar()) {
|
|
return varp->vscp()->scopep();
|
|
}
|
|
}
|
|
|
|
// Note: the recursive invocation can cause a re-hash but that will not invalidate references
|
|
AstScope*& resultr = cache[this];
|
|
if (!resultr) {
|
|
// Mark to prevent infinite recursion on circular graphs - should never be called on such
|
|
resultr = reinterpret_cast<AstScope*>(1);
|
|
// Find scope based on sources, falling back on the root scope
|
|
AstScope* const rootp = v3Global.rootp()->topScopep()->scopep();
|
|
AstScope* foundp = nullptr;
|
|
foreachSource([&](DfgVertex& src) {
|
|
AstScope* const scp = src.scopep(cache, true);
|
|
if (scp != rootp) {
|
|
foundp = scp;
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
resultr = foundp ? foundp : rootp;
|
|
}
|
|
|
|
// Die on a graph circular through operation vertices
|
|
UASSERT_OBJ(resultr != reinterpret_cast<AstScope*>(1), this,
|
|
"DfgVertex::scopep called on graph with circular operations");
|
|
|
|
// Done
|
|
return resultr;
|
|
}
|
|
|
|
void DfgVertex::unlinkDelete(DfgGraph& dfg) {
|
|
// Unlink sink edges
|
|
while (!m_sinks.empty()) m_sinks.frontp()->unlinkSrcp();
|
|
// Remove from graph
|
|
dfg.removeVertex(*this);
|
|
// Delete - this will unlink sources
|
|
delete this;
|
|
}
|
|
|
|
class DfgPatternString final {
|
|
std::ostream& m_os;
|
|
|
|
std::map<std::string, std::string> m_internedConsts; // Interned constants
|
|
std::map<uint32_t, std::string> m_internedSelLsbs; // Interned lsb value for selects
|
|
std::map<uint32_t, std::string> m_internedWordWidths; // Interned widths
|
|
std::map<uint32_t, std::string> m_internedWideWidths; // Interned widths
|
|
std::map<const DfgVertex*, std::string> m_internedVertices; // Interned vertices
|
|
// Multiplicity and depth of vertices
|
|
std::map<const DfgVertex*, std::pair<uint32_t, uint32_t>> m_multiplicityAndDepth;
|
|
|
|
static std::string toLetters(size_t value, bool lowerCase = false) {
|
|
const char base = lowerCase ? 'a' : 'A';
|
|
std::string s;
|
|
do { s += static_cast<char>(base + value % 26); } while (value /= 26);
|
|
return s;
|
|
}
|
|
|
|
const std::string& internConst(const DfgConst& vtx) {
|
|
const auto pair = m_internedConsts.emplace(vtx.num().ascii(false), "");
|
|
if (pair.second) pair.first->second += toLetters(m_internedConsts.size() - 1);
|
|
return pair.first->second;
|
|
}
|
|
|
|
const std::string& internSelLsb(uint32_t value) {
|
|
const auto pair = m_internedSelLsbs.emplace(value, "");
|
|
if (pair.second) pair.first->second += toLetters(m_internedSelLsbs.size() - 1);
|
|
return pair.first->second;
|
|
}
|
|
|
|
const std::string& internWordWidth(uint32_t value) {
|
|
const auto pair = m_internedWordWidths.emplace(value, "");
|
|
if (pair.second) pair.first->second += toLetters(m_internedWordWidths.size() - 1, true);
|
|
return pair.first->second;
|
|
}
|
|
|
|
const std::string& internWideWidth(uint32_t value) {
|
|
const auto pair = m_internedWideWidths.emplace(value, "");
|
|
if (pair.second) pair.first->second += toLetters(m_internedWideWidths.size() - 1);
|
|
return pair.first->second;
|
|
}
|
|
|
|
const std::string& internVertex(const DfgVertex& vtx) {
|
|
const auto pair = m_internedVertices.emplace(&vtx, "");
|
|
if (pair.second) pair.first->second += toLetters(m_internedVertices.size() - 1);
|
|
return pair.first->second;
|
|
}
|
|
|
|
void recordMultiplicityAndDepth(const DfgVertex& vtx, uint32_t depth) {
|
|
std::pair<uint32_t, uint32_t>& value = m_multiplicityAndDepth
|
|
.emplace(std::piecewise_construct, //
|
|
std::forward_as_tuple(&vtx), //
|
|
std::forward_as_tuple(0, depth))
|
|
.first->second;
|
|
value.first += 1;
|
|
value.second = std::max(value.second, depth);
|
|
if (!depth) return;
|
|
vtx.foreachSource([&](const DfgVertex& src) {
|
|
recordMultiplicityAndDepth(src, depth - 1);
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// Render the vertx into ss, and return true if the recursion reached the given depth,
|
|
// meaning an S-expression with that nesting level has been rendered.
|
|
void render(const DfgVertex& vtx, uint32_t depth, bool isRoot = true) {
|
|
if (const DfgConst* const constp = vtx.cast<DfgConst>()) {
|
|
// Base case 1: constant
|
|
if (constp->isZero()) {
|
|
m_os << "(CONST ZERO)";
|
|
} else if (constp->isOnes()) {
|
|
m_os << "(CONST ONES)";
|
|
} else {
|
|
m_os << "(CONST #" << internConst(*constp) << ')';
|
|
}
|
|
} else if (!isRoot && m_multiplicityAndDepth.at(&vtx).first > 1) {
|
|
// Base case 2: vertex appearing multiple times
|
|
m_os << internVertex(vtx);
|
|
} else if (!vtx.foreachSource([&](const DfgVertex&) { return true; })) {
|
|
// Base case 3: vertex with no inputs (input variable)
|
|
m_os << '(' << vtx.typeName() << ')';
|
|
} else if (depth == 0) {
|
|
// Base case 4: deep vertex (apperaing only once)
|
|
m_os << "_";
|
|
} else {
|
|
// Recursively print an S-expression for the vertex
|
|
m_os << '(';
|
|
// Name
|
|
m_os << vtx.typeName();
|
|
// Specials
|
|
if (const DfgSel* const selp = vtx.cast<DfgSel>()) {
|
|
m_os << '@';
|
|
if (selp->lsb() == 0) {
|
|
m_os << '0';
|
|
} else {
|
|
m_os << internSelLsb(selp->lsb());
|
|
}
|
|
}
|
|
// Operands
|
|
vtx.foreachSource([&](const DfgVertex& src) {
|
|
m_os << ' ';
|
|
render(src, depth - 1, false);
|
|
return false;
|
|
});
|
|
// S-expression end
|
|
m_os << ')';
|
|
}
|
|
|
|
// Annotate type
|
|
m_os << ':';
|
|
if (!vtx.dtype().isPacked()) {
|
|
vtx.dtype().astDtypep()->dumpSmall(m_os);
|
|
} else {
|
|
const uint32_t width = vtx.size();
|
|
if (width == 1) {
|
|
m_os << '1';
|
|
} else if (width <= VL_QUADSIZE) {
|
|
m_os << internWordWidth(width);
|
|
} else {
|
|
m_os << internWideWidth(width);
|
|
}
|
|
}
|
|
|
|
// Mark it if it has multiple sinks
|
|
if (vtx.hasMultipleSinks()) m_os << '*';
|
|
}
|
|
|
|
public:
|
|
DfgPatternString(std::ostream& os, const DfgVertex& vtx, uint32_t depth)
|
|
: m_os{os} {
|
|
recordMultiplicityAndDepth(vtx, depth);
|
|
render(vtx, depth, false);
|
|
using Pair = std::pair<std::string, const DfgVertex*>;
|
|
std::vector<Pair> vertices;
|
|
for (const auto& pair : m_multiplicityAndDepth) {
|
|
if (pair.second.first == 1) continue;
|
|
vertices.emplace_back(internVertex(*pair.first), pair.first);
|
|
}
|
|
std::sort(vertices.begin(), vertices.end(), [](const Pair& a, const Pair& b) { //
|
|
return a.first < b.first;
|
|
});
|
|
for (const Pair& pair : vertices) {
|
|
m_os << " | " << pair.first << " is ";
|
|
render(*pair.second, m_multiplicityAndDepth.at(pair.second).second);
|
|
}
|
|
}
|
|
};
|
|
|
|
std::string DfgVertex::patternString(uint32_t depth) const {
|
|
std::ostringstream oss;
|
|
DfgPatternString{oss, *this, depth};
|
|
return oss.str();
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// DfgVisitor
|
|
|
|
#include "V3Dfg__gen_visitor_defns.h" // From ./astgen
|