From: whitequark Date: Mon, 21 Dec 2020 21:00:46 +0000 (+0000) Subject: cxxrtl: completely rewrite netlist layout code. X-Git-Tag: working-ls180~153^2~2 X-Git-Url: https://git.libre-soc.org/?a=commitdiff_plain;h=b2221c1077d43a193c1a66d8017a7ed9ed1a2373;p=yosys.git cxxrtl: completely rewrite netlist layout code. The exact shape of C++ code emitted by CXXRTL has a critical effect on performance, both compile-time and runtime. CXXRTL's performance greatly improved when it started localizing and inlining wires, not only because this assists the optimizer and register allocator, but also because inlining code into edge-triggered regions cuts the time spent in eval() by at least a factor of two. However, the logic of netlist layout has always been ad-hoc, fragile, and very hard to understand and modify. After commit ece25a45, which introduced outlining, the same logic started being applied to two distinct netlists at once instead of one, which barely worked. This commit does four major changes: * There is now a single unambiguous source of truth (per subgraph) for the layout of any emitted wire. * Netlist layout is now done entirely during analysis using well known graph algorithms; no graph operations happen when emitting. * Netlist layout now happens completely separately for eval() and debug_eval() subgraphs. * Unreachable (within subgraph scope) netlist nodes are now neither emitted nor considered for wire inlining decisions. The netlist layout code should also now closely match the described semantics. As a part of this large cleanup, it includes many miscellaneous improvements: * The "bare minimum" debug level introduced in commit dd6a761d was split into two levels; -g1 now emits debug information *only* for inputs and state wires, and -g2 now emits debug information for all public members. The old behavior matches -g2. This is done to avoid bloat on low optimization levels. * Debug aliases and inlined connections are now handled separately, and complex RHS never interferes with inlined connections. * Aliases to outlined wires now carry a pointer to the outline. * Cell sync outputs can now be emitted in debug_eval(). * Black box debug information now includes comb/sync driver flags. * The comment emitted for inlined cells is now accurate. * Debug information statistics now has less noise. * Netlist layout code is now much better documented. Due to more precise inlining decisions, unmodified (i.e. with no Yosys script being used) netlists now have much more logic inlined into edge-triggered regions. On Minerva SoC SRAM, this improves runtime by 20-25% across compilers and optimization levels. Due to more precise reachability analysis, much less C++ code is now emitted, especially at the maximum debug level. On Minerva SoC SRAM, this improves clang compile time by 30-50% depending on options. gcc is not affected. --- diff --git a/backends/cxxrtl/cxxrtl_backend.cc b/backends/cxxrtl/cxxrtl_backend.cc index 458ec5556..338648c89 100644 --- a/backends/cxxrtl/cxxrtl_backend.cc +++ b/backends/cxxrtl/cxxrtl_backend.cc @@ -214,6 +214,11 @@ bool is_internal_cell(RTLIL::IdString type) return !type.isPublic() && !type.begins_with("$paramod"); } +bool is_effectful_cell(RTLIL::IdString type) +{ + return type == ID($memwr) || type.isPublic(); +} + bool is_cxxrtl_blackbox_cell(const RTLIL::Cell *cell) { RTLIL::Module *cell_module = cell->module->design->module(cell->type); @@ -227,18 +232,15 @@ enum class CxxrtlPortType { SYNC = 2, }; -CxxrtlPortType cxxrtl_port_type(const RTLIL::Cell *cell, RTLIL::IdString port) +CxxrtlPortType cxxrtl_port_type(RTLIL::Module *module, RTLIL::IdString port) { - RTLIL::Module *cell_module = cell->module->design->module(cell->type); - if (cell_module == nullptr || !cell_module->get_bool_attribute(ID(cxxrtl_blackbox))) - return CxxrtlPortType::UNKNOWN; - RTLIL::Wire *cell_output_wire = cell_module->wire(port); - log_assert(cell_output_wire != nullptr); - bool is_comb = cell_output_wire->get_bool_attribute(ID(cxxrtl_comb)); - bool is_sync = cell_output_wire->get_bool_attribute(ID(cxxrtl_sync)); + RTLIL::Wire *output_wire = module->wire(port); + log_assert(output_wire != nullptr); + bool is_comb = output_wire->get_bool_attribute(ID(cxxrtl_comb)); + bool is_sync = output_wire->get_bool_attribute(ID(cxxrtl_sync)); if (is_comb && is_sync) log_cmd_error("Port `%s.%s' is marked as both `cxxrtl_comb` and `cxxrtl_sync`.\n", - log_id(cell_module), log_signal(cell_output_wire)); + log_id(module), log_signal(output_wire)); else if (is_comb) return CxxrtlPortType::COMB; else if (is_sync) @@ -246,6 +248,14 @@ CxxrtlPortType cxxrtl_port_type(const RTLIL::Cell *cell, RTLIL::IdString port) return CxxrtlPortType::UNKNOWN; } +CxxrtlPortType cxxrtl_port_type(const RTLIL::Cell *cell, RTLIL::IdString port) +{ + RTLIL::Module *cell_module = cell->module->design->module(cell->type); + if (cell_module == nullptr || !cell_module->get_bool_attribute(ID(cxxrtl_blackbox))) + return CxxrtlPortType::UNKNOWN; + return cxxrtl_port_type(cell_module, port); +} + bool is_cxxrtl_comb_port(const RTLIL::Cell *cell, RTLIL::IdString port) { return cxxrtl_port_type(cell, port) == CxxrtlPortType::COMB; @@ -273,8 +283,9 @@ struct FlowGraph { std::vector nodes; dict> wire_comb_defs, wire_sync_defs, wire_uses; - dict, hash_ptr_ops> node_comb_defs, node_uses; - dict wire_def_inlinable, wire_use_inlinable; + dict, hash_ptr_ops> node_comb_defs, node_sync_defs, node_uses; + dict wire_def_inlinable; + dict> wire_use_inlinable; dict bit_has_state; ~FlowGraph() @@ -291,6 +302,7 @@ struct FlowGraph { // A sync def means that a wire holds design state because it is driven directly by // a flip-flop output. Such a wire can never be unbuffered. wire_sync_defs[chunk.wire].insert(node); + node_sync_defs[node].insert(chunk.wire); } else { // A comb def means that a wire doesn't hold design state. It might still be connected, // indirectly, to a flip-flop output. @@ -311,19 +323,33 @@ struct FlowGraph { if (chunk.wire) { wire_uses[chunk.wire].insert(node); node_uses[node].insert(chunk.wire); - // Only a single use of an entire wire in the right order can be inlined. - // (But the use can include other chunks.) - if (!wire_use_inlinable.count(chunk.wire)) - wire_use_inlinable[chunk.wire] = true; + // Only a single use of an entire wire in the right order can be inlined. (But the use can include + // other chunks.) This is tracked per-node because a wire used by multiple nodes can still be inlined + // if all but one of those nodes is dead. + if (!wire_use_inlinable[chunk.wire].count(node)) + wire_use_inlinable[chunk.wire][node] = true; else - wire_use_inlinable[chunk.wire] = false; + wire_use_inlinable[chunk.wire][node] = false; } } bool is_inlinable(const RTLIL::Wire *wire) const { - if (wire_def_inlinable.count(wire) && wire_use_inlinable.count(wire)) - return wire_def_inlinable.at(wire) && wire_use_inlinable.at(wire); + // Can the wire be inlined at all? + if (wire_def_inlinable.count(wire)) + return wire_def_inlinable.at(wire); + return false; + } + + bool is_inlinable(const RTLIL::Wire *wire, const pool &nodes) const + { + // Can the wire be inlined, knowing that the given nodes are reachable? + if (nodes.size() != 1) + return false; + Node *node = *nodes.begin(); + log_assert(node_uses.at(node).count(wire)); + if (is_inlinable(wire) && wire_use_inlinable.count(wire) && wire_use_inlinable.at(wire).count(node)) + return wire_use_inlinable.at(wire).at(node); return false; } @@ -523,6 +549,52 @@ std::string get_hdl_name(T *object) return object->name.str().substr(1); } +struct WireType { + enum Type { + // Non-referenced wire; is not a part of the design. + UNUSED, + // Double-buffered wire; is a class member, and holds design state. + BUFFERED, + // Single-buffered wire; is a class member, but holds no state. + MEMBER, + // Single-buffered wire; is a class member, and is computed on demand. + OUTLINE, + // Local wire; is a local variable in eval method. + LOCAL, + // Inline wire; is an unnamed temporary in eval method. + INLINE, + // Alias wire; is replaced with aliasee, except in debug info. + ALIAS, + // Const wire; is replaced with constant, except in debug info. + CONST, + }; + + Type type = UNUSED; + const RTLIL::Cell *cell_subst = nullptr; // for INLINE + RTLIL::SigSpec sig_subst = {}; // for INLINE, ALIAS, and CONST + + WireType() = default; + + WireType(Type type) : type(type) { + log_assert(type == UNUSED || type == BUFFERED || type == MEMBER || type == OUTLINE || type == LOCAL); + } + + WireType(Type type, const RTLIL::Cell *cell) : type(type), cell_subst(cell) { + log_assert(type == INLINE && is_inlinable_cell(cell->type)); + } + + WireType(Type type, RTLIL::SigSpec sig) : type(type), sig_subst(sig) { + log_assert(type == INLINE || (type == ALIAS && sig.is_wire()) || (type == CONST && sig.is_fully_const())); + } + + bool is_buffered() const { return type == BUFFERED; } + bool is_member() const { return type == BUFFERED || type == MEMBER || type == OUTLINE; } + bool is_outline() const { return type == OUTLINE; } + bool is_named() const { return is_member() || type == LOCAL; } + bool is_local() const { return type == LOCAL || type == INLINE; } + bool is_exact() const { return type == ALIAS || type == CONST; } +}; + struct CxxrtlWorker { bool split_intf = false; std::string intf_filename; @@ -542,6 +614,7 @@ struct CxxrtlWorker { bool inline_public = false; bool debug_info = false; + bool debug_member = false; bool debug_alias = false; bool debug_eval = false; @@ -554,13 +627,8 @@ struct CxxrtlWorker { dict edge_types; pool writable_memories; dict> transparent_for; - dict> schedule; - pool unbuffered_wires; - pool localized_wires; - dict inlined_wires; - dict debug_const_wires; - dict debug_alias_wires; - pool debug_outlined_wires; + dict> schedule, debug_schedule; + dict wire_types, debug_wire_types; dict bit_has_state; dict> blackbox_specializations; dict eval_converges; @@ -798,24 +866,31 @@ struct CxxrtlWorker { dump_const(chunk.data, chunk.width, chunk.offset); return false; } else { - if (inlined_wires.count(chunk.wire) && (!for_debug || !debug_outlined_wires[chunk.wire])) { - log_assert(!is_lhs); - const FlowGraph::Node &node = inlined_wires[chunk.wire]; - switch (node.type) { - case FlowGraph::Node::Type::CONNECT: - dump_connect_expr(node.connect, for_debug); - break; - case FlowGraph::Node::Type::CELL_EVAL: - log_assert(is_inlinable_cell(node.cell->type)); - dump_cell_expr(node.cell, for_debug); + const auto &wire_type = (for_debug ? debug_wire_types : wire_types)[chunk.wire]; + switch (wire_type.type) { + case WireType::BUFFERED: + f << mangle(chunk.wire) << (is_lhs ? ".next" : ".curr"); + break; + case WireType::MEMBER: + case WireType::LOCAL: + case WireType::OUTLINE: + f << mangle(chunk.wire); + break; + case WireType::INLINE: + log_assert(!is_lhs); + if (wire_type.cell_subst != nullptr) { + dump_cell_expr(wire_type.cell_subst, for_debug); break; - default: - log_assert(false); - } - } else if (unbuffered_wires[chunk.wire]) { - f << mangle(chunk.wire); - } else { - f << mangle(chunk.wire) << (is_lhs ? ".next" : ".curr"); + } + YS_FALLTHROUGH + case WireType::ALIAS: + case WireType::CONST: + log_assert(!is_lhs); + return dump_sigspec(wire_type.sig_subst.extract(chunk.offset, chunk.width), is_lhs, for_debug); + case WireType::UNUSED: + log_assert(is_lhs); + f << "value<" << chunk.width << ">()"; + return false; } if (chunk.width == chunk.wire->width && chunk.offset == 0) return false; @@ -876,22 +951,39 @@ struct CxxrtlWorker { f << ".val()"; } - void collect_sigspec_rhs(const RTLIL::SigSpec &sig, std::vector &cells) + void dump_inlined_cells(const std::vector &cells) + { + if (cells.empty()) { + f << indent << "// connection\n"; + } else if (cells.size() == 1) { + dump_attrs(cells.front()); + f << indent << "// cell " << cells.front()->name.str() << "\n"; + } else { + f << indent << "// cells"; + for (auto cell : cells) + f << " " << cell->name.str(); + f << "\n"; + } + } + + void collect_sigspec_rhs(const RTLIL::SigSpec &sig, bool for_debug, std::vector &cells) { for (auto chunk : sig.chunks()) { - if (!chunk.wire || !inlined_wires.count(chunk.wire)) + if (!chunk.wire) continue; - - const FlowGraph::Node &node = inlined_wires[chunk.wire]; - switch (node.type) { - case FlowGraph::Node::Type::CONNECT: - collect_connect(node.connect, cells); - break; - case FlowGraph::Node::Type::CELL_EVAL: - collect_cell_eval(node.cell, cells); + const auto &wire_type = wire_types[chunk.wire]; + switch (wire_type.type) { + case WireType::INLINE: + if (wire_type.cell_subst != nullptr) { + collect_cell_eval(wire_type.cell_subst, for_debug, cells); + break; + } + YS_FALLTHROUGH + case WireType::ALIAS: + collect_sigspec_rhs(wire_type.sig_subst, for_debug, cells); break; default: - log_assert(false); + break; } } } @@ -901,44 +993,12 @@ struct CxxrtlWorker { dump_sigspec_rhs(conn.second, for_debug); } - bool is_connect_inlined(const RTLIL::SigSig &conn) - { - return conn.first.is_wire() && inlined_wires.count(conn.first.as_wire()); - } - - bool is_connect_outlined(const RTLIL::SigSig &conn) - { - for (auto chunk : conn.first.chunks()) - if (debug_outlined_wires.count(chunk.wire)) - return true; - return false; - } - - void collect_connect(const RTLIL::SigSig &conn, std::vector &cells) - { - if (!is_connect_inlined(conn)) - return; - - collect_sigspec_rhs(conn.second, cells); - } - void dump_connect(const RTLIL::SigSig &conn, bool for_debug = false) { - if (!for_debug && is_connect_inlined(conn)) - return; - if (for_debug && !is_connect_outlined(conn)) - return; + std::vector inlined_cells; + collect_sigspec_rhs(conn.second, for_debug, inlined_cells); + dump_inlined_cells(inlined_cells); - std::vector inlined_cells; - collect_sigspec_rhs(conn.second, inlined_cells); - if (for_debug || inlined_cells.empty()) { - f << indent << "// connection\n"; - } else { - f << indent << "// cells"; - for (auto inlined_cell : inlined_cells) - f << " " << inlined_cell.str(); - f << "\n"; - } f << indent; dump_sigspec_lhs(conn.first, for_debug); f << " = "; @@ -946,7 +1006,12 @@ struct CxxrtlWorker { f << ";\n"; } - void dump_cell_sync(const RTLIL::Cell *cell) + void collect_connect(const RTLIL::SigSig &conn, bool for_debug, std::vector &cells) + { + collect_sigspec_rhs(conn.second, for_debug, cells); + } + + void dump_cell_sync(const RTLIL::Cell *cell, bool for_debug = false) { const char *access = is_cxxrtl_blackbox_cell(cell) ? "->" : "."; f << indent << "// cell " << cell->name.str() << " syncs\n"; @@ -954,7 +1019,7 @@ struct CxxrtlWorker { if (cell->output(conn.first)) if (is_cxxrtl_sync_port(cell, conn.first)) { f << indent; - dump_sigspec_lhs(conn.second); + dump_sigspec_lhs(conn.second, for_debug); f << " = " << mangle(cell) << access << mangle_wire_name(conn.first) << ".curr;\n"; } } @@ -1023,58 +1088,11 @@ struct CxxrtlWorker { } } - bool is_cell_inlined(const RTLIL::Cell *cell) - { - return is_inlinable_cell(cell->type) && cell->hasPort(ID::Y) && cell->getPort(ID::Y).is_wire() && - inlined_wires.count(cell->getPort(ID::Y).as_wire()); - } - - bool is_cell_outlined(const RTLIL::Cell *cell) - { - if (is_internal_cell(cell->type)) - for (auto conn : cell->connections()) - if (cell->output(conn.first)) - for (auto chunk : conn.second.chunks()) - if (debug_outlined_wires.count(chunk.wire)) - return true; - return false; - } - - void collect_cell_eval(const RTLIL::Cell *cell, std::vector &cells) - { - if (!is_cell_inlined(cell)) - return; - - cells.push_back(cell->name); - for (auto port : cell->connections()) - if (port.first != ID::Y) - collect_sigspec_rhs(port.second, cells); - } - void dump_cell_eval(const RTLIL::Cell *cell, bool for_debug = false) { - if (!for_debug && is_cell_inlined(cell)) - return; - if (for_debug && !is_cell_outlined(cell)) - return; - if (cell->type == ID($meminit)) - return; // Handled elsewhere. - - std::vector inlined_cells; - if (is_inlinable_cell(cell->type)) { - for (auto port : cell->connections()) - if (port.first != ID::Y) - collect_sigspec_rhs(port.second, inlined_cells); - } - if (inlined_cells.empty()) { - dump_attrs(cell); - f << indent << "// cell " << cell->name.str() << "\n"; - } else { - f << indent << "// cells"; - for (auto inlined_cell : inlined_cells) - f << " " << inlined_cell.str(); - f << "\n"; - } + std::vector inlined_cells; + collect_cell_eval(cell, for_debug, inlined_cells); + dump_inlined_cells(inlined_cells); // Elidable cells if (is_inlinable_cell(cell->type)) { @@ -1085,6 +1103,7 @@ struct CxxrtlWorker { f << ";\n"; // Flip-flops } else if (is_ff_cell(cell->type)) { + log_assert(!for_debug); if (cell->hasPort(ID::CLK) && cell->getPort(ID::CLK).is_wire()) { // Edge-sensitive logic RTLIL::SigBit clk_bit = cell->getPort(ID::CLK)[0]; @@ -1185,6 +1204,7 @@ struct CxxrtlWorker { // Memory ports } else if (cell->type.in(ID($memrd), ID($memwr))) { if (cell->getParam(ID::CLK_ENABLE).as_bool()) { + log_assert(!for_debug); RTLIL::SigBit clk_bit = cell->getPort(ID::CLK)[0]; clk_bit = sigmaps[clk_bit.wire->module](clk_bit); if (clk_bit.wire) { @@ -1292,6 +1312,7 @@ struct CxxrtlWorker { log_cmd_error("Unsupported internal cell `%s'.\n", cell->type.c_str()); // User cells } else { + log_assert(!for_debug); log_assert(cell->known()); bool buffered_inputs = false; const char *access = is_cxxrtl_blackbox_cell(cell) ? "->" : "."; @@ -1301,7 +1322,7 @@ struct CxxrtlWorker { log_assert(cell_module != nullptr && cell_module->wire(conn.first) && conn.second.is_wire()); RTLIL::Wire *cell_module_wire = cell_module->wire(conn.first); f << indent << mangle(cell) << access << mangle_wire_name(conn.first); - if (!is_cxxrtl_blackbox_cell(cell) && !unbuffered_wires[cell_module_wire]) { + if (!is_cxxrtl_blackbox_cell(cell) && wire_types[cell_module_wire].is_buffered()) { buffered_inputs = true; f << ".next"; } @@ -1369,6 +1390,14 @@ struct CxxrtlWorker { } } + void collect_cell_eval(const RTLIL::Cell *cell, bool for_debug, std::vector &cells) + { + cells.push_back(cell); + for (auto port : cell->connections()) + if (cell->input(port.first)) + collect_sigspec_rhs(port.second, for_debug, cells); + } + void dump_assign(const RTLIL::SigSig &sigsig) { f << indent; @@ -1513,67 +1542,64 @@ struct CxxrtlWorker { void dump_wire(const RTLIL::Wire *wire, bool is_local) { - if (is_local && localized_wires[wire] && !inlined_wires.count(wire)) { - dump_attrs(wire); - f << indent << "value<" << wire->width << "> " << mangle(wire) << ";\n"; - } - if (!is_local && !localized_wires[wire]) { - std::string width; - if (wire->module->has_attribute(ID(cxxrtl_blackbox)) && wire->has_attribute(ID(cxxrtl_width))) { - width = wire->get_string_attribute(ID(cxxrtl_width)); - } else { - width = std::to_string(wire->width); - } + const auto &wire_type = wire_types[wire]; + if (!wire_type.is_named() || wire_type.is_local() != is_local) + return; - dump_attrs(wire); - f << indent; - if (wire->port_input && wire->port_output) - f << "/*inout*/ "; - else if (wire->port_input) - f << "/*input*/ "; - else if (wire->port_output) - f << "/*output*/ "; - f << (unbuffered_wires[wire] ? "value" : "wire") << "<" << width << "> " << mangle(wire); - if (wire->has_attribute(ID::init)) { - f << " "; - dump_const_init(wire->attributes.at(ID::init)); - } - f << ";\n"; - if (edge_wires[wire]) { - if (unbuffered_wires[wire]) { - f << indent << "value<" << width << "> prev_" << mangle(wire); - if (wire->has_attribute(ID::init)) { - f << " "; - dump_const_init(wire->attributes.at(ID::init)); - } - f << ";\n"; + dump_attrs(wire); + f << indent; + if (wire->port_input && wire->port_output) + f << "/*inout*/ "; + else if (wire->port_input) + f << "/*input*/ "; + else if (wire->port_output) + f << "/*output*/ "; + f << (wire_type.is_buffered() ? "wire" : "value"); + if (wire->module->has_attribute(ID(cxxrtl_blackbox)) && wire->has_attribute(ID(cxxrtl_width))) { + f << "<" << wire->get_string_attribute(ID(cxxrtl_width)) << ">"; + } else { + f << "<" << wire->width << ">"; + } + f << " " << mangle(wire); + if (wire->has_attribute(ID::init)) { + f << " "; + dump_const_init(wire->attributes.at(ID::init)); + } + f << ";\n"; + if (edge_wires[wire]) { + if (!wire_type.is_buffered()) { + f << indent << "value<" << wire->width << "> prev_" << mangle(wire); + if (wire->has_attribute(ID::init)) { + f << " "; + dump_const_init(wire->attributes.at(ID::init)); } - for (auto edge_type : edge_types) { - if (edge_type.first.wire == wire) { - std::string prev, next; - if (unbuffered_wires[wire]) { - prev = "prev_" + mangle(edge_type.first.wire); - next = mangle(edge_type.first.wire); - } else { - prev = mangle(edge_type.first.wire) + ".curr"; - next = mangle(edge_type.first.wire) + ".next"; - } - prev += ".slice<" + std::to_string(edge_type.first.offset) + ">().val()"; - next += ".slice<" + std::to_string(edge_type.first.offset) + ">().val()"; - if (edge_type.second != RTLIL::STn) { - f << indent << "bool posedge_" << mangle(edge_type.first) << "() const {\n"; - inc_indent(); - f << indent << "return !" << prev << " && " << next << ";\n"; - dec_indent(); - f << indent << "}\n"; - } - if (edge_type.second != RTLIL::STp) { - f << indent << "bool negedge_" << mangle(edge_type.first) << "() const {\n"; - inc_indent(); - f << indent << "return " << prev << " && !" << next << ";\n"; - dec_indent(); - f << indent << "}\n"; - } + f << ";\n"; + } + for (auto edge_type : edge_types) { + if (edge_type.first.wire == wire) { + std::string prev, next; + if (!wire_type.is_buffered()) { + prev = "prev_" + mangle(edge_type.first.wire); + next = mangle(edge_type.first.wire); + } else { + prev = mangle(edge_type.first.wire) + ".curr"; + next = mangle(edge_type.first.wire) + ".next"; + } + prev += ".slice<" + std::to_string(edge_type.first.offset) + ">().val()"; + next += ".slice<" + std::to_string(edge_type.first.offset) + ">().val()"; + if (edge_type.second != RTLIL::STn) { + f << indent << "bool posedge_" << mangle(edge_type.first) << "() const {\n"; + inc_indent(); + f << indent << "return !" << prev << " && " << next << ";\n"; + dec_indent(); + f << indent << "}\n"; + } + if (edge_type.second != RTLIL::STp) { + f << indent << "bool negedge_" << mangle(edge_type.first) << "() const {\n"; + inc_indent(); + f << indent << "return " << prev << " && !" << next << ";\n"; + dec_indent(); + f << indent << "}\n"; } } } @@ -1582,19 +1608,19 @@ struct CxxrtlWorker { void dump_debug_wire(const RTLIL::Wire *wire, bool is_local) { - if (!debug_outlined_wires[wire]) + const auto &wire_type = wire_types[wire]; + if (wire_type.is_member()) return; - bool is_outlined_member = wire->name.isPublic() && - !(debug_const_wires.count(wire) || debug_alias_wires.count(wire)); - if (is_local && !is_outlined_member) { - dump_attrs(wire); - f << indent << "value<" << wire->width << "> " << mangle(wire) << ";\n"; - } - if (!is_local && is_outlined_member) { - dump_attrs(wire); - f << indent << "/*outline*/ value<" << wire->width << "> " << mangle(wire) << ";\n"; - } + const auto &debug_wire_type = debug_wire_types[wire]; + if (!debug_wire_type.is_named() || debug_wire_type.is_local() != is_local) + return; + + dump_attrs(wire); + f << indent; + if (debug_wire_type.is_outline()) + f << "/*outline*/ "; + f << "value<" << wire->width << "> " << mangle(wire) << ";\n"; } void dump_memory(RTLIL::Module *module, const RTLIL::Memory *memory) @@ -1691,17 +1717,19 @@ struct CxxrtlWorker { inc_indent(); for (auto wire : module->wires()) dump_debug_wire(wire, /*is_local=*/true); - for (auto node : schedule[module]) { + for (auto node : debug_schedule[module]) { switch (node.type) { case FlowGraph::Node::Type::CONNECT: dump_connect(node.connect, /*for_debug=*/true); break; + case FlowGraph::Node::Type::CELL_SYNC: + dump_cell_sync(node.cell, /*for_debug=*/true); + break; case FlowGraph::Node::Type::CELL_EVAL: dump_cell_eval(node.cell, /*for_debug=*/true); break; - case FlowGraph::Node::Type::CELL_SYNC: - case FlowGraph::Node::Type::PROCESS: - break; + default: + log_abort(); } } dec_indent(); @@ -1712,14 +1740,10 @@ struct CxxrtlWorker { inc_indent(); f << indent << "bool changed = false;\n"; for (auto wire : module->wires()) { - if (inlined_wires.count(wire)) - continue; - if (unbuffered_wires[wire]) { - if (edge_wires[wire]) - f << indent << "prev_" << mangle(wire) << " = " << mangle(wire) << ";\n"; - continue; - } - if (!module->get_bool_attribute(ID(cxxrtl_blackbox)) || wire->port_id != 0) + const auto &wire_type = wire_types[wire]; + if (wire_type.type == WireType::MEMBER && edge_wires[wire]) + f << indent << "prev_" << mangle(wire) << " = " << mangle(wire) << ";\n"; + if (wire_type.is_buffered()) f << indent << "if (" << mangle(wire) << ".commit()) changed = true;\n"; } if (!module->get_bool_attribute(ID(cxxrtl_blackbox))) { @@ -1742,99 +1766,129 @@ struct CxxrtlWorker { void dump_debug_info_method(RTLIL::Module *module) { size_t count_public_wires = 0; - size_t count_const_wires = 0; - size_t count_alias_wires = 0; - size_t count_inline_wires = 0; size_t count_member_wires = 0; - size_t count_skipped_wires = 0; + size_t count_undriven = 0; size_t count_driven_sync = 0; size_t count_driven_comb = 0; - size_t count_undriven = 0; size_t count_mixed_driver = 0; + size_t count_alias_wires = 0; + size_t count_const_wires = 0; + size_t count_inline_wires = 0; + 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; - if (module->get_bool_attribute(ID(cxxrtl_blackbox)) && (wire->port_id == 0)) - continue; count_public_wires++; - if (debug_const_wires.count(wire)) { - // Wire tied to a constant - f << indent << "static const value<" << wire->width << "> const_" << mangle(wire) << " = "; - dump_const(debug_const_wires[wire]); - f << ";\n"; - f << indent << "items.add(path + " << escape_cxx_string(get_hdl_name(wire)); - f << ", debug_item(const_" << mangle(wire) << ", "; - f << wire->start_offset << "));\n"; - count_const_wires++; - } else if (debug_alias_wires.count(wire)) { - // Alias of a member wire - f << indent << "items.add(path + " << escape_cxx_string(get_hdl_name(wire)); - f << ", debug_item(debug_alias(), " << mangle(debug_alias_wires[wire]) << ", "; - f << wire->start_offset << "));\n"; - count_alias_wires++; - } else if (debug_outlined_wires.count(wire)) { - // Inlined but rematerializable wire - f << indent << "items.add(path + " << escape_cxx_string(get_hdl_name(wire)); - f << ", debug_item(debug_eval_outline, " << mangle(wire) << ", "; - f << wire->start_offset << "));\n"; - count_inline_wires++; - } else if (!localized_wires.count(wire)) { - // Member wire - std::vector flags; - - if (wire->port_input && wire->port_output) - flags.push_back("INOUT"); - else if (wire->port_input) - flags.push_back("INPUT"); - else if (wire->port_output) - flags.push_back("OUTPUT"); - - bool has_driven_sync = false; - bool has_driven_comb = false; - bool has_undriven = false; - SigSpec sig(wire); - for (auto bit : sig.bits()) - if (!bit_has_state.count(bit)) - has_undriven = true; - else if (bit_has_state[bit]) - has_driven_sync = true; - else - has_driven_comb = true; - 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_undriven) - flags.push_back("UNDRIVEN"); - if (!has_driven_sync && !has_driven_comb && has_undriven) - count_undriven++; - 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) << ", "; - f << wire->start_offset; - bool first = true; - for (auto flag : flags) { - if (first) { - first = false; - f << ", "; + 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 { - f << "|"; + has_undriven = true; } - f << "debug_item::" << flag; + 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 << "));\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 << "));\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 << "));\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 << "));\n"; + count_inline_wires++; + break; + } + default: { + // Localized or inlined wire with no debug information + count_skipped_wires++; + break; } - f << "));\n"; - count_member_wires++; - } else { - // Localized or inlined wire with no debug information - count_skipped_wires++; } } if (!module->get_bool_attribute(ID(cxxrtl_blackbox))) { @@ -1858,15 +1912,17 @@ struct CxxrtlWorker { log_debug("Debug information statistics for module `%s':\n", log_id(module)); 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); log_debug(" Driven sync: %zu\n", count_driven_sync); log_debug(" Driven comb: %zu\n", count_driven_comb); log_debug(" Mixed driver: %zu\n", count_mixed_driver); - log_debug(" Undriven: %zu\n", count_undriven); - log_debug(" Inline wires: %zu\n", count_inline_wires); - log_debug(" Alias wires: %zu\n", count_alias_wires); - log_debug(" Const wires: %zu\n", count_const_wires); - log_debug(" Other wires: %zu%s\n", count_skipped_wires, - count_skipped_wires > 0 ? " (debug information unavailable)" : ""); + if (!module->get_bool_attribute(ID(cxxrtl_blackbox))) { + log_debug(" Inline wires: %zu\n", count_inline_wires); + log_debug(" Alias wires: %zu\n", count_alias_wires); + log_debug(" Const wires: %zu\n", count_const_wires); + log_debug(" Other wires: %zu%s\n", count_skipped_wires, + count_skipped_wires > 0 ? " (debug unavailable)" : ""); + } } void dump_metadata_map(const dict &metadata_map) @@ -2031,7 +2087,7 @@ struct CxxrtlWorker { f << "\n"; f << indent << "void debug_eval();\n"; for (auto wire : module->wires()) - if (debug_outlined_wires.count(wire)) { + if (debug_wire_types[wire].is_outline()) { f << indent << "debug_outline debug_eval_outline { std::bind(&" << mangle(module) << "::debug_eval, this) };\n"; break; @@ -2221,8 +2277,11 @@ struct CxxrtlWorker { if (module->get_bool_attribute(ID(cxxrtl_blackbox))) { for (auto port : module->ports) { RTLIL::Wire *wire = module->wire(port); - if (wire->port_input && !wire->port_output) - unbuffered_wires.insert(wire); + if (wire->port_input && !wire->port_output) { + wire_types[wire] = debug_wire_types[wire] = {WireType::MEMBER}; + } else if (wire->port_input || wire->port_output) { + wire_types[wire] = debug_wire_types[wire] = {WireType::BUFFERED}; + } if (wire->has_attribute(ID(cxxrtl_edge))) { RTLIL::Const edge_attr = wire->attributes[ID(cxxrtl_edge)]; if (!(edge_attr.flags & RTLIL::CONST_FLAG_STRING) || (int)edge_attr.decode_string().size() != GetSize(wire)) @@ -2252,6 +2311,8 @@ struct CxxrtlWorker { continue; } + // Construct a flow graph where each node is a basic computational operation generally corresponding + // to a fragment of the RTLIL netlist. FlowGraph flow; for (auto conn : module->connections()) @@ -2351,6 +2412,9 @@ struct CxxrtlWorker { } } + // Construct a linear order of the flow graph that minimizes the amount of feedback arcs. A flow graph + // without feedback arcs can generally be evaluated in a single pass, i.e. it always requires only + // a single delta cycle. Scheduler scheduler; dict::Vertex*, hash_ptr_ops> node_vertex_map; for (auto node : flow.nodes) @@ -2365,20 +2429,21 @@ struct CxxrtlWorker { } } - auto eval_order = scheduler.schedule(); - pool evaluated; + // Find out whether the order includes any feedback arcs. + std::vector node_order; + pool evaluated_nodes; pool feedback_wires; - for (auto vertex : eval_order) { + for (auto vertex : scheduler.schedule()) { auto node = vertex->data; - schedule[module].push_back(*node); + node_order.push_back(node); // Any wire that is an output of node vo and input of node vi where vo is scheduled later than vi // is a feedback wire. Feedback wires indicate apparent logic loops in the design, which may be // caused by a true logic loop, but usually are a benign result of dependency tracking that works - // on wire, not bit, level. Nevertheless, feedback wires cannot be localized. - evaluated.insert(node); + // on wire, not bit, level. Nevertheless, feedback wires cannot be unbuffered. + evaluated_nodes.insert(node); for (auto wire : flow.node_comb_defs[node]) for (auto succ_node : flow.wire_uses[wire]) - if (evaluated[succ_node]) + if (evaluated_nodes[succ_node]) feedback_wires.insert(wire); } if (!feedback_wires.empty()) { @@ -2388,27 +2453,85 @@ struct CxxrtlWorker { log(" %s\n", log_id(wire)); } + // Conservatively assign wire types. Assignment of types BUFFERED and MEMBER is final, but assignment + // of type LOCAL may be further refined to UNUSED or INLINE. for (auto wire : module->wires()) { + auto &wire_type = wire_types[wire]; + wire_type = {WireType::BUFFERED}; + if (feedback_wires[wire]) continue; if (wire->port_output && !module->get_bool_attribute(ID::top)) continue; - if (wire->name.begins_with("$") && !unbuffer_internal) continue; - if (wire->name.begins_with("\\") && !unbuffer_public) continue; + if (!wire->name.isPublic() && !unbuffer_internal) continue; + if (wire->name.isPublic() && !unbuffer_public) continue; if (flow.wire_sync_defs.count(wire) > 0) continue; - unbuffered_wires.insert(wire); + wire_type = {WireType::MEMBER}; + if (edge_wires[wire]) continue; if (wire->get_bool_attribute(ID::keep)) continue; if (wire->port_input || wire->port_output) continue; - if (wire->name.begins_with("$") && !localize_internal) continue; - if (wire->name.begins_with("\\") && !localize_public) continue; - localized_wires.insert(wire); - if (!flow.is_inlinable(wire)) continue; - if (wire->name.begins_with("$") && !inline_internal) continue; - if (wire->name.begins_with("\\") && !inline_public) continue; - if (flow.wire_comb_defs[wire].size() > 1) - log_cmd_error("Wire %s.%s has multiple drivers.\n", log_id(module), log_id(wire)); - log_assert(flow.wire_comb_defs[wire].size() == 1); - inlined_wires[wire] = **flow.wire_comb_defs[wire].begin(); + if (!wire->name.isPublic() && !localize_internal) continue; + if (wire->name.isPublic() && !localize_public) continue; + wire_type = {WireType::LOCAL}; + } + + // Discover nodes reachable from primary outputs (i.e. members) and collect reachable wire users. + pool worklist; + for (auto node : flow.nodes) { + if (node->type == FlowGraph::Node::Type::CELL_EVAL && is_effectful_cell(node->cell->type)) + worklist.insert(node); // node has effects + else if (flow.node_sync_defs.count(node)) + worklist.insert(node); // node is a flip-flop + else if (flow.node_comb_defs.count(node)) { + for (auto wire : flow.node_comb_defs[node]) + if (wire_types[wire].is_member()) + worklist.insert(node); // node drives public wires + } } + dict> live_wires; + pool live_nodes; + while (!worklist.empty()) { + auto node = worklist.pop(); + live_nodes.insert(node); + for (auto wire : flow.node_uses[node]) { + live_wires[wire].insert(node); + for (auto pred_node : flow.wire_comb_defs[wire]) + if (!live_nodes[pred_node]) + worklist.insert(pred_node); + } + } + + // Refine wire types taking into account the amount of uses from reachable nodes only. + for (auto wire : module->wires()) { + auto &wire_type = wire_types[wire]; + if (!wire_type.is_local()) continue; + if (!wire->name.isPublic() && !inline_internal) continue; + if (wire->name.isPublic() && !inline_public) continue; + + if (live_wires[wire].empty()) { + wire_type = {WireType::UNUSED}; // wire never used + } else if (flow.is_inlinable(wire, live_wires[wire])) { + if (flow.wire_comb_defs[wire].size() > 1) + log_cmd_error("Wire %s.%s has multiple drivers!\n", log_id(module), log_id(wire)); + log_assert(flow.wire_comb_defs[wire].size() == 1); + FlowGraph::Node *node = *flow.wire_comb_defs[wire].begin(); + switch (node->type) { + case FlowGraph::Node::Type::CELL_EVAL: + if (!is_inlinable_cell(node->cell->type)) continue; + wire_type = {WireType::INLINE, node->cell}; // wire replaced with cell + break; + case FlowGraph::Node::Type::CONNECT: + wire_type = {WireType::INLINE, node->connect.second}; // wire replaced with sig + break; + default: continue; + } + live_nodes.erase(node); + } + } + + // Emit reachable nodes in eval(). + for (auto node : node_order) + if (live_nodes[node]) + schedule[module].push_back(*node); // For maximum performance, the state of the simulation (which is the same as the set of its double buffered // wires, since using a singly buffered wire for any kind of state introduces a race condition) should contain @@ -2417,10 +2540,9 @@ struct CxxrtlWorker { // as a wire with multiple drivers where one of them is combinatorial and the other is synchronous. Such designs // also require more than one delta cycle to converge. pool buffered_comb_wires; - for (auto wire : module->wires()) { - if (flow.wire_comb_defs[wire].size() > 0 && !unbuffered_wires[wire] && !feedback_wires[wire]) + for (auto wire : module->wires()) + if (wire_types[wire].is_buffered() && !feedback_wires[wire] && flow.wire_comb_defs[wire].size() > 0) buffered_comb_wires.insert(wire); - } if (!buffered_comb_wires.empty()) { has_buffered_comb_wires = true; log("Module `%s' contains buffered combinatorial wires:\n", log_id(module)); @@ -2428,81 +2550,115 @@ struct CxxrtlWorker { log(" %s\n", log_id(wire)); } + // Record whether eval() requires only one delta cycle in this module. eval_converges[module] = feedback_wires.empty() && buffered_comb_wires.empty(); - for (auto item : flow.bit_has_state) - bit_has_state.insert(item); + if (debug_info) { + // Annotate wire bits with the type of their driver; this is exposed in the debug metadata. + for (auto item : flow.bit_has_state) + bit_has_state.insert(item); - if (debug_info && debug_eval) { - // Find wires that can be be outlined, i.e. whose values can be always recovered from - // the values of other wires. (This is the inverse of inlining--any wire that can be - // inlined can also be outlined.) Although this may seem strictly less efficient, since - // such values are computed at least twice, second-order effects make outlining useful. - pool worklist, visited; + // Assign debug information wire types to public wires according to the chosen debug level. + // Unlike with optimized wire types, all assignments here are final. for (auto wire : module->wires()) { - if (!wire->name.isPublic()) - continue; - worklist.insert(wire); + const auto &wire_type = wire_types[wire]; + auto &debug_wire_type = debug_wire_types[wire]; + if (!wire->name.isPublic()) continue; + + if (!debug_info) continue; + if (wire->port_input || wire_type.is_buffered()) + debug_wire_type = wire_type; // wire contains state + + if (!debug_member) continue; + if (wire_type.is_member()) + debug_wire_type = wire_type; // wire is a member + + if (!debug_alias) continue; + const RTLIL::Wire *it = wire; + while (flow.is_inlinable(it)) { + log_assert(flow.wire_comb_defs[it].size() == 1); + FlowGraph::Node *node = *flow.wire_comb_defs[it].begin(); + if (node->type != FlowGraph::Node::Type::CONNECT) break; // not an alias + RTLIL::SigSpec rhs = node->connect.second; + if (rhs.is_fully_const()) { + debug_wire_type = {WireType::CONST, rhs}; // wire replaced with const + } else if (rhs.is_wire()) { + if (wire_types[rhs.as_wire()].is_member()) + debug_wire_type = {WireType::ALIAS, rhs}; // wire replaced with wire + else if (debug_eval && rhs.as_wire()->name.isPublic()) + debug_wire_type = {WireType::ALIAS, rhs}; // wire replaced with outline + it = rhs.as_wire(); // and keep looking + continue; + } + break; + } + + if (!debug_eval) continue; + if (!debug_wire_type.is_exact() && !wire_type.is_member()) + debug_wire_type = {WireType::OUTLINE}; // wire is local or inlined + } + + // Discover nodes reachable from primary outputs (i.e. outlines) up until primary inputs (i.e. members) + // and collect reachable wire users. + pool worklist; + for (auto node : flow.nodes) { + if (flow.node_comb_defs.count(node)) + for (auto wire : flow.node_comb_defs[node]) + if (debug_wire_types[wire].is_outline()) + worklist.insert(node); // node drives outline } + dict> live_wires; + pool live_nodes; while (!worklist.empty()) { - const RTLIL::Wire *wire = worklist.pop(); - visited.insert(wire); - if (!localized_wires.count(wire) && !inlined_wires.count(wire)) - continue; // member wire, doesn't need outlining - if (wire->name.isPublic() || !inlined_wires.count(wire)) - debug_outlined_wires.insert(wire); // allow outlining of internal wires only - for (auto node : flow.wire_comb_defs[wire]) - for (auto node_use : flow.node_uses[node]) - if (!visited.count(node_use)) - worklist.insert(node_use); + auto node = worklist.pop(); + live_nodes.insert(node); + for (auto wire : flow.node_uses[node]) { + if (debug_wire_types[wire].is_member()) + continue; // node uses member + if (debug_wire_types[wire].is_exact()) + continue; // node uses alias or const + live_wires[wire].insert(node); + for (auto pred_node : flow.wire_comb_defs[wire]) + if (!live_nodes[pred_node]) + worklist.insert(pred_node); + } } - } - if (debug_info && debug_alias) { - // Find wires that alias other wires or are tied to a constant. Both of these cases are - // directly expressible in the debug information, improving coverage at zero cost. + + // Assign debug information wire types to internal wires used by reachable nodes. This is similar + // to refining optimized wire types with the exception that the assignments here are first and final. for (auto wire : module->wires()) { - if (!wire->name.isPublic()) - continue; - const RTLIL::Wire *cursor = wire; - RTLIL::SigSpec alias_of; - while (1) { - if (!(flow.wire_def_inlinable.count(cursor) && flow.wire_def_inlinable[cursor])) - break; // not an alias: complex def - log_assert(flow.wire_comb_defs[cursor].size() == 1); - FlowGraph::Node *node = *flow.wire_comb_defs[cursor].begin(); - if (node->type != FlowGraph::Node::Type::CONNECT) - break; // not an alias: def by cell - RTLIL::SigSpec rhs_sig = node->connect.second; - if (rhs_sig.is_fully_const()) { - alias_of = rhs_sig; // alias of const - break; - } else if (rhs_sig.is_wire()) { - RTLIL::Wire *rhs_wire = rhs_sig.as_wire(); // possible alias of wire - if (rhs_wire->port_input && !rhs_wire->port_output) { - alias_of = rhs_wire; // alias of input + const auto &wire_type = wire_types[wire]; + auto &debug_wire_type = debug_wire_types[wire]; + if (wire->name.isPublic()) continue; + + if (live_wires[wire].empty()) { + continue; // wire never used + } else if (flow.is_inlinable(wire, live_wires[wire])) { + log_assert(flow.wire_comb_defs[wire].size() == 1); + FlowGraph::Node *node = *flow.wire_comb_defs[wire].begin(); + switch (node->type) { + case FlowGraph::Node::Type::CELL_EVAL: + if (!is_inlinable_cell(node->cell->type)) continue; + debug_wire_type = {WireType::INLINE, node->cell}; // wire replaced with cell break; - } else if (!localized_wires.count(rhs_wire) && !inlined_wires.count(rhs_wire)) { - alias_of = rhs_wire; // alias of member + case FlowGraph::Node::Type::CONNECT: + debug_wire_type = {WireType::INLINE, node->connect.second}; // wire replaced with sig break; - } else { - if (rhs_wire->name.isPublic() && debug_outlined_wires.count(rhs_wire)) - alias_of = rhs_wire; // alias of either outline or another alias - cursor = rhs_wire; // keep looking - } - } else { - break; // not an alias: complex rhs + default: continue; } + live_nodes.erase(node); + } else if (wire_type.is_local()) { + debug_wire_type = {WireType::LOCAL}; // wire not inlinable + } else { + log_assert(wire_type.is_member()); + debug_wire_type = wire_type; // wire is a member } - if (alias_of.empty()) { - continue; - } else if (alias_of.is_fully_const()) { - debug_const_wires[wire] = alias_of.as_const(); - } else if (alias_of.is_wire()) { - debug_alias_wires[wire] = alias_of.as_wire(); - } else log_abort(); - if (inlined_wires.count(wire)) - debug_outlined_wires.erase(wire); } + + // Emit reachable nodes in debug_eval(). + for (auto node : node_order) + if (live_nodes[node]) + debug_schedule[module].push_back(*node); } } if (has_feedback_arcs || has_buffered_comb_wires) { @@ -2593,7 +2749,7 @@ struct CxxrtlWorker { struct CxxrtlBackend : public Backend { static const int DEFAULT_OPT_LEVEL = 6; - static const int DEFAULT_DEBUG_LEVEL = 3; + static const int DEFAULT_DEBUG_LEVEL = 4; CxxrtlBackend() : Backend("cxxrtl", "convert design to C++ RTL simulation") { } void help() override @@ -2811,18 +2967,22 @@ struct CxxrtlBackend : public Backend { log(" more visibility and generate more code, but do not pessimize evaluation.\n"); log("\n"); log(" -g0\n"); - log(" no debug information. the C API is unavailable.\n"); + log(" no debug information. the C API is disabled.\n"); log("\n"); log(" -g1\n"); - log(" debug information for member public wires only. this is the bare minimum\n"); - log(" necessary to access all design state. enables the C API.\n"); + log(" include bare minimum of debug information necessary to access all design\n"); + log(" state. the C API is enabled.\n"); log("\n"); log(" -g2\n"); - log(" like -g1, and include debug information for public wires that are tied\n"); - log(" to a constant or another public wire.\n"); + log(" like -g1, but include debug information for all public wires that are\n"); + log(" directly accessible through the C++ interface.\n"); log("\n"); log(" -g3\n"); - log(" like -g2, and compute debug information on demand for all public wires\n"); + log(" like -g2, and include debug information for public wires that are tied\n"); + log(" to a constant or another public wire.\n"); + log("\n"); + log(" -g4\n"); + log(" like -g3, and compute debug information on demand for all public wires\n"); log(" that were optimized out.\n"); log("\n"); } @@ -2922,12 +3082,15 @@ struct CxxrtlBackend : public Backend { } switch (debug_level) { // the highest level here must match DEFAULT_DEBUG_LEVEL - case 3: + case 4: worker.debug_eval = true; YS_FALLTHROUGH - case 2: + case 3: worker.debug_alias = true; YS_FALLTHROUGH + case 2: + worker.debug_member = true; + YS_FALLTHROUGH case 1: worker.debug_info = true; YS_FALLTHROUGH