From 914e14946d2d476b2747caeed36e8882e091422c Mon Sep 17 00:00:00 2001 From: Robert O'Callahan Date: Tue, 16 Dec 2025 01:39:39 +0000 Subject: [PATCH 1/3] Implement design_equal command --- passes/cmds/Makefile.inc | 1 + passes/cmds/design_equal.cc | 352 +++++++++++++++++++++++++++++ tests/various/design_equal_fail.ys | 22 ++ tests/various/design_equal_pass.ys | 17 ++ 4 files changed, 392 insertions(+) create mode 100644 passes/cmds/design_equal.cc create mode 100644 tests/various/design_equal_fail.ys create mode 100644 tests/various/design_equal_pass.ys diff --git a/passes/cmds/Makefile.inc b/passes/cmds/Makefile.inc index b1b1383b1..dc12c92c2 100644 --- a/passes/cmds/Makefile.inc +++ b/passes/cmds/Makefile.inc @@ -5,6 +5,7 @@ endif OBJS += passes/cmds/add.o OBJS += passes/cmds/delete.o OBJS += passes/cmds/design.o +OBJS += passes/cmds/design_equal.o OBJS += passes/cmds/select.o OBJS += passes/cmds/show.o OBJS += passes/cmds/viz.o diff --git a/passes/cmds/design_equal.cc b/passes/cmds/design_equal.cc new file mode 100644 index 000000000..a949db9ff --- /dev/null +++ b/passes/cmds/design_equal.cc @@ -0,0 +1,352 @@ +/* + * yosys -- Yosys Open SYnthesis Suite + * + * Copyright (C) 2012 Claire Xenia Wolf + * + * 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/rtlil.h" + +YOSYS_NAMESPACE_BEGIN + +class ModuleComparator +{ + RTLIL::Module *mod_a; + RTLIL::Module *mod_b; + +public: + ModuleComparator(RTLIL::Module *mod_a, RTLIL::Module *mod_b) : mod_a(mod_a), mod_b(mod_b) {} + + bool compare_sigbit(const RTLIL::SigBit &a, const RTLIL::SigBit &b) + { + if (a.wire == nullptr && b.wire == nullptr) + return a.data == b.data; + if (a.wire != nullptr && b.wire != nullptr) + return a.wire->name == b.wire->name && a.offset == b.offset; + return false; + } + + bool compare_sigspec(const RTLIL::SigSpec &a, const RTLIL::SigSpec &b) + { + if (a.size() != b.size()) return false; + auto it_a = a.begin(), it_b = b.begin(); + for (; it_a != a.end(); ++it_a, ++it_b) { + if (!compare_sigbit(*it_a, *it_b)) return false; + } + return true; + } + + std::string compare_attributes(const RTLIL::AttrObject *a, const RTLIL::AttrObject *b) + { + for (const auto &it : a->attributes) { + if (b->attributes.count(it.first) == 0) + return "missing attribute " + std::string(log_id(it.first)) + " in second design"; + if (it.second != b->attributes.at(it.first)) + return "attribute " + std::string(log_id(it.first)) + " mismatch: " + log_const(it.second) + " != " + log_const(b->attributes.at(it.first)); + } + for (const auto &it : b->attributes) + if (a->attributes.count(it.first) == 0) + return "missing attribute " + std::string(log_id(it.first)) + " in first design"; + return ""; + } + + std::string compare_wires(const RTLIL::Wire *a, const RTLIL::Wire *b) + { + if (a->name != b->name) + return "name mismatch: " + std::string(log_id(a->name)) + " != " + log_id(b->name); + if (a->width != b->width) + return "width mismatch: " + std::to_string(a->width) + " != " + std::to_string(b->width); + if (a->start_offset != b->start_offset) + return "start_offset mismatch: " + std::to_string(a->start_offset) + " != " + std::to_string(b->start_offset); + if (a->port_id != b->port_id) + return "port_id mismatch: " + std::to_string(a->port_id) + " != " + std::to_string(b->port_id); + if (a->port_input != b->port_input) + return "port_input mismatch: " + std::to_string(a->port_input) + " != " + std::to_string(b->port_input); + if (a->port_output != b->port_output) + return "port_output mismatch: " + std::to_string(a->port_output) + " != " + std::to_string(b->port_output); + if (a->upto != b->upto) + return "upto mismatch: " + std::to_string(a->upto) + " != " + std::to_string(b->upto); + if (a->is_signed != b->is_signed) + return "is_signed mismatch: " + std::to_string(a->is_signed) + " != " + std::to_string(b->is_signed); + if (std::string mismatch = compare_attributes(a, b); !mismatch.empty()) + return mismatch; + return ""; + } + + void check_wires() + { + for (const auto &it : mod_a->wires_) { + if (mod_b->wires_.count(it.first) == 0) + log_error("Module %s missing wire %s in second design.\n", log_id(mod_a->name), log_id(it.first)); + if (std::string mismatch = compare_wires(it.second, mod_b->wires_.at(it.first)); !mismatch.empty()) + log_error("Module %s wire %s %s.\n", log_id(mod_a->name), log_id(it.first), mismatch); + } + for (const auto &it : mod_b->wires_) + if (mod_a->wires_.count(it.first) == 0) + log_error("Module %s missing wire %s in first design.\n", log_id(mod_b->name), log_id(it.first)); + } + + std::string compare_memories(const RTLIL::Memory *a, const RTLIL::Memory *b) + { + if (a->name != b->name) + return "name mismatch: " + std::string(log_id(a->name)) + " != " + log_id(b->name); + if (a->width != b->width) + return "width mismatch: " + std::to_string(a->width) + " != " + std::to_string(b->width); + if (a->start_offset != b->start_offset) + return "start_offset mismatch: " + std::to_string(a->start_offset) + " != " + std::to_string(b->start_offset); + if (a->size != b->size) + return "size mismatch: " + std::to_string(a->size) + " != " + std::to_string(b->size); + if (std::string mismatch = compare_attributes(a, b); !mismatch.empty()) + return mismatch; + return ""; + } + + std::string compare_cells(const RTLIL::Cell *a, const RTLIL::Cell *b) + { + if (a->name != b->name) + return "name mismatch: " + std::string(log_id(a->name)) + " != " + log_id(b->name); + if (a->type != b->type) + return "type mismatch: " + std::string(log_id(a->type)) + " != " + log_id(b->type); + if (std::string mismatch = compare_attributes(a, b); !mismatch.empty()) + return mismatch; + + for (const auto &it : a->parameters) { + if (b->parameters.count(it.first) == 0) + return "parameter mismatch: missing parameter " + std::string(log_id(it.first)) + " in second design"; + if (it.second != b->parameters.at(it.first)) + return "parameter mismatch: " + std::string(log_id(it.first)) + " mismatch: " + log_const(it.second) + " != " + log_const(b->parameters.at(it.first)); + } + for (const auto &it : b->parameters) + if (a->parameters.count(it.first) == 0) + return "parameter mismatch: missing parameter " + std::string(log_id(it.first)) + " in first design"; + + for (const auto &it : a->connections()) { + if (b->connections().count(it.first) == 0) + return "connection mismatch: missing connection " + std::string(log_id(it.first)) + " in second design"; + if (!compare_sigspec(it.second, b->connections().at(it.first))) + return "connection " + std::string(log_id(it.first)) + " mismatch: " + log_signal(it.second) + " != " + log_signal(b->connections().at(it.first)); + } + for (const auto &it : b->connections()) + if (a->connections().count(it.first) == 0) + return "connection mismatch: missing connection " + std::string(log_id(it.first)) + " in first design"; + + return ""; + } + + void check_cells() + { + for (const auto &it : mod_a->cells_) { + if (mod_b->cells_.count(it.first) == 0) + log_error("Module %s missing cell %s in second design.\n", log_id(mod_a->name), log_id(it.first)); + if (std::string mismatch = compare_cells(it.second, mod_b->cells_.at(it.first)); !mismatch.empty()) + log_error("Module %s cell %s %s.\n", log_id(mod_a->name), log_id(it.first), mismatch); + } + for (const auto &it : mod_b->cells_) + if (mod_a->cells_.count(it.first) == 0) + log_error("Module %s missing cell %s in first design.\n", log_id(mod_b->name), log_id(it.first)); + } + + void check_memories() + { + for (const auto &it : mod_a->memories) { + if (mod_b->memories.count(it.first) == 0) + log_error("Module %s missing memory %s in second design.\n", log_id(mod_a->name), log_id(it.first)); + if (std::string mismatch = compare_memories(it.second, mod_b->memories.at(it.first)); !mismatch.empty()) + log_error("Module %s memory %s %s.\n", log_id(mod_a->name), log_id(it.first), mismatch); + } + for (const auto &it : mod_b->memories) + if (mod_a->memories.count(it.first) == 0) + log_error("Module %s missing memory %s in first design.\n", log_id(mod_b->name), log_id(it.first)); + } + + std::string compare_case_rules(const RTLIL::CaseRule *a, const RTLIL::CaseRule *b) + { + if (std::string mismatch = compare_attributes(a, b); !mismatch.empty()) return mismatch; + + if (a->compare.size() != b->compare.size()) + return "compare size mismatch: " + std::to_string(a->compare.size()) + " != " + std::to_string(b->compare.size()); + for (size_t i = 0; i < a->compare.size(); i++) + if (!compare_sigspec(a->compare[i], b->compare[i])) + return "compare " + std::to_string(i) + " mismatch: " + log_signal(a->compare[i]) + " != " + log_signal(b->compare[i]); + + if (a->actions.size() != b->actions.size()) + return "actions size mismatch: " + std::to_string(a->actions.size()) + " != " + std::to_string(b->actions.size()); + for (size_t i = 0; i < a->actions.size(); i++) { + if (!compare_sigspec(a->actions[i].first, b->actions[i].first)) + return "action " + std::to_string(i) + " first mismatch: " + log_signal(a->actions[i].first) + " != " + log_signal(b->actions[i].first); + if (!compare_sigspec(a->actions[i].second, b->actions[i].second)) + return "action " + std::to_string(i) + " second mismatch: " + log_signal(a->actions[i].second) + " != " + log_signal(b->actions[i].second); + } + + if (a->switches.size() != b->switches.size()) + return "switches size mismatch: " + std::to_string(a->switches.size()) + " != " + std::to_string(b->switches.size()); + for (size_t i = 0; i < a->switches.size(); i++) + if (std::string mismatch = compare_switch_rules(a->switches[i], b->switches[i]); !mismatch.empty()) + return "switch " + std::to_string(i) + " " + mismatch; + + return ""; + } + + std::string compare_switch_rules(const RTLIL::SwitchRule *a, const RTLIL::SwitchRule *b) + { + if (std::string mismatch = compare_attributes(a, b); !mismatch.empty()) + return mismatch; + if (!compare_sigspec(a->signal, b->signal)) + return "signal mismatch: " + log_signal(a->signal) + " != " + log_signal(b->signal); + + if (a->cases.size() != b->cases.size()) + return "cases size mismatch: " + std::to_string(a->cases.size()) + " != " + std::to_string(b->cases.size()); + for (size_t i = 0; i < a->cases.size(); i++) + if (std::string mismatch = compare_case_rules(a->cases[i], b->cases[i]); !mismatch.empty()) + return "case " + std::to_string(i) + " " + mismatch; + + return ""; + } + + std::string compare_sync_rules(const RTLIL::SyncRule *a, const RTLIL::SyncRule *b) + { + if (a->type != b->type) + return "type mismatch: " + std::to_string(a->type) + " != " + std::to_string(b->type); + if (!compare_sigspec(a->signal, b->signal)) + return "signal mismatch: " + log_signal(a->signal) + " != " + log_signal(b->signal); + if (a->actions.size() != b->actions.size()) + return "actions size mismatch: " + std::to_string(a->actions.size()) + " != " + std::to_string(b->actions.size()); + for (size_t i = 0; i < a->actions.size(); i++) { + if (!compare_sigspec(a->actions[i].first, b->actions[i].first)) + return "action " + std::to_string(i) + " first mismatch: " + log_signal(a->actions[i].first) + " != " + log_signal(b->actions[i].first); + if (!compare_sigspec(a->actions[i].second, b->actions[i].second)) + return "action " + std::to_string(i) + " second mismatch: " + log_signal(a->actions[i].second) + " != " + log_signal(b->actions[i].second); + } + if (a->mem_write_actions.size() != b->mem_write_actions.size()) + return "mem_write_actions size mismatch: " + std::to_string(a->mem_write_actions.size()) + " != " + std::to_string(b->mem_write_actions.size()); + for (size_t i = 0; i < a->mem_write_actions.size(); i++) { + const auto &ma = a->mem_write_actions[i]; + const auto &mb = b->mem_write_actions[i]; + if (ma.memid != mb.memid) + return "mem_write_actions " + std::to_string(i) + " memid mismatch: " + log_id(ma.memid) + " != " + log_id(mb.memid); + if (!compare_sigspec(ma.address, mb.address)) + return "mem_write_actions " + std::to_string(i) + " address mismatch: " + log_signal(ma.address) + " != " + log_signal(mb.address); + if (!compare_sigspec(ma.data, mb.data)) + return "mem_write_actions " + std::to_string(i) + " data mismatch: " + log_signal(ma.data) + " != " + log_signal(mb.data); + if (!compare_sigspec(ma.enable, mb.enable)) + return "mem_write_actions " + std::to_string(i) + " enable mismatch: " + log_signal(ma.enable) + " != " + log_signal(mb.enable); + if (ma.priority_mask != mb.priority_mask) + return "mem_write_actions " + std::to_string(i) + " priority_mask mismatch: " + log_const(ma.priority_mask) + " != " + log_const(mb.priority_mask); + if (std::string mismatch = compare_attributes(&ma, &mb); !mismatch.empty()) + return "mem_write_actions " + std::to_string(i) + " " + mismatch; + } + return ""; + } + + std::string compare_processes(const RTLIL::Process *a, const RTLIL::Process *b) + { + if (a->name != b->name) return "name mismatch: " + std::string(log_id(a->name)) + " != " + log_id(b->name); + if (std::string mismatch = compare_attributes(a, b); !mismatch.empty()) + return mismatch; + if (std::string mismatch = compare_case_rules(&a->root_case, &b->root_case); !mismatch.empty()) + return "case rule " + mismatch; + if (a->syncs.size() != b->syncs.size()) + return "sync count mismatch: " + std::to_string(a->syncs.size()) + " != " + std::to_string(b->syncs.size()); + for (size_t i = 0; i < a->syncs.size(); i++) + if (std::string mismatch = compare_sync_rules(a->syncs[i], b->syncs[i]); !mismatch.empty()) + return "sync " + std::to_string(i) + " " + mismatch; + return ""; + } + + void check_processes() + { + for (auto &it : mod_a->processes) { + if (mod_b->processes.count(it.first) == 0) + log_error("Module %s missing process %s in second design.\n", log_id(mod_a->name), log_id(it.first)); + if (std::string mismatch = compare_processes(it.second, mod_b->processes.at(it.first)); !mismatch.empty()) + log_error("Module %s process %s %s.\n", log_id(mod_a->name), log_id(it.first), mismatch.c_str()); + } + for (auto &it : mod_b->processes) + if (mod_a->processes.count(it.first) == 0) + log_error("Module %s missing process %s in first design.\n", log_id(mod_b->name), log_id(it.first)); + } + + void check_connections() + { + const auto &conns_a = mod_a->connections(); + const auto &conns_b = mod_b->connections(); + if (conns_a.size() != conns_b.size()) { + log_error("Module %s connection count differs: %zu != %zu\n", log_id(mod_a->name), conns_a.size(), conns_b.size()); + } else { + for (size_t i = 0; i < conns_a.size(); i++) { + if (!compare_sigspec(conns_a[i].first, conns_b[i].first)) + log_error("Module %s connection %zu LHS %s != %s.\n", log_id(mod_a->name), i, log_signal(conns_a[i].first), log_signal(conns_b[i].first)); + if (!compare_sigspec(conns_a[i].second, conns_b[i].second)) + log_error("Module %s connection %zu RHS %s != %s.\n", log_id(mod_a->name), i, log_signal(conns_a[i].second), log_signal(conns_b[i].second)); + } + } + } + + void check() + { + if (mod_a->name != mod_b->name) + log_error("Modules have different names: %s != %s\n", log_id(mod_a->name), log_id(mod_b->name)); + if (std::string mismatch = compare_attributes(mod_a, mod_b); !mismatch.empty()) + log_error("Module %s %s.\n", log_id(mod_a->name), mismatch); + check_wires(); + check_cells(); + check_memories(); + check_connections(); + check_processes(); + } +}; + +struct DesignEqualPass : public Pass { + DesignEqualPass() : Pass("design_equal", "check if two designs are the same") { } + void help() override + { + log("\n"); + log(" design_equal \n"); + log("\n"); + log("Compare the current design with the design previously saved under the given\n"); + log("name. Abort with an error if the designs are different.\n"); + log("\n"); + } + void execute(std::vector args, RTLIL::Design *design) override + { + if (args.size() != 2) + log_cmd_error("Missing argument.\n"); + + std::string check_name = args[1]; + if (saved_designs.count(check_name) == 0) + log_cmd_error("No saved design '%s' found!\n", check_name.c_str()); + + RTLIL::Design *other = saved_designs.at(check_name); + + for (auto &it : design->modules_) { + RTLIL::Module *mod = it.second; + if (!other->has(mod->name)) + log_error("Second design missing module %s.\n", log_id(mod->name)); + + ModuleComparator cmp(mod, other->module(mod->name)); + cmp.check(); + } + for (auto &it : other->modules_) { + RTLIL::Module *mod = it.second; + if (!design->has(mod->name)) + log_error("First design missing module %s.\n", log_id(mod->name)); + } + + log("Designs are identical.\n"); + } +} DesignEqualPass; + +YOSYS_NAMESPACE_END diff --git a/tests/various/design_equal_fail.ys b/tests/various/design_equal_fail.ys new file mode 100644 index 000000000..330d8d838 --- /dev/null +++ b/tests/various/design_equal_fail.ys @@ -0,0 +1,22 @@ +logger -expect error "Second design missing module top_renamed" 1 + +read_rtlil < Date: Wed, 17 Dec 2025 03:11:06 +0000 Subject: [PATCH 2/3] Add -legalize option to read_rtlil --- frontends/rtlil/rtlil_frontend.cc | 141 +++++++++++++++++++++++++----- kernel/rtlil.cc | 7 ++ kernel/rtlil.h | 3 + 3 files changed, 131 insertions(+), 20 deletions(-) diff --git a/frontends/rtlil/rtlil_frontend.cc b/frontends/rtlil/rtlil_frontend.cc index 271962725..a1412d983 100644 --- a/frontends/rtlil/rtlil_frontend.cc +++ b/frontends/rtlil/rtlil_frontend.cc @@ -40,6 +40,7 @@ struct RTLILFrontendWorker { bool flag_nooverwrite = false; bool flag_overwrite = false; bool flag_lib = false; + bool flag_legalize = false; int line_num; std::string line_buf; @@ -322,6 +323,17 @@ struct RTLILFrontendWorker { return val; } + RTLIL::Wire *legalize_wire(RTLIL::IdString id) + { + int wires_size = current_module->wires_size(); + if (wires_size == 0) + error("No wires found for legalization"); + int hash = hash_ops::hash(id).yield(); + RTLIL::Wire *wire = current_module->wire_at(abs(hash % wires_size)); + log("Legalizing wire `%s' to `%s'.\n", log_id(id), log_id(wire->name)); + return wire; + } + RTLIL::SigSpec parse_sigspec() { RTLIL::SigSpec sig; @@ -339,8 +351,12 @@ struct RTLILFrontendWorker { std::optional id = try_parse_id(); if (id.has_value()) { RTLIL::Wire *wire = current_module->wire(*id); - if (wire == nullptr) - error("Wire `%s' not found.", *id); + if (wire == nullptr) { + if (flag_legalize) + wire = legalize_wire(*id); + else + error("Wire `%s' not found.", *id); + } sig = RTLIL::SigSpec(wire); } else { sig = RTLIL::SigSpec(parse_const()); @@ -349,17 +365,44 @@ struct RTLILFrontendWorker { while (try_parse_char('[')) { int left = parse_integer(); - if (left >= sig.size() || left < 0) - error("bit index %d out of range", left); + if (left >= sig.size() || left < 0) { + if (flag_legalize) { + int legalized; + if (sig.size() == 0) + legalized = 0; + else + legalized = std::max(0, std::min(left, sig.size() - 1)); + log("Legalizing bit index %d to %d.\n", left, legalized); + left = legalized; + } else { + error("bit index %d out of range", left); + } + } if (try_parse_char(':')) { int right = parse_integer(); - if (right < 0) - error("bit index %d out of range", right); - if (left < right) - error("invalid slice [%d:%d]", left, right); - sig = sig.extract(right, left-right+1); + if (right < 0) { + if (flag_legalize) { + log("Legalizing bit index %d to %d.\n", right, 0); + right = 0; + } else + error("bit index %d out of range", right); + } + if (left < right) { + if (flag_legalize) { + log("Legalizing bit index %d to %d.\n", left, right); + left = right; + } else + error("invalid slice [%d:%d]", left, right); + } + if (flag_legalize && left >= sig.size()) + log("Legalizing slice %d:%d by igoring it\n", left, right); + else + sig = sig.extract(right, left - right + 1); } else { - sig = sig.extract(left); + if (flag_legalize && left >= sig.size()) + log("Legalizing slice %d by igoring it\n", left); + else + sig = sig.extract(left); } expect_char(']'); } @@ -476,8 +519,14 @@ struct RTLILFrontendWorker { { std::optional id = try_parse_id(); if (id.has_value()) { - if (current_module->wire(*id) != nullptr) - error("RTLIL error: redefinition of wire %s.", *id); + if (current_module->wire(*id) != nullptr) { + if (flag_legalize) { + log("Legalizing redefinition of wire %s.\n", *id); + pool wires = {current_module->wire(*id)}; + current_module->remove(wires); + } else + error("RTLIL error: redefinition of wire %s.", *id); + } wire = current_module->addWire(std::move(*id)); break; } @@ -528,8 +577,13 @@ struct RTLILFrontendWorker { { std::optional id = try_parse_id(); if (id.has_value()) { - if (current_module->memories.count(*id) != 0) - error("RTLIL error: redefinition of memory %s.", *id); + if (current_module->memories.count(*id) != 0) { + if (flag_legalize) { + log("Legalizing redefinition of memory %s.\n", *id); + current_module->remove(current_module->memories.at(*id)); + } else + error("RTLIL error: redefinition of memory %s.", *id); + } memory->name = std::move(*id); break; } @@ -551,14 +605,36 @@ struct RTLILFrontendWorker { expect_eol(); } + void legalize_width_parameter(RTLIL::Cell *cell, RTLIL::IdString port_name) + { + std::string width_param_name = port_name.str() + "_WIDTH"; + if (cell->parameters.count(width_param_name) == 0) + return; + RTLIL::Const ¶m = cell->parameters.at(width_param_name); + if (param.as_int() != 0) + return; + cell->parameters[width_param_name] = RTLIL::Const(cell->getPort(port_name).size()); + } + void parse_cell() { RTLIL::IdString cell_type = parse_id(); RTLIL::IdString cell_name = parse_id(); expect_eol(); - if (current_module->cell(cell_name) != nullptr) - error("RTLIL error: redefinition of cell %s.", cell_name); + if (current_module->cell(cell_name) != nullptr) { + if (flag_legalize) { + RTLIL::IdString new_name; + int suffix = 1; + do { + new_name = RTLIL::IdString(cell_name.str() + "_" + std::to_string(suffix)); + ++suffix; + } while (current_module->cell(new_name) != nullptr); + log("Legalizing redefinition of cell %s by renaming to %s.\n", cell_name, new_name); + cell_name = new_name; + } else + error("RTLIL error: redefinition of cell %s.", cell_name); + } RTLIL::Cell *cell = current_module->addCell(cell_name, cell_type); cell->attributes = std::move(attrbuf); @@ -587,9 +663,15 @@ struct RTLILFrontendWorker { expect_eol(); } else if (try_parse_keyword("connect")) { RTLIL::IdString port_name = parse_id(); - if (cell->hasPort(port_name)) - error("RTLIL error: redefinition of cell port %s.", port_name); + if (cell->hasPort(port_name)) { + if (flag_legalize) + log("Legalizing redefinition of cell port %s.", port_name); + else + error("RTLIL error: redefinition of cell port %s.", port_name); + } cell->setPort(std::move(port_name), parse_sigspec()); + if (flag_legalize) + legalize_width_parameter(cell, port_name); expect_eol(); } else if (try_parse_keyword("end")) { expect_eol(); @@ -606,6 +688,11 @@ struct RTLILFrontendWorker { error("dangling attribute"); RTLIL::SigSpec s1 = parse_sigspec(); RTLIL::SigSpec s2 = parse_sigspec(); + if (flag_legalize) { + int min_size = std::min(s1.size(), s2.size()); + s1 = s1.extract(0, min_size); + s2 = s2.extract(0, min_size); + } current_module->connect(std::move(s1), std::move(s2)); expect_eol(); } @@ -682,8 +769,13 @@ struct RTLILFrontendWorker { RTLIL::IdString proc_name = parse_id(); expect_eol(); - if (current_module->processes.count(proc_name) != 0) - error("RTLIL error: redefinition of process %s.", proc_name); + if (current_module->processes.count(proc_name) != 0) { + if (flag_legalize) { + log("Legalizing redefinition of process %s.\n", proc_name); + current_module->remove(current_module->processes.at(proc_name)); + } else + error("RTLIL error: redefinition of process %s.", proc_name); + } RTLIL::Process *proc = current_module->addProcess(std::move(proc_name)); proc->attributes = std::move(attrbuf); @@ -804,6 +896,11 @@ struct RTLILFrontend : public Frontend { log(" -lib\n"); log(" only create empty blackbox modules\n"); log("\n"); + log(" -legalize\n"); + log(" prevent semantic errors (e.g. reference to unknown wire, redefinition of wire/cell)\n"); + log(" by deterministically rewriting the input into something valid. Useful when using\n"); + log(" fuzzing to generate random but valid RTLIL.\n"); + log("\n"); } void execute(std::istream *&f, std::string filename, std::vector args, RTLIL::Design *design) override { @@ -828,6 +925,10 @@ struct RTLILFrontend : public Frontend { worker.flag_lib = true; continue; } + if (arg == "-legalize") { + worker.flag_legalize = true; + continue; + } break; } extra_args(f, filename, args, argidx); diff --git a/kernel/rtlil.cc b/kernel/rtlil.cc index d92aec73b..a09f9497a 100644 --- a/kernel/rtlil.cc +++ b/kernel/rtlil.cc @@ -3039,6 +3039,13 @@ void RTLIL::Module::remove(RTLIL::Cell *cell) } } +void RTLIL::Module::remove(RTLIL::Memory *memory) +{ + log_assert(memories.count(memory->name) != 0); + memories.erase(memory->name); + delete memory; +} + void RTLIL::Module::remove(RTLIL::Process *process) { log_assert(processes.count(process->name) != 0); diff --git a/kernel/rtlil.h b/kernel/rtlil.h index f841df1ed..163dbe5a8 100644 --- a/kernel/rtlil.h +++ b/kernel/rtlil.h @@ -2139,6 +2139,8 @@ public: } RTLIL::ObjRange wires() { return RTLIL::ObjRange(&wires_, &refcount_wires_); } + int wires_size() const { return wires_.size(); } + RTLIL::Wire* wire_at(int index) const { return wires_.element(index)->second; } RTLIL::ObjRange cells() { return RTLIL::ObjRange(&cells_, &refcount_cells_); } void add(RTLIL::Binding *binding); @@ -2146,6 +2148,7 @@ public: // Removing wires is expensive. If you have to remove wires, remove them all at once. void remove(const pool &wires); void remove(RTLIL::Cell *cell); + void remove(RTLIL::Memory *memory); void remove(RTLIL::Process *process); void rename(RTLIL::Wire *wire, RTLIL::IdString new_name); From 9ee51c8f27164e85c15ccbfc18910169f6d647b8 Mon Sep 17 00:00:00 2001 From: Robert O'Callahan Date: Sun, 21 Dec 2025 22:45:06 +0000 Subject: [PATCH 3/3] Add AFL++ Grammar-Generator grammar for RTLIL fuzzing, and instructions for how to use it. --- tests/pass-fuzzing.md | 106 ++++++++++++++++++++++++++++ tests/tools/rtlil-fuzz-grammar.json | 104 +++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 tests/pass-fuzzing.md create mode 100644 tests/tools/rtlil-fuzz-grammar.json diff --git a/tests/pass-fuzzing.md b/tests/pass-fuzzing.md new file mode 100644 index 000000000..993f36078 --- /dev/null +++ b/tests/pass-fuzzing.md @@ -0,0 +1,106 @@ +Suppose you're making significant changes to a pass that should not change +the pass's output in any way. It might be useful to run a large number of +automatically generated tests to try to find bugs where the output has +changed. This document describes how to do that. + +Basically we're going to use [AFL++](https://github.com/AFLplusplus/AFLplusplus) with the +[Grammar-Mutator](https://github.com/AFLplusplus/Grammar-Mutator) plugin to generate +RTLIL testcases. For each testcase, we run a Yosys script that applies both the old and new +implementation of the pass to the same design and compares the results. Testcase +generation is coverage-guided, i.e. the fuzzer will try to find testcases that exercise all +code in the old and new implementation of the pass (and in the RTLIL parser). + +## Setup + +These instructions clone tools into subdirectories of your home directory. They assume +you have a Yosys checkout under `$HOME/yosys`, and that you're testing the `opt_merge` pass. +They have been tested with AFL++ revision 68b492b2c7725816068718ef9437b72b40e67519 and Grammar-Mutator revision 05d8f537f8d656f0754e7ad5dcc653c42cb4f8ff. + +Clone and build AFL++ and Grammar-Mutator: +``` +cd $HOME +git clone https://github.com/AFLplusplus/AFLplusplus.git +git -C AFLplusplus checkout stable +git clone https://github.com/AFLplusplus/Grammar-Mutator.git +git -C Grammar-Mutator checkout stable +``` + +Check that `rtlil-fuzz-grammar.json` generates RTLIL constructs relevant to your pass. +Currently it's quite simple and generates a limited set of cells and wires; you may need to +extend it to generate different kinds of cells and other RTLIL constructs (e.g. `proc`). + +Build AFL++ and Grammar-Mutator: +``` +make -C $HOME/AFLplusplus -j all +make -C $HOME/Grammar-Mutator -j GRAMMAR_FILE=$HOME/yosys/tests/tools/rtlil-fuzz-grammar.json +``` + +Create a Yosys commit that adds the old version of your pass as a new command, e.g. copy +`opt_merge.cc` into `old_opt_merge.cc` and change the name of the command to `old_opt_merge`. +[Here's](https://github.com/YosysHQ/yosys/commit/827cd8c998f3e455b14ac990a3159030ddc19b21) an example. + +You may also need to patch in [this commit](https://github.com/YosysHQ/yosys/commit/121c52f514c4ca282b4e6b3b14f71184f3849ddf) to work around a bug involving `std::reverse` on +empty vectors in the RTLIL parser when building with fuzzing instrumentation. +I think this is a clang++ bug so hopefully it will get fixed eventually and that patch will not be +necessary. + +Rebuild Yosys with the AFL++ compiler wrapper. This assumes your config builds Yosys with clang++. +``` +(cd $HOME/yosys; patch -lp1 << EOF) +diff --git a/Makefile b/Makefile +index 9c361294d..c9a98f74c 100644 +--- a/Makefile ++++ b/Makefile +@@ -238,7 +238,7 @@ + LTOFLAGS := $(GCC_LTO) + + ifeq ($(CONFIG),clang) +-CXX = clang++ ++CXX = $(HOME)/AFLplusplus/afl-c++ + CXXFLAGS += -std=$(CXXSTD) $(OPT_LEVEL) + ifeq ($(ENABLE_LTO),1) + LINKFLAGS += -fuse-ld=lld +EOF +make -C yosys clean && make -C yosys -j +``` + +You probably need to configure coredumps to work normally instead of going through some OS service: +``` +echo core | sudo tee /proc/sys/kernel/core_pattern +``` + +## Running the fuzzer + +Generate some initial testcases using Grammar-Mutator: +``` +(cd $HOME/Grammar-Mutator; rm -rf seeds trees; ./grammar_generator-rtlil 100 1000 ./seeds ./trees) +``` + +Now run AFL++. +``` +(cd $HOME/Grammar-Mutator; \ + AFL_CUSTOM_MUTATOR_LIBRARY=./libgrammarmutator-rtlil.so \ + AFL_CUSTOM_MUTATOR_ONLY=1 \ + AFL_BENCH_UNTIL_CRASH=1 \ + YOSYS_WORK_UNITS_PER_THREAD=1 \ + YOSYS_ABORT_ON_LOG_ERROR=1 \ + $HOME/AFLplusplus/afl-fuzz -t 5000 -m none -i seeds -o out -- \ + $HOME/yosys/yosys -p 'read_rtlil -legalize @@; design -save init; old_opt_merge; design -save old; design -load init; opt_merge; design_equal old' \ +) +``` +This will run the fuzzer until the first crash (including any pass output mismatches) and then stop. +Or if you're lucky, the fuzzer will run indefinitely. This uses very little parallelism; if it doesn't find any errors right away, you can increase the test throughput by running AFL++ in parallel using the instructions [here](https://aflplus.plus/docs/parallel_fuzzing). + +## Working with fuzz test failures + +Any failing testcases will be dropped in `$HOME/Grammar-Mutator/out/default/crashes`. +Run `yosys -p 'read_rtlil -legalize ... ; dump'` to get the testcase as legalized RTLIL. + +## Notes on generating semantically valid RTLIL + +`Grammar-Mutator` generates RTLIL files according to the context-free grammar in `rtlil-fuzz-grammar.json`. +However, the testcases must also be semantically valid, e.g. references to wires should only refer to +wires that actually exist. These constraints cannot reasonably be expresed in a CFG. Therefore we +have added a `-legalize` option to the `read_rtlil` command. When `-legalize` is set, when `read_rtlil` +detects a failed semantic check, instead of erroring out it emits a warning and patches the incoming RTLIL +to make it valid. diff --git a/tests/tools/rtlil-fuzz-grammar.json b/tests/tools/rtlil-fuzz-grammar.json new file mode 100644 index 000000000..c27b160f4 --- /dev/null +++ b/tests/tools/rtlil-fuzz-grammar.json @@ -0,0 +1,104 @@ +{ + "": [ + [ + "module \\test\n", + "", "", + "", + "", + "end\n" + ] + ], + "": [ [ " wire width ", "", " ", "", " ", "", "\n" ] ], + "": [ [ "1" ], [ "2" ], [ "3" ], [ "4" ], [ "32" ], [ "128" ] ], + "": [ [ "input ", "" ], [ "output ", "" ], [ "inout ", "" ], [] ], + "": [ + [ + " cell $not ", "", "\n", + " parameter \\A_SIGNED 0\n", + " parameter \\A_WIDTH 0\n", + " parameter \\Y_WIDTH 0\n", + " connect \\A ", "", "\n", + " connect \\Y ", "", "\n", + " end\n" + ], + [ + " cell $and ", "", "\n", + " parameter \\A_SIGNED 0\n", + " parameter \\B_SIGNED 0\n", + " parameter \\A_WIDTH 0\n", + " parameter \\B_WIDTH 0\n", + " parameter \\Y_WIDTH 0\n", + " connect \\A ", "", "\n", + " connect \\B ", "", "\n", + " connect \\Y ", "", "\n", + " end\n" + ], + [ + " cell $or ", "", "\n", + " parameter \\A_SIGNED 0\n", + " parameter \\B_SIGNED 0\n", + " parameter \\A_WIDTH 0\n", + " parameter \\B_WIDTH 0\n", + " parameter \\Y_WIDTH 0\n", + " connect \\A ", "", "\n", + " connect \\B ", "", "\n", + " connect \\Y ", "", "\n", + " end\n" + ], + [ + " cell $xor ", "", "\n", + " parameter \\A_SIGNED 0\n", + " parameter \\B_SIGNED 0\n", + " parameter \\A_WIDTH 0\n", + " parameter \\B_WIDTH 0\n", + " parameter \\Y_WIDTH 0\n", + " connect \\A ", "", "\n", + " connect \\B ", "", "\n", + " connect \\Y ", "", "\n", + " end\n" + ], + [ + " cell ", "", " ", "", "\n", + " connect \\A ", "", "\n", + " connect \\Y ", "", "\n", + " end\n" + ], + [ + " cell ", "", " ", "", "\n", + " connect \\A ", "", "\n", + " connect \\B ", "", "\n", + " connect \\Y ", "", "\n", + " end\n" + ] + ], + "": [ [ "\\wire_a" ], [ "\\wire_b" ], [ "\\wire_c" ], [ "\\wire_d" ], [ "\\wire_e" ], [ "\\wire_f" ], [ "\\wire_g" ], [ "\\wire_h" ], [ "\\wire_i" ], [ "\\wire_j" ] ], + "": [ [ "\\cell_a" ], [ "\\cell_b" ], [ "\\cell_c" ], [ "\\cell_d" ], [ "\\cell_e" ], [ "\\cell_f" ], [ "\\cell_g" ], [ "\\cell_h" ], [ "\\cell_i" ], [ "\\cell_j" ] ], + "": [ [ "\\bb1" ], [ "\\bb2" ] ], + "": [ + [ "", " " ], + [ "{", "", " ", "", "}" ], + [ "" ], + [ "", "[", "", "]" ], + [ "", "[", "", ":", "", "]" ] + ], + "": [ + [ "0'", "" ], + [ "1'", "" ], + [ "2'", "" ], + [ "3'", "" ], + [ "4'", "" ], + [ "31'", "" ], + [ "32'", "" ], + [ "128'", "" ] + ], + "": [ [ "0" ], [ "1" ], [ "x" ], [ "z" ], [ "-" ], [ "m" ] ], + "": [ "0", "1", "2", "3", "31", "32" ], + "": [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ], + "": [ [ " connect ", "", " ", "", "\n" ] ], + + "": [ [ ], [ "", "" ] ], + "": [ [ ], [ "", "" ] ], + "": [ [ ], [ "", "" ] ], + "": [ [ ], [ "", "" ] ], + "": [ [ ], [ "", " ", "" ] ] +}