Add UDP support

This commit is contained in:
Remy Goldschmidt 2026-06-06 18:17:14 -07:00
parent cc9692caab
commit 0feda723a8
No known key found for this signature in database
9 changed files with 1080 additions and 18 deletions

View File

@ -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

View File

@ -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); }
<INITIAL,BASED_CONST>"/*" { comment_caller=YY_START; BEGIN(COMMENT); }
<INITIAL,BASED_CONST,UDPTABLE>"/*" { comment_caller=YY_START; BEGIN(COMMENT); }
<COMMENT>. /* ignore comment body */
<COMMENT>\n /* ignore comment body */
<COMMENT>"*/" { BEGIN(comment_caller); }
<INITIAL,BASED_CONST>[ \t\r\n] /* ignore whitespaces */
<INITIAL,BASED_CONST,UDPTABLE>[ \t\r\n] /* ignore whitespaces */
<INITIAL,BASED_CONST>\\[\r\n] /* ignore continuation sequence */
<INITIAL,BASED_CONST>"//"[^\r\n]* /* ignore one-line comments */
<INITIAL,BASED_CONST,UDPTABLE>"//"[^\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. */
<UDPTABLE>"endtable" { BEGIN(0); return parser::make_TOK_ENDTABLE(out_loc); }
<UDPTABLE>\([01xXbB?][01xXbB?]\) {
string_t val = std::make_unique<std::string>(YYText());
return parser::make_TOK_UDP_VALUE(std::move(val), out_loc);
}
<UDPTABLE>[01xXbB?rRfFpPnN*\-] {
string_t val = std::make_unique<std::string>(YYText());
return parser::make_TOK_UDP_VALUE(std::move(val), out_loc);
}
<UDPTABLE>[:;] { return char_tok(*YYText(), out_loc); }
<INITIAL>. { return char_tok(*YYText(), out_loc); }
<*>. { BEGIN(0); return char_tok(*YYText(), out_loc); }

View File

@ -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<UdpParseData> udp;
AstNode* saveChild(std::unique_ptr<AstNode> child);
AstNode* pushChild(std::unique_ptr<AstNode> child);
@ -429,6 +431,7 @@
#include <string>
#include <memory>
#include "frontends/verilog/verilog_frontend.h"
#include "frontends/verilog/verilog_udp.h"
struct specify_target {
char polarity_op;
@ -487,6 +490,8 @@
%token <string_t> TOK_SVA_LABEL TOK_SPECIFY_OPER TOK_MSG_TASKS
%token <string_t> TOK_BASE TOK_BASED_CONSTVAL TOK_UNBASED_UNSIZED_CONSTVAL
%token <string_t> TOK_USER_TYPE TOK_PKG_USER_TYPE
%token <string_t> 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 <string_t> opt_label opt_sva_label tok_prim_wrapper hierarchical_id hierarchical_type_id integral_number
%type <string_t> type_name
%type <ast_t> opt_enum_init enum_type struct_type enum_struct_type func_return_type typedef_base_type
%type <boolean_t> opt_property always_comb_or_latch always_or_always_ff
%type <boolean_t> opt_property always_comb_or_latch always_or_always_ff udp_output_reg_opt
%type <string_t> udp_entry_tail
%type <boolean_t> opt_signedness_default_signed opt_signedness_default_unsigned
%type <integer_t> integer_atom_type integer_vector_type
%type <al_t> 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<UdpParseData>();
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;

View File

@ -0,0 +1,561 @@
/*
* yosys -- Yosys Open SYnthesis Suite
*
* Copyright (C) 2026 Remy Goldschmidt <taktoa@gmail.com>
*
* 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<AstNode>;
struct UdpLower {
UdpParseData &udp;
AstSrcLocType loc;
int n_in = 0;
std::vector<std::string> in_names; // input port names, header order
std::vector<std::string> 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("<unknown>");
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<AstNode>(loc, t); }
ast_t N(AstNodeType t, ast_t a) { return std::make_unique<AstNode>(loc, t, std::move(a)); }
ast_t N(AstNodeType t, ast_t a, ast_t b) { return std::make_unique<AstNode>(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<AstNode>(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<RTLIL::State>{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<std::pair<char, char>> 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<std::string> &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<std::pair<char, char>> 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<const UdpTableRow*> &rows, char want,
const std::vector<std::string> &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<RTLIL::State>{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<AstNode> 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<const UdpTableRow*> 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<std::string> 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<const UdpTableRow*> 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<RTLIL::State>{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 = <init_val>;` 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<AstNode> make_udp_module(UdpParseData &udp)
{
UdpLower lower(udp);
return lower.run();
}
} // namespace VERILOG_FRONTEND
YOSYS_NAMESPACE_END

View File

@ -0,0 +1,79 @@
/*
* yosys -- Yosys Open SYnthesis Suite
*
* Copyright (C) 2026 Remy Goldschmidt <taktoa@gmail.com>
*
* 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<std::string> 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<std::string> port_order; // port names in header order; [0] is the output
std::string output_name; // name of the (single) output port
pool<std::string> input_names; // names declared as inputs
bool output_declared = false;
bool is_sequential = false; // true once a reg declaration is seen
std::unique_ptr<AST::AstNode> initial_val; // optional initial value expression (or null)
std::unique_ptr<dict<RTLIL::IdString, std::unique_ptr<AST::AstNode>>> attributes;
std::vector<UdpTableRow> 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<std::string> scratch_inputs;
};
// Lower a parsed UDP into a behavioural AST_MODULE. Reports user errors via
// log_file_error(). Never returns null.
std::unique_ptr<AST::AstNode> make_udp_module(UdpParseData &udp);
}
YOSYS_NAMESPACE_END
#endif

105
tests/verilog/udp_cosim.py Normal file
View File

@ -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 <udp.v> [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))

View File

@ -0,0 +1,15 @@
read_verilog <<EOT
primitive my_and (o, a, b);
output o; input a, b;
table
1 1 : 1 ;
0 ? : 0 ;
? 0 : 0 ;
endtable
endprimitive
module top (output o, input a, b);
my_and (o, a, b); // gate-style: no instance name (IEEE 1364-2005 8.6)
endmodule
EOT
hierarchy -top top
select -assert-count 1 top/t:my_and

View File

@ -0,0 +1,12 @@
# A UDP whose rows overlap with conflicting outputs must be rejected.
logger -expect error "non-deterministic state table" 1
read_verilog <<EOT
primitive nd (o, a, b);
output o; reg o; input a, b;
table
// a b : q : q+
1 ? : ? : 1 ;
? 1 : ? : 0 ;
endtable
endprimitive
EOT

137
tests/verilog/udp_sim.sh Executable file
View File

@ -0,0 +1,137 @@
#!/usr/bin/env bash
# Check that Yosys's lowering of Verilog user-defined primitives (UDPs)
# simulates identically to Icarus Verilog's native UDP support, under
# multi-valued (0/1/x/z) logic. Uses the UDP examples from IEEE 1364-2005
# clause 8.
set -e
if ! command -v iverilog >/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