From f69a5fc0779ba91eb92a6152dc33d29523e43347 Mon Sep 17 00:00:00 2001 From: nella Date: Wed, 29 Apr 2026 11:21:26 +0200 Subject: [PATCH 01/33] Elim equiv bits. --- passes/opt/opt_dff.cc | 284 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 283 insertions(+), 1 deletion(-) diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index 709b5fc4c..cc669f902 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -919,6 +919,286 @@ struct OptDffWorker return did_something; } + + struct EqBit { + Cell *cell; + int idx; + SigBit q; + }; + + struct SigKey { + enum Flag : uint16_t { + InitOne = 1u << 0, + InitX = 1u << 1, + PolClk = 1u << 2, + PolCe = 1u << 3, + PolSrst = 1u << 4, + PolArst = 1u << 5, + PolAload = 1u << 6, + PolClr = 1u << 7, + PolSet = 1u << 8, + CeOverSrst = 1u << 9, + }; + + SigBit clk, ce, srst, arst, aload, clr, set; + IdString cell_type; // for SR + uint16_t flags; + + bool operator==(const SigKey &o) const { + return flags == o.flags && clk == o.clk && ce == o.ce && srst == o.srst && arst == o.arst + && aload == o.aload && clr == o.clr && set == o.set && cell_type == o.cell_type; + } + + Hasher hash_into(Hasher h) const { + h.eat(flags); + h.eat(clk); + h.eat(ce); + h.eat(srst); + h.eat(arst); + h.eat(aload); + h.eat(clr); + h.eat(set); + h.eat(cell_type); + return h; + } + }; + + bool is_def(State s) { + return s == State::S0 || s == State::S1; + } + + int sat_mux(QuickConeSat &qcsat, int s, int a, int b) { + return qcsat.ez->OR(qcsat.ez->AND(s, a), qcsat.ez->AND(qcsat.ez->NOT(s), b)); + } + + int sat_const(QuickConeSat &qcsat, State v) { + return v == State::S1 ? qcsat.ez->CONST_TRUE : qcsat.ez->CONST_FALSE; + } + + bool run_eqbits() + { + std::vector bits; + std::vector keys; + dict ff_for_cell; + + // Collect FF bits eligible for merging + for (auto cell : module->selected_cells()) { + if (!cell->is_builtin_ff()) + continue; + + FfData ff(&initvals, cell); + if (!ff.has_clk && !ff.has_gclk) + continue; + + ff_for_cell.emplace(cell, ff); + + for (int i = 0; i < ff.width; i++) { + // X value + if (ff.has_srst && !is_def(ff.val_srst[i])) continue; + if (ff.has_arst && !is_def(ff.val_arst[i])) continue; + + // Missing anchor + bool def_init = is_def(ff.val_init[i]); + if (!def_init && !ff.has_srst && !ff.has_arst) + continue; + + SigKey k = {}; + + // Flags + if (def_init && ff.val_init[i] == State::S1) + k.flags |= SigKey::InitOne; + else if (!def_init) + k.flags |= SigKey::InitX; + + if (ff.has_clk) { + k.clk = ff.sig_clk; + if (ff.pol_clk) k.flags |= SigKey::PolClk; + } + if (ff.has_ce) { + k.ce = ff.sig_ce; + if (ff.pol_ce) k.flags |= SigKey::PolCe; + } + if (ff.has_srst) { + k.srst = ff.sig_srst; + if (ff.pol_srst) k.flags |= SigKey::PolSrst; + if (ff.ce_over_srst) k.flags |= SigKey::CeOverSrst; + } + if (ff.has_arst) { + k.arst = ff.sig_arst; + if (ff.pol_arst) k.flags |= SigKey::PolArst; + } + if (ff.has_aload) { + k.aload = ff.sig_aload; + if (ff.pol_aload) k.flags |= SigKey::PolAload; + } + if (ff.has_sr) { + k.clr = ff.sig_clr[i]; + k.set = ff.sig_set[i]; + k.cell_type = cell->type; + if (ff.pol_clr) k.flags |= SigKey::PolClr; + if (ff.pol_set) k.flags |= SigKey::PolSet; + } + + bits.push_back({cell, i, ff.sig_q[i]}); + keys.push_back(k); + } + } + + if (GetSize(bits) < 2) + return false; + + // Group bits by control signature + dict> buckets; + for (int i = 0; i < GetSize(bits); i++) + buckets[keys[i]].push_back(i); + + std::vector> classes; + classes.reserve(GetSize(buckets)); + for (auto &kv : buckets) + if (GetSize(kv.second) >= 2) + classes.push_back(std::move(kv.second)); + + if (classes.empty()) + return false; + + ModWalker modwalker(module->design, module); + QuickConeSat qcsat(modwalker); + std::vector q_lit(bits.size(), -1); + std::vector n_lit(bits.size(), -1); + + // Per candidate SAT for its next state, model difference + for (auto &cls : classes) { + for (int idx : cls) { + const EqBit &eb = bits[idx]; + const FfData &ff = ff_for_cell.at(eb.cell); + q_lit[idx] = qcsat.importSigBit(eb.q); + int n = qcsat.importSigBit(ff.sig_d[eb.idx]); + + if (ff.has_aload) { + int al = qcsat.importSigBit(ff.sig_aload); + if (!ff.pol_aload) al = qcsat.ez->NOT(al); + int ad = qcsat.importSigBit(ff.sig_ad[eb.idx]); + n = sat_mux(qcsat, al, ad, n); + } + if (ff.has_arst) { + int ar = qcsat.importSigBit(ff.sig_arst); + if (!ff.pol_arst) ar = qcsat.ez->NOT(ar); + n = sat_mux(qcsat, ar, sat_const(qcsat, ff.val_arst[eb.idx]), n); + } + if (ff.has_sr) { + int clr = qcsat.importSigBit(ff.sig_clr[eb.idx]); + if (!ff.pol_clr) clr = qcsat.ez->NOT(clr); + int set = qcsat.importSigBit(ff.sig_set[eb.idx]); + if (!ff.pol_set) set = qcsat.ez->NOT(set); + n = qcsat.ez->AND(qcsat.ez->NOT(clr), qcsat.ez->OR(set, n)); + } + if (ff.has_srst) { + int srst = qcsat.importSigBit(ff.sig_srst); + if (!ff.pol_srst) srst = qcsat.ez->NOT(srst); + n = sat_mux(qcsat,srst, sat_const(qcsat, ff.val_srst[eb.idx]), n); + } + + n_lit[idx] = n; + } + } + + qcsat.prepare(); + bool any_change = false; + bool changed = true; + + // Bit = class rep, split classes whenever two next states differ + while (changed) { + changed = false; + int joint = qcsat.ez->CONST_TRUE; + + for (auto &cls : classes) { + int rep = cls[0]; + for (int k = 1; k < GetSize(cls); k++) + joint = qcsat.ez->AND(joint, qcsat.ez->IFF(q_lit[rep], q_lit[cls[k]])); + } + + std::vector> new_classes; + new_classes.reserve(classes.size()); + + for (auto &cls : classes) { + std::vector> subs; + for (int b : cls) { + bool placed = false; + + // Identical literal - trivially eq + for (auto &sub : subs) { + if (n_lit[sub[0]] == n_lit[b]) { + sub.push_back(b); + placed = true; + break; + } + } + + if (placed) continue; + + for (auto &sub : subs) { + int rep = sub[0]; + int query = qcsat.ez->NOT(qcsat.ez->IFF(n_lit[rep], n_lit[b])); + if (!qcsat.ez->solve(joint, query)) { + sub.push_back(b); + placed = true; + break; + } + } + + if (!placed) + subs.push_back({b}); + } + + if (GetSize(subs) > 1) + changed = true; + for (auto &sub : subs) + if (GetSize(sub) >= 2) + new_classes.push_back(std::move(sub)); + } + + classes = std::move(new_classes); + if (changed) + any_change = true; + } + + if (classes.empty()) + return any_change; + + dict> remove_bits; + + // Drive every non-rep Q from its class rep, drop merged bits from their FFs + for (auto &cls : classes) { + SigBit rep_q = bits[cls[0]].q; + for (int k = 1; k < GetSize(cls); k++) { + const EqBit &eb = bits[cls[k]]; + initvals.remove_init(eb.q); + module->connect(eb.q, rep_q); + remove_bits[eb.cell].insert(eb.idx); + } + } + + for (auto &kv : remove_bits) { + Cell *cell = kv.first; + const std::set &drop = kv.second; + FfData &ff = ff_for_cell.at(cell); + std::vector keep; + + for (int i = 0; i < ff.width; i++) + if (!drop.count(i)) + keep.push_back(i); + + if (keep.empty()) { + module->remove(cell); + } else { + FfData new_ff = ff.slice(keep); + new_ff.cell = cell; + new_ff.emit(); + } + } + + return true; + } }; struct OptDffPass : public Pass { @@ -946,7 +1226,7 @@ struct OptDffPass : public Pass { log(" -simple-dffe\n"); log(" only enables clock enable recognition transform for obvious cases\n"); log("\n"); - log(" -sat\n"); + log(" -sat AAA\n"); log(" additionally invoke SAT solver to detect and remove flip-flops (with\n"); log(" non-constant inputs) that can also be replaced with a constant driver\n"); log("\n"); @@ -987,6 +1267,8 @@ struct OptDffPass : public Pass { did_something = true; if (worker.run_constbits()) did_something = true; + if (opt.sat && worker.run_eqbits()) + did_something = true; } if (did_something) From d85e3f10de301d1bd594042cf71a3d8f15cf4d8a Mon Sep 17 00:00:00 2001 From: nella Date: Wed, 29 Apr 2026 15:55:45 +0200 Subject: [PATCH 02/33] Add tests. --- passes/opt/opt_dff.cc | 4 +- tests/opt/opt_dff_eqbits.ys | 56 ++++++++ tests/opt/opt_dff_eqbits_large.sv | 231 ++++++++++++++++++++++++++++++ tests/opt/opt_dff_eqbits_small.sv | 30 ++++ 4 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 tests/opt/opt_dff_eqbits.ys create mode 100644 tests/opt/opt_dff_eqbits_large.sv create mode 100644 tests/opt/opt_dff_eqbits_small.sv diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index cc669f902..241ea6888 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -1095,7 +1095,7 @@ struct OptDffWorker if (ff.has_srst) { int srst = qcsat.importSigBit(ff.sig_srst); if (!ff.pol_srst) srst = qcsat.ez->NOT(srst); - n = sat_mux(qcsat,srst, sat_const(qcsat, ff.val_srst[eb.idx]), n); + n = sat_mux(qcsat, srst, sat_const(qcsat, ff.val_srst[eb.idx]), n); } n_lit[idx] = n; @@ -1226,7 +1226,7 @@ struct OptDffPass : public Pass { log(" -simple-dffe\n"); log(" only enables clock enable recognition transform for obvious cases\n"); log("\n"); - log(" -sat AAA\n"); + log(" -sat\n"); log(" additionally invoke SAT solver to detect and remove flip-flops (with\n"); log(" non-constant inputs) that can also be replaced with a constant driver\n"); log("\n"); diff --git a/tests/opt/opt_dff_eqbits.ys b/tests/opt/opt_dff_eqbits.ys new file mode 100644 index 000000000..10e9045e4 --- /dev/null +++ b/tests/opt/opt_dff_eqbits.ys @@ -0,0 +1,56 @@ +# small test case +design -reset +read_verilog -sv opt_dff_eqbits_small.sv +hierarchy -top test_case +techmap +opt_dff -sat +synth +opt_dff -sat +opt_clean -purge + +select -assert-count 2 t:$_SDFF_PN0_ + +# equivalence +design -reset +read_verilog -sv opt_dff_eqbits_small.sv +hierarchy -top test_case +prep +design -save gold + +opt_dff -sat +design -save gate + +design -copy-from gold -as gold test_case +design -copy-from gate -as gate test_case +equiv_make gold gate equiv +equiv_induct equiv +equiv_status -assert + + +# large test case +design -reset +read_verilog -sv opt_dff_eqbits_large.sv +hierarchy -top test_case +techmap +opt_dff -sat +synth +opt_dff -sat +opt_clean -purge + +select -assert-count 6 t:$_SDFFE_PN0P_ + +# equivalence +design -reset +read_verilog -sv opt_dff_eqbits_large.sv +hierarchy -top test_case +prep +design -save gold + +opt_dff -sat +design -save gate + +design -copy-from gold -as gold test_case +design -copy-from gate -as gate test_case +equiv_make gold gate equiv +equiv_induct equiv +equiv_status -assert diff --git a/tests/opt/opt_dff_eqbits_large.sv b/tests/opt/opt_dff_eqbits_large.sv new file mode 100644 index 000000000..4b32c7f8e --- /dev/null +++ b/tests/opt/opt_dff_eqbits_large.sv @@ -0,0 +1,231 @@ +module test_case ( + input wire clk, + input wire rst_n, + input wire [3:0] chan_0_data, + input wire chan_0_vld, + input wire chan_1_rdy, + output wire chan_0_rdy, + output wire [207:0] chan_1_data, + output wire chan_1_vld, + output wire idle +); + wire [12:0] state_init[0:15]; + assign state_init[0] = 13'h0000; + assign state_init[1] = 13'h0000; + assign state_init[2] = 13'h0000; + assign state_init[3] = 13'h0000; + assign state_init[4] = 13'h0000; + assign state_init[5] = 13'h0000; + assign state_init[6] = 13'h0000; + assign state_init[7] = 13'h0000; + assign state_init[8] = 13'h0000; + assign state_init[9] = 13'h0000; + assign state_init[10] = 13'h0000; + assign state_init[11] = 13'h0000; + assign state_init[12] = 13'h0000; + assign state_init[13] = 13'h0000; + assign state_init[14] = 13'h0000; + assign state_init[15] = 13'h0000; + + wire [12:0] ch1_init[0:15]; + assign ch1_init[0] = 13'h0000; + assign ch1_init[1] = 13'h0000; + assign ch1_init[2] = 13'h0000; + assign ch1_init[3] = 13'h0000; + assign ch1_init[4] = 13'h0000; + assign ch1_init[5] = 13'h0000; + assign ch1_init[6] = 13'h0000; + assign ch1_init[7] = 13'h0000; + assign ch1_init[8] = 13'h0000; + assign ch1_init[9] = 13'h0000; + assign ch1_init[10] = 13'h0000; + assign ch1_init[11] = 13'h0000; + assign ch1_init[12] = 13'h0000; + assign ch1_init[13] = 13'h0000; + assign ch1_init[14] = 13'h0000; + assign ch1_init[15] = 13'h0000; + + wire [12:0] mask_1fff[0:15]; + assign mask_1fff[0] = 13'h1fff; + assign mask_1fff[1] = 13'h1fff; + assign mask_1fff[2] = 13'h1fff; + assign mask_1fff[3] = 13'h1fff; + assign mask_1fff[4] = 13'h1fff; + assign mask_1fff[5] = 13'h1fff; + assign mask_1fff[6] = 13'h1fff; + assign mask_1fff[7] = 13'h1fff; + assign mask_1fff[8] = 13'h1fff; + assign mask_1fff[9] = 13'h1fff; + assign mask_1fff[10] = 13'h1fff; + assign mask_1fff[11] = 13'h1fff; + assign mask_1fff[12] = 13'h1fff; + assign mask_1fff[13] = 13'h1fff; + assign mask_1fff[14] = 13'h1fff; + assign mask_1fff[15] = 13'h1fff; + + reg [12:0] state_array[0:15]; + reg [3:0] ch0_in_buf; + reg ch0_in_buf_vld; + reg [12:0] ch1_out_buf[0:15]; + reg ch1_out_buf_vld; + reg stg1_vld; + + wire ch1_not_vld; + wire [3:0] ch0_sel_data; + wire ch0_is_vld; + wire ch1_vld_we; + wire ch1_data_we; + wire stg0_vld_out; + wire ch0_buf_ready; + wire ch0_pipe_stall; + wire [1:0] sel_concat; + wire ch0_buf_data_we; + wire ch0_buf_vld_rst; + wire stg0_idle; + wire stg1_idle; + wire ch0_is_inactive; + wire ch1_is_inactive; + wire [12:0] next_state_val[0:15]; + wire state_we; + wire ch0_buf_vld_we; + wire stg1_vld_we; + wire pipe_idle; + + assign ch1_not_vld = ~ch1_out_buf_vld; + assign ch0_sel_data = ch0_in_buf_vld ? ch0_in_buf : chan_0_data; + assign ch0_is_vld = chan_0_vld | ch0_in_buf_vld; + assign ch1_vld_we = chan_1_rdy | ch1_not_vld; + assign ch1_data_we = ch0_is_vld & ch1_vld_we; + assign stg0_vld_out = ch0_is_vld & ch1_data_we; + assign ch0_buf_ready = ~ch0_in_buf_vld; + assign ch0_pipe_stall = ~stg0_vld_out; + assign sel_concat = {ch0_is_vld & ch0_sel_data[0], ch0_is_vld & ~ch0_sel_data[0]}; + assign ch0_buf_data_we = chan_0_vld & ch0_buf_ready & ch0_pipe_stall; + assign ch0_buf_vld_rst = ch0_in_buf_vld & stg0_vld_out; + assign stg0_idle = ~ch0_is_vld; + assign stg1_idle = ~stg1_vld; + assign ch0_is_inactive = ~(chan_0_vld & ch0_buf_ready); + assign ch1_is_inactive = ~(ch1_out_buf_vld & chan_1_rdy); + + assign next_state_val[0] = state_array[0] & {13{sel_concat[0]}} | mask_1fff[0] & {13{sel_concat[1]}}; + assign next_state_val[1] = state_array[1] & {13{sel_concat[0]}} | mask_1fff[1] & {13{sel_concat[1]}}; + assign next_state_val[2] = state_array[2] & {13{sel_concat[0]}} | mask_1fff[2] & {13{sel_concat[1]}}; + assign next_state_val[3] = state_array[3] & {13{sel_concat[0]}} | mask_1fff[3] & {13{sel_concat[1]}}; + assign next_state_val[4] = state_array[4] & {13{sel_concat[0]}} | mask_1fff[4] & {13{sel_concat[1]}}; + assign next_state_val[5] = state_array[5] & {13{sel_concat[0]}} | mask_1fff[5] & {13{sel_concat[1]}}; + assign next_state_val[6] = state_array[6] & {13{sel_concat[0]}} | mask_1fff[6] & {13{sel_concat[1]}}; + assign next_state_val[7] = state_array[7] & {13{sel_concat[0]}} | mask_1fff[7] & {13{sel_concat[1]}}; + assign next_state_val[8] = state_array[8] & {13{sel_concat[0]}} | mask_1fff[8] & {13{sel_concat[1]}}; + assign next_state_val[9] = state_array[9] & {13{sel_concat[0]}} | mask_1fff[9] & {13{sel_concat[1]}}; + assign next_state_val[10] = state_array[10] & {13{sel_concat[0]}} | mask_1fff[10] & {13{sel_concat[1]}}; + assign next_state_val[11] = state_array[11] & {13{sel_concat[0]}} | mask_1fff[11] & {13{sel_concat[1]}}; + assign next_state_val[12] = state_array[12] & {13{sel_concat[0]}} | mask_1fff[12] & {13{sel_concat[1]}}; + assign next_state_val[13] = state_array[13] & {13{sel_concat[0]}} | mask_1fff[13] & {13{sel_concat[1]}}; + assign next_state_val[14] = state_array[14] & {13{sel_concat[0]}} | mask_1fff[14] & {13{sel_concat[1]}}; + assign next_state_val[15] = state_array[15] & {13{sel_concat[0]}} | mask_1fff[15] & {13{sel_concat[1]}}; + + assign state_we = stg0_vld_out & ch0_sel_data[0] | stg0_vld_out & ~ch0_sel_data[0]; + assign ch0_buf_vld_we = ch0_buf_data_we | ch0_buf_vld_rst; + assign stg1_vld_we = stg0_vld_out | stg1_vld; + assign pipe_idle = stg0_idle & stg1_idle & ch0_is_inactive & ch1_is_inactive; + + always @(posedge clk) begin + if (!rst_n) begin + state_array[0] <= state_init[0]; + state_array[1] <= state_init[1]; + state_array[2] <= state_init[2]; + state_array[3] <= state_init[3]; + state_array[4] <= state_init[4]; + state_array[5] <= state_init[5]; + state_array[6] <= state_init[6]; + state_array[7] <= state_init[7]; + state_array[8] <= state_init[8]; + state_array[9] <= state_init[9]; + state_array[10] <= state_init[10]; + state_array[11] <= state_init[11]; + state_array[12] <= state_init[12]; + state_array[13] <= state_init[13]; + state_array[14] <= state_init[14]; + state_array[15] <= state_init[15]; + ch0_in_buf <= 4'h0; + ch0_in_buf_vld <= 1'h0; + ch1_out_buf[0] <= ch1_init[0]; + ch1_out_buf[1] <= ch1_init[1]; + ch1_out_buf[2] <= ch1_init[2]; + ch1_out_buf[3] <= ch1_init[3]; + ch1_out_buf[4] <= ch1_init[4]; + ch1_out_buf[5] <= ch1_init[5]; + ch1_out_buf[6] <= ch1_init[6]; + ch1_out_buf[7] <= ch1_init[7]; + ch1_out_buf[8] <= ch1_init[8]; + ch1_out_buf[9] <= ch1_init[9]; + ch1_out_buf[10] <= ch1_init[10]; + ch1_out_buf[11] <= ch1_init[11]; + ch1_out_buf[12] <= ch1_init[12]; + ch1_out_buf[13] <= ch1_init[13]; + ch1_out_buf[14] <= ch1_init[14]; + ch1_out_buf[15] <= ch1_init[15]; + ch1_out_buf_vld <= 1'h0; + stg1_vld <= 1'h0; + end else begin + state_array[0] <= state_we ? next_state_val[0] : state_array[0]; + state_array[1] <= state_we ? next_state_val[1] : state_array[1]; + state_array[2] <= state_we ? next_state_val[2] : state_array[2]; + state_array[3] <= state_we ? next_state_val[3] : state_array[3]; + state_array[4] <= state_we ? next_state_val[4] : state_array[4]; + state_array[5] <= state_we ? next_state_val[5] : state_array[5]; + state_array[6] <= state_we ? next_state_val[6] : state_array[6]; + state_array[7] <= state_we ? next_state_val[7] : state_array[7]; + state_array[8] <= state_we ? next_state_val[8] : state_array[8]; + state_array[9] <= state_we ? next_state_val[9] : state_array[9]; + state_array[10] <= state_we ? next_state_val[10] : state_array[10]; + state_array[11] <= state_we ? next_state_val[11] : state_array[11]; + state_array[12] <= state_we ? next_state_val[12] : state_array[12]; + state_array[13] <= state_we ? next_state_val[13] : state_array[13]; + state_array[14] <= state_we ? next_state_val[14] : state_array[14]; + state_array[15] <= state_we ? next_state_val[15] : state_array[15]; + ch0_in_buf <= ch0_buf_data_we ? chan_0_data : ch0_in_buf; + ch0_in_buf_vld <= ch0_buf_vld_we ? ch0_buf_ready : ch0_in_buf_vld; + ch1_out_buf[0] <= ch1_data_we ? state_array[0] : ch1_out_buf[0]; + ch1_out_buf[1] <= ch1_data_we ? state_array[1] : ch1_out_buf[1]; + ch1_out_buf[2] <= ch1_data_we ? state_array[2] : ch1_out_buf[2]; + ch1_out_buf[3] <= ch1_data_we ? state_array[3] : ch1_out_buf[3]; + ch1_out_buf[4] <= ch1_data_we ? state_array[4] : ch1_out_buf[4]; + ch1_out_buf[5] <= ch1_data_we ? state_array[5] : ch1_out_buf[5]; + ch1_out_buf[6] <= ch1_data_we ? state_array[6] : ch1_out_buf[6]; + ch1_out_buf[7] <= ch1_data_we ? state_array[7] : ch1_out_buf[7]; + ch1_out_buf[8] <= ch1_data_we ? state_array[8] : ch1_out_buf[8]; + ch1_out_buf[9] <= ch1_data_we ? state_array[9] : ch1_out_buf[9]; + ch1_out_buf[10] <= ch1_data_we ? state_array[10] : ch1_out_buf[10]; + ch1_out_buf[11] <= ch1_data_we ? state_array[11] : ch1_out_buf[11]; + ch1_out_buf[12] <= ch1_data_we ? state_array[12] : ch1_out_buf[12]; + ch1_out_buf[13] <= ch1_data_we ? state_array[13] : ch1_out_buf[13]; + ch1_out_buf[14] <= ch1_data_we ? state_array[14] : ch1_out_buf[14]; + ch1_out_buf[15] <= ch1_data_we ? state_array[15] : ch1_out_buf[15]; + ch1_out_buf_vld <= ch1_vld_we ? ch0_is_vld : ch1_out_buf_vld; + stg1_vld <= stg1_vld_we ? stg0_vld_out : stg1_vld; + end + end + + assign chan_0_rdy = ch0_buf_ready; + assign chan_1_data = { + ch1_out_buf[15], + ch1_out_buf[14], + ch1_out_buf[13], + ch1_out_buf[12], + ch1_out_buf[11], + ch1_out_buf[10], + ch1_out_buf[9], + ch1_out_buf[8], + ch1_out_buf[7], + ch1_out_buf[6], + ch1_out_buf[5], + ch1_out_buf[4], + ch1_out_buf[3], + ch1_out_buf[2], + ch1_out_buf[1], + ch1_out_buf[0] + }; + assign chan_1_vld = ch1_out_buf_vld; + assign idle = pipe_idle; +endmodule diff --git a/tests/opt/opt_dff_eqbits_small.sv b/tests/opt/opt_dff_eqbits_small.sv new file mode 100644 index 000000000..7c6aeba7f --- /dev/null +++ b/tests/opt/opt_dff_eqbits_small.sv @@ -0,0 +1,30 @@ +module test_case ( + input wire clk, + input wire rst_n, + input wire in_val, + output wire out_a, + output wire out_b, + output wire out_c, + output wire out_d +); + reg a, b, c, d; + + always @(posedge clk) begin + if (!rst_n) begin + a <= 1'b0; + b <= 1'b0; + c <= 1'b0; + d <= 1'b0; + end else begin + a <= c & in_val; + b <= d & in_val; + c <= b | in_val; + d <= a | in_val; + end + end + + assign out_a = a; + assign out_b = b; + assign out_c = c; + assign out_d = d; +endmodule From c6bf13bb94ed8b3f5f7eee14eb67823f497f0e4a Mon Sep 17 00:00:00 2001 From: nella Date: Wed, 13 May 2026 10:49:12 +0200 Subject: [PATCH 03/33] Implement worklist and SAT counterexample splitting. --- passes/opt/opt_dff.cc | 113 ++++++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 48 deletions(-) diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index 241ea6888..ef5c56896 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -1104,62 +1104,76 @@ struct OptDffWorker qcsat.prepare(); bool any_change = false; - bool changed = true; + std::vector worklist; + std::vector in_worklist(GetSize(classes), true); - // Bit = class rep, split classes whenever two next states differ - while (changed) { - changed = false; - int joint = qcsat.ez->CONST_TRUE; + for (int i = 0; i < GetSize(classes); i++) { + worklist.push_back(i); + } - for (auto &cls : classes) { - int rep = cls[0]; - for (int k = 1; k < GetSize(cls); k++) - joint = qcsat.ez->AND(joint, qcsat.ez->IFF(q_lit[rep], q_lit[cls[k]])); + while (!worklist.empty()) { + int cls_idx = worklist.back(); + worklist.pop_back(); + in_worklist[cls_idx] = false; + + auto &cls = classes[cls_idx]; + if (GetSize(cls) < 2) continue; + + std::vector assumptions; + for (auto &c : classes) { + if (GetSize(c) < 2) continue; + int rep = c[0]; + for (int k = 1; k < GetSize(c); k++) { + assumptions.push_back(qcsat.ez->IFF(q_lit[rep], q_lit[c[k]])); + } } - std::vector> new_classes; - new_classes.reserve(classes.size()); + // Split at counterexamples + int rep = cls[0]; + for (int i = 1; i < GetSize(cls); i++) { + // Trivially eqivalent + if (n_lit[rep] == n_lit[cls[i]]) + continue; + + int query = qcsat.ez->NOT(qcsat.ez->IFF(n_lit[rep], n_lit[cls[i]])); + std::vector modelExprs; - for (auto &cls : classes) { - std::vector> subs; for (int b : cls) { - bool placed = false; - - // Identical literal - trivially eq - for (auto &sub : subs) { - if (n_lit[sub[0]] == n_lit[b]) { - sub.push_back(b); - placed = true; - break; - } - } - - if (placed) continue; - - for (auto &sub : subs) { - int rep = sub[0]; - int query = qcsat.ez->NOT(qcsat.ez->IFF(n_lit[rep], n_lit[b])); - if (!qcsat.ez->solve(joint, query)) { - sub.push_back(b); - placed = true; - break; - } - } - - if (!placed) - subs.push_back({b}); + modelExprs.push_back(n_lit[b]); } - if (GetSize(subs) > 1) - changed = true; - for (auto &sub : subs) - if (GetSize(sub) >= 2) - new_classes.push_back(std::move(sub)); - } + std::vector modelVals; + assumptions.push_back(query); + + if (qcsat.ez->solve(modelExprs, modelVals, assumptions)) { + // SAT -> partition entire class + std::vector sub0; + std::vector sub1; - classes = std::move(new_classes); - if (changed) - any_change = true; + for (size_t b_idx = 0; b_idx < cls.size(); b_idx++) { + if (modelVals[b_idx]) + sub1.push_back(cls[b_idx]); + else + sub0.push_back(cls[b_idx]); + } + + classes[cls_idx] = std::move(sub0); + classes.push_back(std::move(sub1)); + in_worklist.push_back(false); + + // Partition was split -> the induction hypo weakened + for (int j = 0; j < GetSize(classes); j++) { + if (GetSize(classes[j]) >= 2 && !in_worklist[j]) { + worklist.push_back(j); + in_worklist[j] = true; + } + } + + break; // Process new splits + } + + assumptions.pop_back(); // Remove query for the next pairwise check if UNSAT + } } if (classes.empty()) @@ -1169,7 +1183,10 @@ struct OptDffWorker // Drive every non-rep Q from its class rep, drop merged bits from their FFs for (auto &cls : classes) { + if (GetSize(cls) < 2) + continue; SigBit rep_q = bits[cls[0]].q; + any_change = true; for (int k = 1; k < GetSize(cls); k++) { const EqBit &eb = bits[cls[k]]; initvals.remove_init(eb.q); @@ -1197,7 +1214,7 @@ struct OptDffWorker } } - return true; + return any_change; } }; From bbec8d2902d3bdf65fb90a99881a94c8dfed520b Mon Sep 17 00:00:00 2001 From: nella Date: Wed, 20 May 2026 15:51:04 +0200 Subject: [PATCH 04/33] Gate behind flag. --- passes/opt/opt_dff.cc | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index ef5c56896..e657a8a2d 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -41,6 +41,7 @@ struct OptDffOptions bool simple_dffe; bool sat; bool keepdc; + bool eqbits; }; struct OptDffWorker @@ -977,6 +978,10 @@ struct OptDffWorker bool run_eqbits() { + if(!opt.eqbits) { + return false; + } + std::vector bits; std::vector keys; dict ff_for_cell; @@ -1253,6 +1258,11 @@ struct OptDffPass : public Pass { log(" all result bits to be set to x. this behavior changes when 'a+0' is\n"); log(" replaced by 'a'. the -keepdc option disables all such optimizations.\n"); log("\n"); + log(" -eqbits\n"); + log(" finds groups of flip flop bits provably holding always-equal values\n"); + log(" across cycles and collapses each group to a single bit, potentially\n"); + log(" reducing the number of required flip flops.\n"); + log("\n"); } void execute(std::vector args, RTLIL::Design *design) override @@ -1265,6 +1275,7 @@ struct OptDffPass : public Pass { opt.simple_dffe = false; opt.keepdc = false; opt.sat = false; + opt.eqbits = false; size_t argidx; for (argidx = 1; argidx < args.size(); argidx++) { @@ -1273,6 +1284,7 @@ struct OptDffPass : public Pass { if (args[argidx] == "-simple-dffe") { opt.simple_dffe = true; continue; } if (args[argidx] == "-keepdc") { opt.keepdc = true; continue; } if (args[argidx] == "-sat") { opt.sat = true; continue; } + if (args[argidx] == "-eqbits") { opt.eqbits = true; continue; } break; } extra_args(args, argidx, design); @@ -1284,7 +1296,7 @@ struct OptDffPass : public Pass { did_something = true; if (worker.run_constbits()) did_something = true; - if (opt.sat && worker.run_eqbits()) + if (worker.run_eqbits()) did_something = true; } From 04a1611346afbd7ffbed8b4d625c674694a1f972 Mon Sep 17 00:00:00 2001 From: nella Date: Wed, 20 May 2026 15:58:27 +0200 Subject: [PATCH 05/33] Tests. --- tests/opt/opt_dff_eqbits.ys | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/opt/opt_dff_eqbits.ys b/tests/opt/opt_dff_eqbits.ys index 10e9045e4..181b5d0a7 100644 --- a/tests/opt/opt_dff_eqbits.ys +++ b/tests/opt/opt_dff_eqbits.ys @@ -3,9 +3,9 @@ design -reset read_verilog -sv opt_dff_eqbits_small.sv hierarchy -top test_case techmap -opt_dff -sat +opt_dff -sat -eqbits synth -opt_dff -sat +opt_dff -sat -eqbits opt_clean -purge select -assert-count 2 t:$_SDFF_PN0_ @@ -17,7 +17,7 @@ hierarchy -top test_case prep design -save gold -opt_dff -sat +opt_dff -sat -eqbits design -save gate design -copy-from gold -as gold test_case @@ -32,9 +32,9 @@ design -reset read_verilog -sv opt_dff_eqbits_large.sv hierarchy -top test_case techmap -opt_dff -sat +opt_dff -sat -eqbits synth -opt_dff -sat +opt_dff -sat -eqbits opt_clean -purge select -assert-count 6 t:$_SDFFE_PN0P_ @@ -46,7 +46,7 @@ hierarchy -top test_case prep design -save gold -opt_dff -sat +opt_dff -sat -eqbits design -save gate design -copy-from gold -as gold test_case From 386e63ae20465e401d749ffe160098e7be97f799 Mon Sep 17 00:00:00 2001 From: nella Date: Mon, 25 May 2026 12:49:29 +0200 Subject: [PATCH 06/33] Add prepass for bit simulation. --- kernel/bitsim.h | 79 +++++++++++++++++++++++++++++++++++++++++++ passes/opt/opt_dff.cc | 64 ++++++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 kernel/bitsim.h diff --git a/kernel/bitsim.h b/kernel/bitsim.h new file mode 100644 index 000000000..a0915e28b --- /dev/null +++ b/kernel/bitsim.h @@ -0,0 +1,79 @@ +#ifndef BITSIM_H +#define BITSIM_H + +#include "kernel/modtools.h" + +YOSYS_NAMESPACE_BEGIN + +struct BitSim { + Module *module; + SigMap &sigmap; + ModWalker &modwalker; + dict sim_vals; + uint64_t rng_state; + + BitSim(Module *m, SigMap &sm, ModWalker &mw) + : module(m), sigmap(sm), modwalker(mw), rng_state(1337) {} + + uint64_t xorshift64() { + rng_state ^= rng_state << 13; + rng_state ^= rng_state >> 7; + rng_state ^= rng_state << 17; + return rng_state; + } + + uint64_t eval_bit(SigBit b) { + SigBit mapped = sigmap(b); + if (mapped == State::S0) return 0ULL; + if (mapped == State::S1) return ~0ULL; + if (mapped == State::Sx || mapped == State::Sz) return 0ULL; + + auto it = sim_vals.find(mapped); + if (it != sim_vals.end()) return it->second; + sim_vals[mapped] = 0; + uint64_t res = 0; + + if (!modwalker.has_drivers(mapped)) { + res = xorshift64(); + } else { + auto &drivers = modwalker.signal_drivers[mapped]; + if (drivers.empty()) { + res = xorshift64(); + } else { + auto driver = *drivers.begin(); + Cell *cell = driver.cell; + + if (cell->is_builtin_ff()) { + res = xorshift64(); + } else if (cell->type == ID($_AND_)) { + res = eval_bit(cell->getPort(ID::A)[0]) & eval_bit(cell->getPort(ID::B)[0]); + } else if (cell->type == ID($_OR_)) { + res = eval_bit(cell->getPort(ID::A)[0]) | eval_bit(cell->getPort(ID::B)[0]); + } else if (cell->type == ID($_XOR_)) { + res = eval_bit(cell->getPort(ID::A)[0]) ^ eval_bit(cell->getPort(ID::B)[0]); + } else if (cell->type == ID($_NOT_)) { + res = ~eval_bit(cell->getPort(ID::A)[0]); + } else if (cell->type == ID($_MUX_)) { + uint64_t s = eval_bit(cell->getPort(ID::S)[0]); + uint64_t a = eval_bit(cell->getPort(ID::A)[0]); + uint64_t b = eval_bit(cell->getPort(ID::B)[0]); + res = (a & ~s) | (b & s); + } else if (cell->type == ID($mux)) { + uint64_t s = eval_bit(cell->getPort(ID::S)[0]); + uint64_t a = eval_bit(cell->getPort(ID::A)[driver.offset]); + uint64_t b = eval_bit(cell->getPort(ID::B)[driver.offset]); + res = (a & ~s) | (b & s); + } else { + res = xorshift64(); + } + } + } + + sim_vals[mapped] = res; + return res; + } +}; + +YOSYS_NAMESPACE_END + +#endif diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index e657a8a2d..dbe0e521a 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -26,6 +26,7 @@ #include "kernel/sigtools.h" #include "kernel/ffinit.h" #include "kernel/ff.h" +#include "kernel/bitsim.h" #include "kernel/pattern.h" #include "passes/techmap/simplemap.h" #include @@ -1067,6 +1068,67 @@ struct OptDffWorker return false; ModWalker modwalker(module->design, module); + BitSim sim(module, sigmap, modwalker); + + // Simulation prepass + // Assume same class + for (auto &cls : classes) { + uint64_t class_q_val = sim.xorshift64(); + for (int idx : cls) { + sim.sim_vals[sigmap(bits[idx].q)] = class_q_val; + } + } + + std::vector> refined_classes; + + for (auto &cls : classes) { + dict> sim_buckets; + for (int idx : cls) { + const EqBit &eb = bits[idx]; + const FfData &ff = ff_for_cell.at(eb.cell); + + uint64_t n_val = sim.eval_bit(ff.sig_d[eb.idx]); + + if (ff.has_aload) { + uint64_t al = sim.eval_bit(ff.sig_aload); + if (!ff.pol_aload) al = ~al; + uint64_t ad = sim.eval_bit(ff.sig_ad[eb.idx]); + n_val = (n_val & ~al) | (ad & al); + } + if (ff.has_arst) { + uint64_t ar = sim.eval_bit(ff.sig_arst); + if (!ff.pol_arst) ar = ~ar; + uint64_t ar_val = (ff.val_arst[eb.idx] == State::S1) ? ~0ULL : 0ULL; + n_val = (n_val & ~ar) | (ar_val & ar); + } + if (ff.has_sr) { + uint64_t clr = sim.eval_bit(ff.sig_clr[eb.idx]); + if (!ff.pol_clr) clr = ~clr; + uint64_t set = sim.eval_bit(ff.sig_set[eb.idx]); + if (!ff.pol_set) set = ~set; + n_val = ~clr & (set | n_val); + } + if (ff.has_srst) { + uint64_t srst = sim.eval_bit(ff.sig_srst); + if (!ff.pol_srst) srst = ~srst; + uint64_t srst_val = (ff.val_srst[eb.idx] == State::S1) ? ~0ULL : 0ULL; + n_val = (n_val & ~srst) | (srst_val & srst); + } + + sim_buckets[n_val].push_back(idx); + } + + for (auto &kv : sim_buckets) { + if (GetSize(kv.second) >= 2) { + refined_classes.push_back(std::move(kv.second)); + } + } + } + + classes = std::move(refined_classes); + if (classes.empty()) + return false; + QuickConeSat qcsat(modwalker); std::vector q_lit(bits.size(), -1); std::vector n_lit(bits.size(), -1); @@ -1136,7 +1198,7 @@ struct OptDffWorker // Split at counterexamples int rep = cls[0]; for (int i = 1; i < GetSize(cls); i++) { - // Trivially eqivalent + // Trivially equivalent if (n_lit[rep] == n_lit[cls[i]]) continue; From 68df0be7d2ef7b3a0dd54abe469b81b9494a48e1 Mon Sep 17 00:00:00 2001 From: nella Date: Mon, 25 May 2026 14:16:55 +0200 Subject: [PATCH 07/33] Remove eqbits flag. --- passes/opt/opt_dff.cc | 106 ++++++++++++++++++++---------------- tests/opt/opt_dff_eqbits.ys | 12 ++-- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index dbe0e521a..500df142e 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -42,7 +42,6 @@ struct OptDffOptions bool simple_dffe; bool sat; bool keepdc; - bool eqbits; }; struct OptDffWorker @@ -977,15 +976,9 @@ struct OptDffWorker return v == State::S1 ? qcsat.ez->CONST_TRUE : qcsat.ez->CONST_FALSE; } - bool run_eqbits() + std::vector> gather_initial_eq_classes(std::vector &bits, dict &ff_for_cell) { - if(!opt.eqbits) { - return false; - } - - std::vector bits; std::vector keys; - dict ff_for_cell; // Collect FF bits eligible for merging for (auto cell : module->selected_cells()) { @@ -1050,27 +1043,26 @@ struct OptDffWorker } } - if (GetSize(bits) < 2) - return false; - - // Group bits by control signature dict> buckets; for (int i = 0; i < GetSize(bits); i++) buckets[keys[i]].push_back(i); std::vector> classes; - classes.reserve(GetSize(buckets)); for (auto &kv : buckets) if (GetSize(kv.second) >= 2) classes.push_back(std::move(kv.second)); - if (classes.empty()) - return false; + return classes; + } - ModWalker modwalker(module->design, module); + std::vector> filter_classes_sim( + const std::vector> &classes, + const std::vector &bits, + const dict &ff_for_cell, + ModWalker &modwalker + ) { BitSim sim(module, sigmap, modwalker); - - // Simulation prepass + // Assume same class for (auto &cls : classes) { uint64_t class_q_val = sim.xorshift64(); @@ -1080,15 +1072,13 @@ struct OptDffWorker } std::vector> refined_classes; - for (auto &cls : classes) { dict> sim_buckets; for (int idx : cls) { const EqBit &eb = bits[idx]; const FfData &ff = ff_for_cell.at(eb.cell); - uint64_t n_val = sim.eval_bit(ff.sig_d[eb.idx]); - + if (ff.has_aload) { uint64_t al = sim.eval_bit(ff.sig_aload); if (!ff.pol_aload) al = ~al; @@ -1118,17 +1108,20 @@ struct OptDffWorker sim_buckets[n_val].push_back(idx); } - for (auto &kv : sim_buckets) { - if (GetSize(kv.second) >= 2) { + for (auto &kv : sim_buckets) + if (GetSize(kv.second) >= 2) refined_classes.push_back(std::move(kv.second)); - } - } } - classes = std::move(refined_classes); - if (classes.empty()) - return false; + return refined_classes; + } + std::vector> filter_classes_sat( + std::vector> classes, + const std::vector &bits, + const dict &ff_for_cell, + ModWalker &modwalker + ) { QuickConeSat qcsat(modwalker); std::vector q_lit(bits.size(), -1); std::vector n_lit(bits.size(), -1); @@ -1144,8 +1137,7 @@ struct OptDffWorker if (ff.has_aload) { int al = qcsat.importSigBit(ff.sig_aload); if (!ff.pol_aload) al = qcsat.ez->NOT(al); - int ad = qcsat.importSigBit(ff.sig_ad[eb.idx]); - n = sat_mux(qcsat, al, ad, n); + n = sat_mux(qcsat, al, qcsat.importSigBit(ff.sig_ad[eb.idx]), n); } if (ff.has_arst) { int ar = qcsat.importSigBit(ff.sig_arst); @@ -1170,13 +1162,11 @@ struct OptDffWorker } qcsat.prepare(); - bool any_change = false; std::vector worklist; std::vector in_worklist(GetSize(classes), true); - for (int i = 0; i < GetSize(classes); i++) { + for (int i = 0; i < GetSize(classes); i++) worklist.push_back(i); - } while (!worklist.empty()) { int cls_idx = worklist.back(); @@ -1190,9 +1180,8 @@ struct OptDffWorker for (auto &c : classes) { if (GetSize(c) < 2) continue; int rep = c[0]; - for (int k = 1; k < GetSize(c); k++) { + for (int k = 1; k < GetSize(c); k++) assumptions.push_back(qcsat.ez->IFF(q_lit[rep], q_lit[c[k]])); - } } // Split at counterexamples @@ -1204,14 +1193,12 @@ struct OptDffWorker int query = qcsat.ez->NOT(qcsat.ez->IFF(n_lit[rep], n_lit[cls[i]])); std::vector modelExprs; - - for (int b : cls) { + for (int b : cls) modelExprs.push_back(n_lit[b]); - } std::vector modelVals; assumptions.push_back(query); - + if (qcsat.ez->solve(modelExprs, modelVals, assumptions)) { // SAT -> partition entire class std::vector sub0; @@ -1243,9 +1230,12 @@ struct OptDffWorker } } - if (classes.empty()) - return any_change; + return classes; + } + bool apply_eq_merges(const std::vector> &classes, const std::vector &bits, dict &ff_for_cell) + { + bool any_change = false; dict> remove_bits; // Drive every non-rep Q from its class rep, drop merged bits from their FFs @@ -1283,6 +1273,33 @@ struct OptDffWorker return any_change; } + + bool run_eqbits() + { + if (!opt.sat) + return false; + + std::vector bits; + dict ff_for_cell; + + std::vector> classes = gather_initial_eq_classes(bits, ff_for_cell); + if (classes.empty()) + return false; + + ModWalker modwalker(module->design, module); + + // Simulation prepass + classes = filter_classes_sim(classes, bits, ff_for_cell, modwalker); + if (classes.empty()) + return false; + + // SAT prove + classes = filter_classes_sat(std::move(classes), bits, ff_for_cell, modwalker); + if (classes.empty()) + return false; + + return apply_eq_merges(classes, bits, ff_for_cell); + } }; struct OptDffPass : public Pass { @@ -1320,11 +1337,6 @@ struct OptDffPass : public Pass { log(" all result bits to be set to x. this behavior changes when 'a+0' is\n"); log(" replaced by 'a'. the -keepdc option disables all such optimizations.\n"); log("\n"); - log(" -eqbits\n"); - log(" finds groups of flip flop bits provably holding always-equal values\n"); - log(" across cycles and collapses each group to a single bit, potentially\n"); - log(" reducing the number of required flip flops.\n"); - log("\n"); } void execute(std::vector args, RTLIL::Design *design) override @@ -1337,7 +1349,6 @@ struct OptDffPass : public Pass { opt.simple_dffe = false; opt.keepdc = false; opt.sat = false; - opt.eqbits = false; size_t argidx; for (argidx = 1; argidx < args.size(); argidx++) { @@ -1346,7 +1357,6 @@ struct OptDffPass : public Pass { if (args[argidx] == "-simple-dffe") { opt.simple_dffe = true; continue; } if (args[argidx] == "-keepdc") { opt.keepdc = true; continue; } if (args[argidx] == "-sat") { opt.sat = true; continue; } - if (args[argidx] == "-eqbits") { opt.eqbits = true; continue; } break; } extra_args(args, argidx, design); diff --git a/tests/opt/opt_dff_eqbits.ys b/tests/opt/opt_dff_eqbits.ys index 181b5d0a7..10e9045e4 100644 --- a/tests/opt/opt_dff_eqbits.ys +++ b/tests/opt/opt_dff_eqbits.ys @@ -3,9 +3,9 @@ design -reset read_verilog -sv opt_dff_eqbits_small.sv hierarchy -top test_case techmap -opt_dff -sat -eqbits +opt_dff -sat synth -opt_dff -sat -eqbits +opt_dff -sat opt_clean -purge select -assert-count 2 t:$_SDFF_PN0_ @@ -17,7 +17,7 @@ hierarchy -top test_case prep design -save gold -opt_dff -sat -eqbits +opt_dff -sat design -save gate design -copy-from gold -as gold test_case @@ -32,9 +32,9 @@ design -reset read_verilog -sv opt_dff_eqbits_large.sv hierarchy -top test_case techmap -opt_dff -sat -eqbits +opt_dff -sat synth -opt_dff -sat -eqbits +opt_dff -sat opt_clean -purge select -assert-count 6 t:$_SDFFE_PN0P_ @@ -46,7 +46,7 @@ hierarchy -top test_case prep design -save gold -opt_dff -sat -eqbits +opt_dff -sat design -save gate design -copy-from gold -as gold test_case From 25810193ab72e80041a7a6728e3e3685e7c5610b Mon Sep 17 00:00:00 2001 From: nella Date: Thu, 18 Jun 2026 10:57:20 +0200 Subject: [PATCH 08/33] Reuse sat/hashlib. --- kernel/bitsim.h | 16 ++++++++-------- passes/opt/opt_dff.cc | 19 ++++++------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/kernel/bitsim.h b/kernel/bitsim.h index a0915e28b..c659a5e34 100644 --- a/kernel/bitsim.h +++ b/kernel/bitsim.h @@ -15,10 +15,10 @@ struct BitSim { BitSim(Module *m, SigMap &sm, ModWalker &mw) : module(m), sigmap(sm), modwalker(mw), rng_state(1337) {} - uint64_t xorshift64() { - rng_state ^= rng_state << 13; - rng_state ^= rng_state >> 7; - rng_state ^= rng_state << 17; + uint64_t next_rand() { + uint32_t lo = mkhash_xorshift((uint32_t)rng_state); + uint32_t hi = mkhash_xorshift((uint32_t)(rng_state >> 32) ^ lo); + rng_state = ((uint64_t)hi << 32) | lo; return rng_state; } @@ -34,17 +34,17 @@ struct BitSim { uint64_t res = 0; if (!modwalker.has_drivers(mapped)) { - res = xorshift64(); + res = next_rand(); } else { auto &drivers = modwalker.signal_drivers[mapped]; if (drivers.empty()) { - res = xorshift64(); + res = next_rand(); } else { auto driver = *drivers.begin(); Cell *cell = driver.cell; if (cell->is_builtin_ff()) { - res = xorshift64(); + res = next_rand(); } else if (cell->type == ID($_AND_)) { res = eval_bit(cell->getPort(ID::A)[0]) & eval_bit(cell->getPort(ID::B)[0]); } else if (cell->type == ID($_OR_)) { @@ -64,7 +64,7 @@ struct BitSim { uint64_t b = eval_bit(cell->getPort(ID::B)[driver.offset]); res = (a & ~s) | (b & s); } else { - res = xorshift64(); + res = next_rand(); } } } diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index 500df142e..6984f3f4b 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -968,14 +968,6 @@ struct OptDffWorker return s == State::S0 || s == State::S1; } - int sat_mux(QuickConeSat &qcsat, int s, int a, int b) { - return qcsat.ez->OR(qcsat.ez->AND(s, a), qcsat.ez->AND(qcsat.ez->NOT(s), b)); - } - - int sat_const(QuickConeSat &qcsat, State v) { - return v == State::S1 ? qcsat.ez->CONST_TRUE : qcsat.ez->CONST_FALSE; - } - std::vector> gather_initial_eq_classes(std::vector &bits, dict &ff_for_cell) { std::vector keys; @@ -1065,7 +1057,7 @@ struct OptDffWorker // Assume same class for (auto &cls : classes) { - uint64_t class_q_val = sim.xorshift64(); + uint64_t class_q_val = sim.next_rand(); for (int idx : cls) { sim.sim_vals[sigmap(bits[idx].q)] = class_q_val; } @@ -1137,12 +1129,12 @@ struct OptDffWorker if (ff.has_aload) { int al = qcsat.importSigBit(ff.sig_aload); if (!ff.pol_aload) al = qcsat.ez->NOT(al); - n = sat_mux(qcsat, al, qcsat.importSigBit(ff.sig_ad[eb.idx]), n); + n = qcsat.ez->ITE(al, qcsat.importSigBit(ff.sig_ad[eb.idx]), n); } if (ff.has_arst) { int ar = qcsat.importSigBit(ff.sig_arst); if (!ff.pol_arst) ar = qcsat.ez->NOT(ar); - n = sat_mux(qcsat, ar, sat_const(qcsat, ff.val_arst[eb.idx]), n); + n = qcsat.ez->ITE(ar, qcsat.ez->value(ff.val_arst[eb.idx] == State::S1), n); } if (ff.has_sr) { int clr = qcsat.importSigBit(ff.sig_clr[eb.idx]); @@ -1154,7 +1146,7 @@ struct OptDffWorker if (ff.has_srst) { int srst = qcsat.importSigBit(ff.sig_srst); if (!ff.pol_srst) srst = qcsat.ez->NOT(srst); - n = sat_mux(qcsat, srst, sat_const(qcsat, ff.val_srst[eb.idx]), n); + n = qcsat.ez->ITE(srst, qcsat.ez->value(ff.val_srst[eb.idx] == State::S1), n); } n_lit[idx] = n; @@ -1191,7 +1183,8 @@ struct OptDffWorker if (n_lit[rep] == n_lit[cls[i]]) continue; - int query = qcsat.ez->NOT(qcsat.ez->IFF(n_lit[rep], n_lit[cls[i]])); + // next-state bits differ + int query = qcsat.ez->XOR(n_lit[rep], n_lit[cls[i]]); std::vector modelExprs; for (int b : cls) modelExprs.push_back(n_lit[b]); From 75a30a22d655c591758daed5b4f392791162387f Mon Sep 17 00:00:00 2001 From: nella Date: Thu, 18 Jun 2026 11:43:13 +0200 Subject: [PATCH 09/33] Cleanup bitsim, document hypo. --- kernel/bitsim.h | 79 --------------------------------- passes/opt/opt_dff.cc | 101 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 94 insertions(+), 86 deletions(-) delete mode 100644 kernel/bitsim.h diff --git a/kernel/bitsim.h b/kernel/bitsim.h deleted file mode 100644 index c659a5e34..000000000 --- a/kernel/bitsim.h +++ /dev/null @@ -1,79 +0,0 @@ -#ifndef BITSIM_H -#define BITSIM_H - -#include "kernel/modtools.h" - -YOSYS_NAMESPACE_BEGIN - -struct BitSim { - Module *module; - SigMap &sigmap; - ModWalker &modwalker; - dict sim_vals; - uint64_t rng_state; - - BitSim(Module *m, SigMap &sm, ModWalker &mw) - : module(m), sigmap(sm), modwalker(mw), rng_state(1337) {} - - uint64_t next_rand() { - uint32_t lo = mkhash_xorshift((uint32_t)rng_state); - uint32_t hi = mkhash_xorshift((uint32_t)(rng_state >> 32) ^ lo); - rng_state = ((uint64_t)hi << 32) | lo; - return rng_state; - } - - uint64_t eval_bit(SigBit b) { - SigBit mapped = sigmap(b); - if (mapped == State::S0) return 0ULL; - if (mapped == State::S1) return ~0ULL; - if (mapped == State::Sx || mapped == State::Sz) return 0ULL; - - auto it = sim_vals.find(mapped); - if (it != sim_vals.end()) return it->second; - sim_vals[mapped] = 0; - uint64_t res = 0; - - if (!modwalker.has_drivers(mapped)) { - res = next_rand(); - } else { - auto &drivers = modwalker.signal_drivers[mapped]; - if (drivers.empty()) { - res = next_rand(); - } else { - auto driver = *drivers.begin(); - Cell *cell = driver.cell; - - if (cell->is_builtin_ff()) { - res = next_rand(); - } else if (cell->type == ID($_AND_)) { - res = eval_bit(cell->getPort(ID::A)[0]) & eval_bit(cell->getPort(ID::B)[0]); - } else if (cell->type == ID($_OR_)) { - res = eval_bit(cell->getPort(ID::A)[0]) | eval_bit(cell->getPort(ID::B)[0]); - } else if (cell->type == ID($_XOR_)) { - res = eval_bit(cell->getPort(ID::A)[0]) ^ eval_bit(cell->getPort(ID::B)[0]); - } else if (cell->type == ID($_NOT_)) { - res = ~eval_bit(cell->getPort(ID::A)[0]); - } else if (cell->type == ID($_MUX_)) { - uint64_t s = eval_bit(cell->getPort(ID::S)[0]); - uint64_t a = eval_bit(cell->getPort(ID::A)[0]); - uint64_t b = eval_bit(cell->getPort(ID::B)[0]); - res = (a & ~s) | (b & s); - } else if (cell->type == ID($mux)) { - uint64_t s = eval_bit(cell->getPort(ID::S)[0]); - uint64_t a = eval_bit(cell->getPort(ID::A)[driver.offset]); - uint64_t b = eval_bit(cell->getPort(ID::B)[driver.offset]); - res = (a & ~s) | (b & s); - } else { - res = next_rand(); - } - } - } - - sim_vals[mapped] = res; - return res; - } -}; - -YOSYS_NAMESPACE_END - -#endif diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index 6984f3f4b..e87fcb716 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -26,7 +26,6 @@ #include "kernel/sigtools.h" #include "kernel/ffinit.h" #include "kernel/ff.h" -#include "kernel/bitsim.h" #include "kernel/pattern.h" #include "passes/techmap/simplemap.h" #include @@ -44,6 +43,76 @@ struct OptDffOptions bool keepdc; }; +// Bit-parallel random simulation used as a cheap pre-filter for equivalence +struct BitSim { + Module *module; + SigMap &sigmap; + ModWalker &modwalker; + dict sim_vals; + uint64_t rng_state; + + BitSim(Module *m, SigMap &sm, ModWalker &mw) + : module(m), sigmap(sm), modwalker(mw), rng_state(1337) {} + + uint64_t next_rand() { + uint32_t lo = mkhash_xorshift((uint32_t)rng_state); + uint32_t hi = mkhash_xorshift((uint32_t)(rng_state >> 32) ^ lo); + rng_state = ((uint64_t)hi << 32) | lo; + return rng_state; + } + + uint64_t eval_bit(SigBit b) { + SigBit mapped = sigmap(b); + if (mapped == State::S0) return 0ULL; + if (mapped == State::S1) return ~0ULL; + if (mapped == State::Sx || mapped == State::Sz) return 0ULL; + + auto it = sim_vals.find(mapped); + if (it != sim_vals.end()) return it->second; + sim_vals[mapped] = 0; + uint64_t res = 0; + + if (!modwalker.has_drivers(mapped)) { + res = next_rand(); + } else { + auto &drivers = modwalker.signal_drivers[mapped]; + if (drivers.empty()) { + res = next_rand(); + } else { + auto driver = *drivers.begin(); + Cell *cell = driver.cell; + + if (cell->is_builtin_ff()) { + res = next_rand(); + } else if (cell->type == ID($_AND_)) { + res = eval_bit(cell->getPort(ID::A)[0]) & eval_bit(cell->getPort(ID::B)[0]); + } else if (cell->type == ID($_OR_)) { + res = eval_bit(cell->getPort(ID::A)[0]) | eval_bit(cell->getPort(ID::B)[0]); + } else if (cell->type == ID($_XOR_)) { + res = eval_bit(cell->getPort(ID::A)[0]) ^ eval_bit(cell->getPort(ID::B)[0]); + } else if (cell->type == ID($_NOT_)) { + res = ~eval_bit(cell->getPort(ID::A)[0]); + } else if (cell->type == ID($_MUX_)) { + uint64_t s = eval_bit(cell->getPort(ID::S)[0]); + uint64_t a = eval_bit(cell->getPort(ID::A)[0]); + uint64_t b = eval_bit(cell->getPort(ID::B)[0]); + res = (a & ~s) | (b & s); + } else if (cell->type == ID($mux)) { + uint64_t s = eval_bit(cell->getPort(ID::S)[0]); + uint64_t a = eval_bit(cell->getPort(ID::A)[driver.offset]); + uint64_t b = eval_bit(cell->getPort(ID::B)[driver.offset]); + res = (a & ~s) | (b & s); + } else { + res = next_rand(); + } + } + } + + sim_vals[mapped] = res; + return res; + } +}; + struct OptDffWorker { const OptDffOptions &opt; @@ -927,6 +996,8 @@ struct OptDffWorker SigBit q; }; + // NOTE: This intentionally duplicates a subset of FfData, as flattening just the + // fields that matter for merging into a single comparable/hashable key is cheaper struct SigKey { enum Flag : uint16_t { InitOne = 1u << 0, @@ -965,6 +1036,7 @@ struct OptDffWorker }; bool is_def(State s) { + // Concrete constant bit (0 or 1), as opposed to x/z return s == State::S0 || s == State::S1; } @@ -984,11 +1056,12 @@ struct OptDffWorker ff_for_cell.emplace(cell, ff); for (int i = 0; i < ff.width; i++) { - // X value + // Skip bits whose reset drives an undefined (x) value if (ff.has_srst && !is_def(ff.val_srst[i])) continue; if (ff.has_arst && !is_def(ff.val_arst[i])) continue; - // Missing anchor + // Class members are assumed equal in the current cycle and proven equal in the next, which needs + // a base case anchoring them to a common known value bool def_init = is_def(ff.val_init[i]); if (!def_init && !ff.has_srst && !ff.has_arst) continue; @@ -1118,7 +1191,11 @@ struct OptDffWorker std::vector q_lit(bits.size(), -1); std::vector n_lit(bits.size(), -1); - // Per candidate SAT for its next state, model difference + // Build the next-state function n_lit[idx] of every candidate bit by + // folding the FF's control logic on top of the D input (-> next value) + + // Two bits are equivalent if their next states always agree whenever their + // current states (and those of every other candidate pair) agree for (auto &cls : classes) { for (int idx : cls) { const EqBit &eb = bits[idx]; @@ -1154,6 +1231,12 @@ struct OptDffWorker } qcsat.prepare(); + + // Assume the induction hypo (that every current class is internally equal in the present cycle), and try + // to prove that the members of each class therefore also agree in the next cycle + + // A class survives only if no counterexample exists under that hypo, so combined with the common init/reset + // value that every class shares, this makes the equality an inductive invariant -> bits are eq and safe to merge std::vector worklist; std::vector in_worklist(GetSize(classes), true); @@ -1168,6 +1251,7 @@ struct OptDffWorker auto &cls = classes[cls_idx]; if (GetSize(cls) < 2) continue; + // Induction hypo: assume every candidate class is equal std::vector assumptions; for (auto &c : classes) { if (GetSize(c) < 2) continue; @@ -1176,15 +1260,18 @@ struct OptDffWorker assumptions.push_back(qcsat.ez->IFF(q_lit[rep], q_lit[c[k]])); } - // Split at counterexamples + // Scan the class members against the representative and issue a query per pair, + // stopping early at the first counterexample, which is reused to split the entire + // class at once int rep = cls[0]; for (int i = 1; i < GetSize(cls); i++) { - // Trivially equivalent if (n_lit[rep] == n_lit[cls[i]]) continue; - // next-state bits differ + // Can the next state of the rep and this member ever differ? int query = qcsat.ez->XOR(n_lit[rep], n_lit[cls[i]]); + // Capture every member's next-state value in that model so one counterexample + // partitions the whole class std::vector modelExprs; for (int b : cls) modelExprs.push_back(n_lit[b]); From 46cbeab720af3421da88f932aeb38aaa5c34381e Mon Sep 17 00:00:00 2001 From: nella Date: Thu, 18 Jun 2026 11:58:01 +0200 Subject: [PATCH 10/33] Add effort limit. --- passes/opt/opt_dff.cc | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index e87fcb716..1b1c3bf0e 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -50,9 +50,15 @@ struct BitSim { ModWalker &modwalker; dict sim_vals; uint64_t rng_state; + int max_depth; + int evals_left; BitSim(Module *m, SigMap &sm, ModWalker &mw) - : module(m), sigmap(sm), modwalker(mw), rng_state(1337) {} + : module(m), sigmap(sm), modwalker(mw), rng_state(1337) + { + max_depth = module->design->scratchpad_get_int("opt_dff.sim_depth", 10000); + evals_left = module->design->scratchpad_get_int("opt_dff.sim_evals", 1000000); + } uint64_t next_rand() { uint32_t lo = mkhash_xorshift((uint32_t)rng_state); @@ -61,7 +67,7 @@ struct BitSim { return rng_state; } - uint64_t eval_bit(SigBit b) { + uint64_t eval_bit(SigBit b, int depth = 0) { SigBit mapped = sigmap(b); if (mapped == State::S0) return 0ULL; if (mapped == State::S1) return ~0ULL; @@ -69,6 +75,15 @@ struct BitSim { auto it = sim_vals.find(mapped); if (it != sim_vals.end()) return it->second; + + // Failsafe for huge designs + if (depth >= max_depth || evals_left <= 0) { + uint64_t r = next_rand(); + sim_vals[mapped] = r; + return r; + } + evals_left--; + sim_vals[mapped] = 0; uint64_t res = 0; @@ -85,22 +100,22 @@ struct BitSim { if (cell->is_builtin_ff()) { res = next_rand(); } else if (cell->type == ID($_AND_)) { - res = eval_bit(cell->getPort(ID::A)[0]) & eval_bit(cell->getPort(ID::B)[0]); + res = eval_bit(cell->getPort(ID::A)[0], depth+1) & eval_bit(cell->getPort(ID::B)[0], depth+1); } else if (cell->type == ID($_OR_)) { - res = eval_bit(cell->getPort(ID::A)[0]) | eval_bit(cell->getPort(ID::B)[0]); + res = eval_bit(cell->getPort(ID::A)[0], depth+1) | eval_bit(cell->getPort(ID::B)[0], depth+1); } else if (cell->type == ID($_XOR_)) { - res = eval_bit(cell->getPort(ID::A)[0]) ^ eval_bit(cell->getPort(ID::B)[0]); + res = eval_bit(cell->getPort(ID::A)[0], depth+1) ^ eval_bit(cell->getPort(ID::B)[0], depth+1); } else if (cell->type == ID($_NOT_)) { - res = ~eval_bit(cell->getPort(ID::A)[0]); + res = ~eval_bit(cell->getPort(ID::A)[0], depth+1); } else if (cell->type == ID($_MUX_)) { - uint64_t s = eval_bit(cell->getPort(ID::S)[0]); - uint64_t a = eval_bit(cell->getPort(ID::A)[0]); - uint64_t b = eval_bit(cell->getPort(ID::B)[0]); + uint64_t s = eval_bit(cell->getPort(ID::S)[0], depth+1); + uint64_t a = eval_bit(cell->getPort(ID::A)[0], depth+1); + uint64_t b = eval_bit(cell->getPort(ID::B)[0], depth+1); res = (a & ~s) | (b & s); } else if (cell->type == ID($mux)) { - uint64_t s = eval_bit(cell->getPort(ID::S)[0]); - uint64_t a = eval_bit(cell->getPort(ID::A)[driver.offset]); - uint64_t b = eval_bit(cell->getPort(ID::B)[driver.offset]); + uint64_t s = eval_bit(cell->getPort(ID::S)[0], depth+1); + uint64_t a = eval_bit(cell->getPort(ID::A)[driver.offset], depth+1); + uint64_t b = eval_bit(cell->getPort(ID::B)[driver.offset], depth+1); res = (a & ~s) | (b & s); } else { res = next_rand(); From 049dca7ded732e45be01a8e7bd5ce9646f97451c Mon Sep 17 00:00:00 2001 From: Miodrag Milanovic Date: Fri, 26 Jun 2026 18:02:37 +0200 Subject: [PATCH 11/33] Fix skipping test-build on merge --- .github/workflows/test-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 8a71d39be..33205aa83 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -49,7 +49,7 @@ jobs: - id: set_output run: | if [ "${{ github.event_name }}" = "merge_group" ]; then - echo "should_skip=false" >> $GITHUB_OUTPUT + echo "should_skip=true" >> $GITHUB_OUTPUT elif [ "${{ github.event_name }}" = "push" ]; then should_skip=false else From 9144d2bd2fc91539de4390bcd79a18947ec2f178 Mon Sep 17 00:00:00 2001 From: Miodrag Milanovic Date: Fri, 26 Jun 2026 19:35:44 +0200 Subject: [PATCH 12/33] Moving nix to weekly as non-critical --- .github/workflows/extra-builds.yml | 20 -------------------- .github/workflows/nix.yml | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/nix.yml diff --git a/.github/workflows/extra-builds.yml b/.github/workflows/extra-builds.yml index 644529c9d..d041794d7 100644 --- a/.github/workflows/extra-builds.yml +++ b/.github/workflows/extra-builds.yml @@ -172,32 +172,12 @@ jobs: cmake -B build -DCMAKE_TOOLCHAIN_FILE=${WASI_SDK_PATH}/share/cmake/wasi-sdk-p1.cmake -DCMAKE_BUILD_TYPE=Release -DYOSYS_COMPILER_LAUNCHER=ccache . cmake --build build -j$(nproc) - nix-build: - name: "Build nix flake" - needs: pre_job - if: (github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch') && needs.pre_job.outputs.should_skip != 'true' - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - fail-fast: false - steps: - - uses: actions/checkout@v7 - with: - submodules: true - persist-credentials: false - - uses: cachix/install-nix-action@v31 - with: - install_url: https://releases.nixos.org/nix/nix-2.30.0/install - - run: nix build -L - extra-builds-result: runs-on: ubuntu-latest needs: - vs-build - mingw-build - wasi-build - - nix-build if: always() steps: - name: Check results diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 000000000..120240bd6 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,24 @@ +name: Test nix build + +on: + workflow_dispatch: + schedule: + - cron: '0 5 * * 6' + +jobs: + nix-build: + name: "Build nix flake" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + fail-fast: false + steps: + - uses: actions/checkout@v7 + with: + submodules: true + persist-credentials: false + - uses: cachix/install-nix-action@v31 + with: + install_url: https://releases.nixos.org/nix/nix-2.30.0/install + - run: nix build -L From 0c0d3f717c5be17ec479f2a095adadffd725bfab Mon Sep 17 00:00:00 2001 From: Miodrag Milanovic Date: Fri, 26 Jun 2026 20:57:24 +0200 Subject: [PATCH 13/33] Install just some brew packages --- .github/actions/setup-build-env/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index b61bb95ac..6f631ff30 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -56,7 +56,7 @@ runs: if: runner.os == 'macOS' shell: bash run: | - brew bundle + brew install bison flex gawk libffi git pkg-config python3 bash googletest tcl-tk llvm - name: Linux runtime environment if: runner.os == 'Linux' From 97e2600e5c53a7928669dac0b04254cfe7b4ac4c Mon Sep 17 00:00:00 2001 From: Miodrag Milanovic Date: Mon, 29 Jun 2026 08:32:44 +0200 Subject: [PATCH 14/33] Removed rewrite leftovers from log --- kernel/log.cc | 6 ------ kernel/log.h | 13 ------------- 2 files changed, 19 deletions(-) diff --git a/kernel/log.cc b/kernel/log.cc index 2f5a6350b..2c3e45c2b 100644 --- a/kernel/log.cc +++ b/kernel/log.cc @@ -388,12 +388,6 @@ void log_formatted_file_error(std::string_view filename, int lineno, std::string log_error_with_prefix(prefix, str); } -void logv_file_error(const string &filename, int lineno, - const char *format, va_list ap) -{ - log_formatted_file_error(filename, lineno, vstringf(format, ap)); -} - void log_experimental(const std::string &str) { if (log_experimentals_ignored.count(str) == 0 && log_experimentals.count(str) == 0) { diff --git a/kernel/log.h b/kernel/log.h index d132ba1a0..37260319b 100644 --- a/kernel/log.h +++ b/kernel/log.h @@ -24,7 +24,6 @@ #include -#include #include #define YS_REGEX_COMPILE(param) std::regex(param, \ std::regex_constants::nosubs | \ @@ -44,20 +43,11 @@ # endif #endif -#if defined(_MSC_VER) -// At least this is not in MSVC++ 2013. -# define __PRETTY_FUNCTION__ __FUNCTION__ -#endif - // from libs/sha1/sha1.h class SHA1; YOSYS_NAMESPACE_BEGIN -#define S__LINE__sub2(x) #x -#define S__LINE__sub1(x) S__LINE__sub2(x) -#define S__LINE__ S__LINE__sub1(__LINE__) - // YS_DEBUGTRAP is a macro that is functionally equivalent to a breakpoint // if the platform provides such functionality, and does nothing otherwise. // If no debugger is attached, it starts a just-in-time debugger if available, @@ -120,9 +110,6 @@ extern int log_make_debug; extern int log_force_debug; extern int log_debug_suppressed; -[[deprecated]] -[[noreturn]] void logv_file_error(const string &filename, int lineno, const char *format, va_list ap); - void set_verific_logging(void (*cb)(int msg_type, const char *message_id, const char* file_path, unsigned int left_line, unsigned int left_col, unsigned int right_line, unsigned int right_col, const char *msg)); extern void (*log_verific_callback)(int msg_type, const char *message_id, const char* file_path, unsigned int left_line, unsigned int left_col, unsigned int right_line, unsigned int right_col, const char *msg); From 4a245f9fed374e6406f33172135ce97ed89587b5 Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Tue, 30 Jun 2026 12:40:16 +0300 Subject: [PATCH 15/33] ci: cache build instead of using artifacts --- .github/workflows/release.yml | 15 -------- .github/workflows/test-build.yml | 63 ++++++++++++-------------------- 2 files changed, 23 insertions(+), 55 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86ae739ae..96b48891a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,11 +55,6 @@ jobs: python3 -m pip wheel . --wheel-dir /src/dist ' - - uses: actions/upload-artifact@v4 - with: - name: linux-musl-wheel - path: dist/*.whl - build-manylinux-wheel: runs-on: ubuntu-latest name: Build Linux amd64 wheel (manylinux2014, glibc 2.17+) @@ -121,11 +116,6 @@ jobs: done ' - - uses: actions/upload-artifact@v4 - with: - name: linux-manylinux-wheel - path: dist-manylinux/*.whl - build-macos-wheel: runs-on: macos-15 name: Build macOS arm64 wheel @@ -162,11 +152,6 @@ jobs: mkdir -p dist python3 -m pip wheel . --wheel-dir dist - - uses: actions/upload-artifact@v4 - with: - name: macos-wheel - path: dist/*.whl - release: # allow testing outside main branch if: github.ref_name == 'main' diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 56e94328e..01a7e505c 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -90,7 +90,13 @@ jobs: key: test-build-${{ matrix.os }} save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} restore-keys: | - test-build-${{ matrix.os }}- + test-build-${{ matrix.os }} + + - name: Cache Build Directory + uses: actions/cache@v6 + with: + path: build + key: ${{ runner.os }}-build-${{ github.run_id }} - name: Build shell: bash @@ -104,19 +110,6 @@ jobs: run: | ./build/yosys-config || true - - name: Compress build - shell: bash - run: | - cd build - tar -cvf ../build.tar share/ yosys yosys-* - - - name: Store build artifact - uses: actions/upload-artifact@v7 - with: - name: build-${{ matrix.os }} - path: build.tar - retention-days: 1 - test-yosys: name: Run tests runs-on: ${{ matrix.os }} @@ -142,16 +135,12 @@ jobs: get-build-deps: true get-iverilog: true - - name: Download build artifact - uses: actions/download-artifact@v8 + - name: Restore Build Directory from Cache + uses: actions/cache@v6 with: - name: build-${{ matrix.os }} - - - name: Uncompress build - shell: bash - run: | - mkdir -p build - tar -xvf build.tar -C build + path: build + key: ${{ runner.os }}-build-${{ github.run_id }} + fail-on-cache-miss: true - name: Log yosys-config output run: | @@ -192,19 +181,17 @@ jobs: with: runs-on: ${{ matrix.os }} - - name: Download build artifact - uses: actions/download-artifact@v8 + - name: Restore Build Directory from Cache + uses: actions/cache@v6 with: - name: build-${{ matrix.os }} - - - name: Uncompress build - shell: bash - run: - tar -xvf build.tar + path: build + key: ${{ runner.os }}-build-${{ github.run_id }} + fail-on-cache-miss: true - name: test_cell shell: bash run: | + cd build ./yosys -p 'test_cell -n 20 -s 1 all' ./yosys -p 'test_cell -n 20 -s 1 -nosat -aigmap $pow $pmux' ./yosys -p 'test_cell -n 20 -s 1 -nosat -aigmap $eqx $nex $bweqx' @@ -233,16 +220,12 @@ jobs: get-build-deps: true get-docs-deps: true - - name: Download build artifact - uses: actions/download-artifact@v8 + - name: Restore Build Directory from Cache + uses: actions/cache@v6 with: - name: build-${{ matrix.os }} - - - name: Uncompress build - shell: bash - run: | - mkdir -p build - tar -xvf build.tar -C build + path: build + key: ${{ runner.os }}-build-${{ github.run_id }} + fail-on-cache-miss: true - name: Log yosys-config output run: | From 268ff490307a4b2f0c53881325ecf9fcdfb28eb5 Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Wed, 1 Jul 2026 21:06:52 +0300 Subject: [PATCH 16/33] ci: target different repo for release --- .github/workflows/release.yml | 108 +++++++++++++++++++++++++--------- 1 file changed, 80 insertions(+), 28 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 96b48891a..1e3beaa3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,6 +22,13 @@ jobs: persist-credentials: false ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Cache Dist Directory + uses: actions/cache@v6 + with: + path: dist-x86_64-musllinux + key: ${{ github.run_id }}-x86_64-musllinux-wheel + enableCrossOsArchive: true + - name: Build wheel in Alpine container run: | docker run --rm \ @@ -51,8 +58,8 @@ jobs: # Build the wheel via setup.py pip install --break-system-packages pybind11 cxxheaderparser - mkdir -p /src/dist - python3 -m pip wheel . --wheel-dir /src/dist + mkdir -p /src/dist-x86_64-musllinux + python3 -m pip wheel . --wheel-dir /src/dist-x86_64-musllinux ' build-manylinux-wheel: @@ -66,6 +73,13 @@ jobs: submodules: recursive ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Cache Dist Directory + uses: actions/cache@v6 + with: + path: dist-x86_64-manylinux + key: ${{ github.run_id }}-x86_64-manylinux-wheel + enableCrossOsArchive: true + - name: Build wheel in manylinux2014 container run: | docker run --rm \ @@ -105,14 +119,15 @@ jobs: make cd /src + WHEEL_DIR=/src/dist-x86_64-manylinux pip3 install --upgrade pip setuptools wheel pybind11 cxxheaderparser mkdir -p dist-manylinux - python3 -m pip wheel . --wheel-dir /src/dist-manylinux + python3 -m pip wheel . --wheel-dir $WHEEL_DIR # Tag wheel as manylinux2014 pip3 install auditwheel || true - for whl in /src/dist-manylinux/*.whl; do - auditwheel repair "$whl" --plat manylinux2014_x86_64 -w /src/dist-manylinux/ 2>/dev/null || true + for whl in $WHEEL_DIR/*.whl; do + auditwheel repair "$whl" --plat manylinux2014_x86_64 -w $WHEEL_DIR/ 2>/dev/null || true done ' @@ -145,44 +160,76 @@ jobs: cd verific/tclmain make + - name: Cache Dist Directory + uses: actions/cache@v6 + with: + path: dist-aarch64-macos + key: ${{ github.run_id }}-aarch64-macos-wheel + enableCrossOsArchive: true + - name: Build wheel run: | export PATH="$(brew --prefix bison)/bin:$(brew --prefix flex)/bin:$PATH" export MACOSX_DEPLOYMENT_TARGET=11.0 mkdir -p dist - python3 -m pip wheel . --wheel-dir dist + python3 -m pip wheel . --wheel-dir dist-aarch64-macos release: # allow testing outside main branch - if: github.ref_name == 'main' + # if: github.ref_name == 'main' runs-on: ubuntu-latest needs: [build-linux-wheel, build-manylinux-wheel, build-macos-wheel] name: Create GitHub releases steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: + ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} fetch-depth: 0 - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - path: all-wheels - merge-multiple: true - - - name: Generate release notes - id: meta + - id: meta + name: Get repo metadata and clean up run: | SHORT_SHA=$(git rev-parse --short HEAD) FULL_SHA=$(git rev-parse HEAD) DATE=$(date -u +%Y-%m-%d) - echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" - echo "date=$DATE" >> "$GITHUB_OUTPUT" REPO_URL="${{ github.server_url }}/${{ github.repository }}" + echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" + echo "full_sha=$FULL_SHA" >> "$GITHUB_OUTPUT" + echo "date=$DATE" >> "$GITHUB_OUTPUT" + echo "url=$REPO_URL" >> "$GITHUB_OUTPUT" + rm -rf ./* ./.* + - uses: actions/checkout@v6 + with: + repository: silimate/yosys-wheels + ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + fetch-depth: 0 + - name: Restore dist from cache (x86_64-musllinux) + uses: actions/cache@v6 + with: + path: dist-x86_64-musllinux + key: ${{ github.run_id }}-x86_64-musllinux-wheel + enableCrossOsArchive: true + fail-on-cache-miss: true + - name: Restore dist from cache (x86_64-manylinux) + uses: actions/cache@v6 + with: + path: dist-x86_64-manylinux + key: ${{ github.run_id }}-x86_64-manylinux-wheel + enableCrossOsArchive: true + fail-on-cache-miss: true + - name: Restore dist from cache (aarch64-macos) + uses: actions/cache@v6 + with: + path: dist-aarch64-macos + key: ${{ github.run_id }}-aarch64-macos-wheel + enableCrossOsArchive: true + fail-on-cache-miss: true + - name: Update release notes + run: | printf '%s\n' \ - "Automated build from \`main\` @ [\`${SHORT_SHA}\`](${REPO_URL}/commit/${FULL_SHA})" \ + "Automated build from \`main\` @ [\`${{ steps.meta.outputs.short_sha }}\`](${{ steps.meta.outputs.repo_url }}/commit/${{ steps.meta.outputs.full_sha }})" \ "" \ - "**Built:** ${DATE}" \ + "**Built:** ${{ steps.meta.outputs.date }}" \ "" \ "### Assets" \ "| File | Platform |" \ @@ -196,17 +243,22 @@ jobs: "pip install pyosys-*.whl" \ "\`\`\`" \ > release_notes.md - + - name: Commit and push + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit --all --message="Build for ${{ steps.meta.outputs.date }}-${{ steps.meta.outputs.short_sha }}" + git push - name: Create permanent release run: | TAG="build-${{ steps.meta.outputs.date }}-${{ steps.meta.outputs.short_sha }}" gh release create "$TAG" \ - all-wheels/*.whl \ - --target "${{ github.sha }}" \ + dist-*/*.whl \ + --target "$(git rev-parse HEAD)" \ --title "Build ${{ steps.meta.outputs.date }} (${{ steps.meta.outputs.short_sha }})" \ --notes-file release_notes.md env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.YOSYS_WHEELS_TOKEN }} - name: Update latest release run: | @@ -214,10 +266,10 @@ jobs: git push -f origin latest gh release delete latest --yes 2>/dev/null || true gh release create latest \ - all-wheels/*.whl \ - --target "${{ github.sha }}" \ + dist-*/*.whl \ + --target "$(git rev-parse HEAD)" \ --title "Latest Build (${{ steps.meta.outputs.date }})" \ --notes-file release_notes.md \ --prerelease env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.YOSYS_WHEELS_TOKEN }} From 30a813b090077d8c832d81ac0e320b76908938d4 Mon Sep 17 00:00:00 2001 From: Miodrag Milanovic Date: Thu, 2 Jul 2026 11:34:22 +0200 Subject: [PATCH 17/33] Update ABC as per 2026-07-02 --- abc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abc b/abc index a35f806b8..e026ed538 160000 --- a/abc +++ b/abc @@ -1 +1 @@ -Subproject commit a35f806b8ce47d29537cab973679b5a3523ab085 +Subproject commit e026ed5380f3bdc3beea2ff9ffc23236fc549d5b From 56dd057ab47b627aa5d203482ac6a9cd94680299 Mon Sep 17 00:00:00 2001 From: Mohamed Gaber Date: Thu, 2 Jul 2026 18:56:25 +0300 Subject: [PATCH 18/33] test/ci fixes - fix incorrect repo_url output - fix parsing in test_splitnets to match new format - fix silimate-shell.nix referring to removed derivation --- .github/workflows/release.yml | 2 +- silimate-shell.nix | 1 - tests/various/test_splitnets.tcl | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e3beaa3f..bfd7140e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -196,7 +196,7 @@ jobs: echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" echo "full_sha=$FULL_SHA" >> "$GITHUB_OUTPUT" echo "date=$DATE" >> "$GITHUB_OUTPUT" - echo "url=$REPO_URL" >> "$GITHUB_OUTPUT" + echo "repo_url=$REPO_URL" >> "$GITHUB_OUTPUT" rm -rf ./* ./.* - uses: actions/checkout@v6 with: diff --git a/silimate-shell.nix b/silimate-shell.nix index beef60ae6..01ac4b657 100644 --- a/silimate-shell.nix +++ b/silimate-shell.nix @@ -29,7 +29,6 @@ pkgs.mkShell { iverilog # tests gtkwave # vcd2fst (python3.withPackages(ps: with ps; [pip wheel pybind11 cxxheaderparser])) - gnu-ar gtest ] ++ lib.optionals stdenv.isLinux [ elfutils # provides libdw.so (not to be confused with libdwarf.so) diff --git a/tests/various/test_splitnets.tcl b/tests/various/test_splitnets.tcl index 9cb2b4305..21b10641b 100644 --- a/tests/various/test_splitnets.tcl +++ b/tests/various/test_splitnets.tcl @@ -7,10 +7,10 @@ proc read_stats { file } { set ports 0 set nets 0 foreach line [split $result "\n"] { - if [regexp {Number of wires:[ \t]+([0-9]+)} $line tmp n] { + if [regexp {\s([0-9]+) wires} $line tmp n] { set nets [expr $nets + $n] } - if [regexp {Number of ports:[ \t]+([0-9]+)} $line tmp n] { + if [regexp {(\s[0-9]+) ports} $line tmp n] { set ports [expr $ports + $n] } } From 0e56ca02ed99bacd6668e64de2dc054dd256d9f1 Mon Sep 17 00:00:00 2001 From: nella Date: Mon, 6 Jul 2026 13:47:10 +0200 Subject: [PATCH 19/33] Make opt_dff -sat conflict with -keepdc. --- passes/opt/opt_dff.cc | 13 +++++++++++-- tests/opt/opt_dff_eqbits.ys | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index 1b1c3bf0e..fcb961125 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -1071,7 +1071,7 @@ struct OptDffWorker ff_for_cell.emplace(cell, ff); for (int i = 0; i < ff.width; i++) { - // Skip bits whose reset drives an undefined (x) value + // Skip bits whose reset value is undefined (x) if (ff.has_srst && !is_def(ff.val_srst[i])) continue; if (ff.has_arst && !is_def(ff.val_arst[i])) continue; @@ -1424,7 +1424,9 @@ struct OptDffPass : public Pass { log("\n"); log(" -sat\n"); log(" additionally invoke SAT solver to detect and remove flip-flops (with\n"); - log(" non-constant inputs) that can also be replaced with a constant driver\n"); + log(" non-constant inputs) that can also be replaced with a constant driver,\n"); + log(" or merged with equivalent flip-flops. this reasons in 2-valued logic\n"); + log(" and may resolve don't-care bits, so it is incompatible with -keepdc.\n"); log("\n"); log(" -keepdc\n"); log(" some optimizations change the behavior of the circuit with respect to\n"); @@ -1456,6 +1458,13 @@ struct OptDffPass : public Pass { } extra_args(args, argidx, design); + // The SAT engine reasons in 2-valued logic (a constant x is treated as + // 0), so it can resolve don't-care bits to concrete values -- exactly + // what -keepdc promises not to do. Refuse the combination rather than + // silently ignore -keepdc. + if (opt.sat && opt.keepdc) + log_cmd_error("The -sat and -keepdc options are mutually exclusive.\n"); + bool did_something = false; for (auto mod : design->selected_modules()) { OptDffWorker worker(opt, mod); diff --git a/tests/opt/opt_dff_eqbits.ys b/tests/opt/opt_dff_eqbits.ys index 10e9045e4..f990fe71a 100644 --- a/tests/opt/opt_dff_eqbits.ys +++ b/tests/opt/opt_dff_eqbits.ys @@ -54,3 +54,36 @@ design -copy-from gate -as gate test_case equiv_make gold gate equiv equiv_induct equiv equiv_status -assert + +# verify keepdc exclusivity +design -reset +read_verilog -sv < Date: Mon, 6 Jul 2026 07:40:14 -0700 Subject: [PATCH 20/33] Naming improvements --- passes/sat/async2sync.cc | 126 ++++++++++++++++++------------------- passes/techmap/bwmuxmap.cc | 8 +-- 2 files changed, 67 insertions(+), 67 deletions(-) diff --git a/passes/sat/async2sync.cc b/passes/sat/async2sync.cc index be2355e00..316f672a9 100644 --- a/passes/sat/async2sync.cc +++ b/passes/sat/async2sync.cc @@ -95,27 +95,27 @@ struct Async2syncPass : public Pass { if (trg_width == 0) { if (initstate == State::S0) - initstate = module->Initstate(NEW_ID); + initstate = module->Initstate(NEW_ID2_SUFFIX("initstate")); // SILIMATE: Improve the naming SigBit sig_en = cell->getPort(ID::EN); - cell->setPort(ID::EN, module->And(NEW_ID, sig_en, initstate)); + cell->setPort(ID::EN, module->And(NEW_ID2_SUFFIX("en_init"), sig_en, initstate)); // SILIMATE: Improve the naming } else { SigBit sig_en = cell->getPort(ID::EN); SigSpec sig_args = cell->getPort(ID::ARGS); bool trg_polarity = cell->getParam(ID(TRG_POLARITY)).as_bool(); SigBit sig_trg = cell->getPort(ID::TRG); - Wire *sig_en_q = module->addWire(NEW_ID); - Wire *sig_args_q = module->addWire(NEW_ID, GetSize(sig_args)); + Wire *sig_en_q = module->addWire(NEW_ID2_SUFFIX("en_q")); // SILIMATE: Improve the naming + Wire *sig_args_q = module->addWire(NEW_ID2_SUFFIX("args_q"), GetSize(sig_args)); // SILIMATE: Improve the naming sig_en_q->attributes.emplace(ID::init, State::S0); - module->addDff(NEW_ID, sig_trg, sig_en, sig_en_q, trg_polarity, cell->get_src_attribute()); - module->addDff(NEW_ID, sig_trg, sig_args, sig_args_q, trg_polarity, cell->get_src_attribute()); + module->addDff(NEW_ID2_SUFFIX("en_dff"), sig_trg, sig_en, sig_en_q, trg_polarity, cell->get_src_attribute()); // SILIMATE: Improve the naming + module->addDff(NEW_ID2_SUFFIX("args_dff"), sig_trg, sig_args, sig_args_q, trg_polarity, cell->get_src_attribute()); // SILIMATE: Improve the naming cell->setPort(ID::EN, sig_en_q); cell->setPort(ID::ARGS, sig_args_q); if (cell->type == ID($check)) { SigBit sig_a = cell->getPort(ID::A); - Wire *sig_a_q = module->addWire(NEW_ID); + Wire *sig_a_q = module->addWire(NEW_ID2_SUFFIX("a_q")); // SILIMATE: Improve the naming sig_a_q->attributes.emplace(ID::init, State::S1); - module->addDff(NEW_ID, sig_trg, sig_a, sig_a_q, trg_polarity, cell->get_src_attribute()); + module->addDff(NEW_ID2_SUFFIX("a_dff"), sig_trg, sig_a, sig_a_q, trg_polarity, cell->get_src_attribute()); // SILIMATE: Improve the naming cell->setPort(ID::A, sig_a_q); } } @@ -152,8 +152,8 @@ struct Async2syncPass : public Pass { initvals.remove_init(ff.sig_q); - Wire *new_d = module->addWire(NEW_ID, ff.width); - Wire *new_q = module->addWire(NEW_ID, ff.width); + Wire *new_d = module->addWire(NEW_ID2_SUFFIX("new_d"), ff.width); // SILIMATE: Improve the naming + Wire *new_q = module->addWire(NEW_ID2_SUFFIX("new_q"), ff.width); // SILIMATE: Improve the naming SigSpec sig_set = ff.sig_set; SigSpec sig_clr = ff.sig_clr; @@ -161,21 +161,21 @@ struct Async2syncPass : public Pass { if (!ff.pol_set) { if (!ff.is_fine) - sig_set = module->Not(NEW_ID, sig_set); + sig_set = module->Not(NEW_ID2_SUFFIX("set_hi"), sig_set); // SILIMATE: Improve the naming else - sig_set = module->NotGate(NEW_ID, sig_set); + sig_set = module->NotGate(NEW_ID2_SUFFIX("set_hi"), sig_set); // SILIMATE: Improve the naming } if (ff.pol_clr) { if (!ff.is_fine) - sig_clr_inv = module->Not(NEW_ID, sig_clr); + sig_clr_inv = module->Not(NEW_ID2_SUFFIX("clr_inv"), sig_clr); // SILIMATE: Improve the naming else - sig_clr_inv = module->NotGate(NEW_ID, sig_clr); + sig_clr_inv = module->NotGate(NEW_ID2_SUFFIX("clr_inv"), sig_clr); // SILIMATE: Improve the naming } else { if (!ff.is_fine) - sig_clr = module->Not(NEW_ID, sig_clr); + sig_clr = module->Not(NEW_ID2_SUFFIX("clr_hi"), sig_clr); // SILIMATE: Improve the naming else - sig_clr = module->NotGate(NEW_ID, sig_clr); + sig_clr = module->NotGate(NEW_ID2_SUFFIX("clr_hi"), sig_clr); // SILIMATE: Improve the naming } // At this point, sig_set and sig_clr are now unconditionally @@ -183,26 +183,26 @@ struct Async2syncPass : public Pass { SigSpec set_and_clr; if (!ff.is_fine) - set_and_clr = module->And(NEW_ID, sig_set, sig_clr); + set_and_clr = module->And(NEW_ID2_SUFFIX("set_and_clr"), sig_set, sig_clr); // SILIMATE: Improve the naming else - set_and_clr = module->AndGate(NEW_ID, sig_set, sig_clr); + set_and_clr = module->AndGate(NEW_ID2_SUFFIX("set_and_clr"), sig_set, sig_clr); // SILIMATE: Improve the naming if (!ff.is_fine) { - SigSpec tmp = module->Or(NEW_ID, ff.sig_d, sig_set); - tmp = module->And(NEW_ID, tmp, sig_clr_inv); - module->addBwmux(NEW_ID, tmp, Const(State::Sx, ff.width), set_and_clr, new_d); + SigSpec tmp = module->Or(NEW_ID2_SUFFIX("d_or_set"), ff.sig_d, sig_set); // SILIMATE: Improve the naming + tmp = module->And(NEW_ID2_SUFFIX("d_masked"), tmp, sig_clr_inv); // SILIMATE: Improve the naming + module->addBwmux(NEW_ID2_SUFFIX("d_bwmux"), tmp, Const(State::Sx, ff.width), set_and_clr, new_d); // SILIMATE: Improve the naming - tmp = module->Or(NEW_ID, new_q, sig_set); - tmp = module->And(NEW_ID, tmp, sig_clr_inv); - module->addBwmux(NEW_ID, tmp, Const(State::Sx, ff.width), set_and_clr, ff.sig_q); + tmp = module->Or(NEW_ID2_SUFFIX("q_or_set"), new_q, sig_set); // SILIMATE: Improve the naming + tmp = module->And(NEW_ID2_SUFFIX("q_masked"), tmp, sig_clr_inv); // SILIMATE: Improve the naming + module->addBwmux(NEW_ID2_SUFFIX("q_bwmux"), tmp, Const(State::Sx, ff.width), set_and_clr, ff.sig_q); // SILIMATE: Improve the naming } else { - SigSpec tmp = module->OrGate(NEW_ID, ff.sig_d, sig_set); - tmp = module->AndGate(NEW_ID, tmp, sig_clr_inv); - module->addMuxGate(NEW_ID, tmp, State::Sx, set_and_clr, new_d); + SigSpec tmp = module->OrGate(NEW_ID2_SUFFIX("d_or_set"), ff.sig_d, sig_set); // SILIMATE: Improve the naming + tmp = module->AndGate(NEW_ID2_SUFFIX("d_masked"), tmp, sig_clr_inv); // SILIMATE: Improve the naming + module->addMuxGate(NEW_ID2_SUFFIX("d_mux"), tmp, State::Sx, set_and_clr, new_d); // SILIMATE: Improve the naming - tmp = module->OrGate(NEW_ID, new_q, sig_set); - tmp = module->AndGate(NEW_ID, tmp, sig_clr_inv); - module->addMuxGate(NEW_ID, tmp, State::Sx, set_and_clr, ff.sig_q); + tmp = module->OrGate(NEW_ID2_SUFFIX("q_or_set"), new_q, sig_set); // SILIMATE: Improve the naming + tmp = module->AndGate(NEW_ID2_SUFFIX("q_masked"), tmp, sig_clr_inv); // SILIMATE: Improve the naming + module->addMuxGate(NEW_ID2_SUFFIX("q_mux"), tmp, State::Sx, set_and_clr, ff.sig_q); // SILIMATE: Improve the naming } ff.sig_d = new_d; @@ -217,24 +217,24 @@ struct Async2syncPass : public Pass { initvals.remove_init(ff.sig_q); - Wire *new_d = module->addWire(NEW_ID, ff.width); - Wire *new_q = module->addWire(NEW_ID, ff.width); + Wire *new_d = module->addWire(NEW_ID2_SUFFIX("new_d"), ff.width); // SILIMATE: Improve the naming + Wire *new_q = module->addWire(NEW_ID2_SUFFIX("new_q"), ff.width); // SILIMATE: Improve the naming if (ff.pol_aload) { if (!ff.is_fine) { - module->addMux(NEW_ID, new_q, ff.sig_ad, ff.sig_aload, ff.sig_q); - module->addMux(NEW_ID, ff.sig_d, ff.sig_ad, ff.sig_aload, new_d); + module->addMux(NEW_ID2_SUFFIX("q_aload_mux"), new_q, ff.sig_ad, ff.sig_aload, ff.sig_q); // SILIMATE: Improve the naming + module->addMux(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_d, ff.sig_ad, ff.sig_aload, new_d); // SILIMATE: Improve the naming } else { - module->addMuxGate(NEW_ID, new_q, ff.sig_ad, ff.sig_aload, ff.sig_q); - module->addMuxGate(NEW_ID, ff.sig_d, ff.sig_ad, ff.sig_aload, new_d); + module->addMuxGate(NEW_ID2_SUFFIX("q_aload_mux"), new_q, ff.sig_ad, ff.sig_aload, ff.sig_q); // SILIMATE: Improve the naming + module->addMuxGate(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_d, ff.sig_ad, ff.sig_aload, new_d); // SILIMATE: Improve the naming } } else { if (!ff.is_fine) { - module->addMux(NEW_ID, ff.sig_ad, new_q, ff.sig_aload, ff.sig_q); - module->addMux(NEW_ID, ff.sig_ad, ff.sig_d, ff.sig_aload, new_d); + module->addMux(NEW_ID2_SUFFIX("q_aload_mux"), ff.sig_ad, new_q, ff.sig_aload, ff.sig_q); // SILIMATE: Improve the naming + module->addMux(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_ad, ff.sig_d, ff.sig_aload, new_d); // SILIMATE: Improve the naming } else { - module->addMuxGate(NEW_ID, ff.sig_ad, new_q, ff.sig_aload, ff.sig_q); - module->addMuxGate(NEW_ID, ff.sig_ad, ff.sig_d, ff.sig_aload, new_d); + module->addMuxGate(NEW_ID2_SUFFIX("q_aload_mux"), ff.sig_ad, new_q, ff.sig_aload, ff.sig_q); // SILIMATE: Improve the naming + module->addMuxGate(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_ad, ff.sig_d, ff.sig_aload, new_d); // SILIMATE: Improve the naming } } @@ -250,18 +250,18 @@ struct Async2syncPass : public Pass { initvals.remove_init(ff.sig_q); - Wire *new_q = module->addWire(NEW_ID, ff.width); + Wire *new_q = module->addWire(NEW_ID2_SUFFIX("new_q"), ff.width); // SILIMATE: Improve the naming if (ff.pol_arst) { if (!ff.is_fine) - module->addMux(NEW_ID, new_q, ff.val_arst, ff.sig_arst, ff.sig_q); + module->addMux(NEW_ID2_SUFFIX("arst_mux"), new_q, ff.val_arst, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, new_q, ff.val_arst[0], ff.sig_arst, ff.sig_q); + module->addMuxGate(NEW_ID2_SUFFIX("arst_mux"), new_q, ff.val_arst[0], ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming } else { if (!ff.is_fine) - module->addMux(NEW_ID, ff.val_arst, new_q, ff.sig_arst, ff.sig_q); + module->addMux(NEW_ID2_SUFFIX("arst_mux"), ff.val_arst, new_q, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, ff.val_arst[0], new_q, ff.sig_arst, ff.sig_q); + module->addMuxGate(NEW_ID2_SUFFIX("arst_mux"), ff.val_arst[0], new_q, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming } ff.sig_q = new_q; @@ -284,21 +284,21 @@ struct Async2syncPass : public Pass { initvals.remove_init(ff.sig_q); - Wire *new_q = module->addWire(NEW_ID, ff.width); + Wire *new_q = module->addWire(NEW_ID2_SUFFIX("new_q"), ff.width); // SILIMATE: Improve the naming Wire *new_d; if (ff.has_aload) { - new_d = module->addWire(NEW_ID, ff.width); + new_d = module->addWire(NEW_ID2_SUFFIX("new_d"), ff.width); // SILIMATE: Improve the naming if (ff.pol_aload) { if (!ff.is_fine) - module->addMux(NEW_ID, new_q, ff.sig_ad, ff.sig_aload, new_d); + module->addMux(NEW_ID2_SUFFIX("d_aload_mux"), new_q, ff.sig_ad, ff.sig_aload, new_d); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, new_q, ff.sig_ad, ff.sig_aload, new_d); + module->addMuxGate(NEW_ID2_SUFFIX("d_aload_mux"), new_q, ff.sig_ad, ff.sig_aload, new_d); // SILIMATE: Improve the naming } else { if (!ff.is_fine) - module->addMux(NEW_ID, ff.sig_ad, new_q, ff.sig_aload, new_d); + module->addMux(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_ad, new_q, ff.sig_aload, new_d); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, ff.sig_ad, new_q, ff.sig_aload, new_d); + module->addMuxGate(NEW_ID2_SUFFIX("d_aload_mux"), ff.sig_ad, new_q, ff.sig_aload, new_d); // SILIMATE: Improve the naming } } else { new_d = new_q; @@ -310,36 +310,36 @@ struct Async2syncPass : public Pass { if (!ff.pol_set) { if (!ff.is_fine) - sig_set = module->Not(NEW_ID, sig_set); + sig_set = module->Not(NEW_ID2_SUFFIX("set_hi"), sig_set); // SILIMATE: Improve the naming else - sig_set = module->NotGate(NEW_ID, sig_set); + sig_set = module->NotGate(NEW_ID2_SUFFIX("set_hi"), sig_set); // SILIMATE: Improve the naming } if (ff.pol_clr) { if (!ff.is_fine) - sig_clr = module->Not(NEW_ID, sig_clr); + sig_clr = module->Not(NEW_ID2_SUFFIX("clr_lo"), sig_clr); // SILIMATE: Improve the naming else - sig_clr = module->NotGate(NEW_ID, sig_clr); + sig_clr = module->NotGate(NEW_ID2_SUFFIX("clr_lo"), sig_clr); // SILIMATE: Improve the naming } if (!ff.is_fine) { - SigSpec tmp = module->Or(NEW_ID, new_d, sig_set); - module->addAnd(NEW_ID, tmp, sig_clr, ff.sig_q); + SigSpec tmp = module->Or(NEW_ID2_SUFFIX("d_or_set"), new_d, sig_set); // SILIMATE: Improve the naming + module->addAnd(NEW_ID2_SUFFIX("q_sr"), tmp, sig_clr, ff.sig_q); // SILIMATE: Improve the naming } else { - SigSpec tmp = module->OrGate(NEW_ID, new_d, sig_set); - module->addAndGate(NEW_ID, tmp, sig_clr, ff.sig_q); + SigSpec tmp = module->OrGate(NEW_ID2_SUFFIX("d_or_set"), new_d, sig_set); // SILIMATE: Improve the naming + module->addAndGate(NEW_ID2_SUFFIX("q_sr"), tmp, sig_clr, ff.sig_q); // SILIMATE: Improve the naming } } else if (ff.has_arst) { if (ff.pol_arst) { if (!ff.is_fine) - module->addMux(NEW_ID, new_d, ff.val_arst, ff.sig_arst, ff.sig_q); + module->addMux(NEW_ID2_SUFFIX("arst_mux"), new_d, ff.val_arst, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, new_d, ff.val_arst[0], ff.sig_arst, ff.sig_q); + module->addMuxGate(NEW_ID2_SUFFIX("arst_mux"), new_d, ff.val_arst[0], ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming } else { if (!ff.is_fine) - module->addMux(NEW_ID, ff.val_arst, new_d, ff.sig_arst, ff.sig_q); + module->addMux(NEW_ID2_SUFFIX("arst_mux"), ff.val_arst, new_d, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming else - module->addMuxGate(NEW_ID, ff.val_arst[0], new_d, ff.sig_arst, ff.sig_q); + module->addMuxGate(NEW_ID2_SUFFIX("arst_mux"), ff.val_arst[0], new_d, ff.sig_arst, ff.sig_q); // SILIMATE: Improve the naming } } else { module->connect(ff.sig_q, new_d); diff --git a/passes/techmap/bwmuxmap.cc b/passes/techmap/bwmuxmap.cc index 7fe1cded7..3988daeb5 100644 --- a/passes/techmap/bwmuxmap.cc +++ b/passes/techmap/bwmuxmap.cc @@ -57,10 +57,10 @@ struct BwmuxmapPass : public Pass { auto &sig_b = cell->getPort(ID::B); auto &sig_s = cell->getPort(ID::S); - auto not_s = module->Not(NEW_ID, sig_s); - auto masked_b = module->And(NEW_ID, sig_s, sig_b); - auto masked_a = module->And(NEW_ID, not_s, sig_a); - module->addOr(NEW_ID, masked_a, masked_b, sig_y); + auto not_s = module->Not(NEW_ID2_SUFFIX("not_s"), sig_s); // SILIMATE: Improve the naming + auto masked_b = module->And(NEW_ID2_SUFFIX("masked_b"), sig_s, sig_b); // SILIMATE: Improve the naming + auto masked_a = module->And(NEW_ID2_SUFFIX("masked_a"), not_s, sig_a); // SILIMATE: Improve the naming + module->addOr(NEW_ID2_SUFFIX("y"), masked_a, masked_b, sig_y); // SILIMATE: Improve the naming module->remove(cell); } From b609976d7d5f829881b598835989aad0d0ef4d1b Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Mon, 6 Jul 2026 07:40:22 -0700 Subject: [PATCH 21/33] Add some cpp stuff --- .vscode/settings.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 25ffb81ba..f91385f77 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -71,6 +71,9 @@ "variant": "cpp", "algorithm": "cpp", "*.inc": "cpp", - "tuple": "cpp" + "tuple": "cpp", + "__verbose_trap": "cpp", + "cstddef": "cpp", + "__log_hardening_failure": "cpp" } } From b600028644d14803329277fd2651731f5b0aed5a Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Mon, 6 Jul 2026 07:40:47 -0700 Subject: [PATCH 22/33] Reduce verbosity of opt_dff --- passes/opt/opt_dff.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index 9563a672b..76fcb1125 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -702,7 +702,7 @@ struct OptDffWorker if (new_cell) dff_cells.push_back(new_cell); - log("Adding EN signal on %s (%s) from module %s (D = %s, Q = %s).\n", + log_debug("Adding EN signal on %s (%s) from module %s (D = %s, Q = %s).\n", cell, cell->type.unescape(), module, log_signal(new_ff.sig_d), log_signal(new_ff.sig_q)); } From 9d39a82587045feb1c85b005edcf1e30f41992b8 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Mon, 6 Jul 2026 07:41:08 -0700 Subject: [PATCH 23/33] Allow sim pass to handle 4-input gates --- passes/sat/sim.cc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/passes/sat/sim.cc b/passes/sat/sim.cc index 503e2dcb7..03c21649b 100644 --- a/passes/sat/sim.cc +++ b/passes/sat/sim.cc @@ -658,6 +658,13 @@ struct SimInstance else if (has_a && has_b && !has_c && !has_d && has_s && has_y) // (A,B,S -> Y) cells eval_state = CellTypes::eval(cell, get_state(sig_a), get_state(sig_b), get_state(sig_s), &err); + else if (has_a && has_b && has_c && has_d && !has_s && has_y) + // (A,B,C,D -> Y) cells, e.g. $_AOI4_/$_OAI4_ (CellTypes::eval implements these); + // without this case every 4-input gate fell through to the "unsupported" warning + // below, flooding the log (millions of lines on AOI/OAI-dense designs like AES) + // and leaving the output stuck at X. + eval_state = CellTypes::eval(cell, get_state(sig_a), get_state(sig_b), + get_state(sig_c), get_state(sig_d), &err); else err = true; From bab0ee6bf8cc7091c845502426d85c6d828d2a9f Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Mon, 6 Jul 2026 07:42:47 -0700 Subject: [PATCH 24/33] Speed up opt_* using cut_region with shared_ce --- passes/opt/cut_region.h | 14 ++++++ passes/opt/opt_priority_onehot.cc | 2 +- passes/silimate/opt_compact_prefix.cc | 61 +++++++++++++++++---------- 3 files changed, 54 insertions(+), 23 deletions(-) diff --git a/passes/opt/cut_region.h b/passes/opt/cut_region.h index 3387922ca..d5384b631 100644 --- a/passes/opt/cut_region.h +++ b/passes/opt/cut_region.h @@ -826,6 +826,20 @@ struct CutRegionWorker return sig; } + // Shared ConstEval for the whole matching phase. The netlist is not + // modified until rewrites are applied, so one instance (built once, at + // O(module) cost) serves every fingerprint eval instead of rebuilding + // one per candidate. Callers keep push/pop balanced, so the base state + // stays clean between uses (asserted on each hand-out). + std::unique_ptr shared_ce_ptr; + ConstEval &shared_ce() + { + if (!shared_ce_ptr) + shared_ce_ptr = std::make_unique(module); + log_assert(shared_ce_ptr->stack.empty()); + return *shared_ce_ptr; + } + // 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`. diff --git a/passes/opt/opt_priority_onehot.cc b/passes/opt/opt_priority_onehot.cc index 180a6cb33..eadc7615e 100644 --- a/passes/opt/opt_priority_onehot.cc +++ b/passes/opt/opt_priority_onehot.cc @@ -256,7 +256,7 @@ struct OptPriorityOnehotWorker : CutRegionWorker { bool fingerprint(const Candidate &cand, bool msb_first) { - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec out_sig = sigmap(cand.out_sig); SigSpec valid_sig = sigmap(cand.valid_sig); SigSpec field_sig = sigmap(cand.field_sig); diff --git a/passes/silimate/opt_compact_prefix.cc b/passes/silimate/opt_compact_prefix.cc index fdc902a64..c7b443682 100644 --- a/passes/silimate/opt_compact_prefix.cc +++ b/passes/silimate/opt_compact_prefix.cc @@ -236,7 +236,7 @@ struct OptCompactPrefixWorker : CutRegionWorker for (auto bit : en_cands) cand_bits.push_back(bit); - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec out_s = sigmap(root_sig); SigSpec data_s = sigmap(data_sig); SigSpec cand_s; @@ -415,7 +415,7 @@ struct OptCompactPrefixWorker : CutRegionWorker { if (width < 4) return false; - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec in_s = sigmap(in_sig); SigSpec out_s = sigmap(out_sig); @@ -511,7 +511,7 @@ struct OptCompactPrefixWorker : CutRegionWorker int wd = GetSize(dis_s); int wt = GetSize(data_s); bool self = sigmap(dis_s) == sigmap(data_s); - ConstEval ce(module); + ConstEval &ce = shared_ce(); for (int pol = 0; pol < 2; pol++) { // A self-pair (data == disable bus) can only be probed with all // lanes enabled AND data all-ones, i.e. enable-high polarity. @@ -546,7 +546,7 @@ struct OptCompactPrefixWorker : CutRegionWorker return false; if (lw > wd || lw > wt || wd > 62 || wt > 62) return false; - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec dis_s = sigmap(dis_sig); SigSpec data_s = sigmap(data_sig); SigSpec out_s = sigmap(out_sig); @@ -615,8 +615,14 @@ struct OptCompactPrefixWorker : CutRegionWorker return mask; } + // Evaluate the modulo vector set once and test all four + // (msb_first, offset) variants against the recorded outputs, in the + // original priority order with first-match tie-break. This replaces up + // to four full ConstEval sweeps (one per variant) with a single sweep; + // sets out_msb_first/out_offset on the first surviving variant. bool fingerprint_modulo(const SigSpec &en_sig, const SigSpec &n_sig, - const SigSpec &mask_sig, int width, bool msb_first, int offset) + const SigSpec &mask_sig, int width, + bool &out_msb_first, int &out_offset) { if (width <= 0 || width > 62) return false; @@ -624,7 +630,7 @@ struct OptCompactPrefixWorker : CutRegionWorker // also matches plain gating muxes; require a real counter range. if (GetSize(n_sig) < 2) return false; - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec en_s = sigmap(en_sig); SigSpec n_s = sigmap(n_sig); SigSpec mask_s = sigmap(mask_sig); @@ -653,6 +659,12 @@ struct OptCompactPrefixWorker : CutRegionWorker envals.push_back(lfsr & full); } + // Variants in the original check order (msb/lsb x offset 0/1). + static const struct { bool msb; int off; } variants[] = { + {true, 0}, {false, 0}, {false, 1}, {true, 1} + }; + bool alive[4] = {true, true, true, true}; + for (uint64_t nv : nvals) for (uint64_t ev : envals) { uint64_t actual; @@ -660,10 +672,27 @@ struct OptCompactPrefixWorker : CutRegionWorker {n_s, const_u64(nv, nw)}}, mask_s, actual)) return false; - if (actual != expected_modulo_mask(ev, nv, width, msb_first, offset)) + bool any = false; + for (int v = 0; v < 4; v++) { + if (!alive[v]) + continue; + if (actual != expected_modulo_mask(ev, nv, width, + variants[v].msb, variants[v].off)) + alive[v] = false; + else + any = true; + } + if (!any) return false; } - return true; + + for (int v = 0; v < 4; v++) + if (alive[v]) { + out_msb_first = variants[v].msb; + out_offset = variants[v].off; + return true; + } + return false; } // --- Forward gather/compress: out[k] = data[i_k] where i_k is the @@ -693,7 +722,7 @@ struct OptCompactPrefixWorker : CutRegionWorker { if (width < 4 || width > 62) return false; - ConstEval ce(module); + ConstEval &ce = shared_ce(); SigSpec en_s = sigmap(en_sig); SigSpec data_s = sigmap(data_sig); SigSpec out_s = sigmap(out_sig); @@ -1397,19 +1426,7 @@ struct OptCompactPrefixWorker : CutRegionWorker fp_attempts++; bool msb_first = false; int offset = -1; - if (fingerprint_modulo(en.sig, n.sig, root.sig, width, true, 0)) { - msb_first = true; - offset = 0; - } else if (fingerprint_modulo(en.sig, n.sig, root.sig, width, false, 0)) { - msb_first = false; - offset = 0; - } else if (fingerprint_modulo(en.sig, n.sig, root.sig, width, false, 1)) { - msb_first = false; - offset = 1; - } else if (fingerprint_modulo(en.sig, n.sig, root.sig, width, true, 1)) { - msb_first = true; - offset = 1; - } else { + if (!fingerprint_modulo(en.sig, n.sig, root.sig, width, msb_first, offset)) { log_debug(" mod %s/%s: fingerprint mismatch\n", en.name.c_str(), n.name.c_str()); continue; } From 3aa52fb1e5547785aafcd621efe81c6c4fe27916 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Mon, 6 Jul 2026 07:44:01 -0700 Subject: [PATCH 25/33] Pass carvenetlist --- passes/silimate/CMakeLists.txt | 3 + passes/silimate/carvenetlist.cc | 1017 +++++++++++++++++++++++++++++++ tests/silimate/carvenetlist.ys | 457 ++++++++++++++ 3 files changed, 1477 insertions(+) create mode 100644 passes/silimate/carvenetlist.cc create mode 100644 tests/silimate/carvenetlist.ys diff --git a/passes/silimate/CMakeLists.txt b/passes/silimate/CMakeLists.txt index 734a5a7cd..f948202fd 100644 --- a/passes/silimate/CMakeLists.txt +++ b/passes/silimate/CMakeLists.txt @@ -50,6 +50,9 @@ yosys_pass(splitlarge yosys_pass(splitnetlist splitnetlist.cc ) +yosys_pass(carvenetlist + carvenetlist.cc +) yosys_pass(opt_timing_balance opt_timing_balance.cc ) diff --git a/passes/silimate/carvenetlist.cc b/passes/silimate/carvenetlist.cc new file mode 100644 index 000000000..8c235e30a --- /dev/null +++ b/passes/silimate/carvenetlist.cc @@ -0,0 +1,1017 @@ +/* + * 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 +#include + +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 "_" 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_/fast_ and OpenIP designs as slow_design_/ +// fast_design_, but downstream parsing/decryption expects and design__ +// . 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 \"_\":\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 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._") 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 ("...___pqi[j]") and the capture-flop output net + // ("...___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 pqi_bit_of_group; // group rep -> a "__pqi" bit in that group + pool 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> driver; // net -> (cell, out pin) + dict>> loads; // net -> [(cell, in pin)] + dict> 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 clk_nets; + { + std::vector 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 ins, outs; + std::vector in_wires, out_wires; + }; + std::map 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 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 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 boundary; + 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 launch, capture, boundary_cells; + pool 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 logic; + pool driven = seed; + std::vector 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 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); + 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; + + // 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._" so the trailing port cleanup (which drops the + // scope-dot prefix) recovers the clean "_" 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 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_/fast_design_) 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 "___pqi" boundary name. Per input pin, + // build a fresh "__pqcarve._" 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 "_" 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_" input port even after part (1) rebuilds the + // clean "__pqcarve._" 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_" 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 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_" 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 "_" 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_" 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 proc(logic.begin(), logic.end()); + pool queued = logic; + while (!proc.empty()) { + Cell *c = proc.back(); + proc.pop_back(); + dict 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] = {base.rfind("fast", 0) == 0 ? "fast" : "slow", 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."); + // 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> 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_.slow__A" -> "slow__A", then collapse any remaining dots + // (e.g. exposed clock-gate pins like "slow_.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._", + // tagged PQ_CARVE_OUT) first. They collapse to the clean "_" output + // port and are the authoritative output. A degenerate output bus (const-shift / + // feed-through / self-referential cell) can leave a stale "___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 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 "___pqi" (carrying the directly-read bits) + // and the scope-prefixed ".___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 ("___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(mod->name), log_id(oldid), log_id(newid), 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 "___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 SINGLE-FANOUT 1-input/1-output cell (a lone drive-strength buffer, a + // polarity/QN-flop inverter feeding one load, or a cell->capture-flop output buffer) with + // a direct wire: its one load simply moves to the upstream driver, so delay/power are + // unchanged, and each simple cell 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 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_/design_fast_) 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 drv; + dict> rdr; + dict 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 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 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 diff --git a/tests/silimate/carvenetlist.ys b/tests/silimate/carvenetlist.ys new file mode 100644 index 000000000..402f31ac4 --- /dev/null +++ b/tests/silimate/carvenetlist.ys @@ -0,0 +1,457 @@ +# ============================================================================= +# carvenetlist: carve surround-with-flops cells into per-cell modules. +# +# Each "train" below is one flat cell-under-test surrounded by launch/capture +# flops (ml.dataset.surround_with_flops form): every data input is a launch +# flop's Q (named ___pqi) and every data output feeds a capture flop's +# D (named ___pqo), with a shared fast_clk/slow_clk. carvenetlist must +# carve the logic between the flops into module `cell` with a clean input port +# fast_cell_A and a clean output port fast_cell_Y (never inout), preserving +# feed-throughs (output bit = input bit, or = constant) as plain assigns. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Test 1: feed-through output bit (Y[0] = A[0]). +# Regression: the bare alias used to fuse the launch-side input net with the +# capture-side output net, leaving Y[0] undriven and dragging the Y bus to inout. +# ----------------------------------------------------------------------------- +log -header "Feed-through output bit (Y[0] = A[0])" +log -push +design -reset +read_rtlil < Date: Mon, 6 Jul 2026 07:47:32 -0700 Subject: [PATCH 26/33] Merge from upstream --- .github/actions/setup-build-env/action.yml | 2 +- .github/workflows/extra-builds.yml | 20 - .github/workflows/nix.yml | 24 + .github/workflows/test-build.yml | 2 +- abc | 2 +- kernel/log.cc | 6 - kernel/log.h | 13 - passes/opt/opt_dff.cc | 489 ++++++++++++++++++++- tests/opt/opt_dff_eqbits.ys | 89 ++++ tests/opt/opt_dff_eqbits_large.sv | 231 ++++++++++ tests/opt/opt_dff_eqbits_small.sv | 30 ++ 11 files changed, 865 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/nix.yml create mode 100644 tests/opt/opt_dff_eqbits.ys create mode 100644 tests/opt/opt_dff_eqbits_large.sv create mode 100644 tests/opt/opt_dff_eqbits_small.sv diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index 86d7c21ec..8b3e2d769 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -56,7 +56,7 @@ runs: if: runner.os == 'macOS' shell: bash run: | - brew bundle + brew install bison flex gawk libffi git pkg-config python3 bash googletest tcl-tk llvm - name: Linux runtime environment if: runner.os == 'Linux' diff --git a/.github/workflows/extra-builds.yml b/.github/workflows/extra-builds.yml index 644529c9d..d041794d7 100644 --- a/.github/workflows/extra-builds.yml +++ b/.github/workflows/extra-builds.yml @@ -172,32 +172,12 @@ jobs: cmake -B build -DCMAKE_TOOLCHAIN_FILE=${WASI_SDK_PATH}/share/cmake/wasi-sdk-p1.cmake -DCMAKE_BUILD_TYPE=Release -DYOSYS_COMPILER_LAUNCHER=ccache . cmake --build build -j$(nproc) - nix-build: - name: "Build nix flake" - needs: pre_job - if: (github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch') && needs.pre_job.outputs.should_skip != 'true' - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - fail-fast: false - steps: - - uses: actions/checkout@v7 - with: - submodules: true - persist-credentials: false - - uses: cachix/install-nix-action@v31 - with: - install_url: https://releases.nixos.org/nix/nix-2.30.0/install - - run: nix build -L - extra-builds-result: runs-on: ubuntu-latest needs: - vs-build - mingw-build - wasi-build - - nix-build if: always() steps: - name: Check results diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 000000000..120240bd6 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,24 @@ +name: Test nix build + +on: + workflow_dispatch: + schedule: + - cron: '0 5 * * 6' + +jobs: + nix-build: + name: "Build nix flake" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + fail-fast: false + steps: + - uses: actions/checkout@v7 + with: + submodules: true + persist-credentials: false + - uses: cachix/install-nix-action@v31 + with: + install_url: https://releases.nixos.org/nix/nix-2.30.0/install + - run: nix build -L diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 01a7e505c..2d641deec 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -49,7 +49,7 @@ jobs: - id: set_output run: | if [ "${{ github.event_name }}" = "merge_group" ]; then - echo "should_skip=false" >> $GITHUB_OUTPUT + echo "should_skip=true" >> $GITHUB_OUTPUT elif [ "${{ github.event_name }}" = "push" ]; then should_skip=false else diff --git a/abc b/abc index 648de7a21..bec0695f0 160000 --- a/abc +++ b/abc @@ -1 +1 @@ -Subproject commit 648de7a21751cfab01c253aca6d4751f642577df +Subproject commit bec0695f0de539e974d95ab845e581da326ea3d7 diff --git a/kernel/log.cc b/kernel/log.cc index 2f5a6350b..2c3e45c2b 100644 --- a/kernel/log.cc +++ b/kernel/log.cc @@ -388,12 +388,6 @@ void log_formatted_file_error(std::string_view filename, int lineno, std::string log_error_with_prefix(prefix, str); } -void logv_file_error(const string &filename, int lineno, - const char *format, va_list ap) -{ - log_formatted_file_error(filename, lineno, vstringf(format, ap)); -} - void log_experimental(const std::string &str) { if (log_experimentals_ignored.count(str) == 0 && log_experimentals.count(str) == 0) { diff --git a/kernel/log.h b/kernel/log.h index 673c27ce2..f320537ad 100644 --- a/kernel/log.h +++ b/kernel/log.h @@ -24,7 +24,6 @@ #include -#include #include #define YS_REGEX_COMPILE(param) std::regex(param, \ std::regex_constants::nosubs | \ @@ -44,20 +43,11 @@ # endif #endif -#if defined(_MSC_VER) -// At least this is not in MSVC++ 2013. -# define __PRETTY_FUNCTION__ __FUNCTION__ -#endif - // from libs/sha1/sha1.h class SHA1; YOSYS_NAMESPACE_BEGIN -#define S__LINE__sub2(x) #x -#define S__LINE__sub1(x) S__LINE__sub2(x) -#define S__LINE__ S__LINE__sub1(__LINE__) - // YS_DEBUGTRAP is a macro that is functionally equivalent to a breakpoint // if the platform provides such functionality, and does nothing otherwise. // If no debugger is attached, it starts a just-in-time debugger if available, @@ -120,9 +110,6 @@ extern int log_make_debug; extern int log_force_debug; extern int log_debug_suppressed; -[[deprecated]] -[[noreturn]] void logv_file_error(const string &filename, int lineno, const char *format, va_list ap); - void set_verific_logging(void (*cb)(int msg_type, const char *message_id, const char* file_path, unsigned int left_line, unsigned int left_col, unsigned int right_line, unsigned int right_col, const char *msg)); extern void (*log_verific_callback)(int msg_type, const char *message_id, const char* file_path, unsigned int left_line, unsigned int left_col, unsigned int right_line, unsigned int right_col, const char *msg); diff --git a/passes/opt/opt_dff.cc b/passes/opt/opt_dff.cc index 76fcb1125..a68184cae 100644 --- a/passes/opt/opt_dff.cc +++ b/passes/opt/opt_dff.cc @@ -43,6 +43,91 @@ struct OptDffOptions bool keepdc; }; +// Bit-parallel random simulation used as a cheap pre-filter for equivalence +struct BitSim { + Module *module; + SigMap &sigmap; + ModWalker &modwalker; + dict sim_vals; + uint64_t rng_state; + int max_depth; + int evals_left; + + BitSim(Module *m, SigMap &sm, ModWalker &mw) + : module(m), sigmap(sm), modwalker(mw), rng_state(1337) + { + max_depth = module->design->scratchpad_get_int("opt_dff.sim_depth", 10000); + evals_left = module->design->scratchpad_get_int("opt_dff.sim_evals", 1000000); + } + + uint64_t next_rand() { + uint32_t lo = mkhash_xorshift((uint32_t)rng_state); + uint32_t hi = mkhash_xorshift((uint32_t)(rng_state >> 32) ^ lo); + rng_state = ((uint64_t)hi << 32) | lo; + return rng_state; + } + + uint64_t eval_bit(SigBit b, int depth = 0) { + SigBit mapped = sigmap(b); + if (mapped == State::S0) return 0ULL; + if (mapped == State::S1) return ~0ULL; + if (mapped == State::Sx || mapped == State::Sz) return 0ULL; + + auto it = sim_vals.find(mapped); + if (it != sim_vals.end()) return it->second; + + // Failsafe for huge designs + if (depth >= max_depth || evals_left <= 0) { + uint64_t r = next_rand(); + sim_vals[mapped] = r; + return r; + } + evals_left--; + + sim_vals[mapped] = 0; + uint64_t res = 0; + + if (!modwalker.has_drivers(mapped)) { + res = next_rand(); + } else { + auto &drivers = modwalker.signal_drivers[mapped]; + if (drivers.empty()) { + res = next_rand(); + } else { + auto driver = *drivers.begin(); + Cell *cell = driver.cell; + + if (cell->is_builtin_ff()) { + res = next_rand(); + } else if (cell->type == ID($_AND_)) { + res = eval_bit(cell->getPort(ID::A)[0], depth+1) & eval_bit(cell->getPort(ID::B)[0], depth+1); + } else if (cell->type == ID($_OR_)) { + res = eval_bit(cell->getPort(ID::A)[0], depth+1) | eval_bit(cell->getPort(ID::B)[0], depth+1); + } else if (cell->type == ID($_XOR_)) { + res = eval_bit(cell->getPort(ID::A)[0], depth+1) ^ eval_bit(cell->getPort(ID::B)[0], depth+1); + } else if (cell->type == ID($_NOT_)) { + res = ~eval_bit(cell->getPort(ID::A)[0], depth+1); + } else if (cell->type == ID($_MUX_)) { + uint64_t s = eval_bit(cell->getPort(ID::S)[0], depth+1); + uint64_t a = eval_bit(cell->getPort(ID::A)[0], depth+1); + uint64_t b = eval_bit(cell->getPort(ID::B)[0], depth+1); + res = (a & ~s) | (b & s); + } else if (cell->type == ID($mux)) { + uint64_t s = eval_bit(cell->getPort(ID::S)[0], depth+1); + uint64_t a = eval_bit(cell->getPort(ID::A)[driver.offset], depth+1); + uint64_t b = eval_bit(cell->getPort(ID::B)[driver.offset], depth+1); + res = (a & ~s) | (b & s); + } else { + res = next_rand(); + } + } + } + + sim_vals[mapped] = res; + return res; + } +}; + struct OptDffWorker { const OptDffOptions &opt; @@ -921,6 +1006,397 @@ struct OptDffWorker return did_something; } + + struct EqBit { + Cell *cell; + int idx; + SigBit q; + }; + + // NOTE: This intentionally duplicates a subset of FfData, as flattening just the + // fields that matter for merging into a single comparable/hashable key is cheaper + struct SigKey { + enum Flag : uint16_t { + InitOne = 1u << 0, + InitX = 1u << 1, + PolClk = 1u << 2, + PolCe = 1u << 3, + PolSrst = 1u << 4, + PolArst = 1u << 5, + PolAload = 1u << 6, + PolClr = 1u << 7, + PolSet = 1u << 8, + CeOverSrst = 1u << 9, + }; + + SigBit clk, ce, srst, arst, aload, clr, set; + IdString cell_type; // for SR + uint16_t flags; + + bool operator==(const SigKey &o) const { + return flags == o.flags && clk == o.clk && ce == o.ce && srst == o.srst && arst == o.arst + && aload == o.aload && clr == o.clr && set == o.set && cell_type == o.cell_type; + } + + Hasher hash_into(Hasher h) const { + h.eat(flags); + h.eat(clk); + h.eat(ce); + h.eat(srst); + h.eat(arst); + h.eat(aload); + h.eat(clr); + h.eat(set); + h.eat(cell_type); + return h; + } + }; + + bool is_def(State s) { + // Concrete constant bit (0 or 1), as opposed to x/z + return s == State::S0 || s == State::S1; + } + + std::vector> gather_initial_eq_classes(std::vector &bits, dict &ff_for_cell) + { + std::vector keys; + + // Collect FF bits eligible for merging + for (auto cell : module->selected_cells()) { + if (!cell->is_builtin_ff()) + continue; + + FfData ff(&initvals, cell); + if (!ff.has_clk && !ff.has_gclk) + continue; + + ff_for_cell.emplace(cell, ff); + + for (int i = 0; i < ff.width; i++) { + // Skip bits whose reset value is undefined (x) + if (ff.has_srst && !is_def(ff.val_srst[i])) continue; + if (ff.has_arst && !is_def(ff.val_arst[i])) continue; + + // Class members are assumed equal in the current cycle and proven equal in the next, which needs + // a base case anchoring them to a common known value + bool def_init = is_def(ff.val_init[i]); + if (!def_init && !ff.has_srst && !ff.has_arst) + continue; + + SigKey k = {}; + + // Flags + if (def_init && ff.val_init[i] == State::S1) + k.flags |= SigKey::InitOne; + else if (!def_init) + k.flags |= SigKey::InitX; + + if (ff.has_clk) { + k.clk = ff.sig_clk; + if (ff.pol_clk) k.flags |= SigKey::PolClk; + } + if (ff.has_ce) { + k.ce = ff.sig_ce; + if (ff.pol_ce) k.flags |= SigKey::PolCe; + } + if (ff.has_srst) { + k.srst = ff.sig_srst; + if (ff.pol_srst) k.flags |= SigKey::PolSrst; + if (ff.ce_over_srst) k.flags |= SigKey::CeOverSrst; + } + if (ff.has_arst) { + k.arst = ff.sig_arst; + if (ff.pol_arst) k.flags |= SigKey::PolArst; + } + if (ff.has_aload) { + k.aload = ff.sig_aload; + if (ff.pol_aload) k.flags |= SigKey::PolAload; + } + if (ff.has_sr) { + k.clr = ff.sig_clr[i]; + k.set = ff.sig_set[i]; + k.cell_type = cell->type; + if (ff.pol_clr) k.flags |= SigKey::PolClr; + if (ff.pol_set) k.flags |= SigKey::PolSet; + } + + bits.push_back({cell, i, ff.sig_q[i]}); + keys.push_back(k); + } + } + + dict> buckets; + for (int i = 0; i < GetSize(bits); i++) + buckets[keys[i]].push_back(i); + + std::vector> classes; + for (auto &kv : buckets) + if (GetSize(kv.second) >= 2) + classes.push_back(std::move(kv.second)); + + return classes; + } + + std::vector> filter_classes_sim( + const std::vector> &classes, + const std::vector &bits, + const dict &ff_for_cell, + ModWalker &modwalker + ) { + BitSim sim(module, sigmap, modwalker); + + // Assume same class + for (auto &cls : classes) { + uint64_t class_q_val = sim.next_rand(); + for (int idx : cls) { + sim.sim_vals[sigmap(bits[idx].q)] = class_q_val; + } + } + + std::vector> refined_classes; + for (auto &cls : classes) { + dict> sim_buckets; + for (int idx : cls) { + const EqBit &eb = bits[idx]; + const FfData &ff = ff_for_cell.at(eb.cell); + uint64_t n_val = sim.eval_bit(ff.sig_d[eb.idx]); + + if (ff.has_aload) { + uint64_t al = sim.eval_bit(ff.sig_aload); + if (!ff.pol_aload) al = ~al; + uint64_t ad = sim.eval_bit(ff.sig_ad[eb.idx]); + n_val = (n_val & ~al) | (ad & al); + } + if (ff.has_arst) { + uint64_t ar = sim.eval_bit(ff.sig_arst); + if (!ff.pol_arst) ar = ~ar; + uint64_t ar_val = (ff.val_arst[eb.idx] == State::S1) ? ~0ULL : 0ULL; + n_val = (n_val & ~ar) | (ar_val & ar); + } + if (ff.has_sr) { + uint64_t clr = sim.eval_bit(ff.sig_clr[eb.idx]); + if (!ff.pol_clr) clr = ~clr; + uint64_t set = sim.eval_bit(ff.sig_set[eb.idx]); + if (!ff.pol_set) set = ~set; + n_val = ~clr & (set | n_val); + } + if (ff.has_srst) { + uint64_t srst = sim.eval_bit(ff.sig_srst); + if (!ff.pol_srst) srst = ~srst; + uint64_t srst_val = (ff.val_srst[eb.idx] == State::S1) ? ~0ULL : 0ULL; + n_val = (n_val & ~srst) | (srst_val & srst); + } + + sim_buckets[n_val].push_back(idx); + } + + for (auto &kv : sim_buckets) + if (GetSize(kv.second) >= 2) + refined_classes.push_back(std::move(kv.second)); + } + + return refined_classes; + } + + std::vector> filter_classes_sat( + std::vector> classes, + const std::vector &bits, + const dict &ff_for_cell, + ModWalker &modwalker + ) { + QuickConeSat qcsat(modwalker); + std::vector q_lit(bits.size(), -1); + std::vector n_lit(bits.size(), -1); + + // Build the next-state function n_lit[idx] of every candidate bit by + // folding the FF's control logic on top of the D input (-> next value) + + // Two bits are equivalent if their next states always agree whenever their + // current states (and those of every other candidate pair) agree + for (auto &cls : classes) { + for (int idx : cls) { + const EqBit &eb = bits[idx]; + const FfData &ff = ff_for_cell.at(eb.cell); + q_lit[idx] = qcsat.importSigBit(eb.q); + int n = qcsat.importSigBit(ff.sig_d[eb.idx]); + + if (ff.has_aload) { + int al = qcsat.importSigBit(ff.sig_aload); + if (!ff.pol_aload) al = qcsat.ez->NOT(al); + n = qcsat.ez->ITE(al, qcsat.importSigBit(ff.sig_ad[eb.idx]), n); + } + if (ff.has_arst) { + int ar = qcsat.importSigBit(ff.sig_arst); + if (!ff.pol_arst) ar = qcsat.ez->NOT(ar); + n = qcsat.ez->ITE(ar, qcsat.ez->value(ff.val_arst[eb.idx] == State::S1), n); + } + if (ff.has_sr) { + int clr = qcsat.importSigBit(ff.sig_clr[eb.idx]); + if (!ff.pol_clr) clr = qcsat.ez->NOT(clr); + int set = qcsat.importSigBit(ff.sig_set[eb.idx]); + if (!ff.pol_set) set = qcsat.ez->NOT(set); + n = qcsat.ez->AND(qcsat.ez->NOT(clr), qcsat.ez->OR(set, n)); + } + if (ff.has_srst) { + int srst = qcsat.importSigBit(ff.sig_srst); + if (!ff.pol_srst) srst = qcsat.ez->NOT(srst); + n = qcsat.ez->ITE(srst, qcsat.ez->value(ff.val_srst[eb.idx] == State::S1), n); + } + + n_lit[idx] = n; + } + } + + qcsat.prepare(); + + // Assume the induction hypo (that every current class is internally equal in the present cycle), and try + // to prove that the members of each class therefore also agree in the next cycle + + // A class survives only if no counterexample exists under that hypo, so combined with the common init/reset + // value that every class shares, this makes the equality an inductive invariant -> bits are eq and safe to merge + std::vector worklist; + std::vector in_worklist(GetSize(classes), true); + + for (int i = 0; i < GetSize(classes); i++) + worklist.push_back(i); + + while (!worklist.empty()) { + int cls_idx = worklist.back(); + worklist.pop_back(); + in_worklist[cls_idx] = false; + + auto &cls = classes[cls_idx]; + if (GetSize(cls) < 2) continue; + + // Induction hypo: assume every candidate class is equal + std::vector assumptions; + for (auto &c : classes) { + if (GetSize(c) < 2) continue; + int rep = c[0]; + for (int k = 1; k < GetSize(c); k++) + assumptions.push_back(qcsat.ez->IFF(q_lit[rep], q_lit[c[k]])); + } + + // Scan the class members against the representative and issue a query per pair, + // stopping early at the first counterexample, which is reused to split the entire + // class at once + int rep = cls[0]; + for (int i = 1; i < GetSize(cls); i++) { + if (n_lit[rep] == n_lit[cls[i]]) + continue; + + // Can the next state of the rep and this member ever differ? + int query = qcsat.ez->XOR(n_lit[rep], n_lit[cls[i]]); + // Capture every member's next-state value in that model so one counterexample + // partitions the whole class + std::vector modelExprs; + for (int b : cls) + modelExprs.push_back(n_lit[b]); + + std::vector modelVals; + assumptions.push_back(query); + + if (qcsat.ez->solve(modelExprs, modelVals, assumptions)) { + // SAT -> partition entire class + std::vector sub0; + std::vector sub1; + + for (size_t b_idx = 0; b_idx < cls.size(); b_idx++) { + if (modelVals[b_idx]) + sub1.push_back(cls[b_idx]); + else + sub0.push_back(cls[b_idx]); + } + + classes[cls_idx] = std::move(sub0); + classes.push_back(std::move(sub1)); + in_worklist.push_back(false); + + // Partition was split -> the induction hypo weakened + for (int j = 0; j < GetSize(classes); j++) { + if (GetSize(classes[j]) >= 2 && !in_worklist[j]) { + worklist.push_back(j); + in_worklist[j] = true; + } + } + + break; // Process new splits + } + + assumptions.pop_back(); // Remove query for the next pairwise check if UNSAT + } + } + + return classes; + } + + bool apply_eq_merges(const std::vector> &classes, const std::vector &bits, dict &ff_for_cell) + { + bool any_change = false; + dict> remove_bits; + + // Drive every non-rep Q from its class rep, drop merged bits from their FFs + for (auto &cls : classes) { + if (GetSize(cls) < 2) + continue; + SigBit rep_q = bits[cls[0]].q; + any_change = true; + for (int k = 1; k < GetSize(cls); k++) { + const EqBit &eb = bits[cls[k]]; + initvals.remove_init(eb.q); + module->connect(eb.q, rep_q); + remove_bits[eb.cell].insert(eb.idx); + } + } + + for (auto &kv : remove_bits) { + Cell *cell = kv.first; + const std::set &drop = kv.second; + FfData &ff = ff_for_cell.at(cell); + std::vector keep; + + for (int i = 0; i < ff.width; i++) + if (!drop.count(i)) + keep.push_back(i); + + if (keep.empty()) { + module->remove(cell); + } else { + FfData new_ff = ff.slice(keep); + new_ff.cell = cell; + new_ff.emit(); + } + } + + return any_change; + } + + bool run_eqbits() + { + if (!opt.sat) + return false; + + std::vector bits; + dict ff_for_cell; + + std::vector> classes = gather_initial_eq_classes(bits, ff_for_cell); + if (classes.empty()) + return false; + + ModWalker modwalker(module->design, module); + + // Simulation prepass + classes = filter_classes_sim(classes, bits, ff_for_cell, modwalker); + if (classes.empty()) + return false; + + // SAT prove + classes = filter_classes_sat(std::move(classes), bits, ff_for_cell, modwalker); + if (classes.empty()) + return false; + + return apply_eq_merges(classes, bits, ff_for_cell); + } }; struct OptDffPass : public Pass { @@ -950,7 +1426,9 @@ struct OptDffPass : public Pass { log("\n"); log(" -sat\n"); log(" additionally invoke SAT solver to detect and remove flip-flops (with\n"); - log(" non-constant inputs) that can also be replaced with a constant driver\n"); + log(" non-constant inputs) that can also be replaced with a constant driver,\n"); + log(" or merged with equivalent flip-flops. this reasons in 2-valued logic\n"); + log(" and may resolve don't-care bits, so it is incompatible with -keepdc.\n"); log("\n"); log(" -keepdc\n"); log(" some optimizations change the behavior of the circuit with respect to\n"); @@ -982,6 +1460,13 @@ struct OptDffPass : public Pass { } extra_args(args, argidx, design); + // The SAT engine reasons in 2-valued logic (a constant x is treated as + // 0), so it can resolve don't-care bits to concrete values -- exactly + // what -keepdc promises not to do. Refuse the combination rather than + // silently ignore -keepdc. + if (opt.sat && opt.keepdc) + log_cmd_error("The -sat and -keepdc options are mutually exclusive.\n"); + bool did_something = false; for (auto mod : design->selected_modules()) { OptDffWorker worker(opt, mod); @@ -989,6 +1474,8 @@ struct OptDffPass : public Pass { did_something = true; if (worker.run_constbits()) did_something = true; + if (worker.run_eqbits()) + did_something = true; } if (did_something) diff --git a/tests/opt/opt_dff_eqbits.ys b/tests/opt/opt_dff_eqbits.ys new file mode 100644 index 000000000..f990fe71a --- /dev/null +++ b/tests/opt/opt_dff_eqbits.ys @@ -0,0 +1,89 @@ +# small test case +design -reset +read_verilog -sv opt_dff_eqbits_small.sv +hierarchy -top test_case +techmap +opt_dff -sat +synth +opt_dff -sat +opt_clean -purge + +select -assert-count 2 t:$_SDFF_PN0_ + +# equivalence +design -reset +read_verilog -sv opt_dff_eqbits_small.sv +hierarchy -top test_case +prep +design -save gold + +opt_dff -sat +design -save gate + +design -copy-from gold -as gold test_case +design -copy-from gate -as gate test_case +equiv_make gold gate equiv +equiv_induct equiv +equiv_status -assert + + +# large test case +design -reset +read_verilog -sv opt_dff_eqbits_large.sv +hierarchy -top test_case +techmap +opt_dff -sat +synth +opt_dff -sat +opt_clean -purge + +select -assert-count 6 t:$_SDFFE_PN0P_ + +# equivalence +design -reset +read_verilog -sv opt_dff_eqbits_large.sv +hierarchy -top test_case +prep +design -save gold + +opt_dff -sat +design -save gate + +design -copy-from gold -as gold test_case +design -copy-from gate -as gate test_case +equiv_make gold gate equiv +equiv_induct equiv +equiv_status -assert + +# verify keepdc exclusivity +design -reset +read_verilog -sv < Date: Mon, 6 Jul 2026 09:13:18 -0700 Subject: [PATCH 27/33] 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_ and fast_ -> ). - derive pq_speed from the explicit "fast_" base prefix. Co-authored-by: Cursor --- passes/silimate/carvenetlist.cc | 44 +++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/passes/silimate/carvenetlist.cc b/passes/silimate/carvenetlist.cc index 8c235e30a..4e99c4486 100644 --- a/passes/silimate/carvenetlist.cc +++ b/passes/silimate/carvenetlist.cc @@ -344,6 +344,7 @@ struct CarveNetlistPass : public Pass { }; std::map boundary; + std::map 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; @@ -439,6 +440,15 @@ struct CarveNetlistPass : public Pass { // 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_" and "fast_" would map both to "" -- 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); @@ -448,6 +458,11 @@ struct CarveNetlistPass : public Pass { // 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 "_" prefix ("fast_" covers both + // "fast_" and "fast_design_"). 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 @@ -722,7 +737,7 @@ struct CarveNetlistPass : public Pass { } } - boundary[final] = {base.rfind("fast", 0) == 0 ? "fast" : "slow", drv_cell, drv_pin, load_cell, load_pin}; + boundary[final] = {speed, drv_cell, drv_pin, load_cell, load_pin}; } log("Carved %d cells (%d logic cells tagged)\n", GetSize(boundary), tagged_total); @@ -834,7 +849,7 @@ struct CarveNetlistPass : public Pass { } log_warning("carvenetlist: cannot unify boundary port '%s' -> '%s' in " "module %s (widths %d vs %d differ); leaving original name.\n", - log_id(mod->name), log_id(oldid), log_id(newid), wire->width, + log_id(oldid), log_id(newid), log_id(mod->name), wire->width, ex->width); continue; } @@ -904,12 +919,18 @@ struct CarveNetlistPass : public Pass { } } - // Replace a SINGLE-FANOUT 1-input/1-output cell (a lone drive-strength buffer, a - // polarity/QN-flop inverter feeding one load, or a cell->capture-flop output buffer) with - // a direct wire: its one load simply moves to the upstream driver, so delay/power are - // unchanged, and each simple cell 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. + // 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, @@ -973,6 +994,13 @@ struct CarveNetlistPass : public Pass { 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) + From 00e48706df95ae8afe423e3b7d1edbee2740835a Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Mon, 6 Jul 2026 12:56:38 -0700 Subject: [PATCH 28/33] opt: recognize three QoR logic-depth patterns Extend two existing opt passes and add one new pass to collapse serial/dynamic-index structures that were leaving high logic depth: - opt_first_fit_alloc: recognize the "coalesce-matrix" first-fit allocator variant (same_cat[i][k] coalescing gated on the leader's enable, driven from a raw input enable). Rewrite both the lane_slot allocation and the xbar field gather from one shared log-depth scan. - opt_prienc: detect round-robin / rotated-priority scans (req scanned from idx_last downward with wraparound) and rewrite the depth-N idx--/req[idx] mux chain to rotate -> log-depth priority-encode -> unrotate. - opt_priokey (new): recognize priority-by-key one-hot accumulators and replace each dynamic taken[key] read ($shiftx/$bmux) with the equivalent pairwise-key-compare reduction, dropping the wide dynamic indexing. Supports -strict for full-key-range formal validation. Each includes self-contained tests (equiv_opt / sat -prove-asserts, mux-bound and negative cases) in tests/opt/. Co-authored-by: Cursor --- passes/opt/CMakeLists.txt | 4 + passes/opt/opt_first_fit_alloc.cc | 130 ++++++++- passes/opt/opt_prienc.cc | 264 ++++++++++++++++- passes/opt/opt_priokey.cc | 464 ++++++++++++++++++++++++++++++ tests/opt/opt_first_fit_alloc.ys | 167 +++++++++++ tests/opt/opt_prienc.ys | 126 ++++++++ tests/opt/opt_priokey.ys | 240 ++++++++++++++++ 7 files changed, 1381 insertions(+), 14 deletions(-) create mode 100644 passes/opt/opt_priokey.cc create mode 100644 tests/opt/opt_priokey.ys diff --git a/passes/opt/CMakeLists.txt b/passes/opt/CMakeLists.txt index 8e7d866bd..a651c400d 100644 --- a/passes/opt/CMakeLists.txt +++ b/passes/opt/CMakeLists.txt @@ -108,6 +108,10 @@ yosys_pass(opt_first_fit_alloc opt_first_fit_alloc.cc ) +yosys_pass(opt_priokey + opt_priokey.cc +) + pmgen_command(peepopt peepopt_shiftmul_right.pmg peepopt_shiftmul_left.pmg diff --git a/passes/opt/opt_first_fit_alloc.cc b/passes/opt/opt_first_fit_alloc.cc index a2e2af74b..f8a51d766 100644 --- a/passes/opt/opt_first_fit_alloc.cc +++ b/passes/opt/opt_first_fit_alloc.cc @@ -165,6 +165,56 @@ struct OptFirstFitAllocWorker : CutRegionWorker { return r; } + // ---------------------------------------------------------------- + // Reference semantics of the "coalesce matrix" allocator variant. + // + // Leadership and slot assignment are identical to the greedy first-fit + // above, but the per-lane rank does NOT depend on the lane's own enable: + // every lane k (enabled or not) inherits the slot of the first leader at + // or before k (in priority order) whose category matches cat[k]. This + // models RTL that precomputes a per-leader "same_cat[i][k]" mask (gated + // only on the leader's enable) and forward-coalesces into lane k without + // re-checking en[k]. There is no broadcast lane in this variant. + // ---------------------------------------------------------------- + AllocResult compute_alloc_coalesce(const vector &en, const vector &cat, + int n) const + { + AllocResult r = compute_alloc(en, vector(n, 0), cat, n); + for (int k = 0; k < n; k++) { + r.dsel[k] = 0; + for (int i = 0; i <= k; i++) + if (r.leader[i] && cat[i] == cat[k]) { + r.dsel[k] = r.slot[i]; + break; + } + } + return r; + } + + AllocResult compute_alloc_coalesce_dir(const vector &en, const vector &cat, + int n, bool msb_first) const + { + if (!msb_first) + return compute_alloc_coalesce(en, cat, n); + vector er(n), cr(n); + for (int i = 0; i < n; i++) { + er[i] = en[n - 1 - i]; + cr[i] = cat[n - 1 - i]; + } + AllocResult rr = compute_alloc_coalesce(er, cr, n); + AllocResult r; + r.dsel.assign(n, 0); + r.leader.assign(n, 0); + r.slot.assign(n, 0); + r.M = rr.M; + for (int i = 0; i < n; i++) { + r.dsel[i] = rr.dsel[n - 1 - i]; + r.leader[i] = rr.leader[n - 1 - i]; + r.slot[i] = rr.slot[n - 1 - i]; + } + return r; + } + // ---------------------------------------------------------------- // Test vectors. `nval` is the number of distinct label values (2^c for // the category, 2^a for the xbar attribute). The vectors deliberately @@ -412,6 +462,10 @@ struct OptFirstFitAllocWorker : CutRegionWorker { int field_w = 0; SigSpec en_sig, bc_sig, cat_sig; bool has_bc = false; + // Enable-independent forward coalescing: lanes inherit the slot of the + // first same-category leader at or before them in priority order, + // regardless of their own enable (the "same_cat matrix" RTL shape). + bool coalesce = false; int c = 0; bool msb_first = false; Cell *anchor = nullptr; @@ -594,9 +648,39 @@ struct OptFirstFitAllocWorker : CutRegionWorker { { Cell *anchor = rg.anchor; int n = rg.n, c = rg.c, w = rg.field_w; + SigSpec cat = sigmap(rg.cat_sig); + + // Enable-independent forward coalescing: lane k inherits the slot of the + // unique same-category leader at or before k in priority order, with no + // enable/broadcast gating. The priority position of a lane is a compile- + // time constant, so the "leader at or before k" restriction is static. + if (rg.coalesce) { + auto pos = [&](int l) { return rg.msb_first ? (n - 1 - l) : l; }; + SigSpec out; + for (int k = 0; k < n; k++) { + SigSpec cat_k = cat.extract(k * c, c); + vector g(n, SigBit(State::S0)); + for (int i = 0; i < n; i++) { + if (pos(i) > pos(k)) + continue; + SigBit eq = emit_eq_sig(anchor, cat.extract(i * c, c), cat_k); + g[i] = emit_and(anchor, leader[i], eq); + } + SigSpec rank(Const(0, cnt_w)); + for (int b = 0; b < cnt_w; b++) { + SigSpec terms; + for (int i = 0; i < n; i++) + if (pos(i) <= pos(k)) + terms.append(emit_and(anchor, g[i], slot[i][b])); + rank[b] = emit_reduce_or(anchor, terms); + } + out.append(zext_sig(rank, w)); + } + return out; + } + SigSpec en = sigmap(rg.en_sig); SigSpec bc = rg.has_bc ? sigmap(rg.bc_sig) : SigSpec(); - SigSpec cat = sigmap(rg.cat_sig); // bc rank: (M>=1) ? M-1 : 0 SigBit any_leader = emit_reduce_or(anchor, total); @@ -707,15 +791,22 @@ struct OptFirstFitAllocWorker : CutRegionWorker { internal_bits.insert(bit); pool seen; - auto all_internal = [&](const SigSpec &s) { + // Accept a bus bit if it is a cone-internal (computed) signal or a + // primary input / undriven bit. The enable/broadcast lanes are usually + // computed signals (e.g. valid & format), but some RTL drives the scan + // straight from a top-level request port (e.g. lane_en), so input buses + // must be admissible too. Inputs sort shallowest (depth 0) below, so they + // survive the candidate cap ahead of the deep intermediate nets. + auto all_internal_or_input = [&](const SigSpec &s) { for (auto bit : s) - if (!bit.wire || !internal_bits.count(bit)) + if (!bit.wire || (!internal_bits.count(bit) && + bit_to_driver.at(bit, nullptr) != nullptr)) return false; return true; }; auto add = [&](const SigSpec &sig, const std::string &nm) { SigSpec s = sigmap(sig); - if (GetSize(s) != n || !sig_bus_ok(s) || !all_internal(s)) + if (GetSize(s) != n || !sig_bus_ok(s) || !all_internal_or_input(s)) return; if (!seen.insert(s).second) return; @@ -868,17 +959,28 @@ struct OptFirstFitAllocWorker : CutRegionWorker { bool fpm = fingerprint_dsel(ce, root_sig, n, field_w, en_bus.sig, bc_bus ? bc_bus->sig : SigSpec(), bc_bus != nullptr, cat_sig, c, msb_first, cone_est); - log_debug(" en=%s bc=%s cat=%dx%d %s: fingerprint %s\n", en_bus.name.c_str(), + // Standard first-fit failed: try the enable-independent + // forward-coalescing variant (no broadcast lane). + bool coalesce = false; + if (!fpm && bc_bus == nullptr) { + fpm = fingerprint_dsel(ce, root_sig, n, field_w, en_bus.sig, + SigSpec(), false, cat_sig, c, msb_first, + cone_est, /*coalesce=*/true); + coalesce = fpm; + } + log_debug(" en=%s bc=%s cat=%dx%d %s: fingerprint %s%s\n", en_bus.name.c_str(), bc_bus ? bc_bus->name.c_str() : "-", n, c, - msb_first ? "MSB" : "LSB", fpm ? "MATCH" : "no"); + msb_first ? "MSB" : "LSB", fpm ? "MATCH" : "no", + coalesce ? " (coalesce)" : ""); if (fpm) { out.dsel_sig = root_sig; out.dsel_name = root_name; out.n = n; out.field_w = field_w; out.en_sig = en_bus.sig; - out.bc_sig = bc_bus ? bc_bus->sig : SigSpec(); - out.has_bc = (bc_bus != nullptr); + out.bc_sig = (!coalesce && bc_bus) ? bc_bus->sig : SigSpec(); + out.has_bc = (!coalesce && bc_bus != nullptr); + out.coalesce = coalesce; out.cat_sig = cat_sig; out.c = c; out.msb_first = msb_first; @@ -901,7 +1003,8 @@ struct OptFirstFitAllocWorker : CutRegionWorker { // Any eval failure or single mismatch rejects the candidate. bool fingerprint_dsel(ConstEval &ce, const SigSpec &root, int n, int field_w, const SigSpec &en_sig, const SigSpec &bc_sig, bool has_bc, - const SigSpec &cat_sig, int c, bool msb_first, int64_t cone_est) + const SigSpec &cat_sig, int c, bool msb_first, int64_t cone_est, + bool coalesce = false) { int nval = 1 << c; vector vs = make_vectors(n, nval, has_bc); @@ -920,7 +1023,9 @@ struct OptFirstFitAllocWorker : CutRegionWorker { if (!eval_root(ce, sets, root, res, cone_est)) return false; - AllocResult ar = compute_alloc_dir(tv.en, tv.bc, tv.label, n, msb_first); + AllocResult ar = coalesce + ? compute_alloc_coalesce_dir(tv.en, tv.label, n, msb_first) + : compute_alloc_dir(tv.en, tv.bc, tv.label, n, msb_first); for (int k = 0; k < n; k++) { int got = lane_val(res, k, field_w); int exp = ar.dsel[k] & ((1 << field_w) - 1); @@ -1239,11 +1344,12 @@ struct OptFirstFitAllocWorker : CutRegionWorker { claim_region(rg.dsel_sig, rg.dsel_cut_cells); regions_rewritten++; - log(" %s: %s <- first_fit_alloc(en=%s%s, cat=%dx%d, %s)\n", + log(" %s: %s <- first_fit_alloc(en=%s%s, cat=%dx%d, %s%s)\n", log_id(module), rg.dsel_name.c_str(), log_signal(rg.en_sig), rg.has_bc ? stringf(", bc=%s", log_signal(rg.bc_sig)).c_str() : "", - rg.n, rg.c, rg.msb_first ? "MSB-first" : "LSB-first"); + rg.n, rg.c, rg.msb_first ? "MSB-first" : "LSB-first", + rg.coalesce ? ", coalesce" : ""); if (have_xbar) { SigSpec new_xbar = emit_xbar(rg, xb, leader, slot, cnt_w); diff --git a/passes/opt/opt_prienc.cc b/passes/opt/opt_prienc.cc index acb7b78f4..e7c4a966c 100644 --- a/passes/opt/opt_prienc.cc +++ b/passes/opt/opt_prienc.cc @@ -82,6 +82,7 @@ struct OptPriEncWorker { // Configuration. bool detect_clz = true; bool detect_ctz = true; + bool detect_rr = true; int max_input_width = 256; int min_input_width = 4; @@ -390,6 +391,171 @@ struct OptPriEncWorker { 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. + int fingerprint_rr(SigSpec req_sig, SigSpec start_sig, SigSpec S_sig, + int N, int W) { + ConstEval ce(module); + bool ok0 = true, ok1 = true; + auto deck = gen_test_vectors(N); + int checks = 0; + for (auto& rv : deck) { + for (int s = 0; s < N; s++) { + 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 -1; + 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++; + if (!ok0 && !ok1) return -1; + } + } + if (checks < 2 * N) 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::tuple> 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)))); + cells_added++; + } + SigSpec mask_hi = module->And(NEW_ID2_SUFFIX("rrmask"), req, above); + 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); + cells_added++; + anyreq = module->ReduceOr(NEW_ID2_SUFFIX("rranyreq"), req); + cells_added++; + // any_hi ? cz_hi : cz_all + gsel = module->Mux(NEW_ID2_SUFFIX("rrgsel"), cz_all, cz_hi, any_hi); + 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); + 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& allowed) { + pool visited; + std::queue 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; @@ -584,6 +750,74 @@ struct OptPriEncWorker { } } + // 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 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; + + int W = cand.S_wire->width; + if (W < 2 || W > max_W) continue; + SigSpec S_sig = sigmap(SigSpec(cand.S_wire)); + + vector req_cands, start_cands; + for (Wire* w : wires_snapshot) { + if (w == cand.S_wire) continue; + bool all_in = true; + for (auto bit : sigmap(SigSpec(w))) + if (!cand.cone_bits.count(bit)) { all_in = false; break; } + if (!all_in) continue; + 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; }); + + int pairs = 0; + 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 req_bits; + for (auto bit : req_sig) + if (bit.wire) req_bits.insert(bit); + 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 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(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) { @@ -595,6 +829,14 @@ struct OptPriEncWorker { 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++; + } } }; @@ -624,11 +866,23 @@ struct OptPriEncPass : public Pass { 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.\n"); + log(" detect CLZ patterns only (also disables round-robin).\n"); log("\n"); log(" -ctz\n"); - log(" detect CTZ patterns only.\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"); @@ -648,6 +902,7 @@ struct OptPriEncPass : public Pass { bool only_clz = false; bool only_ctz = false; + bool no_rr = false; int max_width = 64; int min_width = 4; @@ -655,6 +910,7 @@ struct OptPriEncPass : public Pass { 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; } @@ -664,6 +920,9 @@ struct OptPriEncPass : public Pass { 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; @@ -671,6 +930,7 @@ struct OptPriEncPass : public Pass { 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(); diff --git a/passes/opt/opt_priokey.cc b/passes/opt/opt_priokey.cc new file mode 100644 index 000000000..f505c1e7e --- /dev/null +++ b/passes/opt/opt_priokey.cc @@ -0,0 +1,464 @@ +/* + * yosys -- Yosys Open SYnthesis Suite + * + * Copyright (C) 2026 Akash Levy + * + * 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" + +USING_YOSYS_NAMESPACE +PRIVATE_NAMESPACE_BEGIN + +// --------------------------------------------------------------------------- +// opt_priokey: priority-by-key deduplication ("taken" accumulator) rewrite. +// +// RTL that resolves conflicts between several sources that each carry a small +// key is often written as a serial scan over a wide one-hot "set" accumulator +// indexed by the key: +// +// taken = '0; +// for (i = 0; i < P; i++) +// if (act[i] && !taken[key[i]]) begin // this source wins its key +// taken[key[i]] = 1'b1; // claim the key +// ...use win[i]... +// end +// +// This elaborates into a serial chain of dynamic-index reads (taken[key[i]] = +// $shiftx into an S-bit vector) and dynamic-index writes (taken | 1< bit_to_driver; + + int max_slots = 1 << 14; // maximum accumulator width S + int max_chain = 256; // maximum number of sources P + int fp_trials = 256; // ConstEval validation vectors + bool strict = false; // validate over the full key range, not [0,S) + + int regions_rewritten = 0; + int cells_added = 0; + + OptPrioKeyWorker(Module *m) : module(m), sigmap(m) { build_index(); } + + void build_index() { + for (auto c : module->cells()) + for (auto &conn : c->connections()) + if (c->output(conn.first)) + for (auto bit : sigmap(conn.second)) + if (bit.wire) bit_to_driver[bit] = c; + } + + Cell *sole_driver(const SigSpec &s) { + SigSpec ss = sigmap(s); + Cell *d = nullptr; + for (auto bit : ss) { + if (!bit.wire) return nullptr; + auto it = bit_to_driver.find(bit); + if (it == bit_to_driver.end()) return nullptr; + if (d && d != it->second) return nullptr; + d = it->second; + } + return d; + } + + bool is_all_zero(const SigSpec &s) { + for (auto bit : s) + if (bit != SigBit(State::S0)) return false; + return true; + } + + bool is_const_one(const SigSpec &s) { + SigSpec ss = sigmap(s); + if (!ss.is_fully_const()) return false; + return ss.as_const().as_int() == 1; + } + + // Recover the index bus `key` from a one-hot set-mask (1 << key), spelled + // as $shl(1, key) or $shift(1, -key). + SigSpec decode_onehot_key(const SigSpec &mask) { + Cell *d = sole_driver(mask); + if (!d) return SigSpec(); + if ((d->type == ID($shl) || d->type == ID($sshl)) && + is_const_one(d->getPort(ID::A))) + return sigmap(d->getPort(ID::B)); + if (d->type == ID($shift) && is_const_one(d->getPort(ID::A))) { + Cell *neg = sole_driver(d->getPort(ID::B)); + if (neg && neg->type == ID($neg)) + return sigmap(neg->getPort(ID::A)); + } + return SigSpec(); + } + + // A set-arm is `taken_prev` with one bit (1 << key) OR'ed in. After opt it + // appears either as the raw one-hot (prev was 0) or as an $or whose two + // operands are the one-hot mask and the (masked) previous state. Return the + // key bus of the one-hot mask. + SigSpec extract_key_from_setarm(const SigSpec &arm) { + SigSpec k = decode_onehot_key(arm); + if (GetSize(k)) return k; + Cell *d = sole_driver(arm); + if (d && d->type == ID($or)) { + k = decode_onehot_key(d->getPort(ID::A)); + if (GetSize(k)) return k; + k = decode_onehot_key(d->getPort(ID::B)); + if (GetSize(k)) return k; + } + return SigSpec(); + } + + // One guarded "set key" applied to the accumulator (guard may be const-1 + // for an unconditional set). + struct SetStep { + SigBit guard; + SigSpec key; + }; + + // Cache of trace results: chained reads share accumulator prefixes (read j + // indexes the state produced by sets 0..j-1), so without memoization the + // same chain is re-walked once per read -> O(P^2 * S). The cache makes the + // total walk O(P * S). + dict>> acc_memo; + + // Walk an accumulator value back to constant zero, collecting the guarded + // key-sets that produced it. Returns false (leaving `steps` unchanged) if it + // is not a pure set-only accumulator (mux/or of prev with a one-hot key) + // rooted at 0. + bool trace_acc(SigSpec acc, vector &steps, int depth) { + acc = sigmap(acc); + auto mit = acc_memo.find(acc); + if (mit != acc_memo.end()) { + if (mit->second.first) + steps.insert(steps.end(), mit->second.second.begin(), + mit->second.second.end()); + return mit->second.first; + } + int start = GetSize(steps); + bool ok = trace_acc_uncached(acc, steps, depth); + if (ok) { + acc_memo[acc] = {true, vector(steps.begin() + start, + steps.end())}; + } else { + steps.resize(start); // discard any partial trace + acc_memo[acc] = {false, {}}; + } + return ok; + } + + bool trace_acc_uncached(SigSpec acc, vector &steps, int depth) { + if (is_all_zero(acc)) + return true; + if (depth > max_chain) + return false; + Cell *d = sole_driver(acc); + if (!d) + return false; + if (d->type == ID($mux)) { + SigSpec s = sigmap(d->getPort(ID::S)); + if (GetSize(s) != 1) + return false; + if (!trace_acc(d->getPort(ID::A), steps, depth + 1)) + return false; + SigSpec key = extract_key_from_setarm(d->getPort(ID::B)); + if (GetSize(key) == 0) + return false; + steps.push_back(SetStep{s[0], key}); + return true; + } + if (d->type == ID($or)) { + SigSpec a = d->getPort(ID::A), b = d->getPort(ID::B); + SigSpec ka = decode_onehot_key(a), kb = decode_onehot_key(b); + if (GetSize(ka) && trace_acc(b, steps, depth + 1)) { + steps.push_back(SetStep{SigBit(State::S1), ka}); + return true; + } + if (GetSize(kb) && trace_acc(a, steps, depth + 1)) { + steps.push_back(SetStep{SigBit(State::S1), kb}); + return true; + } + } + return false; + } + + // A dynamic read of the accumulator: the 1-bit read cell, the accumulator + // value it indexes (with any out-of-range x-padding stripped) and the index. + struct Read { + Cell *cell; + SigSpec acc; + SigSpec key; + }; + + // Recognize a 1-bit dynamic read of a vector: either $shiftx(A=acc, B=key) + // or $bmux(A={x.., acc}, S=key). Returns false otherwise. + bool match_read(Cell *c, Read &r) { + if (c->type == ID($shiftx)) { + if (GetSize(c->getPort(ID::Y)) != 1) + return false; + r.cell = c; + r.acc = sigmap(c->getPort(ID::A)); + r.key = sigmap(c->getPort(ID::B)); + return GetSize(r.acc) >= 2; + } + if (c->type == ID($bmux)) { + if (c->getParam(ID::WIDTH).as_int() != 1) + return false; + SigSpec a = sigmap(c->getPort(ID::A)); + // Strip the high x-padding to recover the real accumulator bits. + int w = 0; + while (w < GetSize(a) && a[w] != SigBit(State::Sx)) + w++; + if (w < 2) + return false; + r.cell = c; + r.acc = a.extract(0, w); + r.key = sigmap(c->getPort(ID::S)); + return true; + } + return false; + } + + // Prove read == OR over steps of ( guard & key == read_key ) by ConstEval + // fingerprinting over the reachable key range [0,S). Guards and key buses + // (which are disjoint sel slices) are driven as free inputs. + bool validate_read(const Read &rd, const vector &steps, int S) { + Cell *read = rd.cell; + SigSpec read_key = rd.key; + // In strict mode sweep the full key range so the rewrite is proven for + // every value the index bus can take (out-of-range reads included); + // otherwise sweep only the reachable slots [0,S). + int kw = GetSize(read_key); + uint64_t range = (uint64_t)S; + if (strict) { + int cap = kw < 30 ? kw : 30; + range = 1ULL << cap; + } + ConstEval ce(module); + uint64_t lfsr = 0x9e3779b97f4a7c15ULL ^ (uintptr_t)read; + auto rnd = [&]() { + lfsr ^= lfsr << 13; lfsr ^= lfsr >> 7; lfsr ^= lfsr << 17; + return lfsr; + }; + for (int t = 0; t < fp_trials; t++) { + ce.push(); + int rk = (int)(rnd() % range); + ce.set(read_key, Const(rk, GetSize(read_key))); + vector kv(GetSize(steps)); + vector gv(GetSize(steps)); + for (int i = 0; i < GetSize(steps); i++) { + kv[i] = (int)(rnd() % range); + ce.set(steps[i].key, Const(kv[i], GetSize(steps[i].key))); + if (steps[i].guard == State::S1) { + gv[i] = 1; + } else { + gv[i] = (int)(rnd() & 1); + ce.set(SigSpec(steps[i].guard), Const(gv[i], 1)); + } + } + SigSpec out(read->getPort(ID::Y)); + SigSpec undef; + bool ok = ce.eval(out, undef) && out.is_fully_const(); + int actual = ok ? (out.as_const().as_int() & 1) : -1; + int expect = 0; + for (int i = 0; i < GetSize(steps); i++) + if (gv[i] && kv[i] == rk) { expect = 1; break; } + ce.pop(); + if (!ok || actual != expect) + return false; + } + return true; + } + + void rewrite_read(const Read &rd, const vector &steps) { + Cell *read = rd.cell; + cell = read; + SigSpec read_key = rd.key; + SigSpec new_r; + if (steps.empty()) { + new_r = SigSpec(State::S0); + } else { + SigSpec terms; + for (auto &st : steps) { + SigSpec eq = module->Eq(NEW_ID2_SUFFIX("priokey_eq"), + st.key, read_key); + cells_added++; + SigSpec g = module->And(NEW_ID2_SUFFIX("priokey_and"), + SigSpec(st.guard), eq); + cells_added++; + terms.append(g); + } + new_r = module->ReduceOr(NEW_ID2_SUFFIX("priokey_or"), terms); + cells_added++; + } + // Tag wire so the rewrite is externally observable, then detach the old + // dynamic read and drive its consumers from the reduction. + Wire *tag = module->addWire(NEW_ID2_SUFFIX("priokey_read"), 1); + module->connect(SigSpec(tag), new_r); + SigSpec old_y = sigmap(read->getPort(ID::Y)); + Wire *dangling = module->addWire(NEW_ID2_SUFFIX("priokey_dangling"), + GetSize(read->getPort(ID::Y))); + read->setPort(ID::Y, dangling); + module->connect(old_y, SigSpec(tag)); + regions_rewritten++; + } + + void run() { + vector reads; + for (auto c : module->cells()) { + Read r; + if (!match_read(c, r)) + continue; + if (GetSize(r.acc) > max_slots) + continue; + reads.push_back(r); + } + + int max_sources = 0; + int accum_width = 0; + vector zero_reads; // read of the all-zero head (== 0) + for (auto &rd : reads) { + int S = GetSize(rd.acc); + vector steps; + if (!trace_acc(rd.acc, steps, 0)) + continue; + // The all-zero head read is only rewritten (to 0) once we know the + // pattern is really present in this module. + if (steps.empty()) { + zero_reads.push_back(rd); + continue; + } + if (!validate_read(rd, steps, S)) + continue; + rewrite_read(rd, steps); + max_sources = std::max(max_sources, GetSize(steps) + 1); + accum_width = S; + } + if (regions_rewritten) + for (auto &rd : zero_reads) + rewrite_read(rd, {}); + + if (regions_rewritten) + log(" %s: priority-by-key dedup, up to %d source(s), " + "%d-slot accumulator\n", + log_id(module), max_sources, accum_width); + } +}; + +struct OptPrioKeyPass : public Pass { + OptPrioKeyPass() : Pass("opt_priokey", + "detect and rewrite priority-by-key deduplication accumulators") {} + + void help() override { + // |---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---|---v---| + log("\n"); + log(" opt_priokey [options] [selection]\n"); + log("\n"); + log("This pass detects a serial 'set accumulator' that resolves conflicts\n"); + log("between several sources that each carry a small key:\n"); + log("\n"); + log(" taken = '0;\n"); + log(" for (i = 0; i < P; i++)\n"); + log(" if (act[i] && !taken[key[i]]) begin taken[key[i]] = 1; ... end\n"); + log("\n"); + log("Such RTL elaborates into a chain of dynamic-index reads/writes into a\n"); + log("wide one-hot vector ($shiftx / $shift), whose depth grows with both the\n"); + log("number of sources and the accumulator width. Each dynamic read\n"); + log("taken[key[j]] is provably equal to\n"); + log("\n"); + log(" OR over i args, RTLIL::Design *design) override { + log_header(design, "Executing OPT_PRIOKEY pass (priority-by-key dedup).\n"); + + int max_slots = 1 << 14; + int max_sources = 256; + bool strict = false; + size_t argidx; + for (argidx = 1; argidx < args.size(); argidx++) { + if (args[argidx] == "-max-slots" && argidx + 1 < args.size()) { + max_slots = std::stoi(args[++argidx]); continue; + } + if (args[argidx] == "-max-sources" && argidx + 1 < args.size()) { + max_sources = std::stoi(args[++argidx]); continue; + } + if (args[argidx] == "-strict") { + strict = true; continue; + } + break; + } + extra_args(args, argidx, design); + + int total_regions = 0, total_cells = 0; + for (auto module : design->selected_modules()) { + OptPrioKeyWorker worker(module); + worker.max_slots = max_slots; + worker.max_chain = max_sources; + worker.strict = strict; + worker.run(); + total_regions += worker.regions_rewritten; + total_cells += worker.cells_added; + } + + log("Rewrote %d dynamic key-read(s); emitted %d new cell(s).\n", + total_regions, total_cells); + + if (total_regions) + Yosys::run_pass("clean -purge"); + } +} OptPrioKeyPass; + +PRIVATE_NAMESPACE_END diff --git a/tests/opt/opt_first_fit_alloc.ys b/tests/opt/opt_first_fit_alloc.ys index 07101da6a..a4bec64b0 100644 --- a/tests/opt/opt_first_fit_alloc.ys +++ b/tests/opt/opt_first_fit_alloc.ys @@ -590,3 +590,170 @@ design -load postopt select -assert-min 1 w:*ffa_* design -reset log -pop + +# ============================================================================ +# Group G: coalesce-matrix variant (precomputed same_cat[i][k], raw-input en) +# ============================================================================ +# +# Some RTL precomputes a per-leader "same_cat[i][k]" mask (gated ONLY on the +# leader's enable) and forward-coalesces the leader's slot into lane k without +# re-checking en[k]. Disabled lanes after a same-category leader therefore +# inherit that leader's slot (rather than 0). The pass detects this as the +# enable-independent forward-coalescing variant. These modules also drive the +# scan straight from a top-level request port (lane_en), exercising the +# primary-input enable/broadcast candidate path. + +# G1: coalesce-matrix dsel-only, raw-input enable, N=8 (equiv). +log -header "G1: coalesce-matrix allocator, raw-input enable, N=8 (equiv)" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv <= N) ? (si - N) : si; + rr_dut #(N,W) u1(.req(req),.s(s),.grant(g1),.idx_next(n1)); + rr_ref #(N,W) u2(.req(req),.s(s),.grant(g2),.idx_next(n2)); + always_comb begin + assert (g1 == g2); + assert (n1 == n2); + end +endmodule +EOF +hierarchy -top tb +flatten +chformal -lower +opt -full +sat -verify -prove-asserts -show-ports tb +design -reset +log -pop + +# RR3: negative -- a downward-scanning arbiter (opposite rotation) is a +# different function and must not be rewritten as round-robin. +log -header "RR3: downward-scan arbiter -> no round-robin rewrite" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv < Date: Mon, 6 Jul 2026 13:28:54 -0700 Subject: [PATCH 29/33] opt_prienc: give each round-robin req candidate its own fingerprint budget The max_pairs budget was a single running counter shared across all req_wire iterations, so once a start-candidate-heavy first req size exhausted it, every later req size broke on its first start candidate and was silently skipped. Reset the budget per req_wire so all req sizes get a fair chance. (Completeness only; fingerprint_rr still validates every match, so this never affected correctness.) Co-authored-by: Cursor --- passes/opt/opt_prienc.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/passes/opt/opt_prienc.cc b/passes/opt/opt_prienc.cc index e7c4a966c..7231db45e 100644 --- a/passes/opt/opt_prienc.cc +++ b/passes/opt/opt_prienc.cc @@ -782,7 +782,6 @@ struct OptPriEncWorker { std::sort(req_cands.begin(), req_cands.end(), [](Wire* a, Wire* b) { return a->width > b->width; }); - int pairs = 0; bool matched = false; for (Wire* req_wire : req_cands) { if (matched) break; @@ -791,6 +790,10 @@ struct OptPriEncWorker { pool 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; From 189d177478a79f2a21d2f1c3d5ed532a05d7052a Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Mon, 6 Jul 2026 16:18:47 -0700 Subject: [PATCH 30/33] bump verific --- verific | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verific b/verific index cee2b14fd..324ed5d92 160000 --- a/verific +++ b/verific @@ -1 +1 @@ -Subproject commit cee2b14fdace74e89b699027a6e8f28fb86613c5 +Subproject commit 324ed5d92aa63128bd0382c7f013d63719e08079 From 676ac184edf5386f6f9c7d7dfbc9d363179b3142 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Mon, 6 Jul 2026 21:35:51 -0700 Subject: [PATCH 31/33] opt_prienc: guard ConstEval fingerprint inputs against constant/aliased bits The round-robin detector fed sigmap(wire) signals straight into ConstEval::set() while sweeping test vectors. On real designs a candidate bus can have bits tied to constants, repeated bits, or a req/start pair that alias to the same net after sigmap. ConstEval::set() asserts (current_val[i].wire != NULL || current_val[i] == value[i]) when asked to re-pin such a bit to a conflicting value, crashing the pass (consteval.h:83) on designs like veer/picorv32/murax/raygentop under formal synthesis. Add clean_set_signals() and reject any fingerprint candidate whose set-signals contain constant bits, repeated bits, or overlap each other, in both the priority-encoder and round-robin paths. Skipping an unclean candidate only forgoes a possible rewrite; it never produces an incorrect one. Clean candidates (the intended patterns) are unaffected. Co-authored-by: Cursor --- passes/opt/opt_prienc.cc | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/passes/opt/opt_prienc.cc b/passes/opt/opt_prienc.cc index 7231db45e..07089283c 100644 --- a/passes/opt/opt_prienc.cc +++ b/passes/opt/opt_prienc.cc @@ -243,6 +243,23 @@ struct OptPriEncWorker { return vs; } + // 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 sigs) { + pool 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; + } + // Run all candidate test vectors through ConstEval and try to match each of // the four PE variants against the recorded outputs. Returns the matched // variant, or NONE. @@ -257,6 +274,9 @@ struct OptPriEncWorker { 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; + auto vs = gen_test_vectors(N); for (auto& v : vs) { ce.push(); @@ -440,6 +460,8 @@ struct OptPriEncWorker { int fingerprint_rr(SigSpec req_sig, SigSpec start_sig, SigSpec S_sig, int N, int W) { ConstEval ce(module); + if (!clean_set_signals({&req_sig, &start_sig})) + return -1; bool ok0 = true, ok1 = true; auto deck = gen_test_vectors(N); int checks = 0; From 945e4a403ba50928dfb5837e346c4df8af9b4d68 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Mon, 6 Jul 2026 22:05:50 -0700 Subject: [PATCH 32/33] tests/opt: add generalization coverage for the QoR pattern passes Prove the three pattern detectors work on unseen inputs, not just the RTL they were derived from. Because detection is functional (ConstEval fingerprinting over the reachable input space), correctness is established per case with equiv_opt -assert (full) or a SAT miter clamped to the reachable range (non-power-of-two), and detection is confirmed with a w:*tag* probe. opt_priokey: D1-D3 spelling variants (explicit shift-or set, compound derived guard, accumulator also exported) -- all fire and prove equivalent. E1-E2 parameter sweep P=2..8, S=4..32. E3 non-power-of-two S=12 reachable-range equivalence via SAT miter. E4 same shape under -strict declines to rewrite (formal-flow safety). F1-F2 near-miss negatives (clear accumulator, multi-hot set) -> no rewrite. opt_prienc (round-robin): RR4-RR5 DEPTH sweep 8/32, full sequential equivalence. RR6 non-power-of-two DEPTH=7 reachable-range equivalence (SAT miter). RR7 an entirely different spelling (upward wrap-scan, first-hit) of the same arbiter -- fires and proves equivalent. RR8 fixed-priority (no rotating pointer) negative. opt_first_fit_alloc (coalesce): H1 inline same-category compare (no precomputed matrix) spelling. H2 different slot/field shape (N=8, NB=8, W=3). All new cases pass locally; they avoid brittle exact cell-count asserts so they are robust to upstream optimization drift. Co-authored-by: Cursor --- tests/opt/opt_first_fit_alloc.ys | 100 +++++++++ tests/opt/opt_prienc.ys | 182 ++++++++++++++++ tests/opt/opt_priokey.ys | 343 +++++++++++++++++++++++++++++++ 3 files changed, 625 insertions(+) diff --git a/tests/opt/opt_first_fit_alloc.ys b/tests/opt/opt_first_fit_alloc.ys index a4bec64b0..4ffe36684 100644 --- a/tests/opt/opt_first_fit_alloc.ys +++ b/tests/opt/opt_first_fit_alloc.ys @@ -757,3 +757,103 @@ opt_first_fit_alloc select -assert-count 0 w:*ffa_* design -reset log -pop + +# ============================================================================ +# Group H: coalesce generalization (spelling + shape variants) +# ============================================================================ +# +# The coalesce variant is detected by functional fingerprinting of the dsel +# cone, so it should not depend on the exact way the same-category forwarding +# is written or on the specific N/NB/C shape. These cases vary both. + +# H1: coalesce with the same-category forwarding written INLINE (no precomputed +# same_cat[i][k] matrix) -- the leader compares categories directly in its +# forward loop. Functionally identical to G1's matrix form; must still detect +# the enable-independent coalescing variant and prove equivalent. +log -header "H1: coalesce inline-compare spelling, N=8 (equiv + fires)" +log -push +design -reset +read_verilog -sv <= N) ? (si - N) : si; + rr_dut #(N,W) u1(.req(req),.s(s),.grant(g1),.idx_next(n1)); + rr_ref #(N,W) u2(.req(req),.s(s),.grant(g2),.idx_next(n2)); + always_comb begin + assert (g1 == g2); + assert (n1 == n2); + end +endmodule +EOF +hierarchy -top tb +flatten +chformal -lower +opt -full +sat -verify -prove-asserts -show-ports tb +design -reset +log -pop + +# RR7: DIFFERENT RTL SPELLING of the same rotated-priority function. Instead of +# the customer's downward idx-- last-write-wins loop, scan UPWARD from s+1 with +# wraparound and keep the FIRST hit. This is a functionally identical arbiter +# written in an unrelated style; detection is functional so it must still fire +# and prove equivalent. (Combinational, pointer `s` an input -> full equiv at +# power-of-2 N over all pointer values.) +log -header "RR7: upward-scan spelling variant, N=16 (equiv + fires)" +log -push +design -reset +read_verilog -sv < no round-robin rewrite" +log -push +design -reset +read_verilog -sv <= SW'(S)) ? (k - SW'(S)) : k; // into [0,S) + end + logic [P-1:0] w1, w2; + pk_dut #(P,S,SW) u1(.act(act), .sel_flat(sel_c), .win(w1)); + pk_ref #(P,S,SW) u2(.act(act), .sel_flat(sel_c), .win(w2)); + always_comb assert (w1 == w2); +endmodule +EOF +hierarchy -top tb +flatten +chformal -lower +opt -full +sat -verify -prove-asserts -show-ports tb +design -reset +log -pop + +# E4: SAME non-pow2 S=12 under -strict. Strict validation sweeps the FULL key +# range and rejects rewrites that only hold via out-of-range don't-cares, so +# the pass must decline to rewrite. This is the safety mode used by formal +# synthesis flows: no reliance on out-of-range freedom. +log -header "E4: non-pow2 S=12 -strict -> no rewrite (safety)" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv < no rewrite" +log -push +design -reset +read_verilog -sv < Date: Tue, 7 Jul 2026 00:00:24 -0700 Subject: [PATCH 33/33] opt_prienc: require ConstEval fingerprint inputs to be a valid cut The round-robin (and PE/CLZ/CTZ) fingerprints pin candidate request/ start/select signals as free ConstEval inputs and evaluate the encoder output cone. ConstEval::eval() re-computes and re-set()s the FULL output of every combinational cell it needs. If a pinned bit is a combinational cell output and a sibling output bit of that same cell is pulled into the cone, evaluating the sibling re-sets the pinned bit to the cell's real value, contradicting the free value we pinned and tripping the assertion `current_val[i].wire != NULL || current_val[i] == value[i]` in kernel/consteval.h. The earlier clean_set_signals() guard only rejected constant/aliased bits; it did not ensure the pinned signals form a valid cut. Candidates are gathered purely by width, so an internal combinational wire (e.g. a slice of a wider arithmetic result) can be pinned, which is exactly what crashed on veer_speed1/picorv32/murax/raygentop. Add is_valid_consteval_cut(): a pinned bit is a safe leaf when it is a primary input, sequential-cell output or undriven (absent from bit_to_driver, which holds combinational drivers only); a combinational output is safe only if that cell's entire output lies within the pinned cut. Apply it in both fingerprint() and fingerprint_rr(). Declining an unclean cut only forgoes a possible rewrite, never yields a wrong one, and the intended arbiter inputs (request ports, idx_last flop outputs) remain valid cuts so real round-robin patterns still rewrite. Co-authored-by: Cursor --- passes/opt/opt_prienc.cc | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/passes/opt/opt_prienc.cc b/passes/opt/opt_prienc.cc index 07089283c..030cc9f60 100644 --- a/passes/opt/opt_prienc.cc +++ b/passes/opt/opt_prienc.cc @@ -260,6 +260,39 @@ struct OptPriEncWorker { 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 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 all candidate test vectors through ConstEval and try to match each of // the four PE variants against the recorded outputs. Returns the matched // variant, or NONE. @@ -277,6 +310,9 @@ struct OptPriEncWorker { if (!clean_set_signals({&T_sig})) return PEVariant::NONE; + if (!is_valid_consteval_cut(T_sig)) + return PEVariant::NONE; + auto vs = gen_test_vectors(N); for (auto& v : vs) { ce.push(); @@ -462,6 +498,10 @@ struct OptPriEncWorker { ConstEval ce(module); 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 deck = gen_test_vectors(N); int checks = 0;