3
0
Fork 0
mirror of https://github.com/YosysHQ/yosys synced 2026-07-23 23:52:32 +00:00
yosys/passes/silimate/carvenetlist.cc
Akash Levy 80bc373519 carvenetlist: keep cone boundary gate; address review comments
Fix the failing regression test: the single-fanout passthrough removal was
shorting out a cone's only real gate (e.g. a lone $_NOT_ driving an output),
replacing it with a bare wire. That drops the gate entirely (nothing left to
characterize) and, for an inverter, silently drops the inversion, making the
carved cell inequivalent to the RTL. Only short a redundant re-driver whose
input is driven by another in-cone cell; keep a passthrough that reads a
primary input (the cell-under-test's boundary gate).

Also address Greptile review comments:
- fix swapped log_warning arguments in the split-boundary-port diagnostic.
- error out (instead of silently overwriting) when two cell groups rename to
  the same carved module name (e.g. slow_<enc> and fast_<enc> -> <enc>).
- derive pq_speed from the explicit "fast_" base prefix.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 09:13:18 -07:00

1045 lines
46 KiB
C++

/*
* yosys -- Yosys Open SYnthesis Suite
*
* Copyright (C) 2025 Silimate Inc.
*
* 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.
*
* 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.
*
*/
#include "kernel/sigtools.h"
#include "kernel/yosys.h"
#include <algorithm>
#include <map>
USING_YOSYS_NAMESPACE
PRIVATE_NAMESPACE_BEGIN
// Strip a leading RTLIL "\" escape from an IdString's text.
static std::string unescape(const std::string &s) { return (!s.empty() && s[0] == '\\') ? s.substr(1) : s; }
// A buffer/inverter has exactly one input and one output pin (a 2-port cell). Synthesis
// only ever inserts such pass-throughs (drive-strength buffers/inverters) on the single
// net between a train top port and its surrounding flop, so hopping over them reaches the
// real boundary flop regardless of how many were added; any flop/gate has >2 ports.
static bool is_passthrough(Cell *cell)
{
int n_in = 0, n_out = 0;
for (auto &conn : cell->connections()) {
if (cell->input(conn.first)) {
if (++n_in > 1)
return false;
} else if (cell->output(conn.first)) {
if (++n_out > 1)
return false;
}
}
return n_in == 1 && n_out == 1;
}
// True if a port's trailing "_<pin>" segment names a clock (clk/clock, case-insensitive),
// so shared/per-cell clock ports aren't treated as data inputs.
static bool is_clock_pin(const std::string &pin)
{
std::string p;
for (char c : pin)
p += std::tolower((unsigned char)c);
return p.find("clk") != std::string::npos || p.find("clock") != std::string::npos;
}
// True if a flop input pin is an (a)synchronous set/reset/clear/preset/load control pin
// (case-insensitive). Such pins are not the data (D) input, so they must be skipped when
// finding the captured-data pin: an async set/reset flop (e.g. ASYNC_DFFHx1 with pins
// D/RESET/SET/CLK, used by adff_surround) would otherwise let a RESET/SET net (the shared
// arst) be mistaken for the data pin, tying the carved cell's output to arst.
static bool is_set_reset_pin(const std::string &pin)
{
std::string p;
for (char c : pin)
p += std::tolower((unsigned char)c);
for (const char *k : {"reset", "set", "clr", "clear", "arst", "srst", "aload", "sload"})
if (p.find(k) != std::string::npos)
return true;
return false;
}
// Map a carved cell's port-base name to the Preqorsor dataset naming convention: carved
// cells come in as slow_<enc>/fast_<enc> and OpenIP designs as slow_design_<enc>/
// fast_design_<enc>, but downstream parsing/decryption expects <enc> and design_<speed>_
// <enc>. The design_ rewrites run first so "slow_"/"fast_" can't clip them.
static std::string pq_rename(const std::string &base)
{
if (base.rfind("slow_design_", 0) == 0)
return "design_slow_" + base.substr(12);
if (base.rfind("fast_design_", 0) == 0)
return "design_fast_" + base.substr(12);
if (base.rfind("slow_", 0) == 0)
return base.substr(5);
if (base.rfind("fast_", 0) == 0)
return base.substr(5);
return base;
}
struct BoundaryRec {
std::string speed, driving_cell, driving_pin, load_cell, load_pin;
};
struct CarveNetlistPass : public Pass {
CarveNetlistPass()
: Pass("carvenetlist", "carve surround-with-flops cells into per-cell modules and record their flop boundary")
{
}
void help() override
{
// |---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|
log("\n");
log(" carvenetlist\n");
log("\n");
log("Carve each surround-with-flops cell-under-test out of the flat 'train' module into\n");
log("its own flop-free module and record the surrounding flops as that cell's boundary\n");
log("conditions.\n");
log("\n");
log("The training netlist is one giant flat module (\"train\") of independent\n");
log("cells-under-test, each surrounded by flip-flops (see ml.dataset.surround_with_flops):\n");
log("every data input is driven by a launch flop's Q and every data output is captured\n");
log("by a load flop's D, with two shared clocks (fast_clk/slow_clk). This pass carves\n");
log("each cell-under-test out into its own flop-free module (named after the dataset\n");
log("cell) and records the surrounding flops as \\pq_* string attributes on the carved\n");
log("module (\\pq_speed, \\pq_driving_cell, \\pq_driving_pin, \\pq_load_cell,\n");
log("\\pq_load_pin), so OpenSTA can re-impose them (set_driving_cell on the inputs,\n");
log("set_load resolved from the load flop's data pin on the outputs).\n");
log("\n");
log("Anchored on the train top-level ports (the only structure guaranteed to survive\n");
log("synthesis), grouped by cell base name \"<base>_<pin>\":\n");
log("\n");
log(" - launch flop = hop forward from each data input port through any\n");
log(" drive-strength buffers/inverters (1-in/1-out cells) to the\n");
log(" first >2-port cell;\n");
log(" - capture flop = the mirror backward hop from each data output port;\n");
log(" - cell-under-test = the logic cone walked forward from the launch flops'\n");
log(" outputs, excluding the boundary flops; tagged \\submod and\n");
log(" carved by submod.\n");
log("\n");
log("The shared clock network (fast_clk/slow_clk and their buffer tree) is treated as\n");
log("a hard boundary: cones never expand through it, so a carve can't leak across the\n");
log("clock into another cell's surrounding flops.\n");
log("\n");
}
void execute(std::vector<std::string> args, RTLIL::Design *design) override
{
log_header(design, "Executing CARVENETLIST pass (carve surround-with-flops cells into "
"per-cell modules and record their flop boundary).\n");
size_t argidx = 1;
extra_args(args, argidx, design);
Module *train = design->module(ID(train));
if (train == nullptr) {
log_warning("No 'train' module found; nothing to carve.\n");
return;
}
// Marks the zero-area $_BUF_ cells we insert at the capture-flop boundary (below) so
// they can be turned back into plain assigns after carving.
const IdString PQ_PASS = RTLIL::escape_id("pq_passthrough");
// Marks the reconstructed per-pin output bus wire ("__pqcarve.<base>_<pin>") so the
// port-name cleanup can tell it apart from a colliding stale "__pqo" cone-output
// fragment (which must be demoted to an internal net, not aliased onto the output).
const IdString PQ_CARVE_OUT = RTLIL::escape_id("pq_carve_out");
SigMap sigmap(train);
// Re-root *only* fused feed-through net groups toward their launch-side (input) net.
// A feed-through output bit (Y[i] = some input) lets synthesis fuse the launch-flop
// input net ("..._<pin>__pqi[j]") and the capture-flop output net
// ("..._<pin>__pqo[i]") into one SigMap group. Whichever bit SigMap happens to elect
// as the group rep is the net the carve reads and reconstructs from; if it elects the
// "__pqo" output fragment, that fragment is only *driven* by the launch flop (a
// boundary cell carved away), so the cone reads it but nothing drives it -> floating
// bit + inout output bus. Promote the "__pqi" bit, but ONLY for groups that also hold
// a "__pqo" bit (true feed-throughs). Promoting every "__pqi" net would re-root the
// rep of ordinary input groups too, churning internal net names across nearly every
// carved cell (and needlessly invalidating their cached power VCDs).
{
dict<SigBit, SigBit> pqi_bit_of_group; // group rep -> a "__pqi" bit in that group
pool<SigBit> groups_with_pqo; // group reps that contain a "__pqo" bit
for (auto wire : train->wires()) {
std::string nm = unescape(wire->name.str());
bool is_pqi = nm.find("__pqi") != std::string::npos;
bool is_pqo = nm.find("__pqo") != std::string::npos;
if (!is_pqi && !is_pqo)
continue;
for (auto bit : SigSpec(wire)) {
SigBit r = sigmap(bit);
if (r.wire == nullptr)
continue;
if (is_pqi && !pqi_bit_of_group.count(r))
pqi_bit_of_group[r] = bit;
if (is_pqo)
groups_with_pqo.insert(r);
}
}
for (auto &it : pqi_bit_of_group)
if (groups_with_pqo.count(it.first))
sigmap.add(it.second);
}
// Instance-level driver/load maps over canonicalized nets. Keyed by net bit so the
// surrounding flops can be found by topology and excluded from the carve.
dict<SigBit, std::pair<Cell *, IdString>> driver; // net -> (cell, out pin)
dict<SigBit, std::vector<std::pair<Cell *, IdString>>> loads; // net -> [(cell, in pin)]
dict<Cell *, std::vector<SigBit>> cell_out_bits, cell_in_bits;
for (auto cell : train->cells()) {
for (auto &conn : cell->connections()) {
bool is_out = cell->output(conn.first);
bool is_in = cell->input(conn.first);
for (auto bit : sigmap(conn.second)) {
if (bit.wire == nullptr)
continue; // skip constants
if (is_out) {
driver[bit] = {cell, conn.first};
cell_out_bits[cell].push_back(bit);
}
if (is_in) {
loads[bit].push_back({cell, conn.first});
cell_in_bits[cell].push_back(bit);
}
}
}
}
// Identify the shared clock distribution network. Every cell-under-test is driven
// by the same two top-level clocks (fast_clk/slow_clk, named so by
// ml.dataset.surround_with_flops), so the clock tree is the one net set that
// connects otherwise-independent cells: its leaf nets fan out to the CLK pin of
// every flop across every cell/design. Seed from the clock top ports and propagate
// forward only through pass-through clock buffers (stopping at flops). Excluding
// these nets from the cone below keeps each carve bounded by data connectivity
// alone, so it can never leak across the shared clock into a neighbor's flops.
pool<SigBit> clk_nets;
{
std::vector<SigBit> wl;
for (auto wire : train->wires()) {
if (!wire->port_input && !wire->port_output)
continue;
std::string name = unescape(wire->name.str());
auto us = name.rfind('_');
std::string pin = (us == std::string::npos) ? name : name.substr(us + 1);
if (!is_clock_pin(pin))
continue;
for (auto bit : sigmap(SigSpec(wire)))
if (bit.wire)
wl.push_back(bit);
}
while (!wl.empty()) {
SigBit b = wl.back();
wl.pop_back();
if (clk_nets.count(b))
continue;
clk_nets.insert(b);
auto it = loads.find(b);
if (it == loads.end())
continue;
for (auto &pr : it->second) {
if (!is_passthrough(pr.first))
continue; // stop at flops; only follow clock buffers
for (auto ob : cell_out_bits[pr.first])
wl.push_back(ob);
}
}
}
// Group top-level data ports by cell base name (skip clock ports). out_wires keeps the
// actual output port wires (not just their sigmapped bits) so the carved output bus
// can be rebuilt one clean port per pin at the capture-flop boundary below.
struct Group {
std::vector<SigBit> ins, outs;
std::vector<Wire *> in_wires, out_wires;
};
std::map<std::string, Group> groups;
for (auto wire : train->wires()) {
if (!wire->port_input && !wire->port_output)
continue;
std::string name = unescape(wire->name.str());
auto us = name.rfind('_');
if (us == std::string::npos || us == 0)
continue;
std::string pin = name.substr(us + 1);
std::string base = name.substr(0, us);
if (base.empty() || is_clock_pin(pin))
continue;
Group &g = groups[base];
if (wire->port_output)
g.out_wires.push_back(wire);
else
g.in_wires.push_back(wire);
for (auto bit : sigmap(SigSpec(wire))) {
if (bit.wire == nullptr)
continue;
if (wire->port_input)
g.ins.push_back(bit);
else
g.outs.push_back(bit);
}
}
// Forward-hop from an input-port net through buffers to the launch flop (or null)
auto hop_fwd = [&](SigBit bit) -> Cell * {
pool<Cell *> seen;
while (true) {
if (clk_nets.count(bit))
return nullptr; // never walk into the shared clock network
auto it = loads.find(bit);
if (it == loads.end())
return nullptr;
Cell *c = nullptr;
for (auto &pr : it->second) {
if (c && pr.first != c)
return nullptr; // fork: no clean single boundary
c = pr.first;
}
if (c == nullptr || seen.count(c))
return nullptr;
seen.insert(c);
if (!is_passthrough(c))
return c;
auto &outs = cell_out_bits[c];
if (outs.size() != 1)
return nullptr;
bit = outs[0];
}
};
// Backward-hop from an output-port net through buffers to the capture flop (or null)
auto hop_back = [&](SigBit bit) -> Cell * {
pool<Cell *> seen;
while (true) {
if (clk_nets.count(bit))
return nullptr; // never walk into the shared clock network
auto it = driver.find(bit);
if (it == driver.end())
return nullptr;
Cell *c = it->second.first;
if (seen.count(c))
return nullptr;
seen.insert(c);
if (!is_passthrough(c))
return c;
auto &ins = cell_in_bits[c];
if (ins.size() != 1)
return nullptr;
bit = ins[0];
}
};
std::map<std::string, BoundaryRec> boundary;
std::map<std::string, std::string> final_of_base; // carved module name -> the base it came from
int tagged_total = 0;
for (auto &grp : groups) {
const std::string &base = grp.first;
Group &g = grp.second;
// Launch flops drive the inputs; hop through buffers, seed the cone from the
// flop output that drives the cell, and record (type, out-pin) as the driver.
pool<Cell *> launch, capture, boundary_cells;
pool<SigBit> seed;
std::string drv_cell, drv_pin;
for (auto bit : g.ins) {
Cell *flop = hop_fwd(bit);
if (flop == nullptr)
continue;
launch.insert(flop);
for (auto ob : cell_out_bits[flop]) {
if (clk_nets.count(ob))
continue; // a launch flop's data output, never a generated clock
if (!loads.count(ob))
continue; // only the flop output that actually drives the cell
if (drv_cell.empty()) {
drv_cell = unescape(flop->type.str());
drv_pin = unescape(driver[ob].second.str());
}
seed.insert(ob);
}
}
// Capture flops load the outputs; hop back through buffers to the real flop.
for (auto bit : g.outs) {
Cell *flop = hop_back(bit);
if (flop != nullptr)
capture.insert(flop);
}
boundary_cells = launch;
for (auto c : capture)
boundary_cells.insert(c);
// Walk the logic cone forward from the launch outputs, excluding boundary flops
pool<Cell *> logic;
pool<SigBit> driven = seed;
std::vector<SigBit> stack(seed.begin(), seed.end());
while (!stack.empty()) {
SigBit k = stack.back();
stack.pop_back();
if (clk_nets.count(k))
continue; // hard boundary: never expand through the shared clock tree
auto it = loads.find(k);
if (it == loads.end())
continue;
for (auto &pr : it->second) {
Cell *c = pr.first;
if (boundary_cells.count(c) || logic.count(c))
continue;
logic.insert(c);
for (auto ob : cell_out_bits[c]) {
if (clk_nets.count(ob))
continue; // don't follow a cell's generated/forwarded clock
driven.insert(ob);
stack.push_back(ob);
}
}
}
// Redirect cone-cell inputs that physically read a fused feed-through "__pqo"
// fragment to that group's launch-side rep (an input net, after the promotion
// above). The "__pqo" fragment is driven only by the launch flop, which is carved
// away as a boundary cell, so a cone cell left wired to it would read a floating
// net. Restrict the rewrite to bits whose wire is "__pqo"-named so ordinary cone
// reads keep their original net (no cosmetic net-name churn across cells).
for (auto c : logic) {
dict<IdString, SigSpec> rewire;
for (auto &conn : c->connections()) {
if (!c->input(conn.first))
continue;
SigSpec cs = conn.second;
bool changed = false;
for (auto &bit : cs) {
if (bit.wire == nullptr ||
unescape(bit.wire->name.str()).find("__pqo") == std::string::npos)
continue;
SigBit r = sigmap(bit);
if (r != bit) {
bit = r;
changed = true;
}
}
if (changed)
rewire[conn.first] = cs;
}
for (auto &r : rewire)
c->setPort(r.first, r.second);
}
// Tag the cell logic for submod.
std::string final = pq_rename(base);
// pq_rename strips the "slow_"/"fast_" speed prefix off plain cells, so a train
// holding both "slow_<enc>" and "fast_<enc>" would map both to "<enc>" -- carving
// two different cells into one module and silently overwriting the first cell's
// \pq_* boundary record with the second. Refuse rather than emit a wrong netlist.
if (final_of_base.count(final))
log_error("carvenetlist: cell groups '%s' and '%s' both map to carved module "
"name '%s'; cannot carve two cells into one module.\n",
final_of_base.at(final).c_str(), base.c_str(), final.c_str());
final_of_base[final] = base;
for (auto c : logic)
c->set_string_attribute(RTLIL::escape_id("submod"), final);
tagged_total += GetSize(logic);
// A DESIGN cell-under-test (slow_design_/fast_design_) is a whole sequential design,
// not a single mapped cell; flag it so the self-containment pass below also rebuilds
// its launch-flop input boundary (step (1)), which a single-cell carve never needs.
bool is_design = base.rfind("slow_design_", 0) == 0 || base.rfind("fast_design_", 0) == 0;
// Speed comes from the pre-rename base's "<speed>_" prefix ("fast_" covers both
// "fast_<enc>" and "fast_design_<enc>"). The post-rename `final` can't be used:
// pq_rename strips the speed prefix off plain cells, so `final` carries no speed.
std::string speed = base.rfind("fast_", 0) == 0 ? "fast" : "slow";
// Rebuild the output boundary at the capture flops. submod can only export a
// clean output port for a net driven *inside* the carve and read *outside* it.
// After synthesis the bit a capture flop captures often is not such a net: a
// degenerate cell (shifter/mod/div in const or var mode) collapses an output into
// a bare copy of an input (Y=A -- the flop reads the launch-flop net directly), a
// routed input bit, or a constant driven by an abc-mapped tie cell the forward
// cone walk can never reach. Left alone that output bit has no in-cone driver and
// drags its whole bus to inout, or the port vanishes. So, per output-port pin,
// build one fresh bus `cw` driven by a per-bit tagged $_BUF_ copying whatever the
// capture flop reads, then rewire the flop to read `cw`. The buffer's source sets
// the input side: an in-cone net stays internal, a launch-flop/boundary net
// becomes a clean input port, a constant stays constant, and an otherwise
// unreachable source cell (a tie) is cloned into the cell so its (tiny) power is
// characterized rather than stranded in the deleted train module. The carve wire
// is named "__pqcarve.<base>_<pin>" so the trailing port cleanup (which drops the
// scope-dot prefix) recovers the clean "<base>_<pin>" port name.
std::string load_cell, load_pin;
for (auto pw : g.out_wires) {
Wire *cw = train->addWire(
RTLIL::escape_id("__pqcarve." + unescape(pw->name.str())), pw->width);
cw->set_bool_attribute(PQ_CARVE_OUT);
for (int i = 0; i < pw->width; i++) {
Cell *fc = hop_back(sigmap(SigBit(pw, i)));
if (fc == nullptr || !capture.count(fc))
continue;
// Locate the flop's data pin (the lone input that is neither a clock nor a
// set/reset/clear control pin -- an async set/reset flop like ASYNC_DFFHx1
// also exposes RESET/SET inputs that must not be mistaken for D).
IdString dpin;
int dpos = -1;
SigBit draw;
for (auto &conn : fc->connections()) {
if (!fc->input(conn.first) || is_clock_pin(unescape(conn.first.str())) ||
is_set_reset_pin(unescape(conn.first.str())))
continue;
const SigSpec &s = conn.second;
for (int j = 0; j < GetSize(s); j++) {
if (clk_nets.count(sigmap(s[j])))
continue;
dpin = conn.first, dpos = j, draw = s[j];
break;
}
if (dpos >= 0)
break;
}
if (dpos < 0)
continue;
if (load_cell.empty()) {
load_cell = unescape(fc->type.str());
load_pin = unescape(dpin.str());
}
// Pick the buffer's source: clone an otherwise-unreachable constant/source
// cell (e.g. a tie) into the cell so the output bit gains an in-cone
// driver; otherwise copy the net the flop reads as-is.
SigBit src = sigmap(draw);
if (src.wire != nullptr) {
auto dit = driver.find(src);
if (dit != driver.end()) {
Cell *dc = dit->second.first;
IdString opin = dit->second.second;
bool is_source = !cell_in_bits.count(dc) || cell_in_bits.at(dc).empty();
// A genuine constant source (a tie): 0 data inputs, single-bit
// output, unreachable by the forward cone. Clone it into the cell
// (rather than steal the shared original) so the output bit gets an
// in-cone driver and the tie's power is characterized.
if (is_source && GetSize(dc->getPort(opin)) == 1 &&
!logic.count(dc) && !boundary_cells.count(dc)) {
Cell *clone = train->addCell(NEW_ID, dc->type);
clone->parameters = dc->parameters;
// Copy every port (e.g. a $lut's zero-width A pin must exist for
// the cell to pass check) before redirecting the output to a
// fresh net.
for (auto &cc : dc->connections())
clone->setPort(cc.first, cc.second);
Wire *tn = train->addWire(NEW_ID, 1);
clone->setPort(opin, SigSpec(tn));
clone->set_string_attribute(RTLIL::escape_id("submod"), final);
src = SigBit(tn);
}
}
}
Cell *buf = train->addBufGate(NEW_ID, src, SigBit(cw, i));
buf->set_string_attribute(RTLIL::escape_id("submod"), final);
buf->set_string_attribute(PQ_PASS, "1");
// Keep the $_BUF_ alive through submod's internal clean (it would
// otherwise re-alias Y to A, re-merging the boundary); converted to a
// plain assign after the carve.
buf->set_bool_attribute(ID::keep);
std::vector<SigBit> dbits = fc->getPort(dpin).bits();
dbits[dpos] = SigBit(cw, i);
fc->setPort(dpin, SigSpec(dbits));
}
}
// Self-containment repair, in two parts. A single mapped cell's cone is seeded from
// its launch-flop inputs and needs only part (2); a DESIGN cell-under-test
// (slow_design_<enc>/fast_design_<enc>) is a whole sequential design and also needs
// part (1). Left unrepaired, either leaks bogus top-level *input* ports out of the
// carved module (internal flattened nets, constants, clock-gate scan pins, undriven
// opt artifacts, ...), which both mis-drives the gate-level power testbench and escapes
// char.sdc's "slow_*"/"fast_*" data-port matching.
// (1) Primary-input naming: after flatten the launch-flop output net (the carve's
// data input) can keep an internal flattened name ("u0_key", "n_1598636") that
// won SigMap election over the "<base>_<pin>__pqi" boundary name. Per input pin,
// build a fresh "__pqcarve.<base>_<pin>" bus, copy the launch-flop output into it
// with a train-side (boundary) buffer, and rewire the cone to read it, so submod
// exports the input under the clean "<base>_<pin>" name. A whole design renames
// every input; a plain cell's boundary usually survives clean already, so it only
// reconstructs a pin whose seed lost its "__pqi" name (the mirror of the output
// reconstruction below, which already runs for every cell).
// (2) ALL cells -- self-containment: a cone-cell input that reads a net driven from
// *outside* the cone leaks out as a spurious input port (submod can only export a
// port for a net driven inside the carve). The forward walk never reaches a
// constant/tie source (0-input cell, e.g. an async flop's inactive SET/RESET), an
// undriven internal net (inserted scan/"test" pin, opt artifact), or a gate fed
// only by such constants. Per cone-cell input: fold a name-recoverable tie
// (logic_0/logic_1 net) to a literal for a plain cell (area-neutral -- keeps a
// sequential cell at a single mapped instance), clone the source otherwise and
// for design cells (so a shared global tie can't drag in a neighbour's cone and
// the tie's tiny power is characterized in-cone), tie an undriven net to 0, and
// pull a forward-walk-missed gate into the cone.
//
// KNOWN LIMITATION (~3.6% of carved cells): a launch-flop seed that lost its "__pqi"
// name can still leak as a bare "n_<num>" input port even after part (1) rebuilds the
// clean "__pqcarve.<base>_<pin>" bus -- the reconstructed port does not survive to the
// final module (the cone re-reads the flattened seed net). It concentrates in the WIDE
// cells (multi-bit shifters sshl/shl/sshr/shr, mul, mem, and the wider add/sub/div/mod)
// plus the fast aes/aes_inv key (a QN-output DFFHQNx1 launch flop, where the whole
// 128-bit key leaks). Triage narrowed it down:
// - it is deterministic, and the cone walk DOES reach the seed (launch flop found,
// output seeded, reader in the cone -- so it is NOT a cone-walk exclusion);
// - part (1) builds cwi and rewires the cone, but cwi is re-merged/dropped, so the
// final module still exports the "n_<num>" seed.
// Pinpointing the exact step still needs instrumentation of the reconstruction REWRITE
// and the post-submod cleanup (not just the cone walk) to trace the cwi lifecycle. Such
// cells fall back to the model rather than a clean gate-level testbench. See
// docs/dev/carvenetlist_dev.md.
const IdString SUBMOD = RTLIL::escape_id("submod");
dict<SigBit, SigBit> in_remap; // launch-flop output bit -> clean input-port bit
// (1) Reconstruct clean input ports at the launch-flop boundary. A whole design's
// launch-flop outputs all keep internal flattened names and are always renamed; a
// plain cell's "__pqi" boundary name usually survives SigMap election, so only a pin
// whose seed net lost it (and would otherwise leak as a bare "n_<num>" input port,
// the mirror of the output reconstruction below which already runs for every cell) is
// reconstructed. The clean majority is left untouched -- no net-name churn, and no
// risk to the QN-launch-flop cells whose reconstruction is known not to survive.
{
for (auto pw : g.in_wires) {
Wire *cwi = nullptr;
for (int i = 0; i < pw->width; i++) {
Cell *fc = hop_fwd(sigmap(SigBit(pw, i)));
if (fc == nullptr || !launch.count(fc))
continue;
SigBit qi;
bool found = false;
for (auto ob : cell_out_bits[fc]) {
if (clk_nets.count(ob) || !loads.count(ob))
continue; // the launch flop's data output that drives the cone
qi = ob;
found = true;
break;
}
if (!found)
continue;
if (!is_design) {
std::string qn = qi.wire ? unescape(qi.wire->name.str()) : "";
if (qn.find("__pqi") != std::string::npos ||
qn.find(base) != std::string::npos)
continue; // seed already recovers a clean port name: leave it
}
if (cwi == nullptr)
cwi = train->addWire(
RTLIL::escape_id("__pqcarve." + unescape(pw->name.str())), pw->width);
// Copy the launch-flop seed into the clean carve wire with a boundary buffer
// that stays in 'train' (untagged), so cwi is driven from outside the cone and
// submod exports it as the clean "<base>_<pin>" input port. The buffer MUST be
// kept: submod runs opt_clean before extraction, and opt_clean dissolves a
// plain $_BUF_ (aliasing Y=A) -- that would collapse cwi back onto the
// flattened "n_<num>" seed net, so submod would then name the port after the
// seed (the leak). The capture-flop OUTPUT reconstruction keeps its buffer for
// the identical reason. The buffer is untagged, so it is dropped with 'train'
// after the carve; the carved cell keeps only its real gate(s) -- no area hit.
Cell *ib = train->addBufGate(NEW_ID, qi, SigBit(cwi, i));
ib->set_bool_attribute(ID::keep);
in_remap[qi] = SigBit(cwi, i);
}
}
}
// (2) Self-containment (runs for EVERY carved cell): one rewrite pass over the
// (growing) cone. For a design cell in_remap also applies part (1); for a plain cell
// in_remap is empty, so this only folds constants/ties, ties undriven nets to 0, and
// pulls in forward-walk-missed gates -- a no-op on an already-clean cone.
{
std::vector<Cell *> proc(logic.begin(), logic.end());
pool<Cell *> queued = logic;
while (!proc.empty()) {
Cell *c = proc.back();
proc.pop_back();
dict<IdString, SigSpec> rewire;
for (auto &conn : c->connections()) {
if (!c->input(conn.first))
continue;
SigSpec cs = conn.second;
bool changed = false;
for (auto &bit : cs) {
if (bit.wire == nullptr)
continue; // already a constant literal
SigBit s = sigmap(bit);
auto rm = in_remap.find(s);
if (rm != in_remap.end()) {
bit = rm->second; // clean primary input
changed = true;
continue;
}
if (s.wire == nullptr || clk_nets.count(s) || driven.count(s))
continue; // constant / shared clock / produced in-cone: fine
auto dit = driver.find(s);
if (dit != driver.end() && boundary_cells.count(dit->second.first))
continue; // launch-flop output the remap above missed: keep as input
if (dit == driver.end()) {
// Undriven internal net (inserted scan/test pin, etc.): tie to 0.
bit = State::S0;
changed = true;
continue;
}
Cell *dc = dit->second.first;
IdString opin = dit->second.second;
bool is_source = !cell_in_bits.count(dc) || cell_in_bits.at(dc).empty();
if (is_source && GetSize(dc->getPort(opin)) == 1) {
// Pure constant/tie source read from outside the cone. For a plain
// cell fold it to a literal using the synth's logic_0/logic_1 tie-net
// naming (area-neutral -- keeps a sequential cell at a single mapped
// instance). Otherwise (design cell, or an unrecognizable value)
// clone the source into the carve rather than steal a possibly-shared
// original, so the cone bit gains an in-cone driver and the tie's
// (tiny) power is characterized here.
std::string wn = s.wire ? unescape(s.wire->name.str()) : "";
if (!is_design && wn.find("logic_1") != std::string::npos) {
bit = State::S1;
} else if (!is_design && wn.find("logic_0") != std::string::npos) {
bit = State::S0;
} else {
Cell *clone = train->addCell(NEW_ID, dc->type);
clone->parameters = dc->parameters;
for (auto &cc : dc->connections())
clone->setPort(cc.first, cc.second);
Wire *tn = train->addWire(NEW_ID, 1);
clone->setPort(opin, SigSpec(tn));
clone->set_string_attribute(SUBMOD, final);
logic.insert(clone);
driven.insert(SigBit(tn));
bit = SigBit(tn);
}
changed = true;
} else if (!queued.count(dc)) {
// A non-source cell the forward walk missed (only constant-fed):
// pull it into the carve and process its inputs too.
dc->set_string_attribute(SUBMOD, final);
logic.insert(dc);
queued.insert(dc);
proc.push_back(dc);
for (auto ob : cell_out_bits[dc])
driven.insert(ob);
}
}
if (changed)
rewire[conn.first] = cs;
}
for (auto &r : rewire)
c->setPort(r.first, r.second);
}
}
boundary[final] = {speed, drv_cell, drv_pin, load_cell, load_pin};
}
log("Carved %d cells (%d logic cells tagged)\n", GetSize(boundary), tagged_total);
// Carve every tagged cell into its own module (submod names it "train.<final>");
// the untagged flops/clock-network cells stay in 'train'.
Pass::call(design, "submod -separator .");
// Strip the "train." prefix off the carved module names
std::string mprefix = "\\train.";
for (auto mod : design->modules().to_vector()) {
std::string n = mod->name.str();
if (n.rfind(mprefix, 0) == 0)
design->rename(mod, RTLIL::escape_id(n.substr(mprefix.size())));
}
// Map every design module to its instantiations so that renaming a module's
// port wires below can keep the matching port-connection keys on its instances
// in sync (Module::rename only touches the wire, not the instance connections).
dict<IdString, std::vector<Cell *>> insts_by_type;
for (auto mod : design->modules())
for (auto cell : mod->cells())
if (design->module(cell->type) != nullptr)
insts_by_type[cell->type].push_back(cell);
// Clean up the carved cell port names, updating each instance's port-connection key
// to match the renamed port (Module::rename only touches the wire). Two fixups:
// 1. Strip leftover __pqi/__pqo boundary suffixes (present when synthesis kept the
// carved net names; harmless no-op otherwise).
// 2. Drop the flatten instance-scope prefix (everything up to and including the
// first '.') so a port recovers its clean train top-port name, e.g.
// "slow_<enc>.slow_<enc>_A" -> "slow_<enc>_A", then collapse any remaining dots
// (e.g. exposed clock-gate pins like "slow_<enc>.CG_INST.enable") to '_'. The
// flatten earlier scope-prefixes every inlined net with a '.', which OpenSTA
// tolerates but the cocotb/iverilog power flow cannot (it can't access a port
// whose name contains '.', treating the dot as a hierarchy separator).
for (auto mod : design->modules()) {
if (mod->name == ID(train))
continue;
bool stripped = false;
// Process the reconstructed capture-flop output buses ("__pqcarve.<base>_<pin>",
// tagged PQ_CARVE_OUT) first. They collapse to the clean "<base>_<pin>" output
// port and are the authoritative output. A degenerate output bus (const-shift /
// feed-through / self-referential cell) can leave a stale "<base>_<pin>__pqo"
// cone-output fragment that cleans to the same name; whoever is renamed first
// wins the collision below, so the reconstructed bus must go first or the carved
// cell loses its output port (degenerating to input/inout with the result
// stranded). See the collision handling below for how the stale fragment is
// then demoted to an internal net rather than aliased onto the output.
std::vector<Wire *> wires_ordered = mod->wires().to_vector();
std::stable_partition(wires_ordered.begin(), wires_ordered.end(), [](Wire *w) {
return unescape(w->name.str()).rfind("__pqcarve.", 0) == 0;
});
for (auto wire : wires_ordered) {
if (!wire->port_input && !wire->port_output)
continue;
std::string wn = unescape(wire->name.str());
std::string nn = wn;
for (const char *suf : {"__pqi", "__pqo"}) {
size_t pos;
while ((pos = nn.find(suf)) != std::string::npos)
nn.erase(pos, strlen(suf));
}
size_t dot = nn.find('.');
if (dot != std::string::npos)
nn = nn.substr(dot + 1);
std::replace(nn.begin(), nn.end(), '.', '_');
if (nn == wn)
continue;
IdString oldid = wire->name;
IdString newid = RTLIL::escape_id(nn);
// Two boundary wires can clean up to the same per-pin name. This happens
// when synthesis splits one logical cell-input bus across more than one net
// -- e.g. a drive-strength buffer inserted on a subset of the bits, so the
// flattened submodule reads {buffered_bits, flop_bits}. flatten then leaves
// both the train-side "<base>_<pin>__pqi" (carrying the directly-read bits)
// and the scope-prefixed "<base>.<base>_<pin>__pqi" (carrying the buffered
// bits), and submod exports each as its own port. They are the same logical
// pin, so fold the fragments onto one bus port instead of uniquifying (a
// phantom second port the OpenSTA/cocotb flows could not drive): alias the
// loser onto the already-renamed winner -- a no-op on bits they share, and on
// the disjoint bits it wires the winner's otherwise-dangling bit to the
// loser's reader (the in-cone buffer) -- and drop the loser's port flag so the
// trailing clean collapses it away.
if (Wire *ex = mod->wire(newid); ex != nullptr && ex != wire) {
// The reconstructed capture-flop output bus (cw, tagged PQ_CARVE_OUT,
// processed first above so it always wins the name) is the authoritative
// clean output port. The colliding wire is then a stale cone-output
// fragment ("<base>_<pin>__pqo") that the per-bit buffers already *source*
// from -- aliasing it onto cw would turn every buffer into Y[i]=Y[i] (a
// combinational self-loop) and strand the real gate/feed-through/tie
// drivers. So demote the fragment to an internal net (leaving it driven
// in-cone and read by the buffers); the later __pqo neutralization renames
// it. Do NOT connect it to cw.
if (ex->get_bool_attribute(PQ_CARVE_OUT)) {
wire->port_input = false;
wire->port_output = false;
stripped = true;
continue;
}
if (ex->width == wire->width) {
log("carvenetlist: unifying split boundary port '%s' into '%s' in "
"module %s\n", log_id(oldid), log_id(newid), log_id(mod->name));
wire->port_input = false;
wire->port_output = false;
mod->connect(SigSpec(wire), SigSpec(ex));
stripped = true;
continue;
}
log_warning("carvenetlist: cannot unify boundary port '%s' -> '%s' in "
"module %s (widths %d vs %d differ); leaving original name.\n",
log_id(oldid), log_id(newid), log_id(mod->name), wire->width,
ex->width);
continue;
}
mod->rename(wire, newid);
stripped = true;
for (auto inst : insts_by_type[mod->name]) {
if (!inst->hasPort(oldid))
continue;
SigSpec sig = inst->getPort(oldid);
inst->unsetPort(oldid);
inst->setPort(newid, sig);
}
}
// Drop the internal carve-output marker now that collisions are resolved so it
// does not leak into the emitted netlist.
for (auto wire : mod->wires())
if (wire->get_bool_attribute(PQ_CARVE_OUT))
wire->attributes.erase(PQ_CARVE_OUT);
if (stripped)
mod->fixup_ports();
}
// Simplify away the wire-to-wire aliases left by flatten and by the split-port
// unification above, dropping each redundant copy down to the single surviving
// (port-named) net so every carved boundary becomes exactly one clean port.
Pass::call(design, "clean -purge");
// Turn the temporary passthrough buffers back into plain assigns (zero-area
// feed-throughs) now that submod has given each its own clean input/output port.
// Done after the final clean so the assign isn't re-collapsed back into one net.
for (auto mod : design->modules()) {
if (mod->name == ID(train))
continue;
for (auto cell : mod->cells().to_vector()) {
if (cell->type != ID($_BUF_) || !cell->has_attribute(PQ_PASS))
continue;
SigSpec a = cell->getPort(ID::A);
SigSpec y = cell->getPort(ID::Y);
mod->remove(cell);
mod->connect(y, a);
}
}
// Neutralize any __pqi/__pqo token left on an *internal* net. Every output bit is
// now driven through a capture-flop-boundary buffer whose source is the in-cone net
// the flop used to read -- that net keeps its original "<base>_<pin>__pqo" boundary
// name but is no longer a port (the buffer's fresh carve wire is). The leftover name
// is purely cosmetic for an internal net, but strip the token (uniquifying on
// collision) so no boundary marker survives the carve.
for (auto mod : design->modules()) {
if (mod->name == ID(train))
continue;
for (auto wire : mod->wires().to_vector()) {
if (wire->port_input || wire->port_output)
continue;
std::string wn = unescape(wire->name.str());
if (wn.find("__pqi") == std::string::npos && wn.find("__pqo") == std::string::npos)
continue;
std::string nn = wn;
for (const char *suf : {"__pqi", "__pqo"}) {
size_t pos;
while ((pos = nn.find(suf)) != std::string::npos)
nn.erase(pos, strlen(suf));
}
IdString newid = RTLIL::escape_id(nn);
mod->rename(wire, (nn.empty() || mod->wire(newid) != nullptr) ? NEW_ID : newid);
}
}
// Replace a REDUNDANT SINGLE-FANOUT 1-input/1-output cell (a lone drive-strength buffer
// or a cell->capture-flop output buffer sitting on top of a real in-cone gate) with a
// direct wire: its one load simply moves to the upstream driver, so delay/power are
// unchanged, and the cone still carves to its one real gate (matching the normal flow).
// Dropping such an inverter's inversion is fine -- cells are characterized for PPA, not
// function, and toggle activity (hence power) is polarity-independent.
//
// KEEP a passthrough that is the cone's BOUNDARY GATE -- one whose input is not driven by
// another in-cone cell (it reads a primary input / launch-flop boundary net). It is the
// cell-under-test's only logic, not a redundant re-driver, so shorting it would replace
// the whole cone with a bare wire: nothing left to characterize, and for an inverter the
// inversion is silently dropped (the carved cell no longer matches the RTL).
//
// KEEP a passthrough whose output has FANOUT > 1. Such a buffer/inverter is a fanout
// (re-drive) buffer -- e.g. a wide multiplier/adder's input and adder-tree buffer trees,
// where a single input bit fans out to hundreds of partial-product gates. Shorting it
// dumps its whole fanout onto the upstream driver; since characterization drives the
// boundary with set_driving_cell, STA then charges a single flop driving that entire
// fanout, massively inflating the measured delay. (no_inv_delay_calc excludes the buffer's
// own stage delay, but NOT the loading it removes.) So fanout buffers are load-bearing for
// delay and must be kept.
//
// EXCEPTION: a passthrough on a flop's feedback path (e.g. the QN -> INV -> D loop that
// implements the hold of a scan-flop-based enable flop like dffe/sdffce) must be kept --
// shorting it would tie the flop's D onto its own Q/QN and turn a hold into a toggle.
// Detect this as "the cell driving the passthrough's input also reads its output".
//
// DESIGN CELLS (design_slow_<enc>/design_fast_<enc>) are exempted entirely: a whole
// sequential design is characterized for its real function, not just PPA, so its logic
// inverters carry genuine inversions. Shorting them (out=in) would drop every inversion
// and make the carved design inequivalent to RTL (its testbench fails).
for (auto mod : design->modules()) {
if (mod->name == ID(train))
continue;
std::string mn = unescape(mod->name.str());
if (mn.rfind("design_slow_", 0) == 0 || mn.rfind("design_fast_", 0) == 0)
continue;
// Original-topology driver/reader maps (net bit -> output cell / input cells) plus a
// per-net load count (input-pin occurrences) and the output-port bit set, so a
// passthrough's output fanout (loads + whether it drives a module output) is known.
SigMap sm(mod);
dict<SigBit, Cell *> drv;
dict<SigBit, pool<Cell *>> rdr;
dict<SigBit, int> load_cnt;
for (auto cell : mod->cells())
for (auto &conn : cell->connections())
for (auto bit : sm(conn.second)) {
if (bit.wire == nullptr)
continue;
if (cell->output(conn.first))
drv[bit] = cell;
if (cell->input(conn.first)) {
rdr[bit].insert(cell);
load_cnt[bit]++;
}
}
pool<SigBit> outport_bits;
for (auto w : mod->wires())
if (w->port_output)
for (auto bit : sm(SigSpec(w)))
if (bit.wire)
outport_bits.insert(bit);
for (auto cell : mod->cells().to_vector()) {
if (!is_passthrough(cell))
continue;
SigSpec in, out;
for (auto &conn : cell->connections()) {
if (cell->input(conn.first))
in = conn.second;
else if (cell->output(conn.first))
out = conn.second;
}
if (GetSize(in) != 1 || GetSize(out) != 1)
continue;
SigBit ib = sm(in[0]), ob = sm(out[0]);
// Keep the cone's boundary gate: a passthrough whose input has no in-cone driver
// (reads a primary input / boundary net) is the cell-under-test's only logic, so
// shorting it would strand the cone as a bare wire (dropping the gate, and for an
// inverter its inversion). Only redundant re-drivers stacked on a real in-cone
// gate -- whose input IS driven by another cell -- may be shorted away.
if (!drv.count(ib))
continue;
// Keep fanout>1 passthroughs: a fanout (re-drive) buffer/inverter whose removal
// would dump its load onto the upstream driver and inflate its characterized delay.
int fanout = (load_cnt.count(ob) ? load_cnt.at(ob) : 0) +
(outport_bits.count(ob) ? 1 : 0);
if (fanout > 1)
continue;
// Feedback guard: the cell driving this passthrough's input also reads its output.
auto dit = drv.find(ib);
if (dit != drv.end() && dit->second != cell && rdr.count(ob) &&
rdr.at(ob).count(dit->second))
continue;
mod->remove(cell);
mod->connect(out, in);
}
}
// Drop the now-dangling internal nets and fold the wire-to-wire aliases left behind.
Pass::call(design, "clean");
// Record each carved cell's surround boundary as \pq_* string attributes on its
// module, so the Python flow can re-impose the launch/capture flops during OpenSTA
// characterization without a separate boundary side file.
for (auto mod : design->modules()) {
if (mod->name == ID(train))
continue;
auto it = boundary.find(unescape(mod->name.str()));
if (it == boundary.end())
continue;
const BoundaryRec &r = it->second;
mod->set_string_attribute(RTLIL::escape_id("pq_speed"), r.speed);
mod->set_string_attribute(RTLIL::escape_id("pq_driving_cell"), r.driving_cell);
mod->set_string_attribute(RTLIL::escape_id("pq_driving_pin"), r.driving_pin);
mod->set_string_attribute(RTLIL::escape_id("pq_load_cell"), r.load_cell);
mod->set_string_attribute(RTLIL::escape_id("pq_load_pin"), r.load_pin);
}
// Drop the flop-bearing train module
if (Module *t = design->module(ID(train)))
design->remove(t);
}
} CarveNetlistPass;
PRIVATE_NAMESPACE_END