From 1a44645aeffac998204a05e6d12b34073f8d5ad4 Mon Sep 17 00:00:00 2001 From: Catherine Date: Tue, 13 Feb 2024 17:43:53 +0000 Subject: [PATCH] cxxrtl: expose scope information in the C++ API. This commit adds a `debug_scopes` container, which can collect metadata about scopes in a design. Currently the only scope is that of a module. A module scope can be represented either by a module and cell pair, or a `$scopeinfo` cell in a flattened netlist. The metadata produced by the C++ API is identical between these two cases, so flattening remains transparent to a netlist with CXXRTL. The existing `debug_items` method is deprecated. This isn't strictly necessary, but the user experience is better if the path is provided as e.g. `"top "` (as some VCD viewers make it awkward to select topmost anonymous scope), and the upgrade flow encourages that, which should reduce frustration later. While the new `debug_items` method could still be broken in the future as the C++ API permits, this seems unlikely since the debug information can now capture all common netlist aspects and includes several extension points (via `debug_item`, `debug_scope` types). Also, naming of scope paths was normalized to `path` or `top_path`, as applicable. --- backends/cxxrtl/cxxrtl_backend.cc | 314 ++++++++++-------- .../cxxrtl/runtime/cxxrtl/capi/cxxrtl_capi.cc | 12 +- .../cxxrtl/runtime/cxxrtl/capi/cxxrtl_capi.h | 4 +- backends/cxxrtl/runtime/cxxrtl/cxxrtl.h | 70 +++- .../cxxrtl/runtime/cxxrtl/cxxrtl_replay.h | 9 +- 5 files changed, 247 insertions(+), 162 deletions(-) diff --git a/backends/cxxrtl/cxxrtl_backend.cc b/backends/cxxrtl/cxxrtl_backend.cc index 1bd876bbc..877a829d3 100644 --- a/backends/cxxrtl/cxxrtl_backend.cc +++ b/backends/cxxrtl/cxxrtl_backend.cc @@ -25,6 +25,7 @@ #include "kernel/mem.h" #include "kernel/log.h" #include "kernel/fmt.h" +#include "kernel/scopeinfo.h" USING_YOSYS_NAMESPACE PRIVATE_NAMESPACE_BEGIN @@ -2311,11 +2312,14 @@ struct CxxrtlWorker { dict attributes = object->attributes; // Inherently necessary to get access to the object, so a waste of space to emit. attributes.erase(ID::hdlname); + // Internal Yosys attribute that should be removed but isn't. + attributes.erase(ID::module_not_derived); dump_metadata_map(attributes); } void dump_debug_info_method(RTLIL::Module *module) { + size_t count_scopes = 0; size_t count_public_wires = 0; size_t count_member_wires = 0; size_t count_undriven = 0; @@ -2328,153 +2332,188 @@ struct CxxrtlWorker { size_t count_skipped_wires = 0; inc_indent(); f << indent << "assert(path.empty() || path[path.size() - 1] == ' ');\n"; - for (auto wire : module->wires()) { - const auto &debug_wire_type = debug_wire_types[wire]; - if (!wire->name.isPublic()) - continue; - count_public_wires++; - switch (debug_wire_type.type) { - case WireType::BUFFERED: - case WireType::MEMBER: { - // Member wire - std::vector flags; - - if (wire->port_input && wire->port_output) - flags.push_back("INOUT"); - else if (wire->port_output) - flags.push_back("OUTPUT"); - else if (wire->port_input) - flags.push_back("INPUT"); - - bool has_driven_sync = false; - bool has_driven_comb = false; - bool has_undriven = false; - if (!module->get_bool_attribute(ID(cxxrtl_blackbox))) { - for (auto bit : SigSpec(wire)) - if (!bit_has_state.count(bit)) - has_undriven = true; - else if (bit_has_state[bit]) - has_driven_sync = true; - else - has_driven_comb = true; - } else if (wire->port_output) { - switch (cxxrtl_port_type(module, wire->name)) { - case CxxrtlPortType::SYNC: - has_driven_sync = true; - break; - case CxxrtlPortType::COMB: - has_driven_comb = true; - break; - case CxxrtlPortType::UNKNOWN: - has_driven_sync = has_driven_comb = true; - break; - } - } else { - has_undriven = true; - } - if (has_undriven) - flags.push_back("UNDRIVEN"); - if (!has_driven_sync && !has_driven_comb && has_undriven) - count_undriven++; - if (has_driven_sync) - flags.push_back("DRIVEN_SYNC"); - if (has_driven_sync && !has_driven_comb && !has_undriven) - count_driven_sync++; - if (has_driven_comb) - flags.push_back("DRIVEN_COMB"); - if (!has_driven_sync && has_driven_comb && !has_undriven) - count_driven_comb++; - if (has_driven_sync + has_driven_comb + has_undriven > 1) - count_mixed_driver++; - - f << indent << "items.add(path + " << escape_cxx_string(get_hdl_name(wire)); - f << ", debug_item(" << mangle(wire) << ", " << wire->start_offset; - bool first = true; - for (auto flag : flags) { - if (first) { - first = false; - f << ", "; - } else { - f << "|"; - } - f << "debug_item::" << flag; - } - f << "), "; - dump_debug_attrs(wire); + f << indent << "if (scopes) {\n"; + inc_indent(); + // The module is responsible for adding its own scope. + f << indent << "scopes->add(path.empty() ? path : path.substr(0, path.size() - 1), "; + f << escape_cxx_string(get_hdl_name(module)) << ", "; + dump_debug_attrs(module); + f << ", std::move(cell_attrs));\n"; + count_scopes++; + // If there were any submodules that were flattened, the module is also responsible for adding them. + for (auto cell : module->cells()) { + if (cell->type != ID($scopeinfo)) continue; + if (cell->getParam(ID::TYPE).decode_string() == "module") { + auto module_attrs = scopeinfo_attributes(cell, ScopeinfoAttrs::Module); + auto cell_attrs = scopeinfo_attributes(cell, ScopeinfoAttrs::Cell); + cell_attrs.erase(ID::module_not_derived); + f << indent << "scopes->add(path + " << escape_cxx_string(get_hdl_name(cell)) << ", "; + f << escape_cxx_string(cell->get_string_attribute(ID(module))) << ", "; + dump_metadata_map(module_attrs); + f << ", "; + dump_metadata_map(cell_attrs); f << ");\n"; - count_member_wires++; - break; - } - case WireType::ALIAS: { - // Alias of a member wire - const RTLIL::Wire *aliasee = debug_wire_type.sig_subst.as_wire(); - f << indent << "items.add(path + " << escape_cxx_string(get_hdl_name(wire)); - f << ", debug_item("; - // If the aliasee is an outline, then the alias must be an outline, too; otherwise downstream - // tooling has no way to find out about the outline. - if (debug_wire_types[aliasee].is_outline()) - f << "debug_eval_outline"; - else - f << "debug_alias()"; - f << ", " << mangle(aliasee) << ", " << wire->start_offset << "), "; - dump_debug_attrs(aliasee); - f << ");\n"; - count_alias_wires++; - break; - } - case WireType::CONST: { - // Wire tied to a constant - f << indent << "static const value<" << wire->width << "> const_" << mangle(wire) << " = "; - dump_const(debug_wire_type.sig_subst.as_const()); - f << ";\n"; - f << indent << "items.add(path + " << escape_cxx_string(get_hdl_name(wire)); - f << ", debug_item(const_" << mangle(wire) << ", " << wire->start_offset << "), "; - dump_debug_attrs(wire); - f << ");\n"; - count_const_wires++; - break; - } - case WireType::OUTLINE: { - // Localized or inlined, but rematerializable wire - f << indent << "items.add(path + " << escape_cxx_string(get_hdl_name(wire)); - f << ", debug_item(debug_eval_outline, " << mangle(wire) << ", " << wire->start_offset << "), "; - dump_debug_attrs(wire); - f << ");\n"; - count_inline_wires++; - break; - } - default: { - // Localized or inlined wire with no debug information - count_skipped_wires++; - break; - } + } else log_assert(false && "Unknown $scopeinfo type"); + count_scopes++; } - } - if (!module->get_bool_attribute(ID(cxxrtl_blackbox))) { - for (auto &mem : mod_memories[module]) { - if (!mem.memid.isPublic()) + dec_indent(); + f << indent << "}\n"; + f << indent << "if (items) {\n"; + inc_indent(); + for (auto wire : module->wires()) { + const auto &debug_wire_type = debug_wire_types[wire]; + if (!wire->name.isPublic()) continue; - f << indent << "items.add(path + " << escape_cxx_string(mem.packed ? get_hdl_name(mem.cell) : get_hdl_name(mem.mem)); - f << ", debug_item(" << mangle(&mem) << ", "; - f << mem.start_offset << "), "; - if (mem.packed) { - dump_debug_attrs(mem.cell); - } else { - dump_debug_attrs(mem.mem); + count_public_wires++; + switch (debug_wire_type.type) { + case WireType::BUFFERED: + case WireType::MEMBER: { + // Member wire + std::vector flags; + + if (wire->port_input && wire->port_output) + flags.push_back("INOUT"); + else if (wire->port_output) + flags.push_back("OUTPUT"); + else if (wire->port_input) + flags.push_back("INPUT"); + + bool has_driven_sync = false; + bool has_driven_comb = false; + bool has_undriven = false; + if (!module->get_bool_attribute(ID(cxxrtl_blackbox))) { + for (auto bit : SigSpec(wire)) + if (!bit_has_state.count(bit)) + has_undriven = true; + else if (bit_has_state[bit]) + has_driven_sync = true; + else + has_driven_comb = true; + } else if (wire->port_output) { + switch (cxxrtl_port_type(module, wire->name)) { + case CxxrtlPortType::SYNC: + has_driven_sync = true; + break; + case CxxrtlPortType::COMB: + has_driven_comb = true; + break; + case CxxrtlPortType::UNKNOWN: + has_driven_sync = has_driven_comb = true; + break; + } + } else { + has_undriven = true; + } + if (has_undriven) + flags.push_back("UNDRIVEN"); + if (!has_driven_sync && !has_driven_comb && has_undriven) + count_undriven++; + if (has_driven_sync) + flags.push_back("DRIVEN_SYNC"); + if (has_driven_sync && !has_driven_comb && !has_undriven) + count_driven_sync++; + if (has_driven_comb) + flags.push_back("DRIVEN_COMB"); + if (!has_driven_sync && has_driven_comb && !has_undriven) + count_driven_comb++; + if (has_driven_sync + has_driven_comb + has_undriven > 1) + count_mixed_driver++; + + f << indent << "items->add(path + " << escape_cxx_string(get_hdl_name(wire)); + f << ", debug_item(" << mangle(wire) << ", " << wire->start_offset; + bool first = true; + for (auto flag : flags) { + if (first) { + first = false; + f << ", "; + } else { + f << "|"; + } + f << "debug_item::" << flag; + } + f << "), "; + dump_debug_attrs(wire); + f << ");\n"; + count_member_wires++; + break; + } + case WireType::ALIAS: { + // Alias of a member wire + const RTLIL::Wire *aliasee = debug_wire_type.sig_subst.as_wire(); + f << indent << "items->add(path + " << escape_cxx_string(get_hdl_name(wire)); + f << ", debug_item("; + // If the aliasee is an outline, then the alias must be an outline, too; otherwise downstream + // tooling has no way to find out about the outline. + if (debug_wire_types[aliasee].is_outline()) + f << "debug_eval_outline"; + else + f << "debug_alias()"; + f << ", " << mangle(aliasee) << ", " << wire->start_offset << "), "; + dump_debug_attrs(aliasee); + f << ");\n"; + count_alias_wires++; + break; + } + case WireType::CONST: { + // Wire tied to a constant + f << indent << "static const value<" << wire->width << "> const_" << mangle(wire) << " = "; + dump_const(debug_wire_type.sig_subst.as_const()); + f << ";\n"; + f << indent << "items->add(path + " << escape_cxx_string(get_hdl_name(wire)); + f << ", debug_item(const_" << mangle(wire) << ", " << wire->start_offset << "), "; + dump_debug_attrs(wire); + f << ");\n"; + count_const_wires++; + break; + } + case WireType::OUTLINE: { + // Localized or inlined, but rematerializable wire + f << indent << "items->add(path + " << escape_cxx_string(get_hdl_name(wire)); + f << ", debug_item(debug_eval_outline, " << mangle(wire) << ", " << wire->start_offset << "), "; + dump_debug_attrs(wire); + f << ");\n"; + count_inline_wires++; + break; + } + default: { + // Localized or inlined wire with no debug information + count_skipped_wires++; + break; + } } - f << ");\n"; } + if (!module->get_bool_attribute(ID(cxxrtl_blackbox))) { + for (auto &mem : mod_memories[module]) { + if (!mem.memid.isPublic()) + continue; + f << indent << "items->add(path + " << escape_cxx_string(mem.packed ? get_hdl_name(mem.cell) : get_hdl_name(mem.mem)); + f << ", debug_item(" << mangle(&mem) << ", "; + f << mem.start_offset << "), "; + if (mem.packed) { + dump_debug_attrs(mem.cell); + } else { + dump_debug_attrs(mem.mem); + } + f << ");\n"; + } + } + dec_indent(); + f << indent << "}\n"; + if (!module->get_bool_attribute(ID(cxxrtl_blackbox))) { for (auto cell : module->cells()) { if (is_internal_cell(cell->type)) continue; const char *access = is_cxxrtl_blackbox_cell(cell) ? "->" : "."; - f << indent << mangle(cell) << access << "debug_info(items, "; - f << "path + " << escape_cxx_string(get_hdl_name(cell) + ' ') << ");\n"; + f << indent << mangle(cell) << access; + f << "debug_info(items, scopes, path + " << escape_cxx_string(get_hdl_name(cell) + ' ') << ", "; + dump_debug_attrs(cell); + f << ");\n"; } } dec_indent(); log_debug("Debug information statistics for module `%s':\n", log_id(module)); + log_debug(" Scopes: %zu", count_scopes); log_debug(" Public wires: %zu, of which:\n", count_public_wires); log_debug(" Member wires: %zu, of which:\n", count_member_wires); log_debug(" Undriven: %zu (incl. inputs)\n", count_undriven); @@ -2522,7 +2561,8 @@ struct CxxrtlWorker { f << indent << "}\n"; if (debug_info) { f << "\n"; - f << indent << "void debug_info(debug_items &items, std::string path = \"\") override {\n"; + f << indent << "void debug_info(debug_items *items, debug_scopes *scopes, " + << "std::string path, metadata_map &&cell_attrs = {}) override {\n"; dump_debug_info_method(module); f << indent << "}\n"; } @@ -2631,7 +2671,8 @@ struct CxxrtlWorker { } } f << "\n"; - f << indent << "void debug_info(debug_items &items, std::string path = \"\") override;\n"; + f << indent << "void debug_info(debug_items *items, debug_scopes *scopes, " + << "std::string path, metadata_map &&cell_attrs = {}) override;\n"; } dec_indent(); f << indent << "}; // struct " << mangle(module) << "\n"; @@ -2659,7 +2700,8 @@ struct CxxrtlWorker { } f << "\n"; f << indent << "CXXRTL_EXTREMELY_COLD\n"; - f << indent << "void " << mangle(module) << "::debug_info(debug_items &items, std::string path) {\n"; + f << indent << "void " << mangle(module) << "::debug_info(debug_items *items, debug_scopes *scopes, " + << "std::string path, metadata_map &&cell_attrs) {\n"; dump_debug_info_method(module); f << indent << "}\n"; } diff --git a/backends/cxxrtl/runtime/cxxrtl/capi/cxxrtl_capi.cc b/backends/cxxrtl/runtime/cxxrtl/capi/cxxrtl_capi.cc index 593e067a1..3c62401dd 100644 --- a/backends/cxxrtl/runtime/cxxrtl/capi/cxxrtl_capi.cc +++ b/backends/cxxrtl/runtime/cxxrtl/capi/cxxrtl_capi.cc @@ -35,19 +35,19 @@ cxxrtl_handle cxxrtl_create(cxxrtl_toplevel design) { return cxxrtl_create_at(design, ""); } -cxxrtl_handle cxxrtl_create_at(cxxrtl_toplevel design, const char *root) { - std::string path = root; - if (!path.empty()) { +cxxrtl_handle cxxrtl_create_at(cxxrtl_toplevel design, const char *top_path_) { + std::string top_path = top_path_; + if (!top_path.empty()) { // module::debug_info() accepts either an empty path, or a path ending in space to simplify // the logic in generated code. While this is sketchy at best to expose in the C++ API, this // would be a lot worse in the C API, so don't expose it here. - assert(path.back() != ' '); - path += ' '; + assert(top_path.back() != ' '); + top_path += ' '; } cxxrtl_handle handle = new _cxxrtl_handle; handle->module = std::move(design->module); - handle->module->debug_info(handle->objects, path); + handle->module->debug_info(handle->objects, top_path); delete design; return handle; } diff --git a/backends/cxxrtl/runtime/cxxrtl/capi/cxxrtl_capi.h b/backends/cxxrtl/runtime/cxxrtl/capi/cxxrtl_capi.h index c2a9d37e1..ae42733ad 100644 --- a/backends/cxxrtl/runtime/cxxrtl/capi/cxxrtl_capi.h +++ b/backends/cxxrtl/runtime/cxxrtl/capi/cxxrtl_capi.h @@ -55,8 +55,8 @@ cxxrtl_handle cxxrtl_create(cxxrtl_toplevel design); // Create a design handle at a given hierarchy position from a design toplevel. // // This operation is similar to `cxxrtl_create`, except the full hierarchical name of every object -// is prepended with `root`. -cxxrtl_handle cxxrtl_create_at(cxxrtl_toplevel design, const char *root); +// is prepended with `top_path`. +cxxrtl_handle cxxrtl_create_at(cxxrtl_toplevel design, const char *top_path); // Release all resources used by a design and its handle. void cxxrtl_destroy(cxxrtl_handle handle); diff --git a/backends/cxxrtl/runtime/cxxrtl/cxxrtl.h b/backends/cxxrtl/runtime/cxxrtl/cxxrtl.h index f9d22ff9b..b834cd120 100644 --- a/backends/cxxrtl/runtime/cxxrtl/cxxrtl.h +++ b/backends/cxxrtl/runtime/cxxrtl/cxxrtl.h @@ -1331,14 +1331,15 @@ struct debug_items { std::map> table; std::map> attrs_table; - void add(const std::string &name, debug_item &&item, metadata_map &&item_attrs = {}) { - std::unique_ptr &attrs = attrs_table[name]; + void add(const std::string &path, debug_item &&item, metadata_map &&item_attrs = {}) { + assert((path.empty() || path[path.size() - 1] != ' ') && path.find(" ") == std::string::npos); + std::unique_ptr &attrs = attrs_table[path]; if (attrs.get() == nullptr) attrs = std::unique_ptr(new debug_attrs); for (auto attr : item_attrs) attrs->map.insert(attr); item.attrs = attrs.get(); - std::vector &parts = table[name]; + std::vector &parts = table[path]; parts.emplace_back(item); std::sort(parts.begin(), parts.end(), [](const debug_item &a, const debug_item &b) { @@ -1346,25 +1347,58 @@ struct debug_items { }); } - size_t count(const std::string &name) const { - if (table.count(name) == 0) + size_t count(const std::string &path) const { + if (table.count(path) == 0) return 0; - return table.at(name).size(); + return table.at(path).size(); } - const std::vector &at(const std::string &name) const { - return table.at(name); + const std::vector &at(const std::string &path) const { + return table.at(path); } // Like `at()`, but operates only on single-part debug items. - const debug_item &operator [](const std::string &name) const { - const std::vector &parts = table.at(name); + const debug_item &operator [](const std::string &path) const { + const std::vector &parts = table.at(path); assert(parts.size() == 1); return parts.at(0); } - const metadata_map &attrs(const std::string &name) const { - return attrs_table.at(name)->map; + bool is_memory(const std::string &path) const { + return at(path).at(0).type == debug_item::MEMORY; + } + + const metadata_map &attrs(const std::string &path) const { + return attrs_table.at(path)->map; + } +}; + +// Only `module` scopes are defined. The type is implicit, since Yosys does not currently support +// any other scope types. +struct debug_scope { + std::string module_name; + std::unique_ptr module_attrs; + std::unique_ptr cell_attrs; +}; + +struct debug_scopes { + std::map table; + + void add(const std::string &path, const std::string &module_name, metadata_map &&module_attrs, metadata_map &&cell_attrs) { + assert((path.empty() || path[path.size() - 1] != ' ') && path.find(" ") == std::string::npos); + assert(table.count(path) == 0); + debug_scope &scope = table[path]; + scope.module_name = module_name; + scope.module_attrs = std::unique_ptr(new debug_attrs { module_attrs }); + scope.cell_attrs = std::unique_ptr(new debug_attrs { cell_attrs }); + } + + size_t contains(const std::string &path) const { + return table.count(path); + } + + const debug_scope &operator [](const std::string &path) const { + return table.at(path); } }; @@ -1412,8 +1446,16 @@ struct module { return deltas; } - virtual void debug_info(debug_items &items, std::string path = "") { - (void)items, (void)path; + virtual void debug_info(debug_items *items, debug_scopes *scopes, std::string path, metadata_map &&cell_attrs = {}) { + (void)items, (void)scopes, (void)path, (void)cell_attrs; + } + + // Compatibility method. +#if __has_attribute(deprecated) + __attribute__((deprecated("Use `debug_info(path, &items, /*scopes=*/nullptr);` instead. (`path` could be \"top \".)"))) +#endif + void debug_info(debug_items &items, std::string path) { + debug_info(&items, /*scopes=*/nullptr, path); } }; diff --git a/backends/cxxrtl/runtime/cxxrtl/cxxrtl_replay.h b/backends/cxxrtl/runtime/cxxrtl/cxxrtl_replay.h index 00d938454..454895a1f 100644 --- a/backends/cxxrtl/runtime/cxxrtl/cxxrtl_replay.h +++ b/backends/cxxrtl/runtime/cxxrtl/cxxrtl_replay.h @@ -491,9 +491,9 @@ public: template recorder(Args &&...args) : writer(std::forward(args)...) {} - void start(module &module) { + void start(module &module, std::string top_path = "") { debug_items items; - module.debug_info(items); + module.debug_info(&items, /*scopes=*/nullptr, top_path); start(items); } @@ -621,9 +621,10 @@ public: template player(Args &&...args) : reader(std::forward(args)...) {} - void start(module &module) { + // The `top_path` must match the one given to the recorder. + void start(module &module, std::string top_path = "") { debug_items items; - module.debug_info(items); + module.debug_info(&items, /*scopes=*/nullptr, top_path); start(items); }