Skip to content

Add record_dict_pkg for converting any record to a dict_t - #1239

Open
ru551n wants to merge 5 commits into
VUnit:masterfrom
ru551n:record-dict-pkg
Open

ru551n wants to merge 5 commits into
VUnit:masterfrom
ru551n:record-dict-pkg

Conversation

@ru551n

@ru551n ru551n commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

This adds record_dict_pkg, which converts a value of any record type to a dict_t using VHDL-2019 reflection. It is opt-in through vu.add_record_dict().

use vunit_lib.record_dict_pkg.all;
...
function to_dict is new to_dict generic map (record_t => packet_t);  -- once per record type
...
dict := to_dict(packet);
check_equal(get_integer(dict, "addr"), 5);
call("check_packet", arg(to_dict(packet)));  -- or inline, e.g. with the Python bridge

Motivation

Records carry most testbench data, but VUnit's dynamic data structures don't take them. Transactions, register maps, configurations and expected results are records in almost every VHDL testbench. dict_t, queue_t and the logging and check APIs work on scalars, vectors and strings. Today, turning a record into one of those means hand-writing a function per record type.

Hand-written conversions go stale silently. When a field is added to a record, its to_dict (or to_string) still compiles. The new field is just missing from every log line, comparison and model input until someone notices. A reflection-based conversion follows the record declaration automatically, including nested records.

VHDL-2019 makes the generic version possible for the first time. Before 2019, a package couldn't inspect a record's elements, so the only options were per-type code or a code generator that parses the user's sources. std.reflection is the standard's answer to exactly this. Supporting it in VUnit is a reason to adopt the 2019 features that work today.

A dict is a useful middle format. Once a record is a dict_t, existing VUnit code can handle it:

  • Print or log a transaction with field names, without a custom to_string.
  • Compare expected and actual records key by key, so a mismatch names the field.
  • Hand a record to code that takes key-value data. For example, the VUnit Python bridge (VUnit/vunit-python-bridge) passes a dict_t argument to Python as a dict, so a VHDL record can drive a Python reference model with no glue code per record.

Why opt-in

  • It needs VHDL-2019 and a simulator with std.reflection. NVC has had 'reflect since 1.11.0 (2023-12). GHDL 7.0.0-dev and Questa FSE 2025.3 don't provide std.reflection (checked locally). Riviera-PRO supports it, verified on a Riviera-PRO install.
  • So it is not part of add_vhdl_builtins(), and nobody else compiles it. add_record_dict() fails early, with a clear message, when the standard is older than 2019 or the simulator lacks reflection.
  • The gating follows the existing VHDL-2019 call-path support: a new supports_vhdl_reflection() capability on the simulator interface, False by default and True for NVC and Riviera-PRO. Other simulators only need to flip the flag when they gain support.

Behaviour

  • Element names become lower-case keys, since VHDL identifiers are case-insensitive and reflection returns them in upper case.
  • integer, real, boolean, bit, std_ulogic/std_logic, character, time, string, std_ulogic_vector/std_logic_vector, unsigned and signed keep their type (stored with the matching set_*).
  • Other enumerations are stored as the name of their value ("busy").
  • A nested record becomes a nested dict_t (get_dict_t_ref).
  • Any other element type (other arrays, access, file, protected) fails on record_dict_logger with the element's name. Passing a value that isn't a record also fails there.
  • The conversion is one-way: reflection mirrors are read-only, so there is no from_dict.

Why the API looks like this

The ideal call would be to_dict(packet'reflect), but no subprogram can take a reflected value directly. I tried both ways of declaring the parameter on NVC, with and without --relaxed:

Parameter Error Rule
value : value_mirror formal parameter VALUE with access type must have class VARIABLE A mirror is an access type, and access-type parameters must be variables.
variable value : value_mirror actual for formal VALUE with class VARIABLE must be a name denoting a variable The actual for a variable parameter must be a variable name, and packet'reflect is an expression.

Both are language rules, so the mirror has to live in a variable somewhere. A first version of this PR left that to the caller: every call needed use std.reflection.all, a value_mirror variable and two statements.

to_dict is now a generic function over the record type, and it reflects its own parameter. The result:

  • Each call is one expression that takes the record itself, so it can be used inline, for example as an argument.
  • Users never see mirrors or std.reflection.
  • Instances for different record types can share the name to_dict, so the call site stays the same for every type.

The one line per record type remains because VHDL has no implicit instantiation of generic subprograms: NVC rejects a call to the uninstantiated function with cannot call uninstantiated function. A generic package would also need one instantiation per type, and it couldn't be declared inside a process.

Design notes

  • The mirror walk is an internal mirror_to_dict(variable value : inout value_mirror), which recurses for nested records.
  • NVC rejects to_dict(value) as an actual of set_dict_t_ref inside the package. One local variable works around it, and users never see it.
  • With VHDL-2019 call paths, check_only_log compares the location of the log item, so the failure tests pass line_num/file_name, the same way tb_location.vhd does.

Testing

  • vunit/vhdl/record_dict/run.py on NVC 1.23-devel with VHDL-2019: 6/6, with to_dict instantiated for five types in one architecture (four records and integer). It covers scalars, arrays (including ascending ranges, signed and unsigned), nested records, lower-case keys, and both failure messages, with the logger mocked.
  • New acceptance test test_record_dict_vhdl_2019, skipped for simulators other than NVC and Riviera-PRO. The testbench also passes on Riviera-PRO (run on a separate machine with a license).
  • New unit tests for the two errors and the added sources. The full tests/unit suite passes (642 passed).
  • tests/lint license and pycodestyle checks pass; pylint 10/10 on the changed modules.

This PR was written with the help of an AI assistant (Claude).

🤖 Generated with Claude Code

ru551n and others added 5 commits October 17, 2026 18:01
record_dict_pkg uses VHDL-2019 reflection to convert a value of any
record type to a dict_t, so no conversion function has to be written
or generated for each record. It is opt-in through add_record_dict(),
which requires VHDL-2019 and a simulator whose
supports_vhdl_reflection() is true, currently NVC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
to_dict is now a generic function, instantiated once per record type
and called with the record:

    function to_dict is new to_dict generic map (record_t => packet_t);
    dict := to_dict(packet);

A function can't take packet'reflect directly. A mirror is an access
type, and an access type parameter must be of class variable, whose
actual must be a variable name, not an expression. Before, every call
needed a mirror variable and std.reflection. Now the function reflects
its own parameter, so each call is one expression and users never see
mirrors. The instantiation remains since VHDL has no implicit
instantiation of generic subprograms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Riviera-PRO supports VHDL-2019 reflection, so it reports
supports_vhdl_reflection() and runs the acceptance test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Riviera-PRO rejects chained mirror calls, returns simple_name in lower
case, gives time values in units of the resolution limit and indexes
descending arrays by their actual index, where NVC indexes from 'left
upwards. Handle each so the package runs on both simulators.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant