Skip to content

GHDL: generics leak between configurations when ghdl.sim_flags is set #1223

Open

Description

@prds-code

Description

With GHDL, a configuration can be simulated with generics set by another configuration. When ghdl.sim_flags is set, every simulation appends its -g<name>=<value> flags to one list shared by all configurations of the test bench. GHDL uses the last -g for each generic. So a configuration inherits any generic set by an earlier simulation that it doesn't set itself.

Which configurations are affected depends on the order in which they are simulated. Serial runs fail deterministically when a configuration that sets a generic runs before one that relies on its default. Parallel runs fail intermittently, and a test can pass while simulating the wrong configuration.

Reproduction

tb_generic_leak.vhd:

library vunit_lib;
context vunit_lib.vunit_context;

entity tb_generic_leak is
  generic (
    runner_cfg : string;
    a : integer := 0;
    b : integer := 0
  );
end entity;

architecture tb of tb_generic_leak is
begin
  main : process
  begin
    test_runner_setup(runner, runner_cfg);
    info("a = " & integer'image(a) & ", b = " & integer'image(b));
    test_runner_cleanup(runner);
  end process;
end architecture;

run.py:

from pathlib import Path
from vunit import VUnit

vu = VUnit.from_argv()
vu.add_vhdl_builtins()
lib = vu.add_library("lib")
lib.add_source_files(Path(__file__).parent / "tb_generic_leak.vhd")

vu.set_sim_option("ghdl.sim_flags", ["--ieee-asserts=disable"])

tb = lib.test_bench("tb_generic_leak")
# Configurations are simulated in the order they are added when running serially
tb.add_config(name="sets_b", generics=dict(b=1))
tb.add_config(name="sets_a", generics=dict(a=1))  # b is expected to keep its default (0)

vu.main()

Run it with python run.py -p1 -v:

sets_b: INFO - a = 0, b = 1
sets_a: INFO - a = 1, b = 1   <- expected b = 0

The output is correct in either of these cases:

  • The set_sim_option("ghdl.sim_flags", ...) call is left out, because .get(..., []) then returns a new list each time.
  • The call is moved after the add_config() calls, because each existing configuration then gets its own copy of the list. Only configurations added after the option is set share the default configuration's list.

Cause

In vunit/sim_if/ghdl.py, GHDLInterface._get_command() does this:

sim = config.sim_options.get("ghdl.sim_flags", [])
sim += hooks.get_flags(self, "run_flags")
for name, value in config.generics.items():
    sim += [f"-g{name!s}={value!s}"]
sim += [f"--assert-level={config.vhdl_assert_stop_level!s}"]

sim is the list object stored in sim_options, so += extends it in place. That list can be shared between configurations:

  • set_sim_option() copies the value once into each configuration that exists at that point, including the default configuration.
  • Configuration.copy(), used by add_config() and in IndependentSimTestCase._simulate(), makes only a shallow copy of sim_options. So configurations added after the option is set share the default configuration's list.

The shared list therefore grows by one set of -g flags per simulation. Instrumenting _get_command() on a 16-test project showed the same list object for every configuration, and its length went 2 -> 12 -> 24 -> …

Fix

Copying the list before extending it worked in my case, but maybe there is a better/more general fix.

-        sim = config.sim_options.get("ghdl.sim_flags", [])
+        sim = list(config.sim_options.get("ghdl.sim_flags", []))

Environment

  • VUnit 5.0.0.dev12 (master at af47432)

Activity

  1. rustyqt commented on Oct 14, 2026

    @rustyqt

    I can confirm this independently with VUnit 5.0.0.dev7 and GHDL 6.0.0 (mcode backend). In a project that calls set_sim_option("ghdl.sim_flags", ...) before add_config(), configurations ran with generics of other configurations, depending on the run order; some of them passed while simulating the wrong case. GHDL applies the last -g of a name, so only the generics a configuration does not set itself leak, which is why it is easy to miss.

    I also checked the other simulator interfaces on master (b25ebc3): only ghdl.py extends the list stored in sim_options in place. nvc.py and incisive.py extend their local command list, and modelsim.py, rivierapro.py and activehdl.py build a new list with +. So the one-line change of #1224 covers it.

    #1224 has no test yet. This unit test for tests/unit/test_ghdl_interface.py fails on master and passes with #1224 (tests/unit/test_ghdl_interface.py and tests/unit/test_configuration.py: 38 passed). Feel free to take it into #1224:

        @mock.patch.object(GHDLInterface, "determine_version", return_value=5.0)
        def test_sim_flags_are_not_shared_between_configurations(self, determine_version):
            design_unit = Entity("tb_entity", file_name=str(Path("tempdir") / "file.vhd"))
            design_unit.generic_names = ["runner_cfg", "a", "b"]
    
            # A configuration added after setting ghdl.sim_flags is a shallow copy and shares the list of flags
            default_config = Configuration("default", design_unit)
            default_config.set_sim_option("ghdl.sim_flags", ["--ieee-asserts=disable"])
            sets_b = default_config.copy()
            sets_b.set_generic("b", 1)
            sets_a = default_config.copy()
            sets_a.set_generic("a", 1)
    
            simif = GHDLInterface(prefix="prefix", output_path="")
            simif._vhdl_standard = VHDL.standard("2008")  # pylint: disable=protected-access
            simif._project = Project()  # pylint: disable=protected-access
            simif._project.add_library("lib", "lib_path")  # pylint: disable=protected-access
    
            def get_generic_flags(config):
                cmd = simif._get_command(  # pylint: disable=protected-access
                    config, str(Path("output_path") / "ghdl"), False, False, "tb_entity", None
                )
                return [flag for flag in cmd if flag.startswith("-g")]
    
            self.assertEqual(get_generic_flags(sets_b), ["-gb=1"])
            self.assertEqual(get_generic_flags(sets_a), ["-ga=1"])
            self.assertEqual(default_config.sim_options["ghdl.sim_flags"], ["--ieee-asserts=disable"])

    On master it fails with:

    AssertionError: Lists differ: ['-gb=1', '-ga=1'] != ['-ga=1']
    

    A news fragment would complete it, for example docs/news.d/1223.bugfix.rst: "GHDL: a configuration no longer inherits the generics of other configurations through a shared ghdl.sim_flags list."

  2. prds-code commented on Oct 15, 2026

    @prds-code
    Author

    I've added the test and news fragment to #1224 as suggested. Sorry for opening the PR without them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions