Skip to content

alainmarcel/uhdm2rtlil

Repository files navigation

UHDM to RTLIL Frontend

CI

A Yosys frontend that enables SystemVerilog synthesis through UHDM (Universal Hardware Data Model) by converting UHDM representations to Yosys RTLIL (Register Transfer Level Intermediate Language).

Overview

This project bridges the gap between SystemVerilog source code and Yosys synthesis by leveraging two key components:

  1. Surelog - Parses SystemVerilog and generates UHDM
  2. UHDM Frontend - Converts UHDM to Yosys RTLIL

This enables full SystemVerilog synthesis capability in Yosys, including advanced features not available in Yosys's built-in Verilog frontend.

Test Suite Status

  • Total Tests: 169 tests covering comprehensive SystemVerilog features
  • Success Rate: 100% (169/169 tests functional, 0 known failures)
  • Passing: 164 tests with formal equivalence verified between UHDM and Verilog frontends
  • UHDM-Only Success: 5 tests demonstrating UHDM's superior SystemVerilog support:
    • nested_struct - Complex nested structures
    • simple_instance_array - Instance array support
    • simple_package - Package support
    • unique_case - Unique case statement support
    • gen_struct_access - Packed array of structs with field access in generate blocks
  • Recent Additions:
    • array_assign - Unpacked-array-to-array assignments (continuous and procedural), array-typed ternary expressions (out = sel ? a : b where a/b/out are unpacked arrays), multi-dimensional unpacked arrays, and $bits over unpacked arrays; ported from third_party/yosys/tests/svtypes/array_assign.sv — exposed a crash on a missing var_select element resolution that has been guarded so the test now runs cleanly
    • various_port_sign_extend - Module-port sign extension across instantiations: 1- and 2-bit signed/unsigned producer modules feed a PassThrough instance whose 4-bit input must sign-extend signed values and zero-extend unsigned ones; also exercises signed expressions (^, ~, ternary, array reads) passed through narrowing/widening port boundaries; ported from third_party/yosys/tests/various/port_sign_extend.v (the upstream ref module is renamed refmod here because ref is a reserved keyword in SystemVerilog mode)
    • various_struct_access - Nested packed-struct typedefs (3 levels) with parameter of struct type initialised from a vector literal, and a chain of localparams reading struct fields off the parameter (P.d, P.d.c.a) and off other localparams (x.c, y.b, q.c.a); ported from third_party/yosys/tests/various/struct_access.sv — required a Surelog/UHDM null-deref fix in ExprEval::decodeHierPath so localparam logic f = P.a; no longer segfaults at elaboration when the hier_path's Typespec() is null
    • struct_sizebits - SV array/range query system functions on multi-dimensional packed types: $bits, $size(arg, dim), $high/low/left/right(arg, dim), $dimensions, $increment over packed structs/unions, multi-range packed logic [a:b][c:d][e:f], hier_path member access (s.sy.y), and bit-selects on hier paths (s.sz.z[3][3]); ported from third_party/yosys/tests/svtypes/struct_sizebits.sv
    • prefix - Hierarchical references with assorted prefix forms (bare names, block-prefixed, top-prefixed, bit-selects on hier paths, c[j] dynamic bit-select on a hier path) over nested generate scopes; cross-scope reads of generate-block variables a/b/c initialised from genvars are exercised from sibling/outer always blocks; ported from third_party/yosys/tests/verilog/prefix.sv
    • size_cast - SystemVerilog size and type casts: literal-width casts (1'(x), 2'(x), 3'(x)), built-in atom-type casts (byte'(x), int'(x)), typedef-named casts (u3bit_t'(x), s2bit_t'(x)), packed-struct casts (s12bit_packed_struct_t'(x)), composed with bitwise/ternary expressions and '0/'1 fill literals; ported from third_party/yosys/tests/verilog/size_cast.sv (~600 assertions)
    • dynslice - Dynamic indexed-part-select on the LHS of a non-blocking assignment in always @(posedge clk): dout[ctrl*sel +: 16] <= din writes 16 bits of the 128-bit dout register at a runtime-computed offset; ported from third_party/yosys/tests/simple/dynslice.v
    • defvalue - Module-port default values: input [3:0] delta = 10 provides a constant default that is used when an instance does not connect the port. The test instantiates cnt foo (.delta) (connected) and cnt bar (...) (unconnected, defaulted to 10), so bar increments by 10 each clock and foo by the parent's delta. Ported from third_party/yosys/tests/simple/defvalue.sv
    • case_expr_query - System query functions in case expressions and labels: $bits, $size, $high, $low, $left, $right applied to a packed scalar (logic [5:0] out); 12 nested case statements all match (e.g. case ($bits(out)) 6:, case (5) $high(out):) so the body unconditionally drives out = '1 (= 6'h3f); ported from third_party/yosys/tests/simple/case_expr_query.sv
    • case_expr_non_const - Case statements where the case expression and the case-item labels are non-constant references (signed/unsigned reg variables of differing widths): exercises SV LRM 12.5.1 context width and signedness rules, including signed-vs-signed comparisons that require sign-extension of the narrower label and mixed signed/unsigned comparisons that fall back to zero-extension; ported from third_party/yosys/tests/simple/case_expr_non_const.v
    • case_expr_extend - Unbased unsized fill literal '1 assigned inside a case arm in an initial block: case (1'b1 << 1) 2'b10: out = '1; default: out = '0; endcase correctly produces out = 6'h3f (the all-ones fill replicated to the LHS width) rather than 6'h01 (the 1-bit '1 zero-extended); ported from third_party/yosys/tests/simple/case_expr_extend.sv
    • package_task_func - Package tasks and functions called from module scope: P::t(a) (task with output parameter), P::f(3) (function returning i * X), P::g(3) (recursive function), P::Z (package localparam from recursive function); concurrent assert property statements; required three fixes: (1) evaluate_single_operand now resolves package parameters via ref->Actual_group() when not in local_vars (enabling correct compile-time evaluation of f = i * X), (2) initial blocks containing a task_call are routed to the comb import path so import_task_call_comb can inline the task body, (3) module-level assert_stmt nodes under vpiAssertion now generate $check RTLIL cells
    • func_width_scope - Functions whose return width depends on a localparam that is shadowed by a same-named localparam in an enclosing generate block: func1 at module level uses WIDTH_A=5, func2 inside begin : blk uses WIDTH_A=6 (shadows outer), func3 inside blk uses WIDTH_B=7; wire widths (xc=31-bit, yc=63-bit, zc=127-bit) derived from compile-time function calls on zero are all correct; required two Surelog fixes: (1) CompileStmt.cpp — compile function return type against the component context rather than the instantiation context so the correct shadowed localparam value is used; (2) NetlistElaboration.cpp — prevent getComplexValue() from walking up to an ancestor and inheriting a same-named parameter's value, but only when a genuine shadowing conflict exists (parent has the same param name); a localParamShadowsParent guard avoids a regression where genvar index i in a generate loop was incorrectly protected
    • func_block - Functions with part-select LHS on the return variable (func3[A:B] = inp[A:B]) and for-loop bit-select assignments (func1[idx] = inp[idx]); function-local localparam declarations now correctly resolved via the parent param_assign Rhs expression; formally equivalent to the Verilog frontend
    • fmt_always_comb - $display system task in always @* with conditional enable: always @* if (y & (y == (a & b))) $display(a, b, y) generates a $print RTLIL cell (TRG_WIDTH=0, TRG_ENABLE=false) with EN wire defaulting to 0 and set to 1 inside the if true case; reg a = 0/reg b = 0 net declaration initializers set \init attributes (not init processes); formally equivalent to the Verilog frontend output
    • gen_struct_access - Packed array of structs with struct aggregate assignment and hierarchical field access in a generate block: td1 [3:0] pipe_in input port (288-bit packed array of 4 structs), struct aggregate '{f1: pipe_in[3].f1[63:0], f2: pipe_in[3].f2[7:0]} assignment; synthesizes to a pure buffer out = pipe_in[287:216] (element [3] of the array), demonstrating UHDM's superior struct support over the Yosys Verilog frontend; required two new expression handlers (see Recent Fixes)
    • fsm2 - Finite state machine with a 4-bit counter register (cnt), 5 states (100/200/210/300/310), and always @(posedge clk) with non-blocking assignments; verifies that the UHDM frontend produces a proper RTLIL process with switch/case structure (matching the Verilog frontend) rather than mux-cell chains outside the process body
    • func_typename_ret - Functions whose return type is a typedef (local or package-scoped): function automatic T func1 where T = logic[1:0] and function automatic P::S func2 where P::S = logic[3:0]; verifies correct width and sign extension of signed parameters assigned to typedef'd return variables
    • int_types - Integer atom types (integer, int, shortint, longint, byte) and integer vector types (logic, reg, bit) in generate blocks, with all signed/unsigned variants and 1-bit / 2-bit width forms; verifies correct widths, signedness, and sign/zero extension when assigning narrow signed values to wider 128-bit wires both directly (a = x) and through compile-time function calls (c = f(-1)); covers reg signed (which lands as a logic_net in the elaborated model rather than a variables) and bit signed (whose typespec is bit_typespec rather than logic_typespec)
    • net_types - wire/wand/wor net types with logic, integer, and typedef data types; verifies correct multi-driver AND/OR resolution for all type combinations and correct \wand/\wor Yosys attributes; required a Surelog fix (see Recent Fixes)
    • param_int_types - Module-level variable declarations with built-in integer types (integer, int, shortint, longint, byte) with initial values, and matching typed parameters; verifies correct initial values and widths
    • port_int_types - Built-in integer port types (byte, byte unsigned, shortint, shortint unsigned) with correct sign/zero extension when assigned to wider wires: signed types sign-extend, unsigned types zero-extend
    • unbased_unsized_shift - Unbased unsized fill literals ('0, '1) in shift operations: '1 << 8 and '1 << d in a 64-bit context correctly produce 64'hFFFF_FFFF_FFFF_FF00; tests both constant and dynamic shift amounts
    • for_decl_shadow - For-loop variable declarations that shadow outer generate-scope variables (for (integer x = 5; ...) where x shadows gen.x), cross-scope hierarchical assignment via hier_path (gen.x), and compound assignments (+=) mixing the loop counter with the outer variable — fully compile-time evaluated via the interpreter with correct variable scoping and gen-scope output mapping
    • unnamed_block_decl - Unnamed begin/end blocks with local integer variable declarations and variable scoping (inner z shadows output z), fully compile-time evaluated via interpreter to produce z = 5
    • wandwor - wand/wor net types with multi-driver AND/OR resolution, module port connections, multi-bit variants
    • rotate - Barrel shift rotation with nested generate loops (5 levels x 32 bits = 160 always @* blocks), each assigning to a single bit of a generate-local wire via bit selects
    • repwhile - Memory initialization using while and repeat loops in functions (mylog2 with while, myexp2 with repeat), called from for-loop in initial block, producing 128 $meminit_v2 cells
    • asgn_expr_sv - Full SystemVerilog increment/decrement test: pre/post-increment/decrement as statements and expressions, procedural assignment expressions, byte-width concatenation with ++w/w++
    • constmsk_test - OR reduction of concatenations containing constants (|{A[3], 1'b0, A[1]})
    • union_simple - Packed unions: named unions (w_t, instruction_t), anonymous unions, unions nested within structs (s_t containing instruction_t), multi-level member access (ir1.u.opcode, s1.ir.u.imm, u.byte4.d)
    • typedef_param - Typedef'd parameters and localparams (uint2_t, int4_t, int8_t, char_t) with signed types, chained typedef aliases, localparam visibility, and static assertions
    • typedef_package - Package-scoped typedefs (pkg::uint8_t, pkg::enum8_t), enum types with hex values (8'hBB, 8'hCC), package localparam/parameter initialized from enum constants, assertions on package-qualified parameters
    • svtypes_struct_simple - Packed structs with member access in continuous assignments and assertions, nested structs, struct packed signed
    • gen_test1 through gen_test9 - 9 generate block tests: nested generate loops, for-loop in always blocks, conditional generates, hierarchical generates with localparam, nested generate with multiplication-based genvar increment and cross-scope hier paths, descending loops, power operator in initial/always, nested scope shadowing, named generate blocks
    • asgn_binop - Compound assignment operators (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, <<<=, >>>=)
    • arrays03 - Packed multidimensional array support with dynamic element access (in[ix] where in is logic [0:3][7:0])
    • Repeat loop (repeat(N)) support with compile-time unrolling, blocking/non-blocking assignment handling, and loop index/intermediate variable tracking
    • asgn_expr / asgn_expr2 - SystemVerilog assignment expressions: increment/decrement operators, nested assignment expressions
    • port_sign_extend - Port sign extension with signed submodule outputs and signed constants
    • func_tern_hint - Recursive functions with ternary type/width hints in self-determined context
    • svtypes_enum_simple - Bare enums, typedef enums with logic [1:0], parenthesized type declarations ((states_t) state1;), enum constant initialization, FSM transitions, and combinational assertions
    • const_fold_func - Compile-time constant function evaluation with recursive functions (pow_flip_a, pow_flip_b), bitwise AND/OR/XNOR operations, bit-select LHS assignments (out6[exp] = flip(base)), nested function call arguments
  • Recent Fixes:
    • array_assign — empty-operand crash in the vpiEqOp handler ✅
      • Root cause: import_operation's vpiEqOp branch sign-extends a narrower constant operand by inspecting rhs.as_const().back() (the MSB) before extending. When an operand has size 0 — which can happen if the upstream resolver returns an empty SigSpec, e.g. when a var_select on an unpacked array element fails to find the element wire — the subsequent back() call dereferences past the bit-vector end and segfaults
      • Fix: gate the sign-extension block on lhs.size() > 0 && rhs.size() > 0 so a zero-bit operand is left alone. Downstream the comparison still produces a meaningful (possibly trivially-equal) result, which is what the Verilog frontend would do for the same unresolved reference
    • various_struct_access — UHDM ExprEval::decodeHierPath no longer segfaults on a localparam initialiser that does struct-field access on a struct-typed parameter ✅
      • Root cause: decodeHierPath clones path->Typespec() to attach to a constant clone (the substituted parameter value), then dereferences the clone result without checking. For localparam logic f = P.a; (where P is parameter struct_t P = …), the hier_path's Typespec() can be null at the point Surelog reduces the expression during elaboration; clone_tree(nullptr) returns null, and the subsequent rt->VpiParent(cons) crashes
      • Fix: in third_party/Surelog/.../UHDM/.../ExprEval.cpp, gate the typespec-cloning block on path->Typespec() != nullptr and additionally null-check the cloned rt before using it. Surelog/UHDM rebuild propagates into the bundled libuhdm.a linked into both surelog and uhdm2rtlil.so
    • Yosys submodule bumped to v0.64 (was v0.62-40); equiv flow updated for the new strictness ✅
      • What changed upstream: yosys 0.64's equiv_induct treats unmodelled cells as a fatal error rather than a warning. write_verilog escapes built-in gate cell types ($_MUX_, $_AND_, …) to public Verilog identifiers (\$_MUX_, …); reading the synth output back creates cells of public type — satgen has no SAT model for those public types, so equiv_induct aborted. v0.64's abc mapping also prefers $_MUX_ cells where v0.62 used $_ANDNOT_/$_ORNOT_, so this hit the rotate test in particular
      • Fix: in test/test_equivalence.sh, after equiv_make pairs gold/gate by structure (it matches them while they still reference the simcells library stubs from read_verilog -lib +/simcells.v), chtype -map \$_AND_ $_AND_ (and friends — NAND, OR, NOR, XOR, XNOR, NOT, ANDNOT, ORNOT, MUX, NMUX) converts the public-typed cells back to their internal counterparts so satgen has SAT models for the equiv_simple + equiv_induct passes
    • struct_sizebits — array/range query system functions now resolve hier_paths and walk the full multi-dim typespec ✅
      • Root cause: the existing $bits/$size/$high/$low/$left/$right handler in import_expression for vpiSysFuncCall only knew how to inspect a ref_obj argument, only recognised logic_typespec, and only looked at the first range. Anything like $dimensions(s.sy.y) (hier_path argument), $size(s.sz.z, 2) (2-arg form picking a non-outermost dim), $bits of a packed struct/union, or $increment was unhandled — fell through to "unhandled system function call: …" and returned the operand wire itself, making the assertions reference s (an undriven 221-bit struct) and the synth output show s = 221'hxx…
      • Fix: rewrote the handler to (1) follow hier_paths via ExprEval::decodeHierPath(MEMBER) to find the leaf member's typespec; (2) walk that typespec collecting all dimensions (multi-range Ranges() on logic_typespec, then recurse into nested Elem_typespec); (3) strip one outer dim per surrounding bit_select for cases like s.sz.z[3][3]; (4) honour the optional 2nd argument to pick a specific dimension index; (5) added handlers for $dimensions (returns max(1, dims.size())) and $increment (+1 if L<R, else -1); (6) for atomic/struct/union types treat the whole as a single [bits-1:0] dim so $size(s) returns total bits
    • prefix — generate-scope variable initialisers are now applied even when the wire was lazily created by an outer reference ✅
      • Root cause: in import_gen_scope, the wire-creation block AND the var->Expr() initialiser-driver block both lived inside if (!name_map.count(hierarchical_name)). When an outer always @* referenced blk1.blk2[0].b via a hier_path before we visited blk2[0]'s gen_scope, the wire was already created on demand — so when we got to the gen_scope, name_map.count(...) was true and we silently skipped the initialiser. The wire stayed at X, and every assertion reading b (or c) folded to a falsified comparison
      • Fix: split the two concerns. We still create the wire only when missing, but we always look up the wire (creating or finding) and always run the var->Expr() initialiser path against it. Keeps the outer-reference creation order working while making sure each generate-scope variable is driven by its declared initialiser
    • size_cast — SV size/type casts now handle the full set of integral/typedef/struct typespecs and sign-extend signed operands ✅
      • Root cause: the vpiCastOp handler in expression.cpp only computed a target width when the cast typespec was an integer_typespec (the case Surelog uses for literal-width casts like 3'(x)). Every other cast — byte', int', u3bit_t', s12bit_packed_struct_t', etc. — fell through to "Unsupported cast operation" and returned an empty SigSpec. Downstream that empty operand corrupted comparison expressions and triggered a segfault on this 600-assertion test
      • Fix: the cast handler now (1) keeps the integer_typespec-VpiValue fast path for literal-width casts, then (2) falls back to get_width_from_typespec(actual_ts, ...) for every other typespec — covering byte/int/shortint/longint/integer/logic/bit/struct/typedef variants. Cast-result signedness comes from is_typespec_signed(actual_ts) and is propagated as wire->is_signed (and as CONST_FLAG_SIGNED for fully-constant results). Source signedness for the bit-pattern conversion is taken from the operand's UHDM expression via is_expr_signed() (with a fallback to the wire's is_signed), and module->addPos(NEW_ID, operand, result_wire, src_signed) now sign-extends signed operands to the target width when widening
    • dynslice — dynamic indexed-part-select on the LHS of a clocked non-blocking assignment now correctly drives the full base wire via a read-modify-write pattern instead of leaving the base undriven ✅
      • Root cause: in import_always_ff's comb-style path, the LHS of a non-blocking assignment is imported with import_expression. For a dynamic indexed-part-select dout[ctrl*sel +: 16], this produces a fresh 16-bit $shiftx output wire; import_always_ff then created a $0\<that-shiftx-wire> FF temp and wired the sync rule to it, so the actual 128-bit dout register was never driven. Result: assign dout = 128'hxxx…xxx and the always block dropped on the floor
      • Fix: needs_sync_path() (process_helper.cpp) now reports true when an assignment's LHS is an vpiIndexedPartSelect whose base expression is non-constant. The always_ff path then routes through the sync handler (import_assignment_sync) instead. A new branch in import_assignment_sync (process.cpp) detects this LHS form and emits the explicit read-modify-write: shifted_data = (zext din) << offset, shifted_mask = mask_pattern << offset, new_base = (base & ~shifted_mask) | shifted_data, then registers pending_sync_assignments[full_base] = new_base (mux-wrapped under any current_condition). proc_dff then materialises a single 128-bit $dff for dout
    • defvalue — module-input port default values are now tagged on the wire as \defaultvalue instead of being driven by an in-body cont_assign that overrode every parent connection ✅
      • Root cause: Surelog elaborates input [3:0] delta = 10 by emitting a cont_assign with vpiNetDeclAssign:1 that drives the input port wire to the constant inside every instance — including instances that do connect the port externally. import_continuous_assign then materialised the constant as a real driver (module->connect or a sync-always process), which silently won against the parent's port connection (.delta(parent_delta) had no effect because \delta was already pinned to 10 inside the body).
      • Fix: in import_continuous_assign, when an is_net_decl_assign cont_assign with a constant RHS targets a wire whose port_input is set, skip emitting any driver and instead set the wire's \defaultvalue attribute. This matches the Verilog frontend exactly — Yosys's hierarchy pass then substitutes the default at parent-instance sites whose port is left unconnected, while connected instances see the parent's value.
    • case_expr_query — SV array/range query system functions ($bits, $size, $high, $low, $left, $right) now evaluate to constants instead of returning the wire itself ✅
      • Root cause: import_expression for vpiSysFuncCall only special-cased $signed/$unsigned/$floor/$ceil. For any other system function (including the query family) the fallback was return args[0] — i.e. for $bits(out) we returned the wire out, so a case ($bits(out)) 6: ended up comparing out against 6'b000110 at runtime instead of folding to case (6) 6: at compile time
      • Fix: added a single branch that handles all six query functions: $bits/$size → the SigSpec width of the argument as a 32-bit constant; $left/$right/$high/$low → walk the argument's ref_objvariables/netTypespec()->Actual_typespec() (a logic_typespec) → first Range's Left_expr/Right_expr to recover the declared [L:R] indices, then return the requested bound ($high = max(L,R), $low = min(L,R))
    • case_expr_non_const — case-statement signedness detection now walks ref_obj references to their underlying variable/parameter/net so SV LRM 12.5.1 context-extension picks sign-extension when all operands are signed ✅
      • Root cause: the static is_expr_signed() helper in process.cpp only inspected vpiConstant operands (looking for the 's sigil in VpiDecompile()). Any reference (ref_obj to a reg signed x, etc.) returned false, so all_signed was incorrectly false whenever a signed register was used as the case expression or a case label. The narrower signed labels were then zero-extended to the context width and never matched the case expression — producing x for case b/c (which had no default arm) and the wrong value for case f
      • Fix: promoted is_expr_signed() to a member of UhdmImporter and taught it to walk ref_obj->Actual_group() into a variables/parameter/net and consult VpiSigned() first, falling back to is_typespec_signed() on the typespec. The latter is needed because reg signed lands as a logic_net (not a variables) in the elaborated UHDM model and because constant typespecs occasionally carry the only surviving signedness flag
    • case_expr_extend — unbased unsized fill literal '1 inside a case arm now replicates to the LHS width instead of zero-extending the 1-bit '1
      • Root cause: three assignment-import paths in process.cpp did the size-mismatch resize via plain extend_u0() regardless of whether the RHS was a fill literal. import_assignment_sync already detected c->VpiSize() == -1 && VpiValue() == "BIN:1" and replicated S1 to the LHS width, but the two import_assignment_comb overloads (Process*, CaseRule*) and the inline assignment handler in import_statement_comb(CaseRule*) did not — so out = '1 inside an initial-block case arm produced 6'b000001 instead of 6'b111111
      • Fix: added the same fill-ones detection (run before importing the RHS so the original UHDM constant is still inspectable) and the same replication branch (SigSpec(State::S1, lhs.size())) to all three sites. The Process* overload also clears the flag after a compound op, since the post-op result is no longer a raw fill literal
    • int_types (expanded) / const_func (regression-fix) — compile-time function evaluation now widens inputs and sign-extends results correctly across all integral typespec variants ✅
      • Argument width in evaluate_function_call (functions.cpp): each input arg is now resized to the formal parameter's declared width (sign- or zero-extended per the formal's typespec signedness) before being stored in local_vars. Without this, a narrow argument left bitwise operations on the parameter operating at the wrong width — e.g. flip(OUTPUT[15:8]) produced ~inp at 8 bits instead of 24, so nested flip(flip(...)) no longer cancelled out and the for-loop guard never fired (j ran to the 100000 safety cap)
      • Signed-input propagation through the body: input parameters from a signed formal are tagged with CONST_FLAG_SIGNED; the flag rides along through local_vars copy assignments (e.g. f = inp). The end-of-function resize uses ret_signed || (result.flags & CONST_FLAG_SIGNED) to decide between sign- and zero-extension — this matches SV's assignment-widening rule where a signed RHS sign-extends to fit a wider LHS even when the LHS is unsigned (e.g. function automatic longint unsigned f; input integer inp; f = inp; endfunction returning f(-1) yields 64 ones, not 32 ones zero-extended)
      • is_typespec_signed() helper (module.cpp): single switch covering all integral typespec variants — logic_typespec, bit_typespec, int_typespec, integer_typespec, short_int_typespec, long_int_typespec, byte_typespec — used by import_gen_scope's ref_obj/func_call signedness paths
      • reg signed ref_obj path (import_gen_scope): in the elaborated model reg signed x = -1 lands as a logic_net, not variables, so the previous code (which only checked variables/parameter Actual_group results) never saw the signedness. Added a dynamic_cast<const UHDM::net*> branch that inspects net->VpiSigned() and falls back to the net's typespec
    • package_task_func — package tasks/functions, recursive packages functions, and concurrent assertions now correctly synthesized ✅
      • Package parameter resolution in compile-time evaluator: evaluate_single_operand in functions.cpp now follows ref->Actual_group() when a ref_obj is not found in local_vars; if the actual object is a parameter, its VpiValue() is parsed to a constant — this enables f = i * X (where X = P::X = 3) to evaluate correctly to 9 for P::f(3)
      • Initial block with task call: import_initial now detects a top-level vpiTaskCall statement and routes to the comb import path (import_initial_comb) instead of the sync path; this allows import_task_call_comb to inline the task body and correctly drive the output argument (a = 2 from P::t(a))
      • Concurrent assertions (assert property): module-level assert_stmt nodes under module_inst->Assertions() are now imported as $check RTLIL cells with FLAVOR="assert", EN=1'h1, and the property expression as the A input
    • func_block — function-local localparam declarations and part-select return variable assignments now correctly synthesized ✅
      • Root cause: For localparam A = 32 - 1 inside a function, Surelog does not store the resolved value in parameter->VpiValue() or parameter->Expr(). Instead the expression 32 - 1 is kept in the parent param_assign object's Rhs().
      • Fix in import_ref_obj() (expression.cpp): added fallback that checks param->VpiParent() for a param_assign (UhdmType uhdmparam_assign) and evaluates its Rhs() expression when both VpiValue() and Expr() are empty
      • uhdmpart_select LHS handler in process_stmt_to_case(): func3[A:B] = inp[A:B] — the part_select node's Left_range/Right_range are ref_obj objects pointing to the function-local parameters; with the localparam fix, they now resolve to the correct constants (A=31, B=1)
      • Result wire is zero-initialized first (scan_for_direct_return_assignment intentionally does not detect uhdmpart_select LHS), so unassigned bits remain 0 — correct for partial bit assignments
    • fmt_always_comb$display system tasks in always @* blocks now generate proper $print RTLIL cells, and reg a = 0 net declaration initializers correctly set \init wire attributes ✅
      • $display$print: UHDM represents $display(a, b, y) as a sys_func_call (VPI type 56) with args via Tf_call_args(); new import_display_stmt() creates a $print cell using Yosys Fmt::parse_verilog() for the FORMAT string; EN wire defaults to 0 in the process root case and is set to 1 in the active (conditional) case
      • reg a = 0 initializer: continuous assigns with VpiNetDeclAssign=1 and a constant RHS now set the \init attribute on the wire rather than creating an init process (which was clobbering flip-flop outputs)
      • Without $print: the $display logic had no output consumer, so opt removed everything, leaving 0 gates
    • gen_test3 — generate case statement with default: appearing before specific labels now correctly selects the matching label ✅
      • Root cause: Surelog's DesignElaboration.cpp processed default: as a match immediately when it was the first case item in source order, before checking for specific labels that appeared later (e.g., case (i) default: ...; 0: ... with i=0 would pick default: instead of 0:). This caused y[3] to be driven X since the 0: assign y[3] case was never elaborated.
      • Fix: Save default: as a fallback (defaultGenItem) instead of immediately matching; continue searching for a specific label match; only use default: if no specific case matched
    • mem2reg_test2 — register arrays with (* mem2reg *) attribute in always @(posedge clk) blocks now correctly expanded to individual flip-flop wires ✅
      • Root cause: (* mem2reg *) attribute was not propagated by Surelog and the array was kept as a $memory object; the clocked always block generated broken MemWriteAction entries that caused a segfault during synthesis
      • Attribute detection: scan array_net->Attributes() (and inner logic_net->Attributes()) for VpiName() == "mem2reg" → force individual-wire expansion instead of $memory
      • Dynamic write in sync path (mem[addr] <= 0): added per-element mux handling in import_assignment_sync — for each element N, emits \mem[N] = (condition && addr==N) ? rhs : prev_val into pending_sync_assignments, which becomes a posedge sync update
      • Dynamic read (assign data = mem[addr]): handled by the existing import_bit_select mux-chain fix (reads individual element wires)
    • mem2reg_test1 — combinational arrays (reg [W:0] arr [N:0]) accessed in always @* blocks now correctly synthesized using individual element wires and mux logic ✅
      • Root cause: Arrays only used in always @* were still treated as $memory objects; dynamic reads returned X (no writes to the memory) and dynamic writes were ignored
      • Pre-scan: new comb_only_arrays set identifies arrays accessed exclusively from combinational always blocks before module import; these are expanded to individual wires (\array[0], \array[1], etc.) instead of $memory
      • Dynamic writes (array[dyn_addr] = data): extract_assigned_signals skips them (no temp wire needed); import_assignment_comb handles them with per-element $eq+$mux logic using current_comb_values for the "current" value of each element
      • Dynamic reads (out = array[dyn_addr]): import_bit_select builds a mux chain over current_comb_values entries so reads see values written earlier in the same always block
    • partsel_simple — dynamic indexed part-selects (data[offset +: 4], data[offset+3 -: 4]) now correctly synthesized via $shiftx cells (28 gates, formally equivalent) ✅
      • Root cause: import_indexed_part_select only handled constant base expressions and returned an empty SigSpec for dynamic ones; additionally idx << 2 was clipped to 3 bits (the idx width) because vpiLShiftOp used the operand width instead of the context width
      • Fix 1: vpiLShiftOp in import_operation now uses expression_context_width (the LHS wire width) as the result width when available, preventing bit loss
      • Fix 2: import_indexed_part_select now emits a $shiftx(A=data, B=base_lsb, Y_WIDTH=width) cell for non-constant base. For +:, base_lsb = base_expr; for -:, base_lsb is computed via a $sub cell as base_expr − (width−1)
    • gen_struct_access — struct aggregate assignment and packed array of structs field access now correctly synthesized ✅
      • vpiAssignmentPatternOp (op type 75): '{f1: expr1, f2: expr2} struct literals were producing empty signals. Surelog stores the field VALUE expressions directly as Operands() (as hier_path objects, not tagged_pattern wrappers as implied by the UHDM dump's visitor output). Fix in import_operation: early-return handler for vpiAssignmentPatternOp casts each operand as const expr* and concatenates values MSB-first (same order as vpiConcatOp)
      • Packed array of structs field access (sig[i].field[hi:lo] via hier_path): import_hier_path now detects the [bit_select, part_select] Path_elems pattern and computes absolute bit offsets. packed_array_var.Typespec() is null — instead uses pav->Ranges() for array dimension and pav->Elements()[0] (a struct_var) for the element struct typespec. Element offset = (index − arr_low) × element_width; field offset accumulated by iterating struct members in reverse (LSB first); result is a plain extract() on the base wire
    • fsm2 / always_ff RTLIL process structure — import_always_ff now generates a proper switch/case structure inside the process body (matching the Verilog frontend) instead of pre-computing all combinational logic as mux cells outside the process ✅
      • Root cause: The previous implementation used import_statement_syncpending_sync_assignments, which created a flat list of mux cells in the module and a sync rule pointing to the final mux outputs — a structurally very different representation compared to the Verilog frontend's switch/case inside the process
      • Fix: Replaced the "no memory writes" path in import_always_ff with the same $0\temp-wire + import_statement_comb approach used by import_always_comb; added in_always_ff_body_mode flag to suppress current_comb_values reads/writes during the body, enforcing non-blocking assignment semantics (all RHS expressions see original register values, not intermediate $0\ values)
      • Result: Gate count dropped from 109→83 (matching the Verilog frontend), formal equivalence passes, and FSM extraction/optimization passes can now recognize the register structure
    • always_ff path selection regressions — four tests broken by the always_ff comb-path change are now fixed ✅
      • Root cause: The new comb-style path lacked handlers for vpiRepeat, for loops inside conditionals (CaseRule* variant of import_statement_comb had no vpiFor), and memory writes inside for loops
      • aes_kexp128dedup_key fix: array element w[0] (a vpiBitSelect that resolves to a complete wire) had is_part_select=true, causing the temp wire key to get a spurious range suffix "w[0][3:0]" instead of "w[0]"map_to_temp_wire lookup failed → X output; fixed by guarding the range-suffix path with !lhs_spec.is_wire()
      • counters_repeatvpiRepeat loops had no handler in import_statement_comb; new needs_sync_path() helper detects vpiRepeat and routes to the old import_statement_sync path which handles repeat-loop carry tracking via blocking_values
      • asym_ram_sdp_read_wider — for loop inside if (enaB) conditional hit the CaseRule* variant of import_statement_comb which had no vpiFor handler; needs_sync_path() detects for-in-conditional and routes to import_statement_with_loop_vars via the sync path
      • asym_ram_sdp_write_wider — memory writes (RAM[...] <= ...) inside a for loop were correctly detected by scan_for_memory_writes but the custom memory-write CaseRule* path cannot unroll for loops; new has_for_loop() helper detects this pattern and routes to the sync path (import_statement_syncpending_memory_writes → proper memwr cells)
    • func_typename_ret — functions with typedef return types now correctly sign-extend signed parameters when Surelog const-folds the call ✅
      • Root cause: Surelog evaluates func1(1'b1) at elaboration and stores the result as BIN:1, vpiSize:2 (with inp as input reg signed inp, 1-bit signed = -1). The raw bits 1 zero-padded to 2 bits gives 2'b01 = 1 instead of the correct sign-extended 2'b11 = 3
      • Key insight: Even though Surelog doesn't set VpiSigned() on the io_decl, the folded constant's vpiTypespec → ref_typespec → logic_typespec still has VpiSigned():true from the signed parameter declaration
      • Fix in import_constant() (expression.cpp): for vpiBinaryConst with size > const_val.size(), check uhdm_const->Typespec()->Actual_typespec()->VpiSigned() and if true, call extend_u0(size, true) (sign-extend via MSB replication) instead of zero-extending
    • int_types — integer atom/vector types in generate blocks now correctly sign/zero-extend when assigned to wider wires ✅
      • Temp wire naming fix: import_always_comb dedup key for full-wire assignments now uses the actual wire name (e.g., test_integer.a) instead of the bare local variable name (e.g., a), preventing $0\a already exists conflicts across generate blocks with same-named local vars
      • Sign extension in process assignments: import_assignment_comb now checks rhs.is_wire() && rhs.as_wire()->is_signed and calls extend_u0(size, rhs_is_signed) so signed integer/shortint/longint/byte variables sign-extend to wider targets; unsigned variants still zero-extend
      • Generate scope variable initialization: import_gen_scope now processes each variable's Expr() (UHDM's vpiExpr) and creates a module->connect() driving the wire with its initial constant value, so integer x = -1; produces a wire with value 32'hFFFFFFFF
    • net_typesSurelog fix: typed net declarations (wand integer, wor typename, etc.) now produce correct logic_net objects with the right VpiNetType in the elaborated UHDM model ✅
      • Root cause: compileNetDeclaration in Surelog's CompileHelper.cpp overwrote the net keyword (paNetType_Wand) with the data type (paIntegerAtomType_Integer) in the Signal's m_type field, and ElaborationStep::bindPortType_ further overwrote it for typedef-typed nets — the original net keyword was permanently lost
      • Fix: added m_subNetType field to Signal (with getSubNetType()/setSubNetType()) to preserve the original net keyword separately; elabSignal in NetlistElaboration.cpp now forces isNet=true and uses the stored keyword for all VpiNetType computations when getSubNetType() is set
      • Frontend fix: import_net now sets the wiretype attribute for logic_typespec with a non-empty VpiName (preserving typedef names like typename on nets)
    • param_int_types — module-level int, integer, shortint, longint, byte variable declarations now correctly imported as typed wires with their initial values ✅
      • int_var was previously skipped entirely; now handled identically to integer_var, short_int_var, etc.
      • Initial expressions (vpiExpr) for all built-in integer var types now generate a $proc with sync always so Yosys proc+opt_const folds them to the correct constant wire values
    • port_int_typesbyte unsigned and shortint unsigned ports no longer incorrectly marked as signed; $pos extension cell now sign-extends or zero-extends based on the RHS wire's signedness ✅
      • import_port/import_nets: integer typespec signedness now reads VpiSigned() instead of hardcoding true for all built-in integer types
      • import_continuous_assign: addPos now passes rhs_is_signed so signed wires sign-extend and unsigned wires zero-extend when widening
    • unbased_unsized_shift — unbased unsized fill literals ('0, '1, 'x) in shift operations now produce correct full-width results ✅
      • import_constant now expands fill literals to expression_context_width (the LHS width) when processing a continuous assignment, so '1 << 8 in a 64-bit context becomes 64'hFFFF...FFFF << 8 = 64'hFFFF...FF00 instead of the wrong 1'1 << 8 = 1'0 zero-extended to 64'h0
    • forloops — for loops in both clocked (always @(posedge clk)) and combinational (always @*) blocks now compile-time unrolled correctly ✅
      • Module-level loop variables (integer k; declared outside the loop) use vpiRefObj in the init node; both vpiRefVar and vpiRefObj now handled
      • extract_assigned_signals now recurses into vpiFor bodies so dynamically-indexed signals get proper $0\ temp wires
      • import_part_select and import_indexed_part_select substitute loop_values[k] as a constant before looking up the wire — prevents k[1:0] from emitting a wire reference during unrolling
    • '1 fill constant now produces all-ones across the full target width for multi-bit struct fields (e.g., 4-bit field gets 4'b1111 not 4'b0001) ✅
    • gen_test7: always @* block-local variable no longer shadowed by same-named generate-scope genvar — fixed in import_begin_block_comb() by also shadowing the hierarchical name gen.x in name_map
    • Formal equivalence check for constant-only circuits (0 gate cells): now performs actual constant-value comparison between gold and gate netlists instead of trivially passing ✅
    • case_expr_const now passing with correct SV LRM 12.5.1 case-statement semantics ✅
    • port_sign_extend now passing — the UHDM output was correct all along; the equivalence script had a multi-module grep bug ✅
    • Compile-time function evaluator crash fix and improvements ✅
      • Fixed vpiIf/vpiIfElse type casting crash: vpiIf (type 22) must use if_stmt*, vpiIfElse (type 23) must use if_else*
      • Added 7 missing operations: vpiBitAndOp, vpiBitOrOp, vpiBitXNorOp, vpiLogAndOp, vpiLogOrOp, vpiDivOp, vpiModOp
      • Added vpiBitSelect LHS handling for bit-select assignments in compile-time evaluation
      • Added vpiFuncCall argument handling in recursive function evaluation
      • Safety check for .as_string() on empty RTLIL::Const values
    • Unnamed block variable declarations with proper scoping via interpreter-based evaluation: inner block-local variables shadow outer/module-level variables correctly ✅
    • Fixed temp wire naming collision in generate scopes: bit/part-select assignments in nested generate loops (e.g., out[j] in 160 always blocks) now use scope-qualified temp wire names with bit ranges ($0\netgen[0].out[3:3]) to avoid $0\ name collisions across always blocks ✅
    • Compile-time function evaluation now supports while and repeat loop constructs, enabling functions like mylog2 (using while) and myexp2 (using repeat) to be evaluated at compile time ✅
    • Fixed for-loop increment handling: i = i + 1 assignment form now recognized in addition to i++ post-increment ✅
    • Fixed const_shl/const_shr/const_not crash in compile-time evaluator: result_len of -1 caused vector::_M_fill_insert when passed to resize()
    • Surelog fix: Pre-increment/decrement (++x/--x) was incorrectly mapped to vpiPostIncOp/vpiPostDecOp instead of vpiPreIncOp/vpiPreDecOp — fixed two code paths in CompileExpression.cpp
    • Post-increment expressions (w++) now correctly return the old value; pre-increment (++w) returns the new value ✅
    • Built-in signed types (integer, byte, shortint, longint) now marked as signed on wires ✅
    • Explicit width handlers for built-in types in get_width_from_typespec(): byte=8, shortint=16, int/integer=32, longint=64 ✅
    • Fixed non-constant single-bit zero-extension in continuous assignments (concatenation order was reversed, putting result in MSB instead of LSB) ✅
    • Packed union support (union_var, union_typespec) ✅
      • Added union_var handling in Variables() import with wiretype attribute
      • Added union_typespec width calculation (width of widest member) in get_width_from_typespec()
      • Added union_typespec wiretype attribute in net import
      • Extended import_hier_path() to resolve union member access (offset = 0 for all union members)
      • Extended calculate_struct_member_offset() to handle both struct and union typespecs
      • Fixed vpiDecConst crash when VpiSize() returns -1 (default to 32-bit width)
    • Localparam visibility, INT constant width, and blackbox detection ✅
      • Fixed avail_parameters() to skip VpiLocalParam parameters — localparams are stored for expression resolution but not exported as externally-visible parameters
      • Fixed vpiIntConst width: uses VpiSize() from elaborated model when available (e.g., 4-bit for int4_t, 8-bit for int8_t) instead of hardcoded 32
      • Fixed blackbox detection: empty modules without ports (parameter-only top modules) are no longer marked as blackbox
      • Fixed dynports attribute: only set on modules that have both parameters and ports
    • HEX/BIN enum constant crash and package parameter resolution ✅
      • Fixed std::stoi() crash on hex enum values (HEX:BB) — added parse_vpi_value_to_int() helper supporting HEX/BIN/UINT/INT/DEC prefixes
      • Switched package import from AllPackages() to TopPackages() (elaborated) for resolved parameter values
      • Added VpiValue() fallback in import_package() for parameters without Expr() (e.g., localparam PCONST = cc)
      • Enum constant width now uses VpiSize() instead of hardcoded 32
    • Packed struct member access via struct_var type handling ✅
      • Added struct_var to three dynamic_cast chains in import_hier_path() for typespec lookup
      • Fixes s.a, pack1.a, s2.b.x etc. producing 1'x instead of correct bit slices
      • Known limitation: struct packed signed signedness not propagated (Surelog doesn't set VpiSigned on struct_typespec/struct_var)
    • Compound assignment operators (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, <<<=, >>>=) ✅
      • Surelog represents c += b as assignment with vpiOpType = vpiAddOp (not vpiAssignmentOp)
      • Added create_compound_op_cell() helper mapping vpiOpType to RTLIL cells ($add, $sub, $mul, $div, $mod, $and, $or, $xor, $shl, $shr, $sshl, $sshr)
      • Uses current_comb_values to resolve the LHS's current value (e.g., after c = a, c += b becomes a + b)
      • Handles both Process and CaseRule variants of import_assignment_comb
    • Packed multidimensional array support (logic [0:3][7:0], reg8_t [0:3], typedef variants) ✅
      • Fixed wire width computation for logic_typespec with Elem_typespec (element × range instead of just range)
      • Fixed upto/start_offset: packed multi-dim arrays create flat wires instead of reversed-index wires
      • Dynamic element access (in[ix]) generates $sub/$mul/$shiftx for correct 8-bit element extraction
      • Handles both direct packed arrays and typedef aliases (reg2dim_t, reg2dim1_t)
      • Packed array metadata stored as wire attributes for reliable bit_select detection
    • Generate for-loop with complex genvar increment and cross-scope hierarchical references (gen_test5) ✅
      • Surelog fix: DesignElaboration.cpp — for-loop increment now falls back to full expression compilation (m_helper.getValue()) when simple evaluator fails, enabling step = step * CHUNK where CHUNK is a named parameter
      • Surelog/UHDM fix: clone_tree.cpphier_path::DeepClone() now numerically evaluates symbolic bit_select indices (e.g., outer[LAST_START]outer[0]) via evalIndex() helper, so gen_scope_array lookups match correctly across generate scopes
      • Surelog/UHDM fix: ExprEval.cppreduceExpr() for ref_obj now falls back to Actual_group() pointer when name-based lookup fails, resolving localparams in cross-scope contexts
      • UHDM frontend fix: expression.cppimport_hier_path() creates wires on-the-fly for forward references to sibling generate scopes not yet processed
      • 255 gates, formal equivalence verified
    • Parenthesized type variable declarations ((states_t) state1;) ✅
      • Surelog grammar fix: Extended variable_declaration rule in SV3_1aParser.g4 to accept OPEN_PARENS data_type CLOSE_PARENS as a type specifier
      • ANTLR parser strips parentheses at parse time, producing clean VObject tree identical to states_t state1;
      • Surelog correctly creates the variable in UHDM as logic_net + enum_var with proper typespec
    • Repeat loop (vpiRepeat) support in synchronous always blocks ✅
      • Compile-time unrolling of repeat(N) with constant iteration count
      • Automatic detection of loop index variables (i = i+1 pattern) and blocking intermediates (carry)
      • Loop index handled via loop_values for compile-time bit-select resolution (count[i]count[k])
      • Blocking variables handled via input_mapping for runtime signal chain propagation
      • Non-blocking assignments added directly to sync rule actions
      • Proven formally equivalent to Verilog frontend output (65/65 equiv cells)
    • asgn_expr / asgn_expr2 - SystemVerilog assignment expressions ✅
      • Implemented increment/decrement operators (x++, --x) as both statements and expressions
      • Implemented nested assignment expressions (x = (y = z + 1) + 1) with proper side-effect ordering
      • Added emit_comb_assign() and map_to_temp_wire() helpers for correct $0\ temp wire mapping
      • Value tracking (current_comb_values) ensures cell inputs chain correctly through sequential operations
    • port_sign_extend - Signed port propagation from UHDM nets to port wires ✅
      • Fixed import_net() to update signedness on existing port wires from VpiSigned on logic_net
      • Fixed operation signedness for arithmetic/comparison/shift operations via operand analysis
      • Added signed constant detection via int_typespec on Actual_typespec()
      • Unbased unsized literal extension guarded by VpiSize() == -1 check — only true unbased unsized literals ('0, '1, 'x, 'z) are extended to port width; sized constants like 1'b1 are left at original width for proper zero-extension by hierarchy pass
    • custom_map_incomp - Techmap cell handling without blackbox module creation ✅
      • _TECHMAP_REPLACE_ instances now create cells with base module name (namespace-stripped)
      • Parameters passed directly on the cell with proper string constant preservation
      • No module definition created for techmap replacement modules
    • struct_access - Packed struct field access with complex initial blocks ✅
      • Fixed memory analyzer crash on initial blocks (skip vpiInitial in memory analysis)
      • Fixed vpiIf/vpiIfElse type casting in memory_analysis.cpp (vpiIf→if_stmt, vpiIfElse→if_else)
      • Added ReduceBool for multi-bit conditions in mux select and switch/compare operations
      • Implemented combinational import strategy for initial blocks with complex control flow (case/if)
      • Added local variable handling in unnamed begin blocks
      • Split import_initial into sync (simple assignments, for loops) and comb (case/if) strategies
    • multiplier - 4x4 2D array multiplier with parameterized RippleCarryAdder and FullAdder ✅
      • Implemented vpiMultiConcatOp (replication operator {N{expr}})
      • Implemented vpiVarSelect for 2D array part selects (e.g., PP[i-1][M+i-1:0])
      • Added expression context width propagation for Verilog context-determined sizing
      • Fixed parameter resolution in elaborated modules to use actual values over base defaults
    • const_func - Constant functions in generate blocks with string parameters ✅
      • Added vpiStringConst support for string parameter constants
      • Added $floor/$ceil system function handling
      • Added vpiBitNegOp to compile-time and interpreter evaluation
      • Extended for loop unrolling for vpiNeqOp conditions and vpiFuncCall bounds
      • Deduplicated initial block assignments with generate-scope priority
      • Added parameter fallback for part selects (e.g., OUTPUT[15:8])
    • genblk_order - Fixed nested generate blocks with same name ✅
      • Reordered generate scope import to process nested scopes before continuous assignments
      • Added proper Actual_group() checking in hier_path for correct signal resolution
      • Handles generate block name shadowing correctly
    • genvar_loop_decl_1 - Fixed generate scope wire initialization with hierarchical name lookup ✅
    • genvar_loop_decl_2 - Fixed with Surelog update for proper hierarchical path assignment handling ✅
    • carryadd - Now passing with fixed carry addition handling ✅
    • simple_enum - Now passing with proper enum value handling ✅
    • for_decl_shadow - For-declaration variable shadowing in generate scopes ✅
      • Fixed interpreter routing: only route to interpreter when for-init has a type declaration (integer_var); plain for-loops with existing variables still use sync/comb approaches
      • Fixed import_initial_interpreted output mapping to prefer gen-scope wires (gen.x) over same-named module ports (x) by checking gen_scope + "." + name in name_map first
      • Fixed simple-assignment LHS gen-scope resolution: after for-loop cleanup, bare x = x * 2 correctly updates variables["gen.x"] using existing gen-scope fallback
      • Added deduplication via wire_to_value map so multiple interpreter variable aliases (e.g., "x" and "gen.x" both pointing to \gen.x) emit a single init action with the final computed value
    • forgen01 - Fixed nested for loops in initial blocks using interpreter ✅
      • Added support for both ref_obj and ref_var in assignment statements
      • Generalized interpreter usage for any complex initialization patterns
      • Dynamic array detection and size determination
    • asym_ram_sdp_read_wider - Fixed array_net memory detection and dynamic indexing ✅
    • Improved shift register detection to run before array_net processing ✅
    • Fixed traversal depth in has_only_constant_array_accesses for proper dynamic access detection ✅
    • Added support for vpiIf statement type in array access checking ✅
    • Function Support - Significantly improved ✅
      • Function calls in continuous assignments now fully working
      • Proper parameter mapping from function arguments to actual signals
      • Function bodies converted to RTLIL processes with correct wire naming
      • Support for functions with if/else, case statements, and expressions
      • Added support for integer variables in functions (32-bit signed)
      • Fixed loop variable detection for ref_var types enabling loop unrolling
      • Removed hardcoded "result" assumptions - functions can assign to any variable
      • Fixed parameter vs variable detection in function return value scanning
      • Added support for named_begin blocks in functions
      • Functions with output parameters now fully supported
        • Proper distinction between input/output parameters (VpiDirection)
        • Nested function calls with parameter passing working correctly
        • Fixed integer parameter width detection (32-bit for integer types)
        • Removed superfluous X assignments for input parameters
      • code_tidbits_fsm_using_function now passes equivalence check
      • simple_function test added and passing
      • Recursive function support with constant propagation
        • Implemented call stack architecture for tracking recursive function contexts
        • Added constant propagation through function parameters
        • Automatic compile-time evaluation of recursive functions with constant inputs
        • fib and fib_simple tests now passing with efficient RTLIL generation
        • Reduced RTLIL size by 93% through constant propagation optimization
      • New function tests added: function_arith, function_bool, function_case, function_nested (all passing)
      • function_loop, function_mixed, many_functions - Fixed and now passing all tests
      • function_output - Functions with output parameters and nested calls now passing ✅
    • Task Inlining in Always Blocks
      • Task calls in combinational always blocks are now inlined with proper parameter mapping
      • Supports input/output parameters and local variables within tasks
      • Named begin blocks inside tasks create hierarchical wires with correct scoping
      • current_comb_values tracking ensures task bodies read correct in-progress signal values
      • Variable shadowing in nested scopes handled via save/restore pattern on task_mapping
      • scope_task test now passing formal equivalence check
    • Function Inlining in Combinational Always Blocks
      • Function calls with variable arguments in always @* blocks are now inlined into the calling process
      • Eliminates combinational feedback loops caused by separate RTLIL processes for function calls
      • Named begin blocks within functions properly track intermediate values via comb_value_aliases
      • Cell chaining through current_comb_values ensures RHS expressions read from cell outputs, not wires
      • Save/restore pattern on current_comb_values for correct variable scoping in named begin blocks
      • scopes test now passing formal equivalence check (functions + tasks + nested blocks with variable shadowing)
    • Processing Order and autoidx Consistency
      • Fixed processing order: continuous assignments now processed before always blocks
      • Consistent use of Yosys global autoidx counter for unique naming
      • Removed duplicate autoidx increments in intermediate wire creation
      • Better alignment with Verilog frontend naming conventions
    • Added consistent cell naming with source location tracking for all cell types ✅
      • Created generate_cell_name() helper function for standardized naming
      • Applied to all arithmetic, logical, comparison, and reduction operations
      • Improves debugging with source file and line number in cell names

Architecture & Workflow

SystemVerilog (.sv) → [Surelog] → UHDM (.uhdm) → [UHDM Frontend] → RTLIL → [Yosys] → Netlist

Components

1. Surelog (third_party/Surelog/)

  • Industry-grade SystemVerilog parser and elaborator
  • Handles full IEEE 1800-2017 SystemVerilog standard
  • Outputs Universal Hardware Data Model (UHDM)
  • Provides semantic analysis and type checking

2. UHDM Frontend (src/frontends/uhdm/)

  • Core Module (uhdm2rtlil.cpp) - Main frontend entry point, design import, and UHDM elaboration
  • Module Handler (module.cpp) - Module definitions, ports, instances, and wire declarations
  • Process Handler (process.cpp) - Always blocks, procedural statements, and control flow
  • Expression Handler (expression.cpp) - Operations, constants, references, and complex expressions
  • Functions Handler (functions.cpp) - Compile-time constant function evaluation
  • Interpreter (interpreter.cpp) - Statement interpreter for initial block execution
  • Memory Handler (memory.cpp) - Memory inference and array handling
  • Memory Analysis (memory_analysis.cpp) - Advanced memory pattern detection and optimization
  • Clocking Handler (clocking.cpp) - Clock domain analysis and flip-flop generation
  • Package Support (package.cpp) - SystemVerilog package imports, parameters, and type definitions
  • Primitives Support (primitives.cpp) - Verilog primitive gates and gate arrays
  • Reference Module (ref_module.cpp) - Module instance reference resolution and parameter passing
  • Interface Support (interface.cpp) - SystemVerilog interface handling with automatic expansion

3. Yosys (third_party/yosys/)

  • Open-source synthesis framework
  • Processes RTLIL for optimization and technology mapping
  • Provides extensive backend support for various FPGA and ASIC flows

Supported SystemVerilog Features

  • Module System: Module definitions, hierarchical instantiation, parameter passing
  • Data Types:
    • Logic, bit vectors, arrays
    • Packed multidimensional arrays with dynamic element access (e.g., logic [0:3][7:0], typedef variants)
    • Packed structures with member access via bit slicing
    • Packed unions with member access (all members overlay at bit offset 0, width = widest member)
    • Structs containing unions and unions containing structs (nested access)
    • Struct arrays with complex indexing
    • Package types and imports
  • Procedural Blocks:
    • always_ff - Sequential logic with proper clock/reset inference
    • always_comb - Combinational logic
    • always - Mixed sequential/combinational logic
  • Expressions:
    • Arithmetic, logical, bitwise, comparison, ternary operators
    • Compound assignment operators (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, <<<=, >>>=)
    • Increment/decrement operators (x++, --x) as statements and in expressions
    • Assignment expressions (x = (y = expr) + 1) with proper side-effect ordering
    • System function calls ($signed, $unsigned, $floor, $ceil)
    • User-defined function calls with good support (simple functions, arithmetic, boolean logic, case statements, nested if-else)
    • Struct member access (e.g., bus.field)
    • Hierarchical signal references
    • Parameter references with HEX/BIN/DEC formats
    • Loop variable substitution in generate blocks
  • Control Flow: If-else statements, case statements (including constant evaluation in initial blocks), for loops with compile-time unrolling and variable substitution, repeat loops with compile-time unrolling, while loops in compile-time function evaluation, named and unnamed begin blocks with local variable scoping
  • Memory: Array inference, memory initialization, for-loop memory initialization patterns, asymmetric port RAM with different read/write widths
  • Shift Registers: Automatic detection and optimization of shift register patterns (e.g., M[i+1] <= M[i])
  • Generate Blocks:
    • For loops with proper scope handling
    • If-else generate conditions
    • Hierarchical naming (e.g., gen_loop[0].signal)
    • Net and variable imports from generate scopes
  • Packages: Import statements, package parameters (including localparam/parameter from enum constants), package-scoped typedefs and enum types, struct types, functions
  • Net Types: wand and wor (wire-AND and wire-OR) with proper multi-driver resolution
  • Primitives: Gate arrays (and, or, xor, nand, nor, xnor, not, buf)
  • Advanced Features:
    • Interfaces with automatic expansion to individual signals
    • Interface port connections and signal mapping
    • Assertions

Quick Start

Prerequisites

  • GCC/Clang compiler with C++17 support
  • CMake 3.16+
  • Python 3.6+
  • Standard development tools (make, bison, flex)

Build

# Clone with submodules
git clone --recursive https://github.com/username/uhdm2rtlil.git
cd uhdm2rtlil

# Configure git hooks (prevents committing files >10MB)
git config core.hooksPath .githooks

# Build everything (Surelog, Yosys, UHDM Frontend)
make

Basic Usage

# Method 1: Using the test workflow
cd test
bash test_uhdm_workflow.sh simple_counter

# Method 2: Manual workflow
# Step 1: Generate UHDM from SystemVerilog
./build/third_party/Surelog/bin/surelog -parse design.sv

# Step 2: Use Yosys with UHDM frontend (load plugin first)
./out/current/bin/yosys -p "plugin -i uhdm2rtlil.so; read_uhdm slpp_all/surelog.uhdm; synth -top top_module"

Testing Framework

Test Structure

Each test case is a directory containing:

  • dut.sv - SystemVerilog design under test
  • Automatically generated comparison files:
    • *_from_uhdm.il - RTLIL generated via UHDM path
    • *_from_verilog.il - RTLIL generated via Verilog path
    • rtlil_diff.txt - Detailed RTLIL comparison
    • *_from_uhdm_synth.v - Gate-level netlist via UHDM path
    • *_from_verilog_synth.v - Gate-level netlist via Verilog path
    • netlist_diff.txt - Gate-level netlist comparison

Running Tests

# Run internal tests only (our test suite)
make test

# Run all tests (internal + Yosys tests)
make test-all

# Run Yosys tests only
make test-yosys

# Run specific test from test directory
cd test
bash test_uhdm_workflow.sh simple_counter

# Run tests with options from test directory
cd test
bash run_all_tests.sh                    # Run internal tests only
bash run_all_tests.sh --all              # Run all tests (internal + Yosys)
bash run_all_tests.sh --yosys           # Run all Yosys tests
bash run_all_tests.sh --yosys add_sub   # Run specific Yosys test pattern

# Test output explanation:
# ✓ PASSED - UHDM and Verilog frontends produce functionally equivalent results
# ⚠ FUNCTIONAL - Works correctly but with RTLIL differences (normal and expected)
# ✗ FAILED - Significant functional differences or equivalence check failure

# The test framework performs multiple levels of comparison:
# 1. RTLIL comparison - Shows implementation differences
# 2. Synthesis and formal equivalence check - Uses Yosys equiv_make/equiv_simple/equiv_induct
# 3. Validates functional equivalence even when gate counts differ

Yosys Test Integration

The UHDM frontend can run the full Yosys test suite to validate compatibility:

# Run all Yosys tests
make test-yosys

# Run specific Yosys test directory
cd test
./run_all_tests.sh --yosys ../third_party/yosys/tests/arch/common

# Run specific Yosys test
./run_all_tests.sh --yosys ../third_party/yosys/tests/arch/common/add_sub.v

The Yosys test runner:

  • Automatically finds self-contained Verilog/SystemVerilog tests
  • Runs both Verilog and UHDM frontends on each test
  • Performs formal equivalence checking when both frontends succeed
  • Reports UHDM-only successes (tests that only work with UHDM frontend)
  • Creates test results in test/run/ directory structure

Current Test Cases (169 total — 164 passing equivalence, 5 UHDM-only, 0 known failures)

Sequential Logic - Flip-Flops & Registers

  • flipflop - D flip-flop (tests basic sequential logic)
  • dff_styles - Simple D flip-flop with synchronous clock edge
  • dff_different_styles - Multiple DFF variants with different reset styles and polarities
  • dffsr - D flip-flop with async set/reset
  • adff - Async D flip-flop with async reset (from Yosys test suite)
  • adffn - Async D flip-flop with negative-edge async reset (from Yosys test suite)
  • dffs - D flip-flop with synchronous preset (from Yosys test suite)
  • ndffnr - Negative edge flip-flop with reset (from Yosys test suite)
  • latchp - Positive level-sensitive latch (tests latch inference from combinational always blocks)
  • latchn - Negative level-sensitive latch (tests inverted enable condition handling)
  • latchsr - Latch with set/reset functionality (tests nested if-else in combinational context)
  • initval - Initial values on registers with mixed combinational and clocked part-select assignments to the same signal

Counters & Sequential Designs

  • simple_counter - 8-bit counter with async reset (tests increment logic, reset handling)
  • counter - More complex counter design
  • always01 - 4-bit synchronous counter with mux-based async reset
  • always02 - 4-bit counter with synchronous reset in nested block
  • always03 - Mixed blocking and non-blocking assignments with if-else chains
  • counters_repeat - Repeat loop counter using repeat(32) with mixed blocking/non-blocking assignments and carry chain
  • simple_always_ff - Sequential always_ff blocks with clock and reset
  • simple_always_ifelse - Always blocks with if-else conditional logic

Combinational Logic - Boolean & Arithmetic

  • lut_map_and - Simple AND gate (tests basic 2-input logic)
  • lut_map_or - Simple OR gate (tests basic 2-input logic)
  • lut_map_xor - Simple XOR gate (tests basic 2-input logic)
  • lut_map_not - Simple NOT gate (tests unary operators)
  • lut_map_mux - 2-to-1 multiplexer with ternary operator
  • lut_map_cmp - Multiple comparison operators (<=, <, >=, >, ==, !=) with constants
  • logic_ops - Logical operations with bit ordering (from Yosys test suite)
  • opt_share_add_sub - Shared add/subtract using ternary selection (tests operator sharing)
  • simple_assign - Basic continuous assignments
  • partsel_simple - Part selection with dynamic offset using +: and -: operators; data[offset +: 4] and data[offset+3 -: 4] where offset = idx << 2; synthesized via $shiftx cells (28 gates)
  • wreduce_test0 - Signed arithmetic with width reduction
  • wreduce_test1 - Arithmetic operations with output width reduction
  • unbased_unsized - SystemVerilog unbased unsized literals ('0, '1, 'x, 'z) and cast operations
  • unbased_unsized_shift - Unbased unsized fill literals ('0, '1) in shift operations: '1 << 8 in a 64-bit context produces 64'hFFFF_FFFF_FFFF_FF00; tests constant and dynamic shift amounts
  • port_sign_extend - Port sign extension with signed submodule outputs, signed constants, arithmetic operations, and correct unbased unsized vs sized constant handling
  • port_int_types - Built-in integer port types (byte, byte unsigned, shortint, shortint unsigned): signed types sign-extend and unsigned types zero-extend when assigned to wider [31:0] wires
  • param_int_types - Module-level variable declarations using built-in integer types (integer, int, shortint, longint, byte) with initial values; also tests typed parameters (parameter integer a = -1 etc.) — verifies correct widths and initial/parameter values
  • asgn_expr - Assignment expressions: increment/decrement operators as statements and in expressions, nested assignment expressions
  • asgn_expr2 - Assignment expressions with input-dependent outputs (formal equivalence verified)
  • asgn_expr_sv - Full increment/decrement test from Yosys suite: pre/post-increment/decrement as statements and expressions, byte-width concatenation ({1'b1, ++w}, {2'b10, w++}), procedural assignment expressions (x = (y *= 2))
  • asgn_binop - Compound assignment operators (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, <<<=, >>>=)

Multiplexers

  • mux2 - 2-to-1 multiplexer using conditional operator (tests ternary expression)
  • mux4 - 4-to-1 multiplexer using case statement (tests case statement with bit selection)
  • mux8 - 8-to-1 multiplexer using nested conditional operators (tests complex ternary chains)
  • mux16 - 16-to-1 multiplexer using dynamic bit selection (tests non-constant indexed access)

Multipliers & Arithmetic Pipelines

  • mul - Multiplication with correct result width calculation (tests arithmetic width inference)
  • mul_plain - Simple combinational multiplier from Gatemate test suite
  • mul_signed_async - Signed multiplier with async reset and pipeline registers
  • mul_unsigned - 16x24-bit multiplier with 3-stage pipelined result
  • mul_unsigned_sync - Unsigned multiplier with sync reset and pipeline registers
  • mul_unsigned_sync_simple - 6x3-bit synchronous multiplier with clock enable
  • mul_sync_enable_test - 6x3-bit multiplier with synchronous reset and enable
  • mul_sync_reset_test - 6x3-bit multiplier with synchronous reset
  • macc - Multiply-accumulate unit from Xilinx (tests power operator, large constants, process structures)

State Machines

  • simple_fsm - Finite state machine with parameterized states (tests parameter references in case statements)
  • fsm - 3-state finite state machine with grant signals
  • code_tidbits_fsm_using_function - FSM implemented with combinational function (tests function-based state logic)
  • power_state - Output logic decoder for state machine
  • simple_enum - Enum-based state machine with assertions
  • unique_case - Unique case statements with for loops and break statements (UHDM-only)

Functions

  • simple_function - AND-OR tree function driving flip-flop (tests function in continuous assignment)
  • function_arith - Arithmetic function with add/subtract/shift/AND/XOR
  • function_bool - Boolean logic function with if-else modes
  • function_case - Case statement function with shift operations
  • function_loop - Loop-based bit reversal function
  • function_nested - Nested if-else maximum finder
  • function_mixed - Mixed arithmetic, bit manipulation, and conditional modes
  • function_output - Function with output parameter assignments
  • many_functions - Multiple function types: arithmetic, boolean, case, nested, loop, mixed
  • fib - Recursive Fibonacci with wrapper function and output parameters
  • fib_simple - Fibonacci with wrapper function output assignments
  • fib_recursion - Direct recursive Fibonacci in initial block
  • fib_initial - Initial block with function call evaluation
  • func_tern_hint - Recursive functions with ternary type/width hints and self-determined context
  • repwhile - Memory initialization with while/repeat loop functions (mylog2, myexp2) called from for-loop in initial block
  • func_block - Functions with multiple assignment patterns: func1/func2 use for-loop bit-select LHS (func1[idx] = inp[idx]), func3 uses part-select LHS on return variable (func3[A:B] = inp[A:B]) with function-local localparam A = 32-1; parameter B = 1; demonstrates correct localparam resolution via param_assign->Rhs() and uhdmpart_select LHS in process_stmt_to_case
  • const_fold_func - Compile-time constant function evaluation: recursive functions with bitwise ops, bit-select LHS assignments, nested function calls as arguments (2 unproven equiv cells - known failure)

Scope & Variable Shadowing

  • scope_func - Function calls with variable inputs in always blocks (tests function scope resolution)
  • scopes - Functions, tasks, and nested blocks with variable shadowing (tests complex scoping)
  • scope_task - Tasks with nested named blocks and local variables (tests task inlining in always blocks)
  • unnamed_block_decl - Unnamed begin/end blocks with local integer variable declarations and variable scoping (inner z shadows output z, compile-time evaluated to z=5)

Arrays & Memory

  • arrays01 - 4-bit x16 array with synchronous read and write (tests memory inference)
  • arrays03 - Packed multidimensional arrays with dynamic element access (tests logic [0:3][7:0], reg8_t [0:3], and typedef variants)
  • simple_memory - Memory arrays and access patterns
  • simple_memory_noreset - Memory array without reset signal
  • blockrom - Memory initialization using for loops with LFSR pattern (tests loop unrolling and constant evaluation)
  • priority_memory - Priority-based memory access patterns
  • mem2reg_test1 - Combinational array with dynamic write and read in always @*: array[dyn_addr] = in_data; out = array[out_addr] — correctly synthesized with individual element wires and mux chains ✅
  • mem2reg_test2 - (* mem2reg *) annotated 8-element array with for-loop writes and dynamic address read/write in always @(posedge clk)
  • asym_ram_sdp_read_wider - Asymmetric RAM with read port 4x wider than write
  • asym_ram_sdp_write_wider - Asymmetric RAM with write port 4x wider than read
  • sp_read_first - Single port RAM with read-first semantics
  • sp_read_or_write - Single port RAM with read-or-write semantics
  • sp_write_first - Single port RAM with write-first semantics

Data Types & Structs

  • simple_struct - Packed struct handling and member access (tests struct bit slicing)
  • struct_array - Arrays of packed structs with complex indexing and member access
  • struct_access - Packed struct field access with initial block assignments; fixed '1 fill constant extension (4-bit field now gets 4'b1111, not 4'b0001)
  • nested_struct - Nested structs from different packages with complex field access (UHDM-only)
  • nested_struct_nopack - Nested structs without packages (tests synchronous if-else with switch statements)
  • simple_nested_struct_nopack - Simpler nested struct test without packages
  • enum_simple - State machine with typedef enum and state transitions
  • enum_values - Multiple enum types with custom values and attributes
  • svtypes_enum_simple - Bare enums, typedef enums, parenthesized type declarations, enum constant init, FSM with assertions
  • svtypes_struct_simple - Packed structs with member access (s.a, pack1.b), nested struct member access (s2.b.x), struct packed signed, continuous assignments and combinational assertions
  • typedef_simple - Multiple typedef definitions with signed/unsigned types
  • typedef_param - Typedef'd parameters and localparams with signed types, chained typedef aliases (char_t = int8_t = logic signed [7:0]), localparam visibility (only parameter exported, localparam hidden), and static assertions
  • typedef_package - Package-scoped typedefs, enum types with hex values, package localparam/parameter from enum constants, package-qualified assertions
  • package_task_func - Package tasks (P::t) and functions (P::f, P::g) called from module scope, including recursive functions and localparam resolution across package boundaries, with concurrent assert property statements
  • union_simple - Packed unions: named unions (w_t, instruction_t), anonymous unions with nested struct, unions nested within structs (s_t), multi-level member access through union and struct boundaries

Generate & Parameterization

  • param_test - Parameter passing and overrides
  • generate_test - Generate for loops and if-else generate blocks with proper instance naming
  • simple_generate - Generate loop with AND gates clocked per-bit
  • forgen01 - Generate block with nested loops computing prime LUT
  • forgen02 - Generate block implementing parameterized ripple adder
  • gen_test1 - Nested generate loops with if-then conditional blocks and sequential always blocks
  • gen_test2 - For-loop in always block with casez carry propagation
  • gen_test3 - Conditional generate with if/case and multi-assign statements
  • gen_test4 - Hierarchical generate with localparam cross-references (foo[PREV].temp)
  • gen_test5 - Nested generate with multiplication-based genvar increment (step = step * CHUNK) and cross-scope hierarchical references (steps[PREV].outer[LAST_START].val)
  • gen_test6 - Descending generate for-loop (i = 3; i >= 0; i = i-1)
  • gen_test7 - Generate with initial/always blocks and power operator (2 ** 2)
  • gen_test8 - Nested generate scope shadowing with wire constant initialization
  • gen_test9 - Named generate blocks with scope shadowing and XOR computation
  • genblk_order - Nested generate blocks with variable shadowing
  • genvar_loop_decl_1 - Generate loop with inline genvar declaration and width arrays
  • genvar_loop_decl_2 - Generate with genvar shadowing and hierarchical references
  • carryadd - Generate-based carry adder with hierarchical references
  • multiplier - 4x4 2D array multiplier with parameterized RippleCarryAdder and FullAdder using generate loops (known equiv mismatch - SAT proves outputs equivalent)
  • const_func - Constant functions in generate blocks with string parameters, $floor, and bitwise negation
  • forloops - For loops in both clocked and combinational always blocks, with module-level loop variables (integer k), indexed bit-select LHS (q[k]), indexed part-select LHS (p[2*k +: 2]), and part-select RHS ({~a,~b,a,b} >> k[1:0])
  • case_expr_const - Case statement with constant expressions including signed case items, mixed signed/unsigned contexts, and context-width extension per SV LRM 12.5.1

Module Hierarchy & Interfaces

  • simple_hierarchy - Module instantiation and port connections
  • simple_interface - Interface-based connections
  • simple_instance_array - Primitive gate arrays (and, or, xor, nand, not with array instances) (UHDM-only)
  • simple_package - SystemVerilog packages with parameters, structs, and imports (UHDM-only)
  • custom_map_incomp - Custom technology mapping with _TECHMAP_REPLACE_ cell and string parameters
  • arraycells - Array cell instantiation with bit-sliced port connections (e.g., aoi12 p [31:0] (a, b, c, y))

Primitives & Miscellaneous

  • verilog_primitives - Instantiation of buf, not, and xnor primitives
  • escape_id - Module and signal names with special characters and escapes
  • code_hdl_models_decoder_2to4_gates - 2-to-4 decoder using primitive gates
  • code_hdl_models_parallel_crc - 16-bit parallel CRC with combinational feedback logic
  • aes_kexp128 - AES key expansion circuit with XOR feedback and array registers
  • simple_abc9 - ABC9 test collection with blackbox, whitebox, and various port types
  • vector_index - Bit-select assignments on vectors (tests assign wire[bit] = value syntax)
  • constmsk_test - OR reduction of concatenations with constant bits (tests |{A[3], 1'b0, A[1]} and |{A[2], 1'b1, A[0]})
  • rotate - Barrel shift rotation (from Amber23 ARM core) with nested generate loops (5 levels x 32 bits), bit-select assignments in always @* blocks, and wrap() function for circular indexing
  • wandwor - wand and wor net types with multi-driver resolution via AND/OR logic, module instance port connections, and multi-bit variants
  • fmt_always_comb - $display system task in combinational always @* block with conditional enable (if (y & (y == (a & b))) $display(a, b, y)); generates $print RTLIL cell with TRG_WIDTH=0 (no clock triggers), EN wire controlled by the if condition; reg a = 0/reg b = 0 net declaration initializers produce \init wire attributes; formally equivalent to Yosys Verilog frontend

Test Management

The test framework includes automatic handling of known failing tests:

# View known failing tests
cat test/failing_tests.txt

# Format: one test name per line, # for comments

How it works:

  • Tests listed in failing_tests.txt are expected to fail
  • The test runner (run_all_tests.sh) will still run these tests
  • If all failures are listed in failing_tests.txt, the test suite passes with exit code 0
  • This allows CI to pass while acknowledging known issues
  • New unexpected failures will cause the test suite to fail

Current Status:

  • 169 of 169 tests are passing or working as expected (164 equiv + 5 UHDM-only)
  • 0 tests in failing_tests.txt (no known failures)

Important Test Workflow Note

The test workflow runs proc before opt to ensure proper process handling:

hierarchy -check -top $MODULE_NAME
stat
proc    # Convert processes to netlists first
opt     # Then optimize
stat
write_rtlil ${MODULE_NAME}_from_uhdm.il
synth -top $MODULE_NAME

This prevents errors when synthesizing designs with generate blocks and multiple processes.

Project Structure

uhdm2rtlil/
├── src/frontends/uhdm/          # UHDM Frontend implementation
│   ├── uhdm2rtlil.cpp          # Main frontend, design import, interface expansion
│   ├── module.cpp              # Module/port/instance handling  
│   ├── process.cpp             # Always blocks and statements
│   ├── expression.cpp          # Expression evaluation
│   ├── functions.cpp           # Compile-time constant function evaluation
│   ├── interpreter.cpp         # Statement interpreter for initial blocks
│   ├── memory.cpp              # Memory and array support
│   ├── memory_analysis.cpp     # Memory pattern detection
│   ├── clocking.cpp            # Clock domain analysis
│   ├── package.cpp             # Package support
│   ├── primitives.cpp          # Primitive gates
│   ├── ref_module.cpp          # Module references
│   ├── interface.cpp           # Interface declarations and modports
│   └── uhdm2rtlil.h           # Header with class definitions
├── test/                        # Test framework
│   ├── run_all_tests.sh        # Test runner script
│   ├── test_uhdm_workflow.sh   # Individual test workflow
│   ├── test_equivalence.sh     # Formal equivalence checking script
│   ├── failing_tests.txt       # Known failing tests list
│   └── */                      # Individual test cases
├── third_party/                # External dependencies
│   ├── Surelog/               # SystemVerilog parser (includes UHDM)
│   └── yosys/                 # Synthesis framework (pinned at v0.64)
├── .github/workflows/         # CI/CD configuration
├── build/                     # Build artifacts
├── CMakeLists.txt            # CMake build configuration
└── Makefile                   # Top-level build orchestration

Test Results

The UHDM frontend test suite includes 156 test cases:

  • 5 UHDM-only tests - Demonstrate superior SystemVerilog support (nested_struct, simple_instance_array, simple_package, unique_case, gen_struct_access)
  • 164 passing tests - Validated by formal equivalence checking between UHDM and Verilog frontends
  • 0 known failures - All tests pass; failing_tests.txt is empty

Recent Improvements

Unbased Unsized Fill Constant Extension ('1)

  • Fixed '1 fill constants assigned to multi-bit struct fields (and any multi-bit LHS in import_assignment_sync)
  • UHDM represents '1 as VpiSize() == -1, VpiValue() == "BIN:1"import_constant() returns a 1-bit SigSpec(S1) since it has no target-width context
  • Bug: the assignment handler called extend_u0() (zero-extend) when RHS was narrower than LHS, producing 4'b0001 for a 4-bit field instead of 4'b1111
  • Fix in process.cpp (import_assignment_sync): detect fill-ones RHS before importing by checking c->VpiSize() == -1 && VpiValue() == "BIN:1", then on size mismatch replicate S1 to the full LHS width (SigSpec(S1, lhs.size())) instead of zero-extending
  • Fix in interpreter.cpp (evaluate_expression): return -1LL for '1 fill constants so RTLIL::Const(-1, wire->width) in import_initial_interpreted also produces correct all-ones (two's-complement -1 is all bits set for any width)
  • struct_access test now produces assign s = 30'h3fffffff (all 30 bits 1), formally verified ✅

Generate-Scope Variable Shadowing in Named Begin Blocks

  • Fixed gen_test7: always @* begin : proc block declaring reg signed [31:0] x was incorrectly using the generate-scope genvar \cond.x (1-bit) instead of the block-local \proc.x (32-bit)
  • Root cause: import_ref_obj() tries gen_scope + "." + ref_name (e.g., "cond.x") before the plain name_map[ref_name] lookup — this bypassed the block-local shadow
  • Fix in process.cpp (import_begin_block_comb): when shadowing name_map[var_name] with the block-local wire, also shadow the gen-scope hierarchical name name_map[gen_scope + "." + var_name]; both entries are added to block_local_vars for restore on exit
  • gen_test7 now produces out2 = 2 (was 0), formally verified ✅

Robust Formal Equivalence for Constant-Only Circuits

  • Fixed test_equivalence.sh: when no $_ gate cells are present (constant-only module), the script previously wrote # No gates to check and exited 0 — a vacuous pass
  • Fix: compare assign signal = VALUE; lines common to both gold (Verilog) and gate (UHDM) netlists; normalise ?x for high-Z equivalence; skip signals where gold already has undefined bits
  • port_sign_extend equivalence check now passes correctly

Case Statement Signed/Unsigned Context Extension (case_expr_const)

  • Fixed import_case_stmt_comb and the nested import_statement_comb (CaseRule* variant) to correctly implement SV LRM 12.5.1 case-statement comparison semantics
  • Bug 1 — wrong concatenation direction: code used {expr_sig, zeros} (Verilog-style concat puts expr_sig at the MSB side, zeros at the LSB), effectively left-shifting the value; 1'sb12'10 (value 2) instead of 2'01 (zero-extended) or 2'11 (sign-extended)
  • Bug 2 — missing sign extension: signed case items such as 1'sb1 (= −1) must be sign-extended to the context width; zero-extending gives value 1, which does not match 2'sb11
  • Bug 3 — wrong context width: the switch signal width was taken from the case expression alone; when a case item is wider (e.g., 3'b0 in case (1'sb1)), the comparison must occur at the wider width to avoid spurious matches
  • Fix: two-pass approach in import_case_stmt_comb:
    1. Import case expression and all case-item expressions; track width and signedness of each
    2. Context width = max of all; context signed = all operands signed (any unsigned → unsigned context)
    3. Extend switch signal and each compare value to context width using extend_u0(width, is_signed) — sign-extends when both context and that operand are signed, otherwise zero-extends
  • Added is_expr_signed() helper that checks the 's sigil in VpiDecompile() (e.g. "1'sb1", "2'sb11") to determine constant signedness
  • case_expr_const now formally verified: all 8 outputs produce 1'h1

Port Sign Extension Equivalence Check (port_sign_extend)

  • The UHDM frontend was already producing correct output for this test — GeneratorSigned2.out and all sign/zero-extension logic were right
  • Root cause of false failure: test_equivalence.sh compared constant assign statements with a naive file-wide grep, making assign out = 2'h2 (from GeneratorSigned2) match assign out = 1'h1 (from GeneratorSigned1) since grep returns the first occurrence in a multi-module file
  • Fix: replaced the line-by-line shell loop with a single awk pass that tracks the current module declaration and keys the gate lookup table as module:signal; the gold file is then compared against the same module:signal key, ensuring cross-module name collisions are never compared
  • port_sign_extend now passes with 18 common constant assignments verified ✅

Unnamed Block Variable Declaration Support

  • Added interpreter-based evaluation path for initial blocks containing block-local variable declarations
  • block_has_local_variables() helper recursively detects Variables() on begin/named_begin blocks in the UHDM statement tree
  • Extended interpret_statement() in interpreter.cpp to save/restore block-local variables for proper scoping:
    • Before entering a begin block: saves existing variable values and initializes block-local vars to 0
    • After exiting: restores saved values or erases block-local vars that didn't exist in the outer scope
  • New import_initial_interpreted() method runs the interpreter, then creates STi sync actions for variables that correspond to module-level wires
  • Handles variable shadowing: inner block's integer z correctly shadows the module output z, with the shadow removed on block exit
  • import_initial() now has three routing strategies: interpreter (block-local vars), comb (case/if), sync (default)
  • Test unnamed_block_decl verifies the complete scoping chain produces z = 5 through nested begin blocks

Wand/Wor Net Type Support

  • Added wand (wire-AND) and wor (wire-OR) net type propagation from UHDM to RTLIL
  • Surelog encodes these as VpiNetType() values vpiWand (2) and vpiWor (3) on logic_net objects
  • Sets \wand/\wor boolean attributes on RTLIL wires, enabling Yosys hierarchy pass to resolve multi-driver nets with AND/OR logic
  • Handles both the early-return path (net already created as port) and normal net creation path in import_net()
  • Test covers single-bit and multi-bit variants, continuous assignments and module port connections

Compile-Time Function Evaluation with While/Repeat Loops

  • Extended compile-time function evaluator (evaluate_function_stmt) to support vpiWhile and vpiRepeat loop constructs
  • While loops: Evaluate condition each iteration, execute body while non-zero, with 10,000 iteration safety limit
  • Repeat loops: Evaluate repeat count, execute body N times, with 10,000 count safety limit
  • Function return width now uses actual width from func_def->Return() instead of hardcoded 32-bit
  • Fixed const_shl/const_shr/const_not in compile-time evaluator: passing -1 as result_len caused vector::_M_fill_insert crash (unsigned overflow in resize)
  • Added for-loop increment handling for i = i + 1 assignment form (in addition to i++ post-increment)
  • Added memory initialization pattern with function calls: detects for (i=0; i<N; i++) begin mem[i] <= func(i); end and generates $meminit_v2 cells directly
  • Enables compile-time evaluation of functions like mylog2 (while-based) and myexp2 (repeat-based) called from initial block for-loops
  • 128 $meminit_v2 cells generated for the repwhile test (64 for y_table, 64 for x_table), formally verified equivalent

Packed Union Support

  • Implemented packed union handling for union_var and union_typespec UHDM types
  • Width calculation: Union width = width of widest member (unlike structs where width = sum of all members)
  • Member access: Union members all overlay at bit offset 0 — accessing any member returns a slice starting at the same base offset
  • Nested access: Supports arbitrary nesting of structs and unions (e.g., s1.ir.u.opcode where s1 is a struct containing a union ir containing a struct u)
  • Variable handling: union_var in Variables() import sets wiretype attribute and propagates signedness
  • Net handling: union_typespec in net import (for anonymous unions via struct_net) sets wiretype attribute
  • Expression resolution: Extended import_hier_path() and calculate_struct_member_offset() to handle both struct_typespec and union_typespec transparently
  • All 13 assertions in the test are synthesized and formally verified correct by Yosys SAT solver

Compound Assignment Operator Support

  • Implemented all 12 compound assignment operators: +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=, <<<=, >>>=
  • Surelog represents compound assignments (e.g., c += b) as UHDM assignment objects with vpiOpType set to the operation type (vpiAddOp, vpiSubOp, etc.) rather than vpiAssignmentOp (regular assign)
  • Added create_compound_op_cell() helper that maps vpiOpType to the corresponding RTLIL cell ($add, $sub, $mul, $div, $mod, $and, $or, $xor, $shl, $shr, $sshl, $sshr)
  • The LHS's current value is resolved from current_comb_values tracking (e.g., after c = a, the compound c += b correctly generates $add(a, b) instead of $add(c, b))
  • Compound op handling added to both import_assignment_comb variants (Process and CaseRule)
  • All 12 compound operator modules pass formal equivalence checking

Packed Multidimensional Array Support

  • Implemented packed multidimensional array port and net handling for all typespec variants:
    • Variant A: Direct multi-range (logic [0:3][7:0]) — 2+ ranges on a single logic_typespec
    • Variant B: Elem_typespec-based (reg8_t [0:3]) — logic_typespec with Elem_typespec pointing to element type
    • Typedef aliases: (reg2dim1_t = typedef reg8_t [0:3]) — nested Elem_typespec chains
  • Fixed wire width computation in get_width_from_typespec(): multiplies element width by range size for Elem_typespec variants, with typedef alias detection to avoid double-counting
  • Fixed upto/start_offset in import_port() and import_net(): packed multi-dim arrays create flat wires (no upto flag) since they're treated as flattened bit vectors in RTLIL
  • Dynamic element access (in[ix]) in import_bit_select() generates proper arithmetic cells:
    • For [0:N] (ascending) ranges: $sub(N, ix)$mul(result, elem_width)$shiftx(base, offset, Y_WIDTH=elem_width)
    • For [N:0] (descending) ranges: $mul(ix, elem_width)$shiftx(base, offset, Y_WIDTH=elem_width)
  • Packed array metadata (packed_elem_width, packed_outer_left, packed_outer_right) stored as wire attributes during port import for reliable detection in expression handler
  • All 4 test modules (pcktest1–4) pass formal equivalence checking

Repeat Loop Support

  • Implemented vpiRepeat handler in import_statement_sync() for compile-time unrolling of repeat(N) loops
  • Analyzes body statements to classify blocking assignments into loop index variables and intermediate variables
  • Loop index variables (detected by var = var + 1 pattern) are handled via loop_values for compile-time substitution in bit-selects
  • Blocking intermediate variables (like carry) are handled via input_mapping for runtime signal chain propagation
  • Non-blocking assignments are added directly to sync rule actions with resolved bit indices
  • Initial values for blocking variables are read from pending_sync_assignments (preceding statements)
  • Final values are written back to pending_sync_assignments for correct flush at process end
  • Safety limit of 100,000 iterations; non-constant repeat counts produce a warning
  • Formally verified: produces functionally equivalent output to Yosys Verilog frontend

Assignment Expressions and Increment/Decrement Operators

  • Implemented SystemVerilog assignment expressions: x = (y = z + 1) + 1
  • Implemented increment/decrement operators as both statements (x++, --x) and expressions (z = ++x, y = w++)
  • Surelog fix: Pre-increment/decrement (++x/--x) was incorrectly mapped to vpiPostIncOp/vpiPostDecOp — fixed two code paths in CompileExpression.cpp to emit vpiPreIncOp/vpiPreDecOp for prefix operators
  • Pre-increment returns the new value (after modification); post-increment returns the old value (before modification)
  • Handles vpiPostIncOp/vpiPreIncOp/vpiPostDecOp/vpiPreDecOp (ops 62-65) in import_operation() and import_statement_comb()
  • Handles vpiAssignmentOp (op 82) for nested assignment-as-expression in import_operation()
  • Side-effect operations are handled before reduceExpr to prevent incorrect constant folding
  • Added emit_comb_assign() helper for emitting process assignments with proper $0\ temp wire mapping
  • Added map_to_temp_wire() helper for mapping signals to their temp wire equivalents
  • Value tracking (current_comb_values) updated after each side-effect to ensure correct data flow chaining
  • Separate value-tracked and non-tracked imports: tracked values for cell inputs (correct data flow), raw wires for side-effect targets (correct assignment targets)

Port Signedness and Sign Extension

  • Fixed signed port propagation from UHDM elaborated nets to port wires
  • In import_net(), when a net already exists in name_map (created by import_port()), the VpiSigned flag is now checked and applied to the existing wire
  • Added operation signedness analysis: arithmetic, comparison, and shift operations check both operands for signedness
  • Signed constant detection via int_typespec on Actual_typespec() for constants like 1'sb1
  • Unbased unsized literal extension ('0, '1, 'x, 'z) guarded by VpiSize() == -1 — sized constants like 1'b1 (VpiSize() == 1) are no longer incorrectly replicated to port width
  • Re-enabled AllModules import with top-level skip for proper port direction information on non-top modules

Techmap Cell Handling (TECHMAP_REPLACE)

  • Special handling for _TECHMAP_REPLACE_ instances: no blackbox module definition is created
  • Cell is created with the base module name (parent namespace stripped, e.g., custom_map_incomp::ALUALU)
  • Parameters are passed directly on the cell with proper string constant preservation ("AND" not binary)
  • Port connections are made directly from the parent module's wires
  • Test infrastructure updated to handle tests where both frontends fail at hierarchy -check by comparing nohier IL outputs

Initial Block Import Strategy

  • Split import_initial into two strategies based on statement content analysis
  • Sync approach (default): Handles simple assignments and for-loop unrolling using STa/STi sync rules
  • Comb approach: Handles complex control flow (case/if/if_else) using switch rules and combinational processes
  • statement_contains_control_flow() helper recursively checks for vpiIf/vpiIfElse/vpiCase in statement trees
  • Prevents "Failed to get a constant init value" errors from mux cells in PROC_INIT
  • Prevents incorrect sync always continuous driving that would override always_ff blocks

Memory Analyzer Robustness

  • Skip initial blocks (vpiInitial) in analyze_memory_usage_in_processes to avoid spurious memory detection
  • Fixed vpiIf/vpiIfElse type casting in analyze_statement_for_memory: vpiIf uses if_stmt*, vpiIfElse uses if_else*
  • Added proper vpiIfElse handling with both then-stmt and else-stmt traversal

Multi-bit Condition Handling

  • Added ReduceBool normalization for multi-bit conditions across all process import paths
  • Fixes mux select width errors (64-bit conditions reduced to 1-bit for mux select)
  • Fixes switch signal/compare size assertion failures in combinational context
  • Applied consistently in: import_if_stmt_sync, import_if_stmt_comb, import_if_else_comb, and import_statement_comb (both Process and CaseRule overloads)

Local Variables in Unnamed Begin Blocks

  • Extended import_begin_block_comb to handle vpiBegin blocks (not just vpiNamedBegin) with Variables()
  • Local variables in unnamed begin blocks are created as module wires with unique hierarchical names
  • Added unnamed_block_counter for generating unique block names

Recursive Expression Import and Operation Support

  • Refactored expression import to use proper recursive approach for nested expressions
  • Fixed handling of concatenation operations (vpiConcatOp) within other expressions
  • Added support for unary minus operation (vpiMinusOp) for expressions like -$signed({1'b0, a})
  • Added support for shift operations (vpiLShiftOp, vpiRShiftOp) for << and >> operators
  • Concatenation operations inside system function calls now properly import their operands
  • Expression evaluation now correctly handles arbitrary nesting depth
  • Fixed wreduce tests by properly handling complex nested expressions

System Function Call Improvements

  • Enhanced $signed and $unsigned system function handling
  • System function arguments are now properly evaluated recursively
  • Added debug logging for tracking empty operands in system functions
  • Fixed issue where concat operations inside $signed were returning empty signals

Primitive Gate Enhancements

  • Fixed handling of Verilog primitives with multiple outputs (buf, not)
  • Primitives with multiple outputs now create separate cells for each output
  • Proper connection mapping for multi-output primitives
  • Fixed verilog_primitives test with correct multi-output handling

Process and Case Statement Improvements

  • Enhanced constant case evaluation in initial blocks
  • Added support for evaluating case statements with constant conditions
  • Improved handling of integer variables in procedural loops
  • Fixed for-loop unrolling in always @(posedge clk) and always @* blocks:
    • Module-level loop variables (integer k;) use vpiRefObj in the for-loop init node — now handled alongside vpiRefVar (local loop variables)
    • extract_assigned_signals recurses into vpiFor bodies; dynamically-indexed signals (q[k]) get full-width $0\ temp wires with lhs_expr = nullptr
    • import_part_select / import_indexed_part_select substitute loop_values[k] as a constant before resolving the base wire, preventing k[1:0] from emitting a live wire reference during unrolling

Unbased Unsized Literal and Assertion Support

  • Added proper handling for SystemVerilog unbased unsized literals ('0, '1, 'x, 'z)
  • Implemented cast operation support (vpiCastOp) for expressions like 3'('x)
  • Fixed UInt constant parsing to use stoull for large values (e.g., 0xFFFFFFFFFFFFFFFF)
  • Added extract_const_from_value helper function with STRING value support
  • Fixed single-bit constant extension to properly replicate X/Z values across width
  • Added basic support for immediate assertions (vpiImmediateAssert) to prevent crashes
  • Assertions are currently skipped during import (future enhancement: convert to $assert cells)

Memory Write Restructuring for Complex Loop-based Writes

  • Fixed asym_ram_sdp_write_wider test by restructuring memory write generation
  • Implemented proper for-loop unrolling with variable substitution in memory addresses and data slices
  • Added support for indexed part select with loop variable substitution (e.g., diA[(i+1)*minWIDTH-1 -: minWIDTH])
  • Memory writes in loops now generate proper $memwr$ temporary wires matching Verilog frontend structure
  • Added priority values to memwr statements for correct write ordering
  • Eliminated external combinational cells in favor of process-internal switch statements
  • asym_ram_sdp_write_wider restored to passing after always_ff comb-path regression (see Recent Fixes)

Process Structure Improvements for always_ff Blocks

  • Fixed process structure generation to use switch statements inside process bodies instead of external mux cells
  • Added proper handling of simple if statements without else branches in synchronous contexts
  • Correctly distinguishes between vpiIf (type 22) and vpiIfElse (type 23) for proper type casting
  • Fixed type casting: vpiIf statements cast to UHDM::if_stmt*, vpiIfElse to UHDM::if_else*
  • Eliminated redundant assignments in default case for simple if without else
  • Process structures now match Verilog frontend output exactly for better optimization

Net Declaration Assignment Improvements

  • Fixed PROC_INIT pass failures by checking if RHS expressions are constant
  • Constant expressions create init processes (as before)
  • Non-constant expressions (e.g., wire [8:0] Emp = ~(Blk | Wht)) create continuous assignments
  • Prevents "Failed to get a constant init value" errors during synthesis

Expression and Arithmetic Improvements

  • Added support for power operator (**) used in expressions like 2**(SIZEOUT-1)
  • Fixed integer parsing to use std::stoll for large constants (e.g., 549755813888)
  • Corrected Mux (conditional operator) argument ordering: Mux(false_val, true_val, cond)
  • Fixed Add and Sub operations to create proper arithmetic cells using addAdd/addSub
  • All arithmetic operations now generate appropriate cells instead of just wires

Latch Inference Support

  • Added proper detection of combinational always blocks (always @*)
  • Combinational blocks are now routed to dedicated combinational import path
  • Added support for if_else statements in combinational contexts (distinct from if_stmt)
  • Implemented proper type casting using any_cast based on VpiType()
  • Support for arbitrarily nested if/if_else/case statements in combinational logic
  • Temp wire initialization for proper latch inference
  • Three new latch tests added (latchp, latchn, latchsr) demonstrating various latch patterns

For Loop Unrolling Support

  • Added support for unrolling for loops in initial blocks for memory initialization
  • Implemented compile-time expression evaluation with variable substitution
  • Generates $meminit_v2 cells for memory initialization patterns
  • Extracts loop bounds and parameters from UHDM elaborated model
  • Handles complex LFSR-style pseudo-random number generation patterns
  • Supports nested expressions including multiplication, shifts, and XOR operations
  • Properly handles SystemVerilog integer (32-bit) vs 64-bit constant semantics
  • Added blockrom test demonstrating loop-based memory initialization

Interface Expansion Support

  • Added automatic expansion of SystemVerilog interfaces to individual signals
  • Interface instances are replaced with their constituent nets during RTLIL import
  • Interface connections are properly mapped to individual signal connections
  • Interface signal wires are converted to proper input/output ports
  • Supports parameterized interfaces with different widths

Parameter Reference Support

  • Added proper handling of parameter references in expressions (e.g., in case statements)
  • Parameters are now correctly resolved to their constant values instead of being treated as wire references
  • Supports binary constant format "BIN:xx" used by UHDM for proper bit width handling

Dynamic Bit Selection Support

  • Implemented non-constant indexed access (e.g., D[S] where S is a signal)
  • Creates $shiftx cells for dynamic bit selection operations
  • Enables support for multiplexers and other dynamic indexing patterns
  • Added mux16 test demonstrating 16-to-1 multiplexer functionality

Signed Attribute Support

  • Added proper detection of signed ports and nets from UHDM typespecs
  • Implemented signed attribute handling for arithmetic operations
  • Signed types (int, byte, short_int, long_int) are properly marked as signed
  • Logic types with VpiSigned attribute are correctly handled
  • Enables correct signed multiplication and other arithmetic operations

Memory Write Handling in Synchronous Processes

  • Implemented proper architectural fix for memory writes in always_ff blocks
  • Memory writes (mem[addr] <= data) are no longer placed directly in sync rules
  • Instead, creates temporary wires for memory control signals (address, data, enable)
  • Imports statements into process body (root_case) with assignments to temp wires
  • Creates single mem_write_action in sync rule using the temp wires
  • This matches Yosys's expected process model and prevents PROC_MEMWR pass failures
  • Added helper functions is_memory_write() and scan_for_memory_writes() to detect memory operations
  • Verified with Xilinx memory tests: priority_memory, sp_write_first, sp_read_first, sp_read_or_write

Key Improvements

  • simple_interface - Added interface expansion support, converting interface instances to individual signals
  • simple_fsm - Fixed parameter reference handling in case statements, ensuring proper constant resolution
  • simple_memory - Fixed async reset handling with proper temp wire usage in processes
  • simple_instance_array - Added support for primitive gate arrays (and, or, xor, nand, not gates with array syntax)
  • simple_package - Added full package support including imports, parameters, and struct types
  • struct_array - Now passes with improved expression handling and struct support
  • generate_test - Fixed by adding proc before opt in test workflow to handle multiple generated processes correctly
  • nested_struct_nopack - Fixed synchronous if-else handling to generate proper switch statements matching Verilog frontend output
  • mux4 - Fixed case statement width matching to ensure case values have the same width as the switch signal
  • mul - Fixed multiplication result width calculation to match Verilog frontend (sum of operand widths)
  • macc - Fixed power operator support, large constant parsing, Mux ordering, arithmetic cell creation, and process structures
  • vector_index - Fixed net declaration assignments with non-constant expressions to use continuous assignments
  • priority_memory, sp_write_first, sp_read_first, sp_read_or_write - Fixed with proper memory write handling architecture

Formal Equivalence Checking

The test framework now includes formal equivalence checking using Yosys's built-in equivalence checking capabilities:

  • Uses equiv_make, equiv_simple, and equiv_induct to verify functional equivalence
  • Validates that UHDM and Verilog frontends produce functionally equivalent netlists
  • Works even when gate counts differ, as optimization strategies may vary
  • Generates test_equiv.ys scripts in each test directory for debugging

Primitive Gate Support

The UHDM frontend now supports Verilog primitive gates and gate arrays:

  • Supported gates: and, or, xor, nand, nor, xnor, not, buf
  • Array instantiation: and gate_array[3:0] (out, in1, in2);
  • Proper bit-slicing for vectored connections
  • Maps to Yosys internal gate cells ($_AND_, $_OR_, etc.)
  • Fixed import_primitives() function to properly iterate through module->Primitives()
  • Added code_hdl_models_decoder_2to4_gates test demonstrating gate usage

Package Support

The UHDM frontend now supports SystemVerilog packages:

  • Package imports with import package::* syntax
  • Package parameters and constants
  • Package struct types with correct width calculation
  • Cross-module type references from packages
  • Proper wire context resolution during module instantiation

UHDM Elaboration

The UHDM frontend now handles elaboration automatically:

  • The plugin checks if the UHDM design is already elaborated using vpi_get(vpiElaborated)
  • If not elaborated, it performs elaboration using UHDM's ElaboratorListener
  • This removes the need for the -elabuhdm flag in Surelog
  • Elaboration happens transparently when reading UHDM files in Yosys

Development Workflow

Adding SystemVerilog Support

  1. Identify UHDM Objects: Determine which UHDM object types represent the feature
  2. Implement Import: Add handling in appropriate src/frontends/uhdm/*.cpp file
  3. Map to RTLIL: Convert UHDM objects to equivalent RTLIL constructs
  4. Add Tests: Create test cases comparing UHDM vs Verilog frontend outputs
  5. Validate: Ensure generated RTLIL produces correct synthesis results

Development Setup

Git Hooks

The project includes Git hooks to maintain code quality:

# Enable Git hooks (one-time setup)
git config core.hooksPath .githooks

# What the hooks do:
# - Prevent commits of files larger than 10MB
# - Prevent commits of test/run/**/*.v files (generated test outputs)

Debugging

# Enable debug output
export YOSYS_ENABLE_UHDM_DEBUG=1

# Run with verbose logging
./out/current/bin/yosys -p "read_uhdm -debug design.uhdm; write_rtlil output.il"

Key Design Principles

  • Correctness: Generated RTLIL must be functionally equivalent to Verilog frontend
  • Completeness: Support full SystemVerilog feature set over time
  • Performance: Efficient UHDM traversal and RTLIL generation
  • Maintainability: Clear separation of concerns between different handlers

Development Approach: AI-Assisted Implementation

This project is developed using an innovative AI-assisted approach with Claude (Anthropic's AI assistant). The development workflow leverages Claude's ability to understand and work with multiple file formats simultaneously:

How It Works

  1. UHDM Text Analysis: Claude analyzes the UHDM text output (from uhdm-dump) to understand the structure and relationships of SystemVerilog constructs as represented in UHDM.

  2. RTLIL Comparison: The .il files generated by both the UHDM frontend and Verilog frontend are compared to identify differences and ensure functional equivalence.

  3. Iterative Development: Claude can:

    • Read UHDM dumps to understand what objects need to be handled
    • Analyze RTLIL differences to identify missing functionality
    • Suggest and implement fixes based on the patterns observed
    • Test changes and iterate until the outputs match

Example Workflow

# 1. Generate UHDM and dump it for analysis
./build/third_party/Surelog/bin/surelog -parse test.sv
./build/third_party/UHDM/bin/uhdm-dump slpp_all/surelog.uhdm > test.uhdm.txt

# 2. Generate RTLIL from both frontends
yosys -p "read_uhdm slpp_all/surelog.uhdm; write_rtlil test_uhdm.il"
yosys -p "read_verilog test.sv; write_rtlil test_verilog.il"

# 3. Claude analyzes:
# - test.uhdm.txt to understand UHDM structure
# - Differences between test_uhdm.il and test_verilog.il
# - Implements necessary handlers in the frontend code

Benefits of This Approach

  • Rapid Development: Claude can quickly identify patterns and implement handlers
  • Comprehensive Understanding: AI can analyze complex relationships across multiple file formats
  • Systematic Coverage: Each test case systematically expands SystemVerilog support
  • Quality Assurance: Comparing against Yosys's Verilog frontend ensures correctness

This "vibe coding" approach has proven highly effective, enabling the implementation of complex SystemVerilog features like packages, interfaces, and generate blocks in a fraction of the traditional development time.

Continuous Integration

GitHub Actions automatically:

  • Builds all components (Surelog, Yosys, UHDM Frontend)
  • Runs comprehensive test suite
  • Uploads test results and build artifacts
  • Provides clear pass/fail status

See .github/workflows/ci.yml for configuration details.

Contributing

  1. Fork the repository
  2. Clone and set up git hooks:
    git clone --recursive https://github.com/yourusername/uhdm2rtlil.git
    cd uhdm2rtlil
    git config core.hooksPath .githooks
  3. Create a feature branch
  4. Add appropriate test cases
  5. Ensure all tests pass (or update failing_tests.txt if needed)
  6. Submit a pull request

Note: The repository has git hooks configured to prevent committing files larger than 10MB. This helps keep the repository size manageable. If you need to include large files, consider using Git LFS or adding them to .gitignore.

License

See LICENSE file for details.

Related Projects

  • Yosys - Open source synthesis suite
  • Surelog - SystemVerilog parser
  • UHDM - Universal Hardware Data Model

About

Yosys SystemVerilog Parser - UHDM 2 RTLIL Yosys Pass

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors