3
0
Fork 0
mirror of https://github.com/YosysHQ/yosys synced 2026-07-15 03:35:40 +00:00
yosys/passes/opt/opt_prienc.cc
Akash Levy 2ade981e9d Speed up opt_prienc fingerprinting and candidate discovery.
Reuse ConstEval, index candidate wires once, budget large cone walks, and
sample large-N PE/RR fingerprints so WIDTH=64 cases stay practical.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 12:52:21 -07:00

1253 lines
43 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* yosys -- Yosys Open SYnthesis Suite
*
* Copyright (C) 2026 Akash Levy <akash@silimate.com>
*
* 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/yosys.h"
#include "kernel/sigtools.h"
#include "kernel/consteval.h"
#include <algorithm>
#include <functional>
#include <queue>
USING_YOSYS_NAMESPACE
PRIVATE_NAMESPACE_BEGIN
#include "passes/opt/rewrite_utils.h"
// Priority-encoder variants the pass recognises.
enum class PEVariant { NONE, CLZ_FULL, CLZ_SHORT, CTZ_FULL, CTZ_SHORT };
static const char* variant_name(PEVariant v) {
switch (v) {
case PEVariant::CLZ_FULL: return "clz_full";
case PEVariant::CLZ_SHORT: return "clz_short";
case PEVariant::CTZ_FULL: return "ctz_full";
case PEVariant::CTZ_SHORT: return "ctz_short";
default: return "none";
}
}
// Return the index of the highest set bit (MSB) of `c`, or -1 if all zero.
static int const_msb_set(const Const& c, int N) {
auto bits = c.to_bits();
for (int i = N - 1; i >= 0; i--)
if (i < (int)bits.size() && bits[i] == State::S1) return i;
return -1;
}
// Return the index of the lowest set bit (LSB) of `c`, or -1 if all zero.
static int const_lsb_set(const Const& c, int N) {
auto bits = c.to_bits();
for (int i = 0; i < N; i++)
if (i < (int)bits.size() && bits[i] == State::S1) return i;
return -1;
}
struct OptPriEncWorker {
Module* module;
SigMap sigmap;
Cell* cell = nullptr;
// Bit-level driver map (combinational drivers only).
dict<SigBit, Cell*> bit_to_driver;
pool<SigBit> input_port_bits;
pool<Cell*> sequential_cells;
// Configuration.
bool detect_clz = true;
bool detect_ctz = true;
bool detect_rr = true;
int max_input_width = 256;
int min_input_width = 4;
// Stats.
int regions_rewritten = 0;
int cells_added = 0;
// Cache of full-width CLZ/CTZ networks already emitted for a given input
// wire, so that several matched output wires sharing the same input bus
// pull from a single instantiation instead of materialising duplicate
// log-depth trees.
dict<Wire*, SigSpec> clz_full_cache;
dict<Wire*, SigSpec> ctz_full_cache;
OptPriEncWorker(Module* m) : module(m), sigmap(m) { build_indexes(); }
void build_indexes() {
for (auto cell : module->cells()) {
if (is_sequential(cell)) {
sequential_cells.insert(cell);
continue;
}
for (auto& conn : cell->connections()) {
if (!cell->output(conn.first)) continue;
for (auto bit : sigmap(conn.second))
if (bit.wire) bit_to_driver[bit] = cell;
}
}
for (auto wire : module->wires()) {
if (!wire->port_input) continue;
for (auto bit : sigmap(wire))
input_port_bits.insert(bit);
}
}
// Compute the combinational fanin cone of `from`. Outputs the set of cells
// in the cone (cells whose output is reached by BFS) and the "leaf" bits
// (port-input bits or bits driven by sequential cells / undriven).
// Returns 1 on success, 0 if the cone is empty/unusable, -1 if it exceeded
// the size caps (caller may retry with a larger cap / budget).
int get_cone(SigSpec from, pool<Cell*>& cone_cells, pool<SigBit>& leaf_bits,
int max_cone_cells, int max_leaf_bits) {
cone_cells.clear();
leaf_bits.clear();
pool<SigBit> visited;
std::queue<SigBit> worklist;
for (auto bit : sigmap(from)) {
if (!bit.wire) continue;
if (visited.insert(bit).second) worklist.push(bit);
}
while (!worklist.empty()) {
SigBit bit = worklist.front();
worklist.pop();
if (input_port_bits.count(bit)) {
leaf_bits.insert(bit);
if (GetSize(leaf_bits) > max_leaf_bits) return -1;
continue;
}
auto it = bit_to_driver.find(bit);
if (it == bit_to_driver.end()) {
leaf_bits.insert(bit);
if (GetSize(leaf_bits) > max_leaf_bits) return -1;
continue;
}
Cell* drv = it->second;
if (sequential_cells.count(drv)) {
leaf_bits.insert(bit);
if (GetSize(leaf_bits) > max_leaf_bits) return -1;
continue;
}
if (!cone_cells.insert(drv).second) continue;
if (GetSize(cone_cells) > max_cone_cells) return -1;
for (auto& conn : drv->connections()) {
if (!drv->input(conn.first)) continue;
for (auto in_bit : sigmap(conn.second)) {
if (!in_bit.wire) continue;
if (visited.insert(in_bit).second) worklist.push(in_bit);
}
}
}
return cone_cells.empty() ? 0 : 1;
}
// Inverted index: sigmap bit -> wires that contain it. Built once per run()
// so candidate T/req/start discovery is O(|cone_bits|) instead of O(|wires|).
dict<SigBit, vector<Wire*>> bit_to_cand_wires;
dict<Wire*, int> wire_uniq_bit_count;
dict<Wire*, vector<SigBit>> wire_sig_bits;
void build_wire_index(const vector<Wire*>& wires) {
bit_to_cand_wires.clear();
wire_uniq_bit_count.clear();
wire_sig_bits.clear();
for (Wire* w : wires) {
SigSpec ss = sigmap(SigSpec(w));
vector<SigBit> bits;
bits.reserve(GetSize(ss));
pool<SigBit> uniq;
bool has_const = false;
for (auto bit : ss) {
bits.push_back(bit);
if (!bit.wire) { has_const = true; continue; }
if (uniq.insert(bit).second)
bit_to_cand_wires[bit].push_back(w);
}
wire_sig_bits[w] = std::move(bits);
// Wires with const bits can never be clean ConstEval cuts.
if (!has_const)
wire_uniq_bit_count[w] = GetSize(uniq);
}
}
// Wires whose sigmap bits are all inside `cone_bits` (and pass `keep`).
vector<Wire*> wires_in_cone(const pool<SigBit>& cone_bits,
std::function<bool(Wire*)> keep) {
dict<Wire*, int> cover;
for (auto bit : cone_bits) {
auto it = bit_to_cand_wires.find(bit);
if (it == bit_to_cand_wires.end()) continue;
for (Wire* w : it->second) {
if (!keep(w)) continue;
cover[w]++;
}
}
vector<Wire*> out;
for (auto& it : cover) {
Wire* w = it.first;
auto uit = wire_uniq_bit_count.find(w);
if (uit == wire_uniq_bit_count.end()) continue;
if (it.second == uit->second)
out.push_back(w);
}
return out;
}
// Collect wires whose bits are entirely within the cone frontier of S.
// Prefer wider candidates: more fingerprint constraints, fewer false positives.
vector<Wire*> find_candidate_Ts(Wire* S_wire,
const pool<SigBit>& cone_bits,
const pool<SigBit>& control_bits,
int Wbits) {
vector<Wire*> out = wires_in_cone(cone_bits, [&](Wire* w) {
if (w == S_wire) return false;
if (w->width < min_input_width || w->width > max_input_width) return false;
int W_full = clog2_int(w->width + 1);
int W_short = clog2_int(w->width);
if (W_full != Wbits && W_short != Wbits) return false;
auto sit = wire_sig_bits.find(w);
if (sit == wire_sig_bits.end()) return false;
for (auto bit : sit->second)
if (control_bits.count(bit)) return true;
return false;
});
std::sort(out.begin(), out.end(), [](Wire* a, Wire* b) {
return a->width > b->width;
});
return out;
}
// PE deck: zero + one-hots first (reject most non-PEs immediately), then
// denser patterns that separate full vs short / CLZ vs CTZ. For large N the
// prefix/suffix sweeps are sparsified — one-hots already pin every position.
vector<Const> gen_test_vectors(int N) {
vector<Const> vs;
vs.push_back(const_u64(0, N));
for (int k = 0; k < N; k++) {
std::vector<State> bits(N, State::S0);
bits[k] = State::S1;
vs.push_back(Const(bits));
}
auto push_prefix = [&](int k) {
if (k < 1 || k > N) return;
std::vector<State> bits(N, State::S0);
for (int i = 0; i < k; i++) bits[i] = State::S1;
vs.push_back(Const(bits));
};
auto push_suffix_clear = [&](int k) {
if (k < 0 || k >= N) return;
std::vector<State> bits(N, State::S1);
for (int i = 0; i < k; i++) bits[i] = State::S0;
vs.push_back(Const(bits));
};
if (N <= 16) {
for (int k = 1; k <= N; k++) push_prefix(k);
for (int k = 0; k < N; k++) push_suffix_clear(k);
} else {
push_prefix(2);
push_prefix(N / 4);
push_prefix(N / 2);
push_prefix(N - 1);
push_prefix(N);
push_suffix_clear(1);
push_suffix_clear(N / 4);
push_suffix_clear(N / 2);
push_suffix_clear(N - 1);
}
if (N >= 4) {
std::vector<State> aa(N, State::S0), fivefive(N, State::S0), e8(N, State::S0);
for (int i = 0; i < N; i++) {
if (i & 1) aa[i] = State::S1; else fivefive[i] = State::S1;
}
vs.push_back(Const(aa));
vs.push_back(Const(fivefive));
e8[0] = State::S1;
if (N > 1) e8[N - 1] = State::S1;
vs.push_back(Const(e8));
}
return vs;
}
// Leaner RR deck: empty + one-hots + a sparse multi-bit set. One-hots are
// s-independent (sanity); multi-bit + empty×s catch rotation dependence.
vector<Const> gen_rr_test_vectors(int N) {
vector<Const> vs;
vs.push_back(const_u64(0, N));
for (int k = 0; k < N; k++) {
std::vector<State> bits(N, State::S0);
bits[k] = State::S1;
vs.push_back(Const(bits));
}
auto push_prefix = [&](int k) {
if (k < 1 || k > N) return;
std::vector<State> bits(N, State::S0);
for (int i = 0; i < k; i++) bits[i] = State::S1;
vs.push_back(Const(bits));
};
auto push_suffix_clear = [&](int k) {
if (k < 0 || k >= N) return;
std::vector<State> bits(N, State::S1);
for (int i = 0; i < k; i++) bits[i] = State::S0;
vs.push_back(Const(bits));
};
push_prefix(2);
push_prefix(N / 2);
push_prefix(N - 1);
push_prefix(N);
push_suffix_clear(1);
push_suffix_clear(N / 2);
push_suffix_clear(N - 1);
if (N >= 4) {
std::vector<State> aa(N, State::S0), fivefive(N, State::S0), e8(N, State::S0);
for (int i = 0; i < N; i++) {
if (i & 1) aa[i] = State::S1; else fivefive[i] = State::S1;
}
vs.push_back(Const(aa));
vs.push_back(Const(fivefive));
e8[0] = State::S1;
if (N > 1) e8[N - 1] = State::S1;
vs.push_back(Const(e8));
}
return vs;
}
// Full start sweep for small N; capped sample for large N (still includes
// corners / midpoints that distinguish rotation from fixed priority).
static vector<int> rr_start_samples(int N) {
vector<int> out;
if (N <= 16) {
out.reserve(N);
for (int s = 0; s < N; s++) out.push_back(s);
return out;
}
pool<int> sset;
auto add = [&](int s) { if (s >= 0 && s < N) sset.insert(s); };
add(0); add(1); add(2);
add(N - 1); add(N - 2); add(N - 3);
add(N / 4); add(N / 2); add((3 * N) / 4);
int target = std::min(N, 16);
for (int i = 0; GetSize(sset) < target && i < N; i++)
add((i * 7) % N);
out.assign(sset.begin(), sset.end());
std::sort(out.begin(), out.end());
return out;
}
// Sole-driver type gate before get_cone: skip wires driven by cells that
// never root a PE/RR region (avoids BFS on every narrow misc wire).
static bool driver_looks_interesting(Cell* d) {
return d->type.in(ID($mux), ID($pmux), ID($add), ID($sub), ID($or), ID($and),
ID($xor), ID($xnor), ID($not), ID($logic_not), ID($logic_and), ID($logic_or),
ID($reduce_or), ID($reduce_bool), ID($reduce_and), ID($reduce_xor),
ID($eq), ID($ne), ID($lt), ID($le), ID($gt), ID($ge),
ID($shl), ID($shr), ID($sshl), ID($sshr), ID($shift), ID($shiftx),
ID($mod), ID($modfloor), ID($neg), ID($pos));
}
// Cheap structural gate before ConstEval: PE cones are mux/compare/shift heavy.
static bool cone_looks_like_pe(const pool<Cell*>& cells) {
for (Cell* c : cells)
if (c->type.in(ID($mux), ID($pmux), ID($eq), ID($ne), ID($lt), ID($le),
ID($gt), ID($ge), ID($logic_and), ID($logic_or), ID($logic_not),
ID($reduce_or), ID($reduce_bool), ID($reduce_and),
ID($and), ID($or), ID($xor), ID($not),
ID($shl), ID($shr), ID($sshl), ID($sshr), ID($shift), ID($shiftx),
ID($add), ID($sub)))
return true;
return false;
}
// RR RTL dynamic-indexes req[idx] ($shiftx/$shift) or uses mod wrap.
// Do NOT treat $shl/$shr as RR-like: CLZ/CTZ for-loop scans use those and
// would otherwise pay for pointless RR fingerprinting on huge PE cones.
static bool cone_looks_like_rr(const pool<Cell*>& cells) {
int muxes = 0;
for (Cell* c : cells) {
if (c->type.in(ID($shiftx), ID($shift), ID($mod), ID($modfloor)))
return true;
if (c->type.in(ID($mux), ID($pmux))) muxes++;
}
return muxes >= 8;
}
// ConstEval::set() requires every (sigmap-canonical) bit it pins to be a
// distinct free wire bit. Real designs can tie parts of a bus to constants
// or alias nets together, so guard the fingerprint inputs: reject signals
// containing constant or repeated bits, and (across the whole set) any
// overlap between them. This prevents a ConstEval assertion; skipping an
// unclean candidate only forgoes a possible rewrite, never yields a wrong
// one.
static bool clean_set_signals(std::initializer_list<const SigSpec*> sigs) {
pool<SigBit> seen;
for (const SigSpec* sp : sigs)
for (auto bit : *sp) {
if (bit.wire == nullptr) return false;
if (!seen.insert(bit).second) return false;
}
return true;
}
// A set of signals is a valid ConstEval "cut" to pin as free inputs only if
// pinning them can never collide with a value ConstEval derives while
// evaluating the cone. ConstEval::eval() re-computes and re-set()s the FULL
// output of any combinational cell it needs: so if a pinned bit is a
// combinational-cell output and a *sibling* output bit of that same cell
// lies outside the cut (and is pulled into the cone), evaluating the sibling
// re-sets the pinned bit to the cell's real value, which contradicts the
// free value we pinned -> the ConstEval assertion in set() fires.
//
// A bit is a safe leaf when it is a primary input, sequential-cell output or
// undriven (all absent from bit_to_driver, which holds combinational drivers
// only). A combinational-cell output is safe only if that cell's entire
// output lies within the cut. `cut` must be the union of every signal pinned
// together before a shared eval.
bool is_valid_consteval_cut(const SigSpec& cut) {
pool<SigBit> cut_bits;
for (auto bit : cut)
if (bit.wire) cut_bits.insert(bit);
for (auto bit : cut) {
if (bit.wire == nullptr) return false;
auto it = bit_to_driver.find(bit);
if (it == bit_to_driver.end()) continue; // safe leaf
Cell* d = it->second;
for (auto& conn : d->connections()) {
if (!d->output(conn.first)) continue;
for (auto ob : sigmap(conn.second))
if (ob.wire && !cut_bits.count(ob))
return false;
}
}
return true;
}
// Run candidate test vectors through a shared ConstEval. Zero + one-hots
// lead the deck so non-PEs bail before denser patterns. For large N the
// one-hot sweep is sampled; once a single variant remains we return early.
PEVariant fingerprint(ConstEval& ce, SigSpec T_sig, SigSpec S_sig, int N, int Wbits) {
bool clz_full_ok = detect_clz && (Wbits == clog2_int(N + 1));
bool ctz_full_ok = detect_ctz && (Wbits == clog2_int(N + 1));
bool clz_short_ok = detect_clz && (Wbits == clog2_int(N));
bool ctz_short_ok = detect_ctz && (Wbits == clog2_int(N));
if (!clz_full_ok && !ctz_full_ok && !clz_short_ok && !ctz_short_ok)
return PEVariant::NONE;
if (!clean_set_signals({&T_sig}))
return PEVariant::NONE;
if (!is_valid_consteval_cut(T_sig))
return PEVariant::NONE;
auto finish = [&]() -> PEVariant {
if (clz_full_ok) return PEVariant::CLZ_FULL;
if (ctz_full_ok) return PEVariant::CTZ_FULL;
if (clz_short_ok) return PEVariant::CLZ_SHORT;
if (ctz_short_ok) return PEVariant::CTZ_SHORT;
return PEVariant::NONE;
};
auto survivors = [&]() {
return (int)clz_full_ok + (int)ctz_full_ok + (int)clz_short_ok + (int)ctz_short_ok;
};
auto check_vec = [&](const Const& v) -> bool {
ce.push();
ce.set(T_sig, v);
SigSpec out = S_sig;
SigSpec undef;
bool ok = ce.eval(out, undef);
ce.pop();
if (!ok || !out.is_fully_const()) {
clz_full_ok = ctz_full_ok = clz_short_ok = ctz_short_ok = false;
return false;
}
int outval = out.as_const().as_int();
int msb_set = const_msb_set(v, N);
int lsb_set = const_lsb_set(v, N);
bool zero = (msb_set < 0);
int e_clz = zero ? N : (N - 1 - msb_set);
int e_ctz = zero ? N : lsb_set;
if (clz_full_ok && outval != e_clz) clz_full_ok = false;
if (ctz_full_ok && outval != e_ctz) ctz_full_ok = false;
if (clz_short_ok && !zero && outval != e_clz) clz_short_ok = false;
if (ctz_short_ok && !zero && outval != e_ctz) ctz_short_ok = false;
return survivors() > 0;
};
vector<Const> vs;
vs.push_back(const_u64(0, N));
// One-hots: full sweep for small N; corners+stride sample for large N.
pool<int> pos;
auto addp = [&](int k) { if (k >= 0 && k < N) pos.insert(k); };
if (N <= 32) {
for (int k = 0; k < N; k++) addp(k);
} else {
for (int k = 0; k < 4; k++) { addp(k); addp(N - 1 - k); }
int stride = std::max(1, N / 16);
for (int k = 0; k < N; k += stride) addp(k);
}
vector<int> pv(pos.begin(), pos.end());
std::sort(pv.begin(), pv.end());
for (int k : pv) {
std::vector<State> bits(N, State::S0);
bits[k] = State::S1;
vs.push_back(Const(bits));
}
// Multi-bit confirmation vectors (sparse for any N via gen_test_vectors tail).
{
auto tail = gen_test_vectors(N);
for (auto& v : tail) {
int pop = 0;
auto b = v.to_bits();
for (int i = 0; i < N && i < (int)b.size(); i++)
if (b[i] == State::S1) pop++;
if (pop <= 1) continue;
vs.push_back(v);
}
}
int n_checked = 0;
int onehot_end = 1 + GetSize(pv); // zero + one-hots
int early_at = std::min(onehot_end, 1 + std::min(GetSize(pv), 8));
for (auto& v : vs) {
if (!check_vec(v)) return PEVariant::NONE;
n_checked++;
// Unique survivor after zero + a handful of one-hots is enough.
if (n_checked >= early_at && survivors() == 1)
return finish();
}
return finish();
}
// Recursive CLZ on a power-of-2-width input. Returns a (log2(N)+1)-bit
// SigSpec whose MSB is 1 iff T == 0 and whose lower bits are the leading-
// zeros count for nonzero T.
SigSpec emit_clz_pow2(SigSpec T, int N) {
log_assert(N >= 1 && (N & (N - 1)) == 0);
if (N == 1) {
cells_added++;
return module->Not(NEW_ID2_SUFFIX("clznot"), T, false, cell_src(cell));
}
int N2 = N / 2;
SigSpec hi = T.extract(N2, N2);
SigSpec lo = T.extract(0, N2);
SigSpec clz_hi = emit_clz_pow2(hi, N2);
SigSpec clz_lo = emit_clz_pow2(lo, N2);
int W1 = GetSize(clz_hi);
SigBit hi_zero = clz_hi[W1 - 1];
SigBit lo_zero = clz_lo[W1 - 1];
// pad_clz_hi (W bits): {1'b0, clz_hi}. When the mux selects this arm
// (hi != 0), clz_hi's MSB is guaranteed 0, so the top two bits of the
// result are 0.
SigSpec pad_clz_hi = clz_hi;
pad_clz_hi.append(SigSpec(State::S0));
// pad_clz_lo (W bits): logical equivalent of N/2 + clz_lo. The MSB
// becomes lo_zero (= 1 iff x == 0); the next bit becomes ~lo_zero (=
// 1 iff lo != 0, signalling result in [N/2, N-1]); the remaining bits
// are clz_lo[W1-2:0].
SigSpec lo_nonzero_spec = module->Not(NEW_ID2_SUFFIX("clz_lonz"), SigSpec(lo_zero), false, cell_src(cell));
cells_added++;
SigBit lo_nonzero = lo_nonzero_spec[0];
SigSpec pad_clz_lo;
if (W1 >= 2)
pad_clz_lo.append(clz_lo.extract(0, W1 - 1));
pad_clz_lo.append(lo_nonzero);
pad_clz_lo.append(lo_zero);
// $mux: Y = S ? B : A. We want Y = hi_zero ? pad_clz_lo : pad_clz_hi.
cells_added++;
return module->Mux(NEW_ID2_SUFFIX("clzmux"), pad_clz_hi, pad_clz_lo, SigSpec(hi_zero), cell_src(cell));
}
// CLZ of arbitrary-width T, returning a (clog2(N+1))-bit result.
SigSpec emit_clz_full(SigSpec T, int N) {
int Np = 1;
while (Np < N) Np *= 2;
int pad_amount = Np - N;
SigSpec padded = T;
for (int i = 0; i < pad_amount; i++)
padded.append(SigSpec(State::S0));
SigSpec clz_padded = emit_clz_pow2(padded, Np); // log2(Np)+1 bits
if (pad_amount == 0)
return clz_padded;
// result = clz_padded - pad_amount, truncated to W = clog2(N+1) bits.
int W = clog2_int(N + 1);
SigSpec sub = module->Sub(NEW_ID2_SUFFIX("clzsub"), clz_padded, SigSpec(Const(pad_amount, GetSize(clz_padded))), false, cell_src(cell));
cells_added++;
if (GetSize(sub) >= W)
return sub.extract(0, W);
SigSpec out = sub;
while (GetSize(out) < W) out.append(SigSpec(State::S0));
return out;
}
// CTZ via bit-reversal of T followed by CLZ.
SigSpec emit_ctz_full(SigSpec T, int N) {
SigSpec rev;
for (int i = N - 1; i >= 0; i--)
rev.append(T[i]);
return emit_clz_full(rev, N);
}
SigSpec emit_pe(PEVariant v, Wire* T_wire, int N, int out_width) {
bool is_clz = (v == PEVariant::CLZ_FULL || v == PEVariant::CLZ_SHORT);
auto& cache = is_clz ? clz_full_cache : ctz_full_cache;
SigSpec full;
auto it = cache.find(T_wire);
if (it != cache.end()) {
full = it->second;
} else {
SigSpec T_sig = sigmap(SigSpec(T_wire));
full = is_clz ? emit_clz_full(T_sig, N) : emit_ctz_full(T_sig, N);
cache[T_wire] = full;
}
if (v == PEVariant::CLZ_SHORT || v == PEVariant::CTZ_SHORT) {
if (GetSize(full) > 0)
full = full.extract(0, GetSize(full) - 1);
}
// Match the user-visible output width.
if (GetSize(full) > out_width)
full = full.extract(0, out_width);
while (GetSize(full) < out_width)
full.append(SigSpec(State::S0));
return full;
}
// ------------------------------------------------------------------
// Round-robin (rotated priority) detection + rewrite.
//
// A round-robin arbiter grants the first set request bit scanning
// *upward* (increasing index, wrapping) starting just after a stored
// pointer `s` (= idx_last):
//
// grant = anyreq ? (first set bit at index > s, else first set
// bit overall) : 0
// idx_next = anyreq ? grant : s
//
// RTL usually spells this as a DEPTH-iteration loop that walks `idx`
// downward from idx_last with wraparound and keeps the last hit, which
// elaborates into a serial mux/shift chain of depth ~DEPTH. The rewrite
// below is log-depth:
//
// above[i] = (i > s) (per-bit threshold mask)
// mask_hi = req & above
// grant = anyreq ? (|mask_hi ? ctz(mask_hi) : ctz(req)) : 0
//
// where ctz() reuses the log-depth CTZ network. For power-of-2 DEPTH the
// rewrite is fully combinationally equivalent for every pointer value;
// for non-power-of-2 DEPTH it is equivalent for every *reachable* pointer
// (idx_last only ever holds a valid index in [0,DEPTH)), which is the
// range the fingerprint checks. Detection therefore sweeps s over
// [0,DEPTH) only.
// ------------------------------------------------------------------
// kind: 0 = grant, 1 = idx_next.
int rr_expected(const Const& reqv, int s, int N, int W, int kind) {
auto bits = reqv.to_bits();
int lo_all = -1, lo_hi = -1;
for (int i = 0; i < N; i++) {
bool set = (i < (int)bits.size() && bits[i] == State::S1);
if (!set) continue;
if (lo_all < 0) lo_all = i;
if (i > s && lo_hi < 0) lo_hi = i;
}
bool anyreq = (lo_all >= 0);
int gsel = (lo_hi >= 0) ? lo_hi : (lo_all >= 0 ? lo_all : 0);
int val = kind == 0 ? (anyreq ? gsel : 0)
: (anyreq ? gsel : s);
return val & ((W >= 31) ? -1 : ((1 << W) - 1));
}
// Returns matched kind (0 grant, 1 idx_next), or -1 for no match.
// Empty/multi-bit vectors are swept over sampled starts (rotation matters);
// one-hots are s-independent so they run once at s=0 only.
int fingerprint_rr(ConstEval& ce, SigSpec req_sig, SigSpec start_sig, SigSpec S_sig,
int N, int W) {
if (!clean_set_signals({&req_sig, &start_sig}))
return -1;
SigSpec cut = req_sig;
cut.append(start_sig);
if (!is_valid_consteval_cut(cut))
return -1;
bool ok0 = true, ok1 = true;
auto starts = rr_start_samples(N);
int checks = 0;
auto check = [&](const Const& rv, int s) -> bool {
ce.push();
ce.set(req_sig, rv);
ce.set(start_sig, Const(s, W));
SigSpec out = S_sig, undef;
bool ok = ce.eval(out, undef);
ce.pop();
if (!ok || !out.is_fully_const()) return false;
int ov = out.as_const().as_int();
if (ok0 && ov != rr_expected(rv, s, N, W, 0)) ok0 = false;
if (ok1 && ov != rr_expected(rv, s, N, W, 1)) ok1 = false;
checks++;
return ok0 || ok1;
};
// Phase 1: empty req × starts — idx_next must track s; grant stays 0.
Const z = const_u64(0, N);
for (int s : starts)
if (!check(z, s)) return -1;
// Phase 2: one-hots at a single start (result is independent of s).
for (int k = 0; k < N; k++) {
std::vector<State> bits(N, State::S0);
bits[k] = State::S1;
if (!check(Const(bits), 0)) return -1;
}
// Phase 3: sparse multi-bit patterns × starts (rotation-sensitive).
auto multi = gen_rr_test_vectors(N);
for (auto& rv : multi) {
// Skip empty + one-hots already covered above.
int pop = 0;
auto b = rv.to_bits();
for (int i = 0; i < N && i < (int)b.size(); i++)
if (b[i] == State::S1) pop++;
if (pop <= 1) continue;
for (int s : starts)
if (!check(rv, s)) return -1;
}
if (checks < 2 * GetSize(starts)) return -1;
if (ok0) return 0;
if (ok1) return 1;
return -1;
}
// Emit the log-depth round-robin network. Shared subexpressions across
// the grant / idx_next pair for the same (req, start) inputs are cached.
dict<std::pair<Wire*, Wire*>, std::tuple<SigSpec, SigSpec, SigBit>> rr_core_cache;
SigSpec emit_rr(Wire* req_wire, Wire* start_wire, int N, int W, int kind) {
SigSpec req = sigmap(SigSpec(req_wire));
SigSpec s = sigmap(SigSpec(start_wire));
SigSpec gsel;
SigBit anyreq;
auto key = std::make_pair(req_wire, start_wire);
auto it = rr_core_cache.find(key);
if (it != rr_core_cache.end()) {
SigSpec cached_gsel;
std::tie(cached_gsel, std::ignore, anyreq) = it->second;
gsel = cached_gsel;
} else {
SigSpec above;
for (int i = 0; i < N; i++) {
above.append(module->Lt(NEW_ID2_SUFFIX("rrabove"), s, SigSpec(Const(i, W)), false, cell_src(cell)));
cells_added++;
}
SigSpec mask_hi = module->And(NEW_ID2_SUFFIX("rrmask"), req, above, false, cell_src(cell));
cells_added++;
SigSpec cz_hi = emit_ctz_full(mask_hi, N);
SigSpec cz_all = emit_ctz_full(req, N);
auto low_w = [&](SigSpec x) {
if (GetSize(x) > W) return x.extract(0, W);
while (GetSize(x) < W) x.append(SigSpec(State::S0));
return x;
};
cz_hi = low_w(cz_hi);
cz_all = low_w(cz_all);
SigBit any_hi = module->ReduceOr(NEW_ID2_SUFFIX("rranyhi"), mask_hi, false, cell_src(cell));
cells_added++;
anyreq = module->ReduceOr(NEW_ID2_SUFFIX("rranyreq"), req, false, cell_src(cell));
cells_added++;
// any_hi ? cz_hi : cz_all
gsel = module->Mux(NEW_ID2_SUFFIX("rrgsel"), cz_all, cz_hi, any_hi, cell_src(cell));
cells_added++;
rr_core_cache[key] = std::make_tuple(gsel, SigSpec(), anyreq);
}
SigSpec fallback = (kind == 0) ? SigSpec(Const(0, W)) : s;
// anyreq ? gsel : fallback
SigSpec res = module->Mux(NEW_ID2_SUFFIX("rrsel"), fallback, gsel, anyreq, cell_src(cell));
cells_added++;
return res;
}
// Generalisation of cone_depends_only_on_T to a set of allowed leaf bits.
bool cone_depends_only_on_set(SigSpec S_sig, const pool<SigBit>& allowed) {
pool<SigBit> visited;
std::queue<SigBit> worklist;
for (auto bit : sigmap(S_sig)) {
if (!bit.wire) continue;
if (visited.insert(bit).second) worklist.push(bit);
}
while (!worklist.empty()) {
SigBit bit = worklist.front();
worklist.pop();
if (allowed.count(bit)) continue;
if (input_port_bits.count(bit)) return false;
auto it = bit_to_driver.find(bit);
if (it == bit_to_driver.end()) return false;
Cell* drv = it->second;
if (sequential_cells.count(drv)) return false;
for (auto& conn : drv->connections()) {
if (!drv->input(conn.first)) continue;
for (auto in_bit : sigmap(conn.second)) {
if (!in_bit.wire) continue;
if (visited.insert(in_bit).second) worklist.push(in_bit);
}
}
}
return true;
}
struct RRRewrite {
Wire* S_wire;
Wire* req_wire;
Wire* start_wire;
int N;
int W;
int kind;
Cell* sole_driver;
IdString out_port;
};
struct Rewrite {
Wire* S_wire;
Wire* T_wire;
int N;
int Wbits;
PEVariant variant;
Cell* sole_driver;
IdString out_port;
};
// One per (potential) candidate, lazily filled before fingerprinting.
struct Candidate {
Wire* S_wire;
pool<Cell*> cone_cells;
pool<SigBit> leaf_bits;
pool<SigBit> cone_bits;
pool<SigBit> control_bits;
Cell* sole_driver;
IdString out_port;
};
bool get_sole_whole_wire_driver(Wire* S_wire, Cell*& sole_driver, IdString& out_port) {
SigSpec S_sig = sigmap(SigSpec(S_wire));
pool<Cell*> drivers;
for (auto bit : S_sig) {
auto it = bit_to_driver.find(bit);
if (it == bit_to_driver.end()) return false;
drivers.insert(it->second);
}
if (GetSize(drivers) != 1) return false;
sole_driver = *drivers.begin();
SigSpec out_sig;
for (auto& conn : sole_driver->connections()) {
if (sole_driver->output(conn.first)) {
out_port = conn.first;
out_sig = sigmap(conn.second);
break;
}
}
return out_sig == S_sig;
}
bool is_control_input(Cell* c, IdString port) {
if (c->type.in(ID($mux), ID($pmux)))
return port == ID::S;
return c->type.in(
ID($eq), ID($ne), ID($eqx), ID($nex), ID($lt), ID($le), ID($gt), ID($ge),
ID($logic_not), ID($logic_and), ID($logic_or),
ID($reduce_bool), ID($reduce_or), ID($reduce_and),
ID($and), ID($or), ID($xor), ID($xnor), ID($not));
}
// Cheap structural prefilter for a candidate S=f(T). ConstEval will only
// assign T, so any other variable leaf in the fanin cone guarantees the
// fingerprint will fail. Stop traversal at T bits to allow T to be an
// internal wire produced by logic outside the PE region.
bool cone_depends_only_on_T(SigSpec S_sig, const pool<SigBit>& T_bits) {
pool<SigBit> visited;
std::queue<SigBit> worklist;
for (auto bit : sigmap(S_sig)) {
if (!bit.wire) continue;
if (visited.insert(bit).second) worklist.push(bit);
}
while (!worklist.empty()) {
SigBit bit = worklist.front();
worklist.pop();
if (T_bits.count(bit)) continue;
if (input_port_bits.count(bit)) return false;
auto it = bit_to_driver.find(bit);
if (it == bit_to_driver.end()) return false;
Cell* drv = it->second;
if (sequential_cells.count(drv)) return false;
for (auto& conn : drv->connections()) {
if (!drv->input(conn.first)) continue;
for (auto in_bit : sigmap(conn.second)) {
if (!in_bit.wire) continue;
if (visited.insert(in_bit).second) worklist.push(in_bit);
}
}
}
return true;
}
void run() {
vector<Wire*> wires_snapshot(module->wires().begin(), module->wires().end());
build_wire_index(wires_snapshot);
// One ConstEval for the whole module: ctor indexes every cell once.
// Fingerprints only run before we mutate the netlist.
ConstEval ce(module);
// Stage 1: build candidate set with cones, filter by driver/width.
// Probe with a small cone cap first; only a budgeted number of wires
// that overflow get a full-size rewalk (unrolled N=64 loops otherwise
// make every intermediate encoder wire pay for a full BFS).
vector<Candidate> candidates;
int max_W = clog2_int(max_input_width + 1);
int max_cone_cells = std::max(128, max_input_width * 16);
int probe_cone_cells = std::min(64, max_cone_cells);
int large_cone_budget = 24;
// req[N]+start[W] leaves for RR, plus a little slack for aliases/opt junk.
int max_leaf_bits = max_input_width + max_W + max_input_width / 4 + 16;
struct SCand { Wire* w; Cell* drv; IdString port; int rank; };
vector<SCand> s_cands;
for (Wire* S_wire : wires_snapshot) {
if (S_wire->port_input) continue;
int Wbits = S_wire->width;
if (Wbits < 2 || Wbits > max_W) continue;
Cell* sole_driver = nullptr;
IdString out_port;
if (!get_sole_whole_wire_driver(S_wire, sole_driver, out_port)) continue;
if (!driver_looks_interesting(sole_driver)) continue;
int rank = 2;
if (S_wire->port_output) rank = 0;
else if (sole_driver->type.in(ID($mux), ID($pmux), ID($add), ID($sub), ID($and), ID($or)))
rank = 1;
s_cands.push_back({S_wire, sole_driver, out_port, rank});
}
std::sort(s_cands.begin(), s_cands.end(), [](const SCand& a, const SCand& b) {
if (a.rank != b.rank) return a.rank < b.rank;
return a.w->width > b.w->width;
});
for (auto& sc : s_cands) {
Wire* S_wire = sc.w;
Cell* sole_driver = sc.drv;
IdString out_port = sc.port;
pool<Cell*> cone_cells;
pool<SigBit> leaf_bits;
int st = get_cone(SigSpec(S_wire), cone_cells, leaf_bits,
probe_cone_cells, max_leaf_bits);
if (st < 0) {
if (large_cone_budget <= 0) continue;
large_cone_budget--;
st = get_cone(SigSpec(S_wire), cone_cells, leaf_bits,
max_cone_cells, max_leaf_bits);
}
if (st <= 0) continue;
if (!cone_looks_like_pe(cone_cells)) continue;
pool<SigBit> cone_bits = leaf_bits;
pool<SigBit> control_bits;
for (Cell* c : cone_cells) {
for (auto& conn : c->connections()) {
if (c->output(conn.first)) {
for (auto bit : sigmap(conn.second))
if (bit.wire) cone_bits.insert(bit);
}
if (c->input(conn.first) && is_control_input(c, conn.first)) {
for (auto bit : sigmap(conn.second))
if (bit.wire) control_bits.insert(bit);
}
}
}
if (control_bits.empty()) continue;
candidates.push_back({S_wire, std::move(cone_cells), std::move(leaf_bits),
std::move(cone_bits), std::move(control_bits),
sole_driver, out_port});
}
// Stage 2: process candidates in order of cone size (LARGEST first).
// Verific-style lowerings often expose several wires along the same
// chain that all fingerprint as a PE on the same input bus (e.g. a
// "found ? chain_out : default" wrapper mux plus the raw chain tail
// plus a downstream mask & enc-merge). Rewriting only one of them
// leaves the chain alive feeding the others, so we rewrite each
// match independently and de-duplicate the emitted log-depth
// network through the per-input clz/ctz cache.
std::sort(candidates.begin(), candidates.end(),
[](const Candidate& a, const Candidate& b) {
if (GetSize(a.cone_cells) != GetSize(b.cone_cells))
return GetSize(a.cone_cells) > GetSize(b.cone_cells);
return GetSize(a.cone_bits) > GetSize(b.cone_bits);
});
vector<Rewrite> rewrites;
pool<Wire*> claimed_outputs;
pool<Cell*> claimed_drivers;
for (auto& cand : candidates) {
if (claimed_outputs.count(cand.S_wire)) continue;
if (claimed_drivers.count(cand.sole_driver)) continue;
int Wbits = cand.S_wire->width;
SigSpec S_sig = sigmap(SigSpec(cand.S_wire));
vector<Wire*> Ts = find_candidate_Ts(cand.S_wire, cand.cone_bits,
cand.control_bits, Wbits);
for (Wire* T_wire : Ts) {
int N = T_wire->width;
SigSpec T_sig = sigmap(SigSpec(T_wire));
pool<SigBit> T_bits;
for (auto bit : T_sig)
if (bit.wire) T_bits.insert(bit);
if (!cone_depends_only_on_T(S_sig, T_bits)) continue;
PEVariant variant = fingerprint(ce, T_sig, S_sig, N, Wbits);
if (variant == PEVariant::NONE) continue;
log(" %s: %s <- %s(%s) [N=%d, W=%d]\n",
log_id(module), log_id(cand.S_wire), variant_name(variant),
log_id(T_wire), N, Wbits);
rewrites.push_back({cand.S_wire, T_wire, N, Wbits, variant,
cand.sole_driver, cand.out_port});
claimed_outputs.insert(cand.S_wire);
claimed_drivers.insert(cand.sole_driver);
break;
}
}
// Stage 3: round-robin (rotated priority) detection. Reuses the same
// candidate cones; an output S is grant/idx_next of a round-robin
// arbiter over a wide request bus `req` and a same-width-as-S pointer
// `start`, both bottoming out the cone.
vector<RRRewrite> rr_rewrites;
if (detect_rr) {
const int max_pairs = 64;
for (auto& cand : candidates) {
if (claimed_outputs.count(cand.S_wire)) continue;
if (claimed_drivers.count(cand.sole_driver)) continue;
if (!cone_looks_like_rr(cand.cone_cells)) continue;
int W = cand.S_wire->width;
if (W < 2 || W > max_W) continue;
SigSpec S_sig = sigmap(SigSpec(cand.S_wire));
vector<Wire*> in_cone = wires_in_cone(cand.cone_bits, [&](Wire* w) {
return w != cand.S_wire;
});
vector<Wire*> req_cands, start_cands;
for (Wire* w : in_cone) {
int wn = w->width;
if (wn >= min_input_width && wn <= max_input_width &&
clog2_int(wn) == W)
req_cands.push_back(w);
if (wn == W)
start_cands.push_back(w);
}
std::sort(req_cands.begin(), req_cands.end(),
[](Wire* a, Wire* b) { return a->width > b->width; });
bool matched = false;
for (Wire* req_wire : req_cands) {
if (matched) break;
int N = req_wire->width;
SigSpec req_sig = sigmap(SigSpec(req_wire));
pool<SigBit> req_bits;
for (auto bit : req_sig)
if (bit.wire) req_bits.insert(bit);
// Per-req_wire fingerprint budget: a start-candidate-heavy
// first req size must not exhaust a shared budget and starve
// later (narrower) req sizes.
int pairs = 0;
for (Wire* start_wire : start_cands) {
if (start_wire == req_wire) continue;
if (++pairs > max_pairs) break;
SigSpec start_sig = sigmap(SigSpec(start_wire));
pool<SigBit> allowed = req_bits;
for (auto bit : start_sig)
if (bit.wire) allowed.insert(bit);
if (!cone_depends_only_on_set(S_sig, allowed)) continue;
int kind = fingerprint_rr(ce, req_sig, start_sig, S_sig, N, W);
if (kind < 0) continue;
log(" %s: %s <- round_robin_%s(req=%s, start=%s) [N=%d, W=%d]\n",
log_id(module), log_id(cand.S_wire),
kind == 0 ? "grant" : "next",
log_id(req_wire), log_id(start_wire), N, W);
rr_rewrites.push_back({cand.S_wire, req_wire, start_wire, N, W,
kind, cand.sole_driver, cand.out_port});
claimed_outputs.insert(cand.S_wire);
claimed_drivers.insert(cand.sole_driver);
matched = true;
break;
}
}
}
}
// Apply rewrites. We collected first to avoid the index growing stale
// while we add new cells/wires.
for (auto& r : rewrites) {
cell = r.sole_driver;
SigSpec new_S = emit_pe(r.variant, r.T_wire, r.N, r.Wbits);
// Disconnect the old driver by re-pointing its Y to a fresh wire.
Wire* dangling = module->addWire(NEW_ID2_SUFFIX("dangling"), r.Wbits);
r.sole_driver->setPort(r.out_port, dangling);
module->connect(SigSpec(r.S_wire), new_S);
regions_rewritten++;
}
for (auto& r : rr_rewrites) {
cell = r.sole_driver;
SigSpec new_S = emit_rr(r.req_wire, r.start_wire, r.N, r.W, r.kind);
Wire* dangling = module->addWire(NEW_ID2_SUFFIX("dangling"), r.W);
r.sole_driver->setPort(r.out_port, dangling);
module->connect(SigSpec(r.S_wire), new_S);
regions_rewritten++;
}
}
};
struct OptPriEncPass : public Pass {
OptPriEncPass() : Pass("opt_prienc",
"detect and rewrite priority-encoder / CLZ / CTZ regions") {}
void help() override {
// |---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|
log("\n");
log(" opt_prienc [options] [selection]\n");
log("\n");
log("This pass uses functional fingerprinting to detect combinational logic\n");
log("regions that implement a priority encoder, count-leading-zeros (CLZ), or\n");
log("count-trailing-zeros (CTZ) on a single contiguous input wire, regardless\n");
log("of how the RTL was written (unrolled for-loops, casez priority lists,\n");
log("pmux chains, etc.). Each detected region is replaced with a log-depth\n");
log("network built from $mux/$not/$sub cells.\n");
log("\n");
log("Detected variants:\n");
log("\n");
log(" clz_full : result = N when input is 0, else N-1 - msb_set_pos.\n");
log(" Output width = ceil(log2(N+1)).\n");
log(" clz_short : result = N-1 - msb_set_pos for nonzero input; the\n");
log(" output for input==0 is unconstrained. Output width =\n");
log(" ceil(log2(N)).\n");
log(" ctz_full : symmetric to clz_full from the LSB side.\n");
log(" ctz_short : symmetric to clz_short from the LSB side.\n");
log("\n");
log("In addition, the pass detects round-robin (rotated priority)\n");
log("arbiters: grant / idx_next = first set request bit scanning upward\n");
log("(wrapping) from just after a stored pointer idx_last. RTL typically\n");
log("spells this as a DEPTH-iteration idx-- loop over req[idx], which\n");
log("elaborates into a serial chain; it is replaced with a log-depth\n");
log("threshold-mask + CTZ network. For power-of-2 DEPTH the rewrite is\n");
log("equivalent for every pointer value; for other widths it is\n");
log("equivalent for every reachable pointer (idx_last in [0,DEPTH)).\n");
log("\n");
log(" -clz\n");
log(" detect CLZ patterns only (also disables round-robin).\n");
log("\n");
log(" -ctz\n");
log(" detect CTZ patterns only (also disables round-robin).\n");
log("\n");
log(" -no-rr\n");
log(" disable round-robin / rotated-priority detection.\n");
log("\n");
log(" -max-width N\n");
log(" maximum input bus width to consider (default 64).\n");
log("\n");
log(" -min-width N\n");
log(" minimum input bus width to consider (default 4). Smaller\n");
log(" inputs are too easy to alias and rarely worth rewriting.\n");
log("\n");
log("This pass is not invoked by the default 'opt' script; users opt in.\n");
log("After rewriting, the original cone cells become unused and are removed\n");
log("by the trailing 'clean -purge'.\n");
log("\n");
}
void execute(std::vector<std::string> args, RTLIL::Design *design) override {
log_header(design, "Executing OPT_PRIENC pass (priority encoder / CLZ / CTZ).\n");
bool only_clz = false;
bool only_ctz = false;
bool no_rr = false;
int max_width = 64;
int min_width = 4;
size_t argidx;
for (argidx = 1; argidx < args.size(); argidx++) {
if (args[argidx] == "-clz") { only_clz = true; continue; }
if (args[argidx] == "-ctz") { only_ctz = true; continue; }
if (args[argidx] == "-no-rr") { no_rr = true; continue; }
if (args[argidx] == "-max-width" && argidx + 1 < args.size()) {
max_width = std::stoi(args[++argidx]); continue;
}
if (args[argidx] == "-min-width" && argidx + 1 < args.size()) {
min_width = std::stoi(args[++argidx]); continue;
}
break;
}
extra_args(args, argidx, design);
// -clz / -ctz select a single leading/trailing variant and disable
// round-robin detection unless the user re-enables it explicitly.
if (only_clz || only_ctz) no_rr = true;
int total_regions = 0;
int total_cells_added = 0;
for (auto module : design->selected_modules()) {
OptPriEncWorker worker(module);
worker.detect_clz = !only_ctz;
worker.detect_ctz = !only_clz;
worker.detect_rr = !no_rr;
worker.max_input_width = max_width;
worker.min_input_width = min_width;
worker.run();
total_regions += worker.regions_rewritten;
total_cells_added += worker.cells_added;
}
log("Rewrote %d region(s); emitted %d new cell(s).\n",
total_regions, total_cells_added);
Yosys::run_pass("clean -purge");
}
} OptPriEncPass;
PRIVATE_NAMESPACE_END