From f6e16e7f4c65440783047d3f2b03563f36f6dae8 Mon Sep 17 00:00:00 2001
From: whitequark <whitequark@whitequark.org>
Date: Wed, 27 May 2020 00:21:15 +0000
Subject: [PATCH] cxxrtl: generate debug information for non-localized public
 wires.

Debug information describes values, wires, and memories with a simple
C-compatible layout. It can be emitted on demand into a map, which
has no runtime cost when it is unused, and allows late bound designs.

The `hdlname` attribute is used as the lookup key such that original
names, as emitted by the frontend, can be used for debugging and
introspection.
---
 backends/cxxrtl/cxxrtl.cc | 87 ++++++++++++++++++++++++++++++++++++++-
 backends/cxxrtl/cxxrtl.h  | 46 ++++++++++++++++++++-
 2 files changed, 131 insertions(+), 2 deletions(-)

diff --git a/backends/cxxrtl/cxxrtl.cc b/backends/cxxrtl/cxxrtl.cc
index 0cceecbba..edd606c1a 100644
--- a/backends/cxxrtl/cxxrtl.cc
+++ b/backends/cxxrtl/cxxrtl.cc
@@ -502,6 +502,15 @@ std::string escape_cxx_string(const std::string &input)
 	return output;
 }
 
+template<class T>
+std::string get_hdl_name(T *object)
+{
+	if (object->has_attribute(ID::hdlname))
+		return object->get_string_attribute(ID::hdlname);
+	else
+		return object->name.str();
+}
+
 struct CxxrtlWorker {
 	bool split_intf = false;
 	std::string intf_filename;
@@ -516,6 +525,8 @@ struct CxxrtlWorker {
 	bool run_proc_flatten = false;
 	bool max_opt_level = false;
 
+	bool debug_info = false;
+
 	std::ostringstream f;
 	std::string indent;
 	int temporary = 0;
@@ -1593,6 +1604,34 @@ struct CxxrtlWorker {
 		dec_indent();
 	}
 
+	void dump_debug_info_method(RTLIL::Module *module)
+	{
+		inc_indent();
+			f << indent << "assert(path.empty() || path[path.size() - 1] == ' ');\n";
+			for (auto wire : module->wires()) {
+				if (wire->name[0] != '\\')
+					continue;
+				if (localized_wires.count(wire))
+					continue;
+				f << indent << "items.emplace(path + " << escape_cxx_string(get_hdl_name(wire));
+				f << ", debug_item(" << mangle(wire) << "));\n";
+			}
+			for (auto &memory_it : module->memories) {
+				if (memory_it.first[0] != '\\')
+					continue;
+				f << indent << "items.emplace(path + " << escape_cxx_string(get_hdl_name(memory_it.second));
+				f << ", debug_item(" << mangle(memory_it.second) << "));\n";
+			}
+			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";
+			}
+		dec_indent();
+	}
+
 	void dump_metadata_map(const dict<RTLIL::IdString, RTLIL::Const> &metadata_map)
 	{
 		if (metadata_map.empty()) {
@@ -1641,6 +1680,12 @@ struct CxxrtlWorker {
 				dump_commit_method(module);
 				f << indent << "}\n";
 				f << "\n";
+				if (debug_info) {
+					f << indent << "void debug_info(debug_items &items, std::string path = \"\") override {\n";
+					dump_debug_info_method(module);
+					f << indent << "}\n";
+					f << "\n";
+				}
 				f << indent << "static std::unique_ptr<" << mangle(module);
 				f << template_params(module, /*is_decl=*/false) << "> ";
 				f << "create(std::string name, metadata_map parameters, metadata_map attributes);\n";
@@ -1689,7 +1734,7 @@ struct CxxrtlWorker {
 					if (cell_module->get_bool_attribute(ID(cxxrtl_blackbox))) {
 						f << indent << "std::unique_ptr<" << mangle(cell_module) << template_args(cell) << "> ";
 						f << mangle(cell) << " = " << mangle(cell_module) << template_args(cell);
-						f << "::create(" << escape_cxx_string(cell->name.str()) << ", ";
+						f << "::create(" << escape_cxx_string(get_hdl_name(cell)) << ", ";
 						dump_metadata_map(cell->parameters);
 						f << ", ";
 						dump_metadata_map(cell->attributes);
@@ -1703,6 +1748,8 @@ struct CxxrtlWorker {
 					f << "\n";
 				f << indent << "bool eval() override;\n";
 				f << indent << "bool commit() override;\n";
+				if (debug_info)
+					f << indent << "void debug_info(debug_items &items, std::string path = \"\") override;\n";
 			dec_indent();
 			f << indent << "}; // struct " << mangle(module) << "\n";
 			f << "\n";
@@ -1721,6 +1768,12 @@ struct CxxrtlWorker {
 		dump_commit_method(module);
 		f << indent << "}\n";
 		f << "\n";
+		if (debug_info) {
+			f << indent << "void " << mangle(module) << "::debug_info(debug_items &items, std::string path) {\n";
+			dump_debug_info_method(module);
+			f << indent << "}\n";
+			f << "\n";
+		}
 	}
 
 	void dump_design(RTLIL::Design *design)
@@ -2120,6 +2173,7 @@ struct CxxrtlWorker {
 
 struct CxxrtlBackend : public Backend {
 	static const int DEFAULT_OPT_LEVEL = 5;
+	static const int DEFAULT_DEBUG_LEVEL = 1;
 
 	CxxrtlBackend() : Backend("cxxrtl", "convert design to C++ RTL simulation") { }
 	void help() YS_OVERRIDE
@@ -2313,10 +2367,22 @@ struct CxxrtlBackend : public Backend {
 		log("    -O5\n");
 		log("        like -O4, and run `proc; flatten` first.\n");
 		log("\n");
+		log("    -g <level>\n");
+		log("        set the debug level. the default is -g%d. higher debug levels provide\n", DEFAULT_DEBUG_LEVEL);
+		log("        more visibility and generate more code, but do not pessimize evaluation.\n");
+		log("\n");
+		log("    -g0\n");
+		log("        no debug information.\n");
+		log("\n");
+		log("    -g1\n");
+		log("        debug information for non-localized public wires.\n");
+		log("\n");
 	}
+
 	void execute(std::ostream *&f, std::string filename, std::vector<std::string> args, RTLIL::Design *design) YS_OVERRIDE
 	{
 		int opt_level = DEFAULT_OPT_LEVEL;
+		int debug_level = DEFAULT_DEBUG_LEVEL;
 		CxxrtlWorker worker;
 
 		log_header(design, "Executing CXXRTL backend.\n");
@@ -2332,6 +2398,14 @@ struct CxxrtlBackend : public Backend {
 				opt_level = std::stoi(args[argidx].substr(2));
 				continue;
 			}
+			if (args[argidx] == "-g" && argidx+1 < args.size()) {
+				debug_level = std::stoi(args[++argidx]);
+				continue;
+			}
+			if (args[argidx].substr(0, 2) == "-g" && args[argidx].size() == 3 && isdigit(args[argidx][2])) {
+				debug_level = std::stoi(args[argidx].substr(2));
+				continue;
+			}
 			if (args[argidx] == "-header") {
 				worker.split_intf = true;
 				continue;
@@ -2368,6 +2442,17 @@ struct CxxrtlBackend : public Backend {
 				log_cmd_error("Invalid optimization level %d.\n", opt_level);
 		}
 
+		switch (debug_level) {
+			// the highest level here must match DEFAULT_DEBUG_LEVEL
+			case 1:
+				worker.debug_info = true;
+				YS_FALLTHROUGH
+			case 0:
+				break;
+			default:
+				log_cmd_error("Invalid optimization level %d.\n", opt_level);
+		}
+
 		std::ofstream intf_f;
 		if (worker.split_intf) {
 			if (filename == "<stdout>")
diff --git a/backends/cxxrtl/cxxrtl.h b/backends/cxxrtl/cxxrtl.h
index 7b91742e0..14613afb0 100644
--- a/backends/cxxrtl/cxxrtl.h
+++ b/backends/cxxrtl/cxxrtl.h
@@ -49,6 +49,8 @@ namespace cxxrtl {
 // invisible to the compiler, (b) we often operate on non-power-of-2 values and have to clear the high bits anyway.
 // Therefore, using relatively wide chunks and clearing the high bits explicitly and only when we know they may be
 // clobbered results in simpler generated code.
+typedef uint32_t chunk_t;
+
 template<typename T>
 struct chunk_traits {
 	static_assert(std::is_integral<T>::value && std::is_unsigned<T>::value,
@@ -65,7 +67,7 @@ template<size_t Bits>
 struct value : public expr_base<value<Bits>> {
 	static constexpr size_t bits = Bits;
 
-	using chunk = chunk_traits<uint32_t>;
+	using chunk = chunk_traits<chunk_t>;
 	static constexpr chunk::type msb_mask = (Bits % chunk::bits == 0) ? chunk::mask
 		: chunk::mask >> (chunk::bits - (Bits % chunk::bits));
 
@@ -712,6 +714,46 @@ struct metadata {
 
 typedef std::map<std::string, metadata> metadata_map;
 
+// This structure is intended for consumption via foreign function interfaces, like Python's ctypes.
+// Because of this it uses a C-style layout that is easy to parse rather than more idiomatic C++.
+struct debug_item {
+	enum : uint32_t {
+		VALUE  = 0,
+		WIRE   = 1,
+		MEMORY = 2,
+	} type;
+
+	size_t width; // in bits
+	size_t depth; // 1 if `type != MEMORY`
+	chunk_t *curr;
+	chunk_t *next; // nullptr if `type == VALUE || type == MEMORY`
+
+	template<size_t Bits>
+	debug_item(value<Bits> &item) : type(VALUE), width(Bits), depth(1),
+		curr(item.data), next(nullptr) {
+			static_assert(sizeof(item) == value<Bits>::chunks * sizeof(chunk_t),
+			              "value<Bits> is not compatible with C layout");
+		}
+
+	template<size_t Bits>
+	debug_item(wire<Bits> &item) : type(WIRE), width(Bits), depth(1),
+		curr(item.curr.data), next(item.next.data) {
+			static_assert(sizeof(item.curr) == value<Bits>::chunks * sizeof(chunk_t) &&
+			              sizeof(item.next) == value<Bits>::chunks * sizeof(chunk_t),
+			              "wire<Bits> is not compatible with C layout");
+		}
+
+	template<size_t Width>
+	debug_item(memory<Width> &item) : type(MEMORY), width(Width), depth(item.data.size()),
+		curr(item.data.empty() ? nullptr : item.data[0].data), next(nullptr) {
+			static_assert(sizeof(item.data[0]) == value<Width>::chunks * sizeof(chunk_t),
+			              "memory<Width> is not compatible with C layout");
+		}
+};
+static_assert(std::is_standard_layout<debug_item>::value, "debug_item is not compatible with C layout");
+
+typedef std::map<std::string, debug_item> debug_items;
+
 struct module {
 	module() {}
 	virtual ~module() {}
@@ -731,6 +773,8 @@ struct module {
 		} while (commit() && !converged);
 		return deltas;
 	}
+
+	virtual void debug_info(debug_items &items, std::string path = "") {}
 };
 
 } // namespace cxxrtl