Merge pull request #4134 from whitequark/cxxrtl-capture-print

CXXRTL: Allow capturing `$print` cell output
This commit is contained in:
Catherine 2024-01-17 13:13:15 +00:00 committed by GitHub
commit c4c55cb565
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 243 additions and 216 deletions

View File

@ -1072,9 +1072,23 @@ struct CxxrtlWorker {
dump_sigspec_rhs(cell->getPort(ID::EN));
f << " == value<1>{1u}) {\n";
inc_indent();
f << indent << print_output;
fmt.emit_cxxrtl(f, [this](const RTLIL::SigSpec &sig) { dump_sigspec_rhs(sig); });
f << ";\n";
f << indent << "auto formatter = [&](int64_t itime, double ftime) {\n";
inc_indent();
fmt.emit_cxxrtl(f, indent, [this](const RTLIL::SigSpec &sig) { dump_sigspec_rhs(sig); });
dec_indent();
f << indent << "};\n";
f << indent << "if (performer) {\n";
inc_indent();
f << indent << "static const metadata_map attributes = ";
dump_metadata_map(cell->attributes);
f << ";\n";
f << indent << "performer->on_print(formatter(performer->time(), performer->realtime()), attributes);\n";
dec_indent();
f << indent << "} else {\n";
inc_indent();
f << indent << print_output << " << formatter(0, 0.0);\n";
dec_indent();
f << indent << "}\n";
dec_indent();
f << indent << "}\n";
}
@ -1494,11 +1508,11 @@ struct CxxrtlWorker {
};
if (buffered_inputs) {
// If we have any buffered inputs, there's no chance of converging immediately.
f << indent << mangle(cell) << access << "eval();\n";
f << indent << mangle(cell) << access << "eval(performer);\n";
f << indent << "converged = false;\n";
assign_from_outputs(/*cell_converged=*/false);
} else {
f << indent << "if (" << mangle(cell) << access << "eval()) {\n";
f << indent << "if (" << mangle(cell) << access << "eval(performer)) {\n";
inc_indent();
assign_from_outputs(/*cell_converged=*/true);
dec_indent();
@ -2381,7 +2395,8 @@ struct CxxrtlWorker {
dump_reset_method(module);
f << indent << "}\n";
f << "\n";
f << indent << "bool eval() override {\n";
// No default argument, to prevent unintentional `return bb_foo::eval();` calls that drop performer.
f << indent << "bool eval(performer *performer) override {\n";
dump_eval_method(module);
f << indent << "}\n";
f << "\n";
@ -2391,7 +2406,7 @@ struct CxxrtlWorker {
f << indent << "}\n";
f << "\n";
f << indent << "bool commit() override {\n";
f << indent << indent << "null_observer observer;\n";
f << indent << indent << "observer observer;\n";
f << indent << indent << "return commit<>(observer);\n";
f << indent << "}\n";
if (debug_info) {
@ -2478,7 +2493,7 @@ struct CxxrtlWorker {
f << "\n";
f << indent << "void reset() override;\n";
f << "\n";
f << indent << "bool eval() override;\n";
f << indent << "bool eval(performer *performer = nullptr) override;\n";
f << "\n";
f << indent << "template<class ObserverT>\n";
f << indent << "bool commit(ObserverT &observer) {\n";
@ -2486,7 +2501,7 @@ struct CxxrtlWorker {
f << indent << "}\n";
f << "\n";
f << indent << "bool commit() override {\n";
f << indent << indent << "null_observer observer;\n";
f << indent << indent << "observer observer;\n";
f << indent << indent << "return commit<>(observer);\n";
f << indent << "}\n";
if (debug_info) {
@ -2517,7 +2532,7 @@ struct CxxrtlWorker {
dump_reset_method(module);
f << indent << "}\n";
f << "\n";
f << indent << "bool " << mangle(module) << "::eval() {\n";
f << indent << "bool " << mangle(module) << "::eval(performer *performer) {\n";
dump_eval_method(module);
f << indent << "}\n";
if (debug_info) {
@ -2541,7 +2556,6 @@ struct CxxrtlWorker {
RTLIL::Module *top_module = nullptr;
std::vector<RTLIL::Module*> modules;
TopoSort<RTLIL::Module*> topo_design;
bool has_prints = false;
for (auto module : design->modules()) {
if (!design->selected_module(module))
continue;
@ -2554,8 +2568,6 @@ struct CxxrtlWorker {
topo_design.node(module);
for (auto cell : module->cells()) {
if (cell->type == ID($print))
has_prints = true;
if (is_internal_cell(cell->type) || is_cxxrtl_blackbox_cell(cell))
continue;
RTLIL::Module *cell_module = design->module(cell->type);
@ -2614,8 +2626,6 @@ struct CxxrtlWorker {
f << "#include \"" << basename(intf_filename) << "\"\n";
else
f << "#include <cxxrtl/cxxrtl.h>\n";
if (has_prints)
f << "#include <iostream>\n";
f << "\n";
f << "#if defined(CXXRTL_INCLUDE_CAPI_IMPL) || \\\n";
f << " defined(CXXRTL_INCLUDE_VCD_CAPI_IMPL)\n";
@ -3293,7 +3303,7 @@ struct CxxrtlBackend : public Backend {
log(" value<8> p_i_data;\n");
log(" wire<8> p_o_data;\n");
log("\n");
log(" bool eval() override;\n");
log(" bool eval(performer *performer) override;\n");
log(" template<class ObserverT>\n");
log(" bool commit(ObserverT &observer);\n");
log(" bool commit() override;\n");
@ -3308,11 +3318,11 @@ struct CxxrtlBackend : public Backend {
log(" namespace cxxrtl_design {\n");
log("\n");
log(" struct stderr_debug : public bb_p_debug {\n");
log(" bool eval() override {\n");
log(" bool eval(performer *performer) override {\n");
log(" if (posedge_p_clk() && p_en)\n");
log(" fprintf(stderr, \"debug: %%02x\\n\", p_i_data.data[0]);\n");
log(" p_o_data.next = p_i_data;\n");
log(" return bb_p_debug::eval();\n");
log(" return bb_p_debug::eval(performer);\n");
log(" }\n");
log(" };\n");
log("\n");
@ -3413,7 +3423,7 @@ struct CxxrtlBackend : public Backend {
log(" -print-output <stream>\n");
log(" $print cells in the generated code direct their output to <stream>.\n");
log(" must be one of \"std::cout\", \"std::cerr\". if not specified,\n");
log(" \"std::cout\" is used.\n");
log(" \"std::cout\" is used. explicitly provided performer overrides this.\n");
log("\n");
log(" -nohierarchy\n");
log(" use design hierarchy as-is. in most designs, a top module should be\n");

View File

@ -39,6 +39,7 @@
#include <memory>
#include <functional>
#include <sstream>
#include <iostream>
// `cxxrtl::debug_item` has to inherit from `cxxrtl_object` to satisfy strict aliasing requirements.
#include <cxxrtl/capi/cxxrtl_capi.h>
@ -565,7 +566,7 @@ struct value : public expr_base<value<Bits>> {
}
value<Bits> neg() const {
return value<Bits> { 0u }.sub(*this);
return value<Bits>().sub(*this);
}
bool ucmp(const value<Bits> &other) const {
@ -763,121 +764,132 @@ std::ostream &operator<<(std::ostream &os, const value<Bits> &val) {
return os;
}
template<size_t Bits>
struct value_formatted {
const value<Bits> &val;
bool character;
bool justify_left;
char padding;
int width;
int base;
bool signed_;
bool plus;
// Must be kept in sync with `struct FmtPart` in kernel/fmt.h!
// Default member initializers would make this a non-aggregate-type in C++11, so they are commented out.
struct fmt_part {
enum {
STRING = 0,
INTEGER = 1,
CHARACTER = 2,
TIME = 3,
} type;
value_formatted(const value<Bits> &val, bool character, bool justify_left, char padding, int width, int base, bool signed_, bool plus) :
val(val), character(character), justify_left(justify_left), padding(padding), width(width), base(base), signed_(signed_), plus(plus) {}
value_formatted(const value_formatted<Bits> &) = delete;
value_formatted<Bits> &operator=(const value_formatted<Bits> &rhs) = delete;
};
// STRING type
std::string str;
template<size_t Bits>
std::ostream &operator<<(std::ostream &os, const value_formatted<Bits> &vf)
{
value<Bits> val = vf.val;
// INTEGER/CHARACTER types
// + value<Bits> val;
std::string buf;
// INTEGER/CHARACTER/TIME types
enum {
RIGHT = 0,
LEFT = 1,
} justify; // = RIGHT;
char padding; // = '\0';
size_t width; // = 0;
// We might want to replace some of these bit() calls with direct
// chunk access if it turns out to be slow enough to matter.
// INTEGER type
unsigned base; // = 10;
bool signed_; // = false;
bool plus; // = false;
if (!vf.character) {
size_t width = Bits;
if (vf.base != 10) {
width = 0;
for (size_t index = 0; index < Bits; index++)
if (val.bit(index))
width = index + 1;
}
// TIME type
bool realtime; // = false;
// + int64_t itime;
// + double ftime;
if (vf.base == 2) {
for (size_t i = width; i > 0; i--)
buf += (val.bit(i - 1) ? '1' : '0');
} else if (vf.base == 8 || vf.base == 16) {
size_t step = (vf.base == 16) ? 4 : 3;
for (size_t index = 0; index < width; index += step) {
uint8_t value = val.bit(index) | (val.bit(index + 1) << 1) | (val.bit(index + 2) << 2);
if (step == 4)
value |= val.bit(index + 3) << 3;
buf += "0123456789abcdef"[value];
// Format the part as a string.
//
// The values of `itime` and `ftime` are used for `$time` and `$realtime`, correspondingly.
template<size_t Bits>
std::string render(value<Bits> val, int64_t itime, double ftime)
{
// We might want to replace some of these bit() calls with direct
// chunk access if it turns out to be slow enough to matter.
std::string buf;
switch (type) {
case STRING:
return str;
case CHARACTER: {
buf.reserve(Bits/8);
for (int i = 0; i < Bits; i += 8) {
char ch = 0;
for (int j = 0; j < 8 && i + j < int(Bits); j++)
if (val.bit(i + j))
ch |= 1 << j;
if (ch != 0)
buf.append({ch});
}
std::reverse(buf.begin(), buf.end());
break;
}
std::reverse(buf.begin(), buf.end());
} else if (vf.base == 10) {
bool negative = vf.signed_ && val.is_neg();
if (negative)
val = val.neg();
if (val.is_zero())
buf += '0';
while (!val.is_zero()) {
value<Bits> quotient, remainder;
if (Bits >= 4)
std::tie(quotient, remainder) = val.udivmod(value<Bits>{10u});
else
std::tie(quotient, remainder) = std::make_pair(value<Bits>{0u}, val);
buf += '0' + remainder.template trunc<(Bits > 4 ? 4 : Bits)>().val().template get<uint8_t>();
val = quotient;
case INTEGER: {
size_t width = Bits;
if (base != 10) {
width = 0;
for (size_t index = 0; index < Bits; index++)
if (val.bit(index))
width = index + 1;
}
if (base == 2) {
for (size_t i = width; i > 0; i--)
buf += (val.bit(i - 1) ? '1' : '0');
} else if (base == 8 || base == 16) {
size_t step = (base == 16) ? 4 : 3;
for (size_t index = 0; index < width; index += step) {
uint8_t value = val.bit(index) | (val.bit(index + 1) << 1) | (val.bit(index + 2) << 2);
if (step == 4)
value |= val.bit(index + 3) << 3;
buf += "0123456789abcdef"[value];
}
std::reverse(buf.begin(), buf.end());
} else if (base == 10) {
bool negative = signed_ && val.is_neg();
if (negative)
val = val.neg();
if (val.is_zero())
buf += '0';
value<(Bits > 4 ? Bits : 4)> xval = val.template zext<(Bits > 4 ? Bits : 4)>();
while (!xval.is_zero()) {
value<(Bits > 4 ? Bits : 4)> quotient, remainder;
if (Bits >= 4)
std::tie(quotient, remainder) = xval.udivmod(value<(Bits > 4 ? Bits : 4)>{10u});
else
std::tie(quotient, remainder) = std::make_pair(value<(Bits > 4 ? Bits : 4)>{0u}, xval);
buf += '0' + remainder.template trunc<4>().template get<uint8_t>();
xval = quotient;
}
if (negative || plus)
buf += negative ? '-' : '+';
std::reverse(buf.begin(), buf.end());
} else assert(false && "Unsupported base for fmt_part");
break;
}
case TIME: {
buf = realtime ? std::to_string(ftime) : std::to_string(itime);
break;
}
if (negative || vf.plus)
buf += negative ? '-' : '+';
std::reverse(buf.begin(), buf.end());
} else assert(false);
} else {
buf.reserve(Bits/8);
for (int i = 0; i < Bits; i += 8) {
char ch = 0;
for (int j = 0; j < 8 && i + j < int(Bits); j++)
if (val.bit(i + j))
ch |= 1 << j;
if (ch != 0)
buf.append({ch});
}
std::reverse(buf.begin(), buf.end());
}
assert(vf.width == 0 || vf.padding != '\0');
if (!vf.justify_left && buf.size() < vf.width) {
size_t pad_width = vf.width - buf.size();
if (vf.padding == '0' && (buf.front() == '+' || buf.front() == '-')) {
os << buf.front();
buf.erase(0, 1);
std::string str;
assert(width == 0 || padding != '\0');
if (justify == RIGHT && buf.size() < width) {
size_t pad_width = width - buf.size();
if (padding == '0' && (buf.front() == '+' || buf.front() == '-')) {
str += buf.front();
buf.erase(0, 1);
}
str += std::string(pad_width, padding);
}
os << std::string(pad_width, vf.padding);
str += buf;
if (justify == LEFT && buf.size() < width)
str += std::string(width - buf.size(), padding);
return str;
}
os << buf;
if (vf.justify_left && buf.size() < vf.width)
os << std::string(vf.width - buf.size(), vf.padding);
return os;
}
// An object that can be passed to a `commit()` method in order to produce a replay log of every state change in
// the simulation.
struct observer {
// Called when the `commit()` method for a wire is about to update the `chunks` chunks at `base` with `chunks` chunks
// at `value` that have a different bit pattern. It is guaranteed that `chunks` is equal to the wire chunk count and
// `base` points to the first chunk.
virtual void on_commit(size_t chunks, const chunk_t *base, const chunk_t *value) = 0;
// Called when the `commit()` method for a memory is about to update the `chunks` chunks at `&base[chunks * index]`
// with `chunks` chunks at `value` that have a different bit pattern. It is guaranteed that `chunks` is equal to
// the memory element chunk count and `base` points to the first chunk of the first element of the memory.
virtual void on_commit(size_t chunks, const chunk_t *base, const chunk_t *value, size_t index) = 0;
};
// The `null_observer` class has the same interface as `observer`, but has no invocation overhead, since its methods
// are final and have no implementation. This allows the observer feature to be zero-cost when not in use.
struct null_observer final: observer {
void on_commit(size_t chunks, const chunk_t *base, const chunk_t *value) override {}
void on_commit(size_t chunks, const chunk_t *base, const chunk_t *value, size_t index) override {}
};
template<size_t Bits>
@ -916,12 +928,11 @@ struct wire {
// This method intentionally takes a mandatory argument (to make it more difficult to misuse in
// black box implementations, leading to missed observer events). It is generic over its argument
// to make sure the `on_commit` call is devirtualized. This is somewhat awkward but lets us keep
// a single implementation for both this method and the one in `memory`.
// to allow the `on_update` method to be non-virtual.
template<class ObserverT>
bool commit(ObserverT &observer) {
if (curr != next) {
observer.on_commit(curr.chunks, curr.data, next.data);
observer.on_update(curr.chunks, curr.data, next.data);
curr = next;
return true;
}
@ -1003,7 +1014,7 @@ struct memory {
value<Width> elem = data[entry.index];
elem = elem.update(entry.val, entry.mask);
if (data[entry.index] != elem) {
observer.on_commit(value<Width>::chunks, data[0].data, elem.data, entry.index);
observer.on_update(value<Width>::chunks, data[0].data, elem.data, entry.index);
changed |= true;
}
data[entry.index] = elem;
@ -1062,6 +1073,33 @@ struct metadata {
typedef std::map<std::string, metadata> metadata_map;
// An object that can be passed to a `eval()` method in order to act on side effects.
struct performer {
// Called to evaluate a Verilog `$time` expression.
virtual int64_t time() const { return 0; }
// Called to evaluate a Verilog `$realtime` expression.
virtual double realtime() const { return time(); }
// Called when a `$print` cell is triggered.
virtual void on_print(const std::string &output, const metadata_map &attributes) { std::cout << output; }
};
// An object that can be passed to a `commit()` method in order to produce a replay log of every state change in
// the simulation. Unlike `performer`, `observer` does not use virtual calls as their overhead is unacceptable, and
// a comparatively heavyweight template-based solution is justified.
struct observer {
// Called when the `commit()` method for a wire is about to update the `chunks` chunks at `base` with `chunks` chunks
// at `value` that have a different bit pattern. It is guaranteed that `chunks` is equal to the wire chunk count and
// `base` points to the first chunk.
void on_update(size_t chunks, const chunk_t *base, const chunk_t *value) {}
// Called when the `commit()` method for a memory is about to update the `chunks` chunks at `&base[chunks * index]`
// with `chunks` chunks at `value` that have a different bit pattern. It is guaranteed that `chunks` is equal to
// the memory element chunk count and `base` points to the first chunk of the first element of the memory.
void on_update(size_t chunks, const chunk_t *base, const chunk_t *value, size_t index) {}
};
// Tag class to disambiguate values/wires and their aliases.
struct debug_alias {};
@ -1304,17 +1342,14 @@ struct module {
virtual void reset() = 0;
virtual bool eval() = 0;
virtual bool commit() = 0;
virtual bool eval(performer *performer = nullptr) = 0;
virtual bool commit() = 0; // commit observer isn't available since it avoids virtual calls
unsigned int steps = 0;
size_t step() {
++steps;
size_t step(performer *performer = nullptr) {
size_t deltas = 0;
bool converged = false;
do {
converged = eval();
converged = eval(performer);
deltas++;
} while (commit() && !converged);
return deltas;

View File

@ -556,22 +556,20 @@ public:
bool record_incremental(ModuleT &module) {
assert(streaming);
struct : public observer {
struct {
std::unordered_map<const chunk_t*, spool::ident_t> *ident_lookup;
spool::writer *writer;
CXXRTL_ALWAYS_INLINE
void on_commit(size_t chunks, const chunk_t *base, const chunk_t *value) override {
void on_update(size_t chunks, const chunk_t *base, const chunk_t *value) {
writer->write_change(ident_lookup->at(base), chunks, value);
}
CXXRTL_ALWAYS_INLINE
void on_commit(size_t chunks, const chunk_t *base, const chunk_t *value, size_t index) override {
void on_update(size_t chunks, const chunk_t *base, const chunk_t *value, size_t index) {
writer->write_change(ident_lookup->at(base), chunks, value, index);
}
} record_observer;
record_observer.ident_lookup = &ident_lookup;
record_observer.writer = &writer;
} record_observer = { &ident_lookup, &writer };
writer.write_sample(/*incremental=*/true, pointer++, timestamp);
for (auto input_index : inputs) {

View File

@ -569,82 +569,60 @@ std::vector<VerilogFmtArg> Fmt::emit_verilog() const
return args;
}
void Fmt::emit_cxxrtl(std::ostream &f, std::function<void(const RTLIL::SigSpec &)> emit_sig) const
std::string escape_cxx_string(const std::string &input)
{
for (auto &part : parts) {
switch (part.type) {
case FmtPart::STRING:
f << " << \"";
for (char c : part.str) {
switch (c) {
case '\\':
YS_FALLTHROUGH
case '"':
f << '\\' << c;
break;
case '\a':
f << "\\a";
break;
case '\b':
f << "\\b";
break;
case '\f':
f << "\\f";
break;
case '\n':
f << "\\n";
break;
case '\r':
f << "\\r";
break;
case '\t':
f << "\\t";
break;
case '\v':
f << "\\v";
break;
default:
f << c;
break;
}
}
f << '"';
break;
case FmtPart::INTEGER:
case FmtPart::CHARACTER: {
f << " << value_formatted<" << part.sig.size() << ">(";
emit_sig(part.sig);
f << ", " << (part.type == FmtPart::CHARACTER);
f << ", " << (part.justify == FmtPart::LEFT);
f << ", (char)" << (int)part.padding;
f << ", " << part.width;
f << ", " << part.base;
f << ", " << part.signed_;
f << ", " << part.plus;
f << ')';
break;
}
case FmtPart::TIME: {
// CXXRTL only records steps taken, so there's no difference between
// the values taken by $time and $realtime.
f << " << value_formatted<64>(";
f << "value<64>{steps}";
f << ", " << (part.type == FmtPart::CHARACTER);
f << ", " << (part.justify == FmtPart::LEFT);
f << ", (char)" << (int)part.padding;
f << ", " << part.width;
f << ", " << part.base;
f << ", " << part.signed_;
f << ", " << part.plus;
f << ')';
break;
}
default: log_abort();
std::string output = "\"";
for (auto c : input) {
if (::isprint(c)) {
if (c == '\\')
output.push_back('\\');
output.push_back(c);
} else {
char l = c & 0xf, h = (c >> 4) & 0xf;
output.append("\\x");
output.push_back((h < 10 ? '0' + h : 'a' + h - 10));
output.push_back((l < 10 ? '0' + l : 'a' + l - 10));
}
}
output.push_back('"');
if (output.find('\0') != std::string::npos) {
output.insert(0, "std::string {");
output.append(stringf(", %zu}", input.size()));
}
return output;
}
void Fmt::emit_cxxrtl(std::ostream &os, std::string indent, std::function<void(const RTLIL::SigSpec &)> emit_sig) const
{
os << indent << "std::string buf;\n";
for (auto &part : parts) {
os << indent << "buf += fmt_part { ";
os << "fmt_part::";
switch (part.type) {
case FmtPart::STRING: os << "STRING"; break;
case FmtPart::INTEGER: os << "INTEGER"; break;
case FmtPart::CHARACTER: os << "CHARACTER"; break;
case FmtPart::TIME: os << "TIME"; break;
}
os << ", ";
os << escape_cxx_string(part.str) << ", ";
os << "fmt_part::";
switch (part.justify) {
case FmtPart::LEFT: os << "LEFT"; break;
case FmtPart::RIGHT: os << "RIGHT"; break;
}
os << ", ";
os << "(char)" << (int)part.padding << ", ";
os << part.width << ", ";
os << part.base << ", ";
os << part.signed_ << ", ";
os << part.plus << ", ";
os << part.realtime;
os << " }.render(";
emit_sig(part.sig);
os << ", itime, ftime);\n";
}
os << indent << "return buf;\n";
}
std::string Fmt::render() const

View File

@ -50,6 +50,7 @@ struct VerilogFmtArg {
// RTLIL format part, such as the substitutions in:
// "foo {4:> 4du} bar {2:<01hs}"
// Must be kept in sync with `struct fmt_part` in backends/cxxrtl/runtime/cxxrtl/cxxrtl.h!
struct FmtPart {
enum {
STRING = 0,
@ -71,7 +72,7 @@ struct FmtPart {
} justify = RIGHT;
char padding = '\0';
size_t width = 0;
// INTEGER type
unsigned base = 10;
bool signed_ = false;
@ -93,7 +94,7 @@ public:
void parse_verilog(const std::vector<VerilogFmtArg> &args, bool sformat_like, int default_base, RTLIL::IdString task_name, RTLIL::IdString module_name);
std::vector<VerilogFmtArg> emit_verilog() const;
void emit_cxxrtl(std::ostream &f, std::function<void(const RTLIL::SigSpec &)> emit_sig) const;
void emit_cxxrtl(std::ostream &os, std::string indent, std::function<void(const RTLIL::SigSpec &)> emit_sig) const;
std::string render() const;

View File

@ -2,8 +2,13 @@
int main()
{
struct : public performer {
int64_t time() const override { return 1; }
void on_print(const std::string &output, const cxxrtl::metadata_map &) override { std::cerr << output; }
} performer;
cxxrtl_design::p_always__full uut;
uut.p_clk.set(!uut.p_clk);
uut.step();
uut.step(&performer);
return 0;
}