Skip to content

Add one-way to_dict and push for any type using reflection - #1244

Draft
ru551n wants to merge 4 commits into
VUnit:masterfrom
ru551n:reflection-to-dict-push
Draft

ru551n wants to merge 4 commits into
VUnit:masterfrom
ru551n:reflection-to-dict-push

Conversation

@ru551n

@ru551n ru551n commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Both features in this PR are one-way. VHDL-2019 reflection can read a value of any type, but it can't create one: std.reflection has no procedures and no function that builds or writes a value. That's true of the package on both NVC and Riviera-PRO. t'image/t'value aren't allowed on a generic type either: NVC says "non-representable type T" and Riviera-PRO says "Illegal prefix for attribute 'IMAGE". So there is no from_dict and no generic pop that returns the value. Getting a record back still means reading its elements one by one, as with a hand-written conversion. Is a one-way conversion worth adding to VUnit?

Draft, depends on #1242. It builds on the reflection setup there (vu.add_reflection(), reflection_util_pkg). Until #1242 merges, this diff also shows its two commits. The changes of this PR are the last two commits.

This brings back to_dict from the closed #1239 and adds a generic push onto a queue. The closed #1243 explored symmetric push/pop alternatives that don't use reflection.

to_dict: any record to a dict_t

use vunit_lib.record_dict_pkg.all;
...
function to_dict is new to_dict generic map (record_t => packet_t);
...
dict := to_dict(packet);
check_equal(get_integer(dict, "addr"), 5);

As in #1239: element names become lower-case keys, the supported scalar and vector types keep their type, other enumerations become the name of their value, and nested records become nested dicts. Any other element type fails on record_dict_logger, naming the element.

push: any type onto a queue_t

use vunit_lib.reflection_queue_pkg.all;
...
procedure push is new vunit_lib.reflection_queue_pkg.push generic map (t => packet_t);
...
push(queue, packet);
addr := pop_integer(queue);
data := pop_std_ulogic_vector(queue);
state := state_t'value(pop_string(queue));
  • The elements are pushed one by one in declaration order, each with the queue_pkg push of its type, recursing into nested records. So the queue holds real, typed data: it can be popped with the usual functions, encoded and copied.
  • It works for any type, not only records: a lone enumeration is pushed as one item.
  • The element types are the same as for to_dict. Other enumerations are pushed as the name of their value, which 'value turns back. Unsupported types fail on reflection_queue_logger, naming the element.
  • The instantiation uses the selected name vunit_lib.reflection_queue_pkg.push. A plain procedure push is new push works on NVC, but Riviera-PRO reports it as ambiguous with the queue_pkg procedures (COMP96_0334). The selected name only refers to this package's push, which works on both, so the instance is still called push.
  • Don't instantiate it for a type that queue_pkg already covers, such as integer.

Other changes

  • The array helpers that both packages use (get_pos, to_std_ulogic_vector, to_string of an array mirror) move to reflection_util_pkg.

Testing

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

🤖 Generated with Claude Code

ru551n and others added 4 commits October 20, 2026 22:54
A package instantiated in an architecture is made visible with a use
clause naming the instance, such as use my_inst.all. The dependency
scanner read my_inst as a library and warned "failed to find library
'my_inst'". Skip use clauses that name a package instantiated in the same
file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reflection_check_pkg has generic check_equal and check_equal_with_checker
procedures with the parameters of check_pkg's check_equal. They check
with the predefined "=" of the type, and on a mismatch walk the
reflection mirrors of both values and list every element that differs,
one per row:

  Equality check failed (tb_packet.vhd:21)
    hdr.addr: got 6, expected 5
    data(0): got '1', expected '0'

reflection_check_generic_pkg instantiates both for one type, so one
package instantiation gives the same call shape as check_pkg:

  package packet_checks is new vunit_lib.reflection_check_generic_pkg
    generic map (t => packet_t);

The packages are opt-in through vu.add_reflection(), which fails early
when the standard is older than VHDL-2019 or the simulator lacks
reflection. A new supports_vhdl_reflection() simulator capability is True
for NVC and Riviera-PRO. Riviera-PRO needs -dbg for the call path
locations the tests check.

The default of msg is written as the value of check_result_tag, since
NVC can't call an instance of a generic subprogram that leaves out a
parameter with a string constant default (nickg/nvc#1695).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
record_dict_pkg converts a value of any record type to a dict_t, and
reflection_queue_pkg pushes a value of any type onto a queue_t, element
by element with the queue_pkg push of each type. Both walk the VHDL-2019
reflection mirror of the value.

Both are one-way. Reflection can read a value but can't create one, and
'image/'value aren't allowed on a generic type, so there is no from_dict
and no generic pop that returns the value. The elements are read back one
by one, as with a hand-written conversion.

The generic push is instantiated with the selected name
vunit_lib.reflection_queue_pkg.push, since Riviera-PRO reports a plain
"is new push" as ambiguous with the queue_pkg procedures.

The array helpers that both packages use move to reflection_util_pkg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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