diff --git a/README.md b/README.md index 9c07872..ab7b8a1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ the selected simulator by a foreign language interface the package builds itself called through VHPIDIRECT (NVC, GHDL) or the FLI (Questa/ModelSim), or a VHPI application built with the simulator's own compiler driver (Riviera-PRO/Active-HDL). Values cross the interface with their VHDL types: `integer`, `real`, `string`, `boolean`, `std_ulogic`, `unsigned`/`signed`, the vector -types, and `integer_array_t` as a NumPy array. +types, `integer_array_t` as a NumPy array, and VUnit's `dict_t` as a Python `dict`. ## Installation diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 47d731d..248986c 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -271,6 +271,7 @@ bridge, ``eval`` has more result types: * ``eval_std_ulogic_vector``, returning an unconstrained ``std_ulogic_vector``. * ``eval_integer_array``, returning ``integer_array_t``, see :ref:`python_bridge:integer_array`. +* ``eval_dict``, returning ``dict_t``, see :ref:`python_bridge:dict`. ``eval_boolean`` and ``eval_std_ulogic`` are aliased ``eval`` like the other result types. ``eval_std_ulogic_vector`` and ``eval_integer_array`` are not, @@ -330,6 +331,7 @@ VHDL Python ``unsigned`` ``int``, passed as ``arg_unsigned``/``kwarg_unsigned`` ``signed`` ``int``, passed as ``arg_signed``/``kwarg_signed`` ``integer_array_t`` NumPy array, not on Riviera-PRO/Active-HDL +``dict_t`` ``dict`` with ``str`` keys, see :ref:`python_bridge:dict` ========================== ======================================================== An ``unsigned`` or ``signed`` value of any width becomes an exact Python @@ -367,7 +369,7 @@ passed as a number or as the string of its characters: On NVC, GHDL and Questa, ``call`` returns the same types as ``eval``: ``call_boolean``, ``call_std_ulogic``, ``call_std_ulogic_vector``, -``call_integer_array``, ``call_string``, ``call_real_vector`` and +``call_integer_array``, ``call_dict``, ``call_string``, ``call_real_vector`` and ``call_integer_vector_ptr``, plus the procedures ``call_std_ulogic_vector``, ``call_signed`` and ``call_unsigned`` taking the result as an ``out`` parameter. All of the functions but ``call_std_ulogic_vector`` are aliased @@ -490,6 +492,8 @@ Type mapping * ``signed``/``unsigned`` ↔ ``int`` (procedure results only, Python bridge). * ``integer_array_t`` ↔ ``numpy.ndarray`` (Python bridge), see :ref:`python_bridge:integer_array`. +* ``dict_t`` ↔ ``dict`` (results need the Python bridge), see + :ref:`python_bridge:dict`. The types marked as needing the Python bridge are available on NVC, GHDL and Questa, but not on Riviera-PRO/Active-HDL. @@ -524,6 +528,44 @@ the failure. Output of ``print`` is written to the simulator output and flushed after every operation. +.. _python_bridge:dict: + +dict_t and dict +--------------- + +A ``dict_t`` from VUnit's ``dict_pkg`` (string keys, values of mixed types) is +a Python ``dict``. As an argument, ``arg(dict)`` and ``kwarg("name", dict)`` +write the ``dict`` as the Python literal ``{"key": value, ...}``, which works +on all simulators. The values are ``integer``, ``real``, ``string``, +``boolean``, ``std_ulogic``, ``integer_vector``, ``real_vector``, +``integer_vector_ptr_t`` and nested ``dict_t``. Any other value type is +reported like any other argument that cannot be converted. The keys are +visited in the order of ``get_key``, which VUnit does not specify. + +.. code-block:: vhdl + + constant cfg : dict_t := new_dict; + set_integer(cfg, "taps", 8); + set_real(cfg, "gain", 0.5); + call("model.configure", arg(cfg)); + +``eval_dict`` and ``call_dict`` (aliased ``eval`` and ``call``) convert a +Python ``dict`` with ``str`` keys to a new ``dict_t`` that the caller owns and +deallocates. The values are converted strictly: ``int`` to ``integer``, +``float`` to ``real``, ``str`` to ``string``, ``bool`` to ``boolean``, a +``list`` of ``int`` to ``integer_vector_ptr_t`` and a ``dict`` to a nested +``dict_t``, the last two stored with ``set_integer_vector_ptr_t_ref`` and +``set_dict_t_ref``. ``None``, an ``int`` outside the VHDL integer range, a key +that is not a ``str`` or any other value is an error, and ``eval_dict`` then +returns an empty ``dict_t`` after reporting it. ``eval_dict`` and ``call_dict`` +are implemented by the Python bridge: Riviera-PRO/Active-HDL reports that they require NVC, GHDL or +Questa. + +.. code-block:: vhdl + + constant result : dict_t := eval("{'name': 'fir', 'taps': [1, 2, 1], 'gain': 0.5}"); + check_equal(get_string(result, "name"), "fir"); + .. _python_bridge:integer_array: integer_array_t and NumPy @@ -629,8 +671,8 @@ simulator installation change, so a run script needs nothing beyond This application differs from the Python bridge in a few ways: only the default session exists, the operations implemented by the bridge -(``integer_array_t`` values, the ``boolean``, ``std_ulogic``, vector and -``integer_array_t`` results, ``exec_file``) report that they require NVC, GHDL +(``integer_array_t`` values, the ``boolean``, ``std_ulogic``, vector, +``integer_array_t`` and ``dict_t`` results, ``exec_file``) report that they require NVC, GHDL or Questa, a Python error stops the simulation with the message printed by the application rather than through the logger of the session, and ``real`` values outside the single precision float range are rejected. diff --git a/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd b/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd index 3b2e491..18dad98 100644 --- a/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd +++ b/src/vunit_python_bridge/hdl/src/python_ffi_pkg_bridge.vhd @@ -121,6 +121,7 @@ package python_ffi_pkg is constant p_kind_integer_array : integer := 8; constant p_kind_integer_vector : integer := 9; constant p_kind_real_vector : integer := 10; + constant p_kind_dict : integer := 11; -- Names of the operations, used in the error messages impure function p_exec_operation(session : python_session_t := default_session) return string; diff --git a/src/vunit_python_bridge/hdl/src/python_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_pkg.vhd index c238622..dbb6b2a 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg.vhd @@ -13,6 +13,9 @@ use vunit_lib.run_pkg.all; use vunit_lib.runner_pkg.all; use vunit_lib.integer_vector_ptr_pkg.all; use vunit_lib.string_ops.all; +use vunit_lib.dict_pkg.all; +use vunit_lib.dict_2008p_pkg.all; +use vunit_lib.data_types_private_pkg.all; use std.textio.all; @@ -78,6 +81,11 @@ package python_pkg is -- -- H and L are read as 1 and 0. Any other metavalue is an error. -- + -- A dict_t value becomes a Python dict, {"key": value, ...}, whose values + -- are written the same way: integer, real, string, boolean, std_ulogic, + -- integer_vector, real_vector, integer_vector_ptr_t and dict_t values. + -- Any other value type is an error. + -- -- An integer_array_t value is transferred to Python by the Python bridge, -- which is only available for NVC, GHDL and Questa, and is referred to by -- the expression, which means that it can be used in several calls. @@ -93,6 +101,8 @@ package python_pkg is impure function kwarg_signed(kw : string; value : signed) return arg_t; impure function arg(value : integer_array_t) return arg_t; impure function kwarg(kw : string; value : integer_array_t) return arg_t; + impure function arg(value : dict_t) return arg_t; + impure function kwarg(kw : string; value : dict_t) return arg_t; -- The Python expression calling identifier with the given arguments, for -- example to embed a call in a larger exec or eval string: @@ -164,6 +174,10 @@ package python_pkg is ----------------------------------------------------------------------------- -- Results of eval: boolean, std_ulogic, vectors and arrays ----------------------------------------------------------------------------- + -- The result of eval_dict and call_dict is a new dict_t that the caller owns + -- and deallocates. Its values are integer, real, string, boolean, + -- integer_vector_ptr_t (a list of int) and dict_t (a nested dict). + -- -- std_ulogic_vector and integer_array_t results are only available under -- their explicit names, not as eval overloads, which keeps -- check_equal(eval("17"), 17) and length(eval("[1, 2]")) unambiguous. @@ -187,6 +201,11 @@ package python_pkg is expr : string; session : python_session_t := default_session ) return integer_array_t; + impure function eval_dict( + expr : string; session : python_session_t := default_session + ) return dict_t; + alias eval is eval_dict[string, python_session_t return dict_t]; + procedure eval_std_ulogic_vector( expr : string; result : out std_ulogic_vector; session : python_session_t := default_session ); @@ -247,6 +266,13 @@ package python_pkg is alias call is call_integer_array[ string, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, python_session_t return integer_array_t]; + impure function call_dict( + identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return dict_t; + alias call is call_dict[ + string, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, arg_t, python_session_t return dict_t]; + procedure call_std_ulogic_vector( identifier : string; result : out std_ulogic_vector; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; @@ -601,6 +627,44 @@ package body python_pkg is return "__vunit__.staged(" & integer'image(staged_id) & ")"; end; + impure function p_arg_value(value : dict_t; operation : string) return string; + + -- The Python source text of the value stored for a key of a dict + impure function p_dict_item(value : dict_t; key, operation : string) return string is + begin + case get_value_type(value, key) is + when vhdl_integer => return to_string(get_integer(value, key)); + when vhdl_real => return to_string(get_real(value, key), "%.16e"); + when vhdl_string => return p_quoted(get_string(value, key)); + when vhdl_boolean => return arg(get_boolean(value, key)).value; + when ieee_std_ulogic => return p_arg_value(get_std_ulogic(value, key), operation); + when vhdl_integer_vector => return to_py_list_str(get_integer_vector(value, key)); + when vhdl_real_vector => return to_py_list_str(get_real_vector(value, key)); + when vunit_integer_vector_ptr_t => return p_arg_value(get_integer_vector_ptr_t_ref(value, key)); + when vunit_dict_t => return p_arg_value(get_dict_t_ref(value, key), operation); + when others => + return p_failed_value( + operation & " cannot convert the " & to_string(get_value_type(value, key)) & + " value of the dict key """ & key & """" + ); + end case; + end; + + impure function p_arg_value(value : dict_t; operation : string) return string is + variable result : line; + begin + swrite(result, "{"); + for idx in 0 to num_keys(value) - 1 loop + if idx > 0 then + swrite(result, ", "); + end if; + swrite(result, p_quoted(get_key(value, idx)) & ": " & p_dict_item(value, get_key(value, idx), operation)); + end loop; + swrite(result, "}"); + + return result.all; + end; + function arg(value : real_vector) return arg_t is begin return (p_positional_arg, p_arg_value(value)); @@ -661,6 +725,16 @@ package body python_pkg is return (kw, p_arg_value(value, "kwarg")); end; + impure function arg(value : dict_t) return arg_t is + begin + return (p_positional_arg, p_arg_value(value, "arg")); + end; + + impure function kwarg(kw : string; value : dict_t) return arg_t is + begin + return (kw, p_arg_value(value, "kwarg")); + end; + ----------------------------------------------------------------------------- -- Argument groups ----------------------------------------------------------------------------- @@ -875,6 +949,102 @@ package body python_pkg is return result; end; + -- The integers of a comma separated list + function p_split_integers(text : string) return integer_vector is + alias items : string(1 to text'length) is text; + variable count : natural := 0; + variable first : natural := 1; + variable result : integer_vector(0 to text'length); + begin + if items'length = 0 then + return result(1 to 0); + end if; + for idx in items'range loop + if items(idx) = ',' then + result(count) := integer'value(items(first to idx - 1)); + count := count + 1; + first := idx + 1; + end if; + end loop; + result(count) := integer'value(items(first to items'length)); + return result(0 to count); + end; + + impure function p_to_integer_vector_ptr(text : string) return integer_vector_ptr_t is + constant items : integer_vector := p_split_integers(text); + constant result : integer_vector_ptr_t := new_integer_vector_ptr(items'length); + begin + for idx in items'range loop + set(result, idx - items'left, items(idx)); + end loop; + return result; + end; + + -- The real of "hi,lo,exponent,sign", which is sign * (hi * 2**26 + lo) * 2**exponent. The 53 bit + -- integer is exact and the scaling by a power of two is split in two to avoid underflowing 2**exponent. + function p_to_real(text : string) return real is + constant parts : integer_vector := p_split_integers(text); + constant mantissa : real := real(parts(0)) * 67108864.0 + real(parts(1)); + constant half : integer := parts(2) / 2; + begin + return real(parts(3)) * mantissa * 2.0 ** half * 2.0 ** (parts(2) - half); + end; + + -- The dict_t of the records the dict_t result of the bridge is made of, see + -- _dict_entries of runtime.py + impure function p_to_dict(text : string) return dict_t is + alias records : string(1 to text'length) is text; + constant result : dict_t := new_dict; + variable pos : natural := 1; + variable tag : character; + variable key_start, key_length, payload_start, payload_length : natural; + + -- The length in front of a colon, leaving pos after the colon + procedure read_length(length : out natural) is + variable colon : natural := pos; + begin + while records(colon) /= ':' loop + colon := colon + 1; + end loop; + length := natural'value(records(pos to colon - 1)); + pos := colon + 1; + end; + + procedure add(key, payload : string) is + alias value : string(1 to payload'length) is payload; + variable ints : integer_vector_ptr_t; + variable nested : dict_t; + begin + case tag is + when 'i' => set_integer(result, key, integer'value(value)); + when 'r' => set_real(result, key, p_to_real(value)); + when 'b' => set_boolean(result, key, value = "1"); + when 's' => set_string(result, key, value); + when 'd' => + nested := p_to_dict(value); + set_dict_t_ref(result, key, nested); + when others => + ints := p_to_integer_vector_ptr(value); + set_integer_vector_ptr_t_ref(result, key, ints); + end case; + end; + begin + while pos <= records'length loop + tag := records(pos); + pos := pos + 1; + read_length(key_length); + key_start := pos; + pos := pos + key_length; + read_length(payload_length); + payload_start := pos; + pos := pos + payload_length; + add(records(key_start to key_start + key_length - 1), + records(payload_start to payload_start + payload_length - 1)); + end loop; + + return result; + end; + ----------------------------------------------------------------------------- -- exec ----------------------------------------------------------------------------- @@ -952,6 +1122,16 @@ package body python_pkg is return null_integer_array; end; + impure function eval_dict( + expr : string; session : python_session_t := default_session + ) return dict_t is + begin + if p_eval(expr, p_kind_dict, -1, p_eval_operation(expr, session), session) then + return p_to_dict(p_result_string); + end if; + return new_dict; + end; + procedure eval_std_ulogic_vector( expr : string; result : out std_ulogic_vector; session : python_session_t := default_session ) is @@ -1052,6 +1232,16 @@ package body python_pkg is ); end; + impure function call_dict( + identifier : string; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; + session : python_session_t := default_session + ) return dict_t is + begin + return eval_dict( + to_call_str(identifier, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10), session + ); + end; + procedure call_std_ulogic_vector( identifier : string; result : out std_ulogic_vector; arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 : arg_t := null_arg; diff --git a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd index f8274a1..6ec2192 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg_vhpi.vhd @@ -105,6 +105,7 @@ package python_ffi_pkg is constant p_kind_integer_array : integer := 8; constant p_kind_integer_vector : integer := 9; constant p_kind_real_vector : integer := 10; + constant p_kind_dict : integer := 11; -- Name of the operation, used in the error messages impure function p_eval_operation(expr : string; session : python_session_t := default_session) return string; diff --git a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py index 8dc3252..bb00f92 100644 --- a/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py +++ b/src/vunit_python_bridge/hdl/tools/generate_python_pkg.py @@ -58,6 +58,7 @@ dict(vhdl="unsigned", impure=True, fails=True, suffix="_unsigned"), dict(vhdl="signed", impure=True, fails=True, suffix="_signed"), dict(vhdl="integer_array_t", impure=True, fails=True, suffix=""), + dict(vhdl="dict_t", impure=True, fails=True, suffix=""), ] # Result types of eval and call. @@ -156,6 +157,15 @@ # the integer_array_t and integer_vector_ptr_t results. Use eval_integer_array. alias_eval=False, ), + dict( + name="dict", + vhdl="dict_t", + kind="p_kind_dict", + value="p_to_dict(p_result_string)", + default="new_dict", + hand_written_eval=False, + hand_written_call=False, + ), ] # Result types whose width is given by the actual of the result parameter diff --git a/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in b/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in index f5261e0..b4c288f 100644 --- a/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in @@ -13,6 +13,9 @@ use vunit_lib.run_pkg.all; use vunit_lib.runner_pkg.all; use vunit_lib.integer_vector_ptr_pkg.all; use vunit_lib.string_ops.all; +use vunit_lib.dict_pkg.all; +use vunit_lib.dict_2008p_pkg.all; +use vunit_lib.data_types_private_pkg.all; use std.textio.all; @@ -78,6 +81,11 @@ package python_pkg is -- -- H and L are read as 1 and 0. Any other metavalue is an error. -- + -- A dict_t value becomes a Python dict, {"key": value, ...}, whose values + -- are written the same way: integer, real, string, boolean, std_ulogic, + -- integer_vector, real_vector, integer_vector_ptr_t and dict_t values. + -- Any other value type is an error. + -- -- An integer_array_t value is transferred to Python by the Python bridge, -- which is only available for NVC, GHDL and Questa, and is referred to by -- the expression, which means that it can be used in several calls. @@ -153,6 +161,10 @@ $arg_declarations ----------------------------------------------------------------------------- -- Results of eval: boolean, std_ulogic, vectors and arrays ----------------------------------------------------------------------------- + -- The result of eval_dict and call_dict is a new dict_t that the caller owns + -- and deallocates. Its values are integer, real, string, boolean, + -- integer_vector_ptr_t (a list of int) and dict_t (a nested dict). + -- -- std_ulogic_vector and integer_array_t results are only available under -- their explicit names, not as eval overloads, which keeps -- check_equal(eval("17"), 17) and length(eval("[1, 2]")) unambiguous. @@ -503,6 +515,44 @@ package body python_pkg is return "__vunit__.staged(" & integer'image(staged_id) & ")"; end; + impure function p_arg_value(value : dict_t; operation : string) return string; + + -- The Python source text of the value stored for a key of a dict + impure function p_dict_item(value : dict_t; key, operation : string) return string is + begin + case get_value_type(value, key) is + when vhdl_integer => return to_string(get_integer(value, key)); + when vhdl_real => return to_string(get_real(value, key), "%.16e"); + when vhdl_string => return p_quoted(get_string(value, key)); + when vhdl_boolean => return arg(get_boolean(value, key)).value; + when ieee_std_ulogic => return p_arg_value(get_std_ulogic(value, key), operation); + when vhdl_integer_vector => return to_py_list_str(get_integer_vector(value, key)); + when vhdl_real_vector => return to_py_list_str(get_real_vector(value, key)); + when vunit_integer_vector_ptr_t => return p_arg_value(get_integer_vector_ptr_t_ref(value, key)); + when vunit_dict_t => return p_arg_value(get_dict_t_ref(value, key), operation); + when others => + return p_failed_value( + operation & " cannot convert the " & to_string(get_value_type(value, key)) & + " value of the dict key """ & key & """" + ); + end case; + end; + + impure function p_arg_value(value : dict_t; operation : string) return string is + variable result : line; + begin + swrite(result, "{"); + for idx in 0 to num_keys(value) - 1 loop + if idx > 0 then + swrite(result, ", "); + end if; + swrite(result, p_quoted(get_key(value, idx)) & ": " & p_dict_item(value, get_key(value, idx), operation)); + end loop; + swrite(result, "}"); + + return result.all; + end; + $arg_functions ----------------------------------------------------------------------------- @@ -719,6 +769,102 @@ $arg_functions return result; end; + -- The integers of a comma separated list + function p_split_integers(text : string) return integer_vector is + alias items : string(1 to text'length) is text; + variable count : natural := 0; + variable first : natural := 1; + variable result : integer_vector(0 to text'length); + begin + if items'length = 0 then + return result(1 to 0); + end if; + for idx in items'range loop + if items(idx) = ',' then + result(count) := integer'value(items(first to idx - 1)); + count := count + 1; + first := idx + 1; + end if; + end loop; + result(count) := integer'value(items(first to items'length)); + return result(0 to count); + end; + + impure function p_to_integer_vector_ptr(text : string) return integer_vector_ptr_t is + constant items : integer_vector := p_split_integers(text); + constant result : integer_vector_ptr_t := new_integer_vector_ptr(items'length); + begin + for idx in items'range loop + set(result, idx - items'left, items(idx)); + end loop; + return result; + end; + + -- The real of "hi,lo,exponent,sign", which is sign * (hi * 2**26 + lo) * 2**exponent. The 53 bit + -- integer is exact and the scaling by a power of two is split in two to avoid underflowing 2**exponent. + function p_to_real(text : string) return real is + constant parts : integer_vector := p_split_integers(text); + constant mantissa : real := real(parts(0)) * 67108864.0 + real(parts(1)); + constant half : integer := parts(2) / 2; + begin + return real(parts(3)) * mantissa * 2.0 ** half * 2.0 ** (parts(2) - half); + end; + + -- The dict_t of the records the dict_t result of the bridge is made of, see + -- _dict_entries of runtime.py + impure function p_to_dict(text : string) return dict_t is + alias records : string(1 to text'length) is text; + constant result : dict_t := new_dict; + variable pos : natural := 1; + variable tag : character; + variable key_start, key_length, payload_start, payload_length : natural; + + -- The length in front of a colon, leaving pos after the colon + procedure read_length(length : out natural) is + variable colon : natural := pos; + begin + while records(colon) /= ':' loop + colon := colon + 1; + end loop; + length := natural'value(records(pos to colon - 1)); + pos := colon + 1; + end; + + procedure add(key, payload : string) is + alias value : string(1 to payload'length) is payload; + variable ints : integer_vector_ptr_t; + variable nested : dict_t; + begin + case tag is + when 'i' => set_integer(result, key, integer'value(value)); + when 'r' => set_real(result, key, p_to_real(value)); + when 'b' => set_boolean(result, key, value = "1"); + when 's' => set_string(result, key, value); + when 'd' => + nested := p_to_dict(value); + set_dict_t_ref(result, key, nested); + when others => + ints := p_to_integer_vector_ptr(value); + set_integer_vector_ptr_t_ref(result, key, ints); + end case; + end; + begin + while pos <= records'length loop + tag := records(pos); + pos := pos + 1; + read_length(key_length); + key_start := pos; + pos := pos + key_length; + read_length(payload_length); + payload_start := pos; + pos := pos + payload_length; + add(records(key_start to key_start + key_length - 1), + records(payload_start to payload_start + payload_length - 1)); + end loop; + + return result; + end; + ----------------------------------------------------------------------------- -- exec ----------------------------------------------------------------------------- diff --git a/src/vunit_python_bridge/runtime.py b/src/vunit_python_bridge/runtime.py index cf2e044..bc1c846 100644 --- a/src/vunit_python_bridge/runtime.py +++ b/src/vunit_python_bridge/runtime.py @@ -38,6 +38,7 @@ KIND_INTEGER_ARRAY = 8 KIND_INTEGER_VECTOR = 9 KIND_REAL_VECTOR = 10 +KIND_DICT = 11 VHDL_TYPE_NAMES = { KIND_INTEGER: "integer", @@ -51,6 +52,7 @@ KIND_INTEGER_ARRAY: "integer_array_t", KIND_INTEGER_VECTOR: "integer_vector", KIND_REAL_VECTOR: "real_vector", + KIND_DICT: "dict_t", } STD_ULOGIC_VALUES = frozenset("UX01ZWLH-") @@ -435,6 +437,7 @@ def convert_result(self, kind, width): if kind not in VHDL_TYPE_NAMES: raise RuntimeError(f"Internal error: unknown result kind {kind}") converter = "bits" if kind in (KIND_SIGNED, KIND_UNSIGNED) else VHDL_TYPE_NAMES[kind] + converter = "dict" if kind == KIND_DICT else converter return getattr(self, f"_{converter}_result")(kind, value, width) @staticmethod @@ -577,6 +580,63 @@ def _real_vector_result(self, kind, value, _width): data = struct.pack(f"={len(reals)}d", *reals) return (0, 0.0, data, (len(reals),)) + def _dict_result(self, kind, value, _width): + """ + Convert a dict with str keys to text records, see _dict_entries. + """ + data = _encode(self._dict_entries(kind, value)) + return (0, 0.0, data, (len(data),)) + + def _dict_entries(self, kind, value): + """ + The entries of a dict as :: + records, with the tags i, r, s, b, v (int list) and d (nested dict). + """ + if not isinstance(value, dict): + raise _type_error(kind, value, "a dict") + records = [] + for key, item in value.items(): + if not isinstance(key, str): + raise TypeError(f"Cannot convert the dict key {key!r:.100}, a {_type_name(key)}; expected str") + if _is_bool(item): + tag, payload = "b", str(int(bool(item))) + elif _is_integer(item): + tag, payload = "i", str(self._dict_integer(key, item)) + elif _is_float(item): + tag, payload = "r", self._dict_real(key, item) + elif isinstance(item, str): + tag, payload = "s", item + elif isinstance(item, dict): + tag, payload = "d", self._dict_entries(kind, item) + elif isinstance(item, list) and all(_is_integer(element) for element in item): + tag, payload = "v", ",".join(str(self._dict_integer(key, element)) for element in item) + else: + raise TypeError( + f"Cannot convert the value of dict key {key!r:.100}, a {_type_name(item)} ({item!r:.100}); " + "expected int, float, str, bool, a list of int or a dict" + ) + records.append(f"{tag}{len(_encode(key))}:{key}{len(_encode(payload))}:{payload}") + return "".join(records) + + @staticmethod + def _dict_integer(key, value): + value = int(value) + if not INTEGER_LOW <= value <= INTEGER_HIGH: + raise OverflowError( + f"{value} of dict key {key!r:.100} is outside the range of VHDL integer " + f"({INTEGER_LOW} to {INTEGER_HIGH})" + ) + return value + + @staticmethod + def _dict_real(key, value): + if not math.isfinite(value): + raise ValueError(f"{value} of dict key {key!r:.100} cannot be represented as VHDL real") + # hi,lo,exponent,sign of an exact 53 bit mantissa, which VHDL cannot parse from decimal text exactly + mantissa, exponent = math.frexp(abs(float(value))) + high, low = divmod(int(mantissa * 2**53), 2**26) + return f"{high},{low},{exponent - 53},{int(math.copysign(1, value))}" + _DTYPE_WORD_SIZE = { # dtype.str without byte order: (bit_width, is_signed) "b1": (1, False), diff --git a/tests/tb_python_pkg_bridge.vhd b/tests/tb_python_pkg_bridge.vhd index 203e238..3d4c954 100644 --- a/tests/tb_python_pkg_bridge.vhd +++ b/tests/tb_python_pkg_bridge.vhd @@ -16,6 +16,7 @@ library vunit_lib; context vunit_lib.vunit_context; library python_bridge; context python_bridge.python_context; +use vunit_lib.dict_2008p_pkg.all; entity tb_python_pkg_bridge is generic (runner_cfg : string); @@ -34,6 +35,9 @@ architecture tb of tb_python_pkg_bridge is constant std_ulogic_arg_error : string := "arg cannot convert 'X'; expected '0', '1', 'L' or 'H'"; constant std_ulogic_arg_call : string := "describe(__vunit__.error(""arg cannot convert 'X'; expected '0', '1', 'L' or 'H'""))"; + constant dict_arg_error : string := "arg cannot convert the character value of the dict key ""c"""; + constant dict_arg_call : string := + "describe({""c"": __vunit__.error(""arg cannot convert the character value of the dict key \""c\"""")})"; constant unsigned_arg_error : string := "kwarg_unsigned cannot convert ""1010X010""; the value has metavalues"; constant unsigned_arg_call : string := "describe(v=__vunit__.error(""kwarg_unsigned cannot convert \""1010X010\""; the value has metavalues""))"; @@ -59,6 +63,7 @@ begin variable arr, arr_b, result : integer_array_t; variable ptr : integer_vector_ptr_t; + variable dict, inner : dict_t; variable slv4 : std_ulogic_vector(3 downto 0); variable slv9_desc : std_ulogic_vector(8 downto 0) := "UX01ZWLH-"; variable slv9_asc : std_ulogic_vector(0 to 8) := "UX01ZWLH-"; @@ -1060,6 +1065,177 @@ begin unmock(default_logger); unmock(golden_logger); + --------------------------------------------------------------------- + -- dict_t + --------------------------------------------------------------------- + elsif run("Test eval of dict with every value type") then + dict := eval_dict("{'i': -7, 'r': 0.5, 's': 'h\u00e9llo', 'b': True, 'l': [1, 2, 3], 'e': [], 'n': {'x': 1}}"); + check_equal(num_keys(dict), 7); + check_equal(get_integer(dict, "i"), -7); + check_equal(get_real(dict, "r"), 0.5); + check_equal(get_string(dict, "s"), "h" & character'val(195) & character'val(169) & "llo"); + check_equal(get_boolean(dict, "b"), true); + ptr := get_integer_vector_ptr_t_ref(dict, "l"); + check_equal(length(ptr), 3); + check_equal(get(ptr, 2), 3); + check_equal(length(get_integer_vector_ptr_t_ref(dict, "e")), 0); + inner := get_dict_t_ref(dict, "n"); + check_equal(get_integer(inner, "x"), 1); + + -- The eval and call aliases select the overload from the result type + dict := eval("{'a': 1}"); + check_equal(get_integer(dict, "a"), 1); + exec("def make(n):" + " return {'n': n, 'sq': {'v': n * n}}"); + dict := call("make", arg(5)); + check_equal(get_integer(dict, "n"), 5); + check_equal(get_integer(get_dict_t_ref(dict, "sq"), "v"), 25); + dict := call_dict("make", arg(3)); + check_equal(num_keys(dict), 2); + + dict := eval_dict("{}"); + check_equal(num_keys(dict), 0); + + elsif run("Test that a returned dict can be deallocated") then + for i in 1 to 3 loop + dict := eval_dict("{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6}"); + check_equal(get_integer(dict, "f"), 6); + deallocate(dict); + end loop; + + elsif run("Test that eval of dict gives exact reals") then + dict := eval_dict("{'a': 0.1, 'b': -2.5e-100, 'c': 1e300, 'e': 0.0, 'f': 1.7976931348623157e308}"); + check_equal(get_real(dict, "a"), 0.1, max_diff => 0.0); + check_equal(get_real(dict, "b"), -2.5e-100, max_diff => 0.0); + check_equal(get_real(dict, "c"), 1.0e300, max_diff => 0.0); + check_equal(get_real(dict, "e"), 0.0, max_diff => 0.0); + check_equal(get_real(dict, "f"), 1.7976931348623157e308, max_diff => 0.0); + + elsif run("Test that eval of dict is strict about keys and values") then + mock(default_logger, failure); + dict := eval_dict("[1]"); + check_log( + default_logger, + "eval(""[1]"") failed:" & LF & + "TypeError: Cannot convert Python list ([1]) to VHDL dict_t; expected a dict", + failure + ); + check_equal(num_keys(dict), 0); + + dict := eval_dict("{1: 2}"); + check_log( + default_logger, + "eval(""{1: 2}"") failed:" & LF & + "TypeError: Cannot convert the dict key 1, a int; expected str", + failure + ); + + dict := eval_dict("{'a': None}"); + check_log( + default_logger, + "eval(""{'a': None}"") failed:" & LF & + "TypeError: Cannot convert the value of dict key 'a', a NoneType (None); " & + "expected int, float, str, bool, a list of int or a dict", + failure + ); + + dict := eval_dict("{'a': 2**31}"); + check_log( + default_logger, + "eval(""{'a': 2**31}"") failed:" & LF & + "OverflowError: 2147483648 of dict key 'a' is outside the range of VHDL integer " & + "(-2147483648 to 2147483647)", + failure + ); + + dict := eval_dict("{'a': {'b': [1, 'x']}}"); + check_log( + default_logger, + "eval(""{'a': {'b': [1, 'x']}}"") failed:" & LF & + "TypeError: Cannot convert the value of dict key 'b', a list ([1, 'x']); " & + "expected int, float, str, bool, a list of int or a dict", + failure + ); + + dict := call_dict("dict", kwarg("a", 1.5)); + check_equal(get_real(dict, "a"), 1.5); + check_no_log; + unmock(default_logger); + + elsif run("Test arg and kwarg of dict_t values") then + define_describe; + dict := new_dict; + set_integer(dict, "i", 5); + check_equal(call_string("describe", arg(dict)), "{'i': 5}"); + check_equal(call_string("describe", kwarg("d", dict)), "d={'i': 5}"); + check_equal(call_string("describe", arg(new_dict)), "{}"); + + dict := new_dict; + set_real(dict, "r", 0.5); + set_string(dict, "s", "a ""q"""); + set_boolean(dict, "b", false); + set_std_ulogic(dict, "u", 'H'); + set_integer_vector(dict, "iv", integer_vector'(1, 2)); + set_real_vector(dict, "rv", real_vector'(1.5, 2.5)); + inner := new_dict; + set_integer(inner, "n", 1); + set_dict_t_ref(dict, "d", inner); + exec("def show(d):" + " return repr(sorted(d.items()))"); + check_equal( + call_string("show", arg(dict)), + "[('b', False), ('d', {'n': 1}), ('iv', [1, 2]), ('r', 0.5), ('rv', [1.5, 2.5]), ('s', 'a ""q""'), ('u', True)]" + ); + + elsif run("Test dict_t round trip through Python") then + exec("def identity(**kwargs):" + " return kwargs['d']"); + dict := new_dict; + inner := new_dict; + set_integer(inner, "x", -3); + set_dict_t_ref(dict, "inner", inner); + ptr := new_integer_vector_ptr(2); + set(ptr, 0, 4); + set(ptr, 1, 5); + set_integer_vector_ptr_t_ref(dict, "ints", ptr); + set_integer(dict, "i", 1); + set_real(dict, "r", 0.1); + set_string(dict, "s", "a ""quoted"" \ string"); + set_boolean(dict, "b", true); + set_std_ulogic(dict, "u", '1'); + set_integer_vector(dict, "iv", integer_vector'(7, 8)); + inner := call_dict("identity", kwarg("d", dict)); + check_equal(num_keys(inner), 8); + check_equal(get(get_integer_vector_ptr_t_ref(inner, "iv"), 0), 7); + check_equal(get_integer(inner, "i"), 1); + check_equal(get_real(inner, "r"), 0.1); + check_equal(get_string(inner, "s"), "a ""quoted"" \ string"); + check_equal(get_boolean(inner, "b"), true); + check_equal(get_integer(get_dict_t_ref(inner, "inner"), "x"), -3); + check_equal(get(get_integer_vector_ptr_t_ref(inner, "ints"), 1), 5); + + elsif run("Test dict_t values that cannot be converted to Python") then + define_describe; + define_error_helper; + dict := new_dict; + set_character(dict, "c", 'x'); + mock(python_logger, failure); + mock(default_logger, failure); + check_equal(call_string("describe", arg(dict)), ""); + check_log(python_logger, dict_arg_error, failure); + exec("failing_source = r" & py_quotes & dict_arg_call & py_quotes); + check_log( + default_logger, + "eval(""" & dict_arg_call & """) failed:" & LF & + eval_string("expected_error(failing_source, '', True)"), + failure + ); + unmock(python_logger); + unmock(default_logger); + + elsif run("Test a dict_t keyword argument in a group") then + define_describe; + dict := new_dict; + set_integer(dict, "k", 1); + check_equal(call_string("describe", arg(1), arg(2) & kwarg("d", dict) & kwarg("e", 3)), "1, 2, d={'k': 1}, e=3"); + end if; end loop;