2013-03-15 04:24:08 -05:00
|
|
|
/*
|
|
|
|
* yosys -- Yosys Open SYnthesis Suite
|
|
|
|
*
|
|
|
|
* Copyright (C) 2012 Clifford Wolf <clifford@clifford.at>
|
2015-07-02 04:14:30 -05:00
|
|
|
*
|
2013-03-15 04:24:08 -05:00
|
|
|
* Permission to use, copy, modify, and/or distribute this software for any
|
|
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
|
|
* copyright notice and this permission notice appear in all copies.
|
2015-07-02 04:14:30 -05:00
|
|
|
*
|
2013-03-15 04:24:08 -05:00
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
// [[CITE]] Tarjan's strongly connected components algorithm
|
2015-08-13 02:30:20 -05:00
|
|
|
// Tarjan, R. E. (1972), "Depth-first search and linear graph algorithms", SIAM Journal on Computing 1 (2): 146-160, doi:10.1137/0201010
|
2013-03-15 04:24:08 -05:00
|
|
|
// http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
|
|
|
|
|
|
|
|
#include "kernel/register.h"
|
|
|
|
#include "kernel/celltypes.h"
|
|
|
|
#include "kernel/sigtools.h"
|
|
|
|
#include "kernel/log.h"
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <set>
|
|
|
|
|
2014-09-27 09:17:53 -05:00
|
|
|
USING_YOSYS_NAMESPACE
|
|
|
|
PRIVATE_NAMESPACE_BEGIN
|
|
|
|
|
2013-03-15 04:24:08 -05:00
|
|
|
struct SccWorker
|
|
|
|
{
|
|
|
|
RTLIL::Design *design;
|
|
|
|
RTLIL::Module *module;
|
|
|
|
SigMap sigmap;
|
2021-01-11 12:37:27 -06:00
|
|
|
CellTypes ct, specifyCells;
|
2013-03-15 04:24:08 -05:00
|
|
|
|
|
|
|
std::set<RTLIL::Cell*> workQueue;
|
|
|
|
std::map<RTLIL::Cell*, std::set<RTLIL::Cell*>> cellToNextCell;
|
|
|
|
std::map<RTLIL::Cell*, RTLIL::SigSpec> cellToPrevSig, cellToNextSig;
|
|
|
|
|
|
|
|
std::map<RTLIL::Cell*, std::pair<int, int>> cellLabels;
|
|
|
|
std::map<RTLIL::Cell*, int> cellDepth;
|
|
|
|
std::set<RTLIL::Cell*> cellsOnStack;
|
|
|
|
std::vector<RTLIL::Cell*> cellStack;
|
|
|
|
int labelCounter;
|
|
|
|
|
|
|
|
std::map<RTLIL::Cell*, int> cell2scc;
|
|
|
|
std::vector<std::set<RTLIL::Cell*>> sccList;
|
|
|
|
|
|
|
|
void run(RTLIL::Cell *cell, int depth, int maxDepth)
|
|
|
|
{
|
2014-07-28 04:08:55 -05:00
|
|
|
log_assert(workQueue.count(cell) > 0);
|
2013-03-15 04:24:08 -05:00
|
|
|
|
|
|
|
workQueue.erase(cell);
|
|
|
|
cellLabels[cell] = std::pair<int, int>(labelCounter, labelCounter);
|
|
|
|
labelCounter++;
|
|
|
|
|
|
|
|
cellsOnStack.insert(cell);
|
|
|
|
cellStack.push_back(cell);
|
|
|
|
|
|
|
|
if (maxDepth >= 0)
|
|
|
|
cellDepth[cell] = depth;
|
|
|
|
|
|
|
|
for (auto nextCell : cellToNextCell[cell])
|
|
|
|
if (cellLabels.count(nextCell) == 0) {
|
|
|
|
run(nextCell, depth+1, maxDepth);
|
2015-10-25 13:30:49 -05:00
|
|
|
cellLabels[cell].second = min(cellLabels[cell].second, cellLabels[nextCell].second);
|
2013-03-15 04:24:08 -05:00
|
|
|
} else
|
|
|
|
if (cellsOnStack.count(nextCell) > 0 && (maxDepth < 0 || cellDepth[nextCell] + maxDepth > depth)) {
|
2015-10-25 13:30:49 -05:00
|
|
|
cellLabels[cell].second = min(cellLabels[cell].second, cellLabels[nextCell].second);
|
2013-03-15 04:24:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (cellLabels[cell].first == cellLabels[cell].second)
|
2013-03-15 04:29:25 -05:00
|
|
|
{
|
2013-03-15 04:24:08 -05:00
|
|
|
if (cellStack.back() == cell)
|
|
|
|
{
|
|
|
|
cellStack.pop_back();
|
|
|
|
cellsOnStack.erase(cell);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
log("Found an SCC:");
|
|
|
|
std::set<RTLIL::Cell*> scc;
|
|
|
|
while (cellsOnStack.count(cell) > 0) {
|
|
|
|
RTLIL::Cell *c = cellStack.back();
|
|
|
|
cellStack.pop_back();
|
|
|
|
cellsOnStack.erase(c);
|
|
|
|
log(" %s", RTLIL::id2cstr(c->name));
|
|
|
|
cell2scc[c] = sccList.size();
|
|
|
|
scc.insert(c);
|
|
|
|
}
|
|
|
|
sccList.push_back(scc);
|
|
|
|
log("\n");
|
|
|
|
}
|
2013-03-15 04:29:25 -05:00
|
|
|
}
|
2013-03-15 04:24:08 -05:00
|
|
|
}
|
|
|
|
|
2021-01-11 12:37:27 -06:00
|
|
|
SccWorker(RTLIL::Design *design, RTLIL::Module *module, bool nofeedbackMode, bool allCellTypes, bool specifyMode, int maxDepth) :
|
2015-02-10 01:48:55 -06:00
|
|
|
design(design), module(module), sigmap(module)
|
2013-03-15 04:24:08 -05:00
|
|
|
{
|
|
|
|
if (module->processes.size() > 0) {
|
|
|
|
log("Skipping module %s as it contains processes (run 'proc' pass first).\n", module->name.c_str());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (allCellTypes) {
|
|
|
|
ct.setup(design);
|
|
|
|
} else {
|
|
|
|
ct.setup_internals();
|
|
|
|
ct.setup_stdcells();
|
|
|
|
}
|
|
|
|
|
2021-01-11 12:37:27 -06:00
|
|
|
// Discover boxes with specify rules in them, for special handling.
|
|
|
|
if (specifyMode) {
|
|
|
|
for (auto mod : design->modules())
|
|
|
|
if (mod->get_blackbox_attribute(false))
|
|
|
|
for (auto cell : mod->cells())
|
|
|
|
if (cell->type == ID($specify2))
|
|
|
|
{
|
|
|
|
specifyCells.setup_module(mod);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-03-15 04:24:08 -05:00
|
|
|
SigPool selectedSignals;
|
|
|
|
SigSet<RTLIL::Cell*> sigToNextCells;
|
|
|
|
|
2014-07-26 18:49:51 -05:00
|
|
|
for (auto &it : module->wires_)
|
2013-03-15 04:24:08 -05:00
|
|
|
if (design->selected(module, it.second))
|
|
|
|
selectedSignals.add(sigmap(RTLIL::SigSpec(it.second)));
|
|
|
|
|
2014-07-26 18:51:45 -05:00
|
|
|
for (auto &it : module->cells_)
|
2013-03-15 04:24:08 -05:00
|
|
|
{
|
|
|
|
RTLIL::Cell *cell = it.second;
|
|
|
|
|
|
|
|
if (!design->selected(module, cell))
|
|
|
|
continue;
|
|
|
|
|
2021-01-11 12:37:27 -06:00
|
|
|
if (!allCellTypes && !ct.cell_known(cell->type) && !specifyCells.cell_known(cell->type))
|
2013-03-15 04:24:08 -05:00
|
|
|
continue;
|
|
|
|
|
|
|
|
workQueue.insert(cell);
|
|
|
|
|
|
|
|
RTLIL::SigSpec inputSignals, outputSignals;
|
|
|
|
|
2021-01-11 12:37:27 -06:00
|
|
|
if (specifyCells.cell_known(cell->type)) {
|
|
|
|
// Use specify rules of the type `(X => Y) = NN` to look for asynchronous paths in boxes.
|
|
|
|
for (auto subcell : design->module(cell->type)->cells())
|
|
|
|
{
|
|
|
|
if (subcell->type != ID($specify2))
|
|
|
|
continue;
|
2013-03-15 04:24:08 -05:00
|
|
|
|
2021-01-11 12:37:27 -06:00
|
|
|
for (auto bit : subcell->getPort(ID::SRC))
|
|
|
|
{
|
|
|
|
if (!bit.wire || !cell->hasPort(bit.wire->name))
|
|
|
|
continue;
|
|
|
|
inputSignals.append(sigmap(cell->getPort(bit.wire->name)));
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto bit : subcell->getPort(ID::DST))
|
|
|
|
{
|
|
|
|
if (!bit.wire || !cell->hasPort(bit.wire->name))
|
|
|
|
continue;
|
|
|
|
outputSignals.append(sigmap(cell->getPort(bit.wire->name)));
|
|
|
|
}
|
2013-03-15 04:24:08 -05:00
|
|
|
}
|
2021-01-11 12:37:27 -06:00
|
|
|
} else {
|
|
|
|
for (auto &conn : cell->connections())
|
|
|
|
{
|
|
|
|
bool isInput = true, isOutput = true;
|
|
|
|
|
|
|
|
if (ct.cell_known(cell->type)) {
|
|
|
|
isInput = ct.cell_input(cell->type, conn.first);
|
|
|
|
isOutput = ct.cell_output(cell->type, conn.first);
|
|
|
|
}
|
2013-03-15 04:24:08 -05:00
|
|
|
|
2021-01-11 12:37:27 -06:00
|
|
|
RTLIL::SigSpec sig = selectedSignals.extract(sigmap(conn.second));
|
|
|
|
sig.sort_and_unify();
|
2013-03-15 04:24:08 -05:00
|
|
|
|
2021-01-11 12:37:27 -06:00
|
|
|
if (isInput)
|
|
|
|
inputSignals.append(sig);
|
|
|
|
if (isOutput)
|
|
|
|
outputSignals.append(sig);
|
|
|
|
}
|
2013-03-15 04:24:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
inputSignals.sort_and_unify();
|
|
|
|
outputSignals.sort_and_unify();
|
|
|
|
|
|
|
|
cellToPrevSig[cell] = inputSignals;
|
|
|
|
cellToNextSig[cell] = outputSignals;
|
|
|
|
sigToNextCells.insert(inputSignals, cell);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto cell : workQueue)
|
2015-02-10 01:48:55 -06:00
|
|
|
{
|
2016-11-05 18:03:35 -05:00
|
|
|
cellToNextCell[cell] = sigToNextCells.find(cellToNextSig[cell]);
|
2015-02-10 01:48:55 -06:00
|
|
|
|
|
|
|
if (!nofeedbackMode && cellToNextCell[cell].count(cell)) {
|
|
|
|
log("Found an SCC:");
|
|
|
|
std::set<RTLIL::Cell*> scc;
|
|
|
|
log(" %s", RTLIL::id2cstr(cell->name));
|
|
|
|
cell2scc[cell] = sccList.size();
|
|
|
|
scc.insert(cell);
|
|
|
|
sccList.push_back(scc);
|
|
|
|
log("\n");
|
2016-05-27 09:33:13 -05:00
|
|
|
}
|
2016-11-05 18:03:35 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
labelCounter = 0;
|
|
|
|
cellLabels.clear();
|
|
|
|
|
|
|
|
while (!workQueue.empty())
|
|
|
|
{
|
|
|
|
RTLIL::Cell *cell = *workQueue.begin();
|
|
|
|
log_assert(cellStack.size() == 0);
|
|
|
|
cellDepth.clear();
|
2016-05-27 09:33:13 -05:00
|
|
|
|
|
|
|
run(cell, 0, maxDepth);
|
2013-03-15 04:24:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
log("Found %d SCCs in module %s.\n", int(sccList.size()), RTLIL::id2cstr(module->name));
|
|
|
|
}
|
|
|
|
|
|
|
|
void select(RTLIL::Selection &sel)
|
|
|
|
{
|
|
|
|
for (int i = 0; i < int(sccList.size()); i++)
|
|
|
|
{
|
|
|
|
std::set<RTLIL::Cell*> &cells = sccList[i];
|
|
|
|
RTLIL::SigSpec prevsig, nextsig, sig;
|
|
|
|
|
|
|
|
for (auto cell : cells) {
|
|
|
|
sel.selected_members[module->name].insert(cell->name);
|
|
|
|
prevsig.append(cellToPrevSig[cell]);
|
|
|
|
nextsig.append(cellToNextSig[cell]);
|
|
|
|
}
|
|
|
|
|
|
|
|
prevsig.sort_and_unify();
|
|
|
|
nextsig.sort_and_unify();
|
|
|
|
sig = prevsig.extract(nextsig);
|
|
|
|
|
2014-07-22 13:15:14 -05:00
|
|
|
for (auto &chunk : sig.chunks())
|
2013-03-15 04:24:08 -05:00
|
|
|
if (chunk.wire != NULL)
|
|
|
|
sel.selected_members[module->name].insert(chunk.wire->name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
struct SccPass : public Pass {
|
|
|
|
SccPass() : Pass("scc", "detect strongly connected components (logic loops)") { }
|
2020-06-18 18:34:52 -05:00
|
|
|
void help() override
|
2013-03-15 04:24:08 -05:00
|
|
|
{
|
|
|
|
// |---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|
|
|
|
|
log("\n");
|
|
|
|
log(" scc [options] [selection]\n");
|
|
|
|
log("\n");
|
|
|
|
log("This command identifies strongly connected components (aka logic loops) in the\n");
|
|
|
|
log("design.\n");
|
|
|
|
log("\n");
|
2015-02-10 01:48:55 -06:00
|
|
|
log(" -expect <num>\n");
|
2021-01-11 12:37:27 -06:00
|
|
|
log(" expect to find exactly <num> SCCs. A different number of SCCs will\n");
|
2015-02-10 01:48:55 -06:00
|
|
|
log(" produce an error.\n");
|
|
|
|
log("\n");
|
2013-03-15 04:24:08 -05:00
|
|
|
log(" -max_depth <num>\n");
|
2015-02-10 01:48:55 -06:00
|
|
|
log(" limit to loops not longer than the specified number of cells. This\n");
|
|
|
|
log(" can e.g. be useful in identifying small local loops in a module that\n");
|
|
|
|
log(" implements one large SCC.\n");
|
|
|
|
log("\n");
|
|
|
|
log(" -nofeedback\n");
|
|
|
|
log(" do not count cells that have their output fed back into one of their\n");
|
|
|
|
log(" inputs as single-cell scc.\n");
|
2013-03-15 04:24:08 -05:00
|
|
|
log("\n");
|
|
|
|
log(" -all_cell_types\n");
|
|
|
|
log(" Usually this command only considers internal non-memory cells. With\n");
|
2014-09-06 01:47:06 -05:00
|
|
|
log(" this option set, all cells are considered. For unknown cells all ports\n");
|
2013-03-15 04:24:08 -05:00
|
|
|
log(" are assumed to be bidirectional 'inout' ports.\n");
|
|
|
|
log("\n");
|
|
|
|
log(" -set_attr <name> <value>\n");
|
2016-11-05 18:04:10 -05:00
|
|
|
log(" set the specified attribute on all cells that are part of a logic\n");
|
|
|
|
log(" loop. the special token {} in the value is replaced with a unique\n");
|
|
|
|
log(" identifier for the logic loop.\n");
|
2013-03-15 04:24:08 -05:00
|
|
|
log("\n");
|
|
|
|
log(" -select\n");
|
|
|
|
log(" replace the current selection with a selection of all cells and wires\n");
|
|
|
|
log(" that are part of a found logic loop\n");
|
|
|
|
log("\n");
|
2021-01-11 12:37:27 -06:00
|
|
|
log(" -specify\n");
|
|
|
|
log(" examine specify rules to detect logic loops in whitebox/blackbox cells\n");
|
|
|
|
log("\n");
|
2013-03-15 04:24:08 -05:00
|
|
|
}
|
2020-06-18 18:34:52 -05:00
|
|
|
void execute(std::vector<std::string> args, RTLIL::Design *design) override
|
2013-03-15 04:24:08 -05:00
|
|
|
{
|
2016-11-05 18:04:10 -05:00
|
|
|
std::map<std::string, std::string> setAttr;
|
2013-03-15 04:24:08 -05:00
|
|
|
bool allCellTypes = false;
|
|
|
|
bool selectMode = false;
|
2015-02-10 01:48:55 -06:00
|
|
|
bool nofeedbackMode = false;
|
2021-01-11 12:37:27 -06:00
|
|
|
bool specifyMode = false;
|
2013-03-15 04:24:08 -05:00
|
|
|
int maxDepth = -1;
|
2015-02-10 01:48:55 -06:00
|
|
|
int expect = -1;
|
2013-03-15 04:24:08 -05:00
|
|
|
|
2016-04-21 16:28:37 -05:00
|
|
|
log_header(design, "Executing SCC pass (detecting logic loops).\n");
|
2013-03-15 04:24:08 -05:00
|
|
|
|
|
|
|
size_t argidx;
|
|
|
|
for (argidx = 1; argidx < args.size(); argidx++) {
|
|
|
|
if (args[argidx] == "-max_depth" && argidx+1 < args.size()) {
|
|
|
|
maxDepth = atoi(args[++argidx].c_str());
|
|
|
|
continue;
|
|
|
|
}
|
2015-02-10 01:48:55 -06:00
|
|
|
if (args[argidx] == "-expect" && argidx+1 < args.size()) {
|
|
|
|
expect = atoi(args[++argidx].c_str());
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (args[argidx] == "-nofeedback") {
|
|
|
|
nofeedbackMode = true;
|
|
|
|
continue;
|
|
|
|
}
|
2013-03-15 04:24:08 -05:00
|
|
|
if (args[argidx] == "-all_cell_types") {
|
|
|
|
allCellTypes = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (args[argidx] == "-set_attr" && argidx+2 < args.size()) {
|
2016-11-05 18:04:10 -05:00
|
|
|
setAttr[args[argidx+1]] = args[argidx+2];
|
2013-03-15 04:24:08 -05:00
|
|
|
argidx += 2;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (args[argidx] == "-select") {
|
|
|
|
selectMode = true;
|
|
|
|
continue;
|
|
|
|
}
|
2021-01-11 12:37:27 -06:00
|
|
|
if (args[argidx] == "-specify") {
|
|
|
|
specifyMode = true;
|
|
|
|
continue;
|
|
|
|
}
|
2013-03-15 04:24:08 -05:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
int origSelectPos = design->selection_stack.size() - 1;
|
|
|
|
extra_args(args, argidx, design);
|
|
|
|
|
|
|
|
RTLIL::Selection newSelection(false);
|
2015-02-10 01:48:55 -06:00
|
|
|
int scc_counter = 0;
|
2013-03-15 04:24:08 -05:00
|
|
|
|
2020-01-06 14:36:11 -06:00
|
|
|
for (auto mod : design->selected_modules())
|
|
|
|
{
|
2021-01-11 12:37:27 -06:00
|
|
|
SccWorker worker(design, mod, nofeedbackMode, allCellTypes, specifyMode, maxDepth);
|
2016-11-05 18:04:10 -05:00
|
|
|
|
2020-01-06 14:36:11 -06:00
|
|
|
if (!setAttr.empty())
|
|
|
|
{
|
|
|
|
for (const auto &cells : worker.sccList)
|
2016-11-05 18:04:10 -05:00
|
|
|
{
|
2020-01-06 14:36:11 -06:00
|
|
|
for (auto attr : setAttr)
|
2016-11-05 18:04:10 -05:00
|
|
|
{
|
2020-01-06 14:36:11 -06:00
|
|
|
IdString attr_name(RTLIL::escape_id(attr.first));
|
|
|
|
string attr_valstr = attr.second;
|
|
|
|
string index = stringf("%d", scc_counter);
|
2016-11-05 18:04:10 -05:00
|
|
|
|
2020-01-06 14:36:11 -06:00
|
|
|
for (size_t pos = 0; (pos = attr_valstr.find("{}", pos)) != string::npos; pos += index.size())
|
|
|
|
attr_valstr.replace(pos, 2, index);
|
2016-11-05 18:04:10 -05:00
|
|
|
|
2020-01-06 14:36:11 -06:00
|
|
|
Const attr_value(attr_valstr);
|
2016-11-05 18:04:10 -05:00
|
|
|
|
2020-01-06 14:36:11 -06:00
|
|
|
for (auto cell : cells)
|
|
|
|
cell->attributes[attr_name] = attr_value;
|
2016-11-05 18:04:10 -05:00
|
|
|
}
|
2013-03-15 04:24:08 -05:00
|
|
|
|
2020-01-06 14:36:11 -06:00
|
|
|
scc_counter++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
scc_counter += GetSize(worker.sccList);
|
2013-03-15 04:24:08 -05:00
|
|
|
}
|
|
|
|
|
2020-01-06 14:36:11 -06:00
|
|
|
if (selectMode)
|
|
|
|
worker.select(newSelection);
|
|
|
|
}
|
|
|
|
|
2015-02-10 01:48:55 -06:00
|
|
|
if (expect >= 0) {
|
|
|
|
if (scc_counter == expect)
|
|
|
|
log("Found and expected %d SCCs.\n", scc_counter);
|
|
|
|
else
|
|
|
|
log_error("Found %d SCCs but expected %d.\n", scc_counter, expect);
|
|
|
|
} else
|
|
|
|
log("Found %d SCCs.\n", scc_counter);
|
|
|
|
|
2013-03-15 04:24:08 -05:00
|
|
|
if (selectMode) {
|
2014-07-28 04:08:55 -05:00
|
|
|
log_assert(origSelectPos >= 0);
|
2013-03-15 04:24:08 -05:00
|
|
|
design->selection_stack[origSelectPos] = newSelection;
|
|
|
|
design->selection_stack[origSelectPos].optimize(design);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} SccPass;
|
2015-07-02 04:14:30 -05:00
|
|
|
|
2014-09-27 09:17:53 -05:00
|
|
|
PRIVATE_NAMESPACE_END
|