From 0feda723a8d9d6895fc4f28575b1bc5d4d53a022 Mon Sep 17 00:00:00 2001 From: Remy Goldschmidt Date: Sat, 6 Jun 2026 18:17:14 -0700 Subject: [PATCH] Add UDP support --- frontends/verilog/CMakeLists.txt | 2 + frontends/verilog/verilog_lexer.l | 24 +- frontends/verilog/verilog_parser.y | 163 +++++++- frontends/verilog/verilog_udp.cc | 561 ++++++++++++++++++++++++++ frontends/verilog/verilog_udp.h | 79 ++++ tests/verilog/udp_cosim.py | 105 +++++ tests/verilog/udp_noname_inst.ys | 15 + tests/verilog/udp_nondeterministic.ys | 12 + tests/verilog/udp_sim.sh | 137 +++++++ 9 files changed, 1080 insertions(+), 18 deletions(-) create mode 100644 frontends/verilog/verilog_udp.cc create mode 100644 frontends/verilog/verilog_udp.h create mode 100644 tests/verilog/udp_cosim.py create mode 100644 tests/verilog/udp_noname_inst.ys create mode 100644 tests/verilog/udp_nondeterministic.ys create mode 100755 tests/verilog/udp_sim.sh diff --git a/frontends/verilog/CMakeLists.txt b/frontends/verilog/CMakeLists.txt index 62cff043f..c67301476 100644 --- a/frontends/verilog/CMakeLists.txt +++ b/frontends/verilog/CMakeLists.txt @@ -23,6 +23,8 @@ yosys_frontend(verilog verilog_frontend.h verilog_lexer.h verilog_location.h + verilog_udp.cc + verilog_udp.h ${FLEX_verilog_lexer_OUTPUTS} ${BISON_verilog_parser_OUTPUTS} INCLUDE_DIRS diff --git a/frontends/verilog/verilog_lexer.l b/frontends/verilog/verilog_lexer.l index 62a7f7bbb..fb4badefb 100644 --- a/frontends/verilog/verilog_lexer.l +++ b/frontends/verilog/verilog_lexer.l @@ -280,6 +280,7 @@ static parser::symbol_type process_str(char *str, int len, bool triple, parser:: %x SYNOPSYS_FLAGS %x IMPORT_DPI %x BASED_CONST +%x UDPTABLE UNSIGNED_NUMBER [0-9][0-9_]* FIXED_POINT_NUMBER_DEC [0-9][0-9_]*\.[0-9][0-9_]*([eE][-+]?[0-9_]+)? @@ -355,6 +356,9 @@ TIME_SCALE_SUFFIX [munpf]?s "module" { return parser::make_TOK_MODULE(out_loc); } "endmodule" { return parser::make_TOK_ENDMODULE(out_loc); } +"primitive" { return parser::make_TOK_PRIMITIVE_DEF(out_loc); } +"endprimitive" { return parser::make_TOK_ENDPRIMITIVE(out_loc); } +"table" { BEGIN(UDPTABLE); return parser::make_TOK_TABLE(out_loc); } "function" { return parser::make_TOK_FUNCTION(out_loc); } "endfunction" { return parser::make_TOK_ENDFUNCTION(out_loc); } "task" { return parser::make_TOK_TASK(out_loc); } @@ -699,15 +703,29 @@ import[ \t\r\n]+\"(DPI|DPI-C)\"[ \t\r\n]+function[ \t\r\n]+ { {FIXED_POINT_NUMBER_DEC}{TIME_SCALE_SUFFIX} { return parser::make_TOK_TIME_SCALE(out_loc); } {FIXED_POINT_NUMBER_NO_DEC}{TIME_SCALE_SUFFIX} { return parser::make_TOK_TIME_SCALE(out_loc); } -"/*" { comment_caller=YY_START; BEGIN(COMMENT); } +"/*" { comment_caller=YY_START; BEGIN(COMMENT); } . /* ignore comment body */ \n /* ignore comment body */ "*/" { BEGIN(comment_caller); } -[ \t\r\n] /* ignore whitespaces */ +[ \t\r\n] /* ignore whitespaces */ \\[\r\n] /* ignore continuation sequence */ -"//"[^\r\n]* /* ignore one-line comments */ +"//"[^\r\n]* /* ignore one-line comments */ + + /* UDP state table: each field is a single symbol (or a parenthesised + transition). ':' and ';' delimit the input/state/output fields and + terminate rows; "endtable" leaves the table sub-language. */ +"endtable" { BEGIN(0); return parser::make_TOK_ENDTABLE(out_loc); } +\([01xXbB?][01xXbB?]\) { + string_t val = std::make_unique(YYText()); + return parser::make_TOK_UDP_VALUE(std::move(val), out_loc); +} +[01xXbB?rRfFpPnN*\-] { + string_t val = std::make_unique(YYText()); + return parser::make_TOK_UDP_VALUE(std::move(val), out_loc); +} +[:;] { return char_tok(*YYText(), out_loc); } . { return char_tok(*YYText(), out_loc); } <*>. { BEGIN(0); return char_tok(*YYText(), out_loc); } diff --git a/frontends/verilog/verilog_parser.y b/frontends/verilog/verilog_parser.y index b394ce074..fb1b9f803 100644 --- a/frontends/verilog/verilog_parser.y +++ b/frontends/verilog/verilog_parser.y @@ -88,6 +88,8 @@ bool current_modport_input, current_modport_output; bool default_nettype_wire = true; std::istream* lexin; + // state of the user-defined primitive currently being parsed + std::unique_ptr udp; AstNode* saveChild(std::unique_ptr child); AstNode* pushChild(std::unique_ptr child); @@ -429,6 +431,7 @@ #include #include #include "frontends/verilog/verilog_frontend.h" + #include "frontends/verilog/verilog_udp.h" struct specify_target { char polarity_op; @@ -487,6 +490,8 @@ %token TOK_SVA_LABEL TOK_SPECIFY_OPER TOK_MSG_TASKS %token TOK_BASE TOK_BASED_CONSTVAL TOK_UNBASED_UNSIZED_CONSTVAL %token TOK_USER_TYPE TOK_PKG_USER_TYPE +%token TOK_UDP_VALUE +%token TOK_PRIMITIVE_DEF TOK_ENDPRIMITIVE TOK_TABLE TOK_ENDTABLE %token TOK_ASSERT TOK_ASSUME TOK_RESTRICT TOK_COVER TOK_FINAL %token ATTR_BEGIN ATTR_END DEFATTR_BEGIN DEFATTR_END %token TOK_MODULE TOK_ENDMODULE TOK_PARAMETER TOK_LOCALPARAM TOK_DEFPARAM @@ -550,7 +555,8 @@ %type opt_label opt_sva_label tok_prim_wrapper hierarchical_id hierarchical_type_id integral_number %type type_name %type opt_enum_init enum_type struct_type enum_struct_type func_return_type typedef_base_type -%type opt_property always_comb_or_latch always_or_always_ff +%type opt_property always_comb_or_latch always_or_always_ff udp_output_reg_opt +%type udp_entry_tail %type opt_signedness_default_signed opt_signedness_default_unsigned %type integer_atom_type integer_vector_type %type attr if_attr case_attr @@ -602,6 +608,7 @@ input: { design: module design | + udp_primitive design | defattr design | task_func_decl design | param_decl design | @@ -714,6 +721,124 @@ module: extra->exitTypeScope(); }; +// User-defined primitives (IEEE 1364-2005 clause 8). The parser collects the +// definition into extra->udp and make_udp_module() lowers it into a normal +// behavioural module. +udp_primitive: + attr TOK_PRIMITIVE_DEF TOK_ID { + auto u = std::make_unique(); + u->loc = @2; + u->name = *$3; + u->attributes = std::move($1); + extra->udp = std::move(u); + } TOK_LPAREN udp_port_list TOK_RPAREN TOK_SEMICOL + udp_port_decls_opt + udp_initial_opt + udp_body + TOK_ENDPRIMITIVE opt_label { + auto mod = make_udp_module(*extra->udp); + SET_AST_NODE_LOC(mod.get(), @2, @$); + checkLabelsMatch(@13, "Primitive name", $3.get(), $13.get()); + extra->saveChild(std::move(mod)); + extra->udp.reset(); + }; + +udp_port_list: + udp_port_item | + udp_port_list TOK_COMMA udp_port_item; + +udp_port_item: + TOK_ID { + extra->udp->port_order.push_back(*$1); + } | + TOK_OUTPUT udp_output_reg_opt TOK_ID { + extra->udp->port_order.push_back(*$3); + extra->udp->output_name = *$3; + extra->udp->output_declared = true; + if ($2) + extra->udp->is_sequential = true; + } udp_init_assign_opt | + TOK_INPUT TOK_ID { + extra->udp->port_order.push_back(*$2); + extra->udp->input_names.insert(*$2); + }; + +udp_port_decls_opt: + udp_port_decls | %empty; + +udp_port_decls: + udp_port_decl | udp_port_decls udp_port_decl; + +udp_port_decl: + TOK_OUTPUT udp_output_reg_opt TOK_ID udp_init_assign_opt TOK_SEMICOL { + extra->udp->output_name = *$3; + extra->udp->output_declared = true; + if ($2) + extra->udp->is_sequential = true; + } | + TOK_INPUT udp_input_id_list TOK_SEMICOL | + TOK_REG TOK_ID TOK_SEMICOL { + extra->udp->is_sequential = true; + }; + +udp_input_id_list: + TOK_ID { + extra->udp->input_names.insert(*$1); + } | + udp_input_id_list TOK_COMMA TOK_ID { + extra->udp->input_names.insert(*$3); + }; + +udp_output_reg_opt: + TOK_REG { $$ = true; } | + %empty { $$ = false; }; + +udp_init_assign_opt: + TOK_EQ expr { + extra->udp->initial_val = std::move($2); + } | + %empty; + +udp_initial_opt: + TOK_INITIAL TOK_ID TOK_EQ expr TOK_SEMICOL { + extra->udp->initial_val = std::move($4); + } | + %empty; + +udp_body: + TOK_TABLE udp_entries TOK_ENDTABLE; + +udp_entries: + udp_entry | udp_entries udp_entry; + +udp_entry: + udp_input_list TOK_COL TOK_UDP_VALUE udp_entry_tail TOK_SEMICOL { + UdpTableRow row; + row.loc = @1; + row.inputs = std::move(extra->udp->scratch_inputs); + extra->udp->scratch_inputs.clear(); + if ($4) { + row.current = *$3; + row.output = *$4; + extra->udp->table_is_sequential = true; + } else { + row.output = *$3; + } + extra->udp->rows.push_back(std::move(row)); + }; + +udp_entry_tail: + TOK_COL TOK_UDP_VALUE { $$ = std::move($2); } | + %empty { $$ = nullptr; }; + +udp_input_list: + TOK_UDP_VALUE { + extra->udp->scratch_inputs.push_back(*$1); + } | + udp_input_list TOK_UDP_VALUE { + extra->udp->scratch_inputs.push_back(*$2); + }; + module_para_opt: TOK_HASH TOK_LPAREN module_para_list TOK_RPAREN | %empty; @@ -1656,6 +1781,7 @@ module_path_primary: | TOK_ID // Deviate from specification: Normally string would not be allowed, however they are necessary for the ecp5 tests | TOK_STRING + | TOK_LPAREN module_path_expression TOK_RPAREN // | module_path_concatenation // | module_path_multiple_concatenation // | function_subroutine_call @@ -1723,10 +1849,15 @@ system_timing_arg : TOK_NEGEDGE ignspec_id | ignspec_expr ; +// Arguments may be omitted (e.g. the optional notifier/condition slots of +// $setuphold), which appears as empty fields between commas. +opt_system_timing_arg : + system_timing_arg | %empty ; + system_timing_args : - system_timing_arg | + opt_system_timing_arg | system_timing_args TOK_IGNORED_SPECIFY_AND system_timing_arg | - system_timing_args TOK_COMMA system_timing_arg ; + system_timing_args TOK_COMMA opt_system_timing_arg ; // for the time being this is OK, but we may write our own expr here. // as I'm not sure it is legal to use a full expr here (probably not) @@ -2314,6 +2445,17 @@ single_cell_no_array: log_assert(extra->cell_hack); SET_AST_NODE_LOC(extra->cell_hack, @1, @$); extra->cell_hack = nullptr; + } | + /* no instance name: gate/UDP-style instantiation (IEEE 1364-2005 7.1, 8.6) */ { + extra->astbuf2 = extra->astbuf1->clone(); + if (extra->astbuf2->type != AST_PRIMITIVE) + extra->astbuf2->str = stringf("$cell$%d", autoidx++); + extra->cell_hack = extra->astbuf2.get(); + extra->ast_stack.back()->children.push_back(std::move(extra->astbuf2)); + } TOK_LPAREN cell_port_list TOK_RPAREN { + log_assert(extra->cell_hack); + SET_AST_NODE_LOC(extra->cell_hack, @1, @$); + extra->cell_hack = nullptr; } single_cell_arraylist: @@ -2339,18 +2481,9 @@ prim_list: prim_list TOK_COMMA single_prim; single_prim: - single_cell | - /* no name */ { - extra->astbuf2 = extra->astbuf1->clone(); - log_assert(!extra->cell_hack); - extra->cell_hack = extra->astbuf2.get(); - // TODO optimize again - extra->ast_stack.back()->children.push_back(std::move(extra->astbuf2)); - } TOK_LPAREN cell_port_list TOK_RPAREN { - log_assert(extra->cell_hack); - SET_AST_NODE_LOC(extra->cell_hack, @1, @$); - extra->cell_hack = nullptr; - } + // single_cell now also covers the no-instance-name form (for both gate + // primitives and UDP/module instantiations). + single_cell cell_parameter_list_opt: TOK_HASH TOK_LPAREN cell_parameter_list TOK_RPAREN | %empty; diff --git a/frontends/verilog/verilog_udp.cc b/frontends/verilog/verilog_udp.cc new file mode 100644 index 000000000..8c2e63b52 --- /dev/null +++ b/frontends/verilog/verilog_udp.cc @@ -0,0 +1,561 @@ +/* + * yosys -- Yosys Open SYnthesis Suite + * + * Copyright (C) 2026 Remy Goldschmidt + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * --- + * + * Lowering of Verilog user-defined primitives (UDPs, IEEE 1364-2005 clause 8) + * into ordinary Yosys netlists. + * + * A UDP is an event-driven, four-valued state machine: on every change of an + * input it re-evaluates its state table using the new input values, the + * *previous* input values (to recognise transitions), and the current output + * state. We reproduce exactly that behaviour with a small netlist: + * + * assign out = G(in, prev_in, state); // combinational table evaluation + * prev_in <= in; // $ff: previous input vector + * state <= out; // $ff: previous output / state + * + * The two registers are `$ff` cells (formal/simulation flip-flops with an + * implicit global clock that advances once per simulation event). Because + * `out` is purely combinational it reflects the result of the current event + * immediately, while the registers lag by one event and therefore hold the + * "previous" values that the next evaluation needs. Under `yosys sim` (which + * advances the global clock once per input change) this matches Icarus + * Verilog's native UDP simulation bit-for-bit, including all x behaviour. + * + * The table is matched with case-equality (`$eqx`/`$nex`) so that the four + * values 0/1/x are distinguished exactly; z inputs are treated as x by the + * surrounding logic just as the standard requires. + */ + +#include "frontends/verilog/verilog_udp.h" +#include "frontends/verilog/verilog_frontend.h" +#include "kernel/log.h" + +YOSYS_NAMESPACE_BEGIN +using namespace AST; + +namespace VERILOG_FRONTEND { + +namespace { + +using ast_t = std::unique_ptr; + +struct UdpLower { + UdpParseData &udp; + AstSrcLocType loc; + + int n_in = 0; + std::vector in_names; // input port names, header order + std::vector sig_names; // z->x mapped input signals (used for matching) + std::string out_name; + + AstNode *mod = nullptr; // module being built (for adding wires/cells) + AstNode *out_wire = nullptr; + int autoidx = 0; + + UdpLower(UdpParseData &udp) : udp(udp), loc(udp.loc) {} + + [[noreturn]] void error(const std::string &msg) + { + std::string fn = loc.begin.filename ? *loc.begin.filename : std::string(""); + const char *nm = udp.name.empty() ? "" : udp.name.c_str() + (udp.name[0] == '\\' ? 1 : 0); + log_file_error(fn, loc.begin.line, "UDP `%s': %s\n", nm, msg.c_str()); + } + + // --- tiny AST builders --------------------------------------------------- + ast_t N(AstNodeType t) { return std::make_unique(loc, t); } + ast_t N(AstNodeType t, ast_t a) { return std::make_unique(loc, t, std::move(a)); } + ast_t N(AstNodeType t, ast_t a, ast_t b) { return std::make_unique(loc, t, std::move(a), std::move(b)); } + ast_t N(AstNodeType t, ast_t a, ast_t b, ast_t c) { return std::make_unique(loc, t, std::move(a), std::move(b), std::move(c)); } + + ast_t mk_id(const std::string &name) { auto n = N(AST_IDENTIFIER); n->str = name; return n; } + ast_t mk_const(RTLIL::State s) { return AstNode::mkconst_bits(loc, std::vector{s}, false); } + + ast_t mk_and(ast_t a, ast_t b) + { + if (!a) return b; + if (!b) return a; + return N(AST_BIT_AND, std::move(a), std::move(b)); + } + ast_t mk_or(ast_t a, ast_t b) + { + if (!a) return b; + if (!b) return a; + return N(AST_BIT_OR, std::move(a), std::move(b)); + } + // case-equality: (sig === value), always a definite 0/1 + ast_t mk_eqx(ast_t sig, RTLIL::State value) { return N(AST_EQX, std::move(sig), mk_const(value)); } + // case-inequality of two one-bit signals: (a !== b) + ast_t mk_nex(ast_t a, ast_t b) { return N(AST_NEX, std::move(a), std::move(b)); } + + // --- symbol helpers ------------------------------------------------------ + // One-bit expression that is true iff `sig` matches the level symbol. + // Returns null for the unconstrained symbols '?' (and 'b' is expanded). + ast_t level_match(char sym, ast_t sig) + { + switch (tolower(sym)) { + case '0': return mk_eqx(std::move(sig), RTLIL::State::S0); + case '1': return mk_eqx(std::move(sig), RTLIL::State::S1); + case 'x': return mk_eqx(std::move(sig), RTLIL::State::Sx); + case '?': return nullptr; // matches anything + case 'b': { + // 0 or 1 (not x) + auto a = mk_eqx(sig->clone(), RTLIL::State::S0); + auto b = mk_eqx(std::move(sig), RTLIL::State::S1); + return mk_or(std::move(a), std::move(b)); + } + default: + error(stringf("unexpected level symbol `%c' in table", sym)); + } + } + + static bool is_edge_symbol(const std::string &s) + { + if (s.size() >= 1 && s.front() == '(') + return true; + if (s.size() == 1) { + char c = tolower(s[0]); + return c == 'r' || c == 'f' || c == 'p' || c == 'n' || c == '*'; + } + return false; + } + + // One-bit expression that is true iff the transition (piv -> in) matches the + // edge symbol. Both operands are one-bit signals. + ast_t edge_match(const std::string &sym, const std::string &piv, const std::string &in) + { + std::vector> pairs; + if (!sym.empty() && sym.front() == '(') { + if (sym.size() != 4 || sym.back() != ')') + error(stringf("malformed edge specifier `%s' in table", sym.c_str())); + pairs.emplace_back(sym[1], sym[2]); + } else { + switch (tolower(sym[0])) { + case '*': pairs.emplace_back('?', '?'); break; + case 'r': pairs.emplace_back('0', '1'); break; + case 'f': pairs.emplace_back('1', '0'); break; + case 'p': pairs = {{'0','1'},{'0','x'},{'x','1'}}; break; + case 'n': pairs = {{'1','0'},{'1','x'},{'x','0'}}; break; + default: error(stringf("unexpected edge symbol `%s' in table", sym.c_str())); + } + } + ast_t acc; + for (auto &p : pairs) { + ast_t m = mk_and(level_match(p.first, mk_id(piv)), level_match(p.second, mk_id(in))); + // an edge is always a change of value + m = mk_and(std::move(m), mk_nex(mk_id(piv), mk_id(in))); + acc = mk_or(std::move(acc), std::move(m)); + } + return acc; + } + + // --- row matching -------------------------------------------------------- + // `piv_names` is empty for a combinational table (no edges, no previous + // inputs). `state_name` is empty for a combinational table. + ast_t row_match(const UdpTableRow &row, const std::vector &piv_names, + const std::string &state_name) + { + ast_t acc; + int edge_count = 0; + for (int i = 0; i < n_in; i++) { + const std::string &sym = row.inputs[i]; + if (is_edge_symbol(sym)) { + if (piv_names.empty()) + error("input transitions are only allowed in sequential UDPs"); + if (++edge_count > 1) + error("a UDP table row may contain at most one input transition"); + acc = mk_and(std::move(acc), edge_match(sym, piv_names[i], sig_names[i])); + } else { + acc = mk_and(std::move(acc), level_match(sym[0], mk_id(sig_names[i]))); + } + } + if (!row.current.empty()) { + if (is_edge_symbol(row.current)) + error("the current-state field of a UDP row must be a level"); + acc = mk_and(std::move(acc), level_match(row.current[0], mk_id(state_name))); + } + return acc; // null means "matches unconditionally" + } + + bool row_has_edge(const UdpTableRow &row) + { + for (auto &sym : row.inputs) + if (is_edge_symbol(sym)) + return true; + return false; + } + + char row_output(const UdpTableRow &row) + { + char c = row.output.empty() ? '?' : tolower(row.output[0]); + if (c != '0' && c != '1' && c != 'x' && c != '-') + error(stringf("unexpected output symbol `%s' in table", row.output.c_str())); + return c; + } + + // --- determinism check --------------------------------------------------- + // The set of values {0,1,x} (bits 0,1,2) a level symbol can match. + static unsigned level_set(char c) + { + switch (tolower(c)) { + case '0': return 1; + case '1': return 2; + case 'x': return 4; + case 'b': return 3; + case '?': return 7; + default: return 7; // empty current-state field: unconstrained + } + } + + static std::vector> edge_pairs(const std::string &s) + { + if (!s.empty() && s.front() == '(') + return {{s[1], s[2]}}; + switch (tolower(s.empty() ? '?' : s[0])) { + case '*': return {{'?', '?'}}; + case 'r': return {{'0', '1'}}; + case 'f': return {{'1', '0'}}; + case 'p': return {{'0','1'},{'0','x'},{'x','1'}}; + case 'n': return {{'1','0'},{'1','x'},{'x','0'}}; + } + return {}; + } + + // The set of (from,to) transitions an edge symbol can match, encoded as a + // bitmask over from*3+to (with from != to). + static unsigned edge_set(const std::string &s) + { + unsigned m = 0; + for (auto &p : edge_pairs(s)) { + unsigned fs = level_set(p.first), ts = level_set(p.second); + for (int f = 0; f < 3; f++) + for (int t = 0; t < 3; t++) + if ((fs >> f & 1) && (ts >> t & 1) && f != t) + m |= 1u << (f * 3 + t); + } + return m; + } + + int edge_pos(const UdpTableRow &row) + { + int pos = -1; + for (int i = 0; i < n_in; i++) + if (is_edge_symbol(row.inputs[i])) { + if (pos >= 0) { + loc = row.loc; + error("a UDP table row may contain at most one input transition"); + } + pos = i; + } + return pos; + } + + // Reject tables whose behaviour is not uniquely determined: two rows that + // can match the same evaluation (same input values / transition and current + // state) but specify different definite outputs. `x` outputs are + // don't-cares and never conflict; level-sensitive rows are allowed to + // override edge-sensitive rows (IEEE 1364-2005 8.7), so only same-kind rows + // are compared. + void check_determinism() + { + for (size_t a = 0; a < udp.rows.size(); a++) + for (size_t b = a + 1; b < udp.rows.size(); b++) { + const UdpTableRow &A = udp.rows[a], &B = udp.rows[b]; + char oa = row_output(A), ob = row_output(B); + if (oa == ob || oa == 'x' || ob == 'x') + continue; + int ea = edge_pos(A), eb = edge_pos(B); + if (ea != eb) + continue; // level-vs-edge (defined), or edges on different inputs + bool overlap = true; + for (int i = 0; i < n_in && overlap; i++) { + if (i == ea) + overlap = (edge_set(A.inputs[i]) & edge_set(B.inputs[i])) != 0; + else + overlap = (level_set(A.inputs[i][0]) & level_set(B.inputs[i][0])) != 0; + } + if (overlap && !A.current.empty() && !B.current.empty()) + overlap = (level_set(A.current[0]) & level_set(B.current[0])) != 0; + if (overlap) { + loc = A.loc; + error(stringf("non-deterministic state table: the rows at lines %d and %d " + "match the same input combination but specify different " + "outputs (`%c' and `%c')", + A.loc.begin.line, B.loc.begin.line, oa, ob)); + } + } + } + + // OR of the match expressions of all rows whose output symbol is `want`. + // Returns null for an empty bucket. Matches Icarus Verilog, which groups + // table rows by output value rather than by table order. + ast_t bucket_match(const std::vector &rows, char want, + const std::vector &piv_names, const std::string &state_name) + { + ast_t acc; + for (auto *row : rows) { + if (row_output(*row) != want) + continue; + ast_t m = row_match(*row, piv_names, state_name); + if (!m) + m = AstNode::mkconst_int(loc, 1, false, 1); // unconditional match + acc = mk_or(std::move(acc), std::move(m)); + } + return acc; + } + + // `cond ? then_expr : else_expr`, dropping the choice when the bucket is + // empty (cond is null). + ast_t sel(ast_t cond, ast_t then_expr, ast_t else_expr) + { + if (!cond) + return else_expr; + return N(AST_TERNARY, std::move(cond), std::move(then_expr), std::move(else_expr)); + } + + // --- netlist construction helpers --------------------------------------- + AstNode *add_wire(const std::string &name, bool is_input, bool is_output, int port_id) + { + auto w = N(AST_WIRE); + w->str = name; + w->is_input = is_input; + w->is_output = is_output; + if (port_id) + w->port_id = port_id; + AstNode *ptr = w.get(); + mod->children.push_back(std::move(w)); + return ptr; + } + + // A $ff (implicit global clock flip-flop): q <= d on every simulation event. + void add_ff(ast_t d_expr, const std::string &q_name, bool have_init, RTLIL::State init) + { + AstNode *q = add_wire(q_name, false, false, 0); + if (have_init) + q->set_attribute(ID::init, AstNode::mkconst_bits(loc, std::vector{init}, false)); + + auto cell = N(AST_CELL); + cell->str = stringf("$udp$ff$%d", autoidx++); + auto celltype = N(AST_CELLTYPE); + celltype->str = "$ff"; + cell->children.push_back(std::move(celltype)); + + auto para = N(AST_PARASET); + para->str = "\\WIDTH"; + para->children.push_back(AstNode::mkconst_int(loc, 1, false, 32)); + cell->children.push_back(std::move(para)); + + auto arg_d = N(AST_ARGUMENT); + arg_d->str = "\\D"; + arg_d->children.push_back(std::move(d_expr)); + cell->children.push_back(std::move(arg_d)); + + auto arg_q = N(AST_ARGUMENT); + arg_q->str = "\\Q"; + arg_q->children.push_back(mk_id(q_name)); + cell->children.push_back(std::move(arg_q)); + + mod->children.push_back(std::move(cell)); + } + + // --- entry point --------------------------------------------------------- + std::unique_ptr run() + { + // Resolve and validate ports. + if (udp.port_order.size() < 2) + error("a UDP must have an output and at least one input"); + if (!udp.output_declared) + error("missing output port declaration"); + out_name = udp.output_name; + if (udp.port_order[0] != out_name) + error("the output port must be the first port in the port list"); + for (size_t i = 1; i < udp.port_order.size(); i++) { + if (!udp.input_names.count(udp.port_order[i])) + error(stringf("port `%s' is missing an input declaration", + udp.port_order[i].c_str() + 1)); + in_names.push_back(udp.port_order[i]); + } + n_in = GetSize(in_names); + + for (auto &row : udp.rows) { + if (GetSize(row.inputs) != n_in) { + loc = row.loc; + error(stringf("table row has %d input fields but the UDP has %d inputs", + GetSize(row.inputs), n_in)); + } + } + if (udp.is_sequential && !udp.table_is_sequential) + error("sequential UDP has a combinational state table"); + if (!udp.is_sequential && udp.table_is_sequential) + error("combinational UDP has a sequential state table"); + if (udp.rows.empty()) + error("empty state table"); + + check_determinism(); + loc = udp.loc; + + // Build the module skeleton. + auto module = N(AST_MODULE); + module->str = udp.name; + if (udp.attributes) + for (auto &it : *udp.attributes) + module->attributes[it.first] = std::move(it.second); + module->set_attribute(ID(udp), AstNode::mkconst_int(loc, 1, false)); + mod = module.get(); + + int port_id = 1; + out_wire = add_wire(out_name, false, true, port_id++); + for (auto &in : in_names) + add_wire(in, true, false, port_id++); + + // "The z values passed to UDP inputs shall be treated the same as x + // values." Map each input through (in === 1'bz) ? 1'bx : in and use the + // mapped signal everywhere the table is matched. (The constants also + // force these cells to be evaluated in the first simulation step.) + for (int i = 0; i < n_in; i++) { + std::string xn = stringf("$udp$zx%d", i); + add_wire(xn, false, false, 0); + ast_t map = N(AST_TERNARY, + mk_eqx(mk_id(in_names[i]), RTLIL::State::Sz), + mk_const(RTLIL::State::Sx), mk_id(in_names[i])); + mod->children.push_back(N(AST_ASSIGN, mk_id(xn), std::move(map))); + sig_names.push_back(xn); + } + + if (!udp.is_sequential) + build_combinational(); + else + build_sequential(); + + mod = nullptr; + return module; + } + + void build_combinational() + { + // out = 0 if any 0-row matches, else 1 if any 1-row matches, else x. + // (x-output rows do not force x; they simply are not 0- or 1-rows.) + std::vector rows; + for (auto &row : udp.rows) { + if (row_has_edge(row)) + error("combinational UDPs may not contain input transitions"); + if (row_output(row) == '-') + error("`-' (no change) is only allowed in sequential UDPs"); + rows.push_back(&row); + } + ast_t expr = sel(bucket_match(rows, '0', {}, ""), mk_const(RTLIL::State::S0), + sel(bucket_match(rows, '1', {}, ""), mk_const(RTLIL::State::S1), + mk_const(RTLIL::State::Sx))); + mod->children.push_back(N(AST_ASSIGN, mk_id(out_name), std::move(expr))); + } + + void build_sequential() + { + // State registers: one $ff per input for the previous input value, plus + // one $ff for the previous output (the current state). + std::vector piv_names; + for (int i = 0; i < n_in; i++) + piv_names.push_back(stringf("$udp$piv%d", i)); + std::string state_name = "$udp$state"; + + // out = G(in, prev_in, state): + // level rows (priority) override edge rows (priority); if nothing + // matches, hold the state when no input changed, else x. + std::vector level_rows, edge_rows; + for (auto &row : udp.rows) { + if (row_has_edge(row)) + edge_rows.push_back(&row); + else + level_rows.push_back(&row); + } + + // A UDP only re-evaluates its table when one of its inputs *changes*; + // otherwise the output holds. So: + // out = event ? table(in, prev_in, state) : state + // where event = (in !== prev_in). The constant bit padded onto each + // side forces this comparison (and hence the whole output) to be + // evaluated in the very first simulation step, when all inputs are still + // x and would otherwise never be re-evaluated; there the event is false, + // so the output correctly holds the initial state. + auto in_concat = N(AST_CONCAT); + auto piv_concat = N(AST_CONCAT); + for (int i = 0; i < n_in; i++) { + in_concat->children.push_back(mk_id(sig_names[i])); + piv_concat->children.push_back(mk_id(piv_names[i])); + } + in_concat->children.push_back(mk_const(RTLIL::State::S0)); + piv_concat->children.push_back(mk_const(RTLIL::State::S0)); + ast_t event = N(AST_NEX, std::move(in_concat), std::move(piv_concat)); + + // table(): rows are tried by output value in a fixed priority that + // matches Icarus Verilog's evaluator (vvp/udp.cc): + // level 0 > level 1 > level x > level hold > edge 0 > edge 1 > edge hold > x + // Level entries dominate edge entries; an edge entry on an input that did + // not transition cannot match (handled inside the per-row match). + auto S = [&](RTLIL::State s) { return mk_const(s); }; + auto St = [&]() { return mk_id(state_name); }; + ast_t table = + sel(bucket_match(level_rows, '0', piv_names, state_name), S(RTLIL::State::S0), + sel(bucket_match(level_rows, '1', piv_names, state_name), S(RTLIL::State::S1), + sel(bucket_match(level_rows, 'x', piv_names, state_name), S(RTLIL::State::Sx), + sel(bucket_match(level_rows, '-', piv_names, state_name), St(), + sel(bucket_match(edge_rows, '0', piv_names, state_name), S(RTLIL::State::S0), + sel(bucket_match(edge_rows, '1', piv_names, state_name), S(RTLIL::State::S1), + sel(bucket_match(edge_rows, '-', piv_names, state_name), St(), + S(RTLIL::State::Sx)))))))); + ast_t out_expr = N(AST_TERNARY, std::move(event), std::move(table), mk_id(state_name)); + mod->children.push_back(N(AST_ASSIGN, mk_id(out_name), std::move(out_expr))); + + // The output is combinational; also give it the initial value so the + // very first sampled output is correct even before it is evaluated. + RTLIL::State init = udp_init_state(); + out_wire->set_attribute(ID::init, AstNode::mkconst_bits(loc, std::vector{init}, false)); + + // previous-input registers (store the z->x mapped values) + for (int i = 0; i < n_in; i++) + add_ff(mk_id(sig_names[i]), piv_names[i], /*have_init=*/true, RTLIL::State::Sx); + + // state register (previous output) + add_ff(mk_id(out_name), state_name, /*have_init=*/true, init); + } + + // Reduce the UDP's initial-value expression to a single 4-state bit. + // `initial q = ;` is restricted by the standard to 1'b0, 1'b1, + // 1'bx, 1, or 0, all of which the front-end parses to an AST_CONSTANT. + RTLIL::State udp_init_state() + { + if (!udp.initial_val) + return RTLIL::State::Sx; + AstNode *e = udp.initial_val.get(); + if (e->type != AST_CONSTANT || e->bits.empty()) + error("initial value must be a constant 1'b0, 1'b1 or 1'bx"); + return e->bits.front(); + } +}; + +} // anonymous namespace + +std::unique_ptr make_udp_module(UdpParseData &udp) +{ + UdpLower lower(udp); + return lower.run(); +} + +} // namespace VERILOG_FRONTEND + +YOSYS_NAMESPACE_END diff --git a/frontends/verilog/verilog_udp.h b/frontends/verilog/verilog_udp.h new file mode 100644 index 000000000..9d2c444c9 --- /dev/null +++ b/frontends/verilog/verilog_udp.h @@ -0,0 +1,79 @@ +/* + * yosys -- Yosys Open SYnthesis Suite + * + * Copyright (C) 2026 Remy Goldschmidt + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * --- + * + * Support for Verilog user-defined primitives (UDPs), IEEE 1364-2005 + * clause 8. The Verilog parser collects a UDP definition into a + * UdpParseData structure; make_udp_module() then lowers it into an + * ordinary behavioural AST module that the rest of Yosys synthesises + * using the existing FF/latch inference passes. + * + */ + +#ifndef VERILOG_UDP_H +#define VERILOG_UDP_H + +#include "kernel/yosys.h" +#include "frontends/ast/ast.h" + +YOSYS_NAMESPACE_BEGIN + +namespace VERILOG_FRONTEND +{ + // A single row of the UDP state table. The input symbols are stored in + // port-list order (i.e. matching the order of the input ports in the UDP + // header, not the order of the input declarations). + struct UdpTableRow { + std::vector inputs; // one raw symbol per input port + std::string current; // current-state symbol ("" if combinational) + std::string output; // output / next-state symbol + AST::AstSrcLocType loc; + }; + + // Everything the parser collects while reading a `primitive ... endprimitive` + // block. Consumed by make_udp_module(). + struct UdpParseData { + AST::AstSrcLocType loc; + std::string name; // UDP name (escaped, e.g. "\\foo") + + std::vector port_order; // port names in header order; [0] is the output + std::string output_name; // name of the (single) output port + pool input_names; // names declared as inputs + bool output_declared = false; + bool is_sequential = false; // true once a reg declaration is seen + + std::unique_ptr initial_val; // optional initial value expression (or null) + + std::unique_ptr>> attributes; + + std::vector rows; + bool table_is_sequential = false; // table rows carry a current-state field + + // scratch used while accumulating the input fields of the row currently + // being parsed + std::vector scratch_inputs; + }; + + // Lower a parsed UDP into a behavioural AST_MODULE. Reports user errors via + // log_file_error(). Never returns null. + std::unique_ptr make_udp_module(UdpParseData &udp); +} + +YOSYS_NAMESPACE_END + +#endif diff --git a/tests/verilog/udp_cosim.py b/tests/verilog/udp_cosim.py new file mode 100644 index 000000000..9150e4250 --- /dev/null +++ b/tests/verilog/udp_cosim.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# Differential check of Yosys's UDP lowering against Icarus Verilog. +# +# For the `primitive` in the given .v file, drive random single-input changes +# (4-state: 0/1/x/z), simulate with iverilog (which evaluates the UDP natively) +# and with `yosys sim` (which simulates the lowered netlist), and assert that the +# two output traces are equal under multi-valued logic. +# +# Usage: udp_cosim.py [num_stimuli] [steps] +# Requires: iverilog, vvp, python3, and $YOSYS pointing at the yosys binary. + +import os, sys, re, subprocess, random, tempfile + +YOSYS = os.environ.get("YOSYS", "yosys") + +def sh(cmd): + return subprocess.run(cmd, shell=True, capture_output=True, text=True) + +def parse_primitive(path): + txt = open(path).read() + m = re.search(r'primitive\s+(\w+)\s*\(([^)]*)\)', txt) + ports = [re.sub(r'\b(output|input|reg)\b', '', p).strip() + for p in m.group(2).split(',')] + return m.group(1), ports[0], ports[1:] + +def gen_tb(name, out, ins, stim): + decl = "\n".join(" reg %s;" % s for s in ins) + inst = ", ".join([out] + ins) + body = "\n".join(" #10 %s = 1'b%s;" % (ins[i], v) for (i, v) in stim) + return ("module tb;\n%s\n wire %s;\n %s dut(%s);\n" + " initial begin\n $dumpfile(\"wave\");\n $dumpvars(0, tb);\n" + "%s\n #10 $finish;\n end\nendmodule\n" + % (decl, out, name, inst, body)) + +def trace(vcd, signame): + sid = None + for line in vcd.splitlines(): + m = re.match(r'\$var\s+\w+\s+1\s+(\S+)\s+(\S+)', line) + if m and m.group(2) == signame: + sid = m.group(1); break + if sid is None: + return None + t, out = 0, [] + for line in vcd.splitlines(): + line = line.strip() + if line.startswith('#'): + t = int(line[1:]) + elif len(line) >= 2 and line[0] in '01xz' and line[1:] == sid: + out.append((t, line[0])) + else: + mm = re.match(r'b(\S+)\s+(\S+)', line) + if mm and mm.group(2) == sid: + out.append((t, mm.group(1)[-1])) + return out + +def value_at(tr, t): + v = 'x' + for (tt, vv) in tr: + if tt <= t: v = vv + else: break + return v + +def run_one(udp_file, name, out, ins, stim): + with tempfile.TemporaryDirectory() as wd: + open(os.path.join(wd, "tb.v"), "w").write(gen_tb(name, out, ins, stim)) + if sh("iverilog -o %s/sim.vvp %s %s/tb.v" % (wd, udp_file, wd)).returncode != 0: + return None # iverilog rejected this table; skip + sh("cd %s && vvp sim.vvp -fst >/dev/null 2>&1" % wd) + sh("cd %s && vvp sim.vvp >/dev/null 2>&1" % wd) + ref = trace(open("%s/wave.vcd" % wd).read(), out) + r = sh("%s -q -p 'read_verilog %s; sim -r %s/wave.fst -scope tb -vcd %s/y.vcd'" + % (YOSYS, udp_file, wd, wd)) + if r.returncode != 0: + raise SystemExit("yosys sim failed:\n" + r.stdout + r.stderr) + mod = trace(open("%s/y.vcd" % wd).read(), out) + end = 10 * (len(stim) + 1) + ts = sorted(set([t for t, _ in ref] + [t for t, _ in mod] + list(range(0, end + 1, 10)))) + return [(t, value_at(ref, t), value_at(mod, t)) for t in ts + if value_at(ref, t) != value_at(mod, t)] + +if __name__ == "__main__": + udp_file = sys.argv[1] + nstim = int(sys.argv[2]) if len(sys.argv) > 2 else 20 + steps = int(sys.argv[3]) if len(sys.argv) > 3 else 60 + name, out, ins = parse_primitive(udp_file) + tested = 0 + for seed in range(nstim): + rnd = random.Random(seed * 1009 + 1) + vals4 = ['0', '1', 'x', 'z'] + stim = [(rnd.randrange(len(ins)), + rnd.choice(vals4) if rnd.random() < 0.3 else rnd.choice('01')) + for _ in range(steps)] + mm = run_one(udp_file, name, out, ins, stim) + if mm is None: + continue + tested += 1 + if mm: + print("MISMATCH in %s (seed %d):" % (name, seed)) + for (t, a, b) in mm[:12]: + print(" t=%d iverilog=%s yosys=%s" % (t, a, b)) + print("stimulus:", stim) + raise SystemExit(1) + if tested == 0: + raise SystemExit("iverilog rejected %s for all stimuli" % name) + print("OK %s (%d stimuli matched)" % (name, tested)) diff --git a/tests/verilog/udp_noname_inst.ys b/tests/verilog/udp_noname_inst.ys new file mode 100644 index 000000000..4bfcf6768 --- /dev/null +++ b/tests/verilog/udp_noname_inst.ys @@ -0,0 +1,15 @@ +read_verilog </dev/null 2>&1; then + echo "skipping udp_sim.sh: iverilog not found" + exit 0 +fi + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +# 8.2 combinational multiplexer +cat > "$WORK/mux.v" <<'EOF' +primitive multiplexer (mux, control, dataA, dataB); +output mux; input control, dataA, dataB; +table +// c a b : mux + 0 1 ? : 1 ; + 0 0 ? : 0 ; + 1 ? 1 : 1 ; + 1 ? 0 : 0 ; + x 0 0 : 0 ; + x 1 1 : 1 ; +endtable +endprimitive +EOF + +# 8.3 level-sensitive latch +cat > "$WORK/latch.v" <<'EOF' +primitive latch (q, clock, data); +output q; reg q; input clock, data; +table +// clk data : q : q+ + 0 1 : ? : 1 ; + 0 0 : ? : 0 ; + 1 ? : ? : - ; +endtable +endprimitive +EOF + +# 8.4 rising-edge D flip-flop +cat > "$WORK/dff.v" <<'EOF' +primitive d_edge_ff (q, clock, data); +output q; reg q; input clock, data; +table +// clk data : q : q+ + (01) 0 : ? : 0 ; + (01) 1 : ? : 1 ; + (0?) 1 : 1 : 1 ; + (0?) 0 : 0 : 0 ; + (?0) ? : ? : - ; + ? (??): ? : - ; +endtable +endprimitive +EOF + +# 8.5 D flip-flop with an initial value +cat > "$WORK/dff1.v" <<'EOF' +primitive dff1 (q, clk, d); +input clk, d; output q; reg q; +initial q = 1'b1; +table + r 0 : ? : 0 ; + r 1 : ? : 1 ; + f ? : ? : - ; + ? * : ? : - ; +endtable +endprimitive +EOF + +# 8.5 set/reset flip-flop with an initial value +cat > "$WORK/srff.v" <<'EOF' +primitive srff (q, s, r); +output q; reg q; input s, r; +initial q = 1'b1; +table + 1 0 : ? : 1 ; + f 0 : 1 : - ; + 0 r : ? : 0 ; + 0 f : 0 : - ; + 1 1 : ? : 0 ; +endtable +endprimitive +EOF + +# 8.7 edge-triggered JK flip-flop with level-sensitive preset/clear +cat > "$WORK/jk.v" <<'EOF' +primitive jk_edge_ff (q, clock, j, k, preset, clear); +output q; reg q; input clock, j, k, preset, clear; +table + ? ?? 01 : ? : 1 ; + ? ?? *1 : 1 : 1 ; + ? ?? 10 : ? : 0 ; + ? ?? 1* : 0 : 0 ; + r 00 00 : 0 : 1 ; + r 00 11 : ? : - ; + r 01 11 : ? : 0 ; + r 10 11 : ? : 1 ; + r 11 11 : 0 : 1 ; + r 11 11 : 1 : 0 ; + f ?? ?? : ? : - ; + b *? ?? : ? : - ; + b ?* ?? : ? : - ; +endtable +endprimitive +EOF + +cat > "$WORK/dff_complex.v" <<'EOF' +primitive dff_complex (q, v, clk, d, xcr); +output q; reg q; input v, clk, d, xcr; +table + * ? ? ? : ? : x; + ? (x1) 0 0 : ? : 0; + ? (x1) 1 0 : ? : 1; + ? (x1) 0 1 : 0 : 0; + ? (x1) 1 1 : 1 : 1; + ? (x1) ? x : ? : -; + ? (bx) 0 ? : 0 : -; + ? (bx) 1 ? : 1 : -; + ? (x0) b ? : ? : -; + ? (x0) ? x : ? : -; + ? (01) 0 ? : ? : 0; + ? (01) 1 ? : ? : 1; + ? (10) ? ? : ? : -; + ? b * ? : ? : -; + ? ? ? * : ? : -; +endtable +endprimitive +EOF + +for u in mux latch dff dff1 srff jk dff_complex; do + python3 "$(dirname "$0")/udp_cosim.py" "$WORK/$u.v" 20 60 +done