Verdi Python NPI (pynpi): exporting a full design model from a VCS KDB - instances, widths, and where each signal is written
Goal: export a VCS-elaborated design (simv.daidir/kdb.elab++, built with vcs -kdb) as JSON with the instance tree, each signal's elaborated width/range/array shape, and the file:line of every statement that writes each signal, using Verdi's Python NPI ($VERDI_HOME/share/NPI/python, from pynpi import npisys, lang).
What worked (Verdi W-2024.09): - npisys.init([argv0]); npisys.load_design(['-dbdir', daidir]); ... npisys.end(). load_design CHECKS OUT A VERDI LICENCE (the same features as the GUI) until end(), so read everything first, call end(), and only then do slow work like json.dump. With that, a 169-instance design held the seat for ~2.4 s. - lang.get_top_inst_list() also returns packages. Keep only handles whose type() is npiModule/npiInterface/npiProgram. - Child instances come from handle.internal_scope_handles() (modules, interfaces, npiGenScope generate blocks, tasks). instance_handles() came back empty. Recurse into npiGenScope and prefix its signals with 'blk.' / 'g[0].'. - Signals: net_handles(), variables_handles(), array_net_handles(). - Width: handle.size() gives the DECLARATION's default for a type-parameter-typed signal, and the element count for arrays. For the elaborated bit width, use lang_l1.get_typespec_size(sig.typespec_handle().cps_obj); it also works for structs. Ranges come from typespec_handle().range_handles() with left_range_handle()/right_range_handle().int_value(); an array's element type is elem_typespec_handle(). signed() returns -1 when undefined, so test ==1. - Writers: cont_assign_handles() -> lhs_handle(); process_handles() -> stmt_handle(), then recurse through stmt_handles/stmt_handle/else_stmt_handle/case_item_handles plus for_init_stmt_handles/for_inc_stmt_handles (without the last two, for-loop variables look unwritten). An assignment's blocking() is 0 for nonblocking. - For a select lvalue (q[i], q[i].f, q[7:0]), follow parent_handle() up to the variable or net. Inside generate blocks, key the write by the target's full_name relative to the instance path, because a block's statement may write the enclosing instance's signal. - Speed: almost all the time goes into NPI's own handle/get_str/iterate calls. Probing every child-statement method on every statement doubles the calls. Dispatch by statement type (npiBegin -> stmt_handles, npiIfElse -> stmt_handle + else_stmt_handle, npiCase -> case_item_handles, npiFor -> init/inc/stmt, assignments -> none), and fall back to probing all of them for unknown types. - hier_tree_trv_inst() needs a string name, not a handle.
Validation: comparing against an independent slang elaboration of the same RTL matched every instance and every signal's shape. The only difference was a string variable (slang width 0, NPI 32).
Field check on a second run (Verdi W-2024.09): npisys.load_design changes the mtime of simv.daidir/kdb.elab++ (it creates and removes an entry; no file inside changes). So do not key an export cache on that directory's mtime, and expect a read-only daidir to possibly fail. Also, port_handles size() on an unpacked-array port returned 4 for a [0:3] array of 32-bit elements, which looks like the element count rather than bits. Check ports against the net's typespec before trusting the size. The process stays a single python3 with no NPI child processes, so an RSS watchdog on that PID covers the whole export.
More field notes, Verdi W-2024.09: (1) For an 'integer' (and other built-in signed types), the variable's own signed() returns -1. Read sig.typespec_handle().signed() as the fallback; without it, every integer looked unsigned. (2) Interface ports are npiRefObj handles whose direction is npiUndefined, not nets. Mark them and leave them out of port-shape comparisons. port.high_conn_handle() gives the connected npiNet with full_name, for single-net bindings. (3) $readmemh/$readmemb come through as npiSysTaskCall. argument_handles()[1] is the memory they write, so count it as a write, or memories initialised from a hex file look unwritten. (4) Correction to my earlier comment: a read-only copy of a daidir (chmod -R a-w) did load and export fine in a later test, even though load_design touches kdb.elab++. Keying the cache on the newest mtime plus total size of the FILES in kdb.elab++ (find -type f) survives loads. (5) Seat hygiene: when an export fails after the exporter ran (licence denied, time or RSS guard), store a marker under the cache key, and have later runs answer 'tried' without starting Verdi until the KDB's files change. Otherwise every viewer open retries and asks for a seat again.