/* * yosys -- Yosys Open SYnthesis Suite * * Copyright (C) 2026 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. */ // Shared cut-region matching infrastructure for the functional rewrite // passes (opt_argmax, opt_priority_onehot, opt_compact_prefix). These passes // find combinational regions between "cut" signals (module ports, FF data // pins, or internal buses), verify their function by ConstEval // fingerprinting, and replace the region while leaving surrounding logic // untouched. // // This header is designed to be included INSIDE each pass's private // namespace (after PRIVATE_NAMESPACE_BEGIN), so the shared code has a single // source without introducing link-level coupling between the passes. // // All graph walks and fingerprint evaluations are charged against // per-module work budgets so that adversarial netlist shapes (deep shared // cones with hundreds of same-width candidate buses) degrade into skipped // candidates instead of multi-minute runtimes. struct CutRegionWorker { struct RootCand { SigSpec sig; std::string name; }; struct BusCand { SigSpec sig; std::string name; int entries = 0; int elem_width = 0; bool is_const = false; }; Module *module; SigMap sigmap; dict bit_to_driver; pool input_port_bits; pool claimed_bits; std::string last_cut_fail; // Work budgets, decremented as the search runs. Walk steps count cells // visited by cone/cut traversals; eval steps approximate ConstEval cost // as (test vectors x cone cells); attempts count cut-closure trials // (each one also carries pool/queue setup overhead, so the count is // bounded separately from the step total). When a budget runs out the // remaining candidates in the module are skipped (matching is // best-effort). int64_t walk_budget = 20000000; int64_t eval_budget = 20000000; int64_t attempt_budget = 65536; bool walk_exhausted() const { return walk_budget <= 0 || attempt_budget <= 0; } bool eval_exhausted() const { return eval_budget <= 0; } void charge_walk(int64_t n) { walk_budget -= n; } void charge_eval(int64_t n) { eval_budget -= n; } // One visible note per module when a budget runs out, so QoR changes // caused by truncated candidate searches are diagnosable from the log // (the pass options can then raise the budget for that design). bool budget_noted = false; void note_budget(const char *pass_name, int skipped_roots) { if (budget_noted) return; if (!walk_exhausted() && !eval_exhausted()) return; budget_noted = true; const char *which = attempt_budget <= 0 ? "attempt budget" : walk_budget <= 0 ? "walk budget" : "eval budget"; log_debug("Note: %s search %s exhausted in module %s; %d remaining root candidate(s) skipped. " "Use the pass budget options to raise the limit if QoR matters more than runtime here.\n", pass_name, which, log_id(module), skipped_roots); } CutRegionWorker(Module *module) : module(module), sigmap(module) { build_indexes(); } bool is_sequential(Cell *c) { return c->type.in( ID($ff), ID($dff), ID($dffe), ID($adff), ID($adffe), ID($sdff), ID($sdffe), ID($sdffce), ID($dffsr), ID($dffsre), ID($_DFF_P_), ID($_DFF_N_), ID($_DFFE_PP_), ID($_DFFE_PN_), ID($_DFFE_NP_), ID($_DFFE_NN_), ID($_DFF_PP0_), ID($_DFF_PP1_), ID($_DFF_PN0_), ID($_DFF_PN1_), ID($_DFF_NP0_), ID($_DFF_NP1_), ID($_DFF_NN0_), ID($_DFF_NN1_), ID($dlatch), ID($adlatch), ID($dlatchsr), ID($mem), ID($mem_v2), ID($meminit), ID($meminit_v2), ID($memrd), ID($memrd_v2), ID($memwr), ID($memwr_v2), ID($fsm), ID($assert), ID($assume), ID($cover), ID($live), ID($fair), ID($print), ID($check), ID($anyconst), ID($anyseq), ID($allconst), ID($allseq), ID($initstate)); } void build_indexes() { for (auto c : module->cells()) { if (is_sequential(c)) continue; for (auto &conn : c->connections()) { if (!c->output(conn.first)) continue; for (auto bit : sigmap(conn.second)) { if (!bit.wire) continue; auto it = bit_to_driver.find(bit); if (it == bit_to_driver.end()) bit_to_driver[bit] = c; else if (it->second != c) it->second = nullptr; } } } for (auto w : module->wires()) { if (!w->port_input) continue; for (auto bit : sigmap(SigSpec(w))) if (bit.wire) input_port_bits.insert(bit); } } // Combinational fanin cone of `from`. Leaves are port-input bits or bits // driven by sequential cells / undriven. Returns false if size limits // are exceeded. `cell_order` records the cells in BFS discovery order // (closest to `from` first). bool get_cone(SigSpec from, pool &cone_cells, pool &leaf_bits, int max_cone_cells, int max_leaf_bits, vector *cell_order = nullptr) { pool visited; std::queue 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 false; continue; } Cell *drv = bit_to_driver.at(bit, nullptr); if (drv == nullptr) { leaf_bits.insert(bit); if (GetSize(leaf_bits) > max_leaf_bits) return false; continue; } if (!cone_cells.insert(drv).second) continue; charge_walk(1); if (GetSize(cone_cells) > max_cone_cells) return false; if (cell_order != nullptr) cell_order->push_back(drv); 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; } // Walk the cone of `root`, cutting it at the bits in `allowed`. Returns // true iff the cut cone closes (no other primary input / undriven bit is // reached). `hit_bits`, when given, collects the allowed bits the cone // actually uses. `forced_bits`, when given, is the subset of allowed // bits the fingerprint will force: no forced bit may be driven by a cell // inside the cut cone, since ConstEval caches whole cell outputs and // evaluating such a driver would conflict with the forced values (when // `forced_bits` is null, all of `allowed` is treated as forced). bool cut_cone_walk(const SigSpec &root, const pool &allowed, int max_cells, pool *hit_bits = nullptr, pool *cells_out = nullptr, const pool *forced_bits = nullptr, const pool *full_leaves = nullptr, const pool *full_cells = nullptr) { attempt_budget--; // Walk-free fast path: when no allowed bit has a combinational // driver, no cut can shadow any cone leaf, so the cut closes iff it // covers every leaf of the full cone (and the cut cone is the full // cone). This answers the dominant class of failing candidates // (port/FF-level bus pairs) in a handful of hash lookups. if (full_leaves != nullptr && full_cells != nullptr) { bool allowed_all_leaf = true; for (auto bit : allowed) if (bit_to_driver.at(bit, nullptr) != nullptr) { allowed_all_leaf = false; break; } if (allowed_all_leaf) { for (auto leaf : *full_leaves) if (!allowed.count(leaf)) { last_cut_fail = stringf("leaf %s", log_signal(leaf)); return false; } if (GetSize(*full_cells) > max_cells) { last_cut_fail = "size limit"; return false; } if (hit_bits != nullptr) *hit_bits = *full_leaves; if (cells_out != nullptr) *cells_out = *full_cells; return true; } } pool visited; pool cells_seen; std::queue worklist; for (auto bit : sigmap(root)) { 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)) { if (hit_bits != nullptr) hit_bits->insert(bit); continue; } Cell *drv = bit_to_driver.at(bit, nullptr); if (drv == nullptr) { last_cut_fail = stringf("leaf %s", log_signal(bit)); return false; } if (!cells_seen.insert(drv).second) continue; charge_walk(1); if (GetSize(cells_seen) > max_cells || walk_exhausted()) { last_cut_fail = "size limit"; 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); } } } const pool &check = (forced_bits != nullptr) ? *forced_bits : (hit_bits != nullptr) ? *hit_bits : allowed; for (auto bit : check) { Cell *drv = bit_to_driver.at(bit, nullptr); if (drv != nullptr && cells_seen.count(drv)) { last_cut_fail = stringf("forced bit %s driven inside cone", log_signal(bit)); return false; } } if (cells_out != nullptr) *cells_out = cells_seen; return true; } // Walk the cone of `root` cut at `allowed`, collecting up to `max_extra` // remaining boundary bits (inputs the cut does not cover) instead of // failing on them. Aborts early once the limit is crossed, since callers // only probe small uncovered sets. bool cut_cone_extra_leaves(const SigSpec &root, const pool &allowed, int max_cells, pool &extra_leaves, int max_extra) { attempt_budget--; pool visited; pool cells_seen; std::queue worklist; for (auto bit : sigmap(root)) { 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; Cell *drv = bit_to_driver.at(bit, nullptr); if (drv == nullptr) { extra_leaves.insert(bit); if (GetSize(extra_leaves) > max_extra) return false; continue; } if (!cells_seen.insert(drv).second) continue; charge_walk(1); if (GetSize(cells_seen) > max_cells || walk_exhausted()) 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; } bool sig_fully_driven(const SigSpec &sig) { for (auto bit : sigmap(sig)) { if (!bit.wire) return false; if (input_port_bits.count(bit)) return false; if (bit_to_driver.at(bit, nullptr) == nullptr) return false; } return true; } // Collect the bus bits into `seen_bits`, rejecting constant or repeated // bits (fingerprints drive each bus bit independently). bool sig_bits_unique(const SigSpec &sig, pool &seen_bits) { for (auto bit : sigmap(sig)) if (!bit.wire || !seen_bits.insert(bit).second) return false; return true; } // Cut buses only need to consist of wire bits; FF outputs (cone leaves) // are valid region boundaries even though they have no comb driver. bool sig_bus_ok(const SigSpec &sig) { for (auto bit : sigmap(sig)) if (!bit.wire) return false; return true; } pool sig_bit_pool(const SigSpec &sig) { pool bits; for (auto bit : sigmap(sig)) if (bit.wire) bits.insert(bit); return bits; } // Depth of each cone cell measured from the cone leaves (cells reading // only leaf/port bits have depth 1). Used to order candidate cut buses // so signals produced by shallow pre-logic are tried first. dict compute_cone_depths(const pool &cone_cells) { dict depth; dict> succs; dict npreds; std::queue ready; for (auto c : cone_cells) { pool preds; for (auto &conn : c->connections()) { if (!c->input(conn.first)) continue; for (auto bit : sigmap(conn.second)) { if (!bit.wire) continue; Cell *drv = bit_to_driver.at(bit, nullptr); if (drv != nullptr && drv != c && cone_cells.count(drv)) preds.insert(drv); } } npreds[c] = GetSize(preds); for (auto p : preds) succs[p].push_back(c); if (preds.empty()) { depth[c] = 1; ready.push(c); } } while (!ready.empty()) { Cell *c = ready.front(); ready.pop(); for (auto s : succs.at(c, vector())) { if (depth.at(s, 0) < depth.at(c) + 1) depth[s] = depth.at(c) + 1; if (--npreds.at(s) == 0) ready.push(s); } } return depth; } bool find_anchor_driver(const SigSpec &out_sig, Cell *&anchor) { for (auto bit : sigmap(out_sig)) { Cell *drv = bit_to_driver.at(bit, nullptr); if (drv != nullptr) { anchor = drv; return true; } } return false; } // Detach the existing drivers of `out_sig` (bit-precise: other bits of // shared driver outputs keep their connections). The caller then drives // `out_sig` from the replacement logic; the disconnected cells become // dead and are removed by the trailing 'clean -purge'. void disconnect_root(const SigSpec &out_sig, Cell *anchor, const char *dangling_suffix) { pool target_bits; for (auto bit : sigmap(out_sig)) if (bit.wire) target_bits.insert(bit); pool seen_cells; for (auto target : target_bits) { Cell *drv = bit_to_driver.at(target, nullptr); if (drv == nullptr || seen_cells.count(drv)) continue; seen_cells.insert(drv); for (auto &conn : drv->connections()) { if (!drv->output(conn.first)) continue; SigSpec orig = conn.second; SigSpec replacement = orig; bool changed = false; Cell *cell = drv; Wire *dangling = module->addWire(NEW_ID2_SUFFIX(dangling_suffix), GetSize(orig)); for (int i = 0; i < GetSize(orig); i++) { if (target_bits.count(sigmap(orig[i]))) { replacement[i] = SigBit(dangling, i); changed = true; } } if (changed) drv->setPort(conn.first, replacement); } } (void)anchor; } // Claim the root and every signal produced inside the matched region, so // functionally identical sub-roots (e.g. the data input of the region's // final mux) are not rewritten again. void claim_region(const SigSpec &root_sig, const pool &cut_cells) { for (auto bit : sigmap(root_sig)) if (bit.wire) claimed_bits.insert(bit); for (auto c : cut_cells) for (auto &conn : c->connections()) { if (!c->output(conn.first)) continue; for (auto bit : sigmap(conn.second)) if (bit.wire) claimed_bits.insert(bit); } } bool root_claimed(const SigSpec &root_sig) { for (auto bit : sigmap(root_sig)) if (bit.wire && claimed_bits.count(bit)) return true; return false; } // Parse a split name of the form "base[index]" (Verific lowers packed // multi-dimensional ports, nets and array FFs into per-lane wires named // this way). bool parse_indexed_port_name(Wire *wire, std::string &base, int &index) { std::string name = wire->name.str(); size_t rbrack = name.size(); if (rbrack == 0 || name[rbrack - 1] != ']') return false; size_t lbrack = name.rfind('['); if (lbrack == std::string::npos || lbrack + 1 >= rbrack - 1) return false; for (size_t i = lbrack + 1; i < rbrack - 1; i++) if (!isdigit(name[i])) return false; base = name.substr(0, lbrack); index = atoi(name.substr(lbrack + 1, rbrack - lbrack - 2).c_str()); return true; } // Group per-lane split wires into contiguous, equal-width buses. The run // may start at any base index; the resulting sig is the ascending-index // concatenation, so lane k is sig[k*elem_width ...] = the (base+k)-th // wire. vector collect_split_buses(const vector &wires) { std::map>> groups; for (auto w : wires) { std::string base; int index = -1; if (parse_indexed_port_name(w, base, index)) groups[base].push_back({index, w}); } vector buses; for (auto &it : groups) { auto entries = it.second; std::sort(entries.begin(), entries.end(), [](const std::pair &a, const std::pair &b) { return a.first < b.first; }); if (entries.empty()) continue; bool contiguous = true; int base_index = entries.front().first; int elem_width = GetSize(entries.front().second); for (int i = 0; i < GetSize(entries); i++) { if (entries[i].first != base_index + i || GetSize(entries[i].second) != elem_width) { contiguous = false; break; } } if (!contiguous) continue; SigSpec sig; for (auto &entry : entries) sig.append(SigSpec(entry.second)); buses.push_back({sig, it.first, GetSize(entries), elem_width}); } return buses; } // All bits produced inside the cone or appearing as its leaves; the // universe wire-run and split-bus collection draws candidates from. pool cone_sig_bit_pool(const pool &cone_cells, const pool &leaf_bits) { pool bits = leaf_bits; for (auto c : cone_cells) for (auto &conn : c->connections()) if (c->output(conn.first)) for (auto bit : sigmap(conn.second)) if (bit.wire) bits.insert(bit); return bits; } // Maximal contiguous in-cone wire-bit runs, longest first (real region // buses are wide; incidental wires are short and must not exhaust the // cap). Constant edge bits (e.g. the never-written top bit of a [W:0] // vector) are trimmed instead of rejecting the whole wire. vector collect_wire_run_buses(const pool &cone_sig_bits, int cap) { vector wire_runs; for (auto wb : module->wires()) { if (GetSize(wb) < 2) continue; SigSpec sig = sigmap(SigSpec(wb)); int run_start = -1; for (int i = 0; i <= GetSize(sig); i++) { bool ok = i < GetSize(sig) && sig[i].wire && cone_sig_bits.count(sig[i]); if (ok && run_start < 0) run_start = i; if (!ok && run_start >= 0) { int run_len = i - run_start; if (run_len >= 2) { SigSpec run = sig.extract(run_start, run_len); std::string name = (run_len == GetSize(wb)) ? wb->name.str() : stringf("%s[%d+:%d]", wb->name.str().c_str(), run_start, run_len); wire_runs.push_back({run, name}); } run_start = -1; } } } std::stable_sort(wire_runs.begin(), wire_runs.end(), [](const BusCand &a, const BusCand &b) { return GetSize(a.sig) > GetSize(b.sig); }); if (GetSize(wire_runs) > cap) wire_runs.resize(cap); return wire_runs; } // Split-wire buses whose lanes touch the cone. vector collect_cone_split_buses(const pool &cone_sig_bits) { vector cone_wires; for (auto wb : module->wires()) { bool touches = false; for (auto bit : sigmap(SigSpec(wb))) if (bit.wire && cone_sig_bits.count(bit)) { touches = true; break; } if (touches) cone_wires.push_back(wb); } return collect_split_buses(cone_wires); } // Per-seed cone cache: the seed sweep is the dominant fixed cost in // FF-heavy modules and is shared by every matching mode of a pass. struct SeedCone { pool cells; vector order; bool valid = false; }; dict> seed_cone_cache; std::shared_ptr seed_cone(const SigSpec &seed, int max_cone_cells, int max_leaf_bits) { auto it = seed_cone_cache.find(seed); if (it != seed_cone_cache.end()) return it->second; auto sc = std::make_shared(); pool leaf_bits; sc->valid = !walk_exhausted() && get_cone(seed, sc->cells, leaf_bits, max_cone_cells, max_leaf_bits, &sc->order); seed_cone_cache[seed] = sc; return sc; } // Collect candidate root signals. Module output ports and FF data inputs // are seeds; internal signals inside seed cones (cell connections, and // optionally whole wires fully inside a seed cone) are added so that // regions wrapped in extra combinational post-logic are still found. // Internal candidates are taken round-robin across the seed orders so a // module with many FFs cannot starve the seeds whose cones hold the // region. `width_ok` filters candidate widths; `seed_cone_interesting` // gates internal harvesting per seed cone (e.g. "contains $bmux"). vector collect_root_candidates( std::function width_ok, std::function &)> seed_cone_interesting, bool wire_roots, int max_cone_cells, int max_leaf_bits, int max_internal_roots = 128) { vector roots; pool seen; vector seed_sigs; auto consider_root = [&](const SigSpec &sig, const std::string &name, bool seed) -> bool { if (!width_ok(GetSize(sig))) return false; if (!seen.insert(sig).second) return false; roots.push_back({sig, name}); if (seed) seed_sigs.push_back(sig); return true; }; for (auto w : module->wires()) { if (!w->port_output || w->port_input) continue; consider_root(sigmap(SigSpec(w)), w->name.str(), true); } for (auto c : module->cells()) { if (!c->type.in(ID($ff), ID($dff), ID($dffe), ID($adff), ID($adffe), ID($sdff), ID($sdffe), ID($sdffce), ID($dffsr), ID($dffsre), ID($dlatch), ID($adlatch), ID($dlatchsr))) continue; if (!c->hasPort(ID::D)) continue; consider_root(sigmap(c->getPort(ID::D)), stringf("%s.D", log_id(c->name)), true); } pool cone_out_bits; vector> seed_orders; for (auto &seed : seed_sigs) { auto sc = seed_cone(seed, max_cone_cells, max_leaf_bits); vector order; if (sc->valid && seed_cone_interesting(sc->cells)) order = sc->order; seed_orders.push_back(order); if (wire_roots) for (auto c : order) for (auto &conn : c->connections()) if (c->output(conn.first)) for (auto bit : sigmap(conn.second)) if (bit.wire) cone_out_bits.insert(bit); } // Internal cell connections in round-robin BFS order (closest to a // seed first), so post-logic wrappers are peeled off quickly. These // come before wire roots: connection buses are ordered by proximity // to the seeds, while wire iteration order is arbitrary. int internal_roots = 0; size_t longest_order = 0; for (auto &order : seed_orders) longest_order = std::max(longest_order, order.size()); for (size_t pos = 0; pos < longest_order && internal_roots < max_internal_roots; pos++) { for (auto &order : seed_orders) { if (internal_roots >= max_internal_roots) break; if (pos >= order.size()) continue; Cell *c = order[pos]; for (auto &conn : c->connections()) { SigSpec sig = sigmap(conn.second); if (!width_ok(GetSize(sig))) continue; if (!sig_fully_driven(sig)) continue; if (consider_root(sig, stringf("%s.%s", log_id(c->name), log_id(conn.first)), false)) internal_roots++; } } } // Whole wires fully inside some seed cone (regions written bit by // bit, e.g. a |= scatter chain, are only visible as named wires). int wire_root_count = 0; if (wire_roots) { for (auto w : module->wires()) { if (wire_root_count >= max_internal_roots) break; if (!width_ok(GetSize(w))) continue; SigSpec sig = sigmap(SigSpec(w)); bool inside = true; for (auto bit : sig) if (!bit.wire || !cone_out_bits.count(bit)) { inside = false; break; } if (!inside) continue; if (consider_root(sig, w->name.str(), false)) wire_root_count++; } } return roots; } // --- Small numeric helpers shared by the fingerprints. --- static uint64_t lowmask_u64(int w) { if (w <= 0) return 0; if (w >= 64) return ~0ULL; return (1ULL << w) - 1; } static Const const_u64(uint64_t value, int width) { vector bits(width, State::S0); for (int i = 0; i < width && i < 64; i++) if ((value >> i) & 1ULL) bits[i] = State::S1; return Const(bits); } SigSpec zext_sig(SigSpec sig, int width) { sig = sigmap(sig); if (GetSize(sig) > width) return sig.extract(0, width); if (GetSize(sig) < width) sig.append(SigSpec(State::S0, width - GetSize(sig))); return sig; } // Evaluate `out_sig` under the given input assignments; returns false if // the cut does not fully determine the output. Charges the eval budget // by `cone_cells_estimate`. bool eval_with(ConstEval &ce, const vector> &sets, const SigSpec &out_sig, uint64_t &result, int64_t cone_cells_estimate) { charge_eval(cone_cells_estimate); ce.push(); for (auto &s : sets) ce.set(s.first, s.second); SigSpec out = out_sig; SigSpec undef; bool ok = ce.eval(out, undef); if (ok && out.is_fully_const()) { Const cv = out.as_const(); uint64_t r = 0; for (int i = 0; i < GetSize(cv) && i < 64; i++) if (cv[i] == State::S1) r |= 1ULL << i; result = r; } else { ok = false; } ce.pop(); return ok; } };