From 3c536a06b7ef34eceabc80a39389305405324b5f Mon Sep 17 00:00:00 2001 From: nella Date: Thu, 6 Aug 2026 09:14:37 +0200 Subject: [PATCH 1/4] Detect graph cycles and skip them. --- passes/opt/opt_balance_tree.cc | 57 ++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/passes/opt/opt_balance_tree.cc b/passes/opt/opt_balance_tree.cc index 915a6fb39..5566a8033 100644 --- a/passes/opt/opt_balance_tree.cc +++ b/passes/opt/opt_balance_tree.cc @@ -35,6 +35,11 @@ struct OptBalanceTreeWorker { // Counts of each cell type that are getting balanced dict cell_count; + // Per cell type netlist indexes, rebuilt for each balanced cell type + dict sig_to_driver; + pool input_port_sigs; + pool consumed_cells; + // Check if cell is of the right type and has matching input/output widths // Only allow cells with "natural" output widths (no truncation) to prevent // equivalence issues when rebalancing (see YosysHQ/yosys#5605) @@ -64,6 +69,43 @@ struct OptBalanceTreeWorker { return y_width >= natural_width; } + // Check if the driver graph reachable from head contains a cycle, + // following the same edges as the backward chain traversal + bool has_cycle(Cell *head, IdString cell_type) { + pool on_path, done; + vector> stack = {{head, false}}; + while (!stack.empty()) + { + auto [c, leave] = stack.back(); + stack.pop_back(); + if (leave) { + on_path.erase(c); + done.insert(c); + continue; + } + if (done.count(c)) + continue; + if (on_path.count(c)) + return true; + on_path.insert(c); + stack.push_back({c, true}); + for (IdString port: {ID::A, ID::B}) { + auto sig = sigmap(c->getPort(port)); + Cell *drv = sig_to_driver[sig]; + bool drv_ok = drv && is_right_type(drv, cell_type); + for (auto bit : sig) { + if (input_port_sigs.count(bit) && !consumed_cells.count(drv)) { + drv_ok = false; + break; + } + } + if (drv_ok) + stack.push_back({drv, false}); + } + } + return false; + } + // Create a balanced binary tree from a vector of source signals SigSpec create_balanced_tree(vector &sources, IdString cell_type, Cell* cell) { // Base case: if we have no sources, return an empty signal @@ -142,7 +184,7 @@ struct OptBalanceTreeWorker { // Do for each cell type for (auto cell_type : cell_types) { // Index all of the nets in the module - dict sig_to_driver; + sig_to_driver.clear(); dict> sig_to_sink; for (auto cell : module->selected_cells()) { @@ -162,7 +204,7 @@ struct OptBalanceTreeWorker { } // Need to check if any wires connect to module ports - pool input_port_sigs; + input_port_sigs.clear(); pool output_port_sigs; for (auto wire : module->selected_wires()) if (wire->port_input || wire->port_output) { @@ -176,7 +218,7 @@ struct OptBalanceTreeWorker { } // Actual logic starts here - pool consumed_cells; + consumed_cells.clear(); for (auto cell : module->selected_cells()) { // If consumed or not the correct type, skip @@ -189,11 +231,15 @@ struct OptBalanceTreeWorker { pool sinks; pool current_loads = sig_to_sink[y]; pool next_loads; + pool visited_loads; while (!current_loads.empty()) { // Find each sink and see what they are for (auto x : current_loads) { + if (!visited_loads.insert(x).second) + continue; + // If not the correct type, don't follow any further // (but add the originating cell to the list of sinks) if (!is_right_type(x, cell_type)) @@ -246,6 +292,11 @@ struct OptBalanceTreeWorker { if (consumed_cells.count(head_cell)) continue; + // Abandon chains containing combinational loops, since + // rebalancing them is not sound (and would not terminate) + if (has_cycle(head_cell, cell_type)) + continue; + // Get sources of the chain dict sources; dict signeds; From 7bbb69872c041fcd325882424887401dce0deba7 Mon Sep 17 00:00:00 2001 From: nella Date: Thu, 6 Aug 2026 09:14:44 +0200 Subject: [PATCH 2/4] Add regression tests. --- tests/opt/opt_balance_tree.ys | 93 +++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/opt/opt_balance_tree.ys b/tests/opt/opt_balance_tree.ys index 030449731..dd95576c3 100644 --- a/tests/opt/opt_balance_tree.ys +++ b/tests/opt/opt_balance_tree.ys @@ -1244,3 +1244,96 @@ design -load postopt design -reset log -pop + + +# Test 31 +log -header "Combinational loop is left untouched" +log -push +design -reset +read_verilog < Date: Thu, 6 Aug 2026 10:51:49 +0200 Subject: [PATCH 3/4] Use toposort. --- passes/opt/opt_balance_tree.cc | 66 ++++++++++++++-------------------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/passes/opt/opt_balance_tree.cc b/passes/opt/opt_balance_tree.cc index 5566a8033..352bb1e8f 100644 --- a/passes/opt/opt_balance_tree.cc +++ b/passes/opt/opt_balance_tree.cc @@ -21,6 +21,7 @@ #include "kernel/yosys.h" #include "kernel/sigtools.h" +#include "kernel/utils.h" #include USING_YOSYS_NAMESPACE @@ -69,41 +70,36 @@ struct OptBalanceTreeWorker { return y_width >= natural_width; } + // Get the driver of a cell input port if it continues the chain, else nullptr + Cell *chain_driver(Cell *cell, IdString port, IdString cell_type) { + auto sig = sigmap(cell->getPort(port)); + Cell *drv = sig_to_driver[sig]; + if (!drv || !is_right_type(drv, cell_type)) + return nullptr; + for (auto bit : sig) + if (input_port_sigs.count(bit) && !consumed_cells.count(drv)) + return nullptr; + return drv; + } + // Check if the driver graph reachable from head contains a cycle, // following the same edges as the backward chain traversal bool has_cycle(Cell *head, IdString cell_type) { - pool on_path, done; - vector> stack = {{head, false}}; - while (!stack.empty()) + TopoSort> toposort; + toposort.analyze_loops = false; + vector queue = {head}; + while (!queue.empty()) { - auto [c, leave] = stack.back(); - stack.pop_back(); - if (leave) { - on_path.erase(c); - done.insert(c); - continue; - } - if (done.count(c)) - continue; - if (on_path.count(c)) - return true; - on_path.insert(c); - stack.push_back({c, true}); - for (IdString port: {ID::A, ID::B}) { - auto sig = sigmap(c->getPort(port)); - Cell *drv = sig_to_driver[sig]; - bool drv_ok = drv && is_right_type(drv, cell_type); - for (auto bit : sig) { - if (input_port_sigs.count(bit) && !consumed_cells.count(drv)) { - drv_ok = false; - break; - } + Cell *c = queue.back(); + queue.pop_back(); + for (IdString port: {ID::A, ID::B}) + if (Cell *drv = chain_driver(c, port, cell_type)) { + if (!toposort.has_node(drv)) + queue.push_back(drv); + toposort.edge(drv, c); } - if (drv_ok) - stack.push_back({drv, false}); - } } - return false; + return !toposort.sort(); } // Create a balanced binary tree from a vector of source signals @@ -308,19 +304,11 @@ struct OptBalanceTreeWorker { bfs_queue.pop_front(); for (IdString port: {ID::A, ID::B}) { - auto sig = sigmap(x->getPort(port)); - Cell* drv = sig_to_driver[sig]; - bool drv_ok = drv && is_right_type(drv, cell_type); - for (auto bit : sig) { - if (input_port_sigs.count(bit) && !consumed_cells.count(drv)) { - drv_ok = false; - break; - } - } - if (drv_ok) { + if (Cell *drv = chain_driver(x, port, cell_type)) { inner_cells++; bfs_queue.push_back(drv); } else { + auto sig = sigmap(x->getPort(port)); sources[sig]++; signeds[sig] = x->getParam(port == ID::A ? ID::A_SIGNED : ID::B_SIGNED).as_bool(); } From e20909bb3b2ca2c938d3456b69d893611f8e6e98 Mon Sep 17 00:00:00 2001 From: nella Date: Thu, 6 Aug 2026 11:10:12 +0200 Subject: [PATCH 4/4] Collect chains via TopoSort. --- passes/opt/opt_balance_tree.cc | 60 ++++++++++++++++------------------ 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/passes/opt/opt_balance_tree.cc b/passes/opt/opt_balance_tree.cc index 352bb1e8f..aa1a44de2 100644 --- a/passes/opt/opt_balance_tree.cc +++ b/passes/opt/opt_balance_tree.cc @@ -22,7 +22,6 @@ #include "kernel/yosys.h" #include "kernel/sigtools.h" #include "kernel/utils.h" -#include USING_YOSYS_NAMESPACE PRIVATE_NAMESPACE_BEGIN @@ -82,26 +81,6 @@ struct OptBalanceTreeWorker { return drv; } - // Check if the driver graph reachable from head contains a cycle, - // following the same edges as the backward chain traversal - bool has_cycle(Cell *head, IdString cell_type) { - TopoSort> toposort; - toposort.analyze_loops = false; - vector queue = {head}; - while (!queue.empty()) - { - Cell *c = queue.back(); - queue.pop_back(); - for (IdString port: {ID::A, ID::B}) - if (Cell *drv = chain_driver(c, port, cell_type)) { - if (!toposort.has_node(drv)) - queue.push_back(drv); - toposort.edge(drv, c); - } - } - return !toposort.sort(); - } - // Create a balanced binary tree from a vector of source signals SigSpec create_balanced_tree(vector &sources, IdString cell_type, Cell* cell) { // Base case: if we have no sources, return an empty signal @@ -288,28 +267,45 @@ struct OptBalanceTreeWorker { if (consumed_cells.count(head_cell)) continue; + // Collect the chain cone into a topological sort + TopoSort> toposort; + toposort.analyze_loops = false; + toposort.node(head_cell); + vector queue = {head_cell}; + while (!queue.empty()) + { + Cell *x = queue.back(); + queue.pop_back(); + for (IdString port: {ID::A, ID::B}) + if (Cell *drv = chain_driver(x, port, cell_type)) { + if (!toposort.has_node(drv)) + queue.push_back(drv); + toposort.edge(drv, x); + } + } + // Abandon chains containing combinational loops, since // rebalancing them is not sound (and would not terminate) - if (has_cycle(head_cell, cell_type)) + if (!toposort.sort()) continue; - // Get sources of the chain + // Get sources of the chain: process cells from head to + // drivers, counting the paths leading back to the head so + // reconvergent sources are counted with multiplicity dict sources; dict signeds; - int inner_cells = 0; - std::deque bfs_queue = {head_cell}; - while (bfs_queue.size()) + int inner_cells = GetSize(toposort.sorted) - 1; + dict reach; + reach[head_cell] = 1; + for (int i = GetSize(toposort.sorted); i-- > 0; ) { - Cell* x = bfs_queue.front(); - bfs_queue.pop_front(); - + Cell* x = toposort.sorted[i]; for (IdString port: {ID::A, ID::B}) { if (Cell *drv = chain_driver(x, port, cell_type)) { - inner_cells++; - bfs_queue.push_back(drv); + reach[drv] += reach[x]; } else { auto sig = sigmap(x->getPort(port)); - sources[sig]++; + sources[sig] += reach[x]; signeds[sig] = x->getParam(port == ID::A ? ID::A_SIGNED : ID::B_SIGNED).as_bool(); } }